chore: initial commit for phase03

This commit is contained in:
hibna
2026-02-21 13:37:46 +03:00
parent 8eb7c90958
commit d0c20581b6
12 changed files with 1175 additions and 0 deletions
+170
View File
@@ -0,0 +1,170 @@
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 });
});
}