Commit Graph

60 Commits

Author SHA1 Message Date
fa321fb6a5 Profil + vCard mit neuen Mitarbeiterfeldern; Modal breiter
- VCardBuilder: Titel (N-Präfix), Abteilung (ORG), Privat-E-Mail, Mobil,
  Zentrale, Fax, Website, Geschäfts-/Privatadresse, Foto (PHOTO URI)
- Profilseite: Foto über echte URL, Kontakt-Sektion (alle Nummern/Mails/Web),
  Adresse geschäftlich+privat, Branding via BrandingService (Reseller-Vererbung)
- Mitarbeiter-Formular nutzt das breite Modal (kein Overflow mehr)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 19:33:46 +02:00
862385dbe0 Mitarbeiter: erweitertes Datenmodell + Tab-Formular
- Neue Felder: Anrede, akad. Titel, Privat-E-Mail, Fax, Zentrale, Website,
  Geschäfts-/Privatadresse (JSON), Über mich
- Foto-Upload (S3) + öffentliche Auslieferung /p/photo/{id}.jpg, Avatar in Liste
- Social-/Kontakt-Links: GET/PUT /api/employees/{id}/contact-links (Replace)
- Formular in Tabs: Allgemein / Kontakt / Adresse / Social / Zugang & NFC
- Telefonfelder mit Länder-Vorwahl + Emoji-Flagge (PhoneInput), Adress-Land
  per Flaggen-Auswahl (CountrySelect), countries.ts (Vorwahlen)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 18:56:22 +02:00
46b5c4e7ad UI: Branding & Domains wieder volle Breite (Padding bleibt)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 18:30:44 +02:00
440952560e UI: Domains-Seite Breite & Padding (Layout-Fix)
Karten auf max. 940px begrenzt, Standard-Adresse-Karte mit Innenabstand.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 18:26:00 +02:00
eb46395f79 UI: Branding-Karten Innenabstand (Padding)
Die globale .card hat kein Padding; Branding-Karten füllten daher randlos.
Padding 1.4rem/1.5rem ergänzt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 18:19:41 +02:00
990e1dbe86 UI: Branding-Seite Breite begrenzen (Layout-Fix)
Grid max-width 940px + schmalere Farb-/Slogan-Felder, damit die Karte auf
breiten Monitoren nicht über die ganze Fläche zieht.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 18:18:12 +02:00
8daef8e98f White-Label Phase 5: DNS-Automatik für Firmen-Subdomains
- DnsProvisioner (dependency-frei, cURL) legt pro Reseller *.<slug>.<portal>
  A-Record via Hetzner-Cloud-DNS-API an (deckt firma.reseller.portal ab,
  was der globale *.<portal>-Eintrag nicht kann)
- ResellerDnsListener (Doctrine postPersist/preRemove), fail-soft,
  überspringt Plattform-Reseller
- Env HCLOUD_DNS_TOKEN/HCLOUD_DNS_ZONE_NAME (leer = aus); Terraform reicht
  Cloud-Token + Zone an die App-Nodes durch (nur bei manage_dns)
- Ziel-IP = APP_EDGE_IP oder DNS-Auflösung der Portal-Domain

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 18:14:41 +02:00
c542b2f9be White-Label Phase 4: Custom-Domains (CRUD + DNS-Verify)
- DomainController /api/my-domains: Liste (Standard-Host + edgeTarget),
  Anlegen (pending), DNS-Verify (CNAME/A → Plattform-Edge), Löschen
- Domain-API auf read-only beschränkt → verified-Domains nur via
  verifiziertem Controller (kein TLS-Gate-Bypass)
- DomainsView neu: Standard-Adresse, eigene Domains, DNS-Anleitung,
  Prüfen-Button; Reseller-Topnav „Domains"
- APP_EDGE_IP optional (sonst Portal-DNS-Auflösung)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 17:24:57 +02:00
a233c34599 White-Label Phase 3: Branding-Verwaltung
- BrandingAdminController: GET/PUT /api/my-branding (Farben, Tagline),
  Logo-Upload/-Löschen, öffentliche Logo-Auslieferung /api/branding/logo/...
- BrandingService liefert logoUrl aus S3-logoKey (Firma → Reseller → Default)
- BrandingView (Reseller-Topnav + Firmen-Sidebar): Farbwähler, Slogan,
  Logo-Upload mit Live-Vorschau, Anzeige der eigenen Adresse

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 15:15:27 +02:00
bcd8ba969a White-Label Phase 2: Login-Scoping je Host
- TenantAccessPolicy: Plattform-Host nur Plattform-/Reseller-Admins,
  Reseller-Host nur dessen Nutzer, Firmen-Host nur Firmen-Nutzer
  (+ zugehöriger Reseller-Admin); Plattform-Admin überall.
- LoginSuccessHandler prüft vor JWT-Ausstellung → 403 bei falschem Host.
- Login zeigt die 403-Hinweismeldung.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 14:40:20 +02:00
b8f9a50731 White-Label Phase 1: Host→Tenant-Auflösung + Branding
- Domain-Entity polymorph (Reseller ODER Firma)
- TenantResolver: Host → Plattform / reseller.portal / firma.reseller.portal
  / verifizierte Custom-Domain
- Öffentliches GET /api/branding (Name, Ebene, Farben, Logo) nach Host
- TLS-Gate nutzt TenantResolver (nur bekannte Hosts → Zertifikat)
- Frontend: Branding-Store lädt vor Mount, färbt Theme um, TenantBrand-
  Komponente (Logo/Name je Tenant), Login zeigt Tenant
- Vite-Proxy reicht Original-Host durch (lokales White-Label-Testing)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 12:58:20 +02:00
936e25e162 Deploy: git safe.directory für Rollout setzen
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 19:37:00 +02:00
e792c4d4f5 Fix: Caddy als trusted proxy → generierte URLs nutzen https
Symfony vertraute Caddys X-Forwarded-Proto nicht, daher lauteten
QR- und Wallet-Barcode-URLs http:// statt https://. framework.trusted_proxies
auf %env(TRUSTED_PROXIES)% gesetzt (Prod: 10.0.0.0/16, Dev: 127.0.0.1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 19:37:00 +02:00
183b7c10b0 UI: Portal-Dashboard nur Reseller-KPI
Plattform-Admin sah auf dem Dashboard auch Firmen/Mitarbeiter — jetzt nur noch
Reseller (konsistent zur Portal-Navi). Firmen-KPI nur Reseller, Mitarbeiter-KPI
nur Reseller/Firma.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 17:22:23 +02:00
366d57da9b UI: Portal-Navi auf Reseller reduziert
Plattform-Admin braucht Firmen/Mitarbeiter/Produkte/Bestellungen nicht direkt
(Zugriff via „Einloggen als" in einen Reseller). Portal-Topnav = Dashboard,
Reseller, Einstellungen. Reseller behält Einloggen-als/Produkte/Bestellungen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 17:15:54 +02:00
cc33040b4b Deploy: HTTP/3 (QUIC) in Caddy deaktiviert
Manche Security-Suites (AVG Webschutz) zerlegen QUIC → ERR_QUIC_PROTOCOL_ERROR
bei betroffenen Kunden. Caddy global auf `servers { protocols h1 h2 }` (nur
HTTP/1.1 + HTTP/2 über TCP), kein alt-svc h3 mehr. Caddy-Server bekommt
ignore_changes=[user_data] (Caddyfile-Änderung per reload, kein Recreate).
Live-Caddy bereits nachgezogen (Caddyfile in-place + caddy reload).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 10:00:18 +02:00
9bd8b45588 UI: Einloggen-als-Mitarbeiterliste im Screenshot-Layout
Aufgeklappte Firmen-Mitarbeiterliste: Spalten Mitarbeiter · E-Mail · Telefon ·
Position · Aktionen; „Einloggen als" als orange umrandeter Button mit Login-Icon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 10:45:13 +02:00
cc92b48869 UI: Reseller aufklappbar (Admins) + Firmen-Aufklapp nur einloggbare
- ResellersView: Zeilen aufklappbar → Reseller-Admins (login + ROLE_RESELLER_ADMIN)
  je „Einloggen als"; Plattform-Org ohne Admins bleibt zu.
- CompaniesView: Aufklapp listet nur noch einloggbare Mitarbeiter (login=true),
  „kein Login"-Zeilen entfallen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 18:24:56 +02:00
7af4eafcad UI: „Einloggen als" je Reseller (Portal-Admin)
ResellersView: pro Reseller ein „Einloggen als" → impersoniert den Reseller-Admin
(login + ROLE_RESELLER_ADMIN der zugehörigen Firma) → Wechsel in Reseller-Kontext.
Eigene Plattform-Org (nur Plattform-Admins) bekommt korrekt keinen Button.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 16:02:20 +02:00
44661d9b02 UI: Ebenen-Menüs angepasst (Portal-Mitarbeiter, Reseller „Einloggen als")
- Portal-Topnav: zusätzlich „Mitarbeiter" (firmenübergreifend, nur einloggbare
  Mitarbeiter; Firma-Spalte statt Standort, kein Hinzufügen, „Einloggen als").
- Reseller-Topnav: „Firmen" → „Einloggen als" (gleiche Firmen-&-Mitarbeiter-
  Ansicht). Plattform behält Label „Firmen".
- EmployeesView: portalMode (isPlatformAdmin) filtert auf login=true.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 13:10:23 +02:00
d66c7cc4aa UI: „Firmen & Mitarbeiter"-Ansicht mit „Einloggen als" (scoped)
CompaniesView zeigt jetzt die Firmenkunden des Resellers (eigene Org-Firma via
resellerOrg ausgeblendet) mit Kennzahlen (Standorte/Mitarbeiter/aktive Profile/
Domains/erstellt) und aufklappbarer Mitarbeiterliste. „Einloggen als"
(Impersonation) je Firma (Firmen-Admin) und je Mitarbeiter mit Login → wechselt
in den Firmen-Kontext (linke Navi). Nur Mitarbeiter/Firmen des eigenen Mandanten.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 08:56:44 +02:00
4be88dfd45 UI: Zwei-Ebenen-Navigation (Portal/Reseller oben, Firma links)
Klare Trennung der Ebenen gegen Verwechslung: Topbar trägt die Portal-/Reseller-
Navigation (Dashboard, Reseller, Firmen, Produkte, Bestellungen, Einstellungen)
+ Level-Badge (PORTAL/RESELLER/Firmenname). Die linke Sidebar zeigt NUR Firmen-
Ebene (Mitarbeiter, Editor, Bestellungen, Standorte, Domains, Design, Wallet,
Einstellungen) und nur im Firmen-Kontext (Firmen-Admin oder via „Einloggen als").
Reseller/Portal-Admin → links leer, Inhalt volle Breite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 08:53:20 +02:00
faece5870d Deploy-Fix: nginx routet /w (Wallet) zum Backend + nginx-Recreate bei Rollout
- nginx.prod.conf: /w (Wallet-Landing, .pkpass, Google-Redirect, Logo) fehlte in
  der Backend-Location-Regex → Wallet-Routen landeten in der SPA (index.html).
- update.sh: nginx.prod.conf ist ein Single-File-Bind-Mount (am Inode gepinnt);
  git reset ersetzt die Datei → nginx-Container force-recreaten, damit die
  aktuelle Config greift (statt nur reload).

Live-Nodes bereits nachgezogen; Apple-Wallet-Pass funktioniert über Caddy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 09:17:30 +02:00
c49ff37746 Deploy: Terraform-Code-Rollout auf App-Nodes (ohne Recreate)
terraform_data.app_deploy führt per remote-exec auf jedem App-Node ein Update
aus (git reset auf origin + deploy/update.sh: SPA bauen, composer, migrate(app-1),
cache:clear), getriggert über var.deploy_version (z. B. Git-SHA). Server werden
NICHT ersetzt: hcloud_server.app ignoriert user_data-Änderungen (cloud-init nur
Erstboot). Gemeinsames deploy/update.sh (cloud-init ruft es ebenfalls auf).
Fix: ${PRIV:-} in der .tftpl als $${PRIV:-} escaped (templatefile-Kollision).

Workflow: tofu apply -var deploy_version=$(git rev-parse --short HEAD)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 19:47:59 +02:00
4c0aced823 UI: Logo „( vcard4 reseller )" + KPI-Kacheln mit Farb-Icons
- BrandLogo-Komponente (Klammer-Wortmarke im Markenlook) in Sidebar + Login.
- Dashboard-KPIs im Referenz-Stil: farbiger Kreis-Icon (orange/grün/blau/grau)
  + Zahl + Label.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 19:31:38 +02:00
1f45e35ab5 UI: helle Seitenleiste statt schwarz + aufgewertete Topbar
Look & Feel an die Referenz angepasst: weiße Navigation mit Orange-Akzent
(aktives Item orange-soft, Chevrons, graue Icons) statt dunkler Sidebar.
Topbar mit Nutzer-Block (Avatar-Initialen, Name, Rolle, Firma). Navi bleibt
flach (keine Gruppen-Einteilung). name im CurrentUser-Typ ergänzt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 19:19:59 +02:00
18894c7b52 Wallet-Design Frontend: Editor + Live-Apple-Pass-Vorschau (pro Firma)
WalletDesignView (/app/wallet, Nav „Wallet" im Firmen-Kontext): Farben
(Hintergrund/Text/Label), Titel, Logo-Upload, Feld-Editor (Binding + Label +
Slot, hinzufügen/sortieren/entfernen). Live-Vorschau im Apple-Stil mit echten
Beispieldaten. Hinweis, dass die Anordnung durch Apple/Google fix ist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 19:05:40 +02:00
488ddc115f Wallet-Design pro Firma (Backend): Farben, Titel, Logo, Felder/Slots
- Company.walletConfig (json, Migration mit MariaDB-sicherem Backfill).
- WalletService liest die Firmen-Config: Hintergrund-/Text-/Label-Farbe, Titel,
  echtes Firmenlogo (aus Object-Storage, in pass.json-Bilder skaliert; Google via
  öffentliche Logo-URL), frei wählbare Datenfelder mit Label je Apple-Slot
  (primary/secondary/auxiliary/back) → Google-Header/Textmodule.
- WalletDesignController: GET/PUT Design, POST/DELETE Logo (S3); öffentliche
  Logo-Route /w/logo/{companyId}.png. Beispieldaten (1. Mitarbeiter) für Vorschau.
- Verifiziert: pass.json übernimmt Farben/Titel/Felder, Signatur bleibt gültig.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 19:01:34 +02:00
03355f89f3 Deploy-Fix: composer install + privates NIC im cloud-init
Zwei Bugs, die den ersten Live-Deploy lahmlegten:
1. composer install fehlte → vendor/ (gitignored, /app gemountet) fehlte →
   Symfony bootete nicht (autoload_runtime.php missing), Migrationen/Seed
   fielen durch. Jetzt: composer install --no-dev im php-Container nach up.
2. Hetzner-Privatnetz-NIC kam auf einem Node nicht hoch → DB unerreichbar
   (/health degraded). Jetzt: privates Interface defensiv per DHCP hochziehen
   und auf 10.x-IP warten, bevor migriert wird.

Manuell auf den Live-Nodes bereits nachgezogen; dieser Fix macht Re-Deploys
reproduzierbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 18:43:45 +02:00
eaa5c506de Deploy: Servertyp cx22 → cx23 (cx22 von Hetzner abgekündigt)
cx22 existiert nicht mehr; aktueller Intel-Nachfolger ist cx23 (2 vCPU/4GB),
in nbg1 verfügbar. Defaults in variables.tf + Beispiel angepasst.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 15:19:42 +02:00
4d0146d6c2 Deploy: Auto-DNS über offizielle Hetzner Cloud DNS API (ein Token)
Hetzner hat DNS in die Cloud-API integriert → der hcloud-Provider (>=1.64) bringt
hcloud_zone/hcloud_zone_rrset mit. germanbrew/hetznerdns (separate API + eigener
Token) entfernt. dns.tf legt mit manage_dns=true Apex (@) + Wildcard (*) als
A-Records auf die caddy_ip; Zone wird per Name nachgeschlagen. Plan verifiziert
(12 to add). Kein separater DNS-Token mehr nötig.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 15:03:17 +02:00
70e979eae7 Deploy: cloud-init seedet die DB (app:seed) nach den Migrationen
Bisher liefen nur Migrationen → keine Daten, kein Admin-Login. app:seed ist
idempotent (überspringt, wenn admin@vcard4reseller.de existiert), läuft nur auf
app-1 (RUN_MIGRATIONS). Damit ist der dokumentierte Login admin/admin direkt nutzbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 14:26:00 +02:00
a3bdb3f81b Deploy: Auto-DNS optional/deaktiviert (manage_dns=false unblockt apply)
Der germanbrew/hetznerdns-Provider ruft schon beim Init die API auf und scheitert
am leeren Token, selbst ohne DNS-Ressourcen. Da DNS standardmäßig manuell gesetzt
wird (manage_dns=false), Provider entfernt + dns.tf → dns.tf.disabled (Code bleibt
für späteres Aktivieren erhalten). Lock-File auf hcloud-only reduziert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 14:24:42 +02:00
b316d0baf8 Docs: Wallet-Setup-Anleitung (Apple & Google) per OpenSSL
Schritt-für-Schritt zum Erzeugen der Zugangsdaten (Pass Type ID + Cert/Key/WWDR
als PEM via CSR; Google Issuer + Service-Account + Freigabe) und Eintragen der
Env-Variablen. Verlinkt aus deploy/README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 22:33:22 +02:00
3dfb0b2831 Wallet: QR auf Profilseite → Apple/Google Wallet-Pass
- WalletService (dependency-frei): Google signierter RS256-„Save"-JWT-Link;
  Apple .pkpass (pass.json + GD-Icons + manifest + PKCS#7 via openssl + zip).
  Konfigurationsgesteuert (env), ohne Zugangsdaten deaktiviert.
- WalletController: /w/{code} Landing (Geräteerkennung + Buttons),
  /w/{code}/qr.png, /apple.pkpass, /google (302). Adressierung via shortCode.
- Öffentliche Profilseite: QR-Bereich „Zur Wallet hinzufügen" (nur wenn
  Provider konfiguriert + shortCode vorhanden).
- .env Wallet-Block (leer=aus), KONZEPT §12 + deploy/README dokumentiert.

Verifiziert: not-configured → ausgeblendet/404; mit Test-Zertifikaten valides
signiertes .pkpass + Google-Save-JWT. Produktiv: echte Apple-/Google-Creds nötig.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 22:28:14 +02:00
bbe7c1b71c Bestellungen: Karten-Vorschau je Position vor dem Bestellen
- Vorschau-Button (👁) je Position im Bestell-Dialog (aktiv wenn
  Produkt+Mitarbeiter gewählt): rendert card.pdf via pdf.js (Vorder-/
  Rückseite) im Vorschau-Modal mit echten Mitarbeiterdaten × Produktlayout.
- Auch im Bestell-Detail je Position (Reseller-Prüfung vor Produktion).
- Modal: optionaler wide-Prop für Bestell-/Vorschau-Modal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 13:45:17 +02:00
7c3b06c996 Bestellungen: Frontend (Firma bestellt, Reseller wickelt ab)
- OrdersView: Firma sieht eigene Bestellungen + „Neue Bestellung"
  (Positionen Produkt+Mitarbeiter+Menge); Reseller/Plattform sehen
  eingehende (mit Firma-Spalte), setzen Status vorwärts, stornieren,
  PDF je Position. Status-Badges, Detail-Modal.
- Nav-Eintrag „Bestellungen" (Firma + Reseller + Plattform), Route.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 13:24:23 +02:00
b09931997b Bestellungen: PrintOrder/OrderItem + OrderController (Backend)
- PrintOrder (company, status-Workflow new/in_production/shipped/completed/
  cancelled, number, createdBy, items) + OrderItem (product, employee, quantity)
- OrderController: GET Liste (scoped: Firma eigene / Reseller alle seiner
  Firmen / Plattform alle), GET Detail (inkl. PDF-Link je Position),
  POST anlegen (Firmen-Admin), PATCH /status (Reseller wickelt ab / Firma
  storniert solange neu). Produkt-/Mitarbeiter-Sichtbarkeit geprüft.
- Migration + Demo-Bestellung (Muster: 100 Visitenkarten + 10 NFC). KONZEPT §13.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 12:59:36 +02:00
6dd6d3a96e Nav: Standorte/Domains nur Firma, Editor nicht für Plattform
Standorte & Domains sind firmenbezogen → nur im Firmen-Kontext.
Editor (Produktdesign) nur für Reseller (für Kundenfirmen) & Firmen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 18:02:47 +02:00
01f29f7e21 Nav: „Design" nur im Firmen-Kontext (Plattform/Reseller ausgeblendet)
Branding/Design ist nur für Firmen relevant; Admins/Reseller brauchen es nicht.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 17:35:22 +02:00
8c613ec014 Produkte: Nav-Eintrag „Visitenkarten" → „Editor"
Der Editor gestaltet jetzt jedes Produkt, nicht nur Visitenkarten.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 16:02:47 +02:00
6e8dcaff4e Produkte: Frontend-Verwaltung & Editor-Produktauswahl
- ProductsView: CRUD-Liste (eigene editierbar, globale read-only),
  Format-Defaults je Produktart, Nav-Eintrag (Plattform/Reseller)
- Card-Editor: Produktauswahl, Design je Firma+Produkt laden/speichern
  (?product=), Format vom Produkt geerbt (read-only) inkl. Asset/PDF-Calls

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 15:58:11 +02:00
f5807aefce Produkte: Produktkatalog-Backend (Visitenkarte/Namensschild/NFC)
- Product-Entität (reseller=null=global, kind, Format-Defaults, sides,
  nfc/print flags, active, sortOrder) als ResellerOwnedInterface
- Sichtbarkeit: global + eigener Reseller (TenantExtension-Sonderfall),
  Schreiben nur Eigentümer (ProductVoter PRODUCT_EDIT), Reseller-Stamping
- CardTemplate koppelt an Product (Design je Firma+Produkt); Editor-,
  Asset- und PDF-Controller produktbewusst (?product=)
- Seed: 3 globale Produkte + 1 Reseller-eigenes; KONZEPT §13 Produktkatalog

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 15:20:52 +02:00
2dc40c6ea5 Rechte: Mitarbeiter & Benutzer vereint, ROLE_CONTACT als Basis
- Jeder Mitarbeiter hat (leere) Login-/Passwortfelder; Standardrolle ROLE_CONTACT
  (reines Profil). Hochstufen über die Rechtegruppe.
- Ebenen-Ladder: contact(0) < employee(1) < company_admin(2) < reseller_admin(3)
  < platform_admin(4); role_hierarchy + RoleService entsprechend.
- PATCH /api/employees/{id}/access: Rechtegruppe setzen (+ optional Passwort/Login);
  DELETE .../login → zurück auf Kontakt.
- Sicherheit: Passwort/userIdentifier per #[Ignore] aus der API-Serialisierung.
- Frontend: separate Benutzer-Ansicht entfernt; Mitarbeiter-Liste mit
  Rechtegruppe-Spalte, Rollen/Login + 'Arbeiten als' inline im Bearbeiten-Dialog.

Verifiziert: kein Passwort-Leak, roles/login im Payload, Hochstufen Kontakt→
Mitarbeiter + Login, Eskalation→403, Login entziehen→Kontakt; UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 21:44:57 +02:00
ae9936586b Rechte: 'Arbeiten als' (Impersonation, nur absteigend)
- ImpersonationController POST /api/impersonate/{id}: gibt JWT für Ziel-
  Mitarbeiter aus (imp-Claim für Audit); nur niedrigere Ebene + eigener
  Mandanten-Teilbaum (RoleService.levelOfRoles)
- Frontend: auth-Store impersonate/stopImpersonation (Original-Token gesichert),
  'Arbeiten als'-Buttons in der Logins-Übersicht (nur erlaubte Ziele),
  Impersonation-Banner mit 'Beenden' im Layout

Verifiziert: Admin→Reseller/Firma/Mitarbeiter, Eskalation/Cross-Tenant→403,
Kontextwechsel + Banner im Browser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 17:40:37 +02:00
bcc06e697b Rechte: User in Employee verschmolzen (eine Identität pro Person)
Beseitigt die Doppelung Admin-Login vs. Mitarbeiter — jeder ist ein Employee
mit optionalem Login/Rechtegruppe (Voraussetzung für Mitarbeiter-Zeiterfassung).

- Employee implementiert UserInterface/PasswordAuthenticated (loginEmail unique,
  password, roles); User-Entität entfernt; Security-Provider → Employee.loginEmail
- Plattform = Reseller mit isPlatform + Org-Firma; Reseller haben Org-Firma
  (Company.isResellerOrg) für ihr Personal → alles = Reseller→Firma→Mitarbeiter
- TenantContext leitet Reseller/Company aus dem Mitarbeiter ab (Reseller-/
  Plattform-Admin = reseller-weit)
- UserAdminController: Login pro Mitarbeiter vergeben/entziehen
  (POST/DELETE /api/employees/{id}/login), /api/users = Logins-Übersicht
- Provisioning/Seed auf das neue Modell; Migrationen zu einer Baseline gesquasht
- Frontend: EmployeesView Login-Block + UsersView (Logins-Übersicht)

Verifiziert: Login, /me, Mandantenscoping, delegierter Grant (Eskalation→403),
öffentliches Profil, SPA-Flow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 17:27:38 +02:00
cac6b26a0d Rechte: Benutzer-Verwaltung & Rechtegruppe je Mitarbeiter (Frontend)
- UsersView: Benutzer-Liste + Anlegen (Rechtegruppen-Dropdown = nur erlaubte
  Gruppen), scope-gefiltert; Nav-Eintrag Benutzer (ab Firmen-Admin)
- EmployeesView: Block 'Zugang/Rechtegruppe' im Bearbeiten-Dialog —
  Login anlegen/entfernen pro Mitarbeiter (delegationsgeprüft)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 15:23:34 +02:00
25370ebfbc Rechte: delegierte Benutzerverwaltung (Backend)
- RoleService: Ebenen-Modell + assignableGroups + assertCanAssign
  (Rolle <= eigene Ebene UND nur eigener Mandanten-Teilbaum)
- UserAdminController: /api/users CRUD + /assignable-groups, scope-gefiltert,
  mehrere Logins pro Ebene, Mitarbeiter-Login via employeeId
- Schutz gegen Privilege-Escalation & Cross-Tenant verifiziert (403)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 15:15:03 +02:00
46a75f859b Konzept: Zeiterfassung (§14) + überarbeitetes Rechte-Konzept (§2)
- §14 Zeiterfassung (Kommen/Gehen): TimeEntry append-only/revisionssicher,
  Stempeln per App/Kiosk/Web (NFC/PIN), Compliance (ArbZG/DSGVO/BetrVG)
- §2 neu: mehrere Logins pro Ebene, Rechtegruppen, delegierte Rechtevergabe
  (Rolle <= eigene Ebene + nur eigener Mandanten-Teilbaum), RoleAssignmentVoter

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 13:19:23 +02:00
79e996ab03 Deployment: Caddy-Edge (TLS + On-Demand für Custom-Domains) + Hetzner DNS
- Caddy ersetzt den Hetzner-LB: terminiert TLS (Portal-Domain automatisch) und
  load-balanced per reverse_proxy über die App-Nodes. Für Custom-Domains (§11)
  On-Demand-TLS, autorisiert über GET /internal/tls-allowed.
- TlsCheckController + DomainRepository::findVerifiedByHostname: erlaubt Zertifikate
  nur für Portal-Domain oder verifizierte Domains (Schutz vor Cert-Flooding).
- Terraform: hcloud_load_balancer entfernt, Caddy-Server + Firewall (80/443) +
  cloud-init-caddy (Caddyfile templated mit Upstreams/Domain/ACME).
- Optional Hetzner DNS via API (manage_dns): A-Record Portal + Wildcard → Caddy.
- nginx.prod: /internal zu Symfony geroutet; APP_PORTAL_DOMAIN-Env.

Validiert: Caddyfile (caddy validate), Terraform (validate), /internal/tls-allowed (200/403/400).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 22:13:29 +02:00