Files
illusory-mapp/packages/logic/domains/auth/config.base.ts
2026-02-28 16:20:47 +02:00

206 lines
6.8 KiB
TypeScript

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" },
},
},
});
// - - -