/** Moderation for admins: the report queue, suspensions, and a few numbers. */ import { and, count, desc, eq, isNotNull } from 'drizzle-orm'; import { alias } from 'drizzle-orm/pg-core'; import type { FastifyInstance } from 'fastify'; import type { AdminReportDTO, AdminStatsDTO } from '@musicshare/shared'; import { db, schema } from '../db'; import { requireAdmin } from '../lib/auth'; import { HttpError } from '../lib/http'; import { pauseSharing, toUserSummary } from '../services/nowPlaying'; export async function adminRoutes(app: FastifyInstance) { app.get('/admin/stats', async (req): Promise => { requireAdmin(req); const [[users], [listening], [open], [suspended]] = await Promise.all([ db.select({ n: count() }).from(schema.users), db.select({ n: count() }).from(schema.nowPlaying).where(eq(schema.nowPlaying.isPlaying, true)), db.select({ n: count() }).from(schema.reports).where(eq(schema.reports.status, 'open')), db.select({ n: count() }).from(schema.users).where(isNotNull(schema.users.suspendedAt)), ]); return { users: users!.n, listeningNow: listening!.n, openReports: open!.n, suspended: suspended!.n }; }); app.get<{ Querystring: { status?: 'open' | 'resolved' } }>('/admin/reports', async (req): Promise => { requireAdmin(req); const reporter = alias(schema.users, 'reporter'); const reported = alias(schema.users, 'reported'); const rows = await db .select({ report: schema.reports, reporter, reported }) .from(schema.reports) .leftJoin(reporter, eq(reporter.id, schema.reports.reporterId)) .innerJoin(reported, eq(reported.id, schema.reports.reportedId)) .where(eq(schema.reports.status, req.query.status === 'resolved' ? 'resolved' : 'open')) .orderBy(desc(schema.reports.createdAt)) .limit(200); return rows.map(({ report, reporter: by, reported: who }) => ({ id: report.id, reason: report.reason, note: report.note, status: report.status, createdAt: report.createdAt.toISOString(), reporter: by ? toUserSummary(by) : null, reported: { ...toUserSummary(who), suspended: Boolean(who.suspendedAt) }, comment: report.commentBody ? { id: report.commentId, body: report.commentBody } : null, })); }); app.post<{ Params: { id: string } }>('/admin/reports/:id/resolve', async (req) => { requireAdmin(req); if (!/^[0-9a-f-]{36}$/i.test(req.params.id)) throw new HttpError(404, 'Report not found'); await db.update(schema.reports).set({ status: 'resolved' }).where(eq(schema.reports.id, req.params.id)); return { ok: true }; }); /** Suspend: signs them out everywhere, blocks login, hides their profile, stops sharing. */ app.post<{ Params: { username: string } }>('/admin/users/:username/suspend', async (req) => { const admin = requireAdmin(req); const [user] = await db.select().from(schema.users).where(eq(schema.users.username, req.params.username.toLowerCase())); if (!user) throw new HttpError(404, 'User not found'); if (user.id === admin.id) throw new HttpError(400, "You can't suspend yourself"); if (user.role === 'admin') throw new HttpError(400, "Admins can't be suspended"); await db.update(schema.users).set({ suspendedAt: new Date() }).where(eq(schema.users.id, user.id)); await db.delete(schema.sessions).where(eq(schema.sessions.userId, user.id)); await pauseSharing(user.id); req.log.warn({ by: admin.username, user: user.username }, 'user suspended'); return { ok: true }; }); app.delete<{ Params: { username: string } }>('/admin/users/:username/suspend', async (req) => { requireAdmin(req); await db .update(schema.users) .set({ suspendedAt: null }) .where(and(eq(schema.users.username, req.params.username.toLowerCase()), isNotNull(schema.users.suspendedAt))); return { ok: true }; }); }