import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { ConfigService } from '@nestjs/config'; import { UsersService } from '../../users/users.service'; interface JwtPayload { sub: string; email: string; role: string; /** Present on impersonation tokens: the acting admin. */ act?: { sub: string; role: string }; } @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor( configService: ConfigService, private readonly usersService: UsersService, ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: configService.getOrThrow('jwt.secret'), }); } async validate(payload: JwtPayload) { // Resolve the role (and active status) from the database on every request // rather than trusting the token. A token's `role` claim is frozen at login, // so a role change or deactivation would otherwise stay ineffective until the // token expires. `payload.sub` is the impersonated user on impersonation // tokens, so this keeps "login as user" working with the target's live role. const user = await this.usersService.findById(payload.sub); if (!user || !user.isActive) { throw new UnauthorizedException(); } return { id: user.id, email: user.email ?? user.phone, role: user.role, // Non-null only while an admin is impersonating this user. impersonatedBy: payload.act?.sub ?? null, impersonatorRole: payload.act?.role ?? null, }; } }