aboutsummaryrefslogtreecommitdiff
path: root/modern/src/settings/UserPage.jsx
diff options
context:
space:
mode:
Diffstat (limited to 'modern/src/settings/UserPage.jsx')
-rw-r--r--modern/src/settings/UserPage.jsx60
1 files changed, 48 insertions, 12 deletions
diff --git a/modern/src/settings/UserPage.jsx b/modern/src/settings/UserPage.jsx
index 939859ae..d9a608ef 100644
--- a/modern/src/settings/UserPage.jsx
+++ b/modern/src/settings/UserPage.jsx
@@ -14,10 +14,15 @@ import {
FormGroup,
TextField,
Button,
+ InputAdornment,
+ IconButton,
+ OutlinedInput,
} from '@mui/material';
import makeStyles from '@mui/styles/makeStyles';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
+import CachedIcon from '@mui/icons-material/Cached';
+import CloseIcon from '@mui/icons-material/Close';
import { useDispatch, useSelector } from 'react-redux';
import dayjs from 'dayjs';
import EditItemView from './components/EditItemView';
@@ -56,6 +61,8 @@ const UserPage = () => {
const currentUser = useSelector((state) => state.session.user);
const registrationEnabled = useSelector((state) => state.session.server.registration);
const openIdForced = useSelector((state) => state.session.server.openIdForce);
+ const totpEnable = useSelector((state) => state.session.server.attributes.totpEnable);
+ const totpForce = useSelector((state) => state.session.server.attributes.totpForce);
const mapStyles = useMapStyles();
const commonUserAttributes = useCommonUserAttributes(t);
@@ -82,6 +89,15 @@ const UserPage = () => {
}
});
+ const handleGenerateTotp = useCatch(async () => {
+ const response = await fetch('/api/users/totp', { method: 'POST' });
+ if (response.ok) {
+ setItem({ ...item, totpKey: await response.text() })
+ } else {
+ throw Error(await response.text());
+ }
+ });
+
const query = useQuery();
const [queryHandled, setQueryHandled] = useState(false);
const attribute = query.get('attribute');
@@ -103,7 +119,7 @@ const UserPage = () => {
}
};
- const validate = () => item && item.name && item.email && (item.id || item.password);
+ const validate = () => item && item.name && item.email && (item.id || item.password) && (admin || !totpForce || item.totpKey);
return (
<EditItemView
@@ -127,22 +143,42 @@ const UserPage = () => {
<AccordionDetails className={classes.details}>
<TextField
value={item.name || ''}
- onChange={(event) => setItem({ ...item, name: event.target.value })}
+ onChange={(e) => setItem({ ...item, name: e.target.value })}
label={t('sharedName')}
/>
<TextField
value={item.email || ''}
- onChange={(event) => setItem({ ...item, email: event.target.value })}
+ onChange={(e) => setItem({ ...item, email: e.target.value })}
label={t('userEmail')}
disabled={fixedEmail}
/>
{!openIdForced && (
<TextField
type="password"
- onChange={(event) => setItem({ ...item, password: event.target.value })}
+ onChange={(e) => setItem({ ...item, password: e.target.value })}
label={t('userPassword')}
/>
)}
+ {totpEnable && (
+ <FormControl>
+ <InputLabel>{t('loginTotpKey')}</InputLabel>
+ <OutlinedInput
+ readOnly
+ label={t('loginTotpKey')}
+ value={item.totpKey || ''}
+ endAdornment={(
+ <InputAdornment position="end">
+ <IconButton size="small" edge="end" onClick={handleGenerateTotp}>
+ <CachedIcon fontSize="small" />
+ </IconButton>
+ <IconButton size="small" edge="end" onClick={() => setItem({ ...item, totpKey: null })}>
+ <CloseIcon fontSize="small" />
+ </IconButton>
+ </InputAdornment>
+ )}
+ />
+ </FormControl>
+ )}
</AccordionDetails>
</Accordion>
<Accordion>
@@ -154,7 +190,7 @@ const UserPage = () => {
<AccordionDetails className={classes.details}>
<TextField
value={item.phone || ''}
- onChange={(event) => setItem({ ...item, phone: event.target.value })}
+ onChange={(e) => setItem({ ...item, phone: e.target.value })}
label={t('sharedPhone')}
/>
<FormControl>
@@ -176,7 +212,7 @@ const UserPage = () => {
<Select
label={t('settingsCoordinateFormat')}
value={item.coordinateFormat || 'dd'}
- onChange={(event) => setItem({ ...item, coordinateFormat: event.target.value })}
+ onChange={(e) => setItem({ ...item, coordinateFormat: e.target.value })}
>
<MenuItem value="dd">{t('sharedDecimalDegrees')}</MenuItem>
<MenuItem value="ddm">{t('sharedDegreesDecimalMinutes')}</MenuItem>
@@ -241,12 +277,12 @@ const UserPage = () => {
/>
<TextField
value={item.poiLayer || ''}
- onChange={(event) => setItem({ ...item, poiLayer: event.target.value })}
+ onChange={(e) => setItem({ ...item, poiLayer: e.target.value })}
label={t('mapPoiLayer')}
/>
<FormGroup>
<FormControlLabel
- control={<Checkbox checked={item.twelveHourFormat} onChange={(event) => setItem({ ...item, twelveHourFormat: event.target.checked })} />}
+ control={<Checkbox checked={item.twelveHourFormat} onChange={(e) => setItem({ ...item, twelveHourFormat: e.target.checked })} />}
label={t('settingsTwelveHourFormat')}
/>
</FormGroup>
@@ -262,19 +298,19 @@ const UserPage = () => {
<TextField
type="number"
value={item.latitude || 0}
- onChange={(event) => setItem({ ...item, latitude: Number(event.target.value) })}
+ onChange={(e) => setItem({ ...item, latitude: Number(e.target.value) })}
label={t('positionLatitude')}
/>
<TextField
type="number"
value={item.longitude || 0}
- onChange={(event) => setItem({ ...item, longitude: Number(event.target.value) })}
+ onChange={(e) => setItem({ ...item, longitude: Number(e.target.value) })}
label={t('positionLongitude')}
/>
<TextField
type="number"
value={item.zoom || 0}
- onChange={(event) => setItem({ ...item, zoom: Number(event.target.value) })}
+ onChange={(e) => setItem({ ...item, zoom: Number(e.target.value) })}
label={t('serverZoom')}
/>
<Button
@@ -378,7 +414,7 @@ const UserPage = () => {
<AccordionDetails className={classes.details}>
<TextField
value={deleteEmail}
- onChange={(event) => setDeleteEmail(event.target.value)}
+ onChange={(e) => setDeleteEmail(e.target.value)}
label={t('userEmail')}
error={deleteFailed}
/>