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

mendersoftware / gui / 897326496

pending completion
897326496

Pull #3752

gitlab-ci

mzedel
chore(e2e): made use of shared timeout & login checking values to remove code duplication

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3752: chore(e2e-tests): slightly simplified log in test + separated log out test

4395 of 6392 branches covered (68.76%)

8060 of 9780 relevant lines covered (82.41%)

126.17 hits per line

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

89.8
/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 { connect } 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 { customSort, toggle } from '../../helpers';
40
import { getCurrentUser, getTenantCapabilities } from '../../selectors';
41
import CopyCode from '../common/copy-code';
42
import Time, { RelativeTime } from '../common/time';
43

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

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

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

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

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

117
  const onGenerateClick = useCallback(
13✔
118
    () => generateToken({ name, expiresIn: expirationTime }).then(results => setToken(results[results.length - 1])),
1✔
119
    [name, expirationTime]
120
  );
121

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

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

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

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

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

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

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

194
export const AccessTokenManagement = ({ generateToken, getTokens, revokeToken, isEnterprise, rolesById, tokens = [], userRoles = [] }) => {
6!
195
  const [showGeneration, setShowGeneration] = useState(false);
6✔
196
  const [showRevocation, setShowRevocation] = useState(false);
6✔
197
  const [currentToken, setCurrentToken] = useState(null);
6✔
198

199
  const { classes } = useStyles();
6✔
200

201
  useEffect(() => {
6✔
202
    getTokens();
4✔
203
  }, []);
204

205
  const toggleGenerateClick = () => {
6✔
206
    setCurrentToken(null);
1✔
207
    setShowGeneration(toggle);
1✔
208
  };
209

210
  const toggleRevocationClick = () => {
6✔
211
    setCurrentToken(null);
×
212
    setShowRevocation(toggle);
×
213
  };
214

215
  const onRevokeClick = token => revokeToken(token).then(() => toggleRevocationClick());
6✔
216

217
  const onRevokeTokenClick = token => {
6✔
218
    toggleRevocationClick();
×
219
    setCurrentToken(token);
×
220
  };
221

222
  const hasLastUsedInfo = useMemo(() => tokens.some(token => !!token.last_used), [tokens]);
6✔
223

224
  const columns = useMemo(
6✔
225
    () =>
226
      columnData.reduce((accu, column) => {
4✔
227
        if (!column.canShow({ hasLastUsedInfo })) {
20✔
228
          return accu;
3✔
229
        }
230
        accu.push(column);
17✔
231
        return accu;
17✔
232
      }, []),
233
    [hasLastUsedInfo]
234
  );
235

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

280
const actionCreators = { generateToken, getTokens, revokeToken };
6✔
281

282
const mapStateToProps = state => {
6✔
283
  const { isEnterprise } = getTenantCapabilities(state);
3✔
284
  const { tokens, roles: userRoles } = getCurrentUser(state);
3✔
285
  return {
3✔
286
    isEnterprise,
287
    rolesById: state.users.rolesById,
288
    tokens,
289
    userRoles
290
  };
291
};
292

293
export default connect(mapStateToProps, actionCreators)(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