import { CanActivate, ExecutionContext, ForbiddenException, Inject, Injectable, UnauthorizedException, } 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; role?: string; } @Injectable() export class AdminGuard implements CanActivate { constructor( @Inject(AUTH) private readonly auth: Auth, @Inject(DB) private readonly db: Db, ) {} async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); const headers = fromNodeHeaders(request.raw.headers); const result = await this.auth.api.getSession({ headers }); if (!result) { throw new UnauthorizedException('Invalid or expired session'); } const user = result.user as UserWithRole; // 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'); } (request as FastifyRequest & { user: unknown; session: unknown }).user = result.user; (request as FastifyRequest & { user: unknown; session: unknown }).session = result.session; return true; } }