import { NextRequest, NextResponse } from 'next/server'; import { createClient } from '@/lib/supabase/server'; // ─── Server-side URL fetch proxy (bypasses CORS) ────────────────────────────── export async function POST(request: NextRequest) { try { const supabase = await createClient(); const { data: { user } } = await supabase.auth.getUser(); if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); const { data: profile } = await supabase.from('user_profiles').select('role').eq('id', user.id).single(); if (!['administrator', 'admin'].includes(profile?.role || '')) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } const { url } = await request.json(); if (!url || typeof url !== 'string') { return NextResponse.json({ error: 'url required' }, { status: 400 }); } // Validate URL let parsed: URL; try { parsed = new URL(url); } catch { return NextResponse.json({ error: 'Invalid URL' }, { status: 400 }); } // Only allow http/https if (!['http:', 'https:'].includes(parsed.protocol)) { return NextResponse.json({ error: 'Only http/https URLs allowed' }, { status: 400 }); } const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 15_000); let response: Response; try { response = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; AV Beat/1.0; +https://avbeat.com)', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', }, signal: controller.signal, }); } finally { clearTimeout(timer); } if (!response.ok) { return NextResponse.json({ error: `Fetch failed: HTTP ${response.status}` }, { status: 400 }); } const contentType = response.headers.get('content-type') || ''; if (!contentType.includes('text/html') && !contentType.includes('text/plain') && !contentType.includes('application/xhtml')) { return NextResponse.json({ error: 'URL does not return readable content' }, { status: 400 }); } const html = await response.text(); // Strip HTML tags and extract readable text const text = html .replace(//gi, '') .replace(//gi, '') .replace(//gi, '') .replace(//gi, '') .replace(//gi, '') .replace(//gi, '') .replace(/<[^>]+>/g, ' ') .replace(/ /g, ' ') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, "'") .replace(/\s{2,}/g, ' ') .trim(); // Limit to ~8000 chars to fit in context window const truncated = text.slice(0, 8000); const wordCount = truncated.split(/\s+/).filter(Boolean).length; return NextResponse.json({ content: truncated, wordCount, domain: parsed.hostname, url, }); } catch (err: any) { return NextResponse.json({ error: err.message }, { status: 500 }); } }