mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
fix: show password and two-factor settings after a password and TOTP sign-in #1432
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { AccountSecuritySettings } from '../account-security-settings';
|
||||
import { useAccountStore, type AccountEntry } from '@/stores/account-store';
|
||||
import { useAccountSecurityStore } from '@/stores/account-security-store';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
toastSuccess: vi.fn(),
|
||||
toastError: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('next-intl', () => {
|
||||
const t = Object.assign((key: string) => key, { raw: (key: string) => key });
|
||||
return { useTranslations: () => t, useLocale: () => 'en' };
|
||||
});
|
||||
|
||||
vi.mock('@/hooks/use-config', () => ({
|
||||
useConfig: () => ({ oauthEnabled: false }),
|
||||
}));
|
||||
|
||||
vi.mock('@/stores/toast-store', () => ({
|
||||
toast: { success: mocks.toastSuccess, error: mocks.toastError, info: vi.fn() },
|
||||
}));
|
||||
|
||||
function signIn(authMode: AccountEntry['authMode'], providerSession: boolean | undefined) {
|
||||
const entry = {
|
||||
id: 'me@example.com@mail.example.com',
|
||||
label: 'me@example.com',
|
||||
serverUrl: 'https://mail.example.com',
|
||||
username: 'me@example.com',
|
||||
authMode,
|
||||
cookieSlot: 0,
|
||||
rememberMe: false,
|
||||
providerSession,
|
||||
displayName: '',
|
||||
email: 'me@example.com',
|
||||
avatarColor: '#000000',
|
||||
lastLoginAt: 0,
|
||||
isConnected: true,
|
||||
hasError: false,
|
||||
isDefault: true,
|
||||
} as AccountEntry;
|
||||
useAccountStore.setState({ accounts: [entry], activeAccountId: entry.id });
|
||||
// No client keeps the probe effect quiet; the verdict is set directly.
|
||||
useAuthStore.setState({ isAuthenticated: false, authMode, client: null, activeAccountId: entry.id });
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useAccountSecurityStore.setState({ isStalwart: true, isProbing: false, otpEnabled: false, isSaving: false });
|
||||
});
|
||||
|
||||
describe('AccountSecuritySettings sign-in section', () => {
|
||||
it('shows password and two-factor to a password sign-in', () => {
|
||||
signIn('basic', false);
|
||||
render(<AccountSecuritySettings />);
|
||||
expect(screen.getByText('password.title')).toBeInTheDocument();
|
||||
expect(screen.getByText('totp.section_title')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows them to a password plus TOTP sign-in, which runs on OAuth tokens (#1432)', () => {
|
||||
signIn('oauth', false);
|
||||
render(<AccountSecuritySettings />);
|
||||
expect(screen.getByText('password.title')).toBeInTheDocument();
|
||||
expect(screen.getByText('totp.section_title')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['an identity-provider sign-in', 'oauth', true],
|
||||
['a session from before providerSession was recorded', 'oauth', undefined],
|
||||
['an access-token sign-in', 'token', false],
|
||||
] as const)('hides them for %s', (_name, authMode, providerSession) => {
|
||||
signIn(authMode, providerSession);
|
||||
render(<AccountSecuritySettings />);
|
||||
expect(screen.queryByText('password.title')).toBeNull();
|
||||
expect(screen.queryByText('totp.section_title')).toBeNull();
|
||||
expect(screen.getByText('email_client.title')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('AccountSecuritySettings password change', () => {
|
||||
function changePassword(container: HTMLElement) {
|
||||
const [current, ...fresh] = [
|
||||
container.querySelector('input[autocomplete="current-password"]'),
|
||||
...container.querySelectorAll('input[autocomplete="new-password"]'),
|
||||
] as HTMLInputElement[];
|
||||
fireEvent.change(current, { target: { value: 'old-password' } });
|
||||
for (const input of fresh) fireEvent.change(input, { target: { value: 'new-password-1' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'password.submit' }));
|
||||
}
|
||||
|
||||
it('signs a token session out, since Stalwart revokes its tokens with the old password', async () => {
|
||||
signIn('oauth', false);
|
||||
const change = vi.fn(async () => {});
|
||||
const logout = vi.fn(async () => {});
|
||||
useAccountSecurityStore.setState({ changePassword: change });
|
||||
useAuthStore.setState({ logout });
|
||||
const { container } = render(<AccountSecuritySettings />);
|
||||
changePassword(container);
|
||||
await waitFor(() => expect(logout).toHaveBeenCalledWith({ endProviderSession: false }));
|
||||
expect(change).toHaveBeenCalledWith('old-password', 'new-password-1', undefined);
|
||||
expect(mocks.toastSuccess).toHaveBeenCalledWith('password.success', 'password.sign_in_again');
|
||||
});
|
||||
|
||||
it('keeps a password session signed in', async () => {
|
||||
signIn('basic', false);
|
||||
const change = vi.fn(async () => {});
|
||||
const logout = vi.fn(async () => {});
|
||||
useAccountSecurityStore.setState({ changePassword: change });
|
||||
useAuthStore.setState({ logout });
|
||||
const { container } = render(<AccountSecuritySettings />);
|
||||
changePassword(container);
|
||||
await waitFor(() => expect(mocks.toastSuccess).toHaveBeenCalledWith('password.success'));
|
||||
expect(logout).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,7 @@ import { toast } from '@/stores/toast-store';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { sanitizeI18nHtml } from '@/lib/email-sanitization';
|
||||
import { IS_LITE } from '@/lib/lite';
|
||||
import { isPasswordSession } from '@/lib/auth/password-session';
|
||||
import { useConfig } from '@/hooks/use-config';
|
||||
import { totpIssuer } from '@/lib/totp-issuer';
|
||||
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
@@ -54,6 +55,13 @@ function PasswordChangeSection() {
|
||||
setOtpCode('');
|
||||
setNewPassword('');
|
||||
setConfirmPassword('');
|
||||
// Stalwart revokes the account's tokens with the old password, refresh
|
||||
// token included, so a password plus TOTP session ends here (#1432).
|
||||
if (useAuthStore.getState().authMode !== 'basic') {
|
||||
toast.success(t('password.success'), t('password.sign_in_again'));
|
||||
void useAuthStore.getState().logout({ endProviderSession: false });
|
||||
return;
|
||||
}
|
||||
toast.success(t('password.success'));
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : t('password.error_generic');
|
||||
@@ -1475,9 +1483,10 @@ export function AccountSecuritySettings() {
|
||||
const t = useTranslations('settings.security');
|
||||
const { isStalwart, isProbing, probe, fetchAll, fetchAuthInfo, fetchPublicKeys, fetchCryptoInfo } = useAccountSecurityStore();
|
||||
const { isAuthenticated, authMode, client } = useAuthStore();
|
||||
// OAuth and access-token sign-ins hold no password to change, and mail
|
||||
// apps need an app password instead.
|
||||
const withoutPassword = authMode === 'oauth' || authMode === 'token';
|
||||
const activeAccount = useAccountStore((s) => s.getActiveAccount());
|
||||
// Identity-provider and access-token sign-ins hold no password to change
|
||||
// here, and mail apps need an app password instead.
|
||||
const withoutPassword = !isPasswordSession(authMode, activeAccount);
|
||||
|
||||
// Wait for `client` before probing. On reload the persisted `isAuthenticated`
|
||||
// flips true before the async OAuth reconnect sets `client`; probing in that
|
||||
@@ -1541,8 +1550,9 @@ export function AccountSecuritySettings() {
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Sign-in: password and two-factor. OAuth and token sessions have
|
||||
neither here, so the page opens with the apps and devices section. */}
|
||||
{/* Sign-in: password and two-factor. Identity-provider and token
|
||||
sessions have neither here, so the page opens with the apps and
|
||||
devices section. */}
|
||||
{!withoutPassword && (
|
||||
<SettingsSection id="signin" title={t('title')} description={t('description')}>
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -16,6 +16,7 @@ import { getMaxAccounts } from '@/lib/account-utils';
|
||||
import { formatFileSize, cn } from '@/lib/utils';
|
||||
import { toUnicodeDomain, toUnicodeEmail } from '@/lib/idn';
|
||||
import { IS_LITE } from '@/lib/lite';
|
||||
import { isPasswordSession } from '@/lib/auth/password-session';
|
||||
import { usePolicyStore } from '@/stores/policy-store';
|
||||
import { AccountArchiveSettings } from './account-archive-settings';
|
||||
import { DisplayNameSection } from './account-security-settings';
|
||||
@@ -48,9 +49,11 @@ export function AccountSettings() {
|
||||
const archiveEnabled = usePolicyStore((s) => s.isFeatureEnabled('accountArchiveEnabled')) && !IS_LITE && !isDemoMode;
|
||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
||||
// The display name is editable on Stalwart (the principal's name) for
|
||||
// password sign-ins; OAuth and token sessions cannot manage the principal.
|
||||
// password sign-ins; identity-provider and token sessions cannot manage
|
||||
// the principal.
|
||||
const { isStalwart, probe, fetchPrincipal } = useAccountSecurityStore();
|
||||
const canEditDisplayName = isStalwart === true && authMode !== 'oauth' && authMode !== 'token';
|
||||
const activeAccount = useAccountStore((s) => (activeAccountId ? s.getAccountById(activeAccountId) : undefined));
|
||||
const canEditDisplayName = isStalwart === true && isPasswordSession(authMode, activeAccount);
|
||||
// Wait for `client` before probing, as the Security page does: on reload
|
||||
// the persisted session flips authenticated before the client reconnects.
|
||||
useEffect(() => {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { AccountEntry } from '@/stores/account-store';
|
||||
|
||||
/**
|
||||
* The session was signed in with the account's own password, so the server
|
||||
* holds a password (and maybe TOTP) the user can manage here. A password plus
|
||||
* TOTP login runs on OAuth tokens (authMode 'oauth') but is still one of them
|
||||
* (#1432). A sign-in through the identity provider may come from an outside
|
||||
* IdP that holds the password instead, and a pasted access token says nothing
|
||||
* about how the account signs in. Accounts signed in before `providerSession`
|
||||
* was recorded count as provider sign-ins.
|
||||
*/
|
||||
export function isPasswordSession(
|
||||
authMode: AccountEntry['authMode'] | null | undefined,
|
||||
account: Pick<AccountEntry, 'providerSession'> | null | undefined,
|
||||
): boolean {
|
||||
if (authMode === 'basic') return true;
|
||||
return authMode === 'oauth' && account?.providerSession === false;
|
||||
}
|
||||
@@ -1980,6 +1980,7 @@
|
||||
"confirm": "تأكيد كلمة المرور الجديدة",
|
||||
"submit": "تغيير كلمة المرور",
|
||||
"success": "تم تغيير كلمة المرور بنجاح",
|
||||
"sign_in_again": "سجّل الدخول مرة أخرى باستخدام كلمة المرور الجديدة.",
|
||||
"error_title": "فشل تغيير كلمة المرور",
|
||||
"error_mismatch": "كلمتا المرور الجديدتان غير متطابقتين",
|
||||
"error_min_length": "يجب أن تتكون كلمة المرور من 8 أحرف على الأقل",
|
||||
|
||||
@@ -1980,6 +1980,7 @@
|
||||
"confirm": "Confirma la contrasenya nova",
|
||||
"submit": "Canvia la contrasenya",
|
||||
"success": "Contrasenya canviada correctament",
|
||||
"sign_in_again": "Torna a iniciar la sessió amb la contrasenya nova.",
|
||||
"error_title": "No s'ha pogut canviar la contrasenya",
|
||||
"error_mismatch": "Les contrasenyes noves no coincideixen",
|
||||
"error_min_length": "La contrasenya ha de tenir com a mínim 8 caràcters",
|
||||
|
||||
@@ -1973,6 +1973,7 @@
|
||||
"confirm": "Potvrdit nové heslo",
|
||||
"submit": "Změnit heslo",
|
||||
"success": "Heslo bylo změněno",
|
||||
"sign_in_again": "Přihlaste se znovu pomocí nového hesla.",
|
||||
"error_title": "Změna hesla selhala",
|
||||
"error_mismatch": "Nová hesla se neshodují",
|
||||
"error_min_length": "Heslo musí obsahovat alespoň 8 znaků",
|
||||
|
||||
@@ -1976,6 +1976,7 @@
|
||||
"confirm": "Bekræft ny adgangskode",
|
||||
"submit": "Skift adgangskode",
|
||||
"success": "Adgangskode ændret succesfuldt",
|
||||
"sign_in_again": "Log ind igen med din nye adgangskode.",
|
||||
"error_title": "Adgangskodeændring mislykkedes",
|
||||
"error_mismatch": "De nye adgangskoder er ikke ens",
|
||||
"error_min_length": "Adgangskoden skal være mindst 8 tegn",
|
||||
|
||||
@@ -1973,6 +1973,7 @@
|
||||
"confirm": "Neues Passwort bestätigen",
|
||||
"submit": "Passwort ändern",
|
||||
"success": "Passwort erfolgreich geändert",
|
||||
"sign_in_again": "Melden Sie sich mit Ihrem neuen Passwort erneut an.",
|
||||
"error_title": "Passwortänderung fehlgeschlagen",
|
||||
"error_mismatch": "Neue Passwörter stimmen nicht überein",
|
||||
"error_min_length": "Das Passwort muss mindestens 8 Zeichen lang sein",
|
||||
|
||||
@@ -1980,6 +1980,7 @@
|
||||
"confirm": "Confirm new password",
|
||||
"submit": "Change password",
|
||||
"success": "Password changed successfully",
|
||||
"sign_in_again": "Sign in again with your new password.",
|
||||
"error_title": "Password change failed",
|
||||
"error_mismatch": "New passwords do not match",
|
||||
"error_min_length": "Password must be at least 8 characters",
|
||||
|
||||
@@ -1973,6 +1973,7 @@
|
||||
"confirm": "Confirmar nueva contraseña",
|
||||
"submit": "Cambiar contraseña",
|
||||
"success": "Contraseña cambiada exitosamente",
|
||||
"sign_in_again": "Vuelve a iniciar sesión con tu nueva contraseña.",
|
||||
"error_title": "Error al cambiar la contraseña",
|
||||
"error_mismatch": "Las nuevas contraseñas no coinciden",
|
||||
"error_min_length": "La contraseña debe tener al menos 8 caracteres",
|
||||
|
||||
@@ -1980,6 +1980,7 @@
|
||||
"confirm": "Kinnita uus parool",
|
||||
"submit": "Muuda parooli",
|
||||
"success": "Parool muudeti edukalt",
|
||||
"sign_in_again": "Logi uuesti sisse oma uue parooliga.",
|
||||
"error_title": "Parooli muutmine ebaõnnestus",
|
||||
"error_mismatch": "Uued paroolid ei kattu",
|
||||
"error_min_length": "Parool peab olema vähemalt 8 tähemärki pikk",
|
||||
|
||||
@@ -1980,6 +1980,7 @@
|
||||
"confirm": "تأیید رمز جدید",
|
||||
"submit": "تغییر رمز",
|
||||
"success": "رمز تغییر کرد",
|
||||
"sign_in_again": "با گذرواژه جدید دوباره وارد شوید.",
|
||||
"error_title": "خطا",
|
||||
"error_mismatch": "مطابقت ندارد",
|
||||
"error_min_length": "حداقل ۸ کاراکتر",
|
||||
|
||||
@@ -1973,6 +1973,7 @@
|
||||
"confirm": "Confirmer le nouveau mot de passe",
|
||||
"submit": "Changer le mot de passe",
|
||||
"success": "Mot de passe changé avec succès",
|
||||
"sign_in_again": "Reconnectez-vous avec votre nouveau mot de passe.",
|
||||
"error_title": "Échec du changement de mot de passe",
|
||||
"error_mismatch": "Les nouveaux mots de passe ne correspondent pas",
|
||||
"error_min_length": "Le mot de passe doit comporter au moins 8 caractères",
|
||||
|
||||
@@ -1938,6 +1938,7 @@
|
||||
"confirm": "אשר סיסמה חדשה",
|
||||
"submit": "שנה סיסמה",
|
||||
"success": "הסיסמה שונתה בהצלחה",
|
||||
"sign_in_again": "יש להתחבר שוב עם הסיסמה החדשה.",
|
||||
"error_title": "שינוי הסיסמה נכשל",
|
||||
"error_mismatch": "סיסמאות חדשות אינן תואמות",
|
||||
"error_min_length": "הסיסמה חייבת להיות לפחות 8 תווים",
|
||||
|
||||
@@ -1976,6 +1976,7 @@
|
||||
"confirm": "Új jelszó megerősítése",
|
||||
"submit": "Jelszó módosítása",
|
||||
"success": "Jelszó sikeresen módosítva",
|
||||
"sign_in_again": "Jelentkezz be újra az új jelszavaddal.",
|
||||
"error_title": "Jelszó módosítás sikertelen",
|
||||
"error_mismatch": "Az új jelszavak nem egyeznek",
|
||||
"error_min_length": "A jelszónak legalább 8 karakter hosszúnak kell lennie",
|
||||
|
||||
@@ -1973,6 +1973,7 @@
|
||||
"confirm": "Conferma nuova password",
|
||||
"submit": "Cambia password",
|
||||
"success": "Password cambiata con successo",
|
||||
"sign_in_again": "Accedi di nuovo con la nuova password.",
|
||||
"error_title": "Cambio password fallito",
|
||||
"error_mismatch": "Le nuove password non corrispondono",
|
||||
"error_min_length": "La password deve contenere almeno 8 caratteri",
|
||||
|
||||
@@ -1973,6 +1973,7 @@
|
||||
"confirm": "新しいパスワードの確認",
|
||||
"submit": "パスワードを変更",
|
||||
"success": "パスワードが正常に変更されました",
|
||||
"sign_in_again": "新しいパスワードでもう一度サインインしてください。",
|
||||
"error_title": "パスワードの変更に失敗しました",
|
||||
"error_mismatch": "新しいパスワードが一致しません",
|
||||
"error_min_length": "パスワードは8文字以上である必要があります",
|
||||
|
||||
@@ -1973,6 +1973,7 @@
|
||||
"confirm": "새 비밀번호 확인",
|
||||
"submit": "비밀번호 변경",
|
||||
"success": "비밀번호가 성공적으로 변경되었어요",
|
||||
"sign_in_again": "새 비밀번호로 다시 로그인하세요.",
|
||||
"error_title": "비밀번호 변경 실패",
|
||||
"error_mismatch": "새 비밀번호가 서로 일치하지 않아요",
|
||||
"error_min_length": "비밀번호는 최소 8자 이상이어야 해요",
|
||||
|
||||
@@ -1973,6 +1973,7 @@
|
||||
"confirm": "Apstipriniet jauno paroli",
|
||||
"submit": "Mainīt paroli",
|
||||
"success": "Parole veiksmīgi mainīta",
|
||||
"sign_in_again": "Piesakieties vēlreiz ar savu jauno paroli.",
|
||||
"error_title": "Neizdevās mainīt paroli",
|
||||
"error_mismatch": "Jaunās paroles nesakrīt",
|
||||
"error_min_length": "Parolei jābūt vismaz 8 rakstzīmes garai",
|
||||
|
||||
@@ -1980,6 +1980,7 @@
|
||||
"confirm": "Шинэ нууц үгээ баталгаажуулах",
|
||||
"submit": "Нууц үг солих",
|
||||
"success": "Нууц үг амжилттай өөрчлөгдсөн",
|
||||
"sign_in_again": "Шинэ нууц үгээрээ дахин нэвтэрнэ үү.",
|
||||
"error_title": "Нууц үг солигдож чадсангүй",
|
||||
"error_mismatch": "Шинэ нууц үг таарахгүй байна",
|
||||
"error_min_length": "Нууц үг хамгийн багадаа 8 тэмдэгт байх ёстой",
|
||||
|
||||
@@ -1980,6 +1980,7 @@
|
||||
"confirm": "Bekreft nytt passord",
|
||||
"submit": "Endre passord",
|
||||
"success": "Passordet ble endret",
|
||||
"sign_in_again": "Logg inn på nytt med det nye passordet ditt.",
|
||||
"error_title": "Passordendring mislyktes",
|
||||
"error_mismatch": "De nye passordene er ikke like",
|
||||
"error_min_length": "Passordet må inneholde minst 8 tegn",
|
||||
|
||||
@@ -1973,6 +1973,7 @@
|
||||
"confirm": "Bevestig nieuw wachtwoord",
|
||||
"submit": "Wachtwoord wijzigen",
|
||||
"success": "Wachtwoord succesvol gewijzigd",
|
||||
"sign_in_again": "Meld je opnieuw aan met je nieuwe wachtwoord.",
|
||||
"error_title": "Wachtwoord wijzigen mislukt",
|
||||
"error_mismatch": "Nieuwe wachtwoorden komen niet overeen",
|
||||
"error_min_length": "Wachtwoord moet minimaal 8 tekens bevatten",
|
||||
|
||||
@@ -1973,6 +1973,7 @@
|
||||
"confirm": "Potwierdź nowe hasło",
|
||||
"submit": "Zmień hasło",
|
||||
"success": "Hasło zostało zmienione",
|
||||
"sign_in_again": "Zaloguj się ponownie, używając nowego hasła.",
|
||||
"error_title": "Zmiana hasła nie powiodła się",
|
||||
"error_mismatch": "Nowe hasła nie są zgodne",
|
||||
"error_min_length": "Hasło musi mieć co najmniej 8 znaków",
|
||||
|
||||
@@ -1973,6 +1973,7 @@
|
||||
"confirm": "Confirmar nova senha",
|
||||
"submit": "Alterar senha",
|
||||
"success": "Senha alterada com sucesso",
|
||||
"sign_in_again": "Inicie sessão novamente com a sua nova palavra-passe.",
|
||||
"error_title": "Falha ao alterar a senha",
|
||||
"error_mismatch": "As novas senhas não coincidem",
|
||||
"error_min_length": "A senha deve ter pelo menos 8 caracteres",
|
||||
|
||||
@@ -1980,6 +1980,7 @@
|
||||
"confirm": "Confirmă noua parolă",
|
||||
"submit": "Schimbă parola",
|
||||
"success": "Parola a fost modificată cu succes",
|
||||
"sign_in_again": "Conectează-te din nou cu parola nouă.",
|
||||
"error_title": "Schimbarea parolei a eșuat",
|
||||
"error_mismatch": "Noile parole nu se potrivesc",
|
||||
"error_min_length": "Parola trebuie să conțină cel puțin 8 caractere",
|
||||
|
||||
@@ -1973,6 +1973,7 @@
|
||||
"confirm": "Подтвердите новый пароль",
|
||||
"submit": "Изменить пароль",
|
||||
"success": "Пароль успешно изменён",
|
||||
"sign_in_again": "Войдите снова с новым паролем.",
|
||||
"error_title": "Не удалось изменить пароль",
|
||||
"error_mismatch": "Новые пароли не совпадают",
|
||||
"error_min_length": "Пароль должен содержать не менее 8 символов",
|
||||
|
||||
@@ -1980,6 +1980,7 @@
|
||||
"confirm": "Potvrdiť nové heslo",
|
||||
"submit": "Zmeniť heslo",
|
||||
"success": "Heslo bolo zmenené",
|
||||
"sign_in_again": "Prihláste sa znova pomocou nového hesla.",
|
||||
"error_title": "Zmena hesla zlyhala",
|
||||
"error_mismatch": "Nové heslá sa nezhodujú",
|
||||
"error_min_length": "Heslo musí mať aspoň 8 znakov",
|
||||
|
||||
@@ -4506,6 +4506,7 @@
|
||||
"confirm": "ยืนยันรหัสผ่านใหม่",
|
||||
"submit": "เปลี่ยนรหัสผ่าน",
|
||||
"success": "เปลี่ยนรหัสผ่านสำเร็จแล้ว",
|
||||
"sign_in_again": "โปรดลงชื่อเข้าใช้อีกครั้งด้วยรหัสผ่านใหม่",
|
||||
"error_title": "เปลี่ยนรหัสผ่านไม่สำเร็จ",
|
||||
"error_mismatch": "รหัสผ่านใหม่ไม่ตรงกัน",
|
||||
"error_min_length": "รหัสผ่านต้องมีความยาวอย่างน้อย 8 อักขระ",
|
||||
|
||||
@@ -1973,6 +1973,7 @@
|
||||
"confirm": "Yeni Parolayı Onayla",
|
||||
"submit": "Parolayı Değiştir",
|
||||
"success": "Parola başarıyla değiştirildi",
|
||||
"sign_in_again": "Yeni şifrenizle tekrar oturum açın.",
|
||||
"error_title": "Parola değiştirme başarısız",
|
||||
"error_mismatch": "Yeni parolalar eşleşmiyor",
|
||||
"error_min_length": "Parola en az 8 karakter olmalıdır",
|
||||
|
||||
@@ -1973,6 +1973,7 @@
|
||||
"confirm": "Підтвердьте новий пароль",
|
||||
"submit": "Змінити пароль",
|
||||
"success": "Пароль успішно змінено",
|
||||
"sign_in_again": "Увійдіть знову з новим паролем.",
|
||||
"error_title": "Не вдалося змінити пароль",
|
||||
"error_mismatch": "Нові паролі не збігаються",
|
||||
"error_min_length": "Пароль має бути не менше 8 символів",
|
||||
|
||||
@@ -1980,6 +1980,7 @@
|
||||
"confirm": "確認新密碼",
|
||||
"submit": "變更密碼",
|
||||
"success": "已成功變更密碼",
|
||||
"sign_in_again": "請使用新密碼重新登入。",
|
||||
"error_title": "變更密碼失敗",
|
||||
"error_mismatch": "兩次輸入的新密碼不相符",
|
||||
"error_min_length": "密碼必須至少為 8 個字元",
|
||||
|
||||
@@ -1973,6 +1973,7 @@
|
||||
"confirm": "确认新密码",
|
||||
"submit": "更改密码",
|
||||
"success": "密码修改成功",
|
||||
"sign_in_again": "请使用新密码重新登录。",
|
||||
"error_title": "密码修改失败",
|
||||
"error_mismatch": "新密码不匹配",
|
||||
"error_min_length": "密码必须至少为 8 个字符",
|
||||
|
||||
Reference in New Issue
Block a user