69 lines
2.0 KiB
TypeScript

import { Injectable, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";
import { ConfigService } from "@nestjs/config";
import type { AuthenticatedUser } from "@customer-portal/domain/auth";
import { UsersService } from "@bff/modules/users/users.service";
import { mapPrismaUserToDomain } from "@bff/infra/mappers";
import type { Request } from "express";
const cookieExtractor = (req: Request): string | null => {
const cookieSource: unknown = Reflect.get(req, "cookies");
if (!cookieSource || typeof cookieSource !== "object") {
return null;
}
const token = Reflect.get(cookieSource, "access_token") as unknown;
return typeof token === "string" && token.length > 0 ? token : null;
};
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private configService: ConfigService,
private readonly usersService: UsersService
) {
const jwtSecret = configService.get<string>("JWT_SECRET");
if (!jwtSecret) {
throw new Error("JWT_SECRET is required in environment variables");
}
const options = {
jwtFromRequest: ExtractJwt.fromExtractors([
ExtractJwt.fromAuthHeaderAsBearerToken(),
cookieExtractor,
]),
ignoreExpiration: false,
secretOrKey: jwtSecret,
};
super(options);
}
async validate(payload: {
sub: string;
email: string;
role: string;
iat?: number;
exp?: number;
}): Promise<AuthenticatedUser> {
// Validate payload structure
if (!payload.sub || !payload.email) {
throw new Error("Invalid JWT payload");
}
const prismaUser = await this.usersService.findByIdInternal(payload.sub);
if (!prismaUser) {
throw new UnauthorizedException("User not found");
}
if (prismaUser.email !== payload.email) {
throw new UnauthorizedException("Token subject does not match user record");
}
const profile = mapPrismaUserToDomain(prismaUser);
return profile;
}
}