mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
feat: add registration invite code handling and improve error translations
- Updated AuthViews component to conditionally show invite code field based on registrationInviteRequired prop. - Enhanced error handling in auth API functions to use translateServerError for better user feedback. - Added new translations for various server error messages in English, Spanish, Russian, Chinese (Simplified and Traditional). - Modified demo initial bootstrap state to include registrationInviteRequired flag. - Updated types to include registrationInviteRequired in WebBootstrapResponse.
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
|||||||
} from './handlers/notifications';
|
} from './handlers/notifications';
|
||||||
import { handlePublicUploadSendFile } from './handlers/sends';
|
import { handlePublicUploadSendFile } from './handlers/sends';
|
||||||
import { jsonResponse } from './utils/response';
|
import { jsonResponse } from './utils/response';
|
||||||
|
import { StorageService } from './services/storage';
|
||||||
import type { Env } from './types';
|
import type { Env } from './types';
|
||||||
|
|
||||||
type PublicRateLimiter = (category?: string, maxRequests?: number) => Promise<Response | null>;
|
type PublicRateLimiter = (category?: string, maxRequests?: number) => Promise<Response | null>;
|
||||||
@@ -31,6 +32,7 @@ export interface WebBootstrapResponse {
|
|||||||
defaultKdfIterations: number;
|
defaultKdfIterations: number;
|
||||||
jwtUnsafeReason: JwtUnsafeReason;
|
jwtUnsafeReason: JwtUnsafeReason;
|
||||||
jwtSecretMinLength: number;
|
jwtSecretMinLength: number;
|
||||||
|
registrationInviteRequired: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSameOriginWriteRequest(request: Request): boolean {
|
function isSameOriginWriteRequest(request: Request): boolean {
|
||||||
@@ -238,7 +240,7 @@ async function handleWebsiteIcon(host: string, fallbackMode: 'default' | 'not-fo
|
|||||||
return fallbackMode === 'not-found' ? handleMissingWebsiteIcon() : handleNwFavicon();
|
return fallbackMode === 'not-found' ? handleMissingWebsiteIcon() : handleNwFavicon();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildWebBootstrapResponse(env: Env): WebBootstrapResponse {
|
export async function buildWebBootstrapResponse(env: Env): Promise<WebBootstrapResponse> {
|
||||||
const secret = (env.JWT_SECRET || '').trim();
|
const secret = (env.JWT_SECRET || '').trim();
|
||||||
const jwtUnsafeReason =
|
const jwtUnsafeReason =
|
||||||
!secret
|
!secret
|
||||||
@@ -248,11 +250,14 @@ export function buildWebBootstrapResponse(env: Env): WebBootstrapResponse {
|
|||||||
: secret.length < LIMITS.auth.jwtSecretMinLength
|
: secret.length < LIMITS.auth.jwtSecretMinLength
|
||||||
? 'too_short'
|
? 'too_short'
|
||||||
: null;
|
: null;
|
||||||
|
const storage = new StorageService(env.DB);
|
||||||
|
const userCount = await storage.getUserCount();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
defaultKdfIterations: LIMITS.auth.defaultKdfIterations,
|
defaultKdfIterations: LIMITS.auth.defaultKdfIterations,
|
||||||
jwtUnsafeReason,
|
jwtUnsafeReason,
|
||||||
jwtSecretMinLength: LIMITS.auth.jwtSecretMinLength,
|
jwtSecretMinLength: LIMITS.auth.jwtSecretMinLength,
|
||||||
|
registrationInviteRequired: userCount > 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,7 +281,7 @@ export async function handlePublicRoute(
|
|||||||
if ((path === '/api/web-bootstrap' || path === '/web-bootstrap') && method === 'GET') {
|
if ((path === '/api/web-bootstrap' || path === '/web-bootstrap') && method === 'GET') {
|
||||||
const blocked = await enforcePublicRateLimit('public-read', LIMITS.rateLimit.publicReadRequestsPerMinute);
|
const blocked = await enforcePublicRateLimit('public-read', LIMITS.rateLimit.publicReadRequestsPerMinute);
|
||||||
if (blocked) return blocked;
|
if (blocked) return blocked;
|
||||||
return jsonResponse(buildWebBootstrapResponse(env));
|
return jsonResponse(await buildWebBootstrapResponse(env));
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconMatch = path.match(/^\/icons\/([^/]+)\/icon\.png$/i);
|
const iconMatch = path.match(/^\/icons\/([^/]+)\/icon\.png$/i);
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ export default function App() {
|
|||||||
const [session, setSessionState] = useState<SessionState | null>(initialBootstrap.session);
|
const [session, setSessionState] = useState<SessionState | null>(initialBootstrap.session);
|
||||||
const [profile, setProfile] = useState<Profile | null>(initialProfileSnapshot);
|
const [profile, setProfile] = useState<Profile | null>(initialProfileSnapshot);
|
||||||
const [defaultKdfIterations, setDefaultKdfIterations] = useState(initialBootstrap.defaultKdfIterations);
|
const [defaultKdfIterations, setDefaultKdfIterations] = useState(initialBootstrap.defaultKdfIterations);
|
||||||
|
const [registrationInviteRequired, setRegistrationInviteRequired] = useState(initialBootstrap.registrationInviteRequired);
|
||||||
const [jwtWarning, setJwtWarning] = useState<{ reason: JwtUnsafeReason; minLength: number } | null>(initialBootstrap.jwtWarning);
|
const [jwtWarning, setJwtWarning] = useState<{ reason: JwtUnsafeReason; minLength: number } | null>(initialBootstrap.jwtWarning);
|
||||||
|
|
||||||
const [loginValues, setLoginValues] = useState({ email: '', password: '' });
|
const [loginValues, setLoginValues] = useState({ email: '', password: '' });
|
||||||
@@ -413,6 +414,7 @@ export default function App() {
|
|||||||
const normalizedCurrentHashPath = currentHashPath.replace(/^\/+/, '').replace(/\/+$/, '');
|
const normalizedCurrentHashPath = currentHashPath.replace(/^\/+/, '').replace(/\/+$/, '');
|
||||||
const isDemoPublicSendRoute = /^send\/[^/]+(?:\/[^/]+)?$/i.test(normalizedCurrentHashPath);
|
const isDemoPublicSendRoute = /^send\/[^/]+(?:\/[^/]+)?$/i.test(normalizedCurrentHashPath);
|
||||||
setDefaultKdfIterations(initialBootstrap.defaultKdfIterations);
|
setDefaultKdfIterations(initialBootstrap.defaultKdfIterations);
|
||||||
|
setRegistrationInviteRequired(initialBootstrap.registrationInviteRequired);
|
||||||
setJwtWarning(null);
|
setJwtWarning(null);
|
||||||
setSession(null);
|
setSession(null);
|
||||||
setProfile(null);
|
setProfile(null);
|
||||||
@@ -427,6 +429,7 @@ export default function App() {
|
|||||||
const boot = await bootstrapAppSession(initialBootstrap);
|
const boot = await bootstrapAppSession(initialBootstrap);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setDefaultKdfIterations(boot.defaultKdfIterations);
|
setDefaultKdfIterations(boot.defaultKdfIterations);
|
||||||
|
setRegistrationInviteRequired(boot.registrationInviteRequired);
|
||||||
setJwtWarning(boot.jwtWarning);
|
setJwtWarning(boot.jwtWarning);
|
||||||
setSession(boot.session);
|
setSession(boot.session);
|
||||||
setProfile(boot.profile);
|
setProfile(boot.profile);
|
||||||
@@ -1408,6 +1411,12 @@ export default function App() {
|
|||||||
if (phase === 'app' && location === '/' && !isPublicSendRoute) navigate('/vault');
|
if (phase === 'app' && location === '/' && !isPublicSendRoute) navigate('/vault');
|
||||||
}, [phase, location, isPublicSendRoute, navigate]);
|
}, [phase, location, isPublicSendRoute, navigate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (phase === 'register' && (location === '/' || location === '/login') && !isPublicSendRoute) {
|
||||||
|
navigate('/register');
|
||||||
|
}
|
||||||
|
}, [phase, location, isPublicSendRoute, navigate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (phase === 'app' && isImportHashRoute && location !== IMPORT_ROUTE) {
|
if (phase === 'app' && isImportHashRoute && location !== IMPORT_ROUTE) {
|
||||||
navigate(IMPORT_ROUTE);
|
navigate(IMPORT_ROUTE);
|
||||||
@@ -1605,6 +1614,7 @@ export default function App() {
|
|||||||
unlockPreparing={unlockPreparing}
|
unlockPreparing={unlockPreparing}
|
||||||
loginValues={loginValues}
|
loginValues={loginValues}
|
||||||
registerValues={registerValues}
|
registerValues={registerValues}
|
||||||
|
registrationInviteRequired={registrationInviteRequired}
|
||||||
unlockPassword={unlockPassword}
|
unlockPassword={unlockPassword}
|
||||||
emailForLock={profile?.email || session?.email || ''}
|
emailForLock={profile?.email || session?.email || ''}
|
||||||
loginHintLoading={loginHintState.loading}
|
loginHintLoading={loginHintState.loading}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ interface AuthViewsProps {
|
|||||||
unlockPreparing: boolean;
|
unlockPreparing: boolean;
|
||||||
loginValues: LoginValues;
|
loginValues: LoginValues;
|
||||||
registerValues: RegisterValues;
|
registerValues: RegisterValues;
|
||||||
|
registrationInviteRequired?: boolean;
|
||||||
unlockPassword: string;
|
unlockPassword: string;
|
||||||
emailForLock: string;
|
emailForLock: string;
|
||||||
loginHintLoading: boolean;
|
loginHintLoading: boolean;
|
||||||
@@ -77,6 +78,7 @@ export default function AuthViews(props: AuthViewsProps) {
|
|||||||
const loginBusy = props.pendingAction === 'login';
|
const loginBusy = props.pendingAction === 'login';
|
||||||
const registerBusy = props.pendingAction === 'register';
|
const registerBusy = props.pendingAction === 'register';
|
||||||
const unlockBusy = props.pendingAction === 'unlock';
|
const unlockBusy = props.pendingAction === 'unlock';
|
||||||
|
const showInviteCodeField = props.registrationInviteRequired !== false || !!props.registerValues.inviteCode.trim();
|
||||||
|
|
||||||
if (props.mode === 'locked') {
|
if (props.mode === 'locked') {
|
||||||
return (
|
return (
|
||||||
@@ -184,17 +186,19 @@ export default function AuthViews(props: AuthViewsProps) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="field">
|
{showInviteCodeField ? (
|
||||||
<span>{t('txt_invite_code_optional')}</span>
|
<label className="field">
|
||||||
<input
|
<span>{t('txt_invite_code_required')}</span>
|
||||||
className="input"
|
<input
|
||||||
value={props.registerValues.inviteCode}
|
className="input"
|
||||||
autoComplete="off"
|
value={props.registerValues.inviteCode}
|
||||||
onInput={(e) =>
|
autoComplete="off"
|
||||||
props.onChangeRegister({ ...props.registerValues, inviteCode: (e.currentTarget as HTMLInputElement).value })
|
onInput={(e) =>
|
||||||
}
|
props.onChangeRegister({ ...props.registerValues, inviteCode: (e.currentTarget as HTMLInputElement).value })
|
||||||
/>
|
}
|
||||||
</label>
|
/>
|
||||||
|
</label>
|
||||||
|
) : null}
|
||||||
<button type="submit" className="btn btn-primary full" disabled={registerBusy}>
|
<button type="submit" className="btn btn-primary full" disabled={registerBusy}>
|
||||||
<UserPlus size={16} className="btn-icon" />
|
<UserPlus size={16} className="btn-icon" />
|
||||||
{registerBusy ? t('txt_registering') : t('txt_create_account')}
|
{registerBusy ? t('txt_registering') : t('txt_create_account')}
|
||||||
|
|||||||
+15
-15
@@ -1,5 +1,5 @@
|
|||||||
import { bytesToBase64, decryptBw, encryptBw, hkdfExpand, pbkdf2 } from '../crypto';
|
import { bytesToBase64, decryptBw, encryptBw, hkdfExpand, pbkdf2 } from '../crypto';
|
||||||
import { t } from '../i18n';
|
import { t, translateServerError } from '../i18n';
|
||||||
import type { AuthorizedDevice } from '../types';
|
import type { AuthorizedDevice } from '../types';
|
||||||
import type {
|
import type {
|
||||||
Profile,
|
Profile,
|
||||||
@@ -297,12 +297,12 @@ export async function refreshAccessToken(session: SessionState): Promise<Refresh
|
|||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
transient: isTransientRefreshStatus(resp.status),
|
transient: isTransientRefreshStatus(resp.status),
|
||||||
error: json?.error_description || json?.error || 'Session refresh failed',
|
error: translateServerError(json?.error_description || json?.error, t('txt_session_refresh_failed')),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const json = await parseJson<TokenSuccess>(resp);
|
const json = await parseJson<TokenSuccess>(resp);
|
||||||
if (!json?.access_token) {
|
if (!json?.access_token) {
|
||||||
return { ok: false, transient: false, error: 'Session refresh failed' };
|
return { ok: false, transient: false, error: t('txt_session_refresh_failed') };
|
||||||
}
|
}
|
||||||
return { ok: true, token: json };
|
return { ok: true, token: json };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -400,11 +400,11 @@ export async function registerAccount(args: {
|
|||||||
|
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const json = await parseJson<TokenError>(resp);
|
const json = await parseJson<TokenError>(resp);
|
||||||
return { ok: false, message: json?.error_description || json?.error || 'Register failed' };
|
return { ok: false, message: translateServerError(json?.error_description || json?.error, t('txt_register_failed')) };
|
||||||
}
|
}
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { ok: false, message: error instanceof Error ? error.message : 'Register failed' };
|
return { ok: false, message: error instanceof Error ? translateServerError(error.message, error.message) : t('txt_register_failed') };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,7 +416,7 @@ export async function getPasswordHint(email: string): Promise<{ masterPasswordHi
|
|||||||
});
|
});
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const body = await parseJson<TokenError>(resp);
|
const body = await parseJson<TokenError>(resp);
|
||||||
throw new Error(body?.error_description || body?.error || 'Failed to load password hint');
|
throw new Error(translateServerError(body?.error_description || body?.error, t('txt_password_hint_load_failed')));
|
||||||
}
|
}
|
||||||
const body = (await parseJson<{ masterPasswordHint?: string | null }>(resp)) || {};
|
const body = (await parseJson<{ masterPasswordHint?: string | null }>(resp)) || {};
|
||||||
return { masterPasswordHint: body.masterPasswordHint ?? null };
|
return { masterPasswordHint: body.masterPasswordHint ?? null };
|
||||||
@@ -469,10 +469,10 @@ export function createAuthedFetch(getSession: () => SessionState | null, setSess
|
|||||||
const refreshed = await refreshAccessTokenOnce(refreshSource);
|
const refreshed = await refreshAccessTokenOnce(refreshSource);
|
||||||
if (!refreshed.ok) {
|
if (!refreshed.ok) {
|
||||||
if (refreshed.transient) {
|
if (refreshed.transient) {
|
||||||
throw new Error(refreshed.error || 'Session refresh temporarily unavailable');
|
throw new Error(refreshed.error || t('txt_session_refresh_failed'));
|
||||||
}
|
}
|
||||||
setSession(null);
|
setSession(null);
|
||||||
throw new Error('Session expired');
|
throw new Error(t('txt_session_refresh_failed'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextSession: SessionState = {
|
const nextSession: SessionState = {
|
||||||
@@ -512,7 +512,7 @@ export async function updateProfile(
|
|||||||
});
|
});
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const body = await parseJson<TokenError>(resp);
|
const body = await parseJson<TokenError>(resp);
|
||||||
throw new Error(body?.error_description || body?.error || 'Save profile failed');
|
throw new Error(translateServerError(body?.error_description || body?.error, t('txt_save_profile_failed')));
|
||||||
}
|
}
|
||||||
const body = await parseJson<Profile>(resp);
|
const body = await parseJson<Profile>(resp);
|
||||||
if (!body) throw new Error('Invalid profile');
|
if (!body) throw new Error('Invalid profile');
|
||||||
@@ -575,7 +575,7 @@ export async function setTotp(
|
|||||||
});
|
});
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const body = await parseJson<TokenError>(resp);
|
const body = await parseJson<TokenError>(resp);
|
||||||
throw new Error(body?.error_description || body?.error || 'TOTP update failed');
|
throw new Error(translateServerError(body?.error_description || body?.error, t('txt_totp_update_failed')));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,7 +590,7 @@ export async function verifyMasterPassword(
|
|||||||
});
|
});
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const body = await parseJson<TokenError>(resp);
|
const body = await parseJson<TokenError>(resp);
|
||||||
throw new Error(body?.error_description || body?.error || 'Master password verify failed');
|
throw new Error(translateServerError(body?.error_description || body?.error, t('txt_master_password_verify_failed')));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -625,7 +625,7 @@ export async function getTotpRecoveryCode(
|
|||||||
});
|
});
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const body = await parseJson<TokenError>(resp);
|
const body = await parseJson<TokenError>(resp);
|
||||||
throw new Error(body?.error_description || body?.error || 'Failed to get recovery code');
|
throw new Error(translateServerError(body?.error_description || body?.error, t('txt_get_recovery_code_failed')));
|
||||||
}
|
}
|
||||||
const body = (await parseJson<{ code?: string }>(resp)) || {};
|
const body = (await parseJson<{ code?: string }>(resp)) || {};
|
||||||
return String(body.code || '');
|
return String(body.code || '');
|
||||||
@@ -647,7 +647,7 @@ export async function recoverTwoFactor(
|
|||||||
});
|
});
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const body = await parseJson<TokenError>(resp);
|
const body = await parseJson<TokenError>(resp);
|
||||||
throw new Error(body?.error_description || body?.error || 'Recover 2FA failed');
|
throw new Error(translateServerError(body?.error_description || body?.error, t('txt_recover_2fa_failed')));
|
||||||
}
|
}
|
||||||
return (await parseJson<{ newRecoveryCode?: string }>(resp)) || {};
|
return (await parseJson<{ newRecoveryCode?: string }>(resp)) || {};
|
||||||
}
|
}
|
||||||
@@ -708,7 +708,7 @@ export async function getApiKey(authedFetch: AuthedFetch, masterPasswordHash: st
|
|||||||
});
|
});
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const body = await parseJson<TokenError>(resp);
|
const body = await parseJson<TokenError>(resp);
|
||||||
throw new Error(body?.error_description || body?.error || 'Failed to get API key');
|
throw new Error(translateServerError(body?.error_description || body?.error, t('txt_get_api_key_failed')));
|
||||||
}
|
}
|
||||||
const body = (await parseJson<{ apiKey?: string }>(resp)) || {};
|
const body = (await parseJson<{ apiKey?: string }>(resp)) || {};
|
||||||
return String(body.apiKey || '');
|
return String(body.apiKey || '');
|
||||||
@@ -722,7 +722,7 @@ export async function rotateApiKey(authedFetch: AuthedFetch, masterPasswordHash:
|
|||||||
});
|
});
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const body = await parseJson<TokenError>(resp);
|
const body = await parseJson<TokenError>(resp);
|
||||||
throw new Error(body?.error_description || body?.error || 'Failed to rotate API key');
|
throw new Error(translateServerError(body?.error_description || body?.error, t('txt_rotate_api_key_failed')));
|
||||||
}
|
}
|
||||||
const body = (await parseJson<{ apiKey?: string }>(resp)) || {};
|
const body = (await parseJson<{ apiKey?: string }>(resp)) || {};
|
||||||
return String(body.apiKey || '');
|
return String(body.apiKey || '');
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { t } from '@/lib/i18n';
|
import { t } from '@/lib/i18n';
|
||||||
import type { DomainRules, TokenError } from '@/lib/types';
|
import type { DomainRules } from '@/lib/types';
|
||||||
import { parseErrorMessage, parseJson, type AuthedFetch } from './shared';
|
import { parseErrorMessage, parseJson, type AuthedFetch } from './shared';
|
||||||
|
|
||||||
function normalizeDomainsResponse(body: Partial<DomainRules> & Record<string, unknown>): DomainRules {
|
function normalizeDomainsResponse(body: Partial<DomainRules> & Record<string, unknown>): DomainRules {
|
||||||
@@ -53,8 +53,7 @@ export async function saveDomainRules(
|
|||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const body = await parseJson<TokenError>(resp);
|
throw new Error(await parseErrorMessage(resp, t('txt_domain_rules_save_failed')));
|
||||||
throw new Error(body?.error_description || body?.error || t('txt_domain_rules_save_failed'));
|
|
||||||
}
|
}
|
||||||
const body = await parseJson<Partial<DomainRules> & Record<string, unknown>>(resp);
|
const body = await parseJson<Partial<DomainRules> & Record<string, unknown>>(resp);
|
||||||
if (!body) throw new Error(t('txt_domain_rules_invalid_response'));
|
if (!body) throw new Error(t('txt_domain_rules_invalid_response'));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { t } from '../i18n';
|
import { t, translateServerError } from '../i18n';
|
||||||
import type { SessionState, TokenError } from '../types';
|
import type { SessionState, TokenError } from '../types';
|
||||||
|
|
||||||
export type AuthedFetch = (input: string, init?: RequestInit) => Promise<Response>;
|
export type AuthedFetch = (input: string, init?: RequestInit) => Promise<Response>;
|
||||||
@@ -46,7 +46,7 @@ export function parseContentDispositionFileName(response: Response, fallback: st
|
|||||||
|
|
||||||
export async function parseErrorMessage(resp: Response, fallback: string): Promise<string> {
|
export async function parseErrorMessage(resp: Response, fallback: string): Promise<string> {
|
||||||
const body = await parseJson<TokenError>(resp);
|
const body = await parseJson<TokenError>(resp);
|
||||||
return body?.error_description || body?.error || fallback;
|
return translateServerError(body?.error_description || body?.error, fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createApiError(message: string, status?: number): Error & { status?: number } {
|
export function createApiError(message: string, status?: number): Error & { status?: number } {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
unlockVaultKey,
|
unlockVaultKey,
|
||||||
} from '@/lib/api/auth';
|
} from '@/lib/api/auth';
|
||||||
import { readInviteCodeFromUrl } from '@/lib/app-support';
|
import { readInviteCodeFromUrl } from '@/lib/app-support';
|
||||||
|
import { t, translateServerError } from '@/lib/i18n';
|
||||||
import type { AppPhase, Profile, SessionState, TokenSuccess, WebBootstrapResponse } from '@/lib/types';
|
import type { AppPhase, Profile, SessionState, TokenSuccess, WebBootstrapResponse } from '@/lib/types';
|
||||||
|
|
||||||
export interface PendingTotp {
|
export interface PendingTotp {
|
||||||
@@ -23,6 +24,7 @@ export type JwtUnsafeReason = 'missing' | 'default' | 'too_short';
|
|||||||
|
|
||||||
export interface BootstrapAppResult {
|
export interface BootstrapAppResult {
|
||||||
defaultKdfIterations: number;
|
defaultKdfIterations: number;
|
||||||
|
registrationInviteRequired?: boolean;
|
||||||
jwtWarning: { reason: JwtUnsafeReason; minLength: number } | null;
|
jwtWarning: { reason: JwtUnsafeReason; minLength: number } | null;
|
||||||
session: SessionState | null;
|
session: SessionState | null;
|
||||||
profile: Profile | null;
|
profile: Profile | null;
|
||||||
@@ -32,6 +34,7 @@ export interface BootstrapAppResult {
|
|||||||
|
|
||||||
export interface InitialAppBootstrapState {
|
export interface InitialAppBootstrapState {
|
||||||
defaultKdfIterations: number;
|
defaultKdfIterations: number;
|
||||||
|
registrationInviteRequired?: boolean;
|
||||||
jwtWarning: { reason: JwtUnsafeReason; minLength: number } | null;
|
jwtWarning: { reason: JwtUnsafeReason; minLength: number } | null;
|
||||||
session: SessionState | null;
|
session: SessionState | null;
|
||||||
phase: AppPhase;
|
phase: AppPhase;
|
||||||
@@ -96,8 +99,10 @@ function readWindowBootstrap(): WebBootstrapResponse {
|
|||||||
return raw && typeof raw === 'object' ? raw : {};
|
return raw && typeof raw === 'object' ? raw : {};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeBootstrapResponse(boot: WebBootstrapResponse): Pick<InitialAppBootstrapState, 'defaultKdfIterations' | 'jwtWarning'> {
|
function normalizeBootstrapResponse(boot: WebBootstrapResponse): Pick<InitialAppBootstrapState, 'defaultKdfIterations' | 'registrationInviteRequired' | 'jwtWarning'> {
|
||||||
const defaultKdfIterations = Number(boot.defaultKdfIterations || 600000);
|
const defaultKdfIterations = Number(boot.defaultKdfIterations || 600000);
|
||||||
|
const registrationInviteRequired =
|
||||||
|
typeof boot.registrationInviteRequired === 'boolean' ? boot.registrationInviteRequired : undefined;
|
||||||
const jwtUnsafeReason = boot.jwtUnsafeReason || null;
|
const jwtUnsafeReason = boot.jwtUnsafeReason || null;
|
||||||
const jwtWarning = jwtUnsafeReason
|
const jwtWarning = jwtUnsafeReason
|
||||||
? {
|
? {
|
||||||
@@ -108,6 +113,7 @@ function normalizeBootstrapResponse(boot: WebBootstrapResponse): Pick<InitialApp
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
defaultKdfIterations,
|
defaultKdfIterations,
|
||||||
|
registrationInviteRequired,
|
||||||
jwtWarning,
|
jwtWarning,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -163,16 +169,22 @@ function buildTransientProfile(token: TokenSuccess, email: string, fallbackProfi
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveUnauthenticatedPhase(registrationInviteRequired: boolean | undefined, fallback: AppPhase): AppPhase {
|
||||||
|
return registrationInviteRequired === false ? 'register' : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
export function readInitialAppBootstrapState(): InitialAppBootstrapState {
|
export function readInitialAppBootstrapState(): InitialAppBootstrapState {
|
||||||
const { defaultKdfIterations, jwtWarning } = normalizeBootstrapResponse(readWindowBootstrap());
|
const { defaultKdfIterations, registrationInviteRequired, jwtWarning } = normalizeBootstrapResponse(readWindowBootstrap());
|
||||||
const session = loadSession();
|
const session = loadSession();
|
||||||
const hasInviteCode = !!readInviteCodeFromUrl();
|
const hasInviteCode = !!readInviteCodeFromUrl();
|
||||||
|
const unauthenticatedPhase = hasInviteCode ? 'register' : 'login';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
defaultKdfIterations,
|
defaultKdfIterations,
|
||||||
|
registrationInviteRequired,
|
||||||
jwtWarning,
|
jwtWarning,
|
||||||
session,
|
session,
|
||||||
phase: jwtWarning ? 'login' : session ? 'locked' : hasInviteCode ? 'register' : 'login',
|
phase: jwtWarning ? 'login' : session ? 'locked' : resolveUnauthenticatedPhase(registrationInviteRequired, unauthenticatedPhase),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,11 +192,13 @@ export async function bootstrapAppSession(initial: InitialAppBootstrapState = re
|
|||||||
const remoteBoot = await fetchBootstrapConfig();
|
const remoteBoot = await fetchBootstrapConfig();
|
||||||
const normalizedBoot = normalizeBootstrapResponse(remoteBoot);
|
const normalizedBoot = normalizeBootstrapResponse(remoteBoot);
|
||||||
const defaultKdfIterations = normalizedBoot.defaultKdfIterations || initial.defaultKdfIterations;
|
const defaultKdfIterations = normalizedBoot.defaultKdfIterations || initial.defaultKdfIterations;
|
||||||
|
const registrationInviteRequired = normalizedBoot.registrationInviteRequired ?? initial.registrationInviteRequired;
|
||||||
const jwtWarning = normalizedBoot.jwtWarning ?? initial.jwtWarning;
|
const jwtWarning = normalizedBoot.jwtWarning ?? initial.jwtWarning;
|
||||||
|
|
||||||
if (jwtWarning) {
|
if (jwtWarning) {
|
||||||
return {
|
return {
|
||||||
defaultKdfIterations,
|
defaultKdfIterations,
|
||||||
|
registrationInviteRequired,
|
||||||
jwtWarning,
|
jwtWarning,
|
||||||
session: null,
|
session: null,
|
||||||
profile: null,
|
profile: null,
|
||||||
@@ -196,10 +210,11 @@ export async function bootstrapAppSession(initial: InitialAppBootstrapState = re
|
|||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
return {
|
return {
|
||||||
defaultKdfIterations,
|
defaultKdfIterations,
|
||||||
|
registrationInviteRequired,
|
||||||
jwtWarning: null,
|
jwtWarning: null,
|
||||||
session: null,
|
session: null,
|
||||||
profile: null,
|
profile: null,
|
||||||
phase: initial.phase,
|
phase: resolveUnauthenticatedPhase(registrationInviteRequired, initial.phase),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,6 +222,7 @@ export async function bootstrapAppSession(initial: InitialAppBootstrapState = re
|
|||||||
if (cachedProfile) {
|
if (cachedProfile) {
|
||||||
return {
|
return {
|
||||||
defaultKdfIterations,
|
defaultKdfIterations,
|
||||||
|
registrationInviteRequired,
|
||||||
jwtWarning: null,
|
jwtWarning: null,
|
||||||
session: loaded,
|
session: loaded,
|
||||||
profile: cachedProfile,
|
profile: cachedProfile,
|
||||||
@@ -217,6 +233,7 @@ export async function bootstrapAppSession(initial: InitialAppBootstrapState = re
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
defaultKdfIterations,
|
defaultKdfIterations,
|
||||||
|
registrationInviteRequired,
|
||||||
jwtWarning: null,
|
jwtWarning: null,
|
||||||
session: loaded,
|
session: loaded,
|
||||||
profile: null,
|
profile: null,
|
||||||
@@ -311,7 +328,7 @@ export async function performPasswordLogin(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
kind: 'error',
|
kind: 'error',
|
||||||
message: tokenError.error_description || tokenError.error || 'Login failed',
|
message: translateServerError(tokenError.error_description || tokenError.error, t('txt_login_failed')),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,7 +345,7 @@ export async function performTotpLogin(
|
|||||||
return completeLogin(token, pendingTotp.email, pendingTotp.masterKey);
|
return completeLogin(token, pendingTotp.email, pendingTotp.masterKey);
|
||||||
}
|
}
|
||||||
const tokenError = token as { error_description?: string; error?: string };
|
const tokenError = token as { error_description?: string; error?: string };
|
||||||
throw new Error(tokenError.error_description || tokenError.error || 'TOTP verify failed');
|
throw new Error(translateServerError(tokenError.error_description || tokenError.error, t('txt_totp_verify_failed')));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function performRecoverTwoFactorLogin(
|
export async function performRecoverTwoFactorLogin(
|
||||||
@@ -404,7 +421,7 @@ export async function performUnlock(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
kind: 'error',
|
kind: 'error',
|
||||||
message: tokenError.error_description || tokenError.error || 'Unlock failed',
|
message: translateServerError(tokenError.error_description || tokenError.error, t('txt_unlock_failed')),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export function createDemoBackupSettings(): AdminBackupSettings {
|
|||||||
export function createDemoInitialBootstrapState(): InitialAppBootstrapState {
|
export function createDemoInitialBootstrapState(): InitialAppBootstrapState {
|
||||||
return {
|
return {
|
||||||
defaultKdfIterations: 600000,
|
defaultKdfIterations: 600000,
|
||||||
|
registrationInviteRequired: true,
|
||||||
jwtWarning: null,
|
jwtWarning: null,
|
||||||
session: null,
|
session: null,
|
||||||
phase: 'login',
|
phase: 'login',
|
||||||
|
|||||||
@@ -789,6 +789,7 @@ async function runDemoRemoteRestoreProgress(fileName: string): Promise<void> {
|
|||||||
export function createDemoInitialBootstrapState(): InitialAppBootstrapState {
|
export function createDemoInitialBootstrapState(): InitialAppBootstrapState {
|
||||||
return {
|
return {
|
||||||
defaultKdfIterations: 600000,
|
defaultKdfIterations: 600000,
|
||||||
|
registrationInviteRequired: true,
|
||||||
jwtWarning: null,
|
jwtWarning: null,
|
||||||
session: null,
|
session: null,
|
||||||
phase: 'login',
|
phase: 'login',
|
||||||
|
|||||||
@@ -93,6 +93,41 @@ export function t(key: string, params?: I18nParams): string {
|
|||||||
return template.replace(/\{(\w+)\}/g, (_, name: string) => String(params[name] ?? ''));
|
return template.replace(/\{(\w+)\}/g, (_, name: string) => String(params[name] ?? ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function translateServerError(message: string | null | undefined, fallback: string): string {
|
||||||
|
const normalized = String(message || '').trim();
|
||||||
|
if (!normalized) return fallback;
|
||||||
|
|
||||||
|
const rateLimitMatch = normalized.match(/^Rate limit exceeded\. Try again in (\d+) seconds\.$/i);
|
||||||
|
if (rateLimitMatch) {
|
||||||
|
return t('txt_rate_limit_try_again_seconds', { seconds: rateLimitMatch[1] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = {
|
||||||
|
'Account is disabled': 'txt_server_error_account_disabled',
|
||||||
|
'Client IP is required': 'txt_server_error_client_ip_required',
|
||||||
|
'ClientId or clientSecret is incorrect. Try again': 'txt_server_error_client_credentials_incorrect',
|
||||||
|
'Email already registered': 'txt_server_error_email_already_registered',
|
||||||
|
'Email and password are required': 'txt_server_error_email_password_required',
|
||||||
|
'Email is required': 'txt_server_error_email_required',
|
||||||
|
'Invite code is invalid or expired': 'txt_server_error_invite_invalid_or_expired',
|
||||||
|
'Invite code is required': 'txt_server_error_invite_required',
|
||||||
|
'Invalid refresh token': 'txt_server_error_invalid_refresh_token',
|
||||||
|
'Invalid request payload': 'txt_server_error_invalid_request_payload',
|
||||||
|
'JWT_SECRET is not set': 'txt_server_error_jwt_secret_missing',
|
||||||
|
'JWT_SECRET is using the default/sample value. Please change it.': 'txt_server_error_jwt_secret_default',
|
||||||
|
'JWT_SECRET must be at least 32 characters': 'txt_server_error_jwt_secret_too_short',
|
||||||
|
'Parameter error': 'txt_server_error_parameter_error',
|
||||||
|
'Refresh token is required': 'txt_server_error_refresh_token_required',
|
||||||
|
'Registration is temporarily unavailable, retry once': 'txt_server_error_registration_retry',
|
||||||
|
'TOTP token is required': 'txt_server_error_totp_token_required',
|
||||||
|
'Two factor required.': 'txt_server_error_two_factor_required',
|
||||||
|
'Two-step token is invalid. Try again.': 'txt_server_error_two_factor_invalid',
|
||||||
|
'Username or password is incorrect. Try again': 'txt_server_error_username_password_incorrect',
|
||||||
|
}[normalized];
|
||||||
|
|
||||||
|
return key ? t(key) : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
export function getLocale(): Locale {
|
export function getLocale(): Locale {
|
||||||
return locale;
|
return locale;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -349,6 +349,7 @@ const en: Record<string, string> = {
|
|||||||
"txt_create": "Create",
|
"txt_create": "Create",
|
||||||
"txt_create_account": "Create Account",
|
"txt_create_account": "Create Account",
|
||||||
"txt_registering": "Creating account...",
|
"txt_registering": "Creating account...",
|
||||||
|
"txt_register_failed": "Register failed",
|
||||||
"txt_create_folder": "Create Folder",
|
"txt_create_folder": "Create Folder",
|
||||||
"txt_create_folder_failed": "Create folder failed",
|
"txt_create_folder_failed": "Create folder failed",
|
||||||
"txt_create_item_failed": "Create item failed",
|
"txt_create_item_failed": "Create item failed",
|
||||||
@@ -408,6 +409,7 @@ const en: Record<string, string> = {
|
|||||||
"txt_disable_this_send": "Disable this send",
|
"txt_disable_this_send": "Disable this send",
|
||||||
"txt_disable_totp": "Disable TOTP",
|
"txt_disable_totp": "Disable TOTP",
|
||||||
"txt_disable_totp_failed": "Disable TOTP failed",
|
"txt_disable_totp_failed": "Disable TOTP failed",
|
||||||
|
"txt_totp_update_failed": "Update TOTP failed",
|
||||||
"txt_download": "Download",
|
"txt_download": "Download",
|
||||||
"txt_downloading": "Downloading...",
|
"txt_downloading": "Downloading...",
|
||||||
"txt_downloading_percent": "Downloading {percent}%",
|
"txt_downloading_percent": "Downloading {percent}%",
|
||||||
@@ -467,12 +469,33 @@ const en: Record<string, string> = {
|
|||||||
"txt_identity_details": "Identity Details",
|
"txt_identity_details": "Identity Details",
|
||||||
"txt_ie_browser": "IE Browser",
|
"txt_ie_browser": "IE Browser",
|
||||||
"txt_create_invite_failed": "Failed to create invite",
|
"txt_create_invite_failed": "Failed to create invite",
|
||||||
"txt_invite_code_optional": "Invite Code (Not required for the first account; required for all others)",
|
"txt_invite_code_required": "Invite Code (Required)",
|
||||||
"txt_invite_created": "Invite created",
|
"txt_invite_created": "Invite created",
|
||||||
"txt_invite_revoked": "Invite revoked",
|
"txt_invite_revoked": "Invite revoked",
|
||||||
"txt_revoke_invite_failed": "Failed to revoke invite",
|
"txt_revoke_invite_failed": "Failed to revoke invite",
|
||||||
"txt_invite_validity_hours": "Invite validity (hours)",
|
"txt_invite_validity_hours": "Invite validity (hours)",
|
||||||
"txt_invites": "Invites",
|
"txt_invites": "Invites",
|
||||||
|
"txt_rate_limit_try_again_seconds": "Too many requests. Try again in {seconds} seconds.",
|
||||||
|
"txt_server_error_account_disabled": "Account is disabled",
|
||||||
|
"txt_server_error_client_credentials_incorrect": "Client ID or client secret is incorrect. Try again.",
|
||||||
|
"txt_server_error_client_ip_required": "Client IP is required",
|
||||||
|
"txt_server_error_email_already_registered": "Email already registered",
|
||||||
|
"txt_server_error_email_password_required": "Email and password are required",
|
||||||
|
"txt_server_error_email_required": "Email is required",
|
||||||
|
"txt_server_error_invalid_refresh_token": "Session expired. Please sign in again.",
|
||||||
|
"txt_server_error_invalid_request_payload": "Invalid request payload",
|
||||||
|
"txt_server_error_invite_invalid_or_expired": "Invite code is invalid or expired",
|
||||||
|
"txt_server_error_invite_required": "Invite code is required",
|
||||||
|
"txt_server_error_jwt_secret_default": "JWT_SECRET is using the default/sample value. Please change it.",
|
||||||
|
"txt_server_error_jwt_secret_missing": "JWT_SECRET is not set",
|
||||||
|
"txt_server_error_jwt_secret_too_short": "JWT_SECRET must be at least 32 characters",
|
||||||
|
"txt_server_error_parameter_error": "Parameter error",
|
||||||
|
"txt_server_error_refresh_token_required": "Session is missing. Please sign in again.",
|
||||||
|
"txt_server_error_registration_retry": "Registration is temporarily unavailable. Please retry once.",
|
||||||
|
"txt_server_error_totp_token_required": "Two-step token is required",
|
||||||
|
"txt_server_error_two_factor_invalid": "Two-step token is invalid. Try again.",
|
||||||
|
"txt_server_error_two_factor_required": "Two factor required.",
|
||||||
|
"txt_server_error_username_password_incorrect": "Username or password is incorrect. Try again.",
|
||||||
"txt_ios": "iOS",
|
"txt_ios": "iOS",
|
||||||
"txt_item": "Item",
|
"txt_item": "Item",
|
||||||
"txt_item_created": "Item created",
|
"txt_item_created": "Item created",
|
||||||
@@ -546,6 +569,7 @@ const en: Record<string, string> = {
|
|||||||
"txt_master_password_is_required": "Master password is required",
|
"txt_master_password_is_required": "Master password is required",
|
||||||
"txt_master_password_is_required_2": "Master password is required.",
|
"txt_master_password_is_required_2": "Master password is required.",
|
||||||
"txt_master_password_must_be_at_least_12_chars": "Master password must be at least 12 chars",
|
"txt_master_password_must_be_at_least_12_chars": "Master password must be at least 12 chars",
|
||||||
|
"txt_master_password_verify_failed": "Master password verify failed",
|
||||||
"txt_master_password_reprompt": "Master password reprompt",
|
"txt_master_password_reprompt": "Master password reprompt",
|
||||||
"txt_master_password_reprompt_2": "Master Password Reprompt",
|
"txt_master_password_reprompt_2": "Master Password Reprompt",
|
||||||
"txt_max_access_count": "Max Access Count",
|
"txt_max_access_count": "Max Access Count",
|
||||||
@@ -632,6 +656,9 @@ const en: Record<string, string> = {
|
|||||||
"txt_api_key_rotated": "API key rotated",
|
"txt_api_key_rotated": "API key rotated",
|
||||||
"txt_rotate_api_key_confirm": "Rotate API key? The current key will stop working immediately.",
|
"txt_rotate_api_key_confirm": "Rotate API key? The current key will stop working immediately.",
|
||||||
"txt_api_key_is_empty": "API key is empty",
|
"txt_api_key_is_empty": "API key is empty",
|
||||||
|
"txt_get_api_key_failed": "Failed to get API key",
|
||||||
|
"txt_get_recovery_code_failed": "Failed to get recovery code",
|
||||||
|
"txt_rotate_api_key_failed": "Failed to rotate API key",
|
||||||
"txt_api_key_dialog_intro": "Your API key can be used to authenticate with the Bitwarden CLI.",
|
"txt_api_key_dialog_intro": "Your API key can be used to authenticate with the Bitwarden CLI.",
|
||||||
"txt_api_key_warning_body": "Your API key is an alternative authentication mechanism. Keep it secret.",
|
"txt_api_key_warning_body": "Your API key is an alternative authentication mechanism. Keep it secret.",
|
||||||
"txt_oauth_client_credentials": "OAuth 2.0 Client Credentials",
|
"txt_oauth_client_credentials": "OAuth 2.0 Client Credentials",
|
||||||
@@ -669,6 +696,7 @@ const en: Record<string, string> = {
|
|||||||
"txt_save_profile": "Save Profile",
|
"txt_save_profile": "Save Profile",
|
||||||
"txt_save_profile_failed": "Save profile failed",
|
"txt_save_profile_failed": "Save profile failed",
|
||||||
"txt_search_sends": "Search sends...",
|
"txt_search_sends": "Search sends...",
|
||||||
|
"txt_session_refresh_failed": "Session refresh failed. Please sign in again.",
|
||||||
"txt_search_your_secure_vault": "Search your secure vault...",
|
"txt_search_your_secure_vault": "Search your secure vault...",
|
||||||
"txt_clear_search": "Clear search",
|
"txt_clear_search": "Clear search",
|
||||||
"txt_clear_search_esc": "Clear search (Esc)",
|
"txt_clear_search_esc": "Clear search (Esc)",
|
||||||
|
|||||||
@@ -349,6 +349,7 @@ const es: Record<string, string> = {
|
|||||||
"txt_create": "Crear",
|
"txt_create": "Crear",
|
||||||
"txt_create_account": "Crear cuenta",
|
"txt_create_account": "Crear cuenta",
|
||||||
"txt_registering": "Creando cuenta...",
|
"txt_registering": "Creando cuenta...",
|
||||||
|
"txt_register_failed": "Error al registrarse",
|
||||||
"txt_create_folder": "Crear carpeta",
|
"txt_create_folder": "Crear carpeta",
|
||||||
"txt_create_folder_failed": "Error al crear carpeta",
|
"txt_create_folder_failed": "Error al crear carpeta",
|
||||||
"txt_create_item_failed": "Error al crear elemento",
|
"txt_create_item_failed": "Error al crear elemento",
|
||||||
@@ -408,6 +409,7 @@ const es: Record<string, string> = {
|
|||||||
"txt_disable_this_send": "Desactivar este envío",
|
"txt_disable_this_send": "Desactivar este envío",
|
||||||
"txt_disable_totp": "Desactivar TOTP",
|
"txt_disable_totp": "Desactivar TOTP",
|
||||||
"txt_disable_totp_failed": "Error al desactivar TOTP",
|
"txt_disable_totp_failed": "Error al desactivar TOTP",
|
||||||
|
"txt_totp_update_failed": "Error al actualizar TOTP",
|
||||||
"txt_download": "Descargar",
|
"txt_download": "Descargar",
|
||||||
"txt_downloading": "Descargando...",
|
"txt_downloading": "Descargando...",
|
||||||
"txt_downloading_percent": "Descargando {percent}%",
|
"txt_downloading_percent": "Descargando {percent}%",
|
||||||
@@ -467,12 +469,33 @@ const es: Record<string, string> = {
|
|||||||
"txt_identity_details": "Detalles de identidad",
|
"txt_identity_details": "Detalles de identidad",
|
||||||
"txt_ie_browser": "Navegador Internet Explorer",
|
"txt_ie_browser": "Navegador Internet Explorer",
|
||||||
"txt_create_invite_failed": "Error al crear invitación",
|
"txt_create_invite_failed": "Error al crear invitación",
|
||||||
"txt_invite_code_optional": "Código de invitación (No obligatorio para la primera cuenta; obligatorio para todas las demás)",
|
"txt_invite_code_required": "Código de invitación (obligatorio)",
|
||||||
"txt_invite_created": "Invitación creada",
|
"txt_invite_created": "Invitación creada",
|
||||||
"txt_invite_revoked": "Invitación revocada",
|
"txt_invite_revoked": "Invitación revocada",
|
||||||
"txt_revoke_invite_failed": "Error al revocar invitación",
|
"txt_revoke_invite_failed": "Error al revocar invitación",
|
||||||
"txt_invite_validity_hours": "Validez de la invitación en horas",
|
"txt_invite_validity_hours": "Validez de la invitación en horas",
|
||||||
"txt_invites": "Invitaciones",
|
"txt_invites": "Invitaciones",
|
||||||
|
"txt_rate_limit_try_again_seconds": "Demasiadas solicitudes. Inténtalo de nuevo en {seconds} segundos.",
|
||||||
|
"txt_server_error_account_disabled": "La cuenta está deshabilitada",
|
||||||
|
"txt_server_error_client_credentials_incorrect": "El ID de cliente o el secreto de cliente no son correctos. Inténtalo de nuevo.",
|
||||||
|
"txt_server_error_client_ip_required": "Se requiere la IP del cliente",
|
||||||
|
"txt_server_error_email_already_registered": "Este correo ya está registrado",
|
||||||
|
"txt_server_error_email_password_required": "Correo y contraseña son obligatorios",
|
||||||
|
"txt_server_error_email_required": "El correo es obligatorio",
|
||||||
|
"txt_server_error_invalid_refresh_token": "La sesión caducó. Inicia sesión de nuevo.",
|
||||||
|
"txt_server_error_invalid_request_payload": "Solicitud no válida",
|
||||||
|
"txt_server_error_invite_invalid_or_expired": "El código de invitación no es válido o ha caducado",
|
||||||
|
"txt_server_error_invite_required": "El código de invitación es obligatorio",
|
||||||
|
"txt_server_error_jwt_secret_default": "JWT_SECRET usa el valor predeterminado/de ejemplo. Cámbialo.",
|
||||||
|
"txt_server_error_jwt_secret_missing": "JWT_SECRET no está configurado",
|
||||||
|
"txt_server_error_jwt_secret_too_short": "JWT_SECRET debe tener al menos 32 caracteres",
|
||||||
|
"txt_server_error_parameter_error": "Error de parámetros",
|
||||||
|
"txt_server_error_refresh_token_required": "Falta la sesión. Inicia sesión de nuevo.",
|
||||||
|
"txt_server_error_registration_retry": "El registro no está disponible temporalmente. Inténtalo una vez más.",
|
||||||
|
"txt_server_error_totp_token_required": "El código de verificación en dos pasos es obligatorio",
|
||||||
|
"txt_server_error_two_factor_invalid": "El código de verificación en dos pasos no es válido. Inténtalo de nuevo.",
|
||||||
|
"txt_server_error_two_factor_required": "Se requiere verificación en dos pasos.",
|
||||||
|
"txt_server_error_username_password_incorrect": "Usuario o contraseña incorrectos. Inténtalo de nuevo.",
|
||||||
"txt_ios": "iOS",
|
"txt_ios": "iOS",
|
||||||
"txt_item": "Elemento",
|
"txt_item": "Elemento",
|
||||||
"txt_item_created": "Elemento creado",
|
"txt_item_created": "Elemento creado",
|
||||||
@@ -546,6 +569,7 @@ const es: Record<string, string> = {
|
|||||||
"txt_master_password_is_required": "La contraseña maestra es obligatoria",
|
"txt_master_password_is_required": "La contraseña maestra es obligatoria",
|
||||||
"txt_master_password_is_required_2": "La contraseña maestra es obligatoria.",
|
"txt_master_password_is_required_2": "La contraseña maestra es obligatoria.",
|
||||||
"txt_master_password_must_be_at_least_12_chars": "La contraseña maestra debe tener al menos 12 caracteres",
|
"txt_master_password_must_be_at_least_12_chars": "La contraseña maestra debe tener al menos 12 caracteres",
|
||||||
|
"txt_master_password_verify_failed": "Error al verificar la contraseña maestra",
|
||||||
"txt_master_password_reprompt": "Solicitar contraseña maestra de nuevo",
|
"txt_master_password_reprompt": "Solicitar contraseña maestra de nuevo",
|
||||||
"txt_master_password_reprompt_2": "Solicitar contraseña maestra de nuevo",
|
"txt_master_password_reprompt_2": "Solicitar contraseña maestra de nuevo",
|
||||||
"txt_max_access_count": "Número máximo de accesos",
|
"txt_max_access_count": "Número máximo de accesos",
|
||||||
@@ -632,6 +656,9 @@ const es: Record<string, string> = {
|
|||||||
"txt_api_key_rotated": "Clave API rotada",
|
"txt_api_key_rotated": "Clave API rotada",
|
||||||
"txt_rotate_api_key_confirm": "¿Rotar clave API? La clave actual dejará de funcionar inmediatamente.",
|
"txt_rotate_api_key_confirm": "¿Rotar clave API? La clave actual dejará de funcionar inmediatamente.",
|
||||||
"txt_api_key_is_empty": "La clave API está vacía",
|
"txt_api_key_is_empty": "La clave API está vacía",
|
||||||
|
"txt_get_api_key_failed": "Error al obtener la clave API",
|
||||||
|
"txt_get_recovery_code_failed": "Error al obtener el código de recuperación",
|
||||||
|
"txt_rotate_api_key_failed": "Error al rotar la clave API",
|
||||||
"txt_api_key_dialog_intro": "Su clave API puede usarse para autenticarse con la CLI de Bitwarden.",
|
"txt_api_key_dialog_intro": "Su clave API puede usarse para autenticarse con la CLI de Bitwarden.",
|
||||||
"txt_api_key_warning_body": "Su clave API es un mecanismo de autenticación alternativo. Manténgala secreta.",
|
"txt_api_key_warning_body": "Su clave API es un mecanismo de autenticación alternativo. Manténgala secreta.",
|
||||||
"txt_oauth_client_credentials": "Credenciales de cliente OAuth 2.0",
|
"txt_oauth_client_credentials": "Credenciales de cliente OAuth 2.0",
|
||||||
@@ -669,6 +696,7 @@ const es: Record<string, string> = {
|
|||||||
"txt_save_profile": "Guardar perfil",
|
"txt_save_profile": "Guardar perfil",
|
||||||
"txt_save_profile_failed": "Error al guardar perfil",
|
"txt_save_profile_failed": "Error al guardar perfil",
|
||||||
"txt_search_sends": "Buscar envíos...",
|
"txt_search_sends": "Buscar envíos...",
|
||||||
|
"txt_session_refresh_failed": "Error al actualizar la sesión. Inicia sesión de nuevo.",
|
||||||
"txt_search_your_secure_vault": "Buscar en su bóveda segura...",
|
"txt_search_your_secure_vault": "Buscar en su bóveda segura...",
|
||||||
"txt_clear_search": "Limpiar búsqueda",
|
"txt_clear_search": "Limpiar búsqueda",
|
||||||
"txt_clear_search_esc": "Limpiar búsqueda (Esc)",
|
"txt_clear_search_esc": "Limpiar búsqueda (Esc)",
|
||||||
|
|||||||
@@ -349,6 +349,7 @@ const ru: Record<string, string> = {
|
|||||||
"txt_create": "Создать",
|
"txt_create": "Создать",
|
||||||
"txt_create_account": "Создать учетную запись",
|
"txt_create_account": "Создать учетную запись",
|
||||||
"txt_registering": "Создание учетной записи...",
|
"txt_registering": "Создание учетной записи...",
|
||||||
|
"txt_register_failed": "Не удалось зарегистрироваться",
|
||||||
"txt_create_folder": "Создать папку",
|
"txt_create_folder": "Создать папку",
|
||||||
"txt_create_folder_failed": "Создать папку не удалось",
|
"txt_create_folder_failed": "Создать папку не удалось",
|
||||||
"txt_create_item_failed": "Создать элемент не удалось",
|
"txt_create_item_failed": "Создать элемент не удалось",
|
||||||
@@ -408,6 +409,7 @@ const ru: Record<string, string> = {
|
|||||||
"txt_disable_this_send": "Отключить эту отправку",
|
"txt_disable_this_send": "Отключить эту отправку",
|
||||||
"txt_disable_totp": "Отключить TOTP",
|
"txt_disable_totp": "Отключить TOTP",
|
||||||
"txt_disable_totp_failed": "Отключить TOTP не удалось",
|
"txt_disable_totp_failed": "Отключить TOTP не удалось",
|
||||||
|
"txt_totp_update_failed": "Не удалось обновить TOTP",
|
||||||
"txt_download": "Скачать",
|
"txt_download": "Скачать",
|
||||||
"txt_downloading": "Загрузка...",
|
"txt_downloading": "Загрузка...",
|
||||||
"txt_downloading_percent": "Загрузка {percent}%",
|
"txt_downloading_percent": "Загрузка {percent}%",
|
||||||
@@ -467,12 +469,33 @@ const ru: Record<string, string> = {
|
|||||||
"txt_identity_details": "Данные личности",
|
"txt_identity_details": "Данные личности",
|
||||||
"txt_ie_browser": "IE-браузер",
|
"txt_ie_browser": "IE-браузер",
|
||||||
"txt_create_invite_failed": "Не удалось создать приглашение",
|
"txt_create_invite_failed": "Не удалось создать приглашение",
|
||||||
"txt_invite_code_optional": "Пригласительный код (не требуется для первой учетной записи; требуется для всех остальных)",
|
"txt_invite_code_required": "Пригласительный код (обязательно)",
|
||||||
"txt_invite_created": "Приглашение создано",
|
"txt_invite_created": "Приглашение создано",
|
||||||
"txt_invite_revoked": "Приглашение отозвано",
|
"txt_invite_revoked": "Приглашение отозвано",
|
||||||
"txt_revoke_invite_failed": "Не удалось отозвать приглашение",
|
"txt_revoke_invite_failed": "Не удалось отозвать приглашение",
|
||||||
"txt_invite_validity_hours": "Срок действия приглашения (часы)",
|
"txt_invite_validity_hours": "Срок действия приглашения (часы)",
|
||||||
"txt_invites": "Приглашает",
|
"txt_invites": "Приглашает",
|
||||||
|
"txt_rate_limit_try_again_seconds": "Слишком много запросов. Повторите попытку через {seconds} секунд.",
|
||||||
|
"txt_server_error_account_disabled": "Учетная запись отключена",
|
||||||
|
"txt_server_error_client_credentials_incorrect": "ID клиента или секрет клиента неверны. Повторите попытку.",
|
||||||
|
"txt_server_error_client_ip_required": "Требуется IP клиента",
|
||||||
|
"txt_server_error_email_already_registered": "Этот адрес электронной почты уже зарегистрирован",
|
||||||
|
"txt_server_error_email_password_required": "Требуются адрес электронной почты и пароль",
|
||||||
|
"txt_server_error_email_required": "Требуется адрес электронной почты",
|
||||||
|
"txt_server_error_invalid_refresh_token": "Сеанс истек. Войдите снова.",
|
||||||
|
"txt_server_error_invalid_request_payload": "Недопустимый запрос",
|
||||||
|
"txt_server_error_invite_invalid_or_expired": "Код приглашения недействителен или истек",
|
||||||
|
"txt_server_error_invite_required": "Требуется код приглашения",
|
||||||
|
"txt_server_error_jwt_secret_default": "JWT_SECRET использует значение по умолчанию/пример. Измените его.",
|
||||||
|
"txt_server_error_jwt_secret_missing": "JWT_SECRET не настроен",
|
||||||
|
"txt_server_error_jwt_secret_too_short": "JWT_SECRET должен содержать не менее 32 символов",
|
||||||
|
"txt_server_error_parameter_error": "Ошибка параметров",
|
||||||
|
"txt_server_error_refresh_token_required": "Сеанс отсутствует. Войдите снова.",
|
||||||
|
"txt_server_error_registration_retry": "Регистрация временно недоступна. Повторите попытку один раз.",
|
||||||
|
"txt_server_error_totp_token_required": "Требуется код двухэтапной проверки",
|
||||||
|
"txt_server_error_two_factor_invalid": "Код двухэтапной проверки недействителен. Повторите попытку.",
|
||||||
|
"txt_server_error_two_factor_required": "Требуется двухэтапная проверка.",
|
||||||
|
"txt_server_error_username_password_incorrect": "Имя пользователя или пароль неверны. Повторите попытку.",
|
||||||
"txt_ios": "iOS",
|
"txt_ios": "iOS",
|
||||||
"txt_item": "Товар",
|
"txt_item": "Товар",
|
||||||
"txt_item_created": "Объект создан",
|
"txt_item_created": "Объект создан",
|
||||||
@@ -546,6 +569,7 @@ const ru: Record<string, string> = {
|
|||||||
"txt_master_password_is_required": "Требуется мастер-пароль",
|
"txt_master_password_is_required": "Требуется мастер-пароль",
|
||||||
"txt_master_password_is_required_2": "Требуется мастер-пароль.",
|
"txt_master_password_is_required_2": "Требуется мастер-пароль.",
|
||||||
"txt_master_password_must_be_at_least_12_chars": "Мастер-пароль должен содержать не менее 12 символов.",
|
"txt_master_password_must_be_at_least_12_chars": "Мастер-пароль должен содержать не менее 12 символов.",
|
||||||
|
"txt_master_password_verify_failed": "Не удалось проверить мастер-пароль",
|
||||||
"txt_master_password_reprompt": "Повторный запрос мастер-пароля",
|
"txt_master_password_reprompt": "Повторный запрос мастер-пароля",
|
||||||
"txt_master_password_reprompt_2": "Повторный запрос мастер-пароля",
|
"txt_master_password_reprompt_2": "Повторный запрос мастер-пароля",
|
||||||
"txt_max_access_count": "Максимальное количество доступов",
|
"txt_max_access_count": "Максимальное количество доступов",
|
||||||
@@ -632,6 +656,9 @@ const ru: Record<string, string> = {
|
|||||||
"txt_api_key_rotated": "Ключ API поменян",
|
"txt_api_key_rotated": "Ключ API поменян",
|
||||||
"txt_rotate_api_key_confirm": "Поменять ключ API? Текущий ключ немедленно перестанет работать.",
|
"txt_rotate_api_key_confirm": "Поменять ключ API? Текущий ключ немедленно перестанет работать.",
|
||||||
"txt_api_key_is_empty": "Ключ API пуст",
|
"txt_api_key_is_empty": "Ключ API пуст",
|
||||||
|
"txt_get_api_key_failed": "Не удалось получить ключ API",
|
||||||
|
"txt_get_recovery_code_failed": "Не удалось получить код восстановления",
|
||||||
|
"txt_rotate_api_key_failed": "Не удалось сменить ключ API",
|
||||||
"txt_api_key_dialog_intro": "Ваш ключ API можно использовать для аутентификации с помощью Bitwarden CLI.",
|
"txt_api_key_dialog_intro": "Ваш ключ API можно использовать для аутентификации с помощью Bitwarden CLI.",
|
||||||
"txt_api_key_warning_body": "Ваш ключ API — это альтернативный механизм аутентификации. Держите это в секрете.",
|
"txt_api_key_warning_body": "Ваш ключ API — это альтернативный механизм аутентификации. Держите это в секрете.",
|
||||||
"txt_oauth_client_credentials": "Учетные данные клиента OAuth 2.0",
|
"txt_oauth_client_credentials": "Учетные данные клиента OAuth 2.0",
|
||||||
@@ -669,6 +696,7 @@ const ru: Record<string, string> = {
|
|||||||
"txt_save_profile": "Сохранить профиль",
|
"txt_save_profile": "Сохранить профиль",
|
||||||
"txt_save_profile_failed": "Сохранить профиль не удалось",
|
"txt_save_profile_failed": "Сохранить профиль не удалось",
|
||||||
"txt_search_sends": "Поиск отправляет...",
|
"txt_search_sends": "Поиск отправляет...",
|
||||||
|
"txt_session_refresh_failed": "Не удалось обновить сеанс. Войдите снова.",
|
||||||
"txt_search_your_secure_vault": "Найдите свое безопасное хранилище...",
|
"txt_search_your_secure_vault": "Найдите свое безопасное хранилище...",
|
||||||
"txt_clear_search": "Очистить поиск",
|
"txt_clear_search": "Очистить поиск",
|
||||||
"txt_clear_search_esc": "Очистить поиск (Esc)",
|
"txt_clear_search_esc": "Очистить поиск (Esc)",
|
||||||
|
|||||||
@@ -349,6 +349,7 @@ const zhCN: Record<string, string> = {
|
|||||||
"txt_create": "创建",
|
"txt_create": "创建",
|
||||||
"txt_create_account": "创建账户",
|
"txt_create_account": "创建账户",
|
||||||
"txt_registering": "正在注册...",
|
"txt_registering": "正在注册...",
|
||||||
|
"txt_register_failed": "注册失败",
|
||||||
"txt_create_folder": "创建文件夹",
|
"txt_create_folder": "创建文件夹",
|
||||||
"txt_create_folder_failed": "创建文件夹失败",
|
"txt_create_folder_failed": "创建文件夹失败",
|
||||||
"txt_create_item_failed": "创建项目失败",
|
"txt_create_item_failed": "创建项目失败",
|
||||||
@@ -408,6 +409,7 @@ const zhCN: Record<string, string> = {
|
|||||||
"txt_disable_this_send": "禁用此 Send",
|
"txt_disable_this_send": "禁用此 Send",
|
||||||
"txt_disable_totp": "停用 TOTP",
|
"txt_disable_totp": "停用 TOTP",
|
||||||
"txt_disable_totp_failed": "禁用 TOTP 失败",
|
"txt_disable_totp_failed": "禁用 TOTP 失败",
|
||||||
|
"txt_totp_update_failed": "更新 TOTP 失败",
|
||||||
"txt_download": "下载",
|
"txt_download": "下载",
|
||||||
"txt_downloading": "下载中...",
|
"txt_downloading": "下载中...",
|
||||||
"txt_downloading_percent": "下载中 {percent}%",
|
"txt_downloading_percent": "下载中 {percent}%",
|
||||||
@@ -467,12 +469,33 @@ const zhCN: Record<string, string> = {
|
|||||||
"txt_identity_details": "身份详情",
|
"txt_identity_details": "身份详情",
|
||||||
"txt_ie_browser": "IE 浏览器",
|
"txt_ie_browser": "IE 浏览器",
|
||||||
"txt_create_invite_failed": "创建邀请码失败",
|
"txt_create_invite_failed": "创建邀请码失败",
|
||||||
"txt_invite_code_optional": "邀请码(首位注册者无需填写,其他人必填)",
|
"txt_invite_code_required": "邀请码(必填)",
|
||||||
"txt_invite_created": "邀请码已创建",
|
"txt_invite_created": "邀请码已创建",
|
||||||
"txt_invite_revoked": "邀请码已撤销",
|
"txt_invite_revoked": "邀请码已撤销",
|
||||||
"txt_revoke_invite_failed": "撤销邀请码失败",
|
"txt_revoke_invite_failed": "撤销邀请码失败",
|
||||||
"txt_invite_validity_hours": "邀请码有效期(小时)",
|
"txt_invite_validity_hours": "邀请码有效期(小时)",
|
||||||
"txt_invites": "邀请码",
|
"txt_invites": "邀请码",
|
||||||
|
"txt_rate_limit_try_again_seconds": "请求过于频繁,请在 {seconds} 秒后重试",
|
||||||
|
"txt_server_error_account_disabled": "账号已被禁用",
|
||||||
|
"txt_server_error_client_credentials_incorrect": "客户端 ID 或客户端密钥不正确,请重试",
|
||||||
|
"txt_server_error_client_ip_required": "无法获取客户端 IP",
|
||||||
|
"txt_server_error_email_already_registered": "该邮箱已注册",
|
||||||
|
"txt_server_error_email_password_required": "邮箱和密码不能为空",
|
||||||
|
"txt_server_error_email_required": "邮箱不能为空",
|
||||||
|
"txt_server_error_invalid_refresh_token": "登录状态已失效,请重新登录",
|
||||||
|
"txt_server_error_invalid_request_payload": "请求内容无效",
|
||||||
|
"txt_server_error_invite_invalid_or_expired": "邀请码无效或已过期",
|
||||||
|
"txt_server_error_invite_required": "邀请码不能为空",
|
||||||
|
"txt_server_error_jwt_secret_default": "JWT_SECRET 正在使用默认示例值,请修改后再继续",
|
||||||
|
"txt_server_error_jwt_secret_missing": "JWT_SECRET 未设置",
|
||||||
|
"txt_server_error_jwt_secret_too_short": "JWT_SECRET 至少需要 32 个字符",
|
||||||
|
"txt_server_error_parameter_error": "请求参数错误",
|
||||||
|
"txt_server_error_refresh_token_required": "登录状态缺失,请重新登录",
|
||||||
|
"txt_server_error_registration_retry": "注册暂时不可用,请重试一次",
|
||||||
|
"txt_server_error_totp_token_required": "请输入两步验证码",
|
||||||
|
"txt_server_error_two_factor_invalid": "两步验证码无效,请重试",
|
||||||
|
"txt_server_error_two_factor_required": "需要两步验证",
|
||||||
|
"txt_server_error_username_password_incorrect": "用户名或密码不正确,请重试",
|
||||||
"txt_ios": "iOS",
|
"txt_ios": "iOS",
|
||||||
"txt_item": "项目",
|
"txt_item": "项目",
|
||||||
"txt_item_created": "项目已创建",
|
"txt_item_created": "项目已创建",
|
||||||
@@ -546,6 +569,7 @@ const zhCN: Record<string, string> = {
|
|||||||
"txt_master_password_is_required": "主密码不能为空",
|
"txt_master_password_is_required": "主密码不能为空",
|
||||||
"txt_master_password_is_required_2": "请输入主密码",
|
"txt_master_password_is_required_2": "请输入主密码",
|
||||||
"txt_master_password_must_be_at_least_12_chars": "主密码至少需要 12 个字符",
|
"txt_master_password_must_be_at_least_12_chars": "主密码至少需要 12 个字符",
|
||||||
|
"txt_master_password_verify_failed": "主密码验证失败",
|
||||||
"txt_master_password_reprompt": "主密码二次确认",
|
"txt_master_password_reprompt": "主密码二次确认",
|
||||||
"txt_master_password_reprompt_2": "主密码二次确认",
|
"txt_master_password_reprompt_2": "主密码二次确认",
|
||||||
"txt_max_access_count": "最大访问次数",
|
"txt_max_access_count": "最大访问次数",
|
||||||
@@ -632,6 +656,9 @@ const zhCN: Record<string, string> = {
|
|||||||
"txt_api_key_rotated": "API 密钥已轮换",
|
"txt_api_key_rotated": "API 密钥已轮换",
|
||||||
"txt_rotate_api_key_confirm": "轮换 API 密钥?当前密钥将立即失效。",
|
"txt_rotate_api_key_confirm": "轮换 API 密钥?当前密钥将立即失效。",
|
||||||
"txt_api_key_is_empty": "API 密钥为空",
|
"txt_api_key_is_empty": "API 密钥为空",
|
||||||
|
"txt_get_api_key_failed": "获取 API 密钥失败",
|
||||||
|
"txt_get_recovery_code_failed": "获取恢复代码失败",
|
||||||
|
"txt_rotate_api_key_failed": "轮换 API 密钥失败",
|
||||||
"txt_api_key_dialog_intro": "您的 API 密钥可用于在 Bitwarden CLI 中进行身份验证。",
|
"txt_api_key_dialog_intro": "您的 API 密钥可用于在 Bitwarden CLI 中进行身份验证。",
|
||||||
"txt_api_key_warning_body": "您的 API 密钥是一种替代身份验证机制。请严格保密。",
|
"txt_api_key_warning_body": "您的 API 密钥是一种替代身份验证机制。请严格保密。",
|
||||||
"txt_oauth_client_credentials": "OAuth 2.0 客户端凭据",
|
"txt_oauth_client_credentials": "OAuth 2.0 客户端凭据",
|
||||||
@@ -669,6 +696,7 @@ const zhCN: Record<string, string> = {
|
|||||||
"txt_save_profile": "保存资料",
|
"txt_save_profile": "保存资料",
|
||||||
"txt_save_profile_failed": "保存资料失败",
|
"txt_save_profile_failed": "保存资料失败",
|
||||||
"txt_search_sends": "搜索 Send...",
|
"txt_search_sends": "搜索 Send...",
|
||||||
|
"txt_session_refresh_failed": "会话刷新失败,请重新登录",
|
||||||
"txt_search_your_secure_vault": "搜索你的密码库...",
|
"txt_search_your_secure_vault": "搜索你的密码库...",
|
||||||
"txt_clear_search": "清空搜索",
|
"txt_clear_search": "清空搜索",
|
||||||
"txt_clear_search_esc": "清空搜索(Esc)",
|
"txt_clear_search_esc": "清空搜索(Esc)",
|
||||||
|
|||||||
@@ -349,6 +349,7 @@ const zhTW: Record<string, string> = {
|
|||||||
"txt_create": "創建",
|
"txt_create": "創建",
|
||||||
"txt_create_account": "創建賬戶",
|
"txt_create_account": "創建賬戶",
|
||||||
"txt_registering": "正在註冊...",
|
"txt_registering": "正在註冊...",
|
||||||
|
"txt_register_failed": "註冊失敗",
|
||||||
"txt_create_folder": "創建文件夾",
|
"txt_create_folder": "創建文件夾",
|
||||||
"txt_create_folder_failed": "創建文件夾失敗",
|
"txt_create_folder_failed": "創建文件夾失敗",
|
||||||
"txt_create_item_failed": "創建項目失敗",
|
"txt_create_item_failed": "創建項目失敗",
|
||||||
@@ -408,6 +409,7 @@ const zhTW: Record<string, string> = {
|
|||||||
"txt_disable_this_send": "禁用此 Send",
|
"txt_disable_this_send": "禁用此 Send",
|
||||||
"txt_disable_totp": "停用 TOTP",
|
"txt_disable_totp": "停用 TOTP",
|
||||||
"txt_disable_totp_failed": "禁用 TOTP 失敗",
|
"txt_disable_totp_failed": "禁用 TOTP 失敗",
|
||||||
|
"txt_totp_update_failed": "更新 TOTP 失敗",
|
||||||
"txt_download": "下載",
|
"txt_download": "下載",
|
||||||
"txt_downloading": "下載中...",
|
"txt_downloading": "下載中...",
|
||||||
"txt_downloading_percent": "下載中 {percent}%",
|
"txt_downloading_percent": "下載中 {percent}%",
|
||||||
@@ -467,12 +469,33 @@ const zhTW: Record<string, string> = {
|
|||||||
"txt_identity_details": "身份詳情",
|
"txt_identity_details": "身份詳情",
|
||||||
"txt_ie_browser": "IE 瀏覽器",
|
"txt_ie_browser": "IE 瀏覽器",
|
||||||
"txt_create_invite_failed": "創建邀請碼失敗",
|
"txt_create_invite_failed": "創建邀請碼失敗",
|
||||||
"txt_invite_code_optional": "邀請碼(首位註冊者無需填寫,其他人必填)",
|
"txt_invite_code_required": "邀請碼(必填)",
|
||||||
"txt_invite_created": "邀請碼已創建",
|
"txt_invite_created": "邀請碼已創建",
|
||||||
"txt_invite_revoked": "邀請碼已撤銷",
|
"txt_invite_revoked": "邀請碼已撤銷",
|
||||||
"txt_revoke_invite_failed": "撤銷邀請碼失敗",
|
"txt_revoke_invite_failed": "撤銷邀請碼失敗",
|
||||||
"txt_invite_validity_hours": "邀請碼有效期(小時)",
|
"txt_invite_validity_hours": "邀請碼有效期(小時)",
|
||||||
"txt_invites": "邀請碼",
|
"txt_invites": "邀請碼",
|
||||||
|
"txt_rate_limit_try_again_seconds": "請求過於頻繁,請在 {seconds} 秒後重試",
|
||||||
|
"txt_server_error_account_disabled": "帳號已被禁用",
|
||||||
|
"txt_server_error_client_credentials_incorrect": "客戶端 ID 或客戶端密鑰不正確,請重試",
|
||||||
|
"txt_server_error_client_ip_required": "無法獲取客戶端 IP",
|
||||||
|
"txt_server_error_email_already_registered": "該郵箱已註冊",
|
||||||
|
"txt_server_error_email_password_required": "郵箱和密碼不能為空",
|
||||||
|
"txt_server_error_email_required": "郵箱不能為空",
|
||||||
|
"txt_server_error_invalid_refresh_token": "登入狀態已失效,請重新登入",
|
||||||
|
"txt_server_error_invalid_request_payload": "請求內容無效",
|
||||||
|
"txt_server_error_invite_invalid_or_expired": "邀請碼無效或已過期",
|
||||||
|
"txt_server_error_invite_required": "邀請碼不能為空",
|
||||||
|
"txt_server_error_jwt_secret_default": "JWT_SECRET 正在使用默認示例值,請修改後再繼續",
|
||||||
|
"txt_server_error_jwt_secret_missing": "JWT_SECRET 未設置",
|
||||||
|
"txt_server_error_jwt_secret_too_short": "JWT_SECRET 至少需要 32 個字符",
|
||||||
|
"txt_server_error_parameter_error": "請求參數錯誤",
|
||||||
|
"txt_server_error_refresh_token_required": "登入狀態缺失,請重新登入",
|
||||||
|
"txt_server_error_registration_retry": "註冊暫時不可用,請重試一次",
|
||||||
|
"txt_server_error_totp_token_required": "請輸入兩步驗證碼",
|
||||||
|
"txt_server_error_two_factor_invalid": "兩步驗證碼無效,請重試",
|
||||||
|
"txt_server_error_two_factor_required": "需要兩步驗證",
|
||||||
|
"txt_server_error_username_password_incorrect": "用戶名或密碼不正確,請重試",
|
||||||
"txt_ios": "iOS",
|
"txt_ios": "iOS",
|
||||||
"txt_item": "項目",
|
"txt_item": "項目",
|
||||||
"txt_item_created": "項目已創建",
|
"txt_item_created": "項目已創建",
|
||||||
@@ -546,6 +569,7 @@ const zhTW: Record<string, string> = {
|
|||||||
"txt_master_password_is_required": "主密碼不能為空",
|
"txt_master_password_is_required": "主密碼不能為空",
|
||||||
"txt_master_password_is_required_2": "請輸入主密碼",
|
"txt_master_password_is_required_2": "請輸入主密碼",
|
||||||
"txt_master_password_must_be_at_least_12_chars": "主密碼至少需要 12 個字符",
|
"txt_master_password_must_be_at_least_12_chars": "主密碼至少需要 12 個字符",
|
||||||
|
"txt_master_password_verify_failed": "主密碼驗證失敗",
|
||||||
"txt_master_password_reprompt": "主密碼二次確認",
|
"txt_master_password_reprompt": "主密碼二次確認",
|
||||||
"txt_master_password_reprompt_2": "主密碼二次確認",
|
"txt_master_password_reprompt_2": "主密碼二次確認",
|
||||||
"txt_max_access_count": "最大訪問次數",
|
"txt_max_access_count": "最大訪問次數",
|
||||||
@@ -632,6 +656,9 @@ const zhTW: Record<string, string> = {
|
|||||||
"txt_api_key_rotated": "API 密鑰已輪換",
|
"txt_api_key_rotated": "API 密鑰已輪換",
|
||||||
"txt_rotate_api_key_confirm": "輪換 API 密鑰?當前密鑰將立即失效。",
|
"txt_rotate_api_key_confirm": "輪換 API 密鑰?當前密鑰將立即失效。",
|
||||||
"txt_api_key_is_empty": "API 密鑰為空",
|
"txt_api_key_is_empty": "API 密鑰為空",
|
||||||
|
"txt_get_api_key_failed": "獲取 API 密鑰失敗",
|
||||||
|
"txt_get_recovery_code_failed": "獲取恢復代碼失敗",
|
||||||
|
"txt_rotate_api_key_failed": "輪換 API 密鑰失敗",
|
||||||
"txt_api_key_dialog_intro": "您的 API 密鑰可用於在 Bitwarden CLI 中進行身份驗證。",
|
"txt_api_key_dialog_intro": "您的 API 密鑰可用於在 Bitwarden CLI 中進行身份驗證。",
|
||||||
"txt_api_key_warning_body": "您的 API 密鑰是一種替代身份驗證機制。請嚴格保密。",
|
"txt_api_key_warning_body": "您的 API 密鑰是一種替代身份驗證機制。請嚴格保密。",
|
||||||
"txt_oauth_client_credentials": "OAuth 2.0 客戶端憑據",
|
"txt_oauth_client_credentials": "OAuth 2.0 客戶端憑據",
|
||||||
@@ -669,6 +696,7 @@ const zhTW: Record<string, string> = {
|
|||||||
"txt_save_profile": "保存資料",
|
"txt_save_profile": "保存資料",
|
||||||
"txt_save_profile_failed": "保存資料失敗",
|
"txt_save_profile_failed": "保存資料失敗",
|
||||||
"txt_search_sends": "搜索 Send...",
|
"txt_search_sends": "搜索 Send...",
|
||||||
|
"txt_session_refresh_failed": "會話刷新失敗,請重新登入",
|
||||||
"txt_search_your_secure_vault": "搜索你的密碼庫...",
|
"txt_search_your_secure_vault": "搜索你的密碼庫...",
|
||||||
"txt_clear_search": "清空搜索",
|
"txt_clear_search": "清空搜索",
|
||||||
"txt_clear_search_esc": "清空搜索(Esc)",
|
"txt_clear_search_esc": "清空搜索(Esc)",
|
||||||
|
|||||||
@@ -287,6 +287,7 @@ export interface WebBootstrapResponse {
|
|||||||
defaultKdfIterations?: number;
|
defaultKdfIterations?: number;
|
||||||
jwtUnsafeReason?: 'missing' | 'default' | 'too_short' | null;
|
jwtUnsafeReason?: 'missing' | 'default' | 'too_short' | null;
|
||||||
jwtSecretMinLength?: number;
|
jwtSecretMinLength?: number;
|
||||||
|
registrationInviteRequired?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TokenSuccess {
|
export interface TokenSuccess {
|
||||||
|
|||||||
Reference in New Issue
Block a user