Browse Source

fix login persistence

Nostr-Signature: e02d4dbaf56fb0498ca6871ae25bd5da1061eeca1d28c88d54ff5f6549982f11 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc 647fa0385224b33546c55c786b3c2cf3b2cfab5de9f9748ce814e40e8c6819131ebb9e86d7682bffa327e3b690297f17bcfb2f6b2d5fb6b65e1d9474d66659b1
main
Silberengel 3 weeks ago
parent
commit
af9c1252d3
  1. 1
      nostr/commit-signatures.jsonl
  2. 75
      src/lib/components/NavBar.svelte
  3. 106
      src/lib/stores/user-store.ts
  4. 45
      src/routes/+layout.svelte
  5. 36
      src/routes/+page.svelte
  6. 75
      src/routes/repos/[npub]/[repo]/+page.svelte

1
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":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":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":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"}

75
src/lib/components/NavBar.svelte

@ -7,31 +7,70 @@
import UserBadge from './UserBadge.svelte'; import UserBadge from './UserBadge.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { userStore } from '../stores/user-store.js'; 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'; import { determineUserLevel, decodePubkey } from '../services/nostr/user-level-service.js';
let userPubkey = $state<string | null>(null); let userPubkey = $state<string | null>(null);
let mobileMenuOpen = $state(false); let mobileMenuOpen = $state(false);
onMount(() => { // Sync with userStore changes
// Check auth asynchronously (don't await in onMount cleanup) $effect(() => {
checkAuth(); 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;
}
});
// Update activity on mount onMount(() => {
// 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(); updateActivity();
} else {
// User not logged in - check auth
checkAuth();
}
// Set up activity tracking for user interactions // Set up activity tracking for user interactions
const updateActivityOnInteraction = () => updateActivity(); const updateActivityOnInteraction = () => {
if (userPubkey) {
updateActivity();
}
};
// Track various user interactions // Track various user interactions
document.addEventListener('click', updateActivityOnInteraction, { passive: true }); document.addEventListener('click', updateActivityOnInteraction, { passive: true });
document.addEventListener('keydown', updateActivityOnInteraction, { passive: true }); document.addEventListener('keydown', updateActivityOnInteraction, { passive: true });
document.addEventListener('scroll', 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 () => { return () => {
document.removeEventListener('click', updateActivityOnInteraction); document.removeEventListener('click', updateActivityOnInteraction);
document.removeEventListener('keydown', updateActivityOnInteraction); document.removeEventListener('keydown', updateActivityOnInteraction);
document.removeEventListener('scroll', updateActivityOnInteraction); document.removeEventListener('scroll', updateActivityOnInteraction);
clearInterval(expiryCheckInterval);
}; };
}); });
@ -118,6 +157,30 @@
// Update activity tracking on successful login // Update activity tracking on successful login
updateActivity(); 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 // Show success message
const { hasUnlimitedAccess } = await import('../../lib/utils/user-access.js'); const { hasUnlimitedAccess } = await import('../../lib/utils/user-access.js');
if (hasUnlimitedAccess(levelResult.level)) { if (hasUnlimitedAccess(levelResult.level)) {

106
src/lib/stores/user-store.ts

@ -1,9 +1,11 @@
/** /**
* User store for managing user state and access level across the application * 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 { writable } from 'svelte/store';
import type { UserLevel } from '../services/nostr/user-level-service.js'; import type { UserLevel } from '../services/nostr/user-level-service.js';
import { isSessionExpired, clearActivity } from '../services/activity-tracker.js';
export interface UserState { export interface UserState {
userPubkey: string | null; userPubkey: string | null;
@ -13,6 +15,8 @@ export interface UserState {
error: string | null; error: string | null;
} }
const STORAGE_KEY = 'gitrepublic_user_state';
const initialState: UserState = { const initialState: UserState = {
userPubkey: null, userPubkey: null,
userPubkeyHex: null, userPubkeyHex: null,
@ -21,16 +25,100 @@ const initialState: UserState = {
error: null 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() { function createUserStore() {
const { subscribe, set, update } = writable<UserState>(initialState); const { subscribe, set, update } = writable<UserState>(getInitialState());
return { return {
subscribe, subscribe,
set, set: (newState: UserState) => {
update, set(newState);
reset: () => set(initialState), 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) => { setChecking: (checking: boolean) => {
update(state => ({ ...state, checkingLevel: checking })); update(state => {
const newState = { ...state, checkingLevel: checking };
saveToStorage(newState);
return newState;
});
}, },
setUser: ( setUser: (
userPubkey: string | null, userPubkey: string | null,
@ -38,14 +126,18 @@ function createUserStore() {
userLevel: UserLevel, userLevel: UserLevel,
error: string | null = null error: string | null = null
) => { ) => {
update(state => ({ update(state => {
const newState = {
...state, ...state,
userPubkey, userPubkey,
userPubkeyHex, userPubkeyHex,
userLevel, userLevel,
checkingLevel: false, checkingLevel: false,
error error
})); };
saveToStorage(newState);
return newState;
});
} }
}; };
} }

45
src/routes/+layout.svelte

@ -10,7 +10,7 @@
import { getPublicKeyWithNIP07, isNIP07Available } from '$lib/services/nostr/nip07-signer.js'; import { getPublicKeyWithNIP07, isNIP07Available } from '$lib/services/nostr/nip07-signer.js';
import { determineUserLevel, decodePubkey } from '$lib/services/nostr/user-level-service.js'; import { determineUserLevel, decodePubkey } from '$lib/services/nostr/user-level-service.js';
import { userStore } from '$lib/stores/user-store.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) // Accept children as a snippet prop (Svelte 5)
let { children }: { children: Snippet } = $props(); let { children }: { children: Snippet } = $props();
@ -50,14 +50,10 @@
} }
applyTheme(); applyTheme();
// Check for session expiry (24 hours) // Update activity on mount (if user is logged in)
if (isSessionExpired()) { // Session expiry is handled by user store initialization and NavBar
// Session expired - logout user const currentState = $userStore;
userStore.reset(); if (currentState.userPubkey && currentState.userPubkeyHex) {
clearActivity();
console.log('Session expired after 24 hours of inactivity');
} else {
// Update activity on mount
updateActivity(); updateActivity();
} }
@ -71,21 +67,27 @@
} }
} }
// Set up periodic session expiry check (every 5 minutes) // Listen for pending transfers events from login functions
const expiryCheckInterval = setInterval(() => { const handlePendingTransfersEvent = (event: Event) => {
if (isSessionExpired()) { const customEvent = event as CustomEvent;
userStore.reset(); if (customEvent.detail?.transfers) {
clearActivity(); // Filter out dismissed transfers
console.log('Session expired after 24 hours of inactivity'); pendingTransfers = customEvent.detail.transfers.filter(
// Optionally redirect to home page (t: { eventId: string }) => !dismissedTransfers.has(t.eventId)
if ($page.url.pathname !== '/') { );
goto('/');
}
} }
}, 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 () => { return () => {
clearInterval(expiryCheckInterval); window.removeEventListener('pendingTransfers', handlePendingTransfersEvent);
}; };
}); });
@ -221,6 +223,7 @@
dismissedTransfers.clear(); dismissedTransfers.clear();
} }
}); });
</script> </script>
{#if !isSplashPage} {#if !isSplashPage}

36
src/routes/+page.svelte

@ -6,7 +6,7 @@
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { determineUserLevel, decodePubkey } from '../lib/services/nostr/user-level-service.js'; import { determineUserLevel, decodePubkey } from '../lib/services/nostr/user-level-service.js';
import { userStore } from '../lib/stores/user-store.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<string | null>(null); let userPubkey = $state<string | null>(null);
let userPubkeyHex = $state<string | null>(null); let userPubkeyHex = $state<string | null>(null);
@ -32,6 +32,13 @@
// Prevent body scroll when splash page is shown // Prevent body scroll when splash page is shown
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
// 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 // Check userStore first - if user is already logged in, use store values
const currentUser = $userStore; const currentUser = $userStore;
if (currentUser.userPubkey && currentUser.userPubkeyHex) { if (currentUser.userPubkey && currentUser.userPubkeyHex) {
@ -39,6 +46,8 @@
userPubkey = currentUser.userPubkey; userPubkey = currentUser.userPubkey;
userPubkeyHex = currentUser.userPubkeyHex; userPubkeyHex = currentUser.userPubkeyHex;
checkingAuth = false; checkingAuth = false;
// Update activity to extend session
updateActivity();
// Don't redirect immediately - let the user see they're logged in // Don't redirect immediately - let the user see they're logged in
// They can click "View Repositories" or navigate away // They can click "View Repositories" or navigate away
} else { } else {
@ -46,6 +55,7 @@
checkingAuth = true; checkingAuth = true;
checkAuth(); checkAuth();
} }
}
// Return cleanup function // Return cleanup function
return () => { return () => {
@ -167,6 +177,30 @@
// Update activity tracking on successful login // Update activity tracking on successful login
updateActivity(); 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; checkingLevel = false;
levelMessage = null; levelMessage = null;

75
src/routes/repos/[npub]/[repo]/+page.svelte

@ -1436,30 +1436,95 @@
// Re-check maintainer status and bookmark status after login // Re-check maintainer status and bookmark status after login
await checkMaintainerStatus(); await checkMaintainerStatus();
await loadBookmarkStatus(); 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; return;
} }
// Fallback: try NIP-07 // Fallback: try NIP-07 - need to check write access and update store
try { try {
if (!isNIP07Available()) { if (!isNIP07Available()) {
alert('NIP-07 extension not found. Please install a Nostr extension like Alby or nos2x.'); alert('NIP-07 extension not found. Please install a Nostr extension like Alby or nos2x.');
return; return;
} }
const pubkey = await getPublicKeyWithNIP07(); const pubkey = await getPublicKeyWithNIP07();
userPubkey = pubkey; let pubkeyHex: string;
// Convert to hex if needed // Convert to hex if needed
if (/^[0-9a-f]{64}$/i.test(pubkey)) { if (/^[0-9a-f]{64}$/i.test(pubkey)) {
userPubkeyHex = pubkey.toLowerCase(); pubkeyHex = pubkey.toLowerCase();
userPubkey = pubkey;
} else { } else {
try { try {
const decoded = nip19.decode(pubkey); const decoded = nip19.decode(pubkey);
if (decoded.type === 'npub') { if (decoded.type === 'npub') {
userPubkeyHex = decoded.data as string; pubkeyHex = decoded.data as string;
userPubkey = pubkey;
} else {
throw new Error('Invalid pubkey format');
} }
} catch { } 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 // Re-check maintainer status and bookmark status after login
await checkMaintainerStatus(); await checkMaintainerStatus();
await loadBookmarkStatus(); await loadBookmarkStatus();

Loading…
Cancel
Save