Roadmap 6.17. Veröffentlichungs- und Ablehnungsmails zu API-Meldungen gehen an Pressekontakt oder Firmen-E-Mail — Empfänger ohne (bekanntes) Konto. Die Mail sagte „in Ihrem Konto … mit Passwort oder Magic-Link“, gelesen wurde „geht nicht“. Jetzt ein Kasten mit der Adresse und dem Knopf „Anmeldelink anfordern“: signierter Link (30 Tage, Adresse verschlüsselt) auf /zugang, ein Klick schickt den Anmeldelink, die Anmeldung landet auf der Meldung. Ablehnung zu API-Meldungen: nur ansehen, neu eingereicht wird auf dem ursprünglichen Weg. Kunden mit Konto: Knopf „Im pressekonto öffnen“. Stufen 2 und 3 des Trichters messen die neue Seite mit. Changelog: Verbessert Public: Wer eine Veröffentlichungs- oder Ablehnungsmail bekommt, erhält mit einem Klick einen Anmeldelink an genau diese Adresse – ohne Konto und ohne Passwort. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
142 lines
5.6 KiB
PHP
142 lines
5.6 KiB
PHP
<?php
|
||
|
||
use App\Enums\ConversionEventType;
|
||
use App\Enums\ConversionSource;
|
||
use App\Models\Company;
|
||
use App\Models\PressRelease;
|
||
use App\Scopes\PortalScope;
|
||
use App\Services\Auth\ContactAccessService;
|
||
use App\Services\Auth\MailAccessLink;
|
||
use App\Services\Company\NewsroomClaimService;
|
||
use App\Services\Conversion\ConversionRecorder;
|
||
use Illuminate\Support\Facades\RateLimiter;
|
||
use Livewire\Attributes\Layout;
|
||
use Livewire\Attributes\Locked;
|
||
use Livewire\Component;
|
||
|
||
/**
|
||
* Anmeldelink an die Adresse, an die eine Kundenmail ging (Roadmap 6.17).
|
||
*
|
||
* Erreichbar nur über den signierten Link aus der Veröffentlichungs- oder
|
||
* Ablehnungsmail (MailAccessLink). Ein Klick schickt den Anmeldelink; nach
|
||
* der Anmeldung landet der Empfänger auf der Meldung. Die Seite zeigt die
|
||
* Adresse nur maskiert.
|
||
*/
|
||
new #[Layout('components.layouts.auth.pressekonto', ['heading' => 'Ohne Passwort anmelden', 'eyebrow' => 'Zugang zu Ihrer Pressemitteilung', 'topRightLabel' => 'Lieber mit Passwort?', 'topRightLinkText' => 'Anmelden', 'topRightLinkHref' => '/login'])] class extends Component {
|
||
#[Locked]
|
||
public string $token = '';
|
||
|
||
#[Locked]
|
||
public string $source = '';
|
||
|
||
public string $maskedEmail = '';
|
||
|
||
public string $title = '';
|
||
|
||
public bool $sent = false;
|
||
|
||
public function mount(MailAccessLink $links, ConversionRecorder $recorder): mixed
|
||
{
|
||
$data = $links->decode(request()->query('t'));
|
||
$pressRelease = $data === null ? null : $this->pressRelease($data['press_release_id']);
|
||
|
||
abort_if($pressRelease === null, 404);
|
||
|
||
$user = auth()->user();
|
||
|
||
if ($user !== null && mb_strtolower((string) $user->email) === $data['email'] && $user->can('view', $pressRelease)) {
|
||
return redirect()->route('me.press-releases.show', ['id' => $pressRelease->id]);
|
||
}
|
||
|
||
$this->token = (string) request()->query('t');
|
||
$this->source = $this->sourceFromRequest()->value;
|
||
$this->maskedEmail = NewsroomClaimService::mask($data['email']);
|
||
$this->title = (string) $pressRelease->title;
|
||
|
||
$company = $this->company($pressRelease);
|
||
|
||
if ($company !== null) {
|
||
$recorder->recordManagePageOpened(request(), $company, ConversionSource::parse($this->source), $pressRelease);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
public function requestLink(MailAccessLink $links, ContactAccessService $contactAccess, ConversionRecorder $recorder): void
|
||
{
|
||
$data = $links->decode($this->token);
|
||
$pressRelease = $data === null ? null : $this->pressRelease($data['press_release_id']);
|
||
|
||
if ($pressRelease === null) {
|
||
return;
|
||
}
|
||
|
||
$key = 'mail-access|'.hash('sha256', $data['email']);
|
||
|
||
if (RateLimiter::tooManyAttempts($key, 3)) {
|
||
$this->addError('link', __('Zu viele Anfragen. Bitte versuchen Sie es in :minutes Minuten erneut.', [
|
||
'minutes' => max(1, (int) ceil(RateLimiter::availableIn($key) / 60)),
|
||
]));
|
||
|
||
return;
|
||
}
|
||
|
||
RateLimiter::hit($key, 3600);
|
||
|
||
$source = ConversionSource::parse($this->source);
|
||
$sent = $contactAccess->requestMagicAccess($data['email'], request()->ip(), $source, $pressRelease->company_id, $pressRelease->id);
|
||
$company = $this->company($pressRelease);
|
||
|
||
if ($sent && $company !== null) {
|
||
$recorder->record(ConversionEventType::AccessRequested, $source, $company, $pressRelease);
|
||
}
|
||
|
||
$this->sent = true;
|
||
}
|
||
|
||
private function sourceFromRequest(): ConversionSource
|
||
{
|
||
$source = ConversionSource::parse((string) request()->query('src'));
|
||
|
||
return in_array($source, [ConversionSource::MailPublish, ConversionSource::MailReject], true) ? $source : ConversionSource::Organic;
|
||
}
|
||
|
||
private function pressRelease(int $id): ?PressRelease
|
||
{
|
||
return PressRelease::query()->withoutGlobalScope(PortalScope::class)->find($id);
|
||
}
|
||
|
||
private function company(PressRelease $pressRelease): ?Company
|
||
{
|
||
return $pressRelease->company_id === null
|
||
? null
|
||
: Company::query()->withoutGlobalScope(PortalScope::class)->find($pressRelease->company_id);
|
||
}
|
||
}; ?>
|
||
|
||
<div>
|
||
@if ($sent)
|
||
<div class="mb-6 rounded-[2px] border border-bg-rule bg-bg-elev px-4 py-3 text-[13.5px] leading-[1.6] text-ink-2" role="status">
|
||
{{ __('Wir haben einen Anmeldelink an :email geschickt. Er ist 15 Minuten gültig; nach dem Klick landen Sie direkt bei Ihrer Pressemitteilung.', ['email' => $maskedEmail]) }}
|
||
</div>
|
||
@else
|
||
<p class="text-[13.5px] text-ink-2 leading-[1.65] !-mt-2 mb-4">
|
||
{{ __('Sie brauchen weder ein Konto noch ein Passwort. Wir schicken einen Anmeldelink an die Adresse, an die unsere Mail ging:') }}
|
||
</p>
|
||
<p class="mb-2 font-mono text-[14px] text-ink">{{ $maskedEmail }}</p>
|
||
<p class="mb-6 text-[12.5px] text-ink-3">{{ __('Pressemitteilung:') }} <span class="text-ink-2">{{ $title }}</span></p>
|
||
|
||
@error('link')
|
||
<p class="field-error mb-4">{{ $message }}</p>
|
||
@enderror
|
||
|
||
<button type="button" class="auth-btn-primary" wire:click="requestLink" wire:loading.attr="disabled" wire:target="requestLink">
|
||
<span wire:loading.remove wire:target="requestLink">{{ __('Anmeldelink senden') }}</span>
|
||
<span wire:loading wire:target="requestLink">{{ __('Wird gesendet …') }}</span>
|
||
</button>
|
||
@endif
|
||
|
||
<p class="mt-6 text-[12.5px] text-ink-3 leading-[1.6]">
|
||
{{ __('Nach der Anmeldung können Sie die Meldung ansehen und – je nach Stand – korrigieren, die Aufrufe verfolgen und den Newsroom Ihrer Firma pflegen. Das ist kostenlos.') }}
|
||
</p>
|
||
</div>
|