From 3892c5f3c6a9bf9a6955db9ea8f86321b15d2531 Mon Sep 17 00:00:00 2001 From: Schalli Date: Mon, 21 Sep 2026 17:21:53 +0200 Subject: [PATCH] refactor(quick-260921-m34): Aufgabe 3b - Prisma-nahe Formen getypt, Erkenner-Falle gemeldet - auth.service.ts:393: (response as any).cookie war schlicht ueberfluessig. response ist in derselben Signatur bereits Response aus express, die Schwesterstelle :190 kommt ohne Zusicherung aus. Ersatzlos entfernt. - calendar.service.ts: das lokal gebaute data-Objekt traegt jetzt Prisma.CalendarSourceUncheckedCreateInput bzw. ...UncheckedUpdateInput statt Record plus Zusicherung. Damit fallen beide `data as any` weg, ohne dass ein Feld behauptet wird. - user.service.ts: `let created: any` -> User (die Zuweisung steht im try, der catch endet ausnahmslos mit throw). `const updateData: any` wird aus der Signatur hergeleitet - Omit plus dem daraus berechneten passwordHash; die Parameterform ist dafuer als UpdateUserInput benannt und nicht neu erfunden. `const results: any[]` wird Pick[], die Spaltenauswahl steht als Konstante daneben. - tenant.controller.ts:69: Elementtyp aus dem hergeleitet, was die Schleife hineinlegt (fuenf Tenant-Spalten plus userCount). BEFUND 3 (D-03, gemeldet, kein Verhalten betroffen) Die naheliegende Prisma-Schreibweise Prisma.UserGetPayload<{ select: typeof X }> laesst rls-access-inventory.spec.ts rot werden: der Erkenner zaehlt JEDE select:-Angabe ausserhalb eines erkannten Modellaufrufs als Verstoss und unterscheidet Typposition nicht von Aufrufposition. Gemessen beim ersten Versuch. Der Erkenner ist die Mandantenkontrolle (T-M34-03) und wurde NICHT aufgeweicht - stattdessen leitet der Zeilentyp ueber Pick her, was ohne das Wort select auskommt. Begruendung steht am Typ. noExplicitAny in apps/api/src: 38 -> 31. type-check 4/4, lint 5/5 (0 error), apps/api 72/1143, apps/web 73/531, rls-access-inventory 30/30. noNonNullAssertion 56, as unknown as 33, Unterdrueckungsmarker 1. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J --- apps/api/src/auth/auth.service.ts | 2 +- apps/api/src/calendar/calendar.service.ts | 9 +-- apps/api/src/tenant/tenant.controller.ts | 10 ++- apps/api/src/user/user.service.ts | 85 ++++++++++++++++------- 4 files changed, 72 insertions(+), 34 deletions(-) diff --git a/apps/api/src/auth/auth.service.ts b/apps/api/src/auth/auth.service.ts index 07bef7d..07e98c4 100644 --- a/apps/api/src/auth/auth.service.ts +++ b/apps/api/src/auth/auth.service.ts @@ -390,7 +390,7 @@ export class AuthService { mustChangePassword: false, }; const token = this.jwtService.sign(payload); - (response as any).cookie('session', token, { + response.cookie('session', token, { httpOnly: true, secure: this.configService.get('NODE_ENV') === 'production', sameSite: 'lax', diff --git a/apps/api/src/calendar/calendar.service.ts b/apps/api/src/calendar/calendar.service.ts index 9d1e2ed..825b080 100644 --- a/apps/api/src/calendar/calendar.service.ts +++ b/apps/api/src/calendar/calendar.service.ts @@ -4,6 +4,7 @@ import { Logger, NotFoundException, } from '@nestjs/common'; +import { Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; import { forTenant } from '../prisma/prisma-tenant.extension'; import { CryptoService } from '../crypto/crypto.service'; @@ -185,7 +186,7 @@ export class CalendarService { // Exchange EWS servers are commonly on internal networks — SSRF check skipped for exchange type if (dto.type !== 'exchange') await this.validateUrlNotPrivate(dto.url); - const data: Record = { + const data: Prisma.CalendarSourceUncheckedCreateInput = { userId, tenantId, name: dto.name, @@ -203,7 +204,7 @@ export class CalendarService { const tenantPrisma = forTenant(this.prisma, tenantId, userId); const created = await tenantPrisma.calendarSource.create({ - data: data as any, + data, select: SOURCE_SAFE_SELECT, }); @@ -234,7 +235,7 @@ export class CalendarService { await this.validateUrlNotPrivate(dto.url); } - const data: Record = {}; + const data: Prisma.CalendarSourceUncheckedUpdateInput = {}; if (dto.name !== undefined) data.name = dto.name; if (dto.type !== undefined) data.type = dto.type; if (dto.url !== undefined) data.url = dto.url; @@ -252,7 +253,7 @@ export class CalendarService { const updated = await tenantPrisma.calendarSource.update({ where: { id }, - data: data as any, + data, select: { ...SOURCE_SAFE_SELECT, encryptedPassword: true, diff --git a/apps/api/src/tenant/tenant.controller.ts b/apps/api/src/tenant/tenant.controller.ts index 14cc543..c667740 100644 --- a/apps/api/src/tenant/tenant.controller.ts +++ b/apps/api/src/tenant/tenant.controller.ts @@ -10,7 +10,7 @@ import { Post, UseGuards, } from '@nestjs/common'; -import { Role } from '@prisma/client'; +import { Role, type Tenant } from '@prisma/client'; import { Roles } from '../auth/decorators/roles.decorator'; import { RolesGuard } from '../auth/guards/roles.guard'; import { forTenant } from '../prisma/prisma-tenant.extension'; @@ -66,7 +66,13 @@ export class TenantController { orderBy: { name: 'asc' }, }); - const results: any[] = []; + // Elementtyp aus dem hergeleitet, was die Schleife unten tatsaechlich + // hineinlegt: die fuenf uebernommenen Tenant-Spalten plus die gezaehlte + // Benutzerzahl. Kein erfundenes Feld. + const results: (Pick< + Tenant, + 'id' | 'name' | 'slug' | 'isActive' | 'createdAt' + > & { userCount: number })[] = []; for (const tenant of tenants) { const tenantPrisma = forTenant(this.prisma, tenant.id); const userCount = await tenantPrisma.user.count({ diff --git a/apps/api/src/user/user.service.ts b/apps/api/src/user/user.service.ts index 218c1e3..ae64136 100644 --- a/apps/api/src/user/user.service.ts +++ b/apps/api/src/user/user.service.ts @@ -1,5 +1,6 @@ import { ConflictException, Injectable, Logger } from '@nestjs/common'; import * as argon2 from 'argon2'; +import type { User } from '@prisma/client'; import { GroupsService } from '../groups/groups.service'; import { forTenant } from '../prisma/prisma-tenant.extension'; import { prismaErrorCode } from '../prisma/prisma-error'; @@ -18,6 +19,51 @@ import { PrismaService } from '../prisma/prisma.service'; * im selben Commit auf die neue Signatur umgestellt (260910-das, Aufgabe 3), * damit die Typpruefung nach jeder Aufgabe sauber bleibt. */ +/** + * Spaltenauswahl der plattformweiten Benutzerliste. Als eigene Konstante, + * damit der Elementtyp der Sammelliste unten mit `Prisma.UserGetPayload` + * aus GENAU dieser Auswahl hergeleitet wird — eine zweite Beschreibung + * derselben Felder waere eine Behauptung, die beim naechsten Feld + * auseinanderlaeuft. + */ +const PLATFORM_USER_SELECT = { + id: true, + username: true, + email: true, + displayName: true, + role: true, + isActive: true, + tenantId: true, + createdAt: true, + lastLoginAt: true, +} as const; + +/** + * Elementtyp der Sammelliste, aus PLATFORM_USER_SELECT hergeleitet statt + * daneben beschrieben. Bewusst `Pick` und NICHT + * `Prisma.UserGetPayload<{ select: ... }>`: die zweite Form traegt das Wort + * select in eine Typangabe, und der Erkenner in rls-access-inventory.spec.ts + * zaehlt jede select-Angabe ausserhalb eines Modellaufrufs als Verstoss + * (gemessen, 260921-m34 Aufgabe 3b). Der Erkenner ist die Mandantenkontrolle + * und wird nicht fuer eine Typschreibweise aufgeweicht (T-M34-03). + */ +type PlatformUserRow = Pick; + +/** + * Felder, die `UserService.update()` entgegennimmt. Als eigener Typ, damit + * das intern zusammengebaute `updateData` unten daraus abgeleitet werden + * kann statt daneben noch einmal von Hand beschrieben zu werden. + */ +interface UpdateUserInput { + username?: string; + email?: string; + password?: string; + displayName?: string; + role?: 'SUPER_ADMIN' | 'ADMIN' | 'USER'; + isActive?: boolean; + mustChangePassword?: boolean; +} + @Injectable() export class UserService { private readonly logger = new Logger(UserService.name); @@ -108,7 +154,10 @@ export class UserService { const { password, ...rest } = data; const tenantPrisma = forTenant(this.prisma, data.tenantId); - let created: any; + // Der Rueckgabewert von user.create() ist das vollstaendige User-Modell. + // Die Zuweisung steht im try, der catch endet ausnahmslos mit throw — + // nach dem Block ist `created` deshalb belegt, ohne Behauptung. + let created: User; try { created = await tenantPrisma.user.create({ data: { @@ -145,21 +194,13 @@ export class UserService { * weil auch ein Namens- oder Adresswechsel auf denselben plattformweiten * Schluessel treffen kann. */ - async update( - tenantId: string, - id: string, - data: { - username?: string; - email?: string; - password?: string; - displayName?: string; - role?: 'SUPER_ADMIN' | 'ADMIN' | 'USER'; - isActive?: boolean; - mustChangePassword?: boolean; - }, - ) { + async update(tenantId: string, id: string, data: UpdateUserInput) { const { password, ...rest } = data; - const updateData: any = { ...rest }; + // Aus der Signatur hergeleitet: alles ausser `password`, dafuer der + // daraus berechnete `passwordHash`. Nichts erfunden, nichts weggelassen. + const updateData: Omit & { + passwordHash?: string; + } = { ...rest }; if (updateData.username) { updateData.username = updateData.username.toLowerCase(); @@ -232,22 +273,12 @@ export class UserService { async findAllForPlatformAdmin() { const tenants = await this.prisma.tenant.findMany({ select: { id: true } }); - const results: any[] = []; + const results: PlatformUserRow[] = []; for (const tenant of tenants) { const tenantPrisma = forTenant(this.prisma, tenant.id); const users = await tenantPrisma.user.findMany({ where: { tenantId: tenant.id }, - select: { - id: true, - username: true, - email: true, - displayName: true, - role: true, - isActive: true, - tenantId: true, - createdAt: true, - lastLoginAt: true, - }, + select: PLATFORM_USER_SELECT, }); results.push(...users); }