refactor(quick-260921-m34): Aufgabe 1 - Mandantenbindung entzaubert, 105 unnoetige any-Zusicherungen entfernt

- prisma-tenant.extension.ts: (prisma as any) und die Handannotation an
  $allOperations in forTenant()/forSystem() entfernt; Kopfkommentar
  unveraendert. .then((results: any[]) => ...) auf unknown[] umgestellt.
- 105 Aufrufstellen `const X = forTenant(...) as any` / `forSystem(...) as
  any` von der Zusicherung befreit, Zuweisungsform woertlich erhalten
  (rls-access-inventory.spec.ts bleibt scharf, 30/30 gruen einzeln
  geprueft).
- withTenantTransaction(): Prisma.TransactionClient fuer tx probiert,
  gemessen verworfen - bricht das Testdoppel in
  prisma-tenant.extension.spec.ts (TS2322 auf einem absichtlich
  unvollstaendigen Fake-Objekt). tx bleibt any, mit Begruendung am Typ.
- Gefolge des jetzt getypten Klienten entfernt: any[]-Annotationen und
  .map((x: any) => ...) in groups.service.ts, module-grants.service.ts,
  dkv.service.ts, ldap-config.service.ts, tenders.controller.ts:270.
- Befund (D-03): tender-matching.service.ts:159 trug eine Handannotation
  (match: { tender: unknown }), die den Wert nur deshalb auf unknown
  verengte, um TS7006 unter dem alten any-Klienten zu vermeiden - mit dem
  getypten Klienten war das falsch. Annotation geloescht, kein Ersatz
  durch Zusicherung.
- Zwei any bleiben gezielt in groups.service.ts (u/a in
  ensureDefaultGroup(), gefolge von tx: any) - Begruendung am Code.

noExplicitAny apps/api/src: 288 -> 149 (Schranke 155). type-check 4/4,
lint 5/5 (0 error). apps/api 72/1143 gruen, apps/web 73/531 gruen,
rls-access-inventory.spec.ts 30/30 gruen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
This commit is contained in:
2026-09-21 16:19:16 +02:00
parent 8d845e732e
commit b188946e31
24 changed files with 147 additions and 148 deletions
@@ -121,7 +121,7 @@ export class TenderDigestScheduler implements OnModuleInit {
// `distinct(['userId'])` liefert dann nur EINE der moeglichen
// tenantId-Werte je Nutzer, welche ist von der internen Zeilenreihenfolge
// abhaengig. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md.
const systemPrisma = forSystem(this.prisma) as any;
const systemPrisma = forSystem(this.prisma);
const candidates: { userId: string; tenantId: string }[] = await systemPrisma.tenderMatch.findMany({
where: { notifiedAt: null },
select: { userId: true, tenantId: true },
@@ -145,7 +145,7 @@ export class TenderDigestScheduler implements OnModuleInit {
// `userId` sieht dieser Zugriff den ganzen Mandanten, exakt wie vor
// der Migration. Ein Systemkontext fuer Hintergrunddienste ist
// Etappe 3c, nicht Teil dieser Aenderung.
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
const tenantPrisma = forTenant(this.prisma, tenantId);
const pref = await tenantPrisma.tenderNotificationPref.findUnique({
where: { userId },
@@ -102,7 +102,7 @@ export class TenderEmailConfigService {
* by userId (T-17-01) — a user only ever reads their own mailbox.
*/
async getConfigForApi(userId: string, tenantId: string) {
const tenantPrisma = forTenant(this.prisma, tenantId, userId) as any;
const tenantPrisma = forTenant(this.prisma, tenantId, userId);
const safe = await tenantPrisma.tenderEmailConfig.findUnique({
where: { userId },
select: EMAIL_CONFIG_SAFE_SELECT,
@@ -152,7 +152,7 @@ export class TenderEmailConfigService {
*/
async saveConfig(ctx: { userId: string; tenantId: string }, dto: TenderEmailConfigDto) {
const { userId, tenantId } = ctx;
const tenantPrisma = forTenant(this.prisma, tenantId, userId) as any;
const tenantPrisma = forTenant(this.prisma, tenantId, userId);
let encryptedInboxCreds: string | undefined;
const credChanged =
@@ -240,7 +240,7 @@ export class TenderEmailConfigService {
if (!username || !password) {
try {
const tenantPrisma = forTenant(this.prisma, tenantId, userId) as any;
const tenantPrisma = forTenant(this.prisma, tenantId, userId);
const existing = await tenantPrisma.tenderEmailConfig.findUnique({ where: { userId } });
if (existing?.encryptedInboxCreds) {
const stored = JSON.parse(this.crypto.decrypt(existing.encryptedInboxCreds)) as {
@@ -72,7 +72,7 @@ export class TenderMatchingService {
// und wuerde stumm. Treffer-Anlage und Sofortmeldung bleiben je Profil
// GEBUNDEN (unten); der Katalog-Lesezugriff (`tender`, D-03) bleibt
// ungebunden. Eine LEERE Profilliste ist Nichtstun (keine Treffer).
const systemPrisma = forSystem(this.prisma) as any;
const systemPrisma = forSystem(this.prisma);
const savedSearches: Prisma.TenderSavedSearchGetPayload<Record<string, never>>[] =
await systemPrisma.tenderSavedSearch.findMany();
@@ -95,7 +95,7 @@ export class TenderMatchingService {
// Gebunden an den Mandanten DIESES Profils (260909-laa, Aufgabe 3)
// — EIN gebundener Client je Profil, nicht je Treffer, sonst
// entstuende pro Zeile eine eigene Transaktion.
const tenantPrisma = forTenant(this.prisma, search.tenantId) as any;
const tenantPrisma = forTenant(this.prisma, search.tenantId);
for (const hit of hits) {
await tenantPrisma.tenderMatch.upsert({
@@ -134,7 +134,7 @@ export class TenderMatchingService {
try {
// Gebunden an den Mandanten DIESES Profils (260909-laa, Aufgabe 3)
// — EIN gebundener Client je Profil.
const tenantPrisma = forTenant(this.prisma, profile.tenantId) as any;
const tenantPrisma = forTenant(this.prisma, profile.tenantId);
const fresh = await tenantPrisma.tenderMatch.findMany({
where: {
@@ -156,7 +156,7 @@ export class TenderMatchingService {
{ email: user.email },
profile.tenantId,
{ name: profile.name },
fresh.map((match: { tender: unknown }) => match.tender),
fresh.map((match) => match.tender),
);
if (sent) {
@@ -45,7 +45,7 @@ export class TenderNotificationPrefService {
* autowrite needed to represent "using the default".
*/
async getForUser(userId: string, tenantId: string): Promise<{ digestInterval: string }> {
const tenantPrisma = forTenant(this.prisma, tenantId, userId) as any;
const tenantPrisma = forTenant(this.prisma, tenantId, userId);
const existing = await tenantPrisma.tenderNotificationPref.findUnique({
where: { userId },
});
@@ -63,7 +63,7 @@ export class TenderNotificationPrefService {
* than creating a new one.
*/
async setForUser(userId: string, tenantId: string, digestInterval: string) {
const tenantPrisma = forTenant(this.prisma, tenantId, userId) as any;
const tenantPrisma = forTenant(this.prisma, tenantId, userId);
try {
return await tenantPrisma.tenderNotificationPref.upsert({
where: { userId },
@@ -69,7 +69,7 @@ export class TenderRssFeedSourceService {
* Bindung nicht überflüssig, sondern das zweite Netz.
*/
async listForUser(userId: string, tenantId: string) {
const tenantPrisma = forTenant(this.prisma, tenantId, userId) as any;
const tenantPrisma = forTenant(this.prisma, tenantId, userId);
return tenantPrisma.tenderRssFeedSource.findMany({
where: { OR: [{ userId: null }, { userId }] },
orderBy: { createdAt: 'asc' },
@@ -93,7 +93,7 @@ export class TenderRssFeedSourceService {
) {
this.assertUrlAllowed(dto.url);
const tenantPrisma = forTenant(this.prisma, ctx.tenantId, ctx.userId) as any;
const tenantPrisma = forTenant(this.prisma, ctx.tenantId, ctx.userId);
const existingCount = await tenantPrisma.tenderRssFeedSource.count({
where: { userId: ctx.userId },
});
@@ -44,7 +44,7 @@ export class TenderSavedSearchService {
* strictly by userId (V4/IDOR) — a foreign userId sees nothing.
*/
async list(userId: string, tenantId: string) {
const tenantPrisma = forTenant(this.prisma, tenantId, userId) as any;
const tenantPrisma = forTenant(this.prisma, tenantId, userId);
return tenantPrisma.tenderSavedSearch.findMany({
where: { userId },
orderBy: { name: 'asc' },
@@ -58,7 +58,7 @@ export class TenderSavedSearchService {
* users, since the uniqueness is scoped per-user.
*/
async create(userId: string, tenantId: string, dto: CreateSavedSearchDto) {
const tenantPrisma = forTenant(this.prisma, tenantId, userId) as any;
const tenantPrisma = forTenant(this.prisma, tenantId, userId);
try {
return await tenantPrisma.tenderSavedSearch.create({
data: {
@@ -87,7 +87,7 @@ export class TenderSavedSearchService {
* leaking whether another user's profile exists).
*/
async update(id: string, userId: string, tenantId: string, dto: UpdateSavedSearchDto) {
const tenantPrisma = forTenant(this.prisma, tenantId, userId) as any;
const tenantPrisma = forTenant(this.prisma, tenantId, userId);
const existing = await tenantPrisma.tenderSavedSearch.findUnique({
where: { id },
});
@@ -124,7 +124,7 @@ export class TenderSavedSearchService {
* update().
*/
async remove(id: string, userId: string, tenantId: string) {
const tenantPrisma = forTenant(this.prisma, tenantId, userId) as any;
const tenantPrisma = forTenant(this.prisma, tenantId, userId);
const existing = await tenantPrisma.tenderSavedSearch.findUnique({
where: { id },
});
@@ -75,7 +75,7 @@ export class TenderTriageService {
update.favoritedAt = dto.isFavorite ? now : null;
}
const tenantPrisma = forTenant(this.prisma, tenantId, userId) as any;
const tenantPrisma = forTenant(this.prisma, tenantId, userId);
try {
return await tenantPrisma.tenderTriage.upsert({
where: { userId_tenderId: { userId, tenderId } },
@@ -111,7 +111,7 @@ export class TenderTriageService {
*/
async listForUser(userId: string, tenantId: string, tenderIds: string[]) {
if (!tenderIds.length) return [];
const tenantPrisma = forTenant(this.prisma, tenantId, userId) as any;
const tenantPrisma = forTenant(this.prisma, tenantId, userId);
return tenantPrisma.tenderTriage.findMany({
where: { userId, tenderId: { in: tenderIds } },
});
@@ -123,7 +123,7 @@ export class TenderTriageService {
* tender-query.builder.ts's buildTenderWhere.
*/
async favoriteIds(userId: string, tenantId: string): Promise<string[]> {
const tenantPrisma = forTenant(this.prisma, tenantId, userId) as any;
const tenantPrisma = forTenant(this.prisma, tenantId, userId);
const rows = await tenantPrisma.tenderTriage.findMany({
where: { userId, isFavorite: true },
select: { tenderId: true },
+1 -1
View File
@@ -267,7 +267,7 @@ export class TendersController {
const { userId, tenantId } = this.extractTriageContext(req);
const feeds = await this.tenderRssFeedSource.listForUser(userId, tenantId);
return feeds.map(({ userId: ownerUserId, ...rest }: any) => ({
return feeds.map(({ userId: ownerUserId, ...rest }) => ({
...rest,
isPlatformWide: ownerUserId === null,
}));