Merge orgin/master into feat-settings-ui

Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>
feat-otp-verification
James Elliott 2023-04-15 03:08:43 +10:00
commit 6c89ee1f9c
No known key found for this signature in database
GPG Key ID: 0F1C4A096E857E49
39 changed files with 156 additions and 156 deletions

View File

@ -70,7 +70,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GoodConfiguration(t *tes
}, },
{ {
ID: "b-client", ID: "b-client",
Description: "Normal DisplayName", Description: "Normal Description",
Secret: MustDecodeSecret("$plaintext$b-client-secret"), Secret: MustDecodeSecret("$plaintext$b-client-secret"),
Policy: twofactor, Policy: twofactor,
RedirectURIs: []string{ RedirectURIs: []string{

View File

@ -3,50 +3,50 @@
"Add": "Add", "Add": "Add",
"Add Credential": "Add Credential", "Add Credential": "Add Credential",
"Added": "Added {{when, datetime}}", "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?", "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", "Attestation Type": "Attestation Type",
"Authenticator GUID": "Authenticator GUID", "Authenticator GUID": "Authenticator GUID",
"Cancel": "Cancel", "Cancel": "Cancel",
"Click to add a Webauthn credential to your account": "Click to add a Webauthn credential to your account", "Click to add a WebAuthn credential to your account": "Click to add a WebAuthn credential to your account",
"Click to copy the": "Click to copy the", "Click to copy the": "Click to copy the",
"Clone Warning": "Clone Warning", "Clone Warning": "Clone Warning",
"Created": "Created", "Created": "Created",
"Delete": "Delete", "Delete": "Delete",
"Details": "Details", "Details": "Details",
"Display extended information for this Webauthn credential": "Display extended information for this Webauthn credential", "Display extended information for this WebAuthn credential": "Display extended information for this WebAuthn credential",
"Edit": "Edit", "Edit": "Edit",
"Edit information for this Webauthn credential": "Edit information for this Webauthn credential", "Edit information for this WebAuthn credential": "Edit information for this WebAuthn credential",
"Edit Webauthn Credential": "Edit Webauthn Credential", "Edit WebAuthn Credential": "Edit WebAuthn Credential",
"Enabled": "Enabled", "Enabled": "Enabled",
"Enter a new name for this Webauthn credential": "Enter a new name for this Webauthn credential:", "Enter a new name for this WebAuthn credential": "Enter a new name for this WebAuthn credential:",
"Enter a description for this credential": "Enter a description for this credential", "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}}", "Extended WebAuthn credential information for security key": "Extended WebAuthn credential information for security key {{description}}",
"Identifier": "Identifier", "Identifier": "Identifier",
"Last Used": "Last Used {{when, datetime}}", "Last Used": "Last Used {{when, datetime}}",
"Manage your security keys": "Manage your security keys", "Manage your security keys": "Manage your security keys",
"Name": "Name", "Name": "Name",
"No": "No", "No": "No",
"No Registered Webauthn Credentials": "No Registered Webauthn Credentials", "No Registered WebAuthn Credentials": "No Registered WebAuthn Credentials",
"Overview": "Overview", "Overview": "Overview",
"Provide the details for the new security key": "Provide the details for the new security key", "Provide the details for the new security key": "Provide the details for the new security key",
"Register Webauthn Credential": "Register Webauthn Credential", "Register WebAuthn Credential": "Register WebAuthn Credential",
"Relying Party ID": "Relying Party ID", "Relying Party ID": "Relying Party ID",
"Remove": "Remove", "Remove": "Remove",
"Remove this Webauthn credential": "Remove this Webauthn credential", "Remove this WebAuthn credential": "Remove this WebAuthn credential",
"Remove Webauthn Credential": "Remove Webauthn Credential", "Remove WebAuthn Credential": "Remove WebAuthn Credential",
"Settings": "Settings", "Settings": "Settings",
"Successfully deleted the Webauthn credential": "Successfully deleted the Webauthn credential", "Successfully deleted the WebAuthn credential": "Successfully deleted the WebAuthn credential",
"Successfully updated the Webauthn credential": "Successfully updated 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 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", "There was a problem updating the WebAuthn credential": "There was a problem updating the WebAuthn credential",
"Transports": "Transports", "Transports": "Transports",
"Two-Factor Authentication": "Two-Factor Authentication", "Two-Factor Authentication": "Two-Factor Authentication",
"Usage Count": "Usage Count", "Usage Count": "Usage Count",
"Webauthn Credential Details": "Webauthn Credential Details", "WebAuthn Credential Details": "WebAuthn Credential Details",
"Webauthn Credentials": "Webauthn Credentials", "WebAuthn Credentials": "WebAuthn Credentials",
"Yes": "Yes", "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 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 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 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" "You must be elevated to update WebAuthn credentials": "You must be elevated to update WebAuthn credentials"
} }

View File

@ -12,7 +12,7 @@ interface Props {
timeout: number; timeout: number;
} }
export default function WebauthnRegisterIcon(props: Props) { export default function WebAuthnRegisterIcon(props: Props) {
const theme = useTheme(); const theme = useTheme();
const [timerPercent, triggerTimer] = useTimer(props.timeout); const [timerPercent, triggerTimer] = useTimer(props.timeout);

View File

@ -7,15 +7,15 @@ import FailureIcon from "@components/FailureIcon";
import FingerTouchIcon from "@components/FingerTouchIcon"; import FingerTouchIcon from "@components/FingerTouchIcon";
import LinearProgressBar from "@components/LinearProgressBar"; import LinearProgressBar from "@components/LinearProgressBar";
import { useTimer } from "@hooks/Timer"; import { useTimer } from "@hooks/Timer";
import { WebauthnTouchState } from "@models/Webauthn"; import { WebAuthnTouchState } from "@models/WebAuthn";
import IconWithContext from "@views/LoginPortal/SecondFactor/IconWithContext"; import IconWithContext from "@views/LoginPortal/SecondFactor/IconWithContext";
interface Props { interface Props {
onRetryClick: () => void; onRetryClick: () => void;
webauthnTouchState: WebauthnTouchState; webauthnTouchState: WebAuthnTouchState;
} }
export default function WebauthnTryIcon(props: Props) { export default function WebAuthnTryIcon(props: Props) {
const touchTimeout = 30; const touchTimeout = 30;
const theme = useTheme(); const theme = useTheme();
const [timerPercent, triggerTimer, clearTimer] = useTimer(touchTimeout * 1000 - 500); const [timerPercent, triggerTimer, clearTimer] = useTimer(touchTimeout * 1000 - 500);
@ -42,7 +42,7 @@ export default function WebauthnTryIcon(props: Props) {
const touch = ( const touch = (
<IconWithContext <IconWithContext
icon={<FingerTouchIcon size={64} animated strong />} icon={<FingerTouchIcon size={64} animated strong />}
className={props.webauthnTouchState === WebauthnTouchState.WaitTouch ? undefined : "hidden"} className={props.webauthnTouchState === WebAuthnTouchState.WaitTouch ? undefined : "hidden"}
> >
<LinearProgressBar value={timerPercent} className={styles.progressBar} height={theme.spacing(2)} /> <LinearProgressBar value={timerPercent} className={styles.progressBar} height={theme.spacing(2)} />
</IconWithContext> </IconWithContext>
@ -51,7 +51,7 @@ export default function WebauthnTryIcon(props: Props) {
const failure = ( const failure = (
<IconWithContext <IconWithContext
icon={<FailureIcon />} icon={<FailureIcon />}
className={props.webauthnTouchState === WebauthnTouchState.Failure ? undefined : "hidden"} className={props.webauthnTouchState === WebAuthnTouchState.Failure ? undefined : "hidden"}
> >
<Button color="secondary" onClick={handleRetryClick}> <Button color="secondary" onClick={handleRetryClick}>
Retry Retry

View File

@ -3,7 +3,7 @@ export const AuthenticatedRoute: string = "/authenticated";
export const ConsentRoute: string = "/consent"; export const ConsentRoute: string = "/consent";
export const SecondFactorRoute: string = "/2fa"; export const SecondFactorRoute: string = "/2fa";
export const SecondFactorWebauthnSubRoute: string = "/webauthn"; export const SecondFactorWebAuthnSubRoute: string = "/webauthn";
export const SecondFactorTOTPSubRoute: string = "/one-time-password"; export const SecondFactorTOTPSubRoute: string = "/one-time-password";
export const SecondFactorPushSubRoute: string = "/push-notification"; export const SecondFactorPushSubRoute: string = "/push-notification";

View File

@ -1,5 +1,5 @@
export enum SecondFactorMethod { export enum SecondFactorMethod {
TOTP = 1, TOTP = 1,
Webauthn, WebAuthn,
MobilePush, MobilePush,
} }

View File

@ -32,7 +32,7 @@ export enum AttestationResult {
FailureSyntax, FailureSyntax,
FailureSupport, FailureSupport,
FailureUnknown, FailureUnknown,
FailureWebauthnNotSupported, FailureWebAuthnNotSupported,
FailureToken, FailureToken,
} }
@ -49,7 +49,7 @@ export enum AssertionResult {
FailureSyntax, FailureSyntax,
FailureUnknown, FailureUnknown,
FailureUnknownSecurity, FailureUnknownSecurity,
FailureWebauthnNotSupported, FailureWebAuthnNotSupported,
FailureChallenge, FailureChallenge,
FailureUnrecognized, FailureUnrecognized,
} }
@ -64,7 +64,7 @@ export function AssertionResultFailureString(result: AssertionResult) {
return "The server responded with an invalid Facet ID for the URL."; return "The server responded with an invalid Facet ID for the URL.";
case AssertionResult.FailureSyntax: case AssertionResult.FailureSyntax:
return "The assertion challenge was rejected as malformed or incompatible by your browser."; return "The assertion challenge was rejected as malformed or incompatible by your browser.";
case AssertionResult.FailureWebauthnNotSupported: case AssertionResult.FailureWebAuthnNotSupported:
return "Your browser does not support the WebAuthN protocol."; return "Your browser does not support the WebAuthN protocol.";
case AssertionResult.FailureUnrecognized: case AssertionResult.FailureUnrecognized:
return "This device is not registered."; return "This device is not registered.";
@ -85,7 +85,7 @@ export function AttestationResultFailureString(result: AttestationResult) {
return "Your browser does not appear to support the configuration."; return "Your browser does not appear to support the configuration.";
case AttestationResult.FailureSyntax: case AttestationResult.FailureSyntax:
return "The attestation challenge was rejected as malformed or incompatible by your browser."; return "The attestation challenge was rejected as malformed or incompatible by your browser.";
case AttestationResult.FailureWebauthnNotSupported: case AttestationResult.FailureWebAuthnNotSupported:
return "Your browser does not support the WebAuthN protocol."; return "Your browser does not support the WebAuthN protocol.";
case AttestationResult.FailureUserConsent: case AttestationResult.FailureUserConsent:
return "You cancelled the attestation request."; return "You cancelled the attestation request.";
@ -105,7 +105,7 @@ export interface AuthenticationResult {
result: AssertionResult; result: AssertionResult;
} }
export interface WebauthnDevice { export interface WebAuthnDevice {
id: string; id: string;
created_at: string; created_at: string;
last_used_at?: string; last_used_at?: string;
@ -140,7 +140,7 @@ export function toTransportName(transport: string) {
} }
} }
export enum WebauthnTouchState { export enum WebAuthnTouchState {
WaitTouch = 1, WaitTouch = 1,
InProgress = 2, InProgress = 2,
Failure = 3, Failure = 3,

View File

@ -22,7 +22,7 @@ export function toEnum(method: Method2FA): SecondFactorMethod {
case "totp": case "totp":
return SecondFactorMethod.TOTP; return SecondFactorMethod.TOTP;
case "webauthn": case "webauthn":
return SecondFactorMethod.Webauthn; return SecondFactorMethod.WebAuthn;
case "mobile_push": case "mobile_push":
return SecondFactorMethod.MobilePush; return SecondFactorMethod.MobilePush;
} }
@ -32,7 +32,7 @@ export function toString(method: SecondFactorMethod): Method2FA {
switch (method) { switch (method) {
case SecondFactorMethod.TOTP: case SecondFactorMethod.TOTP:
return "totp"; return "totp";
case SecondFactorMethod.Webauthn: case SecondFactorMethod.WebAuthn:
return "webauthn"; return "webauthn";
case SecondFactorMethod.MobilePush: case SecondFactorMethod.MobilePush:
return "mobile_push"; return "mobile_push";

View File

@ -0,0 +1,8 @@
import { WebAuthnDevice } from "@models/WebAuthn";
import { WebAuthnDevicesPath } from "@services/Api";
import { GetWithOptionalData } from "@services/Client";
// getWebAuthnDevices returns the list of webauthn devices for the authenticated user.
export async function getWebAuthnDevices(): Promise<WebAuthnDevice[] | null> {
return GetWithOptionalData<WebAuthnDevice[] | null>(WebAuthnDevicesPath);
}

View File

@ -1,8 +0,0 @@
import { WebauthnDevice } from "@models/Webauthn";
import { WebAuthnDevicesPath } from "@services/Api";
import { GetWithOptionalData } from "@services/Client";
// getWebauthnDevices returns the list of webauthn devices for the authenticated user.
export async function getWebauthnDevices(): Promise<WebauthnDevice[] | null> {
return GetWithOptionalData<WebauthnDevice[] | null>(WebAuthnDevicesPath);
}

View File

@ -16,7 +16,7 @@ import {
PublicKeyCredentialCreationOptionsStatus, PublicKeyCredentialCreationOptionsStatus,
PublicKeyCredentialRequestOptionsStatus, PublicKeyCredentialRequestOptionsStatus,
RegistrationResult, RegistrationResult,
} from "@models/Webauthn"; } from "@models/WebAuthn";
import { import {
AuthenticationOKResponse, AuthenticationOKResponse,
OptionalDataServiceResponse, OptionalDataServiceResponse,
@ -28,7 +28,7 @@ import {
} from "@services/Api"; } from "@services/Api";
import { SignInResponse } from "@services/SignIn"; import { SignInResponse } from "@services/SignIn";
export function isWebauthnSecure(): boolean { export function isWebAuthnSecure(): boolean {
if (window.isSecureContext) { if (window.isSecureContext) {
return true; return true;
} }
@ -36,12 +36,12 @@ export function isWebauthnSecure(): boolean {
return window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1"; return window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
} }
export function isWebauthnSupported(): boolean { export function isWebAuthnSupported(): boolean {
return window?.PublicKeyCredential !== undefined && typeof window.PublicKeyCredential === "function"; return window?.PublicKeyCredential !== undefined && typeof window.PublicKeyCredential === "function";
} }
export async function isWebauthnPlatformAuthenticatorAvailable(): Promise<boolean> { export async function isWebAuthnPlatformAuthenticatorAvailable(): Promise<boolean> {
if (!isWebauthnSupported()) { if (!isWebAuthnSupported()) {
return false; return false;
} }
@ -148,7 +148,7 @@ export async function getAuthenticationOptions(): Promise<PublicKeyCredentialReq
}; };
} }
export async function startWebauthnRegistration(options: PublicKeyCredentialCreationOptionsJSON) { export async function startWebAuthnRegistration(options: PublicKeyCredentialCreationOptionsJSON) {
const result: RegistrationResult = { const result: RegistrationResult = {
result: AttestationResult.Failure, result: AttestationResult.Failure,
}; };

View File

@ -8,7 +8,7 @@ import {
SecondFactorPushSubRoute, SecondFactorPushSubRoute,
SecondFactorRoute, SecondFactorRoute,
SecondFactorTOTPSubRoute, SecondFactorTOTPSubRoute,
SecondFactorWebauthnSubRoute, SecondFactorWebAuthnSubRoute,
} from "@constants/Routes"; } from "@constants/Routes";
import { RedirectionURL } from "@constants/SearchParams"; import { RedirectionURL } from "@constants/SearchParams";
import { useConfiguration } from "@hooks/Configuration"; import { useConfiguration } from "@hooks/Configuration";
@ -127,8 +127,8 @@ const LoginPortal = function (props: Props) {
if (configuration.available_methods.size === 0) { if (configuration.available_methods.size === 0) {
navigate(AuthenticatedRoute, false); navigate(AuthenticatedRoute, false);
} else { } else {
if (userInfo.method === SecondFactorMethod.Webauthn) { if (userInfo.method === SecondFactorMethod.WebAuthn) {
navigate(`${SecondFactorRoute}${SecondFactorWebauthnSubRoute}`); navigate(`${SecondFactorRoute}${SecondFactorWebAuthnSubRoute}`);
} else if (userInfo.method === SecondFactorMethod.MobilePush) { } else if (userInfo.method === SecondFactorMethod.MobilePush) {
navigate(`${SecondFactorRoute}${SecondFactorPushSubRoute}`); navigate(`${SecondFactorRoute}${SecondFactorPushSubRoute}`);
} else { } else {

View File

@ -39,12 +39,12 @@ const MethodSelectionDialog = function (props: Props) {
onClick={() => props.onClick(SecondFactorMethod.TOTP)} onClick={() => props.onClick(SecondFactorMethod.TOTP)}
/> />
) : null} ) : null}
{props.methods.has(SecondFactorMethod.Webauthn) && props.webauthnSupported ? ( {props.methods.has(SecondFactorMethod.WebAuthn) && props.webauthnSupported ? (
<MethodItem <MethodItem
id="webauthn-option" id="webauthn-option"
method={translate("Security Key - WebAuthN")} method={translate("Security Key - WebAuthN")}
icon={<FingerTouchIcon size={32} />} icon={<FingerTouchIcon size={32} />}
onClick={() => props.onClick(SecondFactorMethod.Webauthn)} onClick={() => props.onClick(SecondFactorMethod.WebAuthn)}
/> />
) : null} ) : null}
{props.methods.has(SecondFactorMethod.MobilePush) ? ( {props.methods.has(SecondFactorMethod.MobilePush) ? (

View File

@ -9,7 +9,7 @@ import {
RegisterOneTimePasswordRoute, RegisterOneTimePasswordRoute,
SecondFactorPushSubRoute, SecondFactorPushSubRoute,
SecondFactorTOTPSubRoute, SecondFactorTOTPSubRoute,
SecondFactorWebauthnSubRoute, SecondFactorWebAuthnSubRoute,
SettingsRoute, SettingsRoute,
SettingsTwoFactorAuthenticationSubRoute, SettingsTwoFactorAuthenticationSubRoute,
LogoutRoute as SignOutRoute, LogoutRoute as SignOutRoute,
@ -22,11 +22,11 @@ import { UserInfo } from "@models/UserInfo";
import { initiateTOTPRegistrationProcess } from "@services/RegisterDevice"; import { initiateTOTPRegistrationProcess } from "@services/RegisterDevice";
import { AuthenticationLevel } from "@services/State"; import { AuthenticationLevel } from "@services/State";
import { setPreferred2FAMethod } from "@services/UserInfo"; import { setPreferred2FAMethod } from "@services/UserInfo";
import { isWebauthnSupported } from "@services/Webauthn"; import { isWebAuthnSupported } from "@services/WebAuthn";
import MethodSelectionDialog from "@views/LoginPortal/SecondFactor/MethodSelectionDialog"; import MethodSelectionDialog from "@views/LoginPortal/SecondFactor/MethodSelectionDialog";
import OneTimePasswordMethod from "@views/LoginPortal/SecondFactor/OneTimePasswordMethod"; import OneTimePasswordMethod from "@views/LoginPortal/SecondFactor/OneTimePasswordMethod";
import PushNotificationMethod from "@views/LoginPortal/SecondFactor/PushNotificationMethod"; import PushNotificationMethod from "@views/LoginPortal/SecondFactor/PushNotificationMethod";
import WebauthnMethod from "@views/LoginPortal/SecondFactor/WebauthnMethod"; import WebAuthnMethod from "@views/LoginPortal/SecondFactor/WebAuthnMethod";
export interface Props { export interface Props {
authenticationLevel: AuthenticationLevel; authenticationLevel: AuthenticationLevel;
@ -44,12 +44,12 @@ const SecondFactorForm = function (props: Props) {
const [methodSelectionOpen, setMethodSelectionOpen] = useState(false); const [methodSelectionOpen, setMethodSelectionOpen] = useState(false);
const { createInfoNotification, createErrorNotification } = useNotifications(); const { createInfoNotification, createErrorNotification } = useNotifications();
const [registrationInProgress, setRegistrationInProgress] = useState(false); const [registrationInProgress, setRegistrationInProgress] = useState(false);
const [webauthnSupported, setWebauthnSupported] = useState(false); const [stateWebAuthnSupported, setStateWebAuthnSupported] = useState(false);
const { t: translate } = useTranslation(); const { t: translate } = useTranslation();
useEffect(() => { useEffect(() => {
setWebauthnSupported(isWebauthnSupported()); setStateWebAuthnSupported(isWebAuthnSupported());
}, [setWebauthnSupported]); }, [setStateWebAuthnSupported]);
const initiateRegistration = (initiateRegistrationFunc: () => Promise<void>, redirectRoute: string) => { const initiateRegistration = (initiateRegistrationFunc: () => Promise<void>, redirectRoute: string) => {
return async () => { return async () => {
@ -102,7 +102,7 @@ const SecondFactorForm = function (props: Props) {
<MethodSelectionDialog <MethodSelectionDialog
open={methodSelectionOpen} open={methodSelectionOpen}
methods={props.configuration.available_methods} methods={props.configuration.available_methods}
webauthnSupported={webauthnSupported} webauthnSupported={stateWebAuthnSupported}
onClose={() => setMethodSelectionOpen(false)} onClose={() => setMethodSelectionOpen(false)}
onClick={handleMethodSelected} onClick={handleMethodSelected}
/> />
@ -139,12 +139,12 @@ const SecondFactorForm = function (props: Props) {
} }
/> />
<Route <Route
path={SecondFactorWebauthnSubRoute} path={SecondFactorWebAuthnSubRoute}
element={ element={
<WebauthnMethod <WebAuthnMethod
id="webauthn-method" id="webauthn-method"
authenticationLevel={props.authenticationLevel} authenticationLevel={props.authenticationLevel}
// Whether the user has a Webauthn device registered already // Whether the user has a WebAuthn device registered already
registered={props.userInfo.has_webauthn} registered={props.userInfo.has_webauthn}
onRegisterClick={() => { onRegisterClick={() => {
navigate(`${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`); navigate(`${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`);

View File

@ -1,13 +1,13 @@
import React, { useCallback, useEffect, useRef, useState } from "react"; import React, { useCallback, useEffect, useRef, useState } from "react";
import WebauthnTryIcon from "@components/WebauthnTryIcon"; import WebAuthnTryIcon from "@components/WebAuthnTryIcon";
import { RedirectionURL } from "@constants/SearchParams"; import { RedirectionURL } from "@constants/SearchParams";
import { useIsMountedRef } from "@hooks/Mounted"; import { useIsMountedRef } from "@hooks/Mounted";
import { useQueryParam } from "@hooks/QueryParam"; import { useQueryParam } from "@hooks/QueryParam";
import { useWorkflow } from "@hooks/Workflow"; import { useWorkflow } from "@hooks/Workflow";
import { AssertionResult, AssertionResultFailureString, WebauthnTouchState } from "@models/Webauthn"; import { AssertionResult, AssertionResultFailureString, WebAuthnTouchState } from "@models/WebAuthn";
import { AuthenticationLevel } from "@services/State"; import { AuthenticationLevel } from "@services/State";
import { getAuthenticationOptions, getAuthenticationResult, postAuthenticationResponse } from "@services/Webauthn"; import { getAuthenticationOptions, getAuthenticationResult, postAuthenticationResponse } from "@services/WebAuthn";
import MethodContainer, { State as MethodContainerState } from "@views/LoginPortal/SecondFactor/MethodContainer"; import MethodContainer, { State as MethodContainerState } from "@views/LoginPortal/SecondFactor/MethodContainer";
export interface Props { export interface Props {
@ -20,8 +20,8 @@ export interface Props {
onSignInSuccess: (redirectURL: string | undefined) => void; onSignInSuccess: (redirectURL: string | undefined) => void;
} }
const WebauthnMethod = function (props: Props) { const WebAuthnMethod = function (props: Props) {
const [state, setState] = useState(WebauthnTouchState.WaitTouch); const [state, setState] = useState(WebAuthnTouchState.WaitTouch);
const redirectionURL = useQueryParam(RedirectionURL); const redirectionURL = useQueryParam(RedirectionURL);
const [workflow, workflowID] = useWorkflow(); const [workflow, workflowID] = useWorkflow();
const mounted = useIsMountedRef(); const mounted = useIsMountedRef();
@ -37,11 +37,11 @@ const WebauthnMethod = function (props: Props) {
} }
try { try {
setState(WebauthnTouchState.WaitTouch); setState(WebAuthnTouchState.WaitTouch);
const optionsStatus = await getAuthenticationOptions(); const optionsStatus = await getAuthenticationOptions();
if (optionsStatus.status !== 200 || optionsStatus.options == null) { if (optionsStatus.status !== 200 || optionsStatus.options == null) {
setState(WebauthnTouchState.Failure); setState(WebAuthnTouchState.Failure);
onSignInErrorCallback(new Error("Failed to initiate security key sign in process")); onSignInErrorCallback(new Error("Failed to initiate security key sign in process"));
return; return;
@ -52,7 +52,7 @@ const WebauthnMethod = function (props: Props) {
if (result.result !== AssertionResult.Success) { if (result.result !== AssertionResult.Success) {
if (!mounted.current) return; if (!mounted.current) return;
setState(WebauthnTouchState.Failure); setState(WebAuthnTouchState.Failure);
onSignInErrorCallback(new Error(AssertionResultFailureString(result.result))); onSignInErrorCallback(new Error(AssertionResultFailureString(result.result)));
@ -61,14 +61,14 @@ const WebauthnMethod = function (props: Props) {
if (result.response == null) { if (result.response == null) {
onSignInErrorCallback(new Error("The browser did not respond with the expected attestation data.")); onSignInErrorCallback(new Error("The browser did not respond with the expected attestation data."));
setState(WebauthnTouchState.Failure); setState(WebAuthnTouchState.Failure);
return; return;
} }
if (!mounted.current) return; if (!mounted.current) return;
setState(WebauthnTouchState.InProgress); setState(WebAuthnTouchState.InProgress);
const response = await postAuthenticationResponse(result.response, redirectionURL, workflow, workflowID); const response = await postAuthenticationResponse(result.response, redirectionURL, workflow, workflowID);
@ -80,14 +80,14 @@ const WebauthnMethod = function (props: Props) {
if (!mounted.current) return; if (!mounted.current) return;
onSignInErrorCallback(new Error("The server rejected the security key.")); onSignInErrorCallback(new Error("The server rejected the security key."));
setState(WebauthnTouchState.Failure); setState(WebAuthnTouchState.Failure);
} catch (err) { } catch (err) {
// If the request was initiated and the user changed 2FA method in the meantime, // If the request was initiated and the user changed 2FA method in the meantime,
// the process is interrupted to avoid updating state of unmounted component. // the process is interrupted to avoid updating state of unmounted component.
if (!mounted.current) return; if (!mounted.current) return;
console.error(err); console.error(err);
onSignInErrorCallback(new Error("Failed to initiate security key sign in process")); onSignInErrorCallback(new Error("Failed to initiate security key sign in process"));
setState(WebauthnTouchState.Failure); setState(WebAuthnTouchState.Failure);
} }
}, [ }, [
onSignInErrorCallback, onSignInErrorCallback,
@ -121,9 +121,9 @@ const WebauthnMethod = function (props: Props) {
state={methodState} state={methodState}
onRegisterClick={props.onRegisterClick} onRegisterClick={props.onRegisterClick}
> >
<WebauthnTryIcon onRetryClick={doInitiateSignIn} webauthnTouchState={state} /> <WebAuthnTryIcon onRetryClick={doInitiateSignIn} webauthnTouchState={state} />
</MethodContainer> </MethodContainer>
); );
}; };
export default WebauthnMethod; export default WebAuthnMethod;

View File

@ -3,7 +3,7 @@ import React from "react";
import { Grid } from "@mui/material"; import { Grid } from "@mui/material";
import { AutheliaState } from "@services/State"; import { AutheliaState } from "@services/State";
import WebauthnDevices from "@views/Settings/TwoFactorAuthentication/WebauthnDevices"; import WebAuthnDevices from "@views/Settings/TwoFactorAuthentication/WebAuthnDevices";
interface Props { interface Props {
state: AutheliaState; state: AutheliaState;
@ -13,7 +13,7 @@ export default function TwoFactorAuthSettings(props: Props) {
return ( return (
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={12}> <Grid item xs={12}>
<WebauthnDevices state={props.state} /> <WebAuthnDevices state={props.state} />
</Grid> </Grid>
</Grid> </Grid>
); );

View File

@ -3,15 +3,15 @@ import React from "react";
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material"; import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { WebauthnDevice } from "@models/Webauthn"; import { WebAuthnDevice } from "@models/WebAuthn";
interface Props { interface Props {
open: boolean; open: boolean;
device: WebauthnDevice; device: WebAuthnDevice;
handleClose: (ok: boolean) => void; handleClose: (ok: boolean) => void;
} }
export default function WebauthnDeviceDeleteDialog(props: Props) { export default function WebAuthnDeviceDeleteDialog(props: Props) {
const { t: translate } = useTranslation("settings"); const { t: translate } = useTranslation("settings");
const handleCancel = () => { const handleCancel = () => {
@ -20,10 +20,10 @@ export default function WebauthnDeviceDeleteDialog(props: Props) {
return ( return (
<Dialog open={props.open} onClose={handleCancel}> <Dialog open={props.open} onClose={handleCancel}>
<DialogTitle>{translate("Remove Webauthn Credential")}</DialogTitle> <DialogTitle>{translate("Remove WebAuthn Credential")}</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText> <DialogContentText>
{translate("Are you sure you want to remove the Webauthn credential from from your account", { {translate("Are you sure you want to remove the WebAuthn credential from from your account", {
description: props.device.description, description: props.device.description,
})} })}
</DialogContentText> </DialogContentText>

View File

@ -16,23 +16,23 @@ import {
} from "@mui/material"; } from "@mui/material";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { WebauthnDevice, toTransportName } from "@models/Webauthn"; import { WebAuthnDevice, toTransportName } from "@models/WebAuthn";
interface Props { interface Props {
open: boolean; open: boolean;
device: WebauthnDevice; device: WebAuthnDevice;
handleClose: () => void; handleClose: () => void;
} }
export default function WebauthnDetailsDeleteDialog(props: Props) { export default function WebAuthnDetailsDeleteDialog(props: Props) {
const { t: translate } = useTranslation("settings"); const { t: translate } = useTranslation("settings");
return ( return (
<Dialog open={props.open} onClose={props.handleClose}> <Dialog open={props.open} onClose={props.handleClose}>
<DialogTitle>{translate("Webauthn Credential Details")}</DialogTitle> <DialogTitle>{translate("WebAuthn Credential Details")}</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText sx={{ mb: 3 }}> <DialogContentText sx={{ mb: 3 }}>
{translate("Extended Webauthn credential information for security key", { {translate("Extended WebAuthn credential information for security key", {
description: props.device.description, description: props.device.description,
})} })}
</DialogContentText> </DialogContentText>

View File

@ -3,15 +3,15 @@ import React, { MutableRefObject, useRef, useState } from "react";
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField } from "@mui/material"; import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField } from "@mui/material";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { WebauthnDevice } from "@models/Webauthn"; import { WebAuthnDevice } from "@models/WebAuthn";
interface Props { interface Props {
open: boolean; open: boolean;
device: WebauthnDevice; device: WebAuthnDevice;
handleClose: (ok: boolean, name: string) => void; handleClose: (ok: boolean, name: string) => void;
} }
export default function WebauthnDeviceEditDialog(props: Props) { export default function WebAuthnDeviceEditDialog(props: Props) {
const { t: translate } = useTranslation("settings"); const { t: translate } = useTranslation("settings");
const [deviceName, setName] = useState(""); const [deviceName, setName] = useState("");
@ -34,9 +34,9 @@ export default function WebauthnDeviceEditDialog(props: Props) {
return ( return (
<Dialog open={props.open} onClose={handleCancel}> <Dialog open={props.open} onClose={handleCancel}>
<DialogTitle>{translate("Edit Webauthn Credential")}</DialogTitle> <DialogTitle>{translate("Edit WebAuthn Credential")}</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText>{translate("Enter a new name for this Webauthn credential")}</DialogContentText> <DialogContentText>{translate("Enter a new name for this WebAuthn credential")}</DialogContentText>
<TextField <TextField
autoFocus autoFocus
inputRef={nameRef} inputRef={nameRef}

View File

@ -8,19 +8,19 @@ import { Box, Button, CircularProgress, Paper, Stack, Tooltip, Typography } from
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNotifications } from "@hooks/NotificationsContext"; import { useNotifications } from "@hooks/NotificationsContext";
import { WebauthnDevice } from "@models/Webauthn"; import { WebAuthnDevice } from "@models/WebAuthn";
import { deleteDevice, updateDevice } from "@services/Webauthn"; import { deleteDevice, updateDevice } from "@services/WebAuthn";
import WebauthnDeviceDeleteDialog from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceDeleteDialog"; import WebAuthnDeviceDeleteDialog from "@views/Settings/TwoFactorAuthentication/WebAuthnDeviceDeleteDialog";
import WebauthnDeviceDetailsDialog from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceDetailsDialog"; import WebAuthnDeviceDetailsDialog from "@views/Settings/TwoFactorAuthentication/WebAuthnDeviceDetailsDialog";
import WebauthnDeviceEditDialog from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceEditDialog"; import WebAuthnDeviceEditDialog from "@views/Settings/TwoFactorAuthentication/WebAuthnDeviceEditDialog";
interface Props { interface Props {
index: number; index: number;
device: WebauthnDevice; device: WebAuthnDevice;
handleEdit: () => void; handleEdit: () => void;
} }
export default function WebauthnDeviceItem(props: Props) { export default function WebAuthnDeviceItem(props: Props) {
const { t: translate } = useTranslation("settings"); const { t: translate } = useTranslation("settings");
const { createSuccessNotification, createErrorNotification } = useNotifications(); const { createSuccessNotification, createErrorNotification } = useNotifications();
@ -47,19 +47,19 @@ export default function WebauthnDeviceItem(props: Props) {
if (response.data.status === "KO") { if (response.data.status === "KO") {
if (response.data.elevation) { if (response.data.elevation) {
createErrorNotification(translate("You must be elevated to update Webauthn credentials")); createErrorNotification(translate("You must be elevated to update WebAuthn credentials"));
} else if (response.data.authentication) { } else if (response.data.authentication) {
createErrorNotification( createErrorNotification(
translate("You must have a higher authentication level to update Webauthn credentials"), translate("You must have a higher authentication level to update WebAuthn credentials"),
); );
} else { } else {
createErrorNotification(translate("There was a problem updating the Webauthn credential")); createErrorNotification(translate("There was a problem updating the WebAuthn credential"));
} }
return; return;
} }
createSuccessNotification(translate("Successfully updated the Webauthn credential")); createSuccessNotification(translate("Successfully updated the WebAuthn credential"));
props.handleEdit(); props.handleEdit();
}; };
@ -79,19 +79,19 @@ export default function WebauthnDeviceItem(props: Props) {
if (response.data.status === "KO") { if (response.data.status === "KO") {
if (response.data.elevation) { if (response.data.elevation) {
createErrorNotification(translate("You must be elevated to delete Webauthn credentials")); createErrorNotification(translate("You must be elevated to delete WebAuthn credentials"));
} else if (response.data.authentication) { } else if (response.data.authentication) {
createErrorNotification( createErrorNotification(
translate("You must have a higher authentication level to delete Webauthn credentials"), translate("You must have a higher authentication level to delete WebAuthn credentials"),
); );
} else { } else {
createErrorNotification(translate("There was a problem deleting the Webauthn credential")); createErrorNotification(translate("There was a problem deleting the WebAuthn credential"));
} }
return; return;
} }
createSuccessNotification(translate("Successfully deleted the Webauthn credential")); createSuccessNotification(translate("Successfully deleted the WebAuthn credential"));
props.handleEdit(); props.handleEdit();
}; };
@ -100,15 +100,15 @@ export default function WebauthnDeviceItem(props: Props) {
<Fragment> <Fragment>
<Paper variant="outlined"> <Paper variant="outlined">
<Box sx={{ p: 3 }}> <Box sx={{ p: 3 }}>
<WebauthnDeviceDetailsDialog <WebAuthnDeviceDetailsDialog
device={props.device} device={props.device}
open={showDialogDetails} open={showDialogDetails}
handleClose={() => { handleClose={() => {
setShowDialogDetails(false); setShowDialogDetails(false);
}} }}
/> />
<WebauthnDeviceEditDialog device={props.device} open={showDialogEdit} handleClose={handleEdit} /> <WebAuthnDeviceEditDialog device={props.device} open={showDialogEdit} handleClose={handleEdit} />
<WebauthnDeviceDeleteDialog <WebAuthnDeviceDeleteDialog
device={props.device} device={props.device}
open={showDialogDelete} open={showDialogDelete}
handleClose={handleDelete} handleClose={handleDelete}
@ -157,7 +157,7 @@ export default function WebauthnDeviceItem(props: Props) {
</Typography> </Typography>
</Stack> </Stack>
<Tooltip title={translate("Display extended information for this Webauthn credential")}> <Tooltip title={translate("Display extended information for this WebAuthn credential")}>
<Button <Button
variant="outlined" variant="outlined"
color="primary" color="primary"
@ -167,7 +167,7 @@ export default function WebauthnDeviceItem(props: Props) {
{translate("Info")} {translate("Info")}
</Button> </Button>
</Tooltip> </Tooltip>
<Tooltip title={translate("Edit information for this Webauthn credential")}> <Tooltip title={translate("Edit information for this WebAuthn credential")}>
<Button <Button
variant="outlined" variant="outlined"
color="primary" color="primary"
@ -177,7 +177,7 @@ export default function WebauthnDeviceItem(props: Props) {
{translate("Edit")} {translate("Edit")}
</Button> </Button>
</Tooltip> </Tooltip>
<Tooltip title={translate("Remove this Webauthn credential")}> <Tooltip title={translate("Remove this WebAuthn credential")}>
<Button <Button
variant="outlined" variant="outlined"
color="primary" color="primary"

View File

@ -21,10 +21,10 @@ import { PublicKeyCredentialCreationOptionsJSON } from "@simplewebauthn/typescri
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import InformationIcon from "@components/InformationIcon"; import InformationIcon from "@components/InformationIcon";
import WebauthnRegisterIcon from "@components/WebauthnRegisterIcon"; import WebAuthnRegisterIcon from "@components/WebAuthnRegisterIcon";
import { useNotifications } from "@hooks/NotificationsContext"; import { useNotifications } from "@hooks/NotificationsContext";
import { AttestationResult, AttestationResultFailureString, WebauthnTouchState } from "@models/Webauthn"; import { AttestationResult, AttestationResultFailureString, WebAuthnTouchState } from "@models/WebAuthn";
import { finishRegistration, getAttestationCreationOptions, startWebauthnRegistration } from "@services/Webauthn"; import { finishRegistration, getAttestationCreationOptions, startWebAuthnRegistration } from "@services/WebAuthn";
const steps = ["Description", "Verification"]; const steps = ["Description", "Verification"];
@ -34,13 +34,13 @@ interface Props {
setCancelled: () => void; setCancelled: () => void;
} }
const WebauthnDeviceRegisterDialog = function (props: Props) { const WebAuthnDeviceRegisterDialog = function (props: Props) {
const { t: translate } = useTranslation("settings"); const { t: translate } = useTranslation("settings");
const styles = useStyles(); const styles = useStyles();
const { createErrorNotification } = useNotifications(); const { createErrorNotification } = useNotifications();
const [state, setState] = useState(WebauthnTouchState.WaitTouch); const [state, setState] = useState(WebAuthnTouchState.WaitTouch);
const [activeStep, setActiveStep] = useState(0); const [activeStep, setActiveStep] = useState(0);
const [options, setOptions] = useState<PublicKeyCredentialCreationOptionsJSON | null>(null); const [options, setOptions] = useState<PublicKeyCredentialCreationOptionsJSON | null>(null);
const [timeout, setTimeout] = useState<number | null>(null); const [timeout, setTimeout] = useState<number | null>(null);
@ -50,7 +50,7 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
const nameRef = useRef() as MutableRefObject<HTMLInputElement>; const nameRef = useRef() as MutableRefObject<HTMLInputElement>;
const resetStates = () => { const resetStates = () => {
setState(WebauthnTouchState.WaitTouch); setState(WebAuthnTouchState.WaitTouch);
setOptions(null); setOptions(null);
setActiveStep(0); setActiveStep(0);
setTimeout(null); setTimeout(null);
@ -73,9 +73,9 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
setActiveStep(1); setActiveStep(1);
try { try {
setState(WebauthnTouchState.WaitTouch); setState(WebAuthnTouchState.WaitTouch);
const resultCredentialCreation = await startWebauthnRegistration(options); const resultCredentialCreation = await startWebAuthnRegistration(options);
setTimeout(null); setTimeout(null);
@ -99,7 +99,7 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
} }
createErrorNotification(AttestationResultFailureString(resultCredentialCreation.result)); createErrorNotification(AttestationResultFailureString(resultCredentialCreation.result));
setState(WebauthnTouchState.Failure); setState(WebAuthnTouchState.Failure);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
createErrorNotification( createErrorNotification(
@ -109,7 +109,7 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
}, [options, createErrorNotification, handleClose]); }, [options, createErrorNotification, handleClose]);
useEffect(() => { useEffect(() => {
if (state !== WebauthnTouchState.Failure || activeStep !== 0 || !props.open) { if (state !== WebAuthnTouchState.Failure || activeStep !== 0 || !props.open) {
return; return;
} }
@ -151,12 +151,12 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
break; break;
case 409: case 409:
setErrorDescription(true); setErrorDescription(true);
createErrorNotification(translate("A Webauthn Credential with that Description already exists.")); createErrorNotification(translate("A WebAuthn Credential with that Description already exists."));
break; break;
default: default:
createErrorNotification( createErrorNotification(
translate("Error occurred obtaining the Webauthn Credential creation options."), translate("Error occurred obtaining the WebAuthn Credential creation options."),
); );
} }
}, [createErrorNotification, credentialDescription, translate]); }, [createErrorNotification, credentialDescription, translate]);
@ -214,7 +214,7 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
return ( return (
<Fragment> <Fragment>
<Box className={styles.icon}> <Box className={styles.icon}>
{timeout !== null ? <WebauthnRegisterIcon timeout={timeout} /> : null} {timeout !== null ? <WebAuthnRegisterIcon timeout={timeout} /> : null}
</Box> </Box>
<Typography className={styles.instruction}> <Typography className={styles.instruction}>
{translate("Touch the token on your security key")} {translate("Touch the token on your security key")}
@ -234,11 +234,11 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
return ( return (
<Dialog open={props.open} onClose={handleOnClose} maxWidth={"xs"} fullWidth={true}> <Dialog open={props.open} onClose={handleOnClose} maxWidth={"xs"} fullWidth={true}>
<DialogTitle>{translate("Register Webauthn Credential")}</DialogTitle> <DialogTitle>{translate("Register WebAuthn Credential")}</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText sx={{ mb: 3 }}> <DialogContentText sx={{ mb: 3 }}>
{translate( {translate(
"This page allows registration of a new Security Key backed by modern Webauthn Credential technology.", "This page allows registration of a new Security Key backed by modern WebAuthn Credential technology.",
)} )}
</DialogContentText> </DialogContentText>
<Grid container spacing={0} alignItems={"center"} justifyContent={"center"} textAlign={"center"}> <Grid container spacing={0} alignItems={"center"} justifyContent={"center"} textAlign={"center"}>
@ -264,8 +264,8 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button <Button
color={activeStep === 1 && state !== WebauthnTouchState.Failure ? "primary" : "error"} color={activeStep === 1 && state !== WebAuthnTouchState.Failure ? "primary" : "error"}
disabled={activeStep === 1 && state !== WebauthnTouchState.Failure} disabled={activeStep === 1 && state !== WebAuthnTouchState.Failure}
onClick={handleClose} onClick={handleClose}
> >
{translate("Cancel")} {translate("Cancel")}
@ -286,7 +286,7 @@ const WebauthnDeviceRegisterDialog = function (props: Props) {
); );
}; };
export default WebauthnDeviceRegisterDialog; export default WebAuthnDeviceRegisterDialog;
const useStyles = makeStyles((theme: Theme) => ({ const useStyles = makeStyles((theme: Theme) => ({
icon: { icon: {

View File

@ -5,17 +5,17 @@ import { useTranslation } from "react-i18next";
import { AutheliaState } from "@services/State"; import { AutheliaState } from "@services/State";
import LoadingPage from "@views/LoadingPage/LoadingPage"; import LoadingPage from "@views/LoadingPage/LoadingPage";
import WebauthnDeviceRegisterDialog from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceRegisterDialog"; import WebAuthnDeviceRegisterDialog from "@views/Settings/TwoFactorAuthentication/WebAuthnDeviceRegisterDialog";
import WebauthnDevicesStack from "@views/Settings/TwoFactorAuthentication/WebauthnDevicesStack"; import WebAuthnDevicesStack from "@views/Settings/TwoFactorAuthentication/WebAuthnDevicesStack";
interface Props { interface Props {
state: AutheliaState; state: AutheliaState;
} }
export default function WebauthnDevices(props: Props) { export default function WebAuthnDevices(props: Props) {
const { t: translate } = useTranslation("settings"); const { t: translate } = useTranslation("settings");
const [showWebauthnDeviceRegisterDialog, setShowWebauthnDeviceRegisterDialog] = useState<boolean>(false); const [showWebAuthnDeviceRegisterDialog, setShowWebAuthnDeviceRegisterDialog] = useState<boolean>(false);
const [refreshState, setRefreshState] = useState<number>(0); const [refreshState, setRefreshState] = useState<number>(0);
const handleIncrementRefreshState = () => { const handleIncrementRefreshState = () => {
@ -24,13 +24,13 @@ export default function WebauthnDevices(props: Props) {
return ( return (
<Fragment> <Fragment>
<WebauthnDeviceRegisterDialog <WebAuthnDeviceRegisterDialog
open={showWebauthnDeviceRegisterDialog} open={showWebAuthnDeviceRegisterDialog}
onClose={() => { onClose={() => {
handleIncrementRefreshState(); handleIncrementRefreshState();
}} }}
setCancelled={() => { setCancelled={() => {
setShowWebauthnDeviceRegisterDialog(false); setShowWebAuthnDeviceRegisterDialog(false);
handleIncrementRefreshState(); handleIncrementRefreshState();
}} }}
/> />
@ -38,15 +38,15 @@ export default function WebauthnDevices(props: Props) {
<Box sx={{ p: 3 }}> <Box sx={{ p: 3 }}>
<Stack spacing={2}> <Stack spacing={2}>
<Box> <Box>
<Typography variant="h5">{translate("Webauthn Credentials")}</Typography> <Typography variant="h5">{translate("WebAuthn Credentials")}</Typography>
</Box> </Box>
<Box> <Box>
<Tooltip title={translate("Click to add a Webauthn credential to your account")}> <Tooltip title={translate("Click to add a WebAuthn credential to your account")}>
<Button <Button
variant="outlined" variant="outlined"
color="primary" color="primary"
onClick={() => { onClick={() => {
setShowWebauthnDeviceRegisterDialog(true); setShowWebAuthnDeviceRegisterDialog(true);
}} }}
> >
{translate("Add Credential")} {translate("Add Credential")}
@ -54,7 +54,7 @@ export default function WebauthnDevices(props: Props) {
</Tooltip> </Tooltip>
</Box> </Box>
<Suspense fallback={<LoadingPage />}> <Suspense fallback={<LoadingPage />}>
<WebauthnDevicesStack <WebAuthnDevicesStack
refreshState={refreshState} refreshState={refreshState}
incrementRefreshState={handleIncrementRefreshState} incrementRefreshState={handleIncrementRefreshState}
/> />

View File

@ -3,24 +3,24 @@ import React, { Fragment, useEffect, useState } from "react";
import { Stack, Typography } from "@mui/material"; import { Stack, Typography } from "@mui/material";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { WebauthnDevice } from "@models/Webauthn"; import { WebAuthnDevice } from "@models/WebAuthn";
import { getWebauthnDevices } from "@services/UserWebauthnDevices"; import { getWebAuthnDevices } from "@services/UserWebAuthnDevices";
import WebauthnDeviceItem from "@views/Settings/TwoFactorAuthentication/WebauthnDeviceItem"; import WebAuthnDeviceItem from "@views/Settings/TwoFactorAuthentication/WebAuthnDeviceItem";
interface Props { interface Props {
refreshState: number; refreshState: number;
incrementRefreshState: () => void; incrementRefreshState: () => void;
} }
export default function WebauthnDevicesStack(props: Props) { export default function WebAuthnDevicesStack(props: Props) {
const { t: translate } = useTranslation("settings"); const { t: translate } = useTranslation("settings");
const [devices, setDevices] = useState<WebauthnDevice[] | null>(null); const [devices, setDevices] = useState<WebAuthnDevice[] | null>(null);
useEffect(() => { useEffect(() => {
(async function () { (async function () {
setDevices(null); setDevices(null);
const devices = await getWebauthnDevices(); const devices = await getWebAuthnDevices();
setDevices(devices); setDevices(devices);
})(); })();
}, [props.refreshState]); }, [props.refreshState]);
@ -30,11 +30,11 @@ export default function WebauthnDevicesStack(props: Props) {
{devices !== null && devices.length !== 0 ? ( {devices !== null && devices.length !== 0 ? (
<Stack spacing={3}> <Stack spacing={3}>
{devices.map((x, idx) => ( {devices.map((x, idx) => (
<WebauthnDeviceItem key={idx} index={idx} device={x} handleEdit={props.incrementRefreshState} /> <WebAuthnDeviceItem key={idx} index={idx} device={x} handleEdit={props.incrementRefreshState} />
))} ))}
</Stack> </Stack>
) : ( ) : (
<Typography>{translate("No Registered Webauthn Credentials")}</Typography> <Typography>{translate("No Registered WebAuthn Credentials")}</Typography>
)} )}
</Fragment> </Fragment>
); );