refactor(quick-260921-m34): Aufgabe 2b - getypte Anfrage in elf Controllern, zwei Befunde gemeldet

(req as any) und @Req() req: any durch AuthenticatedRequest ersetzt in
dashboard, favorites, calendar, groups, module-grants, module-registry,
tenders, dkv, ldap, settings; @CurrentUser() in user.controller auf AuthUser.
Die abwehrenden Pruefungen ("No tenant context", "No user context") bleiben
lebendig, weil user auf dem Anfragetyp wahlfrei ist - genau das beschreibt
den Zustand auf oeffentlichen Wegen.

Nebengewinn ohne neue Zusicherungen: req.tenantId as string | undefined
(dkv, settings), file.buffer as Buffer und file.mimetype as string
(dkv, user) sind weggefallen, weil der Typ sie jetzt traegt.

BEFUND 1 (D-03, gemeldet) dashboard.controller.ts:74 alt: der Handler las
req.user?.role NACH extractContext und gab sie an getWidgets(role: Role)
weiter, das eine Rolle zwingend verlangt. Die Annahme "hier gibt es immer
einen Aufrufer" stimmt - die Pruefung "No user context" erzwingt sie -, aber
sie stand in einer anderen Methode, wo der Compiler sie nicht sehen konnte.
extractContext gibt die Rolle jetzt mit zurueck: keine neue Pruefung, kein
erfundener Wert, gleiche Reihenfolge, gleiche Meldungen.

BEFUND 2 (D-03, gemeldet) tenders.controller.ts:142: resolveRequestingTenantId
erklaerte string | undefined, liest aber req.tenantId, das TenantGuard fuer
einen SUPER_ADMIN ohne Mandanten auf null setzt. Die Erklaerung war also nie
vollstaendig. Erweitert auf string | null | undefined, und buildTenderWhere
nimmt string | null - beides nur Erklaerung, kein Verhalten: die Funktion
entscheidet seit jeher ueber Wahrheitswert und faellt bei beiden zu
(nur global sichtbare Ausschreibungen).

Fixtures in user.controller.spec.ts ergaenzt (username, mustChangePassword,
originalname, size). Testzahlen unveraendert.

noExplicitAny in apps/api/src: 137 -> 66. type-check 4/4, lint 5/5,
apps/api 72/1143, apps/web 73/531, tenant.guard.ts unveraendert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
This commit is contained in:
2026-09-21 17:08:44 +02:00
parent f2fc39f51c
commit 7c9d7c1223
13 changed files with 182 additions and 155 deletions
+21 -16
View File
@@ -17,6 +17,11 @@ import {
import { FileInterceptor } from '@nestjs/platform-express';
import { Role } from '@prisma/client';
import { Roles } from '../auth/decorators/roles.decorator';
import type {
AuthenticatedRequest,
UploadedFileLike,
} from '../auth/types/auth-user';
import type { Response } from 'express';
import { DkvSchedulerService } from './dkv-scheduler.service';
import { DkvService } from './dkv.service';
import { DkvConfigDto } from './dto/dkv-config.dto';
@@ -58,7 +63,7 @@ export class DkvController {
/** GET /dkv/config — returns module config with username + hasPassword. 404 when not yet configured. */
@Get('config')
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async getConfig(@Req() req: any) {
async getConfig(@Req() req: AuthenticatedRequest) {
const tenantId = this._requireTenant(req);
const config = await this.dkvService.getConfigForApi(tenantId);
if (!config) {
@@ -75,7 +80,7 @@ export class DkvController {
*/
@Put('config')
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async saveConfig(@Req() req: any, @Body() dto: DkvConfigDto) {
async saveConfig(@Req() req: AuthenticatedRequest, @Body() dto: DkvConfigDto) {
const tenantId = this._requireTenant(req);
const result = await this.dkvService.saveConfig(tenantId, dto);
@@ -94,7 +99,7 @@ export class DkvController {
/** POST /dkv/check-now — immediately run the inbox processing pipeline. */
@Post('check-now')
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async checkNow(@Req() req: any) {
async checkNow(@Req() req: AuthenticatedRequest) {
const tenantId = this._requireTenant(req);
return this.dkvService.checkNow(tenantId);
}
@@ -105,7 +110,7 @@ export class DkvController {
*/
@Post('test-connection')
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async testConnection(@Req() req: any, @Body() dto: DkvConfigDto) {
async testConnection(@Req() req: AuthenticatedRequest, @Body() dto: DkvConfigDto) {
const tenantId = this._requireTenant(req);
return this.dkvService.testConnection(tenantId, dto);
}
@@ -118,7 +123,7 @@ export class DkvController {
*/
@Get('history')
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async getHistory(@Req() req: any, @Query() query: DkvHistoryQueryDto) {
async getHistory(@Req() req: AuthenticatedRequest, @Query() query: DkvHistoryQueryDto) {
const tenantId = this._requireTenant(req);
const page = query.page ?? 1;
const limit = query.limit ?? 20;
@@ -137,9 +142,9 @@ export class DkvController {
@Get('exports/:filename')
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async downloadExport(
@Req() req: any,
@Req() req: AuthenticatedRequest,
@Param('filename') filename: string,
@Res() res: any,
@Res() res: Response,
) {
const tenantId = this._requireTenant(req);
@@ -164,7 +169,7 @@ export class DkvController {
/** GET /dkv/vehicles — list all vehicle master records for this tenant. */
@Get('vehicles')
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async listVehicles(@Req() req: any) {
async listVehicles(@Req() req: AuthenticatedRequest) {
const tenantId = this._requireTenant(req);
return this.dkvService.listVehicles(tenantId);
}
@@ -172,7 +177,7 @@ export class DkvController {
/** POST /dkv/vehicles — create a new vehicle master record. */
@Post('vehicles')
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async createVehicle(@Req() req: any, @Body() dto: CreateVehicleDto) {
async createVehicle(@Req() req: AuthenticatedRequest, @Body() dto: CreateVehicleDto) {
const tenantId = this._requireTenant(req);
return this.dkvService.createVehicle(tenantId, dto);
}
@@ -181,7 +186,7 @@ export class DkvController {
@Put('vehicles/:id')
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async updateVehicle(
@Req() req: any,
@Req() req: AuthenticatedRequest,
@Param('id') id: string,
@Body() dto: UpdateVehicleDto,
) {
@@ -192,7 +197,7 @@ export class DkvController {
/** DELETE /dkv/vehicles/:id — delete a vehicle master record. */
@Delete('vehicles/:id')
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async deleteVehicle(@Req() req: any, @Param('id') id: string) {
async deleteVehicle(@Req() req: AuthenticatedRequest, @Param('id') id: string) {
const tenantId = this._requireTenant(req);
return this.dkvService.deleteVehicle(tenantId, id);
}
@@ -213,8 +218,8 @@ export class DkvController {
limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB — generous for any realistic vehicle list (WR-05)
}))
async importVehicles(
@Req() req: any,
@UploadedFile() file: any,
@Req() req: AuthenticatedRequest,
@UploadedFile() file: UploadedFileLike | undefined,
@Body('mode') mode: string,
) {
const tenantId = this._requireTenant(req);
@@ -223,7 +228,7 @@ export class DkvController {
throw new BadRequestException('No CSV file uploaded (field name must be "file")');
}
const csvText = (file.buffer as Buffer).toString('utf-8');
const csvText = file.buffer.toString('utf-8');
const importMode = mode === 'replace' ? 'replace' : 'merge';
return this.dkvService.importVehiclesCsv(tenantId, csvText, importMode);
@@ -232,8 +237,8 @@ export class DkvController {
// ─── Private helpers ───────────────────────────────────────────────────────
/** Extract and validate tenantId from request; throw BadRequestException when absent. */
private _requireTenant(req: any): string {
const tenantId = req.tenantId as string | undefined;
private _requireTenant(req: AuthenticatedRequest): string {
const tenantId = req.tenantId;
if (!tenantId) {
throw new BadRequestException('No tenant context');
}