@dashwire/server 0.1.0 → 0.3.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/auth.service.d.ts +16 -0
- package/dist/auth.service.js +65 -0
- package/dist/db/client.d.ts +2 -0
- package/{src/db/client.ts → dist/db/client.js} +1 -4
- package/dist/db/schema.d.ts +570 -0
- package/dist/db/schema.js +46 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +114 -0
- package/dist/logs.service.d.ts +1 -0
- package/dist/logs.service.js +14 -0
- package/dist/overview.service.d.ts +12 -0
- package/dist/overview.service.js +20 -0
- package/dist/projects.service.d.ts +40 -0
- package/dist/projects.service.js +180 -0
- package/dist/realtime/dashboardNamespace.d.ts +2 -0
- package/dist/realtime/dashboardNamespace.js +26 -0
- package/dist/realtime/sdkNamespace.d.ts +2 -0
- package/dist/realtime/sdkNamespace.js +69 -0
- package/dist/routes/auth.d.ts +2 -0
- package/dist/routes/auth.js +76 -0
- package/dist/routes/overview.d.ts +2 -0
- package/dist/routes/overview.js +21 -0
- package/dist/routes/projects.d.ts +3 -0
- package/dist/routes/projects.js +66 -0
- package/package.json +5 -4
- package/dashwire.db +0 -0
- package/drizzle.config.ts +0 -10
- package/src/auth.service.ts +0 -76
- package/src/db/schema.ts +0 -52
- package/src/index.ts +0 -44
- package/src/logs.service.ts +0 -15
- package/src/overview.service.ts +0 -28
- package/src/projects.service.ts +0 -204
- package/src/realtime/dashboardNamespace.ts +0 -36
- package/src/realtime/sdkNamespace.ts +0 -88
- package/src/routes/auth.ts +0 -90
- package/src/routes/overview.ts +0 -27
- package/src/routes/projects.ts +0 -93
- package/tsconfig.json +0 -11
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { registerProject, listProjects, getProject, listAllTokens, createTokenForProject, setTokenActiveStatus, deleteToken } from "../projects.service.js";
|
|
2
|
+
import { logs } from "../db/schema.js";
|
|
3
|
+
import { desc } from "drizzle-orm/sql/expressions/select";
|
|
4
|
+
import { eq } from "drizzle-orm/sql/expressions/conditions";
|
|
5
|
+
import { db } from "../db/client.js";
|
|
6
|
+
export function projectRoutes(app, io) {
|
|
7
|
+
app.post("/api/projects/:id/register", async (req, reply) => {
|
|
8
|
+
const { id: projectId } = req.params;
|
|
9
|
+
const authHeader = req.headers.authorization;
|
|
10
|
+
const authToken = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : undefined;
|
|
11
|
+
const { projectName, structure } = req.body;
|
|
12
|
+
try {
|
|
13
|
+
const result = registerProject(projectId, projectName, structure, authToken);
|
|
14
|
+
return reply.send(result);
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
return reply.code(401).send({ error: err.message });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
app.get("/api/projects", async (_req, reply) => {
|
|
21
|
+
return reply.send(listProjects());
|
|
22
|
+
});
|
|
23
|
+
app.get("/api/projects/tokens/all", async (_req, reply) => {
|
|
24
|
+
return reply.send(listAllTokens());
|
|
25
|
+
});
|
|
26
|
+
app.post("/api/projects/tokens/new", async (req, reply) => {
|
|
27
|
+
const body = req.body;
|
|
28
|
+
try {
|
|
29
|
+
const token = createTokenForProject(body?.label);
|
|
30
|
+
return reply.send({ token });
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
return reply.code(400).send({ error: err.message });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
app.patch("/api/projects/tokens/:tokenId", async (req, reply) => {
|
|
37
|
+
const { tokenId } = req.params;
|
|
38
|
+
const { active } = req.body;
|
|
39
|
+
setTokenActiveStatus(tokenId, active);
|
|
40
|
+
return reply.send({ success: true });
|
|
41
|
+
});
|
|
42
|
+
app.delete("/api/projects/tokens/:tokenId", async (req, reply) => {
|
|
43
|
+
const { tokenId } = req.params;
|
|
44
|
+
deleteToken(tokenId);
|
|
45
|
+
return reply.send({ success: true });
|
|
46
|
+
});
|
|
47
|
+
app.get("/api/projects/:id", async (req, reply) => {
|
|
48
|
+
const { id: projectId } = req.params;
|
|
49
|
+
const project = getProject(projectId);
|
|
50
|
+
if (!project) {
|
|
51
|
+
return reply.code(404).send({ error: "Project niet gevonden" });
|
|
52
|
+
}
|
|
53
|
+
return reply.send(project);
|
|
54
|
+
});
|
|
55
|
+
app.get("/api/projects/:id/logs", async (req, reply) => {
|
|
56
|
+
const { id: projectId } = req.params;
|
|
57
|
+
const limit = Number(req.query.limit ?? 50);
|
|
58
|
+
const projectLogs = db.select()
|
|
59
|
+
.from(logs)
|
|
60
|
+
.where(eq(logs.projectId, projectId))
|
|
61
|
+
.orderBy(desc(logs.id))
|
|
62
|
+
.limit(limit)
|
|
63
|
+
.all();
|
|
64
|
+
return reply.send(projectLogs.reverse());
|
|
65
|
+
});
|
|
66
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dashwire/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"files": ["dist"],
|
|
7
8
|
"private": false,
|
|
8
9
|
"scripts": {
|
|
9
10
|
"build": "tsc -p tsconfig.json",
|
|
@@ -11,7 +12,8 @@
|
|
|
11
12
|
"start": "node dist/index.js"
|
|
12
13
|
},
|
|
13
14
|
"dependencies": {
|
|
14
|
-
"@dashwire/core": "
|
|
15
|
+
"@dashwire/core": "^0.1.0",
|
|
16
|
+
"@fastify/static": "^7.0.4",
|
|
15
17
|
"better-sqlite3": "^11.3.0",
|
|
16
18
|
"drizzle-orm": "^0.33.0",
|
|
17
19
|
"fastify": "^4.28.1",
|
|
@@ -23,8 +25,7 @@
|
|
|
23
25
|
"@types/better-sqlite3": "^7.6.11",
|
|
24
26
|
"@types/jsonwebtoken": "^9.0.10",
|
|
25
27
|
"@types/node": "^22.7.0",
|
|
26
|
-
"drizzle-kit": "^0.24.2",
|
|
27
28
|
"tsx": "^4.19.1",
|
|
28
29
|
"typescript": "^5.6.0"
|
|
29
30
|
}
|
|
30
|
-
}
|
|
31
|
+
}
|
package/dashwire.db
DELETED
|
Binary file
|
package/drizzle.config.ts
DELETED
package/src/auth.service.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
|
|
2
|
-
import { eq } from "drizzle-orm";
|
|
3
|
-
import jwt from "jsonwebtoken";
|
|
4
|
-
import { db } from "./db/client.js";
|
|
5
|
-
import { users } from "./db/schema.js";
|
|
6
|
-
|
|
7
|
-
const JWT_SECRET = process.env.JWT_SECRET || "super-secret-dashwire-key";
|
|
8
|
-
|
|
9
|
-
function hashPassword(password: string): string {
|
|
10
|
-
const salt = randomBytes(16).toString("hex");
|
|
11
|
-
const buffer = scryptSync(password, salt, 64);
|
|
12
|
-
return `${salt}:${buffer.toString("hex")}`;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function verifyPassword(password: string, storedHash: string): boolean {
|
|
16
|
-
const [salt, key] = storedHash.split(":");
|
|
17
|
-
const buffer = scryptSync(password, salt, 64);
|
|
18
|
-
const storedBuffer = Buffer.from(key, "hex");
|
|
19
|
-
return timingSafeEqual(buffer, storedBuffer);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function hasAnyAdmin(): boolean {
|
|
23
|
-
const admin = db.select().from(users).where(eq(users.role, "admin")).limit(1).get();
|
|
24
|
-
return !!admin;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function createAdminUser(username: string, password: string) {
|
|
28
|
-
if (hasAnyAdmin()) {
|
|
29
|
-
throw new Error("Admin account bestaat al.");
|
|
30
|
-
}
|
|
31
|
-
return createUserRecord(username, password, "admin");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function createUserRecord(username: string, password: string, role: "admin" | "user" = "user") {
|
|
35
|
-
const id = randomBytes(8).toString("hex");
|
|
36
|
-
const passwordHash = hashPassword(password);
|
|
37
|
-
const now = new Date().toISOString();
|
|
38
|
-
|
|
39
|
-
db.insert(users).values({
|
|
40
|
-
id,
|
|
41
|
-
username,
|
|
42
|
-
passwordHash,
|
|
43
|
-
role,
|
|
44
|
-
createdAt: now,
|
|
45
|
-
}).run();
|
|
46
|
-
|
|
47
|
-
return jwt.sign({ id, username, role }, JWT_SECRET, { expiresIn: "7d" });
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function authenticateUser(username: string, password: string) {
|
|
51
|
-
const user = db.select().from(users).where(eq(users.username, username)).get();
|
|
52
|
-
if (!user || !verifyPassword(password, user.passwordHash)) {
|
|
53
|
-
throw new Error("Ongeldige gebruikersnaam of wachtwoord.");
|
|
54
|
-
}
|
|
55
|
-
return jwt.sign({ id: user.id, username: user.username, role: user.role }, JWT_SECRET, { expiresIn: "7d" });
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function verifyAuthToken(token: string): { id: string; username: string; role: string } | null {
|
|
59
|
-
try {
|
|
60
|
-
const decoded = jwt.verify(token, JWT_SECRET) as { id: string; username: string; role: string };
|
|
61
|
-
return decoded;
|
|
62
|
-
} catch {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function listUsers() {
|
|
68
|
-
return db.select({ id: users.id, username: users.username, role: users.role, createdAt: users.createdAt }).from(users).all();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function deleteUser(userId: string, currentUserId: string) {
|
|
72
|
-
if (userId === currentUserId) {
|
|
73
|
-
throw new Error("Je kunt je eigen admin-account niet verwijderen.");
|
|
74
|
-
}
|
|
75
|
-
db.delete(users).where(eq(users.id, userId)).run();
|
|
76
|
-
}
|
package/src/db/schema.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
|
2
|
-
|
|
3
|
-
export const projects = sqliteTable("projects", {
|
|
4
|
-
id: text("id").primaryKey(),
|
|
5
|
-
name: text("name").notNull(),
|
|
6
|
-
apiKeyHash: text("api_key_hash").notNull(),
|
|
7
|
-
structureJson: text("structure_json").notNull(),
|
|
8
|
-
status: text("status", { enum: ["online", "offline"] }).notNull().default("offline"),
|
|
9
|
-
createdAt: text("created_at").notNull(),
|
|
10
|
-
lastSeenAt: text("last_seen_at"),
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
export const projectTokens = sqliteTable("project_tokens", {
|
|
14
|
-
id: text("id").primaryKey(),
|
|
15
|
-
projectId: text("project_id").notNull(),
|
|
16
|
-
token: text("token").notNull().unique(),
|
|
17
|
-
active: integer("active", { mode: "boolean" }).notNull().default(true),
|
|
18
|
-
createdAt: text("created_at").notNull(),
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
export const capabilityState = sqliteTable("capability_state", {
|
|
22
|
-
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
23
|
-
projectId: text("project_id").notNull(),
|
|
24
|
-
capabilityPath: text("capability_path").notNull(),
|
|
25
|
-
valueJson: text("value_json").notNull(),
|
|
26
|
-
updatedAt: text("updated_at").notNull(),
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
export const overviewConfig = sqliteTable("overview_config", {
|
|
30
|
-
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
31
|
-
projectId: text("project_id").notNull(),
|
|
32
|
-
capabilityPath: text("capability_path").notNull(),
|
|
33
|
-
staleSince: text("stale_since"),
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
export const logs = sqliteTable("logs", {
|
|
37
|
-
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
38
|
-
projectId: text("project_id").notNull(),
|
|
39
|
-
level: text("level").notNull(),
|
|
40
|
-
message: text("message").notNull(),
|
|
41
|
-
source: text("source"),
|
|
42
|
-
metadataJson: text("metadata_json"),
|
|
43
|
-
timestamp: text("timestamp").notNull(),
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
export const users = sqliteTable("users", {
|
|
47
|
-
id: text("id").primaryKey(),
|
|
48
|
-
username: text("username").notNull().unique(),
|
|
49
|
-
passwordHash: text("password_hash").notNull(),
|
|
50
|
-
role: text("role").notNull().default("admin"),
|
|
51
|
-
createdAt: text("created_at").notNull(),
|
|
52
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import Fastify from "fastify";
|
|
2
|
-
import { Server as SocketServer } from "socket.io";
|
|
3
|
-
import { projectRoutes } from "./routes/projects.js";
|
|
4
|
-
import { overviewRoutes } from "./routes/overview.js";
|
|
5
|
-
import { authRoutes } from "./routes/auth.js";
|
|
6
|
-
import { attachSdkNamespace } from "./realtime/sdkNamespace.js";
|
|
7
|
-
import { attachDashboardNamespace } from "./realtime/dashboardNamespace.js";
|
|
8
|
-
|
|
9
|
-
export async function startDashwire(userConfig: any = {}) {
|
|
10
|
-
const PORT = Number(userConfig?.server?.port ?? process.env.PORT ?? 4000);
|
|
11
|
-
const HOST = userConfig?.server?.host ?? "0.0.0.0";
|
|
12
|
-
|
|
13
|
-
const app = Fastify({ logger: true });
|
|
14
|
-
|
|
15
|
-
app.addHook("onRequest", async (req, reply) => {
|
|
16
|
-
reply.header("Access-Control-Allow-Origin", "*");
|
|
17
|
-
reply.header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS");
|
|
18
|
-
reply.header("Access-Control-Allow-Headers", "Content-Type, Authorization, x-api-key, x-admin-token");
|
|
19
|
-
|
|
20
|
-
if (req.method === "OPTIONS") {
|
|
21
|
-
return reply.code(200).send();
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
app.get("/health", async () => ({ ok: true }));
|
|
26
|
-
|
|
27
|
-
const io = new SocketServer(app.server, {
|
|
28
|
-
cors: { origin: "*" },
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
projectRoutes(app, io);
|
|
32
|
-
overviewRoutes(app);
|
|
33
|
-
authRoutes(app);
|
|
34
|
-
attachSdkNamespace(io);
|
|
35
|
-
attachDashboardNamespace(io);
|
|
36
|
-
|
|
37
|
-
await app.listen({ port: PORT, host: HOST });
|
|
38
|
-
app.log.info(`⚡ Dashwire server running on http://${HOST}:${PORT}`);
|
|
39
|
-
return app;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (process.argv[1] && process.argv[1].endsWith("index.ts")) {
|
|
43
|
-
startDashwire();
|
|
44
|
-
}
|
package/src/logs.service.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { db } from "./db/client.js";
|
|
2
|
-
import { logs } from "./db/schema.js";
|
|
3
|
-
|
|
4
|
-
export function insertLog(projectId: string, level: string, message: string, source?: string, metadata?: Record<string, unknown>): number {
|
|
5
|
-
const timestamp = new Date().toISOString();
|
|
6
|
-
const res = db.insert(logs).values({
|
|
7
|
-
projectId,
|
|
8
|
-
level,
|
|
9
|
-
message,
|
|
10
|
-
source: source ?? null,
|
|
11
|
-
metadataJson: metadata ? JSON.stringify(metadata) : null,
|
|
12
|
-
timestamp,
|
|
13
|
-
}).run();
|
|
14
|
-
return Number(res.lastInsertRowid);
|
|
15
|
-
}
|
package/src/overview.service.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { db } from "./db/client.js";
|
|
2
|
-
import { overviewConfig } from "./db/schema.js";
|
|
3
|
-
import { eq } from "drizzle-orm";
|
|
4
|
-
|
|
5
|
-
export interface OverviewEntryInput {
|
|
6
|
-
projectId: string;
|
|
7
|
-
capabilityPath: string;
|
|
8
|
-
order: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function setOverviewConfig(entries: OverviewEntryInput[]): void {
|
|
12
|
-
db.delete(overviewConfig).run();
|
|
13
|
-
for (const entry of entries) {
|
|
14
|
-
db.insert(overviewConfig)
|
|
15
|
-
.values({
|
|
16
|
-
projectId: entry.projectId,
|
|
17
|
-
capabilityPath: entry.capabilityPath
|
|
18
|
-
})
|
|
19
|
-
.run();
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function getOverviewConfig(projectId?: string) {
|
|
24
|
-
if (projectId) {
|
|
25
|
-
return db.select().from(overviewConfig).where(eq(overviewConfig.projectId, projectId)).all();
|
|
26
|
-
}
|
|
27
|
-
return db.select().from(overviewConfig).all();
|
|
28
|
-
}
|
package/src/projects.service.ts
DELETED
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { eq, and } from "drizzle-orm";
|
|
3
|
-
import jwt from "jsonwebtoken";
|
|
4
|
-
import { db } from "./db/client.js";
|
|
5
|
-
import { projects, projectTokens, capabilityState, overviewConfig } from "./db/schema.js";
|
|
6
|
-
import type { DashboardStructure, Capability } from "@dashwire/core";
|
|
7
|
-
|
|
8
|
-
const JWT_SECRET = process.env.JWT_SECRET || "super-secret-dashwire-key";
|
|
9
|
-
|
|
10
|
-
function allPaths(structure: DashboardStructure): Set<string> {
|
|
11
|
-
const set = new Set<string>();
|
|
12
|
-
for (const section of structure.sections) {
|
|
13
|
-
for (const cap of section.capabilities) set.add(cap.path);
|
|
14
|
-
}
|
|
15
|
-
return set;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface RegisterResult {
|
|
19
|
-
token: string;
|
|
20
|
-
staleOverviewPaths: string[];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function registerProject(projectId: string, projectName: string, structure: DashboardStructure, authToken?: string): RegisterResult {
|
|
24
|
-
const existing = db.select().from(projects).where(eq(projects.id, projectId)).get();
|
|
25
|
-
const now = new Date().toISOString();
|
|
26
|
-
const structureJsonString = JSON.stringify(structure);
|
|
27
|
-
|
|
28
|
-
let activeToken = authToken;
|
|
29
|
-
|
|
30
|
-
if (authToken) {
|
|
31
|
-
// Valideer enkel of het token geldig en actief is in de database (geen strenge project-ID match vereist)
|
|
32
|
-
const verified = verifyProjectToken(authToken);
|
|
33
|
-
if (!verified) {
|
|
34
|
-
throw new Error("Unauthorized: Invalid or inactive token.");
|
|
35
|
-
}
|
|
36
|
-
activeToken = authToken;
|
|
37
|
-
} else {
|
|
38
|
-
// Zoek het eerste actieve token in de database dat als universele sleutel kan dienen
|
|
39
|
-
const existingTokenRecord = db.select().from(projectTokens).where(eq(projectTokens.active, true)).get();
|
|
40
|
-
if (existingTokenRecord) {
|
|
41
|
-
activeToken = existingTokenRecord.token;
|
|
42
|
-
} else {
|
|
43
|
-
throw new Error("Unauthorized: No active token found. Please generate a token first via the /security page.");
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (!existing) {
|
|
48
|
-
db.insert(projects)
|
|
49
|
-
.values({
|
|
50
|
-
id: projectId,
|
|
51
|
-
name: projectName,
|
|
52
|
-
apiKeyHash: "jwt-secured",
|
|
53
|
-
structureJson: structureJsonString,
|
|
54
|
-
status: "offline",
|
|
55
|
-
createdAt: now,
|
|
56
|
-
})
|
|
57
|
-
.run();
|
|
58
|
-
} else {
|
|
59
|
-
db.update(projects)
|
|
60
|
-
.set({
|
|
61
|
-
name: projectName,
|
|
62
|
-
structureJson: structureJsonString,
|
|
63
|
-
})
|
|
64
|
-
.where(eq(projects.id, projectId))
|
|
65
|
-
.run();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const currentPaths = allPaths(structure);
|
|
69
|
-
const configEntries = db.select().from(overviewConfig).where(eq(overviewConfig.projectId, projectId)).all();
|
|
70
|
-
const staleOverviewPaths: string[] = [];
|
|
71
|
-
for (const entry of configEntries) {
|
|
72
|
-
if (!currentPaths.has(entry.capabilityPath)) {
|
|
73
|
-
staleOverviewPaths.push(entry.capabilityPath);
|
|
74
|
-
db.update(overviewConfig).set({ staleSince: now }).where(eq(overviewConfig.id, entry.id)).run();
|
|
75
|
-
} else if (entry.staleSince) {
|
|
76
|
-
db.update(overviewConfig).set({ staleSince: null }).where(eq(overviewConfig.id, entry.id)).run();
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return { token: activeToken!, staleOverviewPaths };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function verifyProjectToken(token: string): { universal: boolean } | null {
|
|
84
|
-
try {
|
|
85
|
-
jwt.verify(token, JWT_SECRET);
|
|
86
|
-
const record = db.select().from(projectTokens).where(and(eq(projectTokens.token, token), eq(projectTokens.active, true))).get();
|
|
87
|
-
if (!record) return null;
|
|
88
|
-
return { universal: true };
|
|
89
|
-
} catch {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function listAllTokens() {
|
|
95
|
-
return db.select().from(projectTokens).all();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function createTokenForProject(label?: string) {
|
|
99
|
-
const tokenName = label || "Universele Sleutel";
|
|
100
|
-
const token = jwt.sign({ name: tokenName, scope: "dashwire-access" }, JWT_SECRET);
|
|
101
|
-
const id = randomBytes(8).toString("hex");
|
|
102
|
-
const now = new Date().toISOString();
|
|
103
|
-
|
|
104
|
-
db.insert(projectTokens).values({
|
|
105
|
-
id,
|
|
106
|
-
projectId: "universal", // Niet gekoppeld aan 1 specifiek project
|
|
107
|
-
token,
|
|
108
|
-
active: true,
|
|
109
|
-
createdAt: now,
|
|
110
|
-
}).run();
|
|
111
|
-
|
|
112
|
-
return token;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function setTokenActiveStatus(tokenId: string, active: boolean) {
|
|
116
|
-
db.update(projectTokens).set({ active }).where(eq(projectTokens.id, tokenId)).run();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function deleteToken(tokenId: string) {
|
|
120
|
-
db.delete(projectTokens).where(eq(projectTokens.id, tokenId)).run();
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export function setProjectStatus(projectId: string, status: "online" | "offline"): void {
|
|
124
|
-
db.update(projects).set({ status, lastSeenAt: new Date().toISOString() }).where(eq(projects.id, projectId)).run();
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function getProject(projectId: string) {
|
|
128
|
-
const project = db.select().from(projects).where(eq(projects.id, projectId)).get();
|
|
129
|
-
if (!project) return null;
|
|
130
|
-
const state = db.select().from(capabilityState).where(eq(capabilityState.projectId, projectId)).all();
|
|
131
|
-
const stateMap: Record<string, unknown> = {};
|
|
132
|
-
for (const row of state) stateMap[row.capabilityPath] = JSON.parse(row.valueJson);
|
|
133
|
-
|
|
134
|
-
const tokenRecord = db.select().from(projectTokens).where(eq(projectTokens.active, true)).get();
|
|
135
|
-
const token = tokenRecord ? tokenRecord.token : jwt.sign({ scope: "dashwire-access" }, JWT_SECRET);
|
|
136
|
-
|
|
137
|
-
const { apiKeyHash, structureJson, ...safe } = project;
|
|
138
|
-
return {
|
|
139
|
-
...safe,
|
|
140
|
-
token,
|
|
141
|
-
structure: JSON.parse(structureJson) as DashboardStructure,
|
|
142
|
-
state: stateMap,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export function listProjects() {
|
|
147
|
-
return db.select().from(projects).all().map((p) => {
|
|
148
|
-
const tokenRecord = db.select().from(projectTokens).where(eq(projectTokens.active, true)).get();
|
|
149
|
-
const token = tokenRecord ? tokenRecord.token : "";
|
|
150
|
-
return {
|
|
151
|
-
id: p.id,
|
|
152
|
-
name: p.name,
|
|
153
|
-
status: p.status,
|
|
154
|
-
lastSeenAt: p.lastSeenAt,
|
|
155
|
-
token,
|
|
156
|
-
};
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export function upsertCapabilityState(projectId: string, path: string, value: unknown, overwrite = true): void {
|
|
161
|
-
const now = new Date().toISOString();
|
|
162
|
-
const existing = db.select()
|
|
163
|
-
.from(capabilityState)
|
|
164
|
-
.where(eq(capabilityState.projectId, projectId))
|
|
165
|
-
.all()
|
|
166
|
-
.find((r) => r.capabilityPath === path);
|
|
167
|
-
|
|
168
|
-
if (existing) {
|
|
169
|
-
if (overwrite) {
|
|
170
|
-
db.update(capabilityState)
|
|
171
|
-
.set({ valueJson: JSON.stringify(value), updatedAt: now })
|
|
172
|
-
.where(
|
|
173
|
-
and(
|
|
174
|
-
eq(capabilityState.projectId, projectId),
|
|
175
|
-
eq(capabilityState.capabilityPath, path)
|
|
176
|
-
)
|
|
177
|
-
)
|
|
178
|
-
.run();
|
|
179
|
-
}
|
|
180
|
-
} else {
|
|
181
|
-
db.insert(capabilityState)
|
|
182
|
-
.values({
|
|
183
|
-
projectId,
|
|
184
|
-
capabilityPath: path,
|
|
185
|
-
valueJson: JSON.stringify(value),
|
|
186
|
-
updatedAt: now
|
|
187
|
-
})
|
|
188
|
-
.run();
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export function bulkSyncState(projectId: string, state: Record<string, unknown>): void {
|
|
193
|
-
for (const [path, value] of Object.entries(state)) {
|
|
194
|
-
upsertCapabilityState(projectId, path, value, false);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export function findCapability(structure: DashboardStructure, path: string): Capability | undefined {
|
|
199
|
-
for (const section of structure.sections) {
|
|
200
|
-
const found = section.capabilities.find((c) => c.path === path);
|
|
201
|
-
if (found) return found;
|
|
202
|
-
}
|
|
203
|
-
return undefined;
|
|
204
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import type { Server as SocketServer, Socket } from "socket.io";
|
|
2
|
-
|
|
3
|
-
export function attachDashboardNamespace(io: SocketServer): void {
|
|
4
|
-
const dashNsp = io.of("/dashboard");
|
|
5
|
-
const sdkNsp = io.of("/sdk");
|
|
6
|
-
|
|
7
|
-
dashNsp.on("connection", (socket: Socket) => {
|
|
8
|
-
socket.on("subscribe:project", ({ projectId }: { projectId: string }) => {
|
|
9
|
-
if (!projectId) return;
|
|
10
|
-
socket.join(projectId);
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
socket.on("unsubscribe:project", ({ projectId }: { projectId: string }) => {
|
|
14
|
-
if (!projectId) return;
|
|
15
|
-
socket.leave(projectId);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
socket.on(
|
|
19
|
-
"capability:command",
|
|
20
|
-
({ projectId, path, value }: { projectId: string; path: string; value: unknown }) => {
|
|
21
|
-
if (!projectId || !path) return;
|
|
22
|
-
|
|
23
|
-
sdkNsp.to(projectId).emit("capability:command", { path, value });
|
|
24
|
-
}
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
socket.on(
|
|
28
|
-
"action:execute",
|
|
29
|
-
({ projectId, path }: { projectId: string; path: string }) => {
|
|
30
|
-
if (!projectId || !path) return;
|
|
31
|
-
|
|
32
|
-
sdkNsp.to(projectId).emit("action:execute", { path });
|
|
33
|
-
}
|
|
34
|
-
);
|
|
35
|
-
});
|
|
36
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import type { Server as SocketServer, Socket } from "socket.io";
|
|
2
|
-
import {
|
|
3
|
-
verifyProjectToken,
|
|
4
|
-
setProjectStatus,
|
|
5
|
-
upsertCapabilityState,
|
|
6
|
-
} from "../projects.service.js";
|
|
7
|
-
|
|
8
|
-
export function attachSdkNamespace(io: SocketServer): void {
|
|
9
|
-
const sdkNsp = io.of("/sdk");
|
|
10
|
-
|
|
11
|
-
sdkNsp.use((socket, next) => {
|
|
12
|
-
const token = socket.handshake.auth?.token;
|
|
13
|
-
|
|
14
|
-
if (!token) return next(new Error("Token required"));
|
|
15
|
-
if (!verifyProjectToken(token)) return next(new Error("Invalid token"));
|
|
16
|
-
|
|
17
|
-
next();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
sdkNsp.on("connection", (socket: Socket) => {
|
|
21
|
-
const projectId =
|
|
22
|
-
(socket.handshake.query.projectId as string) ||
|
|
23
|
-
(socket.handshake.auth?.projectId as string);
|
|
24
|
-
|
|
25
|
-
if (projectId) {
|
|
26
|
-
socket.data.projectId = projectId;
|
|
27
|
-
socket.join(projectId);
|
|
28
|
-
setProjectStatus(projectId, "online");
|
|
29
|
-
|
|
30
|
-
io.of("/dashboard").to(projectId).emit("project:online", {
|
|
31
|
-
projectId,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
socket.on("state:sync", ({ state }) => {
|
|
36
|
-
const pid = socket.data.projectId || projectId;
|
|
37
|
-
if (!pid) return;
|
|
38
|
-
|
|
39
|
-
setProjectStatus(pid, "online");
|
|
40
|
-
|
|
41
|
-
io.of("/dashboard").to(pid).emit("project:online", {
|
|
42
|
-
projectId: pid,
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
for (const [path, value] of Object.entries(state)) {
|
|
46
|
-
upsertCapabilityState(pid, path, value, false);
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
socket.on("state:update", ({ path, value }) => {
|
|
51
|
-
const pid = socket.data.projectId || projectId;
|
|
52
|
-
if (!pid) return;
|
|
53
|
-
|
|
54
|
-
upsertCapabilityState(pid, path, value, true);
|
|
55
|
-
|
|
56
|
-
io.of("/dashboard").to(pid).emit("state:update", {
|
|
57
|
-
projectId: pid,
|
|
58
|
-
path,
|
|
59
|
-
value,
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
socket.on("log:new", ({ level, message, source, metadata, timestamp }) => {
|
|
64
|
-
const pid = socket.data.projectId || projectId;
|
|
65
|
-
if (!pid) return;
|
|
66
|
-
|
|
67
|
-
io.of("/dashboard").to(pid).emit("log:new", {
|
|
68
|
-
projectId: pid,
|
|
69
|
-
level,
|
|
70
|
-
message,
|
|
71
|
-
source,
|
|
72
|
-
metadata,
|
|
73
|
-
timestamp,
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
socket.on("disconnect", () => {
|
|
78
|
-
const pid = socket.data.projectId || projectId;
|
|
79
|
-
if (!pid) return;
|
|
80
|
-
|
|
81
|
-
setProjectStatus(pid, "offline");
|
|
82
|
-
|
|
83
|
-
io.of("/dashboard").to(pid).emit("project:offline", {
|
|
84
|
-
projectId: pid,
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
}
|