feat: settings router (#4377)

pull/4384/head
James Elliott 2022-11-14 22:13:10 +11:00 committed by GitHub
parent dcd65515fc
commit 0f8de33f2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 482 additions and 402 deletions

View File

@ -36,7 +36,7 @@ import LoginPortal from "@views/LoginPortal/LoginPortal";
import SignOut from "@views/LoginPortal/SignOut/SignOut";
import ResetPasswordStep1 from "@views/ResetPassword/ResetPasswordStep1";
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";
@ -95,7 +95,7 @@ const App: React.FC<Props> = (props: Props) => {
<Route path={RegisterOneTimePasswordRoute} element={<RegisterOneTimePassword />} />
<Route path={LogoutRoute} element={<SignOut />} />
<Route path={ConsentRoute} element={<ConsentView />} />
<Route path={SettingsRoute} element={<SettingsView />} />
<Route path={`${SettingsRoute}/*`} element={<SettingsRouter />} />
<Route
path={`${IndexRoute}*`}
element={

View File

@ -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],
},
}));

View File

@ -12,4 +12,6 @@ export const ResetPasswordStep2Route: string = "/reset-password/step2";
export const RegisterWebauthnRoute: string = "/webauthn/register";
export const RegisterOneTimePasswordRoute: string = "/one-time-password/register";
export const LogoutRoute: string = "/logout";
export const SettingsRoute: string = "/settings";
export const SettingsTwoFactorAuthenticationSubRoute: string = "/two-factor-authentication";

View File

@ -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;
}

View File

@ -1,13 +1,13 @@
import React, { ReactNode, useEffect } from "react";
import SettingsIcon from "@mui/icons-material/Settings";
import { AppBar, Box, Container, Grid, IconButton, Link, Theme, Toolbar, Typography } from "@mui/material";
import { grey } from "@mui/material/colors";
import { AppBar, Box, Container, Grid, IconButton, Theme, Toolbar, Typography } from "@mui/material";
import makeStyles from "@mui/styles/makeStyles";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { ReactComponent as UserSvg } from "@assets/images/user.svg";
import Brand from "@components/Brand";
import TypographyWithTooltip from "@components/TypographyWithTootip";
import { SettingsRoute } from "@root/constants/Routes";
import { getLogoOverride } from "@utils/Configuration";
@ -23,8 +23,6 @@ export interface Props {
showSettings?: boolean;
}
const url = "https://www.authelia.com";
const LoginLayout = function (props: Props) {
const navigate = useNavigate();
const styles = useStyles();
@ -64,9 +62,9 @@ const LoginLayout = function (props: Props) {
</Toolbar>
</AppBar>
<Grid
container
id={props.id}
className={styles.root}
container
spacing={0}
alignItems="center"
justifyContent="center"
@ -97,13 +95,7 @@ const LoginLayout = function (props: Props) {
<Grid item xs={12} className={styles.body}>
{props.children}
</Grid>
{props.showBrand ? (
<Grid item xs={12}>
<Link href={url} target="_blank" underline="hover" className={styles.poweredBy}>
{translate("Powered by")} Authelia
</Link>
</Grid>
) : null}
{props.showBrand ? <Brand /> : null}
</Grid>
</Container>
</Grid>
@ -134,8 +126,4 @@ const useStyles = makeStyles((theme: Theme) => ({
paddingTop: theme.spacing(),
paddingBottom: theme.spacing(),
},
poweredBy: {
fontSize: "0.7em",
color: grey[500],
},
}));

View File

@ -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>
);
};

View File

@ -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 {
AuthenticatedRoute,
@ -14,6 +14,7 @@ import { useConfiguration } from "@hooks/Configuration";
import { useNotifications } from "@hooks/NotificationsContext";
import { useRedirectionURL } from "@hooks/RedirectionURL";
import { useRedirector } from "@hooks/Redirector";
import { useRouterNavigate } from "@hooks/RouterNavigate";
import { useAutheliaState } from "@hooks/State";
import { useUserInfoPOST } from "@hooks/UserInfo";
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.";
const LoginPortal = function (props: Props) {
const navigate = useNavigate();
const location = useLocation();
const redirectionURL = useRedirectionURL();
const { createErrorNotification } = useNotifications();
@ -47,24 +47,8 @@ const LoginPortal = function (props: Props) {
const [state, fetchState, , fetchStateError] = useAutheliaState();
const [userInfo, fetchUserInfo, , fetchUserInfoError] = useUserInfoPOST();
const [configuration, fetchConfiguration, , fetchConfigurationError] = useConfiguration();
const [searchParams] = useSearchParams();
const redirect = 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],
);
const navigate = useRouterNavigate();
// Fetch the state when portal is mounted.
useEffect(() => {
@ -137,17 +121,17 @@ const LoginPortal = function (props: Props) {
if (state.authentication_level === AuthenticationLevel.Unauthenticated) {
setFirstFactorDisabled(false);
redirect(IndexRoute);
navigate(IndexRoute);
} else if (state.authentication_level >= AuthenticationLevel.OneFactor && userInfo && configuration) {
if (configuration.available_methods.size === 0) {
redirect(AuthenticatedRoute, false);
navigate(AuthenticatedRoute, false);
} else {
if (userInfo.method === SecondFactorMethod.Webauthn) {
redirect(`${SecondFactorRoute}${SecondFactorWebauthnSubRoute}`);
navigate(`${SecondFactorRoute}${SecondFactorWebauthnSubRoute}`);
} else if (userInfo.method === SecondFactorMethod.MobilePush) {
redirect(`${SecondFactorRoute}${SecondFactorPushSubRoute}`);
navigate(`${SecondFactorRoute}${SecondFactorPushSubRoute}`);
} else {
redirect(`${SecondFactorRoute}${SecondFactorTOTPSubRoute}`);
navigate(`${SecondFactorRoute}${SecondFactorTOTPSubRoute}`);
}
}
}
@ -155,7 +139,7 @@ const LoginPortal = function (props: Props) {
}, [
state,
redirectionURL,
redirect,
navigate,
userInfo,
setFirstFactorDisabled,
configuration,
@ -244,7 +228,3 @@ function ComponentOrLoading(props: ComponentOrLoadingProps) {
</Fragment>
);
}
function URLSearchParamsHasValues(params?: URLSearchParams) {
return params ? !params.entries().next().done : false;
}

View File

@ -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;

View File

@ -1,360 +1,17 @@
import React, { useEffect, useState } from "react";
import { Box, Typography } from "@mui/material";
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 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 SettingsLayout from "@layouts/SettingsLayout";
import { WebauthnDevice } from "@root/models/Webauthn";
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);
};
export interface Props {}
const SettingsView = function (props: Props) {
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>
<ListItem disablePadding>
<ListItemButton selected={true}>
<ListItemIcon>
<SystemSecurityUpdateGoodIcon />
</ListItemIcon>
<ListItemText primary={translate("Security Keys")} />
</ListItemButton>
</ListItem>
</List>
</Box>
</Drawer>
<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>
</Table>
</Paper>
</Grid>
</Grid>
<SettingsLayout>
<Box>
<Typography>Placeholder</Typography>
</Box>
<AddSecurityKeyDialog open={addKeyOpen} onClose={handleKeyClose} />
</Box>
</SettingsLayout>
);
}
};
export default SettingsView;

View File

@ -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;