From dcb62688be11d2c27149927c2411f764d174e6f2 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Wed, 18 Feb 2026 16:08:21 +0100 Subject: [PATCH] bug-fixes for api --- src/lib/components/NavBar.svelte | 3 +- src/lib/services/git/repo-manager.ts | 6 +- src/lib/services/messaging/event-forwarder.ts | 3 +- .../messaging/preferences-storage.server.ts | 3 +- src/lib/services/nostr/repo-polling.ts | 3 +- src/lib/services/nostr/user-level-service.ts | 3 +- src/lib/utils/api-repo-helper.ts | 56 +++++++++++++++++++ src/routes/+page.svelte | 3 +- .../repos/[npub]/[repo]/branches/+server.ts | 50 +++++------------ .../api/repos/[npub]/[repo]/clone/+server.ts | 3 +- .../repos/[npub]/[repo]/commits/+server.ts | 43 +++++--------- .../repos/[npub]/[repo]/download/+server.ts | 36 ++---------- .../api/repos/[npub]/[repo]/file/+server.ts | 30 ++-------- .../api/repos/[npub]/[repo]/fork/+server.ts | 3 +- .../api/repos/[npub]/[repo]/readme/+server.ts | 30 +++++++--- .../api/repos/[npub]/[repo]/tree/+server.ts | 47 ++++++---------- src/routes/api/user/git-dashboard/+server.ts | 3 +- .../api/user/messaging-preferences/+server.ts | 7 ++- .../messaging-preferences/summary/+server.ts | 3 +- src/routes/api/user/ssh-keys/+server.ts | 3 +- src/routes/signup/+page.svelte | 4 +- 21 files changed, 166 insertions(+), 176 deletions(-) create mode 100644 src/lib/utils/api-repo-helper.ts diff --git a/src/lib/components/NavBar.svelte b/src/lib/components/NavBar.svelte index a04e5df..a4a41f7 100644 --- a/src/lib/components/NavBar.svelte +++ b/src/lib/components/NavBar.svelte @@ -119,7 +119,8 @@ updateActivity(); // Show success message - if (levelResult.level === 'unlimited') { + const { hasUnlimitedAccess } = await import('../../lib/utils/user-access.js'); + if (hasUnlimitedAccess(levelResult.level)) { console.log('Unlimited access granted!'); } else if (levelResult.level === 'rate_limited') { console.log('Logged in with rate-limited access.'); diff --git a/src/lib/services/git/repo-manager.ts b/src/lib/services/git/repo-manager.ts index 7d540c8..cbf4d61 100644 --- a/src/lib/services/git/repo-manager.ts +++ b/src/lib/services/git/repo-manager.ts @@ -124,8 +124,9 @@ export class RepoManager { const isNewRepo = !repoExists; if (isNewRepo && !isExistingRepo) { const { getCachedUserLevel } = await import('../security/user-level-cache.js'); + const { hasUnlimitedAccess } = await import('../utils/user-access.js'); const userLevel = getCachedUserLevel(event.pubkey); - if (!userLevel || userLevel.level !== 'unlimited') { + if (!hasUnlimitedAccess(userLevel?.level)) { throw new Error(`Repository creation requires unlimited access. User has level: ${userLevel?.level || 'none'}`); } } @@ -532,8 +533,9 @@ export class RepoManager { // For private repos, require owner to have unlimited access to prevent unauthorized creation if (!isPublic) { const { getCachedUserLevel } = await import('../security/user-level-cache.js'); + const { hasUnlimitedAccess } = await import('../utils/user-access.js'); const userLevel = getCachedUserLevel(announcementEvent.pubkey); - if (!userLevel || userLevel.level !== 'unlimited') { + if (!hasUnlimitedAccess(userLevel?.level)) { logger.warn({ npub, repoName, diff --git a/src/lib/services/messaging/event-forwarder.ts b/src/lib/services/messaging/event-forwarder.ts index c13a96e..b401cbb 100644 --- a/src/lib/services/messaging/event-forwarder.ts +++ b/src/lib/services/messaging/event-forwarder.ts @@ -621,7 +621,8 @@ export async function forwardEventIfEnabled( try { // Early returns for eligibility checks const cached = getCachedUserLevel(userPubkeyHex); - if (!cached || cached.level !== 'unlimited') { + const { hasUnlimitedAccess } = await import('../utils/user-access.js'); + if (!hasUnlimitedAccess(cached?.level)) { return; } diff --git a/src/lib/services/messaging/preferences-storage.server.ts b/src/lib/services/messaging/preferences-storage.server.ts index 66dd605..272889e 100644 --- a/src/lib/services/messaging/preferences-storage.server.ts +++ b/src/lib/services/messaging/preferences-storage.server.ts @@ -213,7 +213,8 @@ export async function storePreferences( // Verify user has unlimited access const cached = getCachedUserLevel(userPubkeyHex); - if (!cached || cached.level !== 'unlimited') { + const { hasUnlimitedAccess } = await import('../utils/user-access.js'); + if (!hasUnlimitedAccess(cached?.level)) { throw new Error('Messaging forwarding requires unlimited access'); } diff --git a/src/lib/services/nostr/repo-polling.ts b/src/lib/services/nostr/repo-polling.ts index a3d9124..8ae5d9c 100644 --- a/src/lib/services/nostr/repo-polling.ts +++ b/src/lib/services/nostr/repo-polling.ts @@ -167,7 +167,8 @@ export class RepoPollingService { // This prevents spam and abuse if (!isExistingRepo) { const userLevel = getCachedUserLevel(event.pubkey); - if (!userLevel || userLevel.level !== 'unlimited') { + const { hasUnlimitedAccess } = await import('../utils/user-access.js'); + if (!hasUnlimitedAccess(userLevel?.level)) { logger.warn({ eventId: event.id, pubkey: event.pubkey.slice(0, 16) + '...', diff --git a/src/lib/services/nostr/user-level-service.ts b/src/lib/services/nostr/user-level-service.ts index 886b47a..6472ebc 100644 --- a/src/lib/services/nostr/user-level-service.ts +++ b/src/lib/services/nostr/user-level-service.ts @@ -16,6 +16,7 @@ import { createProofEvent } from './relay-write-proof.js'; import { nip19 } from 'nostr-tools'; import { NostrClient } from './nostr-client.js'; import { DEFAULT_NOSTR_RELAYS } from '../../config.js'; +import { hasUnlimitedAccess } from '../../utils/user-access.js'; export type UserLevel = 'unlimited' | 'rate_limited' | 'strictly_rate_limited'; @@ -84,7 +85,7 @@ export async function checkRelayWriteAccess( const result = await response.json(); return { - hasAccess: result.level === 'unlimited', + hasAccess: hasUnlimitedAccess(result.level as UserLevel), error: result.error }; } catch (error) { diff --git a/src/lib/utils/api-repo-helper.ts b/src/lib/utils/api-repo-helper.ts new file mode 100644 index 0000000..703e1b4 --- /dev/null +++ b/src/lib/utils/api-repo-helper.ts @@ -0,0 +1,56 @@ +/** + * Helper utilities for API-based repository fetching + * Used by endpoints to fetch repo metadata without cloning + */ + +import { fetchRepoMetadata, extractGitUrls } from '../services/git/api-repo-fetcher.js'; +import type { NostrEvent } from '../types/nostr.js'; +import logger from '../services/logger.js'; + +/** + * Try to fetch repository metadata via API from clone URLs + * Returns null if API fetching fails or no clone URLs available + */ +export async function tryApiFetch( + announcementEvent: NostrEvent, + npub: string, + repoName: string +): Promise<{ + branches: Array<{ name: string; commit: { sha: string; message: string; author: string; date: string } }>; + defaultBranch: string; + files?: Array<{ name: string; path: string; type: 'file' | 'dir'; size?: number }>; + commits?: Array<{ sha: string; message: string; author: string; date: string }>; +} | null> { + try { + const cloneUrls = extractGitUrls(announcementEvent); + + if (cloneUrls.length === 0) { + logger.debug({ npub, repoName }, 'No clone URLs found for API fetch'); + return null; + } + + // Try each clone URL until one works + for (const url of cloneUrls) { + try { + const metadata = await fetchRepoMetadata(url, npub, repoName); + + if (metadata) { + return { + branches: metadata.branches, + defaultBranch: metadata.defaultBranch, + files: metadata.files, + commits: metadata.commits + }; + } + } catch (err) { + logger.debug({ error: err, url, npub, repoName }, 'API fetch failed for URL, trying next'); + continue; + } + } + + return null; + } catch (err) { + logger.warn({ error: err, npub, repoName }, 'Error attempting API fetch'); + return null; + } +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 75294d7..bc46687 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -171,7 +171,8 @@ levelMessage = null; // Show appropriate message based on level - if (levelResult.level === 'unlimited') { + const { hasUnlimitedAccess } = await import('../lib/utils/user-access.js'); + if (hasUnlimitedAccess(levelResult.level)) { levelMessage = 'Unlimited access granted!'; } else if (levelResult.level === 'rate_limited') { levelMessage = 'Logged in with rate-limited access.'; diff --git a/src/routes/api/repos/[npub]/[repo]/branches/+server.ts b/src/routes/api/repos/[npub]/[repo]/branches/+server.ts index 9d88dec..6c2cb21 100644 --- a/src/routes/api/repos/[npub]/[repo]/branches/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/branches/+server.ts @@ -54,43 +54,21 @@ export const GET: RequestHandler = createRepoGetHandler( } if (events.length > 0) { - // Try to fetch the repository from remote clone URLs - let fetched = false; - try { - fetched = await repoManager.fetchRepoOnDemand( - context.npub, - context.repo, - events[0] - ); - } catch (fetchError) { - // Log the actual error for debugging - console.error('[Branches] Error in fetchRepoOnDemand:', fetchError); - // Continue to check if repo exists anyway (might have been created despite error) - } + // Try API-based fetching first (no cloning) + const { tryApiFetch } = await import('$lib/utils/api-repo-helper.js'); + const apiData = await tryApiFetch(events[0], context.npub, context.repo); - // Always check if repo exists after fetch attempt (might have been created) - // Also clear cache to ensure fileManager sees it - if (existsSync(repoPath)) { - repoCache.delete(RepoCache.repoExistsKey(context.npub, context.repo)); - // Repo exists, continue with normal flow - } else if (!fetched) { - // Fetch failed and repo doesn't exist - throw handleNotFoundError( - 'Repository not found and could not be fetched from remote. The repository may not have any accessible clone URLs.', - { operation: 'getBranches', npub: context.npub, repo: context.repo } - ); - } else { - // Fetch returned true but repo doesn't exist - this shouldn't happen, but clear cache anyway - repoCache.delete(RepoCache.repoExistsKey(context.npub, context.repo)); - // Wait a moment for filesystem to sync, then check again - await new Promise(resolve => setTimeout(resolve, 500)); - if (!existsSync(repoPath)) { - throw handleNotFoundError( - 'Repository fetch completed but repository is not accessible', - { operation: 'getBranches', npub: context.npub, repo: context.repo } - ); - } + if (apiData) { + // Return API data directly without cloning + return json(apiData.branches); } + + // API fetch failed - repo is not cloned and API fetch didn't work + // Return 404 with helpful message suggesting to clone + throw handleNotFoundError( + 'Repository is not cloned locally and could not be fetched via API. Privileged users can clone this repository using the "Clone to Server" button.', + { operation: 'getBranches', npub: context.npub, repo: context.repo } + ); } else { // No events found - could be because: // 1. Repository doesn't exist @@ -119,7 +97,7 @@ export const GET: RequestHandler = createRepoGetHandler( } } - // Double-check repo exists after on-demand fetch + // Double-check repo exists (should be true if we got here) if (!existsSync(repoPath)) { throw handleNotFoundError( 'Repository not found', diff --git a/src/routes/api/repos/[npub]/[repo]/clone/+server.ts b/src/routes/api/repos/[npub]/[repo]/clone/+server.ts index 36bca5b..ba69e54 100644 --- a/src/routes/api/repos/[npub]/[repo]/clone/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/clone/+server.ts @@ -14,6 +14,7 @@ import { NostrClient } from '$lib/services/nostr/nostr-client.js'; import { KIND } from '$lib/types/nostr.js'; import { extractRequestContext } from '$lib/utils/api-context.js'; import { getCachedUserLevel } from '$lib/services/security/user-level-cache.js'; +import { hasUnlimitedAccess } from '$lib/utils/user-access.js'; import logger from '$lib/services/logger.js'; import { handleApiError, handleValidationError } from '$lib/utils/error-handler.js'; @@ -38,7 +39,7 @@ export const POST: RequestHandler = async (event) => { // Check if user has unlimited access const userLevel = getCachedUserLevel(userPubkeyHex); - if (!userLevel || userLevel.level !== 'unlimited') { + if (!hasUnlimitedAccess(userLevel?.level)) { throw error(403, 'Only users with unlimited access can clone repositories to the server.'); } diff --git a/src/routes/api/repos/[npub]/[repo]/commits/+server.ts b/src/routes/api/repos/[npub]/[repo]/commits/+server.ts index 035c2f2..c7a529f 100644 --- a/src/routes/api/repos/[npub]/[repo]/commits/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/commits/+server.ts @@ -35,36 +35,21 @@ export const GET: RequestHandler = createRepoGetHandler( ]); if (events.length > 0) { - // Try to fetch the repository from remote clone URLs - const fetched = await repoManager.fetchRepoOnDemand( - context.npub, - context.repo, - events[0] - ); + // Try API-based fetching first (no cloning) + const { tryApiFetch } = await import('$lib/utils/api-repo-helper.js'); + const apiData = await tryApiFetch(events[0], context.npub, context.repo); - // Always check if repo exists after fetch attempt (might have been created) - // Also clear cache to ensure fileManager sees it - if (existsSync(repoPath)) { - repoCache.delete(RepoCache.repoExistsKey(context.npub, context.repo)); - // Repo exists, continue with normal flow - } else if (!fetched) { - // Fetch failed and repo doesn't exist - throw handleNotFoundError( - 'Repository not found and could not be fetched from remote. The repository may not have any accessible clone URLs.', - { operation: 'getCommits', npub: context.npub, repo: context.repo } - ); - } else { - // Fetch returned true but repo doesn't exist - this shouldn't happen, but clear cache anyway - repoCache.delete(RepoCache.repoExistsKey(context.npub, context.repo)); - // Wait a moment for filesystem to sync, then check again - await new Promise(resolve => setTimeout(resolve, 100)); - if (!existsSync(repoPath)) { - throw handleNotFoundError( - 'Repository fetch completed but repository is not accessible', - { operation: 'getCommits', npub: context.npub, repo: context.repo } - ); - } + if (apiData && apiData.commits) { + // Return API data directly without cloning + const limit = context.limit || 50; + return json(apiData.commits.slice(0, limit)); } + + // API fetch failed - repo is not cloned and API fetch didn't work + throw handleNotFoundError( + 'Repository is not cloned locally and could not be fetched via API. Privileged users can clone this repository using the "Clone to Server" button.', + { operation: 'getCommits', npub: context.npub, repo: context.repo } + ); } else { throw handleNotFoundError( 'Repository announcement not found in Nostr', @@ -86,7 +71,7 @@ export const GET: RequestHandler = createRepoGetHandler( } } - // Double-check repo exists after on-demand fetch + // Double-check repo exists (should be true if we got here) if (!existsSync(repoPath)) { throw handleNotFoundError( 'Repository not found', diff --git a/src/routes/api/repos/[npub]/[repo]/download/+server.ts b/src/routes/api/repos/[npub]/[repo]/download/+server.ts index 2391987..4bd7345 100644 --- a/src/routes/api/repos/[npub]/[repo]/download/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/download/+server.ts @@ -40,36 +40,12 @@ export const GET: RequestHandler = createRepoGetHandler( ]); if (events.length > 0) { - // Try to fetch the repository from remote clone URLs - const fetched = await repoManager.fetchRepoOnDemand( - context.npub, - context.repo, - events[0] + // Download requires the actual repo files, so we can't use API fetching + // Return helpful error message + throw handleNotFoundError( + 'Repository is not cloned locally. To download this repository, privileged users can clone it using the "Clone to Server" button.', + { operation: 'download', npub: context.npub, repo: context.repo } ); - - // Always check if repo exists after fetch attempt (might have been created) - // Also clear cache to ensure fileManager sees it - if (existsSync(repoPath)) { - repoCache.delete(RepoCache.repoExistsKey(context.npub, context.repo)); - // Repo exists, continue with normal flow - } else if (!fetched) { - // Fetch failed and repo doesn't exist - throw handleNotFoundError( - 'Repository not found and could not be fetched from remote. The repository may not have any accessible clone URLs.', - { operation: 'download', npub: context.npub, repo: context.repo } - ); - } else { - // Fetch returned true but repo doesn't exist - this shouldn't happen, but clear cache anyway - repoCache.delete(RepoCache.repoExistsKey(context.npub, context.repo)); - // Wait a moment for filesystem to sync, then check again - await new Promise(resolve => setTimeout(resolve, 100)); - if (!existsSync(repoPath)) { - throw handleNotFoundError( - 'Repository fetch completed but repository is not accessible', - { operation: 'download', npub: context.npub, repo: context.repo } - ); - } - } } else { throw handleNotFoundError( 'Repository announcement not found in Nostr', @@ -91,7 +67,7 @@ export const GET: RequestHandler = createRepoGetHandler( } } - // Double-check repo exists after on-demand fetch + // Double-check repo exists (should be true if we got here) if (!existsSync(repoPath)) { throw handleNotFoundError( 'Repository not found', diff --git a/src/routes/api/repos/[npub]/[repo]/file/+server.ts b/src/routes/api/repos/[npub]/[repo]/file/+server.ts index 04974bc..425d88f 100644 --- a/src/routes/api/repos/[npub]/[repo]/file/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/file/+server.ts @@ -60,30 +60,10 @@ export const GET: RequestHandler = async ({ params, url, request }: { params: { ]); if (events.length > 0) { - // Try to fetch the repository from remote clone URLs - const fetched = await repoManager.fetchRepoOnDemand( - npub, - repo, - events[0] - ); - - // Always check if repo exists after fetch attempt (might have been created) - // Also clear cache to ensure fileManager sees it - if (existsSync(repoPath)) { - repoCache.delete(RepoCache.repoExistsKey(npub, repo)); - // Repo exists, continue with normal flow - } else if (!fetched) { - // Fetch failed and repo doesn't exist - return error(404, 'Repository not found and could not be fetched from remote. The repository may not have any accessible clone URLs.'); - } else { - // Fetch returned true but repo doesn't exist - this shouldn't happen, but clear cache anyway - repoCache.delete(RepoCache.repoExistsKey(npub, repo)); - // Wait a moment for filesystem to sync, then check again - await new Promise(resolve => setTimeout(resolve, 100)); - if (!existsSync(repoPath)) { - return error(404, 'Repository fetch completed but repository is not accessible'); - } - } + // Try API-based fetching first (no cloning) + // For file endpoint, we can't easily fetch individual files via API without cloning + // So we return 404 with helpful message + return error(404, 'Repository is not cloned locally. To view files, privileged users can clone this repository using the "Clone to Server" button.'); } else { return error(404, 'Repository announcement not found in Nostr'); } @@ -99,7 +79,7 @@ export const GET: RequestHandler = async ({ params, url, request }: { params: { } } - // Double-check repo exists after on-demand fetch + // Double-check repo exists (should be true if we got here) if (!existsSync(repoPath)) { return error(404, 'Repository not found'); } diff --git a/src/routes/api/repos/[npub]/[repo]/fork/+server.ts b/src/routes/api/repos/[npub]/[repo]/fork/+server.ts index ac78539..476201b 100644 --- a/src/routes/api/repos/[npub]/[repo]/fork/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/fork/+server.ts @@ -22,6 +22,7 @@ import { ResourceLimits } from '$lib/services/security/resource-limits.js'; import { auditLogger } from '$lib/services/security/audit-logger.js'; import { ForkCountService } from '$lib/services/nostr/fork-count-service.js'; import { getCachedUserLevel } from '$lib/services/security/user-level-cache.js'; +import { hasUnlimitedAccess } from '$lib/utils/user-access.js'; import logger from '$lib/services/logger.js'; import { handleApiError, handleValidationError, handleNotFoundError, handleAuthorizationError } from '$lib/utils/error-handler.js'; @@ -111,7 +112,7 @@ export const POST: RequestHandler = async ({ params, request }) => { // Check if user has unlimited access (required for storing repos locally) const userLevel = getCachedUserLevel(userPubkeyHex); - if (!userLevel || userLevel.level !== 'unlimited') { + if (!hasUnlimitedAccess(userLevel?.level)) { const clientIp = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'; auditLogger.logRepoFork( userPubkeyHex, diff --git a/src/routes/api/repos/[npub]/[repo]/readme/+server.ts b/src/routes/api/repos/[npub]/[repo]/readme/+server.ts index 2c2a9e6..4d5590c 100644 --- a/src/routes/api/repos/[npub]/[repo]/readme/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/readme/+server.ts @@ -45,17 +45,29 @@ export const GET: RequestHandler = createRepoGetHandler( ]); if (events.length > 0) { - // Try to fetch the repository from remote clone URLs - const fetched = await repoManager.fetchRepoOnDemand( - context.npub, - context.repo, - events[0] - ); + // Try API-based fetching first (no cloning) + const { tryApiFetch } = await import('$lib/utils/api-repo-helper.js'); + const apiData = await tryApiFetch(events[0], context.npub, context.repo); - if (!fetched) { - // If fetch fails, return not found (readme endpoint is non-critical) - return json({ found: false }); + if (apiData && apiData.files) { + // Try to find README in API files + const readmeFiles = ['README.md', 'README.markdown', 'README.txt', 'readme.md', 'readme.markdown', 'readme.txt', 'README', 'readme']; + for (const readmeFile of readmeFiles) { + const readmeFileObj = apiData.files.find(f => + f.name.toLowerCase() === readmeFile.toLowerCase() || + f.path.toLowerCase() === readmeFile.toLowerCase() + ); + if (readmeFileObj) { + // Try to fetch README content via API + // For now, return that we found it but can't get content without cloning + // In the future, we could enhance api-repo-fetcher to fetch file content + return json({ found: false }); // Can't get content via API yet + } + } } + + // API fetch failed or README not found - return not found + return json({ found: false }); } else { // No announcement found, return not found return json({ found: false }); diff --git a/src/routes/api/repos/[npub]/[repo]/tree/+server.ts b/src/routes/api/repos/[npub]/[repo]/tree/+server.ts index b9e89d5..9dc91c7 100644 --- a/src/routes/api/repos/[npub]/[repo]/tree/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/tree/+server.ts @@ -35,36 +35,25 @@ export const GET: RequestHandler = createRepoGetHandler( ]); if (events.length > 0) { - // Try to fetch the repository from remote clone URLs - const fetched = await repoManager.fetchRepoOnDemand( - context.npub, - context.repo, - events[0] - ); + // Try API-based fetching first (no cloning) + const { tryApiFetch } = await import('$lib/utils/api-repo-helper.js'); + const apiData = await tryApiFetch(events[0], context.npub, context.repo); - // Always check if repo exists after fetch attempt (might have been created) - // Also clear cache to ensure fileManager sees it - if (existsSync(repoPath)) { - repoCache.delete(RepoCache.repoExistsKey(context.npub, context.repo)); - // Repo exists, continue with normal flow - } else if (!fetched) { - // Fetch failed and repo doesn't exist - throw handleNotFoundError( - 'Repository not found and could not be fetched from remote. The repository may not have any accessible clone URLs.', - { operation: 'listFiles', npub: context.npub, repo: context.repo } - ); - } else { - // Fetch returned true but repo doesn't exist - this shouldn't happen, but clear cache anyway - repoCache.delete(RepoCache.repoExistsKey(context.npub, context.repo)); - // Wait a moment for filesystem to sync, then check again - await new Promise(resolve => setTimeout(resolve, 100)); - if (!existsSync(repoPath)) { - throw handleNotFoundError( - 'Repository fetch completed but repository is not accessible', - { operation: 'listFiles', npub: context.npub, repo: context.repo } - ); - } + if (apiData && apiData.files) { + // Return API data directly without cloning + const path = context.path || ''; + // Filter files by path if specified + const filteredFiles = path + ? apiData.files.filter(f => f.path.startsWith(path)) + : apiData.files.filter(f => !f.path.includes('/') || f.path.split('/').length === 1); + return json(filteredFiles); } + + // API fetch failed - repo is not cloned and API fetch didn't work + throw handleNotFoundError( + 'Repository is not cloned locally and could not be fetched via API. Privileged users can clone this repository using the "Clone to Server" button.', + { operation: 'listFiles', npub: context.npub, repo: context.repo } + ); } else { throw handleNotFoundError( 'Repository announcement not found in Nostr', @@ -86,7 +75,7 @@ export const GET: RequestHandler = createRepoGetHandler( } } - // Double-check repo exists after on-demand fetch + // Double-check repo exists (should be true if we got here) if (!existsSync(repoPath)) { throw handleNotFoundError( 'Repository not found', diff --git a/src/routes/api/user/git-dashboard/+server.ts b/src/routes/api/user/git-dashboard/+server.ts index f6d3241..e93bc53 100644 --- a/src/routes/api/user/git-dashboard/+server.ts +++ b/src/routes/api/user/git-dashboard/+server.ts @@ -10,6 +10,7 @@ import type { RequestHandler } from './$types'; import { extractRequestContext } from '$lib/utils/api-context.js'; import { getAllExternalItems } from '$lib/services/git-platforms/git-platform-fetcher.js'; import { getCachedUserLevel } from '$lib/services/security/user-level-cache.js'; +import { hasUnlimitedAccess } from '$lib/utils/user-access.js'; import logger from '$lib/services/logger.js'; /** @@ -27,7 +28,7 @@ export const GET: RequestHandler = async (event) => { // Check user has unlimited access (same requirement as messaging forwarding) const userLevel = getCachedUserLevel(requestContext.userPubkeyHex); - if (!userLevel || userLevel.level !== 'unlimited') { + if (!hasUnlimitedAccess(userLevel?.level)) { return json({ issues: [], pullRequests: [], diff --git a/src/routes/api/user/messaging-preferences/+server.ts b/src/routes/api/user/messaging-preferences/+server.ts index 1cac542..aa3419c 100644 --- a/src/routes/api/user/messaging-preferences/+server.ts +++ b/src/routes/api/user/messaging-preferences/+server.ts @@ -12,6 +12,7 @@ import type { RequestHandler } from './$types'; import { verifyEvent } from 'nostr-tools'; import { storePreferences, getPreferences, deletePreferences, hasPreferences, getRateLimitStatus } from '$lib/services/messaging/preferences-storage.server.js'; import { getCachedUserLevel } from '$lib/services/security/user-level-cache.js'; +import { hasUnlimitedAccess } from '$lib/utils/user-access.js'; import { extractRequestContext } from '$lib/utils/api-context.js'; import { auditLogger } from '$lib/services/security/audit-logger.js'; import logger from '$lib/services/logger.js'; @@ -87,7 +88,7 @@ export const POST: RequestHandler = async (event) => { // Verify user has unlimited access const cached = getCachedUserLevel(userPubkeyHex); - if (!cached || cached.level !== 'unlimited') { + if (!hasUnlimitedAccess(cached?.level)) { auditLogger.log({ user: userPubkeyHex, ip: clientIp, @@ -152,7 +153,7 @@ export const GET: RequestHandler = async (event) => { // Verify user has unlimited access const cached = getCachedUserLevel(requestContext.userPubkeyHex); - if (!cached || cached.level !== 'unlimited') { + if (!hasUnlimitedAccess(cached?.level)) { return error(403, 'Messaging forwarding requires unlimited access level'); } @@ -189,7 +190,7 @@ export const DELETE: RequestHandler = async (event) => { // Verify user has unlimited access const cached = getCachedUserLevel(requestContext.userPubkeyHex); - if (!cached || cached.level !== 'unlimited') { + if (!hasUnlimitedAccess(cached?.level)) { return error(403, 'Messaging forwarding requires unlimited access level'); } diff --git a/src/routes/api/user/messaging-preferences/summary/+server.ts b/src/routes/api/user/messaging-preferences/summary/+server.ts index 4361cbf..47c9d4e 100644 --- a/src/routes/api/user/messaging-preferences/summary/+server.ts +++ b/src/routes/api/user/messaging-preferences/summary/+server.ts @@ -7,6 +7,7 @@ import { json, error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { getPreferencesSummary } from '$lib/services/messaging/preferences-storage.server.js'; import { getCachedUserLevel } from '$lib/services/security/user-level-cache.js'; +import { hasUnlimitedAccess } from '$lib/utils/user-access.js'; import { extractRequestContext } from '$lib/utils/api-context.js'; import logger from '$lib/services/logger.js'; @@ -24,7 +25,7 @@ export const GET: RequestHandler = async (event) => { // Verify user has unlimited access const cached = getCachedUserLevel(requestContext.userPubkeyHex); - if (!cached || cached.level !== 'unlimited') { + if (!hasUnlimitedAccess(cached?.level)) { return error(403, 'Messaging forwarding requires unlimited access level'); } diff --git a/src/routes/api/user/ssh-keys/+server.ts b/src/routes/api/user/ssh-keys/+server.ts index e1303be..a0b3122 100644 --- a/src/routes/api/user/ssh-keys/+server.ts +++ b/src/routes/api/user/ssh-keys/+server.ts @@ -10,6 +10,7 @@ import type { RequestHandler } from './$types'; import { extractRequestContext } from '$lib/utils/api-context.js'; import { storeAttestation, getUserAttestations, verifyAttestation, calculateSSHKeyFingerprint } from '$lib/services/ssh/ssh-key-attestation.js'; import { getCachedUserLevel } from '$lib/services/security/user-level-cache.js'; +import { hasUnlimitedAccess } from '$lib/utils/user-access.js'; import { auditLogger } from '$lib/services/security/audit-logger.js'; import { verifyEvent } from 'nostr-tools'; import type { NostrEvent } from '$lib/types/nostr.js'; @@ -68,7 +69,7 @@ export const POST: RequestHandler = async (event) => { // Check user has unlimited access (same requirement as messaging forwarding) const userLevel = getCachedUserLevel(requestContext.userPubkeyHex); - if (!userLevel || userLevel.level !== 'unlimited') { + if (!hasUnlimitedAccess(userLevel?.level)) { return error(403, 'SSH key attestation requires unlimited access. Please verify you can write to at least one default Nostr relay.'); } diff --git a/src/routes/signup/+page.svelte b/src/routes/signup/+page.svelte index 6334df2..dda4965 100644 --- a/src/routes/signup/+page.svelte +++ b/src/routes/signup/+page.svelte @@ -9,7 +9,7 @@ import type { NostrEvent } from '../../lib/types/nostr.js'; import { nip19 } from 'nostr-tools'; import { userStore } from '../../lib/stores/user-store.js'; - import { hasUnlimitedAccess } from '../../lib/utils/user-access.js'; + import { hasUnlimitedAccess, isLoggedIn } from '../../lib/utils/user-access.js'; let nip07Available = $state(false); let loading = $state(false); @@ -2197,7 +2197,7 @@