feat: refactor setup handling and enhance asset serving with bootstrap integration

This commit is contained in:
shuaiplus
2026-03-16 23:48:08 +08:00
parent b5f8ef28cc
commit 0ba85229a9
14 changed files with 217 additions and 107 deletions
-11
View File
@@ -1,11 +0,0 @@
import { Env } from '../types';
import { StorageService } from '../services/storage';
import { jsonResponse } from '../utils/response';
// GET /setup/status
export async function handleSetupStatus(request: Request, env: Env): Promise<Response> {
void request;
const storage = new StorageService(env.DB);
const registered = (await storage.isRegistered()) || (await storage.getUserCount()) > 0;
return jsonResponse({ registered });
}
+48
View File
@@ -4,11 +4,54 @@ import { handleRequest } from './router';
import { StorageService } from './services/storage';
import { applyCors, jsonResponse } from './utils/response';
import { runScheduledBackupIfDue, seedDefaultBackupSettings } from './handlers/backup';
import { buildWebBootstrapResponse } from './router-public';
let dbInitialized = false;
let dbInitError: string | null = null;
let dbInitPromise: Promise<void> | null = null;
function isWorkerHandledPath(path: string): boolean {
return (
path.startsWith('/api/') ||
path.startsWith('/identity/') ||
path.startsWith('/icons/') ||
path.startsWith('/notifications/') ||
path.startsWith('/.well-known/') ||
path === '/config' ||
path === '/api/config' ||
path === '/api/version'
);
}
function injectBootstrapIntoHtml(html: string, env: Env): string {
const payload = JSON.stringify(buildWebBootstrapResponse(env)).replace(/</g, '\\u003c');
const script = `<script>window.__NW_BOOT__=${payload};</script>`;
if (html.includes('</head>')) {
return html.replace('</head>', `${script}</head>`);
}
return `${script}${html}`;
}
async function maybeServeAsset(request: Request, env: Env): Promise<Response | null> {
if (!env.ASSETS) return null;
if (request.method !== 'GET' && request.method !== 'HEAD') return null;
const url = new URL(request.url);
if (isWorkerHandledPath(url.pathname)) return null;
const assetResponse = await env.ASSETS.fetch(request);
const contentType = String(assetResponse.headers.get('Content-Type') || '').toLowerCase();
if (request.method === 'GET' && contentType.includes('text/html')) {
const html = await assetResponse.text();
const injected = injectBootstrapIntoHtml(html, env);
return new Response(injected, {
status: assetResponse.status,
statusText: assetResponse.statusText,
headers: assetResponse.headers,
});
}
return assetResponse;
}
async function ensureDatabaseInitialized(env: Env): Promise<void> {
if (dbInitialized) return;
@@ -35,6 +78,11 @@ async function ensureDatabaseInitialized(env: Env): Promise<void> {
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
void ctx;
const assetResponse = await maybeServeAsset(request, env);
if (assetResponse) {
return applyCors(request, assetResponse);
}
await ensureDatabaseInitialized(env);
if (dbInitError) {
// Log full error server-side, return generic message to client.
+8 -21
View File
@@ -7,7 +7,6 @@ import {
handleAccessSendFileV2,
handleDownloadSendFile,
} from './handlers/sends';
import { handleSetupStatus } from './handlers/setup';
import { handleKnownDevice } from './handlers/devices';
import { handleToken, handlePrelogin, handleRevocation } from './handlers/identity';
import {
@@ -23,6 +22,13 @@ import { jsonResponse } from './utils/response';
import type { Env } from './types';
type PublicRateLimiter = (category?: string, maxRequests?: number) => Promise<Response | null>;
type JwtUnsafeReason = 'missing' | 'default' | 'too_short' | null;
export interface WebBootstrapResponse {
defaultKdfIterations: number;
jwtUnsafeReason: JwtUnsafeReason;
jwtSecretMinLength: number;
}
function isSameOriginWriteRequest(request: Request): boolean {
const targetOrigin = new URL(request.url).origin;
@@ -111,7 +117,7 @@ async function handleWebsiteIcon(host: string): Promise<Response> {
}
}
export function buildWebConfigResponse(env: Env, origin: string) {
export function buildWebBootstrapResponse(env: Env): WebBootstrapResponse {
const secret = (env.JWT_SECRET || '').trim();
const jwtUnsafeReason =
!secret
@@ -126,9 +132,6 @@ export function buildWebConfigResponse(env: Env, origin: string) {
defaultKdfIterations: LIMITS.auth.defaultKdfIterations,
jwtUnsafeReason,
jwtSecretMinLength: LIMITS.auth.jwtSecretMinLength,
_icon_service_url: buildIconServiceTemplate(origin),
_icon_service_csp: buildIconServiceCsp(origin),
iconServiceUrl: buildIconServiceTemplate(origin),
};
}
@@ -139,18 +142,6 @@ export async function handlePublicRoute(
method: string,
enforcePublicRateLimit: PublicRateLimiter
): Promise<Response | null> {
if (path === '/setup/status' && method === 'GET') {
const blocked = await enforcePublicRateLimit('public-read', LIMITS.rateLimit.publicReadRequestsPerMinute);
if (blocked) return blocked;
return handleSetupStatus(request, env);
}
if (path === '/api/web/config' && method === 'GET') {
const blocked = await enforcePublicRateLimit('public-read', LIMITS.rateLimit.publicReadRequestsPerMinute);
if (blocked) return blocked;
return jsonResponse(buildWebConfigResponse(env, new URL(request.url).origin));
}
if (path === '/.well-known/appspecific/com.chrome.devtools.json' && method === 'GET') {
return new Response('{}', {
status: 200,
@@ -161,10 +152,6 @@ export async function handlePublicRoute(
});
}
if ((path === '/favicon.ico' || path === '/favicon.svg') && method === 'GET') {
return handleNwFavicon();
}
const iconMatch = path.match(/^\/icons\/([^/]+)\/icon\.png$/i);
if (iconMatch && method === 'GET') {
return handleWebsiteIcon(iconMatch[1]);
+3
View File
@@ -2,6 +2,9 @@
export interface Env {
DB: D1Database;
NOTIFICATIONS_HUB: DurableObjectNamespace;
ASSETS?: {
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
};
// Prefer R2 when available. Optional to support KV-only deployments.
ATTACHMENTS?: R2Bucket;
// Optional fallback for attachment/send file storage (no credit card required).