Revert "fix: resolve all ESLint errors in backend and frontend"
Build and Push Multi-Platform Images / build-and-push (push) Successful in 19s

This reverts commit 07dfd7e840.
This commit is contained in:
2026-06-16 16:19:11 +02:00
parent 14c11bf718
commit 66aeab282c
43 changed files with 204 additions and 399 deletions
+1 -19
View File
@@ -28,26 +28,8 @@ export default tseslint.config(
rules: { rules: {
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn', '@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-argument': 'warn',
'@typescript-eslint/no-unsafe-member-access': 'warn', "prettier/prettier": ["error", { endOfLine: "auto" }],
'@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' }],
}, },
}, },
); );
@@ -59,7 +59,7 @@ export class AgrarmonitorPollingService implements OnModuleInit {
} }
@Cron(process.env['AGRARMONITOR_POLLING_CRON'] || '0 */30 * * * *') @Cron(process.env['AGRARMONITOR_POLLING_CRON'] || '0 */30 * * * *')
scheduledPolling() { async scheduledPolling() {
if (!process.env['AGRARMONITOR_POLLING_CRON']) return; if (!process.env['AGRARMONITOR_POLLING_CRON']) return;
this.runPolling().catch((err) => this.runPolling().catch((err) =>
this.logger.error('Cron-Polling-Fehler:', 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 * * * * *') @Cron(process.env['AGRARMONITOR_UPLOAD_CHECK_CRON'] || '0 * * * * *')
scheduledUploadCheck() { async scheduledUploadCheck() {
if (!process.env['AGRARMONITOR_UPLOAD_CHECK_CRON']) return; if (!process.env['AGRARMONITOR_UPLOAD_CHECK_CRON']) return;
this.processVerarbeiteteDocuments().catch((err) => this.processVerarbeiteteDocuments().catch((err) =>
this.logger.error('Cron-Upload-Check-Fehler:', err), this.logger.error('Cron-Upload-Check-Fehler:', err),
@@ -6,7 +6,6 @@ import {
AesGcmCookieEncryptor, AesGcmCookieEncryptor,
type AgrarmonitorConnectorResult, type AgrarmonitorConnectorResult,
} from 'agrarmonitor-connector'; } from 'agrarmonitor-connector';
import { getErrorMessage } from '../common/error.util';
export interface AgrarmonitorStatusDto { export interface AgrarmonitorStatusDto {
connected: boolean; connected: boolean;
@@ -96,13 +95,13 @@ export class AgrarmonitorService {
registriert: registrierungStatus.registriert, registriert: registrierungStatus.registriert,
freigeschaltet: freigeschaltetStatus.freigeschaltet, freigeschaltet: freigeschaltetStatus.freigeschaltet,
}; };
} catch (err: unknown) { } catch (err: any) {
this.client = null; this.client = null;
return { return {
connected: false, connected: false,
registriert: null, registriert: null,
freigeschaltet: 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;
}
+10 -12
View File
@@ -4,15 +4,6 @@ import { Strategy, ExtractJwt } from 'passport-jwt';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { passportJwtSecret } from 'jwks-rsa'; import { passportJwtSecret } from 'jwks-rsa';
import { mapGroupsToPermissions } from './permissions.enum'; 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() @Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
@@ -33,12 +24,19 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
}); });
} }
validate(payload: JwtPayload): AuthenticatedUser { validate(payload: any): {
const groups = payload.groups ?? []; userId: string;
email: string;
name: string;
preferredUsername: string | null;
groups: string[];
permissions: any[];
} {
const groups = payload.groups || [];
return { return {
userId: payload.sub, userId: payload.sub,
email: payload.email, email: payload.email,
name: payload.name || payload.preferred_username || '', name: payload.name || payload.preferred_username,
preferredUsername: payload.preferred_username ?? null, preferredUsername: payload.preferred_username ?? null,
groups: groups, groups: groups,
permissions: mapGroupsToPermissions(groups), permissions: mapGroupsToPermissions(groups),
@@ -2,7 +2,7 @@ import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import * as fs from 'fs/promises'; import * as fs from 'fs/promises';
import sharp from 'sharp'; import sharp = require('sharp');
import { PdfService } from '../preprocessing/pdf.service'; import { PdfService } from '../preprocessing/pdf.service';
import { QrCodeService } from '../preprocessing/qr-code.service'; import { QrCodeService } from '../preprocessing/qr-code.service';
import { import {
@@ -18,7 +18,6 @@ import {
applyTemplate, applyTemplate,
buildVariables, buildVariables,
} from '../inbox-postprocessor/variable-resolver'; } from '../inbox-postprocessor/variable-resolver';
import { getErrorMessage } from '../common/error.util';
export interface MatchedBarcode { export interface MatchedBarcode {
page: number; page: number;
@@ -57,9 +56,9 @@ export class BarcodeScannerService implements OnApplicationBootstrap {
let rows: BarcodeTemplate[]; let rows: BarcodeTemplate[];
try { try {
rows = await this.templateRepo.find(); rows = await this.templateRepo.find();
} catch (err: unknown) { } catch (err: any) {
this.logger.warn( this.logger.warn(
`Template-Migration: Query fehlgeschlagen: ${getErrorMessage(err)}`, `Template-Migration: Query fehlgeschlagen: ${err.message}`,
); );
return; return;
} }
@@ -95,9 +94,9 @@ export class BarcodeScannerService implements OnApplicationBootstrap {
doc.IsScanned = true; doc.IsScanned = true;
try { try {
await this.documentRepo.save(doc); await this.documentRepo.save(doc);
} catch (err: unknown) { } catch (err: any) {
this.logger.warn( this.logger.warn(
`Scan-Ergebnis konnte nicht gespeichert werden (${doc.Id}): ${getErrorMessage(err)}`, `Scan-Ergebnis konnte nicht gespeichert werden (${doc.Id}): ${err.message}`,
); );
} }
@@ -193,9 +192,9 @@ export class BarcodeScannerService implements OnApplicationBootstrap {
qrCodes.push({ page: i + 1, value: qr.data }); qrCodes.push({ page: i + 1, value: qr.data });
} }
} }
} catch (err: unknown) { } catch (err: any) {
this.logger.warn( this.logger.warn(
`QR-Scan fehlgeschlagen (${pdfPath}, Seite ${i + 1}): ${getErrorMessage(err)}`, `QR-Scan fehlgeschlagen (${pdfPath}, Seite ${i + 1}): ${err.message}`,
); );
} }
} }
@@ -204,10 +203,8 @@ export class BarcodeScannerService implements OnApplicationBootstrap {
await this.pageCache.generate(documentId, images); await this.pageCache.generate(documentId, images);
return { qrCodes, pageCount: images.length }; return { qrCodes, pageCount: images.length };
} catch (err: unknown) { } catch (err: any) {
this.logger.warn( this.logger.warn(`Kein QR-Scan möglich für ${pdfPath}: ${err.message}`);
`Kein QR-Scan möglich für ${pdfPath}: ${getErrorMessage(err)}`,
);
return { qrCodes: [], pageCount: 0 }; return { qrCodes: [], pageCount: 0 };
} finally { } finally {
await this.pdfService.cleanup(images); await this.pdfService.cleanup(images);
@@ -283,10 +280,8 @@ export class BarcodeScannerService implements OnApplicationBootstrap {
let docs: InboxDocument[]; let docs: InboxDocument[];
try { try {
docs = await this.documentRepo.find(); docs = await this.documentRepo.find();
} catch (err: unknown) { } catch (err: any) {
this.logger.warn( this.logger.warn(`Rescan: DB-Query fehlgeschlagen: ${err.message}`);
`Rescan: DB-Query fehlgeschlagen: ${getErrorMessage(err)}`,
);
return { scanned: 0, failed: 0 }; return { scanned: 0, failed: 0 };
} }
if (docs.length === 0) return { scanned: 0, failed: 0 }; if (docs.length === 0) return { scanned: 0, failed: 0 };
@@ -311,10 +306,8 @@ export class BarcodeScannerService implements OnApplicationBootstrap {
doc.PageCount = pageCount; doc.PageCount = pageCount;
await this.documentRepo.save(doc); await this.documentRepo.save(doc);
scanned++; scanned++;
} catch (err: unknown) { } catch (err: any) {
this.logger.warn( this.logger.warn(`Rescan fehlgeschlagen für ${doc.Id}: ${err.message}`);
`Rescan fehlgeschlagen für ${doc.Id}: ${getErrorMessage(err)}`,
);
failed++; failed++;
} }
} }
@@ -3,7 +3,6 @@ import { ConfigService } from '@nestjs/config';
import * as path from 'path'; import * as path from 'path';
import * as fs from 'fs/promises'; import * as fs from 'fs/promises';
import sharp from 'sharp'; import sharp from 'sharp';
import { getErrorMessage } from '../common/error.util';
const THUMBNAIL_WIDTH = 180; const THUMBNAIL_WIDTH = 180;
@@ -55,9 +54,9 @@ export class PageCacheService {
.resize({ width: THUMBNAIL_WIDTH }) .resize({ width: THUMBNAIL_WIDTH })
.png() .png()
.toFile(thumbDest); .toFile(thumbDest);
} catch (err: unknown) { } catch (err: any) {
this.logger.warn( 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`); const to = path.join(dir, `page-${n - 1}.${variant}.png`);
try { try {
await fs.rename(from, to); await fs.rename(from, to);
} catch (err: unknown) { } catch (err: any) {
this.logger.warn( 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 { Controller, Post, Request, HttpCode } from '@nestjs/common';
import { DailyDigestService } from './daily-digest.service'; import { DailyDigestService } from './daily-digest.service';
import type { AuthenticatedRequest } from '../auth/authenticated-request';
import { getErrorMessage } from '../common/error.util';
@Controller('api/daily-digest') @Controller('api/daily-digest')
export class DailyDigestController { export class DailyDigestController {
@@ -9,8 +7,8 @@ export class DailyDigestController {
@Post('send-now') @Post('send-now')
@HttpCode(200) @HttpCode(200)
async sendNow(@Request() req: AuthenticatedRequest) { async sendNow(@Request() req: any) {
const { userId, email, preferredUsername, groups } = req.user!; const { userId, email, preferredUsername, groups } = req.user;
if (!email) { if (!email) {
return { return {
ok: false, ok: false,
@@ -21,12 +19,12 @@ export class DailyDigestController {
await this.dailyDigestService.sendDigestForUser( await this.dailyDigestService.sendDigestForUser(
userId, userId,
email, email,
preferredUsername ?? undefined, preferredUsername,
groups, groups,
); );
return { ok: true }; return { ok: true };
} catch (err: unknown) { } catch (err: any) {
return { ok: false, error: getErrorMessage(err) }; return { ok: false, error: err.message };
} }
} }
} }
@@ -19,7 +19,6 @@ import { Email } from '../database/entities/email.entity';
import { Attachment } from '../database/entities/attachment.entity'; import { Attachment } from '../database/entities/attachment.entity';
import { Content } from '../database/entities/content.entity'; import { Content } from '../database/entities/content.entity';
import { isERechnung } from './zugferd.util'; import { isERechnung } from './zugferd.util';
import { getErrorMessage, getErrorStack } from '../common/error.util';
@Injectable() @Injectable()
export class EmailDownloadService { export class EmailDownloadService {
@@ -46,10 +45,10 @@ export class EmailDownloadService {
this.running = true; this.running = true;
try { try {
await this.fetchAndStore(); await this.fetchAndStore();
} catch (err: unknown) { } catch (err: any) {
this.logger.error( this.logger.error(
`Fehler im E-Mail-Download-Job: ${getErrorMessage(err)}`, `Fehler im E-Mail-Download-Job: ${err.message}`,
getErrorStack(err), err.stack,
); );
} finally { } finally {
this.running = false; this.running = false;
@@ -117,10 +116,10 @@ export class EmailDownloadService {
} }
await this.processMessage(msg); await this.processMessage(msg);
} catch (err: unknown) { } catch (err: any) {
this.logger.error( this.logger.error(
`Fehler beim Abrufen der Nachricht ${messageId}: ${getErrorMessage(err)}`, `Fehler beim Abrufen der Nachricht ${messageId}: ${err.message}`,
getErrorStack(err), err.stack,
); );
} }
} }
@@ -158,7 +157,7 @@ export class EmailDownloadService {
}> = []; }> = [];
for (const att of parsed.attachments) { for (const att of parsed.attachments) {
const entry = this.buildAttachment(att); const entry = await this.buildAttachment(att);
if (entry) attachmentsToPersist.push(entry); if (entry) attachmentsToPersist.push(entry);
} }
@@ -215,9 +214,9 @@ export class EmailDownloadService {
await this.pdfService.cleanup(images); await this.pdfService.cleanup(images);
await fs.unlink(tempPdfPath).catch(() => {}); await fs.unlink(tempPdfPath).catch(() => {});
} catch (err: unknown) { } catch (err: any) {
this.logger.warn( 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}`,
); );
} }
} }
@@ -263,9 +262,9 @@ export class EmailDownloadService {
return { processed, failed }; return { processed, failed };
} }
private buildAttachment( private async buildAttachment(
att: MailAttachment, att: MailAttachment,
): { attachment: Attachment; buffer: Buffer } | null { ): Promise<{ attachment: Attachment; buffer: Buffer } | null> {
const buffer = att.content; const buffer = att.content;
if (!Buffer.isBuffer(buffer) || buffer.length === 0) return null; 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 { EmailPageCacheService } from './email-page-cache.service';
import { RequirePermissions } from '../auth/permissions.decorator'; import { RequirePermissions } from '../auth/permissions.decorator';
import { Permission } from '../auth/permissions.enum'; import { Permission } from '../auth/permissions.enum';
import { getErrorMessage } from '../common/error.util';
@Controller('api/email-import') @Controller('api/email-import')
export class EmailImportController { export class EmailImportController {
@@ -132,14 +131,9 @@ export class EmailImportController {
`inline; filename="preview-${attachmentId}.pdf"`, `inline; filename="preview-${attachmentId}.pdf"`,
); );
res.send(pdfBuffer); res.send(pdfBuffer);
} catch (err: unknown) { } catch (err: any) {
this.logger.error( this.logger.error(`Error generating print preview: ${err.message}`);
`Error generating print preview: ${getErrorMessage(err)}`, throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR);
);
throw new HttpException(
getErrorMessage(err),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} }
} }
@@ -191,12 +185,9 @@ export class EmailImportController {
try { try {
const result = await this.importService.executeImport(importData); const result = await this.importService.executeImport(importData);
return result; return result;
} catch (err: unknown) { } catch (err: any) {
this.logger.error(`Error executing import: ${getErrorMessage(err)}`); this.logger.error(`Error executing import: ${err.message}`);
throw new HttpException( throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR);
getErrorMessage(err),
HttpStatus.INTERNAL_SERVER_ERROR,
);
} }
} }
} }
@@ -17,7 +17,6 @@ import * as path from 'path';
import * as os from 'os'; import * as os from 'os';
import * as fs from 'fs/promises'; import * as fs from 'fs/promises';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import { getErrorMessage } from '../common/error.util';
@Injectable() @Injectable()
export class EmailImportService { export class EmailImportService {
@@ -81,9 +80,9 @@ export class EmailImportService {
await this.attachmentRepo.save(attachment); await this.attachmentRepo.save(attachment);
await this.pdfService.cleanup(images); await this.pdfService.cleanup(images);
} catch (err: unknown) { } catch (err: any) {
this.logger.warn( 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 { } finally {
await fs.unlink(tempPdfPath).catch(() => {}); await fs.unlink(tempPdfPath).catch(() => {});
@@ -157,12 +156,11 @@ export class EmailImportService {
this.logger.debug(`Received Belegnummer: ${result}`); this.logger.debug(`Received Belegnummer: ${result}`);
return String(result); return String(result);
} catch (error: unknown) { } catch (error: any) {
const axiosErr = axios.isAxiosError(error) ? error : undefined; const status = error.response?.status || 'UNKNOWN';
const status = axiosErr?.response?.status ?? 'UNKNOWN'; const detail = error.response?.data
const detail = axiosErr?.response?.data ? JSON.stringify(error.response.data)
? JSON.stringify(axiosErr.response.data) : error.message;
: getErrorMessage(error);
this.logger.error( this.logger.error(
`Failed to fetch Belegnummer from ${url}. Status: ${status}, Detail: ${detail}`, `Failed to fetch Belegnummer from ${url}. Status: ${status}, Detail: ${detail}`,
); );
@@ -193,9 +191,9 @@ export class EmailImportService {
`Releasing Belegnummer: ${cleanNumber} (original: ${number}) via ${url}`, `Releasing Belegnummer: ${cleanNumber} (original: ${number}) via ${url}`,
); );
await axios.get(url); await axios.get(url);
} catch (error: unknown) { } catch (error: any) {
this.logger.error( this.logger.error(
`Failed to release Belegnummer at ${url}: ${getErrorMessage(error)}`, `Failed to release Belegnummer at ${url}: ${error.message}`,
); );
} }
} }
@@ -217,9 +215,9 @@ export class EmailImportService {
`Setting Belegnummer: ${cleanNumber} (original: ${number}) via ${url}`, `Setting Belegnummer: ${cleanNumber} (original: ${number}) via ${url}`,
); );
await axios.get(url); await axios.get(url);
} catch (error: unknown) { } catch (error: any) {
this.logger.error( this.logger.error(
`Failed to set Belegnummer at ${url}: ${getErrorMessage(error)}`, `Failed to set Belegnummer at ${url}: ${error.message}`,
); );
throw new HttpException( throw new HttpException(
'Fehler beim Setzen der Belegnummer', 'Fehler beim Setzen der Belegnummer',
@@ -612,9 +610,7 @@ export class EmailImportService {
docId = statusObj.related_document; docId = statusObj.related_document;
break; break;
} }
} catch { } catch (e) {}
// Task-Status nicht parsebar: nächsten Versuch abwarten
}
} }
if (docId) { if (docId) {
@@ -633,10 +629,7 @@ export class EmailImportService {
// Confirm Belegnummer if used // Confirm Belegnummer if used
if (att.belegnummer && att.barcode?.nummer) { if (att.belegnummer && att.barcode?.nummer) {
await this.setBelegnummer(data.emailDate, att.barcode.nummer).catch( await this.setBelegnummer(data.emailDate, att.barcode.nummer).catch(
(e) => (e) => this.logger.warn(`Failed to set Belegnummer: ${e.message}`),
this.logger.warn(
`Failed to set Belegnummer: ${getErrorMessage(e)}`,
),
); );
} }
@@ -3,7 +3,6 @@ import { ConfigService } from '@nestjs/config';
import * as path from 'path'; import * as path from 'path';
import * as fs from 'fs/promises'; import * as fs from 'fs/promises';
import sharp from 'sharp'; import sharp from 'sharp';
import { getErrorMessage } from '../common/error.util';
const THUMBNAIL_WIDTH = 180; const THUMBNAIL_WIDTH = 180;
@@ -56,9 +55,9 @@ export class EmailPageCacheService {
.resize({ width: THUMBNAIL_WIDTH }) .resize({ width: THUMBNAIL_WIDTH })
.png() .png()
.toFile(thumbDest); .toFile(thumbDest);
} catch (err: unknown) { } catch (err: any) {
this.logger.warn( 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}`,
); );
} }
} }
@@ -19,7 +19,6 @@ import { Content } from '../database/entities/content.entity';
import { PaperlessService } from '../paperless/paperless.service'; import { PaperlessService } from '../paperless/paperless.service';
import { RequirePermissions } from '../auth/permissions.decorator'; import { RequirePermissions } from '../auth/permissions.decorator';
import { Permission } from '../auth/permissions.enum'; import { Permission } from '../auth/permissions.enum';
import { getErrorMessage, getErrorStack } from '../common/error.util';
@Controller('api/emails') @Controller('api/emails')
export class EmailController { export class EmailController {
@@ -174,10 +173,10 @@ export class EmailController {
); );
} }
} }
} catch (err: unknown) { } catch (err: any) {
this.logger.error( this.logger.error(
`Fehler bei Checksummen-Prüfung für Attachment ${attachment.Id}: ${getErrorMessage(err)}`, `Fehler bei Checksummen-Prüfung für Attachment ${attachment.Id}: ${err.message}`,
getErrorStack(err), err.stack,
); );
} }
} }
@@ -204,10 +203,10 @@ export class EmailController {
`Prüfung abgeschlossen. ${updatedCount} E-Mails aktualisiert, ${idsUpdated} Paperless-IDs ergänzt, ${skippedCount} übersprungen.`, `Prüfung abgeschlossen. ${updatedCount} E-Mails aktualisiert, ${idsUpdated} Paperless-IDs ergänzt, ${skippedCount} übersprungen.`,
); );
return { updatedCount, idsUpdated }; return { updatedCount, idsUpdated };
} catch (error: unknown) { } catch (error: any) {
this.logger.error( this.logger.error(
`Kritischer Fehler bei checkAttachments: ${getErrorMessage(error)}`, `Kritischer Fehler bei checkAttachments: ${error.message}`,
getErrorStack(error), error.stack,
); );
throw error; throw error;
} }
@@ -48,7 +48,7 @@ export class FreigabeService {
try { try {
const result = await this.paperlessService.getDocuments(params); const result = await this.paperlessService.getDocuments(params);
return result; return result;
} catch { } catch (err: any) {
// Fallback: Paperless unterstützt den custom_fields-Filter möglicherweise nicht // Fallback: Paperless unterstützt den custom_fields-Filter möglicherweise nicht
// In diesem Fall alle Belege laden und client-seitig filtern // In diesem Fall alle Belege laden und client-seitig filtern
this.logger.warn( this.logger.warn(
@@ -18,7 +18,6 @@ import {
extractSectionToTemp, extractSectionToTemp,
} from './edit-applier'; } from './edit-applier';
import { applyTemplate, buildVariables } from './variable-resolver'; import { applyTemplate, buildVariables } from './variable-resolver';
import { getErrorMessage } from '../common/error.util';
function parseFlexDate(s: string): Date | null { function parseFlexDate(s: string): Date | null {
if (!s) return null; if (!s) return null;
@@ -277,16 +276,16 @@ export class InboxPostprocessorService {
abortProcessing = true; abortProcessing = true;
break; break;
} }
} catch (err: unknown) { } catch (err: any) {
this.logger.error( 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({ results.push({
sectionIndex: currentSectionIndex, sectionIndex: currentSectionIndex,
actionId: action.Id, actionId: action.Id,
actionType: action.ActionType, actionType: action.ActionType,
ok: false, ok: false,
message: getErrorMessage(err), message: err.message,
}); });
} }
} else { } else {
@@ -304,16 +303,16 @@ export class InboxPostprocessorService {
actionType: action.ActionType, actionType: action.ActionType,
ok: true, ok: true,
}); });
} catch (err: unknown) { } catch (err: any) {
this.logger.error( 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({ results.push({
sectionIndex: currentSectionIndex, sectionIndex: currentSectionIndex,
actionId: action.Id, actionId: action.Id,
actionType: action.ActionType, actionType: action.ActionType,
ok: false, 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; if (gv !== undefined) vars[`barcode.${sanitizeKey(g)}`] = gv;
} }
} }
} catch { } catch {}
// Ungültige Regex/Barcode-Daten: Variablen bleiben ungesetzt
}
} }
return vars; return vars;
@@ -3,7 +3,6 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In } from 'typeorm'; import { Repository, In } from 'typeorm';
import { Client } from '../database/entities/client.entity'; import { Client } from '../database/entities/client.entity';
import { UserClient } from '../database/entities/user-client.entity'; import { UserClient } from '../database/entities/user-client.entity';
import type { AuthenticatedRequest } from '../auth/authenticated-request';
@Controller('api/clients') @Controller('api/clients')
export class ClientsController { export class ClientsController {
@@ -16,8 +15,8 @@ export class ClientsController {
) {} ) {}
@Get() @Get()
async getMyClients(@Request() req: AuthenticatedRequest) { async getMyClients(@Request() req: any) {
const userId = req.user!.userId; const userId = req.user.userId;
const mappings = await this.userClientRepo.find({ const mappings = await this.userClientRepo.find({
where: { UserId: userId }, where: { UserId: userId },
}); });
@@ -10,7 +10,6 @@ import {
type InboxSource, type InboxSource,
type StoredQrCode, type StoredQrCode,
} from '../database/entities/inbox-document.entity'; } from '../database/entities/inbox-document.entity';
import { getErrorMessage, getErrorCode } from '../common/error.util';
import { PageCacheService } from '../barcode/page-cache.service'; import { PageCacheService } from '../barcode/page-cache.service';
interface LegacyScanRow { interface LegacyScanRow {
@@ -42,10 +41,10 @@ export class InboxMigrationService implements OnApplicationBootstrap {
withFileTypes: true, withFileTypes: true,
}); });
subdirs = entries.filter((e) => e.isDirectory()).map((e) => e.name); subdirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
} catch (err: unknown) { } catch (err: any) {
if (getErrorCode(err) !== 'ENOENT') { if (err.code !== 'ENOENT') {
this.logger.warn( this.logger.warn(
`Migration: ${this.legacyRoot} nicht lesbar: ${getErrorMessage(err)}`, `Migration: ${this.legacyRoot} nicht lesbar: ${err.message}`,
); );
} }
return; return;
@@ -57,10 +56,8 @@ export class InboxMigrationService implements OnApplicationBootstrap {
let files: string[]; let files: string[];
try { try {
files = await fs.readdir(dir); files = await fs.readdir(dir);
} catch (err: unknown) { } catch (err: any) {
this.logger.warn( this.logger.warn(`Migration: ${dir} nicht lesbar: ${err.message}`);
`Migration: ${dir} nicht lesbar: ${getErrorMessage(err)}`,
);
continue; continue;
} }
@@ -70,9 +67,9 @@ export class InboxMigrationService implements OnApplicationBootstrap {
try { try {
await this.migrateFile(src, subdir, name); await this.migrateFile(src, subdir, name);
migrated += 1; migrated += 1;
} catch (err: unknown) { } catch (err: any) {
this.logger.error( this.logger.error(
`Migration fehlgeschlagen (${src}): ${getErrorMessage(err)}`, `Migration fehlgeschlagen (${src}): ${err.message}`,
); );
} }
} }
@@ -119,8 +116,8 @@ export class InboxMigrationService implements OnApplicationBootstrap {
try { try {
await fs.rename(src, dest); await fs.rename(src, dest);
return; return;
} catch (err: unknown) { } catch (err: any) {
if (getErrorCode(err) !== 'EXDEV') throw err; if (err.code !== 'EXDEV') throw err;
} }
await fs.copyFile(src, dest); await fs.copyFile(src, dest);
try { try {
+16 -21
View File
@@ -21,8 +21,6 @@ import { BarcodeScannerService } from '../barcode/barcode-scanner.service';
import { UserSettingsService } from '../user-settings/user-settings.service'; import { UserSettingsService } from '../user-settings/user-settings.service';
import { RequirePermissions } from '../auth/permissions.decorator'; import { RequirePermissions } from '../auth/permissions.decorator';
import { Permission } from '../auth/permissions.enum'; 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') @Controller('api/inbox')
@RequirePermissions(Permission.VIEW_SCANNER) @RequirePermissions(Permission.VIEW_SCANNER)
@@ -35,7 +33,7 @@ export class InboxController {
) {} ) {}
@Get() @Get()
async list(@Request() req: AuthenticatedRequest) { async list(@Request() req: any) {
const preferredUsername: string | null = const preferredUsername: string | null =
req.user?.preferredUsername ?? null; req.user?.preferredUsername ?? null;
return this.inboxService.listFiles(preferredUsername); return this.inboxService.listFiles(preferredUsername);
@@ -49,7 +47,7 @@ export class InboxController {
@Get(':id/preview') @Get(':id/preview')
async preview( async preview(
@Param('id') id: string, @Param('id') id: string,
@Request() req: AuthenticatedRequest, @Request() req: any,
@Res({ passthrough: true }) res: Response, @Res({ passthrough: true }) res: Response,
): Promise<StreamableFile> { ): Promise<StreamableFile> {
const preferredUsername: string | null = const preferredUsername: string | null =
@@ -71,7 +69,7 @@ export class InboxController {
async pageThumbnail( async pageThumbnail(
@Param('id') id: string, @Param('id') id: string,
@Param('page', ParseIntPipe) page: number, @Param('page', ParseIntPipe) page: number,
@Request() req: AuthenticatedRequest, @Request() req: any,
@Res({ passthrough: true }) res: Response, @Res({ passthrough: true }) res: Response,
): Promise<StreamableFile> { ): Promise<StreamableFile> {
const preferredUsername: string | null = const preferredUsername: string | null =
@@ -90,10 +88,7 @@ export class InboxController {
@Delete(':id') @Delete(':id')
@HttpCode(204) @HttpCode(204)
async remove( async remove(@Param('id') id: string, @Request() req: any): Promise<void> {
@Param('id') id: string,
@Request() req: AuthenticatedRequest,
): Promise<void> {
const preferredUsername: string | null = const preferredUsername: string | null =
req.user?.preferredUsername ?? null; req.user?.preferredUsername ?? null;
await this.inboxService.deleteDocument(id, preferredUsername); await this.inboxService.deleteDocument(id, preferredUsername);
@@ -104,7 +99,7 @@ export class InboxController {
async removePage( async removePage(
@Param('id') id: string, @Param('id') id: string,
@Param('page', ParseIntPipe) page: number, @Param('page', ParseIntPipe) page: number,
@Request() req: AuthenticatedRequest, @Request() req: any,
): Promise<void> { ): Promise<void> {
const preferredUsername: string | null = const preferredUsername: string | null =
req.user?.preferredUsername ?? null; req.user?.preferredUsername ?? null;
@@ -116,7 +111,7 @@ export class InboxController {
async toggleManualSplit( async toggleManualSplit(
@Param('id') id: string, @Param('id') id: string,
@Param('page', ParseIntPipe) page: number, @Param('page', ParseIntPipe) page: number,
@Request() req: AuthenticatedRequest, @Request() req: any,
): Promise<void> { ): Promise<void> {
const preferredUsername: string | null = const preferredUsername: string | null =
req.user?.preferredUsername ?? null; req.user?.preferredUsername ?? null;
@@ -127,7 +122,7 @@ export class InboxController {
@HttpCode(204) @HttpCode(204)
async resetEdits( async resetEdits(
@Param('id') id: string, @Param('id') id: string,
@Request() req: AuthenticatedRequest, @Request() req: any,
): Promise<void> { ): Promise<void> {
const preferredUsername: string | null = const preferredUsername: string | null =
req.user?.preferredUsername ?? null; req.user?.preferredUsername ?? null;
@@ -137,7 +132,7 @@ export class InboxController {
@Post(':id/postprocess') @Post(':id/postprocess')
async postprocess( async postprocess(
@Param('id') id: string, @Param('id') id: string,
@Request() req: AuthenticatedRequest, @Request() req: any,
@Body() @Body()
body: { body: {
sectionOffset?: number; sectionOffset?: number;
@@ -163,7 +158,7 @@ export class InboxController {
@Param('id') id: string, @Param('id') id: string,
@Param('page', ParseIntPipe) page: number, @Param('page', ParseIntPipe) page: number,
@Body() body: { rotation?: number }, @Body() body: { rotation?: number },
@Request() req: AuthenticatedRequest, @Request() req: any,
): Promise<void> { ): Promise<void> {
const rotation = Number(body?.rotation); const rotation = Number(body?.rotation);
if (!Number.isFinite(rotation)) { if (!Number.isFinite(rotation)) {
@@ -183,7 +178,7 @@ export class InboxController {
async pagePreview( async pagePreview(
@Param('id') id: string, @Param('id') id: string,
@Param('page', ParseIntPipe) page: number, @Param('page', ParseIntPipe) page: number,
@Request() req: AuthenticatedRequest, @Request() req: any,
@Res({ passthrough: true }) res: Response, @Res({ passthrough: true }) res: Response,
): Promise<StreamableFile> { ): Promise<StreamableFile> {
const preferredUsername: string | null = const preferredUsername: string | null =
@@ -205,7 +200,7 @@ export class InboxController {
@Param('id') id: string, @Param('id') id: string,
@Param('page', ParseIntPipe) page: number, @Param('page', ParseIntPipe) page: number,
@Body() body: { x: number; y: number; w: number; h: number }, @Body() body: { x: number; y: number; w: number; h: number },
@Request() req: AuthenticatedRequest, @Request() req: any,
): Promise<{ found: string[] }> { ): Promise<{ found: string[] }> {
const preferredUsername: string | null = const preferredUsername: string | null =
req.user?.preferredUsername ?? null; req.user?.preferredUsername ?? null;
@@ -224,8 +219,8 @@ export class InboxController {
@HttpCode(204) @HttpCode(204)
async updateSource( async updateSource(
@Param('id') id: string, @Param('id') id: string,
@Body() body: { source: InboxSource }, @Body() body: { source: any },
@Request() req: AuthenticatedRequest, @Request() req: any,
): Promise<void> { ): Promise<void> {
const preferredUsername: string | null = const preferredUsername: string | null =
req.user?.preferredUsername ?? null; req.user?.preferredUsername ?? null;
@@ -236,7 +231,7 @@ export class InboxController {
async downloadSegment( async downloadSegment(
@Param('id') id: string, @Param('id') id: string,
@Body() body: { pages: number[] }, @Body() body: { pages: number[] },
@Request() req: AuthenticatedRequest, @Request() req: any,
@Res({ passthrough: true }) res: Response, @Res({ passthrough: true }) res: Response,
): Promise<StreamableFile> { ): Promise<StreamableFile> {
const preferredUsername: string | null = const preferredUsername: string | null =
@@ -268,13 +263,13 @@ export class InboxController {
segments: { pages: number[]; filename: string }[]; segments: { pages: number[]; filename: string }[];
sender?: string; sender?: string;
}, },
@Request() req: AuthenticatedRequest, @Request() req: any,
): Promise<void> { ): Promise<void> {
const preferredUsername: string | null = const preferredUsername: string | null =
req.user?.preferredUsername ?? null; req.user?.preferredUsername ?? null;
const smtpOverride = const smtpOverride =
body.sender === 'user' body.sender === 'user'
? await this.userSettingsService.getSmtpConfig(req.user!.userId) ? await this.userSettingsService.getSmtpConfig(req.user.userId)
: null; : null;
await this.inboxService.sendAsEmail(id, preferredUsername, { await this.inboxService.sendAsEmail(id, preferredUsername, {
...body, ...body,
+4 -5
View File
@@ -19,7 +19,6 @@ import {
} from '../database/entities/inbox-document.entity'; } from '../database/entities/inbox-document.entity';
import { MailService } from '../postprocessing/mail.service'; import { MailService } from '../postprocessing/mail.service';
import { buildSegmentBuffer } from '../inbox-postprocessor/edit-applier'; import { buildSegmentBuffer } from '../inbox-postprocessor/edit-applier';
import { getErrorMessage } from '../common/error.util';
export interface InboxFile { export interface InboxFile {
id: string; id: string;
@@ -100,9 +99,9 @@ export class InboxService {
try { try {
const stat = await fs.stat(pdfPath); const stat = await fs.stat(pdfPath);
if (!stat.isFile()) throw new Error('not a file'); if (!stat.isFile()) throw new Error('not a file');
} catch (err: unknown) { } catch (err: any) {
this.logger.warn( 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'); throw new NotFoundException('Dokument nicht gefunden');
} }
@@ -219,9 +218,9 @@ export class InboxService {
await this.documentRepo.delete(doc.Id); await this.documentRepo.delete(doc.Id);
try { try {
await fs.rm(dir, { recursive: true, force: true }); await fs.rm(dir, { recursive: true, force: true });
} catch (err: unknown) { } catch (err: any) {
this.logger.warn( this.logger.warn(
`Dokument-Ordner konnte nicht gelöscht werden (${dir}): ${getErrorMessage(err)}`, `Dokument-Ordner konnte nicht gelöscht werden (${dir}): ${err.message}`,
); );
} }
} }
@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { Kontonummer } from '../database/entities'; import { Kontonummer } from '../database/entities';
@@ -10,7 +10,6 @@ import { Subject, Observable } from 'rxjs';
import { LabelPrintJob } from '../database/entities/label-print-job.entity'; import { LabelPrintJob } from '../database/entities/label-print-job.entity';
import { BarcodeTemplate } from '../database/entities/barcode-template.entity'; import { BarcodeTemplate } from '../database/entities/barcode-template.entity';
import { LabelRendererService } from './label-renderer.service'; import { LabelRendererService } from './label-renderer.service';
import { getErrorMessage } from '../common/error.util';
function isSafeUrl(url: string): boolean { function isSafeUrl(url: string): boolean {
try { try {
@@ -96,10 +95,8 @@ export class LabelPrintAgentService {
const res = await fetch(url); const res = await fetch(url);
const text = (await res.text()).trim(); const text = (await res.text()).trim();
vars['number'] = text; vars['number'] = text;
} catch (err: unknown) { } catch (err: any) {
this.logger.warn( this.logger.warn(`GET-URL fehlgeschlagen (${url}): ${err.message}`);
`GET-URL fehlgeschlagen (${url}): ${getErrorMessage(err)}`,
);
} }
} else { } else {
this.logger.warn(`GET-URL übersprungen (ungültiges Protokoll): ${url}`); this.logger.warn(`GET-URL übersprungen (ungültiges Protokoll): ${url}`);
@@ -152,9 +149,9 @@ export class LabelPrintAgentService {
candidate.LabelVariables ?? {}, candidate.LabelVariables ?? {},
); );
await this.jobRepo.save(candidate); await this.jobRepo.save(candidate);
} catch (err: unknown) { } catch (err: any) {
this.logger.error( 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 { try {
await fetch(url, { method: 'POST' }); await fetch(url, { method: 'POST' });
} catch (err: unknown) { } catch (err: any) {
this.logger.warn( this.logger.warn(`${type}-URL fehlgeschlagen (${url}): ${err.message}`);
`${type}-URL fehlgeschlagen (${url}): ${getErrorMessage(err)}`,
);
} }
} }
} }
@@ -2,7 +2,6 @@ import { Injectable, Logger } from '@nestjs/common';
import * as QRCode from 'qrcode'; import * as QRCode from 'qrcode';
import { Resvg } from '@resvg/resvg-js'; import { Resvg } from '@resvg/resvg-js';
import type { LabelElement } from '../database/entities/barcode-template.entity'; import type { LabelElement } from '../database/entities/barcode-template.entity';
import { getErrorMessage } from '../common/error.util';
const MM_TO_PX = 300 / 25.4; // 300 DPI const MM_TO_PX = 300 / 25.4; // 300 DPI
@@ -85,9 +84,9 @@ export class LabelRendererService {
parts.push( parts.push(
`<image href="data:image/png;base64,${b64}" x="${x}" y="${y}" width="${size}" height="${size}"/>`, `<image href="data:image/png;base64,${b64}" x="${x}" y="${y}" width="${size}" height="${size}"/>`,
); );
} catch (err: unknown) { } catch (err: any) {
this.logger.warn( 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') { } 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 { DocumentType } from '../database/entities/document-type.entity';
import { PaperlessService } from './paperless.service'; import { PaperlessService } from './paperless.service';
import { PostprocessingService } from '../postprocessing/postprocessing.service'; import { PostprocessingService } from '../postprocessing/postprocessing.service';
import { getErrorMessage, getResponseData } from '../common/error.util';
@Injectable() @Injectable()
export class PaperlessProcessorService { export class PaperlessProcessorService {
@@ -50,14 +49,13 @@ export class PaperlessProcessorService {
); );
// Postprocessing nach dem Speichern evaluieren // Postprocessing nach dem Speichern evaluieren
await this.postprocessingService.evaluate(updatedDoc || doc); await this.postprocessingService.evaluate(updatedDoc || doc);
} catch (innerErr: unknown) { } catch (innerErr: any) {
this.logger.error( this.logger.error(
`Fehler bei Dokument ID ${doc.id}: ${getErrorMessage(innerErr)}`, `Fehler bei Dokument ID ${doc.id}: ${innerErr.message}`,
); );
const responseData = getResponseData(innerErr); if (innerErr.response?.data) {
if (responseData) {
this.logger.error( 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 axios, { type AxiosInstance } from 'axios';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import FormData from 'form-data'; import FormData = require('form-data');
@Injectable() @Injectable()
export class PaperlessService { export class PaperlessService {
@@ -133,8 +133,8 @@ export class PaperlessService {
try { try {
const response = await this.client.patch(`/documents/${id}/`, data); const response = await this.client.patch(`/documents/${id}/`, data);
return response.data; return response.data;
} catch (err: unknown) { } catch (err: any) {
const body = axios.isAxiosError(err) ? err.response?.data : undefined; const body = err?.response?.data;
if (body) { if (body) {
this.logger.error( this.logger.error(
`Paperless updateDocument(${id}) Fehlerdetails: ${JSON.stringify(body)}`, `Paperless updateDocument(${id}) Fehlerdetails: ${JSON.stringify(body)}`,
@@ -4,7 +4,6 @@ import { Repository } from 'typeorm';
import { ExportTarget } from '../database/entities/export-target.entity'; import { ExportTarget } from '../database/entities/export-target.entity';
import * as ftp from 'basic-ftp'; import * as ftp from 'basic-ftp';
import { createClient, type WebDAVClient } from 'webdav'; import { createClient, type WebDAVClient } from 'webdav';
import { getErrorMessage } from '../common/error.util';
@Injectable() @Injectable()
export class ExportService { export class ExportService {
@@ -58,8 +57,8 @@ export class ExportService {
}; };
} }
return { success: true, message: 'Verbindung erfolgreich.' }; return { success: true, message: 'Verbindung erfolgreich.' };
} catch (err: unknown) { } catch (err: any) {
return { success: false, message: getErrorMessage(err) }; return { success: false, message: err.message };
} }
} }
@@ -13,7 +13,6 @@ import { PaperlessService } from '../paperless/paperless.service';
import { MailService } from './mail.service'; import { MailService } from './mail.service';
import { ExportService } from './export.service'; import { ExportService } from './export.service';
import axios from 'axios'; import axios from 'axios';
import { getErrorMessage } from '../common/error.util';
const CACHE_TTL_MS = 5 * 60 * 1000; const CACHE_TTL_MS = 5 * 60 * 1000;
@@ -92,18 +91,12 @@ export class PostprocessingService {
'success', 'success',
`Aktion ${action.ActionType} erfolgreich`, `Aktion ${action.ActionType} erfolgreich`,
); );
} catch (err: unknown) { } catch (err: any) {
hasError = true; hasError = true;
this.logger.error( this.logger.error(
`Fehler bei Aktion ${action.Id} (Typ ${action.ActionType}) für Dokument ${doc.id}: ${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',
getErrorMessage(err),
); );
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, { await this.paperlessService.updateDocument(doc.id, {
tags: Array.from(currentTags), tags: Array.from(currentTags),
}); });
} catch (tagErr: unknown) { } catch (tagErr: any) {
this.logger.error( 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); const dt = docTypes.find((x: any) => x.id === doc.document_type);
doc._documentTypeName = dt?.name ?? ''; doc._documentTypeName = dt?.name ?? '';
} }
} catch (err: unknown) { } catch (err: any) {
this.logger.warn(`Konnte Namen nicht auflösen: ${getErrorMessage(err)}`); this.logger.warn(`Konnte Namen nicht auflösen: ${err.message}`);
} }
} }
@@ -65,8 +65,8 @@ export class DocumentPipelineService {
} }
// 3. OCR auf erster Seite // 3. OCR auf erster Seite
// TODO: OCR-Ergebnis wird aktuell nicht gespeichert/weiterverwendet const ocrMarkdown =
await this.ocrService.extractTextAsMarkdown(firstPageBuffer); await this.ocrService.extractTextAsMarkdown(firstPageBuffer);
// 4. Task in DB erstellen // 4. Task in DB erstellen
const year = new Date().getFullYear(); const year = new Date().getFullYear();
@@ -1,7 +1,6 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import axios from 'axios'; import axios from 'axios';
import { getErrorMessage } from '../common/error.util';
@Injectable() @Injectable()
export class OcrService { export class OcrService {
@@ -47,8 +46,8 @@ Antworte nur mit dem extrahierten Markdown-Text, keine Erklärungen.`;
`OCR abgeschlossen: ${markdown.length} Zeichen extrahiert`, `OCR abgeschlossen: ${markdown.length} Zeichen extrahiert`,
); );
return markdown; return markdown;
} catch (error: unknown) { } catch (error: any) {
this.logger.error(`Ollama OCR fehlgeschlagen: ${getErrorMessage(error)}`); this.logger.error(`Ollama OCR fehlgeschlagen: ${error.message}`);
throw error; throw error;
} }
} }
@@ -120,9 +120,7 @@ export class PdfService {
for (const imgPath of imagePaths) { for (const imgPath of imagePaths) {
try { try {
await fs.unlink(imgPath); await fs.unlink(imgPath);
} catch { } catch {}
// Datei bereits entfernt o. Ä.: ignorieren
}
dirs.add(path.dirname(imgPath)); dirs.add(path.dirname(imgPath));
} }
for (const dir of dirs) { for (const dir of dirs) {
@@ -1,5 +1,5 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import sharp from 'sharp'; import sharp = require('sharp');
import jsQR from 'jsqr'; import jsQR from 'jsqr';
export interface QrCodeResult { export interface QrCodeResult {
@@ -18,7 +18,6 @@ import {
InboxDocument, InboxDocument,
type InboxSource, type InboxSource,
} from '../database/entities/inbox-document.entity'; } from '../database/entities/inbox-document.entity';
import { getErrorCode, getErrorMessage } from '../common/error.util';
const STABILITY_MS = 5000; const STABILITY_MS = 5000;
@@ -64,7 +63,7 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy {
this.logger.log(`Starte Überwachung: ${this.sourceRoot}`); this.logger.log(`Starte Überwachung: ${this.sourceRoot}`);
this.watcher = chokidar.watch(this.sourceRoot, { this.watcher = chokidar.watch(this.sourceRoot, {
ignored: /(^|[/\\])\../, ignored: /(^|[\/\\])\../,
persistent: true, persistent: true,
ignoreInitial: true, ignoreInitial: true,
awaitWriteFinish: { awaitWriteFinish: {
@@ -75,9 +74,9 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy {
}); });
this.watcher this.watcher
.on('add', (filePath: string) => void this.handleNewFile(filePath)) .on('add', (filePath: string) => this.handleNewFile(filePath))
.on('error', (error: Error) => .on('error', (error: Error) =>
this.logger.error(`Watcher Fehler: ${getErrorMessage(error)}`), this.logger.error(`Watcher Fehler: ${error.message}`),
); );
this.logger.log('Scanner-Watcher aktiv'); this.logger.log('Scanner-Watcher aktiv');
@@ -97,10 +96,10 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy {
withFileTypes: true, withFileTypes: true,
}); });
subdirs = entries.filter((e) => e.isDirectory()).map((e) => e.name); subdirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
} catch (err: unknown) { } catch (err: any) {
if (!silent) { if (!silent) {
this.logger.warn( this.logger.warn(
`Scanner-Check: Quellverzeichnis nicht lesbar (${this.sourceRoot}): ${getErrorMessage(err)}`, `Scanner-Check: Quellverzeichnis nicht lesbar (${this.sourceRoot}): ${err.message}`,
); );
} }
return; return;
@@ -112,10 +111,10 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy {
let files: string[]; let files: string[];
try { try {
files = await fs.readdir(dir); files = await fs.readdir(dir);
} catch (err: unknown) { } catch (err: any) {
if (!silent) { if (!silent) {
this.logger.warn( this.logger.warn(
`Scanner-Check: ${dir} nicht lesbar: ${getErrorMessage(err)}`, `Scanner-Check: ${dir} nicht lesbar: ${err.message}`,
); );
} }
continue; continue;
@@ -152,8 +151,8 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy {
this.isPeriodicScanning = true; this.isPeriodicScanning = true;
try { try {
await this.initialScan(true); await this.initialScan(true);
} catch (err: unknown) { } catch (err: any) {
this.logger.error(`Periodic Scan Fehler: ${getErrorMessage(err)}`); this.logger.error(`Periodic Scan Fehler: ${err.message}`);
} finally { } finally {
this.isPeriodicScanning = false; this.isPeriodicScanning = false;
} }
@@ -222,14 +221,14 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy {
try { try {
await this.barcodeScanner.scanAndMatch(doc); await this.barcodeScanner.scanAndMatch(doc);
} catch (err: unknown) { } catch (err: any) {
this.logger.warn( 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( this.logger.error(
`Übernahme fehlgeschlagen für ${filePath}: ${getErrorMessage(err)}`, `Übernahme fehlgeschlagen für ${filePath}: ${err.message}`,
); );
} finally { } finally {
this.processing.delete(filePath); this.processing.delete(filePath);
@@ -242,10 +241,8 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy {
pending = await this.documentRepo.find({ pending = await this.documentRepo.find({
where: [{ PageCount: 0 }, { QrCodes: IsNull() }], where: [{ PageCount: 0 }, { QrCodes: IsNull() }],
}); });
} catch (err: unknown) { } catch (err: any) {
this.logger.warn( this.logger.warn(`Backfill: DB-Query fehlgeschlagen: ${err.message}`);
`Backfill: DB-Query fehlgeschlagen: ${getErrorMessage(err)}`,
);
return; return;
} }
@@ -254,10 +251,8 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy {
try { try {
const didScan = await this.barcodeScanner.ensureScanned(doc); const didScan = await this.barcodeScanner.ensureScanned(doc);
if (didScan) scanned += 1; if (didScan) scanned += 1;
} catch (err: unknown) { } catch (err: any) {
this.logger.warn( this.logger.warn(`Backfill fehlgeschlagen (${doc.Id}): ${err.message}`);
`Backfill fehlgeschlagen (${doc.Id}): ${getErrorMessage(err)}`,
);
} }
} }
@@ -272,8 +267,8 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy {
try { try {
await fs.rename(src, dest); await fs.rename(src, dest);
return; return;
} catch (err: unknown) { } catch (err: any) {
if (getErrorCode(err) !== 'EXDEV') throw err; if (err.code !== 'EXDEV') throw err;
} }
// Cross-device: copy + unlink. Wenn unlink scheitert, Kopie zurückrollen, // Cross-device: copy + unlink. Wenn unlink scheitert, Kopie zurückrollen,
// damit ein kaputter Mount nicht bei jedem Neustart Duplikate produziert. // 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 { Client } from '../database/entities/client.entity';
import { Setting } from '../database/entities/setting.entity'; import { Setting } from '../database/entities/setting.entity';
import { CorrespondentSetting } from '../database/entities/correspondent-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 { ExportService } from '../postprocessing/export.service';
import { RequirePermissions } from '../auth/permissions.decorator'; import { RequirePermissions } from '../auth/permissions.decorator';
import { Permission } from '../auth/permissions.enum'; import { Permission } from '../auth/permissions.enum';
@@ -1,15 +1,12 @@
import { Controller, Get, Request } from '@nestjs/common'; import { Controller, Get, Request } from '@nestjs/common';
import { StatsService } from './stats.service'; import { StatsService } from './stats.service';
import type { AuthenticatedRequest } from '../auth/authenticated-request';
@Controller('api/stats') @Controller('api/stats')
export class StatsController { export class StatsController {
constructor(private readonly statsService: StatsService) {} constructor(private readonly statsService: StatsService) {}
@Get('counts') @Get('counts')
async getCounts(@Request() req: AuthenticatedRequest) { async getCounts(@Request() req: any) {
return this.statsService.getDashboardCounts( return this.statsService.getDashboardCounts(req.user?.preferredUsername);
req.user?.preferredUsername ?? undefined,
);
} }
} }
@@ -7,45 +7,38 @@ import {
Put, Put,
Request, Request,
} from '@nestjs/common'; } from '@nestjs/common';
import { import { UserSettingsService } from './user-settings.service';
UserSettingsService,
type UpdateUserSettingsInput,
} from './user-settings.service';
import { RequirePermissions } from '../auth/permissions.decorator'; import { RequirePermissions } from '../auth/permissions.decorator';
import { Permission } from '../auth/permissions.enum'; import { Permission } from '../auth/permissions.enum';
import type { AuthenticatedRequest } from '../auth/authenticated-request';
@Controller('api/user-settings') @Controller('api/user-settings')
export class UserSettingsController { export class UserSettingsController {
constructor(private readonly userSettingsService: UserSettingsService) {} constructor(private readonly userSettingsService: UserSettingsService) {}
@Get() @Get()
async getSettings(@Request() req: AuthenticatedRequest) { async getSettings(@Request() req: any) {
return this.userSettingsService.getSettings( return this.userSettingsService.getSettings(
req.user!.userId, req.user.userId,
req.user!.email, req.user.email,
req.user!.preferredUsername ?? undefined, req.user.preferredUsername,
req.user!.groups, req.user.groups,
); );
} }
@Put() @Put()
async updateSettings( async updateSettings(@Request() req: any, @Body() body: any) {
@Request() req: AuthenticatedRequest,
@Body() body: UpdateUserSettingsInput,
) {
return this.userSettingsService.updateSettings( return this.userSettingsService.updateSettings(
req.user!.userId, req.user.userId,
body, body,
req.user!.email, req.user.email,
req.user!.preferredUsername ?? undefined, req.user.preferredUsername,
req.user!.groups, req.user.groups,
); );
} }
@Get('senders') @Get('senders')
async getSenders(@Request() req: AuthenticatedRequest) { async getSenders(@Request() req: any) {
return this.userSettingsService.getAvailableSenders(req.user!.userId); return this.userSettingsService.getAvailableSenders(req.user.userId);
} }
@Post('test-smtp') @Post('test-smtp')
@@ -5,7 +5,6 @@ import { Repository } from 'typeorm';
import * as nodemailer from 'nodemailer'; import * as nodemailer from 'nodemailer';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import { UserSettings } from '../database/entities/user-settings.entity'; import { UserSettings } from '../database/entities/user-settings.entity';
import { getErrorMessage } from '../common/error.util';
const ALGORITHM = 'aes-256-gcm'; const ALGORITHM = 'aes-256-gcm';
@@ -23,20 +22,6 @@ export interface UserSettingsDto {
dailyDigestEnabled: boolean; 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() @Injectable()
export class UserSettingsService { export class UserSettingsService {
private readonly logger = new Logger(UserSettingsService.name); private readonly logger = new Logger(UserSettingsService.name);
@@ -107,7 +92,19 @@ export class UserSettingsService {
async updateSettings( async updateSettings(
userId: string, 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, email?: string,
preferredUsername?: string, preferredUsername?: string,
groups?: string[], groups?: string[],
@@ -163,8 +160,8 @@ export class UserSettingsService {
try { try {
await transporter.verify(); await transporter.verify();
return { ok: true }; return { ok: true };
} catch (err: unknown) { } catch (err: any) {
return { ok: false, error: getErrorMessage(err) }; return { ok: false, error: err.message };
} }
} }
@@ -20,9 +20,9 @@ export class WebhookController {
@Public() @Public()
@Post('paperless') @Post('paperless')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
handlePaperlessWebhook(@Body() payload: PaperlessWebhookPayload): { async handlePaperlessWebhook(
status: string; @Body() payload: PaperlessWebhookPayload,
} { ): Promise<{ status: string }> {
this.logger.log( this.logger.log(
`Webhook empfangen: action=${payload.action}, document=${payload.document_id}`, `Webhook empfangen: action=${payload.action}, document=${payload.document_id}`,
); );
-14
View File
@@ -19,19 +19,5 @@ export default defineConfig([
ecmaVersion: 2020, ecmaVersion: 2020,
globals: globals.browser, 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',
},
],
},
}, },
]) ])
@@ -121,7 +121,7 @@ export default function DocumentEditModal({ documentId, document, open, onClose,
useEffect(() => { useEffect(() => {
if (open && document) { if (open && document) {
const customFieldsObj: any = {}; let customFieldsObj: any = {};
if (document.customFields) { if (document.customFields) {
document.customFields.forEach(cf => { document.customFields.forEach(cf => {
customFieldsObj[`cf_${cf.field}`] = cf.value; customFieldsObj[`cf_${cf.field}`] = cf.value;
@@ -322,9 +322,7 @@ export default function MailImportWizard({ visible, onClose, onSuccess, email, a
try { try {
const status = await emailImportApi.getJobStatus(jobId); const status = await emailImportApi.getJobStatus(jobId);
if (status?.message) setImportStatus(status.message); if (status?.message) setImportStatus(status.message);
} catch { } catch {}
// Status-Abfrage fehlgeschlagen: nächsten Poll abwarten
}
}, 1500); }, 1500);
try { try {
const finalData = []; const finalData = [];
@@ -441,10 +441,9 @@ function DocTypeFieldsTable({ docTypeId }: { docTypeId: number }) {
case 1: return <Tag>Absender</Tag>; case 1: return <Tag>Absender</Tag>;
case 2: return <Tag>Belegdatum</Tag>; case 2: return <Tag>Belegdatum</Tag>;
case 3: return <Tag>Ablagenummer</Tag>; case 3: return <Tag>Ablagenummer</Tag>;
case 4: { case 4:
const cf = customFields.find(c => c.id === record.TypeIndex); const cf = customFields.find(c => c.id === record.TypeIndex);
return <Tag color="blue">{cf ? cf.name : `CF #${record.TypeIndex}`}</Tag>; return <Tag color="blue">{cf ? cf.name : `CF #${record.TypeIndex}`}</Tag>;
}
case 5: return <Tag>Titel</Tag>; case 5: return <Tag>Titel</Tag>;
default: return <Tag>Unbekannt ({record.Type})</Tag>; default: return <Tag>Unbekannt ({record.Type})</Tag>;
} }
@@ -8,8 +8,6 @@ export function useAuthUrl(url: string | null | undefined): string | null {
useEffect(() => { useEffect(() => {
if (!url) { if (!url) {
// Reset des Blob-URLs, wenn keine Quelle (mehr) vorhanden ist
// eslint-disable-next-line react-hooks/set-state-in-effect
setBlobUrl(null); setBlobUrl(null);
return; return;
} }