presseportale/resources/views/livewire/auth/mail-access.blade.php
Kevin Adametz 1efb30f9ce mail: Anmeldelink an die Adresse der Mail statt „in Ihrem Konto“
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>
2026-10-13 16:06:14 +00:00

142 lines
5.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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>