chore: initial commit for phase03
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
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<string, unknown>;
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user