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,148 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
export async function GET(request: NextRequest) {
try {
const supabase = await createClient();
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { data: profile } = await supabase
.from('user_profiles')
.select('role, is_active')
.eq('id', user.id)
.single();
if (!profile?.is_active) return NextResponse.json({ error: 'Account is inactive' }, { status: 403 });
const allowedRoles = ['contributor', 'author', 'editor', 'administrator', 'admin'];
if (!allowedRoles.includes(profile.role)) return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 });
const { data: articles, error } = await supabase
.from('native_articles')
.select('id, title, status, category, site_id, created_at, updated_at, scheduled_at, published_at, submitted_for_review_at, scope_flagged, editor_note')
.eq('author_id', user.id)
.order('created_at', { ascending: false });
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ articles: articles || [] });
} catch (error) {
console.error('Contributor posts GET error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
try {
const supabase = await createClient();
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { data: profile } = await supabase
.from('user_profiles')
.select('role, is_active, full_name')
.eq('id', user.id)
.single();
if (!profile?.is_active) return NextResponse.json({ error: 'Account is inactive' }, { status: 403 });
const allowedRoles = ['contributor', 'author', 'editor', 'administrator', 'admin'];
if (!allowedRoles.includes(profile.role)) return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 });
const body = await request.json();
const { title, content, excerpt, category, tags, featured_image, featured_image_alt, scheduled_at, site_id = 1, scope_flagged = false } = body;
if (!title) return NextResponse.json({ error: 'Title is required' }, { status: 400 });
// Enforce Featured category lock for contributors and authors
let safeCategory = category;
if (['contributor', 'author'].includes(profile.role) && category?.toLowerCase() === 'featured') {
safeCategory = 'Uncategorized';
}
// ── Banned terms check (skip for editors/admins) ──────────────────────────
if (['contributor', 'author'].includes(profile.role)) {
const textToCheck = [title, content || '', excerpt || ''].join(' ');
const { data: bannedMatch } = await supabase
.rpc('check_banned_terms', { p_text: textToCheck, p_site_id: site_id })
.single();
if (bannedMatch) {
if (bannedMatch.ban_level === 'hard') {
// Save as draft silently, send admin notification
const slug = title.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-').trim() + '-' + Date.now();
await supabase.from('native_articles').insert({
slug, title, content, excerpt, category: safeCategory, tags,
featured_image, featured_image_alt,
author_id: user.id, author_name: profile.full_name || user.email,
status: 'draft', site_id,
banned_term_flagged: true, banned_term_matched: bannedMatch.term,
});
// Notify admins via email (fire and forget)
fetch('/api/admin/banned-term-alert', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: profile.full_name || user.email, term: bannedMatch.term }),
}).catch(() => {});
return NextResponse.json({ blocked: true, error: 'This post could not be submitted. Please contact the editorial team at editor@broadcastbeat.com for assistance.' }, { status: 422 });
}
// Soft ban: flag but allow through
// Will be tagged below
}
}
// Determine status
let status = 'draft';
let submittedForReviewAt = null;
let bannedTermFlagged = false;
let bannedTermMatched: string | null = null;
// Re-check for soft ban flag
if (['contributor', 'author'].includes(profile.role)) {
const textToCheck = [title, content || '', excerpt || ''].join(' ');
const { data: softMatch } = await supabase
.rpc('check_banned_terms', { p_text: textToCheck, p_site_id: site_id })
.single();
if (softMatch && softMatch.ban_level === 'soft') {
bannedTermFlagged = true;
bannedTermMatched = softMatch.term;
}
}
if (profile.role === 'contributor') {
status = scheduled_at ? 'scheduled_pending_review' : 'pending';
submittedForReviewAt = new Date().toISOString();
} else if (profile.role === 'author') {
status = scheduled_at ? 'scheduled' : 'published';
} else {
status = scheduled_at ? 'scheduled' : (body.status || 'published');
}
const slug = title.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-').trim() + '-' + Date.now();
const { data: article, error: insertError } = await supabase
.from('native_articles')
.insert({
slug, title, content, excerpt, category: safeCategory, tags,
featured_image, featured_image_alt,
author_id: user.id, author_name: profile.full_name || user.email,
status, site_id,
scheduled_at: scheduled_at || null,
submitted_for_review_at: submittedForReviewAt,
published_at: status === 'published' ? new Date().toISOString() : null,
scope_flagged: scope_flagged || false,
banned_term_flagged: bannedTermFlagged,
banned_term_matched: bannedTermMatched,
})
.select()
.single();
if (insertError) return NextResponse.json({ error: insertError.message }, { status: 500 });
return NextResponse.json({ article, status }, { status: 201 });
} catch (error) {
console.error('Contributor posts POST error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}