Files
einundzwanzig-verein/resources/views/livewire/auth-button.blade.php
T
HolgerHatGarKeineNode 52cf81abca fix(auth): route all nostrLoggedIn listeners through signed-event verification
The previous commit only updated auth-button + the WithNostrAuth trait,
but six Volt pages (profile, benefits, election/*, members/admin) carry
their own handleNostrLoggedIn(string $pubkey) handlers. The dispatched
payload is now an array, so Livewire's container could not resolve the
string parameter and threw BindingResolutionException on every login.

- All six per-page handlers now accept the signed event and route it
  through NostrAuth::loginWithSignedEvent() like the trait does.
- NostrAuth: add currentOrIssueChallenge() so the sidebar + navbar
  auth-button mounts share one live session challenge instead of
  overwriting each other.
- verifySignedEvent: pass a normalized stdClass to swentel's verify()
  directly, skipping an unnecessary json_encode + json_decode round-trip.
- auth-button: gate the global Escape/Tab capture so it only intercepts
  keys while the overlay is actually visible.
- Update three test files that still called handleNostrLoggedIn with a
  raw pubkey to authenticate via NostrAuth::login() instead.
2026-05-20 01:51:31 +02:00

120 lines
4.8 KiB
PHP

<?php
use App\Support\NostrAuth;
use Livewire\Attributes\Locked;
use Livewire\Attributes\On;
use Livewire\Component;
new class extends Component
{
#[Locked]
public bool $isLoggedIn = false;
#[Locked]
public string $location = 'sidebar'; // 'sidebar' or 'navbar'
#[Locked]
public ?string $nostrChallenge = null;
public function mount(): void
{
$this->isLoggedIn = NostrAuth::check();
if (! $this->isLoggedIn) {
// Sidebar + navbar mount the same component on the same page; using
// currentOrIssueChallenge keeps both rendered data-attributes
// pointing at the same live session value.
$this->nostrChallenge = NostrAuth::currentOrIssueChallenge();
}
}
/**
* JS-driven fallback: re-issue the challenge if the client cannot find
* one in the rendered snapshot (e.g. after a long-lived tab where the
* Volt component snapshot drifted out of sync with the session).
*/
public function requestNostrChallenge(): string
{
$challenge = NostrAuth::issueChallenge();
$this->nostrChallenge = $challenge;
return $challenge;
}
#[On('nostrLoggedIn')]
public function handleNostrLoggedIn($signedEvent = null): void
{
NostrAuth::loginWithSignedEvent($signedEvent);
$this->js('window.location.reload(true);');
}
#[On('nostrLoggedOut')]
public function handleNostrLoggedOut(): void
{
$this->isLoggedIn = false;
}
}
?>
<div x-data="nostrLogin" data-nostr-challenge="{{ $nostrChallenge ?? '' }}">
@if($isLoggedIn)
<form method="post" action="{{ route('logout') }}">
@csrf
<flux:button variant="ghost" icon="arrow-right-start-on-rectangle" type="submit" wire:click="$dispatch('nostrLoggedOut')">Logout</flux:button>
</form>
@else
<flux:button variant="primary"
icon="user"
@click="openNostrLogin"
x-bind:disabled="nostrLoginInProgress"
x-bind:aria-busy="nostrLoginInProgress"
class="cursor-pointer">
<span x-show="!nostrLoginInProgress">Mit Nostr verbinden</span>
<span x-show="nostrLoginInProgress" x-cloak class="inline-flex items-center gap-2">
<flux:icon.arrow-path class="animate-spin size-4" aria-hidden="true"/>
Signiere…
</span>
</flux:button>
@endif
{{-- Full-viewport progress overlay. Visible while the wallet-signing
round-trip is running. Locks input by capturing pointer events and
intercepting Escape/Tab so the user cannot interact with anything
underneath until the redirect resolves (or the flow errors out). --}}
<div x-show="nostrLoginInProgress"
x-cloak
x-transition.opacity.duration.150ms
role="dialog"
aria-modal="true"
x-bind:aria-busy="nostrLoginInProgress"
aria-labelledby="nostr-login-progress-heading-{{ $location }}"
aria-describedby="nostr-login-progress-description-{{ $location }}"
@keydown.window.escape="if (nostrLoginInProgress) { $event.preventDefault(); $event.stopPropagation(); }"
@keydown.window.tab="if (nostrLoginInProgress) { $event.preventDefault(); $event.stopPropagation(); }"
x-effect="document.body.style.overflow = nostrLoginInProgress ? 'hidden' : ''"
class="fixed inset-0 z-[100] flex items-center justify-center bg-zinc-950/70 backdrop-blur-md">
<div class="mx-4 w-full max-w-md rounded-2xl bg-white px-8 py-10 text-center shadow-2xl ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
<div class="relative mx-auto flex size-20 items-center justify-center">
<span class="absolute inset-0 animate-ping rounded-full bg-amber-500/20" aria-hidden="true"></span>
<span class="absolute inset-2 rounded-full bg-amber-500/10" aria-hidden="true"></span>
<flux:icon.arrow-path class="relative size-10 animate-spin text-amber-600 dark:text-amber-400"
aria-hidden="true"/>
</div>
<flux:heading id="nostr-login-progress-heading-{{ $location }}" size="lg" class="mt-6">
Signiere mit deinem Nostr-Wallet
</flux:heading>
<flux:text id="nostr-login-progress-description-{{ $location }}" class="mt-3 text-zinc-600 dark:text-zinc-400">
Bitte bestätige die Login-Anfrage in deiner Browser-Extension.
Du wirst gleich automatisch weitergeleitet.
</flux:text>
<flux:text size="sm" class="mt-6 text-zinc-500 dark:text-zinc-500">
Schließe dieses Fenster nicht.
</flux:text>
</div>
</div>
</div>