import { APIErrorCode, APIException, type RegisterUserRequest, type UpdateUserRequest, type User, } from "../model/interfaces.ts"; import { db, isUserRow, userApiToRow, userRowToApi } from "../model/db.ts"; import { disconnectUser } from "./ws-service.ts"; import { hashPassword } from "../lib/jwt.ts"; import { linkAttachments } from "./attachment-service.ts"; const USER_SELECT = `SELECT u.id, u.username, u.password_hash, u.is_admin, u.created_at, u.updated_at, u.avatar_mime, u.description, u.invited_by, u.email, i.username as invited_by_username FROM users u LEFT JOIN users i ON i.id = u.invited_by`; export async function createUser( request: RegisterUserRequest, inviterId: string | null, ): Promise { const userId = crypto.randomUUID(); const createdAt = new Date(); const existingUser = db.prepare( "SELECT id FROM users WHERE username = ?;", ).get(request.username); if (existingUser) { throw new APIException( APIErrorCode.VALIDATION_ERROR, 400, "Username already exists", ); } const passwordHash = await hashPassword(request.password); db.prepare( `INSERT INTO users (id, username, password_hash, is_admin, created_at, invited_by, email) VALUES (?, ?, ?, ?, ?, ?, ?);`, ).run( userId, request.username, passwordHash, 0, createdAt.toISOString(), inviterId, request.email, ); return { id: userId, username: request.username, passwordHash, isAdmin: false, createdAt, email: request.email, }; } export function getUserById(userId: string): User { const userRow = db.prepare( `${USER_SELECT} WHERE u.id = ?`, ).get(userId); if (!userRow || !isUserRow(userRow)) { throw new APIException(APIErrorCode.NOT_FOUND, 404, "User not found"); } return userRowToApi(userRow); } export function getUserByUsername(username: string): User { const userRow = db.prepare( `${USER_SELECT} WHERE u.username = ?`, ).get(username); if (!userRow || !isUserRow(userRow)) { throw new APIException(APIErrorCode.NOT_FOUND, 404, "User not found"); } return userRowToApi(userRow); } export function getUserByEmail(email: string): User | null { const userRow = db.prepare( `${USER_SELECT} WHERE u.email = ?`, ).get(email); if (!userRow || !isUserRow(userRow)) return null; return userRowToApi(userRow); } export function searchUsers( query: string, limit: number, ): { id: string; username: string; avatarMime: string | null }[] { if (!query) return []; const rows = db.prepare( `SELECT id, username, avatar_mime FROM users WHERE username LIKE ? ORDER BY username LIMIT ?;`, ).all(`${query}%`, limit) as { id: string; username: string; avatar_mime: string | null; }[]; return rows.map((r) => ({ id: r.id, username: r.username, avatarMime: r.avatar_mime, })); } export function searchUsersGlobal(query: string, limit: number): User[] { if (!query.trim()) return []; const pattern = `%${query}%`; const rows = db.prepare( `${USER_SELECT} WHERE (u.username LIKE ? OR u.description LIKE ?) ORDER BY u.username LIMIT ?;`, ).all(pattern, pattern, limit); return rows.filter(isUserRow).map(userRowToApi); } export function listUsers(): User[] { const userRows = db.prepare( `${USER_SELECT}`, ).all(); if (!userRows || !userRows.every(isUserRow)) { throw new APIException(APIErrorCode.NOT_FOUND, 404, "No user found"); } return userRows.map(userRowToApi); } export async function updateUser( userId: string, request: UpdateUserRequest, ): Promise { const user = getUserById(userId); const { password, description, email, ...requestFields } = request; const now = new Date(); const updatedUser: User = { ...user, passwordHash: password ? await hashPassword(password) : user.passwordHash, ...requestFields, description: description ?? user.description, email: email ?? user.email, updatedAt: now, }; const updatedUserRow = userApiToRow(updatedUser); const userResult = db.prepare( `UPDATE users SET username = ?, password_hash = ?, is_admin = ?, description = ?, email = ?, updated_at = ? WHERE id = ?`, ).run( updatedUserRow.username, updatedUserRow.password_hash, updatedUserRow.is_admin, updatedUserRow.description, updatedUserRow.email, now.toISOString(), updatedUserRow.id, ); if (userResult.changes === 0) { throw new APIException(APIErrorCode.NOT_FOUND, 404, "Dump not found"); } if (updatedUser.description) { linkAttachments(updatedUser.description, userId); } return updatedUser; } export function updateUserAvatar(userId: string, mime: string): void { const result = db.prepare( `UPDATE users SET avatar_mime = ?, updated_at = ? WHERE id = ?`, ).run(mime, new Date().toISOString(), userId); if (result.changes === 0) { throw new APIException(APIErrorCode.NOT_FOUND, 404, "User not found"); } } export function getInviteTree( userId: string, ): { id: string; username: string; avatarMime?: string; invitedById: string; createdAt: Date; }[] { const rows = db.prepare(` WITH RECURSIVE tree AS ( SELECT id, username, avatar_mime, invited_by, created_at, 0 AS depth FROM users WHERE invited_by = ? UNION ALL SELECT u.id, u.username, u.avatar_mime, u.invited_by, u.created_at, t.depth + 1 FROM users u INNER JOIN tree t ON u.invited_by = t.id WHERE t.depth < 10 ) SELECT * FROM tree ORDER BY created_at; `).all(userId) as { id: string; username: string; avatar_mime: string | null; invited_by: string; created_at: string; }[]; return rows.map((r) => ({ id: r.id, username: r.username, avatarMime: r.avatar_mime ?? undefined, invitedById: r.invited_by, createdAt: new Date(r.created_at), })); } export function deleteUser(userId: string): void { disconnectUser(userId); const result = db.prepare( `DELETE FROM users WHERE id = ?;`, ).run(userId); if (result.changes === 0) { throw new APIException(APIErrorCode.NOT_FOUND, 404, "User not found"); } }