@synth-deploy/server 0.1.0
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/agent/debrief-retention.d.ts +12 -0
- package/dist/agent/debrief-retention.d.ts.map +1 -0
- package/dist/agent/debrief-retention.js +27 -0
- package/dist/agent/debrief-retention.js.map +1 -0
- package/dist/agent/envoy-client.d.ts +216 -0
- package/dist/agent/envoy-client.d.ts.map +1 -0
- package/dist/agent/envoy-client.js +266 -0
- package/dist/agent/envoy-client.js.map +1 -0
- package/dist/agent/envoy-registry.d.ts +102 -0
- package/dist/agent/envoy-registry.d.ts.map +1 -0
- package/dist/agent/envoy-registry.js +319 -0
- package/dist/agent/envoy-registry.js.map +1 -0
- package/dist/agent/health-checker.d.ts +39 -0
- package/dist/agent/health-checker.d.ts.map +1 -0
- package/dist/agent/health-checker.js +49 -0
- package/dist/agent/health-checker.js.map +1 -0
- package/dist/agent/mcp-client-manager.d.ts +36 -0
- package/dist/agent/mcp-client-manager.d.ts.map +1 -0
- package/dist/agent/mcp-client-manager.js +106 -0
- package/dist/agent/mcp-client-manager.js.map +1 -0
- package/dist/agent/stale-deployment-detector.d.ts +15 -0
- package/dist/agent/stale-deployment-detector.d.ts.map +1 -0
- package/dist/agent/stale-deployment-detector.js +50 -0
- package/dist/agent/stale-deployment-detector.js.map +1 -0
- package/dist/agent/step-runner.d.ts +31 -0
- package/dist/agent/step-runner.d.ts.map +1 -0
- package/dist/agent/step-runner.js +80 -0
- package/dist/agent/step-runner.js.map +1 -0
- package/dist/agent/synth-agent.d.ts +168 -0
- package/dist/agent/synth-agent.d.ts.map +1 -0
- package/dist/agent/synth-agent.js +1195 -0
- package/dist/agent/synth-agent.js.map +1 -0
- package/dist/api/agent.d.ts +36 -0
- package/dist/api/agent.d.ts.map +1 -0
- package/dist/api/agent.js +867 -0
- package/dist/api/agent.js.map +1 -0
- package/dist/api/api-keys.d.ts +4 -0
- package/dist/api/api-keys.d.ts.map +1 -0
- package/dist/api/api-keys.js +118 -0
- package/dist/api/api-keys.js.map +1 -0
- package/dist/api/artifacts.d.ts +5 -0
- package/dist/api/artifacts.d.ts.map +1 -0
- package/dist/api/artifacts.js +142 -0
- package/dist/api/artifacts.js.map +1 -0
- package/dist/api/auth.d.ts +4 -0
- package/dist/api/auth.d.ts.map +1 -0
- package/dist/api/auth.js +280 -0
- package/dist/api/auth.js.map +1 -0
- package/dist/api/deployments.d.ts +11 -0
- package/dist/api/deployments.d.ts.map +1 -0
- package/dist/api/deployments.js +1098 -0
- package/dist/api/deployments.js.map +1 -0
- package/dist/api/environments.d.ts +5 -0
- package/dist/api/environments.d.ts.map +1 -0
- package/dist/api/environments.js +69 -0
- package/dist/api/environments.js.map +1 -0
- package/dist/api/envoy-reports.d.ts +17 -0
- package/dist/api/envoy-reports.d.ts.map +1 -0
- package/dist/api/envoy-reports.js +138 -0
- package/dist/api/envoy-reports.js.map +1 -0
- package/dist/api/envoys.d.ts +5 -0
- package/dist/api/envoys.d.ts.map +1 -0
- package/dist/api/envoys.js +192 -0
- package/dist/api/envoys.js.map +1 -0
- package/dist/api/fleet.d.ts +11 -0
- package/dist/api/fleet.d.ts.map +1 -0
- package/dist/api/fleet.js +394 -0
- package/dist/api/fleet.js.map +1 -0
- package/dist/api/graph.d.ts +8 -0
- package/dist/api/graph.d.ts.map +1 -0
- package/dist/api/graph.js +355 -0
- package/dist/api/graph.js.map +1 -0
- package/dist/api/health.d.ts +20 -0
- package/dist/api/health.d.ts.map +1 -0
- package/dist/api/health.js +248 -0
- package/dist/api/health.js.map +1 -0
- package/dist/api/idp-schemas.d.ts +41 -0
- package/dist/api/idp-schemas.d.ts.map +1 -0
- package/dist/api/idp-schemas.js +17 -0
- package/dist/api/idp-schemas.js.map +1 -0
- package/dist/api/idp.d.ts +6 -0
- package/dist/api/idp.d.ts.map +1 -0
- package/dist/api/idp.js +620 -0
- package/dist/api/idp.js.map +1 -0
- package/dist/api/intake.d.ts +10 -0
- package/dist/api/intake.d.ts.map +1 -0
- package/dist/api/intake.js +418 -0
- package/dist/api/intake.js.map +1 -0
- package/dist/api/partitions.d.ts +5 -0
- package/dist/api/partitions.d.ts.map +1 -0
- package/dist/api/partitions.js +113 -0
- package/dist/api/partitions.js.map +1 -0
- package/dist/api/progress-event-store.d.ts +62 -0
- package/dist/api/progress-event-store.d.ts.map +1 -0
- package/dist/api/progress-event-store.js +118 -0
- package/dist/api/progress-event-store.js.map +1 -0
- package/dist/api/schemas.d.ts +1000 -0
- package/dist/api/schemas.d.ts.map +1 -0
- package/dist/api/schemas.js +328 -0
- package/dist/api/schemas.js.map +1 -0
- package/dist/api/security-boundaries.d.ts +4 -0
- package/dist/api/security-boundaries.d.ts.map +1 -0
- package/dist/api/security-boundaries.js +32 -0
- package/dist/api/security-boundaries.js.map +1 -0
- package/dist/api/settings.d.ts +4 -0
- package/dist/api/settings.d.ts.map +1 -0
- package/dist/api/settings.js +99 -0
- package/dist/api/settings.js.map +1 -0
- package/dist/api/system.d.ts +75 -0
- package/dist/api/system.d.ts.map +1 -0
- package/dist/api/system.js +558 -0
- package/dist/api/system.js.map +1 -0
- package/dist/api/telemetry.d.ts +4 -0
- package/dist/api/telemetry.d.ts.map +1 -0
- package/dist/api/telemetry.js +24 -0
- package/dist/api/telemetry.js.map +1 -0
- package/dist/api/users.d.ts +4 -0
- package/dist/api/users.d.ts.map +1 -0
- package/dist/api/users.js +173 -0
- package/dist/api/users.js.map +1 -0
- package/dist/archive-unpacker.d.ts +24 -0
- package/dist/archive-unpacker.d.ts.map +1 -0
- package/dist/archive-unpacker.js +239 -0
- package/dist/archive-unpacker.js.map +1 -0
- package/dist/artifact-analyzer.d.ts +59 -0
- package/dist/artifact-analyzer.d.ts.map +1 -0
- package/dist/artifact-analyzer.js +334 -0
- package/dist/artifact-analyzer.js.map +1 -0
- package/dist/auth/idp/index.d.ts +9 -0
- package/dist/auth/idp/index.d.ts.map +1 -0
- package/dist/auth/idp/index.js +5 -0
- package/dist/auth/idp/index.js.map +1 -0
- package/dist/auth/idp/ldap.d.ts +56 -0
- package/dist/auth/idp/ldap.d.ts.map +1 -0
- package/dist/auth/idp/ldap.js +276 -0
- package/dist/auth/idp/ldap.js.map +1 -0
- package/dist/auth/idp/oidc.d.ts +27 -0
- package/dist/auth/idp/oidc.d.ts.map +1 -0
- package/dist/auth/idp/oidc.js +97 -0
- package/dist/auth/idp/oidc.js.map +1 -0
- package/dist/auth/idp/role-mapping.d.ts +9 -0
- package/dist/auth/idp/role-mapping.d.ts.map +1 -0
- package/dist/auth/idp/role-mapping.js +16 -0
- package/dist/auth/idp/role-mapping.js.map +1 -0
- package/dist/auth/idp/saml.d.ts +40 -0
- package/dist/auth/idp/saml.d.ts.map +1 -0
- package/dist/auth/idp/saml.js +117 -0
- package/dist/auth/idp/saml.js.map +1 -0
- package/dist/auth/idp/types.d.ts +23 -0
- package/dist/auth/idp/types.d.ts.map +1 -0
- package/dist/auth/idp/types.js +2 -0
- package/dist/auth/idp/types.js.map +1 -0
- package/dist/fleet/fleet-executor.d.ts +35 -0
- package/dist/fleet/fleet-executor.d.ts.map +1 -0
- package/dist/fleet/fleet-executor.js +228 -0
- package/dist/fleet/fleet-executor.js.map +1 -0
- package/dist/fleet/fleet-store.d.ts +13 -0
- package/dist/fleet/fleet-store.d.ts.map +1 -0
- package/dist/fleet/fleet-store.js +13 -0
- package/dist/fleet/fleet-store.js.map +1 -0
- package/dist/fleet/index.d.ts +5 -0
- package/dist/fleet/index.d.ts.map +1 -0
- package/dist/fleet/index.js +4 -0
- package/dist/fleet/index.js.map +1 -0
- package/dist/fleet/representative-selector.d.ts +15 -0
- package/dist/fleet/representative-selector.d.ts.map +1 -0
- package/dist/fleet/representative-selector.js +71 -0
- package/dist/fleet/representative-selector.js.map +1 -0
- package/dist/graph/graph-executor.d.ts +36 -0
- package/dist/graph/graph-executor.d.ts.map +1 -0
- package/dist/graph/graph-executor.js +348 -0
- package/dist/graph/graph-executor.js.map +1 -0
- package/dist/graph/graph-inference.d.ts +22 -0
- package/dist/graph/graph-inference.d.ts.map +1 -0
- package/dist/graph/graph-inference.js +149 -0
- package/dist/graph/graph-inference.js.map +1 -0
- package/dist/graph/graph-store.d.ts +12 -0
- package/dist/graph/graph-store.d.ts.map +1 -0
- package/dist/graph/graph-store.js +61 -0
- package/dist/graph/graph-store.js.map +1 -0
- package/dist/graph/index.d.ts +5 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +4 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +837 -0
- package/dist/index.js.map +1 -0
- package/dist/intake/index.d.ts +6 -0
- package/dist/intake/index.d.ts.map +1 -0
- package/dist/intake/index.js +5 -0
- package/dist/intake/index.js.map +1 -0
- package/dist/intake/intake-processor.d.ts +17 -0
- package/dist/intake/intake-processor.d.ts.map +1 -0
- package/dist/intake/intake-processor.js +99 -0
- package/dist/intake/intake-processor.js.map +1 -0
- package/dist/intake/intake-store.d.ts +7 -0
- package/dist/intake/intake-store.d.ts.map +1 -0
- package/dist/intake/intake-store.js +7 -0
- package/dist/intake/intake-store.js.map +1 -0
- package/dist/intake/registry-poller.d.ts +41 -0
- package/dist/intake/registry-poller.d.ts.map +1 -0
- package/dist/intake/registry-poller.js +202 -0
- package/dist/intake/registry-poller.js.map +1 -0
- package/dist/intake/webhook-handlers.d.ts +37 -0
- package/dist/intake/webhook-handlers.d.ts.map +1 -0
- package/dist/intake/webhook-handlers.js +268 -0
- package/dist/intake/webhook-handlers.js.map +1 -0
- package/dist/logger.d.ts +5 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +15 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp/resources.d.ts +9 -0
- package/dist/mcp/resources.d.ts.map +1 -0
- package/dist/mcp/resources.js +72 -0
- package/dist/mcp/resources.js.map +1 -0
- package/dist/mcp/server.d.ts +15 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +20 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +88 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/middleware/auth.d.ts +29 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +76 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/permissions.d.ts +13 -0
- package/dist/middleware/permissions.d.ts.map +1 -0
- package/dist/middleware/permissions.js +32 -0
- package/dist/middleware/permissions.js.map +1 -0
- package/dist/pattern-store.d.ts +104 -0
- package/dist/pattern-store.d.ts.map +1 -0
- package/dist/pattern-store.js +299 -0
- package/dist/pattern-store.js.map +1 -0
- package/package.json +54 -0
- package/src/agent/debrief-retention.ts +44 -0
- package/src/agent/envoy-client.ts +474 -0
- package/src/agent/envoy-registry.ts +384 -0
- package/src/agent/health-checker.ts +70 -0
- package/src/agent/mcp-client-manager.ts +131 -0
- package/src/agent/stale-deployment-detector.ts +79 -0
- package/src/agent/step-runner.ts +124 -0
- package/src/agent/synth-agent.ts +1567 -0
- package/src/api/agent.ts +1075 -0
- package/src/api/api-keys.ts +129 -0
- package/src/api/artifacts.ts +194 -0
- package/src/api/auth.ts +320 -0
- package/src/api/deployments.ts +1347 -0
- package/src/api/environments.ts +97 -0
- package/src/api/envoy-reports.ts +159 -0
- package/src/api/envoys.ts +237 -0
- package/src/api/fleet.ts +510 -0
- package/src/api/graph.ts +516 -0
- package/src/api/health.ts +311 -0
- package/src/api/idp-schemas.ts +19 -0
- package/src/api/idp.ts +735 -0
- package/src/api/intake.ts +537 -0
- package/src/api/partitions.ts +147 -0
- package/src/api/progress-event-store.ts +153 -0
- package/src/api/schemas.ts +376 -0
- package/src/api/security-boundaries.ts +54 -0
- package/src/api/settings.ts +118 -0
- package/src/api/system.ts +704 -0
- package/src/api/telemetry.ts +32 -0
- package/src/api/users.ts +210 -0
- package/src/archive-unpacker.ts +271 -0
- package/src/artifact-analyzer.ts +438 -0
- package/src/auth/idp/index.ts +8 -0
- package/src/auth/idp/ldap.ts +340 -0
- package/src/auth/idp/oidc.ts +117 -0
- package/src/auth/idp/role-mapping.ts +22 -0
- package/src/auth/idp/saml.ts +148 -0
- package/src/auth/idp/types.ts +22 -0
- package/src/fleet/fleet-executor.ts +309 -0
- package/src/fleet/fleet-store.ts +13 -0
- package/src/fleet/index.ts +4 -0
- package/src/fleet/representative-selector.ts +83 -0
- package/src/graph/graph-executor.ts +446 -0
- package/src/graph/graph-inference.ts +184 -0
- package/src/graph/graph-store.ts +75 -0
- package/src/graph/index.ts +4 -0
- package/src/index.ts +916 -0
- package/src/intake/index.ts +5 -0
- package/src/intake/intake-processor.ts +111 -0
- package/src/intake/intake-store.ts +7 -0
- package/src/intake/registry-poller.ts +230 -0
- package/src/intake/webhook-handlers.ts +328 -0
- package/src/logger.ts +19 -0
- package/src/mcp/resources.ts +98 -0
- package/src/mcp/server.ts +34 -0
- package/src/mcp/tools.ts +117 -0
- package/src/middleware/auth.ts +103 -0
- package/src/middleware/permissions.ts +35 -0
- package/src/pattern-store.ts +409 -0
- package/tests/agent-mode.test.ts +536 -0
- package/tests/api-handlers.test.ts +1245 -0
- package/tests/archive-unpacker.test.ts +179 -0
- package/tests/artifact-analyzer.test.ts +240 -0
- package/tests/auth-middleware.test.ts +189 -0
- package/tests/decision-diary.test.ts +957 -0
- package/tests/diary-reader.test.ts +782 -0
- package/tests/envoy-client.test.ts +342 -0
- package/tests/envoy-reports.test.ts +156 -0
- package/tests/mcp-tools.test.ts +213 -0
- package/tests/orchestration.test.ts +536 -0
- package/tests/partition-deletion.test.ts +143 -0
- package/tests/partition-isolation.test.ts +830 -0
- package/tests/pattern-store.test.ts +371 -0
- package/tests/rbac-enforcement.test.ts +409 -0
- package/tests/ssrf-validation.test.ts +56 -0
- package/tests/stale-deployment.test.ts +85 -0
- package/tests/step-runner.test.ts +308 -0
- package/tests/ui-journey.test.ts +330 -0
- package/tsconfig.json +11 -0
- package/vitest.config.ts +27 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import type { FastifyInstance } from "fastify";
|
|
3
|
+
import bcrypt from "bcryptjs";
|
|
4
|
+
import type { IApiKeyStore } from "@synth-deploy/core";
|
|
5
|
+
import type { ApiKeyId } from "@synth-deploy/core";
|
|
6
|
+
|
|
7
|
+
export function registerApiKeyRoutes(
|
|
8
|
+
app: FastifyInstance,
|
|
9
|
+
apiKeyStore: IApiKeyStore,
|
|
10
|
+
_jwtSecret: Uint8Array,
|
|
11
|
+
): void {
|
|
12
|
+
|
|
13
|
+
// --- GET /api/auth/api-keys ---
|
|
14
|
+
// List current user's active API keys (revokedAt === null).
|
|
15
|
+
app.get("/api/auth/api-keys", async (request, reply) => {
|
|
16
|
+
const user = request.user;
|
|
17
|
+
if (!user) {
|
|
18
|
+
return reply.status(401).send({ error: "Authentication required" });
|
|
19
|
+
}
|
|
20
|
+
const keys = apiKeyStore.listByUserId(user.id).filter(k => k.revokedAt === null);
|
|
21
|
+
return {
|
|
22
|
+
apiKeys: keys.map(k => ({
|
|
23
|
+
id: k.id,
|
|
24
|
+
name: k.name,
|
|
25
|
+
keyPrefix: k.keyPrefix,
|
|
26
|
+
keySuffix: k.keySuffix,
|
|
27
|
+
permissions: k.permissions,
|
|
28
|
+
createdAt: k.createdAt.toISOString(),
|
|
29
|
+
lastUsedAt: k.lastUsedAt ? k.lastUsedAt.toISOString() : null,
|
|
30
|
+
})),
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// --- POST /api/auth/api-keys ---
|
|
35
|
+
// Create a new API key.
|
|
36
|
+
app.post("/api/auth/api-keys", async (request, reply) => {
|
|
37
|
+
const user = request.user;
|
|
38
|
+
if (!user) {
|
|
39
|
+
return reply.status(401).send({ error: "Authentication required" });
|
|
40
|
+
}
|
|
41
|
+
const body = request.body as { name?: string; permissions?: string[] };
|
|
42
|
+
if (!body.name || typeof body.name !== "string" || !body.name.trim()) {
|
|
43
|
+
return reply.status(400).send({ error: "name is required" });
|
|
44
|
+
}
|
|
45
|
+
const permissions = Array.isArray(body.permissions) ? body.permissions : [];
|
|
46
|
+
const rawKey = "synth_" + crypto.randomBytes(16).toString("hex"); // 32 hex chars
|
|
47
|
+
const keyBody = rawKey.slice(6); // strip "synth_"
|
|
48
|
+
const keyPrefix = keyBody.slice(0, 8);
|
|
49
|
+
const keySuffix = keyBody.slice(-4);
|
|
50
|
+
const keyHash = await bcrypt.hash(rawKey, 10);
|
|
51
|
+
const now = new Date();
|
|
52
|
+
const key = apiKeyStore.create({
|
|
53
|
+
id: crypto.randomUUID() as ApiKeyId,
|
|
54
|
+
userId: user.id,
|
|
55
|
+
name: body.name.trim(),
|
|
56
|
+
keyPrefix,
|
|
57
|
+
keySuffix,
|
|
58
|
+
keyHash,
|
|
59
|
+
permissions,
|
|
60
|
+
createdAt: now,
|
|
61
|
+
lastUsedAt: null,
|
|
62
|
+
revokedAt: null,
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
key: {
|
|
66
|
+
id: key.id,
|
|
67
|
+
name: key.name,
|
|
68
|
+
keyPrefix: key.keyPrefix,
|
|
69
|
+
keySuffix: key.keySuffix,
|
|
70
|
+
permissions: key.permissions,
|
|
71
|
+
createdAt: key.createdAt.toISOString(),
|
|
72
|
+
lastUsedAt: null,
|
|
73
|
+
},
|
|
74
|
+
fullKey: rawKey,
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// --- DELETE /api/auth/api-keys/:id ---
|
|
79
|
+
// Revoke an API key.
|
|
80
|
+
app.delete("/api/auth/api-keys/:id", async (request, reply) => {
|
|
81
|
+
const user = request.user;
|
|
82
|
+
if (!user) {
|
|
83
|
+
return reply.status(401).send({ error: "Authentication required" });
|
|
84
|
+
}
|
|
85
|
+
const { id } = request.params as { id: string };
|
|
86
|
+
const key = apiKeyStore.getById(id as ApiKeyId);
|
|
87
|
+
if (!key || key.userId !== user.id) {
|
|
88
|
+
return reply.status(404).send({ error: "API key not found" });
|
|
89
|
+
}
|
|
90
|
+
apiKeyStore.revoke(id as ApiKeyId);
|
|
91
|
+
return reply.status(204).send();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// --- POST /api/auth/api-keys/:id/regenerate ---
|
|
95
|
+
// Regenerate an API key.
|
|
96
|
+
app.post("/api/auth/api-keys/:id/regenerate", async (request, reply) => {
|
|
97
|
+
const user = request.user;
|
|
98
|
+
if (!user) {
|
|
99
|
+
return reply.status(401).send({ error: "Authentication required" });
|
|
100
|
+
}
|
|
101
|
+
const { id } = request.params as { id: string };
|
|
102
|
+
const key = apiKeyStore.getById(id as ApiKeyId);
|
|
103
|
+
if (!key || key.userId !== user.id) {
|
|
104
|
+
return reply.status(404).send({ error: "API key not found" });
|
|
105
|
+
}
|
|
106
|
+
if (key.revokedAt !== null) {
|
|
107
|
+
return reply.status(400).send({ error: "Cannot regenerate a revoked key" });
|
|
108
|
+
}
|
|
109
|
+
const rawKey = "synth_" + crypto.randomBytes(16).toString("hex");
|
|
110
|
+
const keyBody = rawKey.slice(6);
|
|
111
|
+
const keyPrefix = keyBody.slice(0, 8);
|
|
112
|
+
const keySuffix = keyBody.slice(-4);
|
|
113
|
+
const keyHash = await bcrypt.hash(rawKey, 10);
|
|
114
|
+
apiKeyStore.updateHash(id as ApiKeyId, keyHash, keyPrefix, keySuffix);
|
|
115
|
+
const updated = apiKeyStore.getById(id as ApiKeyId)!;
|
|
116
|
+
return {
|
|
117
|
+
key: {
|
|
118
|
+
id: updated.id,
|
|
119
|
+
name: updated.name,
|
|
120
|
+
keyPrefix: updated.keyPrefix,
|
|
121
|
+
keySuffix: updated.keySuffix,
|
|
122
|
+
permissions: updated.permissions,
|
|
123
|
+
createdAt: updated.createdAt.toISOString(),
|
|
124
|
+
lastUsedAt: updated.lastUsedAt ? updated.lastUsedAt.toISOString() : null,
|
|
125
|
+
},
|
|
126
|
+
fullKey: rawKey,
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { FastifyInstance } from "fastify";
|
|
2
|
+
import type { IArtifactStore, ITelemetryStore } from "@synth-deploy/core";
|
|
3
|
+
import { requirePermission, requireEdition } from "../middleware/permissions.js";
|
|
4
|
+
import {
|
|
5
|
+
CreateArtifactSchema,
|
|
6
|
+
UpdateArtifactSchema,
|
|
7
|
+
AddAnnotationSchema,
|
|
8
|
+
AddArtifactVersionSchema,
|
|
9
|
+
} from "./schemas.js";
|
|
10
|
+
import type { ArtifactAnalyzer } from "../artifact-analyzer.js";
|
|
11
|
+
|
|
12
|
+
export function registerArtifactRoutes(
|
|
13
|
+
app: FastifyInstance,
|
|
14
|
+
artifactStore: IArtifactStore,
|
|
15
|
+
telemetry: ITelemetryStore,
|
|
16
|
+
analyzer?: ArtifactAnalyzer,
|
|
17
|
+
): void {
|
|
18
|
+
// List all artifacts
|
|
19
|
+
app.get("/api/artifacts", { preHandler: [requirePermission("artifact.view")] }, async () => {
|
|
20
|
+
return { artifacts: artifactStore.list() };
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Create artifact
|
|
24
|
+
app.post("/api/artifacts", { preHandler: [requirePermission("artifact.create")] }, async (request, reply) => {
|
|
25
|
+
const parsed = CreateArtifactSchema.safeParse(request.body);
|
|
26
|
+
if (!parsed.success) {
|
|
27
|
+
return reply.status(400).send({ error: parsed.error.message });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const artifact = artifactStore.create({
|
|
31
|
+
name: parsed.data.name,
|
|
32
|
+
type: parsed.data.type,
|
|
33
|
+
analysis: {
|
|
34
|
+
summary: "",
|
|
35
|
+
dependencies: [],
|
|
36
|
+
configurationExpectations: {},
|
|
37
|
+
deploymentIntent: "",
|
|
38
|
+
confidence: 0,
|
|
39
|
+
},
|
|
40
|
+
annotations: [],
|
|
41
|
+
learningHistory: [],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
telemetry.record({ actor: (request.user?.email) ?? "anonymous", action: "artifact.created", target: { type: "artifact", id: artifact.id }, details: { name: parsed.data.name, type: parsed.data.type } });
|
|
45
|
+
return reply.status(201).send({ artifact });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Get artifact by ID (with analysis, annotations, learning history)
|
|
49
|
+
app.get<{ Params: { id: string } }>("/api/artifacts/:id", { preHandler: [requirePermission("artifact.view")] }, async (request, reply) => {
|
|
50
|
+
const artifact = artifactStore.get(request.params.id);
|
|
51
|
+
if (!artifact) {
|
|
52
|
+
return reply.status(404).send({ error: "Artifact not found" });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const versions = artifactStore.getVersions(artifact.id);
|
|
56
|
+
|
|
57
|
+
return { artifact, versions };
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Update artifact metadata
|
|
61
|
+
app.put<{ Params: { id: string } }>("/api/artifacts/:id", { preHandler: [requirePermission("artifact.update")] }, async (request, reply) => {
|
|
62
|
+
const parsed = UpdateArtifactSchema.safeParse(request.body);
|
|
63
|
+
if (!parsed.success) {
|
|
64
|
+
return reply.status(400).send({ error: parsed.error.message });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const artifact = artifactStore.get(request.params.id);
|
|
68
|
+
if (!artifact) {
|
|
69
|
+
return reply.status(404).send({ error: "Artifact not found" });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const updated = artifactStore.update(request.params.id, parsed.data as Record<string, unknown>);
|
|
74
|
+
return { artifact: updated };
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (err instanceof Error && err.message.toLowerCase().includes("not found")) {
|
|
77
|
+
return reply.status(404).send({ error: "Artifact not found" });
|
|
78
|
+
}
|
|
79
|
+
app.log.error(err, "Failed to update artifact");
|
|
80
|
+
return reply.status(500).send({ error: "Internal server error" });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Delete artifact
|
|
85
|
+
app.delete<{ Params: { id: string } }>("/api/artifacts/:id", { preHandler: [requirePermission("artifact.delete")] }, async (request, reply) => {
|
|
86
|
+
const artifact = artifactStore.get(request.params.id);
|
|
87
|
+
if (!artifact) {
|
|
88
|
+
return reply.status(404).send({ error: "Artifact not found" });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
artifactStore.delete(request.params.id);
|
|
92
|
+
return reply.status(204).send();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Add user annotation/correction
|
|
96
|
+
app.post<{ Params: { id: string } }>(
|
|
97
|
+
"/api/artifacts/:id/annotations",
|
|
98
|
+
{ preHandler: [requirePermission("artifact.annotate")] },
|
|
99
|
+
async (request, reply) => {
|
|
100
|
+
const artifact = artifactStore.get(request.params.id);
|
|
101
|
+
if (!artifact) {
|
|
102
|
+
return reply.status(404).send({ error: "Artifact not found" });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const parsed = AddAnnotationSchema.safeParse(request.body);
|
|
106
|
+
if (!parsed.success) {
|
|
107
|
+
return reply.status(400).send({ error: parsed.error.message });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const updated = artifactStore.addAnnotation(request.params.id, {
|
|
111
|
+
field: parsed.data.field,
|
|
112
|
+
correction: parsed.data.correction,
|
|
113
|
+
annotatedBy: request.user?.email ?? request.user?.name ?? "unknown",
|
|
114
|
+
annotatedAt: new Date(),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
telemetry.record({ actor: (request.user?.email) ?? "anonymous", action: "artifact.annotated", target: { type: "artifact", id: request.params.id }, details: { field: parsed.data.field } });
|
|
118
|
+
|
|
119
|
+
// Fire async LLM re-analysis incorporating user corrections — don't block the response
|
|
120
|
+
if (analyzer && updated) {
|
|
121
|
+
analyzer.reanalyzeWithAnnotations(updated).then((revised) => {
|
|
122
|
+
if (!revised) return;
|
|
123
|
+
const latest = artifactStore.get(request.params.id);
|
|
124
|
+
if (!latest) return;
|
|
125
|
+
latest.analysis = revised;
|
|
126
|
+
artifactStore.update(request.params.id, { analysis: revised });
|
|
127
|
+
}).catch(() => {});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return reply.status(201).send({ artifact: updated });
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// List versions for artifact
|
|
135
|
+
app.get<{ Params: { id: string } }>(
|
|
136
|
+
"/api/artifacts/:id/versions",
|
|
137
|
+
{ preHandler: [requirePermission("artifact.view")] },
|
|
138
|
+
async (request, reply) => {
|
|
139
|
+
const artifact = artifactStore.get(request.params.id);
|
|
140
|
+
if (!artifact) {
|
|
141
|
+
return reply.status(404).send({ error: "Artifact not found" });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const versions = artifactStore.getVersions(request.params.id);
|
|
145
|
+
return { versions };
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Add new version
|
|
150
|
+
app.post<{ Params: { id: string } }>(
|
|
151
|
+
"/api/artifacts/:id/versions",
|
|
152
|
+
{ preHandler: [requirePermission("artifact.create")] },
|
|
153
|
+
async (request, reply) => {
|
|
154
|
+
const artifact = artifactStore.get(request.params.id);
|
|
155
|
+
if (!artifact) {
|
|
156
|
+
return reply.status(404).send({ error: "Artifact not found" });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const parsed = AddArtifactVersionSchema.safeParse(request.body);
|
|
160
|
+
if (!parsed.success) {
|
|
161
|
+
return reply.status(400).send({ error: parsed.error.message });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const version = artifactStore.addVersion({
|
|
165
|
+
artifactId: request.params.id,
|
|
166
|
+
version: parsed.data.version,
|
|
167
|
+
source: parsed.data.source,
|
|
168
|
+
metadata: parsed.data.metadata ?? {},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return reply.status(201).send({ version });
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Get specific version
|
|
176
|
+
app.get<{ Params: { id: string; versionId: string } }>(
|
|
177
|
+
"/api/artifacts/:id/versions/:versionId",
|
|
178
|
+
{ preHandler: [requirePermission("artifact.view")] },
|
|
179
|
+
async (request, reply) => {
|
|
180
|
+
const artifact = artifactStore.get(request.params.id);
|
|
181
|
+
if (!artifact) {
|
|
182
|
+
return reply.status(404).send({ error: "Artifact not found" });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const versions = artifactStore.getVersions(request.params.id);
|
|
186
|
+
const version = versions.find((v) => v.id === request.params.versionId);
|
|
187
|
+
if (!version) {
|
|
188
|
+
return reply.status(404).send({ error: "Version not found" });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { version };
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
}
|
package/src/api/auth.ts
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import type { FastifyInstance } from "fastify";
|
|
3
|
+
import bcrypt from "bcryptjs";
|
|
4
|
+
import { jwtVerify } from "jose";
|
|
5
|
+
import type { IUserStore, IRoleStore, IUserRoleStore, ISessionStore, UserId, UserPublic } from "@synth-deploy/core";
|
|
6
|
+
import { generateTokens } from "../middleware/auth.js";
|
|
7
|
+
import { LoginSchema, RegisterSchema, RefreshTokenSchema } from "./schemas.js";
|
|
8
|
+
|
|
9
|
+
function toPublicUser(user: { id: UserId; email: string; name: string; authSource?: string; externalId?: string; createdAt: Date; updatedAt: Date }): UserPublic {
|
|
10
|
+
return {
|
|
11
|
+
id: user.id,
|
|
12
|
+
email: user.email,
|
|
13
|
+
name: user.name,
|
|
14
|
+
authSource: (user.authSource as UserPublic["authSource"]) ?? "local",
|
|
15
|
+
createdAt: user.createdAt,
|
|
16
|
+
updatedAt: user.updatedAt,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function registerAuthRoutes(
|
|
21
|
+
app: FastifyInstance,
|
|
22
|
+
userStore: IUserStore,
|
|
23
|
+
roleStore: IRoleStore,
|
|
24
|
+
userRoleStore: IUserRoleStore,
|
|
25
|
+
sessionStore: ISessionStore,
|
|
26
|
+
jwtSecret: Uint8Array,
|
|
27
|
+
): void {
|
|
28
|
+
|
|
29
|
+
// --- GET /api/auth/status ---
|
|
30
|
+
// Returns whether setup is required (no users exist yet)
|
|
31
|
+
app.get("/api/auth/status", async () => {
|
|
32
|
+
const userCount = userStore.count();
|
|
33
|
+
return { needsSetup: userCount === 0 };
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// --- POST /api/auth/register ---
|
|
37
|
+
// First-user registration only. Creates the initial admin user.
|
|
38
|
+
app.post("/api/auth/register", async (request, reply) => {
|
|
39
|
+
const parsed = RegisterSchema.safeParse(request.body);
|
|
40
|
+
if (!parsed.success) {
|
|
41
|
+
return reply.status(400).send({ error: "Validation failed", details: parsed.error.flatten() });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const userCount = userStore.count();
|
|
45
|
+
if (userCount > 0) {
|
|
46
|
+
return reply.status(403).send({ error: "Registration is closed. Users already exist." });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { email, name, password } = parsed.data;
|
|
50
|
+
const passwordHash = await bcrypt.hash(password, 10);
|
|
51
|
+
const userId = crypto.randomUUID() as UserId;
|
|
52
|
+
const now = new Date();
|
|
53
|
+
|
|
54
|
+
const user = userStore.create({
|
|
55
|
+
id: userId,
|
|
56
|
+
email,
|
|
57
|
+
name,
|
|
58
|
+
passwordHash,
|
|
59
|
+
createdAt: now,
|
|
60
|
+
updatedAt: now,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Assign Admin role
|
|
64
|
+
const adminRole = roleStore.getByName("Admin");
|
|
65
|
+
if (adminRole) {
|
|
66
|
+
userRoleStore.assign(userId, adminRole.id, userId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Create session
|
|
70
|
+
const tokens = await generateTokens(userId, jwtSecret);
|
|
71
|
+
sessionStore.create({
|
|
72
|
+
id: crypto.randomUUID(),
|
|
73
|
+
userId,
|
|
74
|
+
token: tokens.token,
|
|
75
|
+
refreshToken: tokens.refreshToken,
|
|
76
|
+
expiresAt: tokens.expiresAt,
|
|
77
|
+
createdAt: now,
|
|
78
|
+
userAgent: request.headers["user-agent"] ?? undefined,
|
|
79
|
+
ipAddress: request.ip ?? undefined,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const permissions = userRoleStore.getUserPermissions(userId);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
user: toPublicUser(user),
|
|
86
|
+
token: tokens.token,
|
|
87
|
+
refreshToken: tokens.refreshToken,
|
|
88
|
+
permissions,
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// --- POST /api/auth/login ---
|
|
93
|
+
app.post("/api/auth/login", async (request, reply) => {
|
|
94
|
+
const parsed = LoginSchema.safeParse(request.body);
|
|
95
|
+
if (!parsed.success) {
|
|
96
|
+
return reply.status(400).send({ error: "Validation failed", details: parsed.error.flatten() });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const { email, password } = parsed.data;
|
|
100
|
+
const user = userStore.getByEmail(email);
|
|
101
|
+
if (!user) {
|
|
102
|
+
return reply.status(401).send({ error: "Invalid email or password" });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const valid = await bcrypt.compare(password, user.passwordHash);
|
|
106
|
+
if (!valid) {
|
|
107
|
+
return reply.status(401).send({ error: "Invalid email or password" });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const tokens = await generateTokens(user.id, jwtSecret);
|
|
111
|
+
sessionStore.create({
|
|
112
|
+
id: crypto.randomUUID(),
|
|
113
|
+
userId: user.id,
|
|
114
|
+
token: tokens.token,
|
|
115
|
+
refreshToken: tokens.refreshToken,
|
|
116
|
+
expiresAt: tokens.expiresAt,
|
|
117
|
+
createdAt: new Date(),
|
|
118
|
+
userAgent: request.headers["user-agent"] ?? undefined,
|
|
119
|
+
ipAddress: request.ip ?? undefined,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const permissions = userRoleStore.getUserPermissions(user.id);
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
user: toPublicUser(user),
|
|
126
|
+
token: tokens.token,
|
|
127
|
+
refreshToken: tokens.refreshToken,
|
|
128
|
+
permissions,
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// --- POST /api/auth/refresh ---
|
|
133
|
+
app.post("/api/auth/refresh", async (request, reply) => {
|
|
134
|
+
const parsed = RefreshTokenSchema.safeParse(request.body);
|
|
135
|
+
if (!parsed.success) {
|
|
136
|
+
return reply.status(400).send({ error: "Validation failed", details: parsed.error.flatten() });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const { refreshToken } = parsed.data;
|
|
140
|
+
|
|
141
|
+
// Verify the refresh token JWT
|
|
142
|
+
try {
|
|
143
|
+
await jwtVerify(refreshToken, jwtSecret);
|
|
144
|
+
} catch {
|
|
145
|
+
return reply.status(401).send({ error: "Invalid refresh token" });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const session = sessionStore.getByRefreshToken(refreshToken);
|
|
149
|
+
if (!session) {
|
|
150
|
+
return reply.status(401).send({ error: "Session not found" });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Delete old session
|
|
154
|
+
sessionStore.deleteByToken(session.token);
|
|
155
|
+
|
|
156
|
+
// Generate new tokens — preserve UA/IP from the original session
|
|
157
|
+
const tokens = await generateTokens(session.userId, jwtSecret);
|
|
158
|
+
sessionStore.create({
|
|
159
|
+
id: crypto.randomUUID(),
|
|
160
|
+
userId: session.userId,
|
|
161
|
+
token: tokens.token,
|
|
162
|
+
refreshToken: tokens.refreshToken,
|
|
163
|
+
expiresAt: tokens.expiresAt,
|
|
164
|
+
createdAt: new Date(),
|
|
165
|
+
userAgent: session.userAgent,
|
|
166
|
+
ipAddress: session.ipAddress,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
token: tokens.token,
|
|
171
|
+
refreshToken: tokens.refreshToken,
|
|
172
|
+
expiresAt: tokens.expiresAt.toISOString(),
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// --- POST /api/auth/logout ---
|
|
177
|
+
app.post("/api/auth/logout", async (request, reply) => {
|
|
178
|
+
const authHeader = request.headers.authorization;
|
|
179
|
+
if (authHeader?.startsWith("Bearer ")) {
|
|
180
|
+
const token = authHeader.slice(7);
|
|
181
|
+
sessionStore.deleteByToken(token);
|
|
182
|
+
}
|
|
183
|
+
return reply.status(204).send();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// --- GET /api/auth/me ---
|
|
187
|
+
app.get("/api/auth/me", async (request, reply) => {
|
|
188
|
+
const user = request.user;
|
|
189
|
+
if (!user) {
|
|
190
|
+
return reply.status(401).send({ error: "Authentication required" });
|
|
191
|
+
}
|
|
192
|
+
const fullUser = userStore.getById(user.id);
|
|
193
|
+
if (!fullUser) {
|
|
194
|
+
return reply.status(401).send({ error: "User not found" });
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
user: toPublicUser(fullUser),
|
|
198
|
+
permissions: user.permissions,
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// --- PUT /api/auth/me ---
|
|
203
|
+
// Update the authenticated user's name and/or email.
|
|
204
|
+
app.put("/api/auth/me", async (request, reply) => {
|
|
205
|
+
const user = request.user;
|
|
206
|
+
if (!user) {
|
|
207
|
+
return reply.status(401).send({ error: "Authentication required" });
|
|
208
|
+
}
|
|
209
|
+
const body = request.body as { name?: string; email?: string };
|
|
210
|
+
const updates: { name?: string; email?: string; updatedAt: Date } = { updatedAt: new Date() };
|
|
211
|
+
if (typeof body.name === "string" && body.name.trim()) {
|
|
212
|
+
updates.name = body.name.trim();
|
|
213
|
+
}
|
|
214
|
+
if (typeof body.email === "string" && body.email.trim()) {
|
|
215
|
+
updates.email = body.email.trim().toLowerCase();
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
const updated = userStore.update(user.id, updates);
|
|
219
|
+
return { user: toPublicUser(updated) };
|
|
220
|
+
} catch (err) {
|
|
221
|
+
const message = err instanceof Error ? err.message : "Update failed";
|
|
222
|
+
return reply.status(400).send({ error: message });
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// --- GET /api/auth/sessions ---
|
|
227
|
+
// List current user's active sessions (without token/refreshToken fields).
|
|
228
|
+
app.get("/api/auth/sessions", async (request, reply) => {
|
|
229
|
+
const user = request.user;
|
|
230
|
+
if (!user) {
|
|
231
|
+
return reply.status(401).send({ error: "Authentication required" });
|
|
232
|
+
}
|
|
233
|
+
const authHeader = request.headers.authorization;
|
|
234
|
+
const currentToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
235
|
+
|
|
236
|
+
const sessions = sessionStore.listByUserId(user.id);
|
|
237
|
+
return {
|
|
238
|
+
sessions: sessions.map(s => ({
|
|
239
|
+
id: s.id,
|
|
240
|
+
createdAt: s.createdAt.toISOString(),
|
|
241
|
+
expiresAt: s.expiresAt.toISOString(),
|
|
242
|
+
current: currentToken ? s.token === currentToken : false,
|
|
243
|
+
userAgent: s.userAgent ?? null,
|
|
244
|
+
ipAddress: s.ipAddress ?? null,
|
|
245
|
+
})),
|
|
246
|
+
};
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// --- DELETE /api/auth/sessions/:id ---
|
|
250
|
+
// Revoke a specific session (cannot revoke current session).
|
|
251
|
+
app.delete("/api/auth/sessions/:id", async (request, reply) => {
|
|
252
|
+
const user = request.user;
|
|
253
|
+
if (!user) {
|
|
254
|
+
return reply.status(401).send({ error: "Authentication required" });
|
|
255
|
+
}
|
|
256
|
+
const { id } = request.params as { id: string };
|
|
257
|
+
const authHeader = request.headers.authorization;
|
|
258
|
+
const currentToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
259
|
+
|
|
260
|
+
const sessions = sessionStore.listByUserId(user.id);
|
|
261
|
+
const target = sessions.find(s => s.id === id);
|
|
262
|
+
if (!target) {
|
|
263
|
+
return reply.status(404).send({ error: "Session not found" });
|
|
264
|
+
}
|
|
265
|
+
if (currentToken && target.token === currentToken) {
|
|
266
|
+
return reply.status(400).send({ error: "Cannot revoke current session" });
|
|
267
|
+
}
|
|
268
|
+
sessionStore.deleteById(id);
|
|
269
|
+
return reply.status(204).send();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// --- DELETE /api/auth/sessions ---
|
|
273
|
+
// Revoke all other sessions (keep current).
|
|
274
|
+
app.delete("/api/auth/sessions", async (request, reply) => {
|
|
275
|
+
const user = request.user;
|
|
276
|
+
if (!user) {
|
|
277
|
+
return reply.status(401).send({ error: "Authentication required" });
|
|
278
|
+
}
|
|
279
|
+
const authHeader = request.headers.authorization;
|
|
280
|
+
const currentToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
281
|
+
|
|
282
|
+
const sessions = sessionStore.listByUserId(user.id);
|
|
283
|
+
for (const s of sessions) {
|
|
284
|
+
if (!currentToken || s.token !== currentToken) {
|
|
285
|
+
sessionStore.deleteById(s.id);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return reply.status(204).send();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// --- POST /api/auth/me/password ---
|
|
292
|
+
// Change the authenticated user's password (local accounts only).
|
|
293
|
+
app.post("/api/auth/me/password", async (request, reply) => {
|
|
294
|
+
const user = request.user;
|
|
295
|
+
if (!user) {
|
|
296
|
+
return reply.status(401).send({ error: "Authentication required" });
|
|
297
|
+
}
|
|
298
|
+
const fullUser = userStore.getById(user.id);
|
|
299
|
+
if (!fullUser) {
|
|
300
|
+
return reply.status(401).send({ error: "User not found" });
|
|
301
|
+
}
|
|
302
|
+
if (fullUser.authSource !== "local") {
|
|
303
|
+
return reply.status(400).send({ error: "Password change is only available for local accounts" });
|
|
304
|
+
}
|
|
305
|
+
const body = request.body as { currentPassword?: string; newPassword?: string };
|
|
306
|
+
if (!body.currentPassword || !body.newPassword) {
|
|
307
|
+
return reply.status(400).send({ error: "currentPassword and newPassword are required" });
|
|
308
|
+
}
|
|
309
|
+
if (body.newPassword.length < 8) {
|
|
310
|
+
return reply.status(400).send({ error: "New password must be at least 8 characters" });
|
|
311
|
+
}
|
|
312
|
+
const valid = await bcrypt.compare(body.currentPassword, fullUser.passwordHash);
|
|
313
|
+
if (!valid) {
|
|
314
|
+
return reply.status(401).send({ error: "Current password is incorrect" });
|
|
315
|
+
}
|
|
316
|
+
const passwordHash = await bcrypt.hash(body.newPassword, 10);
|
|
317
|
+
userStore.update(user.id, { passwordHash, updatedAt: new Date() });
|
|
318
|
+
return reply.status(204).send();
|
|
319
|
+
});
|
|
320
|
+
}
|