import { Injectable, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; import { Strategy } from 'passport-jwt'; import { Request } from 'express'; import { PrismaService } from '../../prisma/prisma.service'; import { forTenant } from '../../prisma/prisma-tenant.extension'; import type { AuthUser, JwtPayload } from '../types/auth-user'; /** * Custom extractor that reads JWT from the httpOnly "session" cookie. */ function cookieExtractor(req: Request): string | null { if (req?.cookies) { return req.cookies.session || null; } return null; } @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor( configService: ConfigService, private readonly prisma: PrismaService, ) { super({ jwtFromRequest: cookieExtractor, ignoreExpiration: false, secretOrKey: configService.get('JWT_SECRET', 'fallback-secret'), }); } /** * Das Token beweist nur, WER angemeldet ist — Rolle, Aktiv-Status und * Kennwort-Pflicht kommen bei JEDER Anfrage frisch aus der Datenbank * (quick-260930, Befund des Nutzers): vorher galt die Rolle aus dem * 30-Tage-Token. Ein herabgestufter Administrator behielt bis zum Ablauf * seine alten Rechte, ein deaktiviertes oder geloeschtes Konto (etwa per * LDAP-Abgleich beim Austritt) arbeitete mit seiner Sitzung weiter, und * Oberflaeche (liest die Rolle ueber /auth/me aus der Datenbank) und API * (las sie aus dem Token) sahen verschiedene Rollen — die Benutzerliste * scheiterte dann im Client. * * Ein Primaerschluessel-Lesezugriff je Anfrage, gebunden an den Mandanten * aus dem Token (`forTenant`); gehoert das Konto nicht (mehr) zu diesem * Mandanten, fehlt es oder ist es deaktiviert, gilt die Sitzung als * ungueltig (401) — die Web-Oberflaeche leitet dann zur Anmeldung. */ async validate(payload: JwtPayload): Promise { const tenantPrisma = forTenant(this.prisma, payload.tenantId); const user = await tenantPrisma.user.findUnique({ where: { id: payload.sub }, select: { id: true, username: true, role: true, tenantId: true, isActive: true, mustChangePassword: true, }, }); if (!user || !user.isActive || user.tenantId !== payload.tenantId) { throw new UnauthorizedException(); } return { id: user.id, username: user.username, role: user.role as AuthUser['role'], tenantId: user.tenantId, mustChangePassword: user.mustChangePassword === true, }; } }