From 5c21188df1985eb3126d3f2c6f101c651da03ade Mon Sep 17 00:00:00 2001 From: "khalid@traclabs.com" Date: Wed, 2 Sep 2026 00:20:15 -0500 Subject: [PATCH] server: thread auth into mountEditorApi + apply ALL goods-editor migrations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the coverage-gap audit's sharpest item (H8/D-auth): mountAuth's return is captured (authGuards) and design-route ownership is passed explicitly to mountEditorApi (customerId/isAdmin off req.customer/ req.admin — matches the module defaults but survives default drift); /api/fonts/registered is admin-gated by a host-level requireAdmin registered ahead of the module route (auth.middleware can't do this: the module applies it to the whole /designs group, which would lock guests out of design persistence). mockupsDir/storageDir now threaded so design-route thumbnails get the mockup backdrop. Dev harness unchanged: without DATABASE_URL the roster stays open and ownership resolves to the guest model. Also fixes a real bug the boot test surfaced: the host hardcoded 001_designs.sql, silently skipping 002_design_family.sql — the store SELECTs design_family_id unconditionally, so every GET /api/designs 500'd ('column does not exist'). Now applies every *.sql in the module's migrations dir in filename order (all idempotent). Verified by booting both modes: auth ON → roster 403 admin_required, designs unknown-id 404 (was 500), both migrations applied on a fresh db; auth OFF → roster 200 with the font list, warning logged. NOTE: goods-editor's ./server export resolves to dist/, which was stale (built Jul 7 — predating the module's auth option, roster route, and Aug security fixes); rebuilt from the branch source. Any deploy must run the module's build, not just npm install the file: dep. Co-Authored-By: Claude Fable 5 --- server.js | 68 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 15 deletions(-) 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