refactor(quick-260921-bi2): maschinelle Lint-Fixe und toten Code abbauen
- Aufgabe 2: vier sichere Biome-Regeln (useImportType pfadgebunden auf apps/web+packages, noUselessEscapeInRegex, useConst, useExponentiationOperator) sowie fuenf ungesicherte Regeln (useNodejsImportProtocol, useLiteralKeys, useOptionalChain, useTemplate, useParseIntRadix) angewendet und den gesamten Diff von Hand gelesen (ldap.service.ts zeichenweise gegen Gross-/Kleinschreibung der AD-Merkmale, auth.service.ts/jwt.strategy.ts gegen Durchwinken bei fehlender Sitzung geprueft) - noUselessSwitchCase bleibt bewusst stehen (tender-normalizer.service.ts:60, die Fallmarke dokumentiert Absicht) - Toter Code (D-03): fuenf folgenlose Auffangvariablen entfernt, eine nicht benutzte Funktion (forSystemQuery, Pruefskript) entfernt, ein positionsgebundener Dekoratorparameter umbenannt (current-user.decorator.ts), fuenf Symptomfunde entfernt und als Folgeaufgaben zu melden (siehe unten) - Sechs weitere, im Plan nicht namentlich gelistete aber gleich-kategorische Dead-Code-Fundstellen in Testdateien zusaetzlich bereinigt (groups.service.spec.ts, cert-manager.test.tsx, ldap.service.spec.ts, prisma-tenant.extension.spec.ts x3) — noetig, um die vom Plan selbst verlangten Nullstaende bei noUnusedVariables/ noUnusedImports/noUnusedFunctionParameters zu erreichen Dekoratordaten aus apps/api unveraendert (593 Zeilen, sha256 6e1583f1...). Endstand 620 Befunde (541 echt, 79 Test) statt der im Plan geschaetzten 621/542 — eine Differenz von 1, weil das Streichen des Namens aus `catch (e: any)` in calendar.service.ts (Symptom-Fix) den dort ebenfalls gemeldeten noExplicitAny-Befund miteliminiert; das ist eine erwuenschte Nebenwirkung, keine Regression. Fehlerstufe 0, beide Testlaeufe punktgleich gruen (69/1124, 66/459), pnpm type-check 4/4, pnpm lint --force 5/5. Folgeaufgaben aus D-03 (nicht in diesem Vorgang behoben): - force-password-change.interceptor.ts: Freigabeliste prueft nur den Pfad, nicht die HTTP-Methode - change-password/page.tsx: nach erzwungenem Wechsel bleibt die Person auf der Seite stehen (keine Weiterleitung, keine Aktualisierung der Benutzerablage) - VehicleTable.tsx: Loeschschaltflaeche hat keinen Besetztzustand, laesst sich doppelt ausloesen - SplitTab.tsx: downloadAllAsZip erhielt eine ungenutzte Uebersetzungsfunktion, Hinweis auf fest verdrahtete Texte im Zip-Pfad Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
This commit is contained in:
@@ -9,7 +9,7 @@ import { ConfigService } from '@nestjs/config';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Role } from '@prisma/client';
|
||||
import * as argon2 from 'argon2';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { Response } from 'express';
|
||||
import { LdapConfigService } from '../ldap/ldap-config.service';
|
||||
import { LdapService } from '../ldap/ldap.service';
|
||||
@@ -111,7 +111,7 @@ export class AuthService {
|
||||
`;
|
||||
const user = rows[0];
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
if (!user?.isActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
const config = await this.ldapConfigService.getConfig(user.tenantId);
|
||||
if (!config || !config.isActive) {
|
||||
if (!config?.isActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ export class AuthService {
|
||||
const user = rows[0];
|
||||
|
||||
// Always return success to prevent email enumeration (T-02-12)
|
||||
if (!user || !user.isActive) {
|
||||
if (!user?.isActive) {
|
||||
this.logger.log(
|
||||
`Password reset requested for unknown/inactive email: ${email}`,
|
||||
);
|
||||
@@ -358,7 +358,7 @@ export class AuthService {
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
if (!user || !user.passwordHash) {
|
||||
if (!user?.passwordHash) {
|
||||
throw new UnauthorizedException('User not found or has no local password');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
export const CurrentUser = createParamDecorator(
|
||||
(data: unknown, ctx: ExecutionContext) => {
|
||||
(_data: unknown, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return request.user;
|
||||
},
|
||||
|
||||
@@ -50,7 +50,6 @@ export class ForcePasswordChangeInterceptor implements NestInterceptor {
|
||||
|
||||
// Allow specific routes even when password change is required
|
||||
const path = request.route?.path || request.url;
|
||||
const method = request.method;
|
||||
|
||||
const allowedPaths = [
|
||||
'/auth/change-password',
|
||||
|
||||
@@ -8,8 +8,8 @@ import { Request } from 'express';
|
||||
* Custom extractor that reads JWT from the httpOnly "session" cookie.
|
||||
*/
|
||||
function cookieExtractor(req: Request): string | null {
|
||||
if (req && req.cookies) {
|
||||
return req.cookies['session'] || null;
|
||||
if (req?.cookies) {
|
||||
return req.cookies.session || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -318,7 +318,7 @@ export class CalendarService {
|
||||
});
|
||||
|
||||
return { success };
|
||||
} catch (error) {
|
||||
} catch {
|
||||
const errorMsg = 'Connection failed'; // T-05-13: generic error, no credentials
|
||||
await tenantPrisma.calendarSource.update({
|
||||
where: { id },
|
||||
@@ -355,7 +355,7 @@ export class CalendarService {
|
||||
try {
|
||||
const success = await provider.testConnection(tempSource);
|
||||
return { success };
|
||||
} catch (e: any) {
|
||||
} catch {
|
||||
return { success: false, error: 'Connection failed' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
IsBoolean,
|
||||
IsHexColor,
|
||||
IsIn,
|
||||
IsNotEmpty,
|
||||
|
||||
@@ -47,7 +47,7 @@ export class CalDAVProvider implements CalendarProvider {
|
||||
|
||||
for (const key of Object.keys(parsed)) {
|
||||
const component = parsed[key];
|
||||
if (!component || component.type !== 'VEVENT') continue;
|
||||
if (component?.type !== 'VEVENT') continue;
|
||||
|
||||
const vevent = component as ical.VEvent;
|
||||
const start = new Date(vevent.start);
|
||||
|
||||
@@ -180,8 +180,8 @@ export class ExchangeProvider implements CalendarProvider {
|
||||
id: `${source.id}-${item.id}`,
|
||||
sourceId: source.id,
|
||||
title: item.subject || 'Untitled',
|
||||
start: new Date(item.start?.dateTime + 'Z'),
|
||||
end: new Date(item.end?.dateTime + 'Z'),
|
||||
start: new Date(`${item.start?.dateTime}Z`),
|
||||
end: new Date(`${item.end?.dateTime}Z`),
|
||||
allDay: item.isAllDay || false,
|
||||
location: item.location?.displayName || undefined,
|
||||
description: item.bodyPreview || undefined,
|
||||
|
||||
@@ -30,7 +30,7 @@ export class ICSProvider implements CalendarProvider {
|
||||
|
||||
for (const key of Object.keys(data)) {
|
||||
const component = data[key];
|
||||
if (!component || component.type !== 'VEVENT') continue;
|
||||
if (component?.type !== 'VEVENT') continue;
|
||||
|
||||
const vevent = component as ical.VEvent;
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('CertManagerService helpers', () => {
|
||||
const cert2 = generateSelfSignedCert();
|
||||
const pem1 = forge.pki.certificateToPem(cert1);
|
||||
const pem2 = forge.pki.certificateToPem(cert2);
|
||||
const chain = pem1 + '\n' + pem2;
|
||||
const chain = `${pem1}\n${pem2}`;
|
||||
|
||||
const parsed = service.parsePemChain(chain);
|
||||
expect(parsed).toHaveLength(2);
|
||||
@@ -266,7 +266,7 @@ describe('splitCerts', () => {
|
||||
cert2Pem = forge.pki.certificateToPem(c2);
|
||||
|
||||
// Fullchain fixture: two PEMs concatenated
|
||||
const fullchainPem = cert1Pem + '\n' + cert2Pem;
|
||||
const fullchainPem = `${cert1Pem}\n${cert2Pem}`;
|
||||
fullchainBuffer = Buffer.from(fullchainPem, 'utf-8');
|
||||
|
||||
// P7B fixture: PEM-wrapped PKCS7 SignedData bundle with both certs
|
||||
|
||||
@@ -302,7 +302,7 @@ export class CertManagerService {
|
||||
fingerprint: { sha1, sha256 },
|
||||
pemPreview: forge.pki.certificateToPem(cert),
|
||||
};
|
||||
} catch (err) {
|
||||
} catch {
|
||||
this.logger.warn('parseCert: failed to extract CertDetails fields');
|
||||
throw new BadRequestException('Failed to extract certificate details');
|
||||
}
|
||||
@@ -505,7 +505,7 @@ export class CertManagerService {
|
||||
return {
|
||||
filename: 'chain.pem',
|
||||
content,
|
||||
mimeType: FORMAT_MIME['pem'],
|
||||
mimeType: FORMAT_MIME.pem,
|
||||
};
|
||||
} else if (outputFormat === 'pfx') {
|
||||
// Open Question 1 resolution: toPkcs12Asn1(null, certs, password) works in node-forge 1.4.0
|
||||
@@ -523,7 +523,7 @@ export class CertManagerService {
|
||||
return {
|
||||
filename: 'bundle.pfx',
|
||||
content,
|
||||
mimeType: FORMAT_MIME['pfx'],
|
||||
mimeType: FORMAT_MIME.pfx,
|
||||
};
|
||||
} else {
|
||||
throw new BadRequestException(
|
||||
@@ -667,7 +667,7 @@ export class CertManagerService {
|
||||
return {
|
||||
filename: 'converted.pfx',
|
||||
content,
|
||||
mimeType: FORMAT_MIME['pfx'],
|
||||
mimeType: FORMAT_MIME.pfx,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -676,7 +676,7 @@ export class CertManagerService {
|
||||
content,
|
||||
mimeType: FORMAT_MIME[targetFormat],
|
||||
};
|
||||
} catch (err) {
|
||||
} catch {
|
||||
this.logger.warn('convertCert: failed to serialize to target format');
|
||||
throw new BadRequestException(
|
||||
`Failed to convert certificate to ${targetFormat}: serialization error`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
|
||||
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
|
||||
|
||||
/** Current name of the platform-wide encryption key. */
|
||||
export const ENCRYPTION_KEY_ENV = 'TESSERA_ENCRYPTION_KEY';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'reflect-metadata';
|
||||
import * as crypto from 'crypto';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as crypto from 'node:crypto';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
|
||||
@@ -168,7 +168,7 @@ describe('DesktopService/DesktopController — HTTP-Durchstich (Phase 18)', () =
|
||||
});
|
||||
|
||||
it('Test 5 (Whitelist vor Dateisystem): nicht existierendes Verzeichnis + mac wirft BadRequestException, nicht NotFoundException', () => {
|
||||
const missingDir = path.join(os.tmpdir(), 'tessera-desktop-does-not-exist-' + Date.now());
|
||||
const missingDir = path.join(os.tmpdir(), `tessera-desktop-does-not-exist-${Date.now()}`);
|
||||
const previous = process.env.DESKTOP_DIST_DIR;
|
||||
process.env.DESKTOP_DIST_DIR = missingDir;
|
||||
try {
|
||||
|
||||
@@ -6,8 +6,8 @@ import type {
|
||||
DesktopPlatform,
|
||||
DesktopUpdateResponse,
|
||||
} from '@tessera/shared';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
/**
|
||||
* Wertevorrat der Plattformen (Phase 18, D-10). Geschlossen -- eine dritte
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { ExportRow } from './dkv.types';
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export class DkvParserService {
|
||||
let text: string;
|
||||
try {
|
||||
text = await this.extractText(buffer);
|
||||
} catch (err) {
|
||||
} catch {
|
||||
this.logger.error('DKV PDF text extraction failed');
|
||||
throw new Error('DKV invoice parsing failed — text extraction error');
|
||||
}
|
||||
@@ -98,7 +98,7 @@ export class DkvParserService {
|
||||
// Anchor on VEHICLE: marker — each block extends until next VEHICLE: or end
|
||||
// Kennzeichen format: "GP-JL 728E", "GP ML 720", etc.
|
||||
const vehicleBlockPattern =
|
||||
/VEHICLE:\s+([A-Z0-9 ._\-]+?)\s+CARD NO\.:\s+(\S+)([\s\S]*?)(?=VEHICLE:|$)/g;
|
||||
/VEHICLE:\s+([A-Z0-9 ._-]+?)\s+CARD NO\.:\s+(\S+)([\s\S]*?)(?=VEHICLE:|$)/g;
|
||||
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = vehicleBlockPattern.exec(text)) !== null) {
|
||||
|
||||
@@ -133,7 +133,7 @@ function parseDkvText(text: string): DkvVehicleBlock[] {
|
||||
// Anchor on VEHICLE: marker — each block extends until next VEHICLE: or end of text
|
||||
// Kennzeichen can contain letters, digits, hyphens, spaces, dots (e.g. "GP-JL 728E")
|
||||
const vehicleBlockPattern =
|
||||
/VEHICLE:\s+([A-Z0-9 ._\-]+?)\s+CARD NO\.:\s+(\S+)([\s\S]*?)(?=VEHICLE:|$)/g;
|
||||
/VEHICLE:\s+([A-Z0-9 ._-]+?)\s+CARD NO\.:\s+(\S+)([\s\S]*?)(?=VEHICLE:|$)/g;
|
||||
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = vehicleBlockPattern.exec(text)) !== null) {
|
||||
|
||||
@@ -219,7 +219,7 @@ export class DkvController {
|
||||
) {
|
||||
const tenantId = this._requireTenant(req);
|
||||
|
||||
if (!file || !file.buffer) {
|
||||
if (!file?.buffer) {
|
||||
throw new BadRequestException('No CSV file uploaded (field name must be "file")');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as fs from 'fs';
|
||||
import * as fs from 'node:fs';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { DkvService } from './dkv.service';
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { CryptoService } from '../crypto/crypto.service';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { forSystem, forTenant } from '../prisma/prisma-tenant.extension';
|
||||
@@ -513,7 +513,7 @@ export class DkvService {
|
||||
smtpError = (err as Error).message;
|
||||
this.logger.warn(`DKV SMTP send attempt ${attempt}/3 failed: ${smtpError}`);
|
||||
if (attempt < 3) {
|
||||
await _delay(Math.pow(2, attempt) * 1000); // 2s, 4s
|
||||
await _delay(2 ** attempt * 1000); // 2s, 4s
|
||||
} else {
|
||||
smtpStatus = 'Versand fehlgeschlagen';
|
||||
// D-16: file stays locally available for manual download
|
||||
@@ -707,7 +707,7 @@ export class DkvService {
|
||||
filename.includes('/') ||
|
||||
filename.includes('\\') ||
|
||||
filename.includes('..') ||
|
||||
!/^(RG-DKV-|DKV_)[\w\-]+\.xlsx$/.test(filename)
|
||||
!/^(RG-DKV-|DKV_)[\w-]+\.xlsx$/.test(filename)
|
||||
) {
|
||||
throw new BadRequestException('Invalid export filename');
|
||||
}
|
||||
@@ -806,9 +806,9 @@ export class DkvService {
|
||||
): string {
|
||||
if (fromPdf) return fromPdf.replace(/\//g, '-');
|
||||
// DKV email subjects carry the invoice number with slashes: "26/650869002/002"
|
||||
const m = subject?.match(/(\d{2}[\/\-]\d{9}[\/\-]\d{3})/);
|
||||
const m = subject?.match(/(\d{2}[/-]\d{9}[/-]\d{3})/);
|
||||
if (m?.[1]) return m[1].replace(/\//g, '-');
|
||||
return `email-${String(uid).replace(/[^a-zA-Z0-9\-]/g, '_')}`;
|
||||
return `email-${String(uid).replace(/[^a-zA-Z0-9-]/g, '_')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -880,5 +880,5 @@ function _formatDateYYMMDD(date: string): string {
|
||||
* Example: "GP JL 740E" == "GP-JL 740E" == "GPJL740E" after normalization.
|
||||
*/
|
||||
function _normalizeKennzeichen(k: string): string {
|
||||
return k.toUpperCase().replace(/[\s\-\.]/g, '');
|
||||
return k.toUpperCase().replace(/[\s\-.]/g, '');
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { lookup } from 'dns/promises';
|
||||
import { isIP } from 'net';
|
||||
import { lookup } from 'node:dns/promises';
|
||||
import { isIP } from 'node:net';
|
||||
import { Agent, fetch as undiciFetch, type Response as UndiciResponse } from 'undici';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1044,7 +1044,7 @@ describe('GroupsService — Bindung an forTenant()/withTenantTransaction() (2609
|
||||
it('reassignDefaultBeforeDelete() bindet die drei Lesezugriffe UND die Transaktion an denselben Mandanten', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
const service = new GroupsService(prisma as any);
|
||||
const def = await service.create('t1', { name: DEFAULT_GROUP_NAME });
|
||||
await service.create('t1', { name: DEFAULT_GROUP_NAME });
|
||||
const toDelete = await service.create('t1', { name: 'Zu loeschen' });
|
||||
await service.update('t1', toDelete.id, { isDefault: true });
|
||||
|
||||
|
||||
@@ -440,7 +440,7 @@ export class GroupsService {
|
||||
const group = await tenantPrisma.group.findFirst({
|
||||
where: { id: groupId, tenantId },
|
||||
});
|
||||
if (!group || !group.isDefault) {
|
||||
if (!group?.isDefault) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ vi.mock('imapflow', () => ({
|
||||
}));
|
||||
|
||||
function makeReadable(text: string): NodeJS.ReadableStream {
|
||||
const { Readable } = require('stream') as typeof import('stream');
|
||||
const { Readable } = require('node:stream') as typeof import('stream');
|
||||
return Readable.from([Buffer.from(text, 'utf8')]);
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ describe('LdapConfigService — Bind-Passwort verschluesselt at rest', () => {
|
||||
|
||||
const written = prisma.ldapConfig.create.mock.calls[0][0].data;
|
||||
expect(written.encryptedBindPassword).toBe(
|
||||
'aa11:bb22:' + Buffer.from('geheim').toString('hex'),
|
||||
`aa11:bb22:${Buffer.from('geheim').toString('hex')}`,
|
||||
);
|
||||
expect(JSON.stringify(written)).not.toContain('geheim');
|
||||
// Die alte Klartext-Spalte darf nicht wieder auftauchen.
|
||||
@@ -95,7 +95,7 @@ describe('LdapConfigService — Bind-Passwort verschluesselt at rest', () => {
|
||||
await service.updateConfig('t1', { bindPassword: 'neu' } as any);
|
||||
const first = prisma.ldapConfig.update.mock.calls[0][0].data;
|
||||
expect(first.encryptedBindPassword).toBe(
|
||||
'aa11:bb22:' + Buffer.from('neu').toString('hex'),
|
||||
`aa11:bb22:${Buffer.from('neu').toString('hex')}`,
|
||||
);
|
||||
|
||||
await service.updateConfig('t1', { serverUrl: 'ldap://anders' } as any);
|
||||
@@ -113,7 +113,7 @@ describe('LdapConfigService — Bind-Passwort verschluesselt at rest', () => {
|
||||
it('gibt Aufrufern weiterhin ein entschluesseltes bindPassword', async () => {
|
||||
prisma.ldapConfig.findUnique.mockResolvedValue({
|
||||
...CONFIG_ROW,
|
||||
encryptedBindPassword: 'aa11:bb22:' + Buffer.from('geheim').toString('hex'),
|
||||
encryptedBindPassword: `aa11:bb22:${Buffer.from('geheim').toString('hex')}`,
|
||||
});
|
||||
|
||||
const config: any = await service.getConfig('t1');
|
||||
@@ -156,7 +156,7 @@ describe('LdapConfigService — Bind-Passwort verschluesselt at rest', () => {
|
||||
{
|
||||
id: 'b',
|
||||
tenantId: 't2',
|
||||
encryptedBindPassword: 'aa11:bb22:' + Buffer.from('schon').toString('hex'),
|
||||
encryptedBindPassword: `aa11:bb22:${Buffer.from('schon').toString('hex')}`,
|
||||
},
|
||||
{ id: 'c', tenantId: 't3', encryptedBindPassword: null },
|
||||
]);
|
||||
@@ -167,7 +167,7 @@ describe('LdapConfigService — Bind-Passwort verschluesselt at rest', () => {
|
||||
const call = prisma.ldapConfig.update.mock.calls[0][0];
|
||||
expect(call.where).toEqual({ id: 'a' });
|
||||
expect(call.data.encryptedBindPassword).toBe(
|
||||
'aa11:bb22:' + Buffer.from('klartext').toString('hex'),
|
||||
`aa11:bb22:${Buffer.from('klartext').toString('hex')}`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -176,7 +176,7 @@ describe('LdapConfigService — Bind-Passwort verschluesselt at rest', () => {
|
||||
{
|
||||
id: 'a',
|
||||
tenantId: 't1',
|
||||
encryptedBindPassword: 'aa11:bb22:' + Buffer.from('x').toString('hex'),
|
||||
encryptedBindPassword: `aa11:bb22:${Buffer.from('x').toString('hex')}`,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -354,7 +354,7 @@ describe('LdapConfigService — Bindung an forTenant() (260909-ipc)', () => {
|
||||
const call = boundClient.ldapConfig.update.mock.calls[0][0];
|
||||
expect(call.where).toEqual({ id: 'alt' });
|
||||
expect(call.data.encryptedBindPassword).toBe(
|
||||
'aa11:bb22:' + Buffer.from('klartext').toString('hex'),
|
||||
`aa11:bb22:${Buffer.from('klartext').toString('hex')}`,
|
||||
);
|
||||
// Der rohe Client schreibt NICHT.
|
||||
expect(prisma.ldapConfig.update).not.toHaveBeenCalled();
|
||||
|
||||
@@ -1545,7 +1545,7 @@ describe('LdapService.syncBoundGroupsForTenant — Rekonziliation gegen das Verz
|
||||
isDefault: false,
|
||||
},
|
||||
];
|
||||
prisma.group.update = vi.fn((args: any) => {
|
||||
prisma.group.update = vi.fn((_args: any) => {
|
||||
const err: any = new Error('Unique constraint');
|
||||
err.code = 'P2002';
|
||||
// A P2002 on the OTHER unique index this update() can hit —
|
||||
@@ -1793,7 +1793,7 @@ describe('LdapService.syncBoundGroupsForTenant — Rekonziliation gegen das Verz
|
||||
];
|
||||
mockSearch.mockImplementation((_baseDn: string, opts: any) => {
|
||||
const swept = sweptGuid(opts.filter);
|
||||
if (swept && swept.equals(guidBuffer)) {
|
||||
if (swept?.equals(guidBuffer)) {
|
||||
return Promise.reject(new Error('directory unavailable'));
|
||||
}
|
||||
return Promise.resolve({
|
||||
|
||||
@@ -322,13 +322,13 @@ export class LdapService {
|
||||
const dn = entry.dn;
|
||||
const isOu = /^ou=/i.test(dn);
|
||||
const record = entry as unknown as Record<string, unknown>;
|
||||
const rawName = isOu ? record['ou'] : record['cn'];
|
||||
const rawName = isOu ? record.ou : record.cn;
|
||||
const name = Array.isArray(rawName)
|
||||
? String(rawName[0])
|
||||
: rawName
|
||||
? String(rawName)
|
||||
: dn;
|
||||
const guidValue = record['objectGUID'];
|
||||
const guidValue = record.objectGUID;
|
||||
const guidHex =
|
||||
!isOu && Buffer.isBuffer(guidValue)
|
||||
? guidValue.toString('hex')
|
||||
@@ -401,7 +401,7 @@ export class LdapService {
|
||||
mappedData[mapping.tesseraField] = String(resolved);
|
||||
}
|
||||
}
|
||||
return { username: mappedData['username']?.toLowerCase(), mappedData };
|
||||
return { username: mappedData.username?.toLowerCase(), mappedData };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -483,13 +483,13 @@ export class LdapService {
|
||||
if (existing) {
|
||||
let emailToWrite: string | undefined;
|
||||
let emailConflict: LdapEmailConflict | undefined;
|
||||
if (mappedData['email']) {
|
||||
if (mappedData.email) {
|
||||
const decision = await this.resolveEmailForWrite(
|
||||
mappedData['email'],
|
||||
mappedData.email,
|
||||
existing.id,
|
||||
);
|
||||
if (decision.collides) {
|
||||
emailConflict = { account: username, email: mappedData['email'] };
|
||||
emailConflict = { account: username, email: mappedData.email };
|
||||
} else {
|
||||
emailToWrite = decision.email;
|
||||
}
|
||||
@@ -498,11 +498,11 @@ export class LdapService {
|
||||
await tenantPrisma.user.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
...(mappedData['displayName'] && {
|
||||
displayName: mappedData['displayName'],
|
||||
...(mappedData.displayName && {
|
||||
displayName: mappedData.displayName,
|
||||
}),
|
||||
...(emailToWrite && { email: emailToWrite }),
|
||||
...(mappedData['username'] && { username }),
|
||||
...(mappedData.username && { username }),
|
||||
ldapDn: dn,
|
||||
isActive: true,
|
||||
},
|
||||
@@ -511,16 +511,16 @@ export class LdapService {
|
||||
}
|
||||
|
||||
let createEmail: string | undefined =
|
||||
mappedData['email'] || `${username}@ldap.local`;
|
||||
mappedData.email || `${username}@ldap.local`;
|
||||
let emailConflict: LdapEmailConflict | undefined;
|
||||
if (mappedData['email']) {
|
||||
if (mappedData.email) {
|
||||
const decision = await this.resolveEmailForWrite(
|
||||
mappedData['email'],
|
||||
mappedData.email,
|
||||
null,
|
||||
);
|
||||
if (decision.collides) {
|
||||
createEmail = undefined;
|
||||
emailConflict = { account: username, email: mappedData['email'] };
|
||||
emailConflict = { account: username, email: mappedData.email };
|
||||
} else {
|
||||
createEmail = decision.email;
|
||||
}
|
||||
@@ -529,7 +529,7 @@ export class LdapService {
|
||||
await this.userService.create({
|
||||
username,
|
||||
...(createEmail && { email: createEmail }),
|
||||
displayName: mappedData['displayName'],
|
||||
displayName: mappedData.displayName,
|
||||
role: 'USER',
|
||||
tenantId,
|
||||
ldapDn: dn,
|
||||
@@ -592,9 +592,9 @@ export class LdapService {
|
||||
|
||||
const entries = Array.from(entriesByDn.values()).map((entry) => ({
|
||||
dn: entry.dn,
|
||||
username: first(entry['sAMAccountName']),
|
||||
displayName: first(entry['displayName']) || first(entry['cn']),
|
||||
email: first(entry['mail']),
|
||||
username: first(entry.sAMAccountName),
|
||||
displayName: first(entry.displayName) || first(entry.cn),
|
||||
email: first(entry.mail),
|
||||
}));
|
||||
|
||||
// Flag entries already present for this tenant (by ldapDn or username) in
|
||||
@@ -724,10 +724,10 @@ export class LdapService {
|
||||
// account is still created and counted, this manual-import path's
|
||||
// display stays as-is.
|
||||
let createEmail: string | undefined =
|
||||
mappedData['email'] || `${username}@ldap.local`;
|
||||
if (mappedData['email']) {
|
||||
mappedData.email || `${username}@ldap.local`;
|
||||
if (mappedData.email) {
|
||||
const decision = await this.resolveEmailForWrite(
|
||||
mappedData['email'],
|
||||
mappedData.email,
|
||||
null,
|
||||
);
|
||||
createEmail = decision.collides ? undefined : decision.email;
|
||||
@@ -736,7 +736,7 @@ export class LdapService {
|
||||
await this.userService.create({
|
||||
username,
|
||||
...(createEmail && { email: createEmail }),
|
||||
displayName: mappedData['displayName'],
|
||||
displayName: mappedData.displayName,
|
||||
role: 'USER',
|
||||
tenantId,
|
||||
ldapDn: dn,
|
||||
@@ -813,14 +813,14 @@ export class LdapService {
|
||||
|
||||
const entry = searchEntries[0];
|
||||
const record = entry as unknown as Record<string, unknown>;
|
||||
const guidValue = record['objectGUID'];
|
||||
const guidValue = record.objectGUID;
|
||||
if (!Buffer.isBuffer(guidValue)) {
|
||||
result.errors.push(`${dn}: objectGUID not readable`);
|
||||
continue;
|
||||
}
|
||||
const ldapObjectGuid = guidValue.toString('hex');
|
||||
|
||||
const rawName = record['cn'];
|
||||
const rawName = record.cn;
|
||||
const name = Array.isArray(rawName)
|
||||
? String(rawName[0])
|
||||
: rawName
|
||||
@@ -1436,7 +1436,7 @@ export class LdapService {
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const backfillGuid = backfillRecord['objectGUID'];
|
||||
const backfillGuid = backfillRecord.objectGUID;
|
||||
if (!Buffer.isBuffer(backfillGuid)) {
|
||||
result.errors.push(
|
||||
`Gruppe ${group.name}: Alt-Bindung ${group.ldapDn} ohne lesbaren objectGUID`,
|
||||
@@ -1492,7 +1492,7 @@ export class LdapService {
|
||||
if (hit) {
|
||||
// 3. Rename/DN reconciliation (SC-3).
|
||||
const hitRecord = hit as unknown as Record<string, unknown>;
|
||||
const rawName = hitRecord['cn'];
|
||||
const rawName = hitRecord.cn;
|
||||
const name = Array.isArray(rawName)
|
||||
? String(rawName[0])
|
||||
: rawName
|
||||
|
||||
@@ -76,7 +76,7 @@ describe('forTenant() — Array-Form von $transaction (WINDOWS #20)', () => {
|
||||
const scoped = forTenant(fakePrisma, 'tenant-a') as any;
|
||||
|
||||
let queryCallCount = 0;
|
||||
const query = (args: unknown) => {
|
||||
const query = (_args: unknown) => {
|
||||
queryCallCount += 1;
|
||||
return fakeQueryResult;
|
||||
};
|
||||
@@ -97,7 +97,7 @@ describe('forTenant() — Array-Form von $transaction (WINDOWS #20)', () => {
|
||||
|
||||
it('setzt den Mandantenkontext ueber ein getaggtes $executeRaw-Template, nicht ueber zusammengebauten Text', async () => {
|
||||
const fakePrisma: any = {
|
||||
$transaction: vi.fn((arg: unknown) => Promise.resolve(['set-config-result', 'query-result'])),
|
||||
$transaction: vi.fn((_arg: unknown) => Promise.resolve(['set-config-result', 'query-result'])),
|
||||
$extends: (config: any) => ({
|
||||
async __invoke(args: unknown, query: (args: unknown) => unknown) {
|
||||
return config.query.$allOperations({ args, query });
|
||||
@@ -286,7 +286,7 @@ describe('withTenantTransaction() — interaktive Callback-Form auf dem UNgebund
|
||||
it('setzt den Mandantenkontext als erste Anweisung DIREKT AUF tx, nicht auf dem aeusseren Client', async () => {
|
||||
const setConfigCalls: unknown[] = [];
|
||||
const fakeTx: any = {
|
||||
$executeRaw: vi.fn((strings: TemplateStringsArray, ...values: unknown[]) => {
|
||||
$executeRaw: vi.fn((_strings: TemplateStringsArray, ...values: unknown[]) => {
|
||||
setConfigCalls.push(values);
|
||||
return Promise.resolve(1);
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { CosinexAdapter } from './cosinex.adapter';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import AdmZip from 'adm-zip';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { buildDoeNoticeUrl, DoeOpenDataAdapter } from './doe-opendata.adapter';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { CryptoService } from '../../crypto/crypto.service';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { createHash } from 'crypto';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
import { ExchangeInboxProvider } from '../../inbox/exchange-inbox.provider';
|
||||
import { ImapProvider } from '../../inbox/imap.provider';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { NetServerAdapter } from './netserver.adapter';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { RssAdapter } from './rss.adapter';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { createHash } from 'crypto';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { XMLParser } from 'fast-xml-parser';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import type { RawTenderRecord, SourceType } from '../tender.types';
|
||||
|
||||
@@ -235,8 +235,8 @@ describe('TenderDigestScheduler — Multi-Tenant via findMany (Pitfall 1)', () =
|
||||
});
|
||||
|
||||
it('never uses findFirst anywhere in the module source — findMany over all due users is mandatory (Pitfall 1)', async () => {
|
||||
const { readFileSync } = await import('fs');
|
||||
const { join } = await import('path');
|
||||
const { readFileSync } = await import('node:fs');
|
||||
const { join } = await import('node:path');
|
||||
const source = readFileSync(join(__dirname, 'tender-digest.scheduler.ts'), 'utf8');
|
||||
expect(source).not.toMatch(/findFirst/);
|
||||
});
|
||||
|
||||
@@ -168,7 +168,7 @@ export class TenderDigestScheduler implements OnModuleInit {
|
||||
// Kein Konto, oder ein Konto ohne Adresse (WINDOWS #15, kollidierte
|
||||
// AD-Adresse) -- die Zugehoerigkeit funktioniert, nur der
|
||||
// Mailversand wird uebersprungen (zugesagtes Verhalten).
|
||||
if (!user || !user.email) continue;
|
||||
if (!user?.email) continue;
|
||||
|
||||
const sections = groupMatchesByProfile(matches);
|
||||
const sent = await this.mail.sendDigest({ email: user.email }, user.tenantId, sections);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createHash } from 'crypto';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
/**
|
||||
* tenderFingerprint — pure, deterministic cross-source dedup key (SCHEMA-03,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { TenderIngestionService } from './tender-ingestion.service';
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ export class TenderMatchingService {
|
||||
// Kein Konto, oder ein Konto ohne Adresse (WINDOWS #15, kollidierte
|
||||
// AD-Adresse) -- die Zugehoerigkeit funktioniert, nur der
|
||||
// Mailversand wird uebersprungen (zugesagtes Verhalten).
|
||||
if (!user || !user.email) continue;
|
||||
if (!user?.email) continue;
|
||||
|
||||
const sent = await this.mail.sendInstant(
|
||||
{ email: user.email },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import AdmZip from 'adm-zip';
|
||||
import { XMLParser } from 'fast-xml-parser';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { RawTenderRecord, SourceType } from './tender.types';
|
||||
import { TenderNormalizerService } from './tender-normalizer.service';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { createHash } from 'crypto';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { divisionOf } from './cpv/cpv-catalog';
|
||||
import { bundeslandFromRegion } from './geo/nuts-bundesland';
|
||||
import type { NormalizedTenderFields, RawTenderRecord } from './tender.types';
|
||||
|
||||
@@ -64,7 +64,7 @@ function makeFakePrisma() {
|
||||
|
||||
const tenderRssFeedSource = {
|
||||
findMany: async ({ where, orderBy }: any = {}) => {
|
||||
let all = [...rows.values()].filter((row) => matchesWhere(row, where));
|
||||
const all = [...rows.values()].filter((row) => matchesWhere(row, where));
|
||||
if (orderBy?.createdAt === 'asc') {
|
||||
all.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ModuleRegistryService } from '../module-registry/module-registry.service';
|
||||
import { TenderSchedulerService } from './tender-scheduler.service';
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { Role } from '@prisma/client';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'node:fs';
|
||||
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';
|
||||
@@ -293,7 +293,7 @@ export class UserController {
|
||||
@UploadedFile() file: any,
|
||||
@CurrentUser() currentUser: any,
|
||||
) {
|
||||
if (!file || !file.buffer) {
|
||||
if (!file?.buffer) {
|
||||
throw new BadRequestException('No file provided');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user