mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-08-05 06:50:10 +00:00
fix(security): harden jwt config and password rotation
This commit is contained in:
@@ -1,5 +0,0 @@
|
|||||||
# JWT Secret for signing tokens (required)
|
|
||||||
# IMPORTANT: change this value before any real deployment.
|
|
||||||
# Generate one with: openssl rand -hex 32
|
|
||||||
# (Example only, 64 hex chars = 32 bytes)
|
|
||||||
JWT_SECRET=Enter-your-JWT-key-here-at-least-32-characters
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Env, User, DEFAULT_DEV_SECRET } from '../types';
|
import { Env, User } from '../types';
|
||||||
import { StorageService } from '../services/storage';
|
import { StorageService } from '../services/storage';
|
||||||
import { AuthService } from '../services/auth';
|
import { AuthService } from '../services/auth';
|
||||||
import { RateLimitService, getClientIdentifier } from '../services/ratelimit';
|
import { RateLimitService, getClientIdentifier } from '../services/ratelimit';
|
||||||
@@ -150,10 +150,9 @@ function normalizeMasterPasswordHint(input: string | null | undefined): string |
|
|||||||
return normalized ? normalized : null;
|
return normalized ? normalized : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function jwtSecretUnsafeReason(env: Env): 'missing' | 'default' | 'too_short' | null {
|
function jwtSecretUnsafeReason(env: Env): 'missing' | 'too_short' | null {
|
||||||
const secret = (env.JWT_SECRET || '').trim();
|
const secret = (env.JWT_SECRET || '').trim();
|
||||||
if (!secret) return 'missing';
|
if (!secret) return 'missing';
|
||||||
if (secret === DEFAULT_DEV_SECRET) return 'default';
|
|
||||||
if (secret.length < LIMITS.auth.jwtSecretMinLength) return 'too_short';
|
if (secret.length < LIMITS.auth.jwtSecretMinLength) return 'too_short';
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -242,8 +241,6 @@ export async function handleRegister(request: Request, env: Env): Promise<Respon
|
|||||||
if (unsafe) {
|
if (unsafe) {
|
||||||
const message = unsafe === 'missing'
|
const message = unsafe === 'missing'
|
||||||
? 'JWT_SECRET is not set'
|
? 'JWT_SECRET is not set'
|
||||||
: unsafe === 'default'
|
|
||||||
? 'JWT_SECRET is using the default/sample value. Please change it.'
|
|
||||||
: 'JWT_SECRET must be at least 32 characters';
|
: 'JWT_SECRET must be at least 32 characters';
|
||||||
return errorResponse(message, 400);
|
return errorResponse(message, 400);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Env, Attachment, Cipher, DEFAULT_DEV_SECRET } from '../types';
|
import { Env, Attachment, Cipher } from '../types';
|
||||||
import { notifyUserCipherUpdate, notifyUserVaultSync } from '../durable/notifications-hub';
|
import { notifyUserCipherUpdate, notifyUserVaultSync } from '../durable/notifications-hub';
|
||||||
import { StorageService } from '../services/storage';
|
import { StorageService } from '../services/storage';
|
||||||
import { jsonResponse, errorResponse } from '../utils/response';
|
import { jsonResponse, errorResponse } from '../utils/response';
|
||||||
@@ -405,10 +405,8 @@ export async function handlePublicDownloadAttachment(
|
|||||||
cipherId: string,
|
cipherId: string,
|
||||||
attachmentId: string
|
attachmentId: string
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const secret = (env.JWT_SECRET || '').trim();
|
const secret = getSafeJwtSecret(env);
|
||||||
if (!secret || secret.length < LIMITS.auth.jwtSecretMinLength || secret === DEFAULT_DEV_SECRET) {
|
if (!secret) return errorResponse('Server configuration error', 500);
|
||||||
return errorResponse('Server configuration error', 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const token = url.searchParams.get('token');
|
const token = url.searchParams.get('token');
|
||||||
@@ -418,7 +416,7 @@ export async function handlePublicDownloadAttachment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify token
|
// Verify token
|
||||||
const claims = await verifyFileDownloadToken(token, env.JWT_SECRET);
|
const claims = await verifyFileDownloadToken(token, secret);
|
||||||
if (!claims) {
|
if (!claims) {
|
||||||
return errorResponse('Invalid or expired token', 401);
|
return errorResponse('Invalid or expired token', 401);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { StorageService } from '../services/storage';
|
|||||||
import { RateLimitService, getClientIdentifier } from '../services/ratelimit';
|
import { RateLimitService, getClientIdentifier } from '../services/ratelimit';
|
||||||
import { jsonResponse, errorResponse } from '../utils/response';
|
import { jsonResponse, errorResponse } from '../utils/response';
|
||||||
import { sanitizeDownloadContentType } from '../utils/content-type';
|
import { sanitizeDownloadContentType } from '../utils/content-type';
|
||||||
import { LIMITS } from '../config/limits';
|
|
||||||
import {
|
import {
|
||||||
createSendAccessToken,
|
createSendAccessToken,
|
||||||
createSendFileDownloadToken,
|
createSendFileDownloadToken,
|
||||||
@@ -113,10 +112,9 @@ export async function handleAccessSendFile(
|
|||||||
idOrAccessId: string,
|
idOrAccessId: string,
|
||||||
fileId: string
|
fileId: string
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const secret = (env.JWT_SECRET || '').trim();
|
const safeSecret = getSafeJwtSecret(env);
|
||||||
if (!secret || secret.length < LIMITS.auth.jwtSecretMinLength) {
|
if (!safeSecret.ok) return safeSecret.response;
|
||||||
return errorResponse('Server configuration error', 500);
|
const { secret } = safeSecret;
|
||||||
}
|
|
||||||
|
|
||||||
const storage = new StorageService(env.DB);
|
const storage = new StorageService(env.DB);
|
||||||
const send = await resolveSendFromIdOrAccessId(storage, idOrAccessId);
|
const send = await resolveSendFromIdOrAccessId(storage, idOrAccessId);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Env, Send, SendAuthType, SendResponse, SendType, DEFAULT_DEV_SECRET } from '../types';
|
import { Env, Send, SendAuthType, SendResponse, SendType } from '../types';
|
||||||
import {
|
import {
|
||||||
notifyUserSendCreate,
|
notifyUserSendCreate,
|
||||||
notifyUserSendDelete,
|
notifyUserSendDelete,
|
||||||
@@ -371,7 +371,7 @@ export function hasEmailAuth(send: Send): boolean {
|
|||||||
|
|
||||||
export function getSafeJwtSecret(env: Env): { ok: true; secret: string } | { ok: false; response: Response } {
|
export function getSafeJwtSecret(env: Env): { ok: true; secret: string } | { ok: false; response: Response } {
|
||||||
const secret = (env.JWT_SECRET || '').trim();
|
const secret = (env.JWT_SECRET || '').trim();
|
||||||
if (!secret || secret.length < LIMITS.auth.jwtSecretMinLength || secret === DEFAULT_DEV_SECRET) {
|
if (!secret || secret.length < LIMITS.auth.jwtSecretMinLength) {
|
||||||
return { ok: false, response: errorResponse('Server configuration error', 500) };
|
return { ok: false, response: errorResponse('Server configuration error', 500) };
|
||||||
}
|
}
|
||||||
return { ok: true, secret };
|
return { ok: true, secret };
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { LIMITS } from './config/limits';
|
import { LIMITS } from './config/limits';
|
||||||
import { DEFAULT_DEV_SECRET } from './types';
|
|
||||||
import {
|
import {
|
||||||
handleAccessSend,
|
handleAccessSend,
|
||||||
handleAccessSendFile,
|
handleAccessSendFile,
|
||||||
@@ -34,7 +33,7 @@ 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>;
|
||||||
type JwtUnsafeReason = 'missing' | 'default' | 'too_short' | null;
|
type JwtUnsafeReason = 'missing' | 'too_short' | null;
|
||||||
|
|
||||||
export interface WebBootstrapResponse {
|
export interface WebBootstrapResponse {
|
||||||
defaultKdfIterations: number;
|
defaultKdfIterations: number;
|
||||||
@@ -308,8 +307,6 @@ export async function buildWebBootstrapResponse(env: Env): Promise<WebBootstrapR
|
|||||||
const jwtUnsafeReason =
|
const jwtUnsafeReason =
|
||||||
!secret
|
!secret
|
||||||
? 'missing'
|
? 'missing'
|
||||||
: secret === DEFAULT_DEV_SECRET
|
|
||||||
? 'default'
|
|
||||||
: secret.length < LIMITS.auth.jwtSecretMinLength
|
: secret.length < LIMITS.auth.jwtSecretMinLength
|
||||||
? 'too_short'
|
? 'too_short'
|
||||||
: null;
|
: null;
|
||||||
|
|||||||
+17
-7
@@ -1,4 +1,4 @@
|
|||||||
import { DEFAULT_DEV_SECRET, Env } from './types';
|
import { Env } from './types';
|
||||||
import { AuthService } from './services/auth';
|
import { AuthService } from './services/auth';
|
||||||
import { RateLimitService, getClientIdentifier } from './services/ratelimit';
|
import { RateLimitService, getClientIdentifier } from './services/ratelimit';
|
||||||
import { handleCors, errorResponse } from './utils/response';
|
import { handleCors, errorResponse } from './utils/response';
|
||||||
@@ -6,14 +6,24 @@ import { LIMITS } from './config/limits';
|
|||||||
import { handleAuthenticatedRoute } from './router-authenticated';
|
import { handleAuthenticatedRoute } from './router-authenticated';
|
||||||
import { handlePublicRoute } from './router-public';
|
import { handlePublicRoute } from './router-public';
|
||||||
|
|
||||||
function jwtSecretUnsafeReason(env: Env): 'missing' | 'default' | 'too_short' | null {
|
function jwtSecretUnsafeReason(env: Env): 'missing' | 'too_short' | null {
|
||||||
const secret = (env.JWT_SECRET || '').trim();
|
const secret = (env.JWT_SECRET || '').trim();
|
||||||
if (!secret) return 'missing';
|
if (!secret) return 'missing';
|
||||||
if (secret === DEFAULT_DEV_SECRET) return 'default';
|
|
||||||
if (secret.length < LIMITS.auth.jwtSecretMinLength) return 'too_short';
|
if (secret.length < LIMITS.auth.jwtSecretMinLength) return 'too_short';
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canServeWithUnsafeJwtSecret(path: string, method: string): boolean {
|
||||||
|
if (method === 'OPTIONS') return true;
|
||||||
|
if (method === 'GET' && (path === '/api/web-bootstrap' || path === '/web-bootstrap')) return true;
|
||||||
|
if (method === 'GET' && (path === '/config' || path === '/api/config' || path === '/api/version')) return true;
|
||||||
|
if (method === 'GET' && path === '/.well-known/appspecific/com.chrome.devtools.json') return true;
|
||||||
|
if (method === 'GET' && path === '/fill-assist/manifest.json') return true;
|
||||||
|
if (method === 'GET' && /^\/fill-assist\/[^/]+$/i.test(path)) return true;
|
||||||
|
if (method === 'GET' && /^\/icons\/[^/]+\/icon\.png$/i.test(path)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function isImportBypassRequest(request: Request, path: string, method: string): boolean {
|
function isImportBypassRequest(request: Request, path: string, method: string): boolean {
|
||||||
if (request.headers.get('X-NodeWarden-Import') !== '1') return false;
|
if (request.headers.get('X-NodeWarden-Import') !== '1') return false;
|
||||||
|
|
||||||
@@ -85,14 +95,14 @@ export async function handleRequest(request: Request, env: Env): Promise<Respons
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicResponse = await handlePublicRoute(request, env, path, method, enforcePublicRateLimit);
|
|
||||||
if (publicResponse) return publicResponse;
|
|
||||||
|
|
||||||
const secretIssue = jwtSecretUnsafeReason(env);
|
const secretIssue = jwtSecretUnsafeReason(env);
|
||||||
if (secretIssue) {
|
if (secretIssue && !canServeWithUnsafeJwtSecret(path, method)) {
|
||||||
return errorResponse('Server configuration error: JWT_SECRET is not set or too weak', 500);
|
return errorResponse('Server configuration error: JWT_SECRET is not set or too weak', 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const publicResponse = await handlePublicRoute(request, env, path, method, enforcePublicRateLimit);
|
||||||
|
if (publicResponse) return publicResponse;
|
||||||
|
|
||||||
const auth = new AuthService(env);
|
const auth = new AuthService(env);
|
||||||
const authHeader = request.headers.get('Authorization');
|
const authHeader = request.headers.get('Authorization');
|
||||||
const verified = await auth.verifyAccessTokenWithUser(authHeader);
|
const verified = await auth.verifyAccessTokenWithUser(authHeader);
|
||||||
|
|||||||
@@ -19,10 +19,6 @@ export interface Env {
|
|||||||
export type UserRole = 'admin' | 'user';
|
export type UserRole = 'admin' | 'user';
|
||||||
export type UserStatus = 'active' | 'banned';
|
export type UserStatus = 'active' | 'banned';
|
||||||
|
|
||||||
// Sample JWT secret used by `.dev.vars.example`.
|
|
||||||
// If runtime JWT_SECRET equals this value, treat it as unsafe.
|
|
||||||
export const DEFAULT_DEV_SECRET = 'Enter-your-JWT-key-here-at-least-32-characters';
|
|
||||||
|
|
||||||
// Attachment model
|
// Attachment model
|
||||||
export interface Attachment {
|
export interface Attachment {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { LIMITS } from '../config/limits';
|
import { LIMITS } from '../config/limits';
|
||||||
import { DEFAULT_DEV_SECRET, Env } from '../types';
|
import { Env } from '../types';
|
||||||
import { errorResponse } from './response';
|
import { errorResponse } from './response';
|
||||||
|
|
||||||
export interface DirectUploadPayload {
|
export interface DirectUploadPayload {
|
||||||
@@ -28,7 +28,7 @@ export function buildDirectUploadUrl(request: Request, path: string, token: stri
|
|||||||
|
|
||||||
export function getSafeJwtSecret(env: Env): string | null {
|
export function getSafeJwtSecret(env: Env): string | null {
|
||||||
const secret = (env.JWT_SECRET || '').trim();
|
const secret = (env.JWT_SECRET || '').trim();
|
||||||
if (!secret || secret.length < LIMITS.auth.jwtSecretMinLength || secret === DEFAULT_DEV_SECRET) {
|
if (!secret || secret.length < LIMITS.auth.jwtSecretMinLength) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return secret;
|
return secret;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import StandalonePageFrame from '@/components/StandalonePageFrame';
|
|||||||
import { t } from '@/lib/i18n';
|
import { t } from '@/lib/i18n';
|
||||||
|
|
||||||
interface JwtWarningPageProps {
|
interface JwtWarningPageProps {
|
||||||
reason: 'missing' | 'default' | 'too_short';
|
reason: 'missing' | 'too_short';
|
||||||
minLength: number;
|
minLength: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,8 +21,6 @@ export default function JwtWarningPage(props: JwtWarningPageProps) {
|
|||||||
const title =
|
const title =
|
||||||
props.reason === 'missing'
|
props.reason === 'missing'
|
||||||
? t('txt_jwt_title_missing')
|
? t('txt_jwt_title_missing')
|
||||||
: props.reason === 'default'
|
|
||||||
? t('txt_jwt_title_default')
|
|
||||||
: t('txt_jwt_title_too_short');
|
: t('txt_jwt_title_too_short');
|
||||||
|
|
||||||
const isMissing = props.reason === 'missing';
|
const isMissing = props.reason === 'missing';
|
||||||
|
|||||||
@@ -591,11 +591,14 @@ export async function changeMasterPassword(
|
|||||||
const oldEnc = await hkdfExpand(current.masterKey, 'enc', 32);
|
const oldEnc = await hkdfExpand(current.masterKey, 'enc', 32);
|
||||||
const oldMac = await hkdfExpand(current.masterKey, 'mac', 32);
|
const oldMac = await hkdfExpand(current.masterKey, 'mac', 32);
|
||||||
const userSym = await decryptBw(args.profileKey, oldEnc, oldMac);
|
const userSym = await decryptBw(args.profileKey, oldEnc, oldMac);
|
||||||
|
if (userSym.length !== 64) {
|
||||||
|
throw new Error('Invalid profile key');
|
||||||
|
}
|
||||||
const nextMasterKey = await pbkdf2(args.newPassword, args.email, current.kdfIterations, 32);
|
const nextMasterKey = await pbkdf2(args.newPassword, args.email, current.kdfIterations, 32);
|
||||||
const nextHash = await pbkdf2(nextMasterKey, args.newPassword, 1, 32);
|
const nextHash = await pbkdf2(nextMasterKey, args.newPassword, 1, 32);
|
||||||
const nextEnc = await hkdfExpand(nextMasterKey, 'enc', 32);
|
const nextEnc = await hkdfExpand(nextMasterKey, 'enc', 32);
|
||||||
const nextMac = await hkdfExpand(nextMasterKey, 'mac', 32);
|
const nextMac = await hkdfExpand(nextMasterKey, 'mac', 32);
|
||||||
const newKey = await encryptBw(userSym.slice(0, 64), nextEnc, nextMac);
|
const newKey = await encryptBw(userSym, nextEnc, nextMac);
|
||||||
const newMasterPasswordHash = bytesToBase64(nextHash);
|
const newMasterPasswordHash = bytesToBase64(nextHash);
|
||||||
|
|
||||||
const resp = await authedFetch('/api/accounts/password', {
|
const resp = await authedFetch('/api/accounts/password', {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export interface PendingPasskeyPassword {
|
|||||||
kdfIterations: number;
|
kdfIterations: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type JwtUnsafeReason = 'missing' | 'default' | 'too_short';
|
export type JwtUnsafeReason = 'missing' | 'too_short';
|
||||||
|
|
||||||
export interface BootstrapAppResult {
|
export interface BootstrapAppResult {
|
||||||
defaultKdfIterations: number;
|
defaultKdfIterations: number;
|
||||||
|
|||||||
@@ -669,7 +669,7 @@ const en: Record<string, string> = {
|
|||||||
"txt_jwt_secret_value_label": "Value:",
|
"txt_jwt_secret_value_label": "Value:",
|
||||||
"txt_jwt_secret_value_requirement": "Random string with at least {min} characters",
|
"txt_jwt_secret_value_requirement": "Random string with at least {min} characters",
|
||||||
"txt_jwt_what_is": "What is JWT?",
|
"txt_jwt_what_is": "What is JWT?",
|
||||||
"txt_jwt_what_is_body": "JWT_SECRET is the server-side signing key used to issue and verify login tokens. If it is missing, too short, or still using the sample value, the instance is not safe to use normally.",
|
"txt_jwt_what_is_body": "JWT_SECRET is the server-side signing key used to issue and verify login tokens. If it is missing or too short, the instance is not safe to use normally.",
|
||||||
"txt_how_to_fix": "How to fix",
|
"txt_how_to_fix": "How to fix",
|
||||||
"txt_jwt_fix_step_1": "Open your deployment environment variables.",
|
"txt_jwt_fix_step_1": "Open your deployment environment variables.",
|
||||||
"txt_jwt_fix_step_2": "If your current key is not random enough, use the 32-character generator below.",
|
"txt_jwt_fix_step_2": "If your current key is not random enough, use the 32-character generator below.",
|
||||||
|
|||||||
@@ -669,7 +669,7 @@ const es: Record<string, string> = {
|
|||||||
"txt_jwt_secret_value_label": "Valor:",
|
"txt_jwt_secret_value_label": "Valor:",
|
||||||
"txt_jwt_secret_value_requirement": "Cadena aleatoria de al menos {min} caracteres",
|
"txt_jwt_secret_value_requirement": "Cadena aleatoria de al menos {min} caracteres",
|
||||||
"txt_jwt_what_is": "Qué es JWT",
|
"txt_jwt_what_is": "Qué es JWT",
|
||||||
"txt_jwt_what_is_body": "JWT_SECRET es la clave de firma del lado del servidor utilizada para emitir y verificar tokens de inicio de sesión. Si no está presente, es demasiado corta o todavía usa el valor de ejemplo, la instancia no es segura para uso normal.",
|
"txt_jwt_what_is_body": "JWT_SECRET es la clave de firma del lado del servidor utilizada para emitir y verificar tokens de inicio de sesión. Si no está presente o es demasiado corta, la instancia no es segura para uso normal.",
|
||||||
"txt_how_to_fix": "Cómo corregirlo",
|
"txt_how_to_fix": "Cómo corregirlo",
|
||||||
"txt_jwt_fix_step_1": "Abra las variables de entorno de su despliegue.",
|
"txt_jwt_fix_step_1": "Abra las variables de entorno de su despliegue.",
|
||||||
"txt_jwt_fix_step_2": "Si su clave actual no es lo suficientemente aleatoria, use el generador de 32 caracteres a continuación.",
|
"txt_jwt_fix_step_2": "Si su clave actual no es lo suficientemente aleatoria, use el generador de 32 caracteres a continuación.",
|
||||||
|
|||||||
@@ -669,7 +669,7 @@ const ru: Record<string, string> = {
|
|||||||
"txt_jwt_secret_value_label": "Значение:",
|
"txt_jwt_secret_value_label": "Значение:",
|
||||||
"txt_jwt_secret_value_requirement": "Случайная строка, содержащая не менее {min} символов.",
|
"txt_jwt_secret_value_requirement": "Случайная строка, содержащая не менее {min} символов.",
|
||||||
"txt_jwt_what_is": "Что такое JWT?",
|
"txt_jwt_what_is": "Что такое JWT?",
|
||||||
"txt_jwt_what_is_body": "JWT_SECRET — это ключ подписи на стороне сервера, используемый для выдачи и проверки токенов входа. Если он отсутствует, слишком короткий или все еще использует образец значения, обычное использование экземпляра небезопасно.",
|
"txt_jwt_what_is_body": "JWT_SECRET — это ключ подписи на стороне сервера, используемый для выдачи и проверки токенов входа. Если он отсутствует или слишком короткий, обычное использование экземпляра небезопасно.",
|
||||||
"txt_how_to_fix": "Как исправить",
|
"txt_how_to_fix": "Как исправить",
|
||||||
"txt_jwt_fix_step_1": "Откройте переменные среды развертывания.",
|
"txt_jwt_fix_step_1": "Откройте переменные среды развертывания.",
|
||||||
"txt_jwt_fix_step_2": "Если ваш текущий ключ недостаточно случайный, используйте 32-значный генератор ниже.",
|
"txt_jwt_fix_step_2": "Если ваш текущий ключ недостаточно случайный, используйте 32-значный генератор ниже.",
|
||||||
|
|||||||
@@ -669,7 +669,7 @@ const zhCN: Record<string, string> = {
|
|||||||
"txt_jwt_secret_value_label": "值:",
|
"txt_jwt_secret_value_label": "值:",
|
||||||
"txt_jwt_secret_value_requirement": "最低 {min} 位随机字符",
|
"txt_jwt_secret_value_requirement": "最低 {min} 位随机字符",
|
||||||
"txt_jwt_what_is": "JWT 是什么",
|
"txt_jwt_what_is": "JWT 是什么",
|
||||||
"txt_jwt_what_is_body": "JWT_SECRET 是服务端用来签发和校验登录令牌的密钥。如果它缺失、过短,或者仍然使用示例值,实例就不能安全地正常使用。",
|
"txt_jwt_what_is_body": "JWT_SECRET 是服务端用来签发和校验登录令牌的密钥。如果它缺失或过短,实例就不能安全地正常使用。",
|
||||||
"txt_how_to_fix": "处理步骤(添加 / 更换)",
|
"txt_how_to_fix": "处理步骤(添加 / 更换)",
|
||||||
"txt_jwt_fix_step_1": "你可以继续下一步,不影响使用。",
|
"txt_jwt_fix_step_1": "你可以继续下一步,不影响使用。",
|
||||||
"txt_jwt_fix_step_2": "如果当前密钥不是强随机值,建议使用下方 32 位生成器。",
|
"txt_jwt_fix_step_2": "如果当前密钥不是强随机值,建议使用下方 32 位生成器。",
|
||||||
|
|||||||
@@ -669,7 +669,7 @@ const zhTW: Record<string, string> = {
|
|||||||
"txt_jwt_secret_value_label": "值:",
|
"txt_jwt_secret_value_label": "值:",
|
||||||
"txt_jwt_secret_value_requirement": "最低 {min} 位隨機字符",
|
"txt_jwt_secret_value_requirement": "最低 {min} 位隨機字符",
|
||||||
"txt_jwt_what_is": "JWT 是什麼",
|
"txt_jwt_what_is": "JWT 是什麼",
|
||||||
"txt_jwt_what_is_body": "JWT_SECRET 是服務端用來簽發和校驗登錄令牌的密鑰。如果它缺失、過短,或者仍然使用示例值,實例就不能安全地正常使用。",
|
"txt_jwt_what_is_body": "JWT_SECRET 是服務端用來簽發和校驗登錄令牌的密鑰。如果它缺失或過短,實例就不能安全地正常使用。",
|
||||||
"txt_how_to_fix": "處理步驟(添加 / 更換)",
|
"txt_how_to_fix": "處理步驟(添加 / 更換)",
|
||||||
"txt_jwt_fix_step_1": "你可以繼續下一步,不影響使用。",
|
"txt_jwt_fix_step_1": "你可以繼續下一步,不影響使用。",
|
||||||
"txt_jwt_fix_step_2": "如果當前密鑰不是強隨機值,建議使用下方 32 位生成器。",
|
"txt_jwt_fix_step_2": "如果當前密鑰不是強隨機值,建議使用下方 32 位生成器。",
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ export interface ListResponse<T> {
|
|||||||
|
|
||||||
export interface WebBootstrapResponse {
|
export interface WebBootstrapResponse {
|
||||||
defaultKdfIterations?: number;
|
defaultKdfIterations?: number;
|
||||||
jwtUnsafeReason?: 'missing' | 'default' | 'too_short' | null;
|
jwtUnsafeReason?: 'missing' | 'too_short' | null;
|
||||||
jwtSecretMinLength?: number;
|
jwtSecretMinLength?: number;
|
||||||
registrationInviteRequired?: boolean;
|
registrationInviteRequired?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user