mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
feat(web): i18n (#2697)
This adds support for i18n so that users may be presented a familiar language to the language the browser language they are using automatically. Currently supported languages: en, es. Co-authored-by: Amir Zarrinkafsh <nightah@me.com> Co-authored-by: James Elliott <james-d-elliott@users.noreply.github.com>
This commit is contained in:
parent
a7a2bc63fe
commit
db046b2d1c
|
@ -12,11 +12,15 @@
|
||||||
"@material-ui/styles": "4.11.4",
|
"@material-ui/styles": "4.11.4",
|
||||||
"axios": "0.25.0",
|
"axios": "0.25.0",
|
||||||
"classnames": "2.3.1",
|
"classnames": "2.3.1",
|
||||||
|
"i18next": "21.6.0",
|
||||||
|
"i18next-browser-languagedetector": "6.1.2",
|
||||||
|
"i18next-http-backend": "1.3.1",
|
||||||
"qrcode.react": "1.0.1",
|
"qrcode.react": "1.0.1",
|
||||||
"query-string": "7.1.0",
|
"query-string": "7.1.0",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-ga": "3.3.0",
|
"react-ga": "3.3.0",
|
||||||
|
"react-i18next": "11.14.3",
|
||||||
"react-loading": "2.0.3",
|
"react-loading": "2.0.3",
|
||||||
"react-otp-input": "2.4.0",
|
"react-otp-input": "2.4.0",
|
||||||
"react-router-dom": "6.2.1",
|
"react-router-dom": "6.2.1",
|
||||||
|
@ -77,6 +81,9 @@
|
||||||
"^@hooks/(.*)$": [
|
"^@hooks/(.*)$": [
|
||||||
"<rootDir>/src/hooks/$1"
|
"<rootDir>/src/hooks/$1"
|
||||||
],
|
],
|
||||||
|
"^@i18n/(.*)$": [
|
||||||
|
"<rootDir>/src/i18n/$1"
|
||||||
|
],
|
||||||
"^@layouts/(.*)$": [
|
"^@layouts/(.*)$": [
|
||||||
"<rootDir>/src/layouts/$1"
|
"<rootDir>/src/layouts/$1"
|
||||||
],
|
],
|
||||||
|
|
|
@ -33,6 +33,9 @@ specifiers:
|
||||||
eslint-plugin-react: 7.28.0
|
eslint-plugin-react: 7.28.0
|
||||||
eslint-plugin-react-hooks: 4.3.0
|
eslint-plugin-react-hooks: 4.3.0
|
||||||
husky: 7.0.4
|
husky: 7.0.4
|
||||||
|
i18next: 21.6.0
|
||||||
|
i18next-browser-languagedetector: 6.1.2
|
||||||
|
i18next-http-backend: 1.3.1
|
||||||
jest: 27.4.7
|
jest: 27.4.7
|
||||||
jest-transform-stub: 2.0.0
|
jest-transform-stub: 2.0.0
|
||||||
jest-watch-typeahead: 1.0.0
|
jest-watch-typeahead: 1.0.0
|
||||||
|
@ -42,6 +45,7 @@ specifiers:
|
||||||
react: 17.0.2
|
react: 17.0.2
|
||||||
react-dom: 17.0.2
|
react-dom: 17.0.2
|
||||||
react-ga: 3.3.0
|
react-ga: 3.3.0
|
||||||
|
react-i18next: 11.14.3
|
||||||
react-loading: 2.0.3
|
react-loading: 2.0.3
|
||||||
react-otp-input: 2.4.0
|
react-otp-input: 2.4.0
|
||||||
react-router-dom: 6.2.1
|
react-router-dom: 6.2.1
|
||||||
|
@ -64,11 +68,15 @@ dependencies:
|
||||||
'@material-ui/styles': 4.11.4_b3482aaf5744fc7c2aeb7941b0e0a78f
|
'@material-ui/styles': 4.11.4_b3482aaf5744fc7c2aeb7941b0e0a78f
|
||||||
axios: 0.25.0
|
axios: 0.25.0
|
||||||
classnames: 2.3.1
|
classnames: 2.3.1
|
||||||
|
i18next: 21.6.0
|
||||||
|
i18next-browser-languagedetector: 6.1.2
|
||||||
|
i18next-http-backend: 1.3.1
|
||||||
qrcode.react: 1.0.1_react@17.0.2
|
qrcode.react: 1.0.1_react@17.0.2
|
||||||
query-string: 7.1.0
|
query-string: 7.1.0
|
||||||
react: 17.0.2
|
react: 17.0.2
|
||||||
react-dom: 17.0.2_react@17.0.2
|
react-dom: 17.0.2_react@17.0.2
|
||||||
react-ga: 3.3.0_react@17.0.2
|
react-ga: 3.3.0_react@17.0.2
|
||||||
|
react-i18next: 11.14.3_i18next@21.6.0+react@17.0.2
|
||||||
react-loading: 2.0.3_react@17.0.2
|
react-loading: 2.0.3_react@17.0.2
|
||||||
react-otp-input: 2.4.0_react-dom@17.0.2+react@17.0.2
|
react-otp-input: 2.4.0_react-dom@17.0.2+react@17.0.2
|
||||||
react-router-dom: 6.2.1_react-dom@17.0.2+react@17.0.2
|
react-router-dom: 6.2.1_react-dom@17.0.2+react@17.0.2
|
||||||
|
@ -3677,6 +3685,12 @@ packages:
|
||||||
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
|
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/cross-fetch/3.1.4:
|
||||||
|
resolution: {integrity: sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==}
|
||||||
|
dependencies:
|
||||||
|
node-fetch: 2.6.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/cross-spawn/6.0.5:
|
/cross-spawn/6.0.5:
|
||||||
resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==}
|
resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==}
|
||||||
engines: {node: '>=4.8'}
|
engines: {node: '>=4.8'}
|
||||||
|
@ -5004,6 +5018,12 @@ packages:
|
||||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/html-parse-stringify/3.0.1:
|
||||||
|
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
||||||
|
dependencies:
|
||||||
|
void-elements: 3.1.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/http-proxy-agent/4.0.1:
|
/http-proxy-agent/4.0.1:
|
||||||
resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==}
|
resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
@ -5040,6 +5060,24 @@ packages:
|
||||||
resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==}
|
resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/i18next-browser-languagedetector/6.1.2:
|
||||||
|
resolution: {integrity: sha512-YDzIGHhMRvr7M+c8B3EQUKyiMBhfqox4o1qkFvt4QXuu5V2cxf74+NCr+VEkUuU0y+RwcupA238eeolW1Yn80g==}
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.16.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/i18next-http-backend/1.3.1:
|
||||||
|
resolution: {integrity: sha512-o79n4GBBRpl20hByC+ne/S1UaSZ4iGAn59Hu2TEZGjN0WLB72L7WrM39Cshziyrssp6MQfdI8wjToU2Q6kpSvA==}
|
||||||
|
dependencies:
|
||||||
|
cross-fetch: 3.1.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/i18next/21.6.0:
|
||||||
|
resolution: {integrity: sha512-RjNuACL35wWZgtkyMcjcCmK7R72u3P6jTNbGKzrvHGI9M0iK5Vn1DsBIwOByppaXLIbe0viJ79Nz2h8w1UwPoQ==}
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.16.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
/iconv-lite/0.4.24:
|
/iconv-lite/0.4.24:
|
||||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -6474,6 +6512,11 @@ packages:
|
||||||
resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
|
resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/node-fetch/2.6.1:
|
||||||
|
resolution: {integrity: sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==}
|
||||||
|
engines: {node: 4.x || >=6.0.0}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/node-int64/0.4.0:
|
/node-int64/0.4.0:
|
||||||
resolution: {integrity: sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=}
|
resolution: {integrity: sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -6951,6 +6994,18 @@ packages:
|
||||||
react: 17.0.2
|
react: 17.0.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-i18next/11.14.3_i18next@21.6.0+react@17.0.2:
|
||||||
|
resolution: {integrity: sha512-Hf2aanbKgYxPjG8ZdKr+PBz9sY6sxXuZWizxCYyJD2YzvJ0W9JTQcddVEjDaKyBoCyd3+5HTerdhc9ehFugc6g==}
|
||||||
|
peerDependencies:
|
||||||
|
i18next: '>= 19.0.0'
|
||||||
|
react: '>= 16.8.0'
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.16.3
|
||||||
|
html-parse-stringify: 3.0.1
|
||||||
|
i18next: 21.6.0
|
||||||
|
react: 17.0.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-is/16.13.1:
|
/react-is/16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
|
|
||||||
|
@ -8102,6 +8157,11 @@ packages:
|
||||||
fsevents: 2.3.2
|
fsevents: 2.3.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/void-elements/3.1.0:
|
||||||
|
resolution: {integrity: sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/w3c-hr-time/1.0.2:
|
/w3c-hr-time/1.0.2:
|
||||||
resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==}
|
resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
35
web/src/i18n/index.ts
Normal file
35
web/src/i18n/index.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import i18n from "i18next";
|
||||||
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
|
import XHR from "i18next-http-backend";
|
||||||
|
import { initReactI18next } from "react-i18next";
|
||||||
|
|
||||||
|
import langEn from "@i18n/locales/en.json";
|
||||||
|
import langEs from "@i18n/locales/es.json";
|
||||||
|
|
||||||
|
const resources = {
|
||||||
|
en: langEn,
|
||||||
|
es: langEs,
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
order: ["querystring", "navigator"],
|
||||||
|
lookupQuerystring: "lng",
|
||||||
|
};
|
||||||
|
|
||||||
|
i18n.use(XHR)
|
||||||
|
.use(LanguageDetector)
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
detection: options,
|
||||||
|
resources,
|
||||||
|
ns: [""],
|
||||||
|
defaultNS: "",
|
||||||
|
fallbackLng: "en",
|
||||||
|
supportedLngs: ["en", "es"],
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false,
|
||||||
|
},
|
||||||
|
debug: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
53
web/src/i18n/locales/en.json
Normal file
53
web/src/i18n/locales/en.json
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
{
|
||||||
|
"Portal": {
|
||||||
|
"An email has been sent to your address to complete the process": "An email has been sent to your address to complete the process.",
|
||||||
|
"Authenticated": "Authenticated",
|
||||||
|
"Cancel": "Cancel",
|
||||||
|
"Contact your administrator to register a device": "Contact your administrator to register a device.",
|
||||||
|
"Could not obtain user settings": "Could not obtain user settings",
|
||||||
|
"Done": "Done",
|
||||||
|
"Enter new password": "Enter new password",
|
||||||
|
"Enter one-time password": "Enter one-time password",
|
||||||
|
"Failed to register device, the provided link is expired or has already been used": "Failed to register device, the provided link is expired or has already been used",
|
||||||
|
"Hi": "Hi",
|
||||||
|
"Incorrect username or password": "Incorrect username or password.",
|
||||||
|
"Loading": "Loading",
|
||||||
|
"Logout": "Logout",
|
||||||
|
"Lost your device?": "Lost your device?",
|
||||||
|
"Methods": "Methods",
|
||||||
|
"Need Google Authenticator?": "Need Google Authenticator?",
|
||||||
|
"New password": "New password",
|
||||||
|
"No verification token provided": "No verification token provided",
|
||||||
|
"OTP Secret copied to clipboard": "OTP Secret copied to clipboard.",
|
||||||
|
"OTP URL copied to clipboard": "OTP URL copied to clipboard.",
|
||||||
|
"One-Time Password": "One-Time Password",
|
||||||
|
"Password has been reset": "Password has been reset.",
|
||||||
|
"Password": "Password",
|
||||||
|
"Passwords do not match": "Passwords do not match.",
|
||||||
|
"Push Notification": "Push Notification",
|
||||||
|
"Register device": "Register device",
|
||||||
|
"Register your first device by clicking on the link below": "Register your first device by clicking on the link below.",
|
||||||
|
"Remember me": "Remember me",
|
||||||
|
"Repeat new password": "Repeat new password",
|
||||||
|
"Reset password": "Reset password",
|
||||||
|
"Reset password?": "Reset password?",
|
||||||
|
"Reset": "Reset",
|
||||||
|
"Scan QR Code": "Scan QR Code",
|
||||||
|
"Secret": "Secret",
|
||||||
|
"Security Key - U2F": "Security Key - U2F",
|
||||||
|
"Select a Device": "Select a Device",
|
||||||
|
"Sign in": "Sign in",
|
||||||
|
"Sign out": "Sign out",
|
||||||
|
"The resource you're attempting to access requires two-factor authentication": "The resource you're attempting to access requires two-factor authentication.",
|
||||||
|
"There was a problem initiating the registration process": "There was a problem initiating the registration process",
|
||||||
|
"There was an issue completing the process. The verification token might have expired": "There was an issue completing the process. The verification token might have expired.",
|
||||||
|
"There was an issue initiating the password reset process": "There was an issue initiating the password reset process.",
|
||||||
|
"There was an issue resetting the password": "There was an issue resetting the password",
|
||||||
|
"There was an issue signing out": "There was an issue signing out",
|
||||||
|
"Time-based One-Time Password": "Time-based One-Time Password",
|
||||||
|
"Username": "Username",
|
||||||
|
"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're being signed out and redirected": "You're being signed out and redirected",
|
||||||
|
"Your supplied password does not meet the password policy requirements": "Your supplied password does not meet the password policy requirements."
|
||||||
|
}
|
||||||
|
}
|
53
web/src/i18n/locales/es.json
Normal file
53
web/src/i18n/locales/es.json
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
{
|
||||||
|
"Portal": {
|
||||||
|
"An email has been sent to your address to complete the process": "Un correo ha sido enviado a su cuenta para completar el proceso",
|
||||||
|
"Authenticated": "Autenticado",
|
||||||
|
"Cancel": "Cancelar",
|
||||||
|
"Contact your administrator to register a device": "Contacte a su administrador para registrar un dispositivo.",
|
||||||
|
"Could not obtain user settings": "Error al obtener configuración de usuario",
|
||||||
|
"Done": "Hecho",
|
||||||
|
"Enter new password": "Ingrese una nueva contraseña",
|
||||||
|
"Enter one-time password": "Ingrese contraseña de un solo uso (OTP)",
|
||||||
|
"Failed to register device, the provided link is expired or has already been used": "Error al registrar dispositivo, el link expiró o ya ha sido utilizado",
|
||||||
|
"Hi": "Hola",
|
||||||
|
"Incorrect username or password": "Usuario y/o contraseña incorrectos",
|
||||||
|
"Loading": "Cargando",
|
||||||
|
"Logout": "Cerrar Sesión",
|
||||||
|
"Lost your device?": "Perdió su dispositivo?",
|
||||||
|
"Methods": "Métodos",
|
||||||
|
"Need Google Authenticator?": "Necesita Google Authenticator?",
|
||||||
|
"New password": "Nueva contraseña",
|
||||||
|
"No verification token provided": "No se ha recibido el token de verificación",
|
||||||
|
"OTP Secret copied to clipboard": "La clave OTP ha sido copiada al portapapeles",
|
||||||
|
"OTP URL copied to clipboard": "la URL OTP ha sido copiada al portapapeles.",
|
||||||
|
"One-Time Password": "Contraseña de un solo uso (OTP)",
|
||||||
|
"Password has been reset": "La contraseña ha sido restablecida.",
|
||||||
|
"Password": "Contraseña",
|
||||||
|
"Passwords do not match": "Las contraseñas no coinciden.",
|
||||||
|
"Push Notification": "Notificaciones Push",
|
||||||
|
"Register device": "Registrar Dispositivo",
|
||||||
|
"Register your first device by clicking on the link below": "Registre su primer dispositivo, haciendo click en el siguiente link.",
|
||||||
|
"Remember me": "Recordarme",
|
||||||
|
"Repeat new password": "Repetir la contraseña",
|
||||||
|
"Reset password": "Restablecer Contraseña",
|
||||||
|
"Reset password?": "Olvidé mi contraseña",
|
||||||
|
"Reset": "Restablecer",
|
||||||
|
"Scan QR Code": "Escanear Código QR",
|
||||||
|
"Secret": "Secreto",
|
||||||
|
"Security Key - U2F": "Llave de Seguridad - U2F",
|
||||||
|
"Select a Device": "Seleccionar Dispositivo",
|
||||||
|
"Sign in": "Iniciar Sesión",
|
||||||
|
"Sign out": "Cerrar Sesión",
|
||||||
|
"The resource you're attempting to access requires two-factor authentication": "El recurso que intenta alcanzar requiere un segundo factor de autenticación (2FA).",
|
||||||
|
"There was a problem initiating the registration process": "Ocurrió un problema al iniciar el proceso de registración",
|
||||||
|
"There was an issue completing the process. The verification token might have expired": "Ocurrió un problema mientras se completaba el proceso. El token de verificación pudo haber expirado.",
|
||||||
|
"There was an issue initiating the password reset process": "Ha ocurrido un error al iniciar el proceso de proceso de restauración de contraseña.",
|
||||||
|
"There was an issue resetting the password": "Ocurrió un error al intentar restablecer la contraseña",
|
||||||
|
"There was an issue signing out": "Ocurrió un error al intentar cerrar sesión",
|
||||||
|
"Time-based One-Time Password": "Contraseña de uso único - OTP",
|
||||||
|
"Username": "Usuario",
|
||||||
|
"You must open the link from the same device and browser that initiated the registration process": "Debe abrir el link desde el mismo dispositivo y navegador desde el que inició el proceso de registración",
|
||||||
|
"You're being signed out and redirected": "Cerrando Sesión y redirigiendo",
|
||||||
|
"Your supplied password does not meet the password policy requirements": "La contraseña suministrada no cumple con los requerimientos de la política de contraseñas"
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import ReactDOM from "react-dom";
|
||||||
import "@root/index.css";
|
import "@root/index.css";
|
||||||
import App from "@root/App";
|
import App from "@root/App";
|
||||||
import * as serviceWorker from "@root/serviceWorker";
|
import * as serviceWorker from "@root/serviceWorker";
|
||||||
|
import "./i18n/index.ts";
|
||||||
|
|
||||||
ReactDOM.render(<App />, document.getElementById("root"));
|
ReactDOM.render(<App />, document.getElementById("root"));
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { makeStyles, Typography, Button, IconButton, Link, CircularProgress, Tex
|
||||||
import { red } from "@material-ui/core/colors";
|
import { red } from "@material-ui/core/colors";
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
import QRCode from "qrcode.react";
|
import QRCode from "qrcode.react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import AppStoreBadges from "@components/AppStoreBadges";
|
import AppStoreBadges from "@components/AppStoreBadges";
|
||||||
|
@ -26,6 +27,7 @@ const RegisterOneTimePassword = function () {
|
||||||
const { createSuccessNotification, createErrorNotification } = useNotifications();
|
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("Portal");
|
||||||
|
|
||||||
// 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.
|
||||||
|
@ -49,17 +51,19 @@ const RegisterOneTimePassword = function () {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
if ((err as Error).message.includes("Request failed with status code 403")) {
|
if ((err as Error).message.includes("Request failed with status code 403")) {
|
||||||
createErrorNotification(
|
createErrorNotification(
|
||||||
"You must open the link from the same device and browser that initiated the registration process",
|
translate(
|
||||||
|
"You must open the link from the same device and browser that initiated the registration process",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
createErrorNotification(
|
createErrorNotification(
|
||||||
"Failed to register device, the provided link is expired or has already been used",
|
translate("Failed to register device, the provided link is expired or has already been used"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
setHasErrored(true);
|
setHasErrored(true);
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}, [processToken, createErrorNotification]);
|
}, [processToken, createErrorNotification, translate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
completeRegistrationProcess();
|
completeRegistrationProcess();
|
||||||
|
@ -82,10 +86,12 @@ const RegisterOneTimePassword = function () {
|
||||||
const qrcodeFuzzyStyle = isLoading || hasErrored ? style.fuzzy : undefined;
|
const qrcodeFuzzyStyle = isLoading || hasErrored ? style.fuzzy : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginLayout title="Scan QR Code">
|
<LoginLayout title={translate("Scan QR Code")}>
|
||||||
<div className={style.root}>
|
<div className={style.root}>
|
||||||
<div className={style.googleAuthenticator}>
|
<div className={style.googleAuthenticator}>
|
||||||
<Typography className={style.googleAuthenticatorText}>Need Google Authenticator?</Typography>
|
<Typography className={style.googleAuthenticatorText}>
|
||||||
|
{translate("Need Google Authenticator?")}
|
||||||
|
</Typography>
|
||||||
<AppStoreBadges
|
<AppStoreBadges
|
||||||
iconSize={128}
|
iconSize={128}
|
||||||
targetBlank
|
targetBlank
|
||||||
|
@ -105,7 +111,7 @@ const RegisterOneTimePassword = function () {
|
||||||
{secretURL !== "empty" ? (
|
{secretURL !== "empty" ? (
|
||||||
<TextField
|
<TextField
|
||||||
id="secret-url"
|
id="secret-url"
|
||||||
label="Secret"
|
label={translate("Secret")}
|
||||||
className={style.secret}
|
className={style.secret}
|
||||||
value={secretURL}
|
value={secretURL}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
|
@ -113,8 +119,12 @@ const RegisterOneTimePassword = function () {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{secretBase32 ? SecretButton(secretBase32, "OTP Secret copied to clipboard.", faKey) : null}
|
{secretBase32
|
||||||
{secretURL !== "empty" ? SecretButton(secretURL, "OTP URL copied to clipboard.", faCopy) : null}
|
? SecretButton(secretBase32, translate("OTP Secret copied to clipboard"), faKey)
|
||||||
|
: null}
|
||||||
|
{secretURL !== "empty"
|
||||||
|
? SecretButton(secretURL, translate("OTP URL copied to clipboard"), faCopy)
|
||||||
|
: null}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
@ -123,7 +133,7 @@ const RegisterOneTimePassword = function () {
|
||||||
onClick={handleDoneClick}
|
onClick={handleDoneClick}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
Done
|
{translate("Done")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</LoginLayout>
|
</LoginLayout>
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useTheme, Typography, Grid } from "@material-ui/core";
|
import { useTheme, Typography, Grid } from "@material-ui/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import ReactLoading from "react-loading";
|
import ReactLoading from "react-loading";
|
||||||
|
|
||||||
const LoadingPage = function () {
|
const LoadingPage = function () {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { t: translate } = useTranslation("Portal");
|
||||||
return (
|
return (
|
||||||
<Grid container alignItems="center" justifyContent="center" style={{ minHeight: "100vh" }}>
|
<Grid container alignItems="center" justifyContent="center" style={{ minHeight: "100vh" }}>
|
||||||
<Grid item style={{ textAlign: "center", display: "inline-block" }}>
|
<Grid item style={{ textAlign: "center", display: "inline-block" }}>
|
||||||
<ReactLoading width={64} height={64} color={theme.custom.loadingBar} type="bars" />
|
<ReactLoading width={64} height={64} color={theme.custom.loadingBar} type="bars" />
|
||||||
<Typography>Loading...</Typography>
|
<Typography>{translate("Loading")}...</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Typography, makeStyles } from "@material-ui/core";
|
import { Typography, makeStyles } from "@material-ui/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import SuccessIcon from "@components/SuccessIcon";
|
import SuccessIcon from "@components/SuccessIcon";
|
||||||
|
|
||||||
const Authenticated = function () {
|
const Authenticated = function () {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
const { t: translate } = useTranslation("Portal");
|
||||||
return (
|
return (
|
||||||
<div id="authenticated-stage">
|
<div id="authenticated-stage">
|
||||||
<div className={classes.iconContainer}>
|
<div className={classes.iconContainer}>
|
||||||
<SuccessIcon />
|
<SuccessIcon />
|
||||||
</div>
|
</div>
|
||||||
<Typography>Authenticated</Typography>
|
<Typography>{translate("Authenticated")}</Typography>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Grid, makeStyles, Button } from "@material-ui/core";
|
import { Grid, makeStyles, Button } from "@material-ui/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { LogoutRoute as SignOutRoute } from "@constants/Routes";
|
import { LogoutRoute as SignOutRoute } from "@constants/Routes";
|
||||||
|
@ -14,17 +15,18 @@ export interface Props {
|
||||||
const AuthenticatedView = function (props: Props) {
|
const AuthenticatedView = function (props: Props) {
|
||||||
const style = useStyles();
|
const style = useStyles();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { t: translate } = useTranslation("Portal");
|
||||||
|
|
||||||
const handleLogoutClick = () => {
|
const handleLogoutClick = () => {
|
||||||
navigate(SignOutRoute);
|
navigate(SignOutRoute);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginLayout id="authenticated-stage" title={`Hi ${props.name}`} showBrand>
|
<LoginLayout id="authenticated-stage" title={`${translate("Hi")} ${props.name}`} showBrand>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Button color="secondary" onClick={handleLogoutClick} id="logout-button">
|
<Button color="secondary" onClick={handleLogoutClick} id="logout-button">
|
||||||
Logout
|
{translate("Logout")}
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} className={style.mainContainer}>
|
<Grid item xs={12} className={style.mainContainer}>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { makeStyles, Grid, Button, FormControlLabel, Checkbox, Link } from "@material-ui/core";
|
import { makeStyles, Grid, Button, FormControlLabel, Checkbox, Link } from "@material-ui/core";
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import FixedTextField from "@components/FixedTextField";
|
import FixedTextField from "@components/FixedTextField";
|
||||||
|
@ -37,6 +38,7 @@ const FirstFactorForm = function (props: Props) {
|
||||||
// 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("Portal");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeout = setTimeout(() => usernameRef.current.focus(), 10);
|
const timeout = setTimeout(() => usernameRef.current.focus(), 10);
|
||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout);
|
||||||
|
@ -66,7 +68,7 @@ const FirstFactorForm = function (props: Props) {
|
||||||
props.onAuthenticationSuccess(res ? res.redirect : undefined);
|
props.onAuthenticationSuccess(res ? res.redirect : undefined);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createErrorNotification("Incorrect username or password.");
|
createErrorNotification(translate("Incorrect username or password"));
|
||||||
props.onAuthenticationFailure();
|
props.onAuthenticationFailure();
|
||||||
setPassword("");
|
setPassword("");
|
||||||
passwordRef.current.focus();
|
passwordRef.current.focus();
|
||||||
|
@ -78,14 +80,14 @@ const FirstFactorForm = function (props: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginLayout id="first-factor-stage" title="Sign in" showBrand>
|
<LoginLayout id="first-factor-stage" title={translate("Sign in")} showBrand>
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<FixedTextField
|
<FixedTextField
|
||||||
// TODO (PR: #806, Issue: #511) potentially refactor
|
// TODO (PR: #806, Issue: #511) potentially refactor
|
||||||
inputRef={usernameRef}
|
inputRef={usernameRef}
|
||||||
id="username-textfield"
|
id="username-textfield"
|
||||||
label="Username"
|
label={translate("Username")}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
required
|
required
|
||||||
value={username}
|
value={username}
|
||||||
|
@ -115,7 +117,7 @@ const FirstFactorForm = function (props: Props) {
|
||||||
// TODO (PR: #806, Issue: #511) potentially refactor
|
// TODO (PR: #806, Issue: #511) potentially refactor
|
||||||
inputRef={passwordRef}
|
inputRef={passwordRef}
|
||||||
id="password-textfield"
|
id="password-textfield"
|
||||||
label="Password"
|
label={translate("Password")}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
required
|
required
|
||||||
fullWidth
|
fullWidth
|
||||||
|
@ -163,7 +165,7 @@ const FirstFactorForm = function (props: Props) {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
className={style.rememberMe}
|
className={style.rememberMe}
|
||||||
label="Remember me"
|
label={translate("Remember me")}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -176,7 +178,7 @@ const FirstFactorForm = function (props: Props) {
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={handleSignIn}
|
onClick={handleSignIn}
|
||||||
>
|
>
|
||||||
Sign in
|
{translate("Sign in")}
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
{props.resetPassword ? (
|
{props.resetPassword ? (
|
||||||
|
@ -187,7 +189,7 @@ const FirstFactorForm = function (props: Props) {
|
||||||
onClick={handleResetPasswordClick}
|
onClick={handleResetPasswordClick}
|
||||||
className={style.resetLink}
|
className={style.resetLink}
|
||||||
>
|
>
|
||||||
Reset password?
|
{translate("Reset password?")}
|
||||||
</Link>
|
</Link>
|
||||||
</Grid>
|
</Grid>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React, { ReactNode, Fragment } from "react";
|
||||||
|
|
||||||
import { makeStyles, Typography, Link, useTheme } from "@material-ui/core";
|
import { makeStyles, Typography, Link, useTheme } from "@material-ui/core";
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import InformationIcon from "@components/InformationIcon";
|
import InformationIcon from "@components/InformationIcon";
|
||||||
import Authenticated from "@views/LoginPortal/Authenticated";
|
import Authenticated from "@views/LoginPortal/Authenticated";
|
||||||
|
@ -27,12 +28,13 @@ export interface Props {
|
||||||
|
|
||||||
const DefaultMethodContainer = function (props: Props) {
|
const DefaultMethodContainer = function (props: Props) {
|
||||||
const style = useStyles();
|
const style = useStyles();
|
||||||
|
const { t: translate } = useTranslation("Portal");
|
||||||
const registerMessage = props.registered
|
const registerMessage = props.registered
|
||||||
? props.title === "Push Notification"
|
? props.title === "Push Notification"
|
||||||
? ""
|
? ""
|
||||||
: "Lost your device?"
|
: translate("Lost your device?")
|
||||||
: "Register device";
|
: translate("Register device");
|
||||||
const selectMessage = "Select a Device";
|
const selectMessage = translate("Select a Device");
|
||||||
|
|
||||||
let container: ReactNode;
|
let container: ReactNode;
|
||||||
let stateClass: string = "";
|
let stateClass: string = "";
|
||||||
|
@ -95,6 +97,7 @@ interface NotRegisteredContainerProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function NotRegisteredContainer(props: NotRegisteredContainerProps) {
|
function NotRegisteredContainer(props: NotRegisteredContainerProps) {
|
||||||
|
const { t: translate } = useTranslation("Portal");
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
@ -102,14 +105,14 @@ function NotRegisteredContainer(props: NotRegisteredContainerProps) {
|
||||||
<InformationIcon />
|
<InformationIcon />
|
||||||
</div>
|
</div>
|
||||||
<Typography style={{ color: "#5858ff" }}>
|
<Typography style={{ color: "#5858ff" }}>
|
||||||
The resource you're attempting to access requires two-factor authentication.
|
{translate("The resource you're attempting to access requires two-factor authentication")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography style={{ color: "#5858ff" }}>
|
<Typography style={{ color: "#5858ff" }}>
|
||||||
{props.title === "Push Notification"
|
{props.title === "Push Notification"
|
||||||
? props.duoSelfEnrollment
|
? props.duoSelfEnrollment
|
||||||
? "Register your first device by clicking on the link below."
|
? translate("Register your first device by clicking on the link below")
|
||||||
: "Contact your administrator to register a device."
|
: translate("Contact your administrator to register a device.")
|
||||||
: "Register your first device by clicking on the link below."}
|
: translate("Register your first device by clicking on the link below")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
Typography,
|
Typography,
|
||||||
useTheme,
|
useTheme,
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import FingerTouchIcon from "@components/FingerTouchIcon";
|
import FingerTouchIcon from "@components/FingerTouchIcon";
|
||||||
import PushNotificationIcon from "@components/PushNotificationIcon";
|
import PushNotificationIcon from "@components/PushNotificationIcon";
|
||||||
|
@ -28,6 +29,7 @@ export interface Props {
|
||||||
const MethodSelectionDialog = function (props: Props) {
|
const MethodSelectionDialog = function (props: Props) {
|
||||||
const style = useStyles();
|
const style = useStyles();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { t: translate } = useTranslation("Portal");
|
||||||
|
|
||||||
const pieChartIcon = (
|
const pieChartIcon = (
|
||||||
<TimerIcon width={24} height={24} period={15} color={theme.palette.primary.main} backgroundColor={"white"} />
|
<TimerIcon width={24} height={24} period={15} color={theme.palette.primary.main} backgroundColor={"white"} />
|
||||||
|
@ -40,7 +42,7 @@ const MethodSelectionDialog = function (props: Props) {
|
||||||
{props.methods.has(SecondFactorMethod.TOTP) ? (
|
{props.methods.has(SecondFactorMethod.TOTP) ? (
|
||||||
<MethodItem
|
<MethodItem
|
||||||
id="one-time-password-option"
|
id="one-time-password-option"
|
||||||
method="Time-based One-Time Password"
|
method={translate("Time-based One-Time Password")}
|
||||||
icon={pieChartIcon}
|
icon={pieChartIcon}
|
||||||
onClick={() => props.onClick(SecondFactorMethod.TOTP)}
|
onClick={() => props.onClick(SecondFactorMethod.TOTP)}
|
||||||
/>
|
/>
|
||||||
|
@ -48,7 +50,7 @@ const MethodSelectionDialog = function (props: Props) {
|
||||||
{props.methods.has(SecondFactorMethod.U2F) && props.u2fSupported ? (
|
{props.methods.has(SecondFactorMethod.U2F) && props.u2fSupported ? (
|
||||||
<MethodItem
|
<MethodItem
|
||||||
id="security-key-option"
|
id="security-key-option"
|
||||||
method="Security Key - U2F"
|
method={translate("Security Key - U2F")}
|
||||||
icon={<FingerTouchIcon size={32} />}
|
icon={<FingerTouchIcon size={32} />}
|
||||||
onClick={() => props.onClick(SecondFactorMethod.U2F)}
|
onClick={() => props.onClick(SecondFactorMethod.U2F)}
|
||||||
/>
|
/>
|
||||||
|
@ -56,7 +58,7 @@ const MethodSelectionDialog = function (props: Props) {
|
||||||
{props.methods.has(SecondFactorMethod.MobilePush) ? (
|
{props.methods.has(SecondFactorMethod.MobilePush) ? (
|
||||||
<MethodItem
|
<MethodItem
|
||||||
id="push-notification-option"
|
id="push-notification-option"
|
||||||
method="Push Notification"
|
method={translate("Push Notification")}
|
||||||
icon={<PushNotificationIcon width={32} height={32} />}
|
icon={<PushNotificationIcon width={32} height={32} />}
|
||||||
onClick={() => props.onClick(SecondFactorMethod.MobilePush)}
|
onClick={() => props.onClick(SecondFactorMethod.MobilePush)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { useRedirectionURL } from "@hooks/RedirectionURL";
|
import { useRedirectionURL } from "@hooks/RedirectionURL";
|
||||||
import { useUserInfoTOTPConfiguration } from "@hooks/UserInfoTOTPConfiguration";
|
import { useUserInfoTOTPConfiguration } from "@hooks/UserInfoTOTPConfiguration";
|
||||||
import { completeTOTPSignIn } from "@services/OneTimePassword";
|
import { completeTOTPSignIn } from "@services/OneTimePassword";
|
||||||
|
@ -31,20 +33,20 @@ const OneTimePasswordMethod = function (props: Props) {
|
||||||
props.authenticationLevel === AuthenticationLevel.TwoFactor ? State.Success : State.Idle,
|
props.authenticationLevel === AuthenticationLevel.TwoFactor ? State.Success : State.Idle,
|
||||||
);
|
);
|
||||||
const redirectionURL = useRedirectionURL();
|
const redirectionURL = useRedirectionURL();
|
||||||
|
const { t: translate } = useTranslation("Portal");
|
||||||
|
|
||||||
const { onSignInSuccess, onSignInError } = props;
|
const { onSignInSuccess, onSignInError } = props;
|
||||||
const onSignInErrorCallback = useRef(onSignInError).current;
|
const onSignInErrorCallback = useRef(onSignInError).current;
|
||||||
const onSignInSuccessCallback = useRef(onSignInSuccess).current;
|
const onSignInSuccessCallback = useRef(onSignInSuccess).current;
|
||||||
|
|
||||||
const [resp, fetch, , err] = useUserInfoTOTPConfiguration();
|
const [resp, fetch, , err] = useUserInfoTOTPConfiguration();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
onSignInErrorCallback(new Error("Could not obtain user settings"));
|
onSignInErrorCallback(new Error(translate("Could not obtain user settings")));
|
||||||
setState(State.Failure);
|
setState(State.Failure);
|
||||||
}
|
}
|
||||||
}, [onSignInErrorCallback, err]);
|
}, [onSignInErrorCallback, err, translate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.registered && props.authenticationLevel === AuthenticationLevel.OneFactor) {
|
if (props.registered && props.authenticationLevel === AuthenticationLevel.OneFactor) {
|
||||||
|
@ -105,8 +107,8 @@ const OneTimePasswordMethod = function (props: Props) {
|
||||||
return (
|
return (
|
||||||
<MethodContainer
|
<MethodContainer
|
||||||
id={props.id}
|
id={props.id}
|
||||||
title="One-Time Password"
|
title={translate("One-Time Password")}
|
||||||
explanation="Enter one-time password"
|
explanation={translate("Enter one-time password")}
|
||||||
duoSelfEnrollment={false}
|
duoSelfEnrollment={false}
|
||||||
registered={props.registered}
|
registered={props.registered}
|
||||||
state={methodState}
|
state={methodState}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
import { Grid, makeStyles, Button } from "@material-ui/core";
|
import { Grid, makeStyles, Button } from "@material-ui/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { Route, Routes, useNavigate } from "react-router-dom";
|
import { Route, Routes, useNavigate } from "react-router-dom";
|
||||||
import u2fApi from "u2f-api";
|
import u2fApi from "u2f-api";
|
||||||
|
|
||||||
|
@ -23,7 +24,7 @@ import OneTimePasswordMethod from "@views/LoginPortal/SecondFactor/OneTimePasswo
|
||||||
import PushNotificationMethod from "@views/LoginPortal/SecondFactor/PushNotificationMethod";
|
import PushNotificationMethod from "@views/LoginPortal/SecondFactor/PushNotificationMethod";
|
||||||
import SecurityKeyMethod from "@views/LoginPortal/SecondFactor/SecurityKeyMethod";
|
import SecurityKeyMethod from "@views/LoginPortal/SecondFactor/SecurityKeyMethod";
|
||||||
|
|
||||||
const EMAIL_SENT_NOTIFICATION = "An email has been sent to your address to complete the process.";
|
const EMAIL_SENT_NOTIFICATION = "An email has been sent to your address to complete the process";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
authenticationLevel: AuthenticationLevel;
|
authenticationLevel: AuthenticationLevel;
|
||||||
|
@ -42,6 +43,7 @@ const SecondFactorForm = function (props: Props) {
|
||||||
const { createInfoNotification, createErrorNotification } = useNotifications();
|
const { createInfoNotification, createErrorNotification } = useNotifications();
|
||||||
const [registrationInProgress, setRegistrationInProgress] = useState(false);
|
const [registrationInProgress, setRegistrationInProgress] = useState(false);
|
||||||
const [u2fSupported, setU2fSupported] = useState(false);
|
const [u2fSupported, setU2fSupported] = useState(false);
|
||||||
|
const { t: translate } = useTranslation("Portal");
|
||||||
|
|
||||||
// Check that U2F is supported.
|
// Check that U2F is supported.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -59,10 +61,10 @@ const SecondFactorForm = function (props: Props) {
|
||||||
setRegistrationInProgress(true);
|
setRegistrationInProgress(true);
|
||||||
try {
|
try {
|
||||||
await initiateRegistrationFunc();
|
await initiateRegistrationFunc();
|
||||||
createInfoNotification(EMAIL_SENT_NOTIFICATION);
|
createInfoNotification(translate(EMAIL_SENT_NOTIFICATION));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createErrorNotification("There was a problem initiating the registration process");
|
createErrorNotification(translate("There was a problem initiating the registration process"));
|
||||||
}
|
}
|
||||||
setRegistrationInProgress(false);
|
setRegistrationInProgress(false);
|
||||||
};
|
};
|
||||||
|
@ -88,7 +90,7 @@ const SecondFactorForm = function (props: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginLayout id="second-factor-stage" title={`Hi ${props.userInfo.display_name}`} showBrand>
|
<LoginLayout id="second-factor-stage" title={`${translate("Hi")} ${props.userInfo.display_name}`} showBrand>
|
||||||
<MethodSelectionDialog
|
<MethodSelectionDialog
|
||||||
open={methodSelectionOpen}
|
open={methodSelectionOpen}
|
||||||
methods={props.configuration.available_methods}
|
methods={props.configuration.available_methods}
|
||||||
|
@ -99,11 +101,11 @@ const SecondFactorForm = function (props: Props) {
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Button color="secondary" onClick={handleLogoutClick} id="logout-button">
|
<Button color="secondary" onClick={handleLogoutClick} id="logout-button">
|
||||||
Logout
|
{translate("Logout")}
|
||||||
</Button>
|
</Button>
|
||||||
{" | "}
|
{" | "}
|
||||||
<Button color="secondary" onClick={handleMethodSelectionClick} id="methods-button">
|
<Button color="secondary" onClick={handleMethodSelectionClick} id="methods-button">
|
||||||
Methods
|
{translate("Methods")}
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} className={style.methodContainer}>
|
<Grid item xs={12} className={style.methodContainer}>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useEffect, useCallback, useState } from "react";
|
import React, { useEffect, useCallback, useState } from "react";
|
||||||
|
|
||||||
import { Typography, makeStyles } from "@material-ui/core";
|
import { Typography, makeStyles } from "@material-ui/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { Navigate } from "react-router-dom";
|
import { Navigate } from "react-router-dom";
|
||||||
|
|
||||||
import { FirstFactorRoute } from "@constants/Routes";
|
import { FirstFactorRoute } from "@constants/Routes";
|
||||||
|
@ -21,6 +22,7 @@ const SignOut = function (props: Props) {
|
||||||
const redirector = useRedirector();
|
const redirector = useRedirector();
|
||||||
const [timedOut, setTimedOut] = useState(false);
|
const [timedOut, setTimedOut] = useState(false);
|
||||||
const [safeRedirect, setSafeRedirect] = useState(false);
|
const [safeRedirect, setSafeRedirect] = useState(false);
|
||||||
|
const { t: translate } = useTranslation("Portal");
|
||||||
|
|
||||||
const doSignOut = useCallback(async () => {
|
const doSignOut = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -36,9 +38,9 @@ const SignOut = function (props: Props) {
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createErrorNotification("There was an issue signing out");
|
createErrorNotification(translate("There was an issue signing out"));
|
||||||
}
|
}
|
||||||
}, [createErrorNotification, redirectionURL, setSafeRedirect, setTimedOut, mounted]);
|
}, [createErrorNotification, redirectionURL, setSafeRedirect, setTimedOut, mounted, translate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
doSignOut();
|
doSignOut();
|
||||||
|
@ -53,8 +55,8 @@ const SignOut = function (props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginLayout title="Sign out">
|
<LoginLayout title={translate("Sign out")}>
|
||||||
<Typography className={style.typo}>You're being signed out and redirected...</Typography>
|
<Typography className={style.typo}>{translate("You're being signed out and redirected")}...</Typography>
|
||||||
</LoginLayout>
|
</LoginLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import { Grid, Button, makeStyles } from "@material-ui/core";
|
import { Grid, Button, makeStyles } from "@material-ui/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import FixedTextField from "@components/FixedTextField";
|
import FixedTextField from "@components/FixedTextField";
|
||||||
|
@ -15,6 +16,7 @@ const ResetPasswordStep1 = function () {
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const { createInfoNotification, createErrorNotification } = useNotifications();
|
const { createInfoNotification, createErrorNotification } = useNotifications();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { t: translate } = useTranslation("Portal");
|
||||||
|
|
||||||
const doInitiateResetPasswordProcess = async () => {
|
const doInitiateResetPasswordProcess = async () => {
|
||||||
if (username === "") {
|
if (username === "") {
|
||||||
|
@ -24,9 +26,9 @@ const ResetPasswordStep1 = function () {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await initiateResetPasswordProcess(username);
|
await initiateResetPasswordProcess(username);
|
||||||
createInfoNotification("An email has been sent to your address to complete the process.");
|
createInfoNotification(translate("An email has been sent to your address to complete the process"));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
createErrorNotification("There was an issue initiating the password reset process.");
|
createErrorNotification(translate("There was an issue initiating the password reset process"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -39,12 +41,12 @@ const ResetPasswordStep1 = function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginLayout title="Reset password" id="reset-password-step1-stage">
|
<LoginLayout title={translate("Reset password")} id="reset-password-step1-stage">
|
||||||
<Grid container className={style.root} spacing={2}>
|
<Grid container className={style.root} spacing={2}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<FixedTextField
|
<FixedTextField
|
||||||
id="username-textfield"
|
id="username-textfield"
|
||||||
label="Username"
|
label={translate("Username")}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
fullWidth
|
fullWidth
|
||||||
error={error}
|
error={error}
|
||||||
|
@ -60,7 +62,7 @@ const ResetPasswordStep1 = function () {
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={6}>
|
<Grid item xs={6}>
|
||||||
<Button id="reset-button" variant="contained" color="primary" fullWidth onClick={handleResetClick}>
|
<Button id="reset-button" variant="contained" color="primary" fullWidth onClick={handleResetClick}>
|
||||||
Reset
|
{translate("Reset")}
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={6}>
|
<Grid item xs={6}>
|
||||||
|
@ -71,7 +73,7 @@ const ResetPasswordStep1 = function () {
|
||||||
fullWidth
|
fullWidth
|
||||||
onClick={handleCancelClick}
|
onClick={handleCancelClick}
|
||||||
>
|
>
|
||||||
Cancel
|
{translate("Cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React, { useState, useCallback, useEffect } from "react";
|
||||||
|
|
||||||
import { Grid, Button, makeStyles } from "@material-ui/core";
|
import { Grid, Button, makeStyles } from "@material-ui/core";
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import FixedTextField from "@components/FixedTextField";
|
import FixedTextField from "@components/FixedTextField";
|
||||||
|
@ -20,6 +21,7 @@ const ResetPasswordStep2 = function () {
|
||||||
const [errorPassword1, setErrorPassword1] = useState(false);
|
const [errorPassword1, setErrorPassword1] = useState(false);
|
||||||
const [errorPassword2, setErrorPassword2] = useState(false);
|
const [errorPassword2, setErrorPassword2] = useState(false);
|
||||||
const { createSuccessNotification, createErrorNotification } = useNotifications();
|
const { createSuccessNotification, createErrorNotification } = useNotifications();
|
||||||
|
const { t: translate } = useTranslation("Portal");
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
// 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.
|
||||||
|
@ -28,7 +30,7 @@ const ResetPasswordStep2 = function () {
|
||||||
const completeProcess = useCallback(async () => {
|
const completeProcess = useCallback(async () => {
|
||||||
if (!processToken) {
|
if (!processToken) {
|
||||||
setFormDisabled(true);
|
setFormDisabled(true);
|
||||||
createErrorNotification("No verification token provided");
|
createErrorNotification(translate("No verification token provided"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,11 +41,11 @@ const ResetPasswordStep2 = function () {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createErrorNotification(
|
createErrorNotification(
|
||||||
"There was an issue completing the process. The verification token might have expired.",
|
translate("There was an issue completing the process. The verification token might have expired"),
|
||||||
);
|
);
|
||||||
setFormDisabled(true);
|
setFormDisabled(true);
|
||||||
}
|
}
|
||||||
}, [processToken, createErrorNotification]);
|
}, [processToken, createErrorNotification, translate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
completeProcess();
|
completeProcess();
|
||||||
|
@ -62,21 +64,23 @@ const ResetPasswordStep2 = function () {
|
||||||
if (password1 !== password2) {
|
if (password1 !== password2) {
|
||||||
setErrorPassword1(true);
|
setErrorPassword1(true);
|
||||||
setErrorPassword2(true);
|
setErrorPassword2(true);
|
||||||
createErrorNotification("Passwords do not match.");
|
createErrorNotification(translate("Passwords do not match"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await resetPassword(password1);
|
await resetPassword(password1);
|
||||||
createSuccessNotification("Password has been reset.");
|
createSuccessNotification(translate("Password has been reset"));
|
||||||
setTimeout(() => navigate(FirstFactorRoute), 1500);
|
setTimeout(() => navigate(FirstFactorRoute), 1500);
|
||||||
setFormDisabled(true);
|
setFormDisabled(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
if ((err as Error).message.includes("0000052D.")) {
|
if ((err as Error).message.includes("0000052D.")) {
|
||||||
createErrorNotification("Your supplied password does not meet the password policy requirements.");
|
createErrorNotification(
|
||||||
|
translate("Your supplied password does not meet the password policy requirements"),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
createErrorNotification("There was an issue resetting the password.");
|
createErrorNotification(translate("There was an issue resetting the password"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -86,12 +90,12 @@ const ResetPasswordStep2 = function () {
|
||||||
const handleCancelClick = () => navigate(FirstFactorRoute);
|
const handleCancelClick = () => navigate(FirstFactorRoute);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginLayout title="Enter new password" id="reset-password-step2-stage">
|
<LoginLayout title={translate("Enter new password")} id="reset-password-step2-stage">
|
||||||
<Grid container className={style.root} spacing={2}>
|
<Grid container className={style.root} spacing={2}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<FixedTextField
|
<FixedTextField
|
||||||
id="password1-textfield"
|
id="password1-textfield"
|
||||||
label="New password"
|
label={translate("New password")}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
type="password"
|
type="password"
|
||||||
value={password1}
|
value={password1}
|
||||||
|
@ -105,7 +109,7 @@ const ResetPasswordStep2 = function () {
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<FixedTextField
|
<FixedTextField
|
||||||
id="password2-textfield"
|
id="password2-textfield"
|
||||||
label="Repeat new password"
|
label={translate("Repeat new password")}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
type="password"
|
type="password"
|
||||||
disabled={formDisabled}
|
disabled={formDisabled}
|
||||||
|
@ -132,7 +136,7 @@ const ResetPasswordStep2 = function () {
|
||||||
onClick={handleResetClick}
|
onClick={handleResetClick}
|
||||||
className={style.fullWidth}
|
className={style.fullWidth}
|
||||||
>
|
>
|
||||||
Reset
|
{translate("Reset")}
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={6}>
|
<Grid item xs={6}>
|
||||||
|
@ -144,7 +148,7 @@ const ResetPasswordStep2 = function () {
|
||||||
onClick={handleCancelClick}
|
onClick={handleCancelClick}
|
||||||
className={style.fullWidth}
|
className={style.fullWidth}
|
||||||
>
|
>
|
||||||
Cancel
|
{translate("Cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
"@components/*": ["components/*"],
|
"@components/*": ["components/*"],
|
||||||
"@constants/*": ["constants/*"],
|
"@constants/*": ["constants/*"],
|
||||||
"@hooks/*": ["hooks/*"],
|
"@hooks/*": ["hooks/*"],
|
||||||
|
"@i18n/*": ["i18n/*"],
|
||||||
"@layouts/*": ["layouts/*"],
|
"@layouts/*": ["layouts/*"],
|
||||||
"@models/*": ["models/*"],
|
"@models/*": ["models/*"],
|
||||||
"@services/*": ["services/*"],
|
"@services/*": ["services/*"],
|
||||||
|
|
Loading…
Reference in New Issue
Block a user