← 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]
)
}