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': '*'
}
});
}
}
};
// 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 env.KODE_STORAGE.get(`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 env.KODE_STORAGE.get(`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 env.KODE_STORAGE.put(`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 env.KODE_STORAGE.get(`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 env.KODE_STORAGE.put(`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)
};
await env.KODE_STORAGE.put(`session:${sessionId}`, JSON.stringify(sessionData), {
expirationTtl: 86400 // 24 hours
});
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 env.KODE_STORAGE.delete(`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 repos = await env.KODE_STORAGE.list({ prefix: 'repo:' });
const repoList = [];
for (const key of repos.keys) {
try {
const repoData = await env.KODE_STORAGE.get(key.name);
if (repoData) {
const repo = JSON.parse(repoData);
const repoId = key.name.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
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
`;
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 env.KODE_STORAGE.get(`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)}
`).join('') :
'
No files in this repository
'
}
${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 repos = await env.KODE_STORAGE.list({ prefix: 'repo:' });
console.log('Found repositories:', repos.keys.length);
for (const key of repos.keys) {
try {
const repoData = await env.KODE_STORAGE.get(key.name);
if (repoData) {
const repo = JSON.parse(repoData);
if (repo.files && repo.files.length > 0) {
console.log(`Checking repo ${key.name} 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 env.KODE_STORAGE.get(`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 env.KODE_STORAGE.get(`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 repos = await env.KODE_STORAGE.list({ prefix: 'repo:' });
const repoList = [];
for (const key of repos.keys) {
const repoData = await env.KODE_STORAGE.get(key.name);
if (repoData) {
const repo = JSON.parse(repoData);
const repoId = key.name.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 env.KODE_STORAGE.put(`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 env.KODE_STORAGE.get(`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 env.KODE_STORAGE.get(`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 env.KODE_STORAGE.put(`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 env.KODE_STORAGE.get(`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 env.KODE_STORAGE.put(`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 repos = await env.KODE_STORAGE.list({ prefix: 'repo:' });
for (const key of repos.keys) {
const repoData = await env.KODE_STORAGE.get(key.name);
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 env.KODE_STORAGE.put(key.name, 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 repos = await env.KODE_STORAGE.list({ prefix: 'repo:' });
for (const key of repos.keys) {
const repoData = await env.KODE_STORAGE.get(key.name);
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 env.KODE_STORAGE.put(key.name, 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 env.KODE_STORAGE.get(`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 env.KODE_STORAGE.delete(`repo:${repoId}`);
return jsonResponse({ success: true });
}
// Get file content - PUBLIC
if (path.match(/^\/api\/files\/[^\/]+$/) && method === 'GET') {
const fileId = path.split('/')[3];
const repos = await env.KODE_STORAGE.list({ prefix: 'repo:' });
for (const key of repos.keys) {
const repoData = await env.KODE_STORAGE.get(key.name);
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 env.KODE_STORAGE.get(`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 env.KODE_STORAGE.get(`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 env.KODE_STORAGE.put(`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 env.KODE_STORAGE.get(`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({ file: file.name, error: error.message });
}
}
// Save repository with new files
await env.KODE_STORAGE.put(`repo:${repoId}`, JSON.stringify(repo));
return jsonResponse({
success: true,
uploaded: uploadedFiles.length,
files: uploadedFiles,
errors: errors
});
}
// Import file from URL - PROTECTED - DIPINDAHKAN ke handler terpisah
if (path === '/api/import-from-url' && method === 'POST') {
// Handler ini sekarang dipindahkan ke fungsi handleImportFromURL terpisah
// yang sudah ditangani di routing utama
return jsonResponse({ error: 'This endpoint is handled separately' }, 404);
}
return jsonResponse({ error: 'Not found' }, 404);
} catch (error) {
console.error('API error:', error);
return jsonResponse({ error: 'API error' }, 500);
}
}
// Serve login page
function serveLoginPage() {
const html = `
Login - CodeHub
Login to CodeHub
Default credentials are pre-filled
Username: admin
Password:admin123
`;
return new Response(html, {
headers: { 'Content-Type': 'text/html; charset=utf-8' }
});
}
// Serve main app dengan tampilan yang disatukan dan fitur upload - DIPERBAIKI
function serveHTML(env, authenticated, user) {
if (!authenticated) {
return new Response('', {
status: 302,
headers: { 'Location': '/login' }
});
}
const html = `
CodeHub - Your Code Repository
Markdown Tips:
• Headers: # H1, ## H2, ### H3
• Bold: **text** or __text__
• Italic: *text* or _text_
• \`code\`: \`inline code\`
• Lists: - item or 1. item
• [Links](url), 
• \`\`\`code blocks\`\`\`
Supported URL Examples:
- Raw GitHub files:
https://raw.githubusercontent.com/user/repo/main/file.js
- Gists:
https://gist.githubusercontent.com/user/gist-id/raw/file.js
- Pastebin-like services
- Any public URL with text content
`;
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, "'");
}