Publish menus from the browser instead of scp

The admin signs in with the shared password, creates a Menu Set, drops photos
into it (shrunk in the browser first), reorders Pages by dragging or with
up/down buttons, previews the Draft exactly as customers would see it, and
publishes it over the previous set. Restore and delete close the loop. Mutations
need a session, a same-origin request and a CSRF token derived from the session;
a Draft's media is served only to a session holder.
This commit is contained in:
tanshu committed 2026-09-22 21:22:26 +05:30
1 parent 9cb4021229
commit 58ecfa0dc3
14 files changed
+1824 -15

No files matched your search

+22 -1
View File
@@ -8,14 +8,17 @@ import { readFile } from 'node:fs/promises';
import path from 'node:path';
import Fastify, { type FastifyError, type FastifyInstance } from 'fastify';
import cookie from '@fastify/cookie';
import formbody from '@fastify/formbody';
import multipart from '@fastify/multipart';
import fastifyStatic from '@fastify/static';
import { parseSiteConfig, type SiteConfig } from './config.ts';
import { SESSION_COOKIE, sessionTtl } from './auth.ts';
import { SESSION_COOKIE, sessionTtl, type LoginThrottle } from './auth.ts';
import { createImagePipeline } from './images/pipeline.ts';
import { SetStore, type ImagePipeline } from './storage/sets.ts';
import { hasFatal, validateSite } from './storage/validate.ts';
import { renderNotFound } from './render/pages.ts';
import { registerPublicRoutes } from './routes/public.ts';
import { registerAdminRoutes } from './routes/admin.ts';
import { readEnvFile } from './env.ts';
export const SESSION_TTL_MS = 8 * 60 * 60 * 1000;
@@ -48,6 +51,8 @@ export interface AppOptions {
pipeline?: ImagePipeline;
/** Test seam: defaults to the real clock. */
now?: () => Date;
/** Test seam: a shared throttle so rate-limit tests are deterministic. */
throttle?: LoginThrottle;
}
export async function buildApp(options: AppOptions): Promise<FastifyInstance> {
@@ -58,6 +63,13 @@ export async function buildApp(options: AppOptions): Promise<FastifyInstance> {
});
await app.register(cookie);
await app.register(formbody);
await app.register(multipart, {
limits: {
fileSize: options.config.limits.maxUploadBytes,
files: options.config.limits.maxPagesPerSet,
},
});
await app.register(fastifyStatic, {
root: options.assetsDir,
prefix: '/assets/',
@@ -80,6 +92,15 @@ export async function buildApp(options: AppOptions): Promise<FastifyInstance> {
sessionSecret: options.sessionSecret,
});
await registerAdminRoutes(app, {
config: options.config,
store,
sessionSecret: options.sessionSecret,
adminPasswordHash: options.adminPasswordHash,
secureCookies: options.secureCookies,
throttle: options.throttle,
});
app.setNotFoundHandler(async (_request, reply) => {
const current = await store.currentBySection();
const live = options.config.sections.filter((section) => current.has(section.slug));