• 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

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

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

2✔
36
import CopyCode from '@northern.tech/common-ui/CopyCode';
2✔
37
import Time, { RelativeTime } from '@northern.tech/common-ui/Time';
2✔
38
import { BaseDialog } from '@northern.tech/common-ui/dialogs/BaseDialog';
2✔
39
import { canAccess as canShow } from '@northern.tech/store/constants';
2✔
40
import { getCurrentUser, getIsEnterprise } from '@northern.tech/store/selectors';
2✔
41
import { generateToken, getTokens, revokeToken } from '@northern.tech/store/thunks';
2✔
42
import { customSort, toggle } from '@northern.tech/utils/helpers';
2✔
43

2✔
44
const useStyles = makeStyles()(theme => ({
9✔
45
  accessTokens: {
2✔
46
    minWidth: 900
2✔
47
  },
2✔
48
  creationDialog: {
2✔
49
    minWidth: 500,
2✔
50
    display: 'flex',
2✔
51
    flexDirection: 'column',
2✔
52
    gap: theme.spacing(2)
2✔
53
  },
2✔
54
  formEntries: {
2✔
55
    minWidth: 270
2✔
56
  },
2✔
57
  warning: {
2✔
58
    color: theme.palette.warning.main
2✔
59
  }
2✔
60
}));
2✔
61

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

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

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

2✔
112
  useEffect(() => {
17✔
113
    const date = new Date();
4✔
114
    date.setSeconds(date.getSeconds() + expirationTime);
4✔
115
    setExpirationDate(date);
4✔
116
    const hint = Object.values(expirationTimes).find(({ value }) => value === expirationTime)?.hint ?? '';
12✔
117
    setHint(hint);
4✔
118
  }, [expirationTime]);
2✔
119

2✔
120
  const onGenerateClick = useCallback(() => generateToken({ name, expiresIn: expirationTime }), [generateToken, name, expirationTime]);
17✔
121

2✔
122
  const onChangeExpirationTime = ({ target: { value } }) => setExpirationTime(value);
17✔
123

2✔
124
  const generationHandler = token ? onCancel : onGenerateClick;
17✔
125

2✔
126
  const generationLabel = token ? 'Close' : 'Create token';
17✔
127

2✔
128
  const nameUpdated = ({ target: { value } }) => setName(value);
17✔
129

2✔
130
  const tokenRoles = useMemo(() => userRoles.map(roleId => rolesById[roleId]?.name).join(', '), [rolesById, userRoles]);
17✔
131

2✔
132
  return (
17✔
133
    <BaseDialog title="Create new token" open onClose={onCancel}>
2✔
134
      <DialogContent className={classes.creationDialog}>
2✔
135
        <form>
2✔
136
          <TextField className={`${classes.formEntries} required`} disabled={!!token} onChange={nameUpdated} placeholder="Name" value={name} />
2✔
137
        </form>
2✔
138
        <div>
2✔
139
          <FormControl className={classes.formEntries}>
2✔
140
            <InputLabel id="token-expiration-label">Expiration</InputLabel>
2✔
141
            <Select labelId="token-expiration-label" label="Expiration" disabled={!!token} onChange={onChangeExpirationTime} value={expirationTime}>
2✔
142
              {Object.entries(expirationTimes).map(([title, item]) => (
2✔
143
                <MenuItem key={item.value} value={item.value}>
77✔
144
                  {title}
2✔
145
                </MenuItem>
2✔
146
              ))}
2✔
147
            </Select>
2✔
148
            {hint ? (
2!
149
              <FormHelperText className={classes.warning}>{hint}</FormHelperText>
2✔
150
            ) : (
2✔
151
              <FormHelperText title={expirationDate.toISOString().slice(0, 10)}>
2✔
152
                expires on <Time format="YYYY-MM-DD" value={expirationDate} />
2✔
153
              </FormHelperText>
2✔
154
            )}
2✔
155
          </FormControl>
2✔
156
        </div>
2✔
157
        {token && (
2✔
158
          <div className="margin-top-small margin-bottom-small">
2✔
159
            <CopyCode code={token} />
2✔
160
            <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>
2✔
161
          </div>
2✔
162
        )}
2✔
163
        {isEnterprise && (
2✔
164
          <FormControl className={classes.formEntries}>
2✔
165
            <TextField label="Permission level" id="role-name" value={tokenRoles} disabled />
2✔
166
            <FormHelperText>The token will have the same permissions as your user</FormHelperText>
2✔
167
          </FormControl>
2✔
168
        )}
2✔
169
      </DialogContent>
2✔
170
      <DialogActions>
2✔
171
        {!token && <Button onClick={onCancel}>Cancel</Button>}
2✔
172
        <Button disabled={!name.length} variant="contained" onClick={generationHandler}>
2✔
173
          {generationLabel}
2✔
174
        </Button>
2✔
175
      </DialogActions>
2✔
176
    </BaseDialog>
2✔
177
  );
2✔
178
};
2✔
179

2✔
180
export const AccessTokenRevocationDialog = ({ onCancel, revokeToken, token }) => (
8✔
181
  <BaseDialog title="Revoke token" open onClose={onCancel}>
3✔
182
    <DialogContent>
2✔
183
      Are you sure you want to revoke the token <b>{token?.name}</b>?
2✔
184
    </DialogContent>
2✔
185
    <DialogActions>
2✔
186
      <Button onClick={onCancel}>Cancel</Button>
2✔
UNCOV
187
      <Button onClick={() => revokeToken(token)}>Revoke Token</Button>
2✔
188
    </DialogActions>
2✔
189
  </BaseDialog>
2✔
190
);
2✔
191

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

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

2✔
203
  useEffect(() => {
21✔
204
    if (!id) {
8!
205
      return;
2✔
206
    }
2✔
207
    dispatch(getTokens());
8✔
208
  }, [dispatch, id]);
2✔
209

2✔
210
  const toggleGenerateClick = () => {
21✔
211
    setCurrentToken(null);
3✔
212
    setShowGeneration(toggle);
3✔
213
  };
2✔
214

2✔
215
  const toggleRevocationClick = () => {
21✔
216
    setCurrentToken(null);
2✔
217
    setShowRevocation(toggle);
2✔
218
  };
2✔
219

2✔
220
  const onRevokeClick = token => dispatch(revokeToken(token)).then(() => toggleRevocationClick());
21✔
221

2✔
222
  const onRevokeTokenClick = token => {
21✔
223
    toggleRevocationClick();
2✔
224
    setCurrentToken(token);
2✔
225
  };
2✔
226

2✔
227
  const onGenerateClick = config =>
21✔
228
    dispatch(generateToken(config))
3✔
229
      .unwrap()
2✔
230
      .then(results => setCurrentToken(results[results.length - 1]));
3✔
231

2✔
232
  const hasLastUsedInfo = useMemo(() => tokens.some(token => !!token.last_used), [tokens]);
21✔
233

2✔
234
  const columns = useMemo(
21✔
235
    () =>
2✔
236
      columnData.reduce((accu, column) => {
13✔
237
        if (!column.canShow({ hasLastUsedInfo })) {
57✔
238
          return accu;
7✔
239
        }
2✔
240
        accu.push(column);
52✔
241
        return accu;
52✔
242
      }, []),
2✔
243
    [hasLastUsedInfo]
2✔
244
  );
2✔
245

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

2✔
292
export default AccessTokenManagement;
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