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.
This commit is contained in:
HolgerHatGarKeineNode
2026-05-20 01:51:31 +02:00
parent 6bb7d93d1d
commit 52cf81abca
11 changed files with 68 additions and 31 deletions
+25 -3
View File
@@ -39,6 +39,25 @@ class NostrAuth
return $challenge;
}
/**
* Return the active session challenge if still valid, otherwise issue a
* fresh one. Keeps the sidebar + navbar auth-button mounts in sync
* without this, the second mount would overwrite the first in the
* session and leave the first instance's rendered data-attribute
* pointing at a stale challenge.
*/
public static function currentOrIssueChallenge(): string
{
$existing = Session::get(self::CHALLENGE_SESSION_KEY);
$expiresAt = (int) Session::get(self::CHALLENGE_EXPIRES_SESSION_KEY, 0);
if (is_string($existing) && $existing !== '' && $expiresAt >= now()->timestamp) {
return $existing;
}
return self::issueChallenge();
}
/**
* Verify a signed NIP-42-style login event and log the holder of the pubkey in.
*
@@ -106,7 +125,10 @@ class NostrAuth
&& $expiresAt >= now()->timestamp
&& hash_equals($expectedChallenge, $challengeFromEvent);
$eventJson = json_encode([
// swentel's verify() accepts an object directly, so we pass a normalized
// stdClass and skip an unnecessary json_encode + json_decode round-trip.
// Casts here enforce the property types swentel checks against.
$normalizedEvent = (object) [
'id' => (string) $signedEvent['id'],
'pubkey' => (string) $signedEvent['pubkey'],
'created_at' => $createdAt,
@@ -114,11 +136,11 @@ class NostrAuth
'tags' => $signedEvent['tags'],
'content' => (string) $signedEvent['content'],
'sig' => (string) $signedEvent['sig'],
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
];
$sigValid = false;
try {
$sigValid = (new NostrEvent)->verify($eventJson);
$sigValid = (new NostrEvent)->verify($normalizedEvent);
} catch (\Throwable) {
$sigValid = false;
}