From 9296390b82ebedb39a91b530bd2276d2ff7d5e0d Mon Sep 17 00:00:00 2001 From: Peifan Li Date: Sat, 3 Jan 2026 12:43:56 -0500 Subject: [PATCH] feat: Add WebAuthn error translations --- .../components/Settings/SecuritySettings.tsx | 18 +++++++++++-- frontend/src/pages/LoginPage.tsx | 7 +++++ frontend/src/utils/locales/ar.ts | 2 ++ frontend/src/utils/locales/de.ts | 2 ++ frontend/src/utils/locales/en.ts | 2 ++ frontend/src/utils/locales/es.ts | 2 ++ frontend/src/utils/locales/fr.ts | 2 ++ frontend/src/utils/locales/ja.ts | 2 ++ frontend/src/utils/locales/ko.ts | 2 ++ frontend/src/utils/locales/pt.ts | 2 ++ frontend/src/utils/locales/ru.ts | 2 ++ frontend/src/utils/locales/zh.ts | 2 ++ frontend/src/utils/translations.ts | 27 +++++++++++++++++++ 13 files changed, 70 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Settings/SecuritySettings.tsx b/frontend/src/components/Settings/SecuritySettings.tsx index 3237d90..3519881 100644 --- a/frontend/src/components/Settings/SecuritySettings.tsx +++ b/frontend/src/components/Settings/SecuritySettings.tsx @@ -2,9 +2,10 @@ import { Box, Button, FormControlLabel, Switch, TextField, Typography } from '@m import { startRegistration } from '@simplewebauthn/browser'; import { useMutation, useQuery } from '@tanstack/react-query'; import axios from 'axios'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useLanguage } from '../../contexts/LanguageContext'; import { Settings } from '../../types'; +import { getWebAuthnErrorTranslationKey } from '../../utils/translations'; import AlertModal from '../AlertModal'; import ConfirmationModal from '../ConfirmationModal'; @@ -39,6 +40,13 @@ const SecuritySettings: React.FC = ({ settings, onChange 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 const createPasskeyMutation = useMutation({ mutationFn: async () => { @@ -82,6 +90,12 @@ const SecuritySettings: React.FC = ({ settings, onChange 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); }, }); @@ -166,7 +180,7 @@ const SecuritySettings: React.FC = ({ settings, onChange onChange('passwordLoginAllowed', e.target.checked)} disabled={!settings.loginEnabled || !passkeysExist} /> diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index ebe4751..11e986c 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -25,6 +25,7 @@ import ConfirmationModal from '../components/ConfirmationModal'; import { useAuth } from '../contexts/AuthContext'; import { useLanguage } from '../contexts/LanguageContext'; import getTheme from '../theme'; +import { getWebAuthnErrorTranslationKey } from '../utils/translations'; const API_URL = import.meta.env.VITE_API_URL; @@ -248,6 +249,12 @@ const LoginPage: React.FC = () => { 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); } }); diff --git a/frontend/src/utils/locales/ar.ts b/frontend/src/utils/locales/ar.ts index b3f882d..7057dac 100644 --- a/frontend/src/utils/locales/ar.ts +++ b/frontend/src/utils/locales/ar.ts @@ -331,6 +331,8 @@ export const ar = { loginWithPasskey: "تسجيل الدخول بمفتاح المرور", authenticating: "جاري المصادقة...", passkeyLoginFailed: "فشلت مصادقة مفتاح المرور. يرجى المحاولة مرة أخرى.", + passkeyErrorPermissionDenied: "لا يُسمح بالطلب من قبل وكيل المستخدم أو المنصة في السياق الحالي، ربما لأن المستخدم رفض الإذن.", + passkeyErrorAlreadyRegistered: "تم تسجيل المصادق مسبقاً.", linkCopied: "تم نسخ الرابط إلى الحافظة", copyFailed: "فشل نسخ الرابط", passkeyRequiresHttps: "يتطلب WebAuthn استخدام HTTPS أو localhost. يرجى الدخول إلى التطبيق عبر HTTPS أو استخدام localhost بدلاً من عنوان IP.", diff --git a/frontend/src/utils/locales/de.ts b/frontend/src/utils/locales/de.ts index cfee1a1..4313e11 100644 --- a/frontend/src/utils/locales/de.ts +++ b/frontend/src/utils/locales/de.ts @@ -334,6 +334,8 @@ export const de = { authenticating: "Wird authentifiziert...", passkeyLoginFailed: "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", 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.", diff --git a/frontend/src/utils/locales/en.ts b/frontend/src/utils/locales/en.ts index 0b754cd..d8d49c6 100644 --- a/frontend/src/utils/locales/en.ts +++ b/frontend/src/utils/locales/en.ts @@ -346,6 +346,8 @@ export const en = { loginWithPasskey: "Login with Passkey", authenticating: "Authenticating...", 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", copyFailed: "Failed to copy link", copyUrl: "Copy URL", diff --git a/frontend/src/utils/locales/es.ts b/frontend/src/utils/locales/es.ts index edd46e4..4ac6868 100644 --- a/frontend/src/utils/locales/es.ts +++ b/frontend/src/utils/locales/es.ts @@ -357,6 +357,8 @@ export const es = { authenticating: "Autenticando...", passkeyLoginFailed: "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", 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.", diff --git a/frontend/src/utils/locales/fr.ts b/frontend/src/utils/locales/fr.ts index c7b4a84..e0355e7 100644 --- a/frontend/src/utils/locales/fr.ts +++ b/frontend/src/utils/locales/fr.ts @@ -357,6 +357,8 @@ export const fr = { authenticating: "Authentification en cours...", passkeyLoginFailed: "É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", 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.", diff --git a/frontend/src/utils/locales/ja.ts b/frontend/src/utils/locales/ja.ts index cbd3ea4..632bb55 100644 --- a/frontend/src/utils/locales/ja.ts +++ b/frontend/src/utils/locales/ja.ts @@ -340,6 +340,8 @@ export const ja = { loginWithPasskey: "パスキーでログイン", authenticating: "認証中...", passkeyLoginFailed: "パスキー認証に失敗しました。もう一度お試しください。", + passkeyErrorPermissionDenied: "ユーザーエージェントまたはプラットフォームが現在のコンテキストでリクエストを許可していません。ユーザーが権限を拒否した可能性があります。", + passkeyErrorAlreadyRegistered: "認証器は以前に登録されています。", linkCopied: "リンクをクリップボードにコピーしました", copyFailed: "リンクのコピーに失敗しました", passkeyRequiresHttps: "WebAuthnにはHTTPSまたはlocalhostが必要です。HTTPS経由でアプリケーションにアクセスするか、IPアドレスの代わりにlocalhostを使用してください。", diff --git a/frontend/src/utils/locales/ko.ts b/frontend/src/utils/locales/ko.ts index a5b023f..4a5890a 100644 --- a/frontend/src/utils/locales/ko.ts +++ b/frontend/src/utils/locales/ko.ts @@ -333,6 +333,8 @@ export const ko = { loginWithPasskey: "패스키로 로그인", authenticating: "인증 중...", passkeyLoginFailed: "패스키 인증에 실패했습니다. 다시 시도해 주세요.", + passkeyErrorPermissionDenied: "사용자 에이전트 또는 플랫폼이 현재 컨텍스트에서 요청을 허용하지 않습니다. 사용자가 권한을 거부했을 수 있습니다.", + passkeyErrorAlreadyRegistered: "인증기가 이전에 등록되었습니다.", linkCopied: "링크가 클립보드에 복사되었습니다", copyFailed: "링크 복사 실패", passkeyRequiresHttps: "WebAuthn은 HTTPS 또는 localhost가 필요합니다. HTTPS를 통해 애플리케이션에 액세스하거나 IP 주소 대신 localhost를 사용하십시오.", diff --git a/frontend/src/utils/locales/pt.ts b/frontend/src/utils/locales/pt.ts index 0815fd9..d9cf32d 100644 --- a/frontend/src/utils/locales/pt.ts +++ b/frontend/src/utils/locales/pt.ts @@ -352,6 +352,8 @@ export const pt = { authenticating: "Autenticando...", passkeyLoginFailed: "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", 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.", diff --git a/frontend/src/utils/locales/ru.ts b/frontend/src/utils/locales/ru.ts index f9bdabc..e54b57e 100644 --- a/frontend/src/utils/locales/ru.ts +++ b/frontend/src/utils/locales/ru.ts @@ -352,6 +352,8 @@ export const ru = { authenticating: "Аутентификация...", passkeyLoginFailed: "Ошибка аутентификации с помощью ключа доступа. Пожалуйста, попробуйте снова.", + passkeyErrorPermissionDenied: "Запрос не разрешен пользовательским агентом или платформой в текущем контексте, возможно, потому что пользователь отклонил разрешение.", + passkeyErrorAlreadyRegistered: "Аутентификатор был ранее зарегистрирован.", linkCopied: "Ссылка скопирована в буфер обмена", copyFailed: "Не удалось скопировать ссылку", passkeyRequiresHttps: "WebAuthn требует HTTPS или localhost. Пожалуйста, войдите в приложение через HTTPS или используйте localhost вместо IP-адреса.", diff --git a/frontend/src/utils/locales/zh.ts b/frontend/src/utils/locales/zh.ts index c358bae..6a552ba 100644 --- a/frontend/src/utils/locales/zh.ts +++ b/frontend/src/utils/locales/zh.ts @@ -322,6 +322,8 @@ export const zh = { loginWithPasskey: "使用通行密钥登录", authenticating: "验证中...", passkeyLoginFailed: "通行密钥验证失败,请重试。", + passkeyErrorPermissionDenied: "用户代理或平台在当前上下文中不允许该请求,可能是因为用户拒绝了权限。", + passkeyErrorAlreadyRegistered: "该认证器之前已注册。", linkCopied: "链接已复制到剪贴板", copyFailed: "复制链接失败", diff --git a/frontend/src/utils/translations.ts b/frontend/src/utils/translations.ts index bfe2d33..e16566c 100644 --- a/frontend/src/utils/translations.ts +++ b/frontend/src/utils/translations.ts @@ -24,3 +24,30 @@ export const translations = { export type Language = 'en' | 'zh' | 'es' | 'de' | 'ja' | 'fr' | 'ko' | 'ar' | 'pt' | 'ru'; 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; +}