import type { FastifyInstance } from 'fastify'; import { eq, desc, count } from 'drizzle-orm'; import { users, games, nodes, auditLogs } from '@source/database'; import { AppError } from '../../lib/errors.js'; import { requireSuperAdmin } from '../../lib/permissions.js'; import { paginate, paginatedResponse, PaginationQuerySchema } from '../../lib/pagination.js'; import { CreateGameSchema, UpdateGameSchema, GameIdParamSchema } from './schemas.js'; export default async function adminRoutes(app: FastifyInstance) { // All admin routes require auth + super admin app.addHook('onRequest', app.authenticate); app.addHook('onRequest', async (request) => { requireSuperAdmin(request); }); // === Users === // GET /api/admin/users app.get('/users', { schema: { querystring: PaginationQuerySchema } }, async (request) => { const { page, perPage, offset, limit } = paginate(request.query as any); const [totalResult] = await app.db.select({ count: count() }).from(users); const userList = await app.db .select({ id: users.id, email: users.email, username: users.username, isSuperAdmin: users.isSuperAdmin, avatarUrl: users.avatarUrl, createdAt: users.createdAt, }) .from(users) .limit(limit) .offset(offset) .orderBy(users.createdAt); return paginatedResponse(userList, totalResult!.count, page, perPage); }); // === Games === // GET /api/admin/games app.get('/games', async () => { const gameList = await app.db .select() .from(games) .orderBy(games.name); return { data: gameList }; }); // POST /api/admin/games app.post('/games', { schema: CreateGameSchema }, async (request, reply) => { const body = request.body as { slug: string; name: string; dockerImage: string; defaultPort: number; startupCommand: string; stopCommand?: string; configFiles?: unknown[]; environmentVars?: unknown[]; }; const existing = await app.db.query.games.findFirst({ where: eq(games.slug, body.slug), }); if (existing) throw AppError.conflict('Game slug already exists'); const [game] = await app.db .insert(games) .values({ ...body, configFiles: body.configFiles ?? [], environmentVars: body.environmentVars ?? [], }) .returning(); return reply.code(201).send(game); }); // PATCH /api/admin/games/:gameId app.patch('/games/:gameId', { schema: { ...GameIdParamSchema, ...UpdateGameSchema } }, async (request) => { const { gameId } = request.params as { gameId: string }; const body = request.body as Record; const [updated] = await app.db .update(games) .set({ ...body, updatedAt: new Date() }) .where(eq(games.id, gameId)) .returning(); if (!updated) throw AppError.notFound('Game not found'); return updated; }); // === Nodes (global view) === // GET /api/admin/nodes app.get('/nodes', async () => { const nodeList = await app.db .select() .from(nodes) .orderBy(nodes.createdAt); return { data: nodeList }; }); // === Audit Logs === // GET /api/admin/audit-logs app.get('/audit-logs', { schema: { querystring: PaginationQuerySchema } }, async (request) => { const { page, perPage, offset, limit } = paginate(request.query as any); const [totalResult] = await app.db.select({ count: count() }).from(auditLogs); const logs = await app.db .select({ id: auditLogs.id, organizationId: auditLogs.organizationId, userId: auditLogs.userId, serverId: auditLogs.serverId, action: auditLogs.action, metadata: auditLogs.metadata, ipAddress: auditLogs.ipAddress, createdAt: auditLogs.createdAt, userEmail: users.email, userName: users.username, }) .from(auditLogs) .innerJoin(users, eq(auditLogs.userId, users.id)) .orderBy(desc(auditLogs.createdAt)) .limit(limit) .offset(offset); return paginatedResponse(logs, totalResult!.count, page, perPage); }); }