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:
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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';
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,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';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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,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,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';
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,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);
|
||||||
|
|||||||
+2
-2
@@ -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 {
|
||||||
|
|||||||
@@ -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,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';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user