diff --git a/server.js b/server.js index e334779..0113f22 100644 --- a/server.js +++ b/server.js @@ -247,11 +247,14 @@ if (facetConfig.key) { // can still iterate on goods-editor / template-manager pieces without // provisioning Postgres locally. let pgPool = null; +// mountAuth's returned guards (requireAdmin etc.) — captured so they can +// be threaded into other module mounts below. Null when auth is disabled. +let authGuards = null; if (process.env.DATABASE_URL) { pgPool = new Pool({ connectionString: process.env.DATABASE_URL }); try { await runMigrations(pgPool, { logger }); - mountAuth(app, { + authGuards = mountAuth(app, { db: pgPool, apiBase: '/api', callbackBaseUrl: process.env.PUBLIC_BASE_URL || `http://localhost:${PORT}`, @@ -279,32 +282,39 @@ if (process.env.DATABASE_URL) { logger.error({ err: err.message }, 'auth-manager bootstrap failed; auth disabled'); try { await pgPool.end(); } catch { /* ignore */ } pgPool = null; + authGuards = null; } } else { logger.warn('DATABASE_URL not set — auth-manager disabled. Set DATABASE_URL in .env to enable customer accounts and admin SSO.'); } -// ── goods-editor designs migration (#23) ────────────────────────── +// ── goods-editor designs migrations (#23) ────────────────────────── // The module documents that the HOST applies its designs schema before -// passing `db` to mountEditorApi (001_designs.sql is idempotent — -// CREATE TABLE IF NOT EXISTS + CREATE OR REPLACE). With the file: dep -// the SQL is reachable through node_modules; a packed install ships -// dist/ only, so a missing file logs a warning instead of crashing — -// the /designs routes still mount and fail loudly until the table is -// created by hand. +// passing `db` to mountEditorApi. Every file in its migrations/ dir is +// idempotent (CREATE TABLE/ADD COLUMN IF NOT EXISTS), so we apply ALL +// of them in filename order — hardcoding 001 silently skipped +// 002_design_family.sql when the module grew it (the store SELECTs +// design_family_id unconditionally → every GET /api/designs 500'd). +// With the file: dep the SQL is reachable through node_modules; a +// packed install ships dist/ only, so a missing dir logs a warning +// instead of crashing — the /designs routes still mount and fail +// loudly until the schema is applied by hand. if (pgPool) { - const designsMigrationPath = join( - __dirname, 'node_modules', 'goods-editor', 'src', 'server', 'migrations', '001_designs.sql', + const designsMigrationsDir = join( + __dirname, 'node_modules', 'goods-editor', 'src', 'server', 'migrations', ); try { - if (existsSync(designsMigrationPath)) { - await pgPool.query(readFileSync(designsMigrationPath, 'utf8')); - logger.info('goods-editor designs migration applied'); + if (existsSync(designsMigrationsDir)) { + const files = readdirSync(designsMigrationsDir).filter((f) => f.endsWith('.sql')).sort(); + for (const file of files) { + await pgPool.query(readFileSync(join(designsMigrationsDir, file), 'utf8')); + } + logger.info({ files }, 'goods-editor designs migrations applied'); } else { - logger.warn({ path: designsMigrationPath }, 'goods-editor designs migration not found — apply 001_designs.sql manually before using design persistence'); + logger.warn({ path: designsMigrationsDir }, 'goods-editor migrations dir not found — apply its *.sql manually before using design persistence'); } } catch (err) { - logger.error({ err: err.message }, 'goods-editor designs migration failed — POST /api/designs will error until the designs table exists'); + logger.error({ err: err.message }, 'goods-editor designs migrations failed — POST /api/designs will error until the schema is applied'); } } @@ -416,6 +426,18 @@ app.post('/api/upload', uploadLimiter, upload.single('image'), async (req, res) // the auth bootstrap failed), in which case the routes simply don't // mount and the client's persistCurrentDesign 404s into its graceful // cart-without-design_id fallback. +// +// Font-registration roster: ops-diagnosis data, admin-only when auth is +// live. Registered BEFORE mountEditorApi so this guard runs ahead of the +// module's own GET /api/fonts/registered handler (non-admins 403, admins +// fall through). We deliberately do NOT pass auth.middleware into +// mountEditorApi for this — the module applies that same middleware to +// the whole /designs group, and a requireAdmin there would lock guests +// out of design persistence (guest rows are a designed-in feature). +if (authGuards) { + app.use('/api/fonts/registered', authGuards.requireAdmin); +} + mountEditorApi(app, { canvas, sharp, @@ -423,9 +445,25 @@ mountEditorApi(app, { fontsDir, stickersDir, uploadsDir, + // Coverage-gap audit items: the design routes' thumbnail generator + // wants the mockup/template roots (previously only passed to + // renderDesignComposite for marketing previews, not here). + mockupsDir: templatesStorageDir, + storageDir: templatesStorageDir, basePath: '/api', logger, db: pgPool, + // Design-route ownership (coverage-gap audit, sharpest item): owned + // rows 404 for non-owners, admins bypass. These match the module's + // documented defaults but are passed explicitly so the enforcement is + // visible here and survives any future change to those defaults. + // req.customer / req.admin are populated by auth-manager's + // authMiddleware, which mountAuth app.use()s ABOVE this mount; when + // auth is disabled both resolve null/false = the open dev harness. + auth: { + customerId: (req) => req.customer?.id ?? null, + isAdmin: (req) => !!req.admin, + }, }); // Template store (admin + public). adminAuth pass-through for v1