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.
124 lines
4.0 KiB
124 lines
4.0 KiB
/** |
|
* Clone repository endpoint |
|
* Only privileged users (unlimited access) can clone repos to the server |
|
*/ |
|
|
|
import { error, json } from '@sveltejs/kit'; |
|
import type { RequestHandler } from './$types'; |
|
import { RepoManager } from '$lib/services/git/repo-manager.js'; |
|
import { requireNpubHex } from '$lib/utils/npub-utils.js'; |
|
import { existsSync } from 'fs'; |
|
import { join } from 'path'; |
|
import { DEFAULT_NOSTR_RELAYS } from '$lib/config.js'; |
|
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 logger from '$lib/services/logger.js'; |
|
import { handleApiError, handleValidationError } from '$lib/utils/error-handler.js'; |
|
|
|
const repoRoot = process.env.GIT_REPO_ROOT || '/repos'; |
|
const repoManager = new RepoManager(repoRoot); |
|
const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS); |
|
|
|
export const POST: RequestHandler = async (event) => { |
|
const { npub, repo } = event.params; |
|
|
|
if (!npub || !repo) { |
|
throw handleValidationError('Missing npub or repo parameter', { operation: 'cloneRepo', npub, repo }); |
|
} |
|
|
|
// Extract user context |
|
const requestContext = extractRequestContext(event); |
|
const userPubkeyHex = requestContext.userPubkeyHex; |
|
|
|
if (!userPubkeyHex) { |
|
throw error(401, 'Authentication required. Please log in to clone repositories.'); |
|
} |
|
|
|
// Check if user has unlimited access |
|
const userLevel = getCachedUserLevel(userPubkeyHex); |
|
if (!userLevel || userLevel.level !== 'unlimited') { |
|
throw error(403, 'Only users with unlimited access can clone repositories to the server.'); |
|
} |
|
|
|
try { |
|
// Decode npub to get pubkey |
|
const repoOwnerPubkey = requireNpubHex(npub); |
|
const repoPath = join(repoRoot, npub, `${repo}.git`); |
|
|
|
// Check if repo already exists |
|
if (existsSync(repoPath)) { |
|
return json({ |
|
success: true, |
|
message: 'Repository already exists locally', |
|
alreadyExists: true |
|
}); |
|
} |
|
|
|
// Fetch repository announcement |
|
const events = await nostrClient.fetchEvents([ |
|
{ |
|
kinds: [KIND.REPO_ANNOUNCEMENT], |
|
authors: [repoOwnerPubkey], |
|
'#d': [repo], |
|
limit: 1 |
|
} |
|
]); |
|
|
|
if (events.length === 0) { |
|
throw handleValidationError( |
|
'Repository announcement not found in Nostr', |
|
{ operation: 'cloneRepo', npub, repo } |
|
); |
|
} |
|
|
|
const announcementEvent = events[0]; |
|
|
|
// Attempt to clone the repository |
|
const cloned = await repoManager.fetchRepoOnDemand(npub, repo, announcementEvent); |
|
|
|
if (!cloned) { |
|
throw handleApiError( |
|
new Error('Failed to clone repository from remote URLs'), |
|
{ operation: 'cloneRepo', npub, repo }, |
|
'Could not clone repository. Please check that the repository has valid clone URLs and is accessible.' |
|
); |
|
} |
|
|
|
// Verify repo exists after cloning |
|
if (!existsSync(repoPath)) { |
|
// Wait a moment for filesystem to sync |
|
await new Promise(resolve => setTimeout(resolve, 500)); |
|
if (!existsSync(repoPath)) { |
|
throw handleApiError( |
|
new Error('Repository clone completed but repository is not accessible'), |
|
{ operation: 'cloneRepo', npub, repo }, |
|
'Repository clone completed but repository is not accessible' |
|
); |
|
} |
|
} |
|
|
|
logger.info({ npub, repo, userPubkeyHex: userPubkeyHex.slice(0, 16) + '...' }, 'Repository cloned successfully'); |
|
|
|
return json({ |
|
success: true, |
|
message: 'Repository cloned successfully', |
|
alreadyExists: false |
|
}); |
|
} catch (err) { |
|
logger.error({ error: err, npub, repo }, 'Error cloning repository'); |
|
|
|
// Re-throw auth errors as-is |
|
if (err instanceof Error && (err.message.includes('401') || err.message.includes('403'))) { |
|
throw err; |
|
} |
|
|
|
const error = err instanceof Error ? err : new Error(String(err)); |
|
throw handleApiError( |
|
error, |
|
{ operation: 'cloneRepo', npub, repo }, |
|
'Failed to clone repository' |
|
); |
|
} |
|
};
|
|
|