feat: settings router (#4377)
parent
dcd65515fc
commit
0f8de33f2f
|
@ -36,7 +36,7 @@ import LoginPortal from "@views/LoginPortal/LoginPortal";
|
||||||
import SignOut from "@views/LoginPortal/SignOut/SignOut";
|
import SignOut from "@views/LoginPortal/SignOut/SignOut";
|
||||||
import ResetPasswordStep1 from "@views/ResetPassword/ResetPasswordStep1";
|
import ResetPasswordStep1 from "@views/ResetPassword/ResetPasswordStep1";
|
||||||
import ResetPasswordStep2 from "@views/ResetPassword/ResetPasswordStep2";
|
import ResetPasswordStep2 from "@views/ResetPassword/ResetPasswordStep2";
|
||||||
import SettingsView from "@views/Settings/SettingsView";
|
import SettingsRouter from "@views/Settings/SettingsRouter";
|
||||||
|
|
||||||
import "@fortawesome/fontawesome-svg-core/styles.css";
|
import "@fortawesome/fontawesome-svg-core/styles.css";
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@ const App: React.FC<Props> = (props: Props) => {
|
||||||
<Route path={RegisterOneTimePasswordRoute} element={<RegisterOneTimePassword />} />
|
<Route path={RegisterOneTimePasswordRoute} element={<RegisterOneTimePassword />} />
|
||||||
<Route path={LogoutRoute} element={<SignOut />} />
|
<Route path={LogoutRoute} element={<SignOut />} />
|
||||||
<Route path={ConsentRoute} element={<ConsentView />} />
|
<Route path={ConsentRoute} element={<ConsentView />} />
|
||||||
<Route path={SettingsRoute} element={<SettingsView />} />
|
<Route path={`${SettingsRoute}/*`} element={<SettingsRouter />} />
|
||||||
<Route
|
<Route
|
||||||
path={`${IndexRoute}*`}
|
path={`${IndexRoute}*`}
|
||||||
element={
|
element={
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { Grid, Link, Theme } from "@mui/material";
|
||||||
|
import { grey } from "@mui/material/colors";
|
||||||
|
import makeStyles from "@mui/styles/makeStyles";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export interface Props {}
|
||||||
|
|
||||||
|
const url = "https://www.authelia.com";
|
||||||
|
|
||||||
|
const Brand = function (props: Props) {
|
||||||
|
const { t: translate } = useTranslation();
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Link href={url} target="_blank" underline="hover" className={styles.poweredBy}>
|
||||||
|
{translate("Powered by")} Authelia
|
||||||
|
</Link>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Brand;
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
|
poweredBy: {
|
||||||
|
fontSize: "0.7em",
|
||||||
|
color: grey[500],
|
||||||
|
},
|
||||||
|
}));
|
|
@ -12,4 +12,6 @@ export const ResetPasswordStep2Route: string = "/reset-password/step2";
|
||||||
export const RegisterWebauthnRoute: string = "/webauthn/register";
|
export const RegisterWebauthnRoute: string = "/webauthn/register";
|
||||||
export const RegisterOneTimePasswordRoute: string = "/one-time-password/register";
|
export const RegisterOneTimePasswordRoute: string = "/one-time-password/register";
|
||||||
export const LogoutRoute: string = "/logout";
|
export const LogoutRoute: string = "/logout";
|
||||||
|
|
||||||
export const SettingsRoute: string = "/settings";
|
export const SettingsRoute: string = "/settings";
|
||||||
|
export const SettingsTwoFactorAuthenticationSubRoute: string = "/two-factor-authentication";
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
|
export function useRouterNavigate() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
(
|
||||||
|
pathname: string,
|
||||||
|
preserveSearchParams: boolean = true,
|
||||||
|
searchParamsOverride: URLSearchParams | undefined = undefined,
|
||||||
|
) => {
|
||||||
|
if (searchParamsOverride && URLSearchParamsHasValues(searchParamsOverride)) {
|
||||||
|
navigate({ pathname: pathname, search: `?${searchParamsOverride.toString()}` });
|
||||||
|
} else if (preserveSearchParams && URLSearchParamsHasValues(searchParams)) {
|
||||||
|
navigate({ pathname: pathname, search: `?${searchParams.toString()}` });
|
||||||
|
} else {
|
||||||
|
navigate({ pathname: pathname });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[navigate, searchParams],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function URLSearchParamsHasValues(params?: URLSearchParams) {
|
||||||
|
return params ? !params.entries().next().done : false;
|
||||||
|
}
|
|
@ -1,13 +1,13 @@
|
||||||
import React, { ReactNode, useEffect } from "react";
|
import React, { ReactNode, useEffect } from "react";
|
||||||
|
|
||||||
import SettingsIcon from "@mui/icons-material/Settings";
|
import SettingsIcon from "@mui/icons-material/Settings";
|
||||||
import { AppBar, Box, Container, Grid, IconButton, Link, Theme, Toolbar, Typography } from "@mui/material";
|
import { AppBar, Box, Container, Grid, IconButton, Theme, Toolbar, Typography } from "@mui/material";
|
||||||
import { grey } from "@mui/material/colors";
|
|
||||||
import makeStyles from "@mui/styles/makeStyles";
|
import makeStyles from "@mui/styles/makeStyles";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { ReactComponent as UserSvg } from "@assets/images/user.svg";
|
import { ReactComponent as UserSvg } from "@assets/images/user.svg";
|
||||||
|
import Brand from "@components/Brand";
|
||||||
import TypographyWithTooltip from "@components/TypographyWithTootip";
|
import TypographyWithTooltip from "@components/TypographyWithTootip";
|
||||||
import { SettingsRoute } from "@root/constants/Routes";
|
import { SettingsRoute } from "@root/constants/Routes";
|
||||||
import { getLogoOverride } from "@utils/Configuration";
|
import { getLogoOverride } from "@utils/Configuration";
|
||||||
|
@ -23,8 +23,6 @@ export interface Props {
|
||||||
showSettings?: boolean;
|
showSettings?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = "https://www.authelia.com";
|
|
||||||
|
|
||||||
const LoginLayout = function (props: Props) {
|
const LoginLayout = function (props: Props) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
@ -64,9 +62,9 @@ const LoginLayout = function (props: Props) {
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
<Grid
|
<Grid
|
||||||
|
container
|
||||||
id={props.id}
|
id={props.id}
|
||||||
className={styles.root}
|
className={styles.root}
|
||||||
container
|
|
||||||
spacing={0}
|
spacing={0}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
|
@ -97,13 +95,7 @@ const LoginLayout = function (props: Props) {
|
||||||
<Grid item xs={12} className={styles.body}>
|
<Grid item xs={12} className={styles.body}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</Grid>
|
</Grid>
|
||||||
{props.showBrand ? (
|
{props.showBrand ? <Brand /> : null}
|
||||||
<Grid item xs={12}>
|
|
||||||
<Link href={url} target="_blank" underline="hover" className={styles.poweredBy}>
|
|
||||||
{translate("Powered by")} Authelia
|
|
||||||
</Link>
|
|
||||||
</Grid>
|
|
||||||
) : null}
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Container>
|
</Container>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -134,8 +126,4 @@ const useStyles = makeStyles((theme: Theme) => ({
|
||||||
paddingTop: theme.spacing(),
|
paddingTop: theme.spacing(),
|
||||||
paddingBottom: theme.spacing(),
|
paddingBottom: theme.spacing(),
|
||||||
},
|
},
|
||||||
poweredBy: {
|
|
||||||
fontSize: "0.7em",
|
|
||||||
color: grey[500],
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
import React, { ReactNode, useCallback, useEffect } from "react";
|
||||||
|
|
||||||
|
import SystemSecurityUpdateGoodIcon from "@mui/icons-material/SystemSecurityUpdateGood";
|
||||||
|
import {
|
||||||
|
AppBar,
|
||||||
|
Box,
|
||||||
|
Drawer,
|
||||||
|
Grid,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemButton,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
Toolbar,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { SettingsRoute, SettingsTwoFactorAuthenticationSubRoute } from "@constants/Routes";
|
||||||
|
import { useRouterNavigate } from "@hooks/RouterNavigate";
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
id?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
title?: string;
|
||||||
|
titlePrefix?: string;
|
||||||
|
drawerWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultDrawerWidth = 240;
|
||||||
|
|
||||||
|
const SettingsLayout = function (props: Props) {
|
||||||
|
const { t: translate } = useTranslation("settings");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.title) {
|
||||||
|
if (props.titlePrefix) {
|
||||||
|
document.title = `${props.titlePrefix} - ${props.title} - Authelia`;
|
||||||
|
} else {
|
||||||
|
document.title = `${props.title} - Authelia`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (props.titlePrefix) {
|
||||||
|
document.title = `${props.titlePrefix} - ${translate("Settings")} - Authelia`;
|
||||||
|
} else {
|
||||||
|
document.title = `${translate("Settings")} - Authelia`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [props.title, props.titlePrefix, translate]);
|
||||||
|
|
||||||
|
const drawerWidth = props.drawerWidth === undefined ? defaultDrawerWidth : props.drawerWidth;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: "flex" }}>
|
||||||
|
<AppBar position="fixed" sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}>
|
||||||
|
<Toolbar variant="dense">
|
||||||
|
<Typography style={{ flexGrow: 1 }}>{translate("Settings")}</Typography>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
<Drawer
|
||||||
|
variant="permanent"
|
||||||
|
sx={{
|
||||||
|
width: drawerWidth,
|
||||||
|
flexShrink: 0,
|
||||||
|
[`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: "border-box" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Toolbar variant="dense" />
|
||||||
|
<Box sx={{ overflow: "auto" }}>
|
||||||
|
<List>
|
||||||
|
<SettingsMenuItem
|
||||||
|
pathname={`${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`}
|
||||||
|
text={translate("Security Keys")}
|
||||||
|
icon={<SystemSecurityUpdateGoodIcon />}
|
||||||
|
/>
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
</Drawer>
|
||||||
|
<Grid container id={props.id} spacing={0}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
|
||||||
|
<Toolbar variant="dense" />
|
||||||
|
{props.children}
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsLayout;
|
||||||
|
|
||||||
|
interface SettingsMenuItemProps {
|
||||||
|
pathname: string;
|
||||||
|
text: string;
|
||||||
|
icon: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingsMenuItem = function (props: SettingsMenuItemProps) {
|
||||||
|
const selected = window.location.pathname === props.pathname;
|
||||||
|
const navigate = useRouterNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem disablePadding onClick={selected ? () => console.log("selected") : () => navigate(props.pathname)}>
|
||||||
|
<ListItemButton selected={selected}>
|
||||||
|
<ListItemIcon>{props.icon}</ListItemIcon>
|
||||||
|
<ListItemText primary={props.text} />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { Fragment, ReactNode, useCallback, useEffect, useState } from "react";
|
import React, { Fragment, ReactNode, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { Route, Routes, useLocation, useNavigate, useSearchParams } from "react-router-dom";
|
import { Route, Routes, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AuthenticatedRoute,
|
AuthenticatedRoute,
|
||||||
|
@ -14,6 +14,7 @@ import { useConfiguration } from "@hooks/Configuration";
|
||||||
import { useNotifications } from "@hooks/NotificationsContext";
|
import { useNotifications } from "@hooks/NotificationsContext";
|
||||||
import { useRedirectionURL } from "@hooks/RedirectionURL";
|
import { useRedirectionURL } from "@hooks/RedirectionURL";
|
||||||
import { useRedirector } from "@hooks/Redirector";
|
import { useRedirector } from "@hooks/Redirector";
|
||||||
|
import { useRouterNavigate } from "@hooks/RouterNavigate";
|
||||||
import { useAutheliaState } from "@hooks/State";
|
import { useAutheliaState } from "@hooks/State";
|
||||||
import { useUserInfoPOST } from "@hooks/UserInfo";
|
import { useUserInfoPOST } from "@hooks/UserInfo";
|
||||||
import { SecondFactorMethod } from "@models/Methods";
|
import { SecondFactorMethod } from "@models/Methods";
|
||||||
|
@ -36,7 +37,6 @@ const RedirectionErrorMessage =
|
||||||
"Redirection was determined to be unsafe and aborted. Ensure the redirection URL is correct.";
|
"Redirection was determined to be unsafe and aborted. Ensure the redirection URL is correct.";
|
||||||
|
|
||||||
const LoginPortal = function (props: Props) {
|
const LoginPortal = function (props: Props) {
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const redirectionURL = useRedirectionURL();
|
const redirectionURL = useRedirectionURL();
|
||||||
const { createErrorNotification } = useNotifications();
|
const { createErrorNotification } = useNotifications();
|
||||||
|
@ -47,24 +47,8 @@ const LoginPortal = function (props: Props) {
|
||||||
const [state, fetchState, , fetchStateError] = useAutheliaState();
|
const [state, fetchState, , fetchStateError] = useAutheliaState();
|
||||||
const [userInfo, fetchUserInfo, , fetchUserInfoError] = useUserInfoPOST();
|
const [userInfo, fetchUserInfo, , fetchUserInfoError] = useUserInfoPOST();
|
||||||
const [configuration, fetchConfiguration, , fetchConfigurationError] = useConfiguration();
|
const [configuration, fetchConfiguration, , fetchConfigurationError] = useConfiguration();
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
|
|
||||||
const redirect = useCallback(
|
const navigate = useRouterNavigate();
|
||||||
(
|
|
||||||
pathname: string,
|
|
||||||
preserveSearchParams: boolean = true,
|
|
||||||
searchParamsOverride: URLSearchParams | undefined = undefined,
|
|
||||||
) => {
|
|
||||||
if (searchParamsOverride && URLSearchParamsHasValues(searchParamsOverride)) {
|
|
||||||
navigate({ pathname: pathname, search: `?${searchParamsOverride.toString()}` });
|
|
||||||
} else if (preserveSearchParams && URLSearchParamsHasValues(searchParams)) {
|
|
||||||
navigate({ pathname: pathname, search: `?${searchParams.toString()}` });
|
|
||||||
} else {
|
|
||||||
navigate({ pathname: pathname });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[navigate, searchParams],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Fetch the state when portal is mounted.
|
// Fetch the state when portal is mounted.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -137,17 +121,17 @@ const LoginPortal = function (props: Props) {
|
||||||
|
|
||||||
if (state.authentication_level === AuthenticationLevel.Unauthenticated) {
|
if (state.authentication_level === AuthenticationLevel.Unauthenticated) {
|
||||||
setFirstFactorDisabled(false);
|
setFirstFactorDisabled(false);
|
||||||
redirect(IndexRoute);
|
navigate(IndexRoute);
|
||||||
} else if (state.authentication_level >= AuthenticationLevel.OneFactor && userInfo && configuration) {
|
} else if (state.authentication_level >= AuthenticationLevel.OneFactor && userInfo && configuration) {
|
||||||
if (configuration.available_methods.size === 0) {
|
if (configuration.available_methods.size === 0) {
|
||||||
redirect(AuthenticatedRoute, false);
|
navigate(AuthenticatedRoute, false);
|
||||||
} else {
|
} else {
|
||||||
if (userInfo.method === SecondFactorMethod.Webauthn) {
|
if (userInfo.method === SecondFactorMethod.Webauthn) {
|
||||||
redirect(`${SecondFactorRoute}${SecondFactorWebauthnSubRoute}`);
|
navigate(`${SecondFactorRoute}${SecondFactorWebauthnSubRoute}`);
|
||||||
} else if (userInfo.method === SecondFactorMethod.MobilePush) {
|
} else if (userInfo.method === SecondFactorMethod.MobilePush) {
|
||||||
redirect(`${SecondFactorRoute}${SecondFactorPushSubRoute}`);
|
navigate(`${SecondFactorRoute}${SecondFactorPushSubRoute}`);
|
||||||
} else {
|
} else {
|
||||||
redirect(`${SecondFactorRoute}${SecondFactorTOTPSubRoute}`);
|
navigate(`${SecondFactorRoute}${SecondFactorTOTPSubRoute}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,7 +139,7 @@ const LoginPortal = function (props: Props) {
|
||||||
}, [
|
}, [
|
||||||
state,
|
state,
|
||||||
redirectionURL,
|
redirectionURL,
|
||||||
redirect,
|
navigate,
|
||||||
userInfo,
|
userInfo,
|
||||||
setFirstFactorDisabled,
|
setFirstFactorDisabled,
|
||||||
configuration,
|
configuration,
|
||||||
|
@ -244,7 +228,3 @@ function ComponentOrLoading(props: ComponentOrLoadingProps) {
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function URLSearchParamsHasValues(params?: URLSearchParams) {
|
|
||||||
return params ? !params.entries().next().done : false;
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { Route, Routes } from "react-router-dom";
|
||||||
|
|
||||||
|
import { IndexRoute, SettingsTwoFactorAuthenticationSubRoute } from "@constants/Routes";
|
||||||
|
import SettingsView from "@views/Settings/SettingsView";
|
||||||
|
import TwoFactorAuthenticationView from "@views/Settings/TwoFactorAuthentication/TwoFactorAuthenticationView";
|
||||||
|
|
||||||
|
export interface Props {}
|
||||||
|
|
||||||
|
const SettingsRouter = function (props: Props) {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path={IndexRoute} element={<SettingsView />} />
|
||||||
|
<Route path={SettingsTwoFactorAuthenticationSubRoute} element={<TwoFactorAuthenticationView />} />
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsRouter;
|
|
@ -1,360 +1,17 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import { Box, Typography } from "@mui/material";
|
||||||
|
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import SettingsLayout from "@layouts/SettingsLayout";
|
||||||
import EditIcon from "@mui/icons-material/Edit";
|
|
||||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
|
||||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
|
||||||
import SystemSecurityUpdateGoodIcon from "@mui/icons-material/SystemSecurityUpdateGood";
|
|
||||||
import {
|
|
||||||
AppBar,
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Collapse,
|
|
||||||
Divider,
|
|
||||||
Drawer,
|
|
||||||
Grid,
|
|
||||||
IconButton,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
ListItemButton,
|
|
||||||
ListItemIcon,
|
|
||||||
ListItemText,
|
|
||||||
Paper,
|
|
||||||
Stack,
|
|
||||||
Switch,
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
Toolbar,
|
|
||||||
Tooltip,
|
|
||||||
Typography,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
import { WebauthnDevice } from "@root/models/Webauthn";
|
export interface Props {}
|
||||||
import { getWebauthnDevices } from "@root/services/UserWebauthnDevices";
|
|
||||||
|
|
||||||
import AddSecurityKeyDialog from "./AddSecurityDialog";
|
|
||||||
|
|
||||||
interface Props {}
|
|
||||||
|
|
||||||
const drawerWidth = 240;
|
|
||||||
|
|
||||||
export default function SettingsView(props: Props) {
|
|
||||||
const { t: translate } = useTranslation("settings");
|
|
||||||
|
|
||||||
const [webauthnDevices, setWebauthnDevices] = useState<WebauthnDevice[] | undefined>();
|
|
||||||
const [addKeyOpen, setAddKeyOpen] = useState<boolean>(false);
|
|
||||||
const [webauthnShowDetails, setWebauthnShowDetails] = useState<number>(-1);
|
|
||||||
|
|
||||||
const handleWebAuthnDetailsChange = (idx: number) => {
|
|
||||||
if (webauthnShowDetails === idx) {
|
|
||||||
setWebauthnShowDetails(-1);
|
|
||||||
} else {
|
|
||||||
setWebauthnShowDetails(idx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async function () {
|
|
||||||
const devices = await getWebauthnDevices();
|
|
||||||
setWebauthnDevices(devices);
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleKeyClose = () => {
|
|
||||||
setAddKeyOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddKeyButtonClick = () => {
|
|
||||||
setAddKeyOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const SettingsView = function (props: Props) {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: "flex" }}>
|
<SettingsLayout>
|
||||||
<AppBar position="fixed" sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}>
|
<Box>
|
||||||
<Toolbar variant="dense">
|
<Typography>Placeholder</Typography>
|
||||||
<Typography style={{ flexGrow: 1 }}>{translate("Settings")}</Typography>
|
|
||||||
</Toolbar>
|
|
||||||
</AppBar>
|
|
||||||
<Drawer
|
|
||||||
variant="permanent"
|
|
||||||
sx={{
|
|
||||||
width: drawerWidth,
|
|
||||||
flexShrink: 0,
|
|
||||||
[`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: "border-box" },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Toolbar variant="dense" />
|
|
||||||
<Box sx={{ overflow: "auto" }}>
|
|
||||||
<List>
|
|
||||||
<ListItem disablePadding>
|
|
||||||
<ListItemButton selected={true}>
|
|
||||||
<ListItemIcon>
|
|
||||||
<SystemSecurityUpdateGoodIcon />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText primary={translate("Security Keys")} />
|
|
||||||
</ListItemButton>
|
|
||||||
</ListItem>
|
|
||||||
</List>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Drawer>
|
</SettingsLayout>
|
||||||
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
|
|
||||||
<Toolbar variant="dense" />
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Typography>{translate("Manage your security keys")}</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Stack spacing={1} direction="row">
|
|
||||||
<Button color="primary" variant="contained" onClick={handleAddKeyButtonClick}>
|
|
||||||
{translate("Add")}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Paper>
|
|
||||||
<Table>
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell />
|
|
||||||
<TableCell>{translate("Name")}</TableCell>
|
|
||||||
<TableCell>{translate("Enabled")}</TableCell>
|
|
||||||
<TableCell align="center">{translate("Actions")}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{webauthnDevices
|
|
||||||
? webauthnDevices.map((x, idx) => {
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<TableRow
|
|
||||||
sx={{ "& > *": { borderBottom: "unset" } }}
|
|
||||||
key={x.kid.toString()}
|
|
||||||
>
|
|
||||||
<TableCell>
|
|
||||||
<Tooltip
|
|
||||||
title={translate("Show Details")}
|
|
||||||
placement="right"
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
aria-label="expand row"
|
|
||||||
size="small"
|
|
||||||
onClick={() => handleWebAuthnDetailsChange(idx)}
|
|
||||||
>
|
|
||||||
{webauthnShowDetails === idx ? (
|
|
||||||
<KeyboardArrowUpIcon />
|
|
||||||
) : (
|
|
||||||
<KeyboardArrowDownIcon />
|
|
||||||
)}
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell component="th" scope="row">
|
|
||||||
{x.description}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Switch defaultChecked={false} size="small" />
|
|
||||||
</TableCell>
|
|
||||||
<TableCell align="center">
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
spacing={1}
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
>
|
|
||||||
<Tooltip title={translate("Edit")} placement="bottom">
|
|
||||||
<IconButton aria-label="edit">
|
|
||||||
<EditIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip
|
|
||||||
title={translate("Delete")}
|
|
||||||
placement="bottom"
|
|
||||||
>
|
|
||||||
<IconButton aria-label="delete">
|
|
||||||
<DeleteIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</Stack>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell
|
|
||||||
style={{ paddingBottom: 0, paddingTop: 0 }}
|
|
||||||
colSpan={4}
|
|
||||||
>
|
|
||||||
<Collapse
|
|
||||||
in={webauthnShowDetails === idx}
|
|
||||||
timeout="auto"
|
|
||||||
unmountOnExit
|
|
||||||
>
|
|
||||||
<Grid container spacing={2} sx={{ mb: 3, margin: 1 }}>
|
|
||||||
<Grid
|
|
||||||
item
|
|
||||||
xs={12}
|
|
||||||
sm={12}
|
|
||||||
md={12}
|
|
||||||
lg={12}
|
|
||||||
xl={12}
|
|
||||||
>
|
|
||||||
<Box sx={{ margin: 1 }}>
|
|
||||||
<Typography
|
|
||||||
variant="h6"
|
|
||||||
gutterBottom
|
|
||||||
component="div"
|
|
||||||
>
|
|
||||||
{translate("Details")}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Grid>
|
|
||||||
<Grid
|
|
||||||
item
|
|
||||||
xs={12}
|
|
||||||
sm={12}
|
|
||||||
md={12}
|
|
||||||
lg={12}
|
|
||||||
xl={12}
|
|
||||||
>
|
|
||||||
<Divider variant="middle" />
|
|
||||||
</Grid>
|
|
||||||
<Grid
|
|
||||||
item
|
|
||||||
xs={12}
|
|
||||||
sm={12}
|
|
||||||
md={12}
|
|
||||||
lg={12}
|
|
||||||
xl={12}
|
|
||||||
>
|
|
||||||
<Typography>
|
|
||||||
{translate(
|
|
||||||
"Webauthn Credential Identifier",
|
|
||||||
{
|
|
||||||
id: x.kid.toString(),
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid
|
|
||||||
item
|
|
||||||
xs={12}
|
|
||||||
sm={12}
|
|
||||||
md={12}
|
|
||||||
lg={12}
|
|
||||||
xl={12}
|
|
||||||
>
|
|
||||||
<Typography>
|
|
||||||
Public Key: {x.public_key}
|
|
||||||
{translate("Webauthn Public Key", {
|
|
||||||
key: x.public_key.toString(),
|
|
||||||
})}
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid
|
|
||||||
item
|
|
||||||
xs={12}
|
|
||||||
sm={12}
|
|
||||||
md={12}
|
|
||||||
lg={12}
|
|
||||||
xl={12}
|
|
||||||
>
|
|
||||||
<Divider variant="middle" />
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={6} sm={6} md={4} lg={4} xl={3}>
|
|
||||||
<Typography>
|
|
||||||
{translate("Relying Party ID")}
|
|
||||||
</Typography>
|
|
||||||
<Typography>{x.rpid}</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={6} sm={6} md={4} lg={4} xl={3}>
|
|
||||||
<Typography>
|
|
||||||
{translate(
|
|
||||||
"Authenticator Attestation GUID",
|
|
||||||
)}
|
|
||||||
</Typography>
|
|
||||||
<Typography>{x.aaguid}</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={6} sm={6} md={4} lg={4} xl={3}>
|
|
||||||
<Typography>
|
|
||||||
{translate("Attestation Type")}
|
|
||||||
</Typography>
|
|
||||||
<Typography>{x.attestation_type}</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={6} sm={6} md={4} lg={4} xl={3}>
|
|
||||||
<Typography>
|
|
||||||
{translate("Transports")}
|
|
||||||
</Typography>
|
|
||||||
<Typography>
|
|
||||||
{x.transports.length === 0
|
|
||||||
? "N/A"
|
|
||||||
: x.transports.join(", ")}
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={6} sm={6} md={4} lg={4} xl={3}>
|
|
||||||
<Typography>
|
|
||||||
{translate("Clone Warning")}
|
|
||||||
</Typography>
|
|
||||||
<Typography>
|
|
||||||
{x.clone_warning
|
|
||||||
? translate("Yes")
|
|
||||||
: translate("No")}
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={6} sm={6} md={4} lg={4} xl={3}>
|
|
||||||
<Typography>
|
|
||||||
{translate("Created")}
|
|
||||||
</Typography>
|
|
||||||
<Typography>
|
|
||||||
{x.created_at.toString()}
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={6} sm={6} md={4} lg={4} xl={3}>
|
|
||||||
<Typography>
|
|
||||||
{translate("Last Used")}
|
|
||||||
</Typography>
|
|
||||||
<Typography>
|
|
||||||
{x.last_used_at === undefined
|
|
||||||
? translate("Never")
|
|
||||||
: x.last_used_at.toString()}
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={6} sm={6} md={4} lg={4} xl={3}>
|
|
||||||
<Typography>
|
|
||||||
{translate("Usage Count")}
|
|
||||||
</Typography>
|
|
||||||
<Typography>
|
|
||||||
{x.sign_count === 0
|
|
||||||
? translate("Never")
|
|
||||||
: x.sign_count}
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid
|
|
||||||
item
|
|
||||||
xs={12}
|
|
||||||
sm={12}
|
|
||||||
md={12}
|
|
||||||
lg={12}
|
|
||||||
xl={12}
|
|
||||||
>
|
|
||||||
<Divider variant="middle" />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Collapse>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
);
|
||||||
})
|
};
|
||||||
: null}
|
|
||||||
</TableBody>
|
export default SettingsView;
|
||||||
</Table>
|
|
||||||
</Paper>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
<AddSecurityKeyDialog open={addKeyOpen} onClose={handleKeyClose} />
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,261 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
|
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||||
|
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Collapse,
|
||||||
|
Divider,
|
||||||
|
Grid,
|
||||||
|
IconButton,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Switch,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import SettingsLayout from "@layouts/SettingsLayout";
|
||||||
|
import { WebauthnDevice } from "@root/models/Webauthn";
|
||||||
|
import { getWebauthnDevices } from "@root/services/UserWebauthnDevices";
|
||||||
|
|
||||||
|
import AddSecurityKeyDialog from "./AddSecurityDialog";
|
||||||
|
|
||||||
|
export interface Props {}
|
||||||
|
|
||||||
|
const TwoFactorAuthenticationView = function (props: Props) {
|
||||||
|
const { t: translate } = useTranslation("settings");
|
||||||
|
|
||||||
|
const [webauthnDevices, setWebauthnDevices] = useState<WebauthnDevice[] | undefined>();
|
||||||
|
const [addKeyOpen, setAddKeyOpen] = useState<boolean>(false);
|
||||||
|
const [webauthnShowDetails, setWebauthnShowDetails] = useState<number>(-1);
|
||||||
|
|
||||||
|
const handleWebAuthnDetailsChange = (idx: number) => {
|
||||||
|
if (webauthnShowDetails === idx) {
|
||||||
|
setWebauthnShowDetails(-1);
|
||||||
|
} else {
|
||||||
|
setWebauthnShowDetails(idx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async function () {
|
||||||
|
const devices = await getWebauthnDevices();
|
||||||
|
setWebauthnDevices(devices);
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleKeyClose = () => {
|
||||||
|
setAddKeyOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddKeyButtonClick = () => {
|
||||||
|
setAddKeyOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsLayout titlePrefix="Two Factor Authentication">
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Typography>{translate("Manage your security keys")}</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Stack spacing={1} direction="row">
|
||||||
|
<Button color="primary" variant="contained" onClick={handleAddKeyButtonClick}>
|
||||||
|
{translate("Add")}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Paper>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell />
|
||||||
|
<TableCell>{translate("Name")}</TableCell>
|
||||||
|
<TableCell>{translate("Enabled")}</TableCell>
|
||||||
|
<TableCell align="center">{translate("Actions")}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{webauthnDevices
|
||||||
|
? webauthnDevices.map((x, idx) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<TableRow
|
||||||
|
sx={{ "& > *": { borderBottom: "unset" } }}
|
||||||
|
key={x.kid.toString()}
|
||||||
|
>
|
||||||
|
<TableCell>
|
||||||
|
<Tooltip title={translate("Show Details")} placement="right">
|
||||||
|
<IconButton
|
||||||
|
aria-label="expand row"
|
||||||
|
size="small"
|
||||||
|
onClick={() => handleWebAuthnDetailsChange(idx)}
|
||||||
|
>
|
||||||
|
{webauthnShowDetails === idx ? (
|
||||||
|
<KeyboardArrowUpIcon />
|
||||||
|
) : (
|
||||||
|
<KeyboardArrowDownIcon />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell component="th" scope="row">
|
||||||
|
{x.description}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Switch defaultChecked={false} size="small" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={1}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
|
<Tooltip title={translate("Edit")} placement="bottom">
|
||||||
|
<IconButton aria-label="edit">
|
||||||
|
<EditIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title={translate("Delete")} placement="bottom">
|
||||||
|
<IconButton aria-label="delete">
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Stack>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
style={{ paddingBottom: 0, paddingTop: 0 }}
|
||||||
|
colSpan={4}
|
||||||
|
>
|
||||||
|
<Collapse
|
||||||
|
in={webauthnShowDetails === idx}
|
||||||
|
timeout="auto"
|
||||||
|
unmountOnExit
|
||||||
|
>
|
||||||
|
<Grid container spacing={2} sx={{ mb: 3, margin: 1 }}>
|
||||||
|
<Grid item xs={12} sm={12} md={12} lg={12} xl={12}>
|
||||||
|
<Box sx={{ margin: 1 }}>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
gutterBottom
|
||||||
|
component="div"
|
||||||
|
>
|
||||||
|
{translate("Details")}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={12} md={12} lg={12} xl={12}>
|
||||||
|
<Divider variant="middle" />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={12} md={12} lg={12} xl={12}>
|
||||||
|
<Typography>
|
||||||
|
{translate("Webauthn Credential Identifier", {
|
||||||
|
id: x.kid.toString(),
|
||||||
|
})}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={12} md={12} lg={12} xl={12}>
|
||||||
|
<Typography>
|
||||||
|
Public Key: {x.public_key}
|
||||||
|
{translate("Webauthn Public Key", {
|
||||||
|
key: x.public_key.toString(),
|
||||||
|
})}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={12} md={12} lg={12} xl={12}>
|
||||||
|
<Divider variant="middle" />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6} sm={6} md={4} lg={4} xl={3}>
|
||||||
|
<Typography>
|
||||||
|
{translate("Relying Party ID")}
|
||||||
|
</Typography>
|
||||||
|
<Typography>{x.rpid}</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6} sm={6} md={4} lg={4} xl={3}>
|
||||||
|
<Typography>
|
||||||
|
{translate("Authenticator Attestation GUID")}
|
||||||
|
</Typography>
|
||||||
|
<Typography>{x.aaguid}</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6} sm={6} md={4} lg={4} xl={3}>
|
||||||
|
<Typography>
|
||||||
|
{translate("Attestation Type")}
|
||||||
|
</Typography>
|
||||||
|
<Typography>{x.attestation_type}</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6} sm={6} md={4} lg={4} xl={3}>
|
||||||
|
<Typography>{translate("Transports")}</Typography>
|
||||||
|
<Typography>
|
||||||
|
{x.transports.length === 0
|
||||||
|
? "N/A"
|
||||||
|
: x.transports.join(", ")}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6} sm={6} md={4} lg={4} xl={3}>
|
||||||
|
<Typography>
|
||||||
|
{translate("Clone Warning")}
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
{x.clone_warning
|
||||||
|
? translate("Yes")
|
||||||
|
: translate("No")}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6} sm={6} md={4} lg={4} xl={3}>
|
||||||
|
<Typography>{translate("Created")}</Typography>
|
||||||
|
<Typography>{x.created_at.toString()}</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6} sm={6} md={4} lg={4} xl={3}>
|
||||||
|
<Typography>{translate("Last Used")}</Typography>
|
||||||
|
<Typography>
|
||||||
|
{x.last_used_at === undefined
|
||||||
|
? translate("Never")
|
||||||
|
: x.last_used_at.toString()}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6} sm={6} md={4} lg={4} xl={3}>
|
||||||
|
<Typography>
|
||||||
|
{translate("Usage Count")}
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
{x.sign_count === 0
|
||||||
|
? translate("Never")
|
||||||
|
: x.sign_count}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={12} md={12} lg={12} xl={12}>
|
||||||
|
<Divider variant="middle" />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Collapse>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: null}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<AddSecurityKeyDialog open={addKeyOpen} onClose={handleKeyClose} />
|
||||||
|
</SettingsLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TwoFactorAuthenticationView;
|
Loading…
Reference in New Issue