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:
2026-09-21 08:58:32 +02:00
parent 8d1c8f320b
commit 636fe0df8f
56 changed files with 126 additions and 147 deletions
+1 -14
View File
@@ -216,19 +216,6 @@ async function forTenantQuery(prisma, tenantId, queryFn, userId) {
return result; return result;
} }
/**
* Spiegelbildlich zu `forSystem()` in apps/api/src/prisma/prisma-tenant.extension.ts
* (Etappe 3c, 260914-eym) — bei jeder Aenderung dort HIER nachziehen: EINE
* getaggte Anweisung setzt `app.system_context = 'true'` und AUSDRUECKLICH
* `app.current_tenant = ''` und `app.current_user = ''`, alle drei als
* Literale; danach die Abfrage in derselben Array-Transaktion.
*/
async function forSystemQuery(prisma, queryFn) {
const setContext = prisma.$executeRaw`SELECT set_config('app.system_context', 'true', true), set_config('app.current_tenant', '', true), set_config('app.current_user', '', true)`;
const [, result] = await prisma.$transaction([setContext, queryFn(prisma)]);
return result;
}
/** /**
* Aufgabe 1 — misst die fuenf im Plan genannten Verhaltensweisen von * Aufgabe 1 — misst die fuenf im Plan genannten Verhaltensweisen von
* forTenant() unter der Rolle ohne BYPASSRLS. * forTenant() unter der Rolle ohne BYPASSRLS.
@@ -3242,7 +3229,7 @@ async function runCalendarAreaChecks(adminUrl, scratchRoleUrl, results) {
// gebunden MIT Benutzer jetzt NICHT mehr lesbar ist (siehe Umkehrung // gebunden MIT Benutzer jetzt NICHT mehr lesbar ist (siehe Umkehrung
// unten). // unten).
const a2Row = rowsForA.find((r) => r.userId === 'user-a2'); const a2Row = rowsForA.find((r) => r.userId === 'user-a2');
const a2CredentialsVisible = Boolean(a2Row && a2Row.encryptedPassword); const a2CredentialsVisible = Boolean(a2Row?.encryptedPassword);
report( report(
results, results,
'calendarsource-ohne-benutzer-sieht-beide-nutzer-desselben-mandanten', 'calendarsource-ohne-benutzer-sieht-beide-nutzer-desselben-mandanten',
+5 -5
View File
@@ -9,7 +9,7 @@ import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { Role } from '@prisma/client'; import { Role } from '@prisma/client';
import * as argon2 from 'argon2'; import * as argon2 from 'argon2';
import { randomUUID } from 'crypto'; import { randomUUID } from 'node:crypto';
import { Response } from 'express'; import { Response } from 'express';
import { LdapConfigService } from '../ldap/ldap-config.service'; import { LdapConfigService } from '../ldap/ldap-config.service';
import { LdapService } from '../ldap/ldap.service'; import { LdapService } from '../ldap/ldap.service';
@@ -111,7 +111,7 @@ export class AuthService {
`; `;
const user = rows[0]; const user = rows[0];
if (!user || !user.isActive) { if (!user?.isActive) {
return null; return null;
} }
@@ -125,7 +125,7 @@ export class AuthService {
} }
const config = await this.ldapConfigService.getConfig(user.tenantId); const config = await this.ldapConfigService.getConfig(user.tenantId);
if (!config || !config.isActive) { if (!config?.isActive) {
return null; return null;
} }
@@ -220,7 +220,7 @@ export class AuthService {
const user = rows[0]; const user = rows[0];
// Always return success to prevent email enumeration (T-02-12) // Always return success to prevent email enumeration (T-02-12)
if (!user || !user.isActive) { if (!user?.isActive) {
this.logger.log( this.logger.log(
`Password reset requested for unknown/inactive email: ${email}`, `Password reset requested for unknown/inactive email: ${email}`,
); );
@@ -358,7 +358,7 @@ export class AuthService {
where: { id: userId }, where: { id: userId },
}); });
if (!user || !user.passwordHash) { if (!user?.passwordHash) {
throw new UnauthorizedException('User not found or has no local password'); throw new UnauthorizedException('User not found or has no local password');
} }
@@ -1,7 +1,7 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common'; import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator( export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => { (_data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest(); const request = ctx.switchToHttp().getRequest();
return request.user; return request.user;
}, },
@@ -50,7 +50,6 @@ export class ForcePasswordChangeInterceptor implements NestInterceptor {
// Allow specific routes even when password change is required // Allow specific routes even when password change is required
const path = request.route?.path || request.url; const path = request.route?.path || request.url;
const method = request.method;
const allowedPaths = [ const allowedPaths = [
'/auth/change-password', '/auth/change-password',
+2 -2
View File
@@ -8,8 +8,8 @@ import { Request } from 'express';
* Custom extractor that reads JWT from the httpOnly "session" cookie. * Custom extractor that reads JWT from the httpOnly "session" cookie.
*/ */
function cookieExtractor(req: Request): string | null { function cookieExtractor(req: Request): string | null {
if (req && req.cookies) { if (req?.cookies) {
return req.cookies['session'] || null; return req.cookies.session || null;
} }
return null; return null;
} }
+2 -2
View File
@@ -318,7 +318,7 @@ export class CalendarService {
}); });
return { success }; return { success };
} catch (error) { } catch {
const errorMsg = 'Connection failed'; // T-05-13: generic error, no credentials const errorMsg = 'Connection failed'; // T-05-13: generic error, no credentials
await tenantPrisma.calendarSource.update({ await tenantPrisma.calendarSource.update({
where: { id }, where: { id },
@@ -355,7 +355,7 @@ export class CalendarService {
try { try {
const success = await provider.testConnection(tempSource); const success = await provider.testConnection(tempSource);
return { success }; return { success };
} catch (e: any) { } catch {
return { success: false, error: 'Connection failed' }; return { success: false, error: 'Connection failed' };
} }
} }
@@ -1,5 +1,4 @@
import { import {
IsBoolean,
IsHexColor, IsHexColor,
IsIn, IsIn,
IsNotEmpty, IsNotEmpty,
@@ -47,7 +47,7 @@ export class CalDAVProvider implements CalendarProvider {
for (const key of Object.keys(parsed)) { for (const key of Object.keys(parsed)) {
const component = parsed[key]; const component = parsed[key];
if (!component || component.type !== 'VEVENT') continue; if (component?.type !== 'VEVENT') continue;
const vevent = component as ical.VEvent; const vevent = component as ical.VEvent;
const start = new Date(vevent.start); const start = new Date(vevent.start);
@@ -180,8 +180,8 @@ export class ExchangeProvider implements CalendarProvider {
id: `${source.id}-${item.id}`, id: `${source.id}-${item.id}`,
sourceId: source.id, sourceId: source.id,
title: item.subject || 'Untitled', title: item.subject || 'Untitled',
start: new Date(item.start?.dateTime + 'Z'), start: new Date(`${item.start?.dateTime}Z`),
end: new Date(item.end?.dateTime + 'Z'), end: new Date(`${item.end?.dateTime}Z`),
allDay: item.isAllDay || false, allDay: item.isAllDay || false,
location: item.location?.displayName || undefined, location: item.location?.displayName || undefined,
description: item.bodyPreview || undefined, description: item.bodyPreview || undefined,
@@ -30,7 +30,7 @@ export class ICSProvider implements CalendarProvider {
for (const key of Object.keys(data)) { for (const key of Object.keys(data)) {
const component = data[key]; const component = data[key];
if (!component || component.type !== 'VEVENT') continue; if (component?.type !== 'VEVENT') continue;
const vevent = component as ical.VEvent; const vevent = component as ical.VEvent;
@@ -116,7 +116,7 @@ describe('CertManagerService helpers', () => {
const cert2 = generateSelfSignedCert(); const cert2 = generateSelfSignedCert();
const pem1 = forge.pki.certificateToPem(cert1); const pem1 = forge.pki.certificateToPem(cert1);
const pem2 = forge.pki.certificateToPem(cert2); const pem2 = forge.pki.certificateToPem(cert2);
const chain = pem1 + '\n' + pem2; const chain = `${pem1}\n${pem2}`;
const parsed = service.parsePemChain(chain); const parsed = service.parsePemChain(chain);
expect(parsed).toHaveLength(2); expect(parsed).toHaveLength(2);
@@ -266,7 +266,7 @@ describe('splitCerts', () => {
cert2Pem = forge.pki.certificateToPem(c2); cert2Pem = forge.pki.certificateToPem(c2);
// Fullchain fixture: two PEMs concatenated // Fullchain fixture: two PEMs concatenated
const fullchainPem = cert1Pem + '\n' + cert2Pem; const fullchainPem = `${cert1Pem}\n${cert2Pem}`;
fullchainBuffer = Buffer.from(fullchainPem, 'utf-8'); fullchainBuffer = Buffer.from(fullchainPem, 'utf-8');
// P7B fixture: PEM-wrapped PKCS7 SignedData bundle with both certs // P7B fixture: PEM-wrapped PKCS7 SignedData bundle with both certs
@@ -302,7 +302,7 @@ export class CertManagerService {
fingerprint: { sha1, sha256 }, fingerprint: { sha1, sha256 },
pemPreview: forge.pki.certificateToPem(cert), pemPreview: forge.pki.certificateToPem(cert),
}; };
} catch (err) { } catch {
this.logger.warn('parseCert: failed to extract CertDetails fields'); this.logger.warn('parseCert: failed to extract CertDetails fields');
throw new BadRequestException('Failed to extract certificate details'); throw new BadRequestException('Failed to extract certificate details');
} }
@@ -505,7 +505,7 @@ export class CertManagerService {
return { return {
filename: 'chain.pem', filename: 'chain.pem',
content, content,
mimeType: FORMAT_MIME['pem'], mimeType: FORMAT_MIME.pem,
}; };
} else if (outputFormat === 'pfx') { } else if (outputFormat === 'pfx') {
// Open Question 1 resolution: toPkcs12Asn1(null, certs, password) works in node-forge 1.4.0 // Open Question 1 resolution: toPkcs12Asn1(null, certs, password) works in node-forge 1.4.0
@@ -523,7 +523,7 @@ export class CertManagerService {
return { return {
filename: 'bundle.pfx', filename: 'bundle.pfx',
content, content,
mimeType: FORMAT_MIME['pfx'], mimeType: FORMAT_MIME.pfx,
}; };
} else { } else {
throw new BadRequestException( throw new BadRequestException(
@@ -667,7 +667,7 @@ export class CertManagerService {
return { return {
filename: 'converted.pfx', filename: 'converted.pfx',
content, content,
mimeType: FORMAT_MIME['pfx'], mimeType: FORMAT_MIME.pfx,
}; };
} }
@@ -676,7 +676,7 @@ export class CertManagerService {
content, content,
mimeType: FORMAT_MIME[targetFormat], mimeType: FORMAT_MIME[targetFormat],
}; };
} catch (err) { } catch {
this.logger.warn('convertCert: failed to serialize to target format'); this.logger.warn('convertCert: failed to serialize to target format');
throw new BadRequestException( throw new BadRequestException(
`Failed to convert certificate to ${targetFormat}: serialization error`, `Failed to convert certificate to ${targetFormat}: serialization error`,
+1 -1
View File
@@ -1,6 +1,6 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; 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. */ /** Current name of the platform-wide encryption key. */
export const ENCRYPTION_KEY_ENV = 'TESSERA_ENCRYPTION_KEY'; export const ENCRYPTION_KEY_ENV = 'TESSERA_ENCRYPTION_KEY';
+5 -5
View File
@@ -1,8 +1,8 @@
import 'reflect-metadata'; import 'reflect-metadata';
import * as crypto from 'crypto'; import * as crypto from 'node:crypto';
import * as fs from 'fs'; import * as fs from 'node:fs';
import * as os from 'os'; import * as os from 'node:os';
import * as path from 'path'; import * as path from 'node:path';
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { BadRequestException, NotFoundException } from '@nestjs/common'; import { BadRequestException, NotFoundException } from '@nestjs/common';
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'; 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', () => { 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; const previous = process.env.DESKTOP_DIST_DIR;
process.env.DESKTOP_DIST_DIR = missingDir; process.env.DESKTOP_DIST_DIR = missingDir;
try { try {
+2 -2
View File
@@ -6,8 +6,8 @@ import type {
DesktopPlatform, DesktopPlatform,
DesktopUpdateResponse, DesktopUpdateResponse,
} from '@tessera/shared'; } from '@tessera/shared';
import * as fs from 'fs'; import * as fs from 'node:fs';
import * as path from 'path'; import * as path from 'node:path';
/** /**
* Wertevorrat der Plattformen (Phase 18, D-10). Geschlossen -- eine dritte * Wertevorrat der Plattformen (Phase 18, D-10). Geschlossen -- eine dritte
+2 -2
View File
@@ -1,6 +1,6 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import * as fs from 'fs'; import * as fs from 'node:fs';
import * as path from 'path'; import * as path from 'node:path';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import { ExportRow } from './dkv.types'; import { ExportRow } from './dkv.types';
+2 -2
View File
@@ -39,7 +39,7 @@ export class DkvParserService {
let text: string; let text: string;
try { try {
text = await this.extractText(buffer); text = await this.extractText(buffer);
} catch (err) { } catch {
this.logger.error('DKV PDF text extraction failed'); this.logger.error('DKV PDF text extraction failed');
throw new Error('DKV invoice parsing failed — text extraction error'); 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 // Anchor on VEHICLE: marker — each block extends until next VEHICLE: or end
// Kennzeichen format: "GP-JL 728E", "GP ML 720", etc. // Kennzeichen format: "GP-JL 728E", "GP ML 720", etc.
const vehicleBlockPattern = 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; let match: RegExpExecArray | null;
while ((match = vehicleBlockPattern.exec(text)) !== null) { while ((match = vehicleBlockPattern.exec(text)) !== null) {
+1 -1
View File
@@ -133,7 +133,7 @@ function parseDkvText(text: string): DkvVehicleBlock[] {
// Anchor on VEHICLE: marker — each block extends until next VEHICLE: or end of text // 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") // Kennzeichen can contain letters, digits, hyphens, spaces, dots (e.g. "GP-JL 728E")
const vehicleBlockPattern = 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; let match: RegExpExecArray | null;
while ((match = vehicleBlockPattern.exec(text)) !== null) { while ((match = vehicleBlockPattern.exec(text)) !== null) {
+1 -1
View File
@@ -219,7 +219,7 @@ export class DkvController {
) { ) {
const tenantId = this._requireTenant(req); const tenantId = this._requireTenant(req);
if (!file || !file.buffer) { if (!file?.buffer) {
throw new BadRequestException('No CSV file uploaded (field name must be "file")'); throw new BadRequestException('No CSV file uploaded (field name must be "file")');
} }
+1 -1
View 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 { afterEach, describe, expect, it, vi } from 'vitest';
import { DkvService } from './dkv.service'; import { DkvService } from './dkv.service';
+7 -7
View File
@@ -5,8 +5,8 @@ import {
NotFoundException, NotFoundException,
} from '@nestjs/common'; } from '@nestjs/common';
import { CryptoService } from '../crypto/crypto.service'; import { CryptoService } from '../crypto/crypto.service';
import * as fs from 'fs'; import * as fs from 'node:fs';
import * as path from 'path'; import * as path from 'node:path';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { forSystem, forTenant } from '../prisma/prisma-tenant.extension'; import { forSystem, forTenant } from '../prisma/prisma-tenant.extension';
@@ -513,7 +513,7 @@ export class DkvService {
smtpError = (err as Error).message; smtpError = (err as Error).message;
this.logger.warn(`DKV SMTP send attempt ${attempt}/3 failed: ${smtpError}`); this.logger.warn(`DKV SMTP send attempt ${attempt}/3 failed: ${smtpError}`);
if (attempt < 3) { if (attempt < 3) {
await _delay(Math.pow(2, attempt) * 1000); // 2s, 4s await _delay(2 ** attempt * 1000); // 2s, 4s
} else { } else {
smtpStatus = 'Versand fehlgeschlagen'; smtpStatus = 'Versand fehlgeschlagen';
// D-16: file stays locally available for manual download // D-16: file stays locally available for manual download
@@ -707,7 +707,7 @@ export class DkvService {
filename.includes('/') || filename.includes('/') ||
filename.includes('\\') || 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'); throw new BadRequestException('Invalid export filename');
} }
@@ -806,9 +806,9 @@ export class DkvService {
): string { ): string {
if (fromPdf) return fromPdf.replace(/\//g, '-'); if (fromPdf) return fromPdf.replace(/\//g, '-');
// DKV email subjects carry the invoice number with slashes: "26/650869002/002" // 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, '-'); 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. * Example: "GP JL 740E" == "GP-JL 740E" == "GPJL740E" after normalization.
*/ */
function _normalizeKennzeichen(k: string): string { 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 { Injectable } from '@nestjs/common';
import { lookup } from 'dns/promises'; import { lookup } from 'node:dns/promises';
import { isIP } from 'net'; import { isIP } from 'node:net';
import { Agent, fetch as undiciFetch, type Response as UndiciResponse } from 'undici'; import { Agent, fetch as undiciFetch, type Response as UndiciResponse } from 'undici';
/** /**
+1 -1
View File
@@ -1044,7 +1044,7 @@ describe('GroupsService — Bindung an forTenant()/withTenantTransaction() (2609
it('reassignDefaultBeforeDelete() bindet die drei Lesezugriffe UND die Transaktion an denselben Mandanten', async () => { it('reassignDefaultBeforeDelete() bindet die drei Lesezugriffe UND die Transaktion an denselben Mandanten', async () => {
const prisma = makeFakePrisma(); const prisma = makeFakePrisma();
const service = new GroupsService(prisma as any); 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' }); const toDelete = await service.create('t1', { name: 'Zu loeschen' });
await service.update('t1', toDelete.id, { isDefault: true }); await service.update('t1', toDelete.id, { isDefault: true });
+1 -1
View File
@@ -440,7 +440,7 @@ export class GroupsService {
const group = await tenantPrisma.group.findFirst({ const group = await tenantPrisma.group.findFirst({
where: { id: groupId, tenantId }, where: { id: groupId, tenantId },
}); });
if (!group || !group.isDefault) { if (!group?.isDefault) {
return false; return false;
} }
+1 -1
View File
@@ -20,7 +20,7 @@ vi.mock('imapflow', () => ({
})); }));
function makeReadable(text: string): NodeJS.ReadableStream { 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')]); 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; const written = prisma.ldapConfig.create.mock.calls[0][0].data;
expect(written.encryptedBindPassword).toBe( 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'); expect(JSON.stringify(written)).not.toContain('geheim');
// Die alte Klartext-Spalte darf nicht wieder auftauchen. // 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); await service.updateConfig('t1', { bindPassword: 'neu' } as any);
const first = prisma.ldapConfig.update.mock.calls[0][0].data; const first = prisma.ldapConfig.update.mock.calls[0][0].data;
expect(first.encryptedBindPassword).toBe( 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); 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 () => { it('gibt Aufrufern weiterhin ein entschluesseltes bindPassword', async () => {
prisma.ldapConfig.findUnique.mockResolvedValue({ prisma.ldapConfig.findUnique.mockResolvedValue({
...CONFIG_ROW, ...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'); const config: any = await service.getConfig('t1');
@@ -156,7 +156,7 @@ describe('LdapConfigService — Bind-Passwort verschluesselt at rest', () => {
{ {
id: 'b', id: 'b',
tenantId: 't2', tenantId: 't2',
encryptedBindPassword: 'aa11:bb22:' + Buffer.from('schon').toString('hex'), encryptedBindPassword: `aa11:bb22:${Buffer.from('schon').toString('hex')}`,
}, },
{ id: 'c', tenantId: 't3', encryptedBindPassword: null }, { 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]; const call = prisma.ldapConfig.update.mock.calls[0][0];
expect(call.where).toEqual({ id: 'a' }); expect(call.where).toEqual({ id: 'a' });
expect(call.data.encryptedBindPassword).toBe( 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', id: 'a',
tenantId: 't1', 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]; const call = boundClient.ldapConfig.update.mock.calls[0][0];
expect(call.where).toEqual({ id: 'alt' }); expect(call.where).toEqual({ id: 'alt' });
expect(call.data.encryptedBindPassword).toBe( expect(call.data.encryptedBindPassword).toBe(
'aa11:bb22:' + Buffer.from('klartext').toString('hex'), `aa11:bb22:${Buffer.from('klartext').toString('hex')}`,
); );
// Der rohe Client schreibt NICHT. // Der rohe Client schreibt NICHT.
expect(prisma.ldapConfig.update).not.toHaveBeenCalled(); expect(prisma.ldapConfig.update).not.toHaveBeenCalled();
+2 -2
View File
@@ -1545,7 +1545,7 @@ describe('LdapService.syncBoundGroupsForTenant — Rekonziliation gegen das Verz
isDefault: false, isDefault: false,
}, },
]; ];
prisma.group.update = vi.fn((args: any) => { prisma.group.update = vi.fn((_args: any) => {
const err: any = new Error('Unique constraint'); const err: any = new Error('Unique constraint');
err.code = 'P2002'; err.code = 'P2002';
// A P2002 on the OTHER unique index this update() can hit — // 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) => { mockSearch.mockImplementation((_baseDn: string, opts: any) => {
const swept = sweptGuid(opts.filter); const swept = sweptGuid(opts.filter);
if (swept && swept.equals(guidBuffer)) { if (swept?.equals(guidBuffer)) {
return Promise.reject(new Error('directory unavailable')); return Promise.reject(new Error('directory unavailable'));
} }
return Promise.resolve({ return Promise.resolve({
+25 -25
View File
@@ -322,13 +322,13 @@ export class LdapService {
const dn = entry.dn; const dn = entry.dn;
const isOu = /^ou=/i.test(dn); const isOu = /^ou=/i.test(dn);
const record = entry as unknown as Record<string, unknown>; 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) const name = Array.isArray(rawName)
? String(rawName[0]) ? String(rawName[0])
: rawName : rawName
? String(rawName) ? String(rawName)
: dn; : dn;
const guidValue = record['objectGUID']; const guidValue = record.objectGUID;
const guidHex = const guidHex =
!isOu && Buffer.isBuffer(guidValue) !isOu && Buffer.isBuffer(guidValue)
? guidValue.toString('hex') ? guidValue.toString('hex')
@@ -401,7 +401,7 @@ export class LdapService {
mappedData[mapping.tesseraField] = String(resolved); 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) { if (existing) {
let emailToWrite: string | undefined; let emailToWrite: string | undefined;
let emailConflict: LdapEmailConflict | undefined; let emailConflict: LdapEmailConflict | undefined;
if (mappedData['email']) { if (mappedData.email) {
const decision = await this.resolveEmailForWrite( const decision = await this.resolveEmailForWrite(
mappedData['email'], mappedData.email,
existing.id, existing.id,
); );
if (decision.collides) { if (decision.collides) {
emailConflict = { account: username, email: mappedData['email'] }; emailConflict = { account: username, email: mappedData.email };
} else { } else {
emailToWrite = decision.email; emailToWrite = decision.email;
} }
@@ -498,11 +498,11 @@ export class LdapService {
await tenantPrisma.user.update({ await tenantPrisma.user.update({
where: { id: existing.id }, where: { id: existing.id },
data: { data: {
...(mappedData['displayName'] && { ...(mappedData.displayName && {
displayName: mappedData['displayName'], displayName: mappedData.displayName,
}), }),
...(emailToWrite && { email: emailToWrite }), ...(emailToWrite && { email: emailToWrite }),
...(mappedData['username'] && { username }), ...(mappedData.username && { username }),
ldapDn: dn, ldapDn: dn,
isActive: true, isActive: true,
}, },
@@ -511,16 +511,16 @@ export class LdapService {
} }
let createEmail: string | undefined = let createEmail: string | undefined =
mappedData['email'] || `${username}@ldap.local`; mappedData.email || `${username}@ldap.local`;
let emailConflict: LdapEmailConflict | undefined; let emailConflict: LdapEmailConflict | undefined;
if (mappedData['email']) { if (mappedData.email) {
const decision = await this.resolveEmailForWrite( const decision = await this.resolveEmailForWrite(
mappedData['email'], mappedData.email,
null, null,
); );
if (decision.collides) { if (decision.collides) {
createEmail = undefined; createEmail = undefined;
emailConflict = { account: username, email: mappedData['email'] }; emailConflict = { account: username, email: mappedData.email };
} else { } else {
createEmail = decision.email; createEmail = decision.email;
} }
@@ -529,7 +529,7 @@ export class LdapService {
await this.userService.create({ await this.userService.create({
username, username,
...(createEmail && { email: createEmail }), ...(createEmail && { email: createEmail }),
displayName: mappedData['displayName'], displayName: mappedData.displayName,
role: 'USER', role: 'USER',
tenantId, tenantId,
ldapDn: dn, ldapDn: dn,
@@ -592,9 +592,9 @@ export class LdapService {
const entries = Array.from(entriesByDn.values()).map((entry) => ({ const entries = Array.from(entriesByDn.values()).map((entry) => ({
dn: entry.dn, dn: entry.dn,
username: first(entry['sAMAccountName']), username: first(entry.sAMAccountName),
displayName: first(entry['displayName']) || first(entry['cn']), displayName: first(entry.displayName) || first(entry.cn),
email: first(entry['mail']), email: first(entry.mail),
})); }));
// Flag entries already present for this tenant (by ldapDn or username) in // 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 // account is still created and counted, this manual-import path's
// display stays as-is. // display stays as-is.
let createEmail: string | undefined = let createEmail: string | undefined =
mappedData['email'] || `${username}@ldap.local`; mappedData.email || `${username}@ldap.local`;
if (mappedData['email']) { if (mappedData.email) {
const decision = await this.resolveEmailForWrite( const decision = await this.resolveEmailForWrite(
mappedData['email'], mappedData.email,
null, null,
); );
createEmail = decision.collides ? undefined : decision.email; createEmail = decision.collides ? undefined : decision.email;
@@ -736,7 +736,7 @@ export class LdapService {
await this.userService.create({ await this.userService.create({
username, username,
...(createEmail && { email: createEmail }), ...(createEmail && { email: createEmail }),
displayName: mappedData['displayName'], displayName: mappedData.displayName,
role: 'USER', role: 'USER',
tenantId, tenantId,
ldapDn: dn, ldapDn: dn,
@@ -813,14 +813,14 @@ export class LdapService {
const entry = searchEntries[0]; const entry = searchEntries[0];
const record = entry as unknown as Record<string, unknown>; const record = entry as unknown as Record<string, unknown>;
const guidValue = record['objectGUID']; const guidValue = record.objectGUID;
if (!Buffer.isBuffer(guidValue)) { if (!Buffer.isBuffer(guidValue)) {
result.errors.push(`${dn}: objectGUID not readable`); result.errors.push(`${dn}: objectGUID not readable`);
continue; continue;
} }
const ldapObjectGuid = guidValue.toString('hex'); const ldapObjectGuid = guidValue.toString('hex');
const rawName = record['cn']; const rawName = record.cn;
const name = Array.isArray(rawName) const name = Array.isArray(rawName)
? String(rawName[0]) ? String(rawName[0])
: rawName : rawName
@@ -1436,7 +1436,7 @@ export class LdapService {
string, string,
unknown unknown
>; >;
const backfillGuid = backfillRecord['objectGUID']; const backfillGuid = backfillRecord.objectGUID;
if (!Buffer.isBuffer(backfillGuid)) { if (!Buffer.isBuffer(backfillGuid)) {
result.errors.push( result.errors.push(
`Gruppe ${group.name}: Alt-Bindung ${group.ldapDn} ohne lesbaren objectGUID`, `Gruppe ${group.name}: Alt-Bindung ${group.ldapDn} ohne lesbaren objectGUID`,
@@ -1492,7 +1492,7 @@ export class LdapService {
if (hit) { if (hit) {
// 3. Rename/DN reconciliation (SC-3). // 3. Rename/DN reconciliation (SC-3).
const hitRecord = hit as unknown as Record<string, unknown>; const hitRecord = hit as unknown as Record<string, unknown>;
const rawName = hitRecord['cn']; const rawName = hitRecord.cn;
const name = Array.isArray(rawName) const name = Array.isArray(rawName)
? String(rawName[0]) ? String(rawName[0])
: rawName : rawName
@@ -76,7 +76,7 @@ describe('forTenant() — Array-Form von $transaction (WINDOWS #20)', () => {
const scoped = forTenant(fakePrisma, 'tenant-a') as any; const scoped = forTenant(fakePrisma, 'tenant-a') as any;
let queryCallCount = 0; let queryCallCount = 0;
const query = (args: unknown) => { const query = (_args: unknown) => {
queryCallCount += 1; queryCallCount += 1;
return fakeQueryResult; 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 () => { it('setzt den Mandantenkontext ueber ein getaggtes $executeRaw-Template, nicht ueber zusammengebauten Text', async () => {
const fakePrisma: any = { 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) => ({ $extends: (config: any) => ({
async __invoke(args: unknown, query: (args: unknown) => unknown) { async __invoke(args: unknown, query: (args: unknown) => unknown) {
return config.query.$allOperations({ args, query }); 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 () => { it('setzt den Mandantenkontext als erste Anweisung DIREKT AUF tx, nicht auf dem aeusseren Client', async () => {
const setConfigCalls: unknown[] = []; const setConfigCalls: unknown[] = [];
const fakeTx: any = { const fakeTx: any = {
$executeRaw: vi.fn((strings: TemplateStringsArray, ...values: unknown[]) => { $executeRaw: vi.fn((_strings: TemplateStringsArray, ...values: unknown[]) => {
setConfigCalls.push(values); setConfigCalls.push(values);
return Promise.resolve(1); return Promise.resolve(1);
}), }),
@@ -1,5 +1,5 @@
import { readFileSync } from 'fs'; import { readFileSync } from 'node:fs';
import { join } from 'path'; import { join } from 'node:path';
import { afterEach, describe, expect, it, vi } from 'vitest'; import { afterEach, describe, expect, it, vi } from 'vitest';
import { CosinexAdapter } from './cosinex.adapter'; import { CosinexAdapter } from './cosinex.adapter';
@@ -1,6 +1,6 @@
import AdmZip from 'adm-zip'; import AdmZip from 'adm-zip';
import { readFileSync } from 'fs'; import { readFileSync } from 'node:fs';
import { join } from 'path'; import { join } from 'node:path';
import { afterEach, describe, expect, it, vi } from 'vitest'; import { afterEach, describe, expect, it, vi } from 'vitest';
import { buildDoeNoticeUrl, DoeOpenDataAdapter } from './doe-opendata.adapter'; import { buildDoeNoticeUrl, DoeOpenDataAdapter } from './doe-opendata.adapter';
@@ -1,5 +1,5 @@
import { readFileSync } from 'fs'; import { readFileSync } from 'node:fs';
import { join } from 'path'; import { join } from 'node:path';
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { describe, expect, it, vi } from 'vitest'; import { describe, expect, it, vi } from 'vitest';
import { import {
@@ -1,7 +1,7 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { CryptoService } from '../../crypto/crypto.service'; import { CryptoService } from '../../crypto/crypto.service';
import * as cheerio from 'cheerio'; import * as cheerio from 'cheerio';
import { createHash } from 'crypto'; import { createHash } from 'node:crypto';
import { ExchangeInboxProvider } from '../../inbox/exchange-inbox.provider'; import { ExchangeInboxProvider } from '../../inbox/exchange-inbox.provider';
import { ImapProvider } from '../../inbox/imap.provider'; import { ImapProvider } from '../../inbox/imap.provider';
@@ -1,5 +1,5 @@
import { readFileSync } from 'fs'; import { readFileSync } from 'node:fs';
import { join } from 'path'; import { join } from 'node:path';
import { afterEach, describe, expect, it, vi } from 'vitest'; import { afterEach, describe, expect, it, vi } from 'vitest';
import { NetServerAdapter } from './netserver.adapter'; import { NetServerAdapter } from './netserver.adapter';
@@ -1,5 +1,5 @@
import { readFileSync } from 'fs'; import { readFileSync } from 'node:fs';
import { join } from 'path'; import { join } from 'node:path';
import { afterEach, describe, expect, it, vi } from 'vitest'; import { afterEach, describe, expect, it, vi } from 'vitest';
import { RssAdapter } from './rss.adapter'; import { RssAdapter } from './rss.adapter';
+1 -1
View File
@@ -1,5 +1,5 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { createHash } from 'crypto'; import { createHash } from 'node:crypto';
import { XMLParser } from 'fast-xml-parser'; import { XMLParser } from 'fast-xml-parser';
import { PrismaService } from '../../prisma/prisma.service'; import { PrismaService } from '../../prisma/prisma.service';
import type { RawTenderRecord, SourceType } from '../tender.types'; 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 () => { 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 { readFileSync } = await import('node:fs');
const { join } = await import('path'); const { join } = await import('node:path');
const source = readFileSync(join(__dirname, 'tender-digest.scheduler.ts'), 'utf8'); const source = readFileSync(join(__dirname, 'tender-digest.scheduler.ts'), 'utf8');
expect(source).not.toMatch(/findFirst/); expect(source).not.toMatch(/findFirst/);
}); });
@@ -168,7 +168,7 @@ export class TenderDigestScheduler implements OnModuleInit {
// Kein Konto, oder ein Konto ohne Adresse (WINDOWS #15, kollidierte // Kein Konto, oder ein Konto ohne Adresse (WINDOWS #15, kollidierte
// AD-Adresse) -- die Zugehoerigkeit funktioniert, nur der // AD-Adresse) -- die Zugehoerigkeit funktioniert, nur der
// Mailversand wird uebersprungen (zugesagtes Verhalten). // Mailversand wird uebersprungen (zugesagtes Verhalten).
if (!user || !user.email) continue; if (!user?.email) continue;
const sections = groupMatchesByProfile(matches); const sections = groupMatchesByProfile(matches);
const sent = await this.mail.sendDigest({ email: user.email }, user.tenantId, sections); const sent = await this.mail.sendDigest({ email: user.email }, user.tenantId, sections);
+1 -1
View File
@@ -1,4 +1,4 @@
import { createHash } from 'crypto'; import { createHash } from 'node:crypto';
/** /**
* tenderFingerprint — pure, deterministic cross-source dedup key (SCHEMA-03, * tenderFingerprint — pure, deterministic cross-source dedup key (SCHEMA-03,
@@ -1,5 +1,5 @@
import { readFileSync } from 'fs'; import { readFileSync } from 'node:fs';
import { join } from 'path'; import { join } from 'node:path';
import { describe, expect, it, vi } from 'vitest'; import { describe, expect, it, vi } from 'vitest';
import { TenderIngestionService } from './tender-ingestion.service'; import { TenderIngestionService } from './tender-ingestion.service';
@@ -150,7 +150,7 @@ export class TenderMatchingService {
// Kein Konto, oder ein Konto ohne Adresse (WINDOWS #15, kollidierte // Kein Konto, oder ein Konto ohne Adresse (WINDOWS #15, kollidierte
// AD-Adresse) -- die Zugehoerigkeit funktioniert, nur der // AD-Adresse) -- die Zugehoerigkeit funktioniert, nur der
// Mailversand wird uebersprungen (zugesagtes Verhalten). // Mailversand wird uebersprungen (zugesagtes Verhalten).
if (!user || !user.email) continue; if (!user?.email) continue;
const sent = await this.mail.sendInstant( const sent = await this.mail.sendInstant(
{ email: user.email }, { email: user.email },
@@ -1,7 +1,7 @@
import AdmZip from 'adm-zip'; import AdmZip from 'adm-zip';
import { XMLParser } from 'fast-xml-parser'; import { XMLParser } from 'fast-xml-parser';
import { readFileSync } from 'fs'; import { readFileSync } from 'node:fs';
import { join } from 'path'; import { join } from 'node:path';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import type { RawTenderRecord, SourceType } from './tender.types'; import type { RawTenderRecord, SourceType } from './tender.types';
import { TenderNormalizerService } from './tender-normalizer.service'; import { TenderNormalizerService } from './tender-normalizer.service';
@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { createHash } from 'crypto'; import { createHash } from 'node:crypto';
import { divisionOf } from './cpv/cpv-catalog'; import { divisionOf } from './cpv/cpv-catalog';
import { bundeslandFromRegion } from './geo/nuts-bundesland'; import { bundeslandFromRegion } from './geo/nuts-bundesland';
import type { NormalizedTenderFields, RawTenderRecord } from './tender.types'; import type { NormalizedTenderFields, RawTenderRecord } from './tender.types';
@@ -64,7 +64,7 @@ function makeFakePrisma() {
const tenderRssFeedSource = { const tenderRssFeedSource = {
findMany: async ({ where, orderBy }: any = {}) => { 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') { if (orderBy?.createdAt === 'asc') {
all.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); all.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
} }
@@ -1,5 +1,5 @@
import { readFileSync } from 'fs'; import { readFileSync } from 'node:fs';
import { join } from 'path'; import { join } from 'node:path';
import { describe, expect, it, vi } from 'vitest'; import { describe, expect, it, vi } from 'vitest';
import { ModuleRegistryService } from '../module-registry/module-registry.service'; import { ModuleRegistryService } from '../module-registry/module-registry.service';
import { TenderSchedulerService } from './tender-scheduler.service'; import { TenderSchedulerService } from './tender-scheduler.service';
+3 -3
View File
@@ -16,8 +16,8 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express'; import { FileInterceptor } from '@nestjs/platform-express';
import { Role } from '@prisma/client'; import { Role } from '@prisma/client';
import * as fs from 'fs'; import * as fs from 'node:fs';
import * as path from 'path'; import * as path from 'node:path';
import { Response } from 'express'; import { Response } from 'express';
import { CurrentUser } from '../auth/decorators/current-user.decorator'; import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { Roles } from '../auth/decorators/roles.decorator'; import { Roles } from '../auth/decorators/roles.decorator';
@@ -293,7 +293,7 @@ export class UserController {
@UploadedFile() file: any, @UploadedFile() file: any,
@CurrentUser() currentUser: any, @CurrentUser() currentUser: any,
) { ) {
if (!file || !file.buffer) { if (!file?.buffer) {
throw new BadRequestException('No file provided'); throw new BadRequestException('No file provided');
} }
+3 -3
View File
@@ -312,12 +312,12 @@
return; return;
} }
showInfo('Tessera ' + version + ' gefunden – Verbindung wird hergestellt …'); showInfo(`Tessera ${version} gefunden – Verbindung wird hergestellt …`);
try { try {
await invoke('save_server_url', { url: normalizedUrl }); await invoke('save_server_url', { url: normalizedUrl });
} catch (err) { } catch (err) {
showError('Die Adresse konnte nicht gespeichert werden: ' + String(err)); showError(`Die Adresse konnte nicht gespeichert werden: ${String(err)}`);
connectBtn.disabled = false; connectBtn.disabled = false;
connectBtn.textContent = 'Verbinden'; connectBtn.textContent = 'Verbinden';
} }
@@ -344,7 +344,7 @@
*/ */
function enterChangeMode(current) { function enterChangeMode(current) {
urlInput.value = current; urlInput.value = current;
currentServer.textContent = 'Aktuell verbunden mit: ' + current; currentServer.textContent = `Aktuell verbunden mit: ${current}`;
currentServer.style.display = 'block'; currentServer.style.display = 'block';
subtitle.textContent = 'Server-Adresse ändern'; subtitle.textContent = 'Server-Adresse ändern';
cancelBtn.hidden = false; cancelBtn.hidden = false;
-2
View File
@@ -2,7 +2,6 @@
import { useState, useTransition } from 'react'; import { useState, useTransition } from 'react';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useRouter } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { login } from '@/lib/auth-actions'; import { login } from '@/lib/auth-actions';
import { sanitizeNextPath } from '@/lib/safe-next'; import { sanitizeNextPath } from '@/lib/safe-next';
@@ -18,7 +17,6 @@ import { DesktopDownloadLinks } from '@/components/desktop/desktop-download-link
*/ */
export default function LoginPage() { export default function LoginPage() {
const t = useTranslations('auth'); const t = useTranslations('auth');
const router = useRouter();
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -2,14 +2,10 @@
import { useState, useTransition, useEffect } from 'react'; import { useState, useTransition, useEffect } from 'react';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useRouter } from 'next/navigation';
import { fetchCurrentUser, changePasswordAction } from '@/lib/auth-actions'; import { fetchCurrentUser, changePasswordAction } from '@/lib/auth-actions';
import { useAuthStore } from '@/lib/stores/auth-store';
export default function ChangePasswordPage() { export default function ChangePasswordPage() {
const t = useTranslations('auth'); const t = useTranslations('auth');
const router = useRouter();
const { user, setUser } = useAuthStore();
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [passwordMismatch, setPasswordMismatch] = useState(false); const [passwordMismatch, setPasswordMismatch] = useState(false);
@@ -1,4 +1,4 @@
import { cleanup, render, screen, fireEvent, waitFor, act } from '@testing-library/react'; import { cleanup, render, screen, fireEvent, waitFor } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest'; import { afterEach, describe, expect, it, vi } from 'vitest';
// Mock next-intl — provide certManager namespace keys // Mock next-intl — provide certManager namespace keys
@@ -17,7 +17,7 @@ const ROLE_STYLES: Record<CertRole, string> = {
'end-entity': 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300', 'end-entity': 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300',
}; };
function downloadAllAsZip(certs: SplitResponse['certs'], t: (k: string) => string) { function downloadAllAsZip(certs: SplitResponse['certs']) {
const files: Record<string, Uint8Array> = {}; const files: Record<string, Uint8Array> = {};
for (const cert of certs) { for (const cert of certs) {
const bytes = Uint8Array.from(atob(cert.content), (c) => c.charCodeAt(0)); const bytes = Uint8Array.from(atob(cert.content), (c) => c.charCodeAt(0));
@@ -79,7 +79,7 @@ export function SplitTab({ file, pemText: _pemText, password: _password }: Split
{result && result.certs.length > 1 && ( {result && result.certs.length > 1 && (
<button <button
onClick={() => downloadAllAsZip(result.certs, t)} onClick={() => downloadAllAsZip(result.certs)}
className="border border-border px-4 py-2 rounded text-sm font-medium hover:bg-secondary transition-colors" className="border border-border px-4 py-2 rounded text-sm font-medium hover:bg-secondary transition-colors"
> >
{t('actions.downloadZip')} {t('actions.downloadZip')}
@@ -161,7 +161,7 @@ export function VehicleTable() {
// Delete confirm // Delete confirm
const [deleteTarget, setDeleteTarget] = useState<DkvVehicle | null>(null); const [deleteTarget, setDeleteTarget] = useState<DkvVehicle | null>(null);
const [isDeleting, setIsDeleting] = useState(false); const [, setIsDeleting] = useState(false);
// Server error below table // Server error below table
const [tableError, setTableError] = useState<string | null>(null); const [tableError, setTableError] = useState<string | null>(null);
@@ -260,9 +260,9 @@ export function EmailAlertConfigForm() {
{/* Host / EWS-URL */} {/* Host / EWS-URL */}
<div> <div>
<label htmlFor="email-alert-host" className={labelCls}> <label htmlFor="email-alert-host" className={labelCls}>
{(form.protocol === 'exchange' {`${form.protocol === 'exchange'
? t('emailAlerts.hostLabelExchange') ? t('emailAlerts.hostLabelExchange')
: t('emailAlerts.hostLabelImap')) + ' *'} : t('emailAlerts.hostLabelImap')} *`}
</label> </label>
<input <input
id="email-alert-host" id="email-alert-host"
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { FormEvent, useEffect, useMemo, useRef, useState } from 'react'; import { type FormEvent, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { updateWidgetConfig } from '@/lib/dashboard-api'; import { updateWidgetConfig } from '@/lib/dashboard-api';
import { import {
+1 -1
View File
@@ -139,7 +139,7 @@ export async function changePasswordAction(
const maxAgeMatch = setCookieHeader.match(/Max-Age=(\d+)/i); const maxAgeMatch = setCookieHeader.match(/Max-Age=(\d+)/i);
if (sessionMatch) { if (sessionMatch) {
newSessionToken = sessionMatch[1]; newSessionToken = sessionMatch[1];
newSessionMaxAge = maxAgeMatch ? parseInt(maxAgeMatch[1]) : undefined; newSessionMaxAge = maxAgeMatch ? parseInt(maxAgeMatch[1], 10) : undefined;
} }
} }
} catch (err) { } catch (err) {
+1 -1
View File
@@ -1,4 +1,4 @@
import { NextRequest, NextResponse } from 'next/server'; import { type NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose'; import { jwtVerify } from 'jose';
import { buildNextParam } from '@/lib/safe-next'; import { buildNextParam } from '@/lib/safe-next';