@dashwire/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/dashwire.db ADDED
Binary file
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "drizzle-kit";
2
+
3
+ export default defineConfig({
4
+ schema: "./src/db/schema.ts",
5
+ out: "./drizzle",
6
+ dialect: "sqlite",
7
+ dbCredentials: {
8
+ url: process.env.DASHWIRE_DB_PATH ?? "./dashwire.db",
9
+ },
10
+ });
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@dashwire/server",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "private": false,
8
+ "scripts": {
9
+ "build": "tsc -p tsconfig.json",
10
+ "dev": "tsx watch src/index.ts",
11
+ "start": "node dist/index.js"
12
+ },
13
+ "dependencies": {
14
+ "@dashwire/core": "workspace:*",
15
+ "better-sqlite3": "^11.3.0",
16
+ "drizzle-orm": "^0.33.0",
17
+ "fastify": "^4.28.1",
18
+ "jsonwebtoken": "^9.0.3",
19
+ "nanoid": "^5.0.7",
20
+ "socket.io": "^4.8.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/better-sqlite3": "^7.6.11",
24
+ "@types/jsonwebtoken": "^9.0.10",
25
+ "@types/node": "^22.7.0",
26
+ "drizzle-kit": "^0.24.2",
27
+ "tsx": "^4.19.1",
28
+ "typescript": "^5.6.0"
29
+ }
30
+ }
@@ -0,0 +1,76 @@
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
+ }
@@ -0,0 +1,60 @@
1
+ import Database from "better-sqlite3";
2
+ import { drizzle } from "drizzle-orm/better-sqlite3";
3
+ import * as schema from "./schema.js";
4
+
5
+ const sqlite = new Database("dashwire.db");
6
+
7
+ sqlite.exec(`
8
+ CREATE TABLE IF NOT EXISTS projects (
9
+ id TEXT PRIMARY KEY,
10
+ name TEXT NOT NULL,
11
+ api_key_hash TEXT NOT NULL,
12
+ structure_json TEXT NOT NULL,
13
+ status TEXT NOT NULL DEFAULT 'offline',
14
+ created_at TEXT NOT NULL,
15
+ last_seen_at TEXT
16
+ );
17
+
18
+ CREATE TABLE IF NOT EXISTS project_tokens (
19
+ id TEXT PRIMARY KEY,
20
+ project_id TEXT NOT NULL,
21
+ token TEXT NOT NULL UNIQUE,
22
+ active INTEGER NOT NULL DEFAULT 1,
23
+ created_at TEXT NOT NULL
24
+ );
25
+
26
+ CREATE TABLE IF NOT EXISTS capability_state (
27
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
28
+ project_id TEXT NOT NULL,
29
+ capability_path TEXT NOT NULL,
30
+ value_json TEXT NOT NULL,
31
+ updated_at TEXT NOT NULL
32
+ );
33
+
34
+ CREATE TABLE IF NOT EXISTS overview_config (
35
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
36
+ project_id TEXT NOT NULL,
37
+ capability_path TEXT NOT NULL,
38
+ stale_since TEXT
39
+ );
40
+
41
+ CREATE TABLE IF NOT EXISTS logs (
42
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
43
+ project_id TEXT NOT NULL,
44
+ level TEXT NOT NULL,
45
+ message TEXT NOT NULL,
46
+ source TEXT,
47
+ metadata_json TEXT,
48
+ timestamp TEXT NOT NULL
49
+ );
50
+
51
+ CREATE TABLE IF NOT EXISTS users (
52
+ id TEXT PRIMARY KEY,
53
+ username TEXT NOT NULL UNIQUE,
54
+ password_hash TEXT NOT NULL,
55
+ role TEXT NOT NULL DEFAULT 'admin',
56
+ created_at TEXT NOT NULL
57
+ );
58
+ `);
59
+
60
+ export const db = drizzle(sqlite, { schema });
@@ -0,0 +1,52 @@
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 ADDED
@@ -0,0 +1,44 @@
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
+ }
@@ -0,0 +1,15 @@
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
+ }
@@ -0,0 +1,28 @@
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
+ }
@@ -0,0 +1,204 @@
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
+ }
@@ -0,0 +1,36 @@
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
+ }
@@ -0,0 +1,88 @@
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
+ }
@@ -0,0 +1,90 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import { hasAnyAdmin, createAdminUser, authenticateUser, createUserRecord, verifyAuthToken, listUsers, deleteUser } from "../auth.service.js";
3
+
4
+ export function authRoutes(app: FastifyInstance) {
5
+ app.get("/api/auth/status", async (_req, reply) => {
6
+ return reply.send({ hasAdmin: hasAnyAdmin() });
7
+ });
8
+
9
+ app.post<{ Body: { username?: string; password?: string } }>(
10
+ "/api/auth/setup",
11
+ async (req, reply) => {
12
+ const { username, password } = req.body || {};
13
+ if (!username || !password) {
14
+ return reply.code(400).send({ error: "Gebruikersnaam en wachtwoord zijn verplicht." });
15
+ }
16
+ try {
17
+ if (hasAnyAdmin()) {
18
+ return reply.code(400).send({ error: "Setup is al voltooid." });
19
+ }
20
+ const token = createAdminUser(username, password);
21
+ return reply.send({ token });
22
+ } catch (err: any) {
23
+ return reply.code(400).send({ error: err.message });
24
+ }
25
+ }
26
+ );
27
+
28
+ app.post<{ Body: { username?: string; password?: string } }>(
29
+ "/api/auth/login",
30
+ async (req, reply) => {
31
+ const { username, password } = req.body || {};
32
+ if (!username || !password) {
33
+ return reply.code(400).send({ error: "Vul alle velden in." });
34
+ }
35
+ try {
36
+ const token = authenticateUser(username, password);
37
+ return reply.send({ token });
38
+ } catch (err: any) {
39
+ return reply.code(401).send({ error: err.message });
40
+ }
41
+ }
42
+ );
43
+
44
+ app.get("/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
+ return reply.send(listUsers());
52
+ });
53
+
54
+ app.post<{ Body: { username: string; password: string; role?: "admin" | "user" } }>(
55
+ "/api/users",
56
+ async (req, reply) => {
57
+ const authHeader = req.headers.authorization;
58
+ const token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : "";
59
+ const verified = verifyAuthToken(token);
60
+ if (!verified || verified.role !== "admin") {
61
+ return reply.code(403).send({ error: "Toegang geweigerd. Alleen voor admins." });
62
+ }
63
+ try {
64
+ const { username, password, role } = req.body;
65
+ const newUser = createUserRecord(username, password, role || "user");
66
+ return reply.send(newUser);
67
+ } catch (err: any) {
68
+ return reply.code(400).send({ error: err.message });
69
+ }
70
+ }
71
+ );
72
+
73
+ // --- AANPASSING IN: de DELETE /api/users/:userId route ---
74
+ app.delete<{ Params: { userId: string } }>("/api/users/:userId", async (req, reply) => {
75
+ const authHeader = req.headers.authorization;
76
+ const token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : "";
77
+ const verified = verifyAuthToken(token);
78
+
79
+ if (!verified || verified.role !== "admin") {
80
+ return reply.code(403).send({ error: "Toegang geweigerd." });
81
+ }
82
+
83
+ try {
84
+ deleteUser(req.params.userId, verified.id);
85
+ return reply.send({ success: true });
86
+ } catch (err: any) {
87
+ return reply.code(400).send({ error: err.message });
88
+ }
89
+ });
90
+ }
@@ -0,0 +1,27 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import { getOverviewConfig, setOverviewConfig } from "../overview.service.js";
3
+
4
+ function requireAdmin(req: any, reply: any): boolean {
5
+ const token = req.headers["x-admin-token"];
6
+ const expected = process.env.DASHWIRE_ADMIN_TOKEN;
7
+ if (!expected || token !== expected) {
8
+ reply.status(401).send({ error: "unauthorized" });
9
+ return false;
10
+ }
11
+ return true;
12
+ }
13
+
14
+ export function overviewRoutes(app: FastifyInstance) {
15
+ app.get("/api/overview-config", async () => {
16
+ return getOverviewConfig();
17
+ });
18
+
19
+ app.put<{ Body: { entries: { projectId: string; capabilityPath: string; order: number }[] } }>(
20
+ "/api/overview-config",
21
+ async (req, reply) => {
22
+ if (!requireAdmin(req, reply)) return;
23
+ setOverviewConfig(req.body.entries);
24
+ return reply.send({ ok: true });
25
+ },
26
+ );
27
+ }
@@ -0,0 +1,93 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import type { Server as SocketServer } from "socket.io";
3
+ import { registerProject, listProjects, getProject, listAllTokens, createTokenForProject, setTokenActiveStatus, deleteToken } from "../projects.service.js";
4
+ import type { DashboardStructure } from "@dashwire/core";
5
+ import { logs } from "../db/schema.js";
6
+ import { desc } from "drizzle-orm/sql/expressions/select";
7
+ import { eq } from "drizzle-orm/sql/expressions/conditions";
8
+ import { db } from "../db/client.js";
9
+
10
+ export function projectRoutes(app: FastifyInstance, io: SocketServer) {
11
+ app.post<{ Params: { id: string }; Body: { projectName: string; structure: DashboardStructure } }>(
12
+ "/api/projects/:id/register",
13
+ async (req, reply) => {
14
+ const { id: projectId } = req.params;
15
+ const authHeader = req.headers.authorization;
16
+ const authToken = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : undefined;
17
+ const { projectName, structure } = req.body;
18
+
19
+ try {
20
+ const result = registerProject(projectId, projectName, structure, authToken);
21
+ return reply.send(result);
22
+ } catch (err: any) {
23
+ return reply.code(401).send({ error: err.message });
24
+ }
25
+ }
26
+ );
27
+
28
+ app.get("/api/projects", async (_req, reply) => {
29
+ return reply.send(listProjects());
30
+ });
31
+
32
+ app.get("/api/projects/tokens/all", async (_req, reply) => {
33
+ return reply.send(listAllTokens());
34
+ });
35
+
36
+ app.post<{ Body: { label?: string } }>(
37
+ "/api/projects/tokens/new",
38
+ async (req, reply) => {
39
+ const body = req.body as { label?: string };
40
+ try {
41
+ const token = createTokenForProject(body?.label);
42
+ return reply.send({ token });
43
+ } catch (err: any) {
44
+ return reply.code(400).send({ error: err.message });
45
+ }
46
+ }
47
+ );
48
+
49
+ app.patch<{ Params: { tokenId: string }; Body: { active: boolean } }>(
50
+ "/api/projects/tokens/:tokenId",
51
+ async (req, reply) => {
52
+ const { tokenId } = req.params;
53
+ const { active } = req.body;
54
+ setTokenActiveStatus(tokenId, active);
55
+ return reply.send({ success: true });
56
+ }
57
+ );
58
+
59
+ app.delete<{ Params: { tokenId: string } }>(
60
+ "/api/projects/tokens/:tokenId",
61
+ async (req, reply) => {
62
+ const { tokenId } = req.params;
63
+ deleteToken(tokenId);
64
+ return reply.send({ success: true });
65
+ }
66
+ );
67
+
68
+ app.get<{ Params: { id: string } }>("/api/projects/:id", async (req, reply) => {
69
+ const { id: projectId } = req.params;
70
+ const project = getProject(projectId);
71
+ if (!project) {
72
+ return reply.code(404).send({ error: "Project niet gevonden" });
73
+ }
74
+ return reply.send(project);
75
+ });
76
+
77
+ app.get<{ Params: { id: string }; Querystring: { limit?: string } }>(
78
+ "/api/projects/:id/logs",
79
+ async (req, reply) => {
80
+ const { id: projectId } = req.params;
81
+ const limit = Number(req.query.limit ?? 50);
82
+
83
+ const projectLogs = db.select()
84
+ .from(logs)
85
+ .where(eq(logs.projectId, projectId))
86
+ .orderBy(desc(logs.id))
87
+ .limit(limit)
88
+ .all();
89
+
90
+ return reply.send(projectLogs.reverse());
91
+ }
92
+ );
93
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ "module": "NodeNext",
7
+ "moduleResolution": "NodeNext",
8
+ "types": ["node"]
9
+ },
10
+ "include": ["src"]
11
+ }