@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.
Files changed (39) hide show
  1. package/dist/auth.service.d.ts +16 -0
  2. package/dist/auth.service.js +65 -0
  3. package/dist/db/client.d.ts +2 -0
  4. package/{src/db/client.ts → dist/db/client.js} +1 -4
  5. package/dist/db/schema.d.ts +570 -0
  6. package/dist/db/schema.js +46 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +114 -0
  9. package/dist/logs.service.d.ts +1 -0
  10. package/dist/logs.service.js +14 -0
  11. package/dist/overview.service.d.ts +12 -0
  12. package/dist/overview.service.js +20 -0
  13. package/dist/projects.service.d.ts +40 -0
  14. package/dist/projects.service.js +180 -0
  15. package/dist/realtime/dashboardNamespace.d.ts +2 -0
  16. package/dist/realtime/dashboardNamespace.js +26 -0
  17. package/dist/realtime/sdkNamespace.d.ts +2 -0
  18. package/dist/realtime/sdkNamespace.js +69 -0
  19. package/dist/routes/auth.d.ts +2 -0
  20. package/dist/routes/auth.js +76 -0
  21. package/dist/routes/overview.d.ts +2 -0
  22. package/dist/routes/overview.js +21 -0
  23. package/dist/routes/projects.d.ts +3 -0
  24. package/dist/routes/projects.js +66 -0
  25. package/package.json +5 -4
  26. package/dashwire.db +0 -0
  27. package/drizzle.config.ts +0 -10
  28. package/src/auth.service.ts +0 -76
  29. package/src/db/schema.ts +0 -52
  30. package/src/index.ts +0 -44
  31. package/src/logs.service.ts +0 -15
  32. package/src/overview.service.ts +0 -28
  33. package/src/projects.service.ts +0 -204
  34. package/src/realtime/dashboardNamespace.ts +0 -36
  35. package/src/realtime/sdkNamespace.ts +0 -88
  36. package/src/routes/auth.ts +0 -90
  37. package/src/routes/overview.ts +0 -27
  38. package/src/routes/projects.ts +0 -93
  39. package/tsconfig.json +0 -11
@@ -0,0 +1,46 @@
1
+ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
2
+ export const projects = sqliteTable("projects", {
3
+ id: text("id").primaryKey(),
4
+ name: text("name").notNull(),
5
+ apiKeyHash: text("api_key_hash").notNull(),
6
+ structureJson: text("structure_json").notNull(),
7
+ status: text("status", { enum: ["online", "offline"] }).notNull().default("offline"),
8
+ createdAt: text("created_at").notNull(),
9
+ lastSeenAt: text("last_seen_at"),
10
+ });
11
+ export const projectTokens = sqliteTable("project_tokens", {
12
+ id: text("id").primaryKey(),
13
+ projectId: text("project_id").notNull(),
14
+ token: text("token").notNull().unique(),
15
+ active: integer("active", { mode: "boolean" }).notNull().default(true),
16
+ createdAt: text("created_at").notNull(),
17
+ });
18
+ export const capabilityState = sqliteTable("capability_state", {
19
+ id: integer("id").primaryKey({ autoIncrement: true }),
20
+ projectId: text("project_id").notNull(),
21
+ capabilityPath: text("capability_path").notNull(),
22
+ valueJson: text("value_json").notNull(),
23
+ updatedAt: text("updated_at").notNull(),
24
+ });
25
+ export const overviewConfig = sqliteTable("overview_config", {
26
+ id: integer("id").primaryKey({ autoIncrement: true }),
27
+ projectId: text("project_id").notNull(),
28
+ capabilityPath: text("capability_path").notNull(),
29
+ staleSince: text("stale_since"),
30
+ });
31
+ export const logs = sqliteTable("logs", {
32
+ id: integer("id").primaryKey({ autoIncrement: true }),
33
+ projectId: text("project_id").notNull(),
34
+ level: text("level").notNull(),
35
+ message: text("message").notNull(),
36
+ source: text("source"),
37
+ metadataJson: text("metadata_json"),
38
+ timestamp: text("timestamp").notNull(),
39
+ });
40
+ export const users = sqliteTable("users", {
41
+ id: text("id").primaryKey(),
42
+ username: text("username").notNull().unique(),
43
+ passwordHash: text("password_hash").notNull(),
44
+ role: text("role").notNull().default("admin"),
45
+ createdAt: text("created_at").notNull(),
46
+ });
@@ -0,0 +1 @@
1
+ export declare function startDashwire(userConfig?: any): void;
package/dist/index.js ADDED
@@ -0,0 +1,114 @@
1
+ import Fastify from "fastify";
2
+ import fastifyStatic from "@fastify/static";
3
+ import { Server as SocketServer } from "socket.io";
4
+ import Database from "better-sqlite3";
5
+ import { fileURLToPath } from "node:url";
6
+ import { dirname, join } from "node:path";
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const PORT = Number(process.env.PORT ?? 4000);
9
+ const dbPath = process.env.DASHWIRE_DB_PATH ?? "./dashwire.db";
10
+ const app = Fastify({ logger: true });
11
+ const sqlite = new Database(dbPath);
12
+ sqlite.pragma("journal_mode = WAL");
13
+ // Database Schema Initialisatie
14
+ sqlite.exec(`
15
+ CREATE TABLE IF NOT EXISTS projects (
16
+ id TEXT PRIMARY KEY,
17
+ name TEXT NOT NULL,
18
+ api_key_hash TEXT NOT NULL,
19
+ structure_version INTEGER NOT NULL DEFAULT 1,
20
+ structure_json TEXT NOT NULL,
21
+ status TEXT NOT NULL DEFAULT 'offline',
22
+ last_seen_at TEXT,
23
+ created_at TEXT NOT NULL
24
+ );
25
+ CREATE TABLE IF NOT EXISTS capability_state (
26
+ project_id TEXT NOT NULL,
27
+ capability_path TEXT NOT NULL,
28
+ value_json TEXT NOT NULL,
29
+ updated_at TEXT NOT NULL,
30
+ PRIMARY KEY (project_id, capability_path)
31
+ );
32
+ `);
33
+ app.addHook("onRequest", async (req, reply) => {
34
+ reply.header("Access-Control-Allow-Origin", "*");
35
+ reply.header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS");
36
+ reply.header("Access-Control-Allow-Headers", "Content-Type, Authorization, x-api-key, x-admin-token");
37
+ if (req.method === "OPTIONS")
38
+ return reply.code(200).send();
39
+ });
40
+ app.get("/health", async () => ({ ok: true }));
41
+ app.get("/api/projects", async () => {
42
+ return sqlite.prepare("SELECT id, name, status, last_seen_at as lastSeenAt, structure_version as structureVersion FROM projects").all();
43
+ });
44
+ app.get("/api/projects/:id", async (req, reply) => {
45
+ const project = sqlite.prepare("SELECT * FROM projects WHERE id = ?").get(req.params.id);
46
+ if (!project)
47
+ return reply.status(404).send({ error: "not found" });
48
+ const states = sqlite.prepare("SELECT capability_path, value_json FROM capability_state WHERE project_id = ?").all(req.params.id);
49
+ const stateMap = {};
50
+ for (const row of states)
51
+ stateMap[row.capability_path] = JSON.parse(row.value_json);
52
+ return {
53
+ id: project.id,
54
+ name: project.name,
55
+ status: project.status,
56
+ structure: JSON.parse(project.structure_json),
57
+ state: stateMap,
58
+ };
59
+ });
60
+ // Serveer de ingebouwde Dashboard UI
61
+ app.register(fastifyStatic, {
62
+ root: join(__dirname, "../public"),
63
+ prefix: "/",
64
+ });
65
+ // SPA catch-all fallback voor Next.js client-side routing
66
+ app.setNotFoundHandler((req, reply) => {
67
+ if (req.url?.startsWith("/api") || req.url?.startsWith("/sdk")) {
68
+ return reply.code(404).send({ error: "Not found" });
69
+ }
70
+ return reply.sendFile("index.html");
71
+ });
72
+ const io = new SocketServer(app.server, { cors: { origin: "*" } });
73
+ const sdkNsp = io.of("/sdk");
74
+ sdkNsp.on("connection", (socket) => {
75
+ const { projectId } = socket.handshake.auth || {};
76
+ if (!projectId)
77
+ return socket.disconnect();
78
+ socket.join(`project:${projectId}`);
79
+ sqlite.prepare("UPDATE projects SET status = 'online', last_seen_at = ? WHERE id = ?").run(new Date().toISOString(), projectId);
80
+ io.of("/dashboard").to(`project:${projectId}`).emit("project:online", { projectId });
81
+ socket.on("state:sync", ({ state }) => {
82
+ const stmt = sqlite.prepare(`INSERT INTO capability_state (project_id, capability_path, value_json, updated_at) VALUES (?, ?, ?, ?) ON CONFLICT(project_id, capability_path) DO UPDATE SET value_json = excluded.value_json, updated_at = excluded.updated_at`);
83
+ const now = new Date().toISOString();
84
+ for (const [path, val] of Object.entries(state || {})) {
85
+ stmt.run(projectId, path, JSON.stringify(val), now);
86
+ io.of("/dashboard").to(`project:${projectId}`).emit("state:update", { projectId, path, value: val });
87
+ }
88
+ });
89
+ socket.on("state:update", ({ path, value }) => {
90
+ const now = new Date().toISOString();
91
+ sqlite.prepare(`INSERT INTO capability_state (project_id, capability_path, value_json, updated_at) VALUES (?, ?, ?, ?) ON CONFLICT(project_id, capability_path) DO UPDATE SET value_json = excluded.value_json, updated_at = excluded.updated_at`).run(projectId, path, JSON.stringify(value), now);
92
+ io.of("/dashboard").to(`project:${projectId}`).emit("state:update", { projectId, path, value });
93
+ });
94
+ socket.on("disconnect", () => {
95
+ sqlite.prepare("UPDATE projects SET status = 'offline', last_seen_at = ? WHERE id = ?").run(new Date().toISOString(), projectId);
96
+ io.of("/dashboard").to(`project:${projectId}`).emit("project:offline", { projectId });
97
+ });
98
+ });
99
+ const dashNsp = io.of("/dashboard");
100
+ dashNsp.on("connection", (socket) => {
101
+ socket.on("subscribe:project", ({ projectId }) => socket.join(`project:${projectId}`));
102
+ socket.on("unsubscribe:project", ({ projectId }) => socket.leave(`project:${projectId}`));
103
+ socket.on("capability:command", ({ projectId, path, value }) => {
104
+ io.of("/sdk").to(`project:${projectId}`).emit("capability:command", { path, value });
105
+ });
106
+ socket.on("action:execute", ({ projectId, path }) => {
107
+ io.of("/sdk").to(`project:${projectId}`).emit("action:execute", { path });
108
+ });
109
+ });
110
+ export function startDashwire(userConfig) {
111
+ app.listen({ port: PORT, host: "0.0.0.0" }).then(() => {
112
+ console.log(`⚡ Dashwire server & dashboard running on http://0.0.0.0:${PORT}`);
113
+ });
114
+ }
@@ -0,0 +1 @@
1
+ export declare function insertLog(projectId: string, level: string, message: string, source?: string, metadata?: Record<string, unknown>): number;
@@ -0,0 +1,14 @@
1
+ import { db } from "./db/client.js";
2
+ import { logs } from "./db/schema.js";
3
+ export function insertLog(projectId, level, message, source, metadata) {
4
+ const timestamp = new Date().toISOString();
5
+ const res = db.insert(logs).values({
6
+ projectId,
7
+ level,
8
+ message,
9
+ source: source ?? null,
10
+ metadataJson: metadata ? JSON.stringify(metadata) : null,
11
+ timestamp,
12
+ }).run();
13
+ return Number(res.lastInsertRowid);
14
+ }
@@ -0,0 +1,12 @@
1
+ export interface OverviewEntryInput {
2
+ projectId: string;
3
+ capabilityPath: string;
4
+ order: number;
5
+ }
6
+ export declare function setOverviewConfig(entries: OverviewEntryInput[]): void;
7
+ export declare function getOverviewConfig(projectId?: string): {
8
+ id: number;
9
+ projectId: string;
10
+ capabilityPath: string;
11
+ staleSince: string | null;
12
+ }[];
@@ -0,0 +1,20 @@
1
+ import { db } from "./db/client.js";
2
+ import { overviewConfig } from "./db/schema.js";
3
+ import { eq } from "drizzle-orm";
4
+ export function setOverviewConfig(entries) {
5
+ db.delete(overviewConfig).run();
6
+ for (const entry of entries) {
7
+ db.insert(overviewConfig)
8
+ .values({
9
+ projectId: entry.projectId,
10
+ capabilityPath: entry.capabilityPath
11
+ })
12
+ .run();
13
+ }
14
+ }
15
+ export function getOverviewConfig(projectId) {
16
+ if (projectId) {
17
+ return db.select().from(overviewConfig).where(eq(overviewConfig.projectId, projectId)).all();
18
+ }
19
+ return db.select().from(overviewConfig).all();
20
+ }
@@ -0,0 +1,40 @@
1
+ import type { DashboardStructure, Capability } from "@dashwire/core";
2
+ export interface RegisterResult {
3
+ token: string;
4
+ staleOverviewPaths: string[];
5
+ }
6
+ export declare function registerProject(projectId: string, projectName: string, structure: DashboardStructure, authToken?: string): RegisterResult;
7
+ export declare function verifyProjectToken(token: string): {
8
+ universal: boolean;
9
+ } | null;
10
+ export declare function listAllTokens(): {
11
+ id: string;
12
+ createdAt: string;
13
+ projectId: string;
14
+ token: string;
15
+ active: boolean;
16
+ }[];
17
+ export declare function createTokenForProject(label?: string): string;
18
+ export declare function setTokenActiveStatus(tokenId: string, active: boolean): void;
19
+ export declare function deleteToken(tokenId: string): void;
20
+ export declare function setProjectStatus(projectId: string, status: "online" | "offline"): void;
21
+ export declare function getProject(projectId: string): {
22
+ token: string;
23
+ structure: DashboardStructure;
24
+ state: Record<string, unknown>;
25
+ id: string;
26
+ name: string;
27
+ status: "online" | "offline";
28
+ createdAt: string;
29
+ lastSeenAt: string | null;
30
+ } | null;
31
+ export declare function listProjects(): {
32
+ id: string;
33
+ name: string;
34
+ status: "online" | "offline";
35
+ lastSeenAt: string | null;
36
+ token: string;
37
+ }[];
38
+ export declare function upsertCapabilityState(projectId: string, path: string, value: unknown, overwrite?: boolean): void;
39
+ export declare function bulkSyncState(projectId: string, state: Record<string, unknown>): void;
40
+ export declare function findCapability(structure: DashboardStructure, path: string): Capability | undefined;
@@ -0,0 +1,180 @@
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
+ const JWT_SECRET = process.env.JWT_SECRET || "super-secret-dashwire-key";
7
+ function allPaths(structure) {
8
+ const set = new Set();
9
+ for (const section of structure.sections) {
10
+ for (const cap of section.capabilities)
11
+ set.add(cap.path);
12
+ }
13
+ return set;
14
+ }
15
+ export function registerProject(projectId, projectName, structure, authToken) {
16
+ const existing = db.select().from(projects).where(eq(projects.id, projectId)).get();
17
+ const now = new Date().toISOString();
18
+ const structureJsonString = JSON.stringify(structure);
19
+ let activeToken = authToken;
20
+ if (authToken) {
21
+ // Valideer enkel of het token geldig en actief is in de database (geen strenge project-ID match vereist)
22
+ const verified = verifyProjectToken(authToken);
23
+ if (!verified) {
24
+ throw new Error("Unauthorized: Invalid or inactive token.");
25
+ }
26
+ activeToken = authToken;
27
+ }
28
+ else {
29
+ // Zoek het eerste actieve token in de database dat als universele sleutel kan dienen
30
+ const existingTokenRecord = db.select().from(projectTokens).where(eq(projectTokens.active, true)).get();
31
+ if (existingTokenRecord) {
32
+ activeToken = existingTokenRecord.token;
33
+ }
34
+ else {
35
+ throw new Error("Unauthorized: No active token found. Please generate a token first via the /security page.");
36
+ }
37
+ }
38
+ if (!existing) {
39
+ db.insert(projects)
40
+ .values({
41
+ id: projectId,
42
+ name: projectName,
43
+ apiKeyHash: "jwt-secured",
44
+ structureJson: structureJsonString,
45
+ status: "offline",
46
+ createdAt: now,
47
+ })
48
+ .run();
49
+ }
50
+ else {
51
+ db.update(projects)
52
+ .set({
53
+ name: projectName,
54
+ structureJson: structureJsonString,
55
+ })
56
+ .where(eq(projects.id, projectId))
57
+ .run();
58
+ }
59
+ const currentPaths = allPaths(structure);
60
+ const configEntries = db.select().from(overviewConfig).where(eq(overviewConfig.projectId, projectId)).all();
61
+ const staleOverviewPaths = [];
62
+ for (const entry of configEntries) {
63
+ if (!currentPaths.has(entry.capabilityPath)) {
64
+ staleOverviewPaths.push(entry.capabilityPath);
65
+ db.update(overviewConfig).set({ staleSince: now }).where(eq(overviewConfig.id, entry.id)).run();
66
+ }
67
+ else if (entry.staleSince) {
68
+ db.update(overviewConfig).set({ staleSince: null }).where(eq(overviewConfig.id, entry.id)).run();
69
+ }
70
+ }
71
+ return { token: activeToken, staleOverviewPaths };
72
+ }
73
+ export function verifyProjectToken(token) {
74
+ try {
75
+ jwt.verify(token, JWT_SECRET);
76
+ const record = db.select().from(projectTokens).where(and(eq(projectTokens.token, token), eq(projectTokens.active, true))).get();
77
+ if (!record)
78
+ return null;
79
+ return { universal: true };
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ }
85
+ export function listAllTokens() {
86
+ return db.select().from(projectTokens).all();
87
+ }
88
+ export function createTokenForProject(label) {
89
+ const tokenName = label || "Universele Sleutel";
90
+ const token = jwt.sign({ name: tokenName, scope: "dashwire-access" }, JWT_SECRET);
91
+ const id = randomBytes(8).toString("hex");
92
+ const now = new Date().toISOString();
93
+ db.insert(projectTokens).values({
94
+ id,
95
+ projectId: "universal", // Niet gekoppeld aan 1 specifiek project
96
+ token,
97
+ active: true,
98
+ createdAt: now,
99
+ }).run();
100
+ return token;
101
+ }
102
+ export function setTokenActiveStatus(tokenId, active) {
103
+ db.update(projectTokens).set({ active }).where(eq(projectTokens.id, tokenId)).run();
104
+ }
105
+ export function deleteToken(tokenId) {
106
+ db.delete(projectTokens).where(eq(projectTokens.id, tokenId)).run();
107
+ }
108
+ export function setProjectStatus(projectId, status) {
109
+ db.update(projects).set({ status, lastSeenAt: new Date().toISOString() }).where(eq(projects.id, projectId)).run();
110
+ }
111
+ export function getProject(projectId) {
112
+ const project = db.select().from(projects).where(eq(projects.id, projectId)).get();
113
+ if (!project)
114
+ return null;
115
+ const state = db.select().from(capabilityState).where(eq(capabilityState.projectId, projectId)).all();
116
+ const stateMap = {};
117
+ for (const row of state)
118
+ stateMap[row.capabilityPath] = JSON.parse(row.valueJson);
119
+ const tokenRecord = db.select().from(projectTokens).where(eq(projectTokens.active, true)).get();
120
+ const token = tokenRecord ? tokenRecord.token : jwt.sign({ scope: "dashwire-access" }, JWT_SECRET);
121
+ const { apiKeyHash, structureJson, ...safe } = project;
122
+ return {
123
+ ...safe,
124
+ token,
125
+ structure: JSON.parse(structureJson),
126
+ state: stateMap,
127
+ };
128
+ }
129
+ export function listProjects() {
130
+ return db.select().from(projects).all().map((p) => {
131
+ const tokenRecord = db.select().from(projectTokens).where(eq(projectTokens.active, true)).get();
132
+ const token = tokenRecord ? tokenRecord.token : "";
133
+ return {
134
+ id: p.id,
135
+ name: p.name,
136
+ status: p.status,
137
+ lastSeenAt: p.lastSeenAt,
138
+ token,
139
+ };
140
+ });
141
+ }
142
+ export function upsertCapabilityState(projectId, path, value, overwrite = true) {
143
+ const now = new Date().toISOString();
144
+ const existing = db.select()
145
+ .from(capabilityState)
146
+ .where(eq(capabilityState.projectId, projectId))
147
+ .all()
148
+ .find((r) => r.capabilityPath === path);
149
+ if (existing) {
150
+ if (overwrite) {
151
+ db.update(capabilityState)
152
+ .set({ valueJson: JSON.stringify(value), updatedAt: now })
153
+ .where(and(eq(capabilityState.projectId, projectId), eq(capabilityState.capabilityPath, path)))
154
+ .run();
155
+ }
156
+ }
157
+ else {
158
+ db.insert(capabilityState)
159
+ .values({
160
+ projectId,
161
+ capabilityPath: path,
162
+ valueJson: JSON.stringify(value),
163
+ updatedAt: now
164
+ })
165
+ .run();
166
+ }
167
+ }
168
+ export function bulkSyncState(projectId, state) {
169
+ for (const [path, value] of Object.entries(state)) {
170
+ upsertCapabilityState(projectId, path, value, false);
171
+ }
172
+ }
173
+ export function findCapability(structure, path) {
174
+ for (const section of structure.sections) {
175
+ const found = section.capabilities.find((c) => c.path === path);
176
+ if (found)
177
+ return found;
178
+ }
179
+ return undefined;
180
+ }
@@ -0,0 +1,2 @@
1
+ import type { Server as SocketServer } from "socket.io";
2
+ export declare function attachDashboardNamespace(io: SocketServer): void;
@@ -0,0 +1,26 @@
1
+ export function attachDashboardNamespace(io) {
2
+ const dashNsp = io.of("/dashboard");
3
+ const sdkNsp = io.of("/sdk");
4
+ dashNsp.on("connection", (socket) => {
5
+ socket.on("subscribe:project", ({ projectId }) => {
6
+ if (!projectId)
7
+ return;
8
+ socket.join(projectId);
9
+ });
10
+ socket.on("unsubscribe:project", ({ projectId }) => {
11
+ if (!projectId)
12
+ return;
13
+ socket.leave(projectId);
14
+ });
15
+ socket.on("capability:command", ({ projectId, path, value }) => {
16
+ if (!projectId || !path)
17
+ return;
18
+ sdkNsp.to(projectId).emit("capability:command", { path, value });
19
+ });
20
+ socket.on("action:execute", ({ projectId, path }) => {
21
+ if (!projectId || !path)
22
+ return;
23
+ sdkNsp.to(projectId).emit("action:execute", { path });
24
+ });
25
+ });
26
+ }
@@ -0,0 +1,2 @@
1
+ import type { Server as SocketServer } from "socket.io";
2
+ export declare function attachSdkNamespace(io: SocketServer): void;
@@ -0,0 +1,69 @@
1
+ import { verifyProjectToken, setProjectStatus, upsertCapabilityState, } from "../projects.service.js";
2
+ export function attachSdkNamespace(io) {
3
+ const sdkNsp = io.of("/sdk");
4
+ sdkNsp.use((socket, next) => {
5
+ const token = socket.handshake.auth?.token;
6
+ if (!token)
7
+ return next(new Error("Token required"));
8
+ if (!verifyProjectToken(token))
9
+ return next(new Error("Invalid token"));
10
+ next();
11
+ });
12
+ sdkNsp.on("connection", (socket) => {
13
+ const projectId = socket.handshake.query.projectId ||
14
+ socket.handshake.auth?.projectId;
15
+ if (projectId) {
16
+ socket.data.projectId = projectId;
17
+ socket.join(projectId);
18
+ setProjectStatus(projectId, "online");
19
+ io.of("/dashboard").to(projectId).emit("project:online", {
20
+ projectId,
21
+ });
22
+ }
23
+ socket.on("state:sync", ({ state }) => {
24
+ const pid = socket.data.projectId || projectId;
25
+ if (!pid)
26
+ return;
27
+ setProjectStatus(pid, "online");
28
+ io.of("/dashboard").to(pid).emit("project:online", {
29
+ projectId: pid,
30
+ });
31
+ for (const [path, value] of Object.entries(state)) {
32
+ upsertCapabilityState(pid, path, value, false);
33
+ }
34
+ });
35
+ socket.on("state:update", ({ path, value }) => {
36
+ const pid = socket.data.projectId || projectId;
37
+ if (!pid)
38
+ return;
39
+ upsertCapabilityState(pid, path, value, true);
40
+ io.of("/dashboard").to(pid).emit("state:update", {
41
+ projectId: pid,
42
+ path,
43
+ value,
44
+ });
45
+ });
46
+ socket.on("log:new", ({ level, message, source, metadata, timestamp }) => {
47
+ const pid = socket.data.projectId || projectId;
48
+ if (!pid)
49
+ return;
50
+ io.of("/dashboard").to(pid).emit("log:new", {
51
+ projectId: pid,
52
+ level,
53
+ message,
54
+ source,
55
+ metadata,
56
+ timestamp,
57
+ });
58
+ });
59
+ socket.on("disconnect", () => {
60
+ const pid = socket.data.projectId || projectId;
61
+ if (!pid)
62
+ return;
63
+ setProjectStatus(pid, "offline");
64
+ io.of("/dashboard").to(pid).emit("project:offline", {
65
+ projectId: pid,
66
+ });
67
+ });
68
+ });
69
+ }
@@ -0,0 +1,2 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ export declare function authRoutes(app: FastifyInstance): void;
@@ -0,0 +1,76 @@
1
+ import { hasAnyAdmin, createAdminUser, authenticateUser, createUserRecord, verifyAuthToken, listUsers, deleteUser } from "../auth.service.js";
2
+ export function authRoutes(app) {
3
+ app.get("/api/auth/status", async (_req, reply) => {
4
+ return reply.send({ hasAdmin: hasAnyAdmin() });
5
+ });
6
+ app.post("/api/auth/setup", async (req, reply) => {
7
+ const { username, password } = req.body || {};
8
+ if (!username || !password) {
9
+ return reply.code(400).send({ error: "Gebruikersnaam en wachtwoord zijn verplicht." });
10
+ }
11
+ try {
12
+ if (hasAnyAdmin()) {
13
+ return reply.code(400).send({ error: "Setup is al voltooid." });
14
+ }
15
+ const token = createAdminUser(username, password);
16
+ return reply.send({ token });
17
+ }
18
+ catch (err) {
19
+ return reply.code(400).send({ error: err.message });
20
+ }
21
+ });
22
+ app.post("/api/auth/login", async (req, reply) => {
23
+ const { username, password } = req.body || {};
24
+ if (!username || !password) {
25
+ return reply.code(400).send({ error: "Vul alle velden in." });
26
+ }
27
+ try {
28
+ const token = authenticateUser(username, password);
29
+ return reply.send({ token });
30
+ }
31
+ catch (err) {
32
+ return reply.code(401).send({ error: err.message });
33
+ }
34
+ });
35
+ app.get("/api/users", async (req, reply) => {
36
+ const authHeader = req.headers.authorization;
37
+ const token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : "";
38
+ const verified = verifyAuthToken(token);
39
+ if (!verified || verified.role !== "admin") {
40
+ return reply.code(403).send({ error: "Toegang geweigerd. Alleen voor admins." });
41
+ }
42
+ return reply.send(listUsers());
43
+ });
44
+ app.post("/api/users", async (req, reply) => {
45
+ const authHeader = req.headers.authorization;
46
+ const token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : "";
47
+ const verified = verifyAuthToken(token);
48
+ if (!verified || verified.role !== "admin") {
49
+ return reply.code(403).send({ error: "Toegang geweigerd. Alleen voor admins." });
50
+ }
51
+ try {
52
+ const { username, password, role } = req.body;
53
+ const newUser = createUserRecord(username, password, role || "user");
54
+ return reply.send(newUser);
55
+ }
56
+ catch (err) {
57
+ return reply.code(400).send({ error: err.message });
58
+ }
59
+ });
60
+ // --- AANPASSING IN: de DELETE /api/users/:userId route ---
61
+ app.delete("/api/users/:userId", async (req, reply) => {
62
+ const authHeader = req.headers.authorization;
63
+ const token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : "";
64
+ const verified = verifyAuthToken(token);
65
+ if (!verified || verified.role !== "admin") {
66
+ return reply.code(403).send({ error: "Toegang geweigerd." });
67
+ }
68
+ try {
69
+ deleteUser(req.params.userId, verified.id);
70
+ return reply.send({ success: true });
71
+ }
72
+ catch (err) {
73
+ return reply.code(400).send({ error: err.message });
74
+ }
75
+ });
76
+ }
@@ -0,0 +1,2 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ export declare function overviewRoutes(app: FastifyInstance): void;
@@ -0,0 +1,21 @@
1
+ import { getOverviewConfig, setOverviewConfig } from "../overview.service.js";
2
+ function requireAdmin(req, reply) {
3
+ const token = req.headers["x-admin-token"];
4
+ const expected = process.env.DASHWIRE_ADMIN_TOKEN;
5
+ if (!expected || token !== expected) {
6
+ reply.status(401).send({ error: "unauthorized" });
7
+ return false;
8
+ }
9
+ return true;
10
+ }
11
+ export function overviewRoutes(app) {
12
+ app.get("/api/overview-config", async () => {
13
+ return getOverviewConfig();
14
+ });
15
+ app.put("/api/overview-config", async (req, reply) => {
16
+ if (!requireAdmin(req, reply))
17
+ return;
18
+ setOverviewConfig(req.body.entries);
19
+ return reply.send({ ok: true });
20
+ });
21
+ }
@@ -0,0 +1,3 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import type { Server as SocketServer } from "socket.io";
3
+ export declare function projectRoutes(app: FastifyInstance, io: SocketServer): void;