import { drizzleAdapter } from "@better-auth/drizzle-adapter/relations-v2"; import { db } from "../server/lib/db"; 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 { claimInvite, INVITE_TOKEN_HEADER, releaseInvite } from '../server/utils/invites'; const MIN_PASSWORD_LENGTH = 8; const accessControl = createAccessControl(defaultStatements); // Admins manage accounts, not content. Signing in as someone else and setting // another user's password or email are left out on purpose: the first would // show their chats, the others would lock them out of their encrypted API keys. // Deleting is left out too, because Better Auth would only remove the account // and leave its agents, topics and files behind. /api/admin/users does that. const roles = { admin: accessControl.newRole({ user: ['create', 'list', 'set-role', 'ban', 'get', 'update'], session: ['list', 'revoke', 'delete'], }), user: accessControl.newRole({ user: [], session: [], }), }; export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "pg", schema, usePlural: true, }), session: { expiresIn: 60 * 60 * 24 * 30, // 1 month updateAge: 60 * 60 * 24 * 1, // 1 day cookieCache: { enabled: true, maxAge: 5 * 60, }, }, baseURL: process.env.NUXT_PUBLIC_URL ?? 'http://localhost:3000', trustedOrigins: [process.env.NUXT_PUBLIC_URL ?? 'http://localhost:3000'], hooks: { before: createAuthMiddleware(async (ctx) => { // Who may register is an instance setting admins change at runtime. if (ctx.path === '/sign-up/email') { 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 token = ctx.headers?.get(INVITE_TOKEN_HEADER); if (!token || !await claimInvite(token)) { throw new APIError('FORBIDDEN', { message: 'This invite link is not valid anymore', code: 'INVITE_INVALID', }); } } } // creating a user as an admin skips the sign up password rules if (ctx.path === '/admin/create-user') { 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', }); } } // The admin endpoints already stop admins from removing or disabling // themselves. Giving up their own admin role is the one way left to // end up with an instance nobody can manage, so that is refused too. if (ctx.path !== '/admin/set-role' && ctx.path !== '/admin/update-user') return; const role = ctx.path === '/admin/set-role' ? ctx.body?.role : ctx.body?.data?.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', }); } }), // The invite was claimed before the account was made. When making it // then fails, say because the email is taken, the use is given back. after: createAuthMiddleware(async (ctx) => { if (ctx.path !== '/sign-up/email' || !isAPIError(ctx.context.returned)) return; const token = ctx.headers?.get(INVITE_TOKEN_HEADER); if (token && await getRegistrationMode() === 'invite') { await releaseInvite(token); } }), }, 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', }, });