@naisys/supervisor 3.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/naisys-supervisor +2 -0
- package/client-dist/android-chrome-192x192.png +0 -0
- package/client-dist/android-chrome-512x512.png +0 -0
- package/client-dist/apple-touch-icon.png +0 -0
- package/client-dist/assets/index-CKg0vgt5.css +1 -0
- package/client-dist/assets/index-WzoDF0aQ.js +177 -0
- package/client-dist/assets/naisys-logo-CzoPnn5I.webp +0 -0
- package/client-dist/favicon-16x16.png +0 -0
- package/client-dist/favicon-32x32.png +0 -0
- package/client-dist/favicon.ico +0 -0
- package/client-dist/index.html +49 -0
- package/client-dist/site.webmanifest +22 -0
- package/dist/api-reference.js +54 -0
- package/dist/auth-middleware.js +116 -0
- package/dist/database/hubDb.js +26 -0
- package/dist/database/supervisorDb.js +18 -0
- package/dist/error-helpers.js +13 -0
- package/dist/hateoas.js +61 -0
- package/dist/logger.js +11 -0
- package/dist/route-helpers.js +7 -0
- package/dist/routes/admin.js +209 -0
- package/dist/routes/agentChat.js +194 -0
- package/dist/routes/agentConfig.js +265 -0
- package/dist/routes/agentLifecycle.js +350 -0
- package/dist/routes/agentMail.js +171 -0
- package/dist/routes/agentRuns.js +90 -0
- package/dist/routes/agents.js +236 -0
- package/dist/routes/api.js +52 -0
- package/dist/routes/attachments.js +18 -0
- package/dist/routes/auth.js +103 -0
- package/dist/routes/costs.js +51 -0
- package/dist/routes/hosts.js +296 -0
- package/dist/routes/models.js +152 -0
- package/dist/routes/root.js +56 -0
- package/dist/routes/schemas.js +31 -0
- package/dist/routes/status.js +20 -0
- package/dist/routes/users.js +420 -0
- package/dist/routes/variables.js +103 -0
- package/dist/schema-registry.js +23 -0
- package/dist/services/agentConfigService.js +182 -0
- package/dist/services/agentHostStatusService.js +178 -0
- package/dist/services/agentService.js +291 -0
- package/dist/services/attachmentProxyService.js +131 -0
- package/dist/services/browserSocketService.js +78 -0
- package/dist/services/chatService.js +201 -0
- package/dist/services/configExportService.js +61 -0
- package/dist/services/costsService.js +127 -0
- package/dist/services/hostService.js +156 -0
- package/dist/services/hubConnectionService.js +320 -0
- package/dist/services/logFileService.js +11 -0
- package/dist/services/mailService.js +154 -0
- package/dist/services/modelService.js +92 -0
- package/dist/services/runsService.js +168 -0
- package/dist/services/userService.js +147 -0
- package/dist/services/variableService.js +23 -0
- package/dist/supervisorServer.js +221 -0
- package/package.json +79 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
|
|
6
|
+
<link rel="icon" type="image/x-icon" href="/supervisor/favicon.ico" />
|
|
7
|
+
<link
|
|
8
|
+
rel="apple-touch-icon"
|
|
9
|
+
sizes="180x180"
|
|
10
|
+
href="/supervisor/apple-touch-icon.png"
|
|
11
|
+
/>
|
|
12
|
+
|
|
13
|
+
<link
|
|
14
|
+
rel="manifest"
|
|
15
|
+
crossorigin="use-credentials"
|
|
16
|
+
href="/supervisor/site.webmanifest"
|
|
17
|
+
/>
|
|
18
|
+
<meta
|
|
19
|
+
name="viewport"
|
|
20
|
+
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
|
21
|
+
/>
|
|
22
|
+
|
|
23
|
+
<meta name="theme-color" content="#1a1b1e" />
|
|
24
|
+
<meta name="mobile-web-app-capable" content="yes" />
|
|
25
|
+
<meta
|
|
26
|
+
name="apple-mobile-web-app-status-bar-style"
|
|
27
|
+
content="black-translucent"
|
|
28
|
+
/>
|
|
29
|
+
<meta name="apple-mobile-web-app-title" content="NAISYS Supervisor" />
|
|
30
|
+
|
|
31
|
+
<meta
|
|
32
|
+
name="description"
|
|
33
|
+
content="NAISYS Supervisor - Multi-agent system management interface"
|
|
34
|
+
/>
|
|
35
|
+
<meta
|
|
36
|
+
name="keywords"
|
|
37
|
+
content="NAISYS, AI, agents, management, supervisor"
|
|
38
|
+
/>
|
|
39
|
+
<meta name="robots" content="noindex, nofollow" />
|
|
40
|
+
<meta name="format-detection" content="telephone=no" />
|
|
41
|
+
|
|
42
|
+
<title>NAISYS Supervisor</title>
|
|
43
|
+
<script type="module" crossorigin src="/supervisor/assets/index-WzoDF0aQ.js"></script>
|
|
44
|
+
<link rel="stylesheet" crossorigin href="/supervisor/assets/index-CKg0vgt5.css">
|
|
45
|
+
</head>
|
|
46
|
+
<body>
|
|
47
|
+
<div id="root"></div>
|
|
48
|
+
</body>
|
|
49
|
+
</html>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "NAISYS Supervisor",
|
|
3
|
+
"short_name": "Supervisor",
|
|
4
|
+
"icons": [
|
|
5
|
+
{
|
|
6
|
+
"src": "/supervisor/android-chrome-192x192.png",
|
|
7
|
+
"sizes": "192x192",
|
|
8
|
+
"type": "image/png"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"src": "/supervisor/android-chrome-512x512.png",
|
|
12
|
+
"sizes": "512x512",
|
|
13
|
+
"type": "image/png"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"theme_color": "#1a1b1e",
|
|
17
|
+
"background_color": "#1a1b1e",
|
|
18
|
+
"display": "standalone",
|
|
19
|
+
"start_url": "/supervisor/",
|
|
20
|
+
"scope": "/supervisor/",
|
|
21
|
+
"orientation": "portrait-primary"
|
|
22
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import scalarReference from "@scalar/fastify-api-reference";
|
|
2
|
+
import { registerAuthMiddleware } from "./auth-middleware.js";
|
|
3
|
+
/**
|
|
4
|
+
* Registers Scalar API reference and the filtered OpenAPI spec endpoint
|
|
5
|
+
* for the Supervisor service.
|
|
6
|
+
*/
|
|
7
|
+
export async function registerApiReference(fastify) {
|
|
8
|
+
// Both the reference page and spec endpoint are inside the auth scope.
|
|
9
|
+
// isPublicRoute treats /supervisor/api-reference as non-public (starts
|
|
10
|
+
// with /supervisor/api), so PUBLIC_READ=true allows GET access while
|
|
11
|
+
// PUBLIC_READ=false requires authentication.
|
|
12
|
+
await fastify.register(async (scope) => {
|
|
13
|
+
registerAuthMiddleware(scope);
|
|
14
|
+
await scope.register(scalarReference, {
|
|
15
|
+
routePrefix: "/supervisor/api-reference",
|
|
16
|
+
configuration: {
|
|
17
|
+
spec: { url: "/supervisor/api/openapi.json" },
|
|
18
|
+
theme: "kepler",
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
scope.get("/supervisor/api/openapi.json", () => {
|
|
22
|
+
const spec = fastify.swagger();
|
|
23
|
+
const filteredPaths = {};
|
|
24
|
+
for (const [path, value] of Object.entries(spec.paths || {})) {
|
|
25
|
+
if (path.startsWith("/supervisor/api/")) {
|
|
26
|
+
filteredPaths[path] = value;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
...spec,
|
|
31
|
+
paths: filteredPaths,
|
|
32
|
+
"x-tagGroups": [
|
|
33
|
+
{
|
|
34
|
+
name: "General",
|
|
35
|
+
tags: ["Discovery", "Authentication", "Hosts", "Status", "Users"],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "Agents",
|
|
39
|
+
tags: ["Agents", "Chat", "Mail", "Runs", "Attachments", "Costs"],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "Configuration",
|
|
43
|
+
tags: ["Models", "Variables"],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "Administration",
|
|
47
|
+
tags: ["Admin"],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=api-reference.js.map
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { AuthCache } from "@naisys/common";
|
|
2
|
+
import { extractBearerToken, hashToken, SESSION_COOKIE_NAME, } from "@naisys/common-node";
|
|
3
|
+
import { findAgentByApiKey } from "@naisys/hub-database";
|
|
4
|
+
import { findSession, findUserByApiKey } from "@naisys/supervisor-database";
|
|
5
|
+
import { createUserForAgent, getUserByUuid, getUserPermissions, } from "./services/userService.js";
|
|
6
|
+
const PUBLIC_PREFIXES = ["/supervisor/api/auth/login"];
|
|
7
|
+
export const authCache = new AuthCache();
|
|
8
|
+
function isPublicRoute(url) {
|
|
9
|
+
if (url === "/supervisor/api/" || url === "/supervisor/api")
|
|
10
|
+
return true;
|
|
11
|
+
for (const prefix of PUBLIC_PREFIXES) {
|
|
12
|
+
if (url.startsWith(prefix))
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
// Non-supervisor-API paths (static files, ERP routes, etc.)
|
|
16
|
+
if (!url.startsWith("/supervisor/api"))
|
|
17
|
+
return true;
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
async function buildSupervisorUser(id, username, uuid) {
|
|
21
|
+
const permissions = await getUserPermissions(id);
|
|
22
|
+
return { id, username, uuid, permissions };
|
|
23
|
+
}
|
|
24
|
+
export async function resolveUserFromToken(token) {
|
|
25
|
+
const tokenHash = hashToken(token);
|
|
26
|
+
const cacheKey = `cookie:${tokenHash}`;
|
|
27
|
+
const cached = authCache.get(cacheKey);
|
|
28
|
+
if (cached !== undefined)
|
|
29
|
+
return cached;
|
|
30
|
+
const session = await findSession(tokenHash);
|
|
31
|
+
if (!session) {
|
|
32
|
+
authCache.set(cacheKey, null);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const user = await buildSupervisorUser(session.userId, session.username, session.uuid);
|
|
36
|
+
authCache.set(cacheKey, user);
|
|
37
|
+
return user;
|
|
38
|
+
}
|
|
39
|
+
export async function resolveUserFromApiKey(apiKey) {
|
|
40
|
+
const apiKeyHash = hashToken(apiKey);
|
|
41
|
+
const cacheKey = `apikey:${apiKeyHash}`;
|
|
42
|
+
const cached = authCache.get(cacheKey);
|
|
43
|
+
if (cached !== undefined)
|
|
44
|
+
return cached;
|
|
45
|
+
// Try supervisor DB first (human users), then hub DB (agents)
|
|
46
|
+
const match = (await findUserByApiKey(apiKey)) ?? (await findAgentByApiKey(apiKey));
|
|
47
|
+
if (!match) {
|
|
48
|
+
authCache.set(cacheKey, null);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
let localUser = await getUserByUuid(match.uuid);
|
|
52
|
+
if (!localUser) {
|
|
53
|
+
localUser = await createUserForAgent(match.username, match.uuid);
|
|
54
|
+
}
|
|
55
|
+
const user = await buildSupervisorUser(localUser.id, localUser.username, localUser.uuid);
|
|
56
|
+
authCache.set(cacheKey, user);
|
|
57
|
+
return user;
|
|
58
|
+
}
|
|
59
|
+
export function registerAuthMiddleware(fastify) {
|
|
60
|
+
const publicRead = process.env.PUBLIC_READ === "true";
|
|
61
|
+
fastify.decorateRequest("supervisorUser", undefined);
|
|
62
|
+
fastify.addHook("onRequest", async (request, reply) => {
|
|
63
|
+
const token = request.cookies?.[SESSION_COOKIE_NAME];
|
|
64
|
+
if (token) {
|
|
65
|
+
const user = await resolveUserFromToken(token);
|
|
66
|
+
if (user)
|
|
67
|
+
request.supervisorUser = user;
|
|
68
|
+
}
|
|
69
|
+
// API key auth (for agents / machine-to-machine)
|
|
70
|
+
if (!request.supervisorUser) {
|
|
71
|
+
const apiKey = extractBearerToken(request.headers.authorization);
|
|
72
|
+
if (apiKey) {
|
|
73
|
+
const user = await resolveUserFromApiKey(apiKey);
|
|
74
|
+
if (user)
|
|
75
|
+
request.supervisorUser = user;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (request.supervisorUser)
|
|
79
|
+
return; // Authenticated
|
|
80
|
+
if (isPublicRoute(request.url))
|
|
81
|
+
return; // Public route
|
|
82
|
+
if (publicRead && request.method === "GET")
|
|
83
|
+
return; // Public read mode
|
|
84
|
+
reply.status(401).send({
|
|
85
|
+
statusCode: 401,
|
|
86
|
+
error: "Unauthorized",
|
|
87
|
+
message: "Authentication required",
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
export function hasPermission(user, permission) {
|
|
92
|
+
return ((user?.permissions.includes(permission) ||
|
|
93
|
+
user?.permissions.includes("supervisor_admin")) ??
|
|
94
|
+
false);
|
|
95
|
+
}
|
|
96
|
+
export function requirePermission(permission) {
|
|
97
|
+
return async (request, reply) => {
|
|
98
|
+
if (!request.supervisorUser) {
|
|
99
|
+
reply.status(401).send({
|
|
100
|
+
statusCode: 401,
|
|
101
|
+
error: "Unauthorized",
|
|
102
|
+
message: "Authentication required",
|
|
103
|
+
});
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (!hasPermission(request.supervisorUser, permission)) {
|
|
107
|
+
reply.status(403).send({
|
|
108
|
+
statusCode: 403,
|
|
109
|
+
error: "Forbidden",
|
|
110
|
+
message: `Permission '${permission}' required`,
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=auth-middleware.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createPrismaClient } from "@naisys/hub-database";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { env } from "process";
|
|
4
|
+
export function getNaisysDatabasePath() {
|
|
5
|
+
if (!env.NAISYS_FOLDER) {
|
|
6
|
+
throw new Error("NAISYS_FOLDER environment variable is not set.");
|
|
7
|
+
}
|
|
8
|
+
const dbFilename = "naisys_hub.db";
|
|
9
|
+
return path.join(env.NAISYS_FOLDER, "database", dbFilename);
|
|
10
|
+
}
|
|
11
|
+
let _db;
|
|
12
|
+
/** Lazily initialized Prisma client. First access creates the connection. */
|
|
13
|
+
export const hubDb = new Proxy({}, {
|
|
14
|
+
get(_target, prop, receiver) {
|
|
15
|
+
if (!_db) {
|
|
16
|
+
throw new Error("hubDb accessed before initialization. Ensure migrations have completed first.");
|
|
17
|
+
}
|
|
18
|
+
return Reflect.get(_db, prop, receiver);
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
export async function initHubDb() {
|
|
22
|
+
if (!_db) {
|
|
23
|
+
_db = await createPrismaClient(getNaisysDatabasePath());
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=hubDb.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createPrismaClient, supervisorDbPath, } from "@naisys/supervisor-database";
|
|
2
|
+
let _db;
|
|
3
|
+
/** Lazily initialized Prisma client. First access creates the connection. */
|
|
4
|
+
const supervisorDb = new Proxy({}, {
|
|
5
|
+
get(_target, prop, receiver) {
|
|
6
|
+
if (!_db) {
|
|
7
|
+
throw new Error("supervisorDb accessed before initialization. Ensure migrations have completed first.");
|
|
8
|
+
}
|
|
9
|
+
return Reflect.get(_db, prop, receiver);
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
export async function initSupervisorDb() {
|
|
13
|
+
if (!_db) {
|
|
14
|
+
_db = await createPrismaClient(supervisorDbPath());
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export default supervisorDb;
|
|
18
|
+
//# sourceMappingURL=supervisorDb.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function notFound(reply, message) {
|
|
2
|
+
reply.status(404);
|
|
3
|
+
return { success: false, message };
|
|
4
|
+
}
|
|
5
|
+
export function badRequest(reply, message) {
|
|
6
|
+
reply.status(400);
|
|
7
|
+
return { success: false, message };
|
|
8
|
+
}
|
|
9
|
+
export function conflict(reply, message) {
|
|
10
|
+
reply.status(409);
|
|
11
|
+
return { success: false, message };
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=error-helpers.js.map
|
package/dist/hateoas.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export const API_PREFIX = "/supervisor/api";
|
|
2
|
+
export function selfLink(path, title) {
|
|
3
|
+
return { rel: "self", href: `${API_PREFIX}${path}`, title };
|
|
4
|
+
}
|
|
5
|
+
export function collectionLink(resource) {
|
|
6
|
+
return {
|
|
7
|
+
rel: "collection",
|
|
8
|
+
href: `${API_PREFIX}/${resource}`,
|
|
9
|
+
title: resource,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function schemaLink(schemaName) {
|
|
13
|
+
return {
|
|
14
|
+
rel: "schema",
|
|
15
|
+
href: `${API_PREFIX}/schemas/${schemaName}`,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function buildQuery(page, pageSize, filters) {
|
|
19
|
+
const params = new URLSearchParams();
|
|
20
|
+
params.set("page", String(page));
|
|
21
|
+
params.set("pageSize", String(pageSize));
|
|
22
|
+
if (filters) {
|
|
23
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
24
|
+
if (value !== undefined)
|
|
25
|
+
params.set(key, value);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return params.toString();
|
|
29
|
+
}
|
|
30
|
+
export function paginationLinks(basePath, page, pageSize, total, filters) {
|
|
31
|
+
const fullPath = `${API_PREFIX}/${basePath}`;
|
|
32
|
+
const totalPages = Math.ceil(total / pageSize);
|
|
33
|
+
const links = [
|
|
34
|
+
{
|
|
35
|
+
rel: "self",
|
|
36
|
+
href: `${fullPath}?${buildQuery(page, pageSize, filters)}`,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
rel: "first",
|
|
40
|
+
href: `${fullPath}?${buildQuery(1, pageSize, filters)}`,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
rel: "last",
|
|
44
|
+
href: `${fullPath}?${buildQuery(Math.max(1, totalPages), pageSize, filters)}`,
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
if (page > 1) {
|
|
48
|
+
links.push({
|
|
49
|
+
rel: "prev",
|
|
50
|
+
href: `${fullPath}?${buildQuery(page - 1, pageSize, filters)}`,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (page < totalPages) {
|
|
54
|
+
links.push({
|
|
55
|
+
rel: "next",
|
|
56
|
+
href: `${fullPath}?${buildQuery(page + 1, pageSize, filters)}`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return links;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=hateoas.js.map
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
let _logger;
|
|
2
|
+
export function initLogger(logger) {
|
|
3
|
+
_logger = logger;
|
|
4
|
+
}
|
|
5
|
+
export function getLogger() {
|
|
6
|
+
if (!_logger) {
|
|
7
|
+
throw new Error("Logger not initialized. Call initLogger() first.");
|
|
8
|
+
}
|
|
9
|
+
return _logger;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { permGate, resolveActions as resolveActionsBase, } from "@naisys/common";
|
|
2
|
+
import { hasPermission } from "./auth-middleware.js";
|
|
3
|
+
export { permGate };
|
|
4
|
+
export function resolveActions(defs, baseHref, ctx) {
|
|
5
|
+
return resolveActionsBase(defs, baseHref, ctx, (perm) => hasPermission(ctx.user, perm));
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=route-helpers.js.map
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { supervisorDbPath } from "@naisys/supervisor-database";
|
|
3
|
+
import { AdminAttachmentListRequestSchema, AdminAttachmentListResponseSchema, AdminInfoResponseSchema, ErrorResponseSchema, RotateAccessKeyResultSchema, ServerLogRequestSchema, ServerLogResponseSchema, } from "@naisys/supervisor-shared";
|
|
4
|
+
import archiver from "archiver";
|
|
5
|
+
import { hasPermission, requirePermission } from "../auth-middleware.js";
|
|
6
|
+
import { getNaisysDatabasePath, hubDb } from "../database/hubDb.js";
|
|
7
|
+
import { API_PREFIX, paginationLinks } from "../hateoas.js";
|
|
8
|
+
import { buildExportFiles, } from "../services/configExportService.js";
|
|
9
|
+
import { getHubAccessKey, isHubConnected, sendRotateAccessKey, } from "../services/hubConnectionService.js";
|
|
10
|
+
import { getLogFilePath, tailLogFile } from "../services/logFileService.js";
|
|
11
|
+
function adminActions(hasAdminPermission) {
|
|
12
|
+
const actions = [];
|
|
13
|
+
if (hasAdminPermission) {
|
|
14
|
+
actions.push({
|
|
15
|
+
rel: "export-config",
|
|
16
|
+
href: `${API_PREFIX}/admin/export-config`,
|
|
17
|
+
method: "GET",
|
|
18
|
+
title: "Export Config",
|
|
19
|
+
}, {
|
|
20
|
+
rel: "view-logs",
|
|
21
|
+
href: `${API_PREFIX}/admin/logs`,
|
|
22
|
+
method: "GET",
|
|
23
|
+
title: "View Logs",
|
|
24
|
+
}, {
|
|
25
|
+
rel: "view-attachments",
|
|
26
|
+
href: `${API_PREFIX}/admin/attachments`,
|
|
27
|
+
method: "GET",
|
|
28
|
+
title: "View Attachments",
|
|
29
|
+
});
|
|
30
|
+
if (getHubAccessKey()) {
|
|
31
|
+
actions.push({
|
|
32
|
+
rel: "rotate-access-key",
|
|
33
|
+
href: `${API_PREFIX}/admin/rotate-access-key`,
|
|
34
|
+
method: "POST",
|
|
35
|
+
title: "Rotate Hub Access Key",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return actions;
|
|
40
|
+
}
|
|
41
|
+
export default function adminRoutes(fastify, _options) {
|
|
42
|
+
// GET / — Admin info
|
|
43
|
+
fastify.get("/", {
|
|
44
|
+
preHandler: [requirePermission("supervisor_admin")],
|
|
45
|
+
schema: {
|
|
46
|
+
description: "Get admin system info",
|
|
47
|
+
tags: ["Admin"],
|
|
48
|
+
response: {
|
|
49
|
+
200: AdminInfoResponseSchema,
|
|
50
|
+
500: ErrorResponseSchema,
|
|
51
|
+
},
|
|
52
|
+
security: [{ cookieAuth: [] }],
|
|
53
|
+
},
|
|
54
|
+
}, async (request, _reply) => {
|
|
55
|
+
const hasAdminPermission = hasPermission(request.supervisorUser, "supervisor_admin");
|
|
56
|
+
const actions = adminActions(hasAdminPermission);
|
|
57
|
+
const [supervisorDbSize, hubDbSize] = await Promise.all([
|
|
58
|
+
fs
|
|
59
|
+
.stat(supervisorDbPath())
|
|
60
|
+
.then((s) => s.size)
|
|
61
|
+
.catch(() => undefined),
|
|
62
|
+
fs
|
|
63
|
+
.stat(getNaisysDatabasePath())
|
|
64
|
+
.then((s) => s.size)
|
|
65
|
+
.catch(() => undefined),
|
|
66
|
+
]);
|
|
67
|
+
return {
|
|
68
|
+
supervisorDbPath: supervisorDbPath(),
|
|
69
|
+
supervisorDbSize,
|
|
70
|
+
hubDbPath: getNaisysDatabasePath(),
|
|
71
|
+
hubDbSize,
|
|
72
|
+
hubConnected: isHubConnected(),
|
|
73
|
+
hubAccessKey: getHubAccessKey(),
|
|
74
|
+
_actions: actions.length > 0 ? actions : undefined,
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
// GET /export-config — Download config zip
|
|
78
|
+
fastify.get("/export-config", {
|
|
79
|
+
preHandler: [requirePermission("supervisor_admin")],
|
|
80
|
+
schema: {
|
|
81
|
+
description: "Export configuration as a zip file",
|
|
82
|
+
tags: ["Admin"],
|
|
83
|
+
security: [{ cookieAuth: [] }],
|
|
84
|
+
},
|
|
85
|
+
}, async (_request, reply) => {
|
|
86
|
+
const users = (await hubDb.users.findMany({
|
|
87
|
+
select: {
|
|
88
|
+
id: true,
|
|
89
|
+
username: true,
|
|
90
|
+
title: true,
|
|
91
|
+
config: true,
|
|
92
|
+
lead_user_id: true,
|
|
93
|
+
archived: true,
|
|
94
|
+
},
|
|
95
|
+
}));
|
|
96
|
+
const variables = await hubDb.variables.findMany({
|
|
97
|
+
select: { key: true, value: true },
|
|
98
|
+
orderBy: { key: "asc" },
|
|
99
|
+
});
|
|
100
|
+
const modelRows = (await hubDb.models.findMany());
|
|
101
|
+
const exportFiles = buildExportFiles(users, variables, modelRows);
|
|
102
|
+
reply.header("Content-Disposition", 'attachment; filename="naisys-config.zip"');
|
|
103
|
+
reply.type("application/zip");
|
|
104
|
+
const archive = archiver("zip", { zlib: { level: 9 } });
|
|
105
|
+
archive.on("error", (err) => {
|
|
106
|
+
reply.log.error(err, "Archiver error");
|
|
107
|
+
});
|
|
108
|
+
for (const file of exportFiles) {
|
|
109
|
+
archive.append(file.content, { name: file.path });
|
|
110
|
+
}
|
|
111
|
+
await archive.finalize();
|
|
112
|
+
return reply.send(archive);
|
|
113
|
+
});
|
|
114
|
+
// POST /rotate-access-key — Rotate hub access key
|
|
115
|
+
fastify.post("/rotate-access-key", {
|
|
116
|
+
preHandler: [requirePermission("supervisor_admin")],
|
|
117
|
+
schema: {
|
|
118
|
+
description: "Rotate the hub access key",
|
|
119
|
+
tags: ["Admin"],
|
|
120
|
+
response: {
|
|
121
|
+
200: RotateAccessKeyResultSchema,
|
|
122
|
+
500: ErrorResponseSchema,
|
|
123
|
+
},
|
|
124
|
+
security: [{ cookieAuth: [] }],
|
|
125
|
+
},
|
|
126
|
+
}, async (_request, reply) => {
|
|
127
|
+
try {
|
|
128
|
+
const result = await sendRotateAccessKey();
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
reply.log.error(error, "Error in POST /admin/rotate-access-key route");
|
|
133
|
+
return reply.status(500).send({
|
|
134
|
+
success: false,
|
|
135
|
+
message: error instanceof Error
|
|
136
|
+
? error.message
|
|
137
|
+
: "Failed to rotate access key",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
// GET /logs — Tail server log files
|
|
142
|
+
fastify.get("/logs", {
|
|
143
|
+
preHandler: [requirePermission("supervisor_admin")],
|
|
144
|
+
schema: {
|
|
145
|
+
description: "Get tail of a server log file",
|
|
146
|
+
tags: ["Admin"],
|
|
147
|
+
querystring: ServerLogRequestSchema,
|
|
148
|
+
response: {
|
|
149
|
+
200: ServerLogResponseSchema,
|
|
150
|
+
500: ErrorResponseSchema,
|
|
151
|
+
},
|
|
152
|
+
security: [{ cookieAuth: [] }],
|
|
153
|
+
},
|
|
154
|
+
}, async (request, _reply) => {
|
|
155
|
+
const { file, lines, minLevel } = request.query;
|
|
156
|
+
const cappedLines = Math.min(lines, 1000);
|
|
157
|
+
const filePath = getLogFilePath(file);
|
|
158
|
+
const { entries, fileSize } = await tailLogFile(filePath, cappedLines, minLevel);
|
|
159
|
+
return {
|
|
160
|
+
entries,
|
|
161
|
+
fileName: `${file}.log`,
|
|
162
|
+
fileSize,
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
// GET /attachments — List all attachments
|
|
166
|
+
fastify.get("/attachments", {
|
|
167
|
+
preHandler: [requirePermission("supervisor_admin")],
|
|
168
|
+
schema: {
|
|
169
|
+
description: "List all uploaded attachments",
|
|
170
|
+
tags: ["Admin"],
|
|
171
|
+
querystring: AdminAttachmentListRequestSchema,
|
|
172
|
+
response: {
|
|
173
|
+
200: AdminAttachmentListResponseSchema,
|
|
174
|
+
500: ErrorResponseSchema,
|
|
175
|
+
},
|
|
176
|
+
security: [{ cookieAuth: [] }],
|
|
177
|
+
},
|
|
178
|
+
}, async (request, _reply) => {
|
|
179
|
+
const { page, pageSize } = request.query;
|
|
180
|
+
const skip = (page - 1) * pageSize;
|
|
181
|
+
const [rows, total] = await Promise.all([
|
|
182
|
+
hubDb.attachments.findMany({
|
|
183
|
+
orderBy: { created_at: "desc" },
|
|
184
|
+
include: {
|
|
185
|
+
uploader: { select: { username: true } },
|
|
186
|
+
},
|
|
187
|
+
skip,
|
|
188
|
+
take: pageSize,
|
|
189
|
+
}),
|
|
190
|
+
hubDb.attachments.count(),
|
|
191
|
+
]);
|
|
192
|
+
return {
|
|
193
|
+
attachments: rows.map((r) => ({
|
|
194
|
+
id: r.public_id,
|
|
195
|
+
filename: r.filename,
|
|
196
|
+
fileSize: r.file_size,
|
|
197
|
+
fileHash: r.file_hash,
|
|
198
|
+
purpose: r.purpose,
|
|
199
|
+
uploadedBy: r.uploader.username,
|
|
200
|
+
createdAt: r.created_at.toISOString(),
|
|
201
|
+
})),
|
|
202
|
+
total,
|
|
203
|
+
page,
|
|
204
|
+
pageSize,
|
|
205
|
+
_links: paginationLinks("admin/attachments", page, pageSize, total),
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=admin.js.map
|