diff --git a/internal/commands/const.go b/internal/commands/const.go index 2e322a2c4..e1b387323 100644 --- a/internal/commands/const.go +++ b/internal/commands/const.go @@ -774,5 +774,5 @@ Layouts: ) const ( - fmtLogServerListening = "Server is listening for %s connections on '%s' path '%s'" + fmtLogServerListening = "Listening for %s connections on '%s' path '%s'" ) diff --git a/internal/server/locales/en/portal.json b/internal/server/locales/en/portal.json index 0a9065c52..434975419 100644 --- a/internal/server/locales/en/portal.json +++ b/internal/server/locales/en/portal.json @@ -8,6 +8,7 @@ "Automatically refresh these permissions without user interaction": "Automatically refresh these permissions without user interaction", "Cancel": "Cancel", "Client ID": "Client ID: {{client_id}}", + "Close": "Close", "Consent Request": "Consent Request", "Contact your administrator to register a device": "Contact your administrator to register a device.", "Could not obtain user settings": "Could not obtain user settings", @@ -50,8 +51,8 @@ "Reset password?": "Reset password?", "Reset": "Reset", "Scan QR Code": "Scan QR Code", + "Scope": "Scope {{name}}", "Secret": "Secret", - "Security Key - WebAuthN": "Security Key - WebAuthN", "Select a Device": "Select a Device", "Sign in": "Sign in", "Sign out": "Sign out", @@ -67,6 +68,7 @@ "Time-based One-Time Password": "Time-based One-Time Password", "Use OpenID to verify your identity": "Use OpenID to verify your identity", "Username": "Username", + "Webauthn - Security Key": "Webauthn - Security Key", "You must open the link from the same device and browser that initiated the registration process": "You must open the link from the same device and browser that initiated the registration process", "You must view and accept the Privacy Policy before using": "You must view and accept the <0>Privacy Policy before using", "You're being signed out and redirected": "You're being signed out and redirected", diff --git a/internal/server/locales/en/settings.json b/internal/server/locales/en/settings.json index c9b4a61ec..2fa312caa 100644 --- a/internal/server/locales/en/settings.json +++ b/internal/server/locales/en/settings.json @@ -1,7 +1,9 @@ { "Actions": "Actions", "Add": "Add", - "Add new Security Key": "Add new Security Key", + "Add Credential": "Add Credential", + "Added": "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 Attestation GUID": "Authenticator Attestation GUID", "Cancel": "Cancel", @@ -9,21 +11,39 @@ "Created": "Created", "Delete": "Delete", "Details": "Details", + "Display extended information for this Webauthn credential": "Display extended information for this Webauthn credential", "Edit": "Edit", + "Edit information for this Webauthn credential": "Edit information for this Webauthn credential", + "Edit Webauthn Credential": "Edit Webauthn Credential", "Enabled": "Enabled", - "Last Used": "Last Used", + "Enter a new name for this Webauthn credential": "Enter a new name for this Webauthn credential:", + "Extended Webauthn credential information for security key": "Extended Webauthn credential information for security key {{description}}", + "Identifier": "Identifier", + "Last Used": "Last Used {{when, datetime}}", "Manage your security keys": "Manage your security keys", "Name": "Name", "No": "No", + "No Registered Webauthn Credentials": "No Registered Webauthn Credentials", "Overview": "Overview", "Provide the details for the new security key": "Provide the details for the new security key", + "Register Webauthn Credential (Security Key)": "Register Webauthn Credential (Security Key)", "Relying Party ID": "Relying Party ID", + "Remove": "Remove", + "Remove this Webauthn credential": "Remove this Webauthn credential", + "Remove Webauthn Credential": "Remove Webauthn Credential", "Settings": "Settings", - "Show Details": "Show Details", + "Successfully deleted the Webauthn credential": "Successfully deleted the Webauthn credential", + "Successfully updated the Webauthn credential": "Successfully updated the Webauthn credential", + "There was a problem deleting the Webauthn credential": "There was a problem deleting the Webauthn credential", + "There was a problem updating the Webauthn credential": "There was a problem updating the Webauthn credential", "Transports": "Transports", "Two-Factor Authentication": "Two-Factor Authentication", "Usage Count": "Usage Count", - "Webauthn Credential Identifier": "Credential Identifier: {{id}}", - "Webauthn Public Key": "Public Key: {{key}}", - "Yes": "Yes" + "Webauthn Credential Details": "Webauthn Credential Details", + "Webauthn Credentials": "Webauthn Credentials", + "Yes": "Yes", + "You must have a higher authentication level to delete Webauthn credentials": "You must have a higher authentication level to delete Webauthn credentials", + "You must be elevated to delete Webauthn credentials": "You must be elevated to delete Webauthn credentials", + "You must have a higher authentication level to update Webauthn credentials": "You must have a higher authentication level to update Webauthn credentials", + "You must be elevated to update Webauthn credentials": "You must be elevated to update Webauthn credentials" } diff --git a/web/package.json b/web/package.json index a5a2ecd04..e9ec143bd 100644 --- a/web/package.json +++ b/web/package.json @@ -37,6 +37,7 @@ "axios": "1.3.2", "broadcast-channel": "4.20.2", "classnames": "2.3.2", + "date-fns": "2.29.3", "i18next": "22.4.9", "i18next-browser-languagedetector": "7.0.1", "i18next-http-backend": "2.1.1", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index e7bb03975..1c9fc2c73 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -30,6 +30,7 @@ specifiers: axios: 1.3.2 broadcast-channel: 4.20.2 classnames: 2.3.2 + date-fns: 2.29.3 esbuild: 0.17.7 esbuild-jest: 0.5.0 eslint: 8.34.0 @@ -83,6 +84,7 @@ dependencies: axios: 1.3.2 broadcast-channel: 4.20.2 classnames: 2.3.2 + date-fns: 2.29.3 i18next: 22.4.9 i18next-browser-languagedetector: 7.0.1 i18next-http-backend: 2.1.1 @@ -5003,6 +5005,11 @@ packages: whatwg-url: 11.0.0 dev: true + /date-fns/2.29.3: + resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==} + engines: {node: '>=0.11'} + dev: false + /debug/2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -5396,6 +5403,7 @@ packages: /eslint-config-prettier/8.6.0_eslint@8.34.0: resolution: {integrity: sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==} + hasBin: true peerDependencies: eslint: '>=7.0.0' dependencies: @@ -6860,6 +6868,7 @@ packages: /jest-cli/29.4.2_@types+node@18.13.0: resolution: {integrity: sha512-b+eGUtXq/K2v7SH3QcJvFvaUaCDS1/YAZBYz0m28Q/Ppyr+1qNaHmVYikOrbHVbZqYQs2IeI3p76uy6BWbXq8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: @@ -7375,6 +7384,7 @@ packages: /jest/29.4.2_@types+node@18.13.0: resolution: {integrity: sha512-+5hLd260vNIHu+7ZgMIooSpKl7Jp5pHKb51e73AJU3owd5dEo/RfVwHbA/na3C/eozrt3hJOLGf96c7EWwIAzg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: @@ -9229,6 +9239,7 @@ packages: /ts-node/10.9.0_4bewfcp2iebiwuold25d6rgcsy: resolution: {integrity: sha512-bunW18GUyaCSYRev4DPf4SQpom3pWH29wKl0sDk5zE7ze19RImEVhCW7K4v3hHKkUyfWotU08ToE2RS+Y49aug==} + hasBin: true peerDependencies: '@swc/core': '>=1.2.50' '@swc/wasm': '>=1.2.50' @@ -9260,6 +9271,7 @@ packages: /tsconfck/2.0.1_typescript@4.9.5: resolution: {integrity: sha512-/ipap2eecmVBmBlsQLBRbUmUNFwNJV/z2E+X0FPtHNjPwroMZQ7m39RMaCywlCulBheYXgMdUlWDd9rzxwMA0Q==} engines: {node: ^14.13.1 || ^16 || >=18, pnpm: ^7.0.1} + hasBin: true peerDependencies: typescript: ^4.3.5 peerDependenciesMeta: @@ -9422,6 +9434,7 @@ packages: /update-browserslist-db/1.0.10_browserslist@4.21.4: resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==} + hasBin: true peerDependencies: browserslist: '>= 4.21.0' dependencies: @@ -9523,6 +9536,7 @@ packages: /vite/4.1.1_@types+node@18.13.0: resolution: {integrity: sha512-LM9WWea8vsxhr782r9ntg+bhSFS06FJgCvvB0+8hf8UWtvaiDagKYWXndjfX6kGl74keHJUcpzrQliDXZlF5yg==} engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true peerDependencies: '@types/node': '>= 14' less: '*' diff --git a/web/src/App.tsx b/web/src/App.tsx index 841447b79..21c99a540 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -12,7 +12,6 @@ import { IndexRoute, LogoutRoute, RegisterOneTimePasswordRoute, - RegisterWebauthnRoute, ResetPasswordStep1Route, ResetPasswordStep2Route, SettingsRoute, @@ -29,7 +28,6 @@ import { getTheme, } from "@utils/Configuration"; import RegisterOneTimePassword from "@views/DeviceRegistration/RegisterOneTimePassword"; -import RegisterWebauthn from "@views/DeviceRegistration/RegisterWebauthn"; import BaseLoadingPage from "@views/LoadingPage/BaseLoadingPage"; import ConsentView from "@views/LoginPortal/ConsentView/ConsentView"; import LoginPortal from "@views/LoginPortal/LoginPortal"; @@ -91,7 +89,6 @@ const App: React.FC = (props: Props) => { } /> } /> - } /> } /> } /> } /> diff --git a/web/src/components/Brand.tsx b/web/src/components/Brand.tsx index 18712c362..797c7f363 100644 --- a/web/src/components/Brand.tsx +++ b/web/src/components/Brand.tsx @@ -14,6 +14,7 @@ const url = "https://www.authelia.com"; const Brand = function (props: Props) { const { t: translate } = useTranslation(); + const styles = useStyles(); const privacyEnabled = getPrivacyPolicyEnabled(); diff --git a/web/src/components/PasswordMeter.tsx b/web/src/components/PasswordMeter.tsx index c274eeaac..0ff622e68 100644 --- a/web/src/components/PasswordMeter.tsx +++ b/web/src/components/PasswordMeter.tsx @@ -19,7 +19,7 @@ const PasswordMeter = function (props: Props) { const [progressColor] = useState(["#D32F2F", "#FF5722", "#FFEB3B", "#AFB42B", "#62D32F"]); const [passwordScore, setPasswordScore] = useState(0); const [maxScores, setMaxScores] = useState(0); - const [feedback, setFeedback] = useState(""); + const [feedback, setFeedback] = useState(null); useEffect(() => { const password = props.value; @@ -114,7 +114,7 @@ const PasswordMeter = function (props: Props) { return ( - + ); }; diff --git a/web/src/components/PrivacyPolicyDrawer.tsx b/web/src/components/PrivacyPolicyDrawer.tsx index dcdb363b5..fd661df06 100644 --- a/web/src/components/PrivacyPolicyDrawer.tsx +++ b/web/src/components/PrivacyPolicyDrawer.tsx @@ -6,10 +6,11 @@ import { usePersistentStorageValue } from "@hooks/PersistentStorage"; import { getPrivacyPolicyEnabled, getPrivacyPolicyRequireAccept } from "@utils/Configuration"; const PrivacyPolicyDrawer = function (props: DrawerProps) { + const { t: translate } = useTranslation(); + const privacyEnabled = getPrivacyPolicyEnabled(); const privacyRequireAccept = getPrivacyPolicyRequireAccept(); const [accepted, setAccepted] = usePersistentStorageValue("privacy-policy-accepted", false); - const { t: translate } = useTranslation(); return privacyEnabled && privacyRequireAccept && !accepted ? ( diff --git a/web/src/components/PrivacyPolicyLink.tsx b/web/src/components/PrivacyPolicyLink.tsx index c9b82fc77..9276eeeb1 100644 --- a/web/src/components/PrivacyPolicyLink.tsx +++ b/web/src/components/PrivacyPolicyLink.tsx @@ -6,10 +6,10 @@ import { useTranslation } from "react-i18next"; import { getPrivacyPolicyURL } from "@utils/Configuration"; const PrivacyPolicyLink = function (props: LinkProps) { - const hrefPrivacyPolicy = getPrivacyPolicyURL(); - const { t: translate } = useTranslation(); + const hrefPrivacyPolicy = getPrivacyPolicyURL(); + return ( diff --git a/web/src/components/WebauthnRegisterIcon.tsx b/web/src/components/WebauthnRegisterIcon.tsx new file mode 100644 index 000000000..d29dc0c33 --- /dev/null +++ b/web/src/components/WebauthnRegisterIcon.tsx @@ -0,0 +1,39 @@ +import React, { useEffect } from "react"; + +import { Box, Theme, useTheme } from "@mui/material"; +import makeStyles from "@mui/styles/makeStyles"; + +import FingerTouchIcon from "@components/FingerTouchIcon"; +import LinearProgressBar from "@components/LinearProgressBar"; +import { useTimer } from "@hooks/Timer"; +import IconWithContext from "@views/LoginPortal/SecondFactor/IconWithContext"; + +interface Props { + timeout: number; +} + +export default function WebauthnRegisterIcon(props: Props) { + const theme = useTheme(); + const [timerPercent, triggerTimer] = useTimer(props.timeout); + + const styles = makeStyles((theme: Theme) => ({ + icon: { + display: "inline-block", + }, + progressBar: { + marginTop: theme.spacing(), + }, + }))(); + + useEffect(() => { + triggerTimer(); + }, [triggerTimer]); + + return ( + + }> + + + + ); +} diff --git a/web/src/constants/Routes.ts b/web/src/constants/Routes.ts index 4e98c3e6a..d7e705918 100644 --- a/web/src/constants/Routes.ts +++ b/web/src/constants/Routes.ts @@ -9,7 +9,6 @@ export const SecondFactorPushSubRoute: string = "/push-notification"; export const ResetPasswordStep1Route: string = "/reset-password/step1"; 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"; diff --git a/web/src/layouts/LoginLayout.tsx b/web/src/layouts/LoginLayout.tsx index a6bd2eb45..5283f88e8 100644 --- a/web/src/layouts/LoginLayout.tsx +++ b/web/src/layouts/LoginLayout.tsx @@ -16,18 +16,19 @@ import { getLogoOverride } from "@utils/Configuration"; export interface Props { id?: string; children?: ReactNode; - title?: string; - titleTooltip?: string; - subtitle?: string; - subtitleTooltip?: string; + title?: string | null; + titleTooltip?: string | null; + subtitle?: string | null; + subtitleTooltip?: string | null; showBrand?: boolean; showSettings?: boolean; } const LoginLayout = function (props: Props) { + const { t: translate } = useTranslation(); + const navigate = useNavigate(); const styles = useStyles(); - const { t: translate } = useTranslation(); const logo = getLogoOverride() ? ( Logo @@ -82,7 +83,7 @@ const LoginLayout = function (props: Props) { ) : null} @@ -91,7 +92,7 @@ const LoginLayout = function (props: Props) { ) : null} diff --git a/web/src/layouts/SettingsLayout.tsx b/web/src/layouts/SettingsLayout.tsx index 12081209b..c5d91597d 100644 --- a/web/src/layouts/SettingsLayout.tsx +++ b/web/src/layouts/SettingsLayout.tsx @@ -33,6 +33,7 @@ const defaultDrawerWidth = 240; const SettingsLayout = function (props: Props) { const { t: translate } = useTranslation("settings"); + const navigate = useRouterNavigate(); useEffect(() => { @@ -65,7 +66,7 @@ const SettingsLayout = function (props: Props) { navigate(IndexRoute); }} > - {"Close"} + {translate("Close")} @@ -114,7 +115,7 @@ const SettingsMenuItem = function (props: SettingsMenuItemProps) { const navigate = useRouterNavigate(); return ( - console.log("selected") : () => navigate(props.pathname)}> + navigate(props.pathname) : undefined}> {props.icon} diff --git a/web/src/models/Webauthn.ts b/web/src/models/Webauthn.ts index c26a87246..e9ebd2844 100644 --- a/web/src/models/Webauthn.ts +++ b/web/src/models/Webauthn.ts @@ -107,8 +107,8 @@ export interface AuthenticationResult { export interface WebauthnDevice { id: string; - created_at: Date; - last_used_at?: Date; + created_at: string; + last_used_at?: string; rpid: string; description: string; kid: Uint8Array; diff --git a/web/src/services/Webauthn.ts b/web/src/services/Webauthn.ts index 9c2d983fa..e3b1e4697 100644 --- a/web/src/services/Webauthn.ts +++ b/web/src/services/Webauthn.ts @@ -144,7 +144,6 @@ export async function startWebauthnRegistration(options: PublicKeyCredentialCrea }; try { - console.log(JSON.stringify(options)); result.response = await startRegistration(options); } catch (e) { const exception = e as DOMException; diff --git a/web/src/views/DeviceRegistration/RegisterOneTimePassword.tsx b/web/src/views/DeviceRegistration/RegisterOneTimePassword.tsx index 289fb9779..9fbe349f2 100644 --- a/web/src/views/DeviceRegistration/RegisterOneTimePassword.tsx +++ b/web/src/views/DeviceRegistration/RegisterOneTimePassword.tsx @@ -20,15 +20,18 @@ import LoginLayout from "@layouts/LoginLayout"; import { completeTOTPRegistrationProcess } from "@services/RegisterDevice"; const RegisterOneTimePassword = function () { + const { t: translate } = useTranslation(); + const styles = useStyles(); + const navigate = useNavigate(); + const { createSuccessNotification, createErrorNotification } = useNotifications(); + // The secret retrieved from the API is all is ok. const [secretURL, setSecretURL] = useState("empty"); const [secretBase32, setSecretBase32] = useState(undefined as string | undefined); - const { createSuccessNotification, createErrorNotification } = useNotifications(); const [hasErrored, setHasErrored] = useState(false); const [isLoading, setIsLoading] = useState(false); - const { t: translate } = useTranslation(); // Get the token from the query param to give it back to the API when requesting // the secret for OTP. diff --git a/web/src/views/LoadingPage/LoadingPage.tsx b/web/src/views/LoadingPage/LoadingPage.tsx index 5181ef879..b4173df0c 100644 --- a/web/src/views/LoadingPage/LoadingPage.tsx +++ b/web/src/views/LoadingPage/LoadingPage.tsx @@ -6,6 +6,7 @@ import BaseLoadingPage from "@views/LoadingPage/BaseLoadingPage"; const LoadingPage = function () { const { t: translate } = useTranslation(); + return ; }; diff --git a/web/src/views/LoginPortal/Authenticated.tsx b/web/src/views/LoginPortal/Authenticated.tsx index 64f5acce3..78ce21f4a 100644 --- a/web/src/views/LoginPortal/Authenticated.tsx +++ b/web/src/views/LoginPortal/Authenticated.tsx @@ -7,8 +7,10 @@ import { useTranslation } from "react-i18next"; import SuccessIcon from "@components/SuccessIcon"; const Authenticated = function () { - const styles = useStyles(); const { t: translate } = useTranslation(); + + const styles = useStyles(); + return (
diff --git a/web/src/views/LoginPortal/AuthenticatedView/AuthenticatedView.tsx b/web/src/views/LoginPortal/AuthenticatedView/AuthenticatedView.tsx index 66bbb0e59..d17f570e1 100644 --- a/web/src/views/LoginPortal/AuthenticatedView/AuthenticatedView.tsx +++ b/web/src/views/LoginPortal/AuthenticatedView/AuthenticatedView.tsx @@ -14,10 +14,12 @@ export interface Props { } const AuthenticatedView = function (props: Props) { - const styles = useStyles(); - const navigate = useNavigate(); const { t: translate } = useTranslation(); + const navigate = useNavigate(); + + const styles = useStyles(); + const handleLogoutClick = () => { navigate(SignOutRoute); }; diff --git a/web/src/views/LoginPortal/ConsentView/ConsentView.tsx b/web/src/views/LoginPortal/ConsentView/ConsentView.tsx index 642d14fce..8325bf59e 100644 --- a/web/src/views/LoginPortal/ConsentView/ConsentView.tsx +++ b/web/src/views/LoginPortal/ConsentView/ConsentView.tsx @@ -47,23 +47,26 @@ function scopeNameToAvatar(id: string) { } const ConsentView = function (props: Props) { - const styles = useStyles(); const { t: translate } = useTranslation(); + + const [userInfo, fetchUserInfo, , fetchUserInfoError] = useUserInfoGET(); + + const { createErrorNotification, resetNotification } = useNotifications(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const redirect = useRedirector(); const consentID = searchParams.get(Identifier); - const { createErrorNotification, resetNotification } = useNotifications(); + const [response, setResponse] = useState(undefined); const [error, setError] = useState(undefined); const [preConfigure, setPreConfigure] = useState(false); + const styles = useStyles(); + const handlePreConfigureChanged = () => { setPreConfigure((preConfigure) => !preConfigure); }; - const [userInfo, fetchUserInfo, , fetchUserInfoError] = useUserInfoGET(); - useEffect(() => { fetchUserInfo(); }, [fetchUserInfo]); @@ -167,7 +170,7 @@ const ConsentView = function (props: Props) {
{response?.scopes.map((scope: string) => ( - + {scopeNameToAvatar(scope)} @@ -180,10 +183,7 @@ const ConsentView = function (props: Props) { {response?.pre_configuration ? ( new BroadcastChannel("login"), []); + const [rememberMe, setRememberMe] = useState(false); const [username, setUsername] = useState(""); const [usernameError, setUsernameError] = useState(false); const [password, setPassword] = useState(""); const [passwordError, setPasswordError] = useState(false); - const { createErrorNotification } = useNotifications(); + // TODO (PR: #806, Issue: #511) potentially refactor const usernameRef = useRef() as MutableRefObject; const passwordRef = useRef() as MutableRefObject; - const { t: translate } = useTranslation(); + + const styles = useStyles(); useEffect(() => { const timeout = setTimeout(() => usernameRef.current.focus(), 10); @@ -122,7 +126,7 @@ const FirstFactorForm = function (props: Props) { onFocus={() => setUsernameError(false)} autoCapitalize="none" autoComplete="username" - onKeyPress={(ev) => { + onKeyDown={(ev) => { if (ev.key === "Enter") { if (!username.length) { setUsernameError(true); @@ -152,7 +156,7 @@ const FirstFactorForm = function (props: Props) { onFocus={() => setPasswordError(false)} type="password" autoComplete="current-password" - onKeyPress={(ev) => { + onKeyDown={(ev) => { if (ev.key === "Enter") { if (!username.length) { usernameRef.current.focus(); @@ -174,7 +178,7 @@ const FirstFactorForm = function (props: Props) { disabled={disabled} checked={rememberMe} onChange={handleRememberMeChange} - onKeyPress={(ev) => { + onKeyDown={(ev) => { if (ev.key === "Enter") { if (!username.length) { usernameRef.current.focus(); diff --git a/web/src/views/LoginPortal/SecondFactor/DeviceSelectionContainer.tsx b/web/src/views/LoginPortal/SecondFactor/DeviceSelectionContainer.tsx index 6decec943..4397eb7c4 100644 --- a/web/src/views/LoginPortal/SecondFactor/DeviceSelectionContainer.tsx +++ b/web/src/views/LoginPortal/SecondFactor/DeviceSelectionContainer.tsx @@ -1,6 +1,6 @@ import React, { ReactNode, useState } from "react"; -import { Button, Container, Grid, Theme, Typography } from "@mui/material"; +import { Box, Button, Container, Grid, Theme, Typography } from "@mui/material"; import makeStyles from "@mui/styles/makeStyles"; import PushNotificationIcon from "@components/PushNotificationIcon"; @@ -127,12 +127,12 @@ function DeviceItem(props: DeviceItemProps) { variant="contained" onClick={props.onSelect} > -
+ -
-
+ + {props.device.name} -
+
); @@ -172,12 +172,12 @@ function MethodItem(props: MethodItemProps) { variant="contained" onClick={props.onSelect} > -
+ -
-
+ + {props.method} -
+ ); diff --git a/web/src/views/LoginPortal/SecondFactor/IconWithContext.tsx b/web/src/views/LoginPortal/SecondFactor/IconWithContext.tsx index 143b052bc..a801a5af2 100644 --- a/web/src/views/LoginPortal/SecondFactor/IconWithContext.tsx +++ b/web/src/views/LoginPortal/SecondFactor/IconWithContext.tsx @@ -1,6 +1,6 @@ import React, { ReactNode } from "react"; -import { Theme } from "@mui/material"; +import { Box, Theme } from "@mui/material"; import makeStyles from "@mui/styles/makeStyles"; import classnames from "classnames"; @@ -30,12 +30,12 @@ const IconWithContext = function (props: IconWithContextProps) { }))(); return ( -
-
-
{props.icon}
-
-
{props.children}
-
+ + + {props.icon} + + {props.children} + ); }; diff --git a/web/src/views/LoginPortal/SecondFactor/MethodContainer.tsx b/web/src/views/LoginPortal/SecondFactor/MethodContainer.tsx index f8324518d..db2581643 100644 --- a/web/src/views/LoginPortal/SecondFactor/MethodContainer.tsx +++ b/web/src/views/LoginPortal/SecondFactor/MethodContainer.tsx @@ -28,14 +28,15 @@ export interface Props { } const DefaultMethodContainer = function (props: Props) { - const styles = useStyles(); const { t: translate } = useTranslation(); + + const styles = useStyles(); + const registerMessage = props.registered ? props.title === "Push Notification" ? "" : translate("Manage devices") : translate("Register device"); - const selectMessage = translate("Select a Device"); let container: ReactNode; let stateClass: string = ""; @@ -62,7 +63,7 @@ const DefaultMethodContainer = function (props: Props) {
{props.onSelectClick && props.registered ? ( - {selectMessage} + {translate("Select a Device")} ) : null} {(props.onRegisterClick && props.title !== "Push Notification") || diff --git a/web/src/views/LoginPortal/SecondFactor/MethodSelectionDialog.tsx b/web/src/views/LoginPortal/SecondFactor/MethodSelectionDialog.tsx index 93a85c0d9..ca3be7bd0 100644 --- a/web/src/views/LoginPortal/SecondFactor/MethodSelectionDialog.tsx +++ b/web/src/views/LoginPortal/SecondFactor/MethodSelectionDialog.tsx @@ -42,7 +42,7 @@ const MethodSelectionDialog = function (props: Props) { {props.methods.has(SecondFactorMethod.Webauthn) && props.webauthnSupported ? ( } onClick={() => props.onClick(SecondFactorMethod.Webauthn)} /> @@ -59,7 +59,7 @@ const MethodSelectionDialog = function (props: Props) { diff --git a/web/src/views/ResetPassword/ResetPasswordStep1.tsx b/web/src/views/ResetPassword/ResetPasswordStep1.tsx index 3e1dd8263..dbf7cc2fb 100644 --- a/web/src/views/ResetPassword/ResetPasswordStep1.tsx +++ b/web/src/views/ResetPassword/ResetPasswordStep1.tsx @@ -53,7 +53,7 @@ const ResetPasswordStep1 = function () { error={error} value={username} onChange={(e) => setUsername(e.target.value)} - onKeyPress={(ev) => { + onKeyDown={(ev) => { if (ev.key === "Enter") { doInitiateResetPasswordProcess(); ev.preventDefault(); diff --git a/web/src/views/ResetPassword/ResetPasswordStep2.tsx b/web/src/views/ResetPassword/ResetPasswordStep2.tsx index ce3669c98..0347cb52d 100644 --- a/web/src/views/ResetPassword/ResetPasswordStep2.tsx +++ b/web/src/views/ResetPassword/ResetPasswordStep2.tsx @@ -153,7 +153,7 @@ const ResetPasswordStep2 = function () { value={password2} onChange={(e) => setPassword2(e.target.value)} error={errorPassword2} - onKeyPress={(ev) => { + onKeyDown={(ev) => { if (ev.key === "Enter") { doResetPassword(); ev.preventDefault(); diff --git a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceDeleteDialog.tsx b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceDeleteDialog.tsx index 29996a720..13dee18ff 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceDeleteDialog.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceDeleteDialog.tsx @@ -1,39 +1,42 @@ import React from "react"; import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material"; +import { useTranslation } from "react-i18next"; import { WebauthnDevice } from "@models/Webauthn"; interface Props { open: boolean; - device: WebauthnDevice | undefined; + device: WebauthnDevice; handleClose: (ok: boolean) => void; } export default function WebauthnDeviceDeleteDialog(props: Props) { + const { t: translate } = useTranslation("settings"); + const handleCancel = () => { props.handleClose(false); }; return ( - {`Remove ${props.device ? props.device.description : "(unknown)"}`} + {translate("Remove Webauthn Credential")} - {`Are you sure you want to remove the device "${ - props.device ? props.device.description : "(unknown)" - }" from your account?`} + {translate("Are you sure you want to remove the Webauthn credential from from your account", { + description: props.device.description, + })} - + diff --git a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceDetailsDialog.tsx b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceDetailsDialog.tsx index 50d7d1b82..db5828ce7 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceDetailsDialog.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceDetailsDialog.tsx @@ -1,5 +1,6 @@ import React, { useState } from "react"; +import { Check, ContentCopy } from "@mui/icons-material"; import { Box, Button, @@ -13,11 +14,12 @@ import { } from "@mui/material"; import { useTranslation } from "react-i18next"; +import LoadingButton from "@components/LoadingButton"; import { WebauthnDevice } from "@models/Webauthn"; interface Props { open: boolean; - device: WebauthnDevice | undefined; + device: WebauthnDevice; handleClose: () => void; } @@ -26,43 +28,43 @@ export default function WebauthnDetailsDeleteDialog(props: Props) { return ( - Security key details + {translate("Webauthn Credential Details")} - {`Extended information for security key ${ - props.device ? props.device.description : "(unknown)" - }`} - {props.device && ( - - - - - - - - - - - )} + + {translate("Extended Webauthn credential information for security key", { + description: props.device.description, + })} + + + + + + + + + + + + + + + + - + ); @@ -71,28 +73,55 @@ export default function WebauthnDetailsDeleteDialog(props: Props) { interface PropertyTextProps { name: string; value: string; - clipboard?: boolean; } -function PropertyText(props: PropertyTextProps) { +function PropertyCopyButton(props: PropertyTextProps) { + const { t: translate } = useTranslation("settings"); + const [copied, setCopied] = useState(false); + const [copying, setCopying] = useState(false); const handleCopyToClipboard = () => { - navigator.clipboard.writeText(props.value); - setCopied(true); - setTimeout(() => { - setCopied(false); - }, 3000); + if (copied) { + return; + } + + (async () => { + setCopying(true); + + await navigator.clipboard.writeText(props.value); + + setTimeout(() => { + setCopying(false); + setCopied(true); + }, 500); + + setTimeout(() => { + setCopied(false); + }, 2000); + })(); }; return ( - + : } + > + {copied ? translate("Copied") : props.name} + + ); +} + +function PropertyText(props: PropertyTextProps) { + return ( + {`${props.name}: `} - - {props.clipboard ? (copied ? "(copied to clipboard)" : "(click to copy)") : props.value} - + {props.value} ); } diff --git a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceEditDialog.tsx b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceEditDialog.tsx index 2d2dd3133..beca9c49a 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceEditDialog.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceEditDialog.tsx @@ -1,19 +1,19 @@ import React, { MutableRefObject, useRef, useState } from "react"; -import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material"; +import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField } from "@mui/material"; import { useTranslation } from "react-i18next"; -import FixedTextField from "@components/FixedTextField"; import { WebauthnDevice } from "@models/Webauthn"; interface Props { open: boolean; - device: WebauthnDevice | undefined; + device: WebauthnDevice; handleClose: (ok: boolean, name: string) => void; } export default function WebauthnDeviceEditDialog(props: Props) { - const { t: translate } = useTranslation(); + const { t: translate } = useTranslation("settings"); + const [deviceName, setName] = useState(""); const nameRef = useRef() as MutableRefObject; const [nameError, setNameError] = useState(false); @@ -34,11 +34,10 @@ export default function WebauthnDeviceEditDialog(props: Props) { return ( - {`Edit ${props.device ? props.device.description : "(unknown)"}`} + {translate("Edit Webauthn Credential")} - Enter a new name for this device: - {translate("Enter a new name for this Webauthn credential")} + { + onKeyDown={(ev) => { if (ev.key === "Enter") { handleConfirm(); ev.preventDefault(); @@ -64,8 +63,8 @@ export default function WebauthnDeviceEditDialog(props: Props) { /> - - + + ); diff --git a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceItem.tsx b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceItem.tsx index e8041cee9..d903df3c4 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceItem.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceItem.tsx @@ -1,10 +1,10 @@ import React, { Fragment, useState } from "react"; +import { Fingerprint } from "@mui/icons-material"; import DeleteIcon from "@mui/icons-material/Delete"; import EditIcon from "@mui/icons-material/Edit"; import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; -import KeyRoundedIcon from "@mui/icons-material/KeyRounded"; -import { Box, Button, Stack, Typography } from "@mui/material"; +import { Box, Button, Paper, Stack, Tooltip, Typography } from "@mui/material"; import { useTranslation } from "react-i18next"; import LoadingButton from "@components/LoadingButton"; @@ -18,8 +18,7 @@ import WebauthnDeviceEditDialog from "@views/Settings/TwoFactorAuthentication/We interface Props { index: number; device: WebauthnDevice; - handleDeviceEdit(index: number, device: WebauthnDevice): void; - handleDeviceDelete(device: WebauthnDevice): void; + handleEdit: () => void; } export default function WebauthnDeviceItem(props: Props) { @@ -49,19 +48,21 @@ export default function WebauthnDeviceItem(props: Props) { if (response.data.status === "KO") { if (response.data.elevation) { - createErrorNotification(translate("You must be elevated to update the device")); + createErrorNotification(translate("You must be elevated to update Webauthn credentials")); } else if (response.data.authentication) { - createErrorNotification(translate("You must have a higher authentication level to update the device")); + createErrorNotification( + translate("You must have a higher authentication level to update Webauthn credentials"), + ); } else { - createErrorNotification(translate("There was a problem updating the device")); + createErrorNotification(translate("There was a problem updating the Webauthn credential")); } return; } - createSuccessNotification(translate("Successfully updated the device")); + createSuccessNotification(translate("Successfully updated the Webauthn credential")); - props.handleDeviceEdit(props.index, { ...props.device, description: name }); + props.handleEdit(); }; const handleDelete = async (ok: boolean) => { @@ -79,78 +80,119 @@ export default function WebauthnDeviceItem(props: Props) { if (response.data.status === "KO") { if (response.data.elevation) { - createErrorNotification(translate("You must be elevated to delete the device")); + createErrorNotification(translate("You must be elevated to delete Webauthn credentials")); } else if (response.data.authentication) { - createErrorNotification(translate("You must have a higher authentication level to delete the device")); + createErrorNotification( + translate("You must have a higher authentication level to delete Webauthn credentials"), + ); } else { - createErrorNotification(translate("There was a problem deleting the device")); + createErrorNotification(translate("There was a problem deleting the Webauthn credential")); } return; } - createSuccessNotification(translate("Successfully deleted the device")); + createSuccessNotification(translate("Successfully deleted the Webauthn credential")); - props.handleDeviceDelete(props.device); + props.handleEdit(); }; return ( - { - setShowDialogDetails(false); - }} - /> - - - - - - - - {props.device.description} - - {` (${props.device.attestation_type.toUpperCase()})`} - - Added {props.device.created_at.toString()} - - {props.device.last_used_at === undefined - ? translate("Never used") - : "Last used " + props.device.last_used_at.toString()} - - - - } - onClick={() => setShowDialogEdit(true)} - > - {translate("Edit")} - - } - onClick={() => setShowDialogDelete(true)} - > - {translate("Remove")} - - + + + { + setShowDialogDetails(false); + }} + /> + + + + + + + + {props.device.description} + + {` (${props.device.attestation_type.toUpperCase()})`} + + + {translate("Added", { + when: new Date(props.device.created_at), + formatParams: { + when: { + hour: "numeric", + minute: "numeric", + year: "numeric", + month: "long", + day: "numeric", + }, + }, + })} + + + {props.device.last_used_at === undefined + ? translate("Never used") + : translate("Last Used", { + when: new Date(props.device.last_used_at), + formatParams: { + when: { + hour: "numeric", + minute: "numeric", + year: "numeric", + month: "long", + day: "numeric", + }, + }, + })} + + + + + + + + } + onClick={() => setShowDialogEdit(true)} + > + {translate("Edit")} + + + + } + onClick={() => setShowDialogDelete(true)} + > + {translate("Remove")} + + + + + ); } diff --git a/web/src/views/DeviceRegistration/RegisterWebauthn.tsx b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceRegisterDialog.tsx similarity index 61% rename from web/src/views/DeviceRegistration/RegisterWebauthn.tsx rename to web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceRegisterDialog.tsx index b340e8504..02fb6916d 100644 --- a/web/src/views/DeviceRegistration/RegisterWebauthn.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDeviceRegisterDialog.tsx @@ -1,18 +1,28 @@ import React, { Fragment, MutableRefObject, useCallback, useEffect, useRef, useState } from "react"; -import { Box, Button, Grid, Stack, Step, StepLabel, Stepper, Theme, Typography } from "@mui/material"; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Grid, + Stack, + Step, + StepLabel, + Stepper, + TextField, + Theme, + Typography, +} from "@mui/material"; import makeStyles from "@mui/styles/makeStyles"; import { PublicKeyCredentialCreationOptionsJSON } from "@simplewebauthn/typescript-types"; import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; -import FixedTextField from "@components/FixedTextField"; import InformationIcon from "@components/InformationIcon"; -import SuccessIcon from "@components/SuccessIcon"; -import WebauthnTryIcon from "@components/WebauthnTryIcon"; -import { SettingsRoute, SettingsTwoFactorAuthenticationSubRoute } from "@constants/Routes"; +import WebauthnRegisterIcon from "@components/WebauthnRegisterIcon"; import { useNotifications } from "@hooks/NotificationsContext"; -import LoginLayout from "@layouts/LoginLayout"; import { AttestationResult, AttestationResultFailureString, @@ -24,25 +34,40 @@ import { finishRegistration, getAttestationCreationOptions, startWebauthnRegistr const steps = ["Confirm device", "Choose name"]; interface Props { - est: AuthenticatorSelectionCriteria; + open: boolean; + onClose: () => void; + setCancelled: () => void; } -const RegisterWebauthn = function (props: Props) { - const [state, setState] = useState(WebauthnTouchState.WaitTouch); +const WebauthnDeviceRegisterDialog = function (props: Props) { + const { t: translate } = useTranslation("settings"); + const styles = useStyles(); - const navigate = useNavigate(); - const { t: translate } = useTranslation(); const { createErrorNotification } = useNotifications(); - const [activeStep, setActiveStep] = React.useState(0); - const [result, setResult] = React.useState(null as null | RegistrationResult); - const [options, setOptions] = useState(null as null | PublicKeyCredentialCreationOptionsJSON); + const [state, setState] = useState(WebauthnTouchState.WaitTouch); + const [activeStep, setActiveStep] = useState(0); + const [result, setResult] = useState(null); + const [options, setOptions] = useState(null); + const [timeout, setTimeout] = useState(null); const [deviceName, setName] = useState(""); + const nameRef = useRef() as MutableRefObject; const [nameError, setNameError] = useState(false); - const handleBackClick = () => { - navigate(`${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`); + const resetStates = () => { + setState(WebauthnTouchState.WaitTouch); + setActiveStep(0); + setResult(null); + setOptions(null); + setTimeout(null); + setName(""); + }; + + const handleClose = () => { + resetStates(); + + props.setCancelled(); }; const finishAttestation = async () => { @@ -58,8 +83,7 @@ const RegisterWebauthn = function (props: Props) { const res = await finishRegistration(result.response, deviceName); switch (res.status) { case AttestationResult.Success: - setActiveStep(2); - navigate(`${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`); + handleClose(); break; case AttestationResult.Failure: createErrorNotification(res.message); @@ -71,7 +95,7 @@ const RegisterWebauthn = function (props: Props) { return; } - console.log("start registration"); + setTimeout(options.timeout ? options.timeout : null); try { setState(WebauthnTouchState.WaitTouch); @@ -79,7 +103,7 @@ const RegisterWebauthn = function (props: Props) { const res = await startWebauthnRegistration(options); - console.log("got response", res.result); + setTimeout(null); if (res.result === AttestationResult.Success) { if (res.response == null) { @@ -103,13 +127,29 @@ const RegisterWebauthn = function (props: Props) { }, [options, createErrorNotification]); useEffect(() => { - if (options !== null) { - startRegistration(); + if (state !== WebauthnTouchState.Failure || activeStep !== 0 || !props.open) { + return; } - }, [options, startRegistration]); + + handleClose(); + }, [props, state, activeStep, handleClose]); useEffect(() => { (async () => { + if (options === null || !props.open || activeStep !== 0) { + return; + } + + await startRegistration(); + })(); + }, [options, props.open, activeStep, startRegistration]); + + useEffect(() => { + (async () => { + if (!props.open || activeStep !== 0) { + return; + } + const res = await getAttestationCreationOptions(); if (res.status !== 200 || !res.options) { createErrorNotification( @@ -119,39 +159,31 @@ const RegisterWebauthn = function (props: Props) { } setOptions(res.options); })(); - }, [setOptions, createErrorNotification]); + }, [setOptions, createErrorNotification, props.open, activeStep]); function renderStep(step: number) { switch (step) { case 0: return ( -
- -
- Touch the token on your security key - - - - - - - + + {timeout !== null ? : null} + + + {translate("Touch the token on your security key")} +
); case 1: return ( -
-
+ + -
- Enter a name for this key + + {translate("Enter a name for this key")} - setName(v.target.value.substring(0, 30))} onFocus={() => setNameError(false)} autoCapitalize="none" autoComplete="webauthn-name" - onKeyPress={(ev) => { + onKeyDown={(ev) => { if (ev.key === "Enter") { if (!deviceName.length) { setNameError(true); } else { - finishAttestation(); + (async () => { + await finishAttestation(); + })(); } ev.preventDefault(); } @@ -178,35 +211,32 @@ const RegisterWebauthn = function (props: Props) { /> - - + -
- ); - case 2: - return ( -
-
- -
- {translate("Registration success")} -
+
); } } + const handleOnClose = () => { + if (activeStep === 0 || !props.open) { + return; + } + + handleClose(); + }; + return ( - - - - + + {translate("Register Webauthn Credential (Security Key)")} + + + {steps.map((label, index) => { const stepProps: { completed?: boolean } = {}; @@ -215,38 +245,34 @@ const RegisterWebauthn = function (props: Props) { } = {}; return ( - {label} + {translate(label)} ); })} + + {renderStep(activeStep)} - + - - + + + + + ); }; -export default RegisterWebauthn; +export default WebauthnDeviceRegisterDialog; const useStyles = makeStyles((theme: Theme) => ({ icon: { paddingTop: theme.spacing(4), paddingBottom: theme.spacing(4), }, - iconContainer: { - marginBottom: theme.spacing(2), - flex: "0 0 100%", - }, instruction: { paddingBottom: theme.spacing(4), }, - methodContainer: { - border: "1px solid #d6d6d6", - borderRadius: "10px", - padding: theme.spacing(4), - marginTop: theme.spacing(2), - marginBottom: theme.spacing(2), - }, })); diff --git a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDevices.tsx b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDevices.tsx index c1e60ead6..2c7cce94d 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDevices.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDevices.tsx @@ -1,11 +1,11 @@ -import React, { Fragment, Suspense } from "react"; +import React, { Fragment, Suspense, useState } from "react"; import { Box, Button, Paper, Stack, Typography } from "@mui/material"; -import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; -import { RegisterWebauthnRoute } from "@constants/Routes"; import { AutheliaState } from "@services/State"; import LoadingPage from "@views/LoadingPage/LoadingPage"; +import WebauthnDeviceRegisterDialog from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceRegisterDialog"; import WebauthnDevicesStack from "@views/Settings/TwoFactorAuthentication/WebauthnDevicesStack"; interface Props { @@ -13,31 +13,49 @@ interface Props { } export default function WebauthnDevices(props: Props) { - const navigate = useNavigate(); + const { t: translate } = useTranslation("settings"); - const initiateRegistration = async (redirectRoute: string) => { - navigate(redirectRoute); - }; + const [showWebauthnDeviceRegisterDialog, setShowWebauthnDeviceRegisterDialog] = useState(false); + const [refreshState, setRefreshState] = useState(0); - const handleAddKeyButtonClick = () => { - initiateRegistration(RegisterWebauthnRoute); + const handleIncrementRefreshState = () => { + setRefreshState((refreshState) => refreshState + 1); }; return ( + { + handleIncrementRefreshState(); + }} + setCancelled={() => { + setShowWebauthnDeviceRegisterDialog(false); + handleIncrementRefreshState(); + }} + /> - Webauthn Devices + {translate("Webauthn Credentials")} - }> - + diff --git a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDevicesStack.tsx b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDevicesStack.tsx index aa3b64d66..cab1ad05f 100644 --- a/web/src/views/Settings/TwoFactorAuthentication/WebauthnDevicesStack.tsx +++ b/web/src/views/Settings/TwoFactorAuthentication/WebauthnDevicesStack.tsx @@ -1,55 +1,40 @@ import React, { Fragment, useEffect, useState } from "react"; import { Stack, Typography } from "@mui/material"; +import { useTranslation } from "react-i18next"; import { WebauthnDevice } from "@models/Webauthn"; import { getWebauthnDevices } from "@services/UserWebauthnDevices"; import WebauthnDeviceItem from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceItem"; -interface Props {} +interface Props { + refreshState: number; + incrementRefreshState: () => void; +} export default function WebauthnDevicesStack(props: Props) { + const { t: translate } = useTranslation("settings"); + const [devices, setDevices] = useState([]); useEffect(() => { (async function () { + setDevices([]); const devices = await getWebauthnDevices(); setDevices(devices); })(); - }, []); - - const handleEdit = (index: number, device: WebauthnDevice) => { - const nextDevices = devices.map((d, i) => { - if (i === index) { - return device; - } else { - return d; - } - }); - - setDevices(nextDevices); - }; - - const handleDelete = (device: WebauthnDevice) => { - setDevices(devices.filter((d) => d.id !== device.id && d.kid !== device.kid)); - }; + }, [props.refreshState]); return ( - {devices ? ( + {devices.length !== 0 ? ( {devices.map((x, idx) => ( - + ))} ) : ( - No Registered Webauthn Devices + {translate("No Registered Webauthn Credentials")} )} );