Freigabe → main: Mobile UI, Webhook, IMAP, Zahlung-Workflow und weitere Features #5

Merged
bjoernpoettker merged 16 commits from Freigabe into main 2026-07-14 07:44:32 +00:00
2 changed files with 95 additions and 5 deletions
Showing only changes of commit 32d9e84985 - Show all commits
@@ -1,6 +1,7 @@
import {
Controller,
Post,
Get,
Body,
Logger,
HttpCode,
@@ -8,8 +9,11 @@ import {
UseGuards,
BadRequestException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ApiKeyGuard } from '../auth/api-key.guard';
import { PaperlessProcessorService } from '../paperless/paperless-processor.service';
import { Setting } from '../database/entities/setting.entity';
export interface PaperlessWebhookPayload {
doc_url?: string;
@@ -18,11 +22,18 @@ export interface PaperlessWebhookPayload {
[key: string]: any;
}
// Tag (Schlüssel) des Settings-Eintrags, der den letzten Webhook-Aufruf festhält.
const LAST_WEBHOOK_CALL_TAG = 'last_webhook_call';
@Controller('api/webhook')
export class WebhookController {
private readonly logger = new Logger(WebhookController.name);
constructor(private readonly paperlessProcessor: PaperlessProcessorService) {}
constructor(
private readonly paperlessProcessor: PaperlessProcessorService,
@InjectRepository(Setting)
private readonly settingRepo: Repository<Setting>,
) {}
@UseGuards(ApiKeyGuard)
@Post('paperless')
@@ -33,6 +44,11 @@ export class WebhookController {
this.logger.warn(
`Webhook ohne ermittelbare Dokument-ID: ${JSON.stringify(payload)}`,
);
await this.recordWebhookCall({
documentId: null,
action: payload.action,
status: 'bad-request',
});
throw new BadRequestException(
'Keine Dokument-ID aus doc_url/document_id ermittelbar',
);
@@ -45,10 +61,14 @@ export class WebhookController {
try {
const result =
await this.paperlessProcessor.processDocumentById(documentId);
return {
status: result.processed ? 'processed' : 'skipped',
const status = result.processed ? 'processed' : 'skipped';
await this.recordWebhookCall({
documentId,
action: payload.action,
status,
reason: result.reason,
};
});
return { status, reason: result.reason };
} catch (err) {
// Fehler tolerieren (wie der bisherige Cron) und 200 zurückgeben,
// damit Paperless keine Retry-Schleife startet.
@@ -56,10 +76,35 @@ export class WebhookController {
this.logger.error(
`Fehler bei Webhook-Verarbeitung von Dokument ${documentId}: ${message}`,
);
await this.recordWebhookCall({
documentId,
action: payload.action,
status: 'error',
message,
});
return { status: 'error', message };
}
}
/**
* Liefert Informationen zum letzten Webhook-Aufruf (Zeitpunkt, Dokument,
* Ergebnis). Über die globalen Guards per JWT oder API-Key zugänglich.
*/
@Get('status')
async getWebhookStatus(): Promise<{ lastCall: unknown }> {
const setting = await this.settingRepo.findOneBy({
Tag: LAST_WEBHOOK_CALL_TAG,
});
if (!setting?.Wert) {
return { lastCall: null };
}
try {
return { lastCall: JSON.parse(setting.Wert) };
} catch {
return { lastCall: { raw: setting.Wert } };
}
}
/**
* Ermittelt die Dokument-ID aus dem Webhook-Payload. Bevorzugt wird das
* optionale Feld `document_id` (für manuelles Testen), andernfalls wird die
@@ -79,4 +124,47 @@ export class WebhookController {
}
return null;
}
/**
* Hält den letzten Webhook-Aufruf in der Settings-Tabelle fest
* (Tag "last_webhook_call"). Fehler beim Speichern werden nur geloggt und
* beeinflussen die Webhook-Antwort nicht.
*/
private async recordWebhookCall(info: {
documentId: number | null;
action?: string;
status: string;
reason?: string;
message?: string;
}): Promise<void> {
try {
const value = JSON.stringify({
at: new Date().toISOString(),
documentId: info.documentId,
action: info.action ?? null,
status: info.status,
...(info.reason ? { reason: info.reason } : {}),
...(info.message ? { message: info.message.slice(0, 100) } : {}),
});
let setting = await this.settingRepo.findOneBy({
Tag: LAST_WEBHOOK_CALL_TAG,
});
if (!setting) {
setting = this.settingRepo.create({
Typ: 0,
Tag: LAST_WEBHOOK_CALL_TAG,
Wert: value,
});
} else {
setting.Wert = value;
}
await this.settingRepo.save(setting);
} catch (err) {
this.logger.error(
'Konnte letzten Webhook-Aufruf nicht in den Settings speichern',
err instanceof Error ? err.stack : String(err),
);
}
}
}
@@ -1,10 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WebhookController } from './webhook.controller';
import { PaperlessModule } from '../paperless/paperless.module';
import { AuthModule } from '../auth/auth.module';
import { Setting } from '../database/entities/setting.entity';
@Module({
imports: [PaperlessModule, AuthModule],
imports: [TypeOrmModule.forFeature([Setting]), PaperlessModule, AuthModule],
controllers: [WebhookController],
})
export class WebhookModule {}