import { Router } from "express"; import { isIP } from "node:net"; import { eq } from "drizzle-orm"; import { z } from "zod"; import { db } from "../db/client.js"; import { ipamEntries } from "../db/schema.js"; import { requireAuth, requireRole } from "../auth/middleware.js"; import { recordAudit } from "../services/audit.js"; import { asyncHandler } from "../utils/asyncHandler.js"; export const ipamRouter = Router(); ipamRouter.use(requireAuth); ipamRouter.get("/", asyncHandler(async (_req, res) => { const rows = await db.select().from(ipamEntries).orderBy(ipamEntries.ipAddress); // matchingDnsRecords will be populated once the DNS module (phase 3) has cached records for cross-reference. res.json({ entries: rows.map((r) => ({ ...r, matchingDnsRecords: [] as string[] })) }); })); const createInput = z.object({ ipAddress: z.string().refine((v) => isIP(v) !== 0, "Must be a valid IPv4 or IPv6 address"), label: z.string().max(200).optional(), vendor: z.string().max(200).optional(), location: z.string().max(200).optional(), notes: z.string().max(4000).optional(), }); const updateInput = createInput.omit({ ipAddress: true }).partial(); ipamRouter.post("/", requireRole("operator"), asyncHandler(async (req, res) => { const parsed = createInput.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ error: "invalid_body", details: parsed.error.flatten() }); } const [existing] = await db .select({ id: ipamEntries.id }) .from(ipamEntries) .where(eq(ipamEntries.ipAddress, parsed.data.ipAddress)) .limit(1); if (existing) { return res.status(409).json({ error: "duplicate_ip" }); } const [created] = await db.insert(ipamEntries).values(parsed.data).returning(); await recordAudit({ actor: req.currentUser!, category: "ipam", action: "create", targetType: "ipam_entry", targetId: created.id, detail: { ipAddress: created.ipAddress }, }); res.status(201).json({ entry: { ...created, matchingDnsRecords: [] } }); })); ipamRouter.patch("/:id", requireRole("operator"), asyncHandler(async (req, res) => { const id = Number(req.params.id); const parsed = updateInput.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ error: "invalid_body", details: parsed.error.flatten() }); } const [existing] = await db.select().from(ipamEntries).where(eq(ipamEntries.id, id)).limit(1); if (!existing) { return res.status(404).json({ error: "not_found" }); } const [updated] = await db .update(ipamEntries) .set({ ...parsed.data, updatedAt: new Date().toISOString() }) .where(eq(ipamEntries.id, id)) .returning(); await recordAudit({ actor: req.currentUser!, category: "ipam", action: "update", targetType: "ipam_entry", targetId: id, detail: { ipAddress: updated.ipAddress }, }); res.json({ entry: { ...updated, matchingDnsRecords: [] } }); })); ipamRouter.delete("/:id", requireRole("operator"), asyncHandler(async (req, res) => { const id = Number(req.params.id); const [existing] = await db.select().from(ipamEntries).where(eq(ipamEntries.id, id)).limit(1); if (!existing) { return res.status(404).json({ error: "not_found" }); } await db.delete(ipamEntries).where(eq(ipamEntries.id, id)); await recordAudit({ actor: req.currentUser!, category: "ipam", action: "delete", targetType: "ipam_entry", targetId: id, detail: { ipAddress: existing.ipAddress }, }); res.status(204).end(); }));