diff --git a/client/src/i18n/translations/ar.ts b/client/src/i18n/translations/ar.ts index 6c1f01d..4cb9001 100644 --- a/client/src/i18n/translations/ar.ts +++ b/client/src/i18n/translations/ar.ts @@ -167,6 +167,22 @@ const ar: Record = { 'settings.deleteBlockedTitle': 'الحذف غير ممكن', 'settings.deleteBlockedMessage': 'أنت المسؤول الوحيد. قم بترقية مستخدم آخر إلى مسؤول قبل حذف حسابك.', 'settings.saveProfile': 'حفظ الملف الشخصي', + 'settings.mfa.title': 'المصادقة الثنائية (2FA)', + 'settings.mfa.description': 'تضيف خطوة ثانية عند تسجيل الدخول. استخدم تطبيق مصادقة (Google Authenticator، Authy، إلخ).', + 'settings.mfa.enabled': 'المصادقة الثنائية مفعّلة على حسابك.', + 'settings.mfa.disabled': 'المصادقة الثنائية غير مفعّلة.', + 'settings.mfa.setup': 'إعداد المصادقة', + 'settings.mfa.scanQr': 'امسح رمز QR بتطبيقك أو أدخل المفتاح يدويًا.', + 'settings.mfa.secretLabel': 'المفتاح السري (إدخال يدوي)', + 'settings.mfa.codePlaceholder': 'رمز من 6 أرقام', + 'settings.mfa.enable': 'تفعيل 2FA', + 'settings.mfa.cancelSetup': 'إلغاء', + 'settings.mfa.disableTitle': 'تعطيل 2FA', + 'settings.mfa.disableHint': 'أدخل كلمة مرور حسابك ورمزًا حاليًا من المصادقة.', + 'settings.mfa.disable': 'تعطيل 2FA', + 'settings.mfa.toastEnabled': 'تم تفعيل المصادقة الثنائية', + 'settings.mfa.toastDisabled': 'تم تعطيل المصادقة الثنائية', + 'settings.mfa.demoBlocked': 'غير متاح في الوضع التجريبي', 'settings.toast.mapSaved': 'تم حفظ إعدادات الخريطة', 'settings.toast.keysSaved': 'تم حفظ مفاتيح API', 'settings.toast.displaySaved': 'تم حفظ إعدادات العرض', @@ -213,6 +229,13 @@ const ar: Record = { 'login.username': 'اسم المستخدم', 'login.oidc.registrationDisabled': 'التسجيل معطّل. تواصل مع المسؤول.', 'login.oidc.noEmail': 'لم يتم استلام بريد إلكتروني من المزوّد.', + 'login.mfaTitle': 'المصادقة الثنائية', + 'login.mfaSubtitle': 'أدخل الرمز المكون من 6 أرقام من تطبيق المصادقة.', + 'login.mfaCodeLabel': 'رمز التحقق', + 'login.mfaCodeRequired': 'أدخل الرمز من تطبيق المصادقة.', + 'login.mfaHint': 'افتح Google Authenticator أو Authy أو أي تطبيق TOTP آخر.', + 'login.mfaBack': '← العودة لتسجيل الدخول', + 'login.mfaVerify': 'تحقق', 'login.oidc.tokenFailed': 'فشلت المصادقة.', 'login.oidc.invalidState': 'جلسة غير صالحة. حاول مرة أخرى.', 'login.demoFailed': 'فشل الدخول إلى العرض التجريبي', diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index b7b1340..2db412b 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -163,6 +163,22 @@ const es: Record = { 'settings.deleteBlockedMessage': 'Eres el único administrador. Asciende a otro usuario a administrador antes de eliminar tu cuenta.', 'settings.roleUser': 'Usuario', 'settings.saveProfile': 'Guardar perfil', + 'settings.mfa.title': 'Autenticación de dos factores (2FA)', + 'settings.mfa.description': 'Añade un segundo paso al iniciar sesión. Usa una app de autenticación (Google Authenticator, Authy, etc.).', + 'settings.mfa.enabled': '2FA está activado en tu cuenta.', + 'settings.mfa.disabled': '2FA no está activado.', + 'settings.mfa.setup': 'Configurar autenticador', + 'settings.mfa.scanQr': 'Escanea este código QR con tu app o introduce la clave manualmente.', + 'settings.mfa.secretLabel': 'Clave secreta (entrada manual)', + 'settings.mfa.codePlaceholder': 'Código de 6 dígitos', + 'settings.mfa.enable': 'Activar 2FA', + 'settings.mfa.cancelSetup': 'Cancelar', + 'settings.mfa.disableTitle': 'Desactivar 2FA', + 'settings.mfa.disableHint': 'Introduce tu contraseña y un código actual de tu autenticador.', + 'settings.mfa.disable': 'Desactivar 2FA', + 'settings.mfa.toastEnabled': 'Autenticación de dos factores activada', + 'settings.mfa.toastDisabled': 'Autenticación de dos factores desactivada', + 'settings.mfa.demoBlocked': 'No disponible en modo demo', 'settings.toast.mapSaved': 'Ajustes del mapa guardados', 'settings.toast.keysSaved': 'Claves API guardadas', 'settings.toast.displaySaved': 'Ajustes de visualización guardados', @@ -210,6 +226,13 @@ const es: Record = { 'login.username': 'Usuario', 'login.oidc.registrationDisabled': 'El registro está desactivado. Contacta con tu administrador.', 'login.oidc.noEmail': 'No se recibió ningún correo del proveedor.', + 'login.mfaTitle': 'Autenticación de dos factores', + 'login.mfaSubtitle': 'Introduce el código de 6 dígitos de tu app de autenticación.', + 'login.mfaCodeLabel': 'Código de verificación', + 'login.mfaCodeRequired': 'Introduce el código de tu app de autenticación.', + 'login.mfaHint': 'Abre Google Authenticator, Authy u otra app TOTP.', + 'login.mfaBack': '← Volver al inicio de sesión', + 'login.mfaVerify': 'Verificar', 'login.oidc.tokenFailed': 'La autenticación falló.', 'login.oidc.invalidState': 'Sesión no válida. Inténtalo de nuevo.', 'login.demoFailed': 'Falló el acceso a la demo', diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts index dc772e4..e20bb6c 100644 --- a/client/src/i18n/translations/fr.ts +++ b/client/src/i18n/translations/fr.ts @@ -164,6 +164,22 @@ const fr: Record = { 'settings.deleteBlockedMessage': 'Vous êtes le seul administrateur. Promouvez un autre utilisateur en tant qu\'administrateur avant de supprimer votre compte.', 'settings.roleUser': 'Utilisateur', 'settings.saveProfile': 'Enregistrer le profil', + 'settings.mfa.title': 'Authentification à deux facteurs (2FA)', + 'settings.mfa.description': 'Ajoute une étape supplémentaire lors de la connexion. Utilisez une application d\'authentification (Google Authenticator, Authy, etc.).', + 'settings.mfa.enabled': '2FA est activé sur votre compte.', + 'settings.mfa.disabled': '2FA n\'est pas activé.', + 'settings.mfa.setup': 'Configurer l\'authentificateur', + 'settings.mfa.scanQr': 'Scannez ce code QR avec votre application ou entrez la clé manuellement.', + 'settings.mfa.secretLabel': 'Clé secrète (saisie manuelle)', + 'settings.mfa.codePlaceholder': 'Code à 6 chiffres', + 'settings.mfa.enable': 'Activer 2FA', + 'settings.mfa.cancelSetup': 'Annuler', + 'settings.mfa.disableTitle': 'Désactiver 2FA', + 'settings.mfa.disableHint': 'Entrez votre mot de passe et un code actuel de votre authentificateur.', + 'settings.mfa.disable': 'Désactiver 2FA', + 'settings.mfa.toastEnabled': 'Authentification à deux facteurs activée', + 'settings.mfa.toastDisabled': 'Authentification à deux facteurs désactivée', + 'settings.mfa.demoBlocked': 'Non disponible en mode démo', 'settings.toast.mapSaved': 'Paramètres de carte enregistrés', 'settings.toast.keysSaved': 'Clés API enregistrées', 'settings.toast.displaySaved': 'Paramètres d\'affichage enregistrés', @@ -211,6 +227,13 @@ const fr: Record = { 'login.username': 'Nom d\'utilisateur', 'login.oidc.registrationDisabled': 'Les inscriptions sont désactivées. Contactez votre administrateur.', 'login.oidc.noEmail': 'Aucun e-mail reçu du fournisseur.', + 'login.mfaTitle': 'Authentification à deux facteurs', + 'login.mfaSubtitle': 'Entrez le code à 6 chiffres de votre application d\'authentification.', + 'login.mfaCodeLabel': 'Code de vérification', + 'login.mfaCodeRequired': 'Entrez le code de votre application d\'authentification.', + 'login.mfaHint': 'Ouvrez Google Authenticator, Authy ou une autre application TOTP.', + 'login.mfaBack': '← Retour à la connexion', + 'login.mfaVerify': 'Vérifier', 'login.oidc.tokenFailed': 'L\'authentification a échoué.', 'login.oidc.invalidState': 'Session invalide. Veuillez réessayer.', 'login.demoFailed': 'Échec de la connexion démo', diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts index 675f1b5..83d0987 100644 --- a/client/src/i18n/translations/nl.ts +++ b/client/src/i18n/translations/nl.ts @@ -164,6 +164,22 @@ const nl: Record = { 'settings.deleteBlockedMessage': 'Je bent de enige beheerder. Maak eerst een andere gebruiker beheerder voordat je je account verwijdert.', 'settings.roleUser': 'Gebruiker', 'settings.saveProfile': 'Profiel opslaan', + 'settings.mfa.title': 'Tweefactorauthenticatie (2FA)', + 'settings.mfa.description': 'Voegt een tweede stap toe bij het inloggen. Gebruik een authenticator-app (Google Authenticator, Authy, etc.).', + 'settings.mfa.enabled': '2FA is ingeschakeld op je account.', + 'settings.mfa.disabled': '2FA is niet ingeschakeld.', + 'settings.mfa.setup': 'Authenticator instellen', + 'settings.mfa.scanQr': 'Scan deze QR-code met je app of voer de sleutel handmatig in.', + 'settings.mfa.secretLabel': 'Geheime sleutel (handmatige invoer)', + 'settings.mfa.codePlaceholder': '6-cijferige code', + 'settings.mfa.enable': '2FA inschakelen', + 'settings.mfa.cancelSetup': 'Annuleren', + 'settings.mfa.disableTitle': '2FA uitschakelen', + 'settings.mfa.disableHint': 'Voer je wachtwoord en een huidige code van je authenticator in.', + 'settings.mfa.disable': '2FA uitschakelen', + 'settings.mfa.toastEnabled': 'Tweefactorauthenticatie ingeschakeld', + 'settings.mfa.toastDisabled': 'Tweefactorauthenticatie uitgeschakeld', + 'settings.mfa.demoBlocked': 'Niet beschikbaar in demomodus', 'settings.toast.mapSaved': 'Kaartinstellingen opgeslagen', 'settings.toast.keysSaved': 'API-sleutels opgeslagen', 'settings.toast.displaySaved': 'Weergave-instellingen opgeslagen', @@ -211,6 +227,13 @@ const nl: Record = { 'login.username': 'Gebruikersnaam', 'login.oidc.registrationDisabled': 'Registratie is uitgeschakeld. Neem contact op met je beheerder.', 'login.oidc.noEmail': 'Geen e-mailadres ontvangen van de provider.', + 'login.mfaTitle': 'Tweefactorauthenticatie', + 'login.mfaSubtitle': 'Voer de 6-cijferige code van je authenticator-app in.', + 'login.mfaCodeLabel': 'Verificatiecode', + 'login.mfaCodeRequired': 'Voer de code van je authenticator-app in.', + 'login.mfaHint': 'Open Google Authenticator, Authy of een andere TOTP-app.', + 'login.mfaBack': '← Terug naar inloggen', + 'login.mfaVerify': 'Verifiëren', 'login.oidc.tokenFailed': 'Authenticatie mislukt.', 'login.oidc.invalidState': 'Ongeldige sessie. Probeer het opnieuw.', 'login.demoFailed': 'Demo-login mislukt', diff --git a/client/src/i18n/translations/ru.ts b/client/src/i18n/translations/ru.ts index 245bdc5..9e385a3 100644 --- a/client/src/i18n/translations/ru.ts +++ b/client/src/i18n/translations/ru.ts @@ -164,6 +164,22 @@ const ru: Record = { 'settings.deleteBlockedMessage': 'Вы единственный администратор. Назначьте другого пользователя администратором перед удалением своего аккаунта.', 'settings.roleUser': 'Пользователь', 'settings.saveProfile': 'Сохранить профиль', + 'settings.mfa.title': 'Двухфакторная аутентификация (2FA)', + 'settings.mfa.description': 'Добавляет второй шаг при входе. Используйте приложение-аутентификатор (Google Authenticator, Authy и др.).', + 'settings.mfa.enabled': '2FA включена для вашего аккаунта.', + 'settings.mfa.disabled': '2FA не включена.', + 'settings.mfa.setup': 'Настроить аутентификатор', + 'settings.mfa.scanQr': 'Отсканируйте QR-код приложением или введите ключ вручную.', + 'settings.mfa.secretLabel': 'Секретный ключ (ручной ввод)', + 'settings.mfa.codePlaceholder': '6-значный код', + 'settings.mfa.enable': 'Включить 2FA', + 'settings.mfa.cancelSetup': 'Отмена', + 'settings.mfa.disableTitle': 'Отключить 2FA', + 'settings.mfa.disableHint': 'Введите пароль аккаунта и текущий код из аутентификатора.', + 'settings.mfa.disable': 'Отключить 2FA', + 'settings.mfa.toastEnabled': 'Двухфакторная аутентификация включена', + 'settings.mfa.toastDisabled': 'Двухфакторная аутентификация отключена', + 'settings.mfa.demoBlocked': 'Недоступно в демо-режиме', 'settings.toast.mapSaved': 'Настройки карты сохранены', 'settings.toast.keysSaved': 'API-ключи сохранены', 'settings.toast.displaySaved': 'Настройки отображения сохранены', @@ -211,6 +227,13 @@ const ru: Record = { 'login.username': 'Имя пользователя', 'login.oidc.registrationDisabled': 'Регистрация отключена. Обратитесь к администратору.', 'login.oidc.noEmail': 'Провайдер не предоставил адрес эл. почты.', + 'login.mfaTitle': 'Двухфакторная аутентификация', + 'login.mfaSubtitle': 'Введите 6-значный код из приложения-аутентификатора.', + 'login.mfaCodeLabel': 'Код подтверждения', + 'login.mfaCodeRequired': 'Введите код из приложения-аутентификатора.', + 'login.mfaHint': 'Откройте Google Authenticator, Authy или другое TOTP-приложение.', + 'login.mfaBack': '← Назад к входу', + 'login.mfaVerify': 'Подтвердить', 'login.oidc.tokenFailed': 'Аутентификация не удалась.', 'login.oidc.invalidState': 'Недействительная сессия. Попробуйте снова.', 'login.demoFailed': 'Ошибка демо-входа', diff --git a/client/src/i18n/translations/zh.ts b/client/src/i18n/translations/zh.ts index addd49e..f603aba 100644 --- a/client/src/i18n/translations/zh.ts +++ b/client/src/i18n/translations/zh.ts @@ -164,6 +164,22 @@ const zh: Record = { 'settings.deleteBlockedMessage': '你是唯一的管理员。请先将其他用户提升为管理员,然后再删除账户。', 'settings.roleUser': '用户', 'settings.saveProfile': '保存资料', + 'settings.mfa.title': '双因素认证 (2FA)', + 'settings.mfa.description': '登录时添加第二步验证。使用身份验证器应用(Google Authenticator、Authy 等)。', + 'settings.mfa.enabled': '您的账户已启用 2FA。', + 'settings.mfa.disabled': '2FA 未启用。', + 'settings.mfa.setup': '设置身份验证器', + 'settings.mfa.scanQr': '使用应用扫描此二维码,或手动输入密钥。', + 'settings.mfa.secretLabel': '密钥(手动输入)', + 'settings.mfa.codePlaceholder': '6 位验证码', + 'settings.mfa.enable': '启用 2FA', + 'settings.mfa.cancelSetup': '取消', + 'settings.mfa.disableTitle': '停用 2FA', + 'settings.mfa.disableHint': '输入您的账户密码和身份验证器中的当前验证码。', + 'settings.mfa.disable': '停用 2FA', + 'settings.mfa.toastEnabled': '双因素认证已启用', + 'settings.mfa.toastDisabled': '双因素认证已停用', + 'settings.mfa.demoBlocked': '演示模式下不可用', 'settings.toast.mapSaved': '地图设置已保存', 'settings.toast.keysSaved': 'API 密钥已保存', 'settings.toast.displaySaved': '显示设置已保存', @@ -211,6 +227,13 @@ const zh: Record = { 'login.username': '用户名', 'login.oidc.registrationDisabled': '注册已关闭。请联系管理员。', 'login.oidc.noEmail': '未从提供商获取到邮箱。', + 'login.mfaTitle': '双因素认证', + 'login.mfaSubtitle': '请输入身份验证器应用中的 6 位验证码。', + 'login.mfaCodeLabel': '验证码', + 'login.mfaCodeRequired': '请输入身份验证器应用中的验证码。', + 'login.mfaHint': '打开 Google Authenticator、Authy 或其他 TOTP 应用。', + 'login.mfaBack': '← 返回登录', + 'login.mfaVerify': '验证', 'login.oidc.tokenFailed': '认证失败。', 'login.oidc.invalidState': '会话无效,请重试。', 'login.demoFailed': '演示登录失败', diff --git a/server/src/db/migrations.ts b/server/src/db/migrations.ts index b62b495..f5cbac1 100644 --- a/server/src/db/migrations.ts +++ b/server/src/db/migrations.ts @@ -194,8 +194,6 @@ function runMigrations(db: Database.Database): void { try { db.exec('ALTER TABLE reservations ADD COLUMN reservation_end_time TEXT'); } catch {} }, () => { - try { db.exec('ALTER TABLE users ADD COLUMN mfa_enabled INTEGER DEFAULT 0'); } catch {} - try { db.exec('ALTER TABLE users ADD COLUMN mfa_secret TEXT'); } catch {} try { db.exec('ALTER TABLE places ADD COLUMN osm_id TEXT'); } catch {} }, () => { @@ -218,6 +216,10 @@ function runMigrations(db: Database.Database): void { created_at DATETIME DEFAULT CURRENT_TIMESTAMP )`); }, + () => { + try { db.exec('ALTER TABLE users ADD COLUMN mfa_enabled INTEGER DEFAULT 0'); } catch {} + try { db.exec('ALTER TABLE users ADD COLUMN mfa_secret TEXT'); } catch {} + }, ]; if (currentVersion < migrations.length) { diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 1a5fae1..6ca4091 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -633,14 +633,21 @@ router.post('/mfa/setup', authenticate, (req: Request, res: Response) => { if (row?.mfa_enabled) { return res.status(400).json({ error: 'MFA is already enabled' }); } - const secret = authenticator.generateSecret(); - mfaSetupPending.set(authReq.user.id, { secret, exp: Date.now() + MFA_SETUP_TTL_MS }); - const otpauth_url = authenticator.keyuri(authReq.user.email, 'NOMAD', secret); + let secret: string, otpauth_url: string; + try { + secret = authenticator.generateSecret(); + mfaSetupPending.set(authReq.user.id, { secret, exp: Date.now() + MFA_SETUP_TTL_MS }); + otpauth_url = authenticator.keyuri(authReq.user.email, 'TREK', secret); + } catch (err) { + console.error('[MFA] Setup error:', err); + return res.status(500).json({ error: 'MFA setup failed' }); + } QRCode.toDataURL(otpauth_url) .then((qr_data_url: string) => { res.json({ secret, otpauth_url, qr_data_url }); }) - .catch(() => { + .catch((err: unknown) => { + console.error('[MFA] QR code generation error:', err); res.status(500).json({ error: 'Could not generate QR code' }); }); });