feat: translate all the things
parent
7e56cf2d15
commit
515309c10e
|
@ -774,5 +774,5 @@ Layouts:
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
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",
|
"Automatically refresh these permissions without user interaction": "Automatically refresh these permissions without user interaction",
|
||||||
"Cancel": "Cancel",
|
"Cancel": "Cancel",
|
||||||
"Client ID": "Client ID: {{client_id}}",
|
"Client ID": "Client ID: {{client_id}}",
|
||||||
|
"Close": "Close",
|
||||||
"Consent Request": "Consent Request",
|
"Consent Request": "Consent Request",
|
||||||
"Contact your administrator to register a device": "Contact your administrator to register a device.",
|
"Contact your administrator to register a device": "Contact your administrator to register a device.",
|
||||||
"Could not obtain user settings": "Could not obtain user settings",
|
"Could not obtain user settings": "Could not obtain user settings",
|
||||||
|
@ -50,8 +51,8 @@
|
||||||
"Reset password?": "Reset password?",
|
"Reset password?": "Reset password?",
|
||||||
"Reset": "Reset",
|
"Reset": "Reset",
|
||||||
"Scan QR Code": "Scan QR Code",
|
"Scan QR Code": "Scan QR Code",
|
||||||
|
"Scope": "Scope {{name}}",
|
||||||
"Secret": "Secret",
|
"Secret": "Secret",
|
||||||
"Security Key - WebAuthN": "Security Key - WebAuthN",
|
|
||||||
"Select a Device": "Select a Device",
|
"Select a Device": "Select a Device",
|
||||||
"Sign in": "Sign in",
|
"Sign in": "Sign in",
|
||||||
"Sign out": "Sign out",
|
"Sign out": "Sign out",
|
||||||
|
@ -67,6 +68,7 @@
|
||||||
"Time-based One-Time Password": "Time-based One-Time Password",
|
"Time-based One-Time Password": "Time-based One-Time Password",
|
||||||
"Use OpenID to verify your identity": "Use OpenID to verify your identity",
|
"Use OpenID to verify your identity": "Use OpenID to verify your identity",
|
||||||
"Username": "Username",
|
"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 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 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",
|
"You're being signed out and redirected": "You're being signed out and redirected",
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
{
|
{
|
||||||
"Actions": "Actions",
|
"Actions": "Actions",
|
||||||
"Add": "Add",
|
"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",
|
"Attestation Type": "Attestation Type",
|
||||||
"Authenticator Attestation GUID": "Authenticator Attestation GUID",
|
"Authenticator Attestation GUID": "Authenticator Attestation GUID",
|
||||||
"Cancel": "Cancel",
|
"Cancel": "Cancel",
|
||||||
|
@ -9,21 +11,39 @@
|
||||||
"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",
|
||||||
"Edit": "Edit",
|
"Edit": "Edit",
|
||||||
|
"Edit information for this Webauthn credential": "Edit information for this Webauthn credential",
|
||||||
|
"Edit Webauthn Credential": "Edit Webauthn Credential",
|
||||||
"Enabled": "Enabled",
|
"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",
|
"Manage your security keys": "Manage your security keys",
|
||||||
"Name": "Name",
|
"Name": "Name",
|
||||||
"No": "No",
|
"No": "No",
|
||||||
|
"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 (Security Key)": "Register Webauthn Credential (Security Key)",
|
||||||
"Relying Party ID": "Relying Party ID",
|
"Relying Party ID": "Relying Party ID",
|
||||||
|
"Remove": "Remove",
|
||||||
|
"Remove this Webauthn credential": "Remove this Webauthn credential",
|
||||||
|
"Remove Webauthn Credential": "Remove Webauthn Credential",
|
||||||
"Settings": "Settings",
|
"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",
|
"Transports": "Transports",
|
||||||
"Two-Factor Authentication": "Two-Factor Authentication",
|
"Two-Factor Authentication": "Two-Factor Authentication",
|
||||||
"Usage Count": "Usage Count",
|
"Usage Count": "Usage Count",
|
||||||
"Webauthn Credential Identifier": "Credential Identifier: {{id}}",
|
"Webauthn Credential Details": "Webauthn Credential Details",
|
||||||
"Webauthn Public Key": "Public Key: {{key}}",
|
"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 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",
|
"axios": "1.3.2",
|
||||||
"broadcast-channel": "4.20.2",
|
"broadcast-channel": "4.20.2",
|
||||||
"classnames": "2.3.2",
|
"classnames": "2.3.2",
|
||||||
|
"date-fns": "2.29.3",
|
||||||
"i18next": "22.4.9",
|
"i18next": "22.4.9",
|
||||||
"i18next-browser-languagedetector": "7.0.1",
|
"i18next-browser-languagedetector": "7.0.1",
|
||||||
"i18next-http-backend": "2.1.1",
|
"i18next-http-backend": "2.1.1",
|
||||||
|
|
|
@ -30,6 +30,7 @@ specifiers:
|
||||||
axios: 1.3.2
|
axios: 1.3.2
|
||||||
broadcast-channel: 4.20.2
|
broadcast-channel: 4.20.2
|
||||||
classnames: 2.3.2
|
classnames: 2.3.2
|
||||||
|
date-fns: 2.29.3
|
||||||
esbuild: 0.17.7
|
esbuild: 0.17.7
|
||||||
esbuild-jest: 0.5.0
|
esbuild-jest: 0.5.0
|
||||||
eslint: 8.34.0
|
eslint: 8.34.0
|
||||||
|
@ -83,6 +84,7 @@ dependencies:
|
||||||
axios: 1.3.2
|
axios: 1.3.2
|
||||||
broadcast-channel: 4.20.2
|
broadcast-channel: 4.20.2
|
||||||
classnames: 2.3.2
|
classnames: 2.3.2
|
||||||
|
date-fns: 2.29.3
|
||||||
i18next: 22.4.9
|
i18next: 22.4.9
|
||||||
i18next-browser-languagedetector: 7.0.1
|
i18next-browser-languagedetector: 7.0.1
|
||||||
i18next-http-backend: 2.1.1
|
i18next-http-backend: 2.1.1
|
||||||
|
@ -5003,6 +5005,11 @@ packages:
|
||||||
whatwg-url: 11.0.0
|
whatwg-url: 11.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/date-fns/2.29.3:
|
||||||
|
resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==}
|
||||||
|
engines: {node: '>=0.11'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/debug/2.6.9:
|
/debug/2.6.9:
|
||||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -5396,6 +5403,7 @@ packages:
|
||||||
|
|
||||||
/eslint-config-prettier/8.6.0_eslint@8.34.0:
|
/eslint-config-prettier/8.6.0_eslint@8.34.0:
|
||||||
resolution: {integrity: sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==}
|
resolution: {integrity: sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==}
|
||||||
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: '>=7.0.0'
|
eslint: '>=7.0.0'
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -6860,6 +6868,7 @@ packages:
|
||||||
/jest-cli/29.4.2_@types+node@18.13.0:
|
/jest-cli/29.4.2_@types+node@18.13.0:
|
||||||
resolution: {integrity: sha512-b+eGUtXq/K2v7SH3QcJvFvaUaCDS1/YAZBYz0m28Q/Ppyr+1qNaHmVYikOrbHVbZqYQs2IeI3p76uy6BWbXq8Q==}
|
resolution: {integrity: sha512-b+eGUtXq/K2v7SH3QcJvFvaUaCDS1/YAZBYz0m28Q/Ppyr+1qNaHmVYikOrbHVbZqYQs2IeI3p76uy6BWbXq8Q==}
|
||||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||||
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
|
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
|
@ -7375,6 +7384,7 @@ packages:
|
||||||
/jest/29.4.2_@types+node@18.13.0:
|
/jest/29.4.2_@types+node@18.13.0:
|
||||||
resolution: {integrity: sha512-+5hLd260vNIHu+7ZgMIooSpKl7Jp5pHKb51e73AJU3owd5dEo/RfVwHbA/na3C/eozrt3hJOLGf96c7EWwIAzg==}
|
resolution: {integrity: sha512-+5hLd260vNIHu+7ZgMIooSpKl7Jp5pHKb51e73AJU3owd5dEo/RfVwHbA/na3C/eozrt3hJOLGf96c7EWwIAzg==}
|
||||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||||
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
|
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
|
@ -9229,6 +9239,7 @@ packages:
|
||||||
|
|
||||||
/ts-node/10.9.0_4bewfcp2iebiwuold25d6rgcsy:
|
/ts-node/10.9.0_4bewfcp2iebiwuold25d6rgcsy:
|
||||||
resolution: {integrity: sha512-bunW18GUyaCSYRev4DPf4SQpom3pWH29wKl0sDk5zE7ze19RImEVhCW7K4v3hHKkUyfWotU08ToE2RS+Y49aug==}
|
resolution: {integrity: sha512-bunW18GUyaCSYRev4DPf4SQpom3pWH29wKl0sDk5zE7ze19RImEVhCW7K4v3hHKkUyfWotU08ToE2RS+Y49aug==}
|
||||||
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@swc/core': '>=1.2.50'
|
'@swc/core': '>=1.2.50'
|
||||||
'@swc/wasm': '>=1.2.50'
|
'@swc/wasm': '>=1.2.50'
|
||||||
|
@ -9260,6 +9271,7 @@ packages:
|
||||||
/tsconfck/2.0.1_typescript@4.9.5:
|
/tsconfck/2.0.1_typescript@4.9.5:
|
||||||
resolution: {integrity: sha512-/ipap2eecmVBmBlsQLBRbUmUNFwNJV/z2E+X0FPtHNjPwroMZQ7m39RMaCywlCulBheYXgMdUlWDd9rzxwMA0Q==}
|
resolution: {integrity: sha512-/ipap2eecmVBmBlsQLBRbUmUNFwNJV/z2E+X0FPtHNjPwroMZQ7m39RMaCywlCulBheYXgMdUlWDd9rzxwMA0Q==}
|
||||||
engines: {node: ^14.13.1 || ^16 || >=18, pnpm: ^7.0.1}
|
engines: {node: ^14.13.1 || ^16 || >=18, pnpm: ^7.0.1}
|
||||||
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: ^4.3.5
|
typescript: ^4.3.5
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
|
@ -9422,6 +9434,7 @@ packages:
|
||||||
|
|
||||||
/update-browserslist-db/1.0.10_browserslist@4.21.4:
|
/update-browserslist-db/1.0.10_browserslist@4.21.4:
|
||||||
resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==}
|
resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==}
|
||||||
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
browserslist: '>= 4.21.0'
|
browserslist: '>= 4.21.0'
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -9523,6 +9536,7 @@ packages:
|
||||||
/vite/4.1.1_@types+node@18.13.0:
|
/vite/4.1.1_@types+node@18.13.0:
|
||||||
resolution: {integrity: sha512-LM9WWea8vsxhr782r9ntg+bhSFS06FJgCvvB0+8hf8UWtvaiDagKYWXndjfX6kGl74keHJUcpzrQliDXZlF5yg==}
|
resolution: {integrity: sha512-LM9WWea8vsxhr782r9ntg+bhSFS06FJgCvvB0+8hf8UWtvaiDagKYWXndjfX6kGl74keHJUcpzrQliDXZlF5yg==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/node': '>= 14'
|
'@types/node': '>= 14'
|
||||||
less: '*'
|
less: '*'
|
||||||
|
|
|
@ -12,7 +12,6 @@ import {
|
||||||
IndexRoute,
|
IndexRoute,
|
||||||
LogoutRoute,
|
LogoutRoute,
|
||||||
RegisterOneTimePasswordRoute,
|
RegisterOneTimePasswordRoute,
|
||||||
RegisterWebauthnRoute,
|
|
||||||
ResetPasswordStep1Route,
|
ResetPasswordStep1Route,
|
||||||
ResetPasswordStep2Route,
|
ResetPasswordStep2Route,
|
||||||
SettingsRoute,
|
SettingsRoute,
|
||||||
|
@ -29,7 +28,6 @@ import {
|
||||||
getTheme,
|
getTheme,
|
||||||
} from "@utils/Configuration";
|
} from "@utils/Configuration";
|
||||||
import RegisterOneTimePassword from "@views/DeviceRegistration/RegisterOneTimePassword";
|
import RegisterOneTimePassword from "@views/DeviceRegistration/RegisterOneTimePassword";
|
||||||
import RegisterWebauthn from "@views/DeviceRegistration/RegisterWebauthn";
|
|
||||||
import BaseLoadingPage from "@views/LoadingPage/BaseLoadingPage";
|
import BaseLoadingPage from "@views/LoadingPage/BaseLoadingPage";
|
||||||
import ConsentView from "@views/LoginPortal/ConsentView/ConsentView";
|
import ConsentView from "@views/LoginPortal/ConsentView/ConsentView";
|
||||||
import LoginPortal from "@views/LoginPortal/LoginPortal";
|
import LoginPortal from "@views/LoginPortal/LoginPortal";
|
||||||
|
@ -91,7 +89,6 @@ const App: React.FC<Props> = (props: Props) => {
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path={ResetPasswordStep1Route} element={<ResetPasswordStep1 />} />
|
<Route path={ResetPasswordStep1Route} element={<ResetPasswordStep1 />} />
|
||||||
<Route path={ResetPasswordStep2Route} element={<ResetPasswordStep2 />} />
|
<Route path={ResetPasswordStep2Route} element={<ResetPasswordStep2 />} />
|
||||||
<Route path={RegisterWebauthnRoute} element={<RegisterWebauthn />} />
|
|
||||||
<Route path={RegisterOneTimePasswordRoute} element={<RegisterOneTimePassword />} />
|
<Route path={RegisterOneTimePasswordRoute} element={<RegisterOneTimePassword />} />
|
||||||
<Route path={LogoutRoute} element={<SignOut />} />
|
<Route path={LogoutRoute} element={<SignOut />} />
|
||||||
<Route path={ConsentRoute} element={<ConsentView />} />
|
<Route path={ConsentRoute} element={<ConsentView />} />
|
||||||
|
|
|
@ -14,6 +14,7 @@ const url = "https://www.authelia.com";
|
||||||
|
|
||||||
const Brand = function (props: Props) {
|
const Brand = function (props: Props) {
|
||||||
const { t: translate } = useTranslation();
|
const { t: translate } = useTranslation();
|
||||||
|
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const privacyEnabled = getPrivacyPolicyEnabled();
|
const privacyEnabled = getPrivacyPolicyEnabled();
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ const PasswordMeter = function (props: Props) {
|
||||||
const [progressColor] = useState(["#D32F2F", "#FF5722", "#FFEB3B", "#AFB42B", "#62D32F"]);
|
const [progressColor] = useState(["#D32F2F", "#FF5722", "#FFEB3B", "#AFB42B", "#62D32F"]);
|
||||||
const [passwordScore, setPasswordScore] = useState(0);
|
const [passwordScore, setPasswordScore] = useState(0);
|
||||||
const [maxScores, setMaxScores] = useState(0);
|
const [maxScores, setMaxScores] = useState(0);
|
||||||
const [feedback, setFeedback] = useState("");
|
const [feedback, setFeedback] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const password = props.value;
|
const password = props.value;
|
||||||
|
@ -114,7 +114,7 @@ const PasswordMeter = function (props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={styles.progressContainer}>
|
<Box className={styles.progressContainer}>
|
||||||
<Box title={feedback} className={classnames(styles.progressBar)} />
|
<Box title={feedback === null ? "" : feedback} className={classnames(styles.progressBar)} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,10 +6,11 @@ import { usePersistentStorageValue } from "@hooks/PersistentStorage";
|
||||||
import { getPrivacyPolicyEnabled, getPrivacyPolicyRequireAccept } from "@utils/Configuration";
|
import { getPrivacyPolicyEnabled, getPrivacyPolicyRequireAccept } from "@utils/Configuration";
|
||||||
|
|
||||||
const PrivacyPolicyDrawer = function (props: DrawerProps) {
|
const PrivacyPolicyDrawer = function (props: DrawerProps) {
|
||||||
|
const { t: translate } = useTranslation();
|
||||||
|
|
||||||
const privacyEnabled = getPrivacyPolicyEnabled();
|
const privacyEnabled = getPrivacyPolicyEnabled();
|
||||||
const privacyRequireAccept = getPrivacyPolicyRequireAccept();
|
const privacyRequireAccept = getPrivacyPolicyRequireAccept();
|
||||||
const [accepted, setAccepted] = usePersistentStorageValue<boolean>("privacy-policy-accepted", false);
|
const [accepted, setAccepted] = usePersistentStorageValue<boolean>("privacy-policy-accepted", false);
|
||||||
const { t: translate } = useTranslation();
|
|
||||||
|
|
||||||
return privacyEnabled && privacyRequireAccept && !accepted ? (
|
return privacyEnabled && privacyRequireAccept && !accepted ? (
|
||||||
<Drawer {...props} anchor="bottom" open={!accepted}>
|
<Drawer {...props} anchor="bottom" open={!accepted}>
|
||||||
|
|
|
@ -6,10 +6,10 @@ import { useTranslation } from "react-i18next";
|
||||||
import { getPrivacyPolicyURL } from "@utils/Configuration";
|
import { getPrivacyPolicyURL } from "@utils/Configuration";
|
||||||
|
|
||||||
const PrivacyPolicyLink = function (props: LinkProps) {
|
const PrivacyPolicyLink = function (props: LinkProps) {
|
||||||
const hrefPrivacyPolicy = getPrivacyPolicyURL();
|
|
||||||
|
|
||||||
const { t: translate } = useTranslation();
|
const { t: translate } = useTranslation();
|
||||||
|
|
||||||
|
const hrefPrivacyPolicy = getPrivacyPolicyURL();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Link {...props} href={hrefPrivacyPolicy} target="_blank" rel="noopener" underline="hover">
|
<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 ResetPasswordStep1Route: string = "/reset-password/step1";
|
||||||
export const ResetPasswordStep2Route: string = "/reset-password/step2";
|
export const ResetPasswordStep2Route: string = "/reset-password/step2";
|
||||||
export const RegisterWebauthnRoute: string = "/webauthn/register";
|
|
||||||
export const RegisterOneTimePasswordRoute: string = "/one-time-password/register";
|
export const RegisterOneTimePasswordRoute: string = "/one-time-password/register";
|
||||||
export const LogoutRoute: string = "/logout";
|
export const LogoutRoute: string = "/logout";
|
||||||
|
|
||||||
|
|
|
@ -16,18 +16,19 @@ import { getLogoOverride } from "@utils/Configuration";
|
||||||
export interface Props {
|
export interface Props {
|
||||||
id?: string;
|
id?: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
title?: string;
|
title?: string | null;
|
||||||
titleTooltip?: string;
|
titleTooltip?: string | null;
|
||||||
subtitle?: string;
|
subtitle?: string | null;
|
||||||
subtitleTooltip?: string;
|
subtitleTooltip?: string | null;
|
||||||
showBrand?: boolean;
|
showBrand?: boolean;
|
||||||
showSettings?: boolean;
|
showSettings?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LoginLayout = function (props: Props) {
|
const LoginLayout = function (props: Props) {
|
||||||
|
const { t: translate } = useTranslation();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const { t: translate } = useTranslation();
|
|
||||||
|
|
||||||
const logo = getLogoOverride() ? (
|
const logo = getLogoOverride() ? (
|
||||||
<img src="./static/media/logo.png" alt="Logo" className={styles.icon} />
|
<img src="./static/media/logo.png" alt="Logo" className={styles.icon} />
|
||||||
|
@ -82,7 +83,7 @@ const LoginLayout = function (props: Props) {
|
||||||
<TypographyWithTooltip
|
<TypographyWithTooltip
|
||||||
variant={"h5"}
|
variant={"h5"}
|
||||||
value={props.title}
|
value={props.title}
|
||||||
tooltip={props.titleTooltip}
|
tooltip={props.titleTooltip !== null ? props.titleTooltip : undefined}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -91,7 +92,7 @@ const LoginLayout = function (props: Props) {
|
||||||
<TypographyWithTooltip
|
<TypographyWithTooltip
|
||||||
variant={"h6"}
|
variant={"h6"}
|
||||||
value={props.subtitle}
|
value={props.subtitle}
|
||||||
tooltip={props.subtitleTooltip}
|
tooltip={props.subtitleTooltip !== null ? props.subtitleTooltip : undefined}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -33,6 +33,7 @@ const defaultDrawerWidth = 240;
|
||||||
|
|
||||||
const SettingsLayout = function (props: Props) {
|
const SettingsLayout = function (props: Props) {
|
||||||
const { t: translate } = useTranslation("settings");
|
const { t: translate } = useTranslation("settings");
|
||||||
|
|
||||||
const navigate = useRouterNavigate();
|
const navigate = useRouterNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -65,7 +66,7 @@ const SettingsLayout = function (props: Props) {
|
||||||
navigate(IndexRoute);
|
navigate(IndexRoute);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{"Close"}
|
{translate("Close")}
|
||||||
</Button>
|
</Button>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
|
@ -114,7 +115,7 @@ const SettingsMenuItem = function (props: SettingsMenuItemProps) {
|
||||||
const navigate = useRouterNavigate();
|
const navigate = useRouterNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem disablePadding onClick={selected ? () => console.log("selected") : () => navigate(props.pathname)}>
|
<ListItem disablePadding onClick={!selected ? () => navigate(props.pathname) : undefined}>
|
||||||
<ListItemButton selected={selected}>
|
<ListItemButton selected={selected}>
|
||||||
<ListItemIcon>{props.icon}</ListItemIcon>
|
<ListItemIcon>{props.icon}</ListItemIcon>
|
||||||
<ListItemText primary={props.text} />
|
<ListItemText primary={props.text} />
|
||||||
|
|
|
@ -107,8 +107,8 @@ export interface AuthenticationResult {
|
||||||
|
|
||||||
export interface WebauthnDevice {
|
export interface WebauthnDevice {
|
||||||
id: string;
|
id: string;
|
||||||
created_at: Date;
|
created_at: string;
|
||||||
last_used_at?: Date;
|
last_used_at?: string;
|
||||||
rpid: string;
|
rpid: string;
|
||||||
description: string;
|
description: string;
|
||||||
kid: Uint8Array;
|
kid: Uint8Array;
|
||||||
|
|
|
@ -144,7 +144,6 @@ export async function startWebauthnRegistration(options: PublicKeyCredentialCrea
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(JSON.stringify(options));
|
|
||||||
result.response = await startRegistration(options);
|
result.response = await startRegistration(options);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const exception = e as DOMException;
|
const exception = e as DOMException;
|
||||||
|
|
|
@ -20,15 +20,18 @@ import LoginLayout from "@layouts/LoginLayout";
|
||||||
import { completeTOTPRegistrationProcess } from "@services/RegisterDevice";
|
import { completeTOTPRegistrationProcess } from "@services/RegisterDevice";
|
||||||
|
|
||||||
const RegisterOneTimePassword = function () {
|
const RegisterOneTimePassword = function () {
|
||||||
|
const { t: translate } = useTranslation();
|
||||||
|
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { createSuccessNotification, createErrorNotification } = useNotifications();
|
||||||
|
|
||||||
// The secret retrieved from the API is all is ok.
|
// The secret retrieved from the API is all is ok.
|
||||||
const [secretURL, setSecretURL] = useState("empty");
|
const [secretURL, setSecretURL] = useState("empty");
|
||||||
const [secretBase32, setSecretBase32] = useState(undefined as string | undefined);
|
const [secretBase32, setSecretBase32] = useState(undefined as string | undefined);
|
||||||
const { createSuccessNotification, createErrorNotification } = useNotifications();
|
|
||||||
const [hasErrored, setHasErrored] = useState(false);
|
const [hasErrored, setHasErrored] = useState(false);
|
||||||
const [isLoading, setIsLoading] = 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
|
// Get the token from the query param to give it back to the API when requesting
|
||||||
// the secret for OTP.
|
// the secret for OTP.
|
||||||
|
|
|
@ -6,6 +6,7 @@ import BaseLoadingPage from "@views/LoadingPage/BaseLoadingPage";
|
||||||
|
|
||||||
const LoadingPage = function () {
|
const LoadingPage = function () {
|
||||||
const { t: translate } = useTranslation();
|
const { t: translate } = useTranslation();
|
||||||
|
|
||||||
return <BaseLoadingPage message={translate("Loading")} />;
|
return <BaseLoadingPage message={translate("Loading")} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,10 @@ import { useTranslation } from "react-i18next";
|
||||||
import SuccessIcon from "@components/SuccessIcon";
|
import SuccessIcon from "@components/SuccessIcon";
|
||||||
|
|
||||||
const Authenticated = function () {
|
const Authenticated = function () {
|
||||||
const styles = useStyles();
|
|
||||||
const { t: translate } = useTranslation();
|
const { t: translate } = useTranslation();
|
||||||
|
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="authenticated-stage">
|
<div id="authenticated-stage">
|
||||||
<div className={styles.iconContainer}>
|
<div className={styles.iconContainer}>
|
||||||
|
|
|
@ -14,10 +14,12 @@ export interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthenticatedView = function (props: Props) {
|
const AuthenticatedView = function (props: Props) {
|
||||||
const styles = useStyles();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { t: translate } = useTranslation();
|
const { t: translate } = useTranslation();
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
const handleLogoutClick = () => {
|
const handleLogoutClick = () => {
|
||||||
navigate(SignOutRoute);
|
navigate(SignOutRoute);
|
||||||
};
|
};
|
||||||
|
|
|
@ -47,23 +47,26 @@ function scopeNameToAvatar(id: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConsentView = function (props: Props) {
|
const ConsentView = function (props: Props) {
|
||||||
const styles = useStyles();
|
|
||||||
const { t: translate } = useTranslation();
|
const { t: translate } = useTranslation();
|
||||||
|
|
||||||
|
const [userInfo, fetchUserInfo, , fetchUserInfoError] = useUserInfoGET();
|
||||||
|
|
||||||
|
const { createErrorNotification, resetNotification } = useNotifications();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const redirect = useRedirector();
|
const redirect = useRedirector();
|
||||||
const consentID = searchParams.get(Identifier);
|
const consentID = searchParams.get(Identifier);
|
||||||
const { createErrorNotification, resetNotification } = useNotifications();
|
|
||||||
const [response, setResponse] = useState<ConsentGetResponseBody | undefined>(undefined);
|
const [response, setResponse] = useState<ConsentGetResponseBody | undefined>(undefined);
|
||||||
const [error, setError] = useState<any>(undefined);
|
const [error, setError] = useState<any>(undefined);
|
||||||
const [preConfigure, setPreConfigure] = useState(false);
|
const [preConfigure, setPreConfigure] = useState(false);
|
||||||
|
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
const handlePreConfigureChanged = () => {
|
const handlePreConfigureChanged = () => {
|
||||||
setPreConfigure((preConfigure) => !preConfigure);
|
setPreConfigure((preConfigure) => !preConfigure);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [userInfo, fetchUserInfo, , fetchUserInfoError] = useUserInfoGET();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUserInfo();
|
fetchUserInfo();
|
||||||
}, [fetchUserInfo]);
|
}, [fetchUserInfo]);
|
||||||
|
@ -167,7 +170,7 @@ const ConsentView = function (props: Props) {
|
||||||
<div className={styles.scopesListContainer}>
|
<div className={styles.scopesListContainer}>
|
||||||
<List className={styles.scopesList}>
|
<List className={styles.scopesList}>
|
||||||
{response?.scopes.map((scope: string) => (
|
{response?.scopes.map((scope: string) => (
|
||||||
<Tooltip title={"Scope " + scope}>
|
<Tooltip title={translate("Scope", { name: scope })}>
|
||||||
<ListItem id={"scope-" + scope} dense>
|
<ListItem id={"scope-" + scope} dense>
|
||||||
<ListItemIcon>{scopeNameToAvatar(scope)}</ListItemIcon>
|
<ListItemIcon>{scopeNameToAvatar(scope)}</ListItemIcon>
|
||||||
<ListItemText primary={translateScopeNameToDescription(scope)} />
|
<ListItemText primary={translateScopeNameToDescription(scope)} />
|
||||||
|
@ -180,10 +183,7 @@ const ConsentView = function (props: Props) {
|
||||||
{response?.pre_configuration ? (
|
{response?.pre_configuration ? (
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={translate("This saves this consent as a pre-configured consent for future use")}
|
||||||
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"
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
|
|
|
@ -30,23 +30,27 @@ export interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const FirstFactorForm = function (props: Props) {
|
const FirstFactorForm = function (props: Props) {
|
||||||
const styles = useStyles();
|
const { t: translate } = useTranslation();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const redirectionURL = useQueryParam(RedirectionURL);
|
const redirectionURL = useQueryParam(RedirectionURL);
|
||||||
const requestMethod = useQueryParam(RequestMethod);
|
const requestMethod = useQueryParam(RequestMethod);
|
||||||
const [workflow] = useWorkflow();
|
const [workflow] = useWorkflow();
|
||||||
|
const { createErrorNotification } = useNotifications();
|
||||||
|
|
||||||
const loginChannel = useMemo(() => new BroadcastChannel<boolean>("login"), []);
|
const loginChannel = useMemo(() => new BroadcastChannel<boolean>("login"), []);
|
||||||
|
|
||||||
const [rememberMe, setRememberMe] = useState(false);
|
const [rememberMe, setRememberMe] = useState(false);
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [usernameError, setUsernameError] = useState(false);
|
const [usernameError, setUsernameError] = useState(false);
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [passwordError, setPasswordError] = useState(false);
|
const [passwordError, setPasswordError] = useState(false);
|
||||||
const { createErrorNotification } = useNotifications();
|
|
||||||
// TODO (PR: #806, Issue: #511) potentially refactor
|
// TODO (PR: #806, Issue: #511) potentially refactor
|
||||||
const usernameRef = useRef() as MutableRefObject<HTMLInputElement>;
|
const usernameRef = useRef() as MutableRefObject<HTMLInputElement>;
|
||||||
const passwordRef = useRef() as MutableRefObject<HTMLInputElement>;
|
const passwordRef = useRef() as MutableRefObject<HTMLInputElement>;
|
||||||
const { t: translate } = useTranslation();
|
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeout = setTimeout(() => usernameRef.current.focus(), 10);
|
const timeout = setTimeout(() => usernameRef.current.focus(), 10);
|
||||||
|
@ -122,7 +126,7 @@ const FirstFactorForm = function (props: Props) {
|
||||||
onFocus={() => setUsernameError(false)}
|
onFocus={() => setUsernameError(false)}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
onKeyPress={(ev) => {
|
onKeyDown={(ev) => {
|
||||||
if (ev.key === "Enter") {
|
if (ev.key === "Enter") {
|
||||||
if (!username.length) {
|
if (!username.length) {
|
||||||
setUsernameError(true);
|
setUsernameError(true);
|
||||||
|
@ -152,7 +156,7 @@ const FirstFactorForm = function (props: Props) {
|
||||||
onFocus={() => setPasswordError(false)}
|
onFocus={() => setPasswordError(false)}
|
||||||
type="password"
|
type="password"
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
onKeyPress={(ev) => {
|
onKeyDown={(ev) => {
|
||||||
if (ev.key === "Enter") {
|
if (ev.key === "Enter") {
|
||||||
if (!username.length) {
|
if (!username.length) {
|
||||||
usernameRef.current.focus();
|
usernameRef.current.focus();
|
||||||
|
@ -174,7 +178,7 @@ const FirstFactorForm = function (props: Props) {
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
checked={rememberMe}
|
checked={rememberMe}
|
||||||
onChange={handleRememberMeChange}
|
onChange={handleRememberMeChange}
|
||||||
onKeyPress={(ev) => {
|
onKeyDown={(ev) => {
|
||||||
if (ev.key === "Enter") {
|
if (ev.key === "Enter") {
|
||||||
if (!username.length) {
|
if (!username.length) {
|
||||||
usernameRef.current.focus();
|
usernameRef.current.focus();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { ReactNode, useState } from "react";
|
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 makeStyles from "@mui/styles/makeStyles";
|
||||||
|
|
||||||
import PushNotificationIcon from "@components/PushNotificationIcon";
|
import PushNotificationIcon from "@components/PushNotificationIcon";
|
||||||
|
@ -127,12 +127,12 @@ function DeviceItem(props: DeviceItemProps) {
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={props.onSelect}
|
onClick={props.onSelect}
|
||||||
>
|
>
|
||||||
<div className={style.icon}>
|
<Box className={style.icon}>
|
||||||
<PushNotificationIcon width={32} height={32} />
|
<PushNotificationIcon width={32} height={32} />
|
||||||
</div>
|
</Box>
|
||||||
<div>
|
<Box>
|
||||||
<Typography>{props.device.name}</Typography>
|
<Typography>{props.device.name}</Typography>
|
||||||
</div>
|
</Box>
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
|
@ -172,12 +172,12 @@ function MethodItem(props: MethodItemProps) {
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={props.onSelect}
|
onClick={props.onSelect}
|
||||||
>
|
>
|
||||||
<div className={style.icon}>
|
<Box className={style.icon}>
|
||||||
<PushNotificationIcon width={32} height={32} />
|
<PushNotificationIcon width={32} height={32} />
|
||||||
</div>
|
</Box>
|
||||||
<div>
|
<Box>
|
||||||
<Typography>{props.method}</Typography>
|
<Typography>{props.method}</Typography>
|
||||||
</div>
|
</Box>
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
|
|
||||||
import { Theme } from "@mui/material";
|
import { Box, Theme } from "@mui/material";
|
||||||
import makeStyles from "@mui/styles/makeStyles";
|
import makeStyles from "@mui/styles/makeStyles";
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
|
|
||||||
|
@ -30,12 +30,12 @@ const IconWithContext = function (props: IconWithContextProps) {
|
||||||
}))();
|
}))();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classnames(props.className, styles.root)}>
|
<Box className={classnames(props.className, styles.root)}>
|
||||||
<div className={styles.iconContainer}>
|
<Box className={styles.iconContainer}>
|
||||||
<div className={styles.icon}>{props.icon}</div>
|
<Box className={styles.icon}>{props.icon}</Box>
|
||||||
</div>
|
</Box>
|
||||||
<div className={styles.context}>{props.children}</div>
|
<Box className={styles.context}>{props.children}</Box>
|
||||||
</div>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -28,14 +28,15 @@ export interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultMethodContainer = function (props: Props) {
|
const DefaultMethodContainer = function (props: Props) {
|
||||||
const styles = useStyles();
|
|
||||||
const { t: translate } = useTranslation();
|
const { t: translate } = useTranslation();
|
||||||
|
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
const registerMessage = props.registered
|
const registerMessage = props.registered
|
||||||
? props.title === "Push Notification"
|
? props.title === "Push Notification"
|
||||||
? ""
|
? ""
|
||||||
: translate("Manage devices")
|
: translate("Manage devices")
|
||||||
: translate("Register device");
|
: translate("Register device");
|
||||||
const selectMessage = translate("Select a Device");
|
|
||||||
|
|
||||||
let container: ReactNode;
|
let container: ReactNode;
|
||||||
let stateClass: string = "";
|
let stateClass: string = "";
|
||||||
|
@ -62,7 +63,7 @@ const DefaultMethodContainer = function (props: Props) {
|
||||||
</div>
|
</div>
|
||||||
{props.onSelectClick && props.registered ? (
|
{props.onSelectClick && props.registered ? (
|
||||||
<Link component="button" id="selection-link" onClick={props.onSelectClick} underline="hover">
|
<Link component="button" id="selection-link" onClick={props.onSelectClick} underline="hover">
|
||||||
{selectMessage}
|
{translate("Select a Device")}
|
||||||
</Link>
|
</Link>
|
||||||
) : null}
|
) : null}
|
||||||
{(props.onRegisterClick && props.title !== "Push Notification") ||
|
{(props.onRegisterClick && props.title !== "Push Notification") ||
|
||||||
|
|
|
@ -42,7 +42,7 @@ const MethodSelectionDialog = function (props: Props) {
|
||||||
{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("Webauthn - Security Key")}
|
||||||
icon={<FingerTouchIcon size={32} />}
|
icon={<FingerTouchIcon size={32} />}
|
||||||
onClick={() => props.onClick(SecondFactorMethod.Webauthn)}
|
onClick={() => props.onClick(SecondFactorMethod.Webauthn)}
|
||||||
/>
|
/>
|
||||||
|
@ -59,7 +59,7 @@ const MethodSelectionDialog = function (props: Props) {
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button color="primary" onClick={props.onClose}>
|
<Button color="primary" onClick={props.onClose}>
|
||||||
Close
|
{translate("Close")}s
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
@ -53,7 +53,7 @@ const ResetPasswordStep1 = function () {
|
||||||
error={error}
|
error={error}
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
onKeyPress={(ev) => {
|
onKeyDown={(ev) => {
|
||||||
if (ev.key === "Enter") {
|
if (ev.key === "Enter") {
|
||||||
doInitiateResetPasswordProcess();
|
doInitiateResetPasswordProcess();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
|
@ -153,7 +153,7 @@ const ResetPasswordStep2 = function () {
|
||||||
value={password2}
|
value={password2}
|
||||||
onChange={(e) => setPassword2(e.target.value)}
|
onChange={(e) => setPassword2(e.target.value)}
|
||||||
error={errorPassword2}
|
error={errorPassword2}
|
||||||
onKeyPress={(ev) => {
|
onKeyDown={(ev) => {
|
||||||
if (ev.key === "Enter") {
|
if (ev.key === "Enter") {
|
||||||
doResetPassword();
|
doResetPassword();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
|
@ -1,39 +1,42 @@
|
||||||
import React from "react";
|
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 { WebauthnDevice } from "@models/Webauthn";
|
import { WebauthnDevice } from "@models/Webauthn";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
device: WebauthnDevice | undefined;
|
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 handleCancel = () => {
|
const handleCancel = () => {
|
||||||
props.handleClose(false);
|
props.handleClose(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.open} onClose={handleCancel}>
|
<Dialog open={props.open} onClose={handleCancel}>
|
||||||
<DialogTitle>{`Remove ${props.device ? props.device.description : "(unknown)"}`}</DialogTitle>
|
<DialogTitle>{translate("Remove Webauthn Credential")}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
{`Are you sure you want to remove the device "${
|
{translate("Are you sure you want to remove the Webauthn credential from from your account", {
|
||||||
props.device ? props.device.description : "(unknown)"
|
description: props.device.description,
|
||||||
}" from your account?`}
|
})}
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleCancel}>Cancel</Button>
|
<Button onClick={handleCancel}>{translate("Cancel")}</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.handleClose(true);
|
props.handleClose(true);
|
||||||
}}
|
}}
|
||||||
autoFocus
|
autoFocus
|
||||||
>
|
>
|
||||||
Remove
|
{translate("Remove")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
import { Check, ContentCopy } from "@mui/icons-material";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
@ -13,11 +14,12 @@ import {
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import LoadingButton from "@components/LoadingButton";
|
||||||
import { WebauthnDevice } from "@models/Webauthn";
|
import { WebauthnDevice } from "@models/Webauthn";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
device: WebauthnDevice | undefined;
|
device: WebauthnDevice;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,43 +28,43 @@ export default function WebauthnDetailsDeleteDialog(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.open} onClose={props.handleClose}>
|
<Dialog open={props.open} onClose={props.handleClose}>
|
||||||
<DialogTitle>Security key details</DialogTitle>
|
<DialogTitle>{translate("Webauthn Credential Details")}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText sx={{ mb: 3 }}>{`Extended information for security key ${
|
<DialogContentText sx={{ mb: 3 }}>
|
||||||
props.device ? props.device.description : "(unknown)"
|
{translate("Extended Webauthn credential information for security key", {
|
||||||
}`}</DialogContentText>
|
description: props.device.description,
|
||||||
{props.device && (
|
})}
|
||||||
<Stack spacing={0} sx={{ minWidth: 400 }}>
|
</DialogContentText>
|
||||||
<PropertyText
|
<Stack spacing={0} sx={{ minWidth: 400 }}>
|
||||||
name={translate("Credential Identifier")}
|
<Box paddingBottom={2}>
|
||||||
value={props.device.kid.toString()}
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
clipboard={true}
|
<PropertyCopyButton name={translate("Identifier")} value={props.device.kid.toString()} />
|
||||||
/>
|
<PropertyCopyButton
|
||||||
<PropertyText
|
name={translate("Public Key")}
|
||||||
name={translate("Public Key")}
|
value={props.device.public_key.toString()}
|
||||||
value={props.device.public_key.toString()}
|
/>
|
||||||
clipboard={true}
|
</Stack>
|
||||||
/>
|
</Box>
|
||||||
<PropertyText name={translate("Relying Party ID")} value={props.device.rpid} />
|
<PropertyText name={translate("Description")} value={props.device.description} />
|
||||||
<PropertyText
|
<PropertyText name={translate("Relying Party ID")} value={props.device.rpid} />
|
||||||
name={translate("Authenticator Attestation GUID")}
|
<PropertyText
|
||||||
value={props.device.aaguid === undefined ? "N/A" : props.device.aaguid}
|
name={translate("Authenticator Attestation GUID")}
|
||||||
/>
|
value={props.device.aaguid === undefined ? "N/A" : props.device.aaguid}
|
||||||
<PropertyText name={translate("Attestation Type")} value={props.device.attestation_type} />
|
/>
|
||||||
<PropertyText
|
<PropertyText name={translate("Attestation Type")} value={props.device.attestation_type} />
|
||||||
name={translate("Transports")}
|
<PropertyText
|
||||||
value={props.device.transports.length === 0 ? "N/A" : props.device.transports.join(", ")}
|
name={translate("Transports")}
|
||||||
/>
|
value={props.device.transports.length === 0 ? "N/A" : props.device.transports.join(", ")}
|
||||||
<PropertyText
|
/>
|
||||||
name={translate("Clone Warning")}
|
<PropertyText
|
||||||
value={props.device.clone_warning ? translate("Yes") : translate("No")}
|
name={translate("Clone Warning")}
|
||||||
/>
|
value={props.device.clone_warning ? translate("Yes") : translate("No")}
|
||||||
<PropertyText name={translate("Usage Count")} value={`${props.device.sign_count}`} />
|
/>
|
||||||
</Stack>
|
<PropertyText name={translate("Usage Count")} value={`${props.device.sign_count}`} />
|
||||||
)}
|
</Stack>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={props.handleClose}>Close</Button>
|
<Button onClick={props.handleClose}>{translate("Close")}</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
@ -71,28 +73,55 @@ export default function WebauthnDetailsDeleteDialog(props: Props) {
|
||||||
interface PropertyTextProps {
|
interface PropertyTextProps {
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
clipboard?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function PropertyText(props: PropertyTextProps) {
|
function PropertyCopyButton(props: PropertyTextProps) {
|
||||||
|
const { t: translate } = useTranslation("settings");
|
||||||
|
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [copying, setCopying] = useState(false);
|
||||||
|
|
||||||
const handleCopyToClipboard = () => {
|
const handleCopyToClipboard = () => {
|
||||||
navigator.clipboard.writeText(props.value);
|
if (copied) {
|
||||||
setCopied(true);
|
return;
|
||||||
setTimeout(() => {
|
}
|
||||||
setCopied(false);
|
|
||||||
}, 3000);
|
(async () => {
|
||||||
|
setCopying(true);
|
||||||
|
|
||||||
|
await navigator.clipboard.writeText(props.value);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopying(false);
|
||||||
|
setCopied(true);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopied(false);
|
||||||
|
}, 2000);
|
||||||
|
})();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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" }}>
|
<Typography display="inline" sx={{ fontWeight: "bold" }}>
|
||||||
{`${props.name}: `}
|
{`${props.name}: `}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography display="inline">
|
<Typography display="inline">{props.value}</Typography>
|
||||||
{props.clipboard ? (copied ? "(copied to clipboard)" : "(click to copy)") : props.value}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import React, { MutableRefObject, useRef, useState } from "react";
|
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 { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import FixedTextField from "@components/FixedTextField";
|
|
||||||
import { WebauthnDevice } from "@models/Webauthn";
|
import { WebauthnDevice } from "@models/Webauthn";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
device: WebauthnDevice | undefined;
|
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();
|
const { t: translate } = useTranslation("settings");
|
||||||
|
|
||||||
const [deviceName, setName] = useState("");
|
const [deviceName, setName] = useState("");
|
||||||
const nameRef = useRef() as MutableRefObject<HTMLInputElement>;
|
const nameRef = useRef() as MutableRefObject<HTMLInputElement>;
|
||||||
const [nameError, setNameError] = useState(false);
|
const [nameError, setNameError] = useState(false);
|
||||||
|
@ -34,11 +34,10 @@ export default function WebauthnDeviceEditDialog(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.open} onClose={handleCancel}>
|
<Dialog open={props.open} onClose={handleCancel}>
|
||||||
<DialogTitle>{`Edit ${props.device ? props.device.description : "(unknown)"}`}</DialogTitle>
|
<DialogTitle>{translate("Edit Webauthn Credential")}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>Enter a new name for this device:</DialogContentText>
|
<DialogContentText>{translate("Enter a new name for this Webauthn credential")}</DialogContentText>
|
||||||
<FixedTextField
|
<TextField
|
||||||
// TODO (PR: #806, Issue: #511) potentially refactor
|
|
||||||
autoFocus
|
autoFocus
|
||||||
inputRef={nameRef}
|
inputRef={nameRef}
|
||||||
id="name-textfield"
|
id="name-textfield"
|
||||||
|
@ -55,7 +54,7 @@ export default function WebauthnDeviceEditDialog(props: Props) {
|
||||||
}}
|
}}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoComplete="webauthn-name"
|
autoComplete="webauthn-name"
|
||||||
onKeyPress={(ev) => {
|
onKeyDown={(ev) => {
|
||||||
if (ev.key === "Enter") {
|
if (ev.key === "Enter") {
|
||||||
handleConfirm();
|
handleConfirm();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
@ -64,8 +63,8 @@ export default function WebauthnDeviceEditDialog(props: Props) {
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleCancel}>Cancel</Button>
|
<Button onClick={handleCancel}>{translate("Cancel")}</Button>
|
||||||
<Button onClick={handleConfirm}>Update</Button>
|
<Button onClick={handleConfirm}>{translate("Update")}</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import React, { Fragment, useState } from "react";
|
import React, { Fragment, useState } from "react";
|
||||||
|
|
||||||
|
import { Fingerprint } from "@mui/icons-material";
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
import EditIcon from "@mui/icons-material/Edit";
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
||||||
import KeyRoundedIcon from "@mui/icons-material/KeyRounded";
|
import { Box, Button, Paper, Stack, Tooltip, Typography } from "@mui/material";
|
||||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import LoadingButton from "@components/LoadingButton";
|
import LoadingButton from "@components/LoadingButton";
|
||||||
|
@ -18,8 +18,7 @@ import WebauthnDeviceEditDialog from "@views/Settings/TwoFactorAuthentication/We
|
||||||
interface Props {
|
interface Props {
|
||||||
index: number;
|
index: number;
|
||||||
device: WebauthnDevice;
|
device: WebauthnDevice;
|
||||||
handleDeviceEdit(index: number, device: WebauthnDevice): void;
|
handleEdit: () => void;
|
||||||
handleDeviceDelete(device: WebauthnDevice): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WebauthnDeviceItem(props: Props) {
|
export default function WebauthnDeviceItem(props: Props) {
|
||||||
|
@ -49,19 +48,21 @@ 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 the device"));
|
createErrorNotification(translate("You must be elevated to update Webauthn credentials"));
|
||||||
} else if (response.data.authentication) {
|
} 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 {
|
} else {
|
||||||
createErrorNotification(translate("There was a problem updating the device"));
|
createErrorNotification(translate("There was a problem updating the Webauthn credential"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
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) => {
|
const handleDelete = async (ok: boolean) => {
|
||||||
|
@ -79,78 +80,119 @@ 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 the device"));
|
createErrorNotification(translate("You must be elevated to delete Webauthn credentials"));
|
||||||
} else if (response.data.authentication) {
|
} 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 {
|
} else {
|
||||||
createErrorNotification(translate("There was a problem deleting the device"));
|
createErrorNotification(translate("There was a problem deleting the Webauthn credential"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
createSuccessNotification(translate("Successfully deleted the device"));
|
createSuccessNotification(translate("Successfully deleted the Webauthn credential"));
|
||||||
|
|
||||||
props.handleDeviceDelete(props.device);
|
props.handleEdit();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<WebauthnDeviceDetailsDialog
|
<Paper variant="outlined">
|
||||||
device={props.device}
|
<Box sx={{ p: 3 }}>
|
||||||
open={showDialogDetails}
|
<WebauthnDeviceDetailsDialog
|
||||||
handleClose={() => {
|
device={props.device}
|
||||||
setShowDialogDetails(false);
|
open={showDialogDetails}
|
||||||
}}
|
handleClose={() => {
|
||||||
/>
|
setShowDialogDetails(false);
|
||||||
<WebauthnDeviceEditDialog device={props.device} open={showDialogEdit} handleClose={handleEdit} />
|
}}
|
||||||
<WebauthnDeviceDeleteDialog device={props.device} open={showDialogDelete} handleClose={handleDelete} />
|
/>
|
||||||
<Stack direction="row" spacing={1} alignItems="center">
|
<WebauthnDeviceEditDialog device={props.device} open={showDialogEdit} handleClose={handleEdit} />
|
||||||
<KeyRoundedIcon fontSize="large" />
|
<WebauthnDeviceDeleteDialog
|
||||||
<Stack spacing={0} sx={{ minWidth: 400 }}>
|
device={props.device}
|
||||||
<Box>
|
open={showDialogDelete}
|
||||||
<Typography display="inline" sx={{ fontWeight: "bold" }}>
|
handleClose={handleDelete}
|
||||||
{props.device.description}
|
/>
|
||||||
</Typography>
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
<Typography
|
<Fingerprint fontSize="large" color={"warning"} />
|
||||||
display="inline"
|
<Stack spacing={0} sx={{ minWidth: 400 }}>
|
||||||
variant="body2"
|
<Box>
|
||||||
>{` (${props.device.attestation_type.toUpperCase()})`}</Typography>
|
<Typography display="inline" sx={{ fontWeight: "bold" }}>
|
||||||
</Box>
|
{props.device.description}
|
||||||
<Typography>Added {props.device.created_at.toString()}</Typography>
|
</Typography>
|
||||||
<Typography>
|
<Typography
|
||||||
{props.device.last_used_at === undefined
|
display="inline"
|
||||||
? translate("Never used")
|
variant="body2"
|
||||||
: "Last used " + props.device.last_used_at.toString()}
|
>{` (${props.device.attestation_type.toUpperCase()})`}</Typography>
|
||||||
</Typography>
|
</Box>
|
||||||
</Stack>
|
<Typography variant={"caption"}>
|
||||||
<Button
|
{translate("Added", {
|
||||||
variant="outlined"
|
when: new Date(props.device.created_at),
|
||||||
color="primary"
|
formatParams: {
|
||||||
startIcon={<InfoOutlinedIcon />}
|
when: {
|
||||||
onClick={() => setShowDialogDetails(true)}
|
hour: "numeric",
|
||||||
>
|
minute: "numeric",
|
||||||
{translate("Info")}
|
year: "numeric",
|
||||||
</Button>
|
month: "long",
|
||||||
<LoadingButton
|
day: "numeric",
|
||||||
loading={loadingEdit}
|
},
|
||||||
variant="outlined"
|
},
|
||||||
color="primary"
|
})}
|
||||||
startIcon={<EditIcon />}
|
</Typography>
|
||||||
onClick={() => setShowDialogEdit(true)}
|
<Typography variant={"caption"}>
|
||||||
>
|
{props.device.last_used_at === undefined
|
||||||
{translate("Edit")}
|
? translate("Never used")
|
||||||
</LoadingButton>
|
: translate("Last Used", {
|
||||||
<LoadingButton
|
when: new Date(props.device.last_used_at),
|
||||||
loading={loadingDelete}
|
formatParams: {
|
||||||
variant="outlined"
|
when: {
|
||||||
color="secondary"
|
hour: "numeric",
|
||||||
startIcon={<DeleteIcon />}
|
minute: "numeric",
|
||||||
onClick={() => setShowDialogDelete(true)}
|
year: "numeric",
|
||||||
>
|
month: "long",
|
||||||
{translate("Remove")}
|
day: "numeric",
|
||||||
</LoadingButton>
|
},
|
||||||
</Stack>
|
},
|
||||||
|
})}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Tooltip title={translate("Display extended information for this Webauthn credential")}>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<InfoOutlinedIcon />}
|
||||||
|
onClick={() => setShowDialogDetails(true)}
|
||||||
|
>
|
||||||
|
{translate("Info")}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title={translate("Edit information for this Webauthn credential")}>
|
||||||
|
<LoadingButton
|
||||||
|
loading={loadingEdit}
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<EditIcon />}
|
||||||
|
onClick={() => setShowDialogEdit(true)}
|
||||||
|
>
|
||||||
|
{translate("Edit")}
|
||||||
|
</LoadingButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title={translate("Remove this Webauthn credential")}>
|
||||||
|
<LoadingButton
|
||||||
|
loading={loadingDelete}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
startIcon={<DeleteIcon />}
|
||||||
|
onClick={() => setShowDialogDelete(true)}
|
||||||
|
>
|
||||||
|
{translate("Remove")}
|
||||||
|
</LoadingButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,28 @@
|
||||||
import React, { Fragment, MutableRefObject, useCallback, useEffect, useRef, useState } from "react";
|
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 makeStyles from "@mui/styles/makeStyles";
|
||||||
import { PublicKeyCredentialCreationOptionsJSON } from "@simplewebauthn/typescript-types";
|
import { PublicKeyCredentialCreationOptionsJSON } from "@simplewebauthn/typescript-types";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
import FixedTextField from "@components/FixedTextField";
|
|
||||||
import InformationIcon from "@components/InformationIcon";
|
import InformationIcon from "@components/InformationIcon";
|
||||||
import SuccessIcon from "@components/SuccessIcon";
|
import WebauthnRegisterIcon from "@components/WebauthnRegisterIcon";
|
||||||
import WebauthnTryIcon from "@components/WebauthnTryIcon";
|
|
||||||
import { SettingsRoute, SettingsTwoFactorAuthenticationSubRoute } from "@constants/Routes";
|
|
||||||
import { useNotifications } from "@hooks/NotificationsContext";
|
import { useNotifications } from "@hooks/NotificationsContext";
|
||||||
import LoginLayout from "@layouts/LoginLayout";
|
|
||||||
import {
|
import {
|
||||||
AttestationResult,
|
AttestationResult,
|
||||||
AttestationResultFailureString,
|
AttestationResultFailureString,
|
||||||
|
@ -24,25 +34,40 @@ import { finishRegistration, getAttestationCreationOptions, startWebauthnRegistr
|
||||||
const steps = ["Confirm device", "Choose name"];
|
const steps = ["Confirm device", "Choose name"];
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
est: AuthenticatorSelectionCriteria;
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
setCancelled: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RegisterWebauthn = function (props: Props) {
|
const WebauthnDeviceRegisterDialog = function (props: Props) {
|
||||||
const [state, setState] = useState(WebauthnTouchState.WaitTouch);
|
const { t: translate } = useTranslation("settings");
|
||||||
|
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const navigate = useNavigate();
|
|
||||||
const { t: translate } = useTranslation();
|
|
||||||
const { createErrorNotification } = useNotifications();
|
const { createErrorNotification } = useNotifications();
|
||||||
|
|
||||||
const [activeStep, setActiveStep] = React.useState(0);
|
const [state, setState] = useState(WebauthnTouchState.WaitTouch);
|
||||||
const [result, setResult] = React.useState(null as null | RegistrationResult);
|
const [activeStep, setActiveStep] = useState(0);
|
||||||
const [options, setOptions] = useState(null as null | PublicKeyCredentialCreationOptionsJSON);
|
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 [deviceName, setName] = useState("");
|
||||||
|
|
||||||
const nameRef = useRef() as MutableRefObject<HTMLInputElement>;
|
const nameRef = useRef() as MutableRefObject<HTMLInputElement>;
|
||||||
const [nameError, setNameError] = useState(false);
|
const [nameError, setNameError] = useState(false);
|
||||||
|
|
||||||
const handleBackClick = () => {
|
const resetStates = () => {
|
||||||
navigate(`${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`);
|
setState(WebauthnTouchState.WaitTouch);
|
||||||
|
setActiveStep(0);
|
||||||
|
setResult(null);
|
||||||
|
setOptions(null);
|
||||||
|
setTimeout(null);
|
||||||
|
setName("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
resetStates();
|
||||||
|
|
||||||
|
props.setCancelled();
|
||||||
};
|
};
|
||||||
|
|
||||||
const finishAttestation = async () => {
|
const finishAttestation = async () => {
|
||||||
|
@ -58,8 +83,7 @@ const RegisterWebauthn = function (props: Props) {
|
||||||
const res = await finishRegistration(result.response, deviceName);
|
const res = await finishRegistration(result.response, deviceName);
|
||||||
switch (res.status) {
|
switch (res.status) {
|
||||||
case AttestationResult.Success:
|
case AttestationResult.Success:
|
||||||
setActiveStep(2);
|
handleClose();
|
||||||
navigate(`${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`);
|
|
||||||
break;
|
break;
|
||||||
case AttestationResult.Failure:
|
case AttestationResult.Failure:
|
||||||
createErrorNotification(res.message);
|
createErrorNotification(res.message);
|
||||||
|
@ -71,7 +95,7 @@ const RegisterWebauthn = function (props: Props) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("start registration");
|
setTimeout(options.timeout ? options.timeout : null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setState(WebauthnTouchState.WaitTouch);
|
setState(WebauthnTouchState.WaitTouch);
|
||||||
|
@ -79,7 +103,7 @@ const RegisterWebauthn = function (props: Props) {
|
||||||
|
|
||||||
const res = await startWebauthnRegistration(options);
|
const res = await startWebauthnRegistration(options);
|
||||||
|
|
||||||
console.log("got response", res.result);
|
setTimeout(null);
|
||||||
|
|
||||||
if (res.result === AttestationResult.Success) {
|
if (res.result === AttestationResult.Success) {
|
||||||
if (res.response == null) {
|
if (res.response == null) {
|
||||||
|
@ -103,13 +127,29 @@ const RegisterWebauthn = function (props: Props) {
|
||||||
}, [options, createErrorNotification]);
|
}, [options, createErrorNotification]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (options !== null) {
|
if (state !== WebauthnTouchState.Failure || activeStep !== 0 || !props.open) {
|
||||||
startRegistration();
|
return;
|
||||||
}
|
}
|
||||||
}, [options, startRegistration]);
|
|
||||||
|
handleClose();
|
||||||
|
}, [props, state, activeStep, handleClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(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();
|
const res = await getAttestationCreationOptions();
|
||||||
if (res.status !== 200 || !res.options) {
|
if (res.status !== 200 || !res.options) {
|
||||||
createErrorNotification(
|
createErrorNotification(
|
||||||
|
@ -119,39 +159,31 @@ const RegisterWebauthn = function (props: Props) {
|
||||||
}
|
}
|
||||||
setOptions(res.options);
|
setOptions(res.options);
|
||||||
})();
|
})();
|
||||||
}, [setOptions, createErrorNotification]);
|
}, [setOptions, createErrorNotification, props.open, activeStep]);
|
||||||
|
|
||||||
function renderStep(step: number) {
|
function renderStep(step: number) {
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case 0:
|
case 0:
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className={styles.icon}>
|
<Box className={styles.icon}>
|
||||||
<WebauthnTryIcon onRetryClick={startRegistration} webauthnTouchState={state} />
|
{timeout !== null ? <WebauthnRegisterIcon timeout={timeout} /> : null}
|
||||||
</div>
|
</Box>
|
||||||
<Typography className={styles.instruction}>Touch the token on your security key</Typography>
|
<Typography className={styles.instruction}>
|
||||||
<Grid container spacing={1}>
|
{translate("Touch the token on your security key")}
|
||||||
<Grid item xs={12}>
|
</Typography>
|
||||||
<Stack direction="row" spacing={1} justifyContent="center">
|
|
||||||
<Button color="primary" onClick={handleBackClick}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
case 1:
|
case 1:
|
||||||
return (
|
return (
|
||||||
<div id="webauthn-registration-name">
|
<Box id="webauthn-registration-name">
|
||||||
<div className={styles.icon}>
|
<Box className={styles.icon}>
|
||||||
<InformationIcon />
|
<InformationIcon />
|
||||||
</div>
|
</Box>
|
||||||
<Typography className={styles.instruction}>Enter a name for this key</Typography>
|
<Typography className={styles.instruction}>{translate("Enter a name for this key")}</Typography>
|
||||||
<Grid container spacing={1}>
|
<Grid container spacing={1}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<FixedTextField
|
<TextField
|
||||||
// TODO (PR: #806, Issue: #511) potentially refactor
|
|
||||||
inputRef={nameRef}
|
inputRef={nameRef}
|
||||||
id="name-textfield"
|
id="name-textfield"
|
||||||
label={translate("Name")}
|
label={translate("Name")}
|
||||||
|
@ -159,18 +191,19 @@ const RegisterWebauthn = function (props: Props) {
|
||||||
required
|
required
|
||||||
value={deviceName}
|
value={deviceName}
|
||||||
error={nameError}
|
error={nameError}
|
||||||
fullWidth
|
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onChange={(v) => setName(v.target.value.substring(0, 30))}
|
onChange={(v) => setName(v.target.value.substring(0, 30))}
|
||||||
onFocus={() => setNameError(false)}
|
onFocus={() => setNameError(false)}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoComplete="webauthn-name"
|
autoComplete="webauthn-name"
|
||||||
onKeyPress={(ev) => {
|
onKeyDown={(ev) => {
|
||||||
if (ev.key === "Enter") {
|
if (ev.key === "Enter") {
|
||||||
if (!deviceName.length) {
|
if (!deviceName.length) {
|
||||||
setNameError(true);
|
setNameError(true);
|
||||||
} else {
|
} else {
|
||||||
finishAttestation();
|
(async () => {
|
||||||
|
await finishAttestation();
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
|
@ -178,35 +211,32 @@ const RegisterWebauthn = function (props: Props) {
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Stack direction="row" spacing={1} justifyContent="center">
|
<Stack direction="row" spacing={1} justifyContent="center" paddingTop={1}>
|
||||||
<Button color="primary" variant="outlined" onClick={startRegistration}>
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
<Button color="primary" variant="contained" onClick={finishAttestation}>
|
<Button color="primary" variant="contained" onClick={finishAttestation}>
|
||||||
Finish
|
{translate("Finish")}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</div>
|
</Box>
|
||||||
);
|
|
||||||
case 2:
|
|
||||||
return (
|
|
||||||
<div id="webauthn-registration-success">
|
|
||||||
<div className={styles.iconContainer}>
|
|
||||||
<SuccessIcon />
|
|
||||||
</div>
|
|
||||||
<Typography>{translate("Registration success")}</Typography>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOnClose = () => {
|
||||||
|
if (activeStep === 0 || !props.open) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginLayout title="Register Security Key">
|
<Dialog open={props.open} onClose={handleOnClose} maxWidth={"xs"} fullWidth={true}>
|
||||||
<Grid container>
|
<DialogTitle>{translate("Register Webauthn Credential (Security Key)")}</DialogTitle>
|
||||||
<Grid item xs={12} className={styles.methodContainer}>
|
<DialogContent>
|
||||||
<Box sx={{ width: "100%" }}>
|
<Grid container spacing={0} alignItems={"center"} justifyContent={"center"} textAlign={"center"}>
|
||||||
|
<Grid item xs={12}>
|
||||||
<Stepper activeStep={activeStep}>
|
<Stepper activeStep={activeStep}>
|
||||||
{steps.map((label, index) => {
|
{steps.map((label, index) => {
|
||||||
const stepProps: { completed?: boolean } = {};
|
const stepProps: { completed?: boolean } = {};
|
||||||
|
@ -215,38 +245,34 @@ const RegisterWebauthn = function (props: Props) {
|
||||||
} = {};
|
} = {};
|
||||||
return (
|
return (
|
||||||
<Step key={label} {...stepProps}>
|
<Step key={label} {...stepProps}>
|
||||||
<StepLabel {...labelProps}>{label}</StepLabel>
|
<StepLabel {...labelProps}>{translate(label)}</StepLabel>
|
||||||
</Step>
|
</Step>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Stepper>
|
</Stepper>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
{renderStep(activeStep)}
|
{renderStep(activeStep)}
|
||||||
</Box>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</DialogContent>
|
||||||
</LoginLayout>
|
<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) => ({
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
icon: {
|
icon: {
|
||||||
paddingTop: theme.spacing(4),
|
paddingTop: theme.spacing(4),
|
||||||
paddingBottom: theme.spacing(4),
|
paddingBottom: theme.spacing(4),
|
||||||
},
|
},
|
||||||
iconContainer: {
|
|
||||||
marginBottom: theme.spacing(2),
|
|
||||||
flex: "0 0 100%",
|
|
||||||
},
|
|
||||||
instruction: {
|
instruction: {
|
||||||
paddingBottom: theme.spacing(4),
|
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 { 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 { AutheliaState } from "@services/State";
|
||||||
import LoadingPage from "@views/LoadingPage/LoadingPage";
|
import LoadingPage from "@views/LoadingPage/LoadingPage";
|
||||||
|
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 {
|
||||||
|
@ -13,31 +13,49 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WebauthnDevices(props: Props) {
|
export default function WebauthnDevices(props: Props) {
|
||||||
const navigate = useNavigate();
|
const { t: translate } = useTranslation("settings");
|
||||||
|
|
||||||
const initiateRegistration = async (redirectRoute: string) => {
|
const [showWebauthnDeviceRegisterDialog, setShowWebauthnDeviceRegisterDialog] = useState<boolean>(false);
|
||||||
navigate(redirectRoute);
|
const [refreshState, setRefreshState] = useState<number>(0);
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddKeyButtonClick = () => {
|
const handleIncrementRefreshState = () => {
|
||||||
initiateRegistration(RegisterWebauthnRoute);
|
setRefreshState((refreshState) => refreshState + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
<WebauthnDeviceRegisterDialog
|
||||||
|
open={showWebauthnDeviceRegisterDialog}
|
||||||
|
onClose={() => {
|
||||||
|
handleIncrementRefreshState();
|
||||||
|
}}
|
||||||
|
setCancelled={() => {
|
||||||
|
setShowWebauthnDeviceRegisterDialog(false);
|
||||||
|
handleIncrementRefreshState();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Paper variant="outlined">
|
<Paper variant="outlined">
|
||||||
<Box sx={{ p: 3 }}>
|
<Box sx={{ p: 3 }}>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="h5">Webauthn Devices</Typography>
|
<Typography variant="h5">{translate("Webauthn Credentials")}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Button variant="outlined" color="primary" onClick={handleAddKeyButtonClick}>
|
<Button
|
||||||
{"Add new device"}
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
setShowWebauthnDeviceRegisterDialog(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{translate("Add Credential")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
<Suspense fallback={<LoadingPage />}>
|
<Suspense fallback={<LoadingPage />}>
|
||||||
<WebauthnDevicesStack />
|
<WebauthnDevicesStack
|
||||||
|
refreshState={refreshState}
|
||||||
|
incrementRefreshState={handleIncrementRefreshState}
|
||||||
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
@ -1,55 +1,40 @@
|
||||||
import React, { Fragment, useEffect, useState } from "react";
|
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 { 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;
|
||||||
|
incrementRefreshState: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
export default function WebauthnDevicesStack(props: Props) {
|
export default function WebauthnDevicesStack(props: Props) {
|
||||||
|
const { t: translate } = useTranslation("settings");
|
||||||
|
|
||||||
const [devices, setDevices] = useState<WebauthnDevice[]>([]);
|
const [devices, setDevices] = useState<WebauthnDevice[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async function () {
|
(async function () {
|
||||||
|
setDevices([]);
|
||||||
const devices = await getWebauthnDevices();
|
const devices = await getWebauthnDevices();
|
||||||
setDevices(devices);
|
setDevices(devices);
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, [props.refreshState]);
|
||||||
|
|
||||||
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));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{devices ? (
|
{devices.length !== 0 ? (
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
{devices.map((x, idx) => (
|
{devices.map((x, idx) => (
|
||||||
<WebauthnDeviceItem
|
<WebauthnDeviceItem key={idx} index={idx} device={x} handleEdit={props.incrementRefreshState} />
|
||||||
key={idx}
|
|
||||||
index={idx}
|
|
||||||
device={x}
|
|
||||||
handleDeviceEdit={handleEdit}
|
|
||||||
handleDeviceDelete={handleDelete}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<Typography>No Registered Webauthn Devices</Typography>
|
<Typography>{translate("No Registered Webauthn Credentials")}</Typography>
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue