Compare commits
6 Commits
b1b30fe1dd
...
Freigabe
| Author | SHA1 | Date | |
|---|---|---|---|
| 969f0ae0b1 | |||
| 41eed1871e | |||
| ed57477324 | |||
| ef7813f9f9 | |||
| 66aeab282c | |||
| 14c11bf718 |
+7
-6
@@ -7,6 +7,13 @@
|
||||
# Produktion: VITE_API_URL leer lassen (nginx Reverse-Proxy leitet /api weiter)
|
||||
# =============================================================================
|
||||
|
||||
# --- Umgebung ---
|
||||
# Produktion: NODE_ENV=production -> KEIN TypeORM-synchronize (Schema via Migrationen),
|
||||
# Migrationen werden beim Start automatisch ausgeführt (migrationsRun).
|
||||
# Aktiviert zudem CORS-Schutz (siehe CORS_ORIGIN weiter unten).
|
||||
# Entwicklung: NODE_ENV leer lassen -> synchronize ON (Schema folgt den Entities).
|
||||
NODE_ENV=production
|
||||
|
||||
# --- Ports ---
|
||||
BACKEND_PORT=7601
|
||||
FRONTEND_PORT=7600
|
||||
@@ -67,9 +74,3 @@ AGRARMONITOR_UPLOAD_CHECK_CRON=0 * * * * * # Upload-Check-Intervall (Standard:
|
||||
# Leer lassen: E-Mails werden ohne Links versendet
|
||||
APP_URL=
|
||||
DAILY_DIGEST_CRON= # Standard: 0 7 * * * (täglich 07:00 Uhr Europe/Berlin)
|
||||
|
||||
# --- IMAP-Ordnerverwaltung ---
|
||||
# Zielordner für importierte E-Mails (wird automatisch angelegt falls nicht vorhanden)
|
||||
IMAP_IMPORTED_FOLDER=importiert
|
||||
# Papierkorb-Ordner für die 90-Tage-Bereinigung (Gmail: "[Gmail]/Papierkorb", Outlook: "Deleted Items")
|
||||
IMAP_TRASH_FOLDER=Trash
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
name: Manual Build and Push (custom tag)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Container-Tag (z. B. stable, v1.2.3, hotfix)"
|
||||
required: true
|
||||
default: "stable"
|
||||
service:
|
||||
description: "Welche Images bauen?"
|
||||
type: choice
|
||||
required: true
|
||||
default: "both"
|
||||
options:
|
||||
- both
|
||||
- backend
|
||||
- frontend
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Show selected inputs
|
||||
run: |
|
||||
echo "Tag: ${{ inputs.tag }}"
|
||||
echo "Service: ${{ inputs.service }}"
|
||||
echo "Ref: ${{ github.ref_name }}"
|
||||
|
||||
- name: Login to Gitea Container Registry
|
||||
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login gitea.poettker-cloud.de -u "${{ gitea.actor }}" --password-stdin
|
||||
|
||||
- name: Build and Push Backend
|
||||
if: ${{ inputs.service == 'both' || inputs.service == 'backend' }}
|
||||
env:
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
IMAGE="gitea.poettker-cloud.de/bjoernpoettker/paperlessmanager-backend:${TAG}"
|
||||
docker build -t "$IMAGE" ./paperless-backend
|
||||
docker push "$IMAGE"
|
||||
|
||||
- name: Build and Push Frontend
|
||||
if: ${{ inputs.service == 'both' || inputs.service == 'frontend' }}
|
||||
env:
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
IMAGE="gitea.poettker-cloud.de/bjoernpoettker/paperlessmanager-frontend:${TAG}"
|
||||
docker build -t "$IMAGE" ./paperless-frontend
|
||||
docker push "$IMAGE"
|
||||
@@ -78,11 +78,37 @@ Use `@Public()` to bypass auth guards entirely.
|
||||
|
||||
### Database
|
||||
|
||||
- TypeORM with MySQL 8+, UTF8MB4, `synchronize: true` (schema auto-migrates)
|
||||
- TypeORM with MySQL/MariaDB, UTF8MB4
|
||||
- Connection config lives in `src/database/data-source.ts` (single source of truth,
|
||||
shared by the NestJS runtime and the TypeORM CLI). `database.module.ts` consumes it.
|
||||
- **Schema strategy depends on `NODE_ENV`:**
|
||||
- Dev (`NODE_ENV` unset): `synchronize: true` — schema auto-migrates from entities.
|
||||
- Production (`NODE_ENV=production`): `synchronize: false` + `migrationsRun: true` —
|
||||
pending migrations in `src/database/migrations/` are applied automatically on boot.
|
||||
**Never run `synchronize` against the production DB** — on MariaDB it issues
|
||||
destructive `ADD`/`DROP COLUMN` churn every boot (see caveat below).
|
||||
- 23 entities in `src/database/entities/`
|
||||
- JSON columns use transformers to normalize empty arrays/objects to `null`
|
||||
- Key entities: `InboxDocument`, `Task`, `Email`, `Attachment`, `Postprocessing`, `BarcodeTemplate`, `LabelPrintJob`, `ApiKey`, `Setting`
|
||||
|
||||
#### Migrations workflow
|
||||
|
||||
```bash
|
||||
npm run migration:generate -- src/database/migrations/<Name> # diff entities → DB
|
||||
npm run migration:run # apply pending
|
||||
npm run migration:revert # roll back last
|
||||
```
|
||||
|
||||
The existing production schema is the implicit baseline (no baseline migration); only
|
||||
future changes get migration files.
|
||||
|
||||
**Caveat — MariaDB reports `json` as `longtext`:** every `@Column({ type: 'json' })`
|
||||
is stored as `longtext ... CHECK (json_valid(...))`, so `migration:generate` always
|
||||
emits spurious no-op `CHANGE` statements (json↔longtext, nullable/default re-declares).
|
||||
**Hand-trim generated migrations** down to the real change before committing. (This same
|
||||
false diff is exactly why `synchronize` must stay off in production — left unchecked it
|
||||
accumulates `ALGORITHM=INSTANT` drops until a table trips the 8126-byte row-size limit.)
|
||||
|
||||
### Document Processing Pipeline
|
||||
|
||||
**Preprocessing** (`preprocessing/document-pipeline.service.ts`):
|
||||
|
||||
@@ -8,6 +8,8 @@ services:
|
||||
ports:
|
||||
- "${BACKEND_PORT:-7601}:3100"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- CORS_ORIGIN=${CORS_ORIGIN:-}
|
||||
- PORT=3100
|
||||
- DB_HOST=${DB_HOST:-db}
|
||||
- DB_PORT=${DB_PORT:-3306}
|
||||
|
||||
@@ -28,26 +28,8 @@ export default tseslint.config(
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'warn',
|
||||
// any-getriebene Typsicherheits-Regeln: als Warnung statt Fehler, da die
|
||||
// Paperless-NGX-API-Antworten (noch) untypisiert sind. Echte Bugs bleiben
|
||||
// Fehler (alle nicht hier herabgestuften Regeln).
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||
'@typescript-eslint/no-unsafe-return': 'warn',
|
||||
'@typescript-eslint/restrict-template-expressions': 'warn',
|
||||
'@typescript-eslint/no-base-to-string': 'warn',
|
||||
'@typescript-eslint/no-redundant-type-constituents': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrors: 'none',
|
||||
},
|
||||
],
|
||||
'prettier/prettier': ['error', { endOfLine: 'auto' }],
|
||||
"prettier/prettier": ["error", { endOfLine: "auto" }],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
Generated
+16
-3
@@ -24,6 +24,7 @@
|
||||
"axios": "^1.14.0",
|
||||
"basic-ftp": "^5.2.1",
|
||||
"chokidar": "^4.0.3",
|
||||
"dotenv": "^17.4.2",
|
||||
"form-data": "^4.0.5",
|
||||
"imapflow": "^1.3.2",
|
||||
"jsqr": "^1.4.0",
|
||||
@@ -3023,6 +3024,18 @@
|
||||
"rxjs": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/config/node_modules/dotenv": {
|
||||
"version": "17.2.3",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
|
||||
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/core": {
|
||||
"version": "11.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.17.tgz",
|
||||
@@ -6291,9 +6304,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.2.3",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
|
||||
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
|
||||
"version": "17.4.2",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
|
||||
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
|
||||
@@ -17,7 +17,11 @@
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"typeorm": "typeorm-ts-node-commonjs -d ./src/database/data-source.ts",
|
||||
"migration:generate": "npm run typeorm -- migration:generate",
|
||||
"migration:run": "npm run typeorm -- migration:run",
|
||||
"migration:revert": "npm run typeorm -- migration:revert"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^11.0.1",
|
||||
@@ -35,6 +39,7 @@
|
||||
"axios": "^1.14.0",
|
||||
"basic-ftp": "^5.2.1",
|
||||
"chokidar": "^4.0.3",
|
||||
"dotenv": "^17.4.2",
|
||||
"form-data": "^4.0.5",
|
||||
"imapflow": "^1.3.2",
|
||||
"jsqr": "^1.4.0",
|
||||
|
||||
@@ -59,7 +59,7 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
||||
}
|
||||
|
||||
@Cron(process.env['AGRARMONITOR_POLLING_CRON'] || '0 */30 * * * *')
|
||||
scheduledPolling() {
|
||||
async scheduledPolling() {
|
||||
if (!process.env['AGRARMONITOR_POLLING_CRON']) return;
|
||||
this.runPolling().catch((err) =>
|
||||
this.logger.error('Cron-Polling-Fehler:', err),
|
||||
@@ -67,7 +67,7 @@ export class AgrarmonitorPollingService implements OnModuleInit {
|
||||
}
|
||||
|
||||
@Cron(process.env['AGRARMONITOR_UPLOAD_CHECK_CRON'] || '0 * * * * *')
|
||||
scheduledUploadCheck() {
|
||||
async scheduledUploadCheck() {
|
||||
if (!process.env['AGRARMONITOR_UPLOAD_CHECK_CRON']) return;
|
||||
this.processVerarbeiteteDocuments().catch((err) =>
|
||||
this.logger.error('Cron-Upload-Check-Fehler:', err),
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
AesGcmCookieEncryptor,
|
||||
type AgrarmonitorConnectorResult,
|
||||
} from 'agrarmonitor-connector';
|
||||
import { getErrorMessage } from '../common/error.util';
|
||||
|
||||
export interface AgrarmonitorStatusDto {
|
||||
connected: boolean;
|
||||
@@ -96,13 +95,13 @@ export class AgrarmonitorService {
|
||||
registriert: registrierungStatus.registriert,
|
||||
freigeschaltet: freigeschaltetStatus.freigeschaltet,
|
||||
};
|
||||
} catch (err: unknown) {
|
||||
} catch (err: any) {
|
||||
this.client = null;
|
||||
return {
|
||||
connected: false,
|
||||
registriert: null,
|
||||
freigeschaltet: null,
|
||||
error: getErrorMessage(err) || 'Verbindung fehlgeschlagen',
|
||||
error: err?.message ?? 'Verbindung fehlgeschlagen',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { Request } from 'express';
|
||||
import type { Permission } from './permissions.enum';
|
||||
|
||||
/** Vom JwtStrategy.validate() an `request.user` angehängte Identität. */
|
||||
export interface AuthenticatedUser {
|
||||
userId: string;
|
||||
email: string;
|
||||
name: string;
|
||||
preferredUsername: string | null;
|
||||
groups: string[];
|
||||
permissions: Permission[];
|
||||
}
|
||||
|
||||
/** Vom ApiKeyGuard an `request.apiKeyMetadata` angehängte Schlüssel-Info. */
|
||||
export interface ApiKeyMetadata {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/** Express-Request mit der durch die Auth-Guards gesetzten Identität. */
|
||||
export interface AuthenticatedRequest extends Request {
|
||||
user?: AuthenticatedUser;
|
||||
apiKeyMetadata?: ApiKeyMetadata;
|
||||
}
|
||||
@@ -4,15 +4,6 @@ import { Strategy, ExtractJwt } from 'passport-jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { passportJwtSecret } from 'jwks-rsa';
|
||||
import { mapGroupsToPermissions } from './permissions.enum';
|
||||
import type { AuthenticatedUser } from './authenticated-request';
|
||||
|
||||
interface JwtPayload {
|
||||
sub: string;
|
||||
email: string;
|
||||
name?: string;
|
||||
preferred_username?: string;
|
||||
groups?: string[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
@@ -33,12 +24,19 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
});
|
||||
}
|
||||
|
||||
validate(payload: JwtPayload): AuthenticatedUser {
|
||||
const groups = payload.groups ?? [];
|
||||
validate(payload: any): {
|
||||
userId: string;
|
||||
email: string;
|
||||
name: string;
|
||||
preferredUsername: string | null;
|
||||
groups: string[];
|
||||
permissions: any[];
|
||||
} {
|
||||
const groups = payload.groups || [];
|
||||
return {
|
||||
userId: payload.sub,
|
||||
email: payload.email,
|
||||
name: payload.name || payload.preferred_username || '',
|
||||
name: payload.name || payload.preferred_username,
|
||||
preferredUsername: payload.preferred_username ?? null,
|
||||
groups: groups,
|
||||
permissions: mapGroupsToPermissions(groups),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import * as fs from 'fs/promises';
|
||||
import sharp from 'sharp';
|
||||
import sharp = require('sharp');
|
||||
import { PdfService } from '../preprocessing/pdf.service';
|
||||
import { QrCodeService } from '../preprocessing/qr-code.service';
|
||||
import {
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
applyTemplate,
|
||||
buildVariables,
|
||||
} from '../inbox-postprocessor/variable-resolver';
|
||||
import { getErrorMessage } from '../common/error.util';
|
||||
|
||||
export interface MatchedBarcode {
|
||||
page: number;
|
||||
@@ -34,7 +33,6 @@ export interface MatchedBarcode {
|
||||
export class BarcodeScannerService implements OnApplicationBootstrap {
|
||||
private readonly logger = new Logger(BarcodeScannerService.name);
|
||||
private templatesCache: BarcodeTemplate[] | null = null;
|
||||
private readonly regenerating = new Map<string, Promise<void>>();
|
||||
|
||||
constructor(
|
||||
private readonly pdfService: PdfService,
|
||||
@@ -58,9 +56,9 @@ export class BarcodeScannerService implements OnApplicationBootstrap {
|
||||
let rows: BarcodeTemplate[];
|
||||
try {
|
||||
rows = await this.templateRepo.find();
|
||||
} catch (err: unknown) {
|
||||
} catch (err: any) {
|
||||
this.logger.warn(
|
||||
`Template-Migration: Query fehlgeschlagen: ${getErrorMessage(err)}`,
|
||||
`Template-Migration: Query fehlgeschlagen: ${err.message}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -96,9 +94,9 @@ export class BarcodeScannerService implements OnApplicationBootstrap {
|
||||
doc.IsScanned = true;
|
||||
try {
|
||||
await this.documentRepo.save(doc);
|
||||
} catch (err: unknown) {
|
||||
} catch (err: any) {
|
||||
this.logger.warn(
|
||||
`Scan-Ergebnis konnte nicht gespeichert werden (${doc.Id}): ${getErrorMessage(err)}`,
|
||||
`Scan-Ergebnis konnte nicht gespeichert werden (${doc.Id}): ${err.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -121,44 +119,6 @@ export class BarcodeScannerService implements OnApplicationBootstrap {
|
||||
return this.matchTemplates(doc.QrCodes ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stellt sicher, dass der Seiten-Cache (thumb/preview-PNGs) für ein Dokument
|
||||
* vorhanden ist. Parallele Aufrufe für dasselbe Dokument warten auf dasselbe
|
||||
* Promise, um doppeltes Rendering zu vermeiden.
|
||||
*/
|
||||
async ensurePageCache(documentId: string, pdfPath: string): Promise<void> {
|
||||
const existing = this.regenerating.get(documentId);
|
||||
if (existing) return existing;
|
||||
|
||||
const work = this.doRegenerateCache(documentId, pdfPath).finally(() => {
|
||||
this.regenerating.delete(documentId);
|
||||
});
|
||||
this.regenerating.set(documentId, work);
|
||||
return work;
|
||||
}
|
||||
|
||||
private async doRegenerateCache(
|
||||
documentId: string,
|
||||
pdfPath: string,
|
||||
): Promise<void> {
|
||||
let images: string[] = [];
|
||||
try {
|
||||
images = await this.pdfService.pdfToImages(pdfPath, 200);
|
||||
await this.pageCache.clear(documentId);
|
||||
await this.pageCache.generate(documentId, images);
|
||||
this.logger.log(
|
||||
`Seiten-Cache regeneriert für ${documentId} (${images.length} Seiten)`,
|
||||
);
|
||||
} catch (err: unknown) {
|
||||
this.logger.warn(
|
||||
`Cache-Regenerierung fehlgeschlagen (${documentId}): ${getErrorMessage(err)}`,
|
||||
);
|
||||
throw err;
|
||||
} finally {
|
||||
await this.pdfService.cleanup(images);
|
||||
}
|
||||
}
|
||||
|
||||
private async matchTemplates(
|
||||
qrCodes: StoredQrCode[],
|
||||
): Promise<MatchedBarcode[]> {
|
||||
@@ -232,9 +192,9 @@ export class BarcodeScannerService implements OnApplicationBootstrap {
|
||||
qrCodes.push({ page: i + 1, value: qr.data });
|
||||
}
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
} catch (err: any) {
|
||||
this.logger.warn(
|
||||
`QR-Scan fehlgeschlagen (${pdfPath}, Seite ${i + 1}): ${getErrorMessage(err)}`,
|
||||
`QR-Scan fehlgeschlagen (${pdfPath}, Seite ${i + 1}): ${err.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -243,10 +203,8 @@ export class BarcodeScannerService implements OnApplicationBootstrap {
|
||||
await this.pageCache.generate(documentId, images);
|
||||
|
||||
return { qrCodes, pageCount: images.length };
|
||||
} catch (err: unknown) {
|
||||
this.logger.warn(
|
||||
`Kein QR-Scan möglich für ${pdfPath}: ${getErrorMessage(err)}`,
|
||||
);
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`Kein QR-Scan möglich für ${pdfPath}: ${err.message}`);
|
||||
return { qrCodes: [], pageCount: 0 };
|
||||
} finally {
|
||||
await this.pdfService.cleanup(images);
|
||||
@@ -322,10 +280,8 @@ export class BarcodeScannerService implements OnApplicationBootstrap {
|
||||
let docs: InboxDocument[];
|
||||
try {
|
||||
docs = await this.documentRepo.find();
|
||||
} catch (err: unknown) {
|
||||
this.logger.warn(
|
||||
`Rescan: DB-Query fehlgeschlagen: ${getErrorMessage(err)}`,
|
||||
);
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`Rescan: DB-Query fehlgeschlagen: ${err.message}`);
|
||||
return { scanned: 0, failed: 0 };
|
||||
}
|
||||
if (docs.length === 0) return { scanned: 0, failed: 0 };
|
||||
@@ -350,10 +306,8 @@ export class BarcodeScannerService implements OnApplicationBootstrap {
|
||||
doc.PageCount = pageCount;
|
||||
await this.documentRepo.save(doc);
|
||||
scanned++;
|
||||
} catch (err: unknown) {
|
||||
this.logger.warn(
|
||||
`Rescan fehlgeschlagen für ${doc.Id}: ${getErrorMessage(err)}`,
|
||||
);
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`Rescan fehlgeschlagen für ${doc.Id}: ${err.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { ConfigService } from '@nestjs/config';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs/promises';
|
||||
import sharp from 'sharp';
|
||||
import { getErrorMessage } from '../common/error.util';
|
||||
|
||||
const THUMBNAIL_WIDTH = 180;
|
||||
|
||||
@@ -55,9 +54,9 @@ export class PageCacheService {
|
||||
.resize({ width: THUMBNAIL_WIDTH })
|
||||
.png()
|
||||
.toFile(thumbDest);
|
||||
} catch (err: unknown) {
|
||||
} catch (err: any) {
|
||||
this.logger.warn(
|
||||
`Seiten-Cache fehlgeschlagen (${documentId} Seite ${page}): ${getErrorMessage(err)}`,
|
||||
`Seiten-Cache fehlgeschlagen (${documentId} Seite ${page}): ${err.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -104,9 +103,9 @@ export class PageCacheService {
|
||||
const to = path.join(dir, `page-${n - 1}.${variant}.png`);
|
||||
try {
|
||||
await fs.rename(from, to);
|
||||
} catch (err: unknown) {
|
||||
} catch (err: any) {
|
||||
this.logger.warn(
|
||||
`Cache-Shift fehlgeschlagen (${documentId} Seite ${n} ${variant}): ${getErrorMessage(err)}`,
|
||||
`Cache-Shift fehlgeschlagen (${documentId} Seite ${n} ${variant}): ${err.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
/**
|
||||
* Hilfsfunktionen, um in catch-Blöcken sicher auf Fehlerwerte vom Typ
|
||||
* `unknown` zuzugreifen, statt unsichere `any`-Zugriffe zu verwenden.
|
||||
*/
|
||||
|
||||
/** Extrahiert eine lesbare Fehlermeldung aus einem unbekannten Fehlerwert. */
|
||||
export function getErrorMessage(err: unknown): string {
|
||||
if (err instanceof Error) return err.message;
|
||||
if (typeof err === 'string') return err;
|
||||
if (
|
||||
err !== null &&
|
||||
typeof err === 'object' &&
|
||||
'message' in err &&
|
||||
typeof (err as { message: unknown }).message === 'string'
|
||||
) {
|
||||
return (err as { message: string }).message;
|
||||
}
|
||||
return String(err);
|
||||
}
|
||||
|
||||
/** Liefert den Stacktrace, falls es sich um ein Error-Objekt handelt. */
|
||||
export function getErrorStack(err: unknown): string | undefined {
|
||||
return err instanceof Error ? err.stack : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert `err.response.data` eines HTTP-Fehlers (z. B. von axios), falls
|
||||
* vorhanden — ohne den Fehlerwert als `any` zu behandeln.
|
||||
*/
|
||||
export function getResponseData(err: unknown): unknown {
|
||||
if (
|
||||
err !== null &&
|
||||
typeof err === 'object' &&
|
||||
'response' in err &&
|
||||
(err as { response: unknown }).response !== null &&
|
||||
typeof (err as { response: unknown }).response === 'object'
|
||||
) {
|
||||
const response = (err as { response: Record<string, unknown> }).response;
|
||||
return 'data' in response ? response.data : undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Liefert den Node-Fehlercode (z. B. 'ENOENT', 'EXDEV'), falls vorhanden. */
|
||||
export function getErrorCode(err: unknown): string | undefined {
|
||||
if (
|
||||
err !== null &&
|
||||
typeof err === 'object' &&
|
||||
'code' in err &&
|
||||
typeof (err as { code: unknown }).code === 'string'
|
||||
) {
|
||||
return (err as { code: string }).code;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { Controller, Post, Request, HttpCode } from '@nestjs/common';
|
||||
import { DailyDigestService } from './daily-digest.service';
|
||||
import type { AuthenticatedRequest } from '../auth/authenticated-request';
|
||||
import { getErrorMessage } from '../common/error.util';
|
||||
|
||||
@Controller('api/daily-digest')
|
||||
export class DailyDigestController {
|
||||
@@ -9,8 +7,8 @@ export class DailyDigestController {
|
||||
|
||||
@Post('send-now')
|
||||
@HttpCode(200)
|
||||
async sendNow(@Request() req: AuthenticatedRequest) {
|
||||
const { userId, email, preferredUsername, groups } = req.user!;
|
||||
async sendNow(@Request() req: any) {
|
||||
const { userId, email, preferredUsername, groups } = req.user;
|
||||
if (!email) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -21,12 +19,12 @@ export class DailyDigestController {
|
||||
await this.dailyDigestService.sendDigestForUser(
|
||||
userId,
|
||||
email,
|
||||
preferredUsername ?? undefined,
|
||||
preferredUsername,
|
||||
groups,
|
||||
);
|
||||
return { ok: true };
|
||||
} catch (err: unknown) {
|
||||
return { ok: false, error: getErrorMessage(err) };
|
||||
} catch (err: any) {
|
||||
return { ok: false, error: err.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import 'reflect-metadata';
|
||||
import { join } from 'path';
|
||||
import { config as loadEnv } from 'dotenv';
|
||||
import { DataSource, DataSourceOptions } from 'typeorm';
|
||||
import {
|
||||
Client,
|
||||
DocumentType,
|
||||
DocumentField,
|
||||
Task,
|
||||
Postprocessing,
|
||||
PostprocessingAction,
|
||||
PostprocessingLog,
|
||||
ExportTarget,
|
||||
Setting,
|
||||
Kontonummer,
|
||||
Document,
|
||||
UserClient,
|
||||
Email,
|
||||
Attachment,
|
||||
Content,
|
||||
ApiKey,
|
||||
CorrespondentSetting,
|
||||
BarcodeTemplate,
|
||||
InboxDocument,
|
||||
InboxPostprocessingAction,
|
||||
CorrespondentEmailMapping,
|
||||
UserSettings,
|
||||
LabelPrintJob,
|
||||
} from './entities';
|
||||
|
||||
// CLI-Kontext: .env laden (Laufzeit im Container liefert die Variablen via Docker,
|
||||
// dotenv ist dort ein No-Op). dotenv überschreibt bereits gesetzte Variablen nicht.
|
||||
loadEnv();
|
||||
loadEnv({ path: join(process.cwd(), '..', '.env') });
|
||||
|
||||
export const entities = [
|
||||
Client,
|
||||
DocumentType,
|
||||
DocumentField,
|
||||
Task,
|
||||
Postprocessing,
|
||||
PostprocessingAction,
|
||||
PostprocessingLog,
|
||||
ExportTarget,
|
||||
Setting,
|
||||
Kontonummer,
|
||||
Document,
|
||||
UserClient,
|
||||
Email,
|
||||
Attachment,
|
||||
Content,
|
||||
ApiKey,
|
||||
CorrespondentSetting,
|
||||
BarcodeTemplate,
|
||||
InboxDocument,
|
||||
InboxPostprocessingAction,
|
||||
CorrespondentEmailMapping,
|
||||
UserSettings,
|
||||
LabelPrintJob,
|
||||
];
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
export const dataSourceOptions: DataSourceOptions = {
|
||||
type: 'mysql',
|
||||
host: process.env.DB_HOST ?? 'localhost',
|
||||
port: Number(process.env.DB_PORT ?? 3306),
|
||||
username: process.env.DB_USERNAME ?? 'root',
|
||||
password: process.env.DB_PASSWORD ?? '',
|
||||
database: process.env.DB_DATABASE ?? 'paperlessadd',
|
||||
charset: 'utf8mb4',
|
||||
entities,
|
||||
migrations: [join(__dirname, 'migrations', '*.{ts,js}')],
|
||||
// In Produktion: kein synchronize (zerstörerischer ALTER-Churn), Migrationen
|
||||
// werden beim Start automatisch ausgeführt. In Dev: synchronize wie bisher.
|
||||
synchronize: !isProduction,
|
||||
migrationsRun: isProduction,
|
||||
};
|
||||
|
||||
// Wird von der TypeORM-CLI verwendet (migration:generate / :run / :revert).
|
||||
export default new DataSource(dataSourceOptions);
|
||||
@@ -1,75 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import {
|
||||
Client,
|
||||
DocumentType,
|
||||
DocumentField,
|
||||
Task,
|
||||
Postprocessing,
|
||||
PostprocessingAction,
|
||||
PostprocessingLog,
|
||||
ExportTarget,
|
||||
Setting,
|
||||
Kontonummer,
|
||||
Document,
|
||||
UserClient,
|
||||
Email,
|
||||
Attachment,
|
||||
Content,
|
||||
ApiKey,
|
||||
CorrespondentSetting,
|
||||
BarcodeTemplate,
|
||||
InboxDocument,
|
||||
InboxPostprocessingAction,
|
||||
CorrespondentEmailMapping,
|
||||
UserSettings,
|
||||
LabelPrintJob,
|
||||
} from './entities';
|
||||
|
||||
const entities = [
|
||||
Client,
|
||||
DocumentType,
|
||||
DocumentField,
|
||||
Task,
|
||||
Postprocessing,
|
||||
PostprocessingAction,
|
||||
PostprocessingLog,
|
||||
ExportTarget,
|
||||
Setting,
|
||||
Kontonummer,
|
||||
Document,
|
||||
UserClient,
|
||||
Email,
|
||||
Attachment,
|
||||
Content,
|
||||
ApiKey,
|
||||
CorrespondentSetting,
|
||||
BarcodeTemplate,
|
||||
InboxDocument,
|
||||
InboxPostprocessingAction,
|
||||
CorrespondentEmailMapping,
|
||||
UserSettings,
|
||||
LabelPrintJob,
|
||||
];
|
||||
import { dataSourceOptions, entities } from './data-source';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
type: 'mysql' as const,
|
||||
host: config.get<string>('DB_HOST', 'localhost'),
|
||||
port: config.get<number>('DB_PORT', 3306),
|
||||
username: config.get<string>('DB_USERNAME', 'root'),
|
||||
password: config.get<string>('DB_PASSWORD', ''),
|
||||
database: config.get<string>('DB_DATABASE', 'paperlessadd'),
|
||||
entities,
|
||||
synchronize: config.get<string>('NODE_ENV') !== 'production',
|
||||
charset: 'utf8mb4',
|
||||
}),
|
||||
}),
|
||||
// Eine einzige Konfigurationsquelle (data-source.ts), geteilt zwischen
|
||||
// NestJS-Laufzeit und TypeORM-CLI (Migrationen).
|
||||
TypeOrmModule.forRoot(dataSourceOptions),
|
||||
TypeOrmModule.forFeature(entities),
|
||||
],
|
||||
exports: [TypeOrmModule],
|
||||
|
||||
@@ -19,7 +19,6 @@ import { Email } from '../database/entities/email.entity';
|
||||
import { Attachment } from '../database/entities/attachment.entity';
|
||||
import { Content } from '../database/entities/content.entity';
|
||||
import { isERechnung } from './zugferd.util';
|
||||
import { getErrorMessage, getErrorStack } from '../common/error.util';
|
||||
|
||||
@Injectable()
|
||||
export class EmailDownloadService {
|
||||
@@ -46,10 +45,10 @@ export class EmailDownloadService {
|
||||
this.running = true;
|
||||
try {
|
||||
await this.fetchAndStore();
|
||||
} catch (err: unknown) {
|
||||
} catch (err: any) {
|
||||
this.logger.error(
|
||||
`Fehler im E-Mail-Download-Job: ${getErrorMessage(err)}`,
|
||||
getErrorStack(err),
|
||||
`Fehler im E-Mail-Download-Job: ${err.message}`,
|
||||
err.stack,
|
||||
);
|
||||
} finally {
|
||||
this.running = false;
|
||||
@@ -69,47 +68,6 @@ export class EmailDownloadService {
|
||||
});
|
||||
}
|
||||
|
||||
@Cron('0 3 * * *', { timeZone: 'Europe/Berlin' })
|
||||
async cleanupImportedEmails(): Promise<void> {
|
||||
if (!this.configService.get<string>('IMAP_HOST')) return;
|
||||
const importedFolder = this.configService.get<string>('IMAP_IMPORTED_FOLDER', 'importiert');
|
||||
const trashFolder = this.configService.get<string>('IMAP_TRASH_FOLDER', 'Trash');
|
||||
const client = this.createImapClient();
|
||||
try {
|
||||
await client.connect();
|
||||
|
||||
// Alte E-Mails (> 90 Tage) in Papierkorb verschieben
|
||||
try {
|
||||
await client.mailboxOpen(importedFolder);
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - 90);
|
||||
const oldUids = await client.search({ before: cutoff }, { uid: true });
|
||||
if (Array.isArray(oldUids) && oldUids.length > 0) {
|
||||
await client.messageMove(oldUids, trashFolder, { uid: true });
|
||||
this.logger.log(`${oldUids.length} alte E-Mail(s) aus "${importedFolder}" in "${trashFolder}" verschoben.`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`Bereinigung "${importedFolder}" nicht möglich: ${err.message}`);
|
||||
}
|
||||
|
||||
// Papierkorb leeren
|
||||
try {
|
||||
await client.mailboxOpen(trashFolder);
|
||||
const trashUids = await client.search({ all: true }, { uid: true });
|
||||
if (Array.isArray(trashUids) && trashUids.length > 0) {
|
||||
await client.messageDelete(trashUids, { uid: true });
|
||||
this.logger.log(`${trashUids.length} E-Mail(s) aus "${trashFolder}" gelöscht.`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`Papierkorb "${trashFolder}" konnte nicht geleert werden: ${err.message}`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.logger.error(`IMAP-Cleanup fehlgeschlagen: ${err.message}`);
|
||||
} finally {
|
||||
await client.logout().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchAndStore(): Promise<void> {
|
||||
const host = this.configService.get<string>('IMAP_HOST');
|
||||
const user = this.configService.get<string>('IMAP_USERNAME');
|
||||
@@ -162,10 +120,10 @@ export class EmailDownloadService {
|
||||
}
|
||||
|
||||
await this.processMessage(msg);
|
||||
} catch (err: unknown) {
|
||||
} catch (err: any) {
|
||||
this.logger.error(
|
||||
`Fehler beim Abrufen der Nachricht ${messageId}: ${getErrorMessage(err)}`,
|
||||
getErrorStack(err),
|
||||
`Fehler beim Abrufen der Nachricht ${messageId}: ${err.message}`,
|
||||
err.stack,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -203,7 +161,7 @@ export class EmailDownloadService {
|
||||
}> = [];
|
||||
|
||||
for (const att of parsed.attachments) {
|
||||
const entry = this.buildAttachment(att);
|
||||
const entry = await this.buildAttachment(att);
|
||||
if (entry) attachmentsToPersist.push(entry);
|
||||
}
|
||||
|
||||
@@ -260,9 +218,9 @@ export class EmailDownloadService {
|
||||
|
||||
await this.pdfService.cleanup(images);
|
||||
await fs.unlink(tempPdfPath).catch(() => {});
|
||||
} catch (err: unknown) {
|
||||
} catch (err: any) {
|
||||
this.logger.warn(
|
||||
`Konnte Vorschaubilder für Anhang ${attachment.Id} nicht generieren: ${getErrorMessage(err)}`,
|
||||
`Konnte Vorschaubilder für Anhang ${attachment.Id} nicht generieren: ${err.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -308,9 +266,9 @@ export class EmailDownloadService {
|
||||
return { processed, failed };
|
||||
}
|
||||
|
||||
private buildAttachment(
|
||||
private async buildAttachment(
|
||||
att: MailAttachment,
|
||||
): { attachment: Attachment; buffer: Buffer } | null {
|
||||
): Promise<{ attachment: Attachment; buffer: Buffer } | null> {
|
||||
const buffer = att.content;
|
||||
if (!Buffer.isBuffer(buffer) || buffer.length === 0) return null;
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import { EmailImportService } from './email-import.service';
|
||||
import { EmailPageCacheService } from './email-page-cache.service';
|
||||
import { RequirePermissions } from '../auth/permissions.decorator';
|
||||
import { Permission } from '../auth/permissions.enum';
|
||||
import { getErrorMessage } from '../common/error.util';
|
||||
|
||||
@Controller('api/email-import')
|
||||
export class EmailImportController {
|
||||
@@ -132,14 +131,9 @@ export class EmailImportController {
|
||||
`inline; filename="preview-${attachmentId}.pdf"`,
|
||||
);
|
||||
res.send(pdfBuffer);
|
||||
} catch (err: unknown) {
|
||||
this.logger.error(
|
||||
`Error generating print preview: ${getErrorMessage(err)}`,
|
||||
);
|
||||
throw new HttpException(
|
||||
getErrorMessage(err),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Error generating print preview: ${err.message}`);
|
||||
throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,12 +185,9 @@ export class EmailImportController {
|
||||
try {
|
||||
const result = await this.importService.executeImport(importData);
|
||||
return result;
|
||||
} catch (err: unknown) {
|
||||
this.logger.error(`Error executing import: ${getErrorMessage(err)}`);
|
||||
throw new HttpException(
|
||||
getErrorMessage(err),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Error executing import: ${err.message}`);
|
||||
throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as crypto from 'crypto';
|
||||
import { getErrorMessage } from '../common/error.util';
|
||||
|
||||
@Injectable()
|
||||
export class EmailImportService {
|
||||
@@ -83,9 +82,9 @@ export class EmailImportService {
|
||||
await this.attachmentRepo.save(attachment);
|
||||
|
||||
await this.pdfService.cleanup(images);
|
||||
} catch (err: unknown) {
|
||||
} catch (err: any) {
|
||||
this.logger.warn(
|
||||
`Fehler bei on-demand Vorschau-Generierung für Anhang ${attachment.Id}: ${getErrorMessage(err)}`,
|
||||
`Fehler bei on-demand Vorschau-Generierung für Anhang ${attachment.Id}: ${err.message}`,
|
||||
);
|
||||
} finally {
|
||||
await fs.unlink(tempPdfPath).catch(() => {});
|
||||
@@ -159,12 +158,11 @@ export class EmailImportService {
|
||||
|
||||
this.logger.debug(`Received Belegnummer: ${result}`);
|
||||
return String(result);
|
||||
} catch (error: unknown) {
|
||||
const axiosErr = axios.isAxiosError(error) ? error : undefined;
|
||||
const status = axiosErr?.response?.status ?? 'UNKNOWN';
|
||||
const detail = axiosErr?.response?.data
|
||||
? JSON.stringify(axiosErr.response.data)
|
||||
: getErrorMessage(error);
|
||||
} catch (error: any) {
|
||||
const status = error.response?.status || 'UNKNOWN';
|
||||
const detail = error.response?.data
|
||||
? JSON.stringify(error.response.data)
|
||||
: error.message;
|
||||
this.logger.error(
|
||||
`Failed to fetch Belegnummer from ${url}. Status: ${status}, Detail: ${detail}`,
|
||||
);
|
||||
@@ -195,9 +193,9 @@ export class EmailImportService {
|
||||
`Releasing Belegnummer: ${cleanNumber} (original: ${number}) via ${url}`,
|
||||
);
|
||||
await axios.get(url);
|
||||
} catch (error: unknown) {
|
||||
} catch (error: any) {
|
||||
this.logger.error(
|
||||
`Failed to release Belegnummer at ${url}: ${getErrorMessage(error)}`,
|
||||
`Failed to release Belegnummer at ${url}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -219,9 +217,9 @@ export class EmailImportService {
|
||||
`Setting Belegnummer: ${cleanNumber} (original: ${number}) via ${url}`,
|
||||
);
|
||||
await axios.get(url);
|
||||
} catch (error: unknown) {
|
||||
} catch (error: any) {
|
||||
this.logger.error(
|
||||
`Failed to set Belegnummer at ${url}: ${getErrorMessage(error)}`,
|
||||
`Failed to set Belegnummer at ${url}: ${error.message}`,
|
||||
);
|
||||
throw new HttpException(
|
||||
'Fehler beim Setzen der Belegnummer',
|
||||
@@ -614,9 +612,7 @@ export class EmailImportService {
|
||||
docId = statusObj.related_document;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Task-Status nicht parsebar: nächsten Versuch abwarten
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (docId) {
|
||||
@@ -635,10 +631,7 @@ export class EmailImportService {
|
||||
// Confirm Belegnummer if used
|
||||
if (att.belegnummer && att.barcode?.nummer) {
|
||||
await this.setBelegnummer(data.emailDate, att.barcode.nummer).catch(
|
||||
(e) =>
|
||||
this.logger.warn(
|
||||
`Failed to set Belegnummer: ${getErrorMessage(e)}`,
|
||||
),
|
||||
(e) => this.logger.warn(`Failed to set Belegnummer: ${e.message}`),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { ConfigService } from '@nestjs/config';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs/promises';
|
||||
import sharp from 'sharp';
|
||||
import { getErrorMessage } from '../common/error.util';
|
||||
|
||||
const THUMBNAIL_WIDTH = 180;
|
||||
|
||||
@@ -56,9 +55,9 @@ export class EmailPageCacheService {
|
||||
.resize({ width: THUMBNAIL_WIDTH })
|
||||
.png()
|
||||
.toFile(thumbDest);
|
||||
} catch (err: unknown) {
|
||||
} catch (err: any) {
|
||||
this.logger.warn(
|
||||
`E-Mail Page Cache fehlgeschlagen (Attachment ${attachmentId} Seite ${page}): ${getErrorMessage(err)}`,
|
||||
`E-Mail Page Cache fehlgeschlagen (Attachment ${attachmentId} Seite ${page}): ${err.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@ import { Email } from '../database/entities/email.entity';
|
||||
import { Attachment } from '../database/entities/attachment.entity';
|
||||
import { Content } from '../database/entities/content.entity';
|
||||
import { PaperlessService } from '../paperless/paperless.service';
|
||||
import { ImapFolderService } from './imap-folder.service';
|
||||
import { RequirePermissions } from '../auth/permissions.decorator';
|
||||
import { Permission } from '../auth/permissions.enum';
|
||||
import { getErrorMessage, getErrorStack } from '../common/error.util';
|
||||
|
||||
@Controller('api/emails')
|
||||
export class EmailController {
|
||||
@@ -32,6 +32,7 @@ export class EmailController {
|
||||
@InjectRepository(Content)
|
||||
private readonly contentRepo: Repository<Content>,
|
||||
private readonly paperlessService: PaperlessService,
|
||||
private readonly imapFolderService: ImapFolderService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@@ -174,10 +175,10 @@ export class EmailController {
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
} catch (err: any) {
|
||||
this.logger.error(
|
||||
`Fehler bei Checksummen-Prüfung für Attachment ${attachment.Id}: ${getErrorMessage(err)}`,
|
||||
getErrorStack(err),
|
||||
`Fehler bei Checksummen-Prüfung für Attachment ${attachment.Id}: ${err.message}`,
|
||||
err.stack,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -203,11 +204,25 @@ export class EmailController {
|
||||
this.logger.log(
|
||||
`Prüfung abgeschlossen. ${updatedCount} E-Mails aktualisiert, ${idsUpdated} Paperless-IDs ergänzt, ${skippedCount} übersprungen.`,
|
||||
);
|
||||
return { updatedCount, idsUpdated };
|
||||
} catch (error: unknown) {
|
||||
this.imapFolderService.cleanupImportedEmails().catch(err =>
|
||||
this.logger.error('IMAP-Cleanup fehlgeschlagen: ' + err.message),
|
||||
);
|
||||
|
||||
let movedToImportiert = 0;
|
||||
if (body.includeProcessed) {
|
||||
const processedEmails = await this.emailRepo.find({
|
||||
where: [{ Status: 1 }, { Status: 3 }],
|
||||
select: ['MessageId'],
|
||||
});
|
||||
const messageIds = processedEmails.map((e) => e.MessageId).filter(Boolean);
|
||||
movedToImportiert = await this.imapFolderService.moveProcessedInboxToImportiert(messageIds);
|
||||
}
|
||||
|
||||
return { updatedCount, idsUpdated, movedToImportiert };
|
||||
} catch (error: any) {
|
||||
this.logger.error(
|
||||
`Kritischer Fehler bei checkAttachments: ${getErrorMessage(error)}`,
|
||||
getErrorStack(error),
|
||||
`Kritischer Fehler bei checkAttachments: ${error.message}`,
|
||||
error.stack,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { ImapFlow } from 'imapflow';
|
||||
|
||||
@Injectable()
|
||||
@@ -21,6 +22,91 @@ export class ImapFolderService {
|
||||
});
|
||||
}
|
||||
|
||||
@Cron('0 3 * * *', { timeZone: 'Europe/Berlin' })
|
||||
async cleanupImportedEmails(): Promise<void> {
|
||||
if (!this.configService.get<string>('IMAP_HOST')) return;
|
||||
const importedFolder = this.configService.get<string>('IMAP_IMPORTED_FOLDER', 'importiert');
|
||||
const trashFolder = this.configService.get<string>('IMAP_TRASH_FOLDER', 'Trash');
|
||||
const client = this.createClient();
|
||||
try {
|
||||
await client.connect();
|
||||
|
||||
// E-Mails älter als 90 Tage in Papierkorb verschieben
|
||||
try {
|
||||
await client.mailboxOpen(importedFolder);
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - 90);
|
||||
const oldUids = await client.search({ before: cutoff }, { uid: true });
|
||||
if (Array.isArray(oldUids) && oldUids.length > 0) {
|
||||
await client.messageMove(oldUids, trashFolder, { uid: true });
|
||||
this.logger.log(`${oldUids.length} alte E-Mail(s) aus "${importedFolder}" in "${trashFolder}" verschoben.`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`Bereinigung "${importedFolder}" nicht möglich: ${err.message}`);
|
||||
}
|
||||
|
||||
// Papierkorb leeren
|
||||
try {
|
||||
await client.mailboxOpen(trashFolder);
|
||||
const trashUids = await client.search({ all: true }, { uid: true });
|
||||
if (Array.isArray(trashUids) && trashUids.length > 0) {
|
||||
await client.messageDelete(trashUids, { uid: true });
|
||||
this.logger.log(`${trashUids.length} E-Mail(s) aus "${trashFolder}" gelöscht.`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`Papierkorb "${trashFolder}" konnte nicht geleert werden: ${err.message}`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.logger.error(`IMAP-Cleanup fehlgeschlagen: ${err.message}`);
|
||||
} finally {
|
||||
await client.logout().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
async moveProcessedInboxToImportiert(messageIds: string[]): Promise<number> {
|
||||
if (!this.configService.get<string>('IMAP_HOST') || messageIds.length === 0) return 0;
|
||||
|
||||
const importedFolder = this.configService.get<string>('IMAP_IMPORTED_FOLDER', 'importiert');
|
||||
const client = this.createClient();
|
||||
let movedCount = 0;
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
|
||||
const mailboxes = await client.list();
|
||||
if (!mailboxes.some(m => m.path === importedFolder)) {
|
||||
await client.mailboxCreate(importedFolder);
|
||||
this.logger.log(`IMAP-Ordner "${importedFolder}" erstellt.`);
|
||||
}
|
||||
|
||||
const status = await client.mailboxOpen('INBOX');
|
||||
if (status.exists === 0) return 0;
|
||||
|
||||
const normalize = (id: string) => id.replace(/^<|>$/g, '').toLowerCase();
|
||||
const idSet = new Set(messageIds.map(normalize));
|
||||
const uidsToMove: number[] = [];
|
||||
|
||||
for await (const msg of client.fetch('1:*', { uid: true, envelope: true })) {
|
||||
const msgId = msg.envelope?.messageId;
|
||||
if (msgId && idSet.has(normalize(msgId))) {
|
||||
uidsToMove.push(msg.uid);
|
||||
}
|
||||
}
|
||||
|
||||
if (uidsToMove.length > 0) {
|
||||
await client.messageMove(uidsToMove, importedFolder, { uid: true });
|
||||
movedCount = uidsToMove.length;
|
||||
this.logger.log(`${movedCount} E-Mail(s) aus INBOX → "${importedFolder}" verschoben.`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.logger.error(`moveProcessedInboxToImportiert fehlgeschlagen: ${err.message}`);
|
||||
} finally {
|
||||
await client.logout().catch(() => {});
|
||||
}
|
||||
|
||||
return movedCount;
|
||||
}
|
||||
|
||||
async moveToImportiert(messageId: string): Promise<void> {
|
||||
if (!this.configService.get<string>('IMAP_HOST')) return;
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ export class FreigabeService {
|
||||
try {
|
||||
const result = await this.paperlessService.getDocuments(params);
|
||||
return result;
|
||||
} catch {
|
||||
} catch (err: any) {
|
||||
// Fallback: Paperless unterstützt den custom_fields-Filter möglicherweise nicht
|
||||
// In diesem Fall alle Belege laden und client-seitig filtern
|
||||
this.logger.warn(
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
extractSectionToTemp,
|
||||
} from './edit-applier';
|
||||
import { applyTemplate, buildVariables } from './variable-resolver';
|
||||
import { getErrorMessage } from '../common/error.util';
|
||||
|
||||
function parseFlexDate(s: string): Date | null {
|
||||
if (!s) return null;
|
||||
@@ -277,16 +276,16 @@ export class InboxPostprocessorService {
|
||||
abortProcessing = true;
|
||||
break;
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
} catch (err: any) {
|
||||
this.logger.error(
|
||||
`Aktion PAPERLESS#${action.Id} für Dokument ${doc.Id} fehlgeschlagen: ${getErrorMessage(err)}`,
|
||||
`Aktion PAPERLESS#${action.Id} für Dokument ${doc.Id} fehlgeschlagen: ${err.message}`,
|
||||
);
|
||||
results.push({
|
||||
sectionIndex: currentSectionIndex,
|
||||
actionId: action.Id,
|
||||
actionType: action.ActionType,
|
||||
ok: false,
|
||||
message: getErrorMessage(err),
|
||||
message: err.message,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -304,16 +303,16 @@ export class InboxPostprocessorService {
|
||||
actionType: action.ActionType,
|
||||
ok: true,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
} catch (err: any) {
|
||||
this.logger.error(
|
||||
`Aktion ${action.ActionType}#${action.Id} für Dokument ${doc.Id} fehlgeschlagen: ${getErrorMessage(err)}`,
|
||||
`Aktion ${action.ActionType}#${action.Id} für Dokument ${doc.Id} fehlgeschlagen: ${err.message}`,
|
||||
);
|
||||
results.push({
|
||||
sectionIndex: currentSectionIndex,
|
||||
actionId: action.Id,
|
||||
actionType: action.ActionType,
|
||||
ok: false,
|
||||
message: getErrorMessage(err),
|
||||
message: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,9 +48,7 @@ export function buildVariables(ctx: ResolverContext): Record<string, string> {
|
||||
if (gv !== undefined) vars[`barcode.${sanitizeKey(g)}`] = gv;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ungültige Regex/Barcode-Daten: Variablen bleiben ungesetzt
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return vars;
|
||||
|
||||
@@ -3,7 +3,6 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, In } from 'typeorm';
|
||||
import { Client } from '../database/entities/client.entity';
|
||||
import { UserClient } from '../database/entities/user-client.entity';
|
||||
import type { AuthenticatedRequest } from '../auth/authenticated-request';
|
||||
|
||||
@Controller('api/clients')
|
||||
export class ClientsController {
|
||||
@@ -16,8 +15,8 @@ export class ClientsController {
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
async getMyClients(@Request() req: AuthenticatedRequest) {
|
||||
const userId = req.user!.userId;
|
||||
async getMyClients(@Request() req: any) {
|
||||
const userId = req.user.userId;
|
||||
const mappings = await this.userClientRepo.find({
|
||||
where: { UserId: userId },
|
||||
});
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
type InboxSource,
|
||||
type StoredQrCode,
|
||||
} from '../database/entities/inbox-document.entity';
|
||||
import { getErrorMessage, getErrorCode } from '../common/error.util';
|
||||
import { PageCacheService } from '../barcode/page-cache.service';
|
||||
|
||||
interface LegacyScanRow {
|
||||
@@ -42,10 +41,10 @@ export class InboxMigrationService implements OnApplicationBootstrap {
|
||||
withFileTypes: true,
|
||||
});
|
||||
subdirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
||||
} catch (err: unknown) {
|
||||
if (getErrorCode(err) !== 'ENOENT') {
|
||||
} catch (err: any) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
this.logger.warn(
|
||||
`Migration: ${this.legacyRoot} nicht lesbar: ${getErrorMessage(err)}`,
|
||||
`Migration: ${this.legacyRoot} nicht lesbar: ${err.message}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
@@ -57,10 +56,8 @@ export class InboxMigrationService implements OnApplicationBootstrap {
|
||||
let files: string[];
|
||||
try {
|
||||
files = await fs.readdir(dir);
|
||||
} catch (err: unknown) {
|
||||
this.logger.warn(
|
||||
`Migration: ${dir} nicht lesbar: ${getErrorMessage(err)}`,
|
||||
);
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`Migration: ${dir} nicht lesbar: ${err.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -70,9 +67,9 @@ export class InboxMigrationService implements OnApplicationBootstrap {
|
||||
try {
|
||||
await this.migrateFile(src, subdir, name);
|
||||
migrated += 1;
|
||||
} catch (err: unknown) {
|
||||
} catch (err: any) {
|
||||
this.logger.error(
|
||||
`Migration fehlgeschlagen (${src}): ${getErrorMessage(err)}`,
|
||||
`Migration fehlgeschlagen (${src}): ${err.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -119,8 +116,8 @@ export class InboxMigrationService implements OnApplicationBootstrap {
|
||||
try {
|
||||
await fs.rename(src, dest);
|
||||
return;
|
||||
} catch (err: unknown) {
|
||||
if (getErrorCode(err) !== 'EXDEV') throw err;
|
||||
} catch (err: any) {
|
||||
if (err.code !== 'EXDEV') throw err;
|
||||
}
|
||||
await fs.copyFile(src, dest);
|
||||
try {
|
||||
|
||||
@@ -21,8 +21,6 @@ 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 type { AuthenticatedRequest } from '../auth/authenticated-request';
|
||||
import type { InboxSource } from '../database/entities/inbox-document.entity';
|
||||
|
||||
@Controller('api/inbox')
|
||||
@RequirePermissions(Permission.VIEW_SCANNER)
|
||||
@@ -35,7 +33,7 @@ export class InboxController {
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
async list(@Request() req: AuthenticatedRequest) {
|
||||
async list(@Request() req: any) {
|
||||
const preferredUsername: string | null =
|
||||
req.user?.preferredUsername ?? null;
|
||||
return this.inboxService.listFiles(preferredUsername);
|
||||
@@ -49,7 +47,7 @@ export class InboxController {
|
||||
@Get(':id/preview')
|
||||
async preview(
|
||||
@Param('id') id: string,
|
||||
@Request() req: AuthenticatedRequest,
|
||||
@Request() req: any,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
): Promise<StreamableFile> {
|
||||
const preferredUsername: string | null =
|
||||
@@ -71,7 +69,7 @@ export class InboxController {
|
||||
async pageThumbnail(
|
||||
@Param('id') id: string,
|
||||
@Param('page', ParseIntPipe) page: number,
|
||||
@Request() req: AuthenticatedRequest,
|
||||
@Request() req: any,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
): Promise<StreamableFile> {
|
||||
const preferredUsername: string | null =
|
||||
@@ -90,10 +88,7 @@ export class InboxController {
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(204)
|
||||
async remove(
|
||||
@Param('id') id: string,
|
||||
@Request() req: AuthenticatedRequest,
|
||||
): Promise<void> {
|
||||
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);
|
||||
@@ -104,7 +99,7 @@ export class InboxController {
|
||||
async removePage(
|
||||
@Param('id') id: string,
|
||||
@Param('page', ParseIntPipe) page: number,
|
||||
@Request() req: AuthenticatedRequest,
|
||||
@Request() req: any,
|
||||
): Promise<void> {
|
||||
const preferredUsername: string | null =
|
||||
req.user?.preferredUsername ?? null;
|
||||
@@ -116,7 +111,7 @@ export class InboxController {
|
||||
async toggleManualSplit(
|
||||
@Param('id') id: string,
|
||||
@Param('page', ParseIntPipe) page: number,
|
||||
@Request() req: AuthenticatedRequest,
|
||||
@Request() req: any,
|
||||
): Promise<void> {
|
||||
const preferredUsername: string | null =
|
||||
req.user?.preferredUsername ?? null;
|
||||
@@ -127,7 +122,7 @@ export class InboxController {
|
||||
@HttpCode(204)
|
||||
async resetEdits(
|
||||
@Param('id') id: string,
|
||||
@Request() req: AuthenticatedRequest,
|
||||
@Request() req: any,
|
||||
): Promise<void> {
|
||||
const preferredUsername: string | null =
|
||||
req.user?.preferredUsername ?? null;
|
||||
@@ -137,7 +132,7 @@ export class InboxController {
|
||||
@Post(':id/postprocess')
|
||||
async postprocess(
|
||||
@Param('id') id: string,
|
||||
@Request() req: AuthenticatedRequest,
|
||||
@Request() req: any,
|
||||
@Body()
|
||||
body: {
|
||||
sectionOffset?: number;
|
||||
@@ -163,7 +158,7 @@ export class InboxController {
|
||||
@Param('id') id: string,
|
||||
@Param('page', ParseIntPipe) page: number,
|
||||
@Body() body: { rotation?: number },
|
||||
@Request() req: AuthenticatedRequest,
|
||||
@Request() req: any,
|
||||
): Promise<void> {
|
||||
const rotation = Number(body?.rotation);
|
||||
if (!Number.isFinite(rotation)) {
|
||||
@@ -183,7 +178,7 @@ export class InboxController {
|
||||
async pagePreview(
|
||||
@Param('id') id: string,
|
||||
@Param('page', ParseIntPipe) page: number,
|
||||
@Request() req: AuthenticatedRequest,
|
||||
@Request() req: any,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
): Promise<StreamableFile> {
|
||||
const preferredUsername: string | null =
|
||||
@@ -205,7 +200,7 @@ export class InboxController {
|
||||
@Param('id') id: string,
|
||||
@Param('page', ParseIntPipe) page: number,
|
||||
@Body() body: { x: number; y: number; w: number; h: number },
|
||||
@Request() req: AuthenticatedRequest,
|
||||
@Request() req: any,
|
||||
): Promise<{ found: string[] }> {
|
||||
const preferredUsername: string | null =
|
||||
req.user?.preferredUsername ?? null;
|
||||
@@ -224,8 +219,8 @@ export class InboxController {
|
||||
@HttpCode(204)
|
||||
async updateSource(
|
||||
@Param('id') id: string,
|
||||
@Body() body: { source: InboxSource },
|
||||
@Request() req: AuthenticatedRequest,
|
||||
@Body() body: { source: any },
|
||||
@Request() req: any,
|
||||
): Promise<void> {
|
||||
const preferredUsername: string | null =
|
||||
req.user?.preferredUsername ?? null;
|
||||
@@ -236,7 +231,7 @@ export class InboxController {
|
||||
async downloadSegment(
|
||||
@Param('id') id: string,
|
||||
@Body() body: { pages: number[] },
|
||||
@Request() req: AuthenticatedRequest,
|
||||
@Request() req: any,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
): Promise<StreamableFile> {
|
||||
const preferredUsername: string | null =
|
||||
@@ -268,13 +263,13 @@ export class InboxController {
|
||||
segments: { pages: number[]; filename: string }[];
|
||||
sender?: string;
|
||||
},
|
||||
@Request() req: AuthenticatedRequest,
|
||||
@Request() req: any,
|
||||
): Promise<void> {
|
||||
const preferredUsername: string | null =
|
||||
req.user?.preferredUsername ?? null;
|
||||
const smtpOverride =
|
||||
body.sender === 'user'
|
||||
? await this.userSettingsService.getSmtpConfig(req.user!.userId)
|
||||
? await this.userSettingsService.getSmtpConfig(req.user.userId)
|
||||
: null;
|
||||
await this.inboxService.sendAsEmail(id, preferredUsername, {
|
||||
...body,
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
} from '../database/entities/inbox-document.entity';
|
||||
import { MailService } from '../postprocessing/mail.service';
|
||||
import { buildSegmentBuffer } from '../inbox-postprocessor/edit-applier';
|
||||
import { getErrorMessage } from '../common/error.util';
|
||||
|
||||
export interface InboxFile {
|
||||
id: string;
|
||||
@@ -100,9 +99,9 @@ export class InboxService {
|
||||
try {
|
||||
const stat = await fs.stat(pdfPath);
|
||||
if (!stat.isFile()) throw new Error('not a file');
|
||||
} catch (err: unknown) {
|
||||
} catch (err: any) {
|
||||
this.logger.warn(
|
||||
`Datei fehlt trotz DB-Eintrag (${doc.Id}): ${getErrorMessage(err)}`,
|
||||
`Datei fehlt trotz DB-Eintrag (${doc.Id}): ${err.message}`,
|
||||
);
|
||||
throw new NotFoundException('Dokument nicht gefunden');
|
||||
}
|
||||
@@ -219,9 +218,9 @@ export class InboxService {
|
||||
await this.documentRepo.delete(doc.Id);
|
||||
try {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
} catch (err: unknown) {
|
||||
} catch (err: any) {
|
||||
this.logger.warn(
|
||||
`Dokument-Ordner konnte nicht gelöscht werden (${dir}): ${getErrorMessage(err)}`,
|
||||
`Dokument-Ordner konnte nicht gelöscht werden (${dir}): ${err.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -232,7 +231,7 @@ export class InboxService {
|
||||
variant: 'preview' | 'thumbnail',
|
||||
preferredUsername: string | null,
|
||||
): Promise<string> {
|
||||
const { doc, pdfPath } = await this.resolveDocument(id, preferredUsername);
|
||||
const { doc } = await this.resolveDocument(id, preferredUsername);
|
||||
if (!Number.isInteger(page) || page < 1 || page > doc.PageCount) {
|
||||
throw new NotFoundException('Seite nicht gefunden');
|
||||
}
|
||||
@@ -244,12 +243,7 @@ export class InboxService {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
} catch {
|
||||
try {
|
||||
await this.barcodeScanner.ensurePageCache(doc.Id, pdfPath);
|
||||
await fs.access(filePath);
|
||||
} catch {
|
||||
throw new NotFoundException('Seite nicht gefunden');
|
||||
}
|
||||
throw new NotFoundException('Seite nicht gefunden');
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Kontonummer } from '../database/entities';
|
||||
|
||||
@@ -10,7 +10,6 @@ import { Subject, Observable } from 'rxjs';
|
||||
import { LabelPrintJob } from '../database/entities/label-print-job.entity';
|
||||
import { BarcodeTemplate } from '../database/entities/barcode-template.entity';
|
||||
import { LabelRendererService } from './label-renderer.service';
|
||||
import { getErrorMessage } from '../common/error.util';
|
||||
|
||||
function isSafeUrl(url: string): boolean {
|
||||
try {
|
||||
@@ -96,10 +95,8 @@ export class LabelPrintAgentService {
|
||||
const res = await fetch(url);
|
||||
const text = (await res.text()).trim();
|
||||
vars['number'] = text;
|
||||
} catch (err: unknown) {
|
||||
this.logger.warn(
|
||||
`GET-URL fehlgeschlagen (${url}): ${getErrorMessage(err)}`,
|
||||
);
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`GET-URL fehlgeschlagen (${url}): ${err.message}`);
|
||||
}
|
||||
} else {
|
||||
this.logger.warn(`GET-URL übersprungen (ungültiges Protokoll): ${url}`);
|
||||
@@ -152,9 +149,9 @@ export class LabelPrintAgentService {
|
||||
candidate.LabelVariables ?? {},
|
||||
);
|
||||
await this.jobRepo.save(candidate);
|
||||
} catch (err: unknown) {
|
||||
} catch (err: any) {
|
||||
this.logger.error(
|
||||
`Label-Rendering fehlgeschlagen für Job ${candidate.Id}: ${getErrorMessage(err)}`,
|
||||
`Label-Rendering fehlgeschlagen für Job ${candidate.Id}: ${err.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -260,10 +257,8 @@ export class LabelPrintAgentService {
|
||||
}
|
||||
try {
|
||||
await fetch(url, { method: 'POST' });
|
||||
} catch (err: unknown) {
|
||||
this.logger.warn(
|
||||
`${type}-URL fehlgeschlagen (${url}): ${getErrorMessage(err)}`,
|
||||
);
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`${type}-URL fehlgeschlagen (${url}): ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
import * as QRCode from 'qrcode';
|
||||
import { Resvg } from '@resvg/resvg-js';
|
||||
import type { LabelElement } from '../database/entities/barcode-template.entity';
|
||||
import { getErrorMessage } from '../common/error.util';
|
||||
|
||||
const MM_TO_PX = 300 / 25.4; // 300 DPI
|
||||
|
||||
@@ -85,9 +84,9 @@ export class LabelRendererService {
|
||||
parts.push(
|
||||
`<image href="data:image/png;base64,${b64}" x="${x}" y="${y}" width="${size}" height="${size}"/>`,
|
||||
);
|
||||
} catch (err: unknown) {
|
||||
} catch (err: any) {
|
||||
this.logger.warn(
|
||||
`QR-Code-Rendering fehlgeschlagen für "${content}": ${getErrorMessage(err)}`,
|
||||
`QR-Code-Rendering fehlgeschlagen für "${content}": ${err.message}`,
|
||||
);
|
||||
}
|
||||
} else if (el.type === 'line') {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { DocumentField } from '../database/entities/document-field.entity';
|
||||
import { DocumentType } from '../database/entities/document-type.entity';
|
||||
import { PaperlessService } from './paperless.service';
|
||||
import { PostprocessingService } from '../postprocessing/postprocessing.service';
|
||||
import { getErrorMessage, getResponseData } from '../common/error.util';
|
||||
|
||||
@Injectable()
|
||||
export class PaperlessProcessorService {
|
||||
@@ -50,14 +49,13 @@ export class PaperlessProcessorService {
|
||||
);
|
||||
// Postprocessing nach dem Speichern evaluieren
|
||||
await this.postprocessingService.evaluate(updatedDoc || doc);
|
||||
} catch (innerErr: unknown) {
|
||||
} catch (innerErr: any) {
|
||||
this.logger.error(
|
||||
`Fehler bei Dokument ID ${doc.id}: ${getErrorMessage(innerErr)}`,
|
||||
`Fehler bei Dokument ID ${doc.id}: ${innerErr.message}`,
|
||||
);
|
||||
const responseData = getResponseData(innerErr);
|
||||
if (responseData) {
|
||||
if (innerErr.response?.data) {
|
||||
this.logger.error(
|
||||
`Paperless API Response: ${JSON.stringify(responseData)}`,
|
||||
`Paperless API Response: ${JSON.stringify(innerErr.response.data)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ConfigService } from '@nestjs/config';
|
||||
import axios, { type AxiosInstance } from 'axios';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import FormData from 'form-data';
|
||||
import FormData = require('form-data');
|
||||
|
||||
@Injectable()
|
||||
export class PaperlessService {
|
||||
@@ -133,8 +133,8 @@ export class PaperlessService {
|
||||
try {
|
||||
const response = await this.client.patch(`/documents/${id}/`, data);
|
||||
return response.data;
|
||||
} catch (err: unknown) {
|
||||
const body = axios.isAxiosError(err) ? err.response?.data : undefined;
|
||||
} catch (err: any) {
|
||||
const body = err?.response?.data;
|
||||
if (body) {
|
||||
this.logger.error(
|
||||
`Paperless updateDocument(${id}) Fehlerdetails: ${JSON.stringify(body)}`,
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Repository } from 'typeorm';
|
||||
import { ExportTarget } from '../database/entities/export-target.entity';
|
||||
import * as ftp from 'basic-ftp';
|
||||
import { createClient, type WebDAVClient } from 'webdav';
|
||||
import { getErrorMessage } from '../common/error.util';
|
||||
|
||||
@Injectable()
|
||||
export class ExportService {
|
||||
@@ -58,8 +57,8 @@ export class ExportService {
|
||||
};
|
||||
}
|
||||
return { success: true, message: 'Verbindung erfolgreich.' };
|
||||
} catch (err: unknown) {
|
||||
return { success: false, message: getErrorMessage(err) };
|
||||
} catch (err: any) {
|
||||
return { success: false, message: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import { PaperlessService } from '../paperless/paperless.service';
|
||||
import { MailService } from './mail.service';
|
||||
import { ExportService } from './export.service';
|
||||
import axios from 'axios';
|
||||
import { getErrorMessage } from '../common/error.util';
|
||||
|
||||
const CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
|
||||
@@ -92,18 +91,12 @@ export class PostprocessingService {
|
||||
'success',
|
||||
`Aktion ${action.ActionType} erfolgreich`,
|
||||
);
|
||||
} catch (err: unknown) {
|
||||
} catch (err: any) {
|
||||
hasError = true;
|
||||
this.logger.error(
|
||||
`Fehler bei Aktion ${action.Id} (Typ ${action.ActionType}) für Dokument ${doc.id}: ${getErrorMessage(err)}`,
|
||||
);
|
||||
await this.log(
|
||||
rule.Id,
|
||||
action.Id,
|
||||
doc.id,
|
||||
'error',
|
||||
getErrorMessage(err),
|
||||
`Fehler bei Aktion ${action.Id} (Typ ${action.ActionType}) für Dokument ${doc.id}: ${err.message}`,
|
||||
);
|
||||
await this.log(rule.Id, action.Id, doc.id, 'error', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,9 +107,9 @@ export class PostprocessingService {
|
||||
await this.paperlessService.updateDocument(doc.id, {
|
||||
tags: Array.from(currentTags),
|
||||
});
|
||||
} catch (tagErr: unknown) {
|
||||
} catch (tagErr: any) {
|
||||
this.logger.error(
|
||||
`Konnte Error-Tag ${this.errorTagId} nicht setzen für Dokument ${doc.id}: ${getErrorMessage(tagErr)}`,
|
||||
`Konnte Error-Tag ${this.errorTagId} nicht setzen für Dokument ${doc.id}: ${tagErr.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -320,8 +313,8 @@ export class PostprocessingService {
|
||||
const dt = docTypes.find((x: any) => x.id === doc.document_type);
|
||||
doc._documentTypeName = dt?.name ?? '';
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
this.logger.warn(`Konnte Namen nicht auflösen: ${getErrorMessage(err)}`);
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`Konnte Namen nicht auflösen: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,8 +65,8 @@ export class DocumentPipelineService {
|
||||
}
|
||||
|
||||
// 3. OCR auf erster Seite
|
||||
// TODO: OCR-Ergebnis wird aktuell nicht gespeichert/weiterverwendet
|
||||
await this.ocrService.extractTextAsMarkdown(firstPageBuffer);
|
||||
const ocrMarkdown =
|
||||
await this.ocrService.extractTextAsMarkdown(firstPageBuffer);
|
||||
|
||||
// 4. Task in DB erstellen
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import axios from 'axios';
|
||||
import { getErrorMessage } from '../common/error.util';
|
||||
|
||||
@Injectable()
|
||||
export class OcrService {
|
||||
@@ -47,8 +46,8 @@ Antworte nur mit dem extrahierten Markdown-Text, keine Erklärungen.`;
|
||||
`OCR abgeschlossen: ${markdown.length} Zeichen extrahiert`,
|
||||
);
|
||||
return markdown;
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(`Ollama OCR fehlgeschlagen: ${getErrorMessage(error)}`);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Ollama OCR fehlgeschlagen: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,9 +120,7 @@ export class PdfService {
|
||||
for (const imgPath of imagePaths) {
|
||||
try {
|
||||
await fs.unlink(imgPath);
|
||||
} catch {
|
||||
// Datei bereits entfernt o. Ä.: ignorieren
|
||||
}
|
||||
} catch {}
|
||||
dirs.add(path.dirname(imgPath));
|
||||
}
|
||||
for (const dir of dirs) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import sharp from 'sharp';
|
||||
import sharp = require('sharp');
|
||||
import jsQR from 'jsqr';
|
||||
|
||||
export interface QrCodeResult {
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
InboxDocument,
|
||||
type InboxSource,
|
||||
} from '../database/entities/inbox-document.entity';
|
||||
import { getErrorCode, getErrorMessage } from '../common/error.util';
|
||||
|
||||
const STABILITY_MS = 5000;
|
||||
|
||||
@@ -64,7 +63,7 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy {
|
||||
this.logger.log(`Starte Überwachung: ${this.sourceRoot}`);
|
||||
|
||||
this.watcher = chokidar.watch(this.sourceRoot, {
|
||||
ignored: /(^|[/\\])\../,
|
||||
ignored: /(^|[\/\\])\../,
|
||||
persistent: true,
|
||||
ignoreInitial: true,
|
||||
awaitWriteFinish: {
|
||||
@@ -75,9 +74,9 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy {
|
||||
});
|
||||
|
||||
this.watcher
|
||||
.on('add', (filePath: string) => void this.handleNewFile(filePath))
|
||||
.on('add', (filePath: string) => this.handleNewFile(filePath))
|
||||
.on('error', (error: Error) =>
|
||||
this.logger.error(`Watcher Fehler: ${getErrorMessage(error)}`),
|
||||
this.logger.error(`Watcher Fehler: ${error.message}`),
|
||||
);
|
||||
|
||||
this.logger.log('Scanner-Watcher aktiv');
|
||||
@@ -97,10 +96,10 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy {
|
||||
withFileTypes: true,
|
||||
});
|
||||
subdirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
||||
} catch (err: unknown) {
|
||||
} catch (err: any) {
|
||||
if (!silent) {
|
||||
this.logger.warn(
|
||||
`Scanner-Check: Quellverzeichnis nicht lesbar (${this.sourceRoot}): ${getErrorMessage(err)}`,
|
||||
`Scanner-Check: Quellverzeichnis nicht lesbar (${this.sourceRoot}): ${err.message}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
@@ -112,10 +111,10 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy {
|
||||
let files: string[];
|
||||
try {
|
||||
files = await fs.readdir(dir);
|
||||
} catch (err: unknown) {
|
||||
} catch (err: any) {
|
||||
if (!silent) {
|
||||
this.logger.warn(
|
||||
`Scanner-Check: ${dir} nicht lesbar: ${getErrorMessage(err)}`,
|
||||
`Scanner-Check: ${dir} nicht lesbar: ${err.message}`,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
@@ -152,8 +151,8 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy {
|
||||
this.isPeriodicScanning = true;
|
||||
try {
|
||||
await this.initialScan(true);
|
||||
} catch (err: unknown) {
|
||||
this.logger.error(`Periodic Scan Fehler: ${getErrorMessage(err)}`);
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Periodic Scan Fehler: ${err.message}`);
|
||||
} finally {
|
||||
this.isPeriodicScanning = false;
|
||||
}
|
||||
@@ -222,14 +221,14 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
try {
|
||||
await this.barcodeScanner.scanAndMatch(doc);
|
||||
} catch (err: unknown) {
|
||||
} catch (err: any) {
|
||||
this.logger.warn(
|
||||
`Barcode-Scan nach Move fehlgeschlagen (${id}): ${getErrorMessage(err)}`,
|
||||
`Barcode-Scan nach Move fehlgeschlagen (${id}): ${err.message}`,
|
||||
);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
} catch (err: any) {
|
||||
this.logger.error(
|
||||
`Übernahme fehlgeschlagen für ${filePath}: ${getErrorMessage(err)}`,
|
||||
`Übernahme fehlgeschlagen für ${filePath}: ${err.message}`,
|
||||
);
|
||||
} finally {
|
||||
this.processing.delete(filePath);
|
||||
@@ -242,10 +241,8 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy {
|
||||
pending = await this.documentRepo.find({
|
||||
where: [{ PageCount: 0 }, { QrCodes: IsNull() }],
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
this.logger.warn(
|
||||
`Backfill: DB-Query fehlgeschlagen: ${getErrorMessage(err)}`,
|
||||
);
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`Backfill: DB-Query fehlgeschlagen: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -254,10 +251,8 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy {
|
||||
try {
|
||||
const didScan = await this.barcodeScanner.ensureScanned(doc);
|
||||
if (didScan) scanned += 1;
|
||||
} catch (err: unknown) {
|
||||
this.logger.warn(
|
||||
`Backfill fehlgeschlagen (${doc.Id}): ${getErrorMessage(err)}`,
|
||||
);
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`Backfill fehlgeschlagen (${doc.Id}): ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,8 +267,8 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy {
|
||||
try {
|
||||
await fs.rename(src, dest);
|
||||
return;
|
||||
} catch (err: unknown) {
|
||||
if (getErrorCode(err) !== 'EXDEV') throw err;
|
||||
} catch (err: any) {
|
||||
if (err.code !== 'EXDEV') throw err;
|
||||
}
|
||||
// Cross-device: copy + unlink. Wenn unlink scheitert, Kopie zurückrollen,
|
||||
// damit ein kaputter Mount nicht bei jedem Neustart Duplikate produziert.
|
||||
|
||||
@@ -22,7 +22,10 @@ import { PaperlessService } from '../paperless/paperless.service';
|
||||
import { Client } from '../database/entities/client.entity';
|
||||
import { Setting } from '../database/entities/setting.entity';
|
||||
import { CorrespondentSetting } from '../database/entities/correspondent-setting.entity';
|
||||
import { InboxPostprocessingAction } from '../database/entities/inbox-postprocessing-action.entity';
|
||||
import {
|
||||
InboxPostprocessingAction,
|
||||
type InboxActionType,
|
||||
} from '../database/entities/inbox-postprocessing-action.entity';
|
||||
import { ExportService } from '../postprocessing/export.service';
|
||||
import { RequirePermissions } from '../auth/permissions.decorator';
|
||||
import { Permission } from '../auth/permissions.enum';
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { Controller, Get, Request } from '@nestjs/common';
|
||||
import { StatsService } from './stats.service';
|
||||
import type { AuthenticatedRequest } from '../auth/authenticated-request';
|
||||
|
||||
@Controller('api/stats')
|
||||
export class StatsController {
|
||||
constructor(private readonly statsService: StatsService) {}
|
||||
|
||||
@Get('counts')
|
||||
async getCounts(@Request() req: AuthenticatedRequest) {
|
||||
return this.statsService.getDashboardCounts(
|
||||
req.user?.preferredUsername ?? undefined,
|
||||
);
|
||||
async getCounts(@Request() req: any) {
|
||||
return this.statsService.getDashboardCounts(req.user?.preferredUsername);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,45 +7,38 @@ import {
|
||||
Put,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
UserSettingsService,
|
||||
type UpdateUserSettingsInput,
|
||||
} from './user-settings.service';
|
||||
import { UserSettingsService } from './user-settings.service';
|
||||
import { RequirePermissions } from '../auth/permissions.decorator';
|
||||
import { Permission } from '../auth/permissions.enum';
|
||||
import type { AuthenticatedRequest } from '../auth/authenticated-request';
|
||||
|
||||
@Controller('api/user-settings')
|
||||
export class UserSettingsController {
|
||||
constructor(private readonly userSettingsService: UserSettingsService) {}
|
||||
|
||||
@Get()
|
||||
async getSettings(@Request() req: AuthenticatedRequest) {
|
||||
async getSettings(@Request() req: any) {
|
||||
return this.userSettingsService.getSettings(
|
||||
req.user!.userId,
|
||||
req.user!.email,
|
||||
req.user!.preferredUsername ?? undefined,
|
||||
req.user!.groups,
|
||||
req.user.userId,
|
||||
req.user.email,
|
||||
req.user.preferredUsername,
|
||||
req.user.groups,
|
||||
);
|
||||
}
|
||||
|
||||
@Put()
|
||||
async updateSettings(
|
||||
@Request() req: AuthenticatedRequest,
|
||||
@Body() body: UpdateUserSettingsInput,
|
||||
) {
|
||||
async updateSettings(@Request() req: any, @Body() body: any) {
|
||||
return this.userSettingsService.updateSettings(
|
||||
req.user!.userId,
|
||||
req.user.userId,
|
||||
body,
|
||||
req.user!.email,
|
||||
req.user!.preferredUsername ?? undefined,
|
||||
req.user!.groups,
|
||||
req.user.email,
|
||||
req.user.preferredUsername,
|
||||
req.user.groups,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('senders')
|
||||
async getSenders(@Request() req: AuthenticatedRequest) {
|
||||
return this.userSettingsService.getAvailableSenders(req.user!.userId);
|
||||
async getSenders(@Request() req: any) {
|
||||
return this.userSettingsService.getAvailableSenders(req.user.userId);
|
||||
}
|
||||
|
||||
@Post('test-smtp')
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Repository } from 'typeorm';
|
||||
import * as nodemailer from 'nodemailer';
|
||||
import * as crypto from 'crypto';
|
||||
import { UserSettings } from '../database/entities/user-settings.entity';
|
||||
import { getErrorMessage } from '../common/error.util';
|
||||
|
||||
const ALGORITHM = 'aes-256-gcm';
|
||||
|
||||
@@ -23,20 +22,6 @@ export interface UserSettingsDto {
|
||||
dailyDigestEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateUserSettingsInput {
|
||||
smtpHost?: string | null;
|
||||
smtpPort?: number | null;
|
||||
smtpSecure?: boolean;
|
||||
smtpUser?: string | null;
|
||||
smtpPass?: string | null;
|
||||
smtpFrom?: string | null;
|
||||
smtpFromName?: string | null;
|
||||
mailSignatureHtml?: string | null;
|
||||
defaultLabelTemplateId?: number | null;
|
||||
emailRecipientHistory?: string[] | null;
|
||||
dailyDigestEnabled?: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class UserSettingsService {
|
||||
private readonly logger = new Logger(UserSettingsService.name);
|
||||
@@ -107,7 +92,19 @@ export class UserSettingsService {
|
||||
|
||||
async updateSettings(
|
||||
userId: string,
|
||||
data: UpdateUserSettingsInput,
|
||||
data: {
|
||||
smtpHost?: string | null;
|
||||
smtpPort?: number | null;
|
||||
smtpSecure?: boolean;
|
||||
smtpUser?: string | null;
|
||||
smtpPass?: string | null;
|
||||
smtpFrom?: string | null;
|
||||
smtpFromName?: string | null;
|
||||
mailSignatureHtml?: string | null;
|
||||
defaultLabelTemplateId?: number | null;
|
||||
emailRecipientHistory?: string[] | null;
|
||||
dailyDigestEnabled?: boolean;
|
||||
},
|
||||
email?: string,
|
||||
preferredUsername?: string,
|
||||
groups?: string[],
|
||||
@@ -163,8 +160,8 @@ export class UserSettingsService {
|
||||
try {
|
||||
await transporter.verify();
|
||||
return { ok: true };
|
||||
} catch (err: unknown) {
|
||||
return { ok: false, error: getErrorMessage(err) };
|
||||
} catch (err: any) {
|
||||
return { ok: false, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ export class WebhookController {
|
||||
@Public()
|
||||
@Post('paperless')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
handlePaperlessWebhook(@Body() payload: PaperlessWebhookPayload): {
|
||||
status: string;
|
||||
} {
|
||||
async handlePaperlessWebhook(
|
||||
@Body() payload: PaperlessWebhookPayload,
|
||||
): Promise<{ status: string }> {
|
||||
this.logger.log(
|
||||
`Webhook empfangen: action=${payload.action}, document=${payload.document_id}`,
|
||||
);
|
||||
|
||||
@@ -19,19 +19,5 @@ export default defineConfig([
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
rules: {
|
||||
// any-getriebene Regel als Warnung (untypisierte Paperless-NGX-API-Daten)
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
// Fast-Refresh-Hinweise sind Dev-Komfort, kein Build-Blocker
|
||||
'react-refresh/only-export-components': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrors: 'none',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
@@ -44,7 +44,7 @@ export const emailsApi = {
|
||||
api.post<{ message: string }>('/api/emails/fetch').then((r) => r.data),
|
||||
|
||||
checkAttachments: (includeProcessed = false) =>
|
||||
api.post<{ updatedCount: number; idsUpdated: number }>('/api/emails/check-attachments', { includeProcessed }).then((r) => r.data),
|
||||
api.post<{ updatedCount: number; idsUpdated: number; movedToImportiert: number }>('/api/emails/check-attachments', { includeProcessed }).then((r) => r.data),
|
||||
|
||||
updateStatus: (id: number, status: number) =>
|
||||
api.patch<{ message?: string }>(`/api/emails/${id}/status`, { status }).then((r) => r.data),
|
||||
|
||||
@@ -121,7 +121,7 @@ export default function DocumentEditModal({ documentId, document, open, onClose,
|
||||
useEffect(() => {
|
||||
if (open && document) {
|
||||
|
||||
const customFieldsObj: any = {};
|
||||
let customFieldsObj: any = {};
|
||||
if (document.customFields) {
|
||||
document.customFields.forEach(cf => {
|
||||
customFieldsObj[`cf_${cf.field}`] = cf.value;
|
||||
|
||||
@@ -322,9 +322,7 @@ export default function MailImportWizard({ visible, onClose, onSuccess, email, a
|
||||
try {
|
||||
const status = await emailImportApi.getJobStatus(jobId);
|
||||
if (status?.message) setImportStatus(status.message);
|
||||
} catch {
|
||||
// Status-Abfrage fehlgeschlagen: nächsten Poll abwarten
|
||||
}
|
||||
} catch {}
|
||||
}, 1500);
|
||||
try {
|
||||
const finalData = [];
|
||||
|
||||
@@ -172,6 +172,7 @@ export default function MailpostfachPage() {
|
||||
const parts = [];
|
||||
if (result.updatedCount > 0) parts.push(`${result.updatedCount} E-Mail(s) aktualisiert`);
|
||||
if (result.idsUpdated > 0) parts.push(`${result.idsUpdated} Paperless-ID(s) ergänzt`);
|
||||
if (result.movedToImportiert > 0) parts.push(`${result.movedToImportiert} E-Mail(s) in „importiert" verschoben`);
|
||||
message.success(parts.length > 0 ? parts.join(', ') + '.' : 'Keine Änderungen.');
|
||||
if (result.updatedCount > 0 || result.idsUpdated > 0) await loadData();
|
||||
} catch {
|
||||
|
||||
@@ -441,10 +441,9 @@ function DocTypeFieldsTable({ docTypeId }: { docTypeId: number }) {
|
||||
case 1: return <Tag>Absender</Tag>;
|
||||
case 2: return <Tag>Belegdatum</Tag>;
|
||||
case 3: return <Tag>Ablagenummer</Tag>;
|
||||
case 4: {
|
||||
case 4:
|
||||
const cf = customFields.find(c => c.id === record.TypeIndex);
|
||||
return <Tag color="blue">{cf ? cf.name : `CF #${record.TypeIndex}`}</Tag>;
|
||||
}
|
||||
case 5: return <Tag>Titel</Tag>;
|
||||
default: return <Tag>Unbekannt ({record.Type})</Tag>;
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ export function useAuthUrl(url: string | null | undefined): string | null {
|
||||
|
||||
useEffect(() => {
|
||||
if (!url) {
|
||||
// Reset des Blob-URLs, wenn keine Quelle (mehr) vorhanden ist
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setBlobUrl(null);
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user