← back to IWasCute

src/app/api/admin/route.ts

284 lines

import { NextRequest, NextResponse } from 'next/server'
import { query } from '@/lib/db'
import { requireAdmin } from '@/lib/auth'

export async function GET(request: NextRequest) {
  const admin = await requireAdmin()
  if (!admin) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

  const { searchParams } = new URL(request.url)
  const view = searchParams.get('view') || 'overview'

  switch (view) {
    case 'overview':
      return getOverview()
    case 'queue':
      return getQueue(searchParams)
    case 'dmca':
      return getDMCA(searchParams)
    case 'users':
      return getUsers(searchParams)
    case 'upload':
      return getUploadDetail(searchParams)
    default:
      return NextResponse.json({ error: `Unknown view: ${view}` }, { status: 400 })
  }
}

async function getOverview() {
  const [pending, approved, rejected, users, revenue, dmca] = await Promise.all([
    query(`SELECT COUNT(*) FROM uploads WHERE status IN ('pending', 'flagged')`),
    query(`SELECT COUNT(*) FROM uploads WHERE status = 'approved'`),
    query(`SELECT COUNT(*) FROM uploads WHERE status = 'rejected'`),
    query(`SELECT COUNT(*) FROM users WHERE role != 'admin'`),
    query(`SELECT COALESCE(SUM(total_earnings), 0) as total FROM photos`),
    query(`SELECT COUNT(*) FROM dmca_claims WHERE status IN ('open', 'investigating')`),
  ])

  // Recent queue
  const recentQueue = await query(`
    SELECT u.id, u.status, u.created_at, u.copyright_relationship, u.copyright_permission,
           usr.display_name, usr.email,
           COUNT(p.id) as photo_count
    FROM uploads u
    JOIN users usr ON u.user_id = usr.id
    LEFT JOIN photos p ON p.upload_id = u.id
    WHERE u.status IN ('pending', 'flagged')
    GROUP BY u.id, usr.display_name, usr.email
    ORDER BY u.created_at DESC
    LIMIT 5
  `)

  // Recent DMCA
  const recentDMCA = await query(`
    SELECT d.*, u.id as upload_uuid
    FROM dmca_claims d
    LEFT JOIN uploads u ON d.upload_id = u.id
    ORDER BY d.filed_at DESC
    LIMIT 5
  `)

  return NextResponse.json({
    stats: {
      pending: parseInt(pending.rows[0].count),
      approved: parseInt(approved.rows[0].count),
      rejected: parseInt(rejected.rows[0].count),
      users: parseInt(users.rows[0].count),
      revenue: parseFloat(revenue.rows[0].total),
      dmca_open: parseInt(dmca.rows[0].count),
    },
    recent_queue: recentQueue.rows,
    recent_dmca: recentDMCA.rows,
  })
}

async function getQueue(params: URLSearchParams) {
  const status = params.get('status')
  const search = params.get('search')

  const conditions = [`u.status IN ('pending', 'flagged', 'in_review')`]
  const values: unknown[] = []
  let idx = 1

  if (status && status !== 'all') {
    conditions.push(`u.status = $${idx}`)
    values.push(status)
    idx++
  }
  if (search) {
    conditions.push(`(usr.display_name ILIKE $${idx} OR u.id::text ILIKE $${idx})`)
    values.push(`%${search}%`)
    idx++
  }

  const where = conditions.join(' AND ')

  const result = await query(`
    SELECT u.*, usr.display_name, usr.email,
           COUNT(p.id) as photo_count,
           json_agg(json_build_object('id', p.id, 'filename', p.filename, 'exif_year', p.exif_year) ORDER BY p.created_at) FILTER (WHERE p.id IS NOT NULL) as photos
    FROM uploads u
    JOIN users usr ON u.user_id = usr.id
    LEFT JOIN photos p ON p.upload_id = u.id
    WHERE ${where}
    GROUP BY u.id, usr.display_name, usr.email
    ORDER BY CASE WHEN u.status = 'flagged' THEN 0 ELSE 1 END, u.created_at DESC
  `, values)

  return NextResponse.json({ queue: result.rows })
}

async function getDMCA(params: URLSearchParams) {
  const status = params.get('status')
  const conditions: string[] = []
  const values: unknown[] = []

  if (status && status !== 'all') {
    conditions.push(`d.status = $1`)
    values.push(status)
  }

  const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : ''

  const result = await query(`
    SELECT d.*,
           u.id as upload_uuid, usr.display_name as upload_user
    FROM dmca_claims d
    LEFT JOIN uploads u ON d.upload_id = u.id
    LEFT JOIN users usr ON u.user_id = usr.id
    ${where}
    ORDER BY d.filed_at DESC
  `, values)

  return NextResponse.json({ claims: result.rows })
}

async function getUsers(params: URLSearchParams) {
  const search = params.get('search')
  const conditions = [`u.role != 'admin'`]
  const values: unknown[] = []

  if (search) {
    conditions.push(`(u.display_name ILIKE $1 OR u.email ILIKE $1)`)
    values.push(`%${search}%`)
  }

  const where = conditions.join(' AND ')

  const result = await query(`
    SELECT u.id, u.email, u.display_name, u.role, u.is_verified, u.created_at,
           COUNT(DISTINCT up.id) as upload_count,
           COUNT(DISTINCT p.id) as photo_count,
           COALESCE(SUM(p.total_earnings), 0) as total_earnings,
           COUNT(DISTINCT CASE WHEN up.status = 'approved' THEN up.id END) as approved_uploads
    FROM users u
    LEFT JOIN uploads up ON up.user_id = u.id
    LEFT JOIN photos p ON p.user_id = u.id
    WHERE ${where}
    GROUP BY u.id
    ORDER BY u.created_at DESC
  `, values)

  return NextResponse.json({ users: result.rows })
}

async function getUploadDetail(params: URLSearchParams) {
  const id = params.get('id')
  if (!id) return NextResponse.json({ error: 'id required' }, { status: 400 })

  const upload = await query(`
    SELECT u.*, usr.display_name, usr.email
    FROM uploads u JOIN users usr ON u.user_id = usr.id
    WHERE u.id = $1
  `, [id])

  const photos = await query(`SELECT * FROM photos WHERE upload_id = $1 ORDER BY created_at`, [id])

  return NextResponse.json({
    upload: upload.rows[0] || null,
    photos: photos.rows,
  })
}

// POST — Admin actions
export async function POST(request: NextRequest) {
  const admin = await requireAdmin()
  if (!admin) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

  const body = await request.json()
  const { action } = body

  switch (action) {
    case 'approve_upload': {
      const { upload_id, notes } = body
      await query(
        `UPDATE uploads SET status = 'approved', reviewed_by = $1, reviewed_at = NOW(), review_notes = $2, updated_at = NOW() WHERE id = $3`,
        [admin.user_id, notes || null, upload_id]
      )
      // Set clearance level on photos
      await query(
        `UPDATE photos SET clearance_level = CASE
          WHEN (SELECT likeness_ai_training FROM uploads WHERE id = $1) THEN 'ai_training'
          WHEN (SELECT likeness_commercial FROM uploads WHERE id = $1) THEN 'commercial'
          ELSE 'editorial'
        END WHERE upload_id = $1`,
        [upload_id]
      )
      await auditLog(admin.user_id, 'upload.approve', 'upload', upload_id)
      return NextResponse.json({ success: true })
    }

    case 'reject_upload': {
      const { upload_id, reason } = body
      await query(
        `UPDATE uploads SET status = 'rejected', reviewed_by = $1, reviewed_at = NOW(), rejection_reason = $2, updated_at = NOW() WHERE id = $3`,
        [admin.user_id, reason || 'Rights verification failed', upload_id]
      )
      await auditLog(admin.user_id, 'upload.reject', 'upload', upload_id)
      return NextResponse.json({ success: true })
    }

    case 'flag_upload': {
      const { upload_id, notes } = body
      await query(
        `UPDATE uploads SET status = 'flagged', review_notes = $1, updated_at = NOW() WHERE id = $2`,
        [notes, upload_id]
      )
      await auditLog(admin.user_id, 'upload.flag', 'upload', upload_id)
      return NextResponse.json({ success: true })
    }

    case 'dmca_takedown': {
      const { claim_id } = body
      await query(
        `UPDATE dmca_claims SET status = 'taken_down', resolved_by = $1, resolved_at = NOW(), updated_at = NOW() WHERE id = $2`,
        [admin.user_id, claim_id]
      )
      // Also update the associated upload
      await query(
        `UPDATE uploads SET status = 'dmca_removed', updated_at = NOW() WHERE id = (SELECT upload_id FROM dmca_claims WHERE id = $1)`,
        [claim_id]
      )
      await auditLog(admin.user_id, 'dmca.takedown', 'dmca_claim', claim_id)
      return NextResponse.json({ success: true })
    }

    case 'dmca_counter': {
      const { claim_id, notes } = body
      await query(
        `UPDATE dmca_claims SET status = 'counter_noticed', resolution_notes = $1, updated_at = NOW() WHERE id = $2`,
        [notes, claim_id]
      )
      await auditLog(admin.user_id, 'dmca.counter_notice', 'dmca_claim', claim_id)
      return NextResponse.json({ success: true })
    }

    case 'dmca_dismiss': {
      const { claim_id, notes } = body
      await query(
        `UPDATE dmca_claims SET status = 'dismissed', resolved_by = $1, resolved_at = NOW(), resolution_notes = $2, updated_at = NOW() WHERE id = $3`,
        [admin.user_id, notes, claim_id]
      )
      await auditLog(admin.user_id, 'dmca.dismiss', 'dmca_claim', claim_id)
      return NextResponse.json({ success: true })
    }

    case 'suspend_user': {
      const { user_id } = body
      // In a real app, add a suspended_at column. For now, log it.
      await auditLog(admin.user_id, 'user.suspend', 'user', user_id)
      return NextResponse.json({ success: true })
    }

    default:
      return NextResponse.json({ error: `Unknown action: ${action}` }, { status: 400 })
  }
}

async function auditLog(userId: string, action: string, targetType: string, targetId: string) {
  await query(
    `INSERT INTO audit_log (user_id, action, target_type, target_id) VALUES ($1, $2, $3, $4)`,
    [userId, action, targetType, targetId]
  )
}