206 lines
6.8 KiB
TypeScript
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/redis";
|
|
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" },
|
|
},
|
|
},
|
|
});
|
|
|
|
// - - -
|