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
+14 -13
View File
@@ -21,6 +21,7 @@ import * as path from 'node:path';
import { Response } from 'express';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { Roles } from '../auth/decorators/roles.decorator';
import type { AuthUser, UploadedFileLike } from '../auth/types/auth-user';
import { RolesGuard } from '../auth/guards/roles.guard';
import { forTenant } from '../prisma/prisma-tenant.extension';
import { PrismaService } from '../prisma/prisma.service';
@@ -65,7 +66,7 @@ export class UserController {
* uebergreifende Sicht von der mandantengebundenen unterscheidet, und sie
* darf nicht eingeebnet werden.
*/
private async resolveTargetUser(currentUser: any, id: string) {
private async resolveTargetUser(currentUser: AuthUser, id: string) {
if (currentUser.role === Role.SUPER_ADMIN) {
return this.userService.findByIdForPlatformAdmin(id);
}
@@ -79,7 +80,7 @@ export class UserController {
*/
@Get()
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async findAll(@CurrentUser() currentUser: any) {
async findAll(@CurrentUser() currentUser: AuthUser) {
if (currentUser.role === Role.SUPER_ADMIN) {
// Plattform-Administratorsicht (Befund F): die bestehende, gewollte
// Funktion der obersten Rolle bleibt erhalten, laeuft aber ueber die
@@ -115,7 +116,7 @@ export class UserController {
*/
@Get(':id')
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async findOne(@Param('id') id: string, @CurrentUser() currentUser: any) {
async findOne(@Param('id') id: string, @CurrentUser() currentUser: AuthUser) {
const user = await this.resolveTargetUser(currentUser, id);
if (!user) {
throw new NotFoundException('User not found');
@@ -139,7 +140,7 @@ export class UserController {
*/
@Post()
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async create(@Body() dto: CreateUserDto, @CurrentUser() currentUser: any) {
async create(@Body() dto: CreateUserDto, @CurrentUser() currentUser: AuthUser) {
// ADMIN can only create users in own tenant
const tenantId =
currentUser.role === Role.SUPER_ADMIN && dto.tenantId
@@ -173,7 +174,7 @@ export class UserController {
async update(
@Param('id') id: string,
@Body() dto: UpdateUserDto,
@CurrentUser() currentUser: any,
@CurrentUser() currentUser: AuthUser,
) {
const user = await this.resolveTargetUser(currentUser, id);
if (!user) {
@@ -228,7 +229,7 @@ export class UserController {
*/
@Delete(':id')
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async remove(@Param('id') id: string, @CurrentUser() currentUser: any) {
async remove(@Param('id') id: string, @CurrentUser() currentUser: AuthUser) {
const user = await this.resolveTargetUser(currentUser, id);
if (!user) {
throw new NotFoundException('User not found');
@@ -290,14 +291,14 @@ export class UserController {
FileInterceptor('file', { limits: { fileSize: 2 * 1024 * 1024 } }),
)
async uploadAvatar(
@UploadedFile() file: any,
@CurrentUser() currentUser: any,
@UploadedFile() file: UploadedFileLike | undefined,
@CurrentUser() currentUser: AuthUser,
) {
if (!file?.buffer) {
throw new BadRequestException('No file provided');
}
const ext = AVATAR_MIME_TO_EXT[file.mimetype as string];
const ext = AVATAR_MIME_TO_EXT[file.mimetype];
if (!ext) {
throw new BadRequestException(
'Invalid file type. Allowed: image/png, image/jpeg, image/webp',
@@ -318,7 +319,7 @@ export class UserController {
}
}
fs.writeFileSync(filePath, file.buffer as Buffer);
fs.writeFileSync(filePath, file.buffer);
// Persist relative path (relative to monorepo root)
const relativePath = path.join('user-files', 'avatars', filename);
@@ -336,7 +337,7 @@ export class UserController {
* Remove the current user's profile picture.
*/
@Delete('me/avatar')
async deleteAvatar(@CurrentUser() currentUser: any) {
async deleteAvatar(@CurrentUser() currentUser: AuthUser) {
const tenantPrisma = forTenant(this.prisma, currentUser.tenantId);
const user = await tenantPrisma.user.findUnique({
where: { id: currentUser.id },
@@ -366,7 +367,7 @@ export class UserController {
@Patch('me/accent-color')
async updateAccentColor(
@Body() body: { color: string | null },
@CurrentUser() currentUser: any,
@CurrentUser() currentUser: AuthUser,
) {
if (body.color !== null && body.color !== undefined && !/^#[0-9a-fA-F]{6}$/.test(body.color)) {
throw new BadRequestException('Invalid color format. Use hex (#rrggbb).');
@@ -388,7 +389,7 @@ export class UserController {
*/
@Get('me/avatar')
async getAvatar(
@CurrentUser() currentUser: any,
@CurrentUser() currentUser: AuthUser,
@Res() res: Response,
) {
const tenantPrisma = forTenant(this.prisma, currentUser.tenantId);