- ApiKeysController verlangt MANAGE_SETTINGS. Bisher konnte jeder
OIDC-Benutzer einen API-Key erzeugen, der im PermissionsGuard jede
Rechteprüfung umgeht (Rechteausweitung zum Admin).
- MailService setzt disableFileAccess/disableUrlAccess für jede Mail.
- send-email im Posteingang prüft den Body streng auf Strings/Zahlen.
Bisher liest nodemailer bei html: { path: '/proc/self/environ' }
beliebige Serverdateien ein und verschickt sie.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
304 lines
8.8 KiB
TypeScript
304 lines
8.8 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
Body,
|
|
Controller,
|
|
Delete,
|
|
Get,
|
|
HttpCode,
|
|
Param,
|
|
ParseIntPipe,
|
|
Post,
|
|
Put,
|
|
Request,
|
|
Res,
|
|
StreamableFile,
|
|
} from '@nestjs/common';
|
|
import type { Response } from 'express';
|
|
import { createReadStream } from 'fs';
|
|
import { InboxService } from './inbox.service';
|
|
import { InboxPostprocessorService } from '../inbox-postprocessor/inbox-postprocessor.service';
|
|
import type { PaperlessEntscheidung } from '../inbox-postprocessor/paperless-entscheidung.util';
|
|
import { BarcodeScannerService } from '../barcode/barcode-scanner.service';
|
|
import { UserSettingsService } from '../user-settings/user-settings.service';
|
|
import { RequirePermissions } from '../auth/permissions.decorator';
|
|
import { Permission } from '../auth/permissions.enum';
|
|
import { PaperlessService } from '../paperless/paperless.service';
|
|
import {
|
|
kennzahlenAusPdf,
|
|
type VergleichKennzahlen,
|
|
} from '../paperless/vergleich-kennzahlen.util';
|
|
import { parseSendEmailBody } from './send-email-body.util';
|
|
|
|
@Controller('api/inbox')
|
|
@RequirePermissions(Permission.VIEW_SCANNER)
|
|
export class InboxController {
|
|
constructor(
|
|
private readonly inboxService: InboxService,
|
|
private readonly postprocessor: InboxPostprocessorService,
|
|
private readonly barcodeScanner: BarcodeScannerService,
|
|
private readonly userSettingsService: UserSettingsService,
|
|
private readonly paperlessService: PaperlessService,
|
|
) {}
|
|
|
|
@Get()
|
|
async list(@Request() req: any) {
|
|
const preferredUsername: string | null =
|
|
req.user?.preferredUsername ?? null;
|
|
return this.inboxService.listFiles(preferredUsername);
|
|
}
|
|
|
|
@Post('rescan')
|
|
async rescan() {
|
|
return this.barcodeScanner.rescanAll();
|
|
}
|
|
|
|
@Get(':id/preview')
|
|
async preview(
|
|
@Param('id') id: string,
|
|
@Request() req: any,
|
|
@Res({ passthrough: true }) res: Response,
|
|
): Promise<StreamableFile> {
|
|
const preferredUsername: string | null =
|
|
req.user?.preferredUsername ?? null;
|
|
const { doc, pdfPath } = await this.inboxService.resolveDocument(
|
|
id,
|
|
preferredUsername,
|
|
);
|
|
|
|
res.setHeader('Content-Type', 'application/pdf');
|
|
res.setHeader(
|
|
'Content-Disposition',
|
|
`inline; filename="${doc.OriginalName}"`,
|
|
);
|
|
return new StreamableFile(createReadStream(pdfPath));
|
|
}
|
|
|
|
@Get(':id/pages/:page/thumbnail')
|
|
async pageThumbnail(
|
|
@Param('id') id: string,
|
|
@Param('page', ParseIntPipe) page: number,
|
|
@Request() req: any,
|
|
@Res({ passthrough: true }) res: Response,
|
|
): Promise<StreamableFile> {
|
|
const preferredUsername: string | null =
|
|
req.user?.preferredUsername ?? null;
|
|
const filePath = await this.inboxService.resolvePageImage(
|
|
id,
|
|
page,
|
|
'thumbnail',
|
|
preferredUsername,
|
|
);
|
|
|
|
res.setHeader('Content-Type', 'image/png');
|
|
res.setHeader('Cache-Control', 'private, max-age=3600');
|
|
return new StreamableFile(createReadStream(filePath));
|
|
}
|
|
|
|
@Delete(':id')
|
|
@HttpCode(204)
|
|
async remove(@Param('id') id: string, @Request() req: any): Promise<void> {
|
|
const preferredUsername: string | null =
|
|
req.user?.preferredUsername ?? null;
|
|
await this.inboxService.deleteDocument(id, preferredUsername);
|
|
}
|
|
|
|
@Delete(':id/pages/:page')
|
|
@HttpCode(204)
|
|
async removePage(
|
|
@Param('id') id: string,
|
|
@Param('page', ParseIntPipe) page: number,
|
|
@Request() req: any,
|
|
): Promise<void> {
|
|
const preferredUsername: string | null =
|
|
req.user?.preferredUsername ?? null;
|
|
await this.inboxService.deletePage(id, page, preferredUsername);
|
|
}
|
|
|
|
@Post(':id/pages/:page/split')
|
|
@HttpCode(204)
|
|
async toggleManualSplit(
|
|
@Param('id') id: string,
|
|
@Param('page', ParseIntPipe) page: number,
|
|
@Request() req: any,
|
|
): Promise<void> {
|
|
const preferredUsername: string | null =
|
|
req.user?.preferredUsername ?? null;
|
|
await this.inboxService.toggleManualSplit(id, page, preferredUsername);
|
|
}
|
|
|
|
@Post(':id/reset-edits')
|
|
@HttpCode(204)
|
|
async resetEdits(
|
|
@Param('id') id: string,
|
|
@Request() req: any,
|
|
): Promise<void> {
|
|
const preferredUsername: string | null =
|
|
req.user?.preferredUsername ?? null;
|
|
await this.inboxService.resetEdits(id, preferredUsername);
|
|
}
|
|
|
|
@Post(':id/postprocess')
|
|
async postprocess(
|
|
@Param('id') id: string,
|
|
@Request() req: any,
|
|
@Body()
|
|
body: {
|
|
sectionOffset?: number;
|
|
processOnlyOne?: boolean;
|
|
entscheidung?: PaperlessEntscheidung;
|
|
},
|
|
) {
|
|
const preferredUsername: string | null =
|
|
req.user?.preferredUsername ?? null;
|
|
const { results, totalSections } = await this.postprocessor.runForDocument(
|
|
id,
|
|
preferredUsername,
|
|
body?.sectionOffset ?? 0,
|
|
body?.processOnlyOne ?? false,
|
|
body?.entscheidung,
|
|
);
|
|
return { results, totalSections };
|
|
}
|
|
|
|
@Put(':id/pages/:page/rotation')
|
|
@HttpCode(204)
|
|
async setPageRotation(
|
|
@Param('id') id: string,
|
|
@Param('page', ParseIntPipe) page: number,
|
|
@Body() body: { rotation?: number },
|
|
@Request() req: any,
|
|
): Promise<void> {
|
|
const rotation = Number(body?.rotation);
|
|
if (!Number.isFinite(rotation)) {
|
|
throw new BadRequestException('rotation muss eine Zahl sein');
|
|
}
|
|
const preferredUsername: string | null =
|
|
req.user?.preferredUsername ?? null;
|
|
await this.inboxService.setPageRotation(
|
|
id,
|
|
page,
|
|
rotation,
|
|
preferredUsername,
|
|
);
|
|
}
|
|
|
|
@Get(':id/pages/:page/preview')
|
|
async pagePreview(
|
|
@Param('id') id: string,
|
|
@Param('page', ParseIntPipe) page: number,
|
|
@Request() req: any,
|
|
@Res({ passthrough: true }) res: Response,
|
|
): Promise<StreamableFile> {
|
|
const preferredUsername: string | null =
|
|
req.user?.preferredUsername ?? null;
|
|
const filePath = await this.inboxService.resolvePageImage(
|
|
id,
|
|
page,
|
|
'preview',
|
|
preferredUsername,
|
|
);
|
|
|
|
res.setHeader('Content-Type', 'image/png');
|
|
res.setHeader('Cache-Control', 'private, max-age=3600');
|
|
return new StreamableFile(createReadStream(filePath));
|
|
}
|
|
|
|
@Post(':id/pages/:page/scan-region')
|
|
async scanRegion(
|
|
@Param('id') id: string,
|
|
@Param('page', ParseIntPipe) page: number,
|
|
@Body() body: { x: number; y: number; w: number; h: number },
|
|
@Request() req: any,
|
|
): Promise<{ found: string[] }> {
|
|
const preferredUsername: string | null =
|
|
req.user?.preferredUsername ?? null;
|
|
return this.inboxService.scanRegion(
|
|
id,
|
|
page,
|
|
body.x,
|
|
body.y,
|
|
body.w,
|
|
body.h,
|
|
preferredUsername,
|
|
);
|
|
}
|
|
|
|
@Post(':id/source')
|
|
@HttpCode(204)
|
|
async updateSource(
|
|
@Param('id') id: string,
|
|
@Body() body: { source: any },
|
|
@Request() req: any,
|
|
): Promise<void> {
|
|
const preferredUsername: string | null =
|
|
req.user?.preferredUsername ?? null;
|
|
await this.inboxService.updateSource(id, body.source, preferredUsername);
|
|
}
|
|
|
|
@Post(':id/download-segment')
|
|
async downloadSegment(
|
|
@Param('id') id: string,
|
|
@Body() body: { pages: number[] },
|
|
@Request() req: any,
|
|
@Res({ passthrough: true }) res: Response,
|
|
): Promise<StreamableFile> {
|
|
const preferredUsername: string | null =
|
|
req.user?.preferredUsername ?? null;
|
|
const { buffer, filename } = await this.inboxService.getSegmentPdfBuffer(
|
|
id,
|
|
preferredUsername,
|
|
body.pages ?? [],
|
|
);
|
|
const { Readable } = await import('stream');
|
|
res.setHeader('Content-Type', 'application/pdf');
|
|
res.setHeader(
|
|
'Content-Disposition',
|
|
`attachment; filename="${encodeURIComponent(filename)}"`,
|
|
);
|
|
return new StreamableFile(Readable.from(buffer));
|
|
}
|
|
|
|
/** Seiten-/Wortzahl des Abschnitts und des vorhandenen Paperless-Dokuments. */
|
|
@Post(':id/vergleich')
|
|
async vergleich(
|
|
@Param('id') id: string,
|
|
@Body() body: { paperlessDocumentId: number; pages: number[] },
|
|
@Request() req: any,
|
|
): Promise<VergleichKennzahlen> {
|
|
const preferredUsername: string | null =
|
|
req.user?.preferredUsername ?? null;
|
|
const { buffer } = await this.inboxService.getSegmentPdfBuffer(
|
|
id,
|
|
preferredUsername,
|
|
body.pages ?? [],
|
|
);
|
|
const [vorhanden, neu] = await Promise.all([
|
|
this.paperlessService.getDocumentKennzahlen(
|
|
Number(body.paperlessDocumentId),
|
|
),
|
|
kennzahlenAusPdf(buffer),
|
|
]);
|
|
return { vorhanden, neu };
|
|
}
|
|
|
|
@Post(':id/send-email')
|
|
@HttpCode(204)
|
|
async sendEmail(
|
|
@Param('id') id: string,
|
|
@Body() rawBody: unknown,
|
|
@Request() req: any,
|
|
): Promise<void> {
|
|
const body = parseSendEmailBody(rawBody);
|
|
const preferredUsername: string | null =
|
|
req.user?.preferredUsername ?? null;
|
|
const smtpOverride =
|
|
body.sender === 'user'
|
|
? await this.userSettingsService.getSmtpConfig(req.user.userId)
|
|
: null;
|
|
await this.inboxService.sendAsEmail(id, preferredUsername, {
|
|
...body,
|
|
smtpOverride: smtpOverride ?? undefined,
|
|
});
|
|
}
|
|
}
|