initial commit: rocket.new export of broadcastbeat

This commit is contained in:
Ryan Salazar
2026-05-07 16:39:17 +00:00
commit 70aa1ad46e
302 changed files with 60710 additions and 0 deletions

View 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(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/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 });
}
}