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

mendersoftware / gui / 1113439055

19 Dec 2023 09:01PM UTC coverage: 82.752% (-17.2%) from 99.964%
1113439055

Pull #4258

gitlab-ci

mender-test-bot
chore: Types update

Signed-off-by: Mender Test Bot <mender@northern.tech>
Pull Request #4258: chore: Types update

4326 of 6319 branches covered (0.0%)

8348 of 10088 relevant lines covered (82.75%)

189.39 hits per line

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

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

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

21
import { TIMEOUTS } from '../../../../constants/appConstants';
22
import { DEVICE_DISMISSAL_STATE, DEVICE_STATES } from '../../../../constants/deviceConstants';
23
import { formatTime } from '../../../../helpers';
24
import Loader from '../../../common/loader';
25
import Time from '../../../common/time';
26

27
const padder = <div key="padder" style={{ flexGrow: 1 }}></div>;
13✔
28

29
const getDismissalConfirmation = (device, authset) => {
13✔
30
  switch (authset.status) {
6!
31
    case DEVICE_STATES.preauth:
32
      return 'The device authentication set will be removed from the preauthorization list.';
1✔
33
    case DEVICE_STATES.accepted:
34
      if (device.auth_sets.length > 1) {
2✔
35
        // if there are other authsets, device will still be in UI
36
        return 'The device with this public key will no longer be accepted, and this authorization request will be removed from the UI.';
1✔
37
      } else {
38
        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.';
1✔
39
      }
40
    case DEVICE_STATES.pending: {
41
      const message = 'You can dismiss this authentication request for now.';
2✔
42
      if (device.auth_sets.length > 1) {
2✔
43
        // it has other authsets
44
        return `${message} This will remove this request from the UI, but won’t affect the device.`;
1✔
45
      }
46
      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.`;
1✔
47
    }
48
    case DEVICE_STATES.rejected:
49
      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.';
1✔
50
    default:
51
      break;
×
52
  }
53
  return '';
×
54
};
55

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

82
const LF = '\n';
13✔
83

84
const AuthSetStatus = ({ authset, device }) => {
13✔
85
  if (authset.status === device.status) {
32✔
86
    return <div className="capitalized">Active</div>;
12✔
87
  }
88
  if (authset.status === DEVICE_STATES.pending) {
20✔
89
    return <Chip size="small" label="new" color="primary" style={{ justifySelf: 'flex-start' }} />;
8✔
90
  }
91
  return <div />;
12✔
92
};
93

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

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

126
  useEffect(() => {
32✔
127
    if (!isExpanded) {
16✔
128
      setShowKey(false);
15✔
129
      setConfirmMessage('');
15✔
130
      setNewStatus('');
15✔
131
    }
132
  }, [isExpanded]);
133

134
  useEffect(() => {
32✔
135
    const data = new TextEncoder().encode(authset.pubkey);
16✔
136
    if (crypto?.subtle) {
16!
137
      crypto.subtle.digest('SHA-256', data).then(hashBuffer => {
×
138
        const hashHex = Array.from(new Uint8Array(hashBuffer))
×
139
          .map(b => b.toString(16).padStart(2, '0'))
×
140
          .join('');
141
        setKeyHash(hashHex);
×
142
      });
143
    } else {
144
      setKeyHash('SHA calculation is not supported by this browser');
16✔
145
    }
146
    // 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
147
    const key = authset.pubkey.endsWith(LF) ? `${authset.pubkey}${LF}` : authset.pubkey;
16!
148
    setEndKey(key);
16✔
149
  }, [authset.pubkey]);
150

151
  const onShowKey = show => {
32✔
152
    onExpand(show && authset.id);
×
153
    setShowKey(show);
×
154
    setConfirmMessage(false);
×
155
  };
156

157
  const onCancelConfirm = () => {
32✔
158
    onExpand(false);
×
159
    setConfirmMessage('');
×
160
  };
161

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

170
  const onConfirm = confirmedState => confirm(device.id, authset.id, confirmedState).then(onCancelConfirm);
32✔
171

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

179
  const onAcceptClick = () => {
32✔
180
    if (total > 1) {
×
181
      return onRequestConfirm(DEVICE_STATES.accepted);
×
182
    }
183
    return onConfirm(DEVICE_STATES.accepted);
×
184
  };
185

186
  const onCopied = (_, result) => {
32✔
187
    setCopied(result);
×
188
    setTimeout(() => setCopied(false), TIMEOUTS.fiveSeconds);
×
189
  };
190

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

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

265
export default AuthsetListItem;
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