Files
ApiCore/ApiCore-Backend/src/auth/auth.controller.ts
T
Nick SzaunigandClaude Sonnet 5 19d5e5d65c fix: PermissionsGuard schliesst jetzt statt bei fehlendem Decorator zu oeffnen
PermissionsGuard erlaubte bisher jede Route ohne @RequirePermission
automatisch fuer jeden eingeloggten Nutzer (fail-open) und verliess sich
allein auf test/authorization-coverage.e2e-spec.ts, um eine vergessene
Decorator-Angabe vor dem Deploy zu erwischen - der Guard selbst hatte
kein Default-Deny.

Neuer Decorator @AnyAuthenticatedUser() macht das bisher stillschweigend
erlaubte Verhalten explizit fuer die 12 Routen, die es tatsaechlich
brauchen (ProfileController komplett, NotificationsController komplett,
AuthController.me - alle bereits vorher auf der Allowlist des e2e-Tests).
PermissionsGuard verweigert jetzt jede Route, die weder
@RequirePermission noch @AnyAuthenticatedUser traegt - eine vergessene
Decorator-Angabe schlaegt jetzt zur Laufzeit fehl (403), nicht mehr nur
im Test. Der e2e-Test prueft jetzt denselben Decorator-Vertrag statt
einer separat gepflegten Allowlist, kann also nicht mehr vom
Guard-Verhalten abweichen.

Neue Unit-Tests fuer PermissionsGuard decken beide Codepfade und das
Fail-Closed-Verhalten ab.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-02 14:44:28 +02:00

102 lines
2.9 KiB
TypeScript

import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Post,
Req,
Res,
} from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';
import type { Request, Response } from 'express';
import { Public } from './decorators/public.decorator';
import { AnyAuthenticatedUser } from './decorators/any-authenticated-user.decorator';
import { CurrentUser } from './decorators/current-user.decorator';
import type { AuthenticatedUser } from './types/authenticated-user';
import { LoginDto } from './dto/login.dto';
import { AccessTokenDto } from './dto/access-token.dto';
import { MfaVerifyDto } from './dto/mfa-verify.dto';
import { AuthService, CurrentUserProfile } from './auth.service';
import { SENSITIVE_ACTION_THROTTLE } from './util/throttle';
import {
clearRefreshTokenCookie,
extractRefreshToken,
setRefreshTokenCookie,
} from './util/refresh-token-cookie';
export interface MfaRequiredResponse {
mfaRequired: true;
mfaToken: string;
}
@ApiTags('auth')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Public()
@Throttle(SENSITIVE_ACTION_THROTTLE)
@Post('login')
@HttpCode(HttpStatus.OK)
async login(
@Body() dto: LoginDto,
@Res({ passthrough: true }) res: Response,
): Promise<AccessTokenDto | MfaRequiredResponse> {
const result = await this.authService.login(dto.email, dto.password);
if (result.mfaRequired) {
return { mfaRequired: true, mfaToken: result.mfaToken };
}
setRefreshTokenCookie(res, result.refreshToken);
return { accessToken: result.accessToken };
}
@Public()
@Throttle(SENSITIVE_ACTION_THROTTLE)
@Post('mfa/verify')
@HttpCode(HttpStatus.OK)
async verifyMfa(
@Body() dto: MfaVerifyDto,
@Res({ passthrough: true }) res: Response,
): Promise<AccessTokenDto> {
const { accessToken, refreshToken } =
await this.authService.verifyMfaAndLogin(dto.mfaToken, dto.code);
setRefreshTokenCookie(res, refreshToken);
return { accessToken };
}
@Public()
@Throttle(SENSITIVE_ACTION_THROTTLE)
@Post('refresh')
@HttpCode(HttpStatus.OK)
async refresh(
@Req() req: Request,
@Res({ passthrough: true }) res: Response,
): Promise<AccessTokenDto> {
const { accessToken, refreshToken } = await this.authService.refresh(
extractRefreshToken(req),
);
setRefreshTokenCookie(res, refreshToken);
return { accessToken };
}
@Public()
@Post('logout')
@HttpCode(HttpStatus.NO_CONTENT)
async logout(
@Req() req: Request,
@Res({ passthrough: true }) res: Response,
): Promise<void> {
await this.authService.logout(extractRefreshToken(req));
clearRefreshTokenCookie(res);
}
@ApiBearerAuth()
@AnyAuthenticatedUser()
@Get('me')
me(@CurrentUser() user: AuthenticatedUser): Promise<CurrentUserProfile> {
return this.authService.getCurrentUserProfile(user.id);
}
}