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>
This commit is contained in:
Kevin Adametz 2026-10-13 16:06:14 +00:00
parent c2f44f42c3
commit 1efb30f9ce
9 changed files with 442 additions and 23 deletions

View file

@ -7,6 +7,7 @@ use App\Enums\EmailNotificationType;
use App\Mail\Concerns\Unsubscribable;
use App\Models\PressRelease;
use App\Models\User;
use App\Services\Auth\MailAccessLink;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
@ -39,6 +40,16 @@ class PressReleasePublished extends Mailable
*/
public ?string $manageNewsroomUrl = null;
/**
* Empfänger ohne Konto (API-Meldung an Pressekontakt oder Firmen-E-Mail,
* Roadmap 6.17): Adresse, an die diese Mail geht, und der Link, der mit
* einem Klick einen Anmeldelink an genau diese Adresse schickt. Null für
* Kunden, die selbst eingereicht haben — die kennen ihr Konto.
*/
public ?string $accessEmail = null;
public ?string $accessUrl = null;
public function __construct(
public readonly User $user,
public readonly PressRelease $pressRelease,
@ -72,6 +83,8 @@ class PressReleasePublished extends Mailable
// Geht die Mail an einen Empfänger ohne Konto (API-Meldung), meldet
// der Abmeldelink diese Adresse ab — nicht das Konto des Einlieferers.
if ($recipientEmail !== null && mb_strtolower(trim($recipientEmail)) !== mb_strtolower((string) $user->email)) {
$this->accessEmail = trim($recipientEmail);
$this->accessUrl = app(MailAccessLink::class)->url($this->accessEmail, $pressRelease, ConversionSource::MailPublish);
$this->configureAddressUnsubscribe($recipientEmail, EmailNotificationType::PressReleasePublished);
} else {
$this->configureUnsubscribe($user, EmailNotificationType::PressReleasePublished);

View file

@ -8,6 +8,7 @@ use App\Enums\Portal;
use App\Mail\Concerns\Unsubscribable;
use App\Models\PressRelease;
use App\Models\User;
use App\Services\Auth\MailAccessLink;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
@ -29,6 +30,16 @@ class PressReleaseRejected extends Mailable
*/
public string $recipientName;
/**
* Empfänger ohne Konto (API-Meldung, Roadmap 6.17): Adresse dieser Mail
* und Link, der einen Anmeldelink an sie schickt. Die Meldung kam über
* einen Verteiler — überarbeitet wird sie dort, nicht über unser Konto
* des Einlieferers; der Link führt deshalb nur zum Ansehen.
*/
public ?string $accessEmail = null;
public ?string $accessUrl = null;
public function __construct(
public readonly User $user,
public readonly PressRelease $pressRelease,
@ -53,6 +64,8 @@ class PressReleaseRejected extends Mailable
// Geht die Mail an einen Empfänger ohne Konto (API-Meldung), meldet
// der Abmeldelink diese Adresse ab — nicht das Konto des Einlieferers.
if ($recipientEmail !== null && mb_strtolower(trim($recipientEmail)) !== mb_strtolower((string) $user->email)) {
$this->accessEmail = trim($recipientEmail);
$this->accessUrl = app(MailAccessLink::class)->url($this->accessEmail, $pressRelease, ConversionSource::MailReject);
$this->configureAddressUnsubscribe($recipientEmail, EmailNotificationType::PressReleaseRejected);
} else {
$this->configureUnsubscribe($user, EmailNotificationType::PressReleaseRejected);

View file

@ -0,0 +1,63 @@
<?php
namespace App\Services\Auth;
use App\Enums\ConversionSource;
use App\Models\PressRelease;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\URL;
/**
* „Anmeldelink an diese Adresse“ aus einer Kundenmail (Roadmap 6.17).
*
* Veröffentlichungs- und Ablehnungsmails zu API-Meldungen gehen an den
* Pressekontakt oder die Firmen-E-Mail — Empfänger, die aus ihrer Sicht kein
* Konto haben. Der Link führt auf eine Seite, die mit einem Klick einen
* Anmeldelink an genau die Adresse schickt, an die die Mail ging, und nach
* der Anmeldung auf der Meldung landet.
*
* Der Link meldet niemanden an: Er löst nur einen Anmeldelink an eine
* Adresse aus, die ihn ohnehin bekommen könnte (`/anmeldelink`). Signiert und
* befristet, die Adresse verschlüsselt — sie steht nicht lesbar in der URL,
* in Logs oder in der Webanalyse. `src` und `pr` stehen zusätzlich im Klartext
* für die Klickmessung (RecordMailLinkClick).
*/
class MailAccessLink
{
public const int VALID_DAYS = 30;
public function url(string $email, PressRelease $pressRelease, ConversionSource $source): string
{
return URL::temporarySignedRoute('mail-access', now()->addDays(self::VALID_DAYS), [
't' => Crypt::encryptString((string) json_encode([
'e' => mb_strtolower(trim($email)),
'p' => $pressRelease->getKey(),
])),
'src' => $source->value,
'pr' => $pressRelease->getKey(),
]);
}
/**
* @return array{email: string, press_release_id: int}|null
*/
public function decode(?string $token): ?array
{
if (blank($token)) {
return null;
}
try {
$data = json_decode(Crypt::decryptString((string) $token), true);
} catch (DecryptException) {
return null;
}
if (! is_array($data) || ! is_string($data['e'] ?? null) || ! filter_var($data['e'], FILTER_VALIDATE_EMAIL) || (int) ($data['p'] ?? 0) <= 0) {
return null;
}
return ['email' => $data['e'], 'press_release_id' => (int) $data['p']];
}
}

View file

@ -24,25 +24,37 @@
@endif
</dl>
{{-- 2) Verwaltung im Konto (Login-Bereich), inkl. Hinweis auf die
Anmeldung per Passwort oder Magic-Link. --}}
<p>
Bearbeiten und verwalten können Sie die Pressemitteilung in Ihrem Konto:<br>
<a href="{{ $showUrl }}">Pressemitteilung im pressekonto öffnen</a>
</p>
<p style="font-size: 12.5px; color: #6b7280;">
Hinweis: Dieser Link führt in Ihren persönlichen Login-Bereich. Die Anmeldung ist
dort mit Ihrem Passwort oder per Magic-Link (Anmelde-Link per E-Mail) möglich.
</p>
{{-- 3) Verwaltungszugang zum Newsroom (Roadmap 1b.4, E4): ein Hinweis
am Ende, kein Angebot — der Zugang ist kostenlos. --}}
@if ($manageNewsroomUrl)
@if ($accessUrl)
{{-- 2) Empfänger ohne Konto (API-Meldung, Roadmap 6.17): ein Klick
schickt einen Anmeldelink an genau diese Adresse. Vorher stand
hier „in Ihrem Konto … mit Passwort oder Magic-Link“ — wer kein
Konto kennt, las daraus „geht nicht“. --}}
<div class="meta">
<p><strong>Etwas ändern oder die Aufrufe sehen?</strong></p>
<p>
Dafür brauchen Sie weder ein Konto noch ein Passwort. Wir schicken einen Anmeldelink an
<strong>{{ $accessEmail }}</strong>, die Adresse, an die diese Mail ging. Danach können Sie
die Meldung korrigieren, die Aufrufe Tag für Tag sehen{{ $pressRelease->company ? ' und den Newsroom von '.$pressRelease->company->name.' pflegen (Beschreibung, Logo, Pressekontakte)' : '' }}.
Das ist kostenlos.
</p>
<x-emails.button :url="$accessUrl">Anmeldelink anfordern</x-emails.button>
</div>
@else
{{-- 2) Kunden mit eigenem Konto: direkt in den Kundenbereich. --}}
<x-emails.button :url="$showUrl">Im pressekonto öffnen</x-emails.button>
<p style="font-size: 12.5px; color: #6b7280;">
Hinweis: Der Newsroom von {{ $pressRelease->company->name }} lässt sich kostenlos pflegen —
Profil, Logo, Pressekontakte und die Aufrufe jeder Meldung im Verlauf. Den Zugang erhält die hinterlegte Firmen-E-Mail:<br>
<a href="{{ $manageNewsroomUrl }}">Diesen Newsroom verwalten und Aufrufe einsehen</a>
Dort sehen Sie die Aufrufe der Meldung und können sie korrigieren.
</p>
{{-- 3) Verwaltungszugang zum Newsroom (Roadmap 1b.4, E4): ein Hinweis
am Ende, kein Angebot — der Zugang ist kostenlos. --}}
@if ($manageNewsroomUrl)
<p style="font-size: 12.5px; color: #6b7280;">
Hinweis: Der Newsroom von {{ $pressRelease->company->name }} lässt sich kostenlos pflegen —
Profil, Logo, Pressekontakte und die Aufrufe jeder Meldung im Verlauf. Den Zugang erhält die hinterlegte Firmen-E-Mail:<br>
<a href="{{ $manageNewsroomUrl }}">Diesen Newsroom verwalten und Aufrufe einsehen</a>
</p>
@endif
@endif
{{-- 4) Erstquelle gegen Kopie (Roadmap 1d, Anhang 17.09.2026): nur bei

View file

@ -14,7 +14,20 @@
<p>Bitte überarbeiten Sie den Inhalt und reichen Sie die Pressemitteilung erneut ein.</p>
@endif
<x-emails.button :url="$editUrl">Pressemitteilung bearbeiten</x-emails.button>
@if ($accessUrl)
{{-- Empfänger ohne Konto (API-Meldung, Roadmap 6.17): ansehen ja,
überarbeitet und neu eingereicht wird über den Verteiler, über
den die Meldung kam. --}}
<p>
Meldung und Ablehnungsgrund können Sie ohne Konto und ohne Passwort ansehen: Wir schicken einen
Anmeldelink an <strong>{{ $accessEmail }}</strong>, die Adresse, an die diese Mail ging.
Eine überarbeitete Fassung reichen Sie bitte auf demselben Weg ein wie diese Meldung.
</p>
<x-emails.button :url="$accessUrl">Anmeldelink anfordern</x-emails.button>
@else
<p>Sie können die Meldung überarbeiten und erneut zur Prüfung einreichen.</p>
<x-emails.button :url="$editUrl">Pressemitteilung bearbeiten</x-emails.button>
@endif
{{-- Roadmap 1d.2 (E6: die Ablehnungsstrecke bleibt frei von Angeboten):
ein Hinweis, was eine Mitteilung braucht, und der Weg zur Freigabe. --}}

View file

@ -0,0 +1,142 @@
<?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>

View file

@ -27,6 +27,13 @@ Route::group(['middleware' => config('fortify.middleware', ['web'])], function (
->middleware(['guest:'.config('fortify.guard')])
->name('magic-link.request');
// Anmeldelink an die Adresse einer Kundenmail (Roadmap 6.17): nur über
// den signierten Link aus Veröffentlichungs- und Ablehnungsmail. Ohne
// `guest`: Ein angemeldeter Empfänger wird zur Meldung weitergeleitet.
Route::livewire('/zugang', 'auth.mail-access')
->middleware(['signed', 'throttle:20,1'])
->name('mail-access');
// Alte Pressekontakt-URL weiterleiten (Bestandslinks/Lesezeichen).
Route::redirect('/pressekontakt-zugang', '/anmeldelink');

View file

@ -0,0 +1,157 @@
<?php
use App\Enums\ConversionEventType;
use App\Enums\ConversionSource;
use App\Enums\Portal;
use App\Mail\MagicLoginLink;
use App\Mail\PressReleasePublished;
use App\Mail\PressReleaseRejected;
use App\Models\Company;
use App\Models\ConversionEvent;
use App\Models\MagicLink;
use App\Models\PressRelease;
use App\Models\User;
use App\Services\Auth\MailAccessLink;
use Database\Seeders\RolesAndPermissionsSeeder;
use Illuminate\Support\Facades\Mail;
use Livewire\Livewire;
use Tests\TestCase;
/**
* Anmeldelink an die Adresse einer Kundenmail (Roadmap 6.17): Empfänger von
* API-Meldungen haben aus ihrer Sicht kein Konto. Ein Klick in der Mail
* schickt den Anmeldelink an genau ihre Adresse, die Anmeldung landet auf
* der Meldung.
*/
beforeEach(function (): void {
/** @var TestCase $this */
$this->seed(RolesAndPermissionsSeeder::class);
Mail::fake();
});
function apiRelease(array $attributes = []): PressRelease
{
$company = Company::factory()->businessportal24()->create([
'name' => 'Firma GmbH', 'slug' => 'firma-gmbh', 'email' => 'presse@firma.de', 'is_active' => true,
]);
return PressRelease::factory()->published()->forPortal(Portal::Businessportal24)->create(array_merge([
'company_id' => $company->id, 'language' => 'de', 'title' => 'Firma GmbH eröffnet Standort',
], $attributes));
}
/**
* @return array{t: string, src: string}
*/
function accessQuery(string $url): array
{
parse_str((string) parse_url($url, PHP_URL_QUERY), $query);
return ['t' => (string) $query['t'], 'src' => (string) $query['src']];
}
// ------------------------------------------------------------------ Mails
test('the publication mail offers a sign-in link to the address it went to when the recipient has no account', function () {
/** @var TestCase $this */
$release = apiRelease();
$mail = new PressReleasePublished($release->user, $release, 'Frau Muster', 'presse@firma.de', true);
$html = $mail->render();
expect($mail->accessEmail)->toBe('presse@firma.de')
->and($mail->accessUrl)->toContain('/zugang?')
->and($mail->accessUrl)->not->toContain('presse@firma.de')
->and($mail->accessUrl)->not->toContain('presse%40firma.de')
->and($html)->toContain('Etwas ändern oder die Aufrufe sehen?')
->and($html)->toContain('weder ein Konto noch ein Passwort')
->and($html)->toContain('presse@firma.de')
->and($html)->toContain('den Newsroom von Firma GmbH pflegen')
->and($html)->toContain('Anmeldelink anfordern')
->and($html)->not->toContain('Im pressekonto öffnen')
->and($html)->not->toContain('mit Ihrem Passwort oder per Magic-Link');
});
test('the rejection mail lets recipients without an account view the release, resubmission stays with the original channel', function () {
/** @var TestCase $this */
$release = apiRelease(['status' => 'rejected']);
$html = (new PressReleaseRejected($release->user, $release, 'Kein Anlass erkennbar.', 'Frau Muster', 'presse@firma.de'))->render();
expect($html)->toContain('ohne Konto und ohne Passwort ansehen')
->and($html)->toContain('auf demselben Weg ein wie diese Meldung')
->and($html)->toContain('Anmeldelink anfordern')
->and($html)->not->toContain('Pressemitteilung bearbeiten');
// Wer selbst eingereicht hat, überarbeitet wie bisher im Konto.
$own = (new PressReleaseRejected($release->user, $release, 'Kein Anlass erkennbar.'))->render();
expect($own)->toContain('Pressemitteilung bearbeiten')
->and($own)->not->toContain('Anmeldelink anfordern');
});
// --------------------------------------------------------- Zugangsseite
test('the access page only opens with a valid signature and shows the address masked', function () {
/** @var TestCase $this */
$release = apiRelease();
$url = app(MailAccessLink::class)->url('presse@firma.de', $release, ConversionSource::MailPublish);
$this->get($url)
->assertOk()
->assertSeeText('p***@firma.de')
->assertSeeText('Firma GmbH eröffnet Standort')
->assertDontSeeText('presse@firma.de');
$this->get(str_replace('src=mail_publish', 'src=mail_reject', $url))->assertForbidden();
$opened = ConversionEvent::query()->where('type', ConversionEventType::ManagePageOpened->value)->sole();
expect($opened->source)->toBe(ConversionSource::MailPublish)
->and($opened->press_release_id)->toBe($release->id);
});
test('one click sends the sign-in link to that address and the login lands on the release', function () {
/** @var TestCase $this */
$release = apiRelease();
$url = app(MailAccessLink::class)->url('presse@firma.de', $release, ConversionSource::MailPublish);
Livewire::withQueryParams(accessQuery($url))
->test('auth.mail-access')
->call('requestLink')
->assertSet('sent', true)
->assertSee('p***@firma.de');
$user = User::query()->where('email', 'presse@firma.de')->firstOrFail();
Mail::assertQueued(MagicLoginLink::class, fn (MagicLoginLink $mail): bool => $mail->user->is($user));
$link = MagicLink::query()->where('user_id', $user->id)->latest('id')->firstOrFail();
expect($link->source)->toBe(ConversionSource::MailPublish->value)
->and($link->payload)->toBe(['company_id' => $release->company_id, 'press_release_id' => $release->id])
->and(ConversionEvent::query()->where('type', ConversionEventType::AccessRequested->value)->sole()->company_id)->toBe($release->company_id);
$plain = str_replace('/magic-login/', '', parse_url(Mail::queued(MagicLoginLink::class)->first()->loginUrl, PHP_URL_PATH));
$this->get('https://pressekonto.test/magic-login/'.ltrim($plain, '/'))
->assertRedirect(route('me.press-releases.show', ['id' => $release->id], absolute: false));
});
test('requests per address are limited', function () {
/** @var TestCase $this */
$release = apiRelease();
$query = accessQuery(app(MailAccessLink::class)->url('presse@firma.de', $release, ConversionSource::MailReject));
foreach (range(1, 3) as $i) {
Livewire::withQueryParams($query)->test('auth.mail-access')->call('requestLink')->assertHasNoErrors();
}
Livewire::withQueryParams($query)->test('auth.mail-access')->call('requestLink')->assertHasErrors('link');
Mail::assertQueuedCount(3);
});
test('a signed-in recipient goes straight to the release', function () {
/** @var TestCase $this */
$release = apiRelease();
$user = User::factory()->create(['email' => 'presse@firma.de', 'is_active' => true]);
$user->assignRole('customer');
$user->companies()->attach($release->company_id, ['role' => 'responsible']);
$this->actingAs($user)
->get(app(MailAccessLink::class)->url('presse@firma.de', $release, ConversionSource::MailPublish))
->assertRedirect(route('me.press-releases.show', ['id' => $release->id]));
});

View file

@ -101,7 +101,7 @@ test('the published mail links both portals for a cross-portal press release', f
->toContain(config('domains.domain_businessportal_url').'/de/press-release/'.$pressRelease->slug);
});
test('the published mail links the account area below the portal links with a login hint', function () {
test('the published mail links the account area below the portal links for customers with an account', function () {
$pressRelease = PressRelease::factory()->published()->forPortal(Portal::Presseecho)->create();
$mailable = new PressReleasePublished($pressRelease->user, $pressRelease);
@ -114,12 +114,11 @@ test('the published mail links the account area below the portal links with a lo
// zwei Parameter (`src`, `pr`), und Blade escapt das trennende `&` im
// HTML korrekt zu `&amp;`.
->toContain(e($mailable->showUrl))
->toContain('Pressemitteilung im pressekonto öffnen')
->toContain('Login-Bereich')
->toContain('mit Ihrem Passwort oder per Magic-Link');
->toContain('Im pressekonto öffnen')
->not->toContain('Anmeldelink anfordern');
// Reihenfolge: erst der öffentliche Portal-Link, darunter der Konto-Link.
expect(mb_strpos($html, $publicUrl))->toBeLessThan(mb_strpos($html, 'Pressemitteilung im pressekonto öffnen'));
expect(mb_strpos($html, $publicUrl))->toBeLessThan(mb_strpos($html, 'Im pressekonto öffnen'));
});
test('the termination mails use the hub ci layout', function () {