You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
294 lines
9.0 KiB
294 lines
9.0 KiB
<script lang="ts"> |
|
import '../app.css'; |
|
import { onMount, setContext } from 'svelte'; |
|
import { page } from '$app/stores'; |
|
import { goto } from '$app/navigation'; |
|
import Footer from '$lib/components/Footer.svelte'; |
|
import NavBar from '$lib/components/NavBar.svelte'; |
|
import TransferNotification from '$lib/components/TransferNotification.svelte'; |
|
import type { Snippet } from 'svelte'; |
|
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'; |
|
|
|
// Accept children as a snippet prop (Svelte 5) |
|
let { children }: { children: Snippet } = $props(); |
|
|
|
// Theme management - default to gitrepublic-dark (purple) |
|
let theme: 'gitrepublic-light' | 'gitrepublic-dark' | 'gitrepublic-black' = 'gitrepublic-dark'; |
|
|
|
// User level checking state |
|
let checkingUserLevel = $state(false); |
|
|
|
// Transfer notification state |
|
type PendingTransfer = { |
|
eventId: string; |
|
fromPubkey: string; |
|
toPubkey: string; |
|
repoTag: string; |
|
repoName: string; |
|
originalOwner: string; |
|
timestamp: number; |
|
createdAt: string; |
|
event: any; |
|
}; |
|
let pendingTransfers = $state<PendingTransfer[]>([]); |
|
let dismissedTransfers = $state<Set<string>>(new Set()); |
|
|
|
onMount(() => { |
|
// Only run client-side code |
|
if (typeof window === 'undefined') return; |
|
|
|
// Check for saved theme preference or default to gitrepublic-dark |
|
const savedTheme = localStorage.getItem('theme') as 'gitrepublic-light' | 'gitrepublic-dark' | 'gitrepublic-black' | null; |
|
if (savedTheme === 'gitrepublic-light' || savedTheme === 'gitrepublic-dark' || savedTheme === 'gitrepublic-black') { |
|
theme = savedTheme; |
|
} else { |
|
// Default to gitrepublic-dark (purple) |
|
theme = 'gitrepublic-dark'; |
|
} |
|
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 |
|
updateActivity(); |
|
} |
|
|
|
// Check user level if not on splash page |
|
// Only check if user store is not already initialized with a logged-in user |
|
if ($page.url.pathname !== '/') { |
|
const currentState = $userStore; |
|
// Only check if we don't have a user or if user level is strictly_rate_limited |
|
if (!currentState.userPubkey || currentState.userLevel === 'strictly_rate_limited') { |
|
checkUserLevel(); |
|
} |
|
} |
|
|
|
// 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('/'); |
|
} |
|
} |
|
}, 5 * 60 * 1000); // Check every 5 minutes |
|
|
|
return () => { |
|
clearInterval(expiryCheckInterval); |
|
}; |
|
}); |
|
|
|
async function checkUserLevel() { |
|
// Only run client-side |
|
if (typeof window === 'undefined') return; |
|
|
|
// Skip if already checking or if user store is already set |
|
const currentState = $userStore; |
|
if (checkingUserLevel || (currentState.userPubkey && currentState.userLevel !== 'strictly_rate_limited')) { |
|
return; |
|
} |
|
|
|
// Only check user level if user has explicitly logged in (has pubkey in store) |
|
// Don't automatically get pubkey from NIP-07 - that should only happen on explicit login |
|
if (!currentState.userPubkey) { |
|
// User not logged in - set to strictly rate limited without checking |
|
userStore.setUser(null, null, 'strictly_rate_limited', null); |
|
return; |
|
} |
|
|
|
checkingUserLevel = true; |
|
userStore.setChecking(true); |
|
|
|
try { |
|
// Use pubkey from store (user has explicitly logged in) |
|
const userPubkey = currentState.userPubkey; |
|
const userPubkeyHex = currentState.userPubkeyHex; |
|
|
|
// Determine user level |
|
const levelResult = await determineUserLevel(userPubkey, userPubkeyHex); |
|
|
|
// Update user store |
|
userStore.setUser( |
|
levelResult.userPubkey, |
|
levelResult.userPubkeyHex, |
|
levelResult.level, |
|
levelResult.error || null |
|
); |
|
|
|
// Update activity if user is logged in |
|
if (levelResult.userPubkey && levelResult.userPubkeyHex) { |
|
updateActivity(); |
|
// Check for pending transfers |
|
checkPendingTransfers(levelResult.userPubkeyHex); |
|
} |
|
} catch (err) { |
|
console.error('Failed to check user level:', err); |
|
// Set to strictly rate limited on error |
|
userStore.setUser(null, null, 'strictly_rate_limited', 'Failed to check user level'); |
|
} finally { |
|
checkingUserLevel = false; |
|
userStore.setChecking(false); |
|
} |
|
} |
|
|
|
async function checkPendingTransfers(userPubkeyHex: string) { |
|
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) { |
|
// Filter out dismissed transfers |
|
pendingTransfers = data.pendingTransfers.filter( |
|
(t: { eventId: string }) => !dismissedTransfers.has(t.eventId) |
|
); |
|
} |
|
} |
|
} catch (err) { |
|
console.error('Failed to check for pending transfers:', err); |
|
} |
|
} |
|
|
|
function dismissTransfer(eventId: string) { |
|
dismissedTransfers.add(eventId); |
|
pendingTransfers = pendingTransfers.filter(t => t.eventId !== eventId); |
|
} |
|
|
|
function applyTheme() { |
|
// Remove all theme attributes first |
|
document.documentElement.removeAttribute('data-theme'); |
|
document.documentElement.removeAttribute('data-theme-light'); |
|
document.documentElement.removeAttribute('data-theme-black'); |
|
|
|
// Apply the selected theme |
|
if (theme === 'gitrepublic-light') { |
|
document.documentElement.setAttribute('data-theme', 'light'); |
|
} else if (theme === 'gitrepublic-dark') { |
|
document.documentElement.setAttribute('data-theme', 'dark'); |
|
} else if (theme === 'gitrepublic-black') { |
|
document.documentElement.setAttribute('data-theme', 'black'); |
|
} |
|
localStorage.setItem('theme', theme); |
|
} |
|
|
|
function toggleTheme() { |
|
// Cycle through themes: gitrepublic-dark -> gitrepublic-light -> gitrepublic-black -> gitrepublic-dark |
|
if (theme === 'gitrepublic-dark') { |
|
theme = 'gitrepublic-light'; |
|
} else if (theme === 'gitrepublic-light') { |
|
theme = 'gitrepublic-black'; |
|
} else { |
|
theme = 'gitrepublic-dark'; |
|
} |
|
applyTheme(); |
|
} |
|
|
|
// Provide theme context to child components |
|
setContext('theme', { |
|
get theme() { return { value: theme }; }, |
|
toggleTheme |
|
}); |
|
|
|
// Hide nav bar and footer on splash page (root path) |
|
const isSplashPage = $derived($page.url.pathname === '/'); |
|
|
|
// Subscribe to user store |
|
const userState = $derived($userStore); |
|
|
|
// Check for transfers when user logs in |
|
$effect(() => { |
|
const currentUser = $userStore; |
|
if (currentUser.userPubkeyHex && !checkingUserLevel) { |
|
checkPendingTransfers(currentUser.userPubkeyHex); |
|
} else if (!currentUser.userPubkeyHex) { |
|
// Clear transfers when user logs out |
|
pendingTransfers = []; |
|
dismissedTransfers.clear(); |
|
} |
|
}); |
|
</script> |
|
|
|
{#if !isSplashPage} |
|
<NavBar /> |
|
{/if} |
|
|
|
<!-- Transfer notifications --> |
|
{#each pendingTransfers as transfer (transfer.eventId)} |
|
<TransferNotification {transfer} on:dismiss={(e) => dismissTransfer(e.detail.eventId)} /> |
|
{/each} |
|
|
|
{#if !isSplashPage && checkingUserLevel} |
|
<div class="user-level-check"> |
|
<div class="check-message"> |
|
<p>Checking user access level...</p> |
|
<div class="spinner"></div> |
|
</div> |
|
</div> |
|
{:else} |
|
{@render children()} |
|
{/if} |
|
|
|
{#if !isSplashPage} |
|
<Footer /> |
|
{/if} |
|
|
|
<style> |
|
.user-level-check { |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
min-height: 50vh; |
|
padding: 2rem; |
|
} |
|
|
|
.check-message { |
|
text-align: center; |
|
} |
|
|
|
.check-message p { |
|
margin-bottom: 1rem; |
|
color: var(--text-primary, #1a1a1a); |
|
font-size: 1.1rem; |
|
} |
|
|
|
.spinner { |
|
border: 3px solid var(--bg-secondary, #e8e8e8); |
|
border-top: 3px solid var(--accent, #007bff); |
|
border-radius: 50%; |
|
width: 40px; |
|
height: 40px; |
|
animation: spin 1s linear infinite; |
|
margin: 0 auto; |
|
} |
|
|
|
@keyframes spin { |
|
0% { transform: rotate(0deg); } |
|
100% { transform: rotate(360deg); } |
|
} |
|
|
|
@media (prefers-color-scheme: dark) { |
|
.check-message p { |
|
color: var(--text-primary, #f5f5f5); |
|
} |
|
|
|
.spinner { |
|
border-color: var(--bg-secondary, #2d2d2d); |
|
border-top-color: var(--accent, #007bff); |
|
} |
|
} |
|
</style>
|
|
|