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 { ModuleManage } from '../module-registry/module.guard'; import type { AuthenticatedRequest, UploadedFileLike, } from '../auth/types/auth-user'; import type { Response } from 'express'; 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, manager level (V4, 261002-icv). * * The whole module is Verwalten-level: `@ModuleManage('dkv-fleet')` on the * class replaces the former per-handler @Roles(ADMIN, SUPER_ADMIN). Access is * therefore limited to administrators and to users with the grant level * "Verwalten" (MANAGE) on the dkv-fleet module. Users with only "Benutzen" * (USE) keep getting 403 exactly as before — nothing was widened. The class * guard additionally requires the dkv-fleet module to be active for the * tenant (the web page already required that). * Global JwtAuthGuard enforces JWT authentication; ModuleGuard enforces the * grant level. 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') @ModuleManage('dkv-fleet') 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') async getConfig(@Req() req: AuthenticatedRequest) { 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') async saveConfig(@Req() req: AuthenticatedRequest, @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') async checkNow(@Req() req: AuthenticatedRequest) { 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') async testConnection(@Req() req: AuthenticatedRequest, @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') async getHistory(@Req() req: AuthenticatedRequest, @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') async downloadExport( @Req() req: AuthenticatedRequest, @Param('filename') filename: string, @Res() res: Response, ) { 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') async listVehicles(@Req() req: AuthenticatedRequest) { const tenantId = this._requireTenant(req); return this.dkvService.listVehicles(tenantId); } /** POST /dkv/vehicles — create a new vehicle master record. */ @Post('vehicles') async createVehicle(@Req() req: AuthenticatedRequest, @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') async updateVehicle( @Req() req: AuthenticatedRequest, @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') async deleteVehicle(@Req() req: AuthenticatedRequest, @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') @UseInterceptors(FileInterceptor('file', { limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB — generous for any realistic vehicle list (WR-05) })) async importVehicles( @Req() req: AuthenticatedRequest, @UploadedFile() file: UploadedFileLike | undefined, @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.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: AuthenticatedRequest): string { const tenantId = req.tenantId; if (!tenantId) { throw new BadRequestException('No tenant context'); } return tenantId; } }