authentication authorization

This commit is contained in:
2025-12-26 15:32:22 +01:00
parent 68ae9da22a
commit 64377de548
54 changed files with 2833 additions and 95 deletions

View File

@@ -1,6 +1,17 @@
import 'reflect-metadata';
import { Reflector } from '@nestjs/core';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { vi } from 'vitest';
import { DashboardController } from './DashboardController';
import { DashboardOverviewDTO } from './dtos/DashboardOverviewDTO';
import { AuthenticationGuard } from '../auth/AuthenticationGuard';
import { AuthorizationGuard } from '../auth/AuthorizationGuard';
import type { AuthorizationService } from '../auth/AuthorizationService';
import { FeatureAvailabilityGuard } from '../policy/FeatureAvailabilityGuard';
import type { PolicyService, PolicySnapshot } from '../policy/PolicyService';
import { DashboardService } from './DashboardService';
describe('DashboardController', () => {
let controller: DashboardController;
@@ -34,10 +45,93 @@ describe('DashboardController', () => {
};
mockService.getDashboardOverview.mockResolvedValue(overview);
const result = await controller.getDashboardOverview(driverId);
const result = await controller.getDashboardOverview(driverId, { user: { userId: driverId } });
expect(mockService.getDashboardOverview).toHaveBeenCalledWith(driverId);
expect(result).toEqual(overview);
});
});
describe('auth guards (HTTP)', () => {
let app: any;
const sessionPort: { getCurrentSession: () => Promise<null | { token: string; user: { id: string } }> } = {
getCurrentSession: vi.fn(async () => null),
};
const authorizationService: Pick<AuthorizationService, 'getRolesForUser'> = {
getRolesForUser: vi.fn(() => []),
};
const policyService: Pick<PolicyService, 'getSnapshot'> = {
getSnapshot: vi.fn(async (): Promise<PolicySnapshot> => ({
policyVersion: 1,
operationalMode: 'normal',
maintenanceAllowlist: { view: [], mutate: [] },
capabilities: {},
loadedFrom: 'defaults',
loadedAtIso: new Date(0).toISOString(),
})),
};
beforeEach(async () => {
const module = await Test.createTestingModule({
controllers: [DashboardController],
providers: [
{
provide: DashboardService,
useValue: {
getDashboardOverview: vi.fn(async () => ({
currentDriver: null,
myUpcomingRaces: [],
otherUpcomingRaces: [],
upcomingRaces: [],
activeLeaguesCount: 0,
nextRace: null,
recentResults: [],
leagueStandingsSummaries: [],
feedSummary: { notificationCount: 0, items: [] },
friends: [],
})),
},
},
],
})
.overrideProvider(DashboardController)
.useFactory({
factory: (dashboardService: DashboardService) => new DashboardController(dashboardService),
inject: [DashboardService],
})
.compile();
app = module.createNestApplication();
const reflector = new Reflector();
app.useGlobalGuards(
new AuthenticationGuard(sessionPort as any),
new AuthorizationGuard(reflector, authorizationService as any),
new FeatureAvailabilityGuard(reflector, policyService as any),
);
await app.init();
});
afterEach(async () => {
await app?.close();
vi.clearAllMocks();
});
it('denies endpoint by default when not authenticated (401)', async () => {
await request(app.getHttpServer()).get('/dashboard/overview?driverId=d1').expect(401);
});
it('allows endpoint when authenticated via session port', async () => {
vi.mocked(sessionPort.getCurrentSession).mockResolvedValueOnce({
token: 't',
user: { id: 'user-1' },
});
await request(app.getHttpServer()).get('/dashboard/overview?driverId=d1').expect(200);
});
});
});

View File

@@ -1,18 +1,29 @@
import { Controller, Get, Query } from '@nestjs/common';
import { Controller, Get, Query, Req, UnauthorizedException, Inject } from '@nestjs/common';
import { ApiTags, ApiResponse, ApiOperation, ApiQuery } from '@nestjs/swagger';
import { DashboardService } from './DashboardService';
import { DashboardOverviewDTO } from './dtos/DashboardOverviewDTO';
type AuthenticatedRequest = {
user?: { userId: string };
};
@ApiTags('dashboard')
@Controller('dashboard')
export class DashboardController {
constructor(private readonly dashboardService: DashboardService) {}
constructor(@Inject(DashboardService) private readonly dashboardService: DashboardService) {}
@Get('overview')
@ApiOperation({ summary: 'Get dashboard overview' })
@ApiQuery({ name: 'driverId', description: 'Driver ID' })
@ApiResponse({ status: 200, description: 'Dashboard overview', type: DashboardOverviewDTO })
async getDashboardOverview(@Query('driverId') driverId: string): Promise<DashboardOverviewDTO> {
return this.dashboardService.getDashboardOverview(driverId);
async getDashboardOverview(
@Query('driverId') _driverId: string,
@Req() req: AuthenticatedRequest,
): Promise<DashboardOverviewDTO> {
const userId = req.user?.userId;
if (!userId) {
throw new UnauthorizedException('Unauthorized');
}
return this.dashboardService.getDashboardOverview(userId);
}
}