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

mendersoftware / gui / 1057188406

01 Nov 2023 04:24AM UTC coverage: 82.824% (-17.1%) from 99.964%
1057188406

Pull #4134

gitlab-ci

web-flow
chore: Bump uuid from 9.0.0 to 9.0.1

Bumps [uuid](https://github.com/uuidjs/uuid) from 9.0.0 to 9.0.1.
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v9.0.0...v9.0.1)

---
updated-dependencies:
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #4134: chore: Bump uuid from 9.0.0 to 9.0.1

4349 of 6284 branches covered (0.0%)

8313 of 10037 relevant lines covered (82.82%)

200.97 hits per line

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

92.54
/src/js/components/settings/accesstokenmanagement.js
1
// Copyright 2022 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, { useCallback, useEffect, useMemo, useState } from 'react';
15
import { useDispatch, useSelector } from 'react-redux';
16

17
// material ui
18
import {
19
  Button,
20
  Dialog,
21
  DialogActions,
22
  DialogContent,
23
  DialogTitle,
24
  FormControl,
25
  FormHelperText,
26
  InputLabel,
27
  MenuItem,
28
  Select,
29
  Table,
30
  TableBody,
31
  TableCell,
32
  TableHead,
33
  TableRow,
34
  TextField
35
} from '@mui/material';
36
import { makeStyles } from 'tss-react/mui';
37

38
import { generateToken, getTokens, revokeToken } from '../../actions/userActions';
39
import { canAccess as canShow } from '../../constants/appConstants';
40
import { customSort, toggle } from '../../helpers';
41
import { getCurrentUser, getIsEnterprise } from '../../selectors';
42
import CopyCode from '../common/copy-code';
43
import Time, { RelativeTime } from '../common/time';
44

45
const useStyles = makeStyles()(theme => ({
12✔
46
  accessTokens: {
47
    minWidth: 900
48
  },
49
  creationDialog: {
50
    minWidth: 500
51
  },
52
  formEntries: {
53
    minWidth: 270
54
  },
55
  warning: {
56
    color: theme.palette.warning.main
57
  }
58
}));
59

60
const creationTimeAttribute = 'created_ts';
6✔
61
const columnData = [
6✔
62
  { id: 'token', label: 'Token', canShow, render: ({ token }) => token.name },
34✔
63
  { id: creationTimeAttribute, label: 'Date created', canShow, render: ({ token }) => <Time value={token[creationTimeAttribute]} /> },
34✔
64
  {
65
    id: 'expiration_date',
66
    label: 'Expires',
67
    canShow,
68
    render: ({ token }) => <RelativeTime updateTime={token.expiration_date} shouldCount="up" />
34✔
69
  },
70
  {
71
    id: 'last_used',
72
    label: 'Last used',
73
    canShow: ({ hasLastUsedInfo }) => hasLastUsedInfo,
9✔
74
    render: ({ token }) => <RelativeTime updateTime={token.last_used} />
34✔
75
  },
76
  {
77
    id: 'actions',
78
    label: 'Manage',
79
    canShow,
80
    render: ({ onRevokeTokenClick, token }) => <Button onClick={() => onRevokeTokenClick(token)}>Revoke</Button>
34✔
81
  }
82
];
83

84
const A_DAY = 24 * 60 * 60;
6✔
85
const expirationTimes = {
6✔
86
  'never': {
87
    value: 0,
88
    hint: (
89
      <>
90
        The token will never expire.
91
        <br />
92
        WARNING: Never-expiring tokens are against security best practices. We highly suggest setting a token expiration date and rotating the secret at least
93
        yearly.
94
      </>
95
    )
96
  },
97
  '7 days': { value: 7 * A_DAY },
98
  '30 days': { value: 30 * A_DAY },
99
  '90 days': { value: 90 * A_DAY },
100
  'a year': { value: 365 * A_DAY }
101
};
102

103
export const AccessTokenCreationDialog = ({ onCancel, generateToken, isEnterprise, rolesById, token, userRoles }) => {
6✔
104
  const [name, setName] = useState('');
49✔
105
  const [expirationTime, setExpirationTime] = useState(expirationTimes['a year'].value);
49✔
106
  const [expirationDate, setExpirationDate] = useState(new Date());
49✔
107
  const [hint, setHint] = useState('');
49✔
108
  const { classes } = useStyles();
49✔
109

110
  useEffect(() => {
49✔
111
    const date = new Date();
5✔
112
    date.setSeconds(date.getSeconds() + expirationTime);
5✔
113
    setExpirationDate(date);
5✔
114
    const hint = Object.values(expirationTimes).find(({ value }) => value === expirationTime)?.hint ?? '';
25✔
115
    setHint(hint);
5✔
116
  }, [expirationTime]);
117

118
  const onGenerateClick = useCallback(() => generateToken({ name, expiresIn: expirationTime }), [generateToken, name, expirationTime]);
49✔
119

120
  const onChangeExpirationTime = ({ target: { value } }) => setExpirationTime(value);
49✔
121

122
  const generationHandler = token ? onCancel : onGenerateClick;
49✔
123

124
  const generationLabel = token ? 'Close' : 'Create token';
49✔
125

126
  const nameUpdated = ({ target: { value } }) => setName(value);
49✔
127

128
  const tokenRoles = useMemo(() => userRoles.map(roleId => rolesById[roleId]?.name).join(', '), [rolesById, userRoles]);
49✔
129

130
  return (
49✔
131
    <Dialog open>
132
      <DialogTitle>Create new token</DialogTitle>
133
      <DialogContent className={classes.creationDialog}>
134
        <form>
135
          <TextField className={`${classes.formEntries} required`} disabled={!!token} onChange={nameUpdated} placeholder="Name" value={name} />
136
        </form>
137
        <div>
138
          <FormControl className={classes.formEntries}>
139
            <InputLabel>Expiration</InputLabel>
140
            <Select disabled={!!token} onChange={onChangeExpirationTime} value={expirationTime}>
141
              {Object.entries(expirationTimes).map(([title, item]) => (
142
                <MenuItem key={item.value} value={item.value}>
245✔
143
                  {title}
144
                </MenuItem>
145
              ))}
146
            </Select>
147
            {hint ? (
49!
148
              <FormHelperText className={classes.warning}>{hint}</FormHelperText>
149
            ) : (
150
              <FormHelperText title={expirationDate.toISOString().slice(0, 10)}>
151
                expires on <Time format="YYYY-MM-DD" value={expirationDate} />
152
              </FormHelperText>
153
            )}
154
          </FormControl>
155
        </div>
156
        {token && (
53✔
157
          <div className="margin-top margin-bottom">
158
            <CopyCode code={token} />
159
            <p className="warning">This is the only time you will be able to see the token, so make sure to store it in a safe place.</p>
160
          </div>
161
        )}
162
        {isEnterprise && (
96✔
163
          <FormControl className={classes.formEntries}>
164
            <TextField label="Permission level" id="role-name" value={tokenRoles} disabled />
165
            <FormHelperText>The token will have the same permissions as your user</FormHelperText>
166
          </FormControl>
167
        )}
168
      </DialogContent>
169
      <DialogActions>
170
        {!token && <Button onClick={onCancel}>Cancel</Button>}
94✔
171
        <Button disabled={!name.length} variant="contained" onClick={generationHandler}>
172
          {generationLabel}
173
        </Button>
174
      </DialogActions>
175
    </Dialog>
176
  );
177
};
178

179
export const AccessTokenRevocationDialog = ({ onCancel, revokeToken, token }) => (
6✔
180
  <Dialog open>
1✔
181
    <DialogTitle>Revoke token</DialogTitle>
182
    <DialogContent>
183
      Are you sure you want to revoke the token <b>{token?.name}</b>?
184
    </DialogContent>
185
    <DialogActions>
186
      <Button onClick={onCancel}>Cancel</Button>
187
      <Button onClick={() => revokeToken(token)}>Revoke Token</Button>
×
188
    </DialogActions>
189
  </Dialog>
190
);
191

192
export const AccessTokenManagement = () => {
6✔
193
  const [showGeneration, setShowGeneration] = useState(false);
26✔
194
  const [showRevocation, setShowRevocation] = useState(false);
25✔
195
  const [currentToken, setCurrentToken] = useState(null);
25✔
196
  const isEnterprise = useSelector(getIsEnterprise);
25✔
197
  const { tokens = [], roles: userRoles = [] } = useSelector(getCurrentUser);
25!
198
  const rolesById = useSelector(state => state.users.rolesById);
43✔
199
  const dispatch = useDispatch();
25✔
200

201
  const { classes } = useStyles();
25✔
202

203
  useEffect(() => {
25✔
204
    dispatch(getTokens());
7✔
205
  }, [dispatch]);
206

207
  const toggleGenerateClick = () => {
25✔
208
    setCurrentToken(null);
4✔
209
    setShowGeneration(toggle);
4✔
210
  };
211

212
  const toggleRevocationClick = () => {
25✔
213
    setCurrentToken(null);
×
214
    setShowRevocation(toggle);
×
215
  };
216

217
  const onRevokeClick = token => dispatch(revokeToken(token)).then(() => toggleRevocationClick());
25✔
218

219
  const onRevokeTokenClick = token => {
25✔
220
    toggleRevocationClick();
×
221
    setCurrentToken(token);
×
222
  };
223

224
  const onGenerateClick = config => dispatch(generateToken(config)).then(results => setCurrentToken(results[results.length - 1]));
25✔
225

226
  const hasLastUsedInfo = useMemo(() => tokens.some(token => !!token.last_used), [tokens]);
25✔
227

228
  const columns = useMemo(
25✔
229
    () =>
230
      columnData.reduce((accu, column) => {
9✔
231
        if (!column.canShow({ hasLastUsedInfo })) {
45✔
232
          return accu;
3✔
233
        }
234
        accu.push(column);
42✔
235
        return accu;
42✔
236
      }, []),
237
    [hasLastUsedInfo]
238
  );
239

240
  return (
25✔
241
    <>
242
      <div className={`flexbox space-between margin-top ${tokens.length ? classes.accessTokens : ''}`}>
25✔
243
        <p className="help-content">Personal access token management</p>
244
        <Button onClick={toggleGenerateClick}>Generate a token</Button>
245
      </div>
246
      {!!tokens.length && (
42✔
247
        <Table className={classes.accessTokens}>
248
          <TableHead>
249
            <TableRow>
250
              {columns.map(column => (
251
                <TableCell key={column.id} padding={column.disablePadding ? 'none' : 'normal'}>
85!
252
                  {column.label}
253
                </TableCell>
254
              ))}
255
            </TableRow>
256
          </TableHead>
257
          <TableBody>
258
            {tokens.sort(customSort(true, creationTimeAttribute)).map(token => (
259
              <TableRow key={token.id} hover>
34✔
260
                {columns.map(column => (
261
                  <TableCell key={column.id}>{column.render({ onRevokeTokenClick, token })}</TableCell>
170✔
262
                ))}
263
              </TableRow>
264
            ))}
265
          </TableBody>
266
        </Table>
267
      )}
268
      {showGeneration && (
36✔
269
        <AccessTokenCreationDialog
270
          onCancel={toggleGenerateClick}
271
          generateToken={onGenerateClick}
272
          isEnterprise={isEnterprise}
273
          rolesById={rolesById}
274
          token={currentToken}
275
          userRoles={userRoles}
276
        />
277
      )}
278
      {showRevocation && <AccessTokenRevocationDialog onCancel={toggleRevocationClick} revokeToken={onRevokeClick} token={currentToken} />}
25!
279
    </>
280
  );
281
};
282

283
export default AccessTokenManagement;
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