import { admin, customSession, magicLink, multiSession, username, } from "better-auth/plugins"; import { getUserController, UserController } from "../user/controller"; import { AuthController, getAuthController } from "./controller"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { FlowExecCtx } from "@/core/flow.execution.context"; import { UserRoleMap } from "@domains/user/data"; import { getRedisInstance } from "@pkg/keystore"; import { APIError } from "better-auth/api"; import { settings } from "@core/settings"; import { betterAuth } from "better-auth"; import { logger } from "@pkg/logger"; import { db, schema } from "@pkg/db"; import { nanoid } from "nanoid"; // Constants const EMAIL_EXPIRES_IN_MINS = 10; const EMAIL_EXPIRES_IN_SECONDS = 60 * EMAIL_EXPIRES_IN_MINS; const COOKIE_CACHE_MAX_AGE = 60 * 5; // Helper to create flow context for better-auth callbacks function createAuthFlowContext(contextLabel: string): FlowExecCtx { return { flowId: `auth:${contextLabel}:${nanoid(10)}`, }; } // Singleton controller instances let authControllerInstance: AuthController | null = null; let userControllerInstance: UserController | null = null; function getAuthControllerInstance(): AuthController { if (!authControllerInstance) { authControllerInstance = getAuthController(); } return authControllerInstance; } function getUserControllerInstance(): UserController { if (!userControllerInstance) { userControllerInstance = getUserController(); } return userControllerInstance; } export const auth = betterAuth({ trustedOrigins: ["http://localhost:5173", settings.betterAuthUrl], advanced: { useSecureCookies: settings.nodeEnv === "production" }, appName: settings.appName, emailAndPassword: { enabled: true, disableSignUp: true, requireEmailVerification: false, }, plugins: [ customSession(async ({ user, session }) => { session.id = session.token; return { user, session }; }), username({ minUsernameLength: 5, maxUsernameLength: 20, usernameValidator: async (username) => { const fctx = createAuthFlowContext("username-check"); const uc = getUserControllerInstance(); const result = await uc .isUsernameAvailable(fctx, username) .match( (isAvailable) => ({ success: true, isAvailable }), (error) => { logger.error( `[${fctx.flowId}] Failed to check username availability`, error, ); return { success: false, isAvailable: false }; }, ); return result.isAvailable; }, }), magicLink({ expiresIn: EMAIL_EXPIRES_IN_SECONDS, rateLimit: { window: 60, max: 4 }, sendMagicLink: async ({ email, token, url }, request) => { const fctx = createAuthFlowContext("magic-link"); const ac = getAuthControllerInstance(); const result = await ac .sendMagicLink(fctx, email, token, url) .match( () => ({ success: true, error: undefined }), (error) => ({ success: false, error }), ); if (!result.success || result?.error) { logger.error( `[${fctx.flowId}] Failed to send magic link`, result.error, ); throw new APIError("INTERNAL_SERVER_ERROR", { message: result.error?.message, }); } }, }), admin({ defaultRole: UserRoleMap.admin, defaultBanReason: "Stop fanum taxing the server bub, losing aura points fr", defaultBanExpiresIn: 60 * 60 * 24, }), multiSession({ maximumSessions: 5 }), ], logger: { log: (level, message, metadata) => { logger.log(level, message, metadata); }, level: settings.isDevelopment ? "debug" : "info", }, database: drizzleAdapter(db, { provider: "pg", schema: { ...schema } }), secondaryStorage: { get: async (key) => { const redis = getRedisInstance(); return await redis.get(key); }, set: async (key, value, ttl) => { const redis = getRedisInstance(); if (ttl) { await redis.setex(key, ttl, value); } else { await redis.set(key, value); } }, delete: async (key) => { const redis = getRedisInstance(); const out = await redis.del(key); if (!out && out !== 0) { return null; } return out.toString() as any; }, }, session: { modelName: "session", expiresIn: 60 * 60 * 24 * 7, updateAge: 60 * 60 * 24, cookieCache: { enabled: true, maxAge: COOKIE_CACHE_MAX_AGE, }, }, user: { changeEmail: { enabled: true, sendChangeEmailVerification: async ( { user, newEmail, url, token }, request, ) => { const fctx = createAuthFlowContext("email-change"); const ac = getAuthControllerInstance(); const result = await ac .sendEmailChangeVerificationEmail( fctx, newEmail, token, url, ) .match( () => ({ success: true, error: undefined }), (error) => ({ success: false, error }), ); if (!result.success || result?.error) { logger.error( `[${fctx.flowId}] Failed to send email change verification`, result.error, ); throw new APIError("INTERNAL_SERVER_ERROR", { message: result.error?.message, }); } }, }, modelName: "user", additionalFields: { onboardingDone: { type: "boolean", defaultValue: false, required: false, }, last2FAVerifiedAt: { type: "date", required: false }, parentId: { required: false, type: "string" }, }, }, }); // - - -