mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
feat: refactor setup handling and enhance asset serving with bootstrap integration
This commit is contained in:
@@ -4,10 +4,8 @@ import type { AuthorizedDevice } from '../types';
|
||||
import type {
|
||||
Profile,
|
||||
SessionState,
|
||||
SetupStatusResponse,
|
||||
TokenError,
|
||||
TokenSuccess,
|
||||
WebConfigResponse,
|
||||
} from '../types';
|
||||
import { parseJson, type AuthedFetch, type SessionSetter } from './shared';
|
||||
|
||||
@@ -93,17 +91,6 @@ export function saveSession(session: SessionState | null): void {
|
||||
localStorage.setItem(SESSION_KEY, JSON.stringify(persisted));
|
||||
}
|
||||
|
||||
export async function getSetupStatus(): Promise<SetupStatusResponse> {
|
||||
const resp = await fetch('/setup/status');
|
||||
const body = await parseJson<SetupStatusResponse>(resp);
|
||||
return { registered: !!body?.registered };
|
||||
}
|
||||
|
||||
export async function getWebConfig(): Promise<WebConfigResponse> {
|
||||
const resp = await fetch('/api/web/config');
|
||||
return (await parseJson<WebConfigResponse>(resp)) || {};
|
||||
}
|
||||
|
||||
export function getCurrentDeviceIdentifier(): string {
|
||||
return (localStorage.getItem(DEVICE_IDENTIFIER_KEY) || '').trim();
|
||||
}
|
||||
|
||||
+46
-22
@@ -2,8 +2,6 @@ import {
|
||||
createAuthedFetch,
|
||||
deriveLoginHash,
|
||||
getProfile,
|
||||
getSetupStatus,
|
||||
getWebConfig,
|
||||
loadSession,
|
||||
loginWithPassword,
|
||||
refreshAccessToken,
|
||||
@@ -11,7 +9,8 @@ import {
|
||||
registerAccount,
|
||||
unlockVaultKey,
|
||||
} from '@/lib/api/auth';
|
||||
import type { AppPhase, Profile, SessionState } from '@/lib/types';
|
||||
import { readInviteCodeFromUrl } from '@/lib/app-support';
|
||||
import type { AppPhase, Profile, SessionState, WebBootstrapResponse } from '@/lib/types';
|
||||
|
||||
export interface PendingTotp {
|
||||
email: string;
|
||||
@@ -22,7 +21,6 @@ export interface PendingTotp {
|
||||
export type JwtUnsafeReason = 'missing' | 'default' | 'too_short';
|
||||
|
||||
export interface BootstrapAppResult {
|
||||
setupRegistered: boolean;
|
||||
defaultKdfIterations: number;
|
||||
jwtWarning: { reason: JwtUnsafeReason; minLength: number } | null;
|
||||
session: SessionState | null;
|
||||
@@ -30,6 +28,13 @@ export interface BootstrapAppResult {
|
||||
phase: AppPhase;
|
||||
}
|
||||
|
||||
export interface InitialAppBootstrapState {
|
||||
defaultKdfIterations: number;
|
||||
jwtWarning: { reason: JwtUnsafeReason; minLength: number } | null;
|
||||
session: SessionState | null;
|
||||
phase: AppPhase;
|
||||
}
|
||||
|
||||
export interface CompletedLogin {
|
||||
session: SessionState;
|
||||
profile: Profile;
|
||||
@@ -80,35 +85,56 @@ async function maybeRefreshSession(session: SessionState): Promise<SessionState
|
||||
};
|
||||
}
|
||||
|
||||
export async function bootstrapAppSession(): Promise<BootstrapAppResult> {
|
||||
const [setup, config] = await Promise.all([getSetupStatus(), getWebConfig()]);
|
||||
const setupRegistered = setup.registered;
|
||||
const defaultKdfIterations = Number(config.defaultKdfIterations || 600000);
|
||||
const jwtUnsafeReason = config.jwtUnsafeReason || null;
|
||||
function readWindowBootstrap(): WebBootstrapResponse {
|
||||
if (typeof window === 'undefined') return {};
|
||||
const raw = (window as Window & { __NW_BOOT__?: WebBootstrapResponse }).__NW_BOOT__;
|
||||
return raw && typeof raw === 'object' ? raw : {};
|
||||
}
|
||||
|
||||
if (jwtUnsafeReason) {
|
||||
return {
|
||||
setupRegistered,
|
||||
defaultKdfIterations,
|
||||
jwtWarning: {
|
||||
export function readInitialAppBootstrapState(): InitialAppBootstrapState {
|
||||
const boot = readWindowBootstrap();
|
||||
const defaultKdfIterations = Number(boot.defaultKdfIterations || 600000);
|
||||
const jwtUnsafeReason = boot.jwtUnsafeReason || null;
|
||||
const jwtWarning = jwtUnsafeReason
|
||||
? {
|
||||
reason: jwtUnsafeReason,
|
||||
minLength: Number(config.jwtSecretMinLength || 32),
|
||||
},
|
||||
minLength: Number(boot.jwtSecretMinLength || 32),
|
||||
}
|
||||
: null;
|
||||
const session = loadSession();
|
||||
const hasInviteCode = !!readInviteCodeFromUrl();
|
||||
|
||||
return {
|
||||
defaultKdfIterations,
|
||||
jwtWarning,
|
||||
session,
|
||||
phase: jwtWarning ? 'login' : session ? 'locked' : hasInviteCode ? 'register' : 'login',
|
||||
};
|
||||
}
|
||||
|
||||
export async function bootstrapAppSession(): Promise<BootstrapAppResult> {
|
||||
const initial = readInitialAppBootstrapState();
|
||||
const defaultKdfIterations = initial.defaultKdfIterations;
|
||||
const jwtWarning = initial.jwtWarning;
|
||||
|
||||
if (jwtWarning) {
|
||||
return {
|
||||
defaultKdfIterations,
|
||||
jwtWarning,
|
||||
session: null,
|
||||
profile: null,
|
||||
phase: 'login',
|
||||
};
|
||||
}
|
||||
|
||||
const loaded = loadSession();
|
||||
const loaded = initial.session;
|
||||
if (!loaded) {
|
||||
return {
|
||||
setupRegistered,
|
||||
defaultKdfIterations,
|
||||
jwtWarning: null,
|
||||
session: null,
|
||||
profile: null,
|
||||
phase: setupRegistered ? 'login' : 'register',
|
||||
phase: initial.phase,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -124,7 +150,6 @@ export async function bootstrapAppSession(): Promise<BootstrapAppResult> {
|
||||
)
|
||||
);
|
||||
return {
|
||||
setupRegistered,
|
||||
defaultKdfIterations,
|
||||
jwtWarning: null,
|
||||
session,
|
||||
@@ -133,12 +158,11 @@ export async function bootstrapAppSession(): Promise<BootstrapAppResult> {
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
setupRegistered,
|
||||
defaultKdfIterations,
|
||||
jwtWarning: null,
|
||||
session: null,
|
||||
profile: null,
|
||||
phase: setupRegistered ? 'login' : 'register',
|
||||
phase: initial.phase === 'register' ? 'register' : 'login',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+22
-4
@@ -374,11 +374,20 @@ const messages: Record<Locale, Record<string, string>> = {
|
||||
txt_jwt_how_to_fix_add: "How to add JWT_SECRET",
|
||||
txt_jwt_how_to_fix_replace: "How to replace JWT_SECRET",
|
||||
txt_jwt_add_step_1: "Use the 32-character generator below and copy a new key.",
|
||||
txt_jwt_add_step_2: "Cloudflare Dashboard -> Workers & Pages -> Your Service -> Settings -> Variables and Secrets, add JWT_SECRET.",
|
||||
txt_jwt_add_step_2_prefix: "Go to Cloudflare Dashboard -> Workers & Pages -> Your Service -> ",
|
||||
txt_jwt_add_step_2_suffix: " -> Variables and Secrets -> Add",
|
||||
txt_jwt_add_step_3: "Save and wait for redeploy, then refresh this page.",
|
||||
txt_jwt_replace_step_1: "Use the 32-character generator below and create a stronger key (minimum {min} characters).",
|
||||
txt_jwt_replace_step_2: "Cloudflare Dashboard -> Workers & Pages -> Your Service -> Settings -> Variables and Secrets, replace JWT_SECRET.",
|
||||
txt_jwt_replace_step_2_prefix: "Go to Cloudflare Dashboard -> Workers & Pages -> Your Service -> ",
|
||||
txt_jwt_replace_step_2_suffix: " -> Variables and Secrets -> Update JWT_SECRET",
|
||||
txt_jwt_replace_step_3: "Save and wait for redeploy, then refresh this page.",
|
||||
txt_jwt_secret_type_label: "Type:",
|
||||
txt_jwt_secret_type_value: "Secret",
|
||||
txt_jwt_secret_name_label: "Variable name:",
|
||||
txt_jwt_secret_value_label: "Value:",
|
||||
txt_jwt_secret_value_requirement: "Random string with at least {min} characters",
|
||||
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_how_to_fix: "How to fix",
|
||||
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.",
|
||||
@@ -1147,11 +1156,20 @@ const zhCNOverrides: Record<string, string> = {
|
||||
txt_jwt_how_to_fix_add: '处理步骤(添加 JWT_SECRET)',
|
||||
txt_jwt_how_to_fix_replace: '处理步骤(更换 JWT_SECRET)',
|
||||
txt_jwt_add_step_1: '使用下方 32 位随机生成器,复制一个新密钥。',
|
||||
txt_jwt_add_step_2: '到 Cloudflare 控制台 -> Workers 和 Pages -> 你的服务 -> 设置 -> 变量和机密,新增 JWT_SECRET。',
|
||||
txt_jwt_add_step_2_prefix: '到 Cloudflare 控制台 -> Workers 和 Pages -> 你的服务 -> ',
|
||||
txt_jwt_add_step_2_suffix: ' -> 变量和机密 -> 新增',
|
||||
txt_jwt_add_step_3: '保存并等待重新部署完成,然后刷新本页确认。',
|
||||
txt_jwt_replace_step_1: '使用下方 32 位随机生成器,生成更强的密钥(至少 {min} 位)。',
|
||||
txt_jwt_replace_step_2: '到 Cloudflare 控制台 -> Workers 和 Pages -> 你的服务 -> 设置 -> 变量和机密,替换 JWT_SECRET。',
|
||||
txt_jwt_replace_step_2_prefix: '到 Cloudflare 控制台 -> Workers 和 Pages -> 你的服务 -> ',
|
||||
txt_jwt_replace_step_2_suffix: ' -> 变量和机密 -> 更新 JWT_SECRET',
|
||||
txt_jwt_replace_step_3: '保存并等待重新部署完成,然后刷新本页确认。',
|
||||
txt_jwt_secret_type_label: '类型:',
|
||||
txt_jwt_secret_type_value: '密钥',
|
||||
txt_jwt_secret_name_label: '变量名称:',
|
||||
txt_jwt_secret_value_label: '值:',
|
||||
txt_jwt_secret_value_requirement: '最低 {min} 位随机字符',
|
||||
txt_jwt_what_is: 'JWT 是什么',
|
||||
txt_jwt_what_is_body: 'JWT_SECRET 是服务端用来签发和校验登录令牌的密钥。如果它缺失、过短,或者仍然使用示例值,实例就不能安全地正常使用。',
|
||||
txt_how_to_fix: '处理步骤(添加 / 更换)',
|
||||
txt_jwt_fix_step_1: '你可以继续下一步,不影响使用。',
|
||||
txt_jwt_fix_step_2: '如果当前密钥不是强随机值,建议使用下方 32 位生成器。',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type AppPhase = 'loading' | 'register' | 'login' | 'locked' | 'app';
|
||||
export type AppPhase = 'register' | 'login' | 'locked' | 'app';
|
||||
|
||||
export interface SessionState {
|
||||
accessToken: string;
|
||||
@@ -256,17 +256,10 @@ export interface ListResponse<T> {
|
||||
data: T[];
|
||||
}
|
||||
|
||||
export interface SetupStatusResponse {
|
||||
registered: boolean;
|
||||
}
|
||||
|
||||
export interface WebConfigResponse {
|
||||
export interface WebBootstrapResponse {
|
||||
defaultKdfIterations?: number;
|
||||
jwtUnsafeReason?: 'missing' | 'default' | 'too_short' | null;
|
||||
jwtSecretMinLength?: number;
|
||||
_icon_service_url?: string;
|
||||
_icon_service_csp?: string;
|
||||
iconServiceUrl?: string;
|
||||
}
|
||||
|
||||
export interface TokenSuccess {
|
||||
|
||||
Reference in New Issue
Block a user