import { Context, Effect, Layer } from "effect"; import { randomUUID } from "node:crypto"; import { DatabaseService } from "../../database/db.js"; import { DatabaseError, User } from "../../../shared/types.js"; export interface CreateUserData { username: string; email: string; passwordHash: string | null; linuxUsername: string; } export class UserRepository extends Context.Tag("UserRepository")< UserRepository, { readonly create: ( data: CreateUserData ) => Effect.Effect; readonly findById: ( id: string ) => Effect.Effect; readonly findByEmail: ( email: string ) => Effect.Effect; readonly findByUsername: ( username: string ) => Effect.Effect; readonly update: ( id: string, data: Partial ) => Effect.Effect; readonly incrementFailedLogins: ( id: string ) => Effect.Effect; readonly resetFailedLogins: ( id: string ) => Effect.Effect; } >() { static Live = Layer.effect( this, Effect.gen(function* (_) { const db = yield* _(DatabaseService); const create = (data: CreateUserData): Effect.Effect => Effect.gen(function* (_) { const id = randomUUID(); const now = Date.now(); yield* _( db.run( `INSERT INTO users ( id, username, email, password_hash, linux_username, created_at, updated_at, is_active, email_verified, failed_login_attempts, last_login_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ id, data.username, data.email, data.passwordHash, data.linuxUsername, now, now, 1, // is_active = true 0, // email_verified = false 0, // failed_login_attempts = 0 null, // last_login_at = null ] ) ); const user = yield* _(findById(id)); if (!user) { return yield* _( Effect.fail( new DatabaseError("Failed to retrieve created user") ) ); } return user; }); const findById = ( id: string ): Effect.Effect => Effect.gen(function* (_) { const row = yield* _( db.get<{ id: string; username: string; email: string; password_hash: string | null; linux_username: string; created_at: number; updated_at: number; is_active: number; email_verified: number; failed_login_attempts: number; last_login_at: number | null; }>("SELECT * FROM users WHERE id = ?", [id]) ); if (!row) { return undefined; } return { id: row.id, username: row.username, email: row.email, passwordHash: row.password_hash, linuxUsername: row.linux_username, createdAt: row.created_at, updatedAt: row.updated_at, isActive: row.is_active === 1, emailVerified: row.email_verified === 1, failedLoginAttempts: row.failed_login_attempts, lastLoginAt: row.last_login_at, }; }); const findByEmail = ( email: string ): Effect.Effect => Effect.gen(function* (_) { const row = yield* _( db.get<{ id: string; username: string; email: string; password_hash: string | null; linux_username: string; created_at: number; updated_at: number; is_active: number; email_verified: number; failed_login_attempts: number; last_login_at: number | null; }>("SELECT * FROM users WHERE email = ?", [email]) ); if (!row) { return undefined; } return { id: row.id, username: row.username, email: row.email, passwordHash: row.password_hash, linuxUsername: row.linux_username, createdAt: row.created_at, updatedAt: row.updated_at, isActive: row.is_active === 1, emailVerified: row.email_verified === 1, failedLoginAttempts: row.failed_login_attempts, lastLoginAt: row.last_login_at, }; }); const findByUsername = ( username: string ): Effect.Effect => Effect.gen(function* (_) { const row = yield* _( db.get<{ id: string; username: string; email: string; password_hash: string | null; linux_username: string; created_at: number; updated_at: number; is_active: number; email_verified: number; failed_login_attempts: number; last_login_at: number | null; }>("SELECT * FROM users WHERE username = ?", [username]) ); if (!row) { return undefined; } return { id: row.id, username: row.username, email: row.email, passwordHash: row.password_hash, linuxUsername: row.linux_username, createdAt: row.created_at, updatedAt: row.updated_at, isActive: row.is_active === 1, emailVerified: row.email_verified === 1, failedLoginAttempts: row.failed_login_attempts, lastLoginAt: row.last_login_at, }; }); const update = ( id: string, data: Partial ): Effect.Effect => Effect.gen(function* (_) { const updateFields: string[] = []; const updateValues: unknown[] = []; if (data.username !== undefined) { updateFields.push("username = ?"); updateValues.push(data.username); } if (data.email !== undefined) { updateFields.push("email = ?"); updateValues.push(data.email); } if (data.passwordHash !== undefined) { updateFields.push("password_hash = ?"); updateValues.push(data.passwordHash); } if (data.linuxUsername !== undefined) { updateFields.push("linux_username = ?"); updateValues.push(data.linuxUsername); } if (data.isActive !== undefined) { updateFields.push("is_active = ?"); updateValues.push(data.isActive ? 1 : 0); } if (data.emailVerified !== undefined) { updateFields.push("email_verified = ?"); updateValues.push(data.emailVerified ? 1 : 0); } if (data.failedLoginAttempts !== undefined) { updateFields.push("failed_login_attempts = ?"); updateValues.push(data.failedLoginAttempts); } if (data.lastLoginAt !== undefined) { updateFields.push("last_login_at = ?"); updateValues.push(data.lastLoginAt); } // Always update updated_at updateFields.push("updated_at = ?"); updateValues.push(Date.now()); if (updateFields.length === 1) { // Only updated_at, nothing to update const user = yield* _(findById(id)); if (!user) { return yield* _( Effect.fail(new DatabaseError("User not found")) ); } return user; } updateValues.push(id); yield* _( db.run( `UPDATE users SET ${updateFields.join(", ")} WHERE id = ?`, updateValues ) ); const user = yield* _(findById(id)); if (!user) { return yield* _( Effect.fail(new DatabaseError("User not found after update")) ); } return user; }); const incrementFailedLogins = ( id: string ): Effect.Effect => Effect.gen(function* (_) { yield* _( db.run( `UPDATE users SET failed_login_attempts = failed_login_attempts + 1, updated_at = ? WHERE id = ?`, [Date.now(), id] ) ); }); const resetFailedLogins = ( id: string ): Effect.Effect => Effect.gen(function* (_) { yield* _( db.run( `UPDATE users SET failed_login_attempts = 0, updated_at = ? WHERE id = ?`, [Date.now(), id] ) ); }); return { create, findById, findByEmail, findByUsername, update, incrementFailedLogins, resetFailedLogins, }; }) ); }