- Add User.role (enum Role/ADMIN) and User.tokenVersion with migration - Login now issues short-lived access token (30m default) + 7d refresh token, both embedding tokenVersion and a typ discriminator - Tokens delivered via HttpOnly SameSite cookies (ir_at, ir_rt scoped to /auth); refresh token never leaves the cookie - New endpoints: POST /auth/refresh (rotation), GET /auth/me, POST /auth/logout (bumps tokenVersion, revoking all tokens) - JWT strategy accepts bearer or cookie, rejects refresh tokens, and verifies tokenVersion + user existence on every request - Global RolesGuard: authenticated routes require ADMIN unless widened via @Roles(...) - Admin SPA: session fully cookie-based, no token in localStorage; router guard restores session via /auth/me; axios auto-refreshes once on 401; stale localStorage keys cleaned up
34 lines
1.2 KiB
TypeScript
34 lines
1.2 KiB
TypeScript
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
|
|
import { Reflector } from '@nestjs/core';
|
|
import { ROLES_KEY } from '../decorators/roles.decorator';
|
|
import type { AuthenticatedUser } from '../strategies/jwt.strategy';
|
|
|
|
/**
|
|
* Role-based access control. Applied globally: any authenticated user
|
|
* reaching a protected route must hold the ADMIN role unless the route
|
|
* declares a wider set with @Roles(...). Routes without a JwtAuthGuard
|
|
* (public endpoints) have no `request.user` and are skipped here — their
|
|
* openness is decided by the controller's own guards.
|
|
*/
|
|
@Injectable()
|
|
export class RolesGuard implements CanActivate {
|
|
constructor(private readonly reflector: Reflector) {}
|
|
|
|
canActivate(context: ExecutionContext): boolean {
|
|
const request = context.switchToHttp().getRequest<{
|
|
user?: AuthenticatedUser;
|
|
}>();
|
|
if (!request.user) {
|
|
return true; // public route — no JwtAuthGuard in front
|
|
}
|
|
const required = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
|
|
context.getHandler(),
|
|
context.getClass(),
|
|
]) ?? ['ADMIN'];
|
|
if (!required.includes(request.user.role)) {
|
|
throw new ForbiddenException('Insufficient role');
|
|
}
|
|
return true;
|
|
}
|
|
}
|