c2ebc8daa0
- Proxmox-Schreibwege und Handelsware-Einstellungen auf @ModuleManage umgestellt - DKV-Fleet: ganze Klasse Verwalten-Stufe, Benutzen allein bleibt ohne Zugriff - Metadaten-Test belegt umgestellte und bewusst Administratoren vorbehaltene Handler - Webseiten (Proxmox, Handelsware, Widget) folgen canManage, DKV-Zugriffsseite erklärt die Stufe Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
173 lines
5.7 KiB
TypeScript
173 lines
5.7 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
Body,
|
|
Controller,
|
|
Delete,
|
|
ForbiddenException,
|
|
Get,
|
|
Param,
|
|
Post,
|
|
Put,
|
|
Req,
|
|
UploadedFile,
|
|
UseInterceptors,
|
|
} from '@nestjs/common';
|
|
import { FileInterceptor } from '@nestjs/platform-express';
|
|
import { decodeUploadFilename } from '../accounting/decode-upload-filename';
|
|
import type { AuthenticatedRequest, UploadedFileLike } from '../auth/types/auth-user';
|
|
import { ModuleManage, UseModule } from '../module-registry/module.guard';
|
|
import { HandelswareAccountDto } from './dto/handelsware-account.dto';
|
|
import { HandelswareSettingsDto } from './dto/handelsware-settings.dto';
|
|
import { HandelswareDatevService } from './handelsware-datev.service';
|
|
|
|
const MAX_NEW_ACCOUNTS = 10_000;
|
|
|
|
/**
|
|
* Das Formularfeld `newAccounts` ist ein JSON-Text (Liste der von der Vorschau
|
|
* gemeldeten neuen Konten). Defensiv gelesen: gueltiges JSON, ein Feld, hoechstens
|
|
* 10 000 Eintraege, jeder mit Text-`name` und ganzzahligem `gegenkonto`.
|
|
*/
|
|
export function parseNewAccountsField(raw: unknown): { name: string; gegenkonto: number }[] {
|
|
const bad = () =>
|
|
new BadRequestException({
|
|
code: 'newAccountsInvalid',
|
|
message: 'Die Angaben zu den neuen Konten sind ungültig.',
|
|
});
|
|
if (raw === undefined || raw === null || raw === '') return [];
|
|
if (typeof raw !== 'string') throw bad();
|
|
let parsed: unknown;
|
|
try {
|
|
parsed = JSON.parse(raw);
|
|
} catch {
|
|
throw bad();
|
|
}
|
|
if (!Array.isArray(parsed) || parsed.length > MAX_NEW_ACCOUNTS) throw bad();
|
|
return parsed.map((entry) => {
|
|
if (
|
|
typeof entry !== 'object' ||
|
|
entry === null ||
|
|
typeof (entry as { name?: unknown }).name !== 'string' ||
|
|
!Number.isInteger((entry as { gegenkonto?: unknown }).gegenkonto)
|
|
) {
|
|
throw bad();
|
|
}
|
|
const { name, gegenkonto } = entry as { name: string; gegenkonto: number };
|
|
return { name, gegenkonto };
|
|
});
|
|
}
|
|
|
|
/**
|
|
* `@UseModule('handelsware-datev')` auf Klassenebene — Aktivierung UND Freigabe.
|
|
* `tenantId` kommt ausschliesslich aus `req.tenantId`. Die Einstellungen aendern
|
|
* Administratoren und Benutzer mit der Freigabestufe Verwalten
|
|
* (`@ModuleManage`, 261002-icv; T-FM5-02); die Kontenliste pflegen alle Benutzer mit
|
|
* Modulzugriff.
|
|
*
|
|
* REIHENFOLGE: alle statischen Routen (`accounts`, `accounts/export-csv`,
|
|
* `accounts/import-csv`) stehen VOR `accounts/:id` — sonst faengt `:id` sie ab
|
|
* (Unit-Tests sehen das nicht, `handelsware-datev.controller.spec.ts` prueft die
|
|
* Deklarationsreihenfolge).
|
|
*/
|
|
@Controller('modules/handelsware-datev')
|
|
@UseModule('handelsware-datev')
|
|
export class HandelswareDatevController {
|
|
constructor(private readonly service: HandelswareDatevService) {}
|
|
|
|
private requireTenantId(req: AuthenticatedRequest): string {
|
|
const tenantId = req.tenantId;
|
|
if (!tenantId) {
|
|
throw new ForbiddenException('Kein Mandantenkontext');
|
|
}
|
|
return tenantId;
|
|
}
|
|
|
|
@Get('settings')
|
|
async getSettings(@Req() req: AuthenticatedRequest) {
|
|
return this.service.getSettings(this.requireTenantId(req));
|
|
}
|
|
|
|
@Put('settings')
|
|
@ModuleManage('handelsware-datev')
|
|
async saveSettings(@Req() req: AuthenticatedRequest, @Body() dto: HandelswareSettingsDto) {
|
|
return this.service.saveSettings(this.requireTenantId(req), dto);
|
|
}
|
|
|
|
@Post('preview')
|
|
@UseInterceptors(FileInterceptor('file', { limits: { fileSize: 5 * 1024 * 1024 } }))
|
|
async preview(
|
|
@Req() req: AuthenticatedRequest,
|
|
@UploadedFile() file: UploadedFileLike | undefined,
|
|
) {
|
|
const tenantId = this.requireTenantId(req);
|
|
if (!file) {
|
|
throw new BadRequestException('Keine Datei hochgeladen');
|
|
}
|
|
return this.service.preview(tenantId, {
|
|
buffer: file.buffer,
|
|
originalname: decodeUploadFilename(file.originalname),
|
|
});
|
|
}
|
|
|
|
@Post('export')
|
|
@UseInterceptors(FileInterceptor('file', { limits: { fileSize: 5 * 1024 * 1024 } }))
|
|
async export(
|
|
@Req() req: AuthenticatedRequest,
|
|
@UploadedFile() file: UploadedFileLike | undefined,
|
|
@Body('buchungsdatum') buchungsdatum?: string,
|
|
@Body('newAccounts') newAccounts?: string,
|
|
) {
|
|
const tenantId = this.requireTenantId(req);
|
|
if (!file) {
|
|
throw new BadRequestException('Keine Datei hochgeladen');
|
|
}
|
|
return this.service.export(
|
|
tenantId,
|
|
{ buffer: file.buffer, originalname: decodeUploadFilename(file.originalname) },
|
|
typeof buchungsdatum === 'string' ? buchungsdatum.trim() : '',
|
|
parseNewAccountsField(newAccounts),
|
|
);
|
|
}
|
|
|
|
@Get('accounts')
|
|
async listAccounts(@Req() req: AuthenticatedRequest) {
|
|
return this.service.listAccounts(this.requireTenantId(req));
|
|
}
|
|
|
|
@Post('accounts')
|
|
async createAccount(@Req() req: AuthenticatedRequest, @Body() dto: HandelswareAccountDto) {
|
|
return this.service.createAccount(this.requireTenantId(req), dto);
|
|
}
|
|
|
|
@Get('accounts/export-csv')
|
|
async exportAccountsCsv(@Req() req: AuthenticatedRequest) {
|
|
return this.service.exportAccountsCsv(this.requireTenantId(req));
|
|
}
|
|
|
|
@Post('accounts/import-csv')
|
|
@UseInterceptors(FileInterceptor('file', { limits: { fileSize: 1024 * 1024 } }))
|
|
async importAccountsCsv(
|
|
@Req() req: AuthenticatedRequest,
|
|
@UploadedFile() file: UploadedFileLike | undefined,
|
|
) {
|
|
const tenantId = this.requireTenantId(req);
|
|
if (!file) {
|
|
throw new BadRequestException('Keine Datei hochgeladen');
|
|
}
|
|
return this.service.importAccountsCsv(tenantId, file.buffer);
|
|
}
|
|
|
|
@Put('accounts/:id')
|
|
async updateAccount(
|
|
@Req() req: AuthenticatedRequest,
|
|
@Param('id') id: string,
|
|
@Body() dto: HandelswareAccountDto,
|
|
) {
|
|
return this.service.updateAccount(this.requireTenantId(req), id, dto);
|
|
}
|
|
|
|
@Delete('accounts/:id')
|
|
async deleteAccount(@Req() req: AuthenticatedRequest, @Param('id') id: string) {
|
|
return this.service.deleteAccount(this.requireTenantId(req), id);
|
|
}
|
|
}
|