636fe0df8f
- 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
243 lines
9.0 KiB
TypeScript
243 lines
9.0 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
Body,
|
|
Controller,
|
|
Delete,
|
|
Get,
|
|
NotFoundException,
|
|
Param,
|
|
Post,
|
|
Put,
|
|
Query,
|
|
Req,
|
|
Res,
|
|
UploadedFile,
|
|
UseInterceptors,
|
|
} from '@nestjs/common';
|
|
import { FileInterceptor } from '@nestjs/platform-express';
|
|
import { Role } from '@prisma/client';
|
|
import { Roles } from '../auth/decorators/roles.decorator';
|
|
import { DkvSchedulerService } from './dkv-scheduler.service';
|
|
import { DkvService } from './dkv.service';
|
|
import { DkvConfigDto } from './dto/dkv-config.dto';
|
|
import { DkvHistoryQueryDto } from './dto/dkv-history.dto';
|
|
import { CreateVehicleDto, UpdateVehicleDto } from './dto/dkv-vehicle.dto';
|
|
|
|
/**
|
|
* DkvController — all /dkv/* routes, ADMIN-only (V4).
|
|
*
|
|
* Every handler carries @Roles(Role.ADMIN, Role.SUPER_ADMIN).
|
|
* Global JwtAuthGuard enforces JWT authentication; RolesGuard enforces the
|
|
* @Roles decorator. No route is publicly accessible.
|
|
*
|
|
* Tenant extraction: `req.tenantId` set by TenantGuard (runs after auth guards).
|
|
* All operations are scoped to the authenticated tenant's data.
|
|
*
|
|
* Routes:
|
|
* GET /dkv/config — get module config (no encrypted creds)
|
|
* PUT /dkv/config — save module config; updates scheduler
|
|
* POST /dkv/check-now — manual inbox poll trigger
|
|
* POST /dkv/test-connection — test inbox connection with form values
|
|
* GET /dkv/history — paginated processing history
|
|
* GET /dkv/exports/:filename — download a saved xlsx file
|
|
* GET /dkv/vehicles — list vehicle master records
|
|
* POST /dkv/vehicles — create a vehicle master record
|
|
* PUT /dkv/vehicles/:id — update a vehicle master record
|
|
* DELETE /dkv/vehicles/:id — delete a vehicle master record
|
|
* POST /dkv/vehicles/import — bulk-import from CSV upload
|
|
*/
|
|
@Controller('dkv')
|
|
export class DkvController {
|
|
constructor(
|
|
private readonly dkvService: DkvService,
|
|
private readonly dkvScheduler: DkvSchedulerService,
|
|
) {}
|
|
|
|
// ─── Config ────────────────────────────────────────────────────────────────
|
|
|
|
/** GET /dkv/config — returns module config with username + hasPassword. 404 when not yet configured. */
|
|
@Get('config')
|
|
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
|
async getConfig(@Req() req: any) {
|
|
const tenantId = this._requireTenant(req);
|
|
const config = await this.dkvService.getConfigForApi(tenantId);
|
|
if (!config) {
|
|
throw new NotFoundException('DKV module not yet configured');
|
|
}
|
|
return config;
|
|
}
|
|
|
|
/**
|
|
* PUT /dkv/config — upsert module config.
|
|
*
|
|
* After saving, re-applies the cron job if isActive is true,
|
|
* or stops the cron job if isActive is false.
|
|
*/
|
|
@Put('config')
|
|
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
|
async saveConfig(@Req() req: any, @Body() dto: DkvConfigDto) {
|
|
const tenantId = this._requireTenant(req);
|
|
const result = await this.dkvService.saveConfig(tenantId, dto);
|
|
|
|
// Update scheduler to reflect the new interval / active state
|
|
if (dto.isActive && dto.pollIntervalMin) {
|
|
this.dkvScheduler.setInterval(dto.pollIntervalMin, tenantId);
|
|
} else if (dto.isActive === false) {
|
|
this.dkvScheduler.stopJob(tenantId);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// ─── Manual trigger + connection test ──────────────────────────────────────
|
|
|
|
/** POST /dkv/check-now — immediately run the inbox processing pipeline. */
|
|
@Post('check-now')
|
|
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
|
async checkNow(@Req() req: any) {
|
|
const tenantId = this._requireTenant(req);
|
|
return this.dkvService.checkNow(tenantId);
|
|
}
|
|
|
|
/**
|
|
* POST /dkv/test-connection — test inbox connection with current form values.
|
|
* Used by the InboxConfigForm "Verbindung testen" button before saving.
|
|
*/
|
|
@Post('test-connection')
|
|
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
|
async testConnection(@Req() req: any, @Body() dto: DkvConfigDto) {
|
|
const tenantId = this._requireTenant(req);
|
|
return this.dkvService.testConnection(tenantId, dto);
|
|
}
|
|
|
|
// ─── History ───────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* GET /dkv/history?page=1&limit=20 — paginated processing history.
|
|
* T-07-06: pagination parameters validated by DkvHistoryQueryDto.
|
|
*/
|
|
@Get('history')
|
|
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
|
async getHistory(@Req() req: any, @Query() query: DkvHistoryQueryDto) {
|
|
const tenantId = this._requireTenant(req);
|
|
const page = query.page ?? 1;
|
|
const limit = query.limit ?? 20;
|
|
return this.dkvService.getHistory(tenantId, page, limit);
|
|
}
|
|
|
|
// ─── Export file download ──────────────────────────────────────────────────
|
|
|
|
/**
|
|
* GET /dkv/exports/:filename — stream a DKV xlsx export file as an attachment.
|
|
*
|
|
* T-07-09: DkvService.getExportFile() validates the filename against the
|
|
* `DKV_*.xlsx` whitelist pattern before reading from user-files/. Any filename
|
|
* containing path separators or non-whitelisted characters is rejected.
|
|
*/
|
|
@Get('exports/:filename')
|
|
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
|
async downloadExport(
|
|
@Req() req: any,
|
|
@Param('filename') filename: string,
|
|
@Res() res: any,
|
|
) {
|
|
const tenantId = this._requireTenant(req);
|
|
|
|
try {
|
|
const buffer = await this.dkvService.getExportFile(tenantId, filename);
|
|
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
|
res.setHeader(
|
|
'Content-Type',
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
);
|
|
res.send(buffer);
|
|
} catch (error) {
|
|
if (error instanceof NotFoundException || error instanceof BadRequestException) {
|
|
throw error;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// ─── Vehicle Master CRUD ───────────────────────────────────────────────────
|
|
|
|
/** GET /dkv/vehicles — list all vehicle master records for this tenant. */
|
|
@Get('vehicles')
|
|
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
|
async listVehicles(@Req() req: any) {
|
|
const tenantId = this._requireTenant(req);
|
|
return this.dkvService.listVehicles(tenantId);
|
|
}
|
|
|
|
/** POST /dkv/vehicles — create a new vehicle master record. */
|
|
@Post('vehicles')
|
|
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
|
async createVehicle(@Req() req: any, @Body() dto: CreateVehicleDto) {
|
|
const tenantId = this._requireTenant(req);
|
|
return this.dkvService.createVehicle(tenantId, dto);
|
|
}
|
|
|
|
/** PUT /dkv/vehicles/:id — update an existing vehicle master record. */
|
|
@Put('vehicles/:id')
|
|
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
|
async updateVehicle(
|
|
@Req() req: any,
|
|
@Param('id') id: string,
|
|
@Body() dto: UpdateVehicleDto,
|
|
) {
|
|
const tenantId = this._requireTenant(req);
|
|
return this.dkvService.updateVehicle(tenantId, id, dto);
|
|
}
|
|
|
|
/** DELETE /dkv/vehicles/:id — delete a vehicle master record. */
|
|
@Delete('vehicles/:id')
|
|
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
|
async deleteVehicle(@Req() req: any, @Param('id') id: string) {
|
|
const tenantId = this._requireTenant(req);
|
|
return this.dkvService.deleteVehicle(tenantId, id);
|
|
}
|
|
|
|
/**
|
|
* POST /dkv/vehicles/import — bulk import from a CSV file upload.
|
|
*
|
|
* Accepts a multipart form with:
|
|
* - `file`: the CSV file (field name must be "file")
|
|
* - `mode`: 'merge' (default) or 'replace'
|
|
*
|
|
* FileInterceptor buffers the upload in memory (no disk write).
|
|
* The controller reads `file.buffer.toString('utf-8')` and passes to DkvService.
|
|
*/
|
|
@Post('vehicles/import')
|
|
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
|
@UseInterceptors(FileInterceptor('file', {
|
|
limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB — generous for any realistic vehicle list (WR-05)
|
|
}))
|
|
async importVehicles(
|
|
@Req() req: any,
|
|
@UploadedFile() file: any,
|
|
@Body('mode') mode: string,
|
|
) {
|
|
const tenantId = this._requireTenant(req);
|
|
|
|
if (!file?.buffer) {
|
|
throw new BadRequestException('No CSV file uploaded (field name must be "file")');
|
|
}
|
|
|
|
const csvText = (file.buffer as Buffer).toString('utf-8');
|
|
const importMode = mode === 'replace' ? 'replace' : 'merge';
|
|
|
|
return this.dkvService.importVehiclesCsv(tenantId, csvText, importMode);
|
|
}
|
|
|
|
// ─── Private helpers ───────────────────────────────────────────────────────
|
|
|
|
/** Extract and validate tenantId from request; throw BadRequestException when absent. */
|
|
private _requireTenant(req: any): string {
|
|
const tenantId = req.tenantId as string | undefined;
|
|
if (!tenantId) {
|
|
throw new BadRequestException('No tenant context');
|
|
}
|
|
return tenantId;
|
|
}
|
|
}
|