feat: integrate todo note microservice and modularize frontend
This commit is contained in:
@@ -32,6 +32,8 @@ interface RuntimeConfig {
|
||||
providerToken: string;
|
||||
heartbeatTimeoutMs: number;
|
||||
taskPendingTimeoutMs: number;
|
||||
databaseVolumeName: string;
|
||||
databaseVolumeSize: string;
|
||||
microservices: MicroserviceConfig[];
|
||||
logFile: string;
|
||||
}
|
||||
@@ -56,6 +58,7 @@ interface MicroserviceConfig {
|
||||
proxyMode: string;
|
||||
frontendOnly: boolean;
|
||||
public: boolean;
|
||||
allowedMethods: string[];
|
||||
allowedPathPrefixes: string[];
|
||||
healthPath: string;
|
||||
timeoutMs: number;
|
||||
@@ -180,6 +183,7 @@ function parseMicroserviceConfig(value: unknown, index: number): MicroserviceCon
|
||||
proxyMode: stringFromRecord(backend, "proxyMode", `${path}.backend`),
|
||||
frontendOnly: booleanFromRecord(backend, "frontendOnly", `${path}.backend`),
|
||||
public: booleanFromRecord(backend, "public", `${path}.backend`),
|
||||
allowedMethods: stringArrayFromRecord(backend, "allowedMethods", `${path}.backend`).map((method) => method.toUpperCase()),
|
||||
allowedPathPrefixes: stringArrayFromRecord(backend, "allowedPathPrefixes", `${path}.backend`),
|
||||
healthPath: stringFromRecord(backend, "healthPath", `${path}.backend`),
|
||||
timeoutMs: numberFromRecord(backend, "timeoutMs", `${path}.backend`),
|
||||
@@ -212,6 +216,8 @@ function readConfig(): RuntimeConfig {
|
||||
providerToken: requiredEnv("PROVIDER_TOKEN"),
|
||||
heartbeatTimeoutMs: readNumberEnv("HEARTBEAT_TIMEOUT_MS"),
|
||||
taskPendingTimeoutMs: readOptionalNumberEnv("TASK_PENDING_TIMEOUT_MS", 10 * 60 * 1000),
|
||||
databaseVolumeName: requiredEnv("DATABASE_VOLUME_NAME"),
|
||||
databaseVolumeSize: requiredEnv("DATABASE_VOLUME_SIZE"),
|
||||
microservices: readMicroservicesEnv(),
|
||||
logFile: requiredEnv("LOG_FILE"),
|
||||
};
|
||||
@@ -242,7 +248,7 @@ function jsonResponse(body: unknown, status = 200): Response {
|
||||
headers: {
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"access-control-allow-origin": "*",
|
||||
"access-control-allow-methods": "GET,POST,OPTIONS",
|
||||
"access-control-allow-methods": "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS",
|
||||
"access-control-allow-headers": "content-type,x-provider-token",
|
||||
},
|
||||
});
|
||||
@@ -832,16 +838,35 @@ async function countPendingTasks(): Promise<number> {
|
||||
return Number(rows[0]?.count ?? 0);
|
||||
}
|
||||
|
||||
async function getPgdataUsage(): Promise<JsonValue> {
|
||||
const rows = await sql<Array<{ database_name: string; database_bytes: string | number; database_pretty: string }>>`
|
||||
SELECT
|
||||
current_database()::text AS database_name,
|
||||
pg_database_size(current_database())::bigint AS database_bytes,
|
||||
pg_size_pretty(pg_database_size(current_database()))::text AS database_pretty
|
||||
`;
|
||||
const row = rows[0];
|
||||
return {
|
||||
volumeName: config.databaseVolumeName,
|
||||
volumeSize: config.databaseVolumeSize,
|
||||
databaseName: row?.database_name ?? "unknown",
|
||||
databaseBytes: Number(row?.database_bytes ?? 0),
|
||||
databasePretty: row?.database_pretty ?? "--",
|
||||
};
|
||||
}
|
||||
|
||||
async function getOverview(): Promise<JsonValue> {
|
||||
const nodes = await getNodes();
|
||||
const pendingTasks = await countPendingTasks();
|
||||
const dockerStatuses = await getNodeDockerStatuses();
|
||||
const systemStatuses = await getNodeSystemStatuses(1);
|
||||
const pgdata = await getPgdataUsage();
|
||||
const online = nodes.filter((node) => node.status === "online").length;
|
||||
return {
|
||||
service: "unidesk-core",
|
||||
ok: true,
|
||||
dbReady,
|
||||
pgdata,
|
||||
uptimeSeconds: Math.floor((Date.now() - serviceStartedAt.getTime()) / 1000),
|
||||
nodeCount: nodes.length,
|
||||
onlineNodeCount: online,
|
||||
@@ -948,6 +973,10 @@ function isMicroservicePathAllowed(service: MicroserviceConfig, path: string): b
|
||||
return service.backend.allowedPathPrefixes.some((prefix) => path === prefix || path.startsWith(prefix));
|
||||
}
|
||||
|
||||
function isMicroserviceMethodAllowed(service: MicroserviceConfig, method: string): boolean {
|
||||
return service.backend.allowedMethods.includes(method.toUpperCase());
|
||||
}
|
||||
|
||||
function readMicroserviceArrayLimits(url: URL): { query: string; jsonArrayLimits: Record<string, JsonValue> } {
|
||||
const params = new URLSearchParams(url.searchParams);
|
||||
const jsonArrayLimits: Record<string, JsonValue> = {};
|
||||
@@ -988,8 +1017,10 @@ async function microserviceRoute(req: Request, url: URL): Promise<Response> {
|
||||
const suffix = slashIndex === -1 ? "" : rest.slice(slashIndex + 1);
|
||||
const service = microserviceById(serviceId);
|
||||
if (service === null) return jsonResponse({ ok: false, error: `microservice not found: ${serviceId}` }, 404);
|
||||
if (suffix === "" || suffix === "status") return jsonResponse({ ok: true, microservice: (await getMicroservices()).find((item) => recordValue(item, "id") === serviceId) ?? service });
|
||||
if (req.method !== "GET" && req.method !== "HEAD") return jsonResponse({ ok: false, error: "microservice frontend proxy only supports GET/HEAD" }, 405);
|
||||
const method = req.method.toUpperCase();
|
||||
if ((suffix === "" || suffix === "status") && (method === "GET" || method === "HEAD")) {
|
||||
return jsonResponse({ ok: true, microservice: (await getMicroservices()).find((item) => recordValue(item, "id") === serviceId) ?? service });
|
||||
}
|
||||
|
||||
const proxyPrefix = "proxy";
|
||||
const targetPath = suffix === "health"
|
||||
@@ -1000,6 +1031,12 @@ async function microserviceRoute(req: Request, url: URL): Promise<Response> {
|
||||
? `/${suffix.slice(proxyPrefix.length + 1)}`
|
||||
: "";
|
||||
if (targetPath.length === 0) return jsonResponse({ ok: false, error: "microservice route must be /status, /health, or /proxy/<path>" }, 404);
|
||||
if (suffix === "health" && method !== "GET" && method !== "HEAD") {
|
||||
return jsonResponse({ ok: false, error: "microservice health only supports GET/HEAD" }, 405);
|
||||
}
|
||||
if (!isMicroserviceMethodAllowed(service, method)) {
|
||||
return jsonResponse({ ok: false, error: "microservice method is not allowed", serviceId, method, allowedMethods: service.backend.allowedMethods }, 405);
|
||||
}
|
||||
if (!isMicroservicePathAllowed(service, targetPath)) {
|
||||
return jsonResponse({ ok: false, error: "microservice path is not allowed", serviceId, targetPath }, 403);
|
||||
}
|
||||
@@ -1007,13 +1044,22 @@ async function microserviceRoute(req: Request, url: URL): Promise<Response> {
|
||||
return jsonResponse({ ok: false, error: `provider does not declare microservice.http capability: ${service.providerId}` }, 409);
|
||||
}
|
||||
const proxyOptions = readMicroserviceArrayLimits(url);
|
||||
const bodyText = method === "GET" || method === "HEAD" ? "" : await req.text();
|
||||
if (bodyText.length > 1024 * 1024) {
|
||||
return jsonResponse({ ok: false, error: "microservice request body is too large", maxBytes: 1024 * 1024 }, 413);
|
||||
}
|
||||
const requestHeaders: Record<string, JsonValue> = {};
|
||||
const contentType = req.headers.get("content-type");
|
||||
if (contentType !== null) requestHeaders["content-type"] = contentType.slice(0, 200);
|
||||
const { taskId, providerOnline } = await createAndSendTask(service.providerId, "microservice.http", {
|
||||
source: "microservice-frontend-proxy",
|
||||
serviceId: service.id,
|
||||
method: req.method,
|
||||
method,
|
||||
targetBaseUrl: service.backend.nodeBaseUrl,
|
||||
path: targetPath,
|
||||
query: proxyOptions.query,
|
||||
requestHeaders,
|
||||
bodyText,
|
||||
jsonArrayLimits: proxyOptions.jsonArrayLimits,
|
||||
timeoutMs: service.backend.timeoutMs,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user