171 lines
5.1 KiB
TypeScript
171 lines
5.1 KiB
TypeScript
import type { FastifyInstance } from 'fastify';
|
|
import { eq, and } from 'drizzle-orm';
|
|
import { randomBytes } from 'crypto';
|
|
import { nodes, allocations } from '@source/database';
|
|
import { AppError } from '../../lib/errors.js';
|
|
import { requirePermission } from '../../lib/permissions.js';
|
|
import { createAuditLog } from '../../lib/audit.js';
|
|
import {
|
|
NodeParamSchema,
|
|
CreateNodeSchema,
|
|
UpdateNodeSchema,
|
|
CreateAllocationSchema,
|
|
} from './schemas.js';
|
|
|
|
export default async function nodeRoutes(app: FastifyInstance) {
|
|
app.addHook('onRequest', app.authenticate);
|
|
|
|
// GET /api/organizations/:orgId/nodes
|
|
app.get('/', async (request) => {
|
|
const { orgId } = request.params as { orgId: string };
|
|
await requirePermission(request, orgId, 'node.read');
|
|
|
|
const nodeList = await app.db
|
|
.select()
|
|
.from(nodes)
|
|
.where(eq(nodes.organizationId, orgId))
|
|
.orderBy(nodes.createdAt);
|
|
|
|
return { data: nodeList };
|
|
});
|
|
|
|
// POST /api/organizations/:orgId/nodes
|
|
app.post('/', { schema: CreateNodeSchema }, async (request, reply) => {
|
|
const { orgId } = request.params as { orgId: string };
|
|
await requirePermission(request, orgId, 'node.manage');
|
|
|
|
const body = request.body as {
|
|
name: string;
|
|
fqdn: string;
|
|
daemonPort?: number;
|
|
grpcPort?: number;
|
|
location?: string;
|
|
memoryTotal: number;
|
|
diskTotal: number;
|
|
memoryOveralloc?: number;
|
|
diskOveralloc?: number;
|
|
};
|
|
|
|
const daemonToken = randomBytes(32).toString('hex');
|
|
|
|
const [node] = await app.db
|
|
.insert(nodes)
|
|
.values({
|
|
organizationId: orgId,
|
|
...body,
|
|
daemonToken,
|
|
})
|
|
.returning();
|
|
|
|
await createAuditLog(app.db, request, {
|
|
organizationId: orgId,
|
|
action: 'node.create',
|
|
metadata: { nodeId: node!.id, name: body.name },
|
|
});
|
|
|
|
return reply.code(201).send(node);
|
|
});
|
|
|
|
// GET /api/organizations/:orgId/nodes/:nodeId
|
|
app.get('/:nodeId', { schema: NodeParamSchema }, async (request) => {
|
|
const { orgId, nodeId } = request.params as { orgId: string; nodeId: string };
|
|
await requirePermission(request, orgId, 'node.read');
|
|
|
|
const node = await app.db.query.nodes.findFirst({
|
|
where: and(eq(nodes.id, nodeId), eq(nodes.organizationId, orgId)),
|
|
});
|
|
if (!node) throw AppError.notFound('Node not found');
|
|
|
|
return node;
|
|
});
|
|
|
|
// PATCH /api/organizations/:orgId/nodes/:nodeId
|
|
app.patch('/:nodeId', { schema: { ...NodeParamSchema, ...UpdateNodeSchema } }, async (request) => {
|
|
const { orgId, nodeId } = request.params as { orgId: string; nodeId: string };
|
|
await requirePermission(request, orgId, 'node.manage');
|
|
|
|
const body = request.body as Record<string, unknown>;
|
|
|
|
const [updated] = await app.db
|
|
.update(nodes)
|
|
.set({ ...body, updatedAt: new Date() })
|
|
.where(and(eq(nodes.id, nodeId), eq(nodes.organizationId, orgId)))
|
|
.returning();
|
|
|
|
if (!updated) throw AppError.notFound('Node not found');
|
|
|
|
await createAuditLog(app.db, request, {
|
|
organizationId: orgId,
|
|
action: 'node.update',
|
|
metadata: { nodeId, ...body },
|
|
});
|
|
|
|
return updated;
|
|
});
|
|
|
|
// DELETE /api/organizations/:orgId/nodes/:nodeId
|
|
app.delete('/:nodeId', { schema: NodeParamSchema }, async (request, reply) => {
|
|
const { orgId, nodeId } = request.params as { orgId: string; nodeId: string };
|
|
await requirePermission(request, orgId, 'node.manage');
|
|
|
|
const node = await app.db.query.nodes.findFirst({
|
|
where: and(eq(nodes.id, nodeId), eq(nodes.organizationId, orgId)),
|
|
});
|
|
if (!node) throw AppError.notFound('Node not found');
|
|
|
|
await app.db.delete(nodes).where(eq(nodes.id, nodeId));
|
|
|
|
await createAuditLog(app.db, request, {
|
|
organizationId: orgId,
|
|
action: 'node.delete',
|
|
metadata: { nodeId, name: node.name },
|
|
});
|
|
|
|
return reply.code(204).send();
|
|
});
|
|
|
|
// === Allocations ===
|
|
|
|
// GET /api/organizations/:orgId/nodes/:nodeId/allocations
|
|
app.get('/:nodeId/allocations', { schema: NodeParamSchema }, async (request) => {
|
|
const { orgId, nodeId } = request.params as { orgId: string; nodeId: string };
|
|
await requirePermission(request, orgId, 'node.read');
|
|
|
|
const allocs = await app.db
|
|
.select()
|
|
.from(allocations)
|
|
.where(eq(allocations.nodeId, nodeId))
|
|
.orderBy(allocations.port);
|
|
|
|
return { data: allocs };
|
|
});
|
|
|
|
// POST /api/organizations/:orgId/nodes/:nodeId/allocations
|
|
app.post('/:nodeId/allocations', { schema: { ...NodeParamSchema, ...CreateAllocationSchema } }, async (request, reply) => {
|
|
const { orgId, nodeId } = request.params as { orgId: string; nodeId: string };
|
|
await requirePermission(request, orgId, 'node.manage');
|
|
|
|
const { ip, ports } = request.body as { ip: string; ports: number[] };
|
|
|
|
const values = ports.map((port) => ({
|
|
nodeId,
|
|
ip,
|
|
port,
|
|
}));
|
|
|
|
const created = await app.db
|
|
.insert(allocations)
|
|
.values(values)
|
|
.onConflictDoNothing()
|
|
.returning();
|
|
|
|
await createAuditLog(app.db, request, {
|
|
organizationId: orgId,
|
|
action: 'allocation.create',
|
|
metadata: { nodeId, ip, ports },
|
|
});
|
|
|
|
return reply.code(201).send({ data: created });
|
|
});
|
|
}
|