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:
@@ -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 });
|
||||
}
|
||||
@@ -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
@@ -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]);
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user