/** * UserStore — persistence layer for the users table. * All queries go through DatabaseConnection for audit + safety. */ import { randomUUID } from 'crypto'; import type { DatabaseConnection } from '../shared/db/DatabaseConnection.js'; import { USER_QUERIES, RESET_TOKEN_QUERIES } from '../shared/db/queries.constant.js'; import type { Role, User, UserRow } from './auth.model.js'; export class UserStore { constructor(private readonly db: DatabaseConnection) {} findByEmail(email: string): UserRow | undefined { return this.db.rawGet(USER_QUERIES.SELECT_BY_EMAIL, [email]); } findById(id: string): User | undefined { const row = this.db.rawGet(USER_QUERIES.SELECT_BY_ID, [id]); if (!row) return undefined; return this.#toUser(row); } create(email: string, passwordHash: string, role: Role = 'viewer'): User { const id = randomUUID(); const createdAt = new Date().toISOString(); this.db.rawRun(USER_QUERIES.INSERT, [id, email, passwordHash, role, createdAt]); return { id, email, role, createdAt, lastLogin: null }; } touchLogin(id: string): void { this.db.rawRun(USER_QUERIES.UPDATE_LAST_LOGIN, [new Date().toISOString(), id]); } updatePassword(id: string, passwordHash: string): void { this.db.rawRun('UPDATE users SET password_hash = ? WHERE id = ?', [passwordHash, id]); } // ── Password reset tokens ────────────────────────────────────────────────── createResetToken(userId: string, token: string, expiresAt: string): void { this.db.rawRun(RESET_TOKEN_QUERIES.INSERT, [token, userId, expiresAt]); } findResetToken( token: string, ): { token: string; user_id: string; expires_at: string; used: number } | undefined { return this.db.rawGet(RESET_TOKEN_QUERIES.FIND, [token]); } markTokenUsed(token: string): void { this.db.rawRun(RESET_TOKEN_QUERIES.MARK_USED, [token]); } purgeExpiredTokens(): void { this.db.rawRun(RESET_TOKEN_QUERIES.PURGE, [new Date().toISOString()]); } #toUser(row: UserRow): User { return { id: row.id, email: row.email, role: row.role, createdAt: row.created_at, lastLogin: row.last_login, }; } }