- PermissionsGuard lehnt Routen ohne @RequirePermissions/@AllowAnyAuthenticated
ab. API-Keys umgehen keine Rechteprüfung mehr, sondern gelten nur auf
Routen mit @AllowApiKey (Paperless-Webhook, externer Upload, checksum,
Agent-Routen des Etikettendrucks).
- Rechte für 24 bisher offene Routen ergänzt, u.a. paperless/documents
(nur Admin), paperless/documents/:id und Kontonummern (Posteingang/
manuell), webhook/status (Admin). routen-rechte.spec.ts prüft alle Routen.
- Etiketten-Agent: printed/error nur für abgeholte, offene Jobs (bedingtes
Update, sonst 409) – ein gedruckter Job kann seine Nummer nicht mehr
freigeben. Feldwerte nur aus den Eingabefeldern der Vorlage ({number}
nicht vorgebbar) und URL-kodiert in GET-/Callback-URLs.
- PUT paperless/inbox/:id ignoriert Freigabe (CF15) und Zahlung (CF16)
ohne die jeweilige Rolle (Funktionstrennung).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
90 lines
2.7 KiB
TypeScript
90 lines
2.7 KiB
TypeScript
import {
|
|
Controller,
|
|
Post,
|
|
Get,
|
|
Body,
|
|
Logger,
|
|
HttpCode,
|
|
HttpStatus,
|
|
UseGuards,
|
|
BadRequestException,
|
|
} from '@nestjs/common';
|
|
import { ApiKeyGuard } from '../auth/api-key.guard';
|
|
import { WebhookQueueService } from './webhook-queue.service';
|
|
import { AllowApiKey, RequirePermissions } from '../auth/permissions.decorator';
|
|
import { Permission } from '../auth/permissions.enum';
|
|
|
|
export interface PaperlessWebhookPayload {
|
|
doc_url?: string;
|
|
document_id?: number; // optional, für manuelles Testen
|
|
action?: string;
|
|
[key: string]: any;
|
|
}
|
|
|
|
@Controller('api/webhook')
|
|
export class WebhookController {
|
|
private readonly logger = new Logger(WebhookController.name);
|
|
|
|
constructor(private readonly webhookQueue: WebhookQueueService) {}
|
|
|
|
@UseGuards(ApiKeyGuard)
|
|
@Post('paperless')
|
|
@AllowApiKey()
|
|
@HttpCode(HttpStatus.OK)
|
|
async handlePaperlessWebhook(@Body() payload: PaperlessWebhookPayload) {
|
|
const documentId = this.extractDocumentId(payload);
|
|
if (!documentId) {
|
|
this.logger.warn(
|
|
`Webhook ohne ermittelbare Dokument-ID: ${JSON.stringify(payload)}`,
|
|
);
|
|
await this.webhookQueue.recordStatus({
|
|
documentId: null,
|
|
action: payload.action,
|
|
status: 'bad-request',
|
|
});
|
|
throw new BadRequestException(
|
|
'Keine Dokument-ID aus doc_url/document_id ermittelbar',
|
|
);
|
|
}
|
|
|
|
this.logger.log(
|
|
`Webhook: action=${payload.action}, document=${documentId} → eingereiht`,
|
|
);
|
|
// Sofort einreihen und antworten; die eigentliche Verarbeitung übernimmt
|
|
// der separate Queue-Prozess (sequenziell, dedupliziert).
|
|
await this.webhookQueue.enqueue(documentId, payload.action);
|
|
return { status: 'queued', documentId };
|
|
}
|
|
|
|
/**
|
|
* Liefert Informationen zum letzten Webhook-Vorgang (Zeitpunkt, Dokument,
|
|
* Ergebnis) und die aktuelle Warteschlangen-Größe. Über die globalen Guards
|
|
* per JWT oder API-Key zugänglich.
|
|
*/
|
|
@Get('status')
|
|
@RequirePermissions(Permission.MANAGE_SETTINGS)
|
|
async getWebhookStatus(): Promise<{ lastCall: unknown; queueSize: number }> {
|
|
return this.webhookQueue.getStatus();
|
|
}
|
|
|
|
/**
|
|
* Ermittelt die Dokument-ID aus dem Webhook-Payload. Bevorzugt wird das
|
|
* optionale Feld `document_id` (für manuelles Testen), andernfalls wird die
|
|
* ID aus dem Paperless-Platzhalter `{{doc_url}}` extrahiert
|
|
* (z.B. ".../documents/123/").
|
|
*/
|
|
private extractDocumentId(payload: PaperlessWebhookPayload): number | null {
|
|
if (
|
|
payload?.document_id != null &&
|
|
!Number.isNaN(Number(payload.document_id))
|
|
) {
|
|
return Number(payload.document_id);
|
|
}
|
|
if (typeof payload?.doc_url === 'string') {
|
|
const m = payload.doc_url.match(/\/documents\/(\d+)/);
|
|
if (m) return Number(m[1]);
|
|
}
|
|
return null;
|
|
}
|
|
}
|