diff --git a/paperless-backend/eslint.config.mjs b/paperless-backend/eslint.config.mjs index 4e9f827..adc3abf 100644 --- a/paperless-backend/eslint.config.mjs +++ b/paperless-backend/eslint.config.mjs @@ -28,8 +28,26 @@ 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', - "prettier/prettier": ["error", { endOfLine: "auto" }], + '@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' }], }, }, ); diff --git a/paperless-backend/src/agrarmonitor/agrarmonitor-polling.service.ts b/paperless-backend/src/agrarmonitor/agrarmonitor-polling.service.ts index 9e0cdab..0f1cd9e 100644 --- a/paperless-backend/src/agrarmonitor/agrarmonitor-polling.service.ts +++ b/paperless-backend/src/agrarmonitor/agrarmonitor-polling.service.ts @@ -59,7 +59,7 @@ export class AgrarmonitorPollingService implements OnModuleInit { } @Cron(process.env['AGRARMONITOR_POLLING_CRON'] || '0 */30 * * * *') - async scheduledPolling() { + 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 * * * * *') - async scheduledUploadCheck() { + scheduledUploadCheck() { if (!process.env['AGRARMONITOR_UPLOAD_CHECK_CRON']) return; this.processVerarbeiteteDocuments().catch((err) => this.logger.error('Cron-Upload-Check-Fehler:', err), diff --git a/paperless-backend/src/agrarmonitor/agrarmonitor.service.ts b/paperless-backend/src/agrarmonitor/agrarmonitor.service.ts index 0d9cf76..0eb2c37 100644 --- a/paperless-backend/src/agrarmonitor/agrarmonitor.service.ts +++ b/paperless-backend/src/agrarmonitor/agrarmonitor.service.ts @@ -6,6 +6,7 @@ import { AesGcmCookieEncryptor, type AgrarmonitorConnectorResult, } from 'agrarmonitor-connector'; +import { getErrorMessage } from '../common/error.util'; export interface AgrarmonitorStatusDto { connected: boolean; @@ -95,13 +96,13 @@ export class AgrarmonitorService { registriert: registrierungStatus.registriert, freigeschaltet: freigeschaltetStatus.freigeschaltet, }; - } catch (err: any) { + } catch (err: unknown) { this.client = null; return { connected: false, registriert: null, freigeschaltet: null, - error: err?.message ?? 'Verbindung fehlgeschlagen', + error: getErrorMessage(err) || 'Verbindung fehlgeschlagen', }; } } diff --git a/paperless-backend/src/auth/authenticated-request.ts b/paperless-backend/src/auth/authenticated-request.ts new file mode 100644 index 0000000..86531d4 --- /dev/null +++ b/paperless-backend/src/auth/authenticated-request.ts @@ -0,0 +1,24 @@ +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; +} diff --git a/paperless-backend/src/auth/jwt.strategy.ts b/paperless-backend/src/auth/jwt.strategy.ts index 1153c05..325072f 100644 --- a/paperless-backend/src/auth/jwt.strategy.ts +++ b/paperless-backend/src/auth/jwt.strategy.ts @@ -4,6 +4,15 @@ 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') { @@ -24,19 +33,12 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { }); } - validate(payload: any): { - userId: string; - email: string; - name: string; - preferredUsername: string | null; - groups: string[]; - permissions: any[]; - } { - const groups = payload.groups || []; + validate(payload: JwtPayload): AuthenticatedUser { + 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), diff --git a/paperless-backend/src/barcode/barcode-scanner.service.ts b/paperless-backend/src/barcode/barcode-scanner.service.ts index 11a57d2..4f335de 100644 --- a/paperless-backend/src/barcode/barcode-scanner.service.ts +++ b/paperless-backend/src/barcode/barcode-scanner.service.ts @@ -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 = require('sharp'); +import sharp from 'sharp'; import { PdfService } from '../preprocessing/pdf.service'; import { QrCodeService } from '../preprocessing/qr-code.service'; import { @@ -18,6 +18,7 @@ import { applyTemplate, buildVariables, } from '../inbox-postprocessor/variable-resolver'; +import { getErrorMessage } from '../common/error.util'; export interface MatchedBarcode { page: number; @@ -56,9 +57,9 @@ export class BarcodeScannerService implements OnApplicationBootstrap { let rows: BarcodeTemplate[]; try { rows = await this.templateRepo.find(); - } catch (err: any) { + } catch (err: unknown) { this.logger.warn( - `Template-Migration: Query fehlgeschlagen: ${err.message}`, + `Template-Migration: Query fehlgeschlagen: ${getErrorMessage(err)}`, ); return; } @@ -94,9 +95,9 @@ export class BarcodeScannerService implements OnApplicationBootstrap { doc.IsScanned = true; try { await this.documentRepo.save(doc); - } catch (err: any) { + } catch (err: unknown) { this.logger.warn( - `Scan-Ergebnis konnte nicht gespeichert werden (${doc.Id}): ${err.message}`, + `Scan-Ergebnis konnte nicht gespeichert werden (${doc.Id}): ${getErrorMessage(err)}`, ); } @@ -192,9 +193,9 @@ export class BarcodeScannerService implements OnApplicationBootstrap { qrCodes.push({ page: i + 1, value: qr.data }); } } - } catch (err: any) { + } catch (err: unknown) { this.logger.warn( - `QR-Scan fehlgeschlagen (${pdfPath}, Seite ${i + 1}): ${err.message}`, + `QR-Scan fehlgeschlagen (${pdfPath}, Seite ${i + 1}): ${getErrorMessage(err)}`, ); } } @@ -203,8 +204,10 @@ export class BarcodeScannerService implements OnApplicationBootstrap { await this.pageCache.generate(documentId, images); return { qrCodes, pageCount: images.length }; - } catch (err: any) { - this.logger.warn(`Kein QR-Scan möglich für ${pdfPath}: ${err.message}`); + } catch (err: unknown) { + this.logger.warn( + `Kein QR-Scan möglich für ${pdfPath}: ${getErrorMessage(err)}`, + ); return { qrCodes: [], pageCount: 0 }; } finally { await this.pdfService.cleanup(images); @@ -280,8 +283,10 @@ export class BarcodeScannerService implements OnApplicationBootstrap { let docs: InboxDocument[]; try { docs = await this.documentRepo.find(); - } catch (err: any) { - this.logger.warn(`Rescan: DB-Query fehlgeschlagen: ${err.message}`); + } catch (err: unknown) { + this.logger.warn( + `Rescan: DB-Query fehlgeschlagen: ${getErrorMessage(err)}`, + ); return { scanned: 0, failed: 0 }; } if (docs.length === 0) return { scanned: 0, failed: 0 }; @@ -306,8 +311,10 @@ export class BarcodeScannerService implements OnApplicationBootstrap { doc.PageCount = pageCount; await this.documentRepo.save(doc); scanned++; - } catch (err: any) { - this.logger.warn(`Rescan fehlgeschlagen für ${doc.Id}: ${err.message}`); + } catch (err: unknown) { + this.logger.warn( + `Rescan fehlgeschlagen für ${doc.Id}: ${getErrorMessage(err)}`, + ); failed++; } } diff --git a/paperless-backend/src/barcode/page-cache.service.ts b/paperless-backend/src/barcode/page-cache.service.ts index 32c5c9a..be3725e 100644 --- a/paperless-backend/src/barcode/page-cache.service.ts +++ b/paperless-backend/src/barcode/page-cache.service.ts @@ -3,6 +3,7 @@ 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; @@ -54,9 +55,9 @@ export class PageCacheService { .resize({ width: THUMBNAIL_WIDTH }) .png() .toFile(thumbDest); - } catch (err: any) { + } catch (err: unknown) { this.logger.warn( - `Seiten-Cache fehlgeschlagen (${documentId} Seite ${page}): ${err.message}`, + `Seiten-Cache fehlgeschlagen (${documentId} Seite ${page}): ${getErrorMessage(err)}`, ); } } @@ -103,9 +104,9 @@ export class PageCacheService { const to = path.join(dir, `page-${n - 1}.${variant}.png`); try { await fs.rename(from, to); - } catch (err: any) { + } catch (err: unknown) { this.logger.warn( - `Cache-Shift fehlgeschlagen (${documentId} Seite ${n} ${variant}): ${err.message}`, + `Cache-Shift fehlgeschlagen (${documentId} Seite ${n} ${variant}): ${getErrorMessage(err)}`, ); } } diff --git a/paperless-backend/src/common/error.util.ts b/paperless-backend/src/common/error.util.ts new file mode 100644 index 0000000..1b97a5a --- /dev/null +++ b/paperless-backend/src/common/error.util.ts @@ -0,0 +1,55 @@ +/** + * 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 }).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; +} diff --git a/paperless-backend/src/daily-digest/daily-digest.controller.ts b/paperless-backend/src/daily-digest/daily-digest.controller.ts index 936fe9d..8e409e4 100644 --- a/paperless-backend/src/daily-digest/daily-digest.controller.ts +++ b/paperless-backend/src/daily-digest/daily-digest.controller.ts @@ -1,5 +1,7 @@ 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 { @@ -7,8 +9,8 @@ export class DailyDigestController { @Post('send-now') @HttpCode(200) - async sendNow(@Request() req: any) { - const { userId, email, preferredUsername, groups } = req.user; + async sendNow(@Request() req: AuthenticatedRequest) { + const { userId, email, preferredUsername, groups } = req.user!; if (!email) { return { ok: false, @@ -19,12 +21,12 @@ export class DailyDigestController { await this.dailyDigestService.sendDigestForUser( userId, email, - preferredUsername, + preferredUsername ?? undefined, groups, ); return { ok: true }; - } catch (err: any) { - return { ok: false, error: err.message }; + } catch (err: unknown) { + return { ok: false, error: getErrorMessage(err) }; } } } diff --git a/paperless-backend/src/email-download/email-download.service.ts b/paperless-backend/src/email-download/email-download.service.ts index a21283d..cd0cb65 100644 --- a/paperless-backend/src/email-download/email-download.service.ts +++ b/paperless-backend/src/email-download/email-download.service.ts @@ -19,6 +19,7 @@ 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 { @@ -45,10 +46,10 @@ export class EmailDownloadService { this.running = true; try { await this.fetchAndStore(); - } catch (err: any) { + } catch (err: unknown) { this.logger.error( - `Fehler im E-Mail-Download-Job: ${err.message}`, - err.stack, + `Fehler im E-Mail-Download-Job: ${getErrorMessage(err)}`, + getErrorStack(err), ); } finally { this.running = false; @@ -116,10 +117,10 @@ export class EmailDownloadService { } await this.processMessage(msg); - } catch (err: any) { + } catch (err: unknown) { this.logger.error( - `Fehler beim Abrufen der Nachricht ${messageId}: ${err.message}`, - err.stack, + `Fehler beim Abrufen der Nachricht ${messageId}: ${getErrorMessage(err)}`, + getErrorStack(err), ); } } @@ -157,7 +158,7 @@ export class EmailDownloadService { }> = []; for (const att of parsed.attachments) { - const entry = await this.buildAttachment(att); + const entry = this.buildAttachment(att); if (entry) attachmentsToPersist.push(entry); } @@ -214,9 +215,9 @@ export class EmailDownloadService { await this.pdfService.cleanup(images); await fs.unlink(tempPdfPath).catch(() => {}); - } catch (err: any) { + } catch (err: unknown) { this.logger.warn( - `Konnte Vorschaubilder für Anhang ${attachment.Id} nicht generieren: ${err.message}`, + `Konnte Vorschaubilder für Anhang ${attachment.Id} nicht generieren: ${getErrorMessage(err)}`, ); } } @@ -262,9 +263,9 @@ export class EmailDownloadService { return { processed, failed }; } - private async buildAttachment( + private buildAttachment( att: MailAttachment, - ): Promise<{ attachment: Attachment; buffer: Buffer } | null> { + ): { attachment: Attachment; buffer: Buffer } | null { const buffer = att.content; if (!Buffer.isBuffer(buffer) || buffer.length === 0) return null; diff --git a/paperless-backend/src/email/email-import.controller.ts b/paperless-backend/src/email/email-import.controller.ts index 7555c60..f847386 100644 --- a/paperless-backend/src/email/email-import.controller.ts +++ b/paperless-backend/src/email/email-import.controller.ts @@ -16,6 +16,7 @@ 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 { @@ -131,9 +132,14 @@ export class EmailImportController { `inline; filename="preview-${attachmentId}.pdf"`, ); res.send(pdfBuffer); - } catch (err: any) { - this.logger.error(`Error generating print preview: ${err.message}`); - throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR); + } catch (err: unknown) { + this.logger.error( + `Error generating print preview: ${getErrorMessage(err)}`, + ); + throw new HttpException( + getErrorMessage(err), + HttpStatus.INTERNAL_SERVER_ERROR, + ); } } @@ -185,9 +191,12 @@ export class EmailImportController { try { const result = await this.importService.executeImport(importData); return result; - } catch (err: any) { - this.logger.error(`Error executing import: ${err.message}`); - throw new HttpException(err.message, HttpStatus.INTERNAL_SERVER_ERROR); + } catch (err: unknown) { + this.logger.error(`Error executing import: ${getErrorMessage(err)}`); + throw new HttpException( + getErrorMessage(err), + HttpStatus.INTERNAL_SERVER_ERROR, + ); } } } diff --git a/paperless-backend/src/email/email-import.service.ts b/paperless-backend/src/email/email-import.service.ts index 022b167..2431c00 100644 --- a/paperless-backend/src/email/email-import.service.ts +++ b/paperless-backend/src/email/email-import.service.ts @@ -17,6 +17,7 @@ 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 { @@ -80,9 +81,9 @@ export class EmailImportService { await this.attachmentRepo.save(attachment); await this.pdfService.cleanup(images); - } catch (err: any) { + } catch (err: unknown) { this.logger.warn( - `Fehler bei on-demand Vorschau-Generierung für Anhang ${attachment.Id}: ${err.message}`, + `Fehler bei on-demand Vorschau-Generierung für Anhang ${attachment.Id}: ${getErrorMessage(err)}`, ); } finally { await fs.unlink(tempPdfPath).catch(() => {}); @@ -156,11 +157,12 @@ export class EmailImportService { this.logger.debug(`Received Belegnummer: ${result}`); return String(result); - } catch (error: any) { - const status = error.response?.status || 'UNKNOWN'; - const detail = error.response?.data - ? JSON.stringify(error.response.data) - : error.message; + } 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); this.logger.error( `Failed to fetch Belegnummer from ${url}. Status: ${status}, Detail: ${detail}`, ); @@ -191,9 +193,9 @@ export class EmailImportService { `Releasing Belegnummer: ${cleanNumber} (original: ${number}) via ${url}`, ); await axios.get(url); - } catch (error: any) { + } catch (error: unknown) { this.logger.error( - `Failed to release Belegnummer at ${url}: ${error.message}`, + `Failed to release Belegnummer at ${url}: ${getErrorMessage(error)}`, ); } } @@ -215,9 +217,9 @@ export class EmailImportService { `Setting Belegnummer: ${cleanNumber} (original: ${number}) via ${url}`, ); await axios.get(url); - } catch (error: any) { + } catch (error: unknown) { this.logger.error( - `Failed to set Belegnummer at ${url}: ${error.message}`, + `Failed to set Belegnummer at ${url}: ${getErrorMessage(error)}`, ); throw new HttpException( 'Fehler beim Setzen der Belegnummer', @@ -610,7 +612,9 @@ export class EmailImportService { docId = statusObj.related_document; break; } - } catch (e) {} + } catch { + // Task-Status nicht parsebar: nächsten Versuch abwarten + } } if (docId) { @@ -629,7 +633,10 @@ 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: ${e.message}`), + (e) => + this.logger.warn( + `Failed to set Belegnummer: ${getErrorMessage(e)}`, + ), ); } diff --git a/paperless-backend/src/email/email-page-cache.service.ts b/paperless-backend/src/email/email-page-cache.service.ts index 80f8ff0..fc8849f 100644 --- a/paperless-backend/src/email/email-page-cache.service.ts +++ b/paperless-backend/src/email/email-page-cache.service.ts @@ -3,6 +3,7 @@ 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 +56,9 @@ export class EmailPageCacheService { .resize({ width: THUMBNAIL_WIDTH }) .png() .toFile(thumbDest); - } catch (err: any) { + } catch (err: unknown) { this.logger.warn( - `E-Mail Page Cache fehlgeschlagen (Attachment ${attachmentId} Seite ${page}): ${err.message}`, + `E-Mail Page Cache fehlgeschlagen (Attachment ${attachmentId} Seite ${page}): ${getErrorMessage(err)}`, ); } } diff --git a/paperless-backend/src/email/email.controller.ts b/paperless-backend/src/email/email.controller.ts index dc18b9b..213b78a 100644 --- a/paperless-backend/src/email/email.controller.ts +++ b/paperless-backend/src/email/email.controller.ts @@ -19,6 +19,7 @@ import { Content } from '../database/entities/content.entity'; import { PaperlessService } from '../paperless/paperless.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 { @@ -173,10 +174,10 @@ export class EmailController { ); } } - } catch (err: any) { + } catch (err: unknown) { this.logger.error( - `Fehler bei Checksummen-Prüfung für Attachment ${attachment.Id}: ${err.message}`, - err.stack, + `Fehler bei Checksummen-Prüfung für Attachment ${attachment.Id}: ${getErrorMessage(err)}`, + getErrorStack(err), ); } } @@ -203,10 +204,10 @@ export class EmailController { `Prüfung abgeschlossen. ${updatedCount} E-Mails aktualisiert, ${idsUpdated} Paperless-IDs ergänzt, ${skippedCount} übersprungen.`, ); return { updatedCount, idsUpdated }; - } catch (error: any) { + } catch (error: unknown) { this.logger.error( - `Kritischer Fehler bei checkAttachments: ${error.message}`, - error.stack, + `Kritischer Fehler bei checkAttachments: ${getErrorMessage(error)}`, + getErrorStack(error), ); throw error; } diff --git a/paperless-backend/src/freigabe/freigabe.service.ts b/paperless-backend/src/freigabe/freigabe.service.ts index f0562a8..d0d361f 100644 --- a/paperless-backend/src/freigabe/freigabe.service.ts +++ b/paperless-backend/src/freigabe/freigabe.service.ts @@ -48,7 +48,7 @@ export class FreigabeService { try { const result = await this.paperlessService.getDocuments(params); return result; - } catch (err: any) { + } catch { // Fallback: Paperless unterstützt den custom_fields-Filter möglicherweise nicht // In diesem Fall alle Belege laden und client-seitig filtern this.logger.warn( diff --git a/paperless-backend/src/inbox-postprocessor/inbox-postprocessor.service.ts b/paperless-backend/src/inbox-postprocessor/inbox-postprocessor.service.ts index 2704547..27912af 100644 --- a/paperless-backend/src/inbox-postprocessor/inbox-postprocessor.service.ts +++ b/paperless-backend/src/inbox-postprocessor/inbox-postprocessor.service.ts @@ -18,6 +18,7 @@ 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; @@ -276,16 +277,16 @@ export class InboxPostprocessorService { abortProcessing = true; break; } - } catch (err: any) { + } catch (err: unknown) { this.logger.error( - `Aktion PAPERLESS#${action.Id} für Dokument ${doc.Id} fehlgeschlagen: ${err.message}`, + `Aktion PAPERLESS#${action.Id} für Dokument ${doc.Id} fehlgeschlagen: ${getErrorMessage(err)}`, ); results.push({ sectionIndex: currentSectionIndex, actionId: action.Id, actionType: action.ActionType, ok: false, - message: err.message, + message: getErrorMessage(err), }); } } else { @@ -303,16 +304,16 @@ export class InboxPostprocessorService { actionType: action.ActionType, ok: true, }); - } catch (err: any) { + } catch (err: unknown) { this.logger.error( - `Aktion ${action.ActionType}#${action.Id} für Dokument ${doc.Id} fehlgeschlagen: ${err.message}`, + `Aktion ${action.ActionType}#${action.Id} für Dokument ${doc.Id} fehlgeschlagen: ${getErrorMessage(err)}`, ); results.push({ sectionIndex: currentSectionIndex, actionId: action.Id, actionType: action.ActionType, ok: false, - message: err.message, + message: getErrorMessage(err), }); } } diff --git a/paperless-backend/src/inbox-postprocessor/variable-resolver.ts b/paperless-backend/src/inbox-postprocessor/variable-resolver.ts index d72c4c2..c04b8b3 100644 --- a/paperless-backend/src/inbox-postprocessor/variable-resolver.ts +++ b/paperless-backend/src/inbox-postprocessor/variable-resolver.ts @@ -48,7 +48,9 @@ export function buildVariables(ctx: ResolverContext): Record { if (gv !== undefined) vars[`barcode.${sanitizeKey(g)}`] = gv; } } - } catch {} + } catch { + // Ungültige Regex/Barcode-Daten: Variablen bleiben ungesetzt + } } return vars; diff --git a/paperless-backend/src/inbox/clients.controller.ts b/paperless-backend/src/inbox/clients.controller.ts index 8baa58b..81e8483 100644 --- a/paperless-backend/src/inbox/clients.controller.ts +++ b/paperless-backend/src/inbox/clients.controller.ts @@ -3,6 +3,7 @@ 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 { @@ -15,8 +16,8 @@ export class ClientsController { ) {} @Get() - async getMyClients(@Request() req: any) { - const userId = req.user.userId; + async getMyClients(@Request() req: AuthenticatedRequest) { + const userId = req.user!.userId; const mappings = await this.userClientRepo.find({ where: { UserId: userId }, }); diff --git a/paperless-backend/src/inbox/inbox-migration.service.ts b/paperless-backend/src/inbox/inbox-migration.service.ts index 737e1d2..1590b73 100644 --- a/paperless-backend/src/inbox/inbox-migration.service.ts +++ b/paperless-backend/src/inbox/inbox-migration.service.ts @@ -10,6 +10,7 @@ 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 { @@ -41,10 +42,10 @@ export class InboxMigrationService implements OnApplicationBootstrap { withFileTypes: true, }); subdirs = entries.filter((e) => e.isDirectory()).map((e) => e.name); - } catch (err: any) { - if (err.code !== 'ENOENT') { + } catch (err: unknown) { + if (getErrorCode(err) !== 'ENOENT') { this.logger.warn( - `Migration: ${this.legacyRoot} nicht lesbar: ${err.message}`, + `Migration: ${this.legacyRoot} nicht lesbar: ${getErrorMessage(err)}`, ); } return; @@ -56,8 +57,10 @@ export class InboxMigrationService implements OnApplicationBootstrap { let files: string[]; try { files = await fs.readdir(dir); - } catch (err: any) { - this.logger.warn(`Migration: ${dir} nicht lesbar: ${err.message}`); + } catch (err: unknown) { + this.logger.warn( + `Migration: ${dir} nicht lesbar: ${getErrorMessage(err)}`, + ); continue; } @@ -67,9 +70,9 @@ export class InboxMigrationService implements OnApplicationBootstrap { try { await this.migrateFile(src, subdir, name); migrated += 1; - } catch (err: any) { + } catch (err: unknown) { this.logger.error( - `Migration fehlgeschlagen (${src}): ${err.message}`, + `Migration fehlgeschlagen (${src}): ${getErrorMessage(err)}`, ); } } @@ -116,8 +119,8 @@ export class InboxMigrationService implements OnApplicationBootstrap { try { await fs.rename(src, dest); return; - } catch (err: any) { - if (err.code !== 'EXDEV') throw err; + } catch (err: unknown) { + if (getErrorCode(err) !== 'EXDEV') throw err; } await fs.copyFile(src, dest); try { diff --git a/paperless-backend/src/inbox/inbox.controller.ts b/paperless-backend/src/inbox/inbox.controller.ts index 34a97d4..558b0a3 100644 --- a/paperless-backend/src/inbox/inbox.controller.ts +++ b/paperless-backend/src/inbox/inbox.controller.ts @@ -21,6 +21,8 @@ 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) @@ -33,7 +35,7 @@ export class InboxController { ) {} @Get() - async list(@Request() req: any) { + async list(@Request() req: AuthenticatedRequest) { const preferredUsername: string | null = req.user?.preferredUsername ?? null; return this.inboxService.listFiles(preferredUsername); @@ -47,7 +49,7 @@ export class InboxController { @Get(':id/preview') async preview( @Param('id') id: string, - @Request() req: any, + @Request() req: AuthenticatedRequest, @Res({ passthrough: true }) res: Response, ): Promise { const preferredUsername: string | null = @@ -69,7 +71,7 @@ export class InboxController { async pageThumbnail( @Param('id') id: string, @Param('page', ParseIntPipe) page: number, - @Request() req: any, + @Request() req: AuthenticatedRequest, @Res({ passthrough: true }) res: Response, ): Promise { const preferredUsername: string | null = @@ -88,7 +90,10 @@ export class InboxController { @Delete(':id') @HttpCode(204) - async remove(@Param('id') id: string, @Request() req: any): Promise { + async remove( + @Param('id') id: string, + @Request() req: AuthenticatedRequest, + ): Promise { const preferredUsername: string | null = req.user?.preferredUsername ?? null; await this.inboxService.deleteDocument(id, preferredUsername); @@ -99,7 +104,7 @@ export class InboxController { async removePage( @Param('id') id: string, @Param('page', ParseIntPipe) page: number, - @Request() req: any, + @Request() req: AuthenticatedRequest, ): Promise { const preferredUsername: string | null = req.user?.preferredUsername ?? null; @@ -111,7 +116,7 @@ export class InboxController { async toggleManualSplit( @Param('id') id: string, @Param('page', ParseIntPipe) page: number, - @Request() req: any, + @Request() req: AuthenticatedRequest, ): Promise { const preferredUsername: string | null = req.user?.preferredUsername ?? null; @@ -122,7 +127,7 @@ export class InboxController { @HttpCode(204) async resetEdits( @Param('id') id: string, - @Request() req: any, + @Request() req: AuthenticatedRequest, ): Promise { const preferredUsername: string | null = req.user?.preferredUsername ?? null; @@ -132,7 +137,7 @@ export class InboxController { @Post(':id/postprocess') async postprocess( @Param('id') id: string, - @Request() req: any, + @Request() req: AuthenticatedRequest, @Body() body: { sectionOffset?: number; @@ -158,7 +163,7 @@ export class InboxController { @Param('id') id: string, @Param('page', ParseIntPipe) page: number, @Body() body: { rotation?: number }, - @Request() req: any, + @Request() req: AuthenticatedRequest, ): Promise { const rotation = Number(body?.rotation); if (!Number.isFinite(rotation)) { @@ -178,7 +183,7 @@ export class InboxController { async pagePreview( @Param('id') id: string, @Param('page', ParseIntPipe) page: number, - @Request() req: any, + @Request() req: AuthenticatedRequest, @Res({ passthrough: true }) res: Response, ): Promise { const preferredUsername: string | null = @@ -200,7 +205,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: any, + @Request() req: AuthenticatedRequest, ): Promise<{ found: string[] }> { const preferredUsername: string | null = req.user?.preferredUsername ?? null; @@ -219,8 +224,8 @@ export class InboxController { @HttpCode(204) async updateSource( @Param('id') id: string, - @Body() body: { source: any }, - @Request() req: any, + @Body() body: { source: InboxSource }, + @Request() req: AuthenticatedRequest, ): Promise { const preferredUsername: string | null = req.user?.preferredUsername ?? null; @@ -231,7 +236,7 @@ export class InboxController { async downloadSegment( @Param('id') id: string, @Body() body: { pages: number[] }, - @Request() req: any, + @Request() req: AuthenticatedRequest, @Res({ passthrough: true }) res: Response, ): Promise { const preferredUsername: string | null = @@ -263,13 +268,13 @@ export class InboxController { segments: { pages: number[]; filename: string }[]; sender?: string; }, - @Request() req: any, + @Request() req: AuthenticatedRequest, ): Promise { 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, diff --git a/paperless-backend/src/inbox/inbox.service.ts b/paperless-backend/src/inbox/inbox.service.ts index cdba0ab..fb3e2e0 100644 --- a/paperless-backend/src/inbox/inbox.service.ts +++ b/paperless-backend/src/inbox/inbox.service.ts @@ -19,6 +19,7 @@ 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; @@ -99,9 +100,9 @@ export class InboxService { try { const stat = await fs.stat(pdfPath); if (!stat.isFile()) throw new Error('not a file'); - } catch (err: any) { + } catch (err: unknown) { this.logger.warn( - `Datei fehlt trotz DB-Eintrag (${doc.Id}): ${err.message}`, + `Datei fehlt trotz DB-Eintrag (${doc.Id}): ${getErrorMessage(err)}`, ); throw new NotFoundException('Dokument nicht gefunden'); } @@ -218,9 +219,9 @@ export class InboxService { await this.documentRepo.delete(doc.Id); try { await fs.rm(dir, { recursive: true, force: true }); - } catch (err: any) { + } catch (err: unknown) { this.logger.warn( - `Dokument-Ordner konnte nicht gelöscht werden (${dir}): ${err.message}`, + `Dokument-Ordner konnte nicht gelöscht werden (${dir}): ${getErrorMessage(err)}`, ); } } diff --git a/paperless-backend/src/kontonummern/kontonummern.service.ts b/paperless-backend/src/kontonummern/kontonummern.service.ts index d9422dc..ae65f2a 100644 --- a/paperless-backend/src/kontonummern/kontonummern.service.ts +++ b/paperless-backend/src/kontonummern/kontonummern.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Kontonummer } from '../database/entities'; diff --git a/paperless-backend/src/label-print-agent/label-print-agent.service.ts b/paperless-backend/src/label-print-agent/label-print-agent.service.ts index 617bba2..a8f26c1 100644 --- a/paperless-backend/src/label-print-agent/label-print-agent.service.ts +++ b/paperless-backend/src/label-print-agent/label-print-agent.service.ts @@ -10,6 +10,7 @@ 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 { @@ -95,8 +96,10 @@ export class LabelPrintAgentService { const res = await fetch(url); const text = (await res.text()).trim(); vars['number'] = text; - } catch (err: any) { - this.logger.warn(`GET-URL fehlgeschlagen (${url}): ${err.message}`); + } catch (err: unknown) { + this.logger.warn( + `GET-URL fehlgeschlagen (${url}): ${getErrorMessage(err)}`, + ); } } else { this.logger.warn(`GET-URL übersprungen (ungültiges Protokoll): ${url}`); @@ -149,9 +152,9 @@ export class LabelPrintAgentService { candidate.LabelVariables ?? {}, ); await this.jobRepo.save(candidate); - } catch (err: any) { + } catch (err: unknown) { this.logger.error( - `Label-Rendering fehlgeschlagen für Job ${candidate.Id}: ${err.message}`, + `Label-Rendering fehlgeschlagen für Job ${candidate.Id}: ${getErrorMessage(err)}`, ); } } @@ -257,8 +260,10 @@ export class LabelPrintAgentService { } try { await fetch(url, { method: 'POST' }); - } catch (err: any) { - this.logger.warn(`${type}-URL fehlgeschlagen (${url}): ${err.message}`); + } catch (err: unknown) { + this.logger.warn( + `${type}-URL fehlgeschlagen (${url}): ${getErrorMessage(err)}`, + ); } } } diff --git a/paperless-backend/src/label-print-agent/label-renderer.service.ts b/paperless-backend/src/label-print-agent/label-renderer.service.ts index 4dba455..e40cccf 100644 --- a/paperless-backend/src/label-print-agent/label-renderer.service.ts +++ b/paperless-backend/src/label-print-agent/label-renderer.service.ts @@ -2,6 +2,7 @@ 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 @@ -84,9 +85,9 @@ export class LabelRendererService { parts.push( ``, ); - } catch (err: any) { + } catch (err: unknown) { this.logger.warn( - `QR-Code-Rendering fehlgeschlagen für "${content}": ${err.message}`, + `QR-Code-Rendering fehlgeschlagen für "${content}": ${getErrorMessage(err)}`, ); } } else if (el.type === 'line') { diff --git a/paperless-backend/src/paperless/paperless-processor.service.ts b/paperless-backend/src/paperless/paperless-processor.service.ts index 9a54b6f..dbb5442 100644 --- a/paperless-backend/src/paperless/paperless-processor.service.ts +++ b/paperless-backend/src/paperless/paperless-processor.service.ts @@ -7,6 +7,7 @@ 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 { @@ -49,13 +50,14 @@ export class PaperlessProcessorService { ); // Postprocessing nach dem Speichern evaluieren await this.postprocessingService.evaluate(updatedDoc || doc); - } catch (innerErr: any) { + } catch (innerErr: unknown) { this.logger.error( - `Fehler bei Dokument ID ${doc.id}: ${innerErr.message}`, + `Fehler bei Dokument ID ${doc.id}: ${getErrorMessage(innerErr)}`, ); - if (innerErr.response?.data) { + const responseData = getResponseData(innerErr); + if (responseData) { this.logger.error( - `Paperless API Response: ${JSON.stringify(innerErr.response.data)}`, + `Paperless API Response: ${JSON.stringify(responseData)}`, ); } } diff --git a/paperless-backend/src/paperless/paperless.service.ts b/paperless-backend/src/paperless/paperless.service.ts index 4a7b33d..ba69dbe 100644 --- a/paperless-backend/src/paperless/paperless.service.ts +++ b/paperless-backend/src/paperless/paperless.service.ts @@ -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 = require('form-data'); +import FormData from '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: any) { - const body = err?.response?.data; + } catch (err: unknown) { + const body = axios.isAxiosError(err) ? err.response?.data : undefined; if (body) { this.logger.error( `Paperless updateDocument(${id}) Fehlerdetails: ${JSON.stringify(body)}`, diff --git a/paperless-backend/src/postprocessing/export.service.ts b/paperless-backend/src/postprocessing/export.service.ts index 2216677..3bf329c 100644 --- a/paperless-backend/src/postprocessing/export.service.ts +++ b/paperless-backend/src/postprocessing/export.service.ts @@ -4,6 +4,7 @@ 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 { @@ -57,8 +58,8 @@ export class ExportService { }; } return { success: true, message: 'Verbindung erfolgreich.' }; - } catch (err: any) { - return { success: false, message: err.message }; + } catch (err: unknown) { + return { success: false, message: getErrorMessage(err) }; } } diff --git a/paperless-backend/src/postprocessing/postprocessing.service.ts b/paperless-backend/src/postprocessing/postprocessing.service.ts index 5c8d6b0..679fd77 100644 --- a/paperless-backend/src/postprocessing/postprocessing.service.ts +++ b/paperless-backend/src/postprocessing/postprocessing.service.ts @@ -13,6 +13,7 @@ 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; @@ -91,12 +92,18 @@ export class PostprocessingService { 'success', `Aktion ${action.ActionType} erfolgreich`, ); - } catch (err: any) { + } catch (err: unknown) { hasError = true; this.logger.error( - `Fehler bei Aktion ${action.Id} (Typ ${action.ActionType}) für Dokument ${doc.id}: ${err.message}`, + `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), ); - await this.log(rule.Id, action.Id, doc.id, 'error', err.message); } } @@ -107,9 +114,9 @@ export class PostprocessingService { await this.paperlessService.updateDocument(doc.id, { tags: Array.from(currentTags), }); - } catch (tagErr: any) { + } catch (tagErr: unknown) { this.logger.error( - `Konnte Error-Tag ${this.errorTagId} nicht setzen für Dokument ${doc.id}: ${tagErr.message}`, + `Konnte Error-Tag ${this.errorTagId} nicht setzen für Dokument ${doc.id}: ${getErrorMessage(tagErr)}`, ); } } @@ -313,8 +320,8 @@ export class PostprocessingService { const dt = docTypes.find((x: any) => x.id === doc.document_type); doc._documentTypeName = dt?.name ?? ''; } - } catch (err: any) { - this.logger.warn(`Konnte Namen nicht auflösen: ${err.message}`); + } catch (err: unknown) { + this.logger.warn(`Konnte Namen nicht auflösen: ${getErrorMessage(err)}`); } } diff --git a/paperless-backend/src/preprocessing/document-pipeline.service.ts b/paperless-backend/src/preprocessing/document-pipeline.service.ts index 5912309..4a7e7ab 100644 --- a/paperless-backend/src/preprocessing/document-pipeline.service.ts +++ b/paperless-backend/src/preprocessing/document-pipeline.service.ts @@ -65,8 +65,8 @@ export class DocumentPipelineService { } // 3. OCR auf erster Seite - const ocrMarkdown = - await this.ocrService.extractTextAsMarkdown(firstPageBuffer); + // TODO: OCR-Ergebnis wird aktuell nicht gespeichert/weiterverwendet + await this.ocrService.extractTextAsMarkdown(firstPageBuffer); // 4. Task in DB erstellen const year = new Date().getFullYear(); diff --git a/paperless-backend/src/preprocessing/ocr.service.ts b/paperless-backend/src/preprocessing/ocr.service.ts index 6eb2822..24bffdf 100644 --- a/paperless-backend/src/preprocessing/ocr.service.ts +++ b/paperless-backend/src/preprocessing/ocr.service.ts @@ -1,6 +1,7 @@ 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 { @@ -46,8 +47,8 @@ Antworte nur mit dem extrahierten Markdown-Text, keine Erklärungen.`; `OCR abgeschlossen: ${markdown.length} Zeichen extrahiert`, ); return markdown; - } catch (error: any) { - this.logger.error(`Ollama OCR fehlgeschlagen: ${error.message}`); + } catch (error: unknown) { + this.logger.error(`Ollama OCR fehlgeschlagen: ${getErrorMessage(error)}`); throw error; } } diff --git a/paperless-backend/src/preprocessing/pdf.service.ts b/paperless-backend/src/preprocessing/pdf.service.ts index 2baf0f0..9a7c70f 100644 --- a/paperless-backend/src/preprocessing/pdf.service.ts +++ b/paperless-backend/src/preprocessing/pdf.service.ts @@ -120,7 +120,9 @@ export class PdfService { for (const imgPath of imagePaths) { try { await fs.unlink(imgPath); - } catch {} + } catch { + // Datei bereits entfernt o. Ä.: ignorieren + } dirs.add(path.dirname(imgPath)); } for (const dir of dirs) { diff --git a/paperless-backend/src/preprocessing/qr-code.service.ts b/paperless-backend/src/preprocessing/qr-code.service.ts index 418496a..2122a8a 100644 --- a/paperless-backend/src/preprocessing/qr-code.service.ts +++ b/paperless-backend/src/preprocessing/qr-code.service.ts @@ -1,5 +1,5 @@ import { Injectable, Logger } from '@nestjs/common'; -import sharp = require('sharp'); +import sharp from 'sharp'; import jsQR from 'jsqr'; export interface QrCodeResult { diff --git a/paperless-backend/src/scanner/scanner-watcher.service.ts b/paperless-backend/src/scanner/scanner-watcher.service.ts index fb59965..f65d065 100644 --- a/paperless-backend/src/scanner/scanner-watcher.service.ts +++ b/paperless-backend/src/scanner/scanner-watcher.service.ts @@ -18,6 +18,7 @@ import { InboxDocument, type InboxSource, } from '../database/entities/inbox-document.entity'; +import { getErrorCode, getErrorMessage } from '../common/error.util'; const STABILITY_MS = 5000; @@ -63,7 +64,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: { @@ -74,9 +75,9 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy { }); this.watcher - .on('add', (filePath: string) => this.handleNewFile(filePath)) + .on('add', (filePath: string) => void this.handleNewFile(filePath)) .on('error', (error: Error) => - this.logger.error(`Watcher Fehler: ${error.message}`), + this.logger.error(`Watcher Fehler: ${getErrorMessage(error)}`), ); this.logger.log('Scanner-Watcher aktiv'); @@ -96,10 +97,10 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy { withFileTypes: true, }); subdirs = entries.filter((e) => e.isDirectory()).map((e) => e.name); - } catch (err: any) { + } catch (err: unknown) { if (!silent) { this.logger.warn( - `Scanner-Check: Quellverzeichnis nicht lesbar (${this.sourceRoot}): ${err.message}`, + `Scanner-Check: Quellverzeichnis nicht lesbar (${this.sourceRoot}): ${getErrorMessage(err)}`, ); } return; @@ -111,10 +112,10 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy { let files: string[]; try { files = await fs.readdir(dir); - } catch (err: any) { + } catch (err: unknown) { if (!silent) { this.logger.warn( - `Scanner-Check: ${dir} nicht lesbar: ${err.message}`, + `Scanner-Check: ${dir} nicht lesbar: ${getErrorMessage(err)}`, ); } continue; @@ -151,8 +152,8 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy { this.isPeriodicScanning = true; try { await this.initialScan(true); - } catch (err: any) { - this.logger.error(`Periodic Scan Fehler: ${err.message}`); + } catch (err: unknown) { + this.logger.error(`Periodic Scan Fehler: ${getErrorMessage(err)}`); } finally { this.isPeriodicScanning = false; } @@ -221,14 +222,14 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy { try { await this.barcodeScanner.scanAndMatch(doc); - } catch (err: any) { + } catch (err: unknown) { this.logger.warn( - `Barcode-Scan nach Move fehlgeschlagen (${id}): ${err.message}`, + `Barcode-Scan nach Move fehlgeschlagen (${id}): ${getErrorMessage(err)}`, ); } - } catch (err: any) { + } catch (err: unknown) { this.logger.error( - `Übernahme fehlgeschlagen für ${filePath}: ${err.message}`, + `Übernahme fehlgeschlagen für ${filePath}: ${getErrorMessage(err)}`, ); } finally { this.processing.delete(filePath); @@ -241,8 +242,10 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy { pending = await this.documentRepo.find({ where: [{ PageCount: 0 }, { QrCodes: IsNull() }], }); - } catch (err: any) { - this.logger.warn(`Backfill: DB-Query fehlgeschlagen: ${err.message}`); + } catch (err: unknown) { + this.logger.warn( + `Backfill: DB-Query fehlgeschlagen: ${getErrorMessage(err)}`, + ); return; } @@ -251,8 +254,10 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy { try { const didScan = await this.barcodeScanner.ensureScanned(doc); if (didScan) scanned += 1; - } catch (err: any) { - this.logger.warn(`Backfill fehlgeschlagen (${doc.Id}): ${err.message}`); + } catch (err: unknown) { + this.logger.warn( + `Backfill fehlgeschlagen (${doc.Id}): ${getErrorMessage(err)}`, + ); } } @@ -267,8 +272,8 @@ export class ScannerWatcherService implements OnModuleInit, OnModuleDestroy { try { await fs.rename(src, dest); return; - } catch (err: any) { - if (err.code !== 'EXDEV') throw err; + } catch (err: unknown) { + if (getErrorCode(err) !== 'EXDEV') throw err; } // Cross-device: copy + unlink. Wenn unlink scheitert, Kopie zurückrollen, // damit ein kaputter Mount nicht bei jedem Neustart Duplikate produziert. diff --git a/paperless-backend/src/settings/settings.controller.ts b/paperless-backend/src/settings/settings.controller.ts index 67fc92b..e0c7297 100644 --- a/paperless-backend/src/settings/settings.controller.ts +++ b/paperless-backend/src/settings/settings.controller.ts @@ -22,10 +22,7 @@ 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, - type InboxActionType, -} from '../database/entities/inbox-postprocessing-action.entity'; +import { InboxPostprocessingAction } 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'; diff --git a/paperless-backend/src/stats/stats.controller.ts b/paperless-backend/src/stats/stats.controller.ts index 2074362..7069fb3 100644 --- a/paperless-backend/src/stats/stats.controller.ts +++ b/paperless-backend/src/stats/stats.controller.ts @@ -1,12 +1,15 @@ 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: any) { - return this.statsService.getDashboardCounts(req.user?.preferredUsername); + async getCounts(@Request() req: AuthenticatedRequest) { + return this.statsService.getDashboardCounts( + req.user?.preferredUsername ?? undefined, + ); } } diff --git a/paperless-backend/src/user-settings/user-settings.controller.ts b/paperless-backend/src/user-settings/user-settings.controller.ts index fb5105b..ee1d844 100644 --- a/paperless-backend/src/user-settings/user-settings.controller.ts +++ b/paperless-backend/src/user-settings/user-settings.controller.ts @@ -7,38 +7,45 @@ import { Put, Request, } from '@nestjs/common'; -import { UserSettingsService } from './user-settings.service'; +import { + UserSettingsService, + type UpdateUserSettingsInput, +} 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: any) { + async getSettings(@Request() req: AuthenticatedRequest) { return this.userSettingsService.getSettings( - req.user.userId, - req.user.email, - req.user.preferredUsername, - req.user.groups, + req.user!.userId, + req.user!.email, + req.user!.preferredUsername ?? undefined, + req.user!.groups, ); } @Put() - async updateSettings(@Request() req: any, @Body() body: any) { + async updateSettings( + @Request() req: AuthenticatedRequest, + @Body() body: UpdateUserSettingsInput, + ) { return this.userSettingsService.updateSettings( - req.user.userId, + req.user!.userId, body, - req.user.email, - req.user.preferredUsername, - req.user.groups, + req.user!.email, + req.user!.preferredUsername ?? undefined, + req.user!.groups, ); } @Get('senders') - async getSenders(@Request() req: any) { - return this.userSettingsService.getAvailableSenders(req.user.userId); + async getSenders(@Request() req: AuthenticatedRequest) { + return this.userSettingsService.getAvailableSenders(req.user!.userId); } @Post('test-smtp') diff --git a/paperless-backend/src/user-settings/user-settings.service.ts b/paperless-backend/src/user-settings/user-settings.service.ts index 8afd147..d8104ef 100644 --- a/paperless-backend/src/user-settings/user-settings.service.ts +++ b/paperless-backend/src/user-settings/user-settings.service.ts @@ -5,6 +5,7 @@ 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'; @@ -22,6 +23,20 @@ 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); @@ -92,19 +107,7 @@ export class UserSettingsService { async updateSettings( userId: string, - 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; - }, + data: UpdateUserSettingsInput, email?: string, preferredUsername?: string, groups?: string[], @@ -160,8 +163,8 @@ export class UserSettingsService { try { await transporter.verify(); return { ok: true }; - } catch (err: any) { - return { ok: false, error: err.message }; + } catch (err: unknown) { + return { ok: false, error: getErrorMessage(err) }; } } diff --git a/paperless-backend/src/webhook/webhook.controller.ts b/paperless-backend/src/webhook/webhook.controller.ts index 8154711..6b22727 100644 --- a/paperless-backend/src/webhook/webhook.controller.ts +++ b/paperless-backend/src/webhook/webhook.controller.ts @@ -20,9 +20,9 @@ export class WebhookController { @Public() @Post('paperless') @HttpCode(HttpStatus.OK) - async handlePaperlessWebhook( - @Body() payload: PaperlessWebhookPayload, - ): Promise<{ status: string }> { + handlePaperlessWebhook(@Body() payload: PaperlessWebhookPayload): { + status: string; + } { this.logger.log( `Webhook empfangen: action=${payload.action}, document=${payload.document_id}`, ); diff --git a/paperless-frontend/eslint.config.js b/paperless-frontend/eslint.config.js index 5e6b472..ee8c5a9 100644 --- a/paperless-frontend/eslint.config.js +++ b/paperless-frontend/eslint.config.js @@ -19,5 +19,19 @@ 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', + }, + ], + }, }, ]) diff --git a/paperless-frontend/src/components/DocumentEditModal.tsx b/paperless-frontend/src/components/DocumentEditModal.tsx index 433c24d..f2c1f8b 100644 --- a/paperless-frontend/src/components/DocumentEditModal.tsx +++ b/paperless-frontend/src/components/DocumentEditModal.tsx @@ -121,7 +121,7 @@ export default function DocumentEditModal({ documentId, document, open, onClose, useEffect(() => { if (open && document) { - let customFieldsObj: any = {}; + const customFieldsObj: any = {}; if (document.customFields) { document.customFields.forEach(cf => { customFieldsObj[`cf_${cf.field}`] = cf.value; diff --git a/paperless-frontend/src/components/MailImportWizard.tsx b/paperless-frontend/src/components/MailImportWizard.tsx index e9465d8..e98c44d 100644 --- a/paperless-frontend/src/components/MailImportWizard.tsx +++ b/paperless-frontend/src/components/MailImportWizard.tsx @@ -322,7 +322,9 @@ export default function MailImportWizard({ visible, onClose, onSuccess, email, a try { const status = await emailImportApi.getJobStatus(jobId); if (status?.message) setImportStatus(status.message); - } catch {} + } catch { + // Status-Abfrage fehlgeschlagen: nächsten Poll abwarten + } }, 1500); try { const finalData = []; diff --git a/paperless-frontend/src/pages/SettingsPage.tsx b/paperless-frontend/src/pages/SettingsPage.tsx index 9e67688..ec84326 100644 --- a/paperless-frontend/src/pages/SettingsPage.tsx +++ b/paperless-frontend/src/pages/SettingsPage.tsx @@ -441,9 +441,10 @@ function DocTypeFieldsTable({ docTypeId }: { docTypeId: number }) { case 1: return Absender; case 2: return Belegdatum; case 3: return Ablagenummer; - case 4: + case 4: { const cf = customFields.find(c => c.id === record.TypeIndex); return {cf ? cf.name : `CF #${record.TypeIndex}`}; + } case 5: return Titel; default: return Unbekannt ({record.Type}); } diff --git a/paperless-frontend/src/utils/auth-resource.tsx b/paperless-frontend/src/utils/auth-resource.tsx index ac5b3d5..1bcaf11 100644 --- a/paperless-frontend/src/utils/auth-resource.tsx +++ b/paperless-frontend/src/utils/auth-resource.tsx @@ -8,6 +8,8 @@ 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; }