From 6be2e1f9515f4948af39d55c98e0fad1d5c458fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nu=C5=A1a=20Puk=C5=A1i=C4=8D?= Date: Wed, 7 Jan 2026 18:49:42 +0100 Subject: [PATCH] Login: simplify, manage actions, signer login flow in modal --- .../controllers/editor/layout_controller.js | 88 +++++++++++ .../controllers/utility/login_controller.js | 11 +- .../utility/signer_modal_controller.js | 148 ++++++++++++++++++ assets/styles/02-layout/layout.css | 2 +- assets/styles/editor-layout.css | 3 - src/Twig/Components/Footer.php | 3 - src/Twig/Components/Header.php | 2 - src/Twig/Components/UserMenu.php | 7 +- templates/components/SignerModal.html.twig | 25 +++ templates/components/UserMenu.html.twig | 14 +- .../editor/panels/_articlelist.html.twig | 15 +- templates/login/index.html.twig | 5 +- 12 files changed, 293 insertions(+), 30 deletions(-) create mode 100644 assets/controllers/utility/signer_modal_controller.js create mode 100644 templates/components/SignerModal.html.twig diff --git a/assets/controllers/editor/layout_controller.js b/assets/controllers/editor/layout_controller.js index 20f47a7..7ea993a 100644 --- a/assets/controllers/editor/layout_controller.js +++ b/assets/controllers/editor/layout_controller.js @@ -14,6 +14,9 @@ export default class extends Controller { console.log('Editor layout controller connected'); this.autoSaveTimer = null; + // Expose method globally so login controllers can save state before reload + window.saveEditorStateBeforeLogin = () => this.saveCompleteStateBeforeLogin(); + // --- Editor State Object --- // See documentation/Editor/Reactivity-and-state-management.md this.state = { @@ -23,6 +26,10 @@ export default class extends Controller { content_event_json: {} // Derived event JSON }; this.hydrateState(); + + // Check for and restore state after login + this.restoreStateAfterLogin(); + this.updateMarkdownEditor(); this.updateQuillEditor(); @@ -114,6 +121,87 @@ export default class extends Controller { if (nmdField) nmdField.value = this.state.content_NMD || ''; } + saveCompleteStateBeforeLogin() { + // Save all editor content and form fields before login/reload + console.log('[Editor] Saving complete editor state before login'); + + // Sync current content first + this.syncContentBeforePublish(); + + const stateToSave = { + timestamp: Date.now(), + editorState: this.state, + formData: {} + }; + + // Save all form fields + const form = this.element.querySelector('form'); + if (form) { + const formData = new FormData(form); + for (const [key, value] of formData.entries()) { + stateToSave.formData[key] = value; + } + } + + localStorage.setItem('editorStateBeforeLogin', JSON.stringify(stateToSave)); + console.log('[Editor] State saved:', stateToSave); + } + + restoreStateAfterLogin() { + // Check if there's a saved state from before login + const savedStateStr = localStorage.getItem('editorStateBeforeLogin'); + if (!savedStateStr) { + console.log('[Editor] No saved state found'); + return; + } + + try { + const savedState = JSON.parse(savedStateStr); + const ageMinutes = (Date.now() - savedState.timestamp) / 1000 / 60; + + // Only restore if saved within last 10 minutes + if (ageMinutes > 10) { + console.log('[Editor] Saved state too old, ignoring'); + localStorage.removeItem('editorStateBeforeLogin'); + return; + } + + console.log('[Editor] Restoring state from before login:', savedState); + + // Restore editor state + if (savedState.editorState) { + this.state = savedState.editorState; + } + + // Restore form fields + if (savedState.formData) { + const form = this.element.querySelector('form'); + if (form) { + for (const [key, value] of Object.entries(savedState.formData)) { + const field = form.elements[key]; + if (field) { + if (field.type === 'checkbox') { + field.checked = value === 'on' || value === '1' || value === true; + } else { + field.value = value; + } + } + } + } + } + + // Clear the saved state + localStorage.removeItem('editorStateBeforeLogin'); + + // Show notification + this.updateStatus('✓ Your work has been restored after login'); + + } catch (e) { + console.error('[Editor] Failed to restore state:', e); + localStorage.removeItem('editorStateBeforeLogin'); + } + } + // --- Tab Switching Logic --- switchMode(event) { const mode = event.currentTarget.dataset.mode; diff --git a/assets/controllers/utility/login_controller.js b/assets/controllers/utility/login_controller.js index a7f4568..07ae18d 100644 --- a/assets/controllers/utility/login_controller.js +++ b/assets/controllers/utility/login_controller.js @@ -1,12 +1,8 @@ import { Controller } from '@hotwired/stimulus'; -import { getComponent } from '@symfony/ux-live-component'; export default class extends Controller { static targets = ['nostrError']; - async initialize() { - this.component = await getComponent(this.element); - } async loginAct(event) { if (!window.nostr) { if (this.hasNostrErrorTarget) { @@ -45,8 +41,13 @@ export default class extends Controller { if (!response.ok) return false; return 'Authentication Successful'; }) + if (!!result) { - this.component.render(); + // Save editor state before reload (if in editor) + if (typeof window.saveEditorStateBeforeLogin === 'function') { + window.saveEditorStateBeforeLogin(); + } + window.location.reload(); } } } diff --git a/assets/controllers/utility/signer_modal_controller.js b/assets/controllers/utility/signer_modal_controller.js new file mode 100644 index 0000000..787266d --- /dev/null +++ b/assets/controllers/utility/signer_modal_controller.js @@ -0,0 +1,148 @@ +import { Controller } from '@hotwired/stimulus'; +import { getPublicKey, SimplePool } from 'nostr-tools'; +import { BunkerSigner } from "nostr-tools/nip46"; +import { setRemoteSignerSession } from '../nostr/signer_manager.js'; + +export default class extends Controller { + static targets = ['dialog', 'qr', 'status']; + + connect() { + console.log('[signer-modal] controller connected'); + this._localSecretKey = null; + this._uri = null; + this._relays = []; + this._secret = null; + this._signer = null; + this._pool = null; + this._didAuth = false; + } + + disconnect() { + try { this._signer?.close?.(); } catch (_) {} + try { this._pool?.close?.([]); } catch (_) {} + } + + async openDialog() { + console.log('[signer-modal] openDialog called', this.hasDialogTarget); + if (this.hasDialogTarget) { + this.dialogTarget.style.display = 'block'; + await this._init(); + } else { + console.error('[signer-modal] dialog target not found'); + } + } + + closeDialog() { + if (this.hasDialogTarget) { + this.dialogTarget.style.display = 'none'; + // Clean up resources + try { this._signer?.close?.(); } catch (_) {} + try { this._pool?.close?.([]); } catch (_) {} + this._didAuth = false; + } + } + + async _init() { + try { + this._setStatus('Requesting pairing QR…'); + const res = await fetch('/nostr-connect/qr'); + if (!res.ok) throw new Error('QR fetch failed'); + const data = await res.json(); + + this._localSecretKey = data.privkey; + this._uri = data.uri; + this._relays = data.relays || [data.relay].filter(Boolean); + this._secret = data.secret || null; + + if (this.hasQrTarget) { + this.qrTarget.innerHTML = `Amber pairing QR`; + } + + this._checkClientPubkeyIntegrity(); + + this._setStatus('Scan with Amber (NIP-46)…'); + await this._createSigner(); + this._setStatus('Remote signer connected. Authenticating…'); + await this._attemptAuth(); + } catch (e) { + console.error('[signer-modal] init error', e); + this._setStatus('Init failed: ' + (e.message || 'unknown')); + } + } + + _checkClientPubkeyIntegrity() { + try { + if (!this._localSecretKey || !this._uri) return; + const derived = getPublicKey(this._localSecretKey); + const m = this._uri.match(/^nostrconnect:\/\/([0-9a-fA-F]{64})/); + if (!m) { + console.warn('[signer-modal] URI missing/invalid pubkey segment'); + return; + } + const uriPk = m[1].toLowerCase(); + if (uriPk !== derived.toLowerCase()) { + console.warn('[signer-modal] Pubkey mismatch: derived != URI', { derived, uriPk }); + } + } catch (e) { + console.warn('[signer-modal] integrity check failed', e); + } + } + + async _createSigner() { + this._pool = new SimplePool(); + this._setStatus('Waiting for remote signer…'); + this._signer = await BunkerSigner.fromURI(this._localSecretKey, this._uri, { pool: this._pool }); + } + + async _attemptAuth() { + if (this._didAuth) return; + this._didAuth = true; + try { + const loginEvent = { + kind: 27235, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ['u', window.location.origin + '/login'], + ['method', 'POST'], + ['t', 'bunker'] + ], + content: '' + }; + this._setStatus('Signing login request…'); + const signed = await this._signer.signEvent(loginEvent); + this._setStatus('Submitting login…'); + const resp = await fetch('/login', { + method: 'POST', + credentials: 'same-origin', + headers: { 'Authorization': 'Nostr ' + btoa(JSON.stringify(signed)) } + }); + if (resp.ok) { + setRemoteSignerSession({ + privkey: this._localSecretKey, + bunkerPointer: this._signer.bp, + uri: this._uri, + relays: this._relays, + secret: this._secret + }); + this._setStatus('Authenticated. Reloading…'); + + // Save editor state before reload (if in editor) + if (typeof window.saveEditorStateBeforeLogin === 'function') { + window.saveEditorStateBeforeLogin(); + } + + setTimeout(() => window.location.reload(), 500); + } else { + this._setStatus('Login failed (' + resp.status + ')'); + } + } catch (e) { + console.error('[signer-modal] auth error', e); + this._setStatus('Auth error: ' + (e.message || 'unknown')); + } + } + + _setStatus(msg) { + if (this.hasStatusTarget) this.statusTarget.textContent = msg; + } +} + diff --git a/assets/styles/02-layout/layout.css b/assets/styles/02-layout/layout.css index fa0105d..7a5911a 100644 --- a/assets/styles/02-layout/layout.css +++ b/assets/styles/02-layout/layout.css @@ -284,7 +284,7 @@ p.measure { margin: 1em auto; } -section{ position: relative; padding: var(--section-spacing) var(--spacing-3); } +section{ padding: var(--section-spacing) var(--spacing-3); } .katex-display>.katex { line-height: 2; diff --git a/assets/styles/editor-layout.css b/assets/styles/editor-layout.css index 5b1d027..67b0b30 100644 --- a/assets/styles/editor-layout.css +++ b/assets/styles/editor-layout.css @@ -448,10 +448,7 @@ main.editor-layout { } .articlelist-placeholder { - color: #bbb; - font-style: italic; padding: 1.5rem 0; - text-align: center; } /* Filesystem-style reading list folders */ diff --git a/src/Twig/Components/Footer.php b/src/Twig/Components/Footer.php index 58bd34d..dcfadd0 100644 --- a/src/Twig/Components/Footer.php +++ b/src/Twig/Components/Footer.php @@ -8,7 +8,4 @@ use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; #[AsTwigComponent] class Footer { - public function __construct() - { - } } diff --git a/src/Twig/Components/Header.php b/src/Twig/Components/Header.php index 857134b..1e345ea 100644 --- a/src/Twig/Components/Header.php +++ b/src/Twig/Components/Header.php @@ -4,8 +4,6 @@ declare(strict_types=1); namespace App\Twig\Components; -use Psr\Cache\InvalidArgumentException; -use Symfony\Contracts\Cache\CacheInterface; use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; #[AsTwigComponent] diff --git a/src/Twig/Components/UserMenu.php b/src/Twig/Components/UserMenu.php index 5596c93..226d3d2 100644 --- a/src/Twig/Components/UserMenu.php +++ b/src/Twig/Components/UserMenu.php @@ -1,12 +1,9 @@ +
+
+ + +
+ + diff --git a/templates/components/UserMenu.html.twig b/templates/components/UserMenu.html.twig index 696993e..de077db 100644 --- a/templates/components/UserMenu.html.twig +++ b/templates/components/UserMenu.html.twig @@ -1,4 +1,5 @@ -
+
+
{% if app.user %}
@@ -22,7 +23,7 @@
  • {{ 'heading.logout'|trans }} + data-action="click->nostr--logout#handleLogout">{{ 'heading.logout'|trans }}
  • {% else %} @@ -36,14 +37,11 @@ Extension
  • - Signer + Signer
  • + {% endif %} -
    -
    -
    -
    -
    +
    diff --git a/templates/editor/panels/_articlelist.html.twig b/templates/editor/panels/_articlelist.html.twig index 366bb1b..ced94e0 100644 --- a/templates/editor/panels/_articlelist.html.twig +++ b/templates/editor/panels/_articlelist.html.twig @@ -81,7 +81,20 @@ {% endfor %} {% else %} -
    Sign in to see your articles.
    +
    +
    Sign in to see your articles.
    + +
      +
    • + + Extension +
    • +
    • + Signer +
    • +
    + +
    {% endif %}
    diff --git a/templates/login/index.html.twig b/templates/login/index.html.twig index 2171f21..fa13046 100644 --- a/templates/login/index.html.twig +++ b/templates/login/index.html.twig @@ -9,11 +9,12 @@
    You are already authenticated.
    {% else %}
    -
    +

    Use your Nostr credentials to authenticate.

    - Login with a remote signer +
    +
    {% endif %}