Commit Graph

282 Commits

Author SHA1 Message Date
schalli f43e92c49f test(09-06): RED — failing mergeCerts spec (PEM chain + password-PFX round-trip)
- 4 new mergeCerts tests: PEM chain 2 blocks, PFX round-trip with password,
  missing PFX password → BadRequestException, garbage input → BadRequestException
- Controller enforces 2-file minimum; service tests use 1-2 files directly
- All 4 fail against NotImplementedException stub (RED confirmed)
- Prior 23 tests remain green
2026-07-02 07:47:28 +02:00
schalli 59694642dd feat(09-05): implement convertCert + wire POST /convert (GREEN)
- Add FileResponse interface and FORMAT_MIME map to service
- Implement convertCert: parses any input format (PEM/DER/PFX/P7B) via
  same logic as parseCert; serializes to pem/der/p7b targetFormat
- DER output uses bytesToHex→Buffer.from(hex,'hex') to avoid utf-8
  corruption (Pitfall 1 / T-09-06)
- P7B output: pkcs7.createSignedData + pem.encode (PEM-wrapped PKCS7)
- Wrap all forge ops in try/catch → BadRequestException (T-09-01)
- Controller: add @Body('pemText') + reject when neither file nor pemText
- Fix: re-add NotImplementedException import for mergeCerts stub
- All 23 API cert-manager tests green (including 4 new convertCert)
2026-07-02 07:37:41 +02:00
schalli 37db58b816 test(09-05): add failing convertCert spec (RED)
- PEM→DER round-trip identity test (re-parses DER base64 → verify CN)
- DER→PEM round-trip identity test (re-parses PEM base64 → verify CN)
- PEM→P7B: asserts mimeType + P7B contains ≥1 cert
- malformed input: expects BadRequestException
- All 4 fail against NotImplementedException stub (RED confirmed)
2026-07-02 07:35:51 +02:00
schalli 2c4ada347c feat(09-04): GREEN — implement splitCerts + SplitResponse interface
Tessera CI/CD / Tests (push) Has been cancelled
Tessera CI/CD / Build & Publish Images (push) Has been cancelled
Tessera CI/CD / Lint & Type Check (push) Has been cancelled
- Export SplitEntry + SplitResponse interfaces
- splitCerts: PEM chain path via parsePemChain; P7B path via messageFromPem (PEM) or messageFromAsn1 (DER)
- Each cert entry: index, filename cert-N.pem, content base64 PEM, subject.cn, validity.notAfter
- BadRequestException on malformed input / unsupported format (T-09-01)
- POST /split already wired in controller with 5MB file limit (T-09-03, T-09-04)
- All 19 API tests green; type-check clean
2026-07-02 07:14:22 +02:00
schalli eec66311a7 test(09-04): RED — failing splitCerts spec (fullchain PEM, P7B bundle, malformed)
- splitCerts fullchain PEM: expects count 2, two single-PEM-block certs with correct CN
- splitCerts P7B PEM bundle: expects at least one cert in result
- splitCerts malformed input: expects BadRequestException
- All three tests FAIL against NotImplementedException stub (RED confirmed)
- All 16 prior tests still pass
2026-07-02 07:12:37 +02:00
schalli ba994635e8 feat(09-03): GREEN — implement parseCert + export CertDetails interface
- Implemented CertManagerService.parseCert for PEM/DER/PFX/P7B inputs
- Exported CertDetails interface (subject, issuer, validity, san, keyType, keyBits, serialNumber, signatureAlgorithm, fingerprint, pemPreview)
- PFX with wrong password → BadRequestException (T-09-02: never logged, never echoed)
- All forge operations wrapped in try/catch → BadRequestException (T-09-01)
- buildReverseOids() converts OID → human-readable algorithm name
- P7B handles both PEM-wrapped and binary DER (RESEARCH Pitfall 4)
- Controller already wired correctly from Plan 01 (fileSize 5MB, pemText, file, password)
- All 16 cert-manager tests pass (16/16)
2026-07-01 23:41:03 +02:00
schalli 7c2e506a2e test(09-03): RED — failing parseCert spec (PEM/DER/PFX/wrong-password/malformed)
Tessera CI/CD / Tests (push) Has been cancelled
Tessera CI/CD / Build & Publish Images (push) Has been cancelled
Tessera CI/CD / Lint & Type Check (push) Has been cancelled
- Added parseCert describe block with 5 failing tests
- Covers PEM input, DER input, PFX+correct-password, PFX+wrong-password (BadRequestException), malformed input (BadRequestException)
- Existing 11 helper tests still pass
- Fixtures built via node-forge (RSA-1024, DER from asn1.toDer, PFX via toPkcs12Asn1)
2026-07-01 23:39:39 +02:00
schalli 637f4674ca merge(09-01): resolve app.module.ts conflict (CertManagerModule + FavoritesModule)
Tessera CI/CD / Lint & Type Check (push) Failing after 37s
Tessera CI/CD / Tests (push) Has been skipped
Tessera CI/CD / Build & Publish Images (push) Has been skipped
2026-07-01 23:25:48 +02:00
schalli 8bb5cf208d feat(09-01): scaffold cert-manager module + shared node-forge helpers (GREEN)
- cert-manager.module.ts: OnModuleInit + seedCertManagerModule (CERT-06)
- cert-manager.seed.ts: slug='cert-manager', category='security-tools', isSystem=true
- cert-manager.service.ts: detectFormat, toForgeBuffer, getFingerprint, parsePemChain;
  operation stubs parseCert/splitCerts/mergeCerts/convertCert throw NotImplementedException
- cert-manager.controller.ts: 4 POST routes with FileInterceptor/FilesInterceptor
  (5 MB limit each), @UseModule('cert-manager') guard, BadRequestException on missing input
- dto/: ParseCertDto, MergeCertsDto, ConvertCertDto
- app.module.ts: CertManagerModule added to imports array
- All 11 Vitest tests pass; type-check clean
2026-07-01 23:21:36 +02:00
schalli a06694f915 test(09-01): add failing spec for cert-manager seed + helpers (RED)
- Test: seedCertManagerModule calls seedModule with slug='cert-manager',
  category='security-tools', isSystem=true
- Test: detectFormat returns pem/der/pfx/p7b based on extension + content sniff
- Test: getFingerprint returns uppercase colon-separated hex (sha1 + sha256)
- Test: parsePemChain returns array of length 2 for two concatenated PEMs
2026-07-01 23:18:08 +02:00
schalli a13a8a763f chore(09-01): install node-forge + Vitest runner for @tessera/api
- Add node-forge@^1.4.0 runtime dependency (certificate crypto)
- Add @types/node-forge@^1.3.14 and vitest@^3 dev dependencies
- Create apps/api/vitest.config.ts (environment: node, passWithNoTests)
- Add test + test:watch scripts to apps/api/package.json
2026-07-01 23:17:23 +02:00
schalli e35276243a fix(calendar): EWS uses NTLM auth + edit form stays open after save
- Replace ews-javascript-api (Basic Auth only) with httpntlm for EWS connections
- testEwsConnection uses GetFolder SOAP via NTLM
- fetchViaEws uses FindItem CalendarView SOAP via NTLM
- Edit form no longer auto-closes on save — shows "Erfolgreich gespeichert" instead
- Test button in edit mode uses saved credentials via /sources/:id/test endpoint
- Add saveSuccess i18n key (de/en)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 14:05:07 +02:00
schalli 51d8c2f14e fix(calendar): SSRF exception for Exchange + error messages + domain in edit
- SSRF check skipped for Exchange type (internal EWS servers are common)
- testConnectionFromConfig catches SSRF/validation errors, returns {success:false,error} instead of throwing 403
- updateSource reads existing.type to determine effective type for SSRF check
- Panel shows saveError/editSaveError on failed add/update
- Edit form initialValues now includes domain field
- i18n: calendar.saveError key added (de+en)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 13:41:32 +02:00
schalli 42daa87e5e feat(calendar): add domain field and test-connection button to Exchange sources
- Prisma: domain String? added to CalendarSource model (db push applied)
- DTOs: domain in CreateCalendarSourceDto, UpdateCalendarSourceDto, new TestCalendarSourceConfigDto
- Service: domain in SOURCE_SAFE_SELECT, addSource, updateSource; new testConnectionFromConfig method
- Controller: POST /calendar/sources/test-config (before :id routes to avoid collision)
- ExchangeProvider: domain in all source interfaces; passed as 3rd arg to EWS WebCredentials
- Frontend: domain in CalendarSource/CreateSourcePayload/UpdateSourcePayload; testSourceConfig API fn
- Form: domain field (Exchange-only), "Test connection" button with idle/loading/success/error states
- i18n: de+en keys for formFieldDomain, formFieldDomainHint, formTestConnection, formTesting, formTestSuccess, formTestFailed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 13:15:13 +02:00
schalli eebceb298d fix(08): apply code review findings (CR-01, CR-02, WR-01–05, IN-01)
- CR-01: fix SSRF bypass — isPrivateIpv6 now delegates ::ffff:<ipv4> to
  isPrivateIpv4, covering 172.16-31.x and 169.254.x ranges
- CR-02: add ParseUUIDPipe to GET /favorites widgetId param + service guard
  so missing widgetId returns 400 instead of leaking all user favorites
- WR-01: link-widget — replace raw 'link.error' key with t('link.error') (4 sites)
- WR-02: favorites-widget — fix load-path error to use t('favorites.error')
- WR-03: widget-catalog-modal — move aria-hidden from outer wrapper to backdrop
- WR-04: calculator — remove duplicate M button (MR clone); MC/MR/M+/M−/MS remain
- WR-05: schema — add FavoriteLink→WidgetInstance FK with onDelete:Cascade
- IN-01: create-widget.dto.ts — update comment from four to eight supported types

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 11:03:16 +02:00
schalli 758d246e98 feat(08-03): FavoriteLink schema + FavoritesModule (CRUD + SSRF icon discovery)
- Add FavoriteLink Prisma model (userId/tenantId/widgetId scope, iconUrl nullable, position)
- IconDiscoveryService: port SSRF-protected icon discovery with redirect: 'manual',
  private IP / blocked-hostname checks, 4000ms timeout, 200k HTML cap (T-08-05)
- FavoritesService: list/create/update/remove all scoped by userId (T-08-06 / Pitfall 3)
- FavoritesController: GET /favorites?widgetId, POST, PATCH :id, DELETE :id
- FavoritesModule registered in AppModule
- tsc --noEmit passes for @tessera/api
2026-07-01 10:25:52 +02:00
schalli 63ec93bd35 feat(08-01): registry foundation for 4 new widget types + Calculator widget (GREEN)
- widget-registry.tsx: extend WidgetType union with calculator/favorites/link/stopwatch
- widget-registry.tsx: add WIDGET_CONSTRAINTS entries with per-widget grid constraints (DASH-11)
- widget-registry.tsx: add SVG icons (CalculatorIcon, FavoritesIcon, LinkIcon, StopwatchIcon)
- widget-registry.tsx: add WIDGET_REGISTRY entries and wire functions for all 4 new types
- calculator-widget.tsx: full arithmetic implementation ported from personal-dashboard
  (parseDisplay, formatNumber, calculate, keyboard handler with stopPropagation)
- widget-catalog-modal.tsx: extend WIDGET_TYPES to include all 8 types
- create-widget.dto.ts: extend @IsIn to accept 8 widget types (T-08-01 mitigated)
- page.tsx: import CalculatorWidget and call wireCalculatorWidget()
- de.json / en.json: add i18n keys for calculator, favorites, link, stopwatch
- All 16 tests passing (GREEN)
2026-07-01 09:57:44 +02:00
schalli 0fba45d2c2 feat(quick-260630-gbh-01): avatar storage endpoints + enriched /auth/me
- Add avatarPath String? column to User model (migration: add_user_avatar)
- POST /users/me/avatar: 2MB limit, image/png/jpeg/webp allowlist, writes to user-files/avatars/{userId}.{ext}
- GET /users/me/avatar: streams avatar with Cache-Control: no-store
- AuthService.getMe(): returns isLocalUser + hasAvatar without leaking passwordHash/ldapDn
- AuthController GET /auth/me: now returns enriched profile via getMe()
2026-06-30 11:57:33 +02:00
schalli dcba4b9977 fix(dkv): search msgfolderroot for EWS subfolder resolution
Tessera CI/CD / Lint & Type Check (push) Successful in 43s
Tessera CI/CD / Tests (push) Successful in 47s
Tessera CI/CD / Build & Publish Images (push) Successful in 24s
FindFolder was searching only under inbox DistinguishedFolderId, missing
folders at mailbox root level. Now searches msgfolderroot (full mailbox)
so custom folders like DKV are found regardless of placement.

Also adds HTTP status check and debug logging for FindFolder responses.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 10:23:30 +02:00
schalli 30eb40c184 feat(dkv): Exchange subfolder support via EWS FindFolder
Tessera CI/CD / Lint & Type Check (push) Successful in 40s
Tessera CI/CD / Tests (push) Successful in 39s
Tessera CI/CD / Build & Publish Images (push) Successful in 22s
Custom folder names (e.g. "DKV" or "INBOX/DKV") now resolved by calling
EWS FindFolder deep-search under inbox. Well-known names still map to
DistinguishedFolderId directly. Falls back to inbox with a warning log
when the subfolder cannot be found.

IMAP already supported subfolder paths natively via ImapFlow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 10:04:24 +02:00
schalli b5bf3ed8c0 fix(dkv): return username in GET /dkv/config response
Tessera CI/CD / Lint & Type Check (push) Successful in 40s
Tessera CI/CD / Tests (push) Successful in 37s
Tessera CI/CD / Build & Publish Images (push) Successful in 22s
loadConfig used CONFIG_SAFE_SELECT which excludes encryptedInboxCreds entirely,
so username was never returned to the frontend — form always showed empty username.

Added getConfigForApi() which loads the safe config + decrypts encryptedInboxCreds
to extract username (never password) and adds hasPassword boolean. Controller
getConfig now calls getConfigForApi instead of loadConfig.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 09:58:59 +02:00
schalli a44e40f101 fix(dkv): correct invoice date extraction; add Exchange IsRead filter + mark-as-read
Tessera CI/CD / Lint & Type Check (push) Successful in 44s
Tessera CI/CD / Tests (push) Successful in 44s
Tessera CI/CD / Build & Publish Images (push) Successful in 21s
Date fix: previous regex matched payment-due date ("10 Tage nach Rechnungsdatum...
10.04.2026") instead of actual Rechnungsdatum. New approach anchors on the
invoice number line (DD/DDDDDDDDD/DDD) and takes the date on the next line,
which is always the actual Rechnungsdatum in DKV PDFs.

Exchange dedup: FindItem now filters IsRead=false (combined with sender filter
via <t:And>), so already-processed emails are skipped automatically.
After downloading attachments, UpdateItem marks the message as read
(using ItemId + ChangeKey from GetItem response), mirroring IMAP \Seen behavior.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 09:47:17 +02:00
schalli 66ffad149e fix(dkv): extract invoice number/date from PDF, rename export files to RG-DKV format
Tessera CI/CD / Lint & Type Check (push) Successful in 41s
Tessera CI/CD / Tests (push) Successful in 38s
Tessera CI/CD / Build & Publish Images (push) Successful in 23s
- Parser now extracts Rechnungsnummer (DD/DDDDDDDDD/DDD) and Rechnungsdatum
  from PDF text, so filename doesn't rely on email subject
- Export filename changed from DKV_YYYY-MM_... to RG-DKV-{nr}-{YYMMDD}.xlsx
  e.g. RG-DKV-26-650869002-002-260331.xlsx
- Subject fallback now also matches slash-separated invoice numbers (26/NNN/NNN)
- writeAndPrune simplified to accept baseName instead of separate fields
- Validation regex and prune prefix updated to match new RG-DKV- pattern

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 09:39:51 +02:00
schalli 5c1aa03270 fix(dkv): handle EV charging rows and service rows in single-tx tab parser
Tessera CI/CD / Lint & Type Check (push) Successful in 40s
Tessera CI/CD / Tests (push) Successful in 37s
Tessera CI/CD / Build & Publish Images (push) Successful in 21s
Analyzed Invoice-4302486921-26_650869002_000.pdf text structure. Three cases:

1. FUEL (fields[4] = numeric tx-nr): km+product merged in fields[5], unit in
   fields[6]. Already working; no change.

2. EV CHARGING (fields contains "DDDD KWH" or "DDDD MIN" unit): column layout
   shifts — no km field, station+ort sometimes merged in fields[1]. Detected by
   regex on unit field; kwhIdx drives relative offset for menge/netto/brutto.
   Ort extracted from fields[2] (kwhIdx>=5) or fields[1] (kwhIdx=4, compact).
   Kilometerstand = 0 (EV chargers don't record odometer).

3. SERVICE ROWS (e.g. "DKV Analytics Premiu"): appear inside a VEHICLE: block but
   fields[4] is non-numeric (product description, not a transaction number). These
   were being parsed as fake vehicle transactions producing wrong ort/km values.
   Now filtered out (return null).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 09:20:12 +02:00
schalli a759e816a0 fix(dkv): fix Kennzeichen matching and NaN/invalid km values in Excel export
Tessera CI/CD / Lint & Type Check (push) Successful in 43s
Tessera CI/CD / Tests (push) Successful in 38s
Tessera CI/CD / Build & Publish Images (push) Successful in 23s
Three issues fixed:

1. Kennzeichen normalization: DKV PDF extracts plates without hyphens
   ("GP JL 740E" vs CSV-imported "GP-JL 740E"). Added _normalizeKennzeichen()
   which strips hyphens, spaces, and dots before lookup — resolves vehicle
   master match failure that caused Marke/Modell/Fahrer to appear empty.

2. Empty-string NaN: parser used ?? '0' which doesn't catch empty strings,
   causing parseDE('') = NaN. Changed to || '0' for km, menge, and totals.

3. Invalid km values: EV charging rows from DKV have misaligned columns —
   km position contains a decimal price (e.g. 18.64 EUR or kWh). Added
   sanity check: non-integer km values are written as null (empty cell)
   instead of a misleading decimal. ExportRow.kilometerstand is now number|null.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 09:12:13 +02:00
schalli c9328f60b7 fix(api): create /app/user-files with nestjs ownership in Dockerfile
Tessera CI/CD / Lint & Type Check (push) Successful in 39s
Tessera CI/CD / Tests (push) Successful in 39s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m36s
Directory must exist before nestjs user takes over — otherwise DkvExportService
cannot write xlsx export files and throws EACCES on first inbox processing run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 08:44:21 +02:00
schalli ccfd3f21cf fix(dkv): fix EWS attachment ID extraction — FileAttachment has no Id attribute
Tessera CI/CD / Lint & Type Check (push) Successful in 44s
Tessera CI/CD / Tests (push) Successful in 37s
Tessera CI/CD / Build & Publish Images (push) Successful in 28s
extractAttrs(block, 't:FileAttachment', 'Id') always returned empty array
because the attachment Id lives in a child <t:AttachmentId Id="..."/>, not
on the <t:FileAttachment> tag itself. This caused all Exchange inbox checks
to silently find zero PDF attachments and report "no matching emails".

Fixed by iterating FileAttachment blocks individually and extracting
t:AttachmentId/@Id from within each block. Also added filename (.pdf)
as fallback when ContentType is application/octet-stream.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 08:41:17 +02:00
schalli 9efa3bab1d fix(dkv): fix inbox processing pipeline — orphan tenant, MIME detection, UNSEEN filter
Tessera CI/CD / Lint & Type Check (push) Successful in 42s
Tessera CI/CD / Tests (push) Successful in 39s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m37s
- Fix orphaned DkvModuleConfig: tenantId pointed to deleted tenant, updated to Default tenant
- DKV controller: return 404 instead of HTTP 200 null when no config exists
- IMAP provider: also detect PDFs sent as application/octet-stream (check filename extension)
- IMAP provider: add seen:false filter so already-processed emails are skipped on re-poll
- IMAP provider: mark email as \Seen after successful PDF download to prevent reprocessing
- Frontend dkv-api: handle 404 from fetchConfig as "not yet configured" (returns null)
- InboxConfigForm: show warning banner when config not yet saved in DB
- InboxConfigForm: add "Jetzt prüfen" button to manually trigger POST /dkv/check-now

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 08:31:36 +02:00
schalli 5779f0f6c9 fix(api): reissue JWT with mustChangePassword=false after password change
Tessera CI/CD / Lint & Type Check (push) Successful in 39s
Tessera CI/CD / Tests (push) Successful in 36s
Tessera CI/CD / Build & Publish Images (push) Successful in 23s
After a successful password change the old cookie still contained
mustChangePassword=true, causing the middleware to redirect back to
/change-password. Now changePassword issues a fresh session cookie.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 16:09:36 +02:00
schalli b28ee472c5 fix(api): use user.id instead of user.sub in changePassword controller
Tessera CI/CD / Lint & Type Check (push) Successful in 41s
Tessera CI/CD / Tests (push) Successful in 39s
Tessera CI/CD / Build & Publish Images (push) Successful in 21s
JWT strategy maps payload.sub to user.id — user.sub was always undefined,
causing Prisma findUnique to fail with id: undefined validation error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 16:02:38 +02:00
schalli 24868ec1b8 chore(db): add migration for all tables missing from history
Tessera CI/CD / Lint & Type Check (push) Successful in 41s
Tessera CI/CD / Tests (push) Successful in 39s
Tessera CI/CD / Build & Publish Images (push) Successful in 28s
Captures DashboardLayout, WidgetInstance, SearchProvider, CalendarSource,
DkvModuleConfig, DkvVehicleMaster, DkvInvoiceHistory, SmtpConfig.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 15:17:27 +02:00
schalli 27c3abf3df fix(deploy): correct prisma binary path in API startup
Tessera CI/CD / Lint & Type Check (push) Successful in 44s
Tessera CI/CD / Tests (push) Successful in 39s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m40s
pnpm puts package binaries in apps/api/node_modules/.bin/, not root.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 15:08:07 +02:00
schalli e26fbd720e fix(deploy): embed prisma migrate deploy in API startup
Tessera CI/CD / Lint & Type Check (push) Successful in 45s
Tessera CI/CD / Tests (push) Successful in 42s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m52s
- Move prisma to runtime dependencies so it's available in prod image
- API runs migrate deploy before starting (handles fresh installs + updates)
- Remove separate migrate service from prod compose

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 14:50:42 +02:00
schalli a4e03830c1 fix(07): NTLM support for Exchange EWS + crypto key init timing fix
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
- ExchangeInboxProvider rewritten to use httpntlm + raw EWS SOAP:
  FindItem / GetItem / GetAttachment via NTLM challenge-response.
  No longer requires Basic Auth on Exchange EWS virtual directory.
  Folder name mapped to EWS DistinguishedFolderId (Inbox/SentItems/etc).
- CalendarCryptoService: move key init from onModuleInit to constructor
  so MailModule.forRootAsync() factory can call decrypt() before NestJS
  lifecycle hooks execute (startup crash when SmtpConfig row has password).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 10:02:48 +02:00
schalli 9b453e3ed3 fix(07): propagate Exchange connection error, add folder selection for Exchange
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
- testConnection now returns { success, message? } instead of boolean so
  admins see the actual EWS/IMAP error in the UI rather than "Unbekannter Fehler"
- Exchange provider: resolveFolder() maps folder string to WellKnownFolderName
  (Inbox, SentItems, DeletedItems, Drafts, JunkEmail + German aliases)
- InboxConfigForm: folder field now shown for both IMAP and Exchange protocols
  with Exchange-specific help text listing valid well-known names
- Controller returns testConnection result directly (no more redundant wrapping)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 09:33:13 +02:00
schalli 01ff137f43 fix(07): UAT fixes — SMTP test email, DKV routing, Exchange domain field
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
- SMTP: add test-to field, send real email via sendMail() instead of verify()
- SMTP: fix no_auth warning shown as error for open-relay servers
- DKV: register dkv-fleet in MODULE_REGISTRY (fixes "Modul nicht gefunden")
- DKV: add dynamic [category]/[moduleSlug]/settings + vehicles sub-routes
- DKV: settings/vehicles links use useParams for consistent URLs
- DKV: add gear icon settings link to module main page
- DKV: rename module to "DKV-Rechnung" in seed + translations
- DKV: add optional domain field for Exchange (WebCredentials 3rd arg)
- DKV: hide IMAP-only fields (Port/Verschlüsselung/Ordner) when Exchange selected
- DKV: hide Exchange-only field (Domain) when IMAP selected
- Prisma: add domain column to DkvModuleConfig

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 21:58:26 +02:00
schalli 4de87a8ea2 fix(07): SMTP test warns when connection passes but no auth configured
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Server reachable without credentials (port 25 open relay) returns
{ success: true, warning: 'no_auth' } instead of green success.
Frontend shows red warning: server reachable but emails will fail.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 01:29:53 +02:00
schalli 853095faef fix(07): SMTP test falls back to stored username and adds 10s timeouts
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Previously only the password fell back to stored value; username could be
missing if form field was cleared. Now both credentials fall back to the
stored config, ensuring auth is always tested when credentials exist.

Also adds explicit 10s timeouts to prevent indefinite hangs on unreachable
SMTP servers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 21:04:58 +02:00
schalli 8320a34035 fix(07): replace TenantMiddleware with TenantGuard to fix tenant context
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Middleware runs before guards in NestJS — req.user was always undefined
when TenantMiddleware executed, so req.tenantId was never set.

Convert to TenantGuard (APP_GUARD, registered after JwtAuthGuard) so it
runs after JWT validation and can read req.user.tenantId correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 20:57:03 +02:00
schalli dd5bc90395 fix(07): DKV/Settings module wiring and missing cron dep
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
- Add `cron@4.4.0` as direct dep (pnpm strict isolation blocks transitive access)
- Import SettingsModule in DkvModule so DkvMailService can inject SettingsService
- Fix dkv.service.ts return key: `count` → `imported` to match declared return type

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 20:28:06 +02:00
schalli d2d224cdd5 fix(07): WR-05 add 5 MB file size limit to CSV vehicle import endpoint
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
FileInterceptor used multer's default memory storage with no size limit.
An oversized file could exhaust Node.js heap before parsing begins.
Add fileSize: 5*1024*1024 (5 MB) — sufficient for any realistic vehicle list.
2026-06-27 17:22:27 +02:00
schalli 9de16babe4 fix(07): WR-04 sanitise Exchange UniqueId in invoice number fallback
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Exchange EWS UniqueIds are base64-encoded and can contain +, /, = characters.
When used as the fallback rechnungsnummer (email-{uid}), a slash would cause
path.join() to resolve into a subdirectory, making writeFileSync fail silently.
Sanitise uid to [a-zA-Z0-9-] before it reaches the filesystem write path.
2026-06-27 17:22:09 +02:00
schalli 49eab55abe fix(07): WR-02 add @Type(Number) coercion to pagination DTO fields
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
HTTP query params arrive as strings. Without @Type(() => Number),
class-transformer never coerces page/limit before @IsInt() runs,
causing HTTP 400 for any request that explicitly passes ?page or ?limit.
Also adds @Max(100) on limit to bound result-set size.
2026-06-27 17:21:28 +02:00
schalli ded652382d fix(07): WR-01 close nodemailer transport in finally to prevent pool leak
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Each sendExportEmail call created a new nodemailer transport which was never
closed, leaving the internal SMTP connection pool alive. With 3-retry backoff,
up to 3 leaked transports per invoice accumulate over time and can exhaust OS
socket limits. Add transport.close() in a finally block.
2026-06-27 17:21:01 +02:00
schalli cb0d378ded fix(07): CR-03 close IMAP connection when getMailboxLock throws
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
If getMailboxLock() failed (e.g. folder not found) the try/finally cleanup
block was never entered, leaving the ImapFlow connection open and leaking.
Move the lock acquisition inside the try block and use lock?.release() in
finally so client.logout() is always called regardless of lock success.
2026-06-27 17:20:39 +02:00
schalli 955a94638c fix(07): CR-02 correct cron expression for pollIntervalMin >= 60
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Has started running
Values >=60 in the cron minute field silently misbehave (*/60 fires once per
hour, */90 fires once per hour, etc.). Use the hours field for intervals >=60:
  <60 min  → */N * * * *
  >=60 min → 0 */H * * * (H = floor(N/60))
Also adds @Max(1440) to DkvConfigDto to bound the field at 24 h.
2026-06-27 17:20:11 +02:00
schalli f3f610f9e1 fix(07): CR-01 rename import return field imported→count to match frontend
Tessera CI/CD / Lint & Type Check (push) Failing after 37s
Tessera CI/CD / Tests (push) Has been skipped
Tessera CI/CD / Build & Deploy (push) Has been skipped
Backend dkv.service.ts returned { imported } but frontend read result.count,
causing the success toast to always display "undefined Fahrzeuge importiert".
Align backend field name to count and update the dkv-api.ts return type to
include mode for completeness.
2026-06-27 17:19:44 +02:00
schalli 2a3d1c1e85 feat(07-04): DkvModule + registry seed + AppModule registration + i18n keys
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Successful in 40s
Tessera CI/CD / Tests (push) Waiting to run
- dkv.seed.ts: seedDkvModule with slug=dkv-fleet, category=fleet, isSystem=true
- dkv.module.ts: imports ModuleRegistryModule + CalendarModule (CalendarCryptoService)
  provides all 7 DKV services + 2 providers + controller; OnModuleInit seeds registry
- app.module.ts: DkvModule added to imports (ScheduleModule + SettingsModule from Plans 01/03)
- de.json + en.json: complete dkvFleet namespace (50+ keys incl. status, col, form, errors)
- settings namespace extended with categoryGeneral, categorySmtp, smtp sub-object
2026-06-27 00:30:08 +02:00
schalli c22d36758c feat(07-04): DkvSchedulerService + DkvController — dynamic cron + REST surface
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Successful in 36s
Tessera CI/CD / Tests (push) Waiting to run
- DkvSchedulerService: SchedulerRegistry.addCronJob (dynamic interval, not static @Cron)
- onModuleInit loads first active config (v1 single-tenant, documented in SUMMARY)
- setInterval() replaces existing job and registers new one with */ cron expression
- stopJob() removes job when config.isActive=false
- cron package resolved via require() workaround (pnpm strict isolation: transitive dep)
- DkvController: 12 handlers all carrying @Roles(Role.ADMIN, Role.SUPER_ADMIN)
- Routes: GET/PUT config, POST check-now, POST test-connection, GET history,
  GET exports/:filename, GET/POST/PUT/DELETE vehicles, POST vehicles/import
- vehicles/import uses FileInterceptor('file') for CSV multipart upload
- exports/:filename streams file as attachment; traversal guard in DkvService
- Controller coordinates scheduler after PUT /dkv/config (no circular dep)
2026-06-27 00:28:18 +02:00
schalli c40a023321 feat(07-04): DkvService — pipeline orchestration + vehicle/config/history logic
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Successful in 38s
Tessera CI/CD / Tests (push) Waiting to run
- Single-flight guard (processing flag) prevents concurrent inbox processing
- processInbox: poll → 3-retry parse → driver-map → xlsx → 3-retry SMTP send → history
- Parse failure (D-10): records Fehler history row with errorMessage
- SMTP failure (D-16): exponential backoff 2s/4s, records Versand fehlgeschlagen
- CONFIG_SAFE_SELECT excludes encryptedInboxCreds (T-07-12)
- getExportFile rejects filenames with path separators or outside DKV_*.xlsx pattern (T-07-09)
- CSV import: merge (upsert by tenantId+kennzeichen) and replace (deleteMany then createMany) modes
- Invoice number extracted from email subject via regex; falls back to email-{uid}
- Vehicle format string resolved via DkvExportService.resolveFahrzeug (D-19)
2026-06-27 00:24:47 +02:00