import { betterAuth, type AuthContext, type BetterAuthOptions, type MiddlewareContext, type MiddlewareOptions, } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { admin, createAuthMiddleware, twoFactor, username, organization } from "better-auth/plugins"; import { UnauthorizedError } from "http-errors-enhanced"; import { convertLegacyUserOnFirstLogin } from "./auth-middlewares/convert-legacy-user"; import { eq } from "drizzle-orm"; import { config } from "../server/core/config"; import { db } from "../server/db/db"; import { cryptoUtils } from "../server/utils/crypto"; import { organization as organizationTable, member, usersTable } from "../server/db/schema"; import { ensureOnlyOneUser } from "./auth-middlewares/only-one-user"; import { authService } from "../server/modules/auth/auth.service"; export type AuthMiddlewareContext = MiddlewareContext>; const createBetterAuth = (secret: string) => { return betterAuth({ secret, baseURL: config.baseUrl, trustedOrigins: config.trustedOrigins, advanced: { cookiePrefix: "zerobyte", useSecureCookies: config.isSecure, }, onAPIError: { throw: true, }, hooks: { before: createAuthMiddleware(async (ctx) => { await ensureOnlyOneUser(ctx); await convertLegacyUserOnFirstLogin(ctx); }), }, database: drizzleAdapter(db, { provider: "sqlite", }), databaseHooks: { user: { delete: { before: async (user) => { await authService.cleanupUserOrganizations(user.id); }, }, create: { before: async (user) => { const anyUser = await db.query.usersTable.findFirst(); const isFirstUser = !anyUser; if (isFirstUser) { user.role = "admin"; } return { data: user }; }, after: async (user) => { const slug = user.email.split("@")[0] + "-" + Math.random().toString(36).slice(-4); const resticPassword = cryptoUtils.generateResticPassword(); const metadata = { resticPassword: await cryptoUtils.sealSecret(resticPassword), }; try { await db.transaction(async (tx) => { const orgId = Bun.randomUUIDv7(); await tx.insert(organizationTable).values({ name: `${user.name}'s Workspace`, slug: slug, id: orgId, createdAt: new Date(), metadata, }); await tx.insert(member).values({ id: Bun.randomUUIDv7(), userId: user.id, role: "owner", organizationId: orgId, createdAt: new Date(), }); }); } catch { await db.delete(usersTable).where(eq(usersTable.id, user.id)); throw new Error(`Failed to create organization for user ${user.id}`); } }, }, }, session: { create: { before: async (session) => { const orgMembership = await db.query.member.findFirst({ where: eq(member.userId, session.userId), }); if (!orgMembership) { throw new UnauthorizedError("User does not belong to any organization"); } return { data: { ...session, activeOrganizationId: orgMembership?.organizationId, }, }; }, }, }, }, emailAndPassword: { enabled: true, }, user: { modelName: "usersTable", additionalFields: { username: { type: "string", returned: true, required: true, }, hasDownloadedResticPassword: { type: "boolean", returned: true, }, }, }, session: { modelName: "sessionsTable", }, plugins: [ username(), admin({ defaultRole: "user", }), organization({ allowUserToCreateOrganization: false, }), twoFactor({ backupCodeOptions: { storeBackupCodes: "encrypted", amount: 5, }, }), ], }); }; type Auth = ReturnType; let _auth: Auth | null = null; const createAuth = async (): Promise => { if (_auth) return _auth; _auth = createBetterAuth(await cryptoUtils.deriveSecret("better-auth")); return _auth; }; export const auth = new Proxy( {}, { get(_, prop, receiver) { if (!_auth) { throw new Error("Auth not initialized. Call initAuth() first."); } return Reflect.get(_auth, prop, receiver); }, }, ) as Auth; export const initAuth = createAuth;