apteva 0.4.3 → 0.4.5
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/dist/App.y11xqt9m.js +227 -0
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/db.ts +93 -19
- package/src/integrations/agentdojo.ts +350 -0
- package/src/openapi.ts +195 -0
- package/src/providers.ts +78 -7
- package/src/routes/api/agent-utils.ts +638 -0
- package/src/routes/api/agents.ts +743 -0
- package/src/routes/api/helpers.ts +12 -0
- package/src/routes/api/integrations.ts +608 -0
- package/src/routes/api/mcp.ts +377 -0
- package/src/routes/api/meta-agent.ts +145 -0
- package/src/routes/api/projects.ts +95 -0
- package/src/routes/api/providers.ts +269 -0
- package/src/routes/api/skills.ts +538 -0
- package/src/routes/api/system.ts +215 -0
- package/src/routes/api/telemetry.ts +142 -0
- package/src/routes/api/users.ts +148 -0
- package/src/routes/api.ts +32 -3474
- package/src/server.ts +1 -1
- package/src/web/components/api/ApiDocsPage.tsx +259 -0
- package/src/web/components/mcp/IntegrationsPanel.tsx +15 -8
- package/src/web/components/mcp/McpPage.tsx +458 -174
- package/src/web/components/settings/SettingsPage.tsx +275 -36
- package/src/web/components/skills/SkillsPage.tsx +330 -1
- package/src/web/components/tasks/TasksPage.tsx +187 -58
- package/src/web/context/TelemetryContext.tsx +14 -1
- package/src/web/hooks/useAgents.ts +9 -0
- package/src/web/types.ts +22 -4
- package/dist/App.mbp9atpm.js +0 -227
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { json } from "./helpers";
|
|
2
|
+
import { META_AGENT_ENABLED, fetchFromAgent, startAgentProcess, setAgentStatus } from "./agent-utils";
|
|
3
|
+
import { AgentDB } from "../../db";
|
|
4
|
+
import { ProviderKeys } from "../../providers";
|
|
5
|
+
import { agentProcesses, getBinaryStatus, BIN_DIR } from "../../server";
|
|
6
|
+
import {
|
|
7
|
+
checkForUpdates,
|
|
8
|
+
getInstalledVersion,
|
|
9
|
+
getAptevaVersion,
|
|
10
|
+
downloadLatestBinary,
|
|
11
|
+
installViaNpm,
|
|
12
|
+
} from "../../binary";
|
|
13
|
+
import { openApiSpec } from "../../openapi";
|
|
14
|
+
|
|
15
|
+
export async function handleSystemRoutes(
|
|
16
|
+
req: Request,
|
|
17
|
+
path: string,
|
|
18
|
+
method: string,
|
|
19
|
+
authContext?: unknown,
|
|
20
|
+
): Promise<Response | null> {
|
|
21
|
+
// GET /api/health - Health check endpoint (no auth required)
|
|
22
|
+
if (path === "/api/health" && method === "GET") {
|
|
23
|
+
const binaryStatus = getBinaryStatus(BIN_DIR);
|
|
24
|
+
const installedVersion = getInstalledVersion();
|
|
25
|
+
return json({
|
|
26
|
+
status: "ok",
|
|
27
|
+
version: getAptevaVersion(),
|
|
28
|
+
timestamp: new Date().toISOString(),
|
|
29
|
+
agents: {
|
|
30
|
+
total: AgentDB.count(),
|
|
31
|
+
running: AgentDB.countRunning(),
|
|
32
|
+
},
|
|
33
|
+
binary: {
|
|
34
|
+
available: binaryStatus.exists,
|
|
35
|
+
platform: binaryStatus.platform,
|
|
36
|
+
arch: binaryStatus.arch,
|
|
37
|
+
version: installedVersion,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// GET /api/features - Feature flags (no auth required)
|
|
43
|
+
if (path === "/api/features" && method === "GET") {
|
|
44
|
+
return json({
|
|
45
|
+
projects: process.env.PROJECTS_ENABLED === "true",
|
|
46
|
+
metaAgent: process.env.META_AGENT_ENABLED === "true",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// GET /api/openapi - OpenAPI spec (no auth required)
|
|
51
|
+
if (path === "/api/openapi" && method === "GET") {
|
|
52
|
+
return json(openApiSpec);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// GET /api/stats - Get statistics
|
|
56
|
+
if (path === "/api/stats" && method === "GET") {
|
|
57
|
+
return json({
|
|
58
|
+
totalAgents: AgentDB.count(),
|
|
59
|
+
runningAgents: AgentDB.countRunning(),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// GET /api/binary - Get binary status
|
|
64
|
+
if (path === "/api/binary" && method === "GET") {
|
|
65
|
+
return json(getBinaryStatus(BIN_DIR));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// GET /api/version - Check agent binary version info
|
|
69
|
+
if (path === "/api/version" && method === "GET") {
|
|
70
|
+
const versionInfo = await checkForUpdates();
|
|
71
|
+
return json(versionInfo);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// POST /api/version/update - Download/install latest agent binary
|
|
75
|
+
if (path === "/api/version/update" && method === "POST") {
|
|
76
|
+
// Get all running agents to restart later
|
|
77
|
+
const runningAgents = AgentDB.findAll().filter(a => a.status === "running");
|
|
78
|
+
const agentsToRestart = runningAgents.map(a => a.id);
|
|
79
|
+
|
|
80
|
+
// Stop all running agents
|
|
81
|
+
for (const agent of runningAgents) {
|
|
82
|
+
const agentProc = agentProcesses.get(agent.id);
|
|
83
|
+
if (agentProc) {
|
|
84
|
+
console.log(`Stopping agent ${agent.name} for update...`);
|
|
85
|
+
agentProc.proc.kill();
|
|
86
|
+
agentProcesses.delete(agent.id);
|
|
87
|
+
}
|
|
88
|
+
setAgentStatus(agent.id, "stopped", "binary_update");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Try npm install first, fall back to direct download
|
|
92
|
+
let result = await installViaNpm();
|
|
93
|
+
if (!result.success) {
|
|
94
|
+
// Fall back to direct download
|
|
95
|
+
result = await downloadLatestBinary(BIN_DIR);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!result.success) {
|
|
99
|
+
return json({ success: false, error: result.error }, 500);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Restart agents that were running
|
|
103
|
+
const restartResults: { id: string; name: string; success: boolean; error?: string }[] = [];
|
|
104
|
+
for (const agentId of agentsToRestart) {
|
|
105
|
+
const agent = AgentDB.findById(agentId);
|
|
106
|
+
if (agent) {
|
|
107
|
+
console.log(`Restarting agent ${agent.name} after update...`);
|
|
108
|
+
const startResult = await startAgentProcess(agent);
|
|
109
|
+
restartResults.push({
|
|
110
|
+
id: agent.id,
|
|
111
|
+
name: agent.name,
|
|
112
|
+
success: startResult.success,
|
|
113
|
+
error: startResult.error,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return json({
|
|
119
|
+
success: true,
|
|
120
|
+
version: result.version,
|
|
121
|
+
restarted: restartResults,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// GET /api/tasks - Get all tasks from all running agents
|
|
126
|
+
if (path === "/api/tasks" && method === "GET") {
|
|
127
|
+
const url = new URL(req.url);
|
|
128
|
+
const status = url.searchParams.get("status") || "all";
|
|
129
|
+
|
|
130
|
+
const runningAgents = AgentDB.findAll().filter(a => a.status === "running" && a.port);
|
|
131
|
+
const allTasks: any[] = [];
|
|
132
|
+
|
|
133
|
+
for (const agent of runningAgents) {
|
|
134
|
+
const data = await fetchFromAgent(agent.id, agent.port!, `/tasks?status=${status}`);
|
|
135
|
+
if (data?.tasks) {
|
|
136
|
+
// Add agent info to each task
|
|
137
|
+
for (const task of data.tasks) {
|
|
138
|
+
allTasks.push({
|
|
139
|
+
...task,
|
|
140
|
+
agentId: agent.id,
|
|
141
|
+
agentName: agent.name,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Sort by created_at descending
|
|
148
|
+
allTasks.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
149
|
+
|
|
150
|
+
return json({ tasks: allTasks, count: allTasks.length });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// GET /api/tasks/:agentId/:taskId - Get a single task with full details
|
|
154
|
+
const singleTaskMatch = path.match(/^\/api\/tasks\/([^/]+)\/([^/]+)$/);
|
|
155
|
+
if (singleTaskMatch && method === "GET") {
|
|
156
|
+
const [, agentId, taskId] = singleTaskMatch;
|
|
157
|
+
const agent = AgentDB.findById(agentId);
|
|
158
|
+
|
|
159
|
+
if (!agent) {
|
|
160
|
+
return json({ error: "Agent not found" }, 404);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (agent.status !== "running" || !agent.port) {
|
|
164
|
+
return json({ error: "Agent is not running" }, 400);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const data = await fetchFromAgent(agent.id, agent.port, `/tasks/${taskId}`);
|
|
168
|
+
if (!data) {
|
|
169
|
+
return json({ error: "Failed to fetch task from agent" }, 500);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return json({ task: { ...data, agentId: agent.id, agentName: agent.name } });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// GET /api/dashboard - Get dashboard statistics
|
|
176
|
+
if (path === "/api/dashboard" && method === "GET") {
|
|
177
|
+
const agents = AgentDB.findAll();
|
|
178
|
+
const runningAgents = agents.filter(a => a.status === "running" && a.port);
|
|
179
|
+
|
|
180
|
+
let totalTasks = 0;
|
|
181
|
+
let pendingTasks = 0;
|
|
182
|
+
let completedTasks = 0;
|
|
183
|
+
let runningTasks = 0;
|
|
184
|
+
|
|
185
|
+
for (const agent of runningAgents) {
|
|
186
|
+
const data = await fetchFromAgent(agent.id, agent.port!, "/tasks?status=all");
|
|
187
|
+
if (data?.tasks) {
|
|
188
|
+
totalTasks += data.tasks.length;
|
|
189
|
+
for (const task of data.tasks) {
|
|
190
|
+
if (task.status === "pending") pendingTasks++;
|
|
191
|
+
else if (task.status === "completed") completedTasks++;
|
|
192
|
+
else if (task.status === "running") runningTasks++;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return json({
|
|
198
|
+
agents: {
|
|
199
|
+
total: agents.length,
|
|
200
|
+
running: runningAgents.length,
|
|
201
|
+
},
|
|
202
|
+
tasks: {
|
|
203
|
+
total: totalTasks,
|
|
204
|
+
pending: pendingTasks,
|
|
205
|
+
running: runningTasks,
|
|
206
|
+
completed: completedTasks,
|
|
207
|
+
},
|
|
208
|
+
providers: {
|
|
209
|
+
configured: ProviderKeys.getConfiguredProviders().length,
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { json } from "./helpers";
|
|
2
|
+
import { TelemetryDB } from "../../db";
|
|
3
|
+
import { telemetryBroadcaster, type TelemetryEvent } from "../../server";
|
|
4
|
+
|
|
5
|
+
export async function handleTelemetryRoutes(
|
|
6
|
+
req: Request,
|
|
7
|
+
path: string,
|
|
8
|
+
method: string,
|
|
9
|
+
): Promise<Response | null> {
|
|
10
|
+
// POST /api/telemetry - Receive telemetry events from agents
|
|
11
|
+
if (path === "/api/telemetry" && method === "POST") {
|
|
12
|
+
try {
|
|
13
|
+
const body = await req.json() as {
|
|
14
|
+
agent_id: string;
|
|
15
|
+
sent_at: string;
|
|
16
|
+
events: Array<{
|
|
17
|
+
id: string;
|
|
18
|
+
timestamp: string;
|
|
19
|
+
category: string;
|
|
20
|
+
type: string;
|
|
21
|
+
level: string;
|
|
22
|
+
trace_id?: string;
|
|
23
|
+
span_id?: string;
|
|
24
|
+
thread_id?: string;
|
|
25
|
+
data?: Record<string, unknown>;
|
|
26
|
+
metadata?: Record<string, unknown>;
|
|
27
|
+
duration_ms?: number;
|
|
28
|
+
error?: string;
|
|
29
|
+
}>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
if (!body.agent_id || !body.events) {
|
|
33
|
+
return json({ error: "agent_id and events are required" }, 400);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Filter out debug events - too noisy
|
|
37
|
+
const filteredEvents = body.events.filter(e => e.level !== "debug");
|
|
38
|
+
const inserted = TelemetryDB.insertBatch(body.agent_id, filteredEvents);
|
|
39
|
+
|
|
40
|
+
// Broadcast to SSE clients
|
|
41
|
+
if (filteredEvents.length > 0) {
|
|
42
|
+
const broadcastEvents: TelemetryEvent[] = filteredEvents.map(e => ({
|
|
43
|
+
id: e.id,
|
|
44
|
+
agent_id: body.agent_id,
|
|
45
|
+
timestamp: e.timestamp,
|
|
46
|
+
category: e.category,
|
|
47
|
+
type: e.type,
|
|
48
|
+
level: e.level,
|
|
49
|
+
trace_id: e.trace_id,
|
|
50
|
+
thread_id: e.thread_id,
|
|
51
|
+
data: e.data,
|
|
52
|
+
duration_ms: e.duration_ms,
|
|
53
|
+
error: e.error,
|
|
54
|
+
}));
|
|
55
|
+
telemetryBroadcaster.broadcast(broadcastEvents);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return json({ received: body.events.length, inserted });
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.error("Telemetry error:", e);
|
|
61
|
+
return json({ error: "Invalid telemetry payload" }, 400);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// GET /api/telemetry/stream - SSE stream for real-time telemetry
|
|
66
|
+
if (path === "/api/telemetry/stream" && method === "GET") {
|
|
67
|
+
let controller: ReadableStreamDefaultController<string>;
|
|
68
|
+
|
|
69
|
+
const stream = new ReadableStream<string>({
|
|
70
|
+
start(c) {
|
|
71
|
+
controller = c;
|
|
72
|
+
telemetryBroadcaster.addClient(controller);
|
|
73
|
+
// Send initial connection message
|
|
74
|
+
controller.enqueue("data: {\"connected\":true}\n\n");
|
|
75
|
+
},
|
|
76
|
+
cancel() {
|
|
77
|
+
telemetryBroadcaster.removeClient(controller);
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return new Response(stream, {
|
|
82
|
+
headers: {
|
|
83
|
+
"Content-Type": "text/event-stream",
|
|
84
|
+
"Cache-Control": "no-cache, no-transform",
|
|
85
|
+
"Connection": "keep-alive",
|
|
86
|
+
"X-Accel-Buffering": "no",
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// GET /api/telemetry/events - Query telemetry events
|
|
92
|
+
if (path === "/api/telemetry/events" && method === "GET") {
|
|
93
|
+
const url = new URL(req.url);
|
|
94
|
+
const projectIdParam = url.searchParams.get("project_id");
|
|
95
|
+
const events = TelemetryDB.query({
|
|
96
|
+
agent_id: url.searchParams.get("agent_id") || undefined,
|
|
97
|
+
project_id: projectIdParam === "null" ? null : projectIdParam || undefined,
|
|
98
|
+
category: url.searchParams.get("category") || undefined,
|
|
99
|
+
level: url.searchParams.get("level") || undefined,
|
|
100
|
+
trace_id: url.searchParams.get("trace_id") || undefined,
|
|
101
|
+
since: url.searchParams.get("since") || undefined,
|
|
102
|
+
until: url.searchParams.get("until") || undefined,
|
|
103
|
+
limit: parseInt(url.searchParams.get("limit") || "100"),
|
|
104
|
+
offset: parseInt(url.searchParams.get("offset") || "0"),
|
|
105
|
+
});
|
|
106
|
+
return json({ events });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// GET /api/telemetry/usage - Get usage statistics
|
|
110
|
+
if (path === "/api/telemetry/usage" && method === "GET") {
|
|
111
|
+
const url = new URL(req.url);
|
|
112
|
+
const projectIdParam = url.searchParams.get("project_id");
|
|
113
|
+
const usage = TelemetryDB.getUsage({
|
|
114
|
+
agent_id: url.searchParams.get("agent_id") || undefined,
|
|
115
|
+
project_id: projectIdParam === "null" ? null : projectIdParam || undefined,
|
|
116
|
+
since: url.searchParams.get("since") || undefined,
|
|
117
|
+
until: url.searchParams.get("until") || undefined,
|
|
118
|
+
group_by: (url.searchParams.get("group_by") as "agent" | "day") || undefined,
|
|
119
|
+
});
|
|
120
|
+
return json({ usage });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// GET /api/telemetry/stats - Get summary statistics
|
|
124
|
+
if (path === "/api/telemetry/stats" && method === "GET") {
|
|
125
|
+
const url = new URL(req.url);
|
|
126
|
+
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
127
|
+
const projectIdParam = url.searchParams.get("project_id");
|
|
128
|
+
const stats = TelemetryDB.getStats({
|
|
129
|
+
agentId,
|
|
130
|
+
projectId: projectIdParam === "null" ? null : projectIdParam || undefined,
|
|
131
|
+
});
|
|
132
|
+
return json({ stats });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// POST /api/telemetry/clear - Clear all telemetry data
|
|
136
|
+
if (path === "/api/telemetry/clear" && method === "POST") {
|
|
137
|
+
const deleted = TelemetryDB.deleteOlderThan(0); // Delete all
|
|
138
|
+
return json({ deleted });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { json } from "./helpers";
|
|
2
|
+
import { UserDB } from "../../db";
|
|
3
|
+
import { createUser, hashPassword, validatePassword } from "../../auth";
|
|
4
|
+
import type { AuthContext } from "../../auth/middleware";
|
|
5
|
+
|
|
6
|
+
export async function handleUserRoutes(
|
|
7
|
+
req: Request,
|
|
8
|
+
path: string,
|
|
9
|
+
method: string,
|
|
10
|
+
authContext?: AuthContext,
|
|
11
|
+
): Promise<Response | null> {
|
|
12
|
+
const user = authContext?.user;
|
|
13
|
+
|
|
14
|
+
// GET /api/users - List all users
|
|
15
|
+
if (path === "/api/users" && method === "GET") {
|
|
16
|
+
const users = UserDB.findAll().map(u => ({
|
|
17
|
+
id: u.id,
|
|
18
|
+
username: u.username,
|
|
19
|
+
email: u.email,
|
|
20
|
+
role: u.role,
|
|
21
|
+
createdAt: u.created_at,
|
|
22
|
+
lastLoginAt: u.last_login_at,
|
|
23
|
+
}));
|
|
24
|
+
return json({ users });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// POST /api/users - Create a new user
|
|
28
|
+
if (path === "/api/users" && method === "POST") {
|
|
29
|
+
try {
|
|
30
|
+
const body = await req.json();
|
|
31
|
+
const { username, password, email, role } = body;
|
|
32
|
+
|
|
33
|
+
if (!username || !password) {
|
|
34
|
+
return json({ error: "Username and password are required" }, 400);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result = await createUser({
|
|
38
|
+
username,
|
|
39
|
+
password,
|
|
40
|
+
email: email || undefined,
|
|
41
|
+
role: role || "user",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (!result.success) {
|
|
45
|
+
return json({ error: result.error }, 400);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return json({
|
|
49
|
+
user: {
|
|
50
|
+
id: result.user!.id,
|
|
51
|
+
username: result.user!.username,
|
|
52
|
+
email: result.user!.email,
|
|
53
|
+
role: result.user!.role,
|
|
54
|
+
createdAt: result.user!.created_at,
|
|
55
|
+
},
|
|
56
|
+
}, 201);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
return json({ error: "Invalid request body" }, 400);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// GET /api/users/:id - Get a specific user
|
|
63
|
+
const userMatch = path.match(/^\/api\/users\/([^/]+)$/);
|
|
64
|
+
if (userMatch && method === "GET") {
|
|
65
|
+
const targetUser = UserDB.findById(userMatch[1]);
|
|
66
|
+
if (!targetUser) {
|
|
67
|
+
return json({ error: "User not found" }, 404);
|
|
68
|
+
}
|
|
69
|
+
return json({
|
|
70
|
+
user: {
|
|
71
|
+
id: targetUser.id,
|
|
72
|
+
username: targetUser.username,
|
|
73
|
+
email: targetUser.email,
|
|
74
|
+
role: targetUser.role,
|
|
75
|
+
createdAt: targetUser.created_at,
|
|
76
|
+
lastLoginAt: targetUser.last_login_at,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// PUT /api/users/:id - Update a user
|
|
82
|
+
if (userMatch && method === "PUT") {
|
|
83
|
+
const targetUser = UserDB.findById(userMatch[1]);
|
|
84
|
+
if (!targetUser) {
|
|
85
|
+
return json({ error: "User not found" }, 404);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const body = await req.json();
|
|
90
|
+
const updates: Parameters<typeof UserDB.update>[1] = {};
|
|
91
|
+
|
|
92
|
+
if (body.email !== undefined) updates.email = body.email;
|
|
93
|
+
if (body.role !== undefined) {
|
|
94
|
+
// Prevent removing last admin
|
|
95
|
+
if (targetUser.role === "admin" && body.role !== "admin") {
|
|
96
|
+
if (UserDB.countAdmins() <= 1) {
|
|
97
|
+
return json({ error: "Cannot remove the last admin" }, 400);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
updates.role = body.role;
|
|
101
|
+
}
|
|
102
|
+
if (body.password !== undefined) {
|
|
103
|
+
const validation = validatePassword(body.password);
|
|
104
|
+
if (!validation.valid) {
|
|
105
|
+
return json({ error: validation.errors.join(". ") }, 400);
|
|
106
|
+
}
|
|
107
|
+
updates.password_hash = await hashPassword(body.password);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const updated = UserDB.update(userMatch[1], updates);
|
|
111
|
+
return json({
|
|
112
|
+
user: updated ? {
|
|
113
|
+
id: updated.id,
|
|
114
|
+
username: updated.username,
|
|
115
|
+
email: updated.email,
|
|
116
|
+
role: updated.role,
|
|
117
|
+
createdAt: updated.created_at,
|
|
118
|
+
lastLoginAt: updated.last_login_at,
|
|
119
|
+
} : null,
|
|
120
|
+
});
|
|
121
|
+
} catch (e) {
|
|
122
|
+
return json({ error: "Invalid request body" }, 400);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// DELETE /api/users/:id - Delete a user
|
|
127
|
+
if (userMatch && method === "DELETE") {
|
|
128
|
+
const targetUser = UserDB.findById(userMatch[1]);
|
|
129
|
+
if (!targetUser) {
|
|
130
|
+
return json({ error: "User not found" }, 404);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Prevent deleting yourself
|
|
134
|
+
if (user && targetUser.id === user.id) {
|
|
135
|
+
return json({ error: "Cannot delete your own account" }, 400);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Prevent deleting last admin
|
|
139
|
+
if (targetUser.role === "admin" && UserDB.countAdmins() <= 1) {
|
|
140
|
+
return json({ error: "Cannot delete the last admin" }, 400);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
UserDB.delete(userMatch[1]);
|
|
144
|
+
return json({ success: true });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return null;
|
|
148
|
+
}
|