mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-app.git
synced 2026-05-05 04:54:53 +00:00
✨ **Nostr Login:** Added server-side fallback for fresh challenges and improved client-side challenge resolution.
- 🔄 `requestNostrChallenge` now issues a new challenge when needed. - 🛡️ Enhanced fallback logic in `nostrLogin.js` to ensure robust challenge retrieval. - ✅ Added test coverage for fresh challenge issuance.
This commit is contained in:
@@ -10,15 +10,39 @@ export default () => ({
|
||||
this.startTime = Date.now();
|
||||
},
|
||||
|
||||
async openNostrLogin() {
|
||||
const livewireComponent = this.$el.closest('[wire\\:id]')?.__livewire;
|
||||
const rawChallenge = livewireComponent?.$wire?.nostrChallenge;
|
||||
async resolveChallenge() {
|
||||
// 1) Prefer the data-attribute rendered straight from Blade. This avoids
|
||||
// Livewire's $wire proxy entirely and survives any reactive snapshot
|
||||
// quirks that have surfaced behind HTTP caches on production.
|
||||
const fromDataset = this.$root?.dataset?.nostrChallenge;
|
||||
if (typeof fromDataset === 'string' && fromDataset !== '') {
|
||||
return fromDataset;
|
||||
}
|
||||
|
||||
// Livewire's $wire proxy returns a function (server-action fallback) when
|
||||
// a property is missing from the snapshot. Only accept a non-empty string.
|
||||
const challenge = typeof rawChallenge === 'string' && rawChallenge !== ''
|
||||
? rawChallenge
|
||||
: null;
|
||||
// 2) Fallback to the Livewire snapshot via $wire.
|
||||
const livewireComponent = this.$el.closest('[wire\\:id]')?.__livewire;
|
||||
const fromWire = livewireComponent?.$wire?.nostrChallenge;
|
||||
if (typeof fromWire === 'string' && fromWire !== '') {
|
||||
return fromWire;
|
||||
}
|
||||
|
||||
// 3) Last resort: ask the server for a freshly issued challenge.
|
||||
if (livewireComponent?.$wire?.requestNostrChallenge) {
|
||||
try {
|
||||
const refreshed = await livewireComponent.$wire.requestNostrChallenge();
|
||||
if (typeof refreshed === 'string' && refreshed !== '') {
|
||||
return refreshed;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('requestNostrChallenge failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
async openNostrLogin() {
|
||||
const challenge = await this.resolveChallenge();
|
||||
|
||||
if (!challenge) {
|
||||
this.showAuthError('Login challenge missing. Please reload and try again.');
|
||||
|
||||
@@ -61,13 +61,37 @@ class extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a fresh Nostr login challenge, persist it to the session, and
|
||||
* return the value. Used both during mount() and as a JS-driven fallback
|
||||
* (see requestNostrChallenge()) when the rendered challenge is missing
|
||||
* on the client — e.g. behind an HTTP cache that stripped the snapshot.
|
||||
*/
|
||||
protected function issueNostrChallenge(): string
|
||||
{
|
||||
$challenge = bin2hex(random_bytes(32));
|
||||
$this->nostrChallenge = $challenge;
|
||||
Session::put('nostr_login_challenge', $challenge);
|
||||
Session::put('nostr_login_challenge_expires_at', now()->addSeconds(self::NOSTR_CHALLENGE_TTL_SECONDS)->timestamp);
|
||||
|
||||
return $challenge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Server-side fallback for the JS layer: returns a fresh challenge.
|
||||
* Always issues a new one so a stale rendered snapshot can be recovered
|
||||
* without forcing the user to reload the page.
|
||||
*/
|
||||
public function requestNostrChallenge(): string
|
||||
{
|
||||
return $this->issueNostrChallenge();
|
||||
}
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->currentLangCountry = session('lang_country') ?? 'de-DE';
|
||||
|
||||
$this->nostrChallenge = bin2hex(random_bytes(32));
|
||||
Session::put('nostr_login_challenge', $this->nostrChallenge);
|
||||
Session::put('nostr_login_challenge_expires_at', now()->addSeconds(self::NOSTR_CHALLENGE_TTL_SECONDS)->timestamp);
|
||||
$this->issueNostrChallenge();
|
||||
|
||||
// Nur beim ersten Mount initialisieren
|
||||
if ($this->k1 === null) {
|
||||
@@ -306,7 +330,8 @@ class extends Component {
|
||||
?>
|
||||
|
||||
<div class="flex min-h-screen" x-data="nostrLogin"
|
||||
x-init="initErrorPolling">
|
||||
x-init="initErrorPolling"
|
||||
data-nostr-challenge="{{ $nostrChallenge ?? '' }}">
|
||||
<div class="flex-1 flex justify-center items-center">
|
||||
<div class="w-80 max-w-80 space-y-6">
|
||||
<!-- Logo -->
|
||||
|
||||
@@ -66,6 +66,23 @@ it('creates a new user and dispatches FetchNostrProfileJob when an unknown pubke
|
||||
expect(auth()->id())->toBe($user->id);
|
||||
});
|
||||
|
||||
it('issues a fresh challenge when requestNostrChallenge is called and the same value is verifiable', function () {
|
||||
$component = Livewire::test('auth.login');
|
||||
|
||||
$initial = Session::get('nostr_login_challenge');
|
||||
expect($initial)->toBeString()->not->toBe('');
|
||||
|
||||
$component->call('requestNostrChallenge');
|
||||
|
||||
$refreshed = Session::get('nostr_login_challenge');
|
||||
expect($refreshed)
|
||||
->toBeString()
|
||||
->not->toBe('')
|
||||
->not->toBe($initial);
|
||||
|
||||
$component->assertSet('nostrChallenge', $refreshed);
|
||||
});
|
||||
|
||||
it('logs in an existing user without creating a duplicate when their pubkey is already known', function () {
|
||||
Queue::fake();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user