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:
68
server.js
68
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
|
||||
|
||||
Reference in New Issue
Block a user