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("One Time Password")}
+
+
+
+ {translate("One Time Password")}
{props.config === undefined || props.config === null ? (
-
+
@@ -48,7 +49,7 @@ export default function TOTPPanel(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";