feat(web): i18n asset overrides (#3040)

This allows overriding translation files in folders with lowercase RFC5646 / BCP47 Format language codes. This also fixes an issues where languages which don't expressly match the language code specified due to having a variant will also match the existing codes.

Co-authored-by: Amir Zarrinkafsh <nightah@me.com>
pull/3107/head^2
James Elliott 2022-04-04 12:15:26 +10:00 committed by GitHub
parent ee9ce27ccf
commit aac4c4772c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 442 additions and 311 deletions

View File

@ -34,15 +34,15 @@ logged for users.
### 4.30.0 ### 4.30.0
The following changes occurred in 4.30.0: The following changes occurred in 4.30.0:
|Previous Key |New Key | | Previous Key | New Key |
|:-----------:|:--------------------:| |:-------------:|:----------------------:|
|host |server.host | | host | server.host |
|port |server.port | | port | server.port |
|tls_key |server.tls.key | | tls_key | server.tls.key |
|tls_cert |server.tls.certificate| | tls_cert | server.tls.certificate |
|log_level |log.level | | log_level | log.level |
|log_file_path|log.file_path | | log_file_path | log.file_path |
|log_format |log.format | | log_format | log.format |
_**Please Note:** you can no longer define secrets for providers that you are not using. For example if you're using the _**Please Note:** you can no longer define secrets for providers that you are not using. For example if you're using the
[filesystem notifier](./notifier/filesystem.md) you must ensure that the `AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE` [filesystem notifier](./notifier/filesystem.md) you must ensure that the `AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE`

View File

@ -108,13 +108,15 @@ Example:
```console ```console
/config/assets/ /config/assets/
├── favicon.ico ├── favicon.ico
└── logo.png ├── logo.png
└── locales/<lang>[-[variant]]/<namespace>.json
``` ```
|Asset |File name| | Asset | File name |
|:-----:|:---------------:| |:-------:|:-------------:|
|Favicon|favicon.ico | | Favicon | favicon.ico |
|Logo |logo.png | | Logo | logo.png |
| locales | see [locales] |
### read_buffer_size ### read_buffer_size
<div markdown="1"> <div markdown="1">
@ -241,4 +243,44 @@ you're able to tune these individually depending on your needs.
### Asset Overrides ### Asset Overrides
If replacing the Logo for your Authelia portal it is recommended to upload a transparent PNG of your desired logo. If replacing the Logo for your Authelia portal it is recommended to upload a transparent PNG of your desired logo.
Authelia will automatically resize the logo to an appropriate size to present in the frontend. Authelia will automatically resize the logo to an appropriate size to present in the frontend.
#### locales
The locales folder holds folders of internationalization locales. This folder can be utilized to override these locales.
They are the names of locales that are returned by the `navigator.langauge` ECMAScript command. These are generally
those in the [RFC5646 / BCP47 Format](https://datatracker.ietf.org/doc/html/rfc5646) except the name normalized to
lowercase for consistency ease; for example the `en-US` locale should be in the directory `en-us`.
Each directory has json files which you can explore the format of in the
[internal/server/locales](https://github.com/authelia/authelia/tree/master/internal/server/locales) directory on
GitHub. The important part is the key names you wish to override. Each file represents a translation namespace. The list
of current namespaces are below:
| Namespace | Purpose |
|:---------:|:-------------------:|
| portal | Portal translations |
A full example for the `en-US` locale for the portal namespace is `locales/en-us/portal.json`.
Languages in browsers are supported in two forms. In their language only form such as `en` for English, and in their
variant form such as `en-AU` for English (Australian). If a user has the browser language `en-AU` we automatically load
the `en` and `en-AU` languages, where any keys in the `en-AU` language take precedence over the `en` language, and the
translations for the `en` language only applying when a translation from `en-AU` is not available.
List of supported languages and variants:
| Description | Language | Additional Variants | Location |
|:-----------:|:--------:|:-------------------:|:----------------------:|
| English | en | N/A | locales/en/portal.json |
| Spanish | es | N/A | locales/es/portal.json |
| German | de | N/A | locales/de/portal.json |
_**Important Note** Currently users can only override languages that already exist in this list either by overriding
the language itself, or adding a variant form of that language. If you'd like support for another language feel free
to make a PR. We also encourage people to make PR's for variants where the difference in the variants is important._
_**Important Note** Overriding these files will not guarantee any form of stability. Users who planning to utilize these
overrides should either check for changes to the files in the
[en](https://github.com/authelia/authelia/tree/master/internal/server/locales/en) translation prior to upgrading or PR
their translation to ensure it is maintained._

View File

@ -2,28 +2,27 @@ package middlewares
import ( import (
"os" "os"
"strings" "path/filepath"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/utils"
) )
// AssetOverrideMiddleware allows overriding and serving of specific embedded assets from disk. // AssetOverrideMiddleware allows overriding and serving of specific embedded assets from disk.
func AssetOverrideMiddleware(assetPath string, next fasthttp.RequestHandler) fasthttp.RequestHandler { func AssetOverrideMiddleware(root string, strip int, next fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) { return func(ctx *fasthttp.RequestCtx) {
uri := string(ctx.RequestURI()) if root == "" {
file := uri[strings.LastIndex(uri, "/")+1:]
if assetPath != "" && utils.IsStringInSlice(file, validOverrideAssets) {
_, err := os.Stat(assetPath + file)
if err != nil {
next(ctx)
} else {
fasthttp.FSHandler(assetPath, strings.Count(uri, "/")-1)(ctx)
}
} else {
next(ctx) next(ctx)
return
} }
_, err := os.Stat(filepath.Join(root, string(fasthttp.NewPathSlashesStripper(strip)(ctx))))
if err != nil {
next(ctx)
return
}
fasthttp.FSHandler(root, strip)(ctx)
} }
} }

View File

@ -57,6 +57,5 @@ const (
) )
var protoHostSeparator = []byte("://") var protoHostSeparator = []byte("://")
var validOverrideAssets = []string{"favicon.ico", "logo.png"}
var errPasswordPolicyNoMet = errors.New("the supplied password does not met the security policy") var errPasswordPolicyNoMet = errors.New("the supplied password does not met the security policy")

View File

@ -0,0 +1,87 @@
package server
import (
"embed"
"errors"
"fmt"
"io/fs"
"net/http"
"github.com/valyala/fasthttp"
"github.com/valyala/fasthttp/fasthttpadaptor"
"github.com/authelia/authelia/v4/internal/utils"
)
//go:embed locales
var locales embed.FS
//go:embed public_html
var assets embed.FS
func newPublicHTMLEmbeddedHandler() fasthttp.RequestHandler {
embeddedPath, _ := fs.Sub(assets, "public_html")
return fasthttpadaptor.NewFastHTTPHandler(http.FileServer(http.FS(embeddedPath)))
}
func newLocalesEmbeddedHandler() (handler fasthttp.RequestHandler) {
var languages []string
entries, err := locales.ReadDir("locales")
if err == nil {
for _, entry := range entries {
if entry.IsDir() && len(entry.Name()) == 2 {
languages = append(languages, entry.Name())
}
}
}
return func(ctx *fasthttp.RequestCtx) {
var (
language, variant, locale, namespace string
)
language = ctx.UserValue("language").(string)
namespace = ctx.UserValue("namespace").(string)
locale = language
if v := ctx.UserValue("variant"); v != nil {
variant = v.(string)
locale = fmt.Sprintf("%s-%s", language, locale)
}
var data []byte
if data, err = locales.ReadFile(fmt.Sprintf("locales/%s/%s.json", locale, namespace)); err != nil {
if variant != "" && utils.IsStringInSliceFold(language, languages) {
data = []byte("{}")
}
if len(data) == 0 {
hfsHandleErr(ctx, err)
return
}
}
ctx.SetContentType("application/json")
ctx.SetBody(data)
}
}
func hfsHandleErr(ctx *fasthttp.RequestCtx, err error) {
switch {
case errors.Is(err, fs.ErrNotExist):
writeStatus(ctx, fasthttp.StatusNotFound)
case errors.Is(err, fs.ErrPermission):
writeStatus(ctx, fasthttp.StatusForbidden)
default:
writeStatus(ctx, fasthttp.StatusInternalServerError)
}
}
func writeStatus(ctx *fasthttp.RequestCtx, status int) {
ctx.SetStatusCode(status)
ctx.SetBodyString(fmt.Sprintf("%d %s", status, fasthttp.StatusMessage(status)))
}

View File

@ -9,7 +9,7 @@ const (
) )
var ( var (
rootFiles = []string{"favicon.ico", "manifest.json", "robots.txt"} rootFiles = []string{"manifest.json", "robots.txt"}
swaggerFiles = []string{ swaggerFiles = []string{
"favicon-16x16.png", "favicon-16x16.png",
"favicon-32x32.png", "favicon-32x32.png",

View File

@ -0,0 +1,58 @@
{
"An email has been sent to your address to complete the process": "Es wurde eine E-Mail an Ihre Adresse geschickt, um den Vorgang abzuschließen.",
"Authenticated": "Authentifiziert",
"Cancel": "Abbrechen",
"Contact your administrator to register a device": "Wenden Sie sich an Ihren Administrator, um ein Gerät zu registrieren.",
"Could not obtain user settings": "Benutzereinstellungen konnten nicht abgerufen werden",
"Done": "Erledigt",
"Enter new password": "Neues Passwort eingeben",
"Enter one-time password": "Einmal-Passwort eingeben",
"Failed to register device, the provided link is expired or has already been used": "Gerät konnte nicht registriert werden, der angegebene Link ist abgelaufen oder wurde bereits verwendet",
"Hi": "Hallo",
"Incorrect username or password": "Falscher Benutzername oder falsches Passwort.",
"Loading": "Laden",
"Logout": "Abmelden",
"Lost your device?": "Haben Sie Ihr Gerät verloren?",
"Methods": "Verfahren",
"Need Google Authenticator?": "Benötigen Sie Google Authenticator?",
"New password": "Neues Passwort",
"No verification token provided": "Kein Verifizierungs-Token vorhanden",
"OTP Secret copied to clipboard": "OTP Secret in die Zwischenablage kopiert.",
"OTP URL copied to clipboard": "OTP-URL in die Zwischenablage kopiert.",
"One-Time Password": "One-Time-Passwort",
"Password has been reset": "Das Passwort wurde zurückgesetzt.",
"Password": "Passwort",
"Passwords do not match": "Die Passwörter stimmen nicht überein.",
"Push Notification": "Push-Benachrichtigung",
"Register device": "Gerät registrieren",
"Register your first device by clicking on the link below": "Registrieren Sie Ihr erstes Gerät, indem Sie auf den unten stehenden Link klicken.",
"Remember me": "Angemeldet bleiben",
"Repeat new password": "Neues Passwort wiederholen",
"Reset password": "Passwort zurücksetzen",
"Reset password?": "Passwort zurücksetzen?",
"Reset": "Zurücksetzen",
"Scan QR Code": "QR-Code scannen",
"Secret": "Geheimnis",
"Security Key - WebAuthN": "Sicherheitsschlüssel - WebAuthN",
"Select a Device": "Gerät auswählen",
"Sign in": "Anmelden",
"Sign out": "Abmelden",
"The resource you're attempting to access requires two-factor authentication": "Die Ressource, auf die Sie zuzugreifen versuchen, erfordert eine Zwei-Faktor-Authentifizierung.",
"There was a problem initiating the registration process": "Es gab ein Problem beim Starten des Registrierungsprozesses.",
"There was an issue completing the process. The verification token might have expired": "Es gab ein Problem beim Abschluss des Prozesses. Das Verifizierungs-Token könnte abgelaufen sein.",
"There was an issue initiating the password reset process": "Es gab ein Problem beim beim Starten des Passwortrücksetzprozesses.",
"There was an issue resetting the password": "Es gab ein Problem beim Zurücksetzen des Passworts",
"There was an issue signing out": "Es gab ein Problem bei der Abmeldung",
"Time-based One-Time Password": "Zeitbasiertes One-Time-Passwort",
"Username": "Benutzername",
"You must open the link from the same device and browser that initiated the registration process": "Sie müssen den Link mit demselben Gerät und demselben Browser öffnen, mit dem Sie den Registrierungsprozess gestartet haben.",
"You're being signed out and redirected": "Sie werden abgemeldet und umgeleitet",
"Your supplied password does not meet the password policy requirements": "Ihr angegebenes Passwort entspricht nicht den Anforderungen der Passwortrichtlinie.",
"Use OpenID to verify your identity": "Verwenden Sie OpenID, um Ihre Identität zu überprüfen",
"Access your display name": "Zugriff auf Ihren Anzeigenamen",
"Access your group membership": "Zugriff auf Ihre Gruppenmitgliedschaft",
"Access your email addresses": "Zugriff auf Ihre E-Mail-Adressen",
"Accept": "Annehmen",
"Deny": "Ablehnen",
"The above application is requesting the following permissions": "Die oben genannte Anwendung bittet um die folgenden Berechtigungen"
}

View File

@ -0,0 +1,65 @@
{
"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 - WebAuthN": "Security Key - WebAuthN",
"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.",
"Use OpenID to verify your identity": "Use OpenID to verify your identity",
"Access your display name": "Access your display name",
"Access your group membership": "Access your group membership",
"Access your email addresses": "Access your email addresses",
"Accept": "Accept",
"Deny": "Deny",
"The above application is requesting the following permissions": "The above application is requesting the following permissions",
"The password does not meet the password policy": "The password does not meet the password policy",
"Must have at least one lowercase letter": "Must have at least one lowercase letter",
"Must have at least one UPPERCASE letter": "Must have at least one UPPERCASE letter",
"Must have at least one number": "Must have at least one number",
"Must have at least one special character": "Must have at least one special character",
"Must be at least {{len}} characters in length": "Must be at least {{len}} characters in length",
"Must not be more than {{len}} characters in length": "Must not be more than {{len}} characters in length"
}

View File

@ -0,0 +1,65 @@
{
"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 - WebAuthN": "Llave de Seguridad - WebAuthN",
"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",
"Use OpenID to verify your identity": "Utilizar OpenID para verificar su identidad",
"Access your display name": "Acceso a su nombre",
"Access your group membership": "Acceso a su(s) grupo(s)",
"Access your email addresses": "Acceso a su dirección de correo",
"Accept": "Aceptar",
"Deny": "Denegar",
"The above application is requesting the following permissions": "La aplicación solicita los siguientes permisos",
"The password does not meet the password policy": "La contraseña no cumple con la política de contraseñas",
"Must have at least one lowercase letter": "Debe contener al menos una letra minúscula",
"Must have at least one UPPERCASE letter": "Debe contener al menos una letra MAYUSCULA",
"Must have at least one number": "Debe contener al menos un número",
"Must have at least one special character": "Debe contener al menos un caracter especial",
"Must be at least {{len}} characters in length": "La longitud mínima es de {{len}} caracteres",
"Must not be more than {{len}} characters in length": "La longitud máxima es de {{len}} caracteres"
}

View File

@ -1,10 +1,7 @@
package server package server
import ( import (
"embed"
"io/fs"
"net" "net"
"net/http"
"os" "os"
"strconv" "strconv"
"time" "time"
@ -13,7 +10,6 @@ import (
"github.com/fasthttp/router" "github.com/fasthttp/router"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
"github.com/valyala/fasthttp/expvarhandler" "github.com/valyala/fasthttp/expvarhandler"
"github.com/valyala/fasthttp/fasthttpadaptor"
"github.com/valyala/fasthttp/pprofhandler" "github.com/valyala/fasthttp/pprofhandler"
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
@ -23,9 +19,6 @@ import (
"github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/middlewares"
) )
//go:embed public_html
var assets embed.FS
func registerRoutes(configuration schema.Configuration, providers middlewares.Providers) fasthttp.RequestHandler { func registerRoutes(configuration schema.Configuration, providers middlewares.Providers) fasthttp.RequestHandler {
autheliaMiddleware := middlewares.AutheliaMiddleware(configuration, providers) autheliaMiddleware := middlewares.AutheliaMiddleware(configuration, providers)
rememberMe := strconv.FormatBool(configuration.Session.RememberMeDuration != schema.RememberMeDisabled) rememberMe := strconv.FormatBool(configuration.Session.RememberMeDuration != schema.RememberMeDisabled)
@ -36,8 +29,8 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr
duoSelfEnrollment = strconv.FormatBool(configuration.DuoAPI.EnableSelfEnrollment) duoSelfEnrollment = strconv.FormatBool(configuration.DuoAPI.EnableSelfEnrollment)
} }
embeddedPath, _ := fs.Sub(assets, "public_html") handlerPublicHTML := newPublicHTMLEmbeddedHandler()
embeddedFS := fasthttpadaptor.NewFastHTTPHandler(http.FileServer(http.FS(embeddedPath))) handlerLocales := newLocalesEmbeddedHandler()
https := configuration.Server.TLS.Key != "" && configuration.Server.TLS.Certificate != "" https := configuration.Server.TLS.Key != "" && configuration.Server.TLS.Certificate != ""
@ -50,17 +43,22 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr
r.OPTIONS("/", autheliaMiddleware(handleOPTIONS)) r.OPTIONS("/", autheliaMiddleware(handleOPTIONS))
for _, f := range rootFiles { for _, f := range rootFiles {
r.GET("/"+f, middlewares.AssetOverrideMiddleware(configuration.Server.AssetPath, embeddedFS)) r.GET("/"+f, handlerPublicHTML)
} }
r.GET("/api/", autheliaMiddleware(serveSwaggerHandler)) r.GET("/api/", autheliaMiddleware(serveSwaggerHandler))
r.GET("/api/"+apiFile, autheliaMiddleware(serveSwaggerAPIHandler)) r.GET("/api/"+apiFile, autheliaMiddleware(serveSwaggerAPIHandler))
for _, file := range swaggerFiles { for _, file := range swaggerFiles {
r.GET("/api/"+file, embeddedFS) r.GET("/api/"+file, handlerPublicHTML)
} }
r.GET("/static/{filepath:*}", middlewares.AssetOverrideMiddleware(configuration.Server.AssetPath, embeddedFS)) r.GET("/favicon.ico", middlewares.AssetOverrideMiddleware(configuration.Server.AssetPath, 0, handlerPublicHTML))
r.GET("/static/media/logo.png", middlewares.AssetOverrideMiddleware(configuration.Server.AssetPath, 2, handlerPublicHTML))
r.GET("/static/{filepath:*}", handlerPublicHTML)
r.GET("/locales/{language:[a-z]{1,3}}-{variant:[a-z0-9-]+}/{namespace:[a-z]+}.json", middlewares.AssetOverrideMiddleware(configuration.Server.AssetPath, 0, handlerLocales))
r.GET("/locales/{language:[a-z]{1,3}}/{namespace:[a-z]+}.json", middlewares.AssetOverrideMiddleware(configuration.Server.AssetPath, 0, handlerLocales))
r.GET("/api/health", autheliaMiddleware(handlers.HealthGet)) r.GET("/api/health", autheliaMiddleware(handlers.HealthGet))
r.GET("/api/state", autheliaMiddleware(handlers.StateGet)) r.GET("/api/state", autheliaMiddleware(handlers.StateGet))

View File

@ -43,7 +43,7 @@ func ServeTemplatedFile(publicDir, file, assetPath, duoSelfEnrollment, rememberM
logoOverride := f logoOverride := f
if assetPath != "" { if assetPath != "" {
if _, err := os.Stat(assetPath + logoFile); err == nil { if _, err := os.Stat(filepath.Join(assetPath, logoFile)); err == nil {
logoOverride = t logoOverride = t
} }
} }

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect, Suspense } from "react";
import { config as faConfig } from "@fortawesome/fontawesome-svg-core"; import { config as faConfig } from "@fortawesome/fontawesome-svg-core";
import { CssBaseline, ThemeProvider } from "@material-ui/core"; import { CssBaseline, ThemeProvider } from "@material-ui/core";
@ -21,6 +21,7 @@ import { getBasePath } from "@utils/BasePath";
import { getDuoSelfEnrollment, getRememberMe, getResetPassword, getTheme } from "@utils/Configuration"; import { getDuoSelfEnrollment, getRememberMe, getResetPassword, getTheme } from "@utils/Configuration";
import RegisterOneTimePassword from "@views/DeviceRegistration/RegisterOneTimePassword"; import RegisterOneTimePassword from "@views/DeviceRegistration/RegisterOneTimePassword";
import RegisterWebauthn from "@views/DeviceRegistration/RegisterWebauthn"; import RegisterWebauthn from "@views/DeviceRegistration/RegisterWebauthn";
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";
import SignOut from "@views/LoginPortal/SignOut/SignOut"; import SignOut from "@views/LoginPortal/SignOut/SignOut";
@ -60,30 +61,32 @@ const App: React.FC = () => {
}, []); }, []);
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline /> <Suspense fallback={<BaseLoadingPage message={"Loading"} />}>
<NotificationsContext.Provider value={{ notification, setNotification }}> <CssBaseline />
<Router basename={getBasePath()}> <NotificationsContext.Provider value={{ notification, setNotification }}>
<NotificationBar onClose={() => setNotification(null)} /> <Router basename={getBasePath()}>
<Routes> <NotificationBar onClose={() => setNotification(null)} />
<Route path={ResetPasswordStep1Route} element={<ResetPasswordStep1 />} /> <Routes>
<Route path={ResetPasswordStep2Route} element={<ResetPasswordStep2 />} /> <Route path={ResetPasswordStep1Route} element={<ResetPasswordStep1 />} />
<Route path={RegisterWebauthnRoute} element={<RegisterWebauthn />} /> <Route path={ResetPasswordStep2Route} element={<ResetPasswordStep2 />} />
<Route path={RegisterOneTimePasswordRoute} element={<RegisterOneTimePassword />} /> <Route path={RegisterWebauthnRoute} element={<RegisterWebauthn />} />
<Route path={LogoutRoute} element={<SignOut />} /> <Route path={RegisterOneTimePasswordRoute} element={<RegisterOneTimePassword />} />
<Route path={ConsentRoute} element={<ConsentView />} /> <Route path={LogoutRoute} element={<SignOut />} />
<Route <Route path={ConsentRoute} element={<ConsentView />} />
path={`${IndexRoute}*`} <Route
element={ path={`${IndexRoute}*`}
<LoginPortal element={
duoSelfEnrollment={getDuoSelfEnrollment()} <LoginPortal
rememberMe={getRememberMe()} duoSelfEnrollment={getDuoSelfEnrollment()}
resetPassword={getResetPassword()} rememberMe={getRememberMe()}
/> resetPassword={getResetPassword()}
} />
/> }
</Routes> />
</Router> </Routes>
</NotificationsContext.Provider> </Router>
</NotificationsContext.Provider>
</Suspense>
</ThemeProvider> </ThemeProvider>
); );
}; };

View File

@ -1,33 +1,28 @@
import i18n from "i18next"; import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector"; import LanguageDetector from "i18next-browser-languagedetector";
import XHR from "i18next-http-backend"; import Backend from "i18next-http-backend";
import { initReactI18next } from "react-i18next"; import { initReactI18next } from "react-i18next";
import langDe from "@i18n/locales/de.json"; i18n.use(Backend)
import langEn from "@i18n/locales/en.json";
import langEs from "@i18n/locales/es.json";
const resources = {
en: langEn,
es: langEs,
de: langDe,
};
const options = {
order: ["querystring", "navigator"],
lookupQuerystring: "lng",
};
i18n.use(XHR)
.use(LanguageDetector) .use(LanguageDetector)
.use(initReactI18next) .use(initReactI18next)
.init({ .init({
detection: options, detection: {
resources, order: ["querystring", "navigator"],
ns: [""], lookupQuerystring: "lng",
defaultNS: "", },
fallbackLng: "en", backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json",
},
ns: ["portal"],
defaultNS: "portal",
fallbackLng: {
default: ["en"],
},
load: "all",
supportedLngs: ["en", "es", "de"], supportedLngs: ["en", "es", "de"],
lowerCaseLng: true,
nonExplicitSupportedLngs: true,
interpolation: { interpolation: {
escapeValue: false, escapeValue: false,
}, },

View File

@ -1,60 +0,0 @@
{
"Portal": {
"An email has been sent to your address to complete the process": "Es wurde eine E-Mail an Ihre Adresse geschickt, um den Vorgang abzuschließen.",
"Authenticated": "Authentifiziert",
"Cancel": "Abbrechen",
"Contact your administrator to register a device": "Wenden Sie sich an Ihren Administrator, um ein Gerät zu registrieren.",
"Could not obtain user settings": "Benutzereinstellungen konnten nicht abgerufen werden",
"Done": "Erledigt",
"Enter new password": "Neues Passwort eingeben",
"Enter one-time password": "Einmal-Passwort eingeben",
"Failed to register device, the provided link is expired or has already been used": "Gerät konnte nicht registriert werden, der angegebene Link ist abgelaufen oder wurde bereits verwendet",
"Hi": "Hallo",
"Incorrect username or password": "Falscher Benutzername oder falsches Passwort.",
"Loading": "Laden",
"Logout": "Abmelden",
"Lost your device?": "Haben Sie Ihr Gerät verloren?",
"Methods": "Verfahren",
"Need Google Authenticator?": "Benötigen Sie Google Authenticator?",
"New password": "Neues Passwort",
"No verification token provided": "Kein Verifizierungs-Token vorhanden",
"OTP Secret copied to clipboard": "OTP Secret in die Zwischenablage kopiert.",
"OTP URL copied to clipboard": "OTP-URL in die Zwischenablage kopiert.",
"One-Time Password": "One-Time-Passwort",
"Password has been reset": "Das Passwort wurde zurückgesetzt.",
"Password": "Passwort",
"Passwords do not match": "Die Passwörter stimmen nicht überein.",
"Push Notification": "Push-Benachrichtigung",
"Register device": "Gerät registrieren",
"Register your first device by clicking on the link below": "Registrieren Sie Ihr erstes Gerät, indem Sie auf den unten stehenden Link klicken.",
"Remember me": "Angemeldet bleiben",
"Repeat new password": "Neues Passwort wiederholen",
"Reset password": "Passwort zurücksetzen",
"Reset password?": "Passwort zurücksetzen?",
"Reset": "Zurücksetzen",
"Scan QR Code": "QR-Code scannen",
"Secret": "Geheimnis",
"Security Key - WebAuthN": "Sicherheitsschlüssel - WebAuthN",
"Select a Device": "Gerät auswählen",
"Sign in": "Anmelden",
"Sign out": "Abmelden",
"The resource you're attempting to access requires two-factor authentication": "Die Ressource, auf die Sie zuzugreifen versuchen, erfordert eine Zwei-Faktor-Authentifizierung.",
"There was a problem initiating the registration process": "Es gab ein Problem beim Starten des Registrierungsprozesses.",
"There was an issue completing the process. The verification token might have expired": "Es gab ein Problem beim Abschluss des Prozesses. Das Verifizierungs-Token könnte abgelaufen sein.",
"There was an issue initiating the password reset process": "Es gab ein Problem beim beim Starten des Passwortrücksetzprozesses.",
"There was an issue resetting the password": "Es gab ein Problem beim Zurücksetzen des Passworts",
"There was an issue signing out": "Es gab ein Problem bei der Abmeldung",
"Time-based One-Time Password": "Zeitbasiertes One-Time-Passwort",
"Username": "Benutzername",
"You must open the link from the same device and browser that initiated the registration process": "Sie müssen den Link mit demselben Gerät und demselben Browser öffnen, mit dem Sie den Registrierungsprozess gestartet haben.",
"You're being signed out and redirected": "Sie werden abgemeldet und umgeleitet",
"Your supplied password does not meet the password policy requirements": "Ihr angegebenes Passwort entspricht nicht den Anforderungen der Passwortrichtlinie.",
"Use OpenID to verify your identity": "Verwenden Sie OpenID, um Ihre Identität zu überprüfen",
"Access your display name": "Zugriff auf Ihren Anzeigenamen",
"Access your group membership": "Zugriff auf Ihre Gruppenmitgliedschaft",
"Access your email addresses": "Zugriff auf Ihre E-Mail-Adressen",
"Accept": "Annehmen",
"Deny": "Ablehnen",
"The above application is requesting the following permissions": "Die oben genannte Anwendung bittet um die folgenden Berechtigungen"
}
}

View File

@ -1,67 +0,0 @@
{
"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 - WebAuthN": "Security Key - WebAuthN",
"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.",
"Use OpenID to verify your identity": "Use OpenID to verify your identity",
"Access your display name": "Access your display name",
"Access your group membership": "Access your group membership",
"Access your email addresses": "Access your email addresses",
"Accept": "Accept",
"Deny": "Deny",
"The above application is requesting the following permissions": "The above application is requesting the following permissions",
"The password does not meet the password policy": "The password does not meet the password policy",
"Must have at least one lowercase letter": "Must have at least one lowercase letter",
"Must have at least one UPPERCASE letter": "Must have at least one UPPERCASE letter",
"Must have at least one number": "Must have at least one number",
"Must have at least one special character": "Must have at least one special character",
"Must be at least {{len}} characters in length": "Must be at least {{len}} characters in length",
"Must not be more than {{len}} characters in length": "Must not be more than {{len}} characters in length"
}
}

View File

@ -1,67 +0,0 @@
{
"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 - WebAuthN": "Llave de Seguridad - WebAuthN",
"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",
"Use OpenID to verify your identity": "Utilizar OpenID para verificar su identidad",
"Access your display name": "Acceso a su nombre",
"Access your group membership": "Acceso a su(s) grupo(s)",
"Access your email addresses": "Acceso a su dirección de correo",
"Accept": "Aceptar",
"Deny": "Denegar",
"The above application is requesting the following permissions": "La aplicación solicita los siguientes permisos",
"The password does not meet the password policy": "La contraseña no cumple con la política de contraseñas",
"Must have at least one lowercase letter": "Debe contener al menos una letra minúscula",
"Must have at least one UPPERCASE letter": "Debe contener al menos una letra MAYUSCULA",
"Must have at least one number": "Debe contener al menos un número",
"Must have at least one special character": "Debe contener al menos un caracter especial",
"Must be at least {{len}} characters in length": "La longitud mínima es de {{len}} caracteres",
"Must not be more than {{len}} characters in length": "La longitud máxima es de {{len}} caracteres"
}
}

View File

@ -27,7 +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"); 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.

View File

@ -0,0 +1,22 @@
import React from "react";
import { Grid, Typography, useTheme } from "@material-ui/core";
import ReactLoading from "react-loading";
export interface Props {
message: string;
}
const BaseLoadingPage = function (props: Props) {
const theme = useTheme();
return (
<Grid container alignItems="center" justifyContent="center" style={{ minHeight: "100vh" }}>
<Grid item style={{ textAlign: "center", display: "inline-block" }}>
<ReactLoading width={64} height={64} color={theme.custom.loadingBar} type="bars" />
<Typography>{props.message}...</Typography>
</Grid>
</Grid>
);
};
export default BaseLoadingPage;

View File

@ -1,20 +1,12 @@
import React from "react"; import React from "react";
import { useTheme, Typography, Grid } from "@material-ui/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import ReactLoading from "react-loading";
import BaseLoadingPage from "@views/LoadingPage/BaseLoadingPage";
const LoadingPage = function () { const LoadingPage = function () {
const theme = useTheme(); const { t: translate } = useTranslation();
const { t: translate } = useTranslation("Portal"); return <BaseLoadingPage message={translate("Loading")} />;
return (
<Grid container alignItems="center" justifyContent="center" style={{ minHeight: "100vh" }}>
<Grid item style={{ textAlign: "center", display: "inline-block" }}>
<ReactLoading width={64} height={64} color={theme.custom.loadingBar} type="bars" />
<Typography>{translate("Loading")}...</Typography>
</Grid>
</Grid>
);
}; };
export default LoadingPage; export default LoadingPage;

View File

@ -7,7 +7,7 @@ import SuccessIcon from "@components/SuccessIcon";
const Authenticated = function () { const Authenticated = function () {
const classes = useStyles(); const classes = useStyles();
const { t: translate } = useTranslation("Portal"); const { t: translate } = useTranslation();
return ( return (
<div id="authenticated-stage"> <div id="authenticated-stage">
<div className={classes.iconContainer}> <div className={classes.iconContainer}>

View File

@ -15,7 +15,7 @@ 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 { t: translate } = useTranslation();
const handleLogoutClick = () => { const handleLogoutClick = () => {
navigate(SignOutRoute); navigate(SignOutRoute);

View File

@ -46,7 +46,7 @@ const ConsentView = function (props: Props) {
const redirect = useRedirector(); const redirect = useRedirector();
const { createErrorNotification, resetNotification } = useNotifications(); const { createErrorNotification, resetNotification } = useNotifications();
const [resp, fetch, , err] = useRequestedScopes(); const [resp, fetch, , err] = useRequestedScopes();
const { t: translate } = useTranslation("Portal"); const { t: translate } = useTranslation();
useEffect(() => { useEffect(() => {
if (err) { if (err) {

View File

@ -38,7 +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"); const { t: translate } = useTranslation();
useEffect(() => { useEffect(() => {
const timeout = setTimeout(() => usernameRef.current.focus(), 10); const timeout = setTimeout(() => usernameRef.current.focus(), 10);
return () => clearTimeout(timeout); return () => clearTimeout(timeout);

View File

@ -28,7 +28,7 @@ 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 { t: translate } = useTranslation();
const registerMessage = props.registered const registerMessage = props.registered
? props.title === "Push Notification" ? props.title === "Push Notification"
? "" ? ""
@ -97,7 +97,7 @@ interface NotRegisteredContainerProps {
} }
function NotRegisteredContainer(props: NotRegisteredContainerProps) { function NotRegisteredContainer(props: NotRegisteredContainerProps) {
const { t: translate } = useTranslation("Portal"); const { t: translate } = useTranslation();
const theme = useTheme(); const theme = useTheme();
return ( return (
<Fragment> <Fragment>

View File

@ -29,7 +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 { t: translate } = useTranslation();
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"} />

View File

@ -33,7 +33,7 @@ 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 { t: translate } = useTranslation();
const { onSignInSuccess, onSignInError } = props; const { onSignInSuccess, onSignInError } = props;
const onSignInErrorCallback = useRef(onSignInError).current; const onSignInErrorCallback = useRef(onSignInError).current;

View File

@ -41,7 +41,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 [webauthnSupported, setWebauthnSupported] = useState(false); const [webauthnSupported, setWebauthnSupported] = useState(false);
const { t: translate } = useTranslation("Portal"); const { t: translate } = useTranslation();
useEffect(() => { useEffect(() => {
setWebauthnSupported(isWebauthnSupported()); setWebauthnSupported(isWebauthnSupported());

View File

@ -22,7 +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 { t: translate } = useTranslation();
const doSignOut = useCallback(async () => { const doSignOut = useCallback(async () => {
try { try {

View File

@ -16,7 +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 { t: translate } = useTranslation();
const doInitiateResetPasswordProcess = async () => { const doInitiateResetPasswordProcess = async () => {
if (username === "") { if (username === "") {

View File

@ -25,7 +25,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 { t: translate } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);