diff options
Diffstat (limited to 'web/src')
| -rw-r--r-- | web/src/constants/Routes.ts | 1 | ||||
| -rw-r--r-- | web/src/hooks/CapsLock.ts | 16 | ||||
| -rw-r--r-- | web/src/layouts/SettingsLayout.tsx | 17 | ||||
| -rw-r--r-- | web/src/models/Configuration.ts | 6 | ||||
| -rw-r--r-- | web/src/models/UserInfo.ts | 1 | ||||
| -rw-r--r-- | web/src/services/Api.ts | 2 | ||||
| -rw-r--r-- | web/src/services/ChangePassword.ts | 17 | ||||
| -rw-r--r-- | web/src/services/Configuration.ts | 9 | ||||
| -rw-r--r-- | web/src/services/UserInfo.ts | 1 | ||||
| -rw-r--r-- | web/src/views/Settings/Common/IdentityVerificationDialog.tsx | 17 | ||||
| -rw-r--r-- | web/src/views/Settings/Security/ChangePasswordDialog.tsx | 306 | ||||
| -rw-r--r-- | web/src/views/Settings/Security/SecurityView.tsx | 244 | ||||
| -rw-r--r-- | web/src/views/Settings/SettingsRouter.tsx | 4 |
13 files changed, 636 insertions, 5 deletions
diff --git a/web/src/constants/Routes.ts b/web/src/constants/Routes.ts index c63fead7b..8897661e3 100644 --- a/web/src/constants/Routes.ts +++ b/web/src/constants/Routes.ts @@ -15,6 +15,7 @@ export const SettingsRoute: string = "/settings"; export const SettingsTwoFactorAuthenticationSubRoute: string = "/two-factor-authentication"; export const RevokeOneTimeCodeRoute: string = "/revoke/one-time-code"; export const RevokeResetPasswordRoute: string = "/revoke/reset-password"; +export const SecuritySubRoute: string = "/security"; export const ConsentRoute: string = "/consent"; export const ConsentOpenIDSubRoute: string = "/openid"; diff --git a/web/src/hooks/CapsLock.ts b/web/src/hooks/CapsLock.ts new file mode 100644 index 000000000..724d3c3c9 --- /dev/null +++ b/web/src/hooks/CapsLock.ts @@ -0,0 +1,16 @@ +import React, { useCallback } from "react"; + +export const useCheckCapsLock = (setCapsLockNotify: React.Dispatch<React.SetStateAction<boolean>>) => { + return useCallback( + (event: React.KeyboardEvent<HTMLDivElement>) => { + if (event.getModifierState("CapsLock")) { + setCapsLockNotify(true); + } else { + setCapsLockNotify(false); + } + }, + [setCapsLockNotify], + ); +}; + +export default useCheckCapsLock; diff --git a/web/src/layouts/SettingsLayout.tsx b/web/src/layouts/SettingsLayout.tsx index 5f540954b..7e00ce9fa 100644 --- a/web/src/layouts/SettingsLayout.tsx +++ b/web/src/layouts/SettingsLayout.tsx @@ -1,6 +1,6 @@ import React, { ReactNode, SyntheticEvent, useCallback, useEffect, useState } from "react"; -import { Close, Dashboard, Menu, SystemSecurityUpdateGood } from "@mui/icons-material"; +import { Close, Dashboard, Menu, Security, SystemSecurityUpdateGood } from "@mui/icons-material"; import { AppBar, Box, @@ -17,7 +17,12 @@ import { import IconButton from "@mui/material/IconButton"; import { useTranslation } from "react-i18next"; -import { IndexRoute, SettingsRoute, SettingsTwoFactorAuthenticationSubRoute } from "@constants/Routes"; +import { + IndexRoute, + SecuritySubRoute, + SettingsRoute, + SettingsTwoFactorAuthenticationSubRoute, +} from "@constants/Routes"; import { useRouterNavigate } from "@hooks/RouterNavigate"; export interface Props { @@ -127,7 +132,7 @@ const SettingsLayout = function (props: Props) { {drawer} </SwipeableDrawer> </Box> - <Box component="main" sx={{ flexGrow: 1, p: 3 }}> + <Box component="main" sx={{ flexGrow: 1, p: { xs: 0, sm: 3 } }}> <Toolbar /> {props.children} </Box> @@ -145,6 +150,12 @@ interface NavItem { const navItems: NavItem[] = [ { keyname: "overview", text: "Overview", pathname: SettingsRoute, icon: <Dashboard color={"primary"} /> }, { + keyname: "security", + text: "Security", + pathname: `${SettingsRoute}${SecuritySubRoute}`, + icon: <Security color={"primary"} />, + }, + { keyname: "twofactor", text: "Two-Factor Authentication", pathname: `${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`, diff --git a/web/src/models/Configuration.ts b/web/src/models/Configuration.ts index 45551b301..f133c3399 100644 --- a/web/src/models/Configuration.ts +++ b/web/src/models/Configuration.ts @@ -2,4 +2,10 @@ import { SecondFactorMethod } from "@models/Methods"; export interface Configuration { available_methods: Set<SecondFactorMethod>; + password_change_disabled: boolean; + password_reset_disabled: boolean; +} + +export interface SecuritySettingsConfiguration { + disable: boolean; } diff --git a/web/src/models/UserInfo.ts b/web/src/models/UserInfo.ts index eb420b7b6..5c7c155ca 100644 --- a/web/src/models/UserInfo.ts +++ b/web/src/models/UserInfo.ts @@ -2,6 +2,7 @@ import { SecondFactorMethod } from "@models/Methods"; export interface UserInfo { display_name: string; + emails: string[]; method: SecondFactorMethod; has_webauthn: boolean; has_totp: boolean; diff --git a/web/src/services/Api.ts b/web/src/services/Api.ts index a6f818013..e88e4d204 100644 --- a/web/src/services/Api.ts +++ b/web/src/services/Api.ts @@ -29,6 +29,8 @@ export const CompletePasswordSignInPath = basePath + "/api/secondfactor/password export const InitiateResetPasswordPath = basePath + "/api/reset-password/identity/start"; export const CompleteResetPasswordPath = basePath + "/api/reset-password/identity/finish"; +export const ChangePasswordPath = basePath + "/api/change-password"; + // Do the password reset during completion. export const ResetPasswordPath = basePath + "/api/reset-password"; export const ChecksSafeRedirectionPath = basePath + "/api/checks/safe-redirection"; diff --git a/web/src/services/ChangePassword.ts b/web/src/services/ChangePassword.ts new file mode 100644 index 000000000..9d89703a0 --- /dev/null +++ b/web/src/services/ChangePassword.ts @@ -0,0 +1,17 @@ +import { ChangePasswordPath } from "@services/Api"; +import { PostWithOptionalResponse } from "@services/Client"; + +interface PostPasswordChange { + username: string; + old_password: string; + new_password: string; +} + +export async function postPasswordChange(username: string, old_password: string, new_password: string) { + const data: PostPasswordChange = { + username, + old_password, + new_password, + }; + return PostWithOptionalResponse(ChangePasswordPath, data); +} diff --git a/web/src/services/Configuration.ts b/web/src/services/Configuration.ts index 9c494f182..e69d47731 100644 --- a/web/src/services/Configuration.ts +++ b/web/src/services/Configuration.ts @@ -5,9 +5,16 @@ import { Method2FA, toSecondFactorMethod } from "@services/UserInfo"; interface ConfigurationPayload { available_methods: Method2FA[]; + password_change_disabled: boolean; + password_reset_disabled: boolean; } export async function getConfiguration(): Promise<Configuration> { const config = await Get<ConfigurationPayload>(ConfigurationPath); - return { ...config, available_methods: new Set(config.available_methods.map(toSecondFactorMethod)) }; + return { + ...config, + available_methods: new Set(config.available_methods.map(toSecondFactorMethod)), + password_change_disabled: config.password_change_disabled, + password_reset_disabled: config.password_reset_disabled, + }; } diff --git a/web/src/services/UserInfo.ts b/web/src/services/UserInfo.ts index f7fbb5a6c..db5302624 100644 --- a/web/src/services/UserInfo.ts +++ b/web/src/services/UserInfo.ts @@ -7,6 +7,7 @@ export type Method2FA = "webauthn" | "totp" | "mobile_push"; export interface UserInfoPayload { display_name: string; + emails: string[]; method: Method2FA; has_webauthn: boolean; has_totp: boolean; diff --git a/web/src/views/Settings/Common/IdentityVerificationDialog.tsx b/web/src/views/Settings/Common/IdentityVerificationDialog.tsx index b9b12f942..7eaa4f129 100644 --- a/web/src/views/Settings/Common/IdentityVerificationDialog.tsx +++ b/web/src/views/Settings/Common/IdentityVerificationDialog.tsx @@ -131,6 +131,22 @@ const IdentityVerificationDialog = function (props: Props) { } }, [codeInput, handleFailure, handleSuccess]); + const handleSubmitKeyDown = useCallback( + (event: React.KeyboardEvent<HTMLDivElement>) => { + if (event.key === "Enter") { + if (!codeInput.length) { + setCodeError(true); + } else if (codeInput.length) { + handleSubmit(); + } else { + setCodeError(false); + codeRef.current?.focus(); + } + } + }, + [codeInput.length, handleSubmit], + ); + useEffect(() => { if (closing || !props.opening || !props.elevation) { return; @@ -190,6 +206,7 @@ const IdentityVerificationDialog = function (props: Props) { error={codeError} disabled={loading} inputRef={codeRef} + onKeyDown={handleSubmitKeyDown} /> </Box> </DialogContent> diff --git a/web/src/views/Settings/Security/ChangePasswordDialog.tsx b/web/src/views/Settings/Security/ChangePasswordDialog.tsx new file mode 100644 index 000000000..4bed82e67 --- /dev/null +++ b/web/src/views/Settings/Security/ChangePasswordDialog.tsx @@ -0,0 +1,306 @@ +import React, { MutableRefObject, useCallback, useEffect, useRef, useState } from "react"; + +import { + Button, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControl, + Grid2, + TextField, +} from "@mui/material"; +import axios from "axios"; +import { useTranslation } from "react-i18next"; + +import PasswordMeter from "@components/PasswordMeter"; +import useCheckCapsLock from "@hooks/CapsLock"; +import { useNotifications } from "@hooks/NotificationsContext"; +import { PasswordPolicyConfiguration, PasswordPolicyMode } from "@models/PasswordPolicy"; +import { postPasswordChange } from "@services/ChangePassword"; +import { getPasswordPolicyConfiguration } from "@services/PasswordPolicyConfiguration"; + +interface Props { + username: string; + disabled?: boolean; + open: boolean; + setClosed: () => void; +} + +const ChangePasswordDialog = (props: Props) => { + const { t: translate } = useTranslation("settings"); + + const { createSuccessNotification, createErrorNotification } = useNotifications(); + + const [loading, setLoading] = useState(false); + const [oldPassword, setOldPassword] = useState(""); + const [oldPasswordError, setOldPasswordError] = useState(false); + const [newPassword, setNewPassword] = useState(""); + const [newPasswordError, setNewPasswordError] = useState(false); + const [repeatNewPassword, setRepeatNewPassword] = useState(""); + const [repeatNewPasswordError, setRepeatNewPasswordError] = useState(false); + const [isCapsLockOnOldPW, setIsCapsLockOnOldPW] = useState(false); + const [isCapsLockOnNewPW, setIsCapsLockOnNewPW] = useState(false); + const [isCapsLockOnRepeatNewPW, setIsCapsLockOnRepeatNewPW] = useState(false); + + const oldPasswordRef = useRef() as MutableRefObject<HTMLInputElement>; + const newPasswordRef = useRef() as MutableRefObject<HTMLInputElement>; + const repeatNewPasswordRef = useRef() as MutableRefObject<HTMLInputElement>; + + const [pPolicy, setPPolicy] = useState<PasswordPolicyConfiguration>({ + max_length: 0, + min_length: 8, + min_score: 0, + require_lowercase: false, + require_number: false, + require_special: false, + require_uppercase: false, + mode: PasswordPolicyMode.Disabled, + }); + + const resetPasswordErrors = useCallback(() => { + setOldPasswordError(false); + setNewPasswordError(false); + setRepeatNewPasswordError(false); + }, []); + + const resetCapsLockErrors = useCallback(() => { + setIsCapsLockOnOldPW(false); + setIsCapsLockOnNewPW(false); + setIsCapsLockOnRepeatNewPW(false); + }, []); + + const resetStates = useCallback(() => { + setOldPassword(""); + setNewPassword(""); + setRepeatNewPassword(""); + + resetPasswordErrors(); + resetCapsLockErrors(); + + setLoading(false); + }, [resetPasswordErrors, resetCapsLockErrors]); + + const handleClose = useCallback(() => { + props.setClosed(); + resetStates(); + }, [props, resetStates]); + + const asyncProcess = useCallback(async () => { + try { + setLoading(true); + const policy = await getPasswordPolicyConfiguration(); + setPPolicy(policy); + setLoading(false); + } catch { + createErrorNotification( + translate("There was an issue completing the process the verification token might have expired"), + ); + setLoading(true); + } + }, [createErrorNotification, translate]); + + useEffect(() => { + asyncProcess(); + }, [asyncProcess]); + + const handlePasswordChange = useCallback(async () => { + setLoading(true); + if (oldPassword.trim() === "" || newPassword.trim() === "" || repeatNewPassword.trim() === "") { + if (oldPassword.trim() === "") { + setOldPasswordError(true); + } + if (newPassword.trim() === "") { + setNewPasswordError(true); + } + if (repeatNewPassword.trim() === "") { + setRepeatNewPasswordError(true); + } + setLoading(false); + return; + } + if (newPassword !== repeatNewPassword) { + setNewPasswordError(true); + setRepeatNewPasswordError(true); + createErrorNotification(translate("Passwords do not match")); + setLoading(false); + return; + } + + try { + await postPasswordChange(props.username, oldPassword, newPassword); + createSuccessNotification(translate("Password changed successfully")); + handleClose(); + } catch (err) { + resetPasswordErrors(); + setLoading(false); + if (axios.isAxiosError(err) && err.response) { + switch (err.response.status) { + case 400: // Bad Request - Weak Password + setNewPasswordError(true); + setRepeatNewPasswordError(true); + createErrorNotification( + translate("Your supplied password does not meet the password policy requirements"), + ); + break; + + case 401: // Unauthorized - Incorrect Password + setOldPasswordError(true); + createErrorNotification(translate("Incorrect password")); + break; + + case 500: // Internal Server Error + default: + createErrorNotification( + translate("There was an issue changing the {{item}}", { item: translate("password") }), + ); + break; + } + } else { + // Handle non-axios errors + createErrorNotification( + translate("There was an issue changing the {{item}}", { item: translate("password") }), + ); + } + return; + } + }, [ + createErrorNotification, + createSuccessNotification, + resetPasswordErrors, + handleClose, + newPassword, + oldPassword, + repeatNewPassword, + props.username, + translate, + ]); + + const useHandleKeyDown = ( + passwordState: string, + setError: React.Dispatch<React.SetStateAction<boolean>>, + nextRef?: React.MutableRefObject<HTMLInputElement>, + ) => { + return useCallback( + (event: React.KeyboardEvent<HTMLDivElement>) => { + if (event.key === "Enter") { + if (!passwordState.length) { + setError(true); + } else if (!nextRef) { + handlePasswordChange().catch(console.error); + } else { + nextRef?.current.focus(); + } + } + }, + [nextRef, passwordState.length, setError], + ); + }; + + const handleOldPWKeyDown = useHandleKeyDown(oldPassword, setOldPasswordError, newPasswordRef); + const handleNewPWKeyDown = useHandleKeyDown(newPassword, setNewPasswordError, repeatNewPasswordRef); + const handleRepeatNewPWKeyDown = useHandleKeyDown(repeatNewPassword, setRepeatNewPasswordError); + + const disabled = props.disabled || false; + + return ( + <Dialog open={props.open} maxWidth="xs"> + <DialogTitle>{translate("Change {{item}}", { item: translate("Password") })}</DialogTitle> + <DialogContent> + <FormControl id={"change-password-form"} disabled={loading}> + <Grid2 container spacing={1} alignItems={"center"} justifyContent={"center"} textAlign={"center"}> + <Grid2 size={{ xs: 12 }} sx={{ pt: 3 }}> + <TextField + inputRef={oldPasswordRef} + id="old-password" + label={translate("Old Password")} + variant="outlined" + required + value={oldPassword} + error={oldPasswordError} + disabled={disabled} + fullWidth + onChange={(v) => setOldPassword(v.target.value)} + onFocus={() => setOldPasswordError(false)} + type="password" + autoCapitalize="off" + autoComplete="off" + onKeyDown={handleOldPWKeyDown} + onKeyUp={useCheckCapsLock(setIsCapsLockOnOldPW)} + helperText={isCapsLockOnOldPW ? translate("Caps Lock is on") : " "} + color={isCapsLockOnOldPW ? "error" : "primary"} + onBlur={() => setIsCapsLockOnOldPW(false)} + /> + </Grid2> + <Grid2 size={{ xs: 12 }} sx={{ mt: 3 }}> + <TextField + inputRef={newPasswordRef} + id="new-password" + label={translate("New Password")} + variant="outlined" + required + fullWidth + disabled={disabled} + value={newPassword} + error={newPasswordError} + onChange={(v) => setNewPassword(v.target.value)} + onFocus={() => setNewPasswordError(false)} + type="password" + autoCapitalize="off" + autoComplete="off" + onKeyDown={handleNewPWKeyDown} + onKeyUp={useCheckCapsLock(setIsCapsLockOnNewPW)} + helperText={isCapsLockOnNewPW ? translate("Caps Lock is on") : " "} + color={isCapsLockOnNewPW ? "error" : "primary"} + onBlur={() => setIsCapsLockOnNewPW(false)} + /> + {pPolicy.mode === PasswordPolicyMode.Disabled ? null : ( + <PasswordMeter value={newPassword} policy={pPolicy} /> + )} + </Grid2> + <Grid2 size={{ xs: 12 }}> + <TextField + inputRef={repeatNewPasswordRef} + id="repeat-new-password" + label={translate("Repeat New Password")} + variant="outlined" + required + fullWidth + disabled={disabled} + value={repeatNewPassword} + error={repeatNewPasswordError} + onChange={(v) => setRepeatNewPassword(v.target.value)} + onFocus={() => setRepeatNewPasswordError(false)} + type="password" + autoCapitalize="off" + autoComplete="off" + onKeyDown={handleRepeatNewPWKeyDown} + onKeyUp={useCheckCapsLock(setIsCapsLockOnRepeatNewPW)} + helperText={isCapsLockOnRepeatNewPW ? translate("Caps Lock is on") : " "} + color={isCapsLockOnRepeatNewPW ? "error" : "primary"} + onBlur={() => setIsCapsLockOnRepeatNewPW(false)} + /> + </Grid2> + </Grid2> + </FormControl> + </DialogContent> + <DialogActions> + <Button id={"password-change-dialog-cancel"} color={"error"} onClick={handleClose}> + {translate("Cancel")} + </Button> + <Button + id={"password-change-dialog-submit"} + color={"primary"} + onClick={handlePasswordChange} + disabled={!(oldPassword.length && newPassword.length && repeatNewPassword.length) || loading} + startIcon={loading ? <CircularProgress color="inherit" size={20} /> : <></>} + > + {translate("Submit")} + </Button> + </DialogActions> + </Dialog> + ); +}; + +export default ChangePasswordDialog; diff --git a/web/src/views/Settings/Security/SecurityView.tsx b/web/src/views/Settings/Security/SecurityView.tsx new file mode 100644 index 000000000..6761591fa --- /dev/null +++ b/web/src/views/Settings/Security/SecurityView.tsx @@ -0,0 +1,244 @@ +import { Fragment, useCallback, useEffect, useState } from "react"; + +import { Box, Button, Container, List, ListItem, Paper, Stack, Tooltip, Typography, useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; + +import { useConfiguration } from "@hooks/Configuration"; +import { useNotifications } from "@hooks/NotificationsContext"; +import { useUserInfoGET } from "@hooks/UserInfo"; +import { UserSessionElevation, getUserSessionElevation } from "@services/UserSessionElevation"; +import IdentityVerificationDialog from "@views/Settings/Common/IdentityVerificationDialog"; +import SecondFactorDialog from "@views/Settings/Common/SecondFactorDialog"; +import ChangePasswordDialog from "@views/Settings/Security/ChangePasswordDialog"; + +const SettingsView = function () { + const { t: translate } = useTranslation("settings"); + const theme = useTheme(); + const { createErrorNotification } = useNotifications(); + + const [userInfo, fetchUserInfo, , fetchUserInfoError] = useUserInfoGET(); + const [elevation, setElevation] = useState<UserSessionElevation>(); + const [dialogSFOpening, setDialogSFOpening] = useState(false); + const [dialogIVOpening, setDialogIVOpening] = useState(false); + const [dialogPWChangeOpen, setDialogPWChangeOpen] = useState(false); + const [dialogPWChangeOpening, setDialogPWChangeOpening] = useState(false); + const [configuration, fetchConfiguration, , fetchConfigurationError] = useConfiguration(); + + const handleResetStateOpening = () => { + setDialogSFOpening(false); + setDialogIVOpening(false); + setDialogPWChangeOpening(false); + }; + + const handleResetState = useCallback(() => { + handleResetStateOpening(); + + setElevation(undefined); + setDialogPWChangeOpen(false); + }, []); + + const handleOpenChangePWDialog = useCallback(() => { + handleResetStateOpening(); + setDialogPWChangeOpen(true); + }, []); + + const handleSFDialogClosed = (ok: boolean, changed: boolean) => { + if (!ok) { + console.warn("Second Factor dialog close callback failed, it was likely cancelled by the user."); + + handleResetState(); + + return; + } + + if (changed) { + handleElevationRefresh() + .catch(console.error) + .then(() => { + setDialogIVOpening(true); + }); + } else { + setDialogIVOpening(true); + } + }; + + const handleSFDialogOpened = () => { + setDialogSFOpening(false); + }; + + const handleIVDialogClosed = useCallback( + (ok: boolean) => { + if (!ok) { + console.warn( + "Identity Verification dialog close callback failed, it was likely cancelled by the user.", + ); + + handleResetState(); + + return; + } + + setElevation(undefined); + if (dialogPWChangeOpening) { + handleOpenChangePWDialog(); + } + }, + [dialogPWChangeOpening, handleOpenChangePWDialog, handleResetState], + ); + + const handleIVDialogOpened = () => { + setDialogIVOpening(false); + }; + + const handleElevationRefresh = async () => { + try { + const result = await getUserSessionElevation(); + setElevation(result); + } catch { + createErrorNotification(translate("Failed to get session elevation status")); + } + }; + + const handleElevation = () => { + handleElevationRefresh().catch(console.error); + + setDialogSFOpening(true); + }; + + const handleChangePassword = () => { + setDialogPWChangeOpening(true); + + handleElevation(); + }; + + useEffect(() => { + if (fetchUserInfoError) { + createErrorNotification(translate("There was an issue retrieving user preferences")); + } + if (fetchConfigurationError) { + createErrorNotification(translate("There was an issue retrieving configuration")); + } + }, [fetchUserInfoError, fetchConfigurationError, createErrorNotification, translate]); + + useEffect(() => { + fetchUserInfo(); + fetchConfiguration(); + }, [fetchUserInfo, fetchConfiguration]); + + const PasswordChangeButton = () => { + const buttonContent = ( + <Button + id="change-password-button" + variant="contained" + sx={{ p: 1, width: "100%" }} + onClick={handleChangePassword} + disabled={configuration?.password_change_disabled || false} + > + {translate("Change Password")} + </Button> + ); + + return configuration?.password_change_disabled ? ( + <Tooltip title={translate("This is disabled by your administrator.")}> + <span>{buttonContent}</span> + </Tooltip> + ) : ( + buttonContent + ); + }; + + return ( + <Fragment> + <SecondFactorDialog + info={userInfo} + elevation={elevation} + opening={dialogSFOpening} + handleClosed={handleSFDialogClosed} + handleOpened={handleSFDialogOpened} + /> + <IdentityVerificationDialog + opening={dialogIVOpening} + elevation={elevation} + handleClosed={handleIVDialogClosed} + handleOpened={handleIVDialogOpened} + /> + <ChangePasswordDialog + username={userInfo?.display_name || ""} + open={dialogPWChangeOpen} + setClosed={() => { + handleResetState(); + }} + /> + + <Container + sx={{ + display: "flex", + justifyContent: "center", + alignItems: "flex-start", + height: "100vh", + pt: 8, + }} + > + <Paper + variant="outlined" + sx={{ + display: "flex", + justifyContent: "center", + alignItems: "center", + height: "auto", + }} + > + <Stack spacing={2} sx={{ m: 2, width: "100%" }}> + <Box sx={{ p: { xs: 1, md: 3 } }}> + <Box + sx={{ + width: "100%", + p: 1.25, + mb: 1, + border: `1px solid ${theme.palette.grey[600]}`, + borderRadius: 1, + }} + > + <Box display="flex" alignItems="center"> + <Typography sx={{ mr: 1 }}>{translate("Email")}:</Typography> + <Typography>{userInfo?.emails?.[0] || ""}</Typography> + </Box> + {userInfo?.emails && userInfo.emails.length > 1 && ( + <List sx={{ width: "100%", padding: 0, pl: 4 }}> + {" "} + {userInfo.emails.slice(1).map((email: string, index: number) => ( + <ListItem key={index} sx={{ paddingTop: 0, paddingBottom: 0 }}> + <Typography>{email}</Typography> + </ListItem> + ))} + </List> + )} + </Box> + <Box + sx={{ + width: "100%", + p: 1.25, + mb: 1, + border: `1px solid ${theme.palette.grey[600]}`, + borderRadius: 1, + }} + > + <Typography> + {translate("Username")}: {userInfo?.display_name || ""} + </Typography> + </Box> + <Box + sx={{ p: 1.25, mb: 1, border: `1px solid ${theme.palette.grey[600]}`, borderRadius: 1 }} + > + <Typography>{translate("Password")}: ●●●●●●●●</Typography> + </Box> + <PasswordChangeButton /> + </Box> + </Stack> + </Paper> + </Container> + </Fragment> + ); +}; + +export default SettingsView; diff --git a/web/src/views/Settings/SettingsRouter.tsx b/web/src/views/Settings/SettingsRouter.tsx index ba8fdf469..601d08a8e 100644 --- a/web/src/views/Settings/SettingsRouter.tsx +++ b/web/src/views/Settings/SettingsRouter.tsx @@ -2,11 +2,12 @@ import React, { useEffect } from "react"; import { Route, Routes } from "react-router-dom"; -import { IndexRoute, SettingsTwoFactorAuthenticationSubRoute } from "@constants/Routes"; +import { IndexRoute, SecuritySubRoute, SettingsTwoFactorAuthenticationSubRoute } from "@constants/Routes"; import { useRouterNavigate } from "@hooks/RouterNavigate"; import { useAutheliaState } from "@hooks/State"; import SettingsLayout from "@layouts/SettingsLayout"; import { AuthenticationLevel } from "@services/State"; +import SecurityView from "@views/Settings/Security/SecurityView"; import SettingsView from "@views/Settings/SettingsView"; import TwoFactorAuthenticationView from "@views/Settings/TwoFactorAuthentication/TwoFactorAuthenticationView"; @@ -30,6 +31,7 @@ const SettingsRouter = function (props: Props) { <SettingsLayout> <Routes> <Route path={IndexRoute} element={<SettingsView />} /> + <Route path={SecuritySubRoute} element={<SecurityView />} /> <Route path={SettingsTwoFactorAuthenticationSubRoute} element={<TwoFactorAuthenticationView />} /> </Routes> </SettingsLayout> |
