0b34e82b21
JwtStrategy.validate las bisher alles aus dem 30-Tage-Token: ein herabgestufter Administrator behielt seine Rechte bis zum Ablauf, ein deaktiviertes oder geloeschtes Konto arbeitete mit seiner Sitzung weiter, und Oberflaeche (/auth/me aus der DB) und API (Token) sahen verschiedene Rollen – die Benutzerliste scheiterte nach einer Rollenaenderung (Befund des Nutzers auf alpha). Jetzt ein gebundener PK-Lesezugriff je Anfrage (forTenant), 401 bei fehlendem, deaktiviertem oder mandantenfremdem Konto. Lokal nachgewiesen: Herabstufen -> sofort 403, Deaktivieren -> sofort 401. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
76 lines
2.6 KiB
TypeScript
76 lines
2.6 KiB
TypeScript
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<string>('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<AuthUser> {
|
|
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,
|
|
};
|
|
}
|
|
}
|