import "server-only"; import { readFile, rename, writeFile } from "node:fs/promises"; import { join } from "node:path"; import type { AccountMigration, AccountStatus, PlatformOrganization, PlatformRole, PlatformUserRecord, OrganizationStatus } from "@/lib/types"; import { hashLocalPassword, verifyLocalPassword } from "@/lib/server/auth/password"; import { createId } from "@/lib/server/ids"; import { isPostgresBackend, queryDatabase, withDatabaseTransaction } from "@/lib/server/database"; import { reassignOwnerData } from "@/lib/server/data-store"; import { dataDir, DEFAULT_OWNER_ID, ensureRuntimeDirs } from "@/lib/server/runtime"; const STORE_FILE = "platform-accounts.json"; const MAX_LOGIN_FAILURES = 5; const LOCK_DURATION_MS = 15 * 60 * 1000; let localWriteQueue: Promise = Promise.resolve(); type AccountState = { users: PlatformUserRecord[]; organizations: PlatformOrganization[]; migrations: AccountMigration[]; }; export type PlatformUserFilters = { organizationId?: string; role?: PlatformRole; includeDisabled?: boolean; }; export type CreatePlatformUserInput = { phone: string; displayName: string; password: string; role: PlatformRole; organizationId?: string; legacySubject?: string; }; export type UpdatePlatformUserInput = Partial> & { password?: string; clearLoginLock?: boolean; }; export class AccountStoreError extends Error { status: number; constructor(message: string, status = 400) { super(message); this.name = "AccountStoreError"; this.status = status; } } export class AccountLoginError extends AccountStoreError { constructor(message = "手机号或密码错误。", status = 401) { super(message, status); this.name = "AccountLoginError"; } } export function normalizePhone(value: string): string { return value.trim().replace(/[\s()-]/g, ""); } export function isValidPhone(value: string): boolean { return /^\+?[0-9]{6,20}$/.test(normalizePhone(value)); } export function platformAccountStoreConfigured(): boolean { return (isPostgresBackend() ? Boolean(process.env.DATABASE_URL?.trim()) : Boolean(process.env.ZHINIAN_AUTH_SESSION_SECRET)); } export async function listPlatformOrganizations(options: { includeDisabled?: boolean } = {}): Promise { if (isPostgresBackend()) { const result = await queryDatabase>( `SELECT * FROM platform_organizations ${options.includeDisabled ? "" : "WHERE status = $1"} ORDER BY created_at ASC`, options.includeDisabled ? [] : ["active"] ); return result.rows.map(organizationFromRow); } const state = await readLocalState(); return state.organizations .filter((organization) => options.includeDisabled || organization.status === "active") .sort((left, right) => left.createdAt.localeCompare(right.createdAt)); } export async function getPlatformOrganization(id: string): Promise { if (isPostgresBackend()) { const result = await queryDatabase>("SELECT * FROM platform_organizations WHERE id = $1", [id]); return result.rows[0] ? organizationFromRow(result.rows[0]) : null; } const state = await readLocalState(); return state.organizations.find((organization) => organization.id === id) || null; } export async function createPlatformOrganization(name: string): Promise { const normalizedName = name.trim(); if (!normalizedName) throw new AccountStoreError("组织名称不能为空。", 400); const now = new Date().toISOString(); const id = createId("org"); const organization: PlatformOrganization = { id, name: normalizedName, status: "active", archiveOwnerId: `archive:${id}`, createdAt: now, updatedAt: now }; if (isPostgresBackend()) { try { const result = await queryDatabase>( "INSERT INTO platform_organizations (id, name, status, archive_owner_id, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *", [organization.id, organization.name, organization.status, organization.archiveOwnerId, organization.createdAt, organization.updatedAt] ); return organizationFromRow(result.rows[0]); } catch (error) { throw databaseError(error); } } return mutateLocalState((state) => { if (state.organizations.some((item) => item.name === normalizedName)) throw new AccountStoreError("组织名称已存在。", 409); state.organizations.push(organization); return organization; }); } export async function updatePlatformOrganization(id: string, patch: { name?: string; status?: OrganizationStatus }): Promise { const nextPatch: Record = {}; if (patch.name !== undefined) { const name = patch.name.trim(); if (!name) throw new AccountStoreError("组织名称不能为空。", 400); nextPatch.name = name; } if (patch.status !== undefined) nextPatch.status = patch.status; if (isPostgresBackend()) { try { const result = await queryDatabase>( "UPDATE platform_organizations SET name = COALESCE($2, name), status = COALESCE($3, status), updated_at = $4 WHERE id = $1 RETURNING *", [id, nextPatch.name ?? null, nextPatch.status ?? null, new Date().toISOString()] ); if (!result.rows[0]) throw new AccountStoreError("Organization not found", 404); return organizationFromRow(result.rows[0]); } catch (error) { if (error instanceof AccountStoreError) throw error; throw databaseError(error); } } return mutateLocalState((state) => { const organization = state.organizations.find((item) => item.id === id); if (!organization) throw new AccountStoreError("组织不存在。", 404); if (patch.name !== undefined && state.organizations.some((item) => item.id !== id && item.name === patch.name?.trim())) { throw new AccountStoreError("组织名称已存在。", 409); } if (patch.name !== undefined) organization.name = patch.name.trim(); if (patch.status !== undefined) organization.status = patch.status; organization.updatedAt = new Date().toISOString(); return organization; }); } export async function deletePlatformOrganization(id: string): Promise { const users = await listPlatformUsers({ organizationId: id, includeDisabled: true }); if (users.length) throw new AccountStoreError("组织仍有账号,不能删除。", 409); if (isPostgresBackend()) { await queryDatabase("DELETE FROM platform_organizations WHERE id = $1", [id]); return; } await mutateLocalState((state) => { state.organizations = state.organizations.filter((organization) => organization.id !== id); }); } export async function listPlatformUsers(filters: PlatformUserFilters = {}): Promise { if (isPostgresBackend()) { const clauses: string[] = []; const values: unknown[] = []; if (filters.organizationId) clauses.push(`organization_id = $${values.push(filters.organizationId)}`); if (filters.role) clauses.push(`role = $${values.push(filters.role)}`); if (!filters.includeDisabled) clauses.push(`status = $${values.push("active")}`); const result = await queryDatabase>( `SELECT * FROM platform_users ${clauses.length ? `WHERE ${clauses.join(" AND ")}` : ""} ORDER BY created_at DESC`, values ); return result.rows.map(userFromRow); } const state = await readLocalState(); return state.users .filter((user) => !filters.organizationId || user.organizationId === filters.organizationId) .filter((user) => !filters.role || user.role === filters.role) .filter((user) => filters.includeDisabled || user.status === "active") .sort((left, right) => right.createdAt.localeCompare(left.createdAt)); } export async function getPlatformUserById(id: string, options: { includeDisabled?: boolean } = {}): Promise { if (isPostgresBackend()) { const result = await queryDatabase>( `SELECT * FROM platform_users WHERE id = $1 ${options.includeDisabled ? "" : "AND status = $2"}`, options.includeDisabled ? [id] : [id, "active"] ); return result.rows[0] ? userFromRow(result.rows[0]) : null; } const state = await readLocalState(); const user = state.users.find((item) => item.id === id) || null; if (user && !options.includeDisabled && user.status !== "active") return null; return user; } export async function findPlatformUserByPhone(phone: string, options: { includeDisabled?: boolean } = {}): Promise { const normalizedPhone = normalizePhone(phone); if (isPostgresBackend()) { const result = await queryDatabase>( `SELECT * FROM platform_users WHERE phone = $1 ${options.includeDisabled ? "" : "AND status = $2"}`, options.includeDisabled ? [normalizedPhone] : [normalizedPhone, "active"] ); return result.rows[0] ? userFromRow(result.rows[0]) : null; } const state = await readLocalState(); const user = state.users.find((item) => item.phone === normalizedPhone) || null; if (user && !options.includeDisabled && user.status !== "active") return null; return user; } export async function createPlatformUser(input: CreatePlatformUserInput): Promise { const phone = normalizePhone(input.phone); if (!isValidPhone(phone)) throw new AccountStoreError("手机号格式不正确。", 400); if (!input.displayName.trim()) throw new AccountStoreError("显示名称不能为空。", 400); if (input.password.length < 8) throw new AccountStoreError("初始密码至少需要 8 位。", 400); const organization = input.organizationId ? await getPlatformOrganization(input.organizationId) : null; if (input.organizationId && (!organization || organization.status !== "active")) { throw new AccountStoreError("账号归属的组织不存在或已停用。", 400); } if (input.role !== "super_admin" && (!organization || organization.status !== "active")) { throw new AccountStoreError("普通账号必须归属有效组织。", 400); } const existing = await findPlatformUserByPhone(phone, { includeDisabled: true }); if (existing) throw new AccountStoreError("该手机号已创建账号。", 409); const password = await hashLocalPassword(input.password); const now = new Date().toISOString(); const user: PlatformUserRecord = { id: createId("user"), phone, displayName: input.displayName.trim(), role: input.role, organizationId: input.organizationId, status: "active", passwordHash: password.hash, passwordSalt: password.salt, failedLoginCount: 0, sessionVersion: 1, legacySubject: input.legacySubject, createdAt: now, updatedAt: now }; if (isPostgresBackend()) { try { const row = userToRow(user); const result = await queryDatabase>( "INSERT INTO platform_users (id, phone, display_name, role, organization_id, status, password_hash, password_salt, failed_login_count, locked_until, session_version, last_login_at, legacy_subject, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) RETURNING *", Object.values(row) ); return userFromRow(result.rows[0]); } catch (error) { throw databaseError(error); } } return mutateLocalState((state) => { state.users.push(user); return user; }); } export async function updatePlatformUser(id: string, patch: UpdatePlatformUserInput): Promise { const current = await getPlatformUserById(id, { includeDisabled: true }); if (!current) throw new AccountStoreError("账号不存在。", 404); const nextOrganizationId = patch.organizationId !== undefined ? patch.organizationId : current.organizationId; const nextRole = patch.role || current.role; const organization = nextOrganizationId ? await getPlatformOrganization(nextOrganizationId) : null; if (nextOrganizationId && (!organization || organization.status !== "active")) { throw new AccountStoreError("账号归属的组织不存在或已停用。", 400); } if (nextRole !== "super_admin") { if (!organization || organization.status !== "active") throw new AccountStoreError("普通账号必须归属有效组织。", 400); } if (patch.password !== undefined && patch.password.length < 8) throw new AccountStoreError("新密码至少需要 8 位。", 400); const nextPassword = patch.password ? await hashLocalPassword(patch.password) : null; const next: PlatformUserRecord = { ...current, displayName: patch.displayName?.trim() || current.displayName, role: nextRole, organizationId: nextOrganizationId, status: patch.status || current.status, passwordHash: nextPassword?.hash || current.passwordHash, passwordSalt: nextPassword?.salt || current.passwordSalt, failedLoginCount: patch.clearLoginLock ? 0 : current.failedLoginCount, lockedUntil: patch.clearLoginLock ? undefined : current.lockedUntil, sessionVersion: nextPassword || patch.role || patch.organizationId !== undefined || patch.status ? current.sessionVersion + 1 : current.sessionVersion, updatedAt: new Date().toISOString() }; if (isPostgresBackend()) { try { const row = userToRow(next); const result = await queryDatabase>( "UPDATE platform_users SET phone=$2, display_name=$3, role=$4, organization_id=$5, status=$6, password_hash=$7, password_salt=$8, failed_login_count=$9, locked_until=$10, session_version=$11, last_login_at=$12, legacy_subject=$13, created_at=$14, updated_at=$15 WHERE id=$1 RETURNING *", Object.values(row) ); if (!result.rows[0]) throw new AccountStoreError("Account not found", 404); return userFromRow(result.rows[0]); } catch (error) { if (error instanceof AccountStoreError) throw error; throw databaseError(error); } } return mutateLocalState((state) => { const index = state.users.findIndex((item) => item.id === id); if (index < 0) throw new AccountStoreError("账号不存在。", 404); state.users[index] = next; return next; }); } export async function deletePlatformUser(id: string): Promise { const user = await getPlatformUserById(id, { includeDisabled: true }); if (!user) throw new AccountStoreError("账号不存在。", 404); if (user.role === "super_admin") throw new AccountStoreError("不能直接删除超级管理员账号。", 400); const organization = user.organizationId ? await getPlatformOrganization(user.organizationId) : null; const archiveOwnerId = organization?.archiveOwnerId || `archive:global`; if (isPostgresBackend()) { await withDatabaseTransaction(async (client) => { for (const table of ["assets", "generation_jobs", "projects", "image_templates"] as const) { await client.query(`UPDATE ${table} SET owner_id = $2 WHERE owner_id = $1`, [user.id, archiveOwnerId]); } const result = await client.query("DELETE FROM platform_users WHERE id = $1 RETURNING id", [id]); if (!result.rowCount) throw new AccountStoreError("Account not found", 404); }); return; } await reassignOwnerData(user.id, archiveOwnerId); await mutateLocalState((state) => { state.users = state.users.filter((item) => item.id !== id); }); } export async function authenticatePlatformUser(phone: string, password: string): Promise { if (isPostgresBackend()) { const result = await withDatabaseTransaction< { user: PlatformUserRecord; error?: never } | { user?: never; error: AccountLoginError } >(async (client) => { const result = await client.query>( "SELECT * FROM platform_users WHERE phone = $1 FOR UPDATE", [normalizePhone(phone)] ); const user = result.rows[0] ? userFromRow(result.rows[0]) : null; if (!user) throw new AccountLoginError(); if (user.status !== "active") throw new AccountLoginError("Account is disabled.", 403); if (user.role !== "super_admin" && user.organizationId) { const organizationResult = await client.query>( "SELECT * FROM platform_organizations WHERE id = $1", [user.organizationId] ); const organization = organizationResult.rows[0] ? organizationFromRow(organizationResult.rows[0]) : null; if (!organization || organization.status !== "active") { throw new AccountLoginError("The account organization is disabled.", 403); } } if (user.lockedUntil && user.lockedUntil > new Date().toISOString()) { throw new AccountLoginError("Too many failed login attempts. Try again in 15 minutes.", 423); } const valid = await verifyLocalPassword(password, user.passwordHash, user.passwordSalt); const now = new Date().toISOString(); if (!valid) { const failedLoginCount = user.failedLoginCount + 1; const lockedUntil = failedLoginCount >= MAX_LOGIN_FAILURES ? new Date(Date.now() + LOCK_DURATION_MS).toISOString() : null; await client.query( "UPDATE platform_users SET failed_login_count=$2, locked_until=$3, updated_at=$4 WHERE id=$1", [user.id, lockedUntil ? 0 : failedLoginCount, lockedUntil, now] ); return { error: lockedUntil ? new AccountLoginError("Too many failed login attempts. Try again in 15 minutes.", 423) : new AccountLoginError() }; } const updated = await client.query>( "UPDATE platform_users SET failed_login_count=0, locked_until=NULL, last_login_at=$2, updated_at=$2 WHERE id=$1 RETURNING *", [user.id, now] ); if (!updated.rows[0]) throw new AccountLoginError(); return { user: userFromRow(updated.rows[0]) }; }); if (result.error) throw result.error; return result.user; } const user = await findPlatformUserByPhone(phone, { includeDisabled: true }); if (!user) throw new AccountLoginError(); if (user.status !== "active") throw new AccountLoginError("账号已停用,请联系管理员。", 403); if (user.role !== "super_admin" && user.organizationId) { const organization = await getPlatformOrganization(user.organizationId); if (!organization || organization.status !== "active") throw new AccountLoginError("所属组织已停用,请联系管理员。", 403); } if (user.lockedUntil && user.lockedUntil > new Date().toISOString()) { throw new AccountLoginError("登录失败次数过多,请 15 分钟后再试。", 423); } const valid = await verifyLocalPassword(password, user.passwordHash, user.passwordSalt); if (!valid) { const failedLoginCount = user.failedLoginCount + 1; const lockedUntil = failedLoginCount >= MAX_LOGIN_FAILURES ? new Date(Date.now() + LOCK_DURATION_MS).toISOString() : undefined; await updateLoginState(user.id, { failedLoginCount: lockedUntil ? 0 : failedLoginCount, lockedUntil }); if (lockedUntil) throw new AccountLoginError("登录失败次数过多,请 15 分钟后再试。", 423); throw new AccountLoginError(); } const now = new Date().toISOString(); await updateLoginState(user.id, { failedLoginCount: 0, lockedUntil: null, lastLoginAt: now }); const refreshed = await getPlatformUserById(user.id, { includeDisabled: true }); if (!refreshed) throw new AccountLoginError(); return refreshed; } export async function changeOwnPassword(userId: string, currentPassword: string, nextPassword: string): Promise { if (isPostgresBackend()) { return withDatabaseTransaction(async (client) => { const result = await client.query>( "SELECT * FROM platform_users WHERE id = $1 FOR UPDATE", [userId] ); const user = result.rows[0] ? userFromRow(result.rows[0]) : null; if (!user || user.status !== "active") throw new AccountStoreError("Account not found or disabled.", 404); if (!await verifyLocalPassword(currentPassword, user.passwordHash, user.passwordSalt)) { throw new AccountStoreError("The current password is incorrect.", 400); } if (!nextPassword || nextPassword.length < 8) throw new AccountStoreError("The new password must be at least 8 characters.", 400); const password = await hashLocalPassword(nextPassword); const updated = await client.query>( "UPDATE platform_users SET password_hash=$2, password_salt=$3, session_version=session_version+1, updated_at=$4 WHERE id=$1 RETURNING *", [userId, password.hash, password.salt, new Date().toISOString()] ); if (!updated.rows[0]) throw new AccountStoreError("Account not found", 404); return userFromRow(updated.rows[0]); }); } const user = await getPlatformUserById(userId, { includeDisabled: true }); if (!user || user.status !== "active") throw new AccountStoreError("账号不存在或已停用。", 404); if (!await verifyLocalPassword(currentPassword, user.passwordHash, user.passwordSalt)) { throw new AccountStoreError("当前密码不正确。", 400); } if (!nextPassword || nextPassword.length < 8) throw new AccountStoreError("新密码至少需要 8 位。", 400); return updatePlatformUser(userId, { password: nextPassword }); } export async function upsertAccountMigration(input: Omit): Promise { const migration: AccountMigration = { ...input, id: createId("migration"), createdAt: new Date().toISOString() }; if (isPostgresBackend()) { const row = migrationToRow(migration); const result = await queryDatabase>( "INSERT INTO platform_account_migrations (id, legacy_owner_id, legacy_phone, platform_user_id, created_at) VALUES ($1,$2,$3,$4,$5) ON CONFLICT (legacy_owner_id) DO UPDATE SET id=EXCLUDED.id, legacy_phone=EXCLUDED.legacy_phone, platform_user_id=EXCLUDED.platform_user_id, created_at=EXCLUDED.created_at RETURNING *", Object.values(row) ); return migrationFromRow(result.rows[0]); } return mutateLocalState((state) => { const index = state.migrations.findIndex((item) => item.legacyOwnerId === input.legacyOwnerId); if (index >= 0) state.migrations[index] = migration; else state.migrations.push(migration); return migration; }); } async function updateLoginState(id: string, patch: { failedLoginCount: number; lockedUntil?: string | null; lastLoginAt?: string }) { const values = { failed_login_count: patch.failedLoginCount, locked_until: patch.lockedUntil || null, ...(patch.lastLoginAt ? { last_login_at: patch.lastLoginAt } : {}), updated_at: new Date().toISOString() }; if (isPostgresBackend()) { const result = await queryDatabase( "UPDATE platform_users SET failed_login_count=$2, locked_until=$3, last_login_at=COALESCE($4, last_login_at), updated_at=$5 WHERE id=$1 RETURNING id", [id, values.failed_login_count, values.locked_until, patch.lastLoginAt ?? null, values.updated_at] ); if (!result.rowCount) throw new AccountStoreError("Account not found", 404); return; } await mutateLocalState((state) => { const user = state.users.find((item) => item.id === id); if (!user) return; user.failedLoginCount = patch.failedLoginCount; user.lockedUntil = patch.lockedUntil || undefined; if (patch.lastLoginAt) user.lastLoginAt = patch.lastLoginAt; user.updatedAt = new Date().toISOString(); }); } async function readLocalState(): Promise { await ensureRuntimeDirs(); const path = join(dataDir(), STORE_FILE); try { return normalizeState(JSON.parse(await readFile(path, "utf8"))); } catch { const state = normalizeState({}); await writeLocalState(state); return state; } } async function writeLocalState(state: AccountState): Promise { await ensureRuntimeDirs(); const path = join(dataDir(), STORE_FILE); const temp = `${path}.${createId("tmp")}.tmp`; await writeFile(temp, JSON.stringify(state, null, 2)); await rename(temp, path); } async function mutateLocalState(mutator: (state: AccountState) => T): Promise { const run = localWriteQueue.then(async () => { const state = await readLocalState(); const result = mutator(state); await writeLocalState(state); return result; }); localWriteQueue = run.catch(() => undefined); return run; } function normalizeState(raw: Partial): AccountState { const organizations = Array.isArray(raw.organizations) ? raw.organizations.map(normalizeOrganization) : []; const users = Array.isArray(raw.users) ? raw.users.map(normalizeUser) : []; if (!users.length && !authRequiredByEnv()) { const now = new Date().toISOString(); const organization: PlatformOrganization = { id: "org-demo", name: "演示组织", status: "active", archiveOwnerId: "archive:org-demo", createdAt: now, updatedAt: now }; organizations.push(organization); users.push({ id: DEFAULT_OWNER_ID, phone: "13800000000", displayName: "智念演示用户", role: "super_admin", organizationId: organization.id, status: "active", passwordHash: "", passwordSalt: "", failedLoginCount: 0, sessionVersion: 1, createdAt: now, updatedAt: now }); } return { users, organizations, migrations: Array.isArray(raw.migrations) ? raw.migrations.map(normalizeMigration) : [] }; } function authRequiredByEnv(): boolean { const disabled = process.env.ZHINIAN_AUTH_DISABLED?.trim().toLowerCase(); if (["1", "true", "yes", "on"].includes(disabled || "")) return false; const explicit = process.env.ZHINIAN_AUTH_REQUIRED?.trim().toLowerCase(); if (["0", "false", "no", "off"].includes(explicit || "")) return false; return true; } function normalizeOrganization(value: PlatformOrganization): PlatformOrganization { const now = new Date().toISOString(); return { id: String(value.id), name: String(value.name || "未命名组织"), status: value.status === "disabled" ? "disabled" : "active", archiveOwnerId: String(value.archiveOwnerId || `archive:${value.id}`), createdAt: String(value.createdAt || now), updatedAt: String(value.updatedAt || now) }; } function normalizeUser(value: PlatformUserRecord): PlatformUserRecord { const now = new Date().toISOString(); return { id: String(value.id), phone: normalizePhone(String(value.phone || "")), displayName: String(value.displayName || "未命名用户"), role: value.role === "super_admin" || value.role === "organization_admin" ? value.role : "user", organizationId: value.organizationId ? String(value.organizationId) : undefined, status: value.status === "disabled" ? "disabled" : "active", passwordHash: String(value.passwordHash || ""), passwordSalt: String(value.passwordSalt || ""), failedLoginCount: Number(value.failedLoginCount || 0), lockedUntil: value.lockedUntil ? String(value.lockedUntil) : undefined, sessionVersion: Number(value.sessionVersion || 1), lastLoginAt: value.lastLoginAt ? String(value.lastLoginAt) : undefined, legacySubject: value.legacySubject ? String(value.legacySubject) : undefined, createdAt: String(value.createdAt || now), updatedAt: String(value.updatedAt || now) }; } function normalizeMigration(value: AccountMigration): AccountMigration { return { id: String(value.id), legacyOwnerId: String(value.legacyOwnerId), legacyPhone: value.legacyPhone ? normalizePhone(String(value.legacyPhone)) : undefined, platformUserId: String(value.platformUserId), createdAt: String(value.createdAt || new Date().toISOString()) }; } function databaseError(error: unknown): AccountStoreError { const database = error as { code?: string; message?: string }; return new AccountStoreError(database.message || "Database operation failed", database.code === "23505" ? 409 : 500); } function organizationToRow(organization: PlatformOrganization) { return { id: organization.id, name: organization.name, status: organization.status, archive_owner_id: organization.archiveOwnerId, created_at: organization.createdAt, updated_at: organization.updatedAt }; } function organizationFromRow(row: Record): PlatformOrganization { return { id: String(row.id), name: String(row.name || ""), status: row.status === "disabled" ? "disabled" : "active", archiveOwnerId: String(row.archive_owner_id || `archive:${row.id}`), createdAt: timestampFromRow(row.created_at), updatedAt: timestampFromRow(row.updated_at) }; } function userToRow(user: PlatformUserRecord) { return { id: user.id, phone: user.phone, display_name: user.displayName, role: user.role, organization_id: user.organizationId || null, status: user.status, password_hash: user.passwordHash, password_salt: user.passwordSalt, failed_login_count: user.failedLoginCount, locked_until: user.lockedUntil || null, session_version: user.sessionVersion, last_login_at: user.lastLoginAt || null, legacy_subject: user.legacySubject || null, created_at: user.createdAt, updated_at: user.updatedAt }; } function userFromRow(row: Record): PlatformUserRecord { return normalizeUser({ id: String(row.id), phone: String(row.phone || ""), displayName: String(row.display_name || ""), role: row.role as PlatformRole, organizationId: row.organization_id ? String(row.organization_id) : undefined, status: row.status as AccountStatus, passwordHash: String(row.password_hash || ""), passwordSalt: String(row.password_salt || ""), failedLoginCount: Number(row.failed_login_count || 0), lockedUntil: row.locked_until ? timestampFromRow(row.locked_until) : undefined, sessionVersion: Number(row.session_version || 1), lastLoginAt: row.last_login_at ? timestampFromRow(row.last_login_at) : undefined, legacySubject: row.legacy_subject ? String(row.legacy_subject) : undefined, createdAt: timestampFromRow(row.created_at), updatedAt: timestampFromRow(row.updated_at) }); } function migrationToRow(migration: AccountMigration) { return { id: migration.id, legacy_owner_id: migration.legacyOwnerId, legacy_phone: migration.legacyPhone || null, platform_user_id: migration.platformUserId, created_at: migration.createdAt }; } function migrationFromRow(row: Record): AccountMigration { return { id: String(row.id), legacyOwnerId: String(row.legacy_owner_id), legacyPhone: row.legacy_phone ? String(row.legacy_phone) : undefined, platformUserId: String(row.platform_user_id), createdAt: timestampFromRow(row.created_at) }; } function timestampFromRow(value: unknown): string { return value instanceof Date ? value.toISOString() : String(value); }