diff --git a/apps/api/src/db/schema.ts b/apps/api/src/db/schema.ts index 5d8f625..dd97b76 100644 --- a/apps/api/src/db/schema.ts +++ b/apps/api/src/db/schema.ts @@ -30,6 +30,7 @@ const varcharBin = (name: string) => /** * Members of the household — identity keyed by oidc_iss + oidc_sub (never email, per D-10). * Color auto-assigned from palette on first login (D-06). + * isAdmin: first-login-wins bootstrap (D-01); gated by app_config.setup_complete in Phase 12. */ export const users = mysqlTable( 'users', @@ -40,6 +41,8 @@ export const users = mysqlTable( displayName: varchar('display_name', { length: 256 }), color: varchar('color', { length: 7 }).notNull(), // hex e.g. '#4A90D9' createdAt: timestamp('created_at').defaultNow().notNull(), + // v1.1 (Phase 10): admin role flag — first-login-wins; Phase 12 tightens bootstrap + isAdmin: boolean('is_admin').default(false).notNull(), }, (t) => [ // Composite unique key — identity is iss+sub, never email (D-10) @@ -48,9 +51,11 @@ export const users = mysqlTable( ); /** - * Encrypted Fastmail app-password credentials per member (D-04). + * Encrypted provider credentials per member (D-04 generic provider shape, Fastmail/CalDAV first). * Keyed by user_id (oidc_sub → user row). Backend-only, never exposed to frontend. * Stored as JSON: { iv, authTag, ciphertext } (AES-256-GCM). + * providerType: generic discriminator (D-04); 'caldav' is the only implemented provider. + * UNIQUE(user_id) enforces one-credential-per-member (D-05) and enables onDuplicateKeyUpdate upsert. */ export const memberCredentials = mysqlTable( 'member_credentials', @@ -64,8 +69,14 @@ export const memberCredentials = mysqlTable( fastmailEmail: varchar('fastmail_email', { length: 256 }).notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(), + // v1.1 (Phase 10, D-04): generic provider discriminator; 'caldav' default for existing rows + providerType: varchar('provider_type', { length: 64 }).notNull().default('caldav'), }, - (t) => [index('idx_member_credentials_user_id').on(t.userId)], + (t) => [ + index('idx_member_credentials_user_id').on(t.userId), + // v1.1 (Phase 10, D-05): one credential per member; enables Drizzle onDuplicateKeyUpdate upsert + unique('uniq_member_credential_user').on(t.userId), + ], ); /** @@ -129,6 +140,8 @@ export const calendarEvents = mysqlTable( // Events with hasRrule=true have dtstartUtc potentially years before any window, // so the windowed query must include them regardless of dtstartUtc range (see RESEARCH.md §Pitfall 5). hasRrule: boolean('has_rrule').default(false).notNull(), + // v1.1 (Phase 10): reminder lead time in minutes; nullable — consumed by Phase 11 + reminderLeadMinutes: int('reminder_lead_minutes'), updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(), }, (t) => [ @@ -256,6 +269,22 @@ export const pushSubscriptions = mysqlTable( ], ); +/** + * Global app configuration — simple key/value store. + * + * v1.1 (Phase 10): ships the table; consumed by downstream phases: + * - setup_complete (boolean string 'true'/'false'): Phase 12 setup wizard writes this key + * after first-time setup; Phase 10's first-login-wins admin bootstrap reads it in Phase 12. + * Key: 'setup_complete', Value: 'true' | 'false' | null (not yet set → treated as false). + * + * Do NOT add setup_complete gating logic here — Phase 12 owns that. + */ +export const appConfig = mysqlTable('app_config', { + key: varchar('key', { length: 128 }).primaryKey(), + value: text('value'), // nullable + updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(), +}); + /** * Items within a list. *