diff --git a/internal/server/locales/en/settings.json b/internal/server/locales/en/settings.json index c36238c39..ff550c74b 100644 --- a/internal/server/locales/en/settings.json +++ b/internal/server/locales/en/settings.json @@ -2,7 +2,8 @@ "Actions": "Actions", "Add": "Add", "Add Credential": "Add Credential", - "Added": "Added {{when, datetime}}", + "Added": "Added", + "Added when": "Added {{when, datetime}}", "Are you sure you want to remove the WebAuthn credential from from your account": "Are you sure you want to remove the WebAuthn credential {{description}} from your account?", "Attestation Type": "Attestation Type", "Authenticator GUID": "Authenticator GUID", @@ -22,11 +23,12 @@ "Enter a description for this credential": "Enter a description for this credential", "Extended WebAuthn credential information for security key": "Extended WebAuthn credential information for security key {{description}}", "Identifier": "Identifier", - "Last Used": "Last Used {{when, datetime}}", + "Last Used": "Last Used", + "Last Used when": "Last Used {{when, datetime}}", "Manage your security keys": "Manage your security keys", "Name": "Name", "No": "No", - "No Registered WebAuthn Credentials": "No Registered WebAuthn Credentials", + "No WebAuthn Credentials have been registered. If you'd like to register one click add.": "No WebAuthn Credentials have been registered. If you'd like to register one click add.", "Overview": "Overview", "Provide the details for the new security key": "Provide the details for the new security key", "Register WebAuthn Credential": "Register WebAuthn Credential", diff --git a/web/src/hooks/WebauthnDevices.ts b/web/src/hooks/WebauthnDevices.ts deleted file mode 100644 index f9bdbd2a5..000000000 --- a/web/src/hooks/WebauthnDevices.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { useRemoteCall } from "@hooks/RemoteCall"; -import { getUserWebauthnDevices } from "@services/UserWebauthnDevices"; - -export function useUserWebauthnDevices() { - return useRemoteCall(getUserWebauthnDevices, []); -} diff --git a/web/src/layouts/SettingsLayout.tsx b/web/src/layouts/SettingsLayout.tsx index c5d91597d..b2d7ed6cf 100644 --- a/web/src/layouts/SettingsLayout.tsx +++ b/web/src/layouts/SettingsLayout.tsx @@ -1,21 +1,22 @@ -import React, { ReactNode, useEffect } from "react"; +import React, { ReactNode, SyntheticEvent, useCallback, useEffect, useState } from "react"; -import { Dashboard } from "@mui/icons-material"; +import { Close, Dashboard } from "@mui/icons-material"; +import MenuIcon from "@mui/icons-material/Menu"; import SystemSecurityUpdateGoodIcon from "@mui/icons-material/SystemSecurityUpdateGood"; import { AppBar, Box, - Button, - Drawer, - Grid, + Divider, List, ListItem, ListItemButton, ListItemIcon, ListItemText, + SwipeableDrawer, Toolbar, Typography, } from "@mui/material"; +import IconButton from "@mui/material/IconButton"; import { useTranslation } from "react-i18next"; import { IndexRoute, SettingsRoute, SettingsTwoFactorAuthenticationSubRoute } from "@constants/Routes"; @@ -33,8 +34,7 @@ const defaultDrawerWidth = 240; const SettingsLayout = function (props: Props) { const { t: translate } = useTranslation("settings"); - - const navigate = useRouterNavigate(); + const [drawerOpen, setDrawerOpen] = useState(false); useEffect(() => { if (props.title) { @@ -54,72 +54,179 @@ const SettingsLayout = function (props: Props) { const drawerWidth = props.drawerWidth === undefined ? defaultDrawerWidth : props.drawerWidth; + const handleToggleDrawer = (event: SyntheticEvent) => { + if ( + event.nativeEvent instanceof KeyboardEvent && + event.nativeEvent.type === "keydown" && + (event.nativeEvent.key === "Tab" || event.nativeEvent.key === "Shift") + ) { + return; + } + + setDrawerOpen((state) => !state); + }; + + const container = window !== undefined ? () => window.document.body : undefined; + + const drawer = ( + + + {translate("Settings")} + + + + {navItems.map((item) => ( + + ))} + + + ); + return ( - theme.zIndex.drawer + 1 }}> - - Authelia {translate("Settings")} - + + + + {translate("Settings")} + - - - - - } /> - } - /> - - - - - - - - {props.children} - - - + + + {drawer} + + + + + {props.children} + ); }; export default SettingsLayout; -interface SettingsMenuItemProps { - pathname: string; +interface NavItem { + keyname?: string; text: string; - icon: ReactNode; + pathname: string; + icon?: ReactNode; } -const SettingsMenuItem = function (props: SettingsMenuItemProps) { +const navItems: NavItem[] = [ + { keyname: "overview", text: "Overview", pathname: SettingsRoute, icon: }, + { + keyname: "twofactor", + text: "Two-Factor Authentication", + pathname: `${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`, + icon: , + }, + { keyname: "close", text: "Close", pathname: IndexRoute, icon: }, +]; + +const DrawerNavItem = function (props: NavItem) { const selected = window.location.pathname === props.pathname || window.location.pathname === props.pathname + "/"; const navigate = useRouterNavigate(); + const handleOnClick = useCallback(() => { + if (selected) { + return; + } + + navigate(props.pathname); + }, [navigate, props, selected]); + return ( - navigate(props.pathname) : undefined}> + - {props.icon} + {props.icon ? {props.icon} : null} ); }; + +/* +interface SettingsMenuProps { + isXL: boolean; + handleClickMenuItem: () => void; + handleToggleDrawer: (open: boolean) => (event: SyntheticEvent) => void; +} + +const SettingsMenu = function (props: SettingsMenuProps) { + const { t: translate } = useTranslation("settings"); + const navigate = useRouterNavigate(); + + return ( + + {props.isXL ? null : ( + + + + + + + )} + + } + onClick={props.handleClickMenuItem} + /> + } + onClick={props.handleClickMenuItem} + /> + { + navigate(IndexRoute); + }} + > + + + + + + + + + + ); +}; + + */ diff --git a/web/src/views/Settings/TwoFactorAuthentication/TOTPDevice.tsx b/web/src/views/Settings/TwoFactorAuthentication/TOTPDevice.tsx index 00e28516f..8743373c1 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/TOTPDevice.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/TOTPDevice.tsx @@ -11,7 +11,6 @@ import { deleteUserTOTPConfiguration } from "@services/UserInfoTOTPConfiguration import DeleteDialog from "@views/Settings/TwoFactorAuthentication/DeleteDialog"; interface Props { - index: number; config: UserInfoTOTPConfiguration; handleRefresh: () => void; } @@ -69,14 +68,14 @@ export default function TOTPDevice(props: Props) { "Are you sure you want to remove the Time-based One Time Password from from your account", )} /> - + - + {props.config.issuer} - + {" (" + translate("{{algorithm}}, {{digits}} digits, {{seconds}} seconds", { algorithm: toAlgorithmString(props.config.algorithm), @@ -87,7 +86,7 @@ export default function TOTPDevice(props: Props) { - {translate("Added", { + {translate("Added when", { when: props.config.created_at, formatParams: { when: { @@ -103,7 +102,7 @@ export default function TOTPDevice(props: Props) { {props.config.last_used_at === undefined ? translate("Never used") - : translate("Last Used", { + : translate("Last Used when", { when: props.config.last_used_at, formatParams: { when: { @@ -117,7 +116,6 @@ export default function TOTPDevice(props: Props) { })} - - + {translate( "The One Time Password has not been registered. If you'd like to register it click add.", @@ -57,9 +58,9 @@ export default function TOTPPanel(props: Props) { ) : ( - - - + + + )} diff --git a/web/src/views/Settings/TwoFactorAuthentication/TOTPRegisterDialogController.tsx b/web/src/views/Settings/TwoFactorAuthentication/TOTPRegisterDialogController.tsx index b6fdb21fa..4244676a0 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/TOTPRegisterDialogController.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/TOTPRegisterDialogController.tsx @@ -2,6 +2,7 @@ import React, { Fragment, useCallback, useEffect, useState } from "react"; import { IconDefinition, faCopy, faKey, faTimesCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Visibility } from "@mui/icons-material"; import { Box, Button, @@ -9,11 +10,11 @@ import { Dialog, DialogActions, DialogContent, + DialogContentText, DialogTitle, FormControl, FormControlLabel, FormLabel, - Grid, IconButton, Link, Radio, @@ -26,6 +27,7 @@ import { Typography, } from "@mui/material"; import { red } from "@mui/material/colors"; +import Grid from "@mui/material/Unstable_Grid2"; import makeStyles from "@mui/styles/makeStyles"; import classnames from "classnames"; import { QRCodeSVG } from "qrcode.react"; @@ -69,6 +71,7 @@ export default function TOTPRegisterDialogController(props: Props) { const [hasErrored, setHasErrored] = useState(false); const [dialValue, setDialValue] = useState(""); const [dialState, setDialState] = useState(State.Idle); + const [totpSecretURLHidden, setTOTPSecretURLHidden] = useState(true); const resetStates = () => { setOptions(null); @@ -85,6 +88,7 @@ export default function TOTPRegisterDialogController(props: Props) { setActiveStep(0); setDialValue(""); setDialState(State.Idle); + setTOTPSecretURLHidden(true); }; const handleClose = useCallback(() => { @@ -112,7 +116,7 @@ export default function TOTPRegisterDialogController(props: Props) { }; useEffect(() => { - if (!props.open || activeStep !== 0) { + if (!props.open || activeStep !== 0 || options !== null) { return; } @@ -126,13 +130,14 @@ export default function TOTPRegisterDialogController(props: Props) { setOptionPeriod(opts.period); setOptionPeriods(opts.periods.map((period) => period.toString())); })(); - }, [props.open, activeStep]); + }, [props.open, activeStep, options]); const handleSetStepPrevious = useCallback(() => { if (activeStep === 0) { return; } + setShowAdvanced(false); setActiveStep((prevState) => (prevState -= 1)); }, [activeStep]); @@ -141,6 +146,7 @@ export default function TOTPRegisterDialogController(props: Props) { return; } + setShowAdvanced(false); setActiveStep((prevState) => (prevState += 1)); }, [activeStep]); @@ -213,7 +219,6 @@ export default function TOTPRegisterDialogController(props: Props) { function SecretButton(text: string | undefined, action: string, icon: IconDefinition) { return ( { navigator.clipboard.writeText(`${text}`); @@ -232,21 +237,20 @@ export default function TOTPRegisterDialogController(props: Props) { return ( {options === null ? ( - + Loading... ) : ( - - + + {translate("To begin select next")} - + )} ); case 1: return ( - - + + {translate("Need Google Authenticator?")} @@ -351,10 +355,10 @@ export default function TOTPRegisterDialogController(props: Props) { /> - + - + {!hasErrored && totpIsLoading ? ( ) : null} @@ -364,22 +368,20 @@ export default function TOTPRegisterDialogController(props: Props) { - + - - {totpSecretURL !== "empty" ? ( - - ) : null} + + { + setTOTPSecretURLHidden((value) => !value); + }} + size="large" + > + + - + {totpSecretBase32 ? SecretButton( totpSecretBase32, @@ -388,18 +390,31 @@ export default function TOTPRegisterDialogController(props: Props) { ) : null} - - {totpSecretURL !== "empty" + + {totpSecretURL !== "" ? SecretButton(totpSecretURL, translate("OTP URL copied to clipboard"), faCopy) : null} + + ); case 2: return ( - + {translate("Register One Time Password (TOTP)")} + + {translate("This dialog allows registration of the One-Time Password.")} + - + {steps.map((label, index) => { const stepProps: { completed?: boolean } = {}; @@ -432,7 +450,7 @@ export default function TOTPRegisterDialogController(props: Props) { })} - + {renderStep(activeStep)} @@ -440,23 +458,13 @@ export default function TOTPRegisterDialogController(props: Props) { - - - @@ -465,10 +473,6 @@ export default function TOTPRegisterDialogController(props: Props) { } const useStyles = makeStyles((theme: Theme) => ({ - root: { - paddingTop: theme.spacing(4), - paddingBottom: theme.spacing(4), - }, qrcode: { marginTop: theme.spacing(2), marginBottom: theme.spacing(2), @@ -483,15 +487,10 @@ const useStyles = makeStyles((theme: Theme) => ({ marginBottom: theme.spacing(1), width: "256px", }, - googleAuthenticator: {}, googleAuthenticatorText: { fontSize: theme.typography.fontSize * 0.8, }, googleAuthenticatorBadges: {}, - secretButtons: {}, - doneButton: { - width: "256px", - }, qrcodeContainer: { position: "relative", display: "inline-block", diff --git a/web/src/views/Settings/TwoFactorAuthentication/TwoFactorAuthenticationView.tsx b/web/src/views/Settings/TwoFactorAuthentication/TwoFactorAuthenticationView.tsx index 839dcc51c..16ef3f4b2 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/TwoFactorAuthenticationView.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/TwoFactorAuthenticationView.tsx @@ -4,7 +4,7 @@ import Grid from "@mui/material/Unstable_Grid2"; import { useNotifications } from "@hooks/NotificationsContext"; import { useUserInfoPOST } from "@hooks/UserInfo"; -import { useUserInfoTOTPConfiguration, useUserInfoTOTPConfigurationOptional } from "@hooks/UserInfoTOTPConfiguration"; +import { useUserInfoTOTPConfigurationOptional } from "@hooks/UserInfoTOTPConfiguration"; import { useUserWebAuthnDevices } from "@hooks/WebAuthnDevices"; import TOTPPanel from "@views/Settings/TwoFactorAuthentication/TOTPPanel"; import WebAuthnDevicesPanel from "@views/Settings/TwoFactorAuthentication/WebAuthnDevicesPanel";