diff --git a/apps/api/drizzle/website/0003_fix_id_types.sql b/apps/api/drizzle/website/0003_fix_id_types.sql new file mode 100644 index 000000000..708444a31 --- /dev/null +++ b/apps/api/drizzle/website/0003_fix_id_types.sql @@ -0,0 +1,18 @@ +-- Website module — fix id column types. +-- +-- `published_by`, `created_by`, and `space_id` were originally declared +-- as `uuid`, but Mana user ids (auth.users.id) and space ids +-- (auth.organizations.id) are Better-Auth nanoids stored as `text`. +-- Publishing failed with `invalid input syntax for type uuid`. +-- +-- Apply with: +-- psql "$DATABASE_URL" -f apps/api/drizzle/website/0003_fix_id_types.sql + +ALTER TABLE "website"."published_snapshots" + ALTER COLUMN "published_by" TYPE text; + +ALTER TABLE "website"."published_snapshots" + ALTER COLUMN "space_id" TYPE text; + +ALTER TABLE "website"."custom_domains" + ALTER COLUMN "created_by" TYPE text; diff --git a/apps/api/src/modules/website/publish.ts b/apps/api/src/modules/website/publish.ts index 477a02e2d..3e256e75e 100644 --- a/apps/api/src/modules/website/publish.ts +++ b/apps/api/src/modules/website/publish.ts @@ -70,7 +70,10 @@ routes.post('/sites/:id/publish', async (c) => { // embed the active space in JWT claims). Nullable — full membership // check lands in M6; M2 stores whatever the client declares. const spaceIdHeader = c.req.header('X-Mana-Space'); - const spaceId = spaceIdHeader && /^[0-9a-f-]{36}$/i.test(spaceIdHeader) ? spaceIdHeader : null; + // Better-Auth nanoid format: 32 URL-safe base64 chars. Keep this lax + // (no hard length) so it doesn't drift if nanoid config changes. + const spaceId = + spaceIdHeader && /^[A-Za-z0-9_-]{16,64}$/.test(spaceIdHeader) ? spaceIdHeader : null; const siteId = c.req.param('id'); if (!siteId) { @@ -177,7 +180,10 @@ routes.post('/sites/:id/publish', async (c) => { } websitePublishTotal.inc({ result: 'error' }); publishTimer(); - throw err; + console.error('[website/publish] unhandled error', err); + return errorResponse(c, err instanceof Error ? err.message : 'Publish failed', 500, { + code: 'PUBLISH_FAILED', + }); } }); diff --git a/apps/api/src/modules/website/schema.ts b/apps/api/src/modules/website/schema.ts index e23bb1983..2c1c3d0b8 100644 --- a/apps/api/src/modules/website/schema.ts +++ b/apps/api/src/modules/website/schema.ts @@ -45,15 +45,15 @@ export const publishedSnapshots = websiteSchema.table( /** True for the row served to the public. Exactly one per slug. */ isCurrent: boolean('is_current').notNull().default(false), publishedAt: timestamp('published_at', { withTimezone: true }).defaultNow().notNull(), - /** User who pressed the publish button. */ - publishedBy: uuid('published_by').notNull(), + /** User who pressed the publish button. Better-Auth nanoid, not UUID. */ + publishedBy: text('published_by').notNull(), /** * Space the site belongs to. Nullable in M2 because mana-auth * doesn't yet thread the active space into JWT claims — the * client can pass it via `X-Mana-Space`, but we don't hard-require * it until server-side membership check lands (M6). */ - spaceId: uuid('space_id'), + spaceId: text('space_id'), }, (table) => [ index('published_snapshots_site_idx').on(table.siteId, table.publishedAt), @@ -121,7 +121,8 @@ export const customDomains = websiteSchema.table( verifiedAt: timestamp('verified_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), - createdBy: uuid('created_by').notNull(), + /** Better-Auth nanoid, not UUID. */ + createdBy: text('created_by').notNull(), }, (table) => [index('custom_domains_site_idx').on(table.siteId, table.status)] // A partial unique index on (hostname) WHERE status='verified' lives