Freigabe → main: Mobile UI, Webhook, IMAP, Zahlung-Workflow und weitere Features #5
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Post,
|
Post,
|
||||||
|
Get,
|
||||||
Body,
|
Body,
|
||||||
Logger,
|
Logger,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
@@ -8,8 +9,11 @@ import {
|
|||||||
UseGuards,
|
UseGuards,
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
import { ApiKeyGuard } from '../auth/api-key.guard';
|
import { ApiKeyGuard } from '../auth/api-key.guard';
|
||||||
import { PaperlessProcessorService } from '../paperless/paperless-processor.service';
|
import { PaperlessProcessorService } from '../paperless/paperless-processor.service';
|
||||||
|
import { Setting } from '../database/entities/setting.entity';
|
||||||
|
|
||||||
export interface PaperlessWebhookPayload {
|
export interface PaperlessWebhookPayload {
|
||||||
doc_url?: string;
|
doc_url?: string;
|
||||||
@@ -18,11 +22,18 @@ export interface PaperlessWebhookPayload {
|
|||||||
[key: string]: any;
|
[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')
|
@Controller('api/webhook')
|
||||||
export class WebhookController {
|
export class WebhookController {
|
||||||
private readonly logger = new Logger(WebhookController.name);
|
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)
|
@UseGuards(ApiKeyGuard)
|
||||||
@Post('paperless')
|
@Post('paperless')
|
||||||
@@ -33,6 +44,11 @@ export class WebhookController {
|
|||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`Webhook ohne ermittelbare Dokument-ID: ${JSON.stringify(payload)}`,
|
`Webhook ohne ermittelbare Dokument-ID: ${JSON.stringify(payload)}`,
|
||||||
);
|
);
|
||||||
|
await this.recordWebhookCall({
|
||||||
|
documentId: null,
|
||||||
|
action: payload.action,
|
||||||
|
status: 'bad-request',
|
||||||
|
});
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
'Keine Dokument-ID aus doc_url/document_id ermittelbar',
|
'Keine Dokument-ID aus doc_url/document_id ermittelbar',
|
||||||
);
|
);
|
||||||
@@ -45,10 +61,14 @@ export class WebhookController {
|
|||||||
try {
|
try {
|
||||||
const result =
|
const result =
|
||||||
await this.paperlessProcessor.processDocumentById(documentId);
|
await this.paperlessProcessor.processDocumentById(documentId);
|
||||||
return {
|
const status = result.processed ? 'processed' : 'skipped';
|
||||||
status: result.processed ? 'processed' : 'skipped',
|
await this.recordWebhookCall({
|
||||||
|
documentId,
|
||||||
|
action: payload.action,
|
||||||
|
status,
|
||||||
reason: result.reason,
|
reason: result.reason,
|
||||||
};
|
});
|
||||||
|
return { status, reason: result.reason };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Fehler tolerieren (wie der bisherige Cron) und 200 zurückgeben,
|
// Fehler tolerieren (wie der bisherige Cron) und 200 zurückgeben,
|
||||||
// damit Paperless keine Retry-Schleife startet.
|
// damit Paperless keine Retry-Schleife startet.
|
||||||
@@ -56,10 +76,35 @@ export class WebhookController {
|
|||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Fehler bei Webhook-Verarbeitung von Dokument ${documentId}: ${message}`,
|
`Fehler bei Webhook-Verarbeitung von Dokument ${documentId}: ${message}`,
|
||||||
);
|
);
|
||||||
|
await this.recordWebhookCall({
|
||||||
|
documentId,
|
||||||
|
action: payload.action,
|
||||||
|
status: 'error',
|
||||||
|
message,
|
||||||
|
});
|
||||||
return { 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
|
* Ermittelt die Dokument-ID aus dem Webhook-Payload. Bevorzugt wird das
|
||||||
* optionale Feld `document_id` (für manuelles Testen), andernfalls wird die
|
* optionale Feld `document_id` (für manuelles Testen), andernfalls wird die
|
||||||
@@ -79,4 +124,47 @@ export class WebhookController {
|
|||||||
}
|
}
|
||||||
return null;
|
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 { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { WebhookController } from './webhook.controller';
|
import { WebhookController } from './webhook.controller';
|
||||||
import { PaperlessModule } from '../paperless/paperless.module';
|
import { PaperlessModule } from '../paperless/paperless.module';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../auth/auth.module';
|
||||||
|
import { Setting } from '../database/entities/setting.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PaperlessModule, AuthModule],
|
imports: [TypeOrmModule.forFeature([Setting]), PaperlessModule, AuthModule],
|
||||||
controllers: [WebhookController],
|
controllers: [WebhookController],
|
||||||
})
|
})
|
||||||
export class WebhookModule {}
|
export class WebhookModule {}
|
||||||
|
|||||||
Reference in New Issue
Block a user