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

mendersoftware / gui / 1081664682

22 Nov 2023 02:11PM UTC coverage: 82.798% (-17.2%) from 99.964%
1081664682

Pull #4214

gitlab-ci

tranchitella
fix: Fixed the infinite page redirects when the back button is pressed

Remove the location and navigate from the useLocationParams.setValue callback
dependencies as they change the set function that is presented in other
useEffect dependencies. This happens when the back button is clicked, which
leads to the location changing infinitely.

Changelog: Title
Ticket: MEN-6847
Ticket: MEN-6796

Signed-off-by: Ihor Aleksandrychiev <ihor.aleksandrychiev@northern.tech>
Signed-off-by: Fabio Tranchitella <fabio.tranchitella@northern.tech>
Pull Request #4214: fix: Fixed the infinite page redirects when the back button is pressed

4319 of 6292 branches covered (0.0%)

8332 of 10063 relevant lines covered (82.8%)

191.0 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';
5✔
61
const columnData = [
5✔
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;
5✔
85
const expirationTimes = {
5✔
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 }) => {
5✔
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 }) => (
5✔
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 = () => {
5✔
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