import { drizzleAdapter } from "@better-auth/drizzle-adapter/relations-v2"; import { APIError, createAuthMiddleware, getSessionFromCtx, isAPIError } from 'better-auth/api'; import { betterAuth } from 'better-auth/minimal'; import { createAccessControl } from 'better-auth/plugins/access'; import { admin } from 'better-auth/plugins/admin'; import { defaultStatements } from 'better-auth/plugins/admin/access'; import { twoFactor } from 'better-auth/plugins/two-factor'; import * as schema from '../drizzle/schema'; import { getRegistrationMode } from '../server/utils/instance'; import { hasUsableAdministrator } from '../server/lib/account-security'; import { authDatabase, currentTransaction } from '../server/lib/auth-transaction'; import { claimInvite, INVITE_TOKEN_HEADER } from '../server/utils/invites'; import { isSetupNeeded } from '../server/utils/setup'; const MIN_PASSWORD_LENGTH = 8; const ADMIN_MUTATIONS = new Set(['/admin/set-role', '/admin/ban-user', '/admin/unban-user', '/admin/update-user']); const transactionRequired = () => new APIError('INTERNAL_SERVER_ERROR', { message: 'This has to run in a transaction', code: 'TRANSACTION_REQUIRED', }); const accessControl = createAccessControl(defaultStatements); const roles = { admin: accessControl.newRole({ user: ['set-role', 'ban'], session: ['revoke'], }), user: accessControl.newRole({ user: [], session: [], }), }; export const auth = betterAuth({ database: drizzleAdapter(authDatabase, { provider: "pg", schema, usePlural: true, }), session: { expiresIn: 60 * 60 * 24 * 30, // 1 month updateAge: 60 * 60 * 24 * 1, // 1 day cookieCache: { enabled: true, maxAge: 60, }, }, baseURL: process.env.NUXT_PUBLIC_URL ?? 'http://localhost:3000', trustedOrigins: [process.env.NUXT_PUBLIC_URL ?? 'http://localhost:3000'], hooks: { before: createAuthMiddleware(async (ctx) => { if (ctx.path === "/change-password" && ctx.request) { throw new APIError("FORBIDDEN", { message: "Use the account password endpoint", code: "USE_ACCOUNT_PASSWORD_ENDPOINT", }); } if (ctx.path === '/sign-up/email') { if (await isSetupNeeded()) { throw new APIError('FORBIDDEN', { message: 'This instance has not been set up yet', code: 'SETUP_NEEDED', }); } const mode = await getRegistrationMode(); if (mode === 'closed') { throw new APIError('FORBIDDEN', { message: 'Registration is closed on this instance', code: 'REGISTRATION_CLOSED', }); } if (mode === 'invite') { const tx = currentTransaction(); if (!tx) throw transactionRequired(); const token = ctx.headers?.get(INVITE_TOKEN_HEADER); if (!token || !await claimInvite(tx, token)) { throw new APIError('FORBIDDEN', { message: 'This invite link is not valid anymore', code: 'INVITE_INVALID', }); } } } if (ctx.path === '/admin/create-user' && ctx.body?.password !== undefined) { const password = ctx.body.password; if (typeof password !== 'string' || password.length < MIN_PASSWORD_LENGTH) { throw new APIError('BAD_REQUEST', { message: 'Password is too short', code: 'PASSWORD_TOO_SHORT', }); } } if (!ADMIN_MUTATIONS.has(ctx.path)) return; if (!currentTransaction()) throw transactionRequired(); if (ctx.path !== '/admin/set-role') return; const role = ctx.body?.role; if (role === undefined) return; const roleList = (Array.isArray(role) ? role : String(role).split(',')).map((r) => String(r).trim()); if (roleList.includes('admin')) return; const session = await getSessionFromCtx(ctx); if (session && String(ctx.body?.userId) === session.user.id) { throw new APIError('BAD_REQUEST', { message: 'You cannot remove your own admin role', code: 'YOU_CANNOT_DEMOTE_YOURSELF', }); } }), after: createAuthMiddleware(async (ctx) => { if (!ADMIN_MUTATIONS.has(ctx.path) || isAPIError(ctx.context.returned)) return; const tx = currentTransaction(); if (!tx) throw transactionRequired(); if (!await hasUsableAdministrator(tx)) { throw new APIError('CONFLICT', { message: 'The instance must keep a usable administrator', code: 'LAST_ADMIN', }); } }), }, plugins: [ twoFactor({ issuer: 'Veridian', }), admin({ ac: accessControl, roles, }), ], emailAndPassword: { // if DISABLE_LOCAL_AUTH is set to "1" or "true" then local auth is disabled enabled: process.env.DISABLE_LOCAL_AUTH?.toLowerCase() !== 'true' && process.env.DISABLE_LOCAL_AUTH !== '1', disableSignUp: process.env.DISABLE_SIGNUP?.toLowerCase() === 'true' || process.env.DISABLE_SIGNUP === '1', }, });