- 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)
- 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
- 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)
- 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>
- 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>
- 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>
- 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()
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
- 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>
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>
- 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>
- 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>
- 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>
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>
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>
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>
- 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>
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.
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.
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.
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.
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.
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.
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.