WS-2: Magic-Link für Firmen & Pressekontakte vereinheitlicht + Schreibzugriff

Magic-Link und Pressekontakt-Zugang zu einer Seite (/anmeldelink) zusammengeführt;
altes Login-Modal entfernt, /pressekontakt-zugang leitet weiter.

- ContactAccessService deckt jetzt Firmen-E-Mail UND Pressekontakt-E-Mail ab,
  portalübergreifend (ohne PortalScope). Eine E-Mail mehrfach hinterlegt → genau
  ein Account, dem alle Firmen + Kontakte zugeordnet werden.
- Zugeordnete Firmen erhalten Pivot-Rolle 'responsible' (Schreibzugriff auf
  Stammdaten, Kontakte, Pressemitteilungen) statt nur 'member'; bestehende
  Lese-Pivots werden hochgestuft, Owner bleiben unangetastet.
- Neuer Login-Listener (SyncCompanyMembershipsOnLogin) frischt die Zuordnungen
  bei JEDEM Login (Magic-Link, Passwort, Google) auf – auch nachträglich (API)
  hinzugekommene Firmen/Kontakte mit gleicher E-Mail greifen.
- Auth-Bereich erzwingt Hellmodus: aus dem Portal übernommene .dark-Klasse wird
  am <html> entfernt (Login war im Dark Mode hängengeblieben).
- Tests: Firmen-E-Mail-Login, Multi-Firmen-Aggregation, Schreibzugriff/Upgrade,
  Per-Login-Re-Sync, Auth-Hellmodus. Sicherheits-Doku aktualisiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-06-16 12:55:49 +00:00
parent 265d1fde53
commit fec2148ebb
12 changed files with 587 additions and 327 deletions

View file

@ -13,6 +13,19 @@ test('login screen can be rendered', function () {
$response->assertStatus(200);
});
test('the auth area forces light mode and never inherits the portal dark class', function () {
/** @var TestCase $this */
$portalUrl = rtrim((string) config('domains.domain_portal_url', 'http://pressekonto.test'), '/');
// Auch mit gesetztem Dark-Cookie aus dem Portal bleibt der Login hell:
// das <html>-Tag trägt keine .dark-Klasse und das Strip-Skript ist präsent.
$response = $this->withCookie('flux_appearance', 'dark')->get($portalUrl.'/login');
$response->assertStatus(200);
$response->assertSee("classList.remove('dark')", false);
expect($response->getContent())->not->toContain('<html lang="en" class="dark"');
});
test('users can authenticate using the login screen', function () {
/** @var TestCase $this */
// Super-Admin damit canAccessAdmin() === true → Login-Redirect auf /dashboard.