query('redirect_uri', MobileAuthController::ALLOWED_REDIRECT_URIS[0]); abort_unless( in_array($redirectUri, MobileAuthController::ALLOWED_REDIRECT_URIS, true), 403, 'Invalid redirect_uri', ); $this->redirectUri = $redirectUri; $this->deviceName = str((string) request()->query('device_name', MobileAuthController::DEFAULT_DEVICE_NAME)) ->limit(64, '') ->whenEmpty(fn () => str(MobileAuthController::DEFAULT_DEVICE_NAME)) ->value(); // The completion/confirm controller reads the flow state from the // session — the wallet/signer callback arrives outside this session, // so it can't carry the redirect target itself. session([ 'mobile_auth' => [ 'redirect_uri' => $this->redirectUri, 'device_name' => $this->deviceName, ], ]); if (auth()->check()) { return; } $this->initChallenge(); } /** * Generate a fresh k1 challenge shared by both login methods: the * Lightning wallet signs it via LNURL-auth, the Nostr signer puts it * into the challenge tag of a kind-22242 event. Whichever callback * verifies first stores the LoginKey row that checkAuth polls for. */ protected function initChallenge(): void { $this->k1 = bin2hex(str()->random(32)); if (app()->environment('local')) { $url = 'https://mmy4dp8eab.sharedwithexpose.com/api/lnurl-auth-callback?tag=login&k1='.$this->k1.'&action=login'; } else { $url = url('/api/lnurl-auth-callback?tag=login&k1='.$this->k1.'&action=login'); } $this->lnurl = lnurl\encodeUrl($url); // NIP-55 signers append the signed event JSON to the callback URL. // Amber strips query strings when rebuilding the URL, so the k1 // travels in the path and the event lands after the trailing slash. $this->signerCallbackUrl = url('/auth/mobile/signed').'/'.$this->k1.'/'; $image = 'public/img/domains/'.session('lang_country', 'de-DE').'.jpg'; if (! file_exists(base_path($image))) { $image = 'public/img/domains/de-DE.jpg'; } $this->qrCode = base64_encode(QrCode::format('png') ->size(300) ->merge('/'.$image, .3) ->errorCorrection('H') ->generate($this->lnurl)); } public function checkAuth(): void { $loginKey = LoginKey::query() ->where('k1', $this->k1) ->where('created_at', '>=', now()->subMinutes(5)) ->first(); if (! $loginKey) { return; } // Same handoff pattern as the Lightning login: navigate via the // client instead of redirecting from inside wire:poll, so a stray // poll tick can't race the completion request. $this->dispatch( 'mobile-login-ready', url: route('auth.mobile.complete', ['k1' => $this->k1]), ); } public function resetAuth(): void { $this->initChallenge(); } public function switchAccount(): void { auth()->guard('web')->logout(); session()->invalidate(); session()->regenerateToken(); $this->redirect(route('auth.mobile', [ 'redirect_uri' => $this->redirectUri, 'device_name' => $this->deviceName, ])); } }; ?>