Neue Seite /newsroom/{slug}/verwalten auf den Web-Portalen: schickt den
Anmeldelink an die hinterlegte Firmen-E-Mail (sonst Pressekontakte), zeigt
sie nur maskiert, und funktioniert auch für Firmen ohne veröffentlichte
Meldung. Konto, Zuordnung und Login laufen durch ContactAccessService —
neu dort: Herkunft (profile_claim auf dem Token) und Ziel-Firma, auf deren
Kundenseite der Login landet. 410-entfernte bleiben Tombstone, aufgegangene
Slugs führen zum Überlebenden (MergedNewsroomLookup, geteilt mit der
Newsroom-Seite). 3 Links je Newsroom und Tag, 15 je IP und Stunde.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
107 lines
3.4 KiB
PHP
107 lines
3.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Company;
|
|
|
|
use App\Enums\ConversionSource;
|
|
use App\Models\Company;
|
|
use App\Scopes\PortalScope;
|
|
use App\Services\Auth\ContactAccessService;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Str;
|
|
|
|
/**
|
|
* „Diesen Newsroom verwalten" (Roadmap 1b.2): Verwaltungszugang zu einer
|
|
* Firma über die hinterlegte Firmen-E-Mail, unabhängig davon, ob der
|
|
* Newsroom öffentlich ist.
|
|
*
|
|
* Der Unterschied zur allgemeinen Anmeldung per E-Mail-Link: Der Besucher
|
|
* gibt KEINE Adresse ein. Der Link geht an das, was an der Firma hinterlegt
|
|
* ist — die Firmen-E-Mail, sonst die Pressekontakte. Wer das Postfach hat,
|
|
* verwaltet die Firma; wer es nicht hat, erfährt nur eine maskierte Adresse.
|
|
* Kein neues Zugangssystem: Konto, Zuordnung und Login laufen durch
|
|
* `ContactAccessService`, genau wie bei der Anmeldung über pressekonto.
|
|
*
|
|
* Die Herkunft `profile_claim` steht auf dem Token — so misst die
|
|
* Konversionsleiter, wie viele Newsrooms über diesen Weg beansprucht werden.
|
|
*/
|
|
class NewsroomClaimService
|
|
{
|
|
public function __construct(private readonly ContactAccessService $contactAccess) {}
|
|
|
|
/**
|
|
* Firma zu einem Slug auf diesen Portalen — auch ohne veröffentlichte
|
|
* Meldung, auch deaktiviert. Nicht aber 410-entfernt oder gelöscht: Die
|
|
* Firma existiert als Datensatz, aber nicht mehr als Newsroom.
|
|
*
|
|
* @param list<string> $portalValues
|
|
*/
|
|
public function findClaimable(array $portalValues, string $slug): ?Company
|
|
{
|
|
return Company::query()
|
|
->withoutGlobalScope(PortalScope::class)
|
|
->whereIn('portal', $portalValues)
|
|
->where('slug', $slug)
|
|
->whereNull('gone_at')
|
|
->first();
|
|
}
|
|
|
|
/**
|
|
* Empfänger des Anmeldelinks: die Firmen-E-Mail, sonst die Adressen der
|
|
* Pressekontakte. Bewusst nicht beides — ein Klick eines Fremden soll
|
|
* nicht fünf Postfächer treffen.
|
|
*
|
|
* @return Collection<int, string>
|
|
*/
|
|
public function recipients(Company $company): Collection
|
|
{
|
|
$companyEmail = Company::emailKey($company->email);
|
|
|
|
if ($companyEmail !== null) {
|
|
return collect([$companyEmail]);
|
|
}
|
|
|
|
$emails = [];
|
|
|
|
foreach ($company->contacts()->withoutGlobalScope(PortalScope::class)->pluck('email') as $email) {
|
|
$key = Company::emailKey($email);
|
|
|
|
if ($key !== null) {
|
|
$emails[$key] = $key;
|
|
}
|
|
}
|
|
|
|
return collect(array_values($emails));
|
|
}
|
|
|
|
/**
|
|
* Verschickt die Anmeldelinks. Gibt zurück, an wie viele Adressen
|
|
* tatsächlich etwas ging — deaktivierte Konten bekommen nichts.
|
|
*/
|
|
public function request(Company $company, ?string $ip): int
|
|
{
|
|
$sent = 0;
|
|
|
|
foreach ($this->recipients($company) as $email) {
|
|
if ($this->contactAccess->requestMagicAccess($email, $ip, ConversionSource::ProfileClaim, $company->getKey())) {
|
|
$sent++;
|
|
}
|
|
}
|
|
|
|
return $sent;
|
|
}
|
|
|
|
/**
|
|
* @return Collection<int, string>
|
|
*/
|
|
public function maskedRecipients(Company $company): Collection
|
|
{
|
|
return $this->recipients($company)->map(fn (string $email): string => self::mask($email));
|
|
}
|
|
|
|
public static function mask(string $email): string
|
|
{
|
|
[$local, $domain] = array_pad(explode('@', $email, 2), 2, '');
|
|
|
|
return Str::substr($local, 0, 1).'***@'.$domain;
|
|
}
|
|
}
|