import { Injectable, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { AuthService } from './auth.service'; /** * JWT Payload interface matching the token structure */ export interface JwtPayload { sub: string; // user ID email: string; role: string; organizationId: string; type: 'access' | 'refresh'; iat?: number; // issued at exp?: number; // expiration } /** * JWT Strategy for Passport authentication * * This strategy: * - Extracts JWT from Authorization Bearer header * - Validates the token signature using the secret * - Validates the payload and retrieves the user * - Injects the user into the request object * * @see https://docs.nestjs.com/security/authentication#implementing-passport-jwt */ @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor( private readonly configService: ConfigService, private readonly authService: AuthService, ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: configService.get('JWT_SECRET'), }); } /** * Validate JWT payload and return user object * * This method is called automatically by Passport after the JWT is verified. * If this method throws an error or returns null/undefined, authentication fails. * * @param payload - Decoded JWT payload * @returns User object to be attached to request.user * @throws UnauthorizedException if user is invalid or inactive */ async validate(payload: JwtPayload) { // Only accept access tokens (not refresh tokens) if (payload.type !== 'access') { throw new UnauthorizedException('Invalid token type'); } // Validate user exists and is active const user = await this.authService.validateUser(payload); if (!user) { throw new UnauthorizedException('User not found or inactive'); } // This object will be attached to request.user return { id: user.id, email: user.email, role: user.role, organizationId: user.organizationId, firstName: user.firstName, lastName: user.lastName, }; } }