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]>
102 lines
2.9 KiB
TypeScript
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);
|
|
}
|
|
}
|