210 lines
7.3 KiB
TypeScript
210 lines
7.3 KiB
TypeScript
import { FastifyInstance } from "fastify"
|
|
import { createHash, randomBytes } from "node:crypto"
|
|
import { and, desc, eq } from "drizzle-orm"
|
|
import { z } from "zod"
|
|
import { instanceAgentScanJobs, instanceAgents } from "../../db/schema"
|
|
|
|
const createAgentSchema = z.object({
|
|
name: z.string().min(1),
|
|
description: z.string().optional().nullable(),
|
|
})
|
|
|
|
const updateAgentSchema = z.object({
|
|
name: z.string().min(1).optional(),
|
|
description: z.string().optional().nullable(),
|
|
active: z.boolean().optional(),
|
|
preferredScannerName: z.string().optional().nullable(),
|
|
scanDefaults: z.record(z.string(), z.any()).optional(),
|
|
})
|
|
|
|
const createScanJobSchema = z.object({
|
|
agentId: z.string().uuid(),
|
|
tenantId: z.number().int().positive().optional(),
|
|
scannerName: z.string().optional().nullable(),
|
|
requestedFilename: z.string().optional().nullable(),
|
|
settings: z.record(z.string(), z.any()).optional(),
|
|
target: z.record(z.string(), z.any()).optional(),
|
|
})
|
|
|
|
const hashToken = (token: string) =>
|
|
createHash("sha256").update(token, "utf8").digest("hex")
|
|
|
|
const createAgentToken = () => `fedeo_agent_${randomBytes(32).toString("hex")}`
|
|
|
|
const requireAdmin = (req: any, reply: any) => {
|
|
if (!req.user?.is_admin) {
|
|
reply.code(403).send({ error: "Admin required" })
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
export default async function instanceAgentRoutes(server: FastifyInstance) {
|
|
server.get("/instance-agents", async () => {
|
|
const rows = await server.db
|
|
.select({
|
|
id: instanceAgents.id,
|
|
createdAt: instanceAgents.createdAt,
|
|
updatedAt: instanceAgents.updatedAt,
|
|
name: instanceAgents.name,
|
|
description: instanceAgents.description,
|
|
tokenPrefix: instanceAgents.tokenPrefix,
|
|
active: instanceAgents.active,
|
|
capabilities: instanceAgents.capabilities,
|
|
scannerNames: instanceAgents.scannerNames,
|
|
printerNames: instanceAgents.printerNames,
|
|
preferredScannerName: instanceAgents.preferredScannerName,
|
|
scanDefaults: instanceAgents.scanDefaults,
|
|
lastSeenAt: instanceAgents.lastSeenAt,
|
|
lastDebugInfo: instanceAgents.lastDebugInfo,
|
|
})
|
|
.from(instanceAgents)
|
|
.orderBy(desc(instanceAgents.createdAt))
|
|
|
|
return { agents: rows }
|
|
})
|
|
|
|
server.post("/instance-agents", async (req, reply) => {
|
|
if (!requireAdmin(req, reply)) return
|
|
|
|
const body = createAgentSchema.parse(req.body)
|
|
const token = createAgentToken()
|
|
|
|
const [agent] = await server.db
|
|
.insert(instanceAgents)
|
|
.values({
|
|
name: body.name,
|
|
description: body.description,
|
|
tokenPrefix: token.slice(0, 24),
|
|
tokenHash: hashToken(token),
|
|
})
|
|
.returning({
|
|
id: instanceAgents.id,
|
|
name: instanceAgents.name,
|
|
description: instanceAgents.description,
|
|
tokenPrefix: instanceAgents.tokenPrefix,
|
|
active: instanceAgents.active,
|
|
preferredScannerName: instanceAgents.preferredScannerName,
|
|
scanDefaults: instanceAgents.scanDefaults,
|
|
createdAt: instanceAgents.createdAt,
|
|
})
|
|
|
|
return {
|
|
agent,
|
|
token,
|
|
}
|
|
})
|
|
|
|
server.patch<{ Params: { id: string } }>("/instance-agents/:id", async (req, reply) => {
|
|
if (!requireAdmin(req, reply)) return
|
|
|
|
const body = updateAgentSchema.parse(req.body)
|
|
const [agent] = await server.db
|
|
.update(instanceAgents)
|
|
.set({
|
|
...body,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(instanceAgents.id, req.params.id))
|
|
.returning({
|
|
id: instanceAgents.id,
|
|
name: instanceAgents.name,
|
|
description: instanceAgents.description,
|
|
active: instanceAgents.active,
|
|
preferredScannerName: instanceAgents.preferredScannerName,
|
|
scanDefaults: instanceAgents.scanDefaults,
|
|
updatedAt: instanceAgents.updatedAt,
|
|
})
|
|
|
|
if (!agent) return reply.code(404).send({ error: "Agent not found" })
|
|
|
|
return { agent }
|
|
})
|
|
|
|
server.post("/scan-jobs", async (req, reply) => {
|
|
const body = createScanJobSchema.parse(req.body)
|
|
const requestedTenantId = body.tenantId || req.user?.tenant_id
|
|
|
|
if (!requestedTenantId) {
|
|
return reply.code(400).send({ error: "tenantId required" })
|
|
}
|
|
|
|
if (body.tenantId && body.tenantId !== req.user?.tenant_id && !req.user?.is_admin) {
|
|
return reply.code(403).send({ error: "Cannot create scan job for another tenant" })
|
|
}
|
|
|
|
const [agent] = await server.db
|
|
.select({
|
|
id: instanceAgents.id,
|
|
active: instanceAgents.active,
|
|
preferredScannerName: instanceAgents.preferredScannerName,
|
|
scanDefaults: instanceAgents.scanDefaults,
|
|
})
|
|
.from(instanceAgents)
|
|
.where(eq(instanceAgents.id, body.agentId))
|
|
.limit(1)
|
|
|
|
if (!agent || !agent.active) {
|
|
return reply.code(404).send({ error: "Active agent not found" })
|
|
}
|
|
|
|
const [job] = await server.db
|
|
.insert(instanceAgentScanJobs)
|
|
.values({
|
|
tenantId: requestedTenantId,
|
|
agentId: body.agentId,
|
|
requestedBy: req.user?.user_id,
|
|
scannerName: body.scannerName || agent.preferredScannerName,
|
|
requestedFilename: body.requestedFilename,
|
|
settings: {
|
|
...((agent.scanDefaults || {}) as Record<string, any>),
|
|
...(body.settings || {}),
|
|
},
|
|
target: body.target || {},
|
|
})
|
|
.returning()
|
|
|
|
return { job }
|
|
})
|
|
|
|
server.get("/scan-jobs", async (req) => {
|
|
const query = req.query as { tenantId?: string }
|
|
const tenantId = req.user?.is_admin && query.tenantId
|
|
? Number(query.tenantId)
|
|
: req.user?.tenant_id
|
|
|
|
const rows = tenantId
|
|
? await server.db
|
|
.select()
|
|
.from(instanceAgentScanJobs)
|
|
.where(eq(instanceAgentScanJobs.tenantId, tenantId))
|
|
.orderBy(desc(instanceAgentScanJobs.createdAt))
|
|
: await server.db
|
|
.select()
|
|
.from(instanceAgentScanJobs)
|
|
.orderBy(desc(instanceAgentScanJobs.createdAt))
|
|
|
|
return { jobs: rows }
|
|
})
|
|
|
|
server.get<{ Params: { id: string } }>("/scan-jobs/:id", async (req, reply) => {
|
|
const conditions = [eq(instanceAgentScanJobs.id, req.params.id)]
|
|
|
|
if (!req.user?.is_admin) {
|
|
if (!req.user?.tenant_id) return reply.code(400).send({ error: "tenant required" })
|
|
conditions.push(eq(instanceAgentScanJobs.tenantId, req.user.tenant_id))
|
|
}
|
|
|
|
const [job] = await server.db
|
|
.select()
|
|
.from(instanceAgentScanJobs)
|
|
.where(and(...conditions))
|
|
.limit(1)
|
|
|
|
if (!job) return reply.code(404).send({ error: "Scan job not found" })
|
|
|
|
return { job }
|
|
})
|
|
}
|