From af9c1252d361d616f59c63d36c1b1b74190e1eac Mon Sep 17 00:00:00 2001 From: Silberengel Date: Fri, 20 Feb 2026 11:50:10 +0100 Subject: [PATCH] fix login persistence Nostr-Signature: e02d4dbaf56fb0498ca6871ae25bd5da1061eeca1d28c88d54ff5f6549982f11 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc 647fa0385224b33546c55c786b3c2cf3b2cfab5de9f9748ce814e40e8c6819131ebb9e86d7682bffa327e3b690297f17bcfb2f6b2d5fb6b65e1d9474d66659b1 --- nostr/commit-signatures.jsonl | 1 + src/lib/components/NavBar.svelte | 77 +++++++++++-- src/lib/stores/user-store.ts | 118 +++++++++++++++++--- src/routes/+layout.svelte | 45 ++++---- src/routes/+page.svelte | 58 ++++++++-- src/routes/repos/[npub]/[repo]/+page.svelte | 75 ++++++++++++- 6 files changed, 316 insertions(+), 58 deletions(-) diff --git a/nostr/commit-signatures.jsonl b/nostr/commit-signatures.jsonl index 8c13ec4..32af41d 100644 --- a/nostr/commit-signatures.jsonl +++ b/nostr/commit-signatures.jsonl @@ -15,3 +15,4 @@ {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771532649,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","adjust responsiveness"]],"content":"Signed commit: adjust responsiveness","id":"b585b4ee5862b2593c0e469974f94b16a1a60e9f57df988cf9ed157acba1c921","sig":"7daeaea11600c77d015448d293f8d7c7500c65d87cd4b496c13ba0fa9922fe5330353a3082eb4f5b540208630e668f163981cdb5e35f027191fb6abd6d0d380f"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771533104,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","add more api help"]],"content":"Signed commit: add more api help","id":"165d9bb66132123e1ac956f442e13f2ffb784e204ecdd1d3643152a5274cdd5a","sig":"deb8866643413806ec43e30faa8a47a78f0ede64616d6304e3b0a87ee3e267122e2308ed67131b73290a3ec10124c19198b05d2b5f142a3ff3e44858d1dff4fe"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771581869,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","gix build and publish CLI to npm"]],"content":"Signed commit: gix build and publish CLI to npm","id":"7515d5ecd835df785a5e896062818b469bcad83a22efa84499d1736e73ae4844","sig":"b4bb7849515c545a609df14939a0a2ddfcd08ee2160cdc01c932a4b0b55668a54fa3fe1d15ad55fe74cfdb23e6c357cf581ab0aaef44da8c64dc098202a7383f"} +{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771584107,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","pubkey lookup for maintainer\ninclude all tags in the r.a. preset\nupdate client tags on publish\nadd verification/correction step"]],"content":"Signed commit: pubkey lookup for maintainer\ninclude all tags in the r.a. preset\nupdate client tags on publish\nadd verification/correction step","id":"cc27d54e23cecca7e126e7a1b9e0881ee9c9addf39a97841992ac35422221e5d","sig":"7c5e7173e4bfc17a71cec49c8ac2fad15ecab3a84ef53ac90ba7ab6f1c051e2e6d108cecfa075917b6be8a9d1d54d3995595a0b95c004995ec89fe8a621315cd"} diff --git a/src/lib/components/NavBar.svelte b/src/lib/components/NavBar.svelte index 8b19d5a..a7ca117 100644 --- a/src/lib/components/NavBar.svelte +++ b/src/lib/components/NavBar.svelte @@ -7,31 +7,70 @@ import UserBadge from './UserBadge.svelte'; import { onMount } from 'svelte'; import { userStore } from '../stores/user-store.js'; - import { clearActivity, updateActivity } from '../services/activity-tracker.js'; + import { clearActivity, updateActivity, isSessionExpired } from '../services/activity-tracker.js'; import { determineUserLevel, decodePubkey } from '../services/nostr/user-level-service.js'; let userPubkey = $state(null); let mobileMenuOpen = $state(false); + // Sync with userStore changes + $effect(() => { + const currentUser = $userStore; + if (currentUser.userPubkey && currentUser.userPubkeyHex) { + // Check if session expired + if (isSessionExpired()) { + userStore.reset(); + userPubkey = null; + } else { + userPubkey = currentUser.userPubkey; + updateActivity(); + } + } else { + userPubkey = null; + } + }); + onMount(() => { - // Check auth asynchronously (don't await in onMount cleanup) - checkAuth(); - - // Update activity on mount - updateActivity(); + // User store already checks session expiry on initialization + // Just restore state from store (which loads from localStorage) + const currentState = $userStore; + if (currentState.userPubkey && currentState.userPubkeyHex) { + // User is logged in - restore state (already synced by $effect, but ensure it's set) + userPubkey = currentState.userPubkey; + // Update activity to extend session + updateActivity(); + } else { + // User not logged in - check auth + checkAuth(); + } // Set up activity tracking for user interactions - const updateActivityOnInteraction = () => updateActivity(); + const updateActivityOnInteraction = () => { + if (userPubkey) { + updateActivity(); + } + }; // Track various user interactions document.addEventListener('click', updateActivityOnInteraction, { passive: true }); document.addEventListener('keydown', updateActivityOnInteraction, { passive: true }); document.addEventListener('scroll', updateActivityOnInteraction, { passive: true }); + // Check session expiry periodically (every 5 minutes) + const expiryCheckInterval = setInterval(() => { + if (isSessionExpired()) { + // Session expired - logout user + userStore.reset(); + userPubkey = null; + clearInterval(expiryCheckInterval); + } + }, 5 * 60 * 1000); // Check every 5 minutes + return () => { document.removeEventListener('click', updateActivityOnInteraction); document.removeEventListener('keydown', updateActivityOnInteraction); document.removeEventListener('scroll', updateActivityOnInteraction); + clearInterval(expiryCheckInterval); }; }); @@ -118,6 +157,30 @@ // Update activity tracking on successful login updateActivity(); + // Check for pending transfer events + if (levelResult.userPubkeyHex) { + try { + const response = await fetch('/api/transfers/pending', { + headers: { + 'X-User-Pubkey': levelResult.userPubkeyHex + } + }); + if (response.ok) { + const data = await response.json(); + if (data.pendingTransfers && data.pendingTransfers.length > 0) { + // Trigger a custom event to notify layout about pending transfers + // The layout component will handle displaying the notifications + window.dispatchEvent(new CustomEvent('pendingTransfers', { + detail: { transfers: data.pendingTransfers } + })); + } + } + } catch (err) { + console.error('Failed to check for pending transfers:', err); + // Don't fail login if transfer check fails + } + } + // Show success message const { hasUnlimitedAccess } = await import('../../lib/utils/user-access.js'); if (hasUnlimitedAccess(levelResult.level)) { diff --git a/src/lib/stores/user-store.ts b/src/lib/stores/user-store.ts index 0bab24e..b8af9b5 100644 --- a/src/lib/stores/user-store.ts +++ b/src/lib/stores/user-store.ts @@ -1,9 +1,11 @@ /** * User store for managing user state and access level across the application + * Persists to localStorage for persistent login across page refreshes */ import { writable } from 'svelte/store'; import type { UserLevel } from '../services/nostr/user-level-service.js'; +import { isSessionExpired, clearActivity } from '../services/activity-tracker.js'; export interface UserState { userPubkey: string | null; @@ -13,6 +15,8 @@ export interface UserState { error: string | null; } +const STORAGE_KEY = 'gitrepublic_user_state'; + const initialState: UserState = { userPubkey: null, userPubkeyHex: null, @@ -21,16 +25,100 @@ const initialState: UserState = { error: null }; +/** + * Load user state from localStorage + */ +function loadFromStorage(): UserState | null { + if (typeof window === 'undefined') return null; + + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (!stored) return null; + + const parsed = JSON.parse(stored) as UserState; + + // Validate that we have required fields + if (!parsed.userPubkey || !parsed.userPubkeyHex) { + return null; + } + + return parsed; + } catch { + return null; + } +} + +/** + * Save user state to localStorage + */ +function saveToStorage(state: UserState): void { + if (typeof window === 'undefined') return; + + try { + // Only save if user is logged in + if (state.userPubkey && state.userPubkeyHex) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); + } else { + // Clear storage if user is logged out + localStorage.removeItem(STORAGE_KEY); + } + } catch (err) { + console.error('Failed to save user state to localStorage:', err); + } +} + +/** + * Initialize state from localStorage or use initial state + */ +function getInitialState(): UserState { + // Check if session has expired (24 hours of inactivity) + if (isSessionExpired()) { + // Session expired - clear storage and return initial state + if (typeof window !== 'undefined') { + localStorage.removeItem(STORAGE_KEY); + clearActivity(); + } + return initialState; + } + + // Try to load from storage + const stored = loadFromStorage(); + if (stored) { + return stored; + } + + return initialState; +} + function createUserStore() { - const { subscribe, set, update } = writable(initialState); + const { subscribe, set, update } = writable(getInitialState()); return { subscribe, - set, - update, - reset: () => set(initialState), + set: (newState: UserState) => { + set(newState); + saveToStorage(newState); + }, + update: (updater: (state: UserState) => UserState) => { + update(state => { + const newState = updater(state); + saveToStorage(newState); + return newState; + }); + }, + reset: () => { + if (typeof window !== 'undefined') { + localStorage.removeItem(STORAGE_KEY); + clearActivity(); + } + set(initialState); + }, setChecking: (checking: boolean) => { - update(state => ({ ...state, checkingLevel: checking })); + update(state => { + const newState = { ...state, checkingLevel: checking }; + saveToStorage(newState); + return newState; + }); }, setUser: ( userPubkey: string | null, @@ -38,14 +126,18 @@ function createUserStore() { userLevel: UserLevel, error: string | null = null ) => { - update(state => ({ - ...state, - userPubkey, - userPubkeyHex, - userLevel, - checkingLevel: false, - error - })); + update(state => { + const newState = { + ...state, + userPubkey, + userPubkeyHex, + userLevel, + checkingLevel: false, + error + }; + saveToStorage(newState); + return newState; + }); } }; } diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index dfd4cfe..d66a532 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -10,7 +10,7 @@ import { getPublicKeyWithNIP07, isNIP07Available } from '$lib/services/nostr/nip07-signer.js'; import { determineUserLevel, decodePubkey } from '$lib/services/nostr/user-level-service.js'; import { userStore } from '$lib/stores/user-store.js'; - import { isSessionExpired, updateActivity, clearActivity } from '$lib/services/activity-tracker.js'; + import { updateActivity } from '$lib/services/activity-tracker.js'; // Accept children as a snippet prop (Svelte 5) let { children }: { children: Snippet } = $props(); @@ -50,14 +50,10 @@ } applyTheme(); - // Check for session expiry (24 hours) - if (isSessionExpired()) { - // Session expired - logout user - userStore.reset(); - clearActivity(); - console.log('Session expired after 24 hours of inactivity'); - } else { - // Update activity on mount + // Update activity on mount (if user is logged in) + // Session expiry is handled by user store initialization and NavBar + const currentState = $userStore; + if (currentState.userPubkey && currentState.userPubkeyHex) { updateActivity(); } @@ -71,21 +67,27 @@ } } - // Set up periodic session expiry check (every 5 minutes) - const expiryCheckInterval = setInterval(() => { - if (isSessionExpired()) { - userStore.reset(); - clearActivity(); - console.log('Session expired after 24 hours of inactivity'); - // Optionally redirect to home page - if ($page.url.pathname !== '/') { - goto('/'); - } + // Listen for pending transfers events from login functions + const handlePendingTransfersEvent = (event: Event) => { + const customEvent = event as CustomEvent; + if (customEvent.detail?.transfers) { + // Filter out dismissed transfers + pendingTransfers = customEvent.detail.transfers.filter( + (t: { eventId: string }) => !dismissedTransfers.has(t.eventId) + ); } - }, 5 * 60 * 1000); // Check every 5 minutes + }; + + window.addEventListener('pendingTransfers', handlePendingTransfersEvent); + + // Session expiry checking is handled by: + // 1. User store initialization (checks on load) + // 2. NavBar component (checks on mount and periodically) + // 3. Splash page (+page.svelte) (checks on mount) + // No need for redundant checks here return () => { - clearInterval(expiryCheckInterval); + window.removeEventListener('pendingTransfers', handlePendingTransfersEvent); }; }); @@ -221,6 +223,7 @@ dismissedTransfers.clear(); } }); + {#if !isSplashPage} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 833570e..fd6edb8 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -6,7 +6,7 @@ import { nip19 } from 'nostr-tools'; import { determineUserLevel, decodePubkey } from '../lib/services/nostr/user-level-service.js'; import { userStore } from '../lib/stores/user-store.js'; - import { updateActivity } from '../lib/services/activity-tracker.js'; + import { updateActivity, isSessionExpired } from '../lib/services/activity-tracker.js'; let userPubkey = $state(null); let userPubkeyHex = $state(null); @@ -32,19 +32,29 @@ // Prevent body scroll when splash page is shown document.body.style.overflow = 'hidden'; - // Check userStore first - if user is already logged in, use store values - const currentUser = $userStore; - if (currentUser.userPubkey && currentUser.userPubkeyHex) { - // User is already logged in - use store values - userPubkey = currentUser.userPubkey; - userPubkeyHex = currentUser.userPubkeyHex; - checkingAuth = false; - // Don't redirect immediately - let the user see they're logged in - // They can click "View Repositories" or navigate away - } else { - // User not logged in - check if extension is available + // Check for session expiry first + if (isSessionExpired()) { + // Session expired - logout user + userStore.reset(); checkingAuth = true; checkAuth(); + } else { + // Check userStore first - if user is already logged in, use store values + const currentUser = $userStore; + if (currentUser.userPubkey && currentUser.userPubkeyHex) { + // User is already logged in - use store values + userPubkey = currentUser.userPubkey; + userPubkeyHex = currentUser.userPubkeyHex; + checkingAuth = false; + // Update activity to extend session + updateActivity(); + // Don't redirect immediately - let the user see they're logged in + // They can click "View Repositories" or navigate away + } else { + // User not logged in - check if extension is available + checkingAuth = true; + checkAuth(); + } } // Return cleanup function @@ -167,6 +177,30 @@ // Update activity tracking on successful login updateActivity(); + // Check for pending transfer events + if (levelResult.userPubkeyHex) { + try { + const response = await fetch('/api/transfers/pending', { + headers: { + 'X-User-Pubkey': levelResult.userPubkeyHex + } + }); + if (response.ok) { + const data = await response.json(); + if (data.pendingTransfers && data.pendingTransfers.length > 0) { + // Trigger a custom event to notify layout about pending transfers + // The layout component will handle displaying the notifications + window.dispatchEvent(new CustomEvent('pendingTransfers', { + detail: { transfers: data.pendingTransfers } + })); + } + } + } catch (err) { + console.error('Failed to check for pending transfers:', err); + // Don't fail login if transfer check fails + } + } + checkingLevel = false; levelMessage = null; diff --git a/src/routes/repos/[npub]/[repo]/+page.svelte b/src/routes/repos/[npub]/[repo]/+page.svelte index 7863c14..31bf039 100644 --- a/src/routes/repos/[npub]/[repo]/+page.svelte +++ b/src/routes/repos/[npub]/[repo]/+page.svelte @@ -1436,30 +1436,95 @@ // Re-check maintainer status and bookmark status after login await checkMaintainerStatus(); await loadBookmarkStatus(); + // Check for pending transfers (user is already logged in via store) + if (userPubkeyHex) { + try { + const response = await fetch('/api/transfers/pending', { + headers: { + 'X-User-Pubkey': userPubkeyHex + } + }); + if (response.ok) { + const data = await response.json(); + if (data.pendingTransfers && data.pendingTransfers.length > 0) { + window.dispatchEvent(new CustomEvent('pendingTransfers', { + detail: { transfers: data.pendingTransfers } + })); + } + } + } catch (err) { + console.error('Failed to check for pending transfers:', err); + } + } return; } - // Fallback: try NIP-07 + // Fallback: try NIP-07 - need to check write access and update store try { if (!isNIP07Available()) { alert('NIP-07 extension not found. Please install a Nostr extension like Alby or nos2x.'); return; } const pubkey = await getPublicKeyWithNIP07(); - userPubkey = pubkey; + let pubkeyHex: string; // Convert to hex if needed if (/^[0-9a-f]{64}$/i.test(pubkey)) { - userPubkeyHex = pubkey.toLowerCase(); + pubkeyHex = pubkey.toLowerCase(); + userPubkey = pubkey; } else { try { const decoded = nip19.decode(pubkey); if (decoded.type === 'npub') { - userPubkeyHex = decoded.data as string; + pubkeyHex = decoded.data as string; + userPubkey = pubkey; + } else { + throw new Error('Invalid pubkey format'); } } catch { - userPubkeyHex = pubkey; + error = 'Invalid public key format'; + return; + } + } + + userPubkeyHex = pubkeyHex; + + // Check write access and update user store + const { determineUserLevel } = await import('$lib/services/nostr/user-level-service.js'); + const levelResult = await determineUserLevel(userPubkey, userPubkeyHex); + + // Update user store with write access level + userStore.setUser( + levelResult.userPubkey, + levelResult.userPubkeyHex, + levelResult.level, + levelResult.error || null + ); + + // Update activity tracking + const { updateActivity } = await import('$lib/services/activity-tracker.js'); + updateActivity(); + + // Check for pending transfer events + if (userPubkeyHex) { + try { + const response = await fetch('/api/transfers/pending', { + headers: { + 'X-User-Pubkey': userPubkeyHex + } + }); + if (response.ok) { + const data = await response.json(); + if (data.pendingTransfers && data.pendingTransfers.length > 0) { + window.dispatchEvent(new CustomEvent('pendingTransfers', { + detail: { transfers: data.pendingTransfers } + })); + } + } + } catch (err) { + console.error('Failed to check for pending transfers:', err); } } + // Re-check maintainer status and bookmark status after login await checkMaintainerStatus(); await loadBookmarkStatus();