fix(auth): enforce live role + active status from DB on every request

JwtStrategy read `role` straight from the JWT payload, so a role change
or deactivation stayed ineffective until the 1h access token expired:
after a promotion the menus showed (via /users/me) but admin endpoints
returned 403 because RolesGuard still saw the old token role; after a
demotion the old admin kept API access. Load the user from the DB in
validate() and use the current role; reject inactive users.

Frontend: poll /users/me in the dashboard layout (+ on window focus) so
the sidebar reflects role changes without a hard reload, and redirect
away from pages the new role can no longer reach.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
keyhan
2026-06-18 18:55:24 +03:30
parent 04b2f040e0
commit 8dc351ab21
2 changed files with 68 additions and 6 deletions
+19 -5
View File
@@ -1,7 +1,8 @@
import { Injectable } from '@nestjs/common';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { UsersService } from '../../users/users.service';
interface JwtPayload {
sub: string;
@@ -13,7 +14,10 @@ interface JwtPayload {
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(configService: ConfigService) {
constructor(
configService: ConfigService,
private readonly usersService: UsersService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
@@ -22,10 +26,20 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
}
async validate(payload: JwtPayload) {
// Resolve the role (and active status) from the database on every request
// rather than trusting the token. A token's `role` claim is frozen at login,
// so a role change or deactivation would otherwise stay ineffective until the
// token expires. `payload.sub` is the impersonated user on impersonation
// tokens, so this keeps "login as user" working with the target's live role.
const user = await this.usersService.findById(payload.sub);
if (!user || !user.isActive) {
throw new UnauthorizedException();
}
return {
id: payload.sub,
email: payload.email,
role: payload.role,
id: user.id,
email: user.email ?? user.phone,
role: user.role,
// Non-null only while an admin is impersonating this user.
impersonatedBy: payload.act?.sub ?? null,
impersonatorRole: payload.act?.role ?? null,