refactor: totp
parent
b03c0b7ace
commit
d7d280dd54
|
@ -2,7 +2,8 @@
|
||||||
"Actions": "Actions",
|
"Actions": "Actions",
|
||||||
"Add": "Add",
|
"Add": "Add",
|
||||||
"Add Credential": "Add Credential",
|
"Add Credential": "Add Credential",
|
||||||
"Added": "Added {{when, datetime}}",
|
"Added": "Added",
|
||||||
|
"Added when": "Added {{when, datetime}}",
|
||||||
"Are you sure you want to remove the WebAuthn credential from from your account": "Are you sure you want to remove the WebAuthn credential {{description}} from your account?",
|
"Are you sure you want to remove the WebAuthn credential from from your account": "Are you sure you want to remove the WebAuthn credential {{description}} from your account?",
|
||||||
"Attestation Type": "Attestation Type",
|
"Attestation Type": "Attestation Type",
|
||||||
"Authenticator GUID": "Authenticator GUID",
|
"Authenticator GUID": "Authenticator GUID",
|
||||||
|
@ -22,11 +23,12 @@
|
||||||
"Enter a description for this credential": "Enter a description for this credential",
|
"Enter a description for this credential": "Enter a description for this credential",
|
||||||
"Extended WebAuthn credential information for security key": "Extended WebAuthn credential information for security key {{description}}",
|
"Extended WebAuthn credential information for security key": "Extended WebAuthn credential information for security key {{description}}",
|
||||||
"Identifier": "Identifier",
|
"Identifier": "Identifier",
|
||||||
"Last Used": "Last Used {{when, datetime}}",
|
"Last Used": "Last Used",
|
||||||
|
"Last Used when": "Last Used {{when, datetime}}",
|
||||||
"Manage your security keys": "Manage your security keys",
|
"Manage your security keys": "Manage your security keys",
|
||||||
"Name": "Name",
|
"Name": "Name",
|
||||||
"No": "No",
|
"No": "No",
|
||||||
"No Registered WebAuthn Credentials": "No Registered WebAuthn Credentials",
|
"No WebAuthn Credentials have been registered. If you'd like to register one click add.": "No WebAuthn Credentials have been registered. If you'd like to register one click add.",
|
||||||
"Overview": "Overview",
|
"Overview": "Overview",
|
||||||
"Provide the details for the new security key": "Provide the details for the new security key",
|
"Provide the details for the new security key": "Provide the details for the new security key",
|
||||||
"Register WebAuthn Credential": "Register WebAuthn Credential",
|
"Register WebAuthn Credential": "Register WebAuthn Credential",
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { useRemoteCall } from "@hooks/RemoteCall";
|
|
||||||
import { getUserWebauthnDevices } from "@services/UserWebauthnDevices";
|
|
||||||
|
|
||||||
export function useUserWebauthnDevices() {
|
|
||||||
return useRemoteCall(getUserWebauthnDevices, []);
|
|
||||||
}
|
|
|
@ -1,21 +1,22 @@
|
||||||
import React, { ReactNode, useEffect } from "react";
|
import React, { ReactNode, SyntheticEvent, useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { Dashboard } from "@mui/icons-material";
|
import { Close, Dashboard } from "@mui/icons-material";
|
||||||
|
import MenuIcon from "@mui/icons-material/Menu";
|
||||||
import SystemSecurityUpdateGoodIcon from "@mui/icons-material/SystemSecurityUpdateGood";
|
import SystemSecurityUpdateGoodIcon from "@mui/icons-material/SystemSecurityUpdateGood";
|
||||||
import {
|
import {
|
||||||
AppBar,
|
AppBar,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Divider,
|
||||||
Drawer,
|
|
||||||
Grid,
|
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemButton,
|
ListItemButton,
|
||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
|
SwipeableDrawer,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { IndexRoute, SettingsRoute, SettingsTwoFactorAuthenticationSubRoute } from "@constants/Routes";
|
import { IndexRoute, SettingsRoute, SettingsTwoFactorAuthenticationSubRoute } from "@constants/Routes";
|
||||||
|
@ -33,8 +34,7 @@ const defaultDrawerWidth = 240;
|
||||||
|
|
||||||
const SettingsLayout = function (props: Props) {
|
const SettingsLayout = function (props: Props) {
|
||||||
const { t: translate } = useTranslation("settings");
|
const { t: translate } = useTranslation("settings");
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
const navigate = useRouterNavigate();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.title) {
|
if (props.title) {
|
||||||
|
@ -54,72 +54,179 @@ const SettingsLayout = function (props: Props) {
|
||||||
|
|
||||||
const drawerWidth = props.drawerWidth === undefined ? defaultDrawerWidth : props.drawerWidth;
|
const drawerWidth = props.drawerWidth === undefined ? defaultDrawerWidth : props.drawerWidth;
|
||||||
|
|
||||||
|
const handleToggleDrawer = (event: SyntheticEvent) => {
|
||||||
|
if (
|
||||||
|
event.nativeEvent instanceof KeyboardEvent &&
|
||||||
|
event.nativeEvent.type === "keydown" &&
|
||||||
|
(event.nativeEvent.key === "Tab" || event.nativeEvent.key === "Shift")
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDrawerOpen((state) => !state);
|
||||||
|
};
|
||||||
|
|
||||||
|
const container = window !== undefined ? () => window.document.body : undefined;
|
||||||
|
|
||||||
|
const drawer = (
|
||||||
|
<Box onClick={handleToggleDrawer} sx={{ textAlign: "center" }}>
|
||||||
|
<Typography variant="h6" sx={{ my: 2 }}>
|
||||||
|
{translate("Settings")}
|
||||||
|
</Typography>
|
||||||
|
<Divider />
|
||||||
|
<List>
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<DrawerNavItem key={item.keyname} text={item.text} pathname={item.pathname} icon={item.icon} />
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: "flex" }}>
|
<Box sx={{ display: "flex" }}>
|
||||||
<AppBar position="fixed" sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}>
|
<AppBar component={"nav"}>
|
||||||
<Toolbar variant="dense">
|
<Toolbar>
|
||||||
<Typography style={{ flexGrow: 1 }}>Authelia {translate("Settings")}</Typography>
|
<IconButton
|
||||||
<Button
|
edge="start"
|
||||||
variant="contained"
|
color="inherit"
|
||||||
color="success"
|
aria-label="open drawer"
|
||||||
onClick={() => {
|
onClick={handleToggleDrawer}
|
||||||
navigate(IndexRoute);
|
sx={{ mr: 2 }}
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{translate("Close")}
|
<MenuIcon />
|
||||||
</Button>
|
</IconButton>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
component={"div"}
|
||||||
|
sx={{ flexGrow: 1, display: { xs: "none", sm: drawerOpen ? "none" : "block" } }}
|
||||||
|
>
|
||||||
|
{translate("Settings")}
|
||||||
|
</Typography>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
<Drawer
|
<Box component={"nav"}>
|
||||||
variant="permanent"
|
<SwipeableDrawer
|
||||||
sx={{
|
container={container}
|
||||||
width: drawerWidth,
|
anchor={"left"}
|
||||||
flexShrink: 0,
|
open={drawerOpen}
|
||||||
[`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: "border-box" },
|
onOpen={handleToggleDrawer}
|
||||||
}}
|
onClose={handleToggleDrawer}
|
||||||
>
|
ModalProps={{
|
||||||
<Toolbar variant="dense" />
|
keepMounted: true,
|
||||||
<Box sx={{ overflow: "auto" }}>
|
}}
|
||||||
<List>
|
sx={{
|
||||||
<SettingsMenuItem pathname={SettingsRoute} text={translate("Overview")} icon={<Dashboard />} />
|
display: { xs: "block" },
|
||||||
<SettingsMenuItem
|
"& .MuiDrawer-paper": { boxSizing: "border-box", width: drawerWidth },
|
||||||
pathname={`${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`}
|
}}
|
||||||
text={translate("Two-Factor Authentication")}
|
>
|
||||||
icon={<SystemSecurityUpdateGoodIcon />}
|
{drawer}
|
||||||
/>
|
</SwipeableDrawer>
|
||||||
</List>
|
</Box>
|
||||||
</Box>
|
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
|
||||||
</Drawer>
|
<Toolbar />
|
||||||
<Grid container id={props.id} spacing={0}>
|
{props.children}
|
||||||
<Grid item xs={12}>
|
</Box>
|
||||||
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
|
|
||||||
<Toolbar variant="dense" />
|
|
||||||
{props.children}
|
|
||||||
</Box>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SettingsLayout;
|
export default SettingsLayout;
|
||||||
|
|
||||||
interface SettingsMenuItemProps {
|
interface NavItem {
|
||||||
pathname: string;
|
keyname?: string;
|
||||||
text: string;
|
text: string;
|
||||||
icon: ReactNode;
|
pathname: string;
|
||||||
|
icon?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SettingsMenuItem = function (props: SettingsMenuItemProps) {
|
const navItems: NavItem[] = [
|
||||||
|
{ keyname: "overview", text: "Overview", pathname: SettingsRoute, icon: <Dashboard color={"primary"} /> },
|
||||||
|
{
|
||||||
|
keyname: "twofactor",
|
||||||
|
text: "Two-Factor Authentication",
|
||||||
|
pathname: `${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`,
|
||||||
|
icon: <SystemSecurityUpdateGoodIcon color={"primary"} />,
|
||||||
|
},
|
||||||
|
{ keyname: "close", text: "Close", pathname: IndexRoute, icon: <Close color={"error"} /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
const DrawerNavItem = function (props: NavItem) {
|
||||||
const selected = window.location.pathname === props.pathname || window.location.pathname === props.pathname + "/";
|
const selected = window.location.pathname === props.pathname || window.location.pathname === props.pathname + "/";
|
||||||
const navigate = useRouterNavigate();
|
const navigate = useRouterNavigate();
|
||||||
|
|
||||||
|
const handleOnClick = useCallback(() => {
|
||||||
|
if (selected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(props.pathname);
|
||||||
|
}, [navigate, props, selected]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem disablePadding onClick={!selected ? () => navigate(props.pathname) : undefined}>
|
<ListItem disablePadding onClick={handleOnClick}>
|
||||||
<ListItemButton selected={selected}>
|
<ListItemButton selected={selected}>
|
||||||
<ListItemIcon>{props.icon}</ListItemIcon>
|
{props.icon ? <ListItemIcon>{props.icon}</ListItemIcon> : null}
|
||||||
<ListItemText primary={props.text} />
|
<ListItemText primary={props.text} />
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
interface SettingsMenuProps {
|
||||||
|
isXL: boolean;
|
||||||
|
handleClickMenuItem: () => void;
|
||||||
|
handleToggleDrawer: (open: boolean) => (event: SyntheticEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingsMenu = function (props: SettingsMenuProps) {
|
||||||
|
const { t: translate } = useTranslation("settings");
|
||||||
|
const navigate = useRouterNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
height: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.isXL ? null : (
|
||||||
|
<Fragment>
|
||||||
|
<IconButton sx={{ mb: 2 }} onClick={props.handleToggleDrawer(false)}>
|
||||||
|
<Close />
|
||||||
|
</IconButton>
|
||||||
|
<Divider sx={{ mb: 2 }} />
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
<List sx={{ mb: 2 }}>
|
||||||
|
<SettingsMenuItem
|
||||||
|
pathname={SettingsRoute}
|
||||||
|
text={translate("Overview")}
|
||||||
|
icon={<Dashboard color={"primary"} />}
|
||||||
|
onClick={props.handleClickMenuItem}
|
||||||
|
/>
|
||||||
|
<SettingsMenuItem
|
||||||
|
pathname={`${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`}
|
||||||
|
text={translate("Two-Factor Authentication")}
|
||||||
|
icon={<SystemSecurityUpdateGoodIcon color={"primary"} />}
|
||||||
|
onClick={props.handleClickMenuItem}
|
||||||
|
/>
|
||||||
|
<ListItem
|
||||||
|
disablePadding
|
||||||
|
onClick={() => {
|
||||||
|
navigate(IndexRoute);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemButton>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Close color={"error"} />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={"Close"} />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
|
@ -11,7 +11,6 @@ import { deleteUserTOTPConfiguration } from "@services/UserInfoTOTPConfiguration
|
||||||
import DeleteDialog from "@views/Settings/TwoFactorAuthentication/DeleteDialog";
|
import DeleteDialog from "@views/Settings/TwoFactorAuthentication/DeleteDialog";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
index: number;
|
|
||||||
config: UserInfoTOTPConfiguration;
|
config: UserInfoTOTPConfiguration;
|
||||||
handleRefresh: () => void;
|
handleRefresh: () => void;
|
||||||
}
|
}
|
||||||
|
@ -69,14 +68,14 @@ export default function TOTPDevice(props: Props) {
|
||||||
"Are you sure you want to remove the Time-based One Time Password from from your account",
|
"Are you sure you want to remove the Time-based One Time Password from from your account",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Stack direction="row" spacing={1} alignItems="center">
|
<Stack direction={"row"} spacing={1} alignItems={"center"}>
|
||||||
<QrCode2 fontSize="large" />
|
<QrCode2 fontSize="large" />
|
||||||
<Stack spacing={0} sx={{ minWidth: 400 }}>
|
<Stack spacing={0} sx={{ minWidth: 400 }}>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography display="inline" sx={{ fontWeight: "bold" }}>
|
<Typography display={"inline"} sx={{ fontWeight: "bold" }}>
|
||||||
{props.config.issuer}
|
{props.config.issuer}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography display="inline" variant="body2">
|
<Typography display={"inline"} variant={"body2"}>
|
||||||
{" (" +
|
{" (" +
|
||||||
translate("{{algorithm}}, {{digits}} digits, {{seconds}} seconds", {
|
translate("{{algorithm}}, {{digits}} digits, {{seconds}} seconds", {
|
||||||
algorithm: toAlgorithmString(props.config.algorithm),
|
algorithm: toAlgorithmString(props.config.algorithm),
|
||||||
|
@ -87,7 +86,7 @@ export default function TOTPDevice(props: Props) {
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Typography variant={"caption"}>
|
<Typography variant={"caption"}>
|
||||||
{translate("Added", {
|
{translate("Added when", {
|
||||||
when: props.config.created_at,
|
when: props.config.created_at,
|
||||||
formatParams: {
|
formatParams: {
|
||||||
when: {
|
when: {
|
||||||
|
@ -103,7 +102,7 @@ export default function TOTPDevice(props: Props) {
|
||||||
<Typography variant={"caption"}>
|
<Typography variant={"caption"}>
|
||||||
{props.config.last_used_at === undefined
|
{props.config.last_used_at === undefined
|
||||||
? translate("Never used")
|
? translate("Never used")
|
||||||
: translate("Last Used", {
|
: translate("Last Used when", {
|
||||||
when: props.config.last_used_at,
|
when: props.config.last_used_at,
|
||||||
formatParams: {
|
formatParams: {
|
||||||
when: {
|
when: {
|
||||||
|
@ -117,7 +116,6 @@ export default function TOTPDevice(props: Props) {
|
||||||
})}
|
})}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Tooltip title={translate("Remove the Time-based One Time Password configuration")}>
|
<Tooltip title={translate("Remove the Time-based One Time Password configuration")}>
|
||||||
<Button
|
<Button
|
||||||
variant={"outlined"}
|
variant={"outlined"}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { Fragment, useState } from "react";
|
import React, { Fragment, useState } from "react";
|
||||||
|
|
||||||
import { Button, Grid, Paper, Stack, Tooltip, Typography } from "@mui/material";
|
import { Button, Paper, Stack, Tooltip, Typography } from "@mui/material";
|
||||||
|
import Grid from "@mui/material/Unstable_Grid2";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { UserInfoTOTPConfiguration } from "@models/TOTPConfiguration";
|
import { UserInfoTOTPConfiguration } from "@models/TOTPConfiguration";
|
||||||
|
@ -26,14 +27,14 @@ export default function TOTPPanel(props: Props) {
|
||||||
props.handleRefreshState();
|
props.handleRefreshState();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Paper variant="outlined" sx={{ p: 3 }}>
|
<Paper variant={"outlined"}>
|
||||||
<Grid container spacing={1}>
|
<Grid container spacing={2} padding={2}>
|
||||||
<Grid item xs={12}>
|
<Grid xs={12} lg={8}>
|
||||||
<Typography variant="h5">{translate("One Time Password")}</Typography>
|
<Typography variant={"h5"}>{translate("One Time Password")}</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
{props.config === undefined || props.config === null ? (
|
{props.config === undefined || props.config === null ? (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Grid item xs={12}>
|
<Grid xs={2}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={translate("Click to add a Time-based One Time Password to your account")}
|
title={translate("Click to add a Time-based One Time Password to your account")}
|
||||||
>
|
>
|
||||||
|
@ -48,7 +49,7 @@ export default function TOTPPanel(props: Props) {
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12}>
|
<Grid xs={12}>
|
||||||
<Typography variant={"subtitle2"}>
|
<Typography variant={"subtitle2"}>
|
||||||
{translate(
|
{translate(
|
||||||
"The One Time Password has not been registered. If you'd like to register it click add.",
|
"The One Time Password has not been registered. If you'd like to register it click add.",
|
||||||
|
@ -57,9 +58,9 @@ export default function TOTPPanel(props: Props) {
|
||||||
</Grid>
|
</Grid>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
) : (
|
) : (
|
||||||
<Grid item xs={12}>
|
<Grid xs={12}>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={3}>
|
||||||
<TOTPDevice index={0} config={props.config} handleRefresh={props.handleRefreshState} />
|
<TOTPDevice config={props.config} handleRefresh={props.handleRefreshState} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React, { Fragment, useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { IconDefinition, faCopy, faKey, faTimesCircle } from "@fortawesome/free-solid-svg-icons";
|
import { IconDefinition, faCopy, faKey, faTimesCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { Visibility } from "@mui/icons-material";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
@ -9,11 +10,11 @@ import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogContentText,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
Grid,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
Link,
|
Link,
|
||||||
Radio,
|
Radio,
|
||||||
|
@ -26,6 +27,7 @@ import {
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { red } from "@mui/material/colors";
|
import { red } from "@mui/material/colors";
|
||||||
|
import Grid from "@mui/material/Unstable_Grid2";
|
||||||
import makeStyles from "@mui/styles/makeStyles";
|
import makeStyles from "@mui/styles/makeStyles";
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
import { QRCodeSVG } from "qrcode.react";
|
import { QRCodeSVG } from "qrcode.react";
|
||||||
|
@ -69,6 +71,7 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
const [hasErrored, setHasErrored] = useState(false);
|
const [hasErrored, setHasErrored] = useState(false);
|
||||||
const [dialValue, setDialValue] = useState("");
|
const [dialValue, setDialValue] = useState("");
|
||||||
const [dialState, setDialState] = useState(State.Idle);
|
const [dialState, setDialState] = useState(State.Idle);
|
||||||
|
const [totpSecretURLHidden, setTOTPSecretURLHidden] = useState(true);
|
||||||
|
|
||||||
const resetStates = () => {
|
const resetStates = () => {
|
||||||
setOptions(null);
|
setOptions(null);
|
||||||
|
@ -85,6 +88,7 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
setActiveStep(0);
|
setActiveStep(0);
|
||||||
setDialValue("");
|
setDialValue("");
|
||||||
setDialState(State.Idle);
|
setDialState(State.Idle);
|
||||||
|
setTOTPSecretURLHidden(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
|
@ -112,7 +116,7 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!props.open || activeStep !== 0) {
|
if (!props.open || activeStep !== 0 || options !== null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,13 +130,14 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
setOptionPeriod(opts.period);
|
setOptionPeriod(opts.period);
|
||||||
setOptionPeriods(opts.periods.map((period) => period.toString()));
|
setOptionPeriods(opts.periods.map((period) => period.toString()));
|
||||||
})();
|
})();
|
||||||
}, [props.open, activeStep]);
|
}, [props.open, activeStep, options]);
|
||||||
|
|
||||||
const handleSetStepPrevious = useCallback(() => {
|
const handleSetStepPrevious = useCallback(() => {
|
||||||
if (activeStep === 0) {
|
if (activeStep === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setShowAdvanced(false);
|
||||||
setActiveStep((prevState) => (prevState -= 1));
|
setActiveStep((prevState) => (prevState -= 1));
|
||||||
}, [activeStep]);
|
}, [activeStep]);
|
||||||
|
|
||||||
|
@ -141,6 +146,7 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setShowAdvanced(false);
|
||||||
setActiveStep((prevState) => (prevState += 1));
|
setActiveStep((prevState) => (prevState += 1));
|
||||||
}, [activeStep]);
|
}, [activeStep]);
|
||||||
|
|
||||||
|
@ -213,7 +219,6 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
function SecretButton(text: string | undefined, action: string, icon: IconDefinition) {
|
function SecretButton(text: string | undefined, action: string, icon: IconDefinition) {
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
className={styles.secretButtons}
|
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(`${text}`);
|
navigator.clipboard.writeText(`${text}`);
|
||||||
|
@ -232,21 +237,20 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{options === null ? (
|
{options === null ? (
|
||||||
<Grid item xs={12}>
|
<Grid xs={12}>
|
||||||
<Typography>Loading...</Typography>
|
<Typography>Loading...</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
) : (
|
) : (
|
||||||
<Fragment>
|
<Grid container>
|
||||||
<Grid item xs={12}>
|
<Grid xs={12}>
|
||||||
<Typography>{translate("To begin select next")}</Typography>
|
<Typography>{translate("To begin select next")}</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} hidden={hideAdvanced}>
|
<Grid xs={12} hidden={hideAdvanced}>
|
||||||
<Button variant={"outlined"} color={"warning"} onClick={toggleAdvanced}>
|
<Button variant={"outlined"} color={"warning"} onClick={toggleAdvanced}>
|
||||||
{showAdvanced ? translate("Hide Advanced") : translate("Show Advanced")}
|
{showAdvanced ? translate("Hide Advanced") : translate("Show Advanced")}
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid
|
<Grid
|
||||||
item
|
|
||||||
xs={12}
|
xs={12}
|
||||||
hidden={hideAdvanced || !showAdvanced}
|
hidden={hideAdvanced || !showAdvanced}
|
||||||
justifyContent={"center"}
|
justifyContent={"center"}
|
||||||
|
@ -330,15 +334,15 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Fragment>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
case 1:
|
case 1:
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Grid item xs={12}>
|
<Grid xs={12}>
|
||||||
<Box className={styles.googleAuthenticator}>
|
<Box>
|
||||||
<Typography className={styles.googleAuthenticatorText}>
|
<Typography className={styles.googleAuthenticatorText}>
|
||||||
{translate("Need Google Authenticator?")}
|
{translate("Need Google Authenticator?")}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
@ -351,10 +355,10 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12}>
|
<Grid xs={12}>
|
||||||
<Box className={classnames(qrcodeFuzzyStyle, styles.qrcodeContainer)}>
|
<Box className={classnames(qrcodeFuzzyStyle, styles.qrcodeContainer)}>
|
||||||
<Link href={totpSecretURL} underline="hover">
|
<Link href={totpSecretURL} underline="hover">
|
||||||
<QRCodeSVG value={totpSecretURL} className={styles.qrcode} size={256} />
|
<QRCodeSVG value={totpSecretURL} className={styles.qrcode} size={128} />
|
||||||
{!hasErrored && totpIsLoading ? (
|
{!hasErrored && totpIsLoading ? (
|
||||||
<CircularProgress className={styles.loader} size={128} />
|
<CircularProgress className={styles.loader} size={128} />
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -364,22 +368,20 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
</Link>
|
</Link>
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12}>
|
<Grid xs={12}>
|
||||||
<Grid container spacing={2} justifyContent={"center"}>
|
<Grid container spacing={2} justifyContent={"center"}>
|
||||||
<Grid item xs={12}>
|
<Grid xs={2}>
|
||||||
{totpSecretURL !== "empty" ? (
|
<IconButton
|
||||||
<TextField
|
color="primary"
|
||||||
id="secret-url"
|
onClick={() => {
|
||||||
label={translate("Secret")}
|
setTOTPSecretURLHidden((value) => !value);
|
||||||
className={styles.secret}
|
}}
|
||||||
value={totpSecretURL}
|
size="large"
|
||||||
InputProps={{
|
>
|
||||||
readOnly: true,
|
<Visibility />
|
||||||
}}
|
</IconButton>
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={2}>
|
<Grid xs={2}>
|
||||||
{totpSecretBase32
|
{totpSecretBase32
|
||||||
? SecretButton(
|
? SecretButton(
|
||||||
totpSecretBase32,
|
totpSecretBase32,
|
||||||
|
@ -388,18 +390,31 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={2}>
|
<Grid xs={2}>
|
||||||
{totpSecretURL !== "empty"
|
{totpSecretURL !== ""
|
||||||
? SecretButton(totpSecretURL, translate("OTP URL copied to clipboard"), faCopy)
|
? SecretButton(totpSecretURL, translate("OTP URL copied to clipboard"), faCopy)
|
||||||
: null}
|
: null}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Grid xs={12}>
|
||||||
|
<TextField
|
||||||
|
id="secret-url"
|
||||||
|
label={translate("Secret")}
|
||||||
|
className={styles.secret}
|
||||||
|
value={totpSecretURL}
|
||||||
|
multiline={true}
|
||||||
|
hidden={totpSecretURLHidden || totpSecretURL === ""}
|
||||||
|
InputProps={{
|
||||||
|
readOnly: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
case 2:
|
case 2:
|
||||||
return (
|
return (
|
||||||
<Grid item xs={12}>
|
<Grid xs={12}>
|
||||||
<OTPDial
|
<OTPDial
|
||||||
passcode={dialValue}
|
passcode={dialValue}
|
||||||
state={dialState}
|
state={dialState}
|
||||||
|
@ -416,8 +431,11 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
<Dialog open={props.open} onClose={handleOnClose} maxWidth={"xs"} fullWidth={true}>
|
<Dialog open={props.open} onClose={handleOnClose} maxWidth={"xs"} fullWidth={true}>
|
||||||
<DialogTitle>{translate("Register One Time Password (TOTP)")}</DialogTitle>
|
<DialogTitle>{translate("Register One Time Password (TOTP)")}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
<DialogContentText sx={{ mb: 3 }}>
|
||||||
|
{translate("This dialog allows registration of the One-Time Password.")}
|
||||||
|
</DialogContentText>
|
||||||
<Grid container spacing={0} alignItems={"center"} justifyContent={"center"} textAlign={"center"}>
|
<Grid container spacing={0} alignItems={"center"} justifyContent={"center"} textAlign={"center"}>
|
||||||
<Grid item xs={12}>
|
<Grid xs={12}>
|
||||||
<Stepper activeStep={activeStep}>
|
<Stepper activeStep={activeStep}>
|
||||||
{steps.map((label, index) => {
|
{steps.map((label, index) => {
|
||||||
const stepProps: { completed?: boolean } = {};
|
const stepProps: { completed?: boolean } = {};
|
||||||
|
@ -432,7 +450,7 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
})}
|
})}
|
||||||
</Stepper>
|
</Stepper>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12}>
|
<Grid xs={12}>
|
||||||
<Grid container spacing={2} paddingY={3} justifyContent={"center"}>
|
<Grid container spacing={2} paddingY={3} justifyContent={"center"}>
|
||||||
{renderStep(activeStep)}
|
{renderStep(activeStep)}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -440,23 +458,13 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
</Grid>
|
</Grid>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button
|
<Button color={"primary"} onClick={handleSetStepPrevious} disabled={activeStep === 0}>
|
||||||
variant={"outlined"}
|
|
||||||
color={"primary"}
|
|
||||||
onClick={handleSetStepPrevious}
|
|
||||||
disabled={activeStep === 0}
|
|
||||||
>
|
|
||||||
{translate("Previous")}
|
{translate("Previous")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant={"outlined"} color={"primary"} onClick={handleClose}>
|
<Button color={"error"} onClick={handleClose}>
|
||||||
{translate("Cancel")}
|
{translate("Cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button color={"primary"} onClick={handleSetStepNext} disabled={activeStep === steps.length - 1}>
|
||||||
variant={"outlined"}
|
|
||||||
color={"primary"}
|
|
||||||
onClick={handleSetStepNext}
|
|
||||||
disabled={activeStep === steps.length - 1}
|
|
||||||
>
|
|
||||||
{translate("Next")}
|
{translate("Next")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
@ -465,10 +473,6 @@ export default function TOTPRegisterDialogController(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => ({
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
root: {
|
|
||||||
paddingTop: theme.spacing(4),
|
|
||||||
paddingBottom: theme.spacing(4),
|
|
||||||
},
|
|
||||||
qrcode: {
|
qrcode: {
|
||||||
marginTop: theme.spacing(2),
|
marginTop: theme.spacing(2),
|
||||||
marginBottom: theme.spacing(2),
|
marginBottom: theme.spacing(2),
|
||||||
|
@ -483,15 +487,10 @@ const useStyles = makeStyles((theme: Theme) => ({
|
||||||
marginBottom: theme.spacing(1),
|
marginBottom: theme.spacing(1),
|
||||||
width: "256px",
|
width: "256px",
|
||||||
},
|
},
|
||||||
googleAuthenticator: {},
|
|
||||||
googleAuthenticatorText: {
|
googleAuthenticatorText: {
|
||||||
fontSize: theme.typography.fontSize * 0.8,
|
fontSize: theme.typography.fontSize * 0.8,
|
||||||
},
|
},
|
||||||
googleAuthenticatorBadges: {},
|
googleAuthenticatorBadges: {},
|
||||||
secretButtons: {},
|
|
||||||
doneButton: {
|
|
||||||
width: "256px",
|
|
||||||
},
|
|
||||||
qrcodeContainer: {
|
qrcodeContainer: {
|
||||||
position: "relative",
|
position: "relative",
|
||||||
display: "inline-block",
|
display: "inline-block",
|
||||||
|
|
|
@ -4,7 +4,7 @@ import Grid from "@mui/material/Unstable_Grid2";
|
||||||
|
|
||||||
import { useNotifications } from "@hooks/NotificationsContext";
|
import { useNotifications } from "@hooks/NotificationsContext";
|
||||||
import { useUserInfoPOST } from "@hooks/UserInfo";
|
import { useUserInfoPOST } from "@hooks/UserInfo";
|
||||||
import { useUserInfoTOTPConfiguration, useUserInfoTOTPConfigurationOptional } from "@hooks/UserInfoTOTPConfiguration";
|
import { useUserInfoTOTPConfigurationOptional } from "@hooks/UserInfoTOTPConfiguration";
|
||||||
import { useUserWebAuthnDevices } from "@hooks/WebAuthnDevices";
|
import { useUserWebAuthnDevices } from "@hooks/WebAuthnDevices";
|
||||||
import TOTPPanel from "@views/Settings/TwoFactorAuthentication/TOTPPanel";
|
import TOTPPanel from "@views/Settings/TwoFactorAuthentication/TOTPPanel";
|
||||||
import WebAuthnDevicesPanel from "@views/Settings/TwoFactorAuthentication/WebAuthnDevicesPanel";
|
import WebAuthnDevicesPanel from "@views/Settings/TwoFactorAuthentication/WebAuthnDevicesPanel";
|
||||||
|
|
Loading…
Reference in New Issue