feat: Add WebAuthn error translations
This commit is contained in:
@@ -2,9 +2,10 @@ import { Box, Button, FormControlLabel, Switch, TextField, Typography } from '@m
|
|||||||
import { startRegistration } from '@simplewebauthn/browser';
|
import { startRegistration } from '@simplewebauthn/browser';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useLanguage } from '../../contexts/LanguageContext';
|
import { useLanguage } from '../../contexts/LanguageContext';
|
||||||
import { Settings } from '../../types';
|
import { Settings } from '../../types';
|
||||||
|
import { getWebAuthnErrorTranslationKey } from '../../utils/translations';
|
||||||
import AlertModal from '../AlertModal';
|
import AlertModal from '../AlertModal';
|
||||||
import ConfirmationModal from '../ConfirmationModal';
|
import ConfirmationModal from '../ConfirmationModal';
|
||||||
|
|
||||||
@@ -39,6 +40,13 @@ const SecuritySettings: React.FC<SecuritySettingsProps> = ({ settings, onChange
|
|||||||
|
|
||||||
const passkeysExist = passkeysData?.exists || false;
|
const passkeysExist = passkeysData?.exists || false;
|
||||||
|
|
||||||
|
// If passkeys don't exist, automatically enable and lock password login
|
||||||
|
useEffect(() => {
|
||||||
|
if (!passkeysExist && settings.loginEnabled && settings.passwordLoginAllowed === false) {
|
||||||
|
onChange('passwordLoginAllowed', true);
|
||||||
|
}
|
||||||
|
}, [passkeysExist, settings.loginEnabled, settings.passwordLoginAllowed, onChange]);
|
||||||
|
|
||||||
// Create passkey mutation
|
// Create passkey mutation
|
||||||
const createPasskeyMutation = useMutation({
|
const createPasskeyMutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
@@ -82,6 +90,12 @@ const SecuritySettings: React.FC<SecuritySettingsProps> = ({ settings, onChange
|
|||||||
errorMessage = error.message;
|
errorMessage = error.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is a WebAuthn error that can be translated
|
||||||
|
const translationKey = getWebAuthnErrorTranslationKey(errorMessage);
|
||||||
|
if (translationKey) {
|
||||||
|
errorMessage = t(translationKey) || errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
showAlert(t('error'), errorMessage);
|
showAlert(t('error'), errorMessage);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -166,7 +180,7 @@ const SecuritySettings: React.FC<SecuritySettingsProps> = ({ settings, onChange
|
|||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Switch
|
<Switch
|
||||||
checked={settings.passwordLoginAllowed !== false}
|
checked={!passkeysExist ? true : (settings.passwordLoginAllowed !== false)}
|
||||||
onChange={(e) => onChange('passwordLoginAllowed', e.target.checked)}
|
onChange={(e) => onChange('passwordLoginAllowed', e.target.checked)}
|
||||||
disabled={!settings.loginEnabled || !passkeysExist}
|
disabled={!settings.loginEnabled || !passkeysExist}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import ConfirmationModal from '../components/ConfirmationModal';
|
|||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { useLanguage } from '../contexts/LanguageContext';
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
import getTheme from '../theme';
|
import getTheme from '../theme';
|
||||||
|
import { getWebAuthnErrorTranslationKey } from '../utils/translations';
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_API_URL;
|
const API_URL = import.meta.env.VITE_API_URL;
|
||||||
|
|
||||||
@@ -248,6 +249,12 @@ const LoginPage: React.FC = () => {
|
|||||||
errorMessage = err.message;
|
errorMessage = err.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is a WebAuthn error that can be translated
|
||||||
|
const translationKey = getWebAuthnErrorTranslationKey(errorMessage);
|
||||||
|
if (translationKey) {
|
||||||
|
errorMessage = t(translationKey) || errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
showAlert(t('error'), errorMessage);
|
showAlert(t('error'), errorMessage);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -331,6 +331,8 @@ export const ar = {
|
|||||||
loginWithPasskey: "تسجيل الدخول بمفتاح المرور",
|
loginWithPasskey: "تسجيل الدخول بمفتاح المرور",
|
||||||
authenticating: "جاري المصادقة...",
|
authenticating: "جاري المصادقة...",
|
||||||
passkeyLoginFailed: "فشلت مصادقة مفتاح المرور. يرجى المحاولة مرة أخرى.",
|
passkeyLoginFailed: "فشلت مصادقة مفتاح المرور. يرجى المحاولة مرة أخرى.",
|
||||||
|
passkeyErrorPermissionDenied: "لا يُسمح بالطلب من قبل وكيل المستخدم أو المنصة في السياق الحالي، ربما لأن المستخدم رفض الإذن.",
|
||||||
|
passkeyErrorAlreadyRegistered: "تم تسجيل المصادق مسبقاً.",
|
||||||
linkCopied: "تم نسخ الرابط إلى الحافظة",
|
linkCopied: "تم نسخ الرابط إلى الحافظة",
|
||||||
copyFailed: "فشل نسخ الرابط",
|
copyFailed: "فشل نسخ الرابط",
|
||||||
passkeyRequiresHttps: "يتطلب WebAuthn استخدام HTTPS أو localhost. يرجى الدخول إلى التطبيق عبر HTTPS أو استخدام localhost بدلاً من عنوان IP.",
|
passkeyRequiresHttps: "يتطلب WebAuthn استخدام HTTPS أو localhost. يرجى الدخول إلى التطبيق عبر HTTPS أو استخدام localhost بدلاً من عنوان IP.",
|
||||||
|
|||||||
@@ -334,6 +334,8 @@ export const de = {
|
|||||||
authenticating: "Wird authentifiziert...",
|
authenticating: "Wird authentifiziert...",
|
||||||
passkeyLoginFailed:
|
passkeyLoginFailed:
|
||||||
"Passkey-Authentifizierung fehlgeschlagen. Bitte versuchen Sie es erneut.",
|
"Passkey-Authentifizierung fehlgeschlagen. Bitte versuchen Sie es erneut.",
|
||||||
|
passkeyErrorPermissionDenied: "Die Anfrage wird vom Benutzer-Agenten oder der Plattform im aktuellen Kontext nicht zugelassen, möglicherweise weil der Benutzer die Berechtigung verweigert hat.",
|
||||||
|
passkeyErrorAlreadyRegistered: "Der Authentifikator wurde bereits zuvor registriert.",
|
||||||
linkCopied: "Link in die Zwischenablage kopiert",
|
linkCopied: "Link in die Zwischenablage kopiert",
|
||||||
copyFailed: "Link konnte nicht kopiert werden",
|
copyFailed: "Link konnte nicht kopiert werden",
|
||||||
passkeyRequiresHttps: "WebAuthn erfordert HTTPS oder localhost. Bitte greifen Sie über HTTPS auf die Anwendung zu oder verwenden Sie localhost anstelle einer IP-Adresse.",
|
passkeyRequiresHttps: "WebAuthn erfordert HTTPS oder localhost. Bitte greifen Sie über HTTPS auf die Anwendung zu oder verwenden Sie localhost anstelle einer IP-Adresse.",
|
||||||
|
|||||||
@@ -346,6 +346,8 @@ export const en = {
|
|||||||
loginWithPasskey: "Login with Passkey",
|
loginWithPasskey: "Login with Passkey",
|
||||||
authenticating: "Authenticating...",
|
authenticating: "Authenticating...",
|
||||||
passkeyLoginFailed: "Passkey authentication failed. Please try again.",
|
passkeyLoginFailed: "Passkey authentication failed. Please try again.",
|
||||||
|
passkeyErrorPermissionDenied: "The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.",
|
||||||
|
passkeyErrorAlreadyRegistered: "The authenticator was previously registered.",
|
||||||
linkCopied: "Link copied to clipboard",
|
linkCopied: "Link copied to clipboard",
|
||||||
copyFailed: "Failed to copy link",
|
copyFailed: "Failed to copy link",
|
||||||
copyUrl: "Copy URL",
|
copyUrl: "Copy URL",
|
||||||
|
|||||||
@@ -357,6 +357,8 @@ export const es = {
|
|||||||
authenticating: "Autenticando...",
|
authenticating: "Autenticando...",
|
||||||
passkeyLoginFailed:
|
passkeyLoginFailed:
|
||||||
"Error en la autenticación con clave de acceso. Por favor, inténtelo de nuevo.",
|
"Error en la autenticación con clave de acceso. Por favor, inténtelo de nuevo.",
|
||||||
|
passkeyErrorPermissionDenied: "La solicitud no está permitida por el agente de usuario o la plataforma en el contexto actual, posiblemente porque el usuario denegó el permiso.",
|
||||||
|
passkeyErrorAlreadyRegistered: "El autenticador ya estaba registrado previamente.",
|
||||||
linkCopied: "Enlace copiado al portapapeles",
|
linkCopied: "Enlace copiado al portapapeles",
|
||||||
copyFailed: "Error al copiar enlace",
|
copyFailed: "Error al copiar enlace",
|
||||||
passkeyRequiresHttps: "WebAuthn requiere HTTPS o localhost. Por favor, acceda a la aplicación a través de HTTPS o utilice localhost en lugar de una dirección IP.",
|
passkeyRequiresHttps: "WebAuthn requiere HTTPS o localhost. Por favor, acceda a la aplicación a través de HTTPS o utilice localhost en lugar de una dirección IP.",
|
||||||
|
|||||||
@@ -357,6 +357,8 @@ export const fr = {
|
|||||||
authenticating: "Authentification en cours...",
|
authenticating: "Authentification en cours...",
|
||||||
passkeyLoginFailed:
|
passkeyLoginFailed:
|
||||||
"Échec de l'authentification par clé d'accès. Veuillez réessayer.",
|
"Échec de l'authentification par clé d'accès. Veuillez réessayer.",
|
||||||
|
passkeyErrorPermissionDenied: "La demande n'est pas autorisée par l'agent utilisateur ou la plateforme dans le contexte actuel, peut-être parce que l'utilisateur a refusé l'autorisation.",
|
||||||
|
passkeyErrorAlreadyRegistered: "L'authentificateur a déjà été enregistré précédemment.",
|
||||||
linkCopied: "Lien copié dans le presse-papiers",
|
linkCopied: "Lien copié dans le presse-papiers",
|
||||||
copyFailed: "Échec de la copie du lien",
|
copyFailed: "Échec de la copie du lien",
|
||||||
passkeyRequiresHttps: "WebAuthn nécessite HTTPS ou localhost. Veuillez accéder à l'application via HTTPS ou utiliser localhost au lieu d'une adresse IP.",
|
passkeyRequiresHttps: "WebAuthn nécessite HTTPS ou localhost. Veuillez accéder à l'application via HTTPS ou utiliser localhost au lieu d'une adresse IP.",
|
||||||
|
|||||||
@@ -340,6 +340,8 @@ export const ja = {
|
|||||||
loginWithPasskey: "パスキーでログイン",
|
loginWithPasskey: "パスキーでログイン",
|
||||||
authenticating: "認証中...",
|
authenticating: "認証中...",
|
||||||
passkeyLoginFailed: "パスキー認証に失敗しました。もう一度お試しください。",
|
passkeyLoginFailed: "パスキー認証に失敗しました。もう一度お試しください。",
|
||||||
|
passkeyErrorPermissionDenied: "ユーザーエージェントまたはプラットフォームが現在のコンテキストでリクエストを許可していません。ユーザーが権限を拒否した可能性があります。",
|
||||||
|
passkeyErrorAlreadyRegistered: "認証器は以前に登録されています。",
|
||||||
linkCopied: "リンクをクリップボードにコピーしました",
|
linkCopied: "リンクをクリップボードにコピーしました",
|
||||||
copyFailed: "リンクのコピーに失敗しました",
|
copyFailed: "リンクのコピーに失敗しました",
|
||||||
passkeyRequiresHttps: "WebAuthnにはHTTPSまたはlocalhostが必要です。HTTPS経由でアプリケーションにアクセスするか、IPアドレスの代わりにlocalhostを使用してください。",
|
passkeyRequiresHttps: "WebAuthnにはHTTPSまたはlocalhostが必要です。HTTPS経由でアプリケーションにアクセスするか、IPアドレスの代わりにlocalhostを使用してください。",
|
||||||
|
|||||||
@@ -333,6 +333,8 @@ export const ko = {
|
|||||||
loginWithPasskey: "패스키로 로그인",
|
loginWithPasskey: "패스키로 로그인",
|
||||||
authenticating: "인증 중...",
|
authenticating: "인증 중...",
|
||||||
passkeyLoginFailed: "패스키 인증에 실패했습니다. 다시 시도해 주세요.",
|
passkeyLoginFailed: "패스키 인증에 실패했습니다. 다시 시도해 주세요.",
|
||||||
|
passkeyErrorPermissionDenied: "사용자 에이전트 또는 플랫폼이 현재 컨텍스트에서 요청을 허용하지 않습니다. 사용자가 권한을 거부했을 수 있습니다.",
|
||||||
|
passkeyErrorAlreadyRegistered: "인증기가 이전에 등록되었습니다.",
|
||||||
linkCopied: "링크가 클립보드에 복사되었습니다",
|
linkCopied: "링크가 클립보드에 복사되었습니다",
|
||||||
copyFailed: "링크 복사 실패",
|
copyFailed: "링크 복사 실패",
|
||||||
passkeyRequiresHttps: "WebAuthn은 HTTPS 또는 localhost가 필요합니다. HTTPS를 통해 애플리케이션에 액세스하거나 IP 주소 대신 localhost를 사용하십시오.",
|
passkeyRequiresHttps: "WebAuthn은 HTTPS 또는 localhost가 필요합니다. HTTPS를 통해 애플리케이션에 액세스하거나 IP 주소 대신 localhost를 사용하십시오.",
|
||||||
|
|||||||
@@ -352,6 +352,8 @@ export const pt = {
|
|||||||
authenticating: "Autenticando...",
|
authenticating: "Autenticando...",
|
||||||
passkeyLoginFailed:
|
passkeyLoginFailed:
|
||||||
"Falha na autenticação com chave de acesso. Por favor, tente novamente.",
|
"Falha na autenticação com chave de acesso. Por favor, tente novamente.",
|
||||||
|
passkeyErrorPermissionDenied: "A solicitação não é permitida pelo agente do usuário ou pela plataforma no contexto atual, possivelmente porque o usuário negou a permissão.",
|
||||||
|
passkeyErrorAlreadyRegistered: "O autenticador já foi registrado anteriormente.",
|
||||||
linkCopied: "Link copiado para a área de transferência",
|
linkCopied: "Link copiado para a área de transferência",
|
||||||
copyFailed: "Falha ao copiar link",
|
copyFailed: "Falha ao copiar link",
|
||||||
passkeyRequiresHttps: "WebAuthn requer HTTPS ou localhost. Por favor, acesse o aplicativo via HTTPS ou use localhost em vez de um endereço IP.",
|
passkeyRequiresHttps: "WebAuthn requer HTTPS ou localhost. Por favor, acesse o aplicativo via HTTPS ou use localhost em vez de um endereço IP.",
|
||||||
|
|||||||
@@ -352,6 +352,8 @@ export const ru = {
|
|||||||
authenticating: "Аутентификация...",
|
authenticating: "Аутентификация...",
|
||||||
passkeyLoginFailed:
|
passkeyLoginFailed:
|
||||||
"Ошибка аутентификации с помощью ключа доступа. Пожалуйста, попробуйте снова.",
|
"Ошибка аутентификации с помощью ключа доступа. Пожалуйста, попробуйте снова.",
|
||||||
|
passkeyErrorPermissionDenied: "Запрос не разрешен пользовательским агентом или платформой в текущем контексте, возможно, потому что пользователь отклонил разрешение.",
|
||||||
|
passkeyErrorAlreadyRegistered: "Аутентификатор был ранее зарегистрирован.",
|
||||||
linkCopied: "Ссылка скопирована в буфер обмена",
|
linkCopied: "Ссылка скопирована в буфер обмена",
|
||||||
copyFailed: "Не удалось скопировать ссылку",
|
copyFailed: "Не удалось скопировать ссылку",
|
||||||
passkeyRequiresHttps: "WebAuthn требует HTTPS или localhost. Пожалуйста, войдите в приложение через HTTPS или используйте localhost вместо IP-адреса.",
|
passkeyRequiresHttps: "WebAuthn требует HTTPS или localhost. Пожалуйста, войдите в приложение через HTTPS или используйте localhost вместо IP-адреса.",
|
||||||
|
|||||||
@@ -322,6 +322,8 @@ export const zh = {
|
|||||||
loginWithPasskey: "使用通行密钥登录",
|
loginWithPasskey: "使用通行密钥登录",
|
||||||
authenticating: "验证中...",
|
authenticating: "验证中...",
|
||||||
passkeyLoginFailed: "通行密钥验证失败,请重试。",
|
passkeyLoginFailed: "通行密钥验证失败,请重试。",
|
||||||
|
passkeyErrorPermissionDenied: "用户代理或平台在当前上下文中不允许该请求,可能是因为用户拒绝了权限。",
|
||||||
|
passkeyErrorAlreadyRegistered: "该认证器之前已注册。",
|
||||||
linkCopied: "链接已复制到剪贴板",
|
linkCopied: "链接已复制到剪贴板",
|
||||||
copyFailed: "复制链接失败",
|
copyFailed: "复制链接失败",
|
||||||
|
|
||||||
|
|||||||
@@ -24,3 +24,30 @@ export const translations = {
|
|||||||
|
|
||||||
export type Language = 'en' | 'zh' | 'es' | 'de' | 'ja' | 'fr' | 'ko' | 'ar' | 'pt' | 'ru';
|
export type Language = 'en' | 'zh' | 'es' | 'de' | 'ja' | 'fr' | 'ko' | 'ar' | 'pt' | 'ru';
|
||||||
export type TranslationKey = keyof typeof translations.en;
|
export type TranslationKey = keyof typeof translations.en;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps WebAuthn error messages to translation keys
|
||||||
|
* @param errorMessage The error message from WebAuthn API
|
||||||
|
* @returns Translation key if a match is found, null otherwise
|
||||||
|
*/
|
||||||
|
export function getWebAuthnErrorTranslationKey(errorMessage: string): TranslationKey | null {
|
||||||
|
if (!errorMessage) return null;
|
||||||
|
|
||||||
|
const lowerMessage = errorMessage.toLowerCase();
|
||||||
|
|
||||||
|
// Check for permission denied error
|
||||||
|
if (lowerMessage.includes('not allowed') ||
|
||||||
|
lowerMessage.includes('denied permission') ||
|
||||||
|
lowerMessage.includes('user denied')) {
|
||||||
|
return 'passkeyErrorPermissionDenied';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for already registered error
|
||||||
|
if (lowerMessage.includes('previously registered') ||
|
||||||
|
lowerMessage.includes('already registered') ||
|
||||||
|
lowerMessage.includes('authenticator was previously')) {
|
||||||
|
return 'passkeyErrorAlreadyRegistered';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user