import { NextRequest, NextResponse } from 'next/server'; import { createClient } from '@/lib/supabase/server'; import nodemailer from 'nodemailer'; const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://avbeat.com'; async function sendSuspensionEmail( email: string, fullName: string, suspensionType: 'suspended' | 'banned', reason: string, durationDays: number | null, expiresAt: string | null ) { const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST, port: parseInt(process.env.SMTP_PORT || '587'), secure: false, auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS, }, }); const isBanned = suspensionType === 'banned'; const durationText = isBanned ? 'permanently' : durationDays ? `for ${durationDays} day${durationDays !== 1 ? 's' : ''}` : 'temporarily'; const expiryText = !isBanned && expiresAt ? `

Your access will be restored on ${new Date(expiresAt).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}.

` : ''; const subject = isBanned ? `Your AV Beat account has been banned` : `Your AV Beat account has been suspended`; const html = `
AV Beat

${isBanned ? '🚫 Account Banned' : '⏸ Account Suspended'}

Hi ${fullName || 'there'},

Your AV Beat account has been ${isBanned ? 'permanently banned' : `suspended ${durationText}`}.

Reason

${reason}

${expiryText}

${isBanned ? 'This decision is final. If you believe this is an error, please contact our support team.' : 'During this period, you will not be able to post threads or replies. If you believe this is an error, please contact our support team.'}

AV Beat — ${siteUrl}

`; await transporter.sendMail({ from: `"${process.env.SMTP_FROM_NAME || 'AV Beat'}" <${process.env.SMTP_FROM_EMAIL}>`, to: email, subject, html, }); } // GET /api/admin/suspensions — list suspensions export async function GET(req: NextRequest) { const supabase = await createClient(); const { data: { user } } = await supabase.auth.getUser(); if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); // Verify admin/moderator role const { data: profile } = await supabase .from('user_profiles') .select('role') .eq('id', user.id) .single(); if (!profile || !['admin', 'moderator'].includes(profile.role)) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } const { searchParams } = new URL(req.url); const userId = searchParams.get('user_id'); const activeOnly = searchParams.get('active') !== 'false'; let query = supabase .from('user_suspensions') .select(` *, user:user_id(id, email, full_name, avatar_url, role), suspended_by_user:suspended_by(id, full_name, email) `) .order('created_at', { ascending: false }); if (userId) query = query.eq('user_id', userId); if (activeOnly) query = query.eq('is_active', true); const { data, error } = await query; if (error) return NextResponse.json({ error: error.message }, { status: 500 }); return NextResponse.json({ suspensions: data || [] }); } // POST /api/admin/suspensions — suspend or ban a user export async function POST(req: NextRequest) { const supabase = await createClient(); const { data: { user } } = await supabase.auth.getUser(); if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); // Verify admin/moderator role const { data: adminProfile } = await supabase .from('user_profiles') .select('role, full_name') .eq('id', user.id) .single(); if (!adminProfile || !['admin', 'moderator'].includes(adminProfile.role)) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } const body = await req.json(); const { user_id, suspension_type, reason, duration_days } = body; if (!user_id || !reason || !suspension_type) { return NextResponse.json({ error: 'user_id, suspension_type, and reason are required' }, { status: 400 }); } // Prevent suspending admins (unless current user is admin) const { data: targetProfile } = await supabase .from('user_profiles') .select('id, email, full_name, role') .eq('id', user_id) .single(); if (!targetProfile) { return NextResponse.json({ error: 'User not found' }, { status: 404 }); } if (targetProfile.role === 'admin' && adminProfile.role !== 'admin') { return NextResponse.json({ error: 'Moderators cannot suspend admins' }, { status: 403 }); } // Deactivate any existing active suspensions for this user await supabase .from('user_suspensions') .update({ is_active: false, updated_at: new Date().toISOString() }) .eq('user_id', user_id) .eq('is_active', true); // Calculate expiry const isBanned = suspension_type === 'banned'; const expiresAt = !isBanned && duration_days ? new Date(Date.now() + duration_days * 24 * 60 * 60 * 1000).toISOString() : null; // Insert suspension record const { data: suspension, error: insertError } = await supabase .from('user_suspensions') .insert({ user_id, suspended_by: user.id, suspension_type, reason: reason.trim(), duration_days: isBanned ? null : (duration_days || null), expires_at: expiresAt, is_active: true, email_sent: false, }) .select() .single(); if (insertError) return NextResponse.json({ error: insertError.message }, { status: 500 }); // Update user_profiles suspension_status await supabase .from('user_profiles') .update({ suspension_status: suspension_type, suspension_reason: reason.trim(), suspended_until: expiresAt, }) .eq('id', user_id); // Log to audit_logs await supabase.from('audit_logs').insert({ user_id: user.id, action: 'user_suspended', target_type: 'user', target_id: user_id, details: { suspension_type, reason: reason.trim(), duration_days: isBanned ? null : duration_days, expires_at: expiresAt, target_email: targetProfile.email, }, }).select().maybeSingle(); // Send notification email let emailSent = false; try { await sendSuspensionEmail( targetProfile.email, targetProfile.full_name, suspension_type, reason.trim(), isBanned ? null : (duration_days || null), expiresAt ); emailSent = true; await supabase .from('user_suspensions') .update({ email_sent: true }) .eq('id', suspension.id); } catch { // Email failure should not block suspension } return NextResponse.json({ suspension, emailSent }, { status: 201 }); } // PATCH /api/admin/suspensions — lift a suspension export async function PATCH(req: NextRequest) { const supabase = await createClient(); const { data: { user } } = await supabase.auth.getUser(); if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); const { data: adminProfile } = await supabase .from('user_profiles') .select('role') .eq('id', user.id) .single(); if (!adminProfile || !['admin', 'moderator'].includes(adminProfile.role)) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } const body = await req.json(); const { suspension_id, lift_reason } = body; if (!suspension_id) { return NextResponse.json({ error: 'suspension_id is required' }, { status: 400 }); } const { data: suspension, error: fetchError } = await supabase .from('user_suspensions') .select('*, user:user_id(id, email, full_name)') .eq('id', suspension_id) .single(); if (fetchError || !suspension) { return NextResponse.json({ error: 'Suspension not found' }, { status: 404 }); } const { error: updateError } = await supabase .from('user_suspensions') .update({ is_active: false, lifted_at: new Date().toISOString(), lifted_by: user.id, lift_reason: lift_reason?.trim() || null, updated_at: new Date().toISOString(), }) .eq('id', suspension_id); if (updateError) return NextResponse.json({ error: updateError.message }, { status: 500 }); // Clear user_profiles suspension_status await supabase .from('user_profiles') .update({ suspension_status: null, suspension_reason: null, suspended_until: null, }) .eq('id', suspension.user_id); // Log to audit_logs await supabase.from('audit_logs').insert({ user_id: user.id, action: 'user_suspension_lifted', target_type: 'user', target_id: suspension.user_id, details: { suspension_id, lift_reason: lift_reason?.trim() || null, }, }).select().maybeSingle(); return NextResponse.json({ success: true }); }