69 lines
2.0 KiB
TypeScript
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 { mapPrismaUserToUserProfile } from "@bff/infra/utils/user-mapper.util";
|
|
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 = mapPrismaUserToUserProfile(prismaUser);
|
|
|
|
return profile;
|
|
}
|
|
}
|