export default { async fetch(request, env) { try { const url = new URL(request.url); const path = url.pathname; // Handle preflight requests if (request.method === 'OPTIONS') { return new Response(null, { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', } }); } // Redirect repo/*.git ke repo/* (tanpa .git) untuk public view if (path.match(/^\/repo\/[^\/]+\.git$/) && !path.includes('/info/refs') && !path.includes('/git-upload-pack')) { const repoId = path.split('/')[2].replace('.git', ''); return new Response('', { status: 301, headers: { 'Location': `/repo/${repoId}` } }); } // Simple routing if (path === '/login' || path === '/api/auth/login') { return handleLogin(request, env); } if (path === '/api/auth/logout') { return handleLogout(request, env); } // Upload file handler if (path === '/api/upload' && request.method === 'POST') { return handleFileUpload(request, env); } // Import from URL handler - DIPINDAHKAN ke API handler if (path === '/api/import-from-url' && request.method === 'POST') { const authResult = await authenticate(request, env); if (!authResult.authenticated) { return jsonResponse({ error: 'Unauthorized' }, 401); } return handleImportFromURL(request, env, authResult.user); } // Public routes if (path.startsWith('/repo/') && !path.includes('/info/refs') && !path.includes('/git-upload-pack')) { return handlePublicRepo(request, env, path); } if (path.startsWith('/raw/')) { return handleRawFile(request, env, path); } if (path.startsWith('/download/')) { return handleDownloadRepo(request, env, path); } // Git protocol if (path.includes('/info/refs') || path.includes('/git-upload-pack')) { return handleGitProtocol(request, env, path); } // API routes if (path.startsWith('/api/')) { const authResult = await authenticate(request, env); const method = request.method; // Only write operations need auth if ((method === 'POST' || method === 'PUT' || method === 'DELETE') && !authResult.authenticated) { return jsonResponse({ error: 'Unauthorized' }, 401); } return handleAPI(request, env, path, authResult.user); } // Homepage const authResult = await authenticate(request, env); if (path === '/' && !authResult.authenticated) { return servePublicHomepage(env); } // Dashboard for logged-in users return serveHTML(env, authResult.authenticated, authResult.user); } catch (error) { console.error('Global error:', error); return new Response('Internal Server Error', { status: 500, headers: { 'Content-Type': 'text/plain', 'Access-Control-Allow-Origin': '*' } }); } } }; // ============================================ // R2 HELPER FUNCTIONS // ============================================ // R2 Get - returns parsed JSON or null async function r2Get(env, key) { try { const object = await env.KODE_STORAGE.get(key); if (!object) return null; const text = await object.text(); return text; } catch (error) { console.error('R2 Get error:', error); return null; } } // R2 Put - stores string data async function r2Put(env, key, data, options = {}) { try { await env.KODE_STORAGE.put(key, data, options); return true; } catch (error) { console.error('R2 Put error:', error); return false; } } // R2 Delete - deletes object async function r2Delete(env, key) { try { await env.KODE_STORAGE.delete(key); return true; } catch (error) { console.error('R2 Delete error:', error); return false; } } // R2 List - lists objects with prefix async function r2List(env, prefix) { try { const listed = await env.KODE_STORAGE.list({ prefix }); return listed.objects || []; } catch (error) { console.error('R2 List error:', error); return []; } } // ============================================ // END R2 HELPER FUNCTIONS // ============================================ // Helper function untuk JSON responses dengan CORS function jsonResponse(data, status = 200, headers = {}) { const responseHeaders = { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', ...headers }; return new Response(JSON.stringify(data), { status, headers: responseHeaders }); } // Authentication function async function authenticate(request, env) { try { const cookieHeader = request.headers.get('Cookie'); if (cookieHeader && cookieHeader.includes('session=')) { const sessionMatch = cookieHeader.match(/session=([^;]+)/); if (sessionMatch) { const sessionId = sessionMatch[1]; const sessionData = await r2Get(env, `session:${sessionId}`); if (sessionData) { const session = JSON.parse(sessionData); if (session.expires > Date.now()) { return { authenticated: true, user: session.user }; } } } } return { authenticated: false, user: null }; } catch (error) { console.error('Auth error:', error); return { authenticated: false, user: null }; } } // File Upload Handler async function handleFileUpload(request, env) { try { const authResult = await authenticate(request, env); if (!authResult.authenticated) { return jsonResponse({ error: 'Unauthorized' }, 401); } const formData = await request.formData(); const file = formData.get('file'); const repoId = formData.get('repoId'); if (!file || !repoId) { return jsonResponse({ error: 'File and repository ID are required' }, 400); } // Get repository const repoData = await r2Get(env, `repo:${repoId}`); if (!repoData) { return jsonResponse({ error: 'Repository not found' }, 404); } const repo = JSON.parse(repoData); // Authorization check if (authResult.user.role !== 'admin' && repo.owner !== authResult.user.id) { return jsonResponse({ error: 'Forbidden' }, 403); } // Read file content const fileContent = await file.text(); const fileName = file.name; const fileExtension = fileName.split('.').pop()?.toLowerCase() || ''; // Determine language based on file extension const languageMap = { 'js': 'javascript', 'jsx': 'javascript', 'ts': 'javascript', 'tsx': 'javascript', 'py': 'python', 'html': 'html', 'htm': 'html', 'css': 'css', 'java': 'java', 'cpp': 'cpp', 'c': 'cpp', 'cc': 'cpp', 'txt': 'text', 'md': 'text', 'json': 'javascript', 'xml': 'text', 'csv': 'text' }; const language = languageMap[fileExtension] || 'text'; // Create file object const fileId = generateId(); const fileData = { id: fileId, name: fileName, content: fileContent, language: language, created: new Date().toISOString(), size: file.size, type: file.type }; if (!repo.files) { repo.files = []; } // Check if file with same name already exists const existingFileIndex = repo.files.findIndex(f => f.name === fileName); if (existingFileIndex !== -1) { // Replace existing file repo.files[existingFileIndex] = fileData; } else { // Add new file repo.files.push(fileData); } await r2Put(env, `repo:${repoId}`, JSON.stringify(repo)); return jsonResponse({ success: true, file: fileData, message: existingFileIndex !== -1 ? 'File updated successfully' : 'File uploaded successfully' }); } catch (error) { console.error('File upload error:', error); return jsonResponse({ error: 'File upload failed' }, 500); } } // Import File from URL Handler - DIPERBAIKI untuk support lebih banyak URL async function handleImportFromURL(request, env, user) { try { const data = await request.json(); const { url, fileName, repoId } = data; if (!url || !repoId) { return jsonResponse({ error: 'URL and repository ID are required' }, 400); } // Validate URL let fileUrl; try { fileUrl = new URL(url); } catch (e) { return jsonResponse({ error: 'Invalid URL' }, 400); } // Get repository const repoData = await r2Get(env, `repo:${repoId}`); if (!repoData) { return jsonResponse({ error: 'Repository not found' }, 404); } const repo = JSON.parse(repoData); // Authorization check if (user.role !== 'admin' && repo.owner !== user.id) { return jsonResponse({ error: 'Forbidden' }, 403); } // Enhanced URL handling dengan headers yang lebih baik const fetchOptions = { method: 'GET', headers: { 'User-Agent': 'Mozilla/5.0 (compatible; CodeHub-Bot/1.0)', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,text/plain;q=0.8,*/*;q=0.7', 'Accept-Language': 'en-US,en;q=0.5', 'Accept-Encoding': 'gzip, deflate, br', 'Connection': 'keep-alive', }, redirect: 'follow' }; // Fetch content from URL dengan error handling yang lebih baik let response; try { response = await fetch(url, fetchOptions); } catch (fetchError) { console.error('Fetch error:', fetchError); return jsonResponse({ error: `Network error: ${fetchError.message}` }, 400); } if (!response.ok) { // Coba tanpa headers jika gagal try { response = await fetch(url); } catch (secondError) { return jsonResponse({ error: `Failed to fetch URL: ${response.status} ${response.statusText}. Also failed retry: ${secondError.message}` }, 400); } if (!response.ok) { return jsonResponse({ error: `Failed to fetch URL: ${response.status} ${response.statusText}` }, 400); } } const content = await response.text(); // Determine file name dengan logic yang lebih baik let finalFileName = fileName; if (!finalFileName) { // Extract filename from URL path const urlPath = fileUrl.pathname; finalFileName = urlPath.split('/').pop() || 'imported-file'; // Remove query parameters dari filename finalFileName = finalFileName.split('?')[0]; finalFileName = finalFileName.split('#')[0]; // Jika masih tidak ada extension, coba dari content type if (!finalFileName.includes('.')) { const contentType = response.headers.get('content-type'); if (contentType) { const extensionMap = { 'text/javascript': 'js', 'application/javascript': 'js', 'application/x-javascript': 'js', 'text/x-python': 'py', 'application/x-python': 'py', 'text/html': 'html', 'text/css': 'css', 'text/plain': 'txt', 'application/json': 'json', 'application/xml': 'xml', 'text/xml': 'xml' }; const ext = extensionMap[contentType] || 'txt'; finalFileName = `${finalFileName}.${ext}`; } else { // Default to .txt jika tidak bisa menentukan finalFileName = `${finalFileName}.txt`; } } } // Determine language based on file extension dengan mapping yang lebih lengkap const fileExtension = finalFileName.split('.').pop()?.toLowerCase() || ''; const languageMap = { 'js': 'javascript', 'jsx': 'javascript', 'ts': 'javascript', 'tsx': 'javascript', 'mjs': 'javascript', 'py': 'python', 'pyw': 'python', 'pyc': 'python', 'html': 'html', 'htm': 'html', 'xhtml': 'html', 'css': 'css', 'scss': 'css', 'sass': 'css', 'less': 'css', 'java': 'java', 'class': 'java', 'jar': 'java', 'cpp': 'cpp', 'c': 'cpp', 'cc': 'cpp', 'cxx': 'cpp', 'h': 'cpp', 'hpp': 'cpp', 'php': 'php', 'php3': 'php', 'php4': 'php', 'php5': 'php', 'phtml': 'php', 'rb': 'ruby', 'erb': 'ruby', 'go': 'go', 'rs': 'rust', 'swift': 'swift', 'kt': 'kotlin', 'kts': 'kotlin', 'txt': 'text', 'md': 'text', 'markdown': 'text', 'rst': 'text', 'json': 'javascript', 'json5': 'javascript', 'xml': 'text', 'xsl': 'text', 'xslt': 'text', 'csv': 'text', 'tsv': 'text', 'sql': 'sql', 'sh': 'bash', 'bash': 'bash', 'zsh': 'bash', 'yaml': 'yaml', 'yml': 'yaml', 'toml': 'toml', 'ini': 'ini', 'cfg': 'ini', 'conf': 'ini' }; const language = languageMap[fileExtension] || 'text'; // Create file object const fileId = generateId(); const fileData = { id: fileId, name: finalFileName, content: content, language: language, created: new Date().toISOString(), size: content.length, type: 'text/plain', importedFrom: url }; if (!repo.files) { repo.files = []; } // Check if file with same name already exists const existingFileIndex = repo.files.findIndex(f => f.name === finalFileName); if (existingFileIndex !== -1) { // Replace existing file repo.files[existingFileIndex] = fileData; } else { // Add new file repo.files.push(fileData); } await r2Put(env, `repo:${repoId}`, JSON.stringify(repo)); return jsonResponse({ success: true, file: fileData, message: existingFileIndex !== -1 ? 'File updated successfully from URL' : 'File imported successfully from URL' }); } catch (error) { console.error('Import from URL error:', error); return jsonResponse({ error: 'Import from URL failed: ' + error.message }, 500); } } // Login handler async function handleLogin(request, env) { if (request.method === 'GET') { return serveLoginPage(); } if (request.method === 'POST') { try { const credentials = await request.json(); const { username, password } = credentials; // Simple credential check if (username === 'admin' && password === 'admin123') { const sessionId = generateId(); const userData = { id: generateId(), username: 'admin', role: 'admin' }; const sessionData = { user: userData, expires: Date.now() + (24 * 60 * 60 * 1000) }; // R2 tidak support expirationTtl, jadi kita handle expiry manual di authenticate() await r2Put(env, `session:${sessionId}`, JSON.stringify(sessionData)); return new Response(JSON.stringify({ success: true, user: userData }), { status: 200, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Set-Cookie': `session=${sessionId}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400` } }); } else { return jsonResponse({ error: 'Invalid credentials' }, 401); } } catch (error) { console.error('Login error:', error); return jsonResponse({ error: 'Login failed' }, 500); } } return new Response('Method not allowed', { status: 405 }); } // Logout handler async function handleLogout(request, env) { try { const cookieHeader = request.headers.get('Cookie'); if (cookieHeader && cookieHeader.includes('session=')) { const sessionMatch = cookieHeader.match(/session=([^;]+)/); if (sessionMatch) { await r2Delete(env, `session:${sessionMatch[1]}`); } } return new Response(JSON.stringify({ success: true }), { headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Set-Cookie': 'session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0' } }); } catch (error) { return jsonResponse({ error: 'Logout failed' }, 500); } } // Public homepage async function servePublicHomepage(env) { try { const repoObjects = await r2List(env, 'repo:'); const repoList = []; for (const obj of repoObjects) { try { const repoData = await r2Get(env, obj.key); if (repoData) { const repo = JSON.parse(repoData); const repoId = obj.key.replace('repo:', ''); repoList.push({ id: repoId, name: repo.name, description: repo.description, created: repo.created, files: repo.files ? repo.files.length : 0 }); } } catch (e) { console.error('Error parsing repo data:', e); } } const html = ` CodeHub - Public Repositories
Login

Welcome to CodeHub

Discover and explore public code repositories. All repositories are publicly accessible.

${repoList.length}
Public Repositories
${repoList.reduce((total, repo) => total + (repo.files || 0), 0)}
Total Files

Public Repositories

${repoList.length === 0 ? '
No public repositories yet. Be the first to create one!
' : repoList.map(repo => `
${escapeHtml(repo.name)}
${escapeHtml(repo.description || 'No description')}
Created: ${new Date(repo.created).toLocaleDateString()} Files: ${repo.files || 0}
`).join('') }
`; return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' } }); } catch (error) { console.error('Error loading repositories:', error); return new Response('Error loading repositories', { status: 500 }); } } // Public repository view dengan README di bagian bawah async function handlePublicRepo(request, env, path) { try { const repoId = path.split('/')[2]; const repoData = await r2Get(env, `repo:${repoId}`); if (!repoData) { return new Response('Repository not found', { status: 404 }); } const repo = JSON.parse(repoData); const baseUrl = new URL(request.url).origin; const gitCloneUrl = `${baseUrl}/repo/${repoId}.git`; const publicRepoUrl = `${baseUrl}/repo/${repoId}`; // Get README content const readme = repo.readme || { content: '# ' + repo.name + '\n\n' + (repo.description || 'No description provided') + '\n\n## About\n\nThis repository contains code files managed by CodeHub.', lastUpdated: repo.created }; const html = ` ${escapeHtml(repo.name)} - CodeHub

${escapeHtml(repo.name)}

${escapeHtml(repo.description || 'No description provided')}
Created: ${new Date(repo.created).toLocaleDateString()} • Files: ${repo.files ? repo.files.length : 0}
Public URL: ${publicRepoUrl}
Git Clone URL: ${gitCloneUrl}
Format: Structured text file with all files
Includes ${repo.files ? repo.files.length : 0} files in folder: ${escapeHtml(repo.name)}/

Files

${repo.files && repo.files.length > 0 ? repo.files.map(file => `
📄 ${escapeHtml(file.name)} ${escapeHtml(file.language)}
Raw
`).join('') : '
No files in this repository
' }
README.md
Last updated: ${new Date(readme.lastUpdated).toLocaleDateString()}
${renderMarkdown(readme.content)}
`; return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' } }); } catch (error) { console.error('Error loading repository:', error); return new Response('Error loading repository', { status: 500 }); } } // Raw file handler - DIPERBAIKI untuk selalu return text/plain async function handleRawFile(request, env, path) { try { const fileId = path.split('/')[2]; console.log('Looking for file with ID:', fileId); if (!fileId) { return new Response('File ID is required', { status: 400 }); } const repoObjects = await r2List(env, 'repo:'); console.log('Found repositories:', repoObjects.length); for (const obj of repoObjects) { try { const repoData = await r2Get(env, obj.key); if (repoData) { const repo = JSON.parse(repoData); if (repo.files && repo.files.length > 0) { console.log(`Checking repo ${obj.key} with ${repo.files.length} files`); const file = repo.files.find(f => f.id === fileId); if (file) { console.log('File found:', file.name); // SELALU return sebagai text/plain, tidak peduli bahasa pemrogramannya return new Response(file.content, { headers: { 'Content-Type': 'text/plain; charset=utf-8', 'Content-Disposition': `inline; filename="${file.name}"`, 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'public, max-age=3600' } }); } } } } catch (e) { console.error('Error parsing repo data:', e); } } console.log('File not found with ID:', fileId); return new Response('File not found', { status: 404 }); } catch (error) { console.error('Error loading file:', error); return new Response('Error loading file', { status: 500 }); } } // Download handler dengan format yang lebih baik async function handleDownloadRepo(request, env, path) { try { const repoId = path.split('/')[2]; const repoData = await r2Get(env, `repo:${repoId}`); if (!repoData) { return new Response('Repository not found', { status: 404 }); } const repo = JSON.parse(repoData); const archiveContent = createTextArchive(repo); return new Response(archiveContent, { headers: { 'Content-Type': 'text/plain; charset=utf-8', 'Content-Disposition': `attachment; filename="${repo.name}-archive.txt"`, 'Access-Control-Allow-Origin': '*' } }); } catch (error) { console.error('Error creating archive:', error); return new Response('Error creating archive', { status: 500 }); } } function createTextArchive(repo) { const folderName = repo.name; const timestamp = new Date().toLocaleString(); let archiveContent = ''; archiveContent += '╔══════════════════════════════════════════════════╗\n'; archiveContent += '║ CODE REPOSITORY ARCHIVE ║\n'; archiveContent += '╠══════════════════════════════════════════════════╣\n'; archiveContent += `║ Repository: ${folderName.padEnd(36)} ║\n`; archiveContent += `║ Generated: ${timestamp.padEnd(37)} ║\n`; archiveContent += `║ Files: ${String(repo.files ? repo.files.length : 0).padEnd(40)} ║\n`; archiveContent += `║ Description: ${(repo.description || 'No description').substring(0, 34).padEnd(35)} ║\n`; archiveContent += '╚══════════════════════════════════════════════════╝\n\n'; archiveContent += 'FILE SUMMARY\n'; archiveContent += '────────────────────────────────────────────────────\n'; if (repo.files && repo.files.length > 0) { let totalSize = 0; const languageCount = {}; repo.files.forEach(file => { totalSize += file.content.length; languageCount[file.language] = (languageCount[file.language] || 0) + 1; }); archiveContent += `Total files: ${repo.files.length}\n`; archiveContent += `Total size: ${totalSize} characters\n`; archiveContent += `Folder structure: ${folderName}/\n\n`; archiveContent += 'Languages:\n'; for (const [lang, count] of Object.entries(languageCount)) { archiveContent += ` - ${lang}: ${count} files\n`; } archiveContent += '\n'; } else { archiveContent += 'No files in this repository\n\n'; } archiveContent += 'FILE CONTENTS\n'; archiveContent += '════════════════════════════════════════════════════\n\n'; if (repo.files && repo.files.length > 0) { repo.files.forEach((file, index) => { const fileNumber = (index + 1).toString().padStart(2, '0'); const filePath = `${folderName}/${file.name}`; const fileSize = file.content.length; const createdDate = file.created ? new Date(file.created).toLocaleDateString() : 'Unknown'; archiveContent += `┌── FILE ${fileNumber}: ${filePath}\n`; archiveContent += `│ Size: ${fileSize} characters\n`; archiveContent += `│ Language: ${file.language}\n`; archiveContent += `│ Created: ${createdDate}\n`; archiveContent += `└────────────────────────────────────────────────────\n\n`; archiveContent += file.content; if (index < repo.files.length - 1) { archiveContent += '\n\n' + '─'.repeat(60) + '\n\n'; } }); } archiveContent += '\n\n' + '═'.repeat(60) + '\n'; archiveContent += `ARCHIVE COMPLETE - ${repo.files ? repo.files.length : 0} files exported\n`; archiveContent += `Location: ${folderName}/\n`; archiveContent += `Exported: ${timestamp}\n`; archiveContent += `Source: CodeHub Repository\n`; archiveContent += '═'.repeat(60) + '\n'; return archiveContent; } // Git protocol handler async function handleGitProtocol(request, env, path) { try { const repoId = path.split('/')[2]?.replace('.git', ''); if (!repoId) { return new Response('Repository not found', { status: 404 }); } const repoData = await r2Get(env, `repo:${repoId}`); if (!repoData) { return new Response('Repository not found', { status: 404 }); } return new Response('001e# service=git-upload-pack\n00000000', { headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement', 'Cache-Control': 'no-cache' } }); } catch (error) { return new Response('Git protocol error', { status: 500 }); } } // API handler dengan semua fitur termasuk README dan edit deskripsi async function handleAPI(request, env, path, user) { const method = request.method; try { // Get all repositories - PUBLIC if (path === '/api/repos' && method === 'GET') { const repoObjects = await r2List(env, 'repo:'); const repoList = []; for (const obj of repoObjects) { const repoData = await r2Get(env, obj.key); if (repoData) { const repo = JSON.parse(repoData); const repoId = obj.key.replace('repo:', ''); repoList.push({ id: repoId, publicUrl: `/repo/${repoId}`, gitUrl: `/repo/${repoId}.git`, ...repo }); } } return jsonResponse(repoList); } // Create repository - PROTECTED if (path === '/api/repos' && method === 'POST') { const data = await request.json(); const repoId = generateId(); const repoData = { name: data.name, description: data.description, created: new Date().toISOString(), owner: user.id, files: [] }; await r2Put(env, `repo:${repoId}`, JSON.stringify(repoData)); return jsonResponse({ id: repoId, publicUrl: `/repo/${repoId}`, gitUrl: `/repo/${repoId}.git`, ...repoData }); } // Get specific repository - PUBLIC if (path.match(/^\/api\/repos\/[^\/]+$/) && method === 'GET') { const repoId = path.split('/')[3]; const repoData = await r2Get(env, `repo:${repoId}`); if (!repoData) { return jsonResponse({ error: 'Repository not found' }, 404); } const repo = JSON.parse(repoData); return jsonResponse({ id: repoId, publicUrl: `/repo/${repoId}`, gitUrl: `/repo/${repoId}.git`, ...repo }); } // Update repository (edit deskripsi) - PROTECTED if (path.match(/^\/api\/repos\/[^\/]+$/) && method === 'PUT') { const repoId = path.split('/')[3]; const data = await request.json(); const repoData = await r2Get(env, `repo:${repoId}`); if (!repoData) { return jsonResponse({ error: 'Repository not found' }, 404); } const repo = JSON.parse(repoData); // Authorization check if (user.role !== 'admin' && repo.owner !== user.id) { return jsonResponse({ error: 'Forbidden' }, 403); } // Update fields yang diizinkan if (data.name !== undefined) repo.name = data.name; if (data.description !== undefined) repo.description = data.description; await r2Put(env, `repo:${repoId}`, JSON.stringify(repo)); return jsonResponse({ id: repoId, publicUrl: `/repo/${repoId}`, gitUrl: `/repo/${repoId}.git`, ...repo }); } // Add file to repository - PROTECTED if (path.match(/^\/api\/repos\/[^\/]+\/files$/) && method === 'POST') { const repoId = path.split('/')[3]; const data = await request.json(); const repoData = await r2Get(env, `repo:${repoId}`); if (!repoData) { return jsonResponse({ error: 'Repository not found' }, 404); } const repo = JSON.parse(repoData); // Authorization check if (user.role !== 'admin' && repo.owner !== user.id) { return jsonResponse({ error: 'Forbidden' }, 403); } const fileId = generateId(); const fileData = { id: fileId, name: data.name, content: data.content, language: data.language || 'text', created: new Date().toISOString() }; if (!repo.files) { repo.files = []; } repo.files.push(fileData); await r2Put(env, `repo:${repoId}`, JSON.stringify(repo)); return jsonResponse(fileData); } // Update file - PROTECTED if (path.match(/^\/api\/files\/[^\/]+$/) && method === 'PUT') { const fileId = path.split('/')[3]; const data = await request.json(); const repoObjects = await r2List(env, 'repo:'); for (const obj of repoObjects) { const repoData = await r2Get(env, obj.key); if (repoData) { const repo = JSON.parse(repoData); if (repo.files) { const fileIndex = repo.files.findIndex(f => f.id === fileId); if (fileIndex !== -1) { // Authorization check if (user.role !== 'admin' && repo.owner !== user.id) { return jsonResponse({ error: 'Forbidden' }, 403); } repo.files[fileIndex].content = data.content; repo.files[fileIndex].name = data.name || repo.files[fileIndex].name; repo.files[fileIndex].language = data.language || repo.files[fileIndex].language; await r2Put(env, obj.key, JSON.stringify(repo)); return jsonResponse(repo.files[fileIndex]); } } } } return jsonResponse({ error: 'File not found' }, 404); } // Delete file - PROTECTED if (path.match(/^\/api\/files\/[^\/]+$/) && method === 'DELETE') { const fileId = path.split('/')[3]; const repoObjects = await r2List(env, 'repo:'); for (const obj of repoObjects) { const repoData = await r2Get(env, obj.key); if (repoData) { const repo = JSON.parse(repoData); if (repo.files) { const fileIndex = repo.files.findIndex(f => f.id === fileId); if (fileIndex !== -1) { // Authorization check if (user.role !== 'admin' && repo.owner !== user.id) { return jsonResponse({ error: 'Forbidden' }, 403); } const deletedFile = repo.files[fileIndex]; repo.files.splice(fileIndex, 1); await r2Put(env, obj.key, JSON.stringify(repo)); return jsonResponse({ success: true, message: 'File deleted successfully', file: deletedFile }); } } } } return jsonResponse({ error: 'File not found' }, 404); } // Delete repository - PROTECTED if (path.match(/^\/api\/repos\/[^\/]+$/) && method === 'DELETE') { const repoId = path.split('/')[3]; const repoData = await r2Get(env, `repo:${repoId}`); if (!repoData) { return jsonResponse({ error: 'Repository not found' }, 404); } const repo = JSON.parse(repoData); // Authorization check if (user.role !== 'admin' && repo.owner !== user.id) { return jsonResponse({ error: 'Forbidden' }, 403); } await r2Delete(env, `repo:${repoId}`); return jsonResponse({ success: true }); } // Get file content - PUBLIC if (path.match(/^\/api\/files\/[^\/]+$/) && method === 'GET') { const fileId = path.split('/')[3]; const repoObjects = await r2List(env, 'repo:'); for (const obj of repoObjects) { const repoData = await r2Get(env, obj.key); if (repoData) { const repo = JSON.parse(repoData); if (repo.files) { const file = repo.files.find(f => f.id === fileId); if (file) { return jsonResponse(file); } } } } return jsonResponse({ error: 'File not found' }, 404); } // Get README - PUBLIC if (path.match(/^\/api\/repos\/[^\/]+\/readme$/) && method === 'GET') { const repoId = path.split('/')[3]; const repoData = await r2Get(env, `repo:${repoId}`); if (!repoData) { return jsonResponse({ error: 'Repository not found' }, 404); } const repo = JSON.parse(repoData); const readme = repo.readme || { content: '# ' + repo.name + '\n\n' + (repo.description || 'No description provided') + '\n\n## About\n\nThis repository contains code files managed by CodeHub.', lastUpdated: repo.created }; return jsonResponse(readme); } // Update README - PROTECTED if (path.match(/^\/api\/repos\/[^\/]+\/readme$/) && method === 'PUT') { const repoId = path.split('/')[3]; const data = await request.json(); const repoData = await r2Get(env, `repo:${repoId}`); if (!repoData) { return jsonResponse({ error: 'Repository not found' }, 404); } const repo = JSON.parse(repoData); // Authorization check if (user.role !== 'admin' && repo.owner !== user.id) { return jsonResponse({ error: 'Forbidden' }, 403); } repo.readme = { content: data.content, lastUpdated: new Date().toISOString() }; await r2Put(env, `repo:${repoId}`, JSON.stringify(repo)); return jsonResponse(repo.readme); } // Upload multiple files - PROTECTED if (path === '/api/upload-multiple' && method === 'POST') { const formData = await request.formData(); const repoId = formData.get('repoId'); const files = formData.getAll('files'); if (!repoId || files.length === 0) { return jsonResponse({ error: 'Repository ID and files are required' }, 400); } // Get repository const repoData = await r2Get(env, `repo:${repoId}`); if (!repoData) { return jsonResponse({ error: 'Repository not found' }, 404); } const repo = JSON.parse(repoData); // Authorization check if (user.role !== 'admin' && repo.owner !== user.id) { return jsonResponse({ error: 'Forbidden' }, 403); } const uploadedFiles = []; const errors = []; for (const file of files) { try { const fileContent = await file.text(); const fileName = file.name; const fileExtension = fileName.split('.').pop()?.toLowerCase() || ''; // Determine language based on file extension const languageMap = { 'js': 'javascript', 'jsx': 'javascript', 'ts': 'javascript', 'tsx': 'javascript', 'py': 'python', 'html': 'html', 'htm': 'html', 'css': 'css', 'java': 'java', 'cpp': 'cpp', 'c': 'cpp', 'cc': 'cpp', 'txt': 'text', 'md': 'text', 'json': 'javascript', 'xml': 'text', 'csv': 'text' }; const language = languageMap[fileExtension] || 'text'; const fileId = generateId(); const fileData = { id: fileId, name: fileName, content: fileContent, language: language, created: new Date().toISOString(), size: file.size, type: file.type }; if (!repo.files) { repo.files = []; } // Check if file with same name already exists const existingFileIndex = repo.files.findIndex(f => f.name === fileName); if (existingFileIndex !== -1) { repo.files[existingFileIndex] = fileData; } else { repo.files.push(fileData); } uploadedFiles.push(fileData); } catch (error) { errors.push({ name: file.name, error: error.message }); } } await r2Put(env, `repo:${repoId}`, JSON.stringify(repo)); return jsonResponse({ success: true, uploadedFiles, errors, message: `${uploadedFiles.length} files uploaded successfully${errors.length > 0 ? `, ${errors.length} failed` : ''}` }); } return jsonResponse({ error: 'Not found' }, 404); } catch (error) { console.error('API error:', error); return jsonResponse({ error: 'Internal server error' }, 500); } } // Login page HTML function serveLoginPage() { const html = ` Login - CodeHub
Back to Home
`; return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' } }); } // Serve main HTML untuk dashboard function serveHTML(env, isAuthenticated, user) { const html = ` CodeHub - Dashboard

Your Repositories

`; return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' } }); } // Utility function function generateId() { return Math.random().toString(36).substring(2) + Date.now().toString(36); } // Markdown renderer function function renderMarkdown(content) { // Simple markdown to HTML converter return content // Headers .replace(/^###### (.*$)/gim, '
$1
') .replace(/^##### (.*$)/gim, '
$1
') .replace(/^#### (.*$)/gim, '

$1

') .replace(/^### (.*$)/gim, '

$1

') .replace(/^## (.*$)/gim, '

$1

') .replace(/^# (.*$)/gim, '

$1

') // Bold & Italic .replace(/\*\*(.*?)\*\*/gim, '$1') .replace(/\*(.*?)\*/gim, '$1') .replace(/_(.*?)_/gim, '$1') // Code .replace(/`(.*?)`/gim, '$1') // Blockquotes .replace(/^> (.*$)/gim, '
$1
') // Lists .replace(/^\s*[-*+] (.*$)/gim, '
  • $1
  • ') .replace(/(
  • .*<\/li>)/s, '') // Links .replace(/\[([^\[]+)\]\(([^\)]+)\)/gim, '$1') // Line breaks .replace(/\n$/gim, '
    ') // Preserve multiple newlines .replace(/\n\n/g, '

    ') .replace(/\n/g, '
    ') // Wrap in paragraphs .replace(/^(?!<(\/)?(h|ul|ol|li|blockquote|code|pre)).*$/gim, '

    $&

    ') // Clean up empty paragraphs .replace(/

    <\/p>/g, '') .replace(/


    <\/p>/g, '') // Handle code blocks .replace(/```([^`]+)```/g, '

    $1
    '); } // HTML escape function function escapeHtml(unsafe) { return unsafe .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); }