Files
paperlessmanager/paperless-backend/src/inbox/inbox.controller.ts
T
bjoernpoettkerandClaude Opus 5.5 bb8bf3714f fix(security): API-Keys nur für Admins, Mailversand ohne Datei-/URL-Zugriff
- 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>
2026-11-04 10:46:43 +01:00

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,
});
}
}