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.
 
 
 
 
 

171 lines
5.2 KiB

/**
* API endpoint for deleting local repository clones
* Only allows deletion by repo owner or admin
*/
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { rm } from 'fs/promises';
import { join, resolve } from 'path';
import { existsSync } from 'fs';
import { createRepoGetHandler } from '$lib/utils/api-handlers.js';
import type { RepoRequestContext } from '$lib/utils/api-context.js';
import { handleApiError, handleAuthorizationError } from '$lib/utils/error-handler.js';
import { auditLogger } from '$lib/services/security/audit-logger.js';
import { nip19 } from 'nostr-tools';
import logger from '$lib/services/logger.js';
import { repoCache, RepoCache } from '$lib/services/git/repo-cache.js';
const repoRoot = typeof process !== 'undefined' && process.env?.GIT_REPO_ROOT
? process.env.GIT_REPO_ROOT
: '/repos';
// Admin pubkeys (can be set via environment variable)
const ADMIN_PUBKEYS = (typeof process !== 'undefined' && process.env?.ADMIN_PUBKEYS
? process.env.ADMIN_PUBKEYS.split(',').map(p => p.trim()).filter(p => p.length > 0)
: []) as string[];
/**
* Check if user is admin
*/
function isAdmin(userPubkeyHex: string | null): boolean {
if (!userPubkeyHex) return false;
return ADMIN_PUBKEYS.some(adminPubkey => {
// Support both hex and npub formats
try {
const decoded = nip19.decode(adminPubkey);
if (decoded.type === 'npub') {
return decoded.data === userPubkeyHex;
}
} catch {
// Not an npub, compare as hex
}
return adminPubkey.toLowerCase() === userPubkeyHex.toLowerCase();
});
}
/**
* Check if user is repo owner
*/
function isOwner(userPubkeyHex: string | null, repoOwnerPubkey: string): boolean {
if (!userPubkeyHex) return false;
return userPubkeyHex.toLowerCase() === repoOwnerPubkey.toLowerCase();
}
export const DELETE: RequestHandler = createRepoGetHandler(
async (context: RepoRequestContext, event) => {
const { npub, repo, repoOwnerPubkey, userPubkeyHex, clientIp } = context;
// Check permissions: must be owner or admin
if (!userPubkeyHex) {
auditLogger.log({
user: undefined,
ip: clientIp,
action: 'repo.delete',
resource: `${npub}/${repo}`,
result: 'denied',
error: 'Authentication required'
});
return handleAuthorizationError('Authentication required to delete repositories');
}
const userIsOwner = isOwner(userPubkeyHex, repoOwnerPubkey);
const userIsAdmin = isAdmin(userPubkeyHex);
if (!userIsOwner && !userIsAdmin) {
auditLogger.log({
user: userPubkeyHex,
ip: clientIp,
action: 'repo.delete',
resource: `${npub}/${repo}`,
result: 'denied',
error: 'Insufficient permissions'
});
return handleAuthorizationError('Only repository owners or admins can delete repositories');
}
// Get repository path
const repoPath = join(repoRoot, npub, `${repo}.git`);
// Security: Ensure resolved path is within repoRoot
const resolvedPath = resolve(repoPath).replace(/\\/g, '/');
const resolvedRoot = resolve(repoRoot).replace(/\\/g, '/');
if (!resolvedPath.startsWith(resolvedRoot + '/')) {
auditLogger.log({
user: userPubkeyHex,
ip: clientIp,
action: 'repo.delete',
resource: `${npub}/${repo}`,
result: 'denied',
error: 'Invalid repository path'
});
return error(403, 'Invalid repository path');
}
// Check if repo exists
if (!existsSync(repoPath)) {
auditLogger.log({
user: userPubkeyHex,
ip: clientIp,
action: 'repo.delete',
resource: `${npub}/${repo}`,
result: 'failure',
error: 'Repository not found'
});
return error(404, 'Repository not found');
}
try {
// Delete the repository directory
await rm(repoPath, { recursive: true, force: true });
// Clear cache
repoCache.delete(RepoCache.repoExistsKey(npub, repo));
// Log successful deletion
auditLogger.log({
user: userPubkeyHex,
ip: clientIp,
action: 'repo.delete',
resource: `${npub}/${repo}`,
result: 'success',
metadata: {
isOwner: userIsOwner,
isAdmin: userIsAdmin
}
});
logger.info({
user: userPubkeyHex,
npub,
repo,
isOwner: userIsOwner,
isAdmin: userIsAdmin
}, 'Repository deleted');
return json({
success: true,
message: 'Repository deleted successfully'
});
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
auditLogger.log({
user: userPubkeyHex,
ip: clientIp,
action: 'repo.delete',
resource: `${npub}/${repo}`,
result: 'failure',
error: errorMessage
});
return handleApiError(err, { operation: 'deleteRepo', npub, repo }, 'Failed to delete repository');
}
},
{
operation: 'deleteRepo',
requireRepoExists: true,
requireRepoAccess: false, // We check permissions manually
requireMaintainer: false // We check owner/admin manually
}
);