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<string, unknown> 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<UpdateUserInput, 'password'> plus dem
daraus berechneten passwordHash; die Parameterform ist dafuer als
UpdateUserInput benannt und nicht neu erfunden. `const results: any[]`
wird Pick<User, keyof typeof PLATFORM_USER_SELECT>[], 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<User, ...>
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
This commit is contained in:
@@ -390,7 +390,7 @@ export class AuthService {
|
|||||||
mustChangePassword: false,
|
mustChangePassword: false,
|
||||||
};
|
};
|
||||||
const token = this.jwtService.sign(payload);
|
const token = this.jwtService.sign(payload);
|
||||||
(response as any).cookie('session', token, {
|
response.cookie('session', token, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: this.configService.get('NODE_ENV') === 'production',
|
secure: this.configService.get('NODE_ENV') === 'production',
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
Logger,
|
Logger,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { forTenant } from '../prisma/prisma-tenant.extension';
|
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||||
import { CryptoService } from '../crypto/crypto.service';
|
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
|
// Exchange EWS servers are commonly on internal networks — SSRF check skipped for exchange type
|
||||||
if (dto.type !== 'exchange') await this.validateUrlNotPrivate(dto.url);
|
if (dto.type !== 'exchange') await this.validateUrlNotPrivate(dto.url);
|
||||||
|
|
||||||
const data: Record<string, unknown> = {
|
const data: Prisma.CalendarSourceUncheckedCreateInput = {
|
||||||
userId,
|
userId,
|
||||||
tenantId,
|
tenantId,
|
||||||
name: dto.name,
|
name: dto.name,
|
||||||
@@ -203,7 +204,7 @@ export class CalendarService {
|
|||||||
|
|
||||||
const tenantPrisma = forTenant(this.prisma, tenantId, userId);
|
const tenantPrisma = forTenant(this.prisma, tenantId, userId);
|
||||||
const created = await tenantPrisma.calendarSource.create({
|
const created = await tenantPrisma.calendarSource.create({
|
||||||
data: data as any,
|
data,
|
||||||
select: SOURCE_SAFE_SELECT,
|
select: SOURCE_SAFE_SELECT,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -234,7 +235,7 @@ export class CalendarService {
|
|||||||
await this.validateUrlNotPrivate(dto.url);
|
await this.validateUrlNotPrivate(dto.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: Record<string, unknown> = {};
|
const data: Prisma.CalendarSourceUncheckedUpdateInput = {};
|
||||||
if (dto.name !== undefined) data.name = dto.name;
|
if (dto.name !== undefined) data.name = dto.name;
|
||||||
if (dto.type !== undefined) data.type = dto.type;
|
if (dto.type !== undefined) data.type = dto.type;
|
||||||
if (dto.url !== undefined) data.url = dto.url;
|
if (dto.url !== undefined) data.url = dto.url;
|
||||||
@@ -252,7 +253,7 @@ export class CalendarService {
|
|||||||
|
|
||||||
const updated = await tenantPrisma.calendarSource.update({
|
const updated = await tenantPrisma.calendarSource.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: data as any,
|
data,
|
||||||
select: {
|
select: {
|
||||||
...SOURCE_SAFE_SELECT,
|
...SOURCE_SAFE_SELECT,
|
||||||
encryptedPassword: true,
|
encryptedPassword: true,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
Post,
|
Post,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Role } from '@prisma/client';
|
import { Role, type Tenant } from '@prisma/client';
|
||||||
import { Roles } from '../auth/decorators/roles.decorator';
|
import { Roles } from '../auth/decorators/roles.decorator';
|
||||||
import { RolesGuard } from '../auth/guards/roles.guard';
|
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||||
import { forTenant } from '../prisma/prisma-tenant.extension';
|
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||||
@@ -66,7 +66,13 @@ export class TenantController {
|
|||||||
orderBy: { name: 'asc' },
|
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) {
|
for (const tenant of tenants) {
|
||||||
const tenantPrisma = forTenant(this.prisma, tenant.id);
|
const tenantPrisma = forTenant(this.prisma, tenant.id);
|
||||||
const userCount = await tenantPrisma.user.count({
|
const userCount = await tenantPrisma.user.count({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ConflictException, Injectable, Logger } from '@nestjs/common';
|
import { ConflictException, Injectable, Logger } from '@nestjs/common';
|
||||||
import * as argon2 from 'argon2';
|
import * as argon2 from 'argon2';
|
||||||
|
import type { User } from '@prisma/client';
|
||||||
import { GroupsService } from '../groups/groups.service';
|
import { GroupsService } from '../groups/groups.service';
|
||||||
import { forTenant } from '../prisma/prisma-tenant.extension';
|
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||||
import { prismaErrorCode } from '../prisma/prisma-error';
|
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),
|
* im selben Commit auf die neue Signatur umgestellt (260910-das, Aufgabe 3),
|
||||||
* damit die Typpruefung nach jeder Aufgabe sauber bleibt.
|
* 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<User, keyof typeof ...>` 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<User, keyof typeof PLATFORM_USER_SELECT>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
private readonly logger = new Logger(UserService.name);
|
private readonly logger = new Logger(UserService.name);
|
||||||
@@ -108,7 +154,10 @@ export class UserService {
|
|||||||
const { password, ...rest } = data;
|
const { password, ...rest } = data;
|
||||||
const tenantPrisma = forTenant(this.prisma, data.tenantId);
|
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 {
|
try {
|
||||||
created = await tenantPrisma.user.create({
|
created = await tenantPrisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -145,21 +194,13 @@ export class UserService {
|
|||||||
* weil auch ein Namens- oder Adresswechsel auf denselben plattformweiten
|
* weil auch ein Namens- oder Adresswechsel auf denselben plattformweiten
|
||||||
* Schluessel treffen kann.
|
* Schluessel treffen kann.
|
||||||
*/
|
*/
|
||||||
async update(
|
async update(tenantId: string, id: string, data: UpdateUserInput) {
|
||||||
tenantId: string,
|
|
||||||
id: string,
|
|
||||||
data: {
|
|
||||||
username?: string;
|
|
||||||
email?: string;
|
|
||||||
password?: string;
|
|
||||||
displayName?: string;
|
|
||||||
role?: 'SUPER_ADMIN' | 'ADMIN' | 'USER';
|
|
||||||
isActive?: boolean;
|
|
||||||
mustChangePassword?: boolean;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
const { password, ...rest } = data;
|
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<UpdateUserInput, 'password'> & {
|
||||||
|
passwordHash?: string;
|
||||||
|
} = { ...rest };
|
||||||
|
|
||||||
if (updateData.username) {
|
if (updateData.username) {
|
||||||
updateData.username = updateData.username.toLowerCase();
|
updateData.username = updateData.username.toLowerCase();
|
||||||
@@ -232,22 +273,12 @@ export class UserService {
|
|||||||
async findAllForPlatformAdmin() {
|
async findAllForPlatformAdmin() {
|
||||||
const tenants = await this.prisma.tenant.findMany({ select: { id: true } });
|
const tenants = await this.prisma.tenant.findMany({ select: { id: true } });
|
||||||
|
|
||||||
const results: any[] = [];
|
const results: PlatformUserRow[] = [];
|
||||||
for (const tenant of tenants) {
|
for (const tenant of tenants) {
|
||||||
const tenantPrisma = forTenant(this.prisma, tenant.id);
|
const tenantPrisma = forTenant(this.prisma, tenant.id);
|
||||||
const users = await tenantPrisma.user.findMany({
|
const users = await tenantPrisma.user.findMany({
|
||||||
where: { tenantId: tenant.id },
|
where: { tenantId: tenant.id },
|
||||||
select: {
|
select: PLATFORM_USER_SELECT,
|
||||||
id: true,
|
|
||||||
username: true,
|
|
||||||
email: true,
|
|
||||||
displayName: true,
|
|
||||||
role: true,
|
|
||||||
isActive: true,
|
|
||||||
tenantId: true,
|
|
||||||
createdAt: true,
|
|
||||||
lastLoginAt: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
results.push(...users);
|
results.push(...users);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user