feat(12-01): schema nullable oidc identity + claimed marker + 0002 migration
- Remove .notNull() from users.oidc_iss and users.oidc_sub (wizard creates local rows before OIDC identity is known; first-login-claims binds later) - Add users.claimed boolean (default false NOT NULL) to distinguish pending wizard rows from OIDC-bound rows (D-07) - Add Phase 12 app_config key documentation + prohibition comment (D-01/SC-3) - Generate migration 0002_lethal_millenium_guard.sql via drizzle-kit generate (MODIFY COLUMN for nullable, ADD COLUMN claimed — no DROP/recreate) - Append backfill: UPDATE users SET claimed=true WHERE oidc_iss IS NOT NULL so existing OIDC users cannot be matched by first-login-claims (D-08) - Apply migration via drizzle-kit migrate — users.claimed column verified in dev DB
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
ALTER TABLE `users` MODIFY COLUMN `oidc_iss` varchar(512);--> statement-breakpoint
|
||||
ALTER TABLE `users` MODIFY COLUMN `oidc_sub` varchar(256);--> statement-breakpoint
|
||||
ALTER TABLE `users` ADD `claimed` boolean DEFAULT false NOT NULL;
|
||||
--> statement-breakpoint
|
||||
-- Phase 12 backfill (D-07): existing OIDC users are effectively claimed —
|
||||
-- prevents first-login-claims (D-08) from matching rows that already have an identity bound.
|
||||
UPDATE `users` SET `claimed` = true WHERE `oidc_iss` IS NOT NULL;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,13 @@
|
||||
"when": 1781374816375,
|
||||
"tag": "0001_famous_mad_thinker",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "5",
|
||||
"when": 1781545048917,
|
||||
"tag": "0002_lethal_millenium_guard",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -31,21 +31,35 @@ 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.
|
||||
*
|
||||
* Phase 12 additions (D-07):
|
||||
* oidcIss / oidcSub: now nullable — wizard creates a local user row before OIDC identity
|
||||
* is known; first-login-claims (D-08) binds them on first OIDC login.
|
||||
* claimed: false = pending wizard user (no OIDC identity bound yet);
|
||||
* true = identity already bound (existing OIDC users backfilled via 0002 migration).
|
||||
*
|
||||
* MariaDB null semantics: multiple NULL+NULL pairs are allowed in a unique index
|
||||
* (NULLs are DISTINCT per ISO SQL / MariaDB), so the uniq_oidc_identity constraint
|
||||
* correctly permits multiple unclaimed rows (D-07, RESEARCH Pitfall 9).
|
||||
*/
|
||||
export const users = mysqlTable(
|
||||
'users',
|
||||
{
|
||||
id: int().primaryKey().autoincrement(),
|
||||
oidcIss: varchar('oidc_iss', { length: 512 }).notNull(),
|
||||
oidcSub: varchar('oidc_sub', { length: 256 }).notNull(),
|
||||
// Phase 12: nullable — set by first-login-claims (D-08) after wizard completes
|
||||
oidcIss: varchar('oidc_iss', { length: 512 }),
|
||||
oidcSub: varchar('oidc_sub', { length: 256 }),
|
||||
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(),
|
||||
// Phase 12: claimed=false → unclaimed wizard row; claimed=true → OIDC identity bound (D-07)
|
||||
claimed: boolean('claimed').default(false).notNull(),
|
||||
},
|
||||
(t) => [
|
||||
// Composite unique key — identity is iss+sub, never email (D-10)
|
||||
// Composite unique key — identity is iss+sub, never email (D-10).
|
||||
// NULL+NULL pairs are DISTINCT in MariaDB unique indexes → multiple unclaimed rows allowed.
|
||||
unique('uniq_oidc_identity').on(t.oidcIss, t.oidcSub),
|
||||
],
|
||||
);
|
||||
@@ -277,6 +291,16 @@ export const pushSubscriptions = mysqlTable(
|
||||
* 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).
|
||||
*
|
||||
* Phase 12 additional keys (written by the wizard, never by this schema):
|
||||
* - 'oidc_issuer' OIDC issuer URL configured by the operator (e.g. Authelia base URL)
|
||||
* - 'oidc_client_id' OIDC client_id registered in Authelia
|
||||
* - 'vapid_public_key' VAPID public key (base64url) for Web Push — safe to store here
|
||||
* - 'app_external_url' External URL of the PWA (used in push payloads, OIDC redirect URI)
|
||||
*
|
||||
* PROHIBITION (D-01 / SC-3): NEVER add columns or keys for:
|
||||
* - 'vapid_private_key' — injected via docker-compose.yml env only; never persisted
|
||||
* - 'app_password_encryption_key' — injected via docker-compose.yml env only; never persisted
|
||||
*
|
||||
* Do NOT add setup_complete gating logic here — Phase 12 owns that.
|
||||
*/
|
||||
export const appConfig = mysqlTable('app_config', {
|
||||
|
||||
Reference in New Issue
Block a user