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;
}
/**
* 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
* 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
// unten).
const a2Row = rowsForA.find((r) => r.userId === 'user-a2');
const a2CredentialsVisible = Boolean(a2Row && a2Row.encryptedPassword);
const a2CredentialsVisible = Boolean(a2Row?.encryptedPassword);
report(
results,
'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 { 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',
+2 -2
View File
@@ -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;
}
+2 -2
View File
@@ -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 -1
View File
@@ -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';
+5 -5
View File
@@ -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 {
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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';
+2 -2
View File
@@ -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) {
+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
// 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) {
+1 -1
View File
@@ -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 -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 { DkvService } from './dkv.service';
+7 -7
View File
@@ -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';
/**
+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 () => {
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 });
+1 -1
View File
@@ -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;
}
+1 -1
View File
@@ -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();
+2 -2
View File
@@ -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({
+25 -25
View File
@@ -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 -1
View File
@@ -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 -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,
@@ -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';
+3 -3
View File
@@ -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');
}
+3 -3
View File
@@ -312,12 +312,12 @@
return;
}
showInfo('Tessera ' + version + ' gefunden – Verbindung wird hergestellt …');
showInfo(`Tessera ${version} gefunden – Verbindung wird hergestellt …`);
try {
await invoke('save_server_url', { url: normalizedUrl });
} catch (err) {
showError('Die Adresse konnte nicht gespeichert werden: ' + String(err));
showError(`Die Adresse konnte nicht gespeichert werden: ${String(err)}`);
connectBtn.disabled = false;
connectBtn.textContent = 'Verbinden';
}
@@ -344,7 +344,7 @@
*/
function enterChangeMode(current) {
urlInput.value = current;
currentServer.textContent = 'Aktuell verbunden mit: ' + current;
currentServer.textContent = `Aktuell verbunden mit: ${current}`;
currentServer.style.display = 'block';
subtitle.textContent = 'Server-Adresse ändern';
cancelBtn.hidden = false;
-2
View File
@@ -2,7 +2,6 @@
import { useState, useTransition } from 'react';
import { useTranslations } from 'next-intl';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { login } from '@/lib/auth-actions';
import { sanitizeNextPath } from '@/lib/safe-next';
@@ -18,7 +17,6 @@ import { DesktopDownloadLinks } from '@/components/desktop/desktop-download-link
*/
export default function LoginPage() {
const t = useTranslations('auth');
const router = useRouter();
const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
@@ -2,14 +2,10 @@
import { useState, useTransition, useEffect } from 'react';
import { useTranslations } from 'next-intl';
import { useRouter } from 'next/navigation';
import { fetchCurrentUser, changePasswordAction } from '@/lib/auth-actions';
import { useAuthStore } from '@/lib/stores/auth-store';
export default function ChangePasswordPage() {
const t = useTranslations('auth');
const router = useRouter();
const { user, setUser } = useAuthStore();
const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
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';
// 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',
};
function downloadAllAsZip(certs: SplitResponse['certs'], t: (k: string) => string) {
function downloadAllAsZip(certs: SplitResponse['certs']) {
const files: Record<string, Uint8Array> = {};
for (const cert of certs) {
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 && (
<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"
>
{t('actions.downloadZip')}
@@ -161,7 +161,7 @@ export function VehicleTable() {
// Delete confirm
const [deleteTarget, setDeleteTarget] = useState<DkvVehicle | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const [, setIsDeleting] = useState(false);
// Server error below table
const [tableError, setTableError] = useState<string | null>(null);
@@ -260,9 +260,9 @@ export function EmailAlertConfigForm() {
{/* Host / EWS-URL */}
<div>
<label htmlFor="email-alert-host" className={labelCls}>
{(form.protocol === 'exchange'
{`${form.protocol === 'exchange'
? t('emailAlerts.hostLabelExchange')
: t('emailAlerts.hostLabelImap')) + ' *'}
: t('emailAlerts.hostLabelImap')} *`}
</label>
<input
id="email-alert-host"
@@ -1,6 +1,6 @@
'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 { updateWidgetConfig } from '@/lib/dashboard-api';
import {
+1 -1
View File
@@ -139,7 +139,7 @@ export async function changePasswordAction(
const maxAgeMatch = setCookieHeader.match(/Max-Age=(\d+)/i);
if (sessionMatch) {
newSessionToken = sessionMatch[1];
newSessionMaxAge = maxAgeMatch ? parseInt(maxAgeMatch[1]) : undefined;
newSessionMaxAge = maxAgeMatch ? parseInt(maxAgeMatch[1], 10) : undefined;
}
}
} 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 { buildNextParam } from '@/lib/safe-next';