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 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={
|
||||
|
|
|
@ -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 RegisterOneTimePasswordRoute: string = "/one-time-password/register";
|
||||
export const LogoutRoute: string = "/logout";
|
||||
|
||||
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 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],
|
||||
},
|
||||
}));
|
||||
|
|
|
@ -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 {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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 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>
|
||||
<SettingsLayout>
|
||||
<Box>
|
||||
<Typography>Placeholder</Typography>
|
||||
</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>
|
||||
</SettingsLayout>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
<AddSecurityKeyDialog open={addKeyOpen} onClose={handleKeyClose} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default SettingsView;
|
||||
|
|
|
@ -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