server: thread auth into mountEditorApi + apply ALL goods-editor migrations

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 <noreply@anthropic.com>
This commit is contained in:
khalid@traclabs.com
2026-09-02 00:20:15 -05:00
parent 26f30ce388
commit 5c21188df1

View File

@@ -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