WS-2: Firmen-Scope für PMs & Magic-Link-Zugang für Pressekontakte

Firmen-Scope (Fundament):
- PM-Zugriff war hart an user_id (Autor) gebunden. Jetzt additiv: Autor ODER
  Mitglied der zugeordneten Firma (Owner via owner_user_id oder company_user-
  Pivot). Geändert in PressReleasePolicy (canManage) sowie den Queries der
  Listen-, Show- und Edit-Komponenten. Helfer User::accessibleCompanyIds()/
  canAccessCompany(). Solo-Owner unverändert; Firmenmitglieder sehen/bearbeiten
  alle PMs ihrer Firma.

Magic-Link-Zugang für Pressekontakte (ContactAccessService):
- Öffentliches, enumeration-sicheres Formular (/pressekontakt-zugang) mit
  Honeypot + Rate-Limit. Eine hinterlegte Kontakt-E-Mail führt zu einem lazy
  angelegten, de-duplizierten customer-Account (aktiv, verifiziert über den
  Magic-Link-Kanal), der den Firmen seiner Kontakte als Mitglied zugeordnet
  wird. Versand über den bestehenden Login-Magic-Link (Generator + Consume
  wiederverwendet) – keine Schema-Änderung, kein paralleles System.
- Dezenter Einstiegslink von der Login-Seite (PM-Frontend-Wiring später).

Tests: PressReleaseCompanyScopeTest (3), ContactAccessTest (6, inkl. De-Dup,
Enumeration-Sicherheit, Honeypot).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-06-16 08:33:12 +00:00
parent 94cb209a9f
commit 980763c362
11 changed files with 493 additions and 7 deletions

View file

@ -330,4 +330,29 @@ class User extends Authenticatable implements MustVerifyEmail
return $this->hasAnyRole(['admin', 'editor', 'customer']);
}
/**
* Firmen, auf die dieser User Zugriff hat: als Owner (owner_user_id) oder
* als Mitglied über den company_user-Pivot. Basis für das Firmen-Scoping
* von Pressemitteilungen (Zugriff = Autor ODER Firmenzugehörigkeit).
*
* @return list<int>
*/
public function accessibleCompanyIds(): array
{
return $this->ownedCompanies()->pluck('companies.id')
->merge($this->companies()->pluck('companies.id'))
->unique()
->values()
->all();
}
public function canAccessCompany(?int $companyId): bool
{
if ($companyId === null) {
return false;
}
return in_array($companyId, $this->accessibleCompanyIds(), true);
}
}