• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / mender-server / 10423

11 Nov 2025 04:53PM UTC coverage: 74.435% (-0.1%) from 74.562%
10423

push

gitlab-ci

web-flow
Merge pull request #1071 from mendersoftware/dependabot/npm_and_yarn/frontend/main/development-dependencies-92732187be

3868 of 5393 branches covered (71.72%)

Branch coverage included in aggregate %.

5 of 5 new or added lines in 2 files covered. (100.0%)

176 existing lines in 95 files now uncovered.

64605 of 86597 relevant lines covered (74.6%)

7.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.88
/frontend/src/js/components/devices/device-details/authsets/AuthSetListItem.tsx
1
// Copyright 2020 Northern.tech AS
2✔
2
//
2✔
3
//    Licensed under the Apache License, Version 2.0 (the "License");
2✔
4
//    you may not use this file except in compliance with the License.
2✔
5
//    You may obtain a copy of the License at
2✔
6
//
2✔
7
//        http://www.apache.org/licenses/LICENSE-2.0
2✔
8
//
2✔
9
//    Unless required by applicable law or agreed to in writing, software
2✔
10
//    distributed under the License is distributed on an "AS IS" BASIS,
2✔
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2✔
12
//    See the License for the specific language governing permissions and
2✔
13
//    limitations under the License.
2✔
14
import { useEffect, useState } from 'react';
2✔
15
import CopyToClipboard from 'react-copy-to-clipboard';
2✔
16

2✔
17
import { FileCopy as CopyPasteIcon } from '@mui/icons-material';
2✔
18
// material ui
2✔
19
import { Accordion, AccordionActions, AccordionDetails, AccordionSummary, Button, Chip, Divider, IconButton } from '@mui/material';
2✔
20

2✔
21
import Loader from '@northern.tech/common-ui/Loader';
2✔
22
import Time from '@northern.tech/common-ui/Time';
2✔
23
import { DEVICE_DISMISSAL_STATE, DEVICE_STATES, TIMEOUTS } from '@northern.tech/store/constants';
2✔
24
import { formatTime } from '@northern.tech/utils/helpers';
2✔
25

2✔
26
const padder = <div key="padder" style={{ flexGrow: 1 }} />;
15✔
27

2✔
28
const getDismissalConfirmation = (device, authset) => {
15✔
29
  switch (authset.status) {
8!
30
    case DEVICE_STATES.preauth:
2✔
31
      return 'The device authentication set will be removed from the preauthorization list.';
3✔
32
    case DEVICE_STATES.accepted:
2✔
33
      if (device.auth_sets.length > 1) {
4✔
34
        // if there are other authsets, device will still be in UI
2✔
35
        return 'The device with this public key will no longer be accepted, and this authorization request will be removed from the UI.';
3✔
36
      } else {
2✔
37
        return 'The device with this public key will no longer be accepted, and will be removed from the UI. If it makes another request in the future, it will show again as pending for you to accept or reject at that time.';
3✔
38
      }
2✔
39
    case DEVICE_STATES.pending: {
2✔
40
      const message = 'You can dismiss this authentication request for now.';
4✔
41
      if (device.auth_sets.length > 1) {
4✔
42
        // it has other authsets
2✔
43
        return `${message} This will remove this request from the UI, but won’t affect the device.`;
3✔
44
      }
2✔
45
      return `${message} The device will be removed from the UI, but if the same device asks for authentication again in the future, it will show again as pending.`;
3✔
46
    }
2✔
47
    case DEVICE_STATES.rejected:
2✔
48
      return 'This request will be removed from the UI, but if the device asks for authentication again in the future, it will show as pending for you to accept or reject it at that time.';
3✔
49
    default:
2✔
50
      break;
2✔
51
  }
2✔
52
  return '';
2✔
53
};
2✔
54

2✔
55
export const getConfirmationMessage = (status, device, authset) => {
15✔
56
  let message = '';
13✔
57
  switch (status) {
13!
58
    case DEVICE_STATES.accepted:
2✔
59
      message = 'By accepting, the device with this identity data and public key will be granted authentication by the server.';
5✔
60
      if (device.status === DEVICE_STATES.accepted) {
5✔
61
        // if device already accepted, and you are accepting a different authset:
2✔
62
        return `${message} The previously accepted public key will be rejected automatically in favor of this new key.`;
4✔
63
      }
2✔
64
      return message;
3✔
65
    case DEVICE_STATES.rejected:
2✔
66
      message = 'The device with this identity data and public key will be rejected, and blocked from communicating with the Mender server.';
4✔
67
      if (device.status === DEVICE_STATES.accepted && authset.status !== DEVICE_STATES.accepted) {
4✔
68
        // if device is accepted but you are rejecting an authset that is not accepted, device status is unaffected:
2✔
69
        return `${message} Rejecting this request will not affect the device status as it is using a different key. `;
3✔
70
      }
2✔
71
      return message;
3✔
72
    case DEVICE_DISMISSAL_STATE:
2✔
73
      message = getDismissalConfirmation(device, authset);
8✔
74
      break;
8✔
75
    default:
2✔
76
      break;
2✔
77
  }
2✔
78
  return message;
8✔
79
};
2✔
80

2✔
81
const LF = '\n';
15✔
82

2✔
83
const AuthSetStatus = ({ authset, device }) => {
15✔
84
  if (authset.status === device.status) {
34✔
85
    return <div className="capitalized">Active</div>;
14✔
86
  }
2✔
87
  if (authset.status === DEVICE_STATES.pending) {
22✔
88
    return <Chip size="small" label="new" color="primary" style={{ justifySelf: 'flex-start' }} />;
10✔
89
  }
2✔
90
  return <div />;
14✔
91
};
2✔
92

2✔
93
const ActionButtons = ({ authset, confirmMessage, newStatus, limitMaxed, onAcceptClick, onDismissClick, onRequestConfirm, userCapabilities }) => {
15✔
94
  const { canManageDevices } = userCapabilities;
34✔
95
  if (!canManageDevices) {
34!
96
    return null;
2✔
97
  }
2✔
98
  return confirmMessage.length ? (
34!
99
    <div>Set to: {newStatus}?</div>
2✔
100
  ) : (
2✔
101
    <div className="action-buttons flexbox">
2✔
102
      {authset.status !== DEVICE_STATES.accepted && authset.status !== DEVICE_STATES.preauth && !limitMaxed ? (
2✔
103
        <a onClick={onAcceptClick}>Accept</a>
2✔
104
      ) : (
2✔
105
        <div>Accept</div>
2✔
106
      )}
2✔
107
      {authset.status !== DEVICE_STATES.rejected && authset.status !== DEVICE_STATES.preauth ? (
2✔
UNCOV
108
        <a onClick={() => onRequestConfirm(DEVICE_STATES.rejected)}>Reject</a>
2✔
109
      ) : (
2✔
110
        <div>Reject</div>
2✔
111
      )}
2✔
112
      <a onClick={onDismissClick}>Dismiss</a>
2✔
113
    </div>
2✔
114
  );
2✔
115
};
2✔
116

2✔
117
const AuthsetListItem = ({ authset, classes, columns, confirm, device, isExpanded, limitMaxed, loading, onExpand, total, userCapabilities }) => {
15✔
118
  const [showKey, setShowKey] = useState(false);
34✔
119
  const [confirmMessage, setConfirmMessage] = useState('');
34✔
120
  const [newStatus, setNewStatus] = useState('');
34✔
121
  const [copied, setCopied] = useState(false);
34✔
122
  const [keyHash, setKeyHash] = useState('');
34✔
123
  const [endKey, setEndKey] = useState('');
34✔
124

2✔
125
  useEffect(() => {
34✔
126
    if (!isExpanded) {
18✔
127
      setShowKey(false);
17✔
128
      setConfirmMessage('');
17✔
129
      setNewStatus('');
17✔
130
    }
2✔
131
  }, [isExpanded]);
2✔
132

2✔
133
  useEffect(() => {
34✔
134
    const data = new TextEncoder().encode(authset.pubkey);
18✔
135
    if (crypto?.subtle) {
18!
136
      crypto.subtle.digest('SHA-256', data).then(hashBuffer => {
18✔
137
        const hashHex = Array.from(new Uint8Array(hashBuffer))
18✔
138
          .map(b => b.toString(16).padStart(2, '0'))
514✔
139
          .join('');
2✔
140
        setKeyHash(hashHex);
18✔
141
      });
2✔
142
    } else {
2✔
143
      setKeyHash('SHA calculation is not supported by this browser');
2✔
144
    }
2✔
145
    // to ensure the pubkey is copied with the new line at the end we have to double it at the end, as one of the endings gets trimmed in the process of copying
2✔
146
    const key = authset.pubkey.endsWith(LF) ? `${authset.pubkey}${LF}` : authset.pubkey;
18!
147
    setEndKey(key);
18✔
148
  }, [authset.pubkey]);
2✔
149

2✔
150
  const onShowKey = show => {
34✔
151
    onExpand(show && authset.id);
2!
152
    setShowKey(show);
2✔
153
    setConfirmMessage(false);
2✔
154
  };
2✔
155

2✔
156
  const onCancelConfirm = () => {
34✔
157
    onExpand(false);
2✔
158
    setConfirmMessage('');
2✔
159
  };
2✔
160

2✔
161
  const onRequestConfirm = status => {
34✔
162
    const message = getConfirmationMessage(status, device, authset);
2✔
163
    setConfirmMessage(message);
2✔
164
    setNewStatus(status);
2✔
165
    setShowKey(false);
2✔
166
    onExpand(authset.id);
2✔
167
  };
2✔
168

2✔
169
  const onConfirm = confirmedState => confirm(device.id, authset.id, confirmedState).then(onCancelConfirm);
34✔
170

2✔
171
  const onDismissClick = () => {
34✔
172
    if (total > 1 || device.status !== DEVICE_STATES.pending) {
2!
173
      return onRequestConfirm(DEVICE_DISMISSAL_STATE);
2✔
174
    }
2✔
175
    return onConfirm(DEVICE_DISMISSAL_STATE);
2✔
176
  };
2✔
177

2✔
178
  const onAcceptClick = () => {
34✔
179
    if (total > 1) {
2!
180
      return onRequestConfirm(DEVICE_STATES.accepted);
2✔
181
    }
2✔
182
    return onConfirm(DEVICE_STATES.accepted);
2✔
183
  };
2✔
184

2✔
185
  const onCopied = (_, result) => {
34✔
186
    setCopied(result);
2✔
187
    setTimeout(() => setCopied(false), TIMEOUTS.fiveSeconds);
2✔
188
  };
2✔
189

2✔
190
  let key = <a onClick={onShowKey}>show key</a>;
34✔
191
  let content = [
34✔
192
    padder,
2✔
193
    <p className="bold expanded" key="content">
2✔
194
      {loading === authset.id ? 'Updating status' : `${confirmMessage} Are you sure you want to continue?`}
2!
195
    </p>
2✔
196
  ];
2✔
197

2✔
198
  if (showKey) {
34!
199
    content = [
2✔
200
      <div key="content">
2✔
201
        <CopyToClipboard text={endKey} onCopy={onCopied}>
2✔
202
          <IconButton style={{ float: 'right', margin: '-20px 0 0 10px' }} size="large">
2✔
203
            <CopyPasteIcon />
2✔
204
          </IconButton>
2✔
205
        </CopyToClipboard>
2✔
206
        <code className="pre-line">{endKey}</code>
2✔
207
        {copied && <p className="green fadeIn">Copied key to clipboard.</p>}
2!
208
        <Divider className={classes.divider} />
2✔
209
        <div title="SHA256">
2✔
210
          Checksum
2✔
211
          <br />
2✔
212
          <code>{keyHash}</code>
2✔
213
        </div>
2✔
214
      </div>,
2✔
215
      padder
2✔
216
    ];
2✔
217
    key = <a onClick={() => onShowKey(false)}>hide key</a>;
2✔
218
  }
2✔
219
  return (
34✔
220
    <Accordion className={classes.accordion} square expanded={isExpanded}>
2✔
221
      <AccordionSummary className={`columns-${columns.length}`}>
2✔
222
        <AuthSetStatus authset={authset} device={device} />
2✔
223
        <div className="capitalized">{authset.status}</div>
2✔
224
        {key}
2✔
225
        <Time value={formatTime(authset.ts)} />
2✔
226
        {loading === authset.id ? (
2!
227
          <div>
2✔
228
            Updating status <Loader table={true} waiting={true} show={true} style={{ height: '4px', marginLeft: '10px' }} />
2✔
229
          </div>
2✔
230
        ) : (
2✔
231
          <ActionButtons
2✔
232
            authset={authset}
2✔
233
            confirmMessage={confirmMessage}
2✔
234
            newStatus={newStatus}
2✔
235
            limitMaxed={limitMaxed}
2✔
236
            onAcceptClick={onAcceptClick}
2✔
237
            onDismissClick={onDismissClick}
2✔
238
            onRequestConfirm={onRequestConfirm}
2✔
239
            userCapabilities={userCapabilities}
2✔
240
          />
2✔
241
        )}
2✔
242
      </AccordionSummary>
2✔
243
      <AccordionDetails>{content}</AccordionDetails>
2✔
244
      {isExpanded && !showKey && (
2✔
245
        <AccordionActions className="margin-right-small">
2✔
246
          {loading === authset.id ? (
2!
247
            <Loader table={true} waiting={true} show={true} style={{ height: '4px' }} />
2✔
248
          ) : (
2✔
249
            <>
2✔
250
              <Button className="margin-right-small" onClick={onCancelConfirm}>
2✔
251
                Cancel
2✔
252
              </Button>
2✔
UNCOV
253
              <Button variant="contained" onClick={() => onConfirm(newStatus)}>
2✔
254
                Confirm
2✔
255
              </Button>
2✔
256
            </>
2✔
257
          )}
2✔
258
        </AccordionActions>
2✔
259
      )}
2✔
260
    </Accordion>
2✔
261
  );
2✔
262
};
2✔
263

2✔
264
export default AuthsetListItem;
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc