initial commit: rocket.new export of broadcastbeat
This commit is contained in:
92
src/app/api/ai/fetch-url/route.ts
Normal file
92
src/app/api/ai/fetch-url/route.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
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; BroadcastBeat/1.0; +https://broadcastbeat.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(/<script[\s\S]*?<\/script>/gi, '')
|
||||
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
||||
.replace(/<nav[\s\S]*?<\/nav>/gi, '')
|
||||
.replace(/<header[\s\S]*?<\/header>/gi, '')
|
||||
.replace(/<footer[\s\S]*?<\/footer>/gi, '')
|
||||
.replace(/<aside[\s\S]*?<\/aside>/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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user