import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' import { getAllowedOrigin, getCorsHeaders } from '../_shared/cors.ts' import { rateLimiters, withRateLimit } from '../_shared/rateLimiter.ts' import { createEdgeFunction } from '../_shared/edgeFunctionWrapper.ts' import { validateString } from '../_shared/typeValidation.ts' import { addSpanEvent } from '../_shared/logger.ts' // Helper to create authenticated Supabase client const createAuthenticatedSupabaseClient = (authHeader: string) => { const supabaseUrl = Deno.env.get('SUPABASE_URL') const supabaseAnonKey = Deno.env.get('SUPABASE_ANON_KEY') if (!supabaseUrl || !supabaseAnonKey) { throw new Error('Missing Supabase environment variables') } return createClient(supabaseUrl, supabaseAnonKey, { global: { headers: { Authorization: authHeader } } }) } /** * Report ban evasion attempts to system alerts */ async function reportBanEvasionToAlerts( supabaseClient: any, userId: string, action: string, requestId: string ): Promise { try { await supabaseClient.rpc('create_system_alert', { p_alert_type: 'ban_attempt', p_severity: 'high', p_message: `Banned user attempted image upload: ${action}`, p_metadata: { user_id: userId, action, request_id: requestId, timestamp: new Date().toISOString() } }); } catch (error) { // Non-blocking - log but don't fail the response } } // Apply strict rate limiting (5 requests/minute) to prevent abuse const handler = createEdgeFunction( { name: 'upload-image', requireAuth: false, // Auth checked per-method corsHeaders: {} // Dynamic CORS }, async (req, context) => { const requestOrigin = req.headers.get('origin'); const allowedOrigin = getAllowedOrigin(requestOrigin); // Check if this is a CORS request with a disallowed origin if (requestOrigin && !allowedOrigin) { addSpanEvent(context.span, 'cors_rejected', { origin: requestOrigin }); return new Response( JSON.stringify({ error: 'Origin not allowed', message: 'The origin of this request is not allowed to access this resource' }), { status: 403, headers: { 'Content-Type': 'application/json' } } ); } const corsHeaders = getCorsHeaders(allowedOrigin); context.span.setAttribute('http_method', req.method); context.span.setAttribute('action', 'upload_image'); const CLOUDFLARE_ACCOUNT_ID = Deno.env.get('CLOUDFLARE_ACCOUNT_ID') const CLOUDFLARE_IMAGES_API_TOKEN = Deno.env.get('CLOUDFLARE_IMAGES_API_TOKEN') if (!CLOUDFLARE_ACCOUNT_ID || !CLOUDFLARE_IMAGES_API_TOKEN) { throw new Error('Missing Cloudflare credentials') } if (req.method === 'DELETE') { // Require authentication for DELETE operations const authHeader = req.headers.get('Authorization') if (!authHeader) { return new Response( JSON.stringify({ error: 'Authentication required', message: 'Authentication required for delete operations' }), { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } const supabase = createAuthenticatedSupabaseClient(authHeader) const { data: { user }, error: authError } = await supabase.auth.getUser() if (authError || !user) { throw new Error('Invalid authentication'); } context.span.setAttribute('user_id', user.id); // Check if user is banned const { data: profile } = await supabase .from('profiles') .select('banned') .eq('user_id', user.id) .single() if (profile?.banned) { await reportBanEvasionToAlerts(supabase, user.id, 'image_delete', context.requestId); addSpanEvent(context.span, 'banned_user_blocked', { action: 'delete' }); return new Response( JSON.stringify({ error: 'Account suspended', message: 'Account suspended. Contact support for assistance.' }), { status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } // Delete image from Cloudflare const requestBody = await req.json(); const { imageId } = requestBody; validateString(imageId, 'imageId', { userId: user.id, requestId: context.requestId }); // Validate imageId format const validImageIdPattern = /^[a-zA-Z0-9_-]{1,100}$/; if (!validImageIdPattern.test(imageId)) { throw new Error('Invalid imageId format - must be alphanumeric with optional hyphens/underscores (max 100 chars)'); } addSpanEvent(context.span, 'delete_image_start', { imageId }); const deleteResponse = await fetch( `https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/images/v1/${imageId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${CLOUDFLARE_IMAGES_API_TOKEN}`, }, } ) const deleteResult = await deleteResponse.json() if (!deleteResponse.ok) { throw new Error(deleteResult.errors?.[0]?.message || deleteResult.error || 'Failed to delete image'); } addSpanEvent(context.span, 'image_deleted', { imageId }); return new Response( JSON.stringify({ success: true, deleted: true }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } if (req.method === 'POST') { // Require authentication for POST operations const authHeader = req.headers.get('Authorization') if (!authHeader) { return new Response( JSON.stringify({ error: 'Authentication required', message: 'Authentication required for upload operations' }), { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } const supabase = createAuthenticatedSupabaseClient(authHeader) const { data: { user }, error: authError } = await supabase.auth.getUser() if (authError || !user) { throw new Error('Invalid authentication'); } context.span.setAttribute('user_id', user.id); // Check if user is banned const { data: profile } = await supabase .from('profiles') .select('banned') .eq('user_id', user.id) .single() if (profile?.banned) { await reportBanEvasionToAlerts(supabase, user.id, 'image_upload', context.requestId); addSpanEvent(context.span, 'banned_user_blocked', { action: 'upload' }); return new Response( JSON.stringify({ error: 'Account suspended', message: 'Account suspended. Contact support for assistance.' }), { status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } // Request a direct upload URL from Cloudflare let requestBody; try { requestBody = await req.json(); } catch (error: unknown) { requestBody = {}; } const { metadata = {}, variant = 'public', requireSignedURLs = false } = requestBody; addSpanEvent(context.span, 'upload_url_request_start'); // Create FormData for the request const formData = new FormData() formData.append('requireSignedURLs', requireSignedURLs.toString()) if (metadata && Object.keys(metadata).length > 0) { formData.append('metadata', JSON.stringify(metadata)) } const directUploadResponse = await fetch( `https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/images/v2/direct_upload`, { method: 'POST', headers: { 'Authorization': `Bearer ${CLOUDFLARE_IMAGES_API_TOKEN}`, }, body: formData, } ) const directUploadResult = await directUploadResponse.json() if (!directUploadResponse.ok) { throw new Error(directUploadResult.errors?.[0]?.message || directUploadResult.error || 'Unable to create upload URL'); } addSpanEvent(context.span, 'upload_url_created', { imageId: directUploadResult.result.id }); return new Response( JSON.stringify({ success: true, uploadURL: directUploadResult.result.uploadURL, id: directUploadResult.result.id, }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } if (req.method === 'GET') { // Require authentication for GET operations const authHeader = req.headers.get('Authorization') if (!authHeader) { return new Response( JSON.stringify({ error: 'Authentication required', message: 'Authentication required for image status operations' }), { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } const supabase = createAuthenticatedSupabaseClient(authHeader) const { data: { user }, error: authError } = await supabase.auth.getUser() if (authError || !user) { throw new Error('Invalid authentication'); } context.span.setAttribute('user_id', user.id); // Check image status endpoint const url = new URL(req.url) const imageId = url.searchParams.get('id') validateString(imageId, 'id', { userId: user.id, requestId: context.requestId }); addSpanEvent(context.span, 'get_image_status_start', { imageId }); const imageResponse = await fetch( `https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/images/v1/${imageId}`, { headers: { 'Authorization': `Bearer ${CLOUDFLARE_IMAGES_API_TOKEN}`, }, } ) const imageResult = await imageResponse.json() if (!imageResponse.ok) { throw new Error(imageResult.errors?.[0]?.message || imageResult.error || 'Unable to retrieve image information'); } // Return the image details with convenient URLs const result = imageResult.result const baseUrl = `https://cdn.thrillwiki.com/images/${result.id}` addSpanEvent(context.span, 'image_status_retrieved', { imageId: result.id }); return new Response( JSON.stringify({ success: true, id: result.id, uploaded: result.uploaded, variants: result.variants, draft: result.draft, urls: result.uploaded ? { public: `${baseUrl}/public`, thumbnail: `${baseUrl}/thumbnail`, medium: `${baseUrl}/medium`, large: `${baseUrl}/large`, avatar: `${baseUrl}/avatar`, } : null }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } return new Response( JSON.stringify({ error: 'Method not allowed', message: 'HTTP method not supported for this endpoint' }), { status: 405, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } ); export default withRateLimit(handler, rateLimiters.strict, {} as any);