- 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
33 lines
967 B
TypeScript
33 lines
967 B
TypeScript
import { ForbiddenException } from '@nestjs/common';
|
|
import { Reflector } from '@nestjs/core';
|
|
import { RolesGuard } from './roles.guard';
|
|
|
|
describe('RolesGuard', () => {
|
|
let guard: RolesGuard;
|
|
|
|
beforeEach(() => {
|
|
guard = new RolesGuard(new Reflector());
|
|
});
|
|
|
|
const context = (user: unknown, roles?: string[]) =>
|
|
({
|
|
switchToHttp: () => ({ getRequest: () => ({ user }) }),
|
|
getHandler: () => (roles ? { __roles: roles } : {}),
|
|
getClass: () => ({}),
|
|
}) as never;
|
|
|
|
it('passes public routes (no authenticated user)', () => {
|
|
expect(guard.canActivate(context(undefined))).toBe(true);
|
|
});
|
|
|
|
it('passes ADMIN users by default', () => {
|
|
expect(guard.canActivate(context({ id: 1n, username: 'a', role: 'ADMIN' }))).toBe(true);
|
|
});
|
|
|
|
it('blocks users without the ADMIN role', () => {
|
|
expect(() => guard.canActivate(context({ id: 1n, username: 'a', role: 'VIEWER' }))).toThrow(
|
|
ForbiddenException,
|
|
);
|
|
});
|
|
});
|