Fix import uniqueness and add detailed request logging
This commit is contained in:
parent
d5b0af1a43
commit
717e45c11f
@ -11,6 +11,8 @@ model Server {
|
|||||||
name String
|
name String
|
||||||
ipAddress String @unique
|
ipAddress String @unique
|
||||||
description String?
|
description String?
|
||||||
|
proxyPort Int?
|
||||||
|
proxyPath String?
|
||||||
services Service[]
|
services Service[]
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@ -29,6 +31,6 @@ model Service {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@unique([serverId, port, endpoint])
|
@@unique([serverId, name, port])
|
||||||
@@index([serverId])
|
@@index([serverId])
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,12 @@
|
|||||||
const { asIntegerPort, normalizeEndpoint, normalizeIp, normalizeMethod } = require("./utils");
|
const { asIntegerPort, normalizeEndpoint, normalizeIp, normalizeMethod } = require("./utils");
|
||||||
|
|
||||||
|
const DEFAULT_SERVER = {
|
||||||
|
name: process.env.IMPORT_SERVER_NAME || "Metatron Host",
|
||||||
|
ipAddress: process.env.IMPORT_SERVER_IP || "147.93.40.215",
|
||||||
|
proxyPort: Number(process.env.IMPORT_SERVER_PROXY_PORT || 9999),
|
||||||
|
proxyPath: process.env.IMPORT_SERVER_PROXY_PATH || "/api/nginx/app",
|
||||||
|
};
|
||||||
|
|
||||||
function pickFirst(source, keys) {
|
function pickFirst(source, keys) {
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
if (source && source[key] !== undefined && source[key] !== null) return source[key];
|
if (source && source[key] !== undefined && source[key] !== null) return source[key];
|
||||||
@ -11,7 +18,7 @@ function arrayFromJson(input) {
|
|||||||
if (Array.isArray(input)) return input;
|
if (Array.isArray(input)) return input;
|
||||||
if (Array.isArray(input.servers)) return input.servers;
|
if (Array.isArray(input.servers)) return input.servers;
|
||||||
if (Array.isArray(input.items)) return input.items;
|
if (Array.isArray(input.items)) return input.items;
|
||||||
if (input.server || input.ip || input.ipAddress || input.host) return [input];
|
if (input.server || input.ip || input.ipAddress || input.host || input.domain) return [input];
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,12 +45,58 @@ function normalizeService(rawService, parentServer) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseDomainServer(rawServer, serverIndex, errors) {
|
||||||
|
const domain = String(rawServer.domain || "").trim();
|
||||||
|
if (!domain) {
|
||||||
|
errors.push({ index: serverIndex, message: "Domain is required for service import entries.", raw: rawServer });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const services = [];
|
||||||
|
rawServer.ports.forEach((port, portIndex) => {
|
||||||
|
const normalizedPort = asIntegerPort(port);
|
||||||
|
if (!normalizedPort) {
|
||||||
|
errors.push({
|
||||||
|
index: serverIndex,
|
||||||
|
serviceIndex: portIndex,
|
||||||
|
message: "Each port must be a valid number between 1 and 65535.",
|
||||||
|
raw: port,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
services.push({
|
||||||
|
name: domain,
|
||||||
|
port: normalizedPort,
|
||||||
|
endpoint: `http://${domain}:${normalizedPort}/`,
|
||||||
|
method: "POST",
|
||||||
|
requestTemplate: null,
|
||||||
|
notes: rawServer.is_backup ? "backup" : null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: DEFAULT_SERVER.name,
|
||||||
|
ipAddress: DEFAULT_SERVER.ipAddress,
|
||||||
|
description: null,
|
||||||
|
proxyPort: DEFAULT_SERVER.proxyPort,
|
||||||
|
proxyPath: DEFAULT_SERVER.proxyPath,
|
||||||
|
multiplePorts: services.length > 1,
|
||||||
|
services,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function parseServerImport(payload) {
|
function parseServerImport(payload) {
|
||||||
const sourceServers = arrayFromJson(payload);
|
const sourceServers = arrayFromJson(payload);
|
||||||
const servers = [];
|
const servers = [];
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
sourceServers.forEach((rawServer, serverIndex) => {
|
sourceServers.forEach((rawServer, serverIndex) => {
|
||||||
|
if (Array.isArray(rawServer.ports)) {
|
||||||
|
const parsed = parseDomainServer(rawServer, serverIndex, errors);
|
||||||
|
if (parsed) servers.push(parsed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const ipAddress = normalizeIp(pickFirst(rawServer, ["ipAddress", "ip", "host", "serverIp"]));
|
const ipAddress = normalizeIp(pickFirst(rawServer, ["ipAddress", "ip", "host", "serverIp"]));
|
||||||
const name = String(pickFirst(rawServer, ["name", "serverName", "title"]) || ipAddress || "Unnamed server").trim();
|
const name = String(pickFirst(rawServer, ["name", "serverName", "title"]) || ipAddress || "Unnamed server").trim();
|
||||||
const serviceList = Array.isArray(rawServer.services)
|
const serviceList = Array.isArray(rawServer.services)
|
||||||
|
|||||||
223
src/routes.js
223
src/routes.js
@ -10,14 +10,16 @@ const serverSchema = z.object({
|
|||||||
name: z.string().trim().min(1),
|
name: z.string().trim().min(1),
|
||||||
ipAddress: z.string().trim().min(1),
|
ipAddress: z.string().trim().min(1),
|
||||||
description: z.string().optional().nullable(),
|
description: z.string().optional().nullable(),
|
||||||
|
proxyPort: z.coerce.number().int().min(1).max(65535).optional().nullable(),
|
||||||
|
proxyPath: z.string().trim().optional().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const serviceSchema = z.object({
|
const serviceSchema = z.object({
|
||||||
serverId: z.string().min(1),
|
serverId: z.string().min(1),
|
||||||
name: z.string().trim().min(1),
|
name: z.string().trim().min(1),
|
||||||
port: z.coerce.number().int().min(1).max(65535),
|
port: z.coerce.number().int().min(1).max(65535),
|
||||||
endpoint: z.string().trim().min(1),
|
endpoint: z.string().trim().optional().nullable(),
|
||||||
method: z.string().optional(),
|
method: z.string().optional().nullable(),
|
||||||
requestTemplate: z.any().optional().nullable(),
|
requestTemplate: z.any().optional().nullable(),
|
||||||
notes: z.string().optional().nullable(),
|
notes: z.string().optional().nullable(),
|
||||||
});
|
});
|
||||||
@ -44,11 +46,9 @@ async function findConflicts(parsedServers) {
|
|||||||
|
|
||||||
if (!existingServer) {
|
if (!existingServer) {
|
||||||
newServers.push(server);
|
newServers.push(server);
|
||||||
newServices.push(...server.services.map((service) => ({ serverIp: server.ipAddress, ...service })));
|
} else {
|
||||||
continue;
|
const sameServer =
|
||||||
}
|
existingServer.name === server.name && (existingServer.description || null) === (server.description || null);
|
||||||
|
|
||||||
const sameServer = existingServer.name === server.name && (existingServer.description || null) === (server.description || null);
|
|
||||||
if (!sameServer) {
|
if (!sameServer) {
|
||||||
conflicts.push({
|
conflicts.push({
|
||||||
type: "server",
|
type: "server",
|
||||||
@ -64,46 +64,73 @@ async function findConflicts(parsedServers) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const service of server.services) {
|
|
||||||
const existingService = existingServer.services.find(
|
|
||||||
(item) => item.port === service.port && item.endpoint === service.endpoint,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!existingService) {
|
|
||||||
newServices.push({ serverId: existingServer.id, serverIp: server.ipAddress, ...service });
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sameService =
|
const batch = new Map();
|
||||||
existingService.name === service.name &&
|
|
||||||
existingService.method === service.method &&
|
|
||||||
JSON.stringify(existingService.requestTemplate || null) === JSON.stringify(service.requestTemplate || null);
|
|
||||||
|
|
||||||
if (sameService) {
|
for (const service of server.services) {
|
||||||
|
const prior = batch.get(service.name);
|
||||||
|
if (prior) {
|
||||||
|
if (prior.port === service.port) {
|
||||||
duplicates.push({
|
duplicates.push({
|
||||||
type: "service",
|
type: "service",
|
||||||
serverIp: server.ipAddress,
|
serverIp: server.ipAddress,
|
||||||
name: service.name,
|
name: service.name,
|
||||||
port: service.port,
|
port: service.port,
|
||||||
endpoint: service.endpoint,
|
|
||||||
});
|
});
|
||||||
} else {
|
continue;
|
||||||
|
}
|
||||||
conflicts.push({
|
conflicts.push({
|
||||||
type: "service",
|
type: "service",
|
||||||
serverIp: server.ipAddress,
|
serverIp: server.ipAddress,
|
||||||
key: `${service.port} ${service.endpoint}`,
|
key: service.name,
|
||||||
existing: existingService,
|
existing: { name: prior.name, port: prior.port },
|
||||||
incoming: service,
|
incoming: { name: service.name, port: service.port },
|
||||||
|
});
|
||||||
|
newServices.push({ serverId: existingServer?.id, serverIp: server.ipAddress, ...service });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
batch.set(service.name, service);
|
||||||
|
|
||||||
|
if (!existingServer) {
|
||||||
|
newServices.push({ serverIp: server.ipAddress, ...service });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingSamePort = existingServer.services.find(
|
||||||
|
(item) => item.name === service.name && item.port === service.port,
|
||||||
|
);
|
||||||
|
if (existingSamePort) {
|
||||||
|
duplicates.push({
|
||||||
|
type: "service",
|
||||||
|
serverIp: server.ipAddress,
|
||||||
|
name: service.name,
|
||||||
|
port: service.port,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingDiffPort = existingServer.services.find(
|
||||||
|
(item) => item.name === service.name && item.port !== service.port,
|
||||||
|
);
|
||||||
|
if (existingDiffPort) {
|
||||||
|
conflicts.push({
|
||||||
|
type: "service",
|
||||||
|
serverIp: server.ipAddress,
|
||||||
|
key: service.name,
|
||||||
|
existing: { name: existingDiffPort.name, port: existingDiffPort.port, method: existingDiffPort.method },
|
||||||
|
incoming: { name: service.name, port: service.port, method: service.method },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newServices.push({ serverId: existingServer.id, serverIp: server.ipAddress, ...service });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { conflicts, duplicates, newServers, newServices };
|
return { conflicts, duplicates, newServers, newServices };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyImport(parsedServers, mode) {
|
async function applyImport(parsedServers, mode, client = prisma) {
|
||||||
const result = {
|
const result = {
|
||||||
serversCreated: 0,
|
serversCreated: 0,
|
||||||
serversUpdated: 0,
|
serversUpdated: 0,
|
||||||
@ -112,40 +139,67 @@ async function applyImport(parsedServers, mode) {
|
|||||||
duplicatesSkipped: 0,
|
duplicatesSkipped: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const guard = async (operation) => {
|
||||||
|
try {
|
||||||
|
return await operation();
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === "P2002") {
|
||||||
|
result.duplicatesSkipped += 1;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
console.error("applyImport guard failure:", error.code || error.name, error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
for (const server of parsedServers) {
|
for (const server of parsedServers) {
|
||||||
const existingServer = await prisma.server.findUnique({
|
const existingServer = await client.server.findUnique({
|
||||||
where: { ipAddress: server.ipAddress },
|
where: { ipAddress: server.ipAddress },
|
||||||
include: { services: true },
|
include: { services: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
let savedServer = existingServer;
|
let savedServer = existingServer;
|
||||||
if (!existingServer) {
|
if (!existingServer) {
|
||||||
savedServer = await prisma.server.create({
|
savedServer = await client.server.create({
|
||||||
data: {
|
data: {
|
||||||
name: server.name,
|
name: server.name,
|
||||||
ipAddress: server.ipAddress,
|
ipAddress: server.ipAddress,
|
||||||
description: server.description,
|
description: server.description,
|
||||||
|
proxyPort: server.proxyPort ?? null,
|
||||||
|
proxyPath: server.proxyPath ?? null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
result.serversCreated += 1;
|
result.serversCreated += 1;
|
||||||
} else if (mode === "overwrite") {
|
} else if (mode === "overwrite") {
|
||||||
savedServer = await prisma.server.update({
|
savedServer = await client.server.update({
|
||||||
where: { id: existingServer.id },
|
where: { id: existingServer.id },
|
||||||
data: {
|
data: {
|
||||||
name: server.name,
|
name: server.name,
|
||||||
description: server.description,
|
description: server.description,
|
||||||
|
proxyPort: server.proxyPort ?? undefined,
|
||||||
|
proxyPath: server.proxyPath ?? undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
result.serversUpdated += 1;
|
result.serversUpdated += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const seen = new Set();
|
||||||
|
|
||||||
for (const service of server.services) {
|
for (const service of server.services) {
|
||||||
const existingService = existingServer?.services.find(
|
const key = `${service.name}:${service.port}`;
|
||||||
(item) => item.port === service.port && item.endpoint === service.endpoint,
|
if (seen.has(key)) {
|
||||||
|
result.duplicatesSkipped += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(key);
|
||||||
|
|
||||||
|
const existingSamePort = existingServer?.services.find(
|
||||||
|
(item) => item.name === service.name && item.port === service.port,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!existingService) {
|
if (!existingSamePort) {
|
||||||
await prisma.service.create({
|
await guard(() =>
|
||||||
|
client.service.create({
|
||||||
data: {
|
data: {
|
||||||
serverId: savedServer.id,
|
serverId: savedServer.id,
|
||||||
name: service.name,
|
name: service.name,
|
||||||
@ -155,18 +209,21 @@ async function applyImport(parsedServers, mode) {
|
|||||||
requestTemplate: service.requestTemplate,
|
requestTemplate: service.requestTemplate,
|
||||||
notes: service.notes,
|
notes: service.notes,
|
||||||
},
|
},
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
result.servicesCreated += 1;
|
result.servicesCreated += 1;
|
||||||
} else if (mode === "overwrite") {
|
} else if (mode === "overwrite") {
|
||||||
await prisma.service.update({
|
await guard(() =>
|
||||||
where: { id: existingService.id },
|
client.service.update({
|
||||||
|
where: { id: existingSamePort.id },
|
||||||
data: {
|
data: {
|
||||||
name: service.name,
|
name: service.name,
|
||||||
method: service.method,
|
method: service.method,
|
||||||
requestTemplate: service.requestTemplate,
|
requestTemplate: service.requestTemplate,
|
||||||
notes: service.notes,
|
notes: service.notes,
|
||||||
},
|
},
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
result.servicesUpdated += 1;
|
result.servicesUpdated += 1;
|
||||||
} else {
|
} else {
|
||||||
result.duplicatesSkipped += 1;
|
result.duplicatesSkipped += 1;
|
||||||
@ -195,10 +252,11 @@ router.get("/servers", async (req, res, next) => {
|
|||||||
|
|
||||||
router.post("/servers", async (req, res, next) => {
|
router.post("/servers", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const data = serverSchema.parse({
|
const parsed = serverSchema.parse({
|
||||||
...req.body,
|
...req.body,
|
||||||
ipAddress: normalizeIp(req.body.ipAddress),
|
ipAddress: normalizeIp(req.body.ipAddress),
|
||||||
});
|
});
|
||||||
|
const data = { ...parsed, proxyPath: parsed.proxyPath || null };
|
||||||
const server = await prisma.server.create({ data, include: includeServices() });
|
const server = await prisma.server.create({ data, include: includeServices() });
|
||||||
res.status(201).json(server);
|
res.status(201).json(server);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -212,7 +270,7 @@ router.post("/services", async (req, res, next) => {
|
|||||||
const service = await prisma.service.create({
|
const service = await prisma.service.create({
|
||||||
data: {
|
data: {
|
||||||
...parsed,
|
...parsed,
|
||||||
endpoint: normalizeEndpoint(parsed.endpoint),
|
endpoint: normalizeEndpoint(parsed.endpoint || "/"),
|
||||||
method: normalizeMethod(parsed.method),
|
method: normalizeMethod(parsed.method),
|
||||||
port: asIntegerPort(parsed.port),
|
port: asIntegerPort(parsed.port),
|
||||||
},
|
},
|
||||||
@ -223,6 +281,65 @@ router.post("/services", async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.patch("/servers/:id", async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const parsed = serverSchema.partial().parse({
|
||||||
|
...req.body,
|
||||||
|
ipAddress: req.body.ipAddress ? normalizeIp(req.body.ipAddress) : undefined,
|
||||||
|
});
|
||||||
|
const data = Object.fromEntries(Object.entries(parsed).filter(([, value]) => value !== undefined));
|
||||||
|
if (data.proxyPath === "") data.proxyPath = null;
|
||||||
|
const server = await prisma.server.update({
|
||||||
|
where: { id: req.params.id },
|
||||||
|
data,
|
||||||
|
include: includeServices(),
|
||||||
|
});
|
||||||
|
res.json(server);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete("/servers/:id", async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
await prisma.server.delete({ where: { id: req.params.id } });
|
||||||
|
res.status(204).end();
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.patch("/services/:id", async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const parsed = serviceSchema.partial().parse(req.body);
|
||||||
|
const data = Object.fromEntries(
|
||||||
|
Object.entries({
|
||||||
|
...parsed,
|
||||||
|
endpoint: parsed.endpoint ? normalizeEndpoint(parsed.endpoint) : undefined,
|
||||||
|
method: parsed.method !== undefined ? normalizeMethod(parsed.method) : undefined,
|
||||||
|
port: parsed.port !== undefined ? asIntegerPort(parsed.port) : undefined,
|
||||||
|
}).filter(([, value]) => value !== undefined),
|
||||||
|
);
|
||||||
|
const service = await prisma.service.update({
|
||||||
|
where: { id: req.params.id },
|
||||||
|
data,
|
||||||
|
include: { server: true },
|
||||||
|
});
|
||||||
|
res.json(service);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete("/services/:id", async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
await prisma.service.delete({ where: { id: req.params.id } });
|
||||||
|
res.status(204).end();
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.post("/imports/preview", async (req, res, next) => {
|
router.post("/imports/preview", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { servers, errors } = parseServerImport(req.body);
|
const { servers, errors } = parseServerImport(req.body);
|
||||||
@ -255,7 +372,10 @@ router.post("/imports/apply", async (req, res, next) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const applied = await applyImport(servers, mode);
|
const applied = await prisma.$transaction(
|
||||||
|
(tx) => applyImport(servers, mode, tx),
|
||||||
|
{ maxWait: 15000, timeout: 60000 },
|
||||||
|
);
|
||||||
res.json({ message: "Import applied.", applied });
|
res.json({ message: "Import applied.", applied });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
@ -274,11 +394,28 @@ router.post("/services/:id/reverse-proxy", async (req, res, next) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = serviceTarget(service.server, service);
|
const server = service.server;
|
||||||
|
const gateway =
|
||||||
|
server.proxyPort != null
|
||||||
|
? {
|
||||||
|
port: server.proxyPort,
|
||||||
|
path: normalizeEndpoint(server.proxyPath || "/api/nginx/app"),
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
const target = gateway
|
||||||
|
? `http://${server.ipAddress}:${gateway.port}${gateway.path}`
|
||||||
|
: serviceTarget(server, service);
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), Number(process.env.REVERSE_PROXY_TIMEOUT_MS || 15000));
|
const timeout = setTimeout(() => controller.abort(), Number(process.env.REVERSE_PROXY_TIMEOUT_MS || 15000));
|
||||||
const method = normalizeMethod(req.body.method || service.method);
|
const method = normalizeMethod(req.body.method || service.method);
|
||||||
const body = req.body.body !== undefined ? req.body.body : service.requestTemplate;
|
const suppliedBody = req.body.body !== undefined ? req.body.body : service.requestTemplate;
|
||||||
|
const body = gateway
|
||||||
|
? {
|
||||||
|
...(suppliedBody && typeof suppliedBody === "object" && !Array.isArray(suppliedBody) ? suppliedBody : {}),
|
||||||
|
domain: service.name,
|
||||||
|
port: service.port,
|
||||||
|
}
|
||||||
|
: suppliedBody;
|
||||||
|
|
||||||
const response = await fetch(target, {
|
const response = await fetch(target, {
|
||||||
method,
|
method,
|
||||||
|
|||||||
@ -24,6 +24,30 @@ app.use(
|
|||||||
);
|
);
|
||||||
app.use(express.json({ limit: "2mb" }));
|
app.use(express.json({ limit: "2mb" }));
|
||||||
|
|
||||||
|
function summarizeBody(body) {
|
||||||
|
const json = JSON.stringify(body);
|
||||||
|
if (!json) return undefined;
|
||||||
|
return json.length > 400 ? `${json.slice(0, 400)}…(+${json.length - 400} chars)` : json;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
const started = process.hrtime.bigint();
|
||||||
|
res.on("finish", () => {
|
||||||
|
const durationMs = Number(process.hrtime.bigint() - started) / 1e6;
|
||||||
|
const entry = {
|
||||||
|
ts: new Date().toISOString(),
|
||||||
|
method: req.method,
|
||||||
|
path: req.originalUrl,
|
||||||
|
status: res.statusCode,
|
||||||
|
durationMs: Number(durationMs.toFixed(2)),
|
||||||
|
bytes: Number(res.getHeader("content-length")) || undefined,
|
||||||
|
body: req.method !== "GET" && req.body && Object.keys(req.body).length > 0 ? summarizeBody(req.body) : undefined,
|
||||||
|
};
|
||||||
|
console.log(`[req] ${JSON.stringify(entry)}`);
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
app.use("/api", routes);
|
app.use("/api", routes);
|
||||||
|
|
||||||
app.use((error, req, res, next) => {
|
app.use((error, req, res, next) => {
|
||||||
@ -32,6 +56,8 @@ app.use((error, req, res, next) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.error(`[err] ${req.method} ${req.originalUrl} — ${error.code || error.name}: ${error.message}`);
|
||||||
|
|
||||||
if (error.name === "ZodError") {
|
if (error.name === "ZodError") {
|
||||||
res.status(400).json({ message: "Validation failed.", issues: error.issues });
|
res.status(400).json({ message: "Validation failed.", issues: error.issues });
|
||||||
return;
|
return;
|
||||||
@ -42,6 +68,11 @@ app.use((error, req, res, next) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error.code === "P2025") {
|
||||||
|
res.status(404).json({ message: "Record not found." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
res.status(500).json({ message: error.message || "Server error." });
|
res.status(500).json({ message: error.message || "Server error." });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user