feat: Add WebAuthn error translations

This commit is contained in:
Peifan Li
2026-01-03 12:43:56 -05:00
parent 35aa348824
commit 9296390b82
13 changed files with 70 additions and 2 deletions

View File

@@ -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<SecuritySettingsProps> = ({ 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<SecuritySettingsProps> = ({ 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<SecuritySettingsProps> = ({ settings, onChange
<FormControlLabel
control={
<Switch
checked={settings.passwordLoginAllowed !== false}
checked={!passkeysExist ? true : (settings.passwordLoginAllowed !== false)}
onChange={(e) => onChange('passwordLoginAllowed', e.target.checked)}
disabled={!settings.loginEnabled || !passkeysExist}
/>

View File

@@ -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);
}
});

View File

@@ -331,6 +331,8 @@ export const ar = {
loginWithPasskey: "تسجيل الدخول بمفتاح المرور",
authenticating: "جاري المصادقة...",
passkeyLoginFailed: "فشلت مصادقة مفتاح المرور. يرجى المحاولة مرة أخرى.",
passkeyErrorPermissionDenied: "لا يُسمح بالطلب من قبل وكيل المستخدم أو المنصة في السياق الحالي، ربما لأن المستخدم رفض الإذن.",
passkeyErrorAlreadyRegistered: "تم تسجيل المصادق مسبقاً.",
linkCopied: "تم نسخ الرابط إلى الحافظة",
copyFailed: "فشل نسخ الرابط",
passkeyRequiresHttps: "يتطلب WebAuthn استخدام HTTPS أو localhost. يرجى الدخول إلى التطبيق عبر HTTPS أو استخدام localhost بدلاً من عنوان IP.",

View File

@@ -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.",

View File

@@ -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",

View File

@@ -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.",

View File

@@ -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.",

View File

@@ -340,6 +340,8 @@ export const ja = {
loginWithPasskey: "パスキーでログイン",
authenticating: "認証中...",
passkeyLoginFailed: "パスキー認証に失敗しました。もう一度お試しください。",
passkeyErrorPermissionDenied: "ユーザーエージェントまたはプラットフォームが現在のコンテキストでリクエストを許可していません。ユーザーが権限を拒否した可能性があります。",
passkeyErrorAlreadyRegistered: "認証器は以前に登録されています。",
linkCopied: "リンクをクリップボードにコピーしました",
copyFailed: "リンクのコピーに失敗しました",
passkeyRequiresHttps: "WebAuthnにはHTTPSまたはlocalhostが必要です。HTTPS経由でアプリケーションにアクセスするか、IPアドレスの代わりにlocalhostを使用してください。",

View File

@@ -333,6 +333,8 @@ export const ko = {
loginWithPasskey: "패스키로 로그인",
authenticating: "인증 중...",
passkeyLoginFailed: "패스키 인증에 실패했습니다. 다시 시도해 주세요.",
passkeyErrorPermissionDenied: "사용자 에이전트 또는 플랫폼이 현재 컨텍스트에서 요청을 허용하지 않습니다. 사용자가 권한을 거부했을 수 있습니다.",
passkeyErrorAlreadyRegistered: "인증기가 이전에 등록되었습니다.",
linkCopied: "링크가 클립보드에 복사되었습니다",
copyFailed: "링크 복사 실패",
passkeyRequiresHttps: "WebAuthn은 HTTPS 또는 localhost가 필요합니다. HTTPS를 통해 애플리케이션에 액세스하거나 IP 주소 대신 localhost를 사용하십시오.",

View File

@@ -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.",

View File

@@ -352,6 +352,8 @@ export const ru = {
authenticating: "Аутентификация...",
passkeyLoginFailed:
"Ошибка аутентификации с помощью ключа доступа. Пожалуйста, попробуйте снова.",
passkeyErrorPermissionDenied: "Запрос не разрешен пользовательским агентом или платформой в текущем контексте, возможно, потому что пользователь отклонил разрешение.",
passkeyErrorAlreadyRegistered: "Аутентификатор был ранее зарегистрирован.",
linkCopied: "Ссылка скопирована в буфер обмена",
copyFailed: "Не удалось скопировать ссылку",
passkeyRequiresHttps: "WebAuthn требует HTTPS или localhost. Пожалуйста, войдите в приложение через HTTPS или используйте localhost вместо IP-адреса.",

View File

@@ -322,6 +322,8 @@ export const zh = {
loginWithPasskey: "使用通行密钥登录",
authenticating: "验证中...",
passkeyLoginFailed: "通行密钥验证失败,请重试。",
passkeyErrorPermissionDenied: "用户代理或平台在当前上下文中不允许该请求,可能是因为用户拒绝了权限。",
passkeyErrorAlreadyRegistered: "该认证器之前已注册。",
linkCopied: "链接已复制到剪贴板",
copyFailed: "复制链接失败",

View File

@@ -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;
}