import { AuthContext, MiddlewareContext, MiddlewareOptions } from "better-auth"; import { AccountRepository } from "../user/account.repository"; import { FlowExecCtx } from "@/core/flow.execution.context"; import { ResultAsync } from "neverthrow"; import type { Err } from "@pkg/result"; import { authErrors } from "./errors"; import { logger } from "@pkg/logger"; import { nanoid } from "nanoid"; import { db } from "@pkg/db"; export class AuthController { private readonly mins = 10; constructor(private accountRepo: AccountRepository) {} sendEmailChangeVerificationEmail( fctx: FlowExecCtx, newEmail: string, token: string, url: string, ): ResultAsync { logger.info("Sending email change verification link", { ...fctx, newEmail, }); logger.debug("Original URL", { ...fctx, url }); const transformedUrl = url .replace("/api/auth/verify-email", "/account/verify-email") .replace("/api/", "/"); logger.debug("Transformed URL", { ...fctx, transformedUrl }); // Simulate email sending with 90/10 success/failure const success = Math.random() > 0.1; if (!success) { logger.error("Failed to send email change verification link", { ...fctx, error: "Simulated email service failure", }); return ResultAsync.fromPromise( Promise.reject( authErrors.emailChangeVerificationFailed( fctx, "Simulated email service failure", ), ), (error) => error as Err, ); } logger.info("Email change verification sent successfully", { ...fctx, newEmail, }); return ResultAsync.fromSafePromise(Promise.resolve(undefined)); } swapAccountPasswordForTwoFactor( fctx: FlowExecCtx, ctx: MiddlewareContext< MiddlewareOptions, AuthContext & { returned?: unknown; responseHeaders?: Headers } >, ) { logger.info("Swapping account password for 2FA", { ...fctx, }); if (!ctx.path.includes("two-factor")) { return ResultAsync.fromSafePromise(Promise.resolve(ctx)); } if (!ctx.body.password || ctx.body.password.length === 0) { return ResultAsync.fromSafePromise(Promise.resolve(ctx)); } logger.info("Rotating password for 2FA setup for user", { ...fctx, userId: ctx.body.userId, }); return this.accountRepo .rotatePassword(fctx, ctx.body.userId, nanoid()) .mapErr((err) => { logger.error("Failed to rotate password for 2FA", { ...fctx, error: err, }); return authErrors.passwordRotationFailed(fctx, err.detail); }) .map((newPassword) => { logger.info("Password rotated successfully for 2FA setup", { ...fctx, }); return { ...ctx, body: { ...ctx.body, password: newPassword }, }; }); } sendMagicLink( fctx: FlowExecCtx, email: string, token: string, url: string, ): ResultAsync { logger.info("Sending magic link", { ...fctx, email }); logger.debug("Original URL", { ...fctx, url }); const transformedUrl = url .replace("/api/auth/magic-link/verify", "/auth/magic-link") .replace("/api/", "/"); logger.debug("Transformed URL", { ...fctx, transformedUrl }); // Simulate email sending with 90/10 success/failure const success = true; if (!success) { logger.error("Failed to send magic link email", { ...fctx, error: "Simulated email service failure", }); return ResultAsync.fromPromise( Promise.reject( authErrors.magicLinkEmailFailed( fctx, "Simulated email service failure", ), ), (error) => error as Err, ); } logger.info("Magic link email sent successfully (NOT REALLY)", { ...fctx, email, }); return ResultAsync.fromSafePromise(Promise.resolve(undefined)); } } export function getAuthController(): AuthController { return new AuthController(new AccountRepository(db)); }