fix: ask for a fresh 2FA code in every app, not only in Mail, and translate the prompt

This commit is contained in:
Linus Rath
2026-10-28 03:27:03 +01:00
parent 86ff6f5bb2
commit fd3e66e64b
33 changed files with 298 additions and 82 deletions
+2
View File
@@ -11,6 +11,7 @@ import { ProInterfaceRedirect } from "@/components/pro/pro-interface-redirect";
import { ImpersonationReconciler } from "@/components/impersonation/impersonation-reconciler";
import { PluginDialogHost } from "@/components/plugins/plugin-dialog-host";
import { CommandPaletteHost } from "@/components/command-palette/command-palette";
import { TotpReauthDialog } from "@/components/totp-reauth-dialog";
import { PluginConnectDialog } from "@/components/plugins/plugin-connect-dialog";
import { QuickRuleHost } from "@/components/filters/quick-rule-host";
import { PluginConsentDialog } from "@/components/plugins/plugin-consent-dialog";
@@ -64,6 +65,7 @@ export default async function LocaleLayout({
{children}
<PluginDialogHost />
<CommandPaletteHost />
<TotpReauthDialog />
<PluginConnectDialog />
<QuickRuleHost />
<PluginConsentDialog />
@@ -0,0 +1,41 @@
import React from 'react';
import { act, fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
// The 2FA re-auth prompt opens in whichever app made the request (it is
// mounted in the locale layout, not inside mail) and is translated.
vi.mock('next-intl/server', () => ({ setRequestLocale: vi.fn() }));
const { TotpReauthDialog } = await import('../totp-reauth-dialog');
const { useTotpReauthStore } = await import('@/stores/totp-reauth-store');
function containsType(node: unknown, type: unknown): boolean {
if (!node || typeof node !== 'object') return false;
if (Array.isArray(node)) return node.some((child) => containsType(child, type));
const el = node as { type?: unknown; props?: { children?: unknown } };
if (el.type === type) return true;
return containsType(el.props?.children, type);
}
describe('TOTP re-auth dialog', () => {
it('is mounted in the locale layout, so every app can show it', async () => {
const { default: LocaleLayout } = await import('@/app/(main)/[locale]/layout');
const tree = await LocaleLayout({ children: null, params: Promise.resolve({ locale: 'en' }) });
expect(containsType(tree, TotpReauthDialog)).toBe(true);
}, 120_000);
it('asks in the user language and hands the code back', async () => {
render(<TotpReauthDialog />);
let answer: Promise<string | null> = Promise.resolve(null);
act(() => { answer = useTotpReauthStore.getState().requestTotp(); });
expect(screen.getByRole('dialog', { name: 'title' })).toBeInTheDocument();
const field = screen.getByRole('textbox', { name: 'code_label' });
expect(field).toHaveFocus();
fireEvent.change(field, { target: { value: '123456' } });
act(() => { fireEvent.click(screen.getByRole('button', { name: 'verify' })); });
await expect(answer).resolves.toBe('123456');
expect(screen.queryByRole('dialog')).toBeNull();
});
});
-2
View File
@@ -81,7 +81,6 @@ import {
} from "@/components/error";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { PromptDialog } from "@/components/ui/prompt-dialog";
import { TotpReauthDialog } from "@/components/totp-reauth-dialog";
import { DragDropProvider } from "@/contexts/drag-drop-context";
import { jmapErrorType } from "@/lib/jmap/jmap-error";
import { isFilterEmpty, activeFilterCount, DEFAULT_SEARCH_FILTERS } from "@/lib/jmap/search-utils";
@@ -5588,7 +5587,6 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
)}
<ShareNotificationToaster client={client} />
<CalendarEventNotificationToaster client={client} />
<TotpReauthDialog />
{floatingComposer && <MvComposeWindow>{composerNode}</MvComposeWindow>}
</div>
</DragDropProvider>
+52 -80
View File
@@ -1,107 +1,79 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { useFocusTrap } from "@/hooks/use-focus-trap";
import { useTranslations } from "next-intl";
import { useTotpReauthStore } from "@/stores/totp-reauth-store";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Dialog } from "@/components/ui/dialog";
import { Shield } from "@/components/icons";
/**
* Modal dialog that prompts the user for a fresh TOTP code when their
* 2FA session expires (TOTP rotates every ~30 seconds).
* Asks for a fresh TOTP code when a 2FA session expires (TOTP rotates every
* ~30 seconds).
*
* Rendered once at the app root level. The JMAP client triggers it via
* the useTotpReauthStore when a 401 is received on a TOTP-authenticated session.
* Mounted once in the locale layout, so the prompt opens in whichever app
* made the request. The JMAP client opens it through useTotpReauthStore
* when a 401 comes back on a TOTP-authenticated session.
*/
export function TotpReauthDialog() {
const t = useTranslations("totp_reauth");
const tCommon = useTranslations("common");
const { isOpen, submit, cancel } = useTotpReauthStore();
const [code, setCode] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
const dialogRef = useFocusTrap({
isActive: isOpen,
onEscape: cancel,
restoreFocus: true,
});
useEffect(() => {
if (isOpen) {
setCode("");
setTimeout(() => inputRef.current?.focus(), 50);
}
if (isOpen) setCode("");
}, [isOpen]);
if (!isOpen) return null;
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (code.length >= 6) {
submit(code);
}
if (code.length >= 6) submit(code);
};
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center">
<div className="absolute inset-0 bg-black/50" onClick={cancel} />
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-label="Two-factor authentication required"
className="relative z-10 w-full max-w-sm mx-4 bg-background rounded-2xl shadow-xl border border-border p-6"
>
<div className="flex items-center gap-3 mb-4">
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-primary/10">
<Shield className="w-5 h-5 text-primary" />
</div>
<div>
<h2 className="text-lg font-semibold text-foreground">Session Expired</h2>
<p className="text-sm text-muted-foreground">Your 2FA code has rotated</p>
</div>
<Dialog
isOpen={isOpen}
onClose={cancel}
title={t("title")}
description={t("description")}
icon={
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-primary/10 flex-shrink-0">
<Shield className="w-5 h-5 text-primary" />
</div>
<p className="text-sm text-muted-foreground mb-4">
Enter a fresh authentication code from your authenticator app to continue.
</p>
<p className="text-xs text-amber-600 dark:text-amber-400 mb-4 leading-relaxed">
To avoid being prompted repeatedly, ask your administrator to enable OAuth authentication
(either Stalwart&apos;s built-in OAuth or an external identity provider).
</p>
<form onSubmit={handleSubmit} className="space-y-4">
<Input
ref={inputRef}
type="text"
inputMode="numeric"
maxLength={6}
value={code}
onChange={(e) => setCode(e.target.value.replace(/\D/g, ''))}
className="h-12 text-center font-mono tracking-widest text-lg bg-muted/40 border-border/60 rounded-xl focus:bg-background focus:border-primary/50 transition-all duration-200"
placeholder="000000"
autoComplete="one-time-code"
aria-label="Authentication code"
/>
<div className="flex gap-3">
<Button
type="button"
variant="outline"
className="flex-1"
onClick={cancel}
>
Cancel
</Button>
<Button
type="submit"
className="flex-1"
disabled={code.length < 6}
>
Verify
</Button>
</div>
</form>
</div>
</div>
}
size="sm"
showClose={false}
dirty={code.length > 0}
initialFocus={inputRef}
onSubmit={handleSubmit}
footer={
<>
<Button type="button" variant="outline" onClick={cancel}>
{tCommon("cancel")}
</Button>
<Button type="submit" disabled={code.length < 6}>
{t("verify")}
</Button>
</>
}
>
<p className="mt-3 mb-4 text-xs text-amber-600 dark:text-amber-400 leading-relaxed">
{t("oauth_hint")}
</p>
<Input
ref={inputRef}
type="text"
inputMode="numeric"
maxLength={6}
value={code}
onChange={(e) => setCode(e.target.value.replace(/\D/g, ""))}
className="h-12 text-center font-mono tracking-widest text-lg"
placeholder="000000"
autoComplete="one-time-code"
aria-label={t("code_label")}
/>
</Dialog>
);
}
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "العودة إلى تسجيل الدخول"
}
},
"totp_reauth": {
"title": "انتهت الجلسة",
"description": "تغيّر رمز المصادقة الثنائية. أدخل رمزًا جديدًا من تطبيق المصادقة للمتابعة.",
"oauth_hint": "لكي لا تظهر هذه المطالبة مجددًا، اطلب من المسؤول تفعيل تسجيل الدخول عبر OAuth (خدمة OAuth المدمجة في Stalwart أو موفّر هوية خارجي).",
"code_label": "رمز المصادقة",
"verify": "تحقّق"
},
"sidebar": {
"close": "إغلاق",
"compose": "إنشاء رسالة",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "Torna a l'inici de sessió"
}
},
"totp_reauth": {
"title": "La sessió ha caducat",
"description": "El codi de doble factor ha canviat. Introduïu un codi nou de l'aplicació d'autenticació per continuar.",
"oauth_hint": "Perquè aquest avís no torni a aparèixer, demaneu a l'administrador que activi l'inici de sessió amb OAuth (l'OAuth integrat de Stalwart o un proveïdor d'identitat extern).",
"code_label": "Codi d'autenticació",
"verify": "Verifica"
},
"sidebar": {
"close": "Tanca",
"compose": "Redacta",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "Zpět na přihlášení"
}
},
"totp_reauth": {
"title": "Relace vypršela",
"description": "Váš dvoufázový kód se změnil. Pro pokračování zadejte nový kód z ověřovací aplikace.",
"oauth_hint": "Aby se tato výzva neopakovala, požádejte správce o zapnutí přihlašování přes OAuth (vestavěné OAuth ve Stalwartu nebo externího poskytovatele identity).",
"code_label": "Ověřovací kód",
"verify": "Ověřit"
},
"sidebar": {
"close": "Zavřít",
"compose": "Napsat",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "Tilbage til login"
}
},
"totp_reauth": {
"title": "Sessionen er udløbet",
"description": "Din tofaktorkode er ændret. Indtast en ny kode fra din godkendelsesapp for at fortsætte.",
"oauth_hint": "Bed din administrator om at slå login med OAuth til (Stalwarts indbyggede OAuth eller en ekstern identitetsudbyder), så du ikke bliver spurgt igen.",
"code_label": "Godkendelseskode",
"verify": "Bekræft"
},
"sidebar": {
"close": "Luk",
"compose": "Ny besked",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "Zurück zur Anmeldung"
}
},
"totp_reauth": {
"title": "Sitzung abgelaufen",
"description": "Ihr Zwei-Faktor-Code hat sich geändert. Geben Sie einen neuen Code aus Ihrer Authenticator-App ein, um fortzufahren.",
"oauth_hint": "Damit diese Abfrage nicht wiederkommt, bitten Sie Ihre Administration, die Anmeldung über OAuth einzuschalten (das integrierte OAuth von Stalwart oder einen externen Identitätsanbieter).",
"code_label": "Authentifizierungscode",
"verify": "Bestätigen"
},
"sidebar": {
"close": "Schließen",
"compose": "Verfassen",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "Back to login"
}
},
"totp_reauth": {
"title": "Session expired",
"description": "Your two-factor code has changed. Enter a new code from your authenticator app to continue.",
"oauth_hint": "To stop this prompt from coming back, ask your administrator to turn on OAuth sign-in (Stalwart's built-in OAuth or an external identity provider).",
"code_label": "Authentication code",
"verify": "Verify"
},
"sidebar": {
"close": "Close",
"compose": "Compose",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "Volver al inicio de sesión"
}
},
"totp_reauth": {
"title": "La sesión ha caducado",
"description": "Tu código de doble factor ha cambiado. Introduce un código nuevo de tu aplicación de autenticación para continuar.",
"oauth_hint": "Para que este aviso no vuelva a aparecer, pide a tu administrador que active el inicio de sesión con OAuth (el OAuth integrado de Stalwart o un proveedor de identidad externo).",
"code_label": "Código de autenticación",
"verify": "Verificar"
},
"sidebar": {
"close": "Cerrar",
"compose": "Redactar",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "Tagasi"
}
},
"totp_reauth": {
"title": "Seanss on aegunud",
"description": "Teie kaheastmelise tuvastuse kood on muutunud. Jätkamiseks sisestage autentimisrakendusest uus kood.",
"oauth_hint": "Et see küsimus ei korduks, paluge administraatoril lülitada sisse OAuth-sisselogimine (Stalwarti sisseehitatud OAuth või väline identiteedipakkuja).",
"code_label": "Autentimiskood",
"verify": "Kinnita"
},
"sidebar": {
"close": "Sulge",
"compose": "Koosta",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "بازگشت به صفحه ورود"
}
},
"totp_reauth": {
"title": "نشست منقضی شد",
"description": "کد احراز هویت دومرحله‌ای شما تغییر کرده است. برای ادامه، کد جدیدی از برنامهٔ احراز هویت وارد کنید.",
"oauth_hint": "برای اینکه این درخواست دوباره نمایش داده نشود، از مدیر بخواهید ورود با OAuth را فعال کند (OAuth داخلی Stalwart یا یک ارائه‌دهندهٔ هویت خارجی).",
"code_label": "کد احراز هویت",
"verify": "تأیید"
},
"sidebar": {
"close": "بستن",
"compose": "ایمیل جدید",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "Retour à la connexion"
}
},
"totp_reauth": {
"title": "Session expirée",
"description": "Votre code à deux facteurs a changé. Saisissez un nouveau code de votre application d'authentification pour continuer.",
"oauth_hint": "Pour ne plus voir cette demande, demandez à votre administrateur d'activer la connexion OAuth (l'OAuth intégré de Stalwart ou un fournisseur d'identité externe).",
"code_label": "Code d'authentification",
"verify": "Vérifier"
},
"sidebar": {
"close": "Fermer",
"compose": "Composer",
+7
View File
@@ -75,6 +75,13 @@
"jmap_server_label": "שרת",
"jmap_server_auto_picked": "שרת שנבחר מדומיין הדוא״ל שלך."
},
"totp_reauth": {
"title": "פג תוקף ההפעלה",
"description": "קוד האימות הדו-שלבי השתנה. כדי להמשיך, הזינו קוד חדש מאפליקציית האימות.",
"oauth_hint": "כדי שהבקשה הזו לא תחזור, בקשו מהמנהל להפעיל כניסה עם OAuth (ה-OAuth המובנה של Stalwart או ספק זהויות חיצוני).",
"code_label": "קוד אימות",
"verify": "אימות"
},
"sidebar": {
"unified_mailbox": "תיבת דואר מאוחדת",
"close": "סגור",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "Vissza a bejelentkezéshez"
}
},
"totp_reauth": {
"title": "A munkamenet lejárt",
"description": "A kétlépcsős kód megváltozott. A folytatáshoz adjon meg egy új kódot a hitelesítő alkalmazásból.",
"oauth_hint": "Hogy ez a kérés ne jelenjen meg újra, kérje meg a rendszergazdát, hogy kapcsolja be az OAuth-bejelentkezést (a Stalwart beépített OAuth-ját vagy egy külső identitásszolgáltatót).",
"code_label": "Hitelesítő kód",
"verify": "Ellenőrzés"
},
"sidebar": {
"close": "Bezárás",
"compose": "Új levél",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "Torna al login"
}
},
"totp_reauth": {
"title": "Sessione scaduta",
"description": "Il codice a due fattori è cambiato. Inserisci un nuovo codice dall'app di autenticazione per continuare.",
"oauth_hint": "Per non ricevere più questa richiesta, chiedi all'amministratore di attivare l'accesso con OAuth (l'OAuth integrato di Stalwart o un provider di identità esterno).",
"code_label": "Codice di autenticazione",
"verify": "Verifica"
},
"sidebar": {
"close": "Chiudi",
"compose": "Scrivi",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "ログインに戻る"
}
},
"totp_reauth": {
"title": "セッションの有効期限が切れました",
"description": "2 段階認証コードが変わりました。続けるには、認証アプリの新しいコードを入力してください。",
"oauth_hint": "この確認を繰り返さないようにするには、OAuth によるサインイン(Stalwart の組み込み OAuth または外部の ID プロバイダー)を有効にするよう管理者に依頼してください。",
"code_label": "認証コード",
"verify": "確認"
},
"sidebar": {
"close": "閉じる",
"compose": "作成",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "로그인 화면으로 돌아가기"
}
},
"totp_reauth": {
"title": "세션이 만료되었습니다",
"description": "2단계 인증 코드가 바뀌었습니다. 계속하려면 인증 앱의 새 코드를 입력하세요.",
"oauth_hint": "이 요청이 다시 나타나지 않게 하려면 관리자에게 OAuth 로그인(Stalwart 내장 OAuth 또는 외부 ID 공급자)을 켜 달라고 요청하세요.",
"code_label": "인증 코드",
"verify": "확인"
},
"sidebar": {
"close": "닫기",
"compose": "메일 쓰기",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "Atgriezties pie pierakstīšanās"
}
},
"totp_reauth": {
"title": "Sesija ir beigusies",
"description": "Jūsu divfaktoru kods ir mainījies. Lai turpinātu, ievadiet jaunu kodu no autentifikācijas lietotnes.",
"oauth_hint": "Lai šis pieprasījums vairs neparādītos, lūdziet administratoram ieslēgt pieteikšanos ar OAuth (Stalwart iebūvēto OAuth vai ārēju identitātes nodrošinātāju).",
"code_label": "Autentifikācijas kods",
"verify": "Apstiprināt"
},
"sidebar": {
"close": "Aizvērt",
"compose": "Rakstīt",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "Нэвтрэх рүү буцах"
}
},
"totp_reauth": {
"title": "Сесс дууссан",
"description": "Таны хоёр шатлалт кодыг сольсон байна. Үргэлжлүүлэхийн тулд баталгаажуулах аппаас шинэ код оруулна уу.",
"oauth_hint": "Энэ хүсэлт дахин гарахгүй байлгахын тулд админаас OAuth нэвтрэлтийг (Stalwart-ын суурилуулсан OAuth эсвэл гадны таних үйлчилгээ) асаахыг хүснэ үү.",
"code_label": "Баталгаажуулах код",
"verify": "Баталгаажуулах"
},
"sidebar": {
"close": "Хаах",
"compose": "Бичилэх",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "Tilbake til pålogging"
}
},
"totp_reauth": {
"title": "Økten er utløpt",
"description": "Tofaktorkoden din er endret. Skriv inn en ny kode fra autentiseringsappen for å fortsette.",
"oauth_hint": "Be administratoren om å slå på pålogging med OAuth (Stalwarts innebygde OAuth eller en ekstern identitetsleverandør), så slipper du denne forespørselen.",
"code_label": "Autentiseringskode",
"verify": "Bekreft"
},
"sidebar": {
"close": "Lukk",
"compose": "Skriv",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "Terug naar inloggen"
}
},
"totp_reauth": {
"title": "Sessie verlopen",
"description": "Je tweestapscode is veranderd. Voer een nieuwe code uit je authenticator-app in om door te gaan.",
"oauth_hint": "Vraag je beheerder om aanmelden via OAuth aan te zetten (de ingebouwde OAuth van Stalwart of een externe identiteitsprovider), dan krijg je deze vraag niet meer.",
"code_label": "Verificatiecode",
"verify": "Bevestigen"
},
"sidebar": {
"close": "Sluiten",
"compose": "Nieuw bericht",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "Powrót do logowania"
}
},
"totp_reauth": {
"title": "Sesja wygasła",
"description": "Twój kod dwuskładnikowy się zmienił. Aby kontynuować, wpisz nowy kod z aplikacji uwierzytelniającej.",
"oauth_hint": "Aby ta prośba się nie powtarzała, poproś administratora o włączenie logowania przez OAuth (wbudowany OAuth w Stalwart lub zewnętrzny dostawca tożsamości).",
"code_label": "Kod uwierzytelniający",
"verify": "Zweryfikuj"
},
"sidebar": {
"close": "Zamknij",
"compose": "Napisz",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "Voltar ao login"
}
},
"totp_reauth": {
"title": "Sessão expirada",
"description": "O seu código de dois fatores mudou. Introduza um código novo da aplicação de autenticação para continuar.",
"oauth_hint": "Para deixar de ver este pedido, peça ao administrador que ative o início de sessão com OAuth (o OAuth integrado do Stalwart ou um fornecedor de identidade externo).",
"code_label": "Código de autenticação",
"verify": "Verificar"
},
"sidebar": {
"close": "Fechar",
"compose": "Escrever",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "Înapoi la autentificare"
}
},
"totp_reauth": {
"title": "Sesiunea a expirat",
"description": "Codul de autentificare în doi pași s-a schimbat. Introduceți un cod nou din aplicația de autentificare pentru a continua.",
"oauth_hint": "Ca această solicitare să nu mai apară, cereți administratorului să activeze conectarea prin OAuth (OAuth-ul integrat în Stalwart sau un furnizor de identitate extern).",
"code_label": "Cod de autentificare",
"verify": "Verifică"
},
"sidebar": {
"close": "Închide",
"compose": "Compune",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "Вернуться ко входу"
}
},
"totp_reauth": {
"title": "Сеанс истёк",
"description": "Код двухфакторной аутентификации сменился. Чтобы продолжить, введите новый код из приложения-аутентификатора.",
"oauth_hint": "Чтобы этот запрос больше не появлялся, попросите администратора включить вход через OAuth (встроенный OAuth Stalwart или внешний поставщик удостоверений).",
"code_label": "Код аутентификации",
"verify": "Подтвердить"
},
"sidebar": {
"close": "Закрыть",
"compose": "Создать",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "Späť na prihlásenie"
}
},
"totp_reauth": {
"title": "Relácia vypršala",
"description": "Váš dvojfaktorový kód sa zmenil. Ak chcete pokračovať, zadajte nový kód z overovacej aplikácie.",
"oauth_hint": "Aby sa táto výzva neopakovala, požiadajte správcu o zapnutie prihlasovania cez OAuth (vstavané OAuth v Stalwarte alebo externého poskytovateľa identity).",
"code_label": "Overovací kód",
"verify": "Overiť"
},
"sidebar": {
"unified_mailbox": "Zjednotená schránka",
"close": "Zavrieť",
+7
View File
@@ -177,6 +177,13 @@
"back_to_login": "กลับไปหน้าเข้าสู่ระบบ"
}
},
"totp_reauth": {
"title": "เซสชันหมดอายุ",
"description": "รหัสยืนยันตัวตนสองขั้นตอนของคุณเปลี่ยนแล้ว ป้อนรหัสใหม่จากแอปยืนยันตัวตนเพื่อดำเนินการต่อ",
"oauth_hint": "หากไม่ต้องการให้ระบบถามอีก โปรดขอให้ผู้ดูแลระบบเปิดการลงชื่อเข้าใช้ด้วย OAuth (OAuth ในตัวของ Stalwart หรือผู้ให้บริการข้อมูลประจำตัวภายนอก)",
"code_label": "รหัสยืนยันตัวตน",
"verify": "ยืนยัน"
},
"sidebar": {
"close": "ปิดแถบด้านข้าง",
"compose": "เขียนจดหมาย",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "Giriş sayfasına dön"
}
},
"totp_reauth": {
"title": "Oturumun süresi doldu",
"description": "İki adımlı doğrulama kodunuz değişti. Devam etmek için kimlik doğrulama uygulamanızdan yeni bir kod girin.",
"oauth_hint": "Bu isteğin tekrar çıkmaması için yöneticinizden OAuth ile oturum açmayı (Stalwart'ın yerleşik OAuth'u veya harici bir kimlik sağlayıcı) açmasını isteyin.",
"code_label": "Doğrulama kodu",
"verify": "Doğrula"
},
"sidebar": {
"close": "Kapat",
"compose": "Yeni E-posta",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "Назад до входу"
}
},
"totp_reauth": {
"title": "Сеанс завершився",
"description": "Код двофакторної автентифікації змінився. Щоб продовжити, введіть новий код із застосунку автентифікації.",
"oauth_hint": "Щоб цей запит більше не з'являвся, попросіть адміністратора ввімкнути вхід через OAuth (вбудований OAuth Stalwart або зовнішнього постачальника ідентичності).",
"code_label": "Код автентифікації",
"verify": "Підтвердити"
},
"sidebar": {
"close": "Закрити",
"compose": "Скласти",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "返回登入頁面"
}
},
"totp_reauth": {
"title": "工作階段已過期",
"description": "您的雙重驗證碼已變更。請輸入驗證器應用程式中的新驗證碼以繼續。",
"oauth_hint": "若不想再看到此提示,請請管理員啟用 OAuth 登入(Stalwart 內建的 OAuth 或外部身分提供者)。",
"code_label": "驗證碼",
"verify": "驗證"
},
"sidebar": {
"close": "關閉",
"compose": "撰寫郵件",
+7
View File
@@ -76,6 +76,13 @@
"back_to_login": "返回登录"
}
},
"totp_reauth": {
"title": "会话已过期",
"description": "您的两步验证码已更新。请输入身份验证器应用中的新验证码以继续。",
"oauth_hint": "如不想再看到此提示,请让管理员启用 OAuth 登录(Stalwart 内置的 OAuth 或外部身份提供商)。",
"code_label": "验证码",
"verify": "验证"
},
"sidebar": {
"close": "关闭",
"compose": "撰写邮件",