feat: translate all the things
parent
7e56cf2d15
commit
515309c10e
|
@ -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'"
|
||||
)
|
||||
|
|
|
@ -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</0> before using",
|
||||
"You're being signed out and redirected": "You're being signed out and redirected",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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: '*'
|
||||
|
|
|
@ -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: Props) => {
|
|||
<Routes>
|
||||
<Route path={ResetPasswordStep1Route} element={<ResetPasswordStep1 />} />
|
||||
<Route path={ResetPasswordStep2Route} element={<ResetPasswordStep2 />} />
|
||||
<Route path={RegisterWebauthnRoute} element={<RegisterWebauthn />} />
|
||||
<Route path={RegisterOneTimePasswordRoute} element={<RegisterOneTimePassword />} />
|
||||
<Route path={LogoutRoute} element={<SignOut />} />
|
||||
<Route path={ConsentRoute} element={<ConsentView />} />
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const password = props.value;
|
||||
|
@ -114,7 +114,7 @@ const PasswordMeter = function (props: Props) {
|
|||
|
||||
return (
|
||||
<Box className={styles.progressContainer}>
|
||||
<Box title={feedback} className={classnames(styles.progressBar)} />
|
||||
<Box title={feedback === null ? "" : feedback} className={classnames(styles.progressBar)} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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<boolean>("privacy-policy-accepted", false);
|
||||
const { t: translate } = useTranslation();
|
||||
|
||||
return privacyEnabled && privacyRequireAccept && !accepted ? (
|
||||
<Drawer {...props} anchor="bottom" open={!accepted}>
|
||||
|
|
|
@ -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 (
|
||||
<Fragment>
|
||||
<Link {...props} href={hrefPrivacyPolicy} target="_blank" rel="noopener" underline="hover">
|
||||
|
|
|
@ -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 (
|
||||
<Box className={styles.icon} sx={{ minHeight: 101 }}>
|
||||
<IconWithContext icon={<FingerTouchIcon size={64} animated strong />}>
|
||||
<LinearProgressBar value={timerPercent} className={styles.progressBar} height={theme.spacing(2)} />
|
||||
</IconWithContext>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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() ? (
|
||||
<img src="./static/media/logo.png" alt="Logo" className={styles.icon} />
|
||||
|
@ -82,7 +83,7 @@ const LoginLayout = function (props: Props) {
|
|||
<TypographyWithTooltip
|
||||
variant={"h5"}
|
||||
value={props.title}
|
||||
tooltip={props.titleTooltip}
|
||||
tooltip={props.titleTooltip !== null ? props.titleTooltip : undefined}
|
||||
/>
|
||||
</Grid>
|
||||
) : null}
|
||||
|
@ -91,7 +92,7 @@ const LoginLayout = function (props: Props) {
|
|||
<TypographyWithTooltip
|
||||
variant={"h6"}
|
||||
value={props.subtitle}
|
||||
tooltip={props.subtitleTooltip}
|
||||
tooltip={props.subtitleTooltip !== null ? props.subtitleTooltip : undefined}
|
||||
/>
|
||||
</Grid>
|
||||
) : null}
|
||||
|
|
|
@ -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")}
|
||||
</Button>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
@ -114,7 +115,7 @@ const SettingsMenuItem = function (props: SettingsMenuItemProps) {
|
|||
const navigate = useRouterNavigate();
|
||||
|
||||
return (
|
||||
<ListItem disablePadding onClick={selected ? () => console.log("selected") : () => navigate(props.pathname)}>
|
||||
<ListItem disablePadding onClick={!selected ? () => navigate(props.pathname) : undefined}>
|
||||
<ListItemButton selected={selected}>
|
||||
<ListItemIcon>{props.icon}</ListItemIcon>
|
||||
<ListItemText primary={props.text} />
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -6,6 +6,7 @@ import BaseLoadingPage from "@views/LoadingPage/BaseLoadingPage";
|
|||
|
||||
const LoadingPage = function () {
|
||||
const { t: translate } = useTranslation();
|
||||
|
||||
return <BaseLoadingPage message={translate("Loading")} />;
|
||||
};
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<div id="authenticated-stage">
|
||||
<div className={styles.iconContainer}>
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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<ConsentGetResponseBody | undefined>(undefined);
|
||||
const [error, setError] = useState<any>(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) {
|
|||
<div className={styles.scopesListContainer}>
|
||||
<List className={styles.scopesList}>
|
||||
{response?.scopes.map((scope: string) => (
|
||||
<Tooltip title={"Scope " + scope}>
|
||||
<Tooltip title={translate("Scope", { name: scope })}>
|
||||
<ListItem id={"scope-" + scope} dense>
|
||||
<ListItemIcon>{scopeNameToAvatar(scope)}</ListItemIcon>
|
||||
<ListItemText primary={translateScopeNameToDescription(scope)} />
|
||||
|
@ -180,10 +183,7 @@ const ConsentView = function (props: Props) {
|
|||
{response?.pre_configuration ? (
|
||||
<Grid item xs={12}>
|
||||
<Tooltip
|
||||
title={
|
||||
translate("This saves this consent as a pre-configured consent for future use") ||
|
||||
"This saves this consent as a pre-configured consent for future use"
|
||||
}
|
||||
title={translate("This saves this consent as a pre-configured consent for future use")}
|
||||
>
|
||||
<FormControlLabel
|
||||
control={
|
||||
|
|
|
@ -30,23 +30,27 @@ export interface Props {
|
|||
}
|
||||
|
||||
const FirstFactorForm = function (props: Props) {
|
||||
const styles = useStyles();
|
||||
const { t: translate } = useTranslation();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const redirectionURL = useQueryParam(RedirectionURL);
|
||||
const requestMethod = useQueryParam(RequestMethod);
|
||||
const [workflow] = useWorkflow();
|
||||
const { createErrorNotification } = useNotifications();
|
||||
|
||||
const loginChannel = useMemo(() => new BroadcastChannel<boolean>("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<HTMLInputElement>;
|
||||
const passwordRef = useRef() as MutableRefObject<HTMLInputElement>;
|
||||
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();
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
<div className={style.icon}>
|
||||
<Box className={style.icon}>
|
||||
<PushNotificationIcon width={32} height={32} />
|
||||
</div>
|
||||
<div>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography>{props.device.name}</Typography>
|
||||
</div>
|
||||
</Box>
|
||||
</Button>
|
||||
</Grid>
|
||||
);
|
||||
|
@ -172,12 +172,12 @@ function MethodItem(props: MethodItemProps) {
|
|||
variant="contained"
|
||||
onClick={props.onSelect}
|
||||
>
|
||||
<div className={style.icon}>
|
||||
<Box className={style.icon}>
|
||||
<PushNotificationIcon width={32} height={32} />
|
||||
</div>
|
||||
<div>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography>{props.method}</Typography>
|
||||
</div>
|
||||
</Box>
|
||||
</Button>
|
||||
</Grid>
|
||||
);
|
||||
|
|
|
@ -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 (
|
||||
<div className={classnames(props.className, styles.root)}>
|
||||
<div className={styles.iconContainer}>
|
||||
<div className={styles.icon}>{props.icon}</div>
|
||||
</div>
|
||||
<div className={styles.context}>{props.children}</div>
|
||||
</div>
|
||||
<Box className={classnames(props.className, styles.root)}>
|
||||
<Box className={styles.iconContainer}>
|
||||
<Box className={styles.icon}>{props.icon}</Box>
|
||||
</Box>
|
||||
<Box className={styles.context}>{props.children}</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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) {
|
|||
</div>
|
||||
{props.onSelectClick && props.registered ? (
|
||||
<Link component="button" id="selection-link" onClick={props.onSelectClick} underline="hover">
|
||||
{selectMessage}
|
||||
{translate("Select a Device")}
|
||||
</Link>
|
||||
) : null}
|
||||
{(props.onRegisterClick && props.title !== "Push Notification") ||
|
||||
|
|
|
@ -42,7 +42,7 @@ const MethodSelectionDialog = function (props: Props) {
|
|||
{props.methods.has(SecondFactorMethod.Webauthn) && props.webauthnSupported ? (
|
||||
<MethodItem
|
||||
id="webauthn-option"
|
||||
method={translate("Security Key - WebAuthN")}
|
||||
method={translate("Webauthn - Security Key")}
|
||||
icon={<FingerTouchIcon size={32} />}
|
||||
onClick={() => props.onClick(SecondFactorMethod.Webauthn)}
|
||||
/>
|
||||
|
@ -59,7 +59,7 @@ const MethodSelectionDialog = function (props: Props) {
|
|||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button color="primary" onClick={props.onClose}>
|
||||
Close
|
||||
{translate("Close")}s
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 (
|
||||
<Dialog open={props.open} onClose={handleCancel}>
|
||||
<DialogTitle>{`Remove ${props.device ? props.device.description : "(unknown)"}`}</DialogTitle>
|
||||
<DialogTitle>{translate("Remove Webauthn Credential")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{`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,
|
||||
})}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCancel}>Cancel</Button>
|
||||
<Button onClick={handleCancel}>{translate("Cancel")}</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
props.handleClose(true);
|
||||
}}
|
||||
autoFocus
|
||||
>
|
||||
Remove
|
||||
{translate("Remove")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
|
|
@ -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,23 +28,24 @@ export default function WebauthnDetailsDeleteDialog(props: Props) {
|
|||
|
||||
return (
|
||||
<Dialog open={props.open} onClose={props.handleClose}>
|
||||
<DialogTitle>Security key details</DialogTitle>
|
||||
<DialogTitle>{translate("Webauthn Credential Details")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText sx={{ mb: 3 }}>{`Extended information for security key ${
|
||||
props.device ? props.device.description : "(unknown)"
|
||||
}`}</DialogContentText>
|
||||
{props.device && (
|
||||
<DialogContentText sx={{ mb: 3 }}>
|
||||
{translate("Extended Webauthn credential information for security key", {
|
||||
description: props.device.description,
|
||||
})}
|
||||
</DialogContentText>
|
||||
<Stack spacing={0} sx={{ minWidth: 400 }}>
|
||||
<PropertyText
|
||||
name={translate("Credential Identifier")}
|
||||
value={props.device.kid.toString()}
|
||||
clipboard={true}
|
||||
/>
|
||||
<PropertyText
|
||||
<Box paddingBottom={2}>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<PropertyCopyButton name={translate("Identifier")} value={props.device.kid.toString()} />
|
||||
<PropertyCopyButton
|
||||
name={translate("Public Key")}
|
||||
value={props.device.public_key.toString()}
|
||||
clipboard={true}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
<PropertyText name={translate("Description")} value={props.device.description} />
|
||||
<PropertyText name={translate("Relying Party ID")} value={props.device.rpid} />
|
||||
<PropertyText
|
||||
name={translate("Authenticator Attestation GUID")}
|
||||
|
@ -59,10 +62,9 @@ export default function WebauthnDetailsDeleteDialog(props: Props) {
|
|||
/>
|
||||
<PropertyText name={translate("Usage Count")} value={`${props.device.sign_count}`} />
|
||||
</Stack>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.handleClose}>Close</Button>
|
||||
<Button onClick={props.handleClose}>{translate("Close")}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
@ -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);
|
||||
if (copied) {
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
setCopying(true);
|
||||
|
||||
await navigator.clipboard.writeText(props.value);
|
||||
|
||||
setTimeout(() => {
|
||||
setCopying(false);
|
||||
setCopied(true);
|
||||
}, 500);
|
||||
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 3000);
|
||||
}, 2000);
|
||||
})();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box onClick={props.clipboard ? handleCopyToClipboard : undefined}>
|
||||
<LoadingButton
|
||||
loading={copying}
|
||||
variant="outlined"
|
||||
color={copied ? "success" : "primary"}
|
||||
onClick={handleCopyToClipboard}
|
||||
startIcon={copied ? <Check /> : <ContentCopy />}
|
||||
>
|
||||
{copied ? translate("Copied") : props.name}
|
||||
</LoadingButton>
|
||||
);
|
||||
}
|
||||
|
||||
function PropertyText(props: PropertyTextProps) {
|
||||
return (
|
||||
<Box>
|
||||
<Typography display="inline" sx={{ fontWeight: "bold" }}>
|
||||
{`${props.name}: `}
|
||||
</Typography>
|
||||
<Typography display="inline">
|
||||
{props.clipboard ? (copied ? "(copied to clipboard)" : "(click to copy)") : props.value}
|
||||
</Typography>
|
||||
<Typography display="inline">{props.value}</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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<HTMLInputElement>;
|
||||
const [nameError, setNameError] = useState(false);
|
||||
|
@ -34,11 +34,10 @@ export default function WebauthnDeviceEditDialog(props: Props) {
|
|||
|
||||
return (
|
||||
<Dialog open={props.open} onClose={handleCancel}>
|
||||
<DialogTitle>{`Edit ${props.device ? props.device.description : "(unknown)"}`}</DialogTitle>
|
||||
<DialogTitle>{translate("Edit Webauthn Credential")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>Enter a new name for this device:</DialogContentText>
|
||||
<FixedTextField
|
||||
// TODO (PR: #806, Issue: #511) potentially refactor
|
||||
<DialogContentText>{translate("Enter a new name for this Webauthn credential")}</DialogContentText>
|
||||
<TextField
|
||||
autoFocus
|
||||
inputRef={nameRef}
|
||||
id="name-textfield"
|
||||
|
@ -55,7 +54,7 @@ export default function WebauthnDeviceEditDialog(props: Props) {
|
|||
}}
|
||||
autoCapitalize="none"
|
||||
autoComplete="webauthn-name"
|
||||
onKeyPress={(ev) => {
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === "Enter") {
|
||||
handleConfirm();
|
||||
ev.preventDefault();
|
||||
|
@ -64,8 +63,8 @@ export default function WebauthnDeviceEditDialog(props: Props) {
|
|||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCancel}>Cancel</Button>
|
||||
<Button onClick={handleConfirm}>Update</Button>
|
||||
<Button onClick={handleCancel}>{translate("Cancel")}</Button>
|
||||
<Button onClick={handleConfirm}>{translate("Update")}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
|
|
@ -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,23 +80,27 @@ 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 (
|
||||
<Fragment>
|
||||
<Paper variant="outlined">
|
||||
<Box sx={{ p: 3 }}>
|
||||
<WebauthnDeviceDetailsDialog
|
||||
device={props.device}
|
||||
open={showDialogDetails}
|
||||
|
@ -104,9 +109,13 @@ export default function WebauthnDeviceItem(props: Props) {
|
|||
}}
|
||||
/>
|
||||
<WebauthnDeviceEditDialog device={props.device} open={showDialogEdit} handleClose={handleEdit} />
|
||||
<WebauthnDeviceDeleteDialog device={props.device} open={showDialogDelete} handleClose={handleDelete} />
|
||||
<WebauthnDeviceDeleteDialog
|
||||
device={props.device}
|
||||
open={showDialogDelete}
|
||||
handleClose={handleDelete}
|
||||
/>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<KeyRoundedIcon fontSize="large" />
|
||||
<Fingerprint fontSize="large" color={"warning"} />
|
||||
<Stack spacing={0} sx={{ minWidth: 400 }}>
|
||||
<Box>
|
||||
<Typography display="inline" sx={{ fontWeight: "bold" }}>
|
||||
|
@ -117,13 +126,39 @@ export default function WebauthnDeviceItem(props: Props) {
|
|||
variant="body2"
|
||||
>{` (${props.device.attestation_type.toUpperCase()})`}</Typography>
|
||||
</Box>
|
||||
<Typography>Added {props.device.created_at.toString()}</Typography>
|
||||
<Typography>
|
||||
<Typography variant={"caption"}>
|
||||
{translate("Added", {
|
||||
when: new Date(props.device.created_at),
|
||||
formatParams: {
|
||||
when: {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
},
|
||||
},
|
||||
})}
|
||||
</Typography>
|
||||
<Typography variant={"caption"}>
|
||||
{props.device.last_used_at === undefined
|
||||
? translate("Never used")
|
||||
: "Last used " + props.device.last_used_at.toString()}
|
||||
: translate("Last Used", {
|
||||
when: new Date(props.device.last_used_at),
|
||||
formatParams: {
|
||||
when: {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
},
|
||||
},
|
||||
})}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
<Tooltip title={translate("Display extended information for this Webauthn credential")}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
|
@ -132,6 +167,8 @@ export default function WebauthnDeviceItem(props: Props) {
|
|||
>
|
||||
{translate("Info")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title={translate("Edit information for this Webauthn credential")}>
|
||||
<LoadingButton
|
||||
loading={loadingEdit}
|
||||
variant="outlined"
|
||||
|
@ -141,6 +178,8 @@ export default function WebauthnDeviceItem(props: Props) {
|
|||
>
|
||||
{translate("Edit")}
|
||||
</LoadingButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={translate("Remove this Webauthn credential")}>
|
||||
<LoadingButton
|
||||
loading={loadingDelete}
|
||||
variant="outlined"
|
||||
|
@ -150,7 +189,10 @@ export default function WebauthnDeviceItem(props: Props) {
|
|||
>
|
||||
{translate("Remove")}
|
||||
</LoadingButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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<RegistrationResult | null>(null);
|
||||
const [options, setOptions] = useState<PublicKeyCredentialCreationOptionsJSON | null>(null);
|
||||
const [timeout, setTimeout] = useState<number | null>(null);
|
||||
const [deviceName, setName] = useState("");
|
||||
|
||||
const nameRef = useRef() as MutableRefObject<HTMLInputElement>;
|
||||
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 (
|
||||
<Fragment>
|
||||
<div className={styles.icon}>
|
||||
<WebauthnTryIcon onRetryClick={startRegistration} webauthnTouchState={state} />
|
||||
</div>
|
||||
<Typography className={styles.instruction}>Touch the token on your security key</Typography>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12}>
|
||||
<Stack direction="row" spacing={1} justifyContent="center">
|
||||
<Button color="primary" onClick={handleBackClick}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Box className={styles.icon}>
|
||||
{timeout !== null ? <WebauthnRegisterIcon timeout={timeout} /> : null}
|
||||
</Box>
|
||||
<Typography className={styles.instruction}>
|
||||
{translate("Touch the token on your security key")}
|
||||
</Typography>
|
||||
</Fragment>
|
||||
);
|
||||
case 1:
|
||||
return (
|
||||
<div id="webauthn-registration-name">
|
||||
<div className={styles.icon}>
|
||||
<Box id="webauthn-registration-name">
|
||||
<Box className={styles.icon}>
|
||||
<InformationIcon />
|
||||
</div>
|
||||
<Typography className={styles.instruction}>Enter a name for this key</Typography>
|
||||
</Box>
|
||||
<Typography className={styles.instruction}>{translate("Enter a name for this key")}</Typography>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12}>
|
||||
<FixedTextField
|
||||
// TODO (PR: #806, Issue: #511) potentially refactor
|
||||
<TextField
|
||||
inputRef={nameRef}
|
||||
id="name-textfield"
|
||||
label={translate("Name")}
|
||||
|
@ -159,18 +191,19 @@ const RegisterWebauthn = function (props: Props) {
|
|||
required
|
||||
value={deviceName}
|
||||
error={nameError}
|
||||
fullWidth
|
||||
disabled={false}
|
||||
onChange={(v) => 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) {
|
|||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Stack direction="row" spacing={1} justifyContent="center">
|
||||
<Button color="primary" variant="outlined" onClick={startRegistration}>
|
||||
Back
|
||||
</Button>
|
||||
<Stack direction="row" spacing={1} justifyContent="center" paddingTop={1}>
|
||||
<Button color="primary" variant="contained" onClick={finishAttestation}>
|
||||
Finish
|
||||
{translate("Finish")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<div id="webauthn-registration-success">
|
||||
<div className={styles.iconContainer}>
|
||||
<SuccessIcon />
|
||||
</div>
|
||||
<Typography>{translate("Registration success")}</Typography>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const handleOnClose = () => {
|
||||
if (activeStep === 0 || !props.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<LoginLayout title="Register Security Key">
|
||||
<Grid container>
|
||||
<Grid item xs={12} className={styles.methodContainer}>
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<Dialog open={props.open} onClose={handleOnClose} maxWidth={"xs"} fullWidth={true}>
|
||||
<DialogTitle>{translate("Register Webauthn Credential (Security Key)")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={0} alignItems={"center"} justifyContent={"center"} textAlign={"center"}>
|
||||
<Grid item xs={12}>
|
||||
<Stepper activeStep={activeStep}>
|
||||
{steps.map((label, index) => {
|
||||
const stepProps: { completed?: boolean } = {};
|
||||
|
@ -215,38 +245,34 @@ const RegisterWebauthn = function (props: Props) {
|
|||
} = {};
|
||||
return (
|
||||
<Step key={label} {...stepProps}>
|
||||
<StepLabel {...labelProps}>{label}</StepLabel>
|
||||
<StepLabel {...labelProps}>{translate(label)}</StepLabel>
|
||||
</Step>
|
||||
);
|
||||
})}
|
||||
</Stepper>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
{renderStep(activeStep)}
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</LoginLayout>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} disabled={activeStep === 0 && state !== WebauthnTouchState.Failure}>
|
||||
{translate("Cancel")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
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),
|
||||
},
|
||||
}));
|
|
@ -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<boolean>(false);
|
||||
const [refreshState, setRefreshState] = useState<number>(0);
|
||||
|
||||
const handleAddKeyButtonClick = () => {
|
||||
initiateRegistration(RegisterWebauthnRoute);
|
||||
const handleIncrementRefreshState = () => {
|
||||
setRefreshState((refreshState) => refreshState + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<WebauthnDeviceRegisterDialog
|
||||
open={showWebauthnDeviceRegisterDialog}
|
||||
onClose={() => {
|
||||
handleIncrementRefreshState();
|
||||
}}
|
||||
setCancelled={() => {
|
||||
setShowWebauthnDeviceRegisterDialog(false);
|
||||
handleIncrementRefreshState();
|
||||
}}
|
||||
/>
|
||||
<Paper variant="outlined">
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Stack spacing={2}>
|
||||
<Box>
|
||||
<Typography variant="h5">Webauthn Devices</Typography>
|
||||
<Typography variant="h5">{translate("Webauthn Credentials")}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button variant="outlined" color="primary" onClick={handleAddKeyButtonClick}>
|
||||
{"Add new device"}
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setShowWebauthnDeviceRegisterDialog(true);
|
||||
}}
|
||||
>
|
||||
{translate("Add Credential")}
|
||||
</Button>
|
||||
</Box>
|
||||
<Suspense fallback={<LoadingPage />}>
|
||||
<WebauthnDevicesStack />
|
||||
<WebauthnDevicesStack
|
||||
refreshState={refreshState}
|
||||
incrementRefreshState={handleIncrementRefreshState}
|
||||
/>
|
||||
</Suspense>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
|
|
@ -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<WebauthnDevice[]>([]);
|
||||
|
||||
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 (
|
||||
<Fragment>
|
||||
{devices ? (
|
||||
{devices.length !== 0 ? (
|
||||
<Stack spacing={3}>
|
||||
{devices.map((x, idx) => (
|
||||
<WebauthnDeviceItem
|
||||
key={idx}
|
||||
index={idx}
|
||||
device={x}
|
||||
handleDeviceEdit={handleEdit}
|
||||
handleDeviceDelete={handleDelete}
|
||||
/>
|
||||
<WebauthnDeviceItem key={idx} index={idx} device={x} handleEdit={props.incrementRefreshState} />
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
<Typography>No Registered Webauthn Devices</Typography>
|
||||
<Typography>{translate("No Registered Webauthn Credentials")}</Typography>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue