import { ResultAsync, errAsync, okAsync } from "neverthrow"; import { FlowExecCtx } from "@core/flow.execution.context"; import { traceResultAsync } from "@core/observability"; import { type Err } from "@pkg/result"; import { Database, eq } from "@pkg/db"; import { BanInfo, User } from "./data"; import { user } from "@pkg/db/schema"; import { userErrors } from "./errors"; import { logDomainEvent } from "@pkg/logger"; export class UserRepository { constructor(private db: Database) {} getUserInfo(fctx: FlowExecCtx, userId: string): ResultAsync { return traceResultAsync({ name: "logic.user.repository.getUserInfo", fctx, attributes: { "app.user.id": userId }, fn: () => { const startedAt = Date.now(); logDomainEvent({ event: "user.get_info.started", fctx, meta: { userId }, }); return ResultAsync.fromPromise( this.db.query.user.findFirst({ where: eq(user.id, userId), }), (error) => { logDomainEvent({ level: "error", event: "user.get_info.failed", fctx, durationMs: Date.now() - startedAt, error, meta: { userId }, }); return userErrors.getUserInfoFailed( fctx, error instanceof Error ? error.message : String(error), ); }, ).andThen((userData) => { if (!userData) { logDomainEvent({ level: "warn", event: "user.get_info.failed", fctx, durationMs: Date.now() - startedAt, error: { code: "NOT_FOUND", message: "User not found" }, meta: { userId }, }); return errAsync(userErrors.userNotFound(fctx)); } logDomainEvent({ event: "user.get_info.succeeded", fctx, durationMs: Date.now() - startedAt, meta: { userId }, }); return okAsync(userData as User); }); }, }); } isUsernameAvailable( fctx: FlowExecCtx, username: string, ): ResultAsync { return traceResultAsync({ name: "logic.user.repository.isUsernameAvailable", fctx, attributes: { "app.user.username": username }, fn: () => { const startedAt = Date.now(); logDomainEvent({ event: "user.username_check.started", fctx, }); return ResultAsync.fromPromise( this.db.query.user.findFirst({ where: eq(user.username, username), }), (error) => { logDomainEvent({ level: "error", event: "user.username_check.failed", fctx, durationMs: Date.now() - startedAt, error, }); return userErrors.usernameCheckFailed( fctx, error instanceof Error ? error.message : String(error), ); }, ).map((existingUser) => { const isAvailable = !existingUser?.id; logDomainEvent({ event: "user.username_check.succeeded", fctx, durationMs: Date.now() - startedAt, meta: { isAvailable }, }); return isAvailable; }); }, }); } updateLastVerified2FaAtToNow( fctx: FlowExecCtx, userId: string, ): ResultAsync { return traceResultAsync({ name: "logic.user.repository.updateLastVerified2FaAtToNow", fctx, attributes: { "app.user.id": userId }, fn: () => { const startedAt = Date.now(); logDomainEvent({ event: "user.update_last_2fa.started", fctx, meta: { userId }, }); return ResultAsync.fromPromise( this.db .update(user) .set({ last2FAVerifiedAt: new Date() }) .where(eq(user.id, userId)) .execute(), (error) => { logDomainEvent({ level: "error", event: "user.update_last_2fa.failed", fctx, durationMs: Date.now() - startedAt, error, meta: { userId }, }); return userErrors.updateFailed( fctx, error instanceof Error ? error.message : String(error), ); }, ).map(() => { logDomainEvent({ event: "user.update_last_2fa.succeeded", fctx, durationMs: Date.now() - startedAt, meta: { userId }, }); return true; }); }, }); } banUser( fctx: FlowExecCtx, userId: string, reason: string, banExpiresAt: Date, ): ResultAsync { return traceResultAsync({ name: "logic.user.repository.banUser", fctx, attributes: { "app.user.id": userId }, fn: () => { const startedAt = Date.now(); logDomainEvent({ event: "user.ban.started", fctx, meta: { userId, reasonLength: reason.length, banExpiresAt: banExpiresAt.toISOString(), }, }); return ResultAsync.fromPromise( this.db .update(user) .set({ banned: true, banReason: reason, banExpires: banExpiresAt, }) .where(eq(user.id, userId)) .execute(), (error) => { logDomainEvent({ level: "error", event: "user.ban.failed", fctx, durationMs: Date.now() - startedAt, error, meta: { userId }, }); return userErrors.banOperationFailed( fctx, error instanceof Error ? error.message : String(error), ); }, ).map(() => { logDomainEvent({ event: "user.ban.succeeded", fctx, durationMs: Date.now() - startedAt, meta: { userId }, }); return true; }); }, }); } isUserBanned(fctx: FlowExecCtx, userId: string): ResultAsync { return traceResultAsync({ name: "logic.user.repository.isUserBanned", fctx, attributes: { "app.user.id": userId }, fn: () => { const startedAt = Date.now(); logDomainEvent({ event: "user.is_banned.started", fctx, meta: { userId }, }); return ResultAsync.fromPromise( this.db.query.user.findFirst({ where: eq(user.id, userId), columns: { banned: true, banExpires: true, }, }), (error) => { logDomainEvent({ level: "error", event: "user.is_banned.failed", fctx, durationMs: Date.now() - startedAt, error, meta: { userId }, }); return userErrors.dbError( fctx, error instanceof Error ? error.message : String(error), ); }, ).andThen((userData) => { if (!userData) { logDomainEvent({ level: "warn", event: "user.is_banned.failed", fctx, durationMs: Date.now() - startedAt, error: { code: "NOT_FOUND", message: "User not found" }, meta: { userId }, }); return errAsync(userErrors.userNotFound(fctx)); } if (!userData.banned) { logDomainEvent({ event: "user.is_banned.succeeded", fctx, durationMs: Date.now() - startedAt, meta: { userId, isBanned: false }, }); return okAsync(false); } if (!userData.banExpires) { logDomainEvent({ event: "user.is_banned.succeeded", fctx, durationMs: Date.now() - startedAt, meta: { userId, isBanned: true, isPermanent: true }, }); return okAsync(true); } const now = new Date(); if (userData.banExpires <= now) { return ResultAsync.fromPromise( this.db .update(user) .set({ banned: false, banReason: null, banExpires: null, }) .where(eq(user.id, userId)) .execute(), (error) => { logDomainEvent({ level: "error", event: "user.unban_after_expiry.failed", fctx, durationMs: Date.now() - startedAt, error, meta: { userId }, }); return userErrors.unbanFailed( fctx, error instanceof Error ? error.message : String(error), ); }, ) .map(() => { logDomainEvent({ event: "user.unban_after_expiry.succeeded", fctx, durationMs: Date.now() - startedAt, meta: { userId }, }); return false; }) .orElse((error) => { logDomainEvent({ level: "warn", event: "user.is_banned.succeeded", fctx, durationMs: Date.now() - startedAt, error, meta: { userId, degraded: true, isBanned: true }, }); return okAsync(true); }); } logDomainEvent({ event: "user.is_banned.succeeded", fctx, durationMs: Date.now() - startedAt, meta: { userId, isBanned: true, banExpires: userData.banExpires.toISOString(), }, }); return okAsync(true); }); }, }); } getBanInfo(fctx: FlowExecCtx, userId: string): ResultAsync { return traceResultAsync({ name: "logic.user.repository.getBanInfo", fctx, attributes: { "app.user.id": userId }, fn: () => { const startedAt = Date.now(); logDomainEvent({ event: "user.ban_info.started", fctx, meta: { userId }, }); return ResultAsync.fromPromise( this.db.query.user.findFirst({ where: eq(user.id, userId), columns: { banned: true, banReason: true, banExpires: true }, }), (error) => { logDomainEvent({ level: "error", event: "user.ban_info.failed", fctx, durationMs: Date.now() - startedAt, error, meta: { userId }, }); return userErrors.getBanInfoFailed( fctx, error instanceof Error ? error.message : String(error), ); }, ).andThen((userData) => { if (!userData) { logDomainEvent({ level: "warn", event: "user.ban_info.failed", fctx, durationMs: Date.now() - startedAt, error: { code: "NOT_FOUND", message: "User not found" }, meta: { userId }, }); return errAsync(userErrors.userNotFound(fctx)); } logDomainEvent({ event: "user.ban_info.succeeded", fctx, durationMs: Date.now() - startedAt, meta: { userId, banned: userData.banned || false }, }); return okAsync({ banned: userData.banned || false, reason: userData.banReason || undefined, expires: userData.banExpires || undefined, }); }); }, }); } }