feat(19-01): add local_credentials schema, 0003 migration, generate-secrets LOCAL_SESSION_SECRET, .dockerignore D-15
- schema.ts: export localCredentials = mysqlTable('local_credentials', {...})
- user_id FK->users(cascade), username, password_hash, createdAt, updatedAt
- UNIQUE(user_id), UNIQUE(username), INDEX(user_id)
- 0003_warm_deathstrike.sql: purely additive CREATE TABLE (no ALTER/DROP/TRUNCATE on existing tables)
- Applied to dev DB: pnpm --filter @familysync/api db:migrate exits 0
- test/setup.ts: add localCredentials to afterEach delete cleanup (FK-safe ordering)
- generate-secrets.mjs: emit LOCAL_SESSION_SECRET (base64 32-byte, >=32 chars, D-05)
- .dockerignore: add apps/api/scripts/ exclusion (D-15/IMG-02) — entire break-glass dir excluded
This commit is contained in:
@@ -309,6 +309,47 @@ export const appConfig = mysqlTable('app_config', {
|
||||
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Local authentication credentials per member (Phase 19 — D-09).
|
||||
*
|
||||
* Stores username + PHC-encoded scrypt password hash for members who authenticate
|
||||
* via local username/password rather than (or before) OIDC.
|
||||
*
|
||||
* Design decisions:
|
||||
* - Separate table from `users` to keep the users row identity-method-agnostic (D-09).
|
||||
* - A user has a local login iff a `local_credentials` row exists (UNIQUE on user_id).
|
||||
* - OIDC-link flow (D-12): when a local user links OIDC, their `local_credentials`
|
||||
* row is deleted — they become OIDC-only.
|
||||
* - CASCADE DELETE on users.id keeps credentials clean when a member is removed.
|
||||
* - username is globally unique (login identifier, separate from displayName).
|
||||
* - password_hash is PHC-encoded: scrypt$N$r$p$<salt_b64url>$<hash_b64url> (varchar 256).
|
||||
*
|
||||
* PROHIBITION: LOCAL_SESSION_SECRET (the signing key for this table's sessions) is an
|
||||
* env-only secret and must NEVER be stored in this table or app_config (SC-3).
|
||||
*/
|
||||
export const localCredentials = mysqlTable(
|
||||
'local_credentials',
|
||||
{
|
||||
id: int().primaryKey().autoincrement(),
|
||||
userId: int('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
username: varchar('username', { length: 128 }).notNull(),
|
||||
// PHC-encoded: scrypt$N$r$p$<salt_base64url>$<hash_base64url> — max ~83 chars
|
||||
passwordHash: varchar('password_hash', { length: 256 }).notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
|
||||
},
|
||||
(t) => [
|
||||
// One local credential per user — user_id is unique (D-09: auth method is per-user property)
|
||||
unique('uniq_local_cred_user').on(t.userId),
|
||||
// Username is globally unique (login identifier; case-sensitive per MariaDB default)
|
||||
unique('uniq_local_cred_username').on(t.username),
|
||||
// Index for fast lookup by user_id (e.g., on middleware / self-change-password)
|
||||
index('idx_local_credentials_user_id').on(t.userId),
|
||||
],
|
||||
);
|
||||
|
||||
/**
|
||||
* Items within a list.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user