import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { ConfigService } from '@nestjs/config'; import { PrismaService } from '../../prisma/prisma.service'; export const ACCESS_TOKEN_COOKIE = 'ir_at'; export interface AuthenticatedUser { id: bigint; username: string; role: string; } /** * Shape of the JWT we issue. * * `sub` is the user ID as a string (bigints are serialized to strings in JSON). * `typ` distinguishes access tokens from refresh tokens; `tv` is the user's * tokenVersion and `role` drives the RolesGuard. */ export interface JwtPayload { sub: string; username: string; role?: string; tv?: number; typ?: 'access' | 'refresh'; } @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor( config: ConfigService, private readonly prisma: PrismaService, ) { const secret = config.get('JWT_SECRET'); if (!secret) { throw new Error('JWT_SECRET is not configured'); } if (secret.length < 32) { throw new Error('JWT_SECRET must be at least 32 characters'); } super({ // Access tokens are accepted from the HttpOnly cookie (browser) or // the Authorization header (non-browser API clients). jwtFromRequest: ExtractJwt.fromExtractors([ ExtractJwt.fromAuthHeaderAsBearerToken(), (req) => req?.cookies?.[ACCESS_TOKEN_COOKIE] ?? null, ]), ignoreExpiration: false, secretOrKey: secret, }); } /** * Runs on every authenticated request. The returned object becomes * `request.user` for downstream controllers. The user and its * tokenVersion are re-checked in the database so tokens of deleted * users, logged-out users, or refresh tokens stop working immediately. */ async validate(payload: JwtPayload): Promise { if (!payload?.sub || !payload.username) { throw new UnauthorizedException('Invalid token payload'); } // Refresh tokens must never be accepted as API credentials. if (payload.typ === 'refresh') { throw new UnauthorizedException('Invalid token type'); } const user = await this.prisma.user .findUnique({ where: { id: BigInt(payload.sub) } }) .catch(() => null); if ( !user || user.username !== payload.username || (payload.tv !== undefined && user.tokenVersion !== payload.tv) ) { throw new UnauthorizedException('Invalid token'); } return { id: user.id, username: user.username, role: user.role }; } }