Commit graph

68 commits

Author SHA1 Message Date
Thomas Reitz
da4a036e8a fix: PDF-Grünton dunkler + Buttons gleichbreit
- darkenColor() Funktion: extrahierte Logo-Farbe um 30% abdunkeln
  (gilt für PDF und DOCX Export) → kräftigerer, druckfreundlicher Ton
- ExpertProfileTab: min-width: 130px für btnPrimary und btnSecondary
  → alle Aktions-Buttons haben einheitliche Mindestbreite

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 14:29:00 +01:00
Thomas Reitz
dabd36349e fix: PDF Leerseite, Button-Größen und Primärfarbe
- PDF-Export: doc.flushPages() vor doc.end() verhindert leere
  Schlussseite (PDFKit bufferPages-Bug nach Footer-Loop)
- ExpertProfileTab: height: 32px für btnPrimary/Secondary/Danger
  sowie chipInput- und headerForm-Inputs → einheitliche Höhe
- Primärfarbe #1a56db → #1040bb (dunkler, besser zum Logo passend)
- LoginPage CSS-Fallback-Gradient ebenfalls auf neue Primärfarbe

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 14:07:20 +01:00
Thomas Reitz
1608f4e936 fix: PDF export — tab chars garbled, unconditional bullets, encrypted PDF attachments
Root causes identified via DB hex-dump and server logs:

1. Tab character in budget lines: DB stores e.g. `**Budget Verantwortung:**\t750.000 EUR`
   (byte 0x09 between `:**` and the amount). PDFKit can't render \t in WinAnsiEncoding,
   producing garbage output like `"sSãUU`. Fix: `.replace(/\t/g, ' ')` in cleaned text.

2. Unconditional bullet: `\u2022 ${sanitize(hasBullet ? cleaned : cleaned)}` always
   prepended `•` — the ternary was a no-op. Fix: only add `•` when hasBullet is true;
   `**...**` header lines now render as Helvetica-Bold without a bullet.

3. ITIL4 Foundation Cert.pdf is owner-password-encrypted. pdf-lib threw
   "Input document is encrypted" → cert was silently skipped.
   Fix: `PdfLib.load(attBuffer, { ignoreEncryption: true })`.

Applies to both PDF and DOCX export paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 13:56:57 +01:00
Thomas Reitz
98e7f48ce2 fix: make GET /settings/branding public to break login loading loop
LoginPage calls /settings/branding to load branding config (logo, colors).
Without @Public(), the JWT guard returns 401, which triggered the axios
response interceptor to attempt a silent refresh, fail, and call
window.location.href = '/login' — creating an infinite reload loop on
the login page itself.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:44:50 +01:00
Thomas Reitz
ad3a580d0b fix: resolve login loading loop caused by Vite HMR reconnects + rate limiting
- Add @SkipThrottle() to POST /auth/refresh so repeated silent-refresh calls
  from page reloads no longer exhaust the rate limit (HTTP 429)
- Configure Vite HMR explicitly with host/clientPort/protocol=wss so the
  WebSocket connects correctly through Traefik instead of reconnecting every ~1s

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:35:18 +01:00
Thomas Reitz
79ad5e4be3 fix(core): PDF-Anhänge korrekt einbetten via pdf-lib + Zeichenbereinigung verbessert
- pdf-lib installiert und importiert
- PDF-Anhänge werden nicht mehr als Platzhalter-Seite angezeigt, sondern
  alle Seiten des Anhang-PDFs direkt in das Export-PDF eingebettet (pdf-lib merge)
- Bild-Anhänge bleiben weiterhin per PDFKit eingebettet
- sanitizePdfText() erweitert: vollständige Windows-1252 U+0080-U+009F Mapping-Tabelle
  für mis-enkodierte Zeichen (€, Anführungszeichen, Gedankenstriche, TM usw.)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:23:03 +01:00
Thomas Reitz
9d7dcaaaea fix(core): PDF-Export Seitenumbruch und Sonderzeichen repariert
- pageBottom von 800 auf 740 reduziert: verhindert PDFKit-Auto-Seitenumbrüche
  mitten in Projekt-Einträgen (bisher: Datum/Rolle/Firma je auf eigener Seite)
- Vor jedem Projekt-Eintrag Header-Höhe (Datum+Rolle+Firma) vorberechnen und
  Seitenumbruch proaktiv auslösen, bevor der Header gezeichnet wird
- sanitizePdfText() Hilfsmethode: ersetzt €→EUR sowie Zeichen außerhalb Latin-1
  die Helvetica (WinAnsiEncoding) nicht rendern kann (bisher: Zeichensalat)
- sanitizePdfText auf Projekt-Texte und Zertifizierungs-Texte angewendet

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:08:56 +01:00
Thomas Reitz
90a0388b22 feat(core+frontend): Anhänge im Export + neues 3-Spalten-Layout im Profil-Reiter
- PDF-Export: alle Anhänge als zusätzliche Seiten (Bilder als Vorschau, andere mit Hinweis)
- DOCX-Export: Bild-Anhänge als zusätzliche Sections (je eine Seite pro Bild)
- ExpertProfileTab: Skills/Sprachen/Erfahrungen nebeneinander (3 Spalten)
- ExpertProfileTab: Zertifizierungen und Profilanlagen nebeneinander (2 Spalten)
- threeColumnRow CSS-Klasse hinzugefügt (responsive auf 1 Spalte bei <900px)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 10:59:04 +01:00
Thomas Reitz
c8b25321e7 feat(core+frontend): Profilzugriff-Gruppen für Admin mit delegierten Berechtigungen
- Neue Prisma-Modelle ProfileAccessGroup + ProfileAccessGroupMember mit canView/canExport/canEdit
- Manuelle Migration 20260314_profile_access_groups
- ProfileAccessModule: CRUD-Endpoints für Gruppen und Mitglieder (nur PLATFORM_ADMIN)
- Neue Admin-Endpoints in ExpertProfileService/-Controller für alle Profil-Mutationen
- verifyOwnership mit skipCheck-Parameter für Admin-Bypass
- ExpertProfileTab + alle Section-Komponenten erhalten apiBase-Prop für Wiederverwendung
- AdminProfileAccessPage: Gruppen-Tab (CRUD) + Profile-Tab (alle User mit Aktionen)
- AdminProfileDetailPage: Profil eines beliebigen Users im Admin-Kontext bearbeiten
- Route /admin/profile-access + /admin/profiles/:userId + Nav-Tab Profilzugriff

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 10:47:36 +01:00
Thomas Reitz
f7736a6fca fix(core): Word-Export — Abstand zwischen linker und rechter Spalte
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 10:16:45 +01:00
Thomas Reitz
1a222edb46 fix(core): Word-Export — Logo-Seitenverhältnis korrekt (tatsächliche Pixel-Dimensionen)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 10:12:09 +01:00
Thomas Reitz
8fc894c74c fix(core): Word-Export — jobTitle, Logo, Akzentfarbe und Firmenfußzeile
- Jobtitel aus Profil statt experiences[0].area unter dem Namen
- Platform-Logo aus Redis über Avatar einfügen
- Dominante Akzentfarbe dynamisch aus Logo extrahieren
- Firmen-Fußzeile aus Redis-Settings als DOCX-Footer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 09:43:29 +01:00
Thomas Reitz
196515daa8 fix(core): PDF-Export — Titel aus jobTitle-Feld statt erster Erfahrung
- getExportData(): jobTitle zu den selektierten User-Feldern hinzugefuegt
- ExportData-Interface: jobTitle: string | null ergaenzt
- PDF: Unter dem Namen wird jetzt data.jobTitle angezeigt statt
  profile.experiences[0].area

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 09:35:45 +01:00
Thomas Reitz
382beab9c3 feat(core): PDF-Export — Akzentfarbe dynamisch aus Branding-Logo extrahiert
- extractDominantColor(): 20x20 Resize via sharp, Alpha gegen Weiss flatten,
  alle gesaettigten Pixel (nicht weiss/schwarz/grau, range > 35) mitteln
- Ergebnis wird als accentColor fuer Timeline-Linien, Ueberschriften,
  Skill-Chips usw. verwendet
- Fallback auf #009688 wenn kein Logo hinterlegt oder keine Farbe extrahierbar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 09:27:41 +01:00
Thomas Reitz
f5d83dc1c3 fix(core): PDF-Export — Footer-Leerseite behoben und Logo ueber Profilfoto
- Footer: doc.page.margins.bottom temporaer auf 0 gesetzt beim Footer-Zeichnen
  verhindert PDFKit Auto-Pagination (footerTextY > maxY wuerde sonst
  eine leere zweite Seite erzeugen)
- Logo: Platform-Branding-Logo (aus Redis platform_branding_logo) wird oben
  in der linken Spalte ueber dem Profilfoto gerendert (fit 180x50px)
- Firmendaten + Branding parallel via Promise.all aus Redis geladen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 09:20:25 +01:00
Thomas Reitz
46ced98bf4 feat(core+frontend): Firmendaten im Admin + PDF-Fusszeile
Backend:
- GET/POST /settings/company (Redis-Key platform_company_settings)
  Felder: name, street, postalCode, city, phone, email, website
- ProfileExportService: RedisService injiziert, laedt Firmendaten vor PDF-Erzeugung
- PDF-Footer: Trennlinie + kompakte Zeile mit allen Firmendaten auf jeder Seite
  (bufferPages=true, switchToPage-Loop vor doc.end())

Frontend:
- AdminCompanyPage: Formular mit Vorschau der Fusszeile
- AdminLayout: neuer Tab 'Firmendaten'
- App.tsx: Route /admin/company

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 09:07:42 +01:00
Thomas Reitz
1d4894b637 fix(core): PDF-Export — Zertifizierungen in linke Spalte verschoben
- ZERTIFIZIERUNGEN von rechter Spalte in linke Spalte verschoben
  (nach ERFAHRUNG, vor FÄHIGKEITEN)
- Textbreite auf leftColWidth (certContentWidth = leftColWidth - 14) angepasst
- Timeline-Linie dynamisch (certEntryStartY gespeichert, Linie nach Content)
- Alte rechte-Spalte-Sektion entfernt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 08:49:25 +01:00
Thomas Reitz
f8aed00645 fix(core): PDF-Export — Abschnittslinien bis zum Spaltenende und Titel angepasst
- pdfSectionTitle: Linie zieht sich jetzt ueber die volle Spaltenbreite (width-Parameter)
  statt nur bis zum Textende
- Titel 'BERUFSERFAHRUNG' umbenannt in 'BERUFSERFAHRUNG / PROJEKTE'

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 08:46:19 +01:00
Thomas Reitz
ed24c061c4 fix(core): PDF-Export — Timeline-Linien korrekt und Faehigkeiten in linke Spalte
- BERUFSERFAHRUNG: Timeline-Linie wird jetzt NACH dem Content gezeichnet
  (entryStartY gespeichert, Linie von entryStartY+8 bis yRight-4)
  Damit stimmt die Laenge exakt mit der tatsaechlichen Eintraghoehe ueberein
- Seitenumbruch-Flag (pageBreakOccurred): Linie wird nicht gezeichnet wenn
  der Content ueber eine Seite hinausgeht
- FAEHIGKEITEN: aus dem full-width Bereich am Seitenende entfernt und in die
  linke Spalte nach ERFAHRUNG verschoben (kleinere Chips: 7pt, 16px, 5px Pad)
- Alte full-width FAEHIGKEITEN-Sektion entfernt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 22:08:18 +01:00
Thomas Reitz
41d944312c fix(core): PDF-Export Icons kleiner (12->9px) und besser positioniert
- iconSize 12 -> 9px
- iconTextOffset 20 -> 14px (kompakter)
- Alle Icons gleichmaessig bei y+1 positioniert (sauberes vertikales Alignment mit 8pt Text)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 21:39:33 +01:00
Thomas Reitz
927de0a809 fix(core): PDF-Export Zeitraum groesser und fetter (8pt -> 10pt)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 21:35:38 +01:00
Thomas Reitz
3d486e0541 fix(core): PDF-Export — Bullets, Zertifizierungen, Bild-Anhaenge
- Tasks: Bullet-Praefix nur fuer Zeilen mit echtem Aufzaehlungszeichen (kein spurious Bullet bei Plaintext)
- Zertifizierungen: Schriftgroesse reduziert (Titel 10->9pt, Aussteller 9->8pt) und Timeline-Linie gekuerzt
- Anhaenge: Bild-Anhaenge werden als zusaetzliche Seiten ans PDF angehaengt
- ExportData-Interface + getExportData() um attachments[] erweitert
- Gleiche Bullet-Fix-Logik im DOCX-Export

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 21:33:01 +01:00
Thomas Reitz
a37942b37d fix: PDF-Export — Dateiname, Fettschrift, Zeichen, Abstaende, Zertifizierungen rechts
5 Korrekturen am Experten-Profil PDF-Export:
1. Dateiname: Vorname_Nachname_CV.pdf/.docx (RFC 5987 Umlaut-sicher)
2. Fettschrift: Zeitraum, Firmenname und Branche in Berufserfahrung
3. Zeichen-Darstellung: Markdown-Marker (**bold**/*italic*/__u__)
   werden aus Tasks-Text entfernt, doppelte Bullet-Praefix-Normalisierung
4. Abstaende: Sprachen 14->11px, Erfahrung 14->11px, Gap 8->4px
5. Zertifizierungen in rechte Spalte verschoben (nach Berufserfahrung)
   statt als Vollbreite-Sektion am Ende

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 21:20:43 +01:00
Thomas Reitz
c333cbfa4b feat: Login-Screen-Branding im Global Admin (Hintergrund + Logo)
Im Bereich Anpassungen (AdminCustomizePage) kann der Platform-Admin
nun den Login-Screen individuell gestalten:
- Hintergrundtyp: Farbverlauf, Einfarbig oder Hintergrundbild
- Farbverlauf: zwei Farbpicker (Von/Bis) mit Hex-Eingabe
- Hintergrundbild: Datei-Upload max. 2MB, Live-Vorschau
- Logo auf Login-Screen: wird automatisch aus dem Sidebar-Logo uebernommen

Backend: settings.controller.ts GET/POST /settings/branding um
loginBgType, loginBgColor1, loginBgColor2, loginBgImage erweitert.
LoginPage laedt Branding per oeffentlichem Endpoint (kein Auth), leitet
containerStyle per useMemo ab und zeigt Logo-Bild statt Hardcode-Text.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 21:09:32 +01:00
Thomas Reitz
2078a90fba fix(expert-profile): Sprachniveau-Validierung mit Frontend-Werten synchronisieren
Backend @IsIn erlaubte nur CEFR-Codes (C1/C2/B2…), Frontend schickte
aber deutsche Bezeichnungen (Verhandlungssicher/Fließend/Gut).
Alle 4 Frontend-Level + CEFR-Codes für Rückwärtskompatibilität aufgenommen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 20:02:10 +01:00
Thomas Reitz
3d75a7f9de feat(frontend+core): Dashboard Kontakte-Tab (O365) + Admin Logo-/Sidebar-Breite
Dashboard Kontakte-Tab:
- DashboardContactsTab.tsx + CSS: O365-Kontakte als Visitenkarten oder Liste
- Kachelansicht (auto-fill Grid) + Listenansicht (6-spaltig) umschaltbar
- Suchfeld (Name, E-Mail, Firma) mit Live-Filter
- Klick öffnet Detail-Modal mit allen Kontaktdaten (E-Mail/Telefon als Links)
- CRM-Import-Button: mappt M365Contact → CreateContactPayload, importiert direkt
- Nicht verbunden / Laden / Fehler States
- DashboardPage: ComingSoonTab entfernt, DashboardContactsTab eingebunden

Admin Branding — Logo-Breite + Sidebar-Breite:
- settings.controller.ts: logoWidth + sidebarWidth in GET/POST Branding
- AdminCustomizePage: Slider Logo-Breite (40–240px) + Sidebar-Breite (200–360px)
  mit Live-Vorschau (skalierte Mini-Sidebar)
- AppLayout: Logo-maxWidth aus branding.logoWidth; Sidebar width + main marginLeft
  dynamisch aus branding.sidebarWidth

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 14:03:45 +01:00
Thomas Reitz
2348602fb0 feat: Erweiterte Profilfelder (analog O365) + Profilbild-Sync aus Microsoft 365
Neue Felder im Benutzerprofil (analog Microsoft 365 /me):
- Stellenbezeichnung (jobTitle), Abteilung (department)
- Firma (companyName), Standort (officeLocation)

Changes:
- Core: Prisma-Migration + neue Felder in User-Model, UpdateUserDto,
  findById/update/updateProfile
- CRM: M365UserProfile-Interface + getM365Profile um neue Felder erweitert;
  neue Methode getM365Photo() lädt 96x96 JPEG als Base64 Data-URL;
  neuer Endpoint GET /crm/office365/photo
- Frontend: AuthContext User-Interface, M365UserProfile-Typ, office365Api.getM365Photo()
  ProfilePage: Neues Formular-Fieldset "Organisation" mit 4 Feldern;
  manueller Sync-Button übernimmt auch Profilbild (immer überschreiben);
  useO365ProfileSync: Auto-Sync lädt Foto nur wenn noch kein INSIGHT-Avatar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 13:08:56 +01:00
Thomas Reitz
22af0375ea feat(core): Scopes für M365-Integration auf ReadWrite erweitern
Calendars.ReadWrite + Contacts.ReadWrite (Delegiert) für
bidirektionale CRM↔Office365 Synchronisation (Kontakte + Termine).
Contacts.ReadWrite neu hinzugefügt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 07:06:43 +01:00
Thomas Reitz
82e6a03bb9 fix(ms365): HTTPS-Protokoll für Integration-Redirect-URI erzwingen
Traefik leitet x-forwarded-proto nicht korrekt weiter, sodass der
Controller http:// statt https:// generierte — Azure lehnt nicht-HTTPS
Redirect-URIs für nicht-localhost ab (AADSTS50011).

Protokoll wird jetzt aus der konfigurierten SSO-Redirect-URI abgeleitet
(immer HTTPS), der Host bleibt dynamisch (IP oder DNS).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 23:26:10 +01:00
Thomas Reitz
1f6e59d362 fix(ms365): direkte OAuth2 URL-Konstruktion statt MSAL für Integration-Flow
MSAL-node v5 erzeugt bei getAuthCodeUrl mit reinen Graph-API-Scopes
(ohne openid) einen fehlerhaften Authorize-URL → AADSTS900561.

getIntegrationAuthUrl und handleIntegrationCallback verwenden jetzt
direkte fetch-Aufrufe (analog zu refreshIntegrationToken) ohne MSAL,
was den Fehler umgeht und denselben Standard-OAuth2-Flow garantiert.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 23:17:38 +01:00
Thomas Reitz
254d00c106 fix(ms365): dynamische Redirect-URI aus Request-Host + Azure-Kompatibilität
Problem: Redirect-URI wurde falsch aus SSO-URI abgeleitet, und unterstützte
nur eine feste URL statt sowohl IP als auch DNS-Name.

Lösung:
- initM365Integration: Host aus x-forwarded-host/host Header lesen,
  korrekte Redirect-URI bauen (proto://host/api/v1/auth/integrations/...)
- Redis-State speichert jetzt {userId, redirectUri} als JSON
- handleIntegrationCallback: gespeicherte redirectUri aus State verwenden
- getIntegrationAuthUrl/handleIntegrationCallback: optionaler redirectUri-Parameter
- Fallback-Derivation: base URL aus SSO-URI + fester Integrations-Pfad

Beide URIs müssen in Azure registriert sein:
- http://172.20.10.59/api/v1/auth/integrations/microsoft-365/callback
- http://insight.xinion.lan/api/v1/auth/integrations/microsoft-365/callback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 23:01:09 +01:00
Thomas Reitz
1ecd7dad82 fix(ms365): OAuth-Connect via API-Call statt direktem Browser-Link
Problem: <a href="/api/v1/auth/integrations/microsoft-365"> sendet keinen
JWT-Authorization-Header (JWT liegt im Memory, nicht als Cookie).

Lösung:
- Backend: initM365Integration gibt JSON {url} zurück statt server-redirect
- Frontend: integrationsApi.connectM365() ruft Endpoint via Axios ab, dann
  window.location.href zur OAuth-URL
- ProfilePage + EmailsTab + CalendarTab + TasksTab: <a href> → <button onClick>

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 22:57:20 +01:00
Thomas Reitz
05ccabfdb4 fix(core): export EntraIdService from AuthModule for IntegrationsModule DI
IntegrationsService benötigt EntraIdService (Token-Refresh), der in
AuthModule als Provider registriert aber nicht exportiert war.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 22:52:56 +01:00
Thomas Reitz
28f6ba84b0 feat(core): Microsoft 365 OAuth-Integration — UserIntegration + IntegrationsModule
- Neues Prisma-Model UserIntegration (AES-256-GCM verschluesselte Tokens)
- Migration: 20260312_user_integrations
- EntraIdService: getIntegrationAuthUrl(), handleIntegrationCallback(), refreshIntegrationToken()
  — erweiterte Scopes: Mail.Read, Calendars.Read, Tasks.ReadWrite, offline_access
- IntegrationsModule mit Controller + Service:
  GET  /auth/integrations/microsoft-365          → OAuth-Flow starten
  GET  /auth/integrations/microsoft-365/callback → Token speichern
  GET  /users/me/integrations                    → Verbindungen auflisten
  DELETE /users/me/integrations/microsoft-365    → Verbindung trennen
  GET  /users/me/integrations/microsoft-365/token → Token fuer CRM-Service
- Env: AZURE_INTEGRATION_REDIRECT_URI, INTEGRATION_ENCRYPTION_KEY

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 22:36:03 +01:00
Thomas Reitz
405ab5f038 feat: add SSL/Domain admin page for custom HTTPS configuration
- Backend: 4 new endpoints in SettingsController (GET/POST/DELETE /settings/ssl, POST /settings/ssl/check-dns)
- Certificate validation via Node.js crypto.X509Certificate (PEM format, expiry, SAN match)
- DNS resolution check via dns.promises.resolve4
- Auto-generates Traefik dynamic config (ssl-domain.yml) with custom domain routing + HTTP->HTTPS redirect
- Frontend: AdminSslPage with DNS name input, cert/key upload, status display
- Docker: Core-service gets access to traefik-certs volume and dynamic config directory

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 17:13:49 +01:00
Thomas Reitz
833bc44acd Update 2026-03-11 10:21:14 +01:00
Thomas Reitz
3bedda2b9d feat: dark mode, collapsible sidebar, branding customization
- Add Light/Dark/System theme toggle with ThemeContext and CSS variables
- Sidebar fully collapsible (icons-only mode, persisted in localStorage)
- Anwendungen section collapsible with chevron toggle
- Admin "Anpassungen" page: logo upload, sidebar color picker with presets
- Backend branding endpoints (GET/POST /settings/branding) stored in Redis
- Optional custom icon upload for external links (click icon field)
- Backend favicon proxy with HTML parsing for reliable icon loading
- Dark mode CSS variables for all components
- Login page SSO button and error styles use CSS variables

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 11:47:51 +01:00
Thomas Reitz
0f9b3d4f36 feat: backend favicon proxy with HTML parsing, collapsible sidebar sections
- Add GET /settings/favicon?url= endpoint that parses HTML for <link rel="icon"> tags
- Falls back to /favicon.ico if no icon link found in HTML
- Caches favicon URLs in Redis (24h TTL)
- Frontend uses backend proxy for reliable favicon loading (fixes Atlassian etc.)
- Anwendungen section in sidebar is now collapsible with chevron toggle

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 11:29:19 +01:00
Thomas Reitz
65c5c7b7dd feat: use website favicons instead of manual icon upload
External links now automatically show the favicon of the target website
using Google's favicon service. No manual icon upload needed — just
enter label and URL.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 11:19:34 +01:00
Thomas Reitz
878f8be45c fix: replace crypto.randomUUID with HTTP-compatible alternative
crypto.randomUUID() is only available in secure contexts (HTTPS).
Since the app runs over HTTP in development, this caused a blank page
crash on the external links admin page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 11:08:21 +01:00
Thomas Reitz
1a87356048 feat: restructure admin area with separate layout, external links management
- Admin section moved to dedicated area with horizontal tab navigation
- Sidebar now shows gear icon link to Administration (PLATFORM_ADMIN only)
- External links management page for configuring sidebar shortcuts
- External links displayed in sidebar for all authenticated users
- Backend: Redis-based CRUD endpoints for external link configuration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 11:04:43 +01:00
Thomas Reitz
bc156e1657 feat: dynamic SSO configuration via Admin UI
SSO config (Tenant ID, Client ID, Client Secret, Redirect URI) can now
be managed entirely from the Admin SSO page. Config is stored in Redis
(persistent) and the MSAL client is reinitialized on save — no server
restart or console access required.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 22:51:01 +01:00
Thomas Reitz
45cf644f81 feat: add Microsoft Entra ID (Azure AD) SSO integration
Backend-driven Authorization Code Flow with @azure/msal-node:
- EntraIdService: MSAL ConfidentialClientApplication, auth URL generation, token exchange
- SsoController: /auth/sso/microsoft (initiate) + /auth/sso/microsoft/callback (callback)
- AuthService.loginViaSso(): User provisioning (find by OID, auto-link by email, or create new)
- CSRF protection via state parameter stored in Redis
- SSO status endpoint for frontend feature detection

Frontend:
- "Mit Microsoft anmelden" button on login page (shown only when SSO is configured)
- SsoCallbackPage: handles redirect from backend, sets token, loads user profile
- AuthContext.loginWithToken(): new method for SSO token handling

Configuration:
- AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_REDIRECT_URI env vars
- docker-compose.yml updated to pass Azure vars to core service

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 22:31:34 +01:00
Thomas Reitz
8efaa49930 feat: add user deletion (backend endpoint + frontend with confirmation)
- Backend: DELETE /api/v1/users/:id endpoint (PLATFORM_ADMIN only)
- Frontend: "Löschen" button with confirmation modal
- Cascading deletes handle auth providers, memberships, profiles

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 22:00:06 +01:00
Thomas Reitz
c8499a0979 feat: circular avatar in DOCX export using sharp
Uses sharp to resize and apply a circular SVG mask to the avatar
before embedding it in the Word document, matching the PDF export.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:39:39 +01:00
Thomas Reitz
f4239760df fix: add PNG icons and Arial font to DOCX export
Replaces text labels (Tel., Mobil, Mail, Adr.) with recolored PNG icons
in the Word export contact section. Sets Arial as default document font
to match Helvetica in the PDF export.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:27:32 +01:00
Thomas Reitz
b948027dab feat: recolor PNG contact icons to match accent color at runtime
Uses pngjs to replace all visible pixels in PNG icons with the
configured accent color while preserving alpha transparency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:22:37 +01:00
Thomas Reitz
9a9800e17e feat: use PNG contact icons in PDF export instead of vector drawing
- Add Phone.png, Mobile.png, Mail.png, Address.png icon assets
- Replace hand-drawn vector icons with professional PNG icons
- Icons stored in packages/core-service/assets/icons/ (included in Docker build)
- Also stored in templates/cv/default/ and Icons/ for reference

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:11:40 +01:00
Thomas Reitz
c40d768827 fix: redesign handset icon as solid silhouette with filled crescent handle
Replace disconnected rectangles+stroke with seamless shape:
earpiece and mouthpiece blocks connected by filled crescent arc

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 12:22:04 +01:00
Thomas Reitz
0c195dc3a9 fix: separate handset icon (landline) and smartphone icon (mobile) in PDF export
- Landline phone: classic telephone handset with earpiece, mouthpiece and curved connector
- Mobile phone: smartphone outline with display and home button

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 12:17:47 +01:00