From 0cb4b2148a2f06b3322f3c3f21449698934b7555 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Thu, 10 Sep 2026 14:59:28 -0500 Subject: [PATCH] Expose roleId on the Better Auth session user --- src/app.d.ts | 6 +++--- src/lib/auth.ts | 12 ++++++++++++ src/lib/server/roles.ts | 28 ++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 src/lib/server/roles.ts diff --git a/src/app.d.ts b/src/app.d.ts index 8f835de..91da9d8 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,11 +1,11 @@ -import type { Session, User } from 'better-auth'; +import type { auth } from '$lib/auth'; import type { ApiKeyRecord } from '$lib/server/api-key'; declare global { namespace App { interface Locals { - session: Session | null; - user: User | null; + session: typeof auth.$Infer.Session.session | null; + user: typeof auth.$Infer.Session.user | null; apiKey?: ApiKeyRecord; } } diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 126237f..b3a1d1b 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -27,6 +27,18 @@ export const auth = betterAuth({ database: prismaAdapter(prisma, { provider: 'postgresql' }), + user: { + // Expose our custom role column on the session user so role checks can + // read `locals.user.roleId` without a database round-trip. + additionalFields: { + roleId: { + type: 'number', + required: false, + defaultValue: 4, + input: false + } + } + }, emailVerification: { sendOnSignUp: true, sendVerificationEmail: async ({ user, token }) => { diff --git a/src/lib/server/roles.ts b/src/lib/server/roles.ts new file mode 100644 index 0000000..fd0d5bb --- /dev/null +++ b/src/lib/server/roles.ts @@ -0,0 +1,28 @@ +/** + * Role hierarchy. Lower ids are more privileged: + * SUPER = 1, ADMIN = 2, STAFF = 3, USER = 4 + * + * `roleId` is exposed on the Better Auth session user (see `auth.ts` + * `user.additionalFields`), so these helpers never need a database call. + */ +export const SUPER = 1; +export const ADMIN = 2; +export const STAFF = 3; +export const USER = 4; + +/** The current user's role id, defaulting to the least privileged role. */ +export function roleId(locals: App.Locals): number { + return locals.user?.roleId ?? USER; +} + +export function isSuper(locals: App.Locals): boolean { + return roleId(locals) <= SUPER; +} + +export function isAdmin(locals: App.Locals): boolean { + return roleId(locals) <= ADMIN; +} + +export function isStaff(locals: App.Locals): boolean { + return roleId(locals) <= STAFF; +}