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. 77
      src/lib/components/NavBar.svelte
  3. 118
      src/lib/stores/user-store.ts
  4. 45
      src/routes/+layout.svelte
  5. 58
      src/routes/+page.svelte
  6. 75
      src/routes/repos/[npub]/[repo]/+page.svelte

1
nostr/commit-signatures.jsonl

@ -15,3 +15,4 @@ @@ -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"}

77
src/lib/components/NavBar.svelte

@ -7,31 +7,70 @@ @@ -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<string | null>(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 @@ @@ -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)) {

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

@ -1,9 +1,11 @@ @@ -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 { @@ -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 = { @@ -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<UserState>(initialState);
const { subscribe, set, update } = writable<UserState>(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() { @@ -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;
});
}
};
}

45
src/routes/+layout.svelte

@ -10,7 +10,7 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -221,6 +223,7 @@
dismissedTransfers.clear();
}
});
</script>
{#if !isSplashPage}

58
src/routes/+page.svelte

@ -6,7 +6,7 @@ @@ -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<string | null>(null);
let userPubkeyHex = $state<string | null>(null);
@ -32,19 +32,29 @@ @@ -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 @@ @@ -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;

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

@ -1436,30 +1436,95 @@ @@ -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();

Loading…
Cancel
Save