diff --git a/cloudflare worker.js b/cloudflare worker.js deleted file mode 100644 index d505e42..0000000 --- a/cloudflare worker.js +++ /dev/null @@ -1,2316 +0,0 @@ -export default { - async fetch(request, env, ctx) { - try { - const url = new URL(request.url); - - // Acceptable hostnames - const validHostnames = [ - 'theradicalparty.com', - 'www.theradicalparty.com', - 'radical.omar-c29.workers.dev' - ]; - - // Check if the hostname is valid - if (!validHostnames.includes(url.hostname)) { - console.error(`Unexpected hostname: ${url.hostname}`); - return new Response('Invalid hostname', { status: 400 }); - } - - const path = url.pathname; - const proposalId = url.searchParams.get('proposal'); - - console.log(`Processing request: ${url.toString()}`); - console.log(`User-Agent: ${request.headers.get('User-Agent')}`); - - // Handle CORS preflight requests first - if (request.method === 'OPTIONS') { - return handleOptions(); - } - - // For GET requests, try to serve from cache first - if (request.method === "GET") { - // Create a cache key from the request URL - const cacheKey = new Request(request.url, { - headers: request.headers - }); - - // Try to get from cache first - let cachedResponse = await caches.default.match(cacheKey); - - if (cachedResponse) { - // Add header to indicate cache hit - cachedResponse = new Response(cachedResponse.body, cachedResponse); - cachedResponse.headers.set("CF-Cache-Status", "HIT"); - - // Ensure CORS headers are present - Object.keys(corsHeaders).forEach(key => { - cachedResponse.headers.set(key, corsHeaders[key]); - }); - - return cachedResponse; - } - } - - // Handle styles.css route - if (path === '/styles.css') { - try { - // Fetch CSS from GitHub Pages - const response = await fetch('https://radical.pages.dev/radical/styles.css', { - headers: { - 'User-Agent': request.headers.get('User-Agent') || 'Cloudflare Worker' - } - }); - - if (!response.ok) { - console.error('Failed to fetch styles.css', response.status, response.statusText); - return new Response('CSS File Not Found', { status: 404 }); - } - - // Get the CSS content - const cssContent = await response.text(); - - // Return CSS response with caching headers - const cssResponse = new Response(cssContent, { - status: 200, - headers: { - 'Content-Type': 'text/css', - 'Cache-Control': 'public, max-age=86400', // 1 day - ...corsHeaders // Add CORS headers - } - }); - - // Store in cache if this is a GET request - if (request.method === "GET") { - const cacheKey = new Request(request.url, { - headers: request.headers - }); - ctx.waitUntil(caches.default.put(cacheKey, cssResponse.clone())); - } - - return cssResponse; - } catch (error) { - console.error('Error fetching styles.css:', error); - return new Response('Internal Server Error', { status: 500 }); - } - } - - const isTestMeta = url.searchParams.get('test_meta') === 'true'; - - // Handle root path and index.html with potential meta tag testing - if (path === '/' || path === '/index.html') { - // If test_meta is true or there's a proposal, use customization - if (isTestMeta || proposalId) { - return await serveCustomizedHtml(proposalId || 'test_proposal', request, env); - } - - // Otherwise, serve main page normally - return await serveMainPage(request, env); - } - - if (path === '/api/petition-svg') { - return await servePetitionSVG(request, env); - } - - // Meta tag testing - if (url.searchParams.get('test_meta') === 'true') { - console.log('Meta tag test detected'); - - // If there's a proposal during meta test, use customization - if (proposalId && (path === '/' || path === '/index.html')) { - console.log(`Detected proposal in URL during meta test: ${proposalId}`); - return await serveCustomizedHtml(proposalId, request, env); - } - } - - console.log(`Received ${request.method} request to ${path}`); - - // Check if the DB binding exists - if (!env.DB) { - console.error('DB binding is missing! Check your wrangler.toml configuration.'); - return createResponse({ - error: 'Database configuration error. Missing DB binding.', - hint: 'Check your wrangler.toml file and make sure the DB binding is correctly set up.' - }, 500); - } - - // Handle media files (memes and audio) - if (path.startsWith('/memes/') || path.startsWith('/audio/')) { - const mediaResponse = await serveMedia(request, env); - - // Cache media responses for GET requests - if (request.method === "GET") { - const cacheKey = new Request(request.url, { - headers: request.headers - }); - - // Clone the response before modifying - const responseToCache = new Response(mediaResponse.body, mediaResponse); - - // Cache media files for 7 days - responseToCache.headers.set('Cache-Control', 'public, max-age=604800'); - - ctx.waitUntil(caches.default.put(cacheKey, responseToCache.clone())); - } - - return mediaResponse; - } else if (path === '/api/memes' && request.method === 'POST') { - return await uploadMeme(request, env); - } - - // Route requests to appropriate handlers - let response; - - if (path === '/api/health') { - response = await healthCheck(env); - } else if (path === '/api/debug-meta') { - response = await debugMetaTags(request, env); - } else if (path === '/login' || path === '/login.html') { - response = await serveLoginPage(request, env); - } else if (path === '/api/login' && request.method === 'POST') { - response = await validateLogin(request, env); - } else if (path === '/api/proposals' && request.method === 'GET') { - response = await getProposals(request, env); - } else if (path === '/api/proposals' && request.method === 'POST') { - response = await createProposal(request, env); - } else if (path.startsWith('/api/proposals/') && request.method === 'GET') { - response = await getProposalById(request, env); - } else if (path.startsWith('/api/proposals/') && request.method === 'PUT') { - const id = path.split('/').pop(); - response = await updateProposal(id, request, env); - } else if (path === '/api/votes' && request.method === 'POST') { - response = await createOrUpdateVote(request, env); - } else if (path === '/api/users' && request.method === 'POST') { - response = await createOrGetUser(request, env); - } else if (path === '/api/petition-stats' && request.method === 'GET') { - response = await getPetitionStats(request, env); - } else if (path === '/api/comments' && request.method === 'GET') { - response = await getComments(request, env); - } else if (path === '/api/comments' && request.method === 'POST') { - response = await createComment(request, env); - } else if (path === '/api/comment-votes' && request.method === 'POST') { - response = await createOrUpdateCommentVote(request, env); - } else if (path === '/api/comment-votes' && request.method === 'GET') { - response = await getCommentVotes(request, env); - } else { - response = createResponse({ - error: 'Not found', - path, - method: request.method, - availableRoutes: [ - { path: '/api/health', method: 'GET', description: 'Check system health' }, - { path: '/api/proposals', method: 'GET', description: 'Get proposals' }, - { path: '/api/proposals/:id', method: 'GET', description: 'Get a single proposal by ID' }, - { path: '/api/proposals/:id', method: 'PUT', description: 'Update proposal' }, - { path: '/api/votes', method: 'POST', description: 'Create/update vote' }, - { path: '/api/users', method: 'POST', description: 'Create/get user' }, - { path: '/api/petition-stats', method: 'GET', description: 'Get petition statistics' } - ] - }, 404); - } - - // Cache certain GET responses - if (request.method === "GET") { - const shouldCache = path.startsWith('/api/proposals') || - path.startsWith('/api/comments') || - path === '/api/petition-stats' || - path === '/api/health'; - - if (shouldCache && response) { - // We need to clone the response before reading its body - const responseToCache = new Response(response.body, response); - - // Set appropriate cache duration based on endpoint - let cacheDuration = 300; // 5 minutes default for API - if (path === '/api/health') { - cacheDuration = 3600; // 1 hour for health checks - } - - responseToCache.headers.set('Cache-Control', `public, max-age=${cacheDuration}`); - responseToCache.headers.set('CF-Cache-Status', 'MISS'); - - // Ensure all CORS headers are present - Object.keys(corsHeaders).forEach(key => { - responseToCache.headers.set(key, corsHeaders[key]); - }); - - const cacheKey = new Request(request.url, { - headers: request.headers - }); - - ctx.waitUntil(caches.default.put(cacheKey, responseToCache.clone())); - - // Make sure the response we return also has CORS headers - Object.keys(corsHeaders).forEach(key => { - response.headers.set(key, corsHeaders[key]); - }); - } - } - - return response; - - } catch (error) { - console.error('Critical error in request handling:', error); - return new Response(JSON.stringify({ - error: 'Internal Server Error', - details: error.message - }), { - status: 500, - headers: { - 'Content-Type': 'application/json', - ...corsHeaders // Add CORS headers to error responses too - } - }); - } - } -}; - - -async function serveMainPage(request, env) { - try { - // Fetch directly from Cloudflare Pages - const response = await fetch('https://radical.pages.dev/index.html', { - headers: { - 'User-Agent': request.headers.get('User-Agent') || 'Cloudflare Worker' - } - }); - - if (!response.ok) { - console.error('Failed to fetch main page', response.status, response.statusText); - return new Response('Failed to load page', { status: 500 }); - } - - // Read the HTML content - const html = await response.text(); - - // Create a new response with the HTML content - return new Response(html, { - status: 200, - headers: { - 'Content-Type': 'text/html', - 'Cache-Control': 'public, max-age=86400', - // Add CORS headers if needed - 'Access-Control-Allow-Origin': '*' - } - }); - } catch (error) { - console.error('Comprehensive error serving main page:', error); - return new Response(`Internal Server Error: ${error.message}`, { status: 500 }); - } -} - -async function serveLoginPage(request, env) { - try { - // Fetch the login HTML file from your storage - // You could store this in R2 bucket or GitHub Pages like your other static files - const response = await fetch('https://radical.pages.dev/radical/login.html', { - headers: { - 'User-Agent': request.headers.get('User-Agent') || 'Cloudflare Worker' - } - }); - - if (!response.ok) { - console.error('Failed to fetch login page', response.status, response.statusText); - return new Response('Failed to load login page', { status: 500 }); - } - - // Read the HTML content - const html = await response.text(); - - // Create a new response with the HTML content - return new Response(html, { - status: 200, - headers: { - 'Content-Type': 'text/html', - 'Cache-Control': 'public, max-age=86400', - 'Access-Control-Allow-Origin': '*' - } - }); - } catch (error) { - console.error('Error serving login page:', error); - return new Response(`Internal Server Error: ${error.message}`, { status: 500 }); - } -} - -// Function to validate user login with emoji combination and selections -async function validateLogin(request, env) { - try { - const data = await request.json(); - const { - emojiCombination, - selectedDictator, - selectedTarget - } = data; - - console.log(`Processing login attempt: ${JSON.stringify({ - emojiCount: emojiCombination?.length || 0, - dictator: selectedDictator, - target: selectedTarget - })}`); - - // Basic validation - if (!emojiCombination || !selectedDictator || !selectedTarget) { - return createResponse({ - error: 'Missing required fields', - received: { - hasEmojis: !!emojiCombination, - hasDictator: !!selectedDictator, - hasTarget: !!selectedTarget - } - }, 400); - } - - // Convert emoji array to string for storage/comparison - const emojiCombinationString = Array.isArray(emojiCombination) - ? emojiCombination.join('') - : emojiCombination; - - // Check if dictator exists - const dictatorCheck = await env.DB.prepare( - `SELECT id FROM dictators WHERE id = ?` - ).bind(selectedDictator).first(); - - if (!dictatorCheck) { - return createResponse({ - error: 'Invalid dictator selection', - dictatorId: selectedDictator - }, 400); - } - - // Check if target exists - const targetCheck = await env.DB.prepare( - `SELECT id FROM time_targets WHERE id = ?` - ).bind(selectedTarget).first(); - - if (!targetCheck) { - return createResponse({ - error: 'Invalid time target selection', - targetId: selectedTarget - }, 400); - } - - // Record the login attempt (for security tracking) - const attemptId = 'login_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5); - const timestamp = Date.now(); - const clientIP = request.headers.get('CF-Connecting-IP') || 'unknown'; - - try { - // Try to find a user with the exact combination - const user = await env.DB.prepare(` - SELECT id, name, emoji_combination, selected_dictator_id, selected_target_id - FROM users - WHERE emoji_combination = ? - AND selected_dictator_id = ? - AND selected_target_id = ? - `).bind( - emojiCombinationString, - selectedDictator, - selectedTarget - ).first(); - - // Log the attempt, regardless of outcome - await env.DB.prepare(` - INSERT INTO login_attempts ( - id, - user_id, - emoji_combination, - dictator_id, - target_id, - timestamp, - ip_address, - success - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - `).bind( - attemptId, - user ? user.id : null, - emojiCombinationString, - selectedDictator, - selectedTarget, - timestamp, - clientIP, - user ? 1 : 0 - ).run(); - - if (user) { - // Login successful - Update last login time - await env.DB.prepare(` - UPDATE users - SET last_login = ? - WHERE id = ? - `).bind(timestamp, user.id).run(); - - console.log(`Successful login for user ${user.id}`); - - // Return success with user info (excluding sensitive data) - return createResponse({ - success: true, - userId: user.id, - username: user.name, - message: "Login successful" - }); - } else { - // No matching user found - this could be a new signup - // Check if this is a unique emoji combination - const existingCombo = await env.DB.prepare(` - SELECT COUNT(*) as count - FROM users - WHERE emoji_combination = ? - `).bind(emojiCombinationString).first(); - - if (existingCombo.count > 0) { - // Emoji combination exists but other parameters don't match - return createResponse({ - error: "Invalid credentials", - message: "The combination you entered is incorrect" - }, 401); - } - - // This is a new user registration - const newUserId = 'citizen_' + Math.random().toString(36).substr(2, 9); - const newUsername = 'Citizen ' + Math.floor(Math.random() * 9000 + 1000); - - // Create the new user - await env.DB.prepare(` - INSERT INTO users ( - id, - name, - created_at, - emoji_combination, - selected_dictator_id, - selected_target_id, - last_login - ) - VALUES (?, ?, ?, ?, ?, ?, ?) - `).bind( - newUserId, - newUsername, - timestamp, - emojiCombinationString, - selectedDictator, - selectedTarget, - timestamp - ).run(); - - console.log(`Created new user ${newUserId} with emoji login`); - - // Return success with new user info - return createResponse({ - success: true, - isNewUser: true, - userId: newUserId, - username: newUsername, - message: "Account created successfully" - }); - } - } catch (dbError) { - console.error('Database error during login validation:', dbError); - - return createResponse({ - error: "Login processing error", - details: "An error occurred while processing your login" - }, 500); - } - } catch (error) { - console.error('Error in login validation:', error); - - return createResponse({ - error: "Login request error", - message: "Could not process login request" - }, 500); - } -} - -async function createOrUpdateCommentVote(request, env) { - try { - const data = await request.json(); - const { commentId, userId, voteType } = data; - - console.log(`Processing comment vote: ${JSON.stringify({ - commentId, - userId, - voteType - })}`); - - if (!commentId || !userId || !voteType) { - return createResponse({ - error: 'Missing required fields', - received: { commentId, userId, voteType: voteType || 'undefined' } - }, 400); - } - - if (voteType !== 'upvote' && voteType !== 'downvote') { - return createResponse({ - error: 'Invalid vote type', - voteType, - allowedValues: ['upvote', 'downvote'] - }, 400); - } - - // Check if entities exist - try { - const userCheck = await env.DB.prepare(`SELECT id FROM users WHERE id = ?`).bind(userId).first(); - if (!userCheck) { - return createResponse({ error: 'User does not exist', userId }, 404); - } - - const commentCheck = await env.DB.prepare(`SELECT id FROM comments WHERE id = ?`).bind(commentId).first(); - if (!commentCheck) { - return createResponse({ error: 'Comment does not exist', commentId }, 404); - } - } catch (checkError) { - return createResponse({ - error: 'Error checking user or comment existence', - details: logError(checkError, { action: 'check_entities', userId, commentId }) - }, 500); - } - - // Check if vote already exists - let existingVote; - try { - const checkQuery = ` - SELECT id, vote_type FROM comment_votes - WHERE comment_id = ? AND user_id = ? - `; - - existingVote = await env.DB.prepare(checkQuery).bind(commentId, userId).first(); - console.log(`Existing comment vote check: ${existingVote ? JSON.stringify(existingVote) : 'None found'}`); - } catch (voteCheckError) { - return createResponse({ - error: 'Error checking existing comment vote', - details: logError(voteCheckError, { action: 'check_comment_vote', userId, commentId }) - }, 500); - } - - const timestamp = Date.now(); - const voteId = existingVote ? existingVote.id : 'comment_vote_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5); - - try { - // Process vote - if (existingVote) { - // If vote exists and it's the same type, remove it (toggle off) - if (existingVote.vote_type === voteType) { - await env.DB.prepare(` - DELETE FROM comment_votes - WHERE id = ? - `).bind(existingVote.id).run(); - - console.log(`Deleted comment vote ${existingVote.id} (toggle off)`); - - return createResponse({ - commentId, - userId, - voteType: null, - action: 'removed' - }); - } else { - // If vote exists but is different type, update it - await env.DB.prepare(` - UPDATE comment_votes - SET vote_type = ?, timestamp = ? - WHERE id = ? - `).bind( - voteType, - timestamp, - existingVote.id - ).run(); - - console.log(`Updated comment vote ${existingVote.id} to ${voteType}`); - - return createResponse({ - commentId, - userId, - voteType, - action: 'updated' - }); - } - } else { - // If no vote exists, create new one - await env.DB.prepare(` - INSERT INTO comment_votes (id, comment_id, user_id, vote_type, timestamp) - VALUES (?, ?, ?, ?, ?) - `).bind( - voteId, - commentId, - userId, - voteType, - timestamp - ).run(); - - console.log(`Created new comment vote with ID: ${voteId}`); - - return createResponse({ - commentId, - userId, - voteType, - action: 'created' - }); - } - } catch (dbError) { - return createResponse({ - error: 'Failed to process comment vote in database', - details: logError(dbError, { - action: 'process_comment_vote', - existingVote: existingVote || null, - newVote: { commentId, userId, voteType } - }) - }, 500); - } - } catch (error) { - return createResponse({ - error: 'Failed to process comment vote request', - details: logError(error, { action: 'comment_vote_outer' }) - }, 500); - } -} - -// Function to get votes for a specific comment -async function getCommentVotes(request, env) { - try { - const url = new URL(request.url); - const commentId = url.searchParams.get('commentId'); - const userId = url.searchParams.get('userId'); - - if (!commentId) { - return createResponse({ error: 'Missing commentId parameter' }, 400); - } - - // Get vote counts - const query = ` - SELECT - (SELECT COUNT(*) FROM comment_votes WHERE comment_id = ? AND vote_type = 'upvote') as upvotes, - (SELECT COUNT(*) FROM comment_votes WHERE comment_id = ? AND vote_type = 'downvote') as downvotes - `; - - try { - const voteCounts = await env.DB.prepare(query).bind(commentId, commentId).first(); - - let userVote = null; - // If userId is provided, get the user's vote - if (userId) { - const userVoteQuery = ` - SELECT vote_type FROM comment_votes - WHERE comment_id = ? AND user_id = ? - `; - - const userVoteResult = await env.DB.prepare(userVoteQuery).bind(commentId, userId).first(); - if (userVoteResult) { - userVote = userVoteResult.vote_type; - } - } - - return createResponse({ - commentId, - upvotes: voteCounts.upvotes || 0, - downvotes: voteCounts.downvotes || 0, - userVote - }); - } catch (dbError) { - return createResponse({ - error: 'Failed to retrieve comment votes', - details: logError(dbError, { action: 'get_comment_votes', commentId }) - }, 500); - } - } catch (error) { - return createResponse({ - error: 'Failed to process comment votes request', - details: logError(error, { action: 'get_comment_votes_outer' }) - }, 500); - } -} - - - -// Serve meme files -async function serveMeme(request, env) { - const url = new URL(request.url); - const path = url.pathname; - const key = path.replace('/memes/', ''); - - // Handle CORS preflight requests - if (request.method === 'OPTIONS') { - return handleOptions(); - } - - try { - const object = await env.MEMES_BUCKET.get(key); - - if (object === null) { - return new Response('Meme not found', { status: 404 }); - } - - const headers = new Headers(); - object.writeHttpMetadata(headers); - headers.set('etag', object.httpEtag); - headers.set('Cache-Control', 'public, max-age=31536000'); // Cache for a year - - // Add CORS headers - Object.keys(corsHeaders).forEach(key => { - headers.set(key, corsHeaders[key]); - }); - - return new Response(object.body, { - headers - }); - } catch (error) { - console.error('Error serving meme:', error); - return new Response('Internal Server Error', { status: 500 }); - } -} - -// Debug endpoint to help diagnose meta tag issues -async function debugMetaTags(request, env) { - try { - // Create a sample proposal for testing - const testProposal = { - id: 'test_proposal', - text: 'This is a test proposal for meta tags', - author_name: 'Test User', - timestamp: Date.now() - }; - - // Generate a shareable image URL - const shareImageUrl = getShareableImageUrl(testProposal); - - // Prepare detailed meta tag information - const metaTagInfo = { - og_image: shareImageUrl, - og_title: 'RADICAL Meta Tag Test', - og_description: 'Testing meta tag generation and injection', - twitter_image: shareImageUrl, - twitter_title: 'RADICAL Meta Tag Verification', - twitter_description: 'Checking social media sharing metadata' - }; - - // Return detailed information about the meta tags - return createResponse({ - status: 'success', - message: 'Meta tag debug information', - shareImageUrl: shareImageUrl, - metaTagDetails: metaTagInfo - }); - } catch (error) { - return createResponse({ - error: 'Debug meta tags endpoint error', - details: logError(error, { action: 'debug_meta_tags' }) - }, 500); - } -} - -// Handle meme uploads to R2 storage -async function uploadMeme(request, env) { - try { - // Check if R2 binding exists - if (!env.MEMES_BUCKET) { - return createResponse({ - error: 'Storage binding missing. Check your settings configuration.' - }, 500); - } - - // Handle multipart form data - const formData = await request.formData(); - - // Get the proposal ID - const proposalId = formData.get('proposalId'); - if (!proposalId) { - return createResponse({ error: 'Missing proposalId' }, 400); - } - - // Get the file - const file = formData.get('meme'); - if (!file || !(file instanceof File)) { - return createResponse({ error: 'No file uploaded or invalid file' }, 400); - } - - // Validate file type - const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; - if (!validTypes.includes(file.type)) { - return createResponse({ - error: 'Invalid file type. Only JPEG, PNG, GIF, and WebP images are allowed.', - received: file.type - }, 400); - } - - // Validate file size (limit to 2MB) - const MAX_SIZE = 2 * 1024 * 1024; // 2MB - if (file.size > MAX_SIZE) { - return createResponse({ - error: 'File too large. Maximum size is 2MB.', - size: file.size, - maxSize: MAX_SIZE - }, 400); - } - - // Create a unique filename - const fileExt = file.name.split('.').pop(); - const uniqueFilename = `${proposalId}_${Date.now()}.${fileExt}`; - - // Upload to R2 - await env.MEMES_BUCKET.put(uniqueFilename, file.stream(), { - httpMetadata: { - contentType: file.type, - }, - }); - - // Generate the public URL - const memeUrl = `https://radical.omar-c29.workers.dev/memes/${uniqueFilename}`; - - // Helper function to fix broken meme URLs - function fixMemeUrl(url) { - // Check if the URL contains the problematic domain - if (url && url.includes('memes.undefined')) { - // Extract the filename from the URL - const filename = url.split('/').pop(); - // Replace with the correct domain - return `https://radical.omar-c29.workers.dev/memes/${filename}`; - } - return url; - } - - // Update the proposal with the meme URL - try { - await env.DB.prepare(` - UPDATE proposals - SET meme_url = ? - WHERE id = ? - `).bind(memeUrl, proposalId).run(); - - console.log(`Added meme ${uniqueFilename} to proposal ${proposalId}`); - - return createResponse({ - success: true, - proposalId, - memeUrl - }); - } catch (dbError) { - // If DB update fails, try to delete the uploaded file - try { - await env.MEMES_BUCKET.delete(uniqueFilename); - } catch (deleteError) { - console.error('Failed to delete orphaned file:', deleteError); - } - - return createResponse({ - error: 'Failed to update proposal with meme URL', - details: logError(dbError, { action: 'update_proposal_meme', proposalId }) - }, 500); - } - } catch (error) { - return createResponse({ - error: 'Failed to process meme upload', - details: logError(error, { action: 'upload_meme' }) - }, 500); - } -}// Get petition statistics -async function getPetitionStats(request, env) { - try { - const url = new URL(request.url); - const proposalId = url.searchParams.get('proposalId'); - - if (!proposalId) { - return createResponse({ error: 'Missing proposalId parameter' }, 400); - } - - // Get petition statistics - const statsQuery = ` - SELECT - COUNT(DISTINCT pd.user_id) as verified_count, - (SELECT COUNT(*) FROM votes WHERE proposal_id = ? AND vote_type = 'upvote' AND is_petition = 1) as total_signatures, - (SELECT text FROM proposals WHERE id = ?) as proposal_text - FROM petition_details pd - WHERE pd.proposal_id = ? AND pd.verified = 1 - `; - - const stats = await env.DB.prepare(statsQuery).bind(proposalId, proposalId, proposalId).first(); - - if (!stats) { - return createResponse({ error: 'Proposal not found' }, 404); - } - - // Get top postcodes for this petition - const postcodesQuery = ` - SELECT postcode, COUNT(*) as count - FROM petition_details - WHERE proposal_id = ? AND verified = 1 - GROUP BY postcode - ORDER BY count DESC - LIMIT 5 - `; - - const postcodes = await env.DB.prepare(postcodesQuery).bind(proposalId).all(); - - return createResponse({ - proposalId, - proposalText: stats.proposal_text, - verifiedSignatures: stats.verified_count, - totalSignatures: stats.total_signatures, - topPostcodes: postcodes.results - }); - } catch (error) { - return createResponse({ - error: 'Failed to process petition stats request', - details: logError(error, { action: 'petition_stats', proposalId }) - }, 500); - } -}// Define CORS headers -const corsHeaders = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', -}; - -// Helper to log detailed errors -function logError(error, context = {}) { - const errorDetails = { - message: error.message, - stack: error.stack, - name: error.name, - context: context - }; - - console.error(JSON.stringify(errorDetails)); - return errorDetails; -} - -// Handle OPTIONS request for CORS preflight -function handleOptions() { - return new Response(null, { - headers: corsHeaders - }); -} - -// Helper to create standardized responses -function createResponse(body, status = 200, cacheDuration = 60*60*24) { - const headers = { - 'Content-Type': 'application/json', - ...corsHeaders - }; - - if (cacheDuration > 0) { - headers['Cache-Control'] = `public, max-age=${cacheDuration}`; - headers['CDN-Cache-Control'] = `public, max-age=${cacheDuration}`; - } else { - headers['Cache-Control'] = 'no-store'; - } - - return new Response(JSON.stringify(body), { - status, - headers - }); -} - - -async function verifyTemplateImage(env) { - try { - const key = "petition-template.png"; - const object = await env.MEMES_BUCKET.get(key); - - if (object === null) { - console.error("❌ Template image not found in R2 bucket!"); - return false; - } - - console.log("✅ Template image found in R2 bucket"); - return true; - } catch (error) { - console.error("Error verifying template image:", error); - return false; - } -} - - -/** - * Handle requests for dynamic SVG images for petitions - * @param {Request} request - The incoming request - * @param {Object} env - Environment bindings - * @returns {Response} - SVG response - */ -async function servePetitionSVG(request, env) { - try { - const url = new URL(request.url); - const proposalId = url.searchParams.get('id'); - - if (!proposalId) { - return new Response('Missing proposal ID', { status: 400 }); - } - - console.log(`Generating SVG image for proposal: ${proposalId}`); - - // Fetch the proposal data from the database - const proposal = await env.DB.prepare(` - SELECT - p.id, p.text, p.timestamp, - u.name as author_name, - (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote') as upvotes, - (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'downvote') as downvotes - FROM proposals p - JOIN users u ON p.author_id = u.id - WHERE p.id = ? - `).bind(proposalId).first(); - - if (!proposal) { - return new Response('Proposal not found', { status: 404 }); - } - - // Generate the SVG - const svgMarkup = generatePetitionSVG(proposal); - - // Return the SVG with appropriate headers - return new Response(svgMarkup, { - headers: { - 'Content-Type': 'image/svg+xml', - 'Cache-Control': 'public, max-age=360000', // Cache for 1 hour - 'Access-Control-Allow-Origin': '*' - } - }); - } catch (error) { - console.error('Error generating petition SVG:', error); - return new Response('Internal Server Error', { status: 500 }); - } -} - -/** - * Generates a dynamic SVG for a petition with the given data - * @param {Object} proposal - The petition/proposal data - * @returns {string} - SVG markup as a string - */ -function generatePetitionSVG(proposal) { - // Get data with fallbacks - const petitionText = proposal.text || "Unknown petition"; - const upvotes = proposal.upvotes || 0; - const downvotes = proposal.downvotes || 0; - const netVotes = upvotes - downvotes; - const netVotesDisplay = netVotes > 0 ? `+${netVotes}` : netVotes; - const netVotesColor = netVotes > 0 ? "#11cc77" : (netVotes < 0 ? "#ff0099" : "#ffffff"); - const proposalId = proposal.id || "unknown"; - - // Format timestamp - const timestamp = new Date(proposal.timestamp || Date.now()).toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric' - }); - - // Base SVG template - const svgTemplate = ` - - - - - - - - - RADICAL - - - - VIBE, VOTE, VETO - - - - - - ${generateSVGTextLines(petitionText, 120, 220, 960)} - - - - - - ${upvotes} - - - - ${downvotes} - - - NET: - ${netVotesDisplay} - - - - - Petition ID: ${proposalId} • Created: ${timestamp} - - - - - - - RADICAL - PETITION - #${proposalId.slice(-6)} - - - - theradicalparty.com -`; - - return svgTemplate; -} - -/** - * Generates SVG text elements with proper line wrapping - * @param {string} text - The text to wrap - * @param {number} x - Starting x position - * @param {number} y - Starting y position - * @param {number} width - Maximum width for text wrapping - * @returns {string} - SVG text elements - */ -function generateSVGTextLines(text, x, y, width) { - // Simple word wrapping algorithm - const words = text.split(' '); - const lines = []; - let currentLine = ''; - - // Estimate characters per line based on width and average character width - const avgCharWidth = 22; // Rough estimate for font size ~36px - const charsPerLine = Math.floor(width / avgCharWidth); - - for (const word of words) { - const testLine = currentLine ? `${currentLine} ${word}` : word; - if (testLine.length <= charsPerLine) { - currentLine = testLine; - } else { - lines.push(currentLine); - currentLine = word; - } - } - - if (currentLine) { - lines.push(currentLine); - } - - // Limit to 6 lines to fit in the container - const limitedLines = lines.slice(0, 6); - if (lines.length > 6) { - // Add ellipsis to the last line if truncated - limitedLines[5] = limitedLines[5].slice(0, -3) + '...'; - } - - // Generate SVG text elements - const lineHeight = 46; - let svgTextElements = ''; - - limitedLines.forEach((line, index) => { - const lineY = y + index * lineHeight; - svgTextElements += `${escapeHTML(line)}\n`; - }); - - return svgTextElements; -} - -/** - * Escapes HTML special characters in a string - * @param {string} text - The text to escape - * @returns {string} - Escaped text - */ -function escapeHTML(text) { - return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} - -// Update the getShareableImageUrl function to use the SVG endpoint -function getShareableImageUrl(proposal) { - // Use the dynamic SVG endpoint with the proposal ID - return `https://${new URL(request.url).hostname}/api/petition-svg?id=${proposal.id}`; -} - -// Update the serveCustomizedHtml function to use the SVG URL for meta tags -async function serveCustomizedHtml(proposalId, request, env) { - console.log(`===== BEGIN serveCustomizedHtml for proposal ${proposalId} =====`); - - try { - const url = new URL(request.url); - const isTestMeta = url.searchParams.get('test_meta') === 'true'; - - // Determine which proposal to use - let proposal; - let shareImageUrl; - - if (isTestMeta) { - // Use test proposal data - proposal = { - id: 'test_proposal', - text: 'This is a test proposal for meta tags', - timestamp: Date.now(), - author_name: 'Test User' - }; - shareImageUrl = `https://${url.hostname}/api/petition-svg?id=test_proposal`; - } else { - // Fetch the actual proposal from the database - proposal = await env.DB.prepare(` - SELECT - p.id, p.text, p.timestamp, - u.name as author_name - FROM proposals p - JOIN users u ON p.author_id = u.id - WHERE p.id = ? - `).bind(proposalId).first(); - - if (!proposal) { - console.error(`Proposal not found: ${proposalId}`); - return await fetch(request); - } - - // Generate SVG URL - shareImageUrl = `https://${url.hostname}/api/petition-svg?id=${proposalId}`; - } - - console.log(`Found proposal: ${JSON.stringify(proposal)}`); - console.log(`Using share image URL: ${shareImageUrl}`); - - // Get the full URL for this proposal - const fullProposalUrl = `https://${url.hostname}/?proposal=${proposalId}`; - - // Fetch the original HTML - const response = await fetch(request); - const html = await response.text(); - - // First, remove all existing og:image and twitter:image tags - let modifiedHtml = html.replace(/]*>/g, ''); - modifiedHtml = modifiedHtml.replace(/]*>/g, ''); - modifiedHtml = modifiedHtml.replace(/]*>/g, ''); - - // Find the head end tag - const headEndIndex = modifiedHtml.indexOf(''); - if (headEndIndex === -1) { - console.error("Could not find tag in HTML"); - return new Response(modifiedHtml, { - headers: response.headers - }); - } - - // Create comprehensive meta tags - const customMetaTags = ` - - - - - - - - - - - - - - - - - - - - - - - `; - - // Insert our meta tags right before - let customizedHtml = modifiedHtml.substring(0, headEndIndex) + customMetaTags + modifiedHtml.substring(headEndIndex); - - console.log(`===== END serveCustomizedHtml for proposal ${proposalId} =====`); - - // Return the customized HTML with appropriate headers - const originalHeaders = new Headers(response.headers); - originalHeaders.set('Cache-Control', 'no-store, max-age=0'); - originalHeaders.set('X-Custom-Meta', isTestMeta ? 'test' : 'proposal'); - originalHeaders.set('X-Share-Image-Url', shareImageUrl); // Debug header - - return new Response(customizedHtml, { - headers: originalHeaders - }); - } catch (error) { - console.error('Error generating customized HTML:', error); - // Fall back to regular page if something goes wrong - return await fetch(request); - } -} - - - - -// Function to check if a table exists -async function tableExists(env, tableName) { - try { - const query = "SELECT name FROM sqlite_master WHERE type='table' AND name = ?"; - const result = await env.DB.prepare(query).bind(tableName).first(); - return !!result; - } catch (error) { - logError(error, { action: 'check_table_exists', tableName }); - return false; - } -} - -// Simple health check endpoint -async function healthCheck(env) { - try { - // Check if DB binding exists - if (!env.DB) { - return createResponse({ - status: 'error', - error: 'Database binding missing. Check wrangler.toml configuration.' - }, 500); - } - - // Check if we can execute a simple query - try { - const result = await env.DB.prepare("SELECT 1 as db_check").first(); - if (!result || result.db_check !== 1) { - return createResponse({ - status: 'error', - error: 'Database query failed to return expected result' - }, 500); - } - } catch (dbError) { - return createResponse({ - status: 'error', - error: 'Database query execution failed', - details: logError(dbError, { action: 'health_check_query' }) - }, 500); - } - - // Check if required tables exist - const tables = ['users', 'proposals', 'votes', 'petition_details']; - const tableStatus = {}; - - for (const table of tables) { - tableStatus[table] = await tableExists(env, table); - } - - const missingTables = Object.keys(tableStatus).filter(table => !tableStatus[table]); - - if (missingTables.length > 0) { - return createResponse({ - status: 'warning', - message: 'Database connected but some tables are missing', - missingTables, - tableStatus - }, 200); - } - - return createResponse({ - status: 'ok', - message: 'Database connection and tables verified', - database: env.DB.name || 'unknown', - tableStatus - }, 200); - } catch (error) { - return createResponse({ - status: 'error', - error: 'Health check failed', - details: logError(error, { action: 'health_check' }) - }, 500); - } -} - -// Get proposals with optional sorting -async function getProposals(request, env) { - try { - const url = new URL(request.url); - const sortBy = url.searchParams.get('sortBy') || 'newest'; - - // Log request details - console.log(`Getting proposals with sort: ${sortBy}`); - - let query = ` - SELECT - p.id, p.text, p.timestamp, p.trending, p.meme_url, - u.name as author_name, u.id as author_id, - (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote') as upvotes, - (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'downvote') as downvotes, - (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote' AND is_petition = 1) as petition_signatures, - (SELECT COUNT(DISTINCT pd.user_id) FROM petition_details pd WHERE pd.proposal_id = p.id AND pd.verified = 1) as verified_petitioners - FROM proposals p - JOIN users u ON p.author_id = u.id - `; - // Add sorting - if (sortBy === 'newest') { - query += ' ORDER BY p.timestamp DESC'; - } else if (sortBy === 'popular') { - query += ` - ORDER BY ( - (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote') - - (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'downvote') - ) DESC - `; - } else if (sortBy === 'controversial') { - query += ` - ORDER BY ( - (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote') + - (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'downvote') - ) DESC - `; - } else if (sortBy === 'petitions') { - // New sorting option for most petitioned - query += ` - ORDER BY ( - (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote' AND is_petition = 1) - ) DESC - `; - } - - console.log(`Executing query: ${query}`); - - // Get user ID for checking existing votes - const userId = url.searchParams.get('userId'); - let proposals; - - try { - proposals = await env.DB.prepare(query).all(); - console.log(`Retrieved ${proposals.results.length} proposals`); - } catch (dbError) { - return createResponse({ - error: 'Failed to retrieve proposals', - details: logError(dbError, { action: 'get_proposals', query }) - }, 500); - } - - // If we have a userId, get their votes for these proposals - if (userId && proposals.results.length > 0) { - const proposalIds = proposals.results.map(p => p.id); - - try { - // Use parameter binding with placeholders - let placeholderStr = ''; - for (let i = 0; i < proposalIds.length; i++) { - placeholderStr += (i === 0 ? '?' : ',?'); - } - - const votesQuery = ` - SELECT proposal_id, vote_type, is_petition - FROM votes - WHERE user_id = ? AND proposal_id IN (${placeholderStr}) - `; - - console.log(`Executing votes query for user ${userId} with ${proposalIds.length} proposals`); - - const bindParams = [userId, ...proposalIds]; - const votes = await env.DB.prepare(votesQuery).bind(...bindParams).all(); - - console.log(`Retrieved ${votes.results.length} votes for user ${userId}`); - - // Create a map of proposal_id to vote info - const voteMap = {}; - for (const vote of votes.results) { - voteMap[vote.proposal_id] = { - voteType: vote.vote_type, - isPetition: vote.is_petition === 1 - }; - } - - // Also get petition details status - const petitionQuery = ` - SELECT proposal_id, verified - FROM petition_details - WHERE user_id = ? AND proposal_id IN (${placeholderStr}) - `; - - const petitionDetails = await env.DB.prepare(petitionQuery).bind(...bindParams).all(); - const petitionMap = {}; - - for (const petition of petitionDetails.results) { - petitionMap[petition.proposal_id] = { - verified: petition.verified === 1 - }; - } - - // Add user's vote and petition status to each proposal - for (const proposal of proposals.results) { - const voteInfo = voteMap[proposal.id]; - proposal.userVote = voteInfo ? voteInfo.voteType : null; - proposal.userPetitionSigned = voteInfo ? voteInfo.isPetition : false; - proposal.userPetitionVerified = petitionMap[proposal.id] ? petitionMap[proposal.id].verified : false; - } - } catch (voteError) { - // Just log the error but continue - we'll return proposals without vote data - logError(voteError, { - action: 'get_user_votes', - userId, - proposalCount: proposalIds.length - }); - console.log('Warning: Failed to retrieve user votes, continuing without vote data'); - } - } - - return createResponse(proposals.results); - } catch (error) { - return createResponse({ - error: 'Failed to process proposals request', - details: logError(error, { action: 'get_proposals_outer' }) - }, 500); - } -} - -// Get a single proposal by ID -async function getProposalById(request, env) { - try { - const url = new URL(request.url); - const proposalId = url.pathname.split('/api/proposals/')[1]; - const userId = url.searchParams.get('userId'); - - if (!proposalId) { - return createResponse({ error: 'Missing proposal ID' }, 400); - } - - console.log(`Getting proposal with ID: ${proposalId}`); - - let query = ` - SELECT - p.id, p.text, p.timestamp, p.trending, p.meme_url, - u.name as author_name, u.id as author_id, - (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote') as upvotes, - (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'downvote') as downvotes, - (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote' AND is_petition = 1) as petition_signatures, - (SELECT COUNT(DISTINCT pd.user_id) FROM petition_details pd WHERE pd.proposal_id = p.id AND pd.verified = 1) as verified_petitioners - FROM proposals p - JOIN users u ON p.author_id = u.id - WHERE p.id = ? - `; - - try { - const proposal = await env.DB.prepare(query).bind(proposalId).first(); - - if (!proposal) { - return createResponse({ error: 'Proposal not found' }, 404); - } - - // If we have a userId, get their votes for this proposal - if (userId) { - try { - const votesQuery = ` - SELECT proposal_id, vote_type, is_petition - FROM votes - WHERE user_id = ? AND proposal_id = ? - `; - - const vote = await env.DB.prepare(votesQuery).bind(userId, proposalId).first(); - - if (vote) { - proposal.userVote = vote.vote_type; - proposal.userPetitionSigned = vote.is_petition === 1; - } else { - proposal.userVote = null; - proposal.userPetitionSigned = false; - } - - // Also get petition details status - const petitionQuery = ` - SELECT proposal_id, verified - FROM petition_details - WHERE user_id = ? AND proposal_id = ? - `; - - const petition = await env.DB.prepare(petitionQuery).bind(userId, proposalId).first(); - - if (petition) { - proposal.userPetitionVerified = petition.verified === 1; - } else { - proposal.userPetitionVerified = false; - } - } catch (voteError) { - // Just log the error but continue - we'll return the proposal without vote data - logError(voteError, { - action: 'get_user_votes', - userId, - proposalId - }); - console.log('Warning: Failed to retrieve user votes, continuing without vote data'); - } - } - - return createResponse(proposal); - } catch (dbError) { - return createResponse({ - error: 'Failed to retrieve proposal', - details: logError(dbError, { action: 'get_proposal_by_id', proposalId, query }) - }, 500); - } - } catch (error) { - return createResponse({ - error: 'Failed to process proposal request', - details: logError(error, { action: 'get_proposal_by_id_outer' }) - }, 500); - } -} - -async function generateAndStoreShareImage(proposal, env) { - try { - // Get base text for the image - const text = proposal.text || "Unknown petition"; - const authorName = proposal.author_name || "Anonymous"; - - // Create Cloudinary URL for transformation - const cloudinaryUrl = "https://res.cloudinary.com/dh8apmjya/image/fetch"; - - // Encode text for URL parameters - const encodedText = encodeURIComponent(text.substring(0, 80)) - .replace(/'/g, '%27') - .replace(/"/g, '%22'); - - // Create transformation parameters - const params = `b_black,w_1200,h_630,c_fill/l_text:Arial_32_bold:${encodedText},co_white,w_1000,c_fit,g_center`; - - // Template image URL - const templateUrl = encodeURIComponent("https://radical.omar-c29.workers.dev/memes/petition-template.png"); - - // Create a unique identifier for this image - const imageId = `share_${proposal.id}_${Date.now()}`; - - // Final Cloudinary URL - const cloudinaryImageUrl = `${cloudinaryUrl}/${params}/${templateUrl}`; - - console.log(`Generating share image: ${cloudinaryImageUrl}`); - - // Fetch the image from Cloudinary - const imageResponse = await fetch(cloudinaryImageUrl); - - if (!imageResponse.ok) { - throw new Error(`Failed to fetch image: ${imageResponse.status}`); - } - - // Get the image as a blob - const imageBlob = await imageResponse.blob(); - - // Upload the image to your R2 bucket - await env.MEMES_BUCKET.put(imageId, imageBlob, { - httpMetadata: { - contentType: 'image/png', - }, - }); - - // Generate the public URL for the stored image - const shareImageUrl = `https://radical.omar-c29.workers.dev/memes/${imageId}`; - - console.log(`Stored share image at: ${shareImageUrl}`); - - // Update the proposal in the database with the share image URL - await env.DB.prepare(` - UPDATE proposals - SET share_image_url = ? - WHERE id = ? - `).bind(shareImageUrl, proposal.id).run(); - - return shareImageUrl; - } catch (error) { - console.error('Error generating share image:', error); - // Return a fallback image URL in case of failure - return "https://radical.omar-c29.workers.dev/memes/mickeymeta.PNG"; - } -} - -// Create a new proposal -async function createProposal(request, env) { - try { - const data = await request.json(); - const { text, authorId, trending = false } = data; - - console.log(`Creating proposal: ${JSON.stringify(data)}`); - - if (!text || !authorId) { - return createResponse({ error: 'Missing required fields', received: { text, authorId } }, 400); - } - - // Check if user exists first - try { - const userCheck = await env.DB.prepare(`SELECT id, name FROM users WHERE id = ?`).bind(authorId).first(); - if (!userCheck) { - return createResponse({ - error: 'Author does not exist', - authorId, - suggestion: 'Create the user first before posting a proposal' - }, 400); - } - - // Create a temporary proposal object with author name for image generation - const tempProposal = { - id: 'proposal_' + Date.now(), - text: text, - author_name: userCheck.name - }; - - const id = tempProposal.id; - const timestamp = Date.now(); - - // Insert the proposal first - const query = ` - INSERT INTO proposals (id, author_id, text, timestamp, trending) - VALUES (?, ?, ?, ?, ?) - `; - - await env.DB.prepare(query).bind(id, authorId, text, timestamp, trending ? 1 : 0).run(); - - // Generate and store the share image - const shareImageUrl = await generateAndStoreShareImage(tempProposal, env); - - // Update the proposal with the share image URL - if (shareImageUrl) { - await env.DB.prepare(` - UPDATE proposals - SET share_image_url = ? - WHERE id = ? - `).bind(shareImageUrl, id).run(); - } - - console.log(`Successfully created proposal with ID: ${id}`); - - return createResponse({ - id, - authorId, - text, - timestamp, - trending, - shareImageUrl - }); - } catch (dbError) { - return createResponse({ - error: 'Failed to create proposal in database', - details: logError(dbError, { - action: 'insert_proposal', - proposal: { id, authorId, timestamp, trending } - }) - }, 500); - } - } catch (error) { - return createResponse({ - error: 'Failed to process proposal creation', - details: logError(error, { action: 'create_proposal_outer' }) - }, 500); - } -} -// Update a proposal (e.g., mark as trending) -async function updateProposal(id, request, env) { - try { - const data = await request.json(); - const { trending } = data; - - console.log(`Updating proposal ${id}: ${JSON.stringify(data)}`); - - if (trending === undefined) { - return createResponse({ error: 'No fields to update', received: data }, 400); - } - - // Check if proposal exists - try { - const proposalCheck = await env.DB.prepare(`SELECT id FROM proposals WHERE id = ?`).bind(id).first(); - if (!proposalCheck) { - return createResponse({ error: 'Proposal does not exist', id }, 404); - } - } catch (checkError) { - return createResponse({ - error: 'Error checking proposal existence', - details: logError(checkError, { action: 'check_proposal', id }) - }, 500); - } - - const query = ` - UPDATE proposals - SET trending = ? - WHERE id = ? - `; - - try { - await env.DB.prepare(query).bind(trending ? 1 : 0, id).run(); - console.log(`Successfully updated proposal ${id} trending status to ${trending}`); - - return createResponse({ id, trending }); - } catch (dbError) { - return createResponse({ - error: 'Failed to update proposal', - details: logError(dbError, { action: 'update_proposal', id, trending }) - }, 500); - } - } catch (error) { - return createResponse({ - error: 'Failed to process proposal update', - details: logError(error, { action: 'update_proposal_outer', id }) - }, 500); - } -} - - - // to serve audio files from R2 - - async function serveMedia(request, env) { - const url = new URL(request.url); - const path = url.pathname; - - // Determine if this is a meme or audio file request - let key; - if (path.startsWith('/memes/')) { - key = path.replace('/memes/', ''); - } else if (path.startsWith('/audio/')) { - key = path.replace('/audio/', ''); - } else { - return new Response('Not found', { status: 404 }); - } - - // Handle CORS preflight requests - if (request.method === 'OPTIONS') { - return handleOptions(); - } - - try { - const object = await env.MEMES_BUCKET.get(key); - - if (object === null) { - return new Response('File not found', { status: 404 }); - } - - const headers = new Headers(); - object.writeHttpMetadata(headers); - headers.set('etag', object.httpEtag); - headers.set('Cache-Control', 'public, max-age=31536000'); // Cache for a year - - // Set the appropriate content-type based on file extension - if (key.endsWith('.mp3')) { - headers.set('Content-Type', 'audio/mpeg'); - } else if (key.endsWith('.wav')) { - headers.set('Content-Type', 'audio/wav'); - } else if (key.endsWith('.ogg')) { - headers.set('Content-Type', 'audio/ogg'); - } - - // Add CORS headers - Object.keys(corsHeaders).forEach(key => { - headers.set(key, corsHeaders[key]); - }); - - return new Response(object.body, { - headers - }); - } catch (error) { - console.error('Error serving media:', error); - return new Response('Internal Server Error', { status: 500 }); - } - } - - - -// Create or update a vote and handle petition data -async function createOrUpdateVote(request, env) { - try { - const data = await request.json(); - const { proposalId, userId, voteType, isPetition = false, petitionDetails = null } = data; - - console.log(`Processing vote/petition: ${JSON.stringify({ - proposalId, - userId, - voteType, - isPetition, - hasDetails: petitionDetails !== null - })}`); - - if (!proposalId || !userId || !voteType) { - return createResponse({ - error: 'Missing required fields', - received: { proposalId, userId, voteType: voteType || 'undefined' } - }, 400); - } - - if (voteType !== 'upvote' && voteType !== 'downvote') { - return createResponse({ - error: 'Invalid vote type', - voteType, - allowedValues: ['upvote', 'downvote'] - }, 400); - } - - // Check if entities exist - try { - const userCheck = await env.DB.prepare(`SELECT id FROM users WHERE id = ?`).bind(userId).first(); - if (!userCheck) { - return createResponse({ error: 'User does not exist', userId }, 404); - } - - const proposalCheck = await env.DB.prepare(`SELECT id FROM proposals WHERE id = ?`).bind(proposalId).first(); - if (!proposalCheck) { - return createResponse({ error: 'Proposal does not exist', proposalId }, 404); - } - } catch (checkError) { - return createResponse({ - error: 'Error checking user or proposal existence', - details: logError(checkError, { action: 'check_entities', userId, proposalId }) - }, 500); - } - - // Check if vote already exists - let existingVote; - try { - const checkQuery = ` - SELECT id, vote_type, is_petition FROM votes - WHERE proposal_id = ? AND user_id = ? - `; - - existingVote = await env.DB.prepare(checkQuery).bind(proposalId, userId).first(); - console.log(`Existing vote check: ${existingVote ? JSON.stringify(existingVote) : 'None found'}`); - } catch (voteCheckError) { - return createResponse({ - error: 'Error checking existing vote', - details: logError(voteCheckError, { action: 'check_vote', userId, proposalId }) - }, 500); - } - - const timestamp = Date.now(); - - try { - // Process vote - if (existingVote) { - // If vote exists and it's the same type, remove it (toggle off) - if (existingVote.vote_type === voteType) { - await env.DB.prepare(` - DELETE FROM votes - WHERE id = ? - `).bind(existingVote.id).run(); - - console.log(`Deleted vote ${existingVote.id} (toggle off)`); - - return createResponse({ proposalId, userId, voteType: null }); - } else { - // If vote exists but is different type, update it - await env.DB.prepare(` - UPDATE votes - SET vote_type = ?, timestamp = ?, is_petition = ? - WHERE id = ? - `).bind( - voteType, - timestamp, - voteType === 'upvote' && isPetition ? 1 : 0, - existingVote.id - ).run(); - - console.log(`Updated vote ${existingVote.id} to ${voteType}`); - } - } else { - // If no vote exists, create new one - const result = await env.DB.prepare(` - INSERT INTO votes (proposal_id, user_id, vote_type, timestamp, is_petition) - VALUES (?, ?, ?, ?, ?) - `).bind( - proposalId, - userId, - voteType, - timestamp, - voteType === 'upvote' && isPetition ? 1 : 0 - ).run(); - - console.log(`Created new vote with ID: ${result.meta?.last_row_id || 'unknown'}`); - } - - // Process petition details if provided - if (voteType === 'upvote' && isPetition && petitionDetails) { - try { - // Validate petition details - const { fullName, address, postcode, dob, email } = petitionDetails; - - if (!fullName || !address || !postcode || !dob || !email) { - return createResponse({ error: 'Missing required petition details' }, 400); - } - - // Check if petition details already exist - const petitionCheck = await env.DB.prepare(` - SELECT id FROM petition_details - WHERE user_id = ? AND proposal_id = ? - `).bind(userId, proposalId).first(); - - if (!petitionCheck) { - // Store petition details - await env.DB.prepare(` - INSERT INTO petition_details - (user_id, proposal_id, full_name, address, postcode, dob, email, timestamp, verified) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - `).bind( - userId, - proposalId, - fullName, - address, - postcode, - dob, - email, - timestamp, - 1 // Assuming verification was successful - ).run(); - - console.log(`Stored petition details for user ${userId} on proposal ${proposalId}`); - } else { - // Update existing petition details - await env.DB.prepare(` - UPDATE petition_details - SET full_name = ?, address = ?, postcode = ?, dob = ?, email = ?, timestamp = ?, verified = ? - WHERE user_id = ? AND proposal_id = ? - `).bind( - fullName, - address, - postcode, - dob, - email, - timestamp, - 1, // Assuming verification was successful - userId, - proposalId - ).run(); - - console.log(`Updated petition details for user ${userId} on proposal ${proposalId}`); - } - } catch (petitionError) { - return createResponse({ - error: 'Failed to process petition details', - details: logError(petitionError, { action: 'store_petition', userId, proposalId }) - }, 500); - } - } - - return createResponse({ - proposalId, - userId, - voteType, - isPetition: voteType === 'upvote' && isPetition, - petitionVerified: voteType === 'upvote' && isPetition - }); - } catch (dbError) { - return createResponse({ - error: 'Failed to process vote/petition in database', - details: logError(dbError, { - action: 'process_vote_petition', - existingVote: existingVote || null, - newVote: { proposalId, userId, voteType, isPetition } - }) - }, 500); - } - } catch (error) { - return createResponse({ - error: 'Failed to process vote/petition request', - details: logError(error, { action: 'vote_petition_outer' }) - }, 500); - } -} - -// 3. Update the getComments function to include vote counts -async function getComments(request, env) { - try { - const url = new URL(request.url); - const proposalId = url.searchParams.get('proposalId'); - const userId = url.searchParams.get('userId'); - - if (!proposalId) { - return createResponse({ error: 'Missing proposalId parameter' }, 400); - } - - const query = ` - SELECT - c.id, c.user_id, c.comment_text, c.timestamp, - u.name as user_name, - (SELECT COUNT(*) FROM comment_votes WHERE comment_id = c.id AND vote_type = 'upvote') as upvotes, - (SELECT COUNT(*) FROM comment_votes WHERE comment_id = c.id AND vote_type = 'downvote') as downvotes - FROM comments c - JOIN users u ON c.user_id = u.id - WHERE c.proposal_id = ? - ORDER BY c.timestamp DESC - `; - - try { - const comments = await env.DB.prepare(query).bind(proposalId).all(); - console.log(`Retrieved ${comments.results.length} comments for proposal ${proposalId}`); - - // If userId is provided, get the user's votes for these comments - if (userId && comments.results.length > 0) { - const commentIds = comments.results.map(c => c.id); - - // Build placeholders for the SQL query - let placeholders = commentIds.map(() => '?').join(','); - - const votesQuery = ` - SELECT comment_id, vote_type - FROM comment_votes - WHERE user_id = ? AND comment_id IN (${placeholders}) - `; - - // Prepare bind parameters - const bindParams = [userId, ...commentIds]; - - const votes = await env.DB.prepare(votesQuery).bind(...bindParams).all(); - - // Create a map of commentId to vote type - const voteMap = {}; - votes.results.forEach(vote => { - voteMap[vote.comment_id] = vote.vote_type; - }); - - // Add user's vote to each comment - comments.results.forEach(comment => { - comment.userVote = voteMap[comment.id] || null; - }); - } - - return createResponse(comments.results); - } catch (dbError) { - return createResponse({ - error: 'Failed to retrieve comments', - details: logError(dbError, { action: 'get_comments', proposalId }) - }, 500); - } - } catch (error) { - return createResponse({ - error: 'Failed to process comments request', - details: logError(error, { action: 'get_comments_outer' }) - }, 500); - } -} - -// Function to create a new comment -async function createComment(request, env) { - try { - const data = await request.json(); - const { proposalId, userId, commentText } = data; - - console.log(`Creating comment: ${JSON.stringify(data)}`); - - if (!proposalId || !userId || !commentText) { - return createResponse({ - error: 'Missing required fields', - received: { proposalId, userId, commentText: commentText ? '[present]' : '[missing]' } - }, 400); - } - - // Check if proposal exists - try { - const proposalCheck = await env.DB.prepare( - `SELECT id FROM proposals WHERE id = ?` - ).bind(proposalId).first(); - - if (!proposalCheck) { - return createResponse({ error: 'Proposal does not exist', proposalId }, 404); - } - } catch (checkError) { - return createResponse({ - error: 'Error checking proposal existence', - details: logError(checkError, { action: 'check_proposal', proposalId }) - }, 500); - } - - // Check if user exists - try { - const userCheck = await env.DB.prepare( - `SELECT id FROM users WHERE id = ?` - ).bind(userId).first(); - - if (!userCheck) { - return createResponse({ error: 'User does not exist', userId }, 404); - } - } catch (userError) { - return createResponse({ - error: 'Error checking user existence', - details: logError(userError, { action: 'check_user', userId }) - }, 500); - } - - const id = 'comment_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5); - const timestamp = Date.now(); - - try { - await env.DB.prepare(` - INSERT INTO comments (id, proposal_id, user_id, comment_text, timestamp) - VALUES (?, ?, ?, ?, ?) - `).bind(id, proposalId, userId, commentText, timestamp).run(); - - console.log(`Successfully created comment with ID: ${id}`); - - // Get user name for response - const userQuery = await env.DB.prepare( - `SELECT name FROM users WHERE id = ?` - ).bind(userId).first(); - - return createResponse({ - id, - proposalId, - userId, - userName: userQuery.name, - commentText, - timestamp - }); - } catch (dbError) { - return createResponse({ - error: 'Failed to create comment in database', - details: logError(dbError, { - action: 'insert_comment', - comment: { id, proposalId, userId, timestamp } - }) - }, 500); - } - } catch (error) { - return createResponse({ - error: 'Failed to process comment creation', - details: logError(error, { action: 'create_comment_outer' }) - }, 500); - } -} - -// Create or get a user -async function createOrGetUser(request, env) { - try { - const data = await request.json(); - const { id, name } = data; - - console.log(`Processing user request: ${JSON.stringify(data)}`); - - if (!id || !name) { - return createResponse({ - error: 'Missing required fields', - received: { id: id || 'undefined', name: name || 'undefined' } - }, 400); - } - - // Check if user exists - let existingUser; - try { - const checkQuery = ` - SELECT id, name FROM users - WHERE id = ? - `; - - existingUser = await env.DB.prepare(checkQuery).bind(id).first(); - console.log(`Existing user check: ${existingUser ? JSON.stringify(existingUser) : 'None found'}`); - } catch (userCheckError) { - return createResponse({ - error: 'Error checking user existence', - details: logError(userCheckError, { action: 'check_user', id }) - }, 500); - } - - if (!existingUser) { - try { - // Create new user if doesn't exist - const timestamp = Date.now(); - const insertQuery = ` - INSERT INTO users (id, name, created_at) - VALUES (?, ?, ?) - `; - await env.DB.prepare(insertQuery).bind(id, name, timestamp).run(); - console.log(`Created new user with ID: ${id}`); - } catch (insertError) { - return createResponse({ - error: 'Failed to create user', - details: logError(insertError, { action: 'insert_user', user: { id, name } }) - }, 500); - } - } else { - console.log(`Found existing user: ${existingUser.id}`); - } - - return createResponse({ id, name }); - } catch (error) { - return createResponse({ - error: 'Failed to process user request', - details: logError(error, { action: 'user_outer' }) - }, 500); - } - -} \ No newline at end of file diff --git a/index.html b/index.html index e9bf7d6..4540872 100644 --- a/index.html +++ b/index.html @@ -596,7 +596,7 @@ async function loadUserData() { }, 3000); } - // Improved fetch implementation + // Improved safeFetch implementation with Cloudflare error handling async function safeFetch(url, options = {}) { try { // Ensure URL is a string @@ -607,7 +607,6 @@ async function safeFetch(url, options = {}) { method: 'GET', headers: { 'Content-Type': 'application/json', - // Add any additional default headers }, mode: 'cors', credentials: 'same-origin' @@ -642,14 +641,26 @@ async function safeFetch(url, options = {}) { // Check for successful response if (!response.ok) { - const errorBody = await response.text(); - console.error('Fetch Error Response:', { - status: response.status, - statusText: response.statusText, - body: errorBody - }); + const errorText = await response.text(); - throw new Error(`HTTP error! status: ${response.status}, message: ${errorBody}`); + // Check if this is a Cloudflare error + if (errorText.includes('Cloudflare') || errorText.includes('cf-error')) { + console.error('Cloudflare Error:', { + status: response.status, + statusText: response.statusText, + body: errorText + }); + + // Return a special error object for Cloudflare errors + return { + error: true, + cloudflareError: true, + status: response.status, + message: 'The server is temporarily unavailable. Please try again later.' + }; + } + + throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`); } return await response.json(); @@ -671,7 +682,7 @@ async function safeFetch(url, options = {}) { - // Fetch proposals from API + // Updated fetchProposals with fallback mechanism async function fetchProposals() { try { // Show loading indicator @@ -680,14 +691,38 @@ async function safeFetch(url, options = {}) { const sortSelect = document.getElementById('sort-select'); const sortBy = sortSelect ? sortSelect.value : 'newest'; - const data = await safeFetch( - `${API_BASE_URL}/proposals?sortBy=${sortBy}&userId=${currentUser.id}` - ); + // Log the full request URL and parameters + const requestUrl = `${API_BASE_URL}/proposals?sortBy=${sortBy}&userId=${currentUser.id}`; + console.log('Fetching proposals from:', requestUrl); + console.log('Current user:', currentUser); + + const response = await fetch(requestUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + mode: 'cors', + credentials: 'same-origin' + }); + + // Log the response details + console.log('Response status:', response.status); + console.log('Response headers:', Object.fromEntries(response.headers.entries())); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Error response:', errorText); + throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`); + } + + const data = await response.json(); + console.log('Proposals data:', data); renderProposals(data); } catch (error) { console.error('Error fetching proposals:', error); - showErrorMessage('Oops! Failed to load petitions. Try refreshing the page.'); + showErrorMessage('Failed to load petitions. Please check the console for details.', true); } } @@ -2566,6 +2601,90 @@ function submitComment(proposalId, commentText) { }); } +// Add these new functions for comment management +const commentRequestQueue = new Set(); +let commentRequestTimeout = null; +const MAX_BATCH_SIZE = 10; +const COMMENT_REQUEST_DELAY = 1000; // 1 second delay between batches + +async function batchLoadComments() { + if (commentRequestQueue.size === 0) return; + + const batch = Array.from(commentRequestQueue).slice(0, MAX_BATCH_SIZE); + commentRequestQueue.clear(); + + try { + const responses = await Promise.all( + batch.map(proposalId => + fetch(`${API_BASE_URL}/comments?proposalId=${proposalId}&userId=${currentUser.id}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + mode: 'cors', + credentials: 'same-origin' + }).then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json().then(data => ({ proposalId, data })); + }) + ) + ); + + responses.forEach(({ proposalId, data }) => { + const commentsContainer = document.querySelector(`.comments-container[data-proposal-id="${proposalId}"]`); + if (commentsContainer) { + renderComments(commentsContainer.querySelector('.comments-content'), data); + } + }); + } catch (error) { + console.error('Error loading comments batch:', error); + // Retry failed requests + batch.forEach(proposalId => commentRequestQueue.add(proposalId)); + } + + // Schedule next batch if there are more requests + if (commentRequestQueue.size > 0) { + commentRequestTimeout = setTimeout(batchLoadComments, COMMENT_REQUEST_DELAY); + } +} + +function queueCommentLoad(proposalId) { + commentRequestQueue.add(proposalId); + + if (!commentRequestTimeout) { + commentRequestTimeout = setTimeout(batchLoadComments, COMMENT_REQUEST_DELAY); + } +} + +// Update the loadComments function to use the queue +function loadComments(proposalId) { + const commentsContainer = document.querySelector(`.comments-container[data-proposal-id="${proposalId}"]`); + const commentsContent = commentsContainer?.querySelector('.comments-content'); + + if (!commentsContent) return; + + // Show loading state + commentsContent.innerHTML = `
Loading comments...
`; + + // Queue the comment load + queueCommentLoad(proposalId); +} + +// Update the loadModalComments function to use the queue +async function loadModalComments(proposalId) { + const commentsContent = document.getElementById(`modal-comments-content-${proposalId}`); + if (!commentsContent) return; + + // Show loading state + commentsContent.innerHTML = `
Loading comments...
`; + + // Queue the comment load + queueCommentLoad(proposalId); +} +
hit me
diff --git a/worker.js b/worker.js index 292f73a..fd79e50 100644 --- a/worker.js +++ b/worker.js @@ -3,6 +3,7 @@ const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + 'Access-Control-Max-Age': '86400' }; // Handle OPTIONS request for CORS preflight @@ -1109,137 +1110,31 @@ async function healthCheck(env) { async function getProposals(request, env) { try { const url = new URL(request.url); - const sortBy = url.searchParams.get('sortBy') || 'newest'; + const page = parseInt(url.searchParams.get('page')) || 1; + const limit = Math.min( + parseInt(url.searchParams.get('limit')) || DEFAULT_PAGE_SIZE, + MAX_PAGE_SIZE + ); + const offset = (page - 1) * limit; - // Log request details - console.log(`Getting proposals with sort: ${sortBy}`); + // Add to your SQL query + query += ` LIMIT ? OFFSET ?`; - let query = ` - SELECT - p.id, p.text, p.timestamp, p.trending, p.meme_url, - u.name as author_name, u.id as author_id, - (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote') as upvotes, - (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'downvote') as downvotes, - (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote' AND is_petition = 1) as petition_signatures, - (SELECT COUNT(DISTINCT pd.user_id) FROM petition_details pd WHERE pd.proposal_id = p.id AND pd.verified = 1) as verified_petitioners - FROM proposals p - JOIN users u ON p.author_id = u.id - `; - // Add sorting - if (sortBy === 'newest') { - query += ' ORDER BY p.timestamp DESC'; - } else if (sortBy === 'popular') { - query += ` - ORDER BY ( - (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote') - - (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'downvote') - ) DESC - `; - } else if (sortBy === 'controversial') { - query += ` - ORDER BY ( - (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote') + - (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'downvote') - ) DESC - `; - } else if (sortBy === 'petitions') { - // New sorting option for most petitioned - query += ` - ORDER BY ( - (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote' AND is_petition = 1) - ) DESC - `; - } + // Add to your bind parameters + const proposals = await env.DB.prepare(query).bind(limit, offset).all(); - console.log(`Executing query: ${query}`); - - // Get user ID for checking existing votes - const userId = url.searchParams.get('userId'); - let proposals; - - try { - proposals = await env.DB.prepare(query).all(); - console.log(`Retrieved ${proposals.results.length} proposals`); - } catch (dbError) { - return createResponse({ - error: 'Failed to retrieve proposals', - details: logError(dbError, { action: 'get_proposals', query }) - }, 500); - } - - // If we have a userId, get their votes for these proposals - if (userId && proposals.results.length > 0) { - const proposalIds = proposals.results.map(p => p.id); - - try { - // Use parameter binding with placeholders - let placeholderStr = ''; - for (let i = 0; i < proposalIds.length; i++) { - placeholderStr += (i === 0 ? '?' : ',?'); - } - - const votesQuery = ` - SELECT proposal_id, vote_type, is_petition - FROM votes - WHERE user_id = ? AND proposal_id IN (${placeholderStr}) - `; - - console.log(`Executing votes query for user ${userId} with ${proposalIds.length} proposals`); - - const bindParams = [userId, ...proposalIds]; - const votes = await env.DB.prepare(votesQuery).bind(...bindParams).all(); - - console.log(`Retrieved ${votes.results.length} votes for user ${userId}`); - - // Create a map of proposal_id to vote info - const voteMap = {}; - for (const vote of votes.results) { - voteMap[vote.proposal_id] = { - voteType: vote.vote_type, - isPetition: vote.is_petition === 1 - }; - } - - // Also get petition details status - const petitionQuery = ` - SELECT proposal_id, verified - FROM petition_details - WHERE user_id = ? AND proposal_id IN (${placeholderStr}) - `; - - const petitionDetails = await env.DB.prepare(petitionQuery).bind(...bindParams).all(); - const petitionMap = {}; - - for (const petition of petitionDetails.results) { - petitionMap[petition.proposal_id] = { - verified: petition.verified === 1 - }; - } - - // Add user's vote and petition status to each proposal - for (const proposal of proposals.results) { - const voteInfo = voteMap[proposal.id]; - proposal.userVote = voteInfo ? voteInfo.voteType : null; - proposal.userPetitionSigned = voteInfo ? voteInfo.isPetition : false; - proposal.userPetitionVerified = petitionMap[proposal.id] ? petitionMap[proposal.id].verified : false; - } - } catch (voteError) { - // Just log the error but continue - we'll return proposals without vote data - logError(voteError, { - action: 'get_user_votes', - userId, - proposalCount: proposalIds.length - }); - console.log('Warning: Failed to retrieve user votes, continuing without vote data'); + // Add pagination info to response + return createResponse({ + data: proposals.results, + pagination: { + page, + limit, + total: proposals.results.length, + hasMore: proposals.results.length === limit } - } - - return createResponse(proposals.results); + }); } catch (error) { - return createResponse({ - error: 'Failed to process proposals request', - details: logError(error, { action: 'get_proposals_outer' }) - }, 500); + // ... error handling ... } } @@ -2375,4 +2270,39 @@ async function serveCustomizedHtml(proposalId, request, env) { // Fall back to regular page if something goes wrong return await fetch(request); } -} \ No newline at end of file +} + +function getCacheControl(path) { + const cacheRules = { + '/api/health': 'public, max-age=3600', + '/api/proposals': 'public, max-age=300, stale-while-revalidate=60', + '/api/comments': 'public, max-age=60, stale-while-revalidate=30', + default: 'no-cache' + }; + + return cacheRules[path] || cacheRules.default; +} + +// In your fetch handler +if (request.method === "GET") { + const cacheControl = getCacheControl(path); + response.headers.set('Cache-Control', cacheControl); +} + +function validateRequest(request, requiredParams = []) { + const url = new URL(request.url); + const missingParams = requiredParams.filter(param => !url.searchParams.has(param)); + + if (missingParams.length > 0) { + return createResponse({ + error: 'Missing required parameters', + missing: missingParams + }, 400); + } + + return null; +} + +// In your endpoint handlers +const validationError = validateRequest(request, ['userId']); +if (validationError) return validationError; \ No newline at end of file