import { ResultAsync, errAsync, okAsync } from "neverthrow"; import { FlowExecCtx } from "@core/flow.execution.context"; 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 { logger } from "@pkg/logger"; export class UserRepository { constructor(private db: Database) {} getUserInfo(fctx: FlowExecCtx, userId: string): ResultAsync { logger.info("Getting user info for user", { flowId: fctx.flowId, userId, }); return ResultAsync.fromPromise( this.db.query.user.findFirst({ where: eq(user.id, userId), }), (error) => { logger.error("Failed to get user info", { flowId: fctx.flowId, error, }); return userErrors.getUserInfoFailed( fctx, error instanceof Error ? error.message : String(error), ); }, ).andThen((userData) => { if (!userData) { logger.error("User not found with id", { flowId: fctx.flowId, userId, }); return errAsync(userErrors.userNotFound(fctx)); } logger.info("User info retrieved successfully for user", { flowId: fctx.flowId, userId, }); return okAsync(userData as User); }); } updateLastVerified2FaAtToNow( fctx: FlowExecCtx, userId: string, ): ResultAsync { logger.info("Updating last 2FA verified timestamp for user", { flowId: fctx.flowId, userId, }); return ResultAsync.fromPromise( this.db .update(user) .set({ last2FAVerifiedAt: new Date() }) .where(eq(user.id, userId)) .execute(), (error) => { logger.error("Failed to update last 2FA verified timestamp", { ...fctx, error, }); return userErrors.updateFailed( fctx, error instanceof Error ? error.message : String(error), ); }, ).map(() => { logger.info("Last 2FA verified timestamp updated successfully", { ...fctx, }); return true; }); } isUsernameAvailable( fctx: FlowExecCtx, username: string, ): ResultAsync { logger.info("Checking username availability", { ...fctx, username, }); return ResultAsync.fromPromise( this.db.query.user.findFirst({ where: eq(user.username, username), }), (error) => { logger.error("Failed to check username availability", { ...fctx, error, }); return userErrors.usernameCheckFailed( fctx, error instanceof Error ? error.message : String(error), ); }, ).map((existingUser) => { const isAvailable = !existingUser?.id; logger.info("Username availability checked", { ...fctx, username, isAvailable, }); return isAvailable; }); } banUser( fctx: FlowExecCtx, userId: string, reason: string, banExpiresAt: Date, ): ResultAsync { logger.info("Banning user", { ...fctx, userId, banExpiresAt: banExpiresAt.toISOString(), reason, }); return ResultAsync.fromPromise( this.db .update(user) .set({ banned: true, banReason: reason, banExpires: banExpiresAt, }) .where(eq(user.id, userId)) .execute(), (error) => { logger.error("Failed to ban user", { ...fctx, error }); return userErrors.banOperationFailed( fctx, error instanceof Error ? error.message : String(error), ); }, ).map(() => { logger.info("User has been banned", { ...fctx, userId, banExpiresAt: banExpiresAt.toISOString(), }); return true; }); } isUserBanned(fctx: FlowExecCtx, userId: string): ResultAsync { logger.info("Checking ban status for user", { ...fctx, userId }); return ResultAsync.fromPromise( this.db.query.user.findFirst({ where: eq(user.id, userId), columns: { banned: true, banExpires: true, }, }), (error) => { logger.error("Failed to check ban status", { ...fctx, error, }); return userErrors.dbError( fctx, error instanceof Error ? error.message : String(error), ); }, ).andThen((userData) => { if (!userData) { logger.error("User not found when checking ban status", { ...fctx, }); return errAsync(userErrors.userNotFound(fctx)); } // If not banned, return false if (!userData.banned) { logger.info("User is not banned", { ...fctx, userId }); return okAsync(false); } // If banned but no expiry date, consider permanently banned if (!userData.banExpires) { logger.info("User is permanently banned", { ...fctx, userId }); return okAsync(true); } const now = new Date(); if (userData.banExpires <= now) { logger.info("User ban has expired, removing ban status", { ...fctx, userId, }); return ResultAsync.fromPromise( this.db .update(user) .set({ banned: false, banReason: null, banExpires: null, }) .where(eq(user.id, userId)) .execute(), (error) => { logger.error("Failed to unban user after expiry", { ...fctx, error, }); return userErrors.unbanFailed( fctx, error instanceof Error ? error.message : String(error), ); }, ) .map(() => { logger.info("User has been unbanned after expiry", { ...fctx, userId, }); return false; }) .orElse((error) => { logger.error( "Failed to unban user after expiry, still returning banned status", { ...fctx, userId, error }, ); // Still return banned status since we couldn't update return okAsync(true); }); } logger.info("User is banned", { ...fctx, userId, banExpires: userData.banExpires.toISOString(), }); return okAsync(true); }); } getBanInfo(fctx: FlowExecCtx, userId: string): ResultAsync { logger.info("Getting ban info for user", { ...fctx, userId }); return ResultAsync.fromPromise( this.db.query.user.findFirst({ where: eq(user.id, userId), columns: { banned: true, banReason: true, banExpires: true }, }), (error) => { logger.error("Failed to get ban info", { ...fctx, error }); return userErrors.getBanInfoFailed( fctx, error instanceof Error ? error.message : String(error), ); }, ).andThen((userData) => { if (!userData) { logger.error("User not found when getting ban info", { ...fctx, }); return errAsync(userErrors.userNotFound(fctx)); } logger.info("Ban info retrieved successfully for user", { ...fctx, userId, }); return okAsync({ banned: userData.banned || false, reason: userData.banReason || undefined, expires: userData.banExpires || undefined, }); }); } }