Instanzweite Scan-Agenten vorbereiten
This commit is contained in:
43
backend/db/migrations/0051_instance_scan_agents.sql
Normal file
43
backend/db/migrations/0051_instance_scan_agents.sql
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
CREATE TABLE "instance_agents" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"token_prefix" text NOT NULL,
|
||||||
|
"token_hash" text NOT NULL,
|
||||||
|
"active" boolean DEFAULT true NOT NULL,
|
||||||
|
"capabilities" jsonb DEFAULT '{"scan":true,"print":false}'::jsonb NOT NULL,
|
||||||
|
"scanner_names" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||||
|
"printer_names" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||||
|
"last_seen_at" timestamp with time zone,
|
||||||
|
"last_debug_info" jsonb,
|
||||||
|
CONSTRAINT "instance_agents_token_hash_unique" UNIQUE("token_hash")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "instance_agent_scan_jobs" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"tenant_id" bigint NOT NULL,
|
||||||
|
"agent_id" uuid NOT NULL,
|
||||||
|
"requested_by" uuid,
|
||||||
|
"status" text DEFAULT 'pending' NOT NULL,
|
||||||
|
"scanner_name" text,
|
||||||
|
"requested_filename" text,
|
||||||
|
"settings" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||||
|
"target" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||||
|
"agent_message" text,
|
||||||
|
"attempts" integer DEFAULT 0 NOT NULL,
|
||||||
|
"claimed_at" timestamp with time zone,
|
||||||
|
"finished_at" timestamp with time zone,
|
||||||
|
"file_id" uuid,
|
||||||
|
CONSTRAINT "instance_agent_scan_jobs_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE cascade,
|
||||||
|
CONSTRAINT "instance_agent_scan_jobs_agent_id_instance_agents_id_fk" FOREIGN KEY ("agent_id") REFERENCES "public"."instance_agents"("id") ON DELETE cascade ON UPDATE cascade,
|
||||||
|
CONSTRAINT "instance_agent_scan_jobs_requested_by_auth_users_id_fk" FOREIGN KEY ("requested_by") REFERENCES "public"."auth_users"("id") ON DELETE set null ON UPDATE cascade,
|
||||||
|
CONSTRAINT "instance_agent_scan_jobs_file_id_files_id_fk" FOREIGN KEY ("file_id") REFERENCES "public"."files"("id") ON DELETE set null ON UPDATE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX "instance_agent_scan_jobs_agent_status_idx" ON "instance_agent_scan_jobs" USING btree ("agent_id","status","created_at");
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX "instance_agent_scan_jobs_tenant_idx" ON "instance_agent_scan_jobs" USING btree ("tenant_id","created_at");
|
||||||
@@ -48,6 +48,8 @@ export * from "./historyitems"
|
|||||||
export * from "./holidays"
|
export * from "./holidays"
|
||||||
export * from "./hourrates"
|
export * from "./hourrates"
|
||||||
export * from "./incominginvoices"
|
export * from "./incominginvoices"
|
||||||
|
export * from "./instance_agents"
|
||||||
|
export * from "./instance_agent_scan_jobs"
|
||||||
export * from "./inventoryitemgroups"
|
export * from "./inventoryitemgroups"
|
||||||
export * from "./inventoryitems"
|
export * from "./inventoryitems"
|
||||||
export * from "./letterheads"
|
export * from "./letterheads"
|
||||||
|
|||||||
59
backend/db/schema/instance_agent_scan_jobs.ts
Normal file
59
backend/db/schema/instance_agent_scan_jobs.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
bigint,
|
||||||
|
jsonb,
|
||||||
|
integer,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
import { files } from "./files"
|
||||||
|
import { instanceAgents } from "./instance_agents"
|
||||||
|
|
||||||
|
export const instanceAgentScanJobs = pgTable("instance_agent_scan_jobs", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
tenantId: bigint("tenant_id", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id, { onDelete: "cascade", onUpdate: "cascade" }),
|
||||||
|
|
||||||
|
agentId: uuid("agent_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => instanceAgents.id, { onDelete: "cascade", onUpdate: "cascade" }),
|
||||||
|
|
||||||
|
requestedBy: uuid("requested_by").references(() => authUsers.id, {
|
||||||
|
onDelete: "set null",
|
||||||
|
onUpdate: "cascade",
|
||||||
|
}),
|
||||||
|
|
||||||
|
status: text("status").notNull().default("pending"),
|
||||||
|
scannerName: text("scanner_name"),
|
||||||
|
requestedFilename: text("requested_filename"),
|
||||||
|
|
||||||
|
settings: jsonb("settings").notNull().default({}),
|
||||||
|
target: jsonb("target").notNull().default({}),
|
||||||
|
agentMessage: text("agent_message"),
|
||||||
|
|
||||||
|
attempts: integer("attempts").notNull().default(0),
|
||||||
|
claimedAt: timestamp("claimed_at", { withTimezone: true }),
|
||||||
|
finishedAt: timestamp("finished_at", { withTimezone: true }),
|
||||||
|
|
||||||
|
fileId: uuid("file_id").references(() => files.id, {
|
||||||
|
onDelete: "set null",
|
||||||
|
onUpdate: "cascade",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type InstanceAgentScanJob = typeof instanceAgentScanJobs.$inferSelect
|
||||||
|
export type NewInstanceAgentScanJob = typeof instanceAgentScanJobs.$inferInsert
|
||||||
38
backend/db/schema/instance_agents.ts
Normal file
38
backend/db/schema/instance_agents.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
boolean,
|
||||||
|
jsonb,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
export const instanceAgents = pgTable("instance_agents", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
description: text("description"),
|
||||||
|
|
||||||
|
tokenPrefix: text("token_prefix").notNull(),
|
||||||
|
tokenHash: text("token_hash").notNull().unique(),
|
||||||
|
|
||||||
|
active: boolean("active").notNull().default(true),
|
||||||
|
|
||||||
|
capabilities: jsonb("capabilities").notNull().default({ scan: true, print: false }),
|
||||||
|
scannerNames: jsonb("scanner_names").notNull().default([]),
|
||||||
|
printerNames: jsonb("printer_names").notNull().default([]),
|
||||||
|
|
||||||
|
lastSeenAt: timestamp("last_seen_at", { withTimezone: true }),
|
||||||
|
lastDebugInfo: jsonb("last_debug_info"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type InstanceAgent = typeof instanceAgents.$inferSelect
|
||||||
|
export type NewInstanceAgent = typeof instanceAgents.$inferInsert
|
||||||
@@ -33,6 +33,8 @@ import portalContractRoutes from "./routes/portal/contracts";
|
|||||||
import mcpRoutes from "./routes/mcp";
|
import mcpRoutes from "./routes/mcp";
|
||||||
import communicationRoutes from "./routes/communication";
|
import communicationRoutes from "./routes/communication";
|
||||||
import telephonyRoutes from "./routes/telephony";
|
import telephonyRoutes from "./routes/telephony";
|
||||||
|
import instanceAgentRoutes from "./routes/instanceAgents";
|
||||||
|
import instanceAgentGatewayRoutes from "./routes/instanceAgentGateway";
|
||||||
|
|
||||||
//Public Links
|
//Public Links
|
||||||
import publiclinksNonAuthenticatedRoutes from "./routes/publiclinks/publiclinks-non-authenticated";
|
import publiclinksNonAuthenticatedRoutes from "./routes/publiclinks/publiclinks-non-authenticated";
|
||||||
@@ -128,6 +130,10 @@ async function main() {
|
|||||||
await devicesApp.register(devicesManagementRoutes)
|
await devicesApp.register(devicesManagementRoutes)
|
||||||
},{prefix: "/devices"})
|
},{prefix: "/devices"})
|
||||||
|
|
||||||
|
await app.register(async (agentApp) => {
|
||||||
|
await agentApp.register(instanceAgentGatewayRoutes)
|
||||||
|
},{prefix: "/instance-agent"})
|
||||||
|
|
||||||
await app.register(corsPlugin);
|
await app.register(corsPlugin);
|
||||||
|
|
||||||
//Geschützte Routes
|
//Geschützte Routes
|
||||||
@@ -158,6 +164,7 @@ async function main() {
|
|||||||
await subApp.register(mcpRoutes);
|
await subApp.register(mcpRoutes);
|
||||||
await subApp.register(communicationRoutes);
|
await subApp.register(communicationRoutes);
|
||||||
await subApp.register(telephonyRoutes);
|
await subApp.register(telephonyRoutes);
|
||||||
|
await subApp.register(instanceAgentRoutes);
|
||||||
|
|
||||||
},{prefix: "/api"})
|
},{prefix: "/api"})
|
||||||
|
|
||||||
|
|||||||
235
backend/src/routes/instanceAgentGateway.ts
Normal file
235
backend/src/routes/instanceAgentGateway.ts
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import { FastifyInstance, FastifyRequest } from "fastify"
|
||||||
|
import multipart from "@fastify/multipart"
|
||||||
|
import { createHash } from "node:crypto"
|
||||||
|
import { and, asc, eq, sql } from "drizzle-orm"
|
||||||
|
import { instanceAgentScanJobs, instanceAgents } from "../../db/schema"
|
||||||
|
import { saveFile } from "../utils/files"
|
||||||
|
|
||||||
|
const hashToken = (token: string) =>
|
||||||
|
createHash("sha256").update(token, "utf8").digest("hex")
|
||||||
|
|
||||||
|
const readAgentToken = (req: FastifyRequest) => {
|
||||||
|
const headerToken = req.headers["x-agent-token"]
|
||||||
|
if (typeof headerToken === "string" && headerToken.length > 0) return headerToken
|
||||||
|
|
||||||
|
const authHeader = req.headers.authorization
|
||||||
|
if (authHeader?.startsWith("Bearer ")) return authHeader.slice(7)
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const pickFileTargets = (target: unknown) => {
|
||||||
|
if (!target || typeof target !== "object" || Array.isArray(target)) return {}
|
||||||
|
|
||||||
|
const allowedFields = [
|
||||||
|
"project",
|
||||||
|
"customer",
|
||||||
|
"contract",
|
||||||
|
"vendor",
|
||||||
|
"incominginvoice",
|
||||||
|
"plant",
|
||||||
|
"createddocument",
|
||||||
|
"vehicle",
|
||||||
|
"product",
|
||||||
|
"check",
|
||||||
|
"inventoryitem",
|
||||||
|
"space",
|
||||||
|
"documentbox",
|
||||||
|
"authProfile",
|
||||||
|
]
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(target as Record<string, any>)
|
||||||
|
.filter(([key, value]) => allowedFields.includes(key) && value !== undefined && value !== null)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function instanceAgentGatewayRoutes(server: FastifyInstance) {
|
||||||
|
await server.register(multipart, {
|
||||||
|
limits: { fileSize: 100 * 1024 * 1024 },
|
||||||
|
})
|
||||||
|
|
||||||
|
const authenticateAgent = async (req: FastifyRequest, reply: any) => {
|
||||||
|
const token = readAgentToken(req)
|
||||||
|
if (!token) {
|
||||||
|
reply.code(401).send({ error: "Agent token required" })
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const [agent] = await server.db
|
||||||
|
.select()
|
||||||
|
.from(instanceAgents)
|
||||||
|
.where(and(
|
||||||
|
eq(instanceAgents.tokenHash, hashToken(token)),
|
||||||
|
eq(instanceAgents.active, true)
|
||||||
|
))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!agent) {
|
||||||
|
reply.code(401).send({ error: "Invalid agent token" })
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return agent
|
||||||
|
}
|
||||||
|
|
||||||
|
server.post("/heartbeat", async (req, reply) => {
|
||||||
|
const agent = await authenticateAgent(req, reply)
|
||||||
|
if (!agent) return
|
||||||
|
|
||||||
|
const body = (req.body || {}) as {
|
||||||
|
capabilities?: Record<string, any>
|
||||||
|
scannerNames?: string[]
|
||||||
|
printerNames?: string[]
|
||||||
|
debugInfo?: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.db
|
||||||
|
.update(instanceAgents)
|
||||||
|
.set({
|
||||||
|
capabilities: body.capabilities || agent.capabilities,
|
||||||
|
scannerNames: body.scannerNames || agent.scannerNames,
|
||||||
|
printerNames: body.printerNames || agent.printerNames,
|
||||||
|
lastDebugInfo: body.debugInfo || null,
|
||||||
|
lastSeenAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(instanceAgents.id, agent.id))
|
||||||
|
|
||||||
|
const [pending] = await server.db
|
||||||
|
.select({ count: sql<number>`count(*)::int` })
|
||||||
|
.from(instanceAgentScanJobs)
|
||||||
|
.where(and(
|
||||||
|
eq(instanceAgentScanJobs.agentId, agent.id),
|
||||||
|
eq(instanceAgentScanJobs.status, "pending")
|
||||||
|
))
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
pendingScanJobs: pending?.count || 0,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server.get("/scan-jobs/next", async (req, reply) => {
|
||||||
|
const agent = await authenticateAgent(req, reply)
|
||||||
|
if (!agent) return
|
||||||
|
|
||||||
|
const [pendingJob] = await server.db
|
||||||
|
.select()
|
||||||
|
.from(instanceAgentScanJobs)
|
||||||
|
.where(and(
|
||||||
|
eq(instanceAgentScanJobs.agentId, agent.id),
|
||||||
|
eq(instanceAgentScanJobs.status, "pending")
|
||||||
|
))
|
||||||
|
.orderBy(asc(instanceAgentScanJobs.createdAt))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!pendingJob) return { job: null }
|
||||||
|
|
||||||
|
const [claimedJob] = await server.db
|
||||||
|
.update(instanceAgentScanJobs)
|
||||||
|
.set({
|
||||||
|
status: "running",
|
||||||
|
claimedAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
attempts: pendingJob.attempts + 1,
|
||||||
|
})
|
||||||
|
.where(and(
|
||||||
|
eq(instanceAgentScanJobs.id, pendingJob.id),
|
||||||
|
eq(instanceAgentScanJobs.status, "pending")
|
||||||
|
))
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
return { job: claimedJob || null }
|
||||||
|
})
|
||||||
|
|
||||||
|
server.post<{ Params: { id: string } }>("/scan-jobs/:id/status", async (req, reply) => {
|
||||||
|
const agent = await authenticateAgent(req, reply)
|
||||||
|
if (!agent) return
|
||||||
|
|
||||||
|
const body = (req.body || {}) as { status?: string; message?: string }
|
||||||
|
const allowedStatuses = ["running", "failed", "canceled"]
|
||||||
|
|
||||||
|
if (!body.status || !allowedStatuses.includes(body.status)) {
|
||||||
|
return reply.code(400).send({ error: "Invalid status" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const [job] = await server.db
|
||||||
|
.update(instanceAgentScanJobs)
|
||||||
|
.set({
|
||||||
|
status: body.status,
|
||||||
|
agentMessage: body.message,
|
||||||
|
finishedAt: ["failed", "canceled"].includes(body.status) ? new Date() : undefined,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(and(
|
||||||
|
eq(instanceAgentScanJobs.id, req.params.id),
|
||||||
|
eq(instanceAgentScanJobs.agentId, agent.id)
|
||||||
|
))
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
if (!job) return reply.code(404).send({ error: "Scan job not found" })
|
||||||
|
|
||||||
|
return { job }
|
||||||
|
})
|
||||||
|
|
||||||
|
server.post<{ Params: { id: string } }>("/scan-jobs/:id/upload", async (req, reply) => {
|
||||||
|
const agent = await authenticateAgent(req, reply)
|
||||||
|
if (!agent) return
|
||||||
|
|
||||||
|
const [job] = await server.db
|
||||||
|
.select()
|
||||||
|
.from(instanceAgentScanJobs)
|
||||||
|
.where(and(
|
||||||
|
eq(instanceAgentScanJobs.id, req.params.id),
|
||||||
|
eq(instanceAgentScanJobs.agentId, agent.id)
|
||||||
|
))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!job) return reply.code(404).send({ error: "Scan job not found" })
|
||||||
|
if (!["running", "pending"].includes(job.status)) {
|
||||||
|
return reply.code(409).send({ error: "Scan job is not uploadable" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: any = await req.file()
|
||||||
|
if (!data?.file) return reply.code(400).send({ error: "No file uploaded" })
|
||||||
|
|
||||||
|
const fileBuffer = await data.toBuffer()
|
||||||
|
const filename = job.requestedFilename || data.filename || `${job.id}.pdf`
|
||||||
|
|
||||||
|
const createdFile = await saveFile(
|
||||||
|
server,
|
||||||
|
job.tenantId,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
filename,
|
||||||
|
content: fileBuffer,
|
||||||
|
contentType: data.mimetype || "application/pdf",
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
...pickFileTargets(job.target),
|
||||||
|
createdBy: job.requestedBy,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!createdFile) return reply.code(500).send({ error: "Could not save scan file" })
|
||||||
|
|
||||||
|
const [updatedJob] = await server.db
|
||||||
|
.update(instanceAgentScanJobs)
|
||||||
|
.set({
|
||||||
|
status: "completed",
|
||||||
|
fileId: createdFile.id,
|
||||||
|
finishedAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(instanceAgentScanJobs.id, job.id))
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
return {
|
||||||
|
job: updatedJob,
|
||||||
|
file: createdFile,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
193
backend/src/routes/instanceAgents.ts
Normal file
193
backend/src/routes/instanceAgents.ts
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
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(),
|
||||||
|
})
|
||||||
|
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
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 })
|
||||||
|
.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,
|
||||||
|
requestedFilename: body.requestedFilename,
|
||||||
|
settings: 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 }
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user