import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { PrismaService } from '../../prisma/prisma.service'; import { expandWithParents } from './acl.constants'; export interface JwtPayload { sub: string; mobile: string; } /** * The authenticated principal attached to `req.user`. * * `permissions` is already expanded with ancestors, so guards can test a rid with * a plain `includes()` — the same shape the legacy frontend held in its Vuex store * after checkParent(). */ export interface AuthenticatedAdmin { id: string; fullName: string; mobile: string; role: string; userTypeId: number | null; branchId: string | null; permissions: string[]; } @Injectable() export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { constructor(private readonly prisma: PrismaService) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: process.env.JWT_SECRET ?? 'dev-only-insecure-secret', }); } async validate(payload: JwtPayload): Promise { const user = await this.prisma.adminUser.findUnique({ where: { id: payload.sub }, include: { userType: { include: { permissions: true } } }, }); if (!user || !user.isActive) { throw new UnauthorizedException(); } // Permissions are re-read on every request rather than baked into the token, // so a permission change takes effect immediately instead of at next login. const rids = user.userType?.permissions.map((p) => p.rid) ?? []; return { id: user.id, fullName: user.fullName, mobile: user.mobile, role: user.role, userTypeId: user.userTypeId, branchId: user.branchId, permissions: expandWithParents(rids), }; } }