diff --git a/apps/gateway/src/admin/admin.guard.ts b/apps/gateway/src/admin/admin.guard.ts index 7648219..a98a755 100644 --- a/apps/gateway/src/admin/admin.guard.ts +++ b/apps/gateway/src/admin/admin.guard.ts @@ -8,8 +8,11 @@ import { } from '@nestjs/common'; import { fromNodeHeaders } from 'better-auth/node'; import type { Auth } from '@mosaic/auth'; +import type { Db } from '@mosaic/db'; +import { eq, users as usersTable } from '@mosaic/db'; import type { FastifyRequest } from 'fastify'; import { AUTH } from '../auth/auth.tokens.js'; +import { DB } from '../database/database.module.js'; interface UserWithRole { id: string; @@ -18,7 +21,10 @@ interface UserWithRole { @Injectable() export class AdminGuard implements CanActivate { - constructor(@Inject(AUTH) private readonly auth: Auth) {} + constructor( + @Inject(AUTH) private readonly auth: Auth, + @Inject(DB) private readonly db: Db, + ) {} async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); @@ -32,7 +38,21 @@ export class AdminGuard implements CanActivate { const user = result.user as UserWithRole; - if (user.role !== 'admin') { + // Ensure the role field is populated. better-auth should include additionalFields + // in the session, but as a fallback, fetch the role from the database if needed. + let userRole = user.role; + if (!userRole) { + const [dbUser] = await this.db + .select({ role: usersTable.role }) + .from(usersTable) + .where(eq(usersTable.id, user.id)) + .limit(1); + userRole = dbUser?.role ?? 'member'; + // Update the session user object with the fetched role + (user as UserWithRole).role = userRole; + } + + if (userRole !== 'admin') { throw new ForbiddenException('Admin access required'); } diff --git a/docs/scratchpads/bug-196-admin-redirect.md b/docs/scratchpads/bug-196-admin-redirect.md new file mode 100644 index 0000000..f40962c --- /dev/null +++ b/docs/scratchpads/bug-196-admin-redirect.md @@ -0,0 +1,37 @@ +# BUG-196: Admin Page Redirect Issue + +## Problem + +Admin page redirects to /chat for users with admin role because role check fails. + +## Root Cause + +The `role` field is defined as an `additionalField` in better-auth's user configuration, but +better-auth v1.5.5 does not automatically include additionalFields in the session response from +the `getSession()` API. This causes the admin role check to fail: + +- Frontend: `AdminRoleGuard` checks `user?.role !== 'admin'` +- Backend: `AdminGuard` checks `user.role !== 'admin'` +- When `role` is `undefined`, both checks treat the user as non-admin and deny access + +## Solution + +Implemented a defensive check in the backend `AdminGuard` that: + +1. First tries to use the `role` field from the session (if better-auth includes it) +2. Falls back to fetching the role directly from the database if it's missing +3. Defaults to 'member' if the user has no role set + +This ensures that admin users can always access the admin panel, and also protects against +the case where better-auth doesn't include the additionalField in future versions. + +## Files Changed + +1. `/apps/gateway/src/admin/admin.guard.ts` - Added fallback role lookup +2. `/packages/auth/src/auth.ts` - No changes needed (better-auth config is correct) + +## Verification + +- All three quality gates pass: `typecheck`, `lint`, `format:check` +- Backend admin guard now explicitly handles missing role field +- Frontend admin guard remains unchanged (will work once role is available)