Browse Source

bug-fixes for api

main
Silberengel 4 weeks ago
parent
commit
dcb62688be
  1. 3
      src/lib/components/NavBar.svelte
  2. 6
      src/lib/services/git/repo-manager.ts
  3. 3
      src/lib/services/messaging/event-forwarder.ts
  4. 3
      src/lib/services/messaging/preferences-storage.server.ts
  5. 3
      src/lib/services/nostr/repo-polling.ts
  6. 3
      src/lib/services/nostr/user-level-service.ts
  7. 56
      src/lib/utils/api-repo-helper.ts
  8. 3
      src/routes/+page.svelte
  9. 50
      src/routes/api/repos/[npub]/[repo]/branches/+server.ts
  10. 3
      src/routes/api/repos/[npub]/[repo]/clone/+server.ts
  11. 43
      src/routes/api/repos/[npub]/[repo]/commits/+server.ts
  12. 36
      src/routes/api/repos/[npub]/[repo]/download/+server.ts
  13. 30
      src/routes/api/repos/[npub]/[repo]/file/+server.ts
  14. 3
      src/routes/api/repos/[npub]/[repo]/fork/+server.ts
  15. 30
      src/routes/api/repos/[npub]/[repo]/readme/+server.ts
  16. 47
      src/routes/api/repos/[npub]/[repo]/tree/+server.ts
  17. 3
      src/routes/api/user/git-dashboard/+server.ts
  18. 7
      src/routes/api/user/messaging-preferences/+server.ts
  19. 3
      src/routes/api/user/messaging-preferences/summary/+server.ts
  20. 3
      src/routes/api/user/ssh-keys/+server.ts
  21. 4
      src/routes/signup/+page.svelte

3
src/lib/components/NavBar.svelte

@ -119,7 +119,8 @@
updateActivity(); updateActivity();
// Show success message // 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!'); console.log('Unlimited access granted!');
} else if (levelResult.level === 'rate_limited') { } else if (levelResult.level === 'rate_limited') {
console.log('Logged in with rate-limited access.'); console.log('Logged in with rate-limited access.');

6
src/lib/services/git/repo-manager.ts

@ -124,8 +124,9 @@ export class RepoManager {
const isNewRepo = !repoExists; const isNewRepo = !repoExists;
if (isNewRepo && !isExistingRepo) { if (isNewRepo && !isExistingRepo) {
const { getCachedUserLevel } = await import('../security/user-level-cache.js'); const { getCachedUserLevel } = await import('../security/user-level-cache.js');
const { hasUnlimitedAccess } = await import('../utils/user-access.js');
const userLevel = getCachedUserLevel(event.pubkey); 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'}`); 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 // For private repos, require owner to have unlimited access to prevent unauthorized creation
if (!isPublic) { if (!isPublic) {
const { getCachedUserLevel } = await import('../security/user-level-cache.js'); const { getCachedUserLevel } = await import('../security/user-level-cache.js');
const { hasUnlimitedAccess } = await import('../utils/user-access.js');
const userLevel = getCachedUserLevel(announcementEvent.pubkey); const userLevel = getCachedUserLevel(announcementEvent.pubkey);
if (!userLevel || userLevel.level !== 'unlimited') { if (!hasUnlimitedAccess(userLevel?.level)) {
logger.warn({ logger.warn({
npub, npub,
repoName, repoName,

3
src/lib/services/messaging/event-forwarder.ts

@ -621,7 +621,8 @@ export async function forwardEventIfEnabled(
try { try {
// Early returns for eligibility checks // Early returns for eligibility checks
const cached = getCachedUserLevel(userPubkeyHex); const cached = getCachedUserLevel(userPubkeyHex);
if (!cached || cached.level !== 'unlimited') { const { hasUnlimitedAccess } = await import('../utils/user-access.js');
if (!hasUnlimitedAccess(cached?.level)) {
return; return;
} }

3
src/lib/services/messaging/preferences-storage.server.ts

@ -213,7 +213,8 @@ export async function storePreferences(
// Verify user has unlimited access // Verify user has unlimited access
const cached = getCachedUserLevel(userPubkeyHex); 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'); throw new Error('Messaging forwarding requires unlimited access');
} }

3
src/lib/services/nostr/repo-polling.ts

@ -167,7 +167,8 @@ export class RepoPollingService {
// This prevents spam and abuse // This prevents spam and abuse
if (!isExistingRepo) { if (!isExistingRepo) {
const userLevel = getCachedUserLevel(event.pubkey); const userLevel = getCachedUserLevel(event.pubkey);
if (!userLevel || userLevel.level !== 'unlimited') { const { hasUnlimitedAccess } = await import('../utils/user-access.js');
if (!hasUnlimitedAccess(userLevel?.level)) {
logger.warn({ logger.warn({
eventId: event.id, eventId: event.id,
pubkey: event.pubkey.slice(0, 16) + '...', pubkey: event.pubkey.slice(0, 16) + '...',

3
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 { nip19 } from 'nostr-tools';
import { NostrClient } from './nostr-client.js'; import { NostrClient } from './nostr-client.js';
import { DEFAULT_NOSTR_RELAYS } from '../../config.js'; import { DEFAULT_NOSTR_RELAYS } from '../../config.js';
import { hasUnlimitedAccess } from '../../utils/user-access.js';
export type UserLevel = 'unlimited' | 'rate_limited' | 'strictly_rate_limited'; export type UserLevel = 'unlimited' | 'rate_limited' | 'strictly_rate_limited';
@ -84,7 +85,7 @@ export async function checkRelayWriteAccess(
const result = await response.json(); const result = await response.json();
return { return {
hasAccess: result.level === 'unlimited', hasAccess: hasUnlimitedAccess(result.level as UserLevel),
error: result.error error: result.error
}; };
} catch (error) { } catch (error) {

56
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;
}
}

3
src/routes/+page.svelte

@ -171,7 +171,8 @@
levelMessage = null; levelMessage = null;
// Show appropriate message based on level // 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!'; levelMessage = 'Unlimited access granted!';
} else if (levelResult.level === 'rate_limited') { } else if (levelResult.level === 'rate_limited') {
levelMessage = 'Logged in with rate-limited access.'; levelMessage = 'Logged in with rate-limited access.';

50
src/routes/api/repos/[npub]/[repo]/branches/+server.ts

@ -54,43 +54,21 @@ export const GET: RequestHandler = createRepoGetHandler(
} }
if (events.length > 0) { if (events.length > 0) {
// Try to fetch the repository from remote clone URLs // Try API-based fetching first (no cloning)
let fetched = false; const { tryApiFetch } = await import('$lib/utils/api-repo-helper.js');
try { const apiData = await tryApiFetch(events[0], context.npub, context.repo);
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)
}
// Always check if repo exists after fetch attempt (might have been created) if (apiData) {
// Also clear cache to ensure fileManager sees it // Return API data directly without cloning
if (existsSync(repoPath)) { return json(apiData.branches);
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 }
);
}
} }
// 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 { } else {
// No events found - could be because: // No events found - could be because:
// 1. Repository doesn't exist // 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)) { if (!existsSync(repoPath)) {
throw handleNotFoundError( throw handleNotFoundError(
'Repository not found', 'Repository not found',

3
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 { KIND } from '$lib/types/nostr.js';
import { extractRequestContext } from '$lib/utils/api-context.js'; import { extractRequestContext } from '$lib/utils/api-context.js';
import { getCachedUserLevel } from '$lib/services/security/user-level-cache.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 logger from '$lib/services/logger.js';
import { handleApiError, handleValidationError } from '$lib/utils/error-handler.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 // Check if user has unlimited access
const userLevel = getCachedUserLevel(userPubkeyHex); 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.'); throw error(403, 'Only users with unlimited access can clone repositories to the server.');
} }

43
src/routes/api/repos/[npub]/[repo]/commits/+server.ts

@ -35,36 +35,21 @@ export const GET: RequestHandler = createRepoGetHandler(
]); ]);
if (events.length > 0) { if (events.length > 0) {
// Try to fetch the repository from remote clone URLs // Try API-based fetching first (no cloning)
const fetched = await repoManager.fetchRepoOnDemand( const { tryApiFetch } = await import('$lib/utils/api-repo-helper.js');
context.npub, const apiData = await tryApiFetch(events[0], context.npub, context.repo);
context.repo,
events[0]
);
// Always check if repo exists after fetch attempt (might have been created) if (apiData && apiData.commits) {
// Also clear cache to ensure fileManager sees it // Return API data directly without cloning
if (existsSync(repoPath)) { const limit = context.limit || 50;
repoCache.delete(RepoCache.repoExistsKey(context.npub, context.repo)); return json(apiData.commits.slice(0, limit));
// 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 }
);
}
} }
// 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 { } else {
throw handleNotFoundError( throw handleNotFoundError(
'Repository announcement not found in Nostr', '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)) { if (!existsSync(repoPath)) {
throw handleNotFoundError( throw handleNotFoundError(
'Repository not found', 'Repository not found',

36
src/routes/api/repos/[npub]/[repo]/download/+server.ts

@ -40,36 +40,12 @@ export const GET: RequestHandler = createRepoGetHandler(
]); ]);
if (events.length > 0) { if (events.length > 0) {
// Try to fetch the repository from remote clone URLs // Download requires the actual repo files, so we can't use API fetching
const fetched = await repoManager.fetchRepoOnDemand( // Return helpful error message
context.npub, throw handleNotFoundError(
context.repo, 'Repository is not cloned locally. To download this repository, privileged users can clone it using the "Clone to Server" button.',
events[0] { 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 { } else {
throw handleNotFoundError( throw handleNotFoundError(
'Repository announcement not found in Nostr', '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)) { if (!existsSync(repoPath)) {
throw handleNotFoundError( throw handleNotFoundError(
'Repository not found', 'Repository not found',

30
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) { if (events.length > 0) {
// Try to fetch the repository from remote clone URLs // Try API-based fetching first (no cloning)
const fetched = await repoManager.fetchRepoOnDemand( // For file endpoint, we can't easily fetch individual files via API without cloning
npub, // So we return 404 with helpful message
repo, return error(404, 'Repository is not cloned locally. To view files, privileged users can clone this repository using the "Clone to Server" button.');
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');
}
}
} else { } else {
return error(404, 'Repository announcement not found in Nostr'); 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)) { if (!existsSync(repoPath)) {
return error(404, 'Repository not found'); return error(404, 'Repository not found');
} }

3
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 { auditLogger } from '$lib/services/security/audit-logger.js';
import { ForkCountService } from '$lib/services/nostr/fork-count-service.js'; import { ForkCountService } from '$lib/services/nostr/fork-count-service.js';
import { getCachedUserLevel } from '$lib/services/security/user-level-cache.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 logger from '$lib/services/logger.js';
import { handleApiError, handleValidationError, handleNotFoundError, handleAuthorizationError } from '$lib/utils/error-handler.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) // Check if user has unlimited access (required for storing repos locally)
const userLevel = getCachedUserLevel(userPubkeyHex); 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'; const clientIp = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown';
auditLogger.logRepoFork( auditLogger.logRepoFork(
userPubkeyHex, userPubkeyHex,

30
src/routes/api/repos/[npub]/[repo]/readme/+server.ts

@ -45,17 +45,29 @@ export const GET: RequestHandler = createRepoGetHandler(
]); ]);
if (events.length > 0) { if (events.length > 0) {
// Try to fetch the repository from remote clone URLs // Try API-based fetching first (no cloning)
const fetched = await repoManager.fetchRepoOnDemand( const { tryApiFetch } = await import('$lib/utils/api-repo-helper.js');
context.npub, const apiData = await tryApiFetch(events[0], context.npub, context.repo);
context.repo,
events[0]
);
if (!fetched) { if (apiData && apiData.files) {
// If fetch fails, return not found (readme endpoint is non-critical) // Try to find README in API files
return json({ found: false }); 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 { } else {
// No announcement found, return not found // No announcement found, return not found
return json({ found: false }); return json({ found: false });

47
src/routes/api/repos/[npub]/[repo]/tree/+server.ts

@ -35,36 +35,25 @@ export const GET: RequestHandler = createRepoGetHandler(
]); ]);
if (events.length > 0) { if (events.length > 0) {
// Try to fetch the repository from remote clone URLs // Try API-based fetching first (no cloning)
const fetched = await repoManager.fetchRepoOnDemand( const { tryApiFetch } = await import('$lib/utils/api-repo-helper.js');
context.npub, const apiData = await tryApiFetch(events[0], context.npub, context.repo);
context.repo,
events[0]
);
// Always check if repo exists after fetch attempt (might have been created) if (apiData && apiData.files) {
// Also clear cache to ensure fileManager sees it // Return API data directly without cloning
if (existsSync(repoPath)) { const path = context.path || '';
repoCache.delete(RepoCache.repoExistsKey(context.npub, context.repo)); // Filter files by path if specified
// Repo exists, continue with normal flow const filteredFiles = path
} else if (!fetched) { ? apiData.files.filter(f => f.path.startsWith(path))
// Fetch failed and repo doesn't exist : apiData.files.filter(f => !f.path.includes('/') || f.path.split('/').length === 1);
throw handleNotFoundError( return json(filteredFiles);
'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 }
);
}
} }
// 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 { } else {
throw handleNotFoundError( throw handleNotFoundError(
'Repository announcement not found in Nostr', '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)) { if (!existsSync(repoPath)) {
throw handleNotFoundError( throw handleNotFoundError(
'Repository not found', 'Repository not found',

3
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 { extractRequestContext } from '$lib/utils/api-context.js';
import { getAllExternalItems } from '$lib/services/git-platforms/git-platform-fetcher.js'; import { getAllExternalItems } from '$lib/services/git-platforms/git-platform-fetcher.js';
import { getCachedUserLevel } from '$lib/services/security/user-level-cache.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 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) // Check user has unlimited access (same requirement as messaging forwarding)
const userLevel = getCachedUserLevel(requestContext.userPubkeyHex); const userLevel = getCachedUserLevel(requestContext.userPubkeyHex);
if (!userLevel || userLevel.level !== 'unlimited') { if (!hasUnlimitedAccess(userLevel?.level)) {
return json({ return json({
issues: [], issues: [],
pullRequests: [], pullRequests: [],

7
src/routes/api/user/messaging-preferences/+server.ts

@ -12,6 +12,7 @@ import type { RequestHandler } from './$types';
import { verifyEvent } from 'nostr-tools'; import { verifyEvent } from 'nostr-tools';
import { storePreferences, getPreferences, deletePreferences, hasPreferences, getRateLimitStatus } from '$lib/services/messaging/preferences-storage.server.js'; 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 { 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 { extractRequestContext } from '$lib/utils/api-context.js';
import { auditLogger } from '$lib/services/security/audit-logger.js'; import { auditLogger } from '$lib/services/security/audit-logger.js';
import logger from '$lib/services/logger.js'; import logger from '$lib/services/logger.js';
@ -87,7 +88,7 @@ export const POST: RequestHandler = async (event) => {
// Verify user has unlimited access // Verify user has unlimited access
const cached = getCachedUserLevel(userPubkeyHex); const cached = getCachedUserLevel(userPubkeyHex);
if (!cached || cached.level !== 'unlimited') { if (!hasUnlimitedAccess(cached?.level)) {
auditLogger.log({ auditLogger.log({
user: userPubkeyHex, user: userPubkeyHex,
ip: clientIp, ip: clientIp,
@ -152,7 +153,7 @@ export const GET: RequestHandler = async (event) => {
// Verify user has unlimited access // Verify user has unlimited access
const cached = getCachedUserLevel(requestContext.userPubkeyHex); const cached = getCachedUserLevel(requestContext.userPubkeyHex);
if (!cached || cached.level !== 'unlimited') { if (!hasUnlimitedAccess(cached?.level)) {
return error(403, 'Messaging forwarding requires unlimited access 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 // Verify user has unlimited access
const cached = getCachedUserLevel(requestContext.userPubkeyHex); const cached = getCachedUserLevel(requestContext.userPubkeyHex);
if (!cached || cached.level !== 'unlimited') { if (!hasUnlimitedAccess(cached?.level)) {
return error(403, 'Messaging forwarding requires unlimited access level'); return error(403, 'Messaging forwarding requires unlimited access level');
} }

3
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 type { RequestHandler } from './$types';
import { getPreferencesSummary } from '$lib/services/messaging/preferences-storage.server.js'; import { getPreferencesSummary } from '$lib/services/messaging/preferences-storage.server.js';
import { getCachedUserLevel } from '$lib/services/security/user-level-cache.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 { extractRequestContext } from '$lib/utils/api-context.js';
import logger from '$lib/services/logger.js'; import logger from '$lib/services/logger.js';
@ -24,7 +25,7 @@ export const GET: RequestHandler = async (event) => {
// Verify user has unlimited access // Verify user has unlimited access
const cached = getCachedUserLevel(requestContext.userPubkeyHex); const cached = getCachedUserLevel(requestContext.userPubkeyHex);
if (!cached || cached.level !== 'unlimited') { if (!hasUnlimitedAccess(cached?.level)) {
return error(403, 'Messaging forwarding requires unlimited access level'); return error(403, 'Messaging forwarding requires unlimited access level');
} }

3
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 { extractRequestContext } from '$lib/utils/api-context.js';
import { storeAttestation, getUserAttestations, verifyAttestation, calculateSSHKeyFingerprint } from '$lib/services/ssh/ssh-key-attestation.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 { 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 { auditLogger } from '$lib/services/security/audit-logger.js';
import { verifyEvent } from 'nostr-tools'; import { verifyEvent } from 'nostr-tools';
import type { NostrEvent } from '$lib/types/nostr.js'; 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) // Check user has unlimited access (same requirement as messaging forwarding)
const userLevel = getCachedUserLevel(requestContext.userPubkeyHex); 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.'); return error(403, 'SSH key attestation requires unlimited access. Please verify you can write to at least one default Nostr relay.');
} }

4
src/routes/signup/+page.svelte

@ -9,7 +9,7 @@
import type { NostrEvent } from '../../lib/types/nostr.js'; import type { NostrEvent } from '../../lib/types/nostr.js';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { userStore } from '../../lib/stores/user-store.js'; 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 nip07Available = $state(false);
let loading = $state(false); let loading = $state(false);
@ -2197,7 +2197,7 @@
<div class="form-actions"> <div class="form-actions">
<button <button
type="submit" type="submit"
disabled={loading || !nip07Available} disabled={loading || !nip07Available || !isLoggedIn($userStore.userLevel) || !hasUnlimitedAccess($userStore.userLevel)}
> >
{loading ? 'Publishing...' : 'Publish Repository Announcement'} {loading ? 'Publishing...' : 'Publish Repository Announcement'}
</button> </button>

Loading…
Cancel
Save