scdb-api 1.0.2

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 (2) hide show
  1. package/index.js +593 -0
  2. package/package.json +14 -0
package/index.js ADDED
@@ -0,0 +1,593 @@
1
+ import express from 'express';
2
+ import session from 'express-session';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import { fileURLToPath } from 'url';
6
+ import { openDatabase, authenticateUser, authenticateApiKey, canRunSql, auditLog, listTables, tableColumns, hashPassword, verifyPassword, generateApiKey, hashApiKey } from 'scdb-core';
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+
10
+ /**
11
+ * Create the Secure DB Express app (API + web panel static). Does not call listen().
12
+ * @param {{
13
+ * databasePath?: string;
14
+ * sessionSecret?: string;
15
+ * staticDir?: string;
16
+ * docsDir?: string | null;
17
+ * }} options - Override env/defaults. staticDir = web panel assets; docsDir = dir with USER_GUIDE.md, ADMIN_GUIDE.md (optional).
18
+ * @returns {express.Express}
19
+ */
20
+ export function createApp(options = {}) {
21
+ const databasePath = options.databasePath ?? process.env.DATABASE_PATH ?? './data/secure-db.sqlite';
22
+ const sessionSecret = options.sessionSecret ?? process.env.SESSION_SECRET ?? 'change-me';
23
+ const staticDir = options.staticDir ?? path.join(__dirname, '../web-panel/public');
24
+ const docsDir = options.docsDir !== undefined ? options.docsDir : null;
25
+
26
+ const app = express();
27
+ app.use(express.json({ limit: '1mb' }));
28
+ app.set('trust proxy', 1);
29
+ app.use(
30
+ session({
31
+ secret: sessionSecret,
32
+ resave: false,
33
+ saveUninitialized: false,
34
+ cookie: { httpOnly: true, secure: process.env.NODE_ENV === 'production', maxAge: 24 * 60 * 60 * 1000 },
35
+ })
36
+ );
37
+
38
+ let db = null;
39
+ async function ensureDb() {
40
+ if (!db) db = await openDatabase(databasePath);
41
+ return db;
42
+ }
43
+
44
+ function getAuth(req) {
45
+ const authHeader = req.headers.authorization;
46
+ if (authHeader?.startsWith('Bearer ')) {
47
+ return { type: 'api_key', key: authHeader.slice(7) };
48
+ }
49
+ if (req.session?.userId != null) {
50
+ return { type: 'session', userId: req.session.userId, username: req.session.username, role: req.session.role };
51
+ }
52
+ return null;
53
+ }
54
+
55
+ async function authMiddleware(req, res, next) {
56
+ const d = await ensureDb();
57
+ const auth = getAuth(req);
58
+ if (!auth) {
59
+ return res.status(401).json({ error: 'Unauthorized' });
60
+ }
61
+ if (auth.type === 'api_key') {
62
+ const keyAuth = authenticateApiKey(d, auth.key, PORT);
63
+ if (!keyAuth) {
64
+ return res.status(401).json({ error: 'Invalid API key' });
65
+ }
66
+ req.auth = {
67
+ principalType: 'api_key',
68
+ principalId: keyAuth.id,
69
+ role: keyAuth.role,
70
+ name: keyAuth.name,
71
+ };
72
+ return next();
73
+ }
74
+ req.auth = {
75
+ principalType: 'user',
76
+ principalId: req.session.userId,
77
+ role: req.session.role,
78
+ name: req.session.username,
79
+ };
80
+ next();
81
+ }
82
+
83
+ function normalizeParams(bodyParams) {
84
+ if (Array.isArray(bodyParams)) return bodyParams;
85
+ if (bodyParams && typeof bodyParams === 'object' && !Array.isArray(bodyParams)) {
86
+ return Object.keys(bodyParams)
87
+ .sort()
88
+ .map((k) => bodyParams[k]);
89
+ }
90
+ return [];
91
+ }
92
+
93
+ app.post('/login', express.json(), async (req, res) => {
94
+ const d = await ensureDb();
95
+ const { username, password } = req.body || {};
96
+ if (!username || !password) {
97
+ return res.status(400).json({ error: 'Username and password required' });
98
+ }
99
+ const user = await authenticateUser(d, username, password);
100
+ if (!user) {
101
+ return res.status(401).json({ error: 'Invalid credentials' });
102
+ }
103
+ req.session.userId = user.id;
104
+ req.session.username = user.username;
105
+ req.session.role = user.role;
106
+ res.json({ username: user.username, role: user.role });
107
+ });
108
+
109
+ app.post('/logout', (req, res) => {
110
+ req.session.destroy(() => {
111
+ res.json({ ok: true });
112
+ });
113
+ });
114
+ app.get('/is-setup', async (req, res) => {
115
+ const d = await ensureDb();
116
+ const existing = d.query('SELECT 1 FROM users WHERE role = ? LIMIT 1', ['admin']);
117
+ res.json({ isSetup: existing.length > 0 });
118
+ });
119
+ app.post('/setup', async (req, res) => {
120
+ const d = await ensureDb();
121
+ const existing = d.query('SELECT 1 FROM users WHERE role = ? LIMIT 1', ['admin']);
122
+ if (existing.length > 0) {
123
+ return res.status(400).json({ error: 'Setup already completed' });
124
+ }
125
+ const { username, password } = req.body || {};
126
+ if (!username || !password || username.length < 2) {
127
+ return res.status(400).json({ error: 'Username (min 2 chars) and password required' });
128
+ }
129
+ const hash = await hashPassword(password);
130
+ d.execute('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)', [username, hash, 'admin']);
131
+ d.save();
132
+ res.status(201).json({ message: 'Admin user created' });
133
+ });
134
+
135
+ app.get('/me', authMiddleware, (req, res) => {
136
+ res.json({ principalType: req.auth.principalType, role: req.auth.role, name: req.auth.name });
137
+ });
138
+
139
+ app.get('/health', async (req, res) => {
140
+ try {
141
+ await ensureDb();
142
+ res.json({ ok: true });
143
+ } catch (e) {
144
+ res.status(500).json({ ok: false, error: String(e.message) });
145
+ }
146
+ });
147
+
148
+ app.post('/query', authMiddleware, async (req, res) => {
149
+ const d = await ensureDb();
150
+ const { sql, params: bodyParams } = req.body || {};
151
+ const params = normalizeParams(bodyParams);
152
+ if (typeof sql !== 'string' || !sql.trim()) {
153
+ return res.status(400).json({ error: 'Missing or invalid sql' });
154
+ }
155
+ const trimmed = sql.trim().toUpperCase();
156
+ if (!canRunSql(req.auth.role, sql)) {
157
+ auditLog(d, {
158
+ principalType: req.auth.principalType,
159
+ principalId: req.auth.principalId,
160
+ role: req.auth.role,
161
+ action: 'query_denied',
162
+ queryPreview: sql.slice(0, 200),
163
+ success: false,
164
+ });
165
+ d.save();
166
+ return res.status(403).json({ error: 'Not allowed to run this query (read-only role)' });
167
+ }
168
+ if (/^(INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|REPLACE)\s/.test(trimmed)) {
169
+ return res.status(400).json({ error: 'Use POST /execute for write queries' });
170
+ }
171
+ try {
172
+ const rows = d.query(sql, params);
173
+ auditLog(d, {
174
+ principalType: req.auth.principalType,
175
+ principalId: req.auth.principalId,
176
+ role: req.auth.role,
177
+ action: 'query',
178
+ queryPreview: sql.slice(0, 200),
179
+ success: true,
180
+ });
181
+ d.save();
182
+ res.json({ rows });
183
+ } catch (err) {
184
+ auditLog(d, {
185
+ principalType: req.auth.principalType,
186
+ principalId: req.auth.principalId,
187
+ role: req.auth.role,
188
+ action: 'query',
189
+ queryPreview: sql.slice(0, 200),
190
+ success: false,
191
+ });
192
+ d.save();
193
+ res.status(400).json({ error: err.message, code: 'SQL_ERROR' });
194
+ }
195
+ });
196
+
197
+ app.post('/execute', authMiddleware, async (req, res) => {
198
+ const d = await ensureDb();
199
+ const { sql, params: bodyParams } = req.body || {};
200
+ const params = normalizeParams(bodyParams);
201
+ if (typeof sql !== 'string' || !sql.trim()) {
202
+ return res.status(400).json({ error: 'Missing or invalid sql' });
203
+ }
204
+ if (!canRunSql(req.auth.role, sql)) {
205
+ auditLog(d, {
206
+ principalType: req.auth.principalType,
207
+ principalId: req.auth.principalId,
208
+ role: req.auth.role,
209
+ action: 'execute_denied',
210
+ queryPreview: sql.slice(0, 200),
211
+ success: false,
212
+ });
213
+ d.save();
214
+ return res.status(403).json({ error: 'Not allowed to run this query' });
215
+ }
216
+ try {
217
+ const result = d.execute(sql, params);
218
+ auditLog(d, {
219
+ principalType: req.auth.principalType,
220
+ principalId: req.auth.principalId,
221
+ role: req.auth.role,
222
+ action: 'execute',
223
+ queryPreview: sql.slice(0, 200),
224
+ success: true,
225
+ });
226
+ d.save();
227
+ res.json(result);
228
+ } catch (err) {
229
+ auditLog(d, {
230
+ principalType: req.auth.principalType,
231
+ principalId: req.auth.principalId,
232
+ role: req.auth.role,
233
+ action: 'execute',
234
+ queryPreview: sql.slice(0, 200),
235
+ success: false,
236
+ });
237
+ d.save();
238
+ res.status(400).json({ error: err.message, code: 'SQL_ERROR' });
239
+ }
240
+ });
241
+
242
+ app.get('/tables', authMiddleware, async (req, res) => {
243
+ const d = await ensureDb();
244
+ try {
245
+ const tables = listTables(d);
246
+ res.json({ tables: tables.map((t) => t.name) });
247
+ } catch (err) {
248
+ res.status(500).json({ error: err.message });
249
+ }
250
+ });
251
+
252
+ app.get('/tables/:name', authMiddleware, async (req, res) => {
253
+ const d = await ensureDb();
254
+ try {
255
+ const columns = tableColumns(d, req.params.name);
256
+ res.json({ columns });
257
+ } catch (err) {
258
+ res.status(400).json({ error: err.message });
259
+ }
260
+ });
261
+
262
+ app.get('/audit', authMiddleware, async (req, res) => {
263
+ if (req.auth.role !== 'admin') {
264
+ return res.status(403).json({ error: 'Admin only' });
265
+ }
266
+ const d = await ensureDb();
267
+ const limit = Math.min(Number(req.query.limit) || 100, 500);
268
+ const rows = d.query(
269
+ 'SELECT id, principal_type, principal_id, role, action, query_preview, success, created_at FROM audit_log ORDER BY id DESC LIMIT ?',
270
+ [limit]
271
+ );
272
+ res.json({ rows });
273
+ });
274
+
275
+ app.get('/api-keys', authMiddleware, async (req, res) => {
276
+ if (req.auth.principalType !== 'user') {
277
+ return res.status(400).json({ error: 'API keys list is only available for logged-in users' });
278
+ }
279
+ const d = await ensureDb();
280
+ let rows;
281
+ if (req.auth.role === 'admin') {
282
+ rows = d.query('SELECT id, name, role, created_at, owner_user_id FROM api_keys ORDER BY id', []);
283
+ } else {
284
+ rows = d.query(
285
+ 'SELECT id, name, role, created_at, owner_user_id FROM api_keys WHERE owner_user_id = ? ORDER BY id',
286
+ [req.auth.principalId]
287
+ );
288
+ }
289
+ res.json({ keys: rows });
290
+ });
291
+
292
+ app.post('/api-keys', authMiddleware, async (req, res) => {
293
+ if (req.auth.principalType !== 'user') {
294
+ return res.status(400).json({ error: 'Create API key is only available for logged-in users' });
295
+ }
296
+ const d = await ensureDb();
297
+ const { name, role } = req.body || {};
298
+ const r = role || 'readwrite';
299
+ if (!['admin', 'readwrite', 'readonly'].includes(r)) {
300
+ return res.status(400).json({ error: 'Invalid role' });
301
+ }
302
+ if (req.auth.role !== 'admin' && r === 'admin') {
303
+ return res.status(403).json({ error: 'Only admins can create admin-role API keys' });
304
+ }
305
+ const plainKey = generateApiKey(process.env.PORT);
306
+ const keyHash = hashApiKey(plainKey);
307
+ const ownerId = req.auth.principalId;
308
+ d.execute(
309
+ 'INSERT INTO api_keys (key_hash, name, role, owner_user_id) VALUES (?, ?, ?, ?)',
310
+ [keyHash, name || null, r, ownerId]
311
+ );
312
+ d.save();
313
+ res.status(201).json({ key: plainKey, name: name || null, role: r });
314
+ });
315
+
316
+ app.delete('/api-keys/:id', authMiddleware, async (req, res) => {
317
+ if (req.auth.principalType !== 'user') {
318
+ return res.status(400).json({ error: 'Delete API key is only available for logged-in users' });
319
+ }
320
+ const id = Number(req.params.id);
321
+ if (!Number.isInteger(id) || id <= 0) {
322
+ return res.status(400).json({ error: 'Invalid key id' });
323
+ }
324
+ const d = await ensureDb();
325
+ const rows = d.query('SELECT id, owner_user_id FROM api_keys WHERE id = ?', [id]);
326
+ if (rows.length === 0) {
327
+ return res.status(404).json({ error: 'API key not found' });
328
+ }
329
+ const key = rows[0];
330
+ const isAdmin = req.auth.role === 'admin';
331
+ const isOwner = key.owner_user_id != null && Number(key.owner_user_id) === req.auth.principalId;
332
+ if (!isAdmin && !isOwner) {
333
+ return res.status(403).json({ error: 'You can only delete your own API keys' });
334
+ }
335
+ d.execute('DELETE FROM api_keys WHERE id = ?', [id]);
336
+ d.save();
337
+ res.json({ ok: true });
338
+ });
339
+
340
+ // Admin-only user management
341
+ app.get('/users', authMiddleware, async (req, res) => {
342
+ if (req.auth.role !== 'admin') {
343
+ return res.status(403).json({ error: 'Admin only' });
344
+ }
345
+ const d = await ensureDb();
346
+ try {
347
+ const rows = d.query('SELECT id, username, role, created_at FROM users ORDER BY id', []);
348
+ res.json({ users: rows });
349
+ } catch (err) {
350
+ if (String(err.message || err).includes('no such column')) {
351
+ const rows = d.query('SELECT id, username, role FROM users ORDER BY id', []);
352
+ res.json({ users: rows });
353
+ } else {
354
+ res.status(500).json({ error: 'Failed to list users' });
355
+ }
356
+ }
357
+ });
358
+
359
+ app.post('/users', authMiddleware, async (req, res) => {
360
+ if (req.auth.role !== 'admin') {
361
+ return res.status(403).json({ error: 'Admin only' });
362
+ }
363
+ const d = await ensureDb();
364
+ const { username, password, role } = req.body || {};
365
+ const r = role || 'readwrite';
366
+ if (!username || typeof username !== 'string' || username.trim().length < 2) {
367
+ return res.status(400).json({ error: 'Username (min 2 chars) is required' });
368
+ }
369
+ if (!password || typeof password !== 'string' || password.length < 6) {
370
+ return res.status(400).json({ error: 'Password (min 6 chars) is required' });
371
+ }
372
+ if (!['admin', 'readwrite', 'readonly'].includes(r)) {
373
+ return res.status(400).json({ error: 'Invalid role' });
374
+ }
375
+ const existing = d.query('SELECT id FROM users WHERE username = ?', [username.trim()]);
376
+ if (existing.length > 0) {
377
+ return res.status(409).json({ error: 'Username already exists' });
378
+ }
379
+ const hash = await hashPassword(password);
380
+ const result = d.execute('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)', [
381
+ username.trim(),
382
+ hash,
383
+ r,
384
+ ]);
385
+ auditLog(d, {
386
+ principalType: req.auth.principalType,
387
+ principalId: req.auth.principalId,
388
+ role: req.auth.role,
389
+ action: 'user_create',
390
+ queryPreview: `user:${username.trim()}, role:${r}`,
391
+ success: true,
392
+ });
393
+ d.save();
394
+ res.status(201).json({ id: result.lastInsertRowid, username: username.trim(), role: r });
395
+ });
396
+
397
+ app.patch('/users/:id', authMiddleware, async (req, res) => {
398
+ if (req.auth.role !== 'admin') {
399
+ return res.status(403).json({ error: 'Admin only' });
400
+ }
401
+ const id = Number(req.params.id);
402
+ if (!Number.isInteger(id) || id <= 0) {
403
+ return res.status(400).json({ error: 'Invalid user id' });
404
+ }
405
+ const { role, password } = req.body || {};
406
+ if (role == null && password == null) {
407
+ return res.status(400).json({ error: 'Nothing to update' });
408
+ }
409
+ const d = await ensureDb();
410
+ const rows = d.query('SELECT id, username, role FROM users WHERE id = ?', [id]);
411
+ if (rows.length === 0) {
412
+ return res.status(404).json({ error: 'User not found' });
413
+ }
414
+ const user = rows[0];
415
+
416
+ let newRole = user.role;
417
+ if (role != null) {
418
+ if (!['admin', 'readwrite', 'readonly'].includes(role)) {
419
+ return res.status(400).json({ error: 'Invalid role' });
420
+ }
421
+ if (user.role === 'admin' && role !== 'admin') {
422
+ const admins = d.query("SELECT COUNT(*) AS c FROM users WHERE role = 'admin'", []);
423
+ const count = Number(admins[0]?.c ?? admins[0]?.C ?? 0);
424
+ if (count <= 1) {
425
+ return res.status(400).json({ error: 'Cannot change role of last admin' });
426
+ }
427
+ }
428
+ newRole = role;
429
+ }
430
+
431
+ let passwordHash = null;
432
+ if (password != null) {
433
+ if (typeof password !== 'string' || password.length < 6) {
434
+ return res.status(400).json({ error: 'Password (min 6 chars) is required' });
435
+ }
436
+ passwordHash = await hashPassword(password);
437
+ }
438
+
439
+ if (passwordHash != null && newRole !== user.role) {
440
+ d.execute('UPDATE users SET password_hash = ?, role = ? WHERE id = ?', [passwordHash, newRole, id]);
441
+ } else if (passwordHash != null) {
442
+ d.execute('UPDATE users SET password_hash = ? WHERE id = ?', [passwordHash, id]);
443
+ } else if (newRole !== user.role) {
444
+ d.execute('UPDATE users SET role = ? WHERE id = ?', [newRole, id]);
445
+ }
446
+
447
+ auditLog(d, {
448
+ principalType: req.auth.principalType,
449
+ principalId: req.auth.principalId,
450
+ role: req.auth.role,
451
+ action: 'user_update',
452
+ queryPreview: `userId:${id}, role:${newRole}, password_changed:${password != null}`,
453
+ success: true,
454
+ });
455
+ d.save();
456
+ res.json({ id, username: user.username, role: newRole });
457
+ });
458
+
459
+ app.delete('/users/:id', authMiddleware, async (req, res) => {
460
+ if (req.auth.role !== 'admin') {
461
+ return res.status(403).json({ error: 'Admin only' });
462
+ }
463
+ const id = Number(req.params.id);
464
+ if (!Number.isInteger(id) || id <= 0) {
465
+ return res.status(400).json({ error: 'Invalid user id' });
466
+ }
467
+ const d = await ensureDb();
468
+ const rows = d.query('SELECT id, username, role FROM users WHERE id = ?', [id]);
469
+ if (rows.length === 0) {
470
+ return res.status(404).json({ error: 'User not found' });
471
+ }
472
+ const user = rows[0];
473
+ if (user.role === 'admin') {
474
+ const admins = d.query("SELECT COUNT(*) AS c FROM users WHERE role = 'admin'", []);
475
+ const count = Number(admins[0]?.c ?? admins[0]?.C ?? 0);
476
+ if (count <= 1) {
477
+ return res.status(400).json({ error: 'Cannot delete last admin' });
478
+ }
479
+ }
480
+ d.execute('DELETE FROM users WHERE id = ?', [id]);
481
+ auditLog(d, {
482
+ principalType: req.auth.principalType,
483
+ principalId: req.auth.principalId,
484
+ role: req.auth.role,
485
+ action: 'user_delete',
486
+ queryPreview: `userId:${id}`,
487
+ success: true,
488
+ });
489
+ d.save();
490
+ res.json({ ok: true });
491
+ });
492
+
493
+ // Profile + docs endpoints
494
+ app.get('/profile', authMiddleware, async (req, res) => {
495
+ if (req.auth.principalType !== 'user') {
496
+ return res.status(400).json({ error: 'Profile is only available for logged-in users' });
497
+ }
498
+ const d = await ensureDb();
499
+ const rows = d.query('SELECT id, username, role, created_at FROM users WHERE id = ?', [req.auth.principalId]);
500
+ if (rows.length === 0) {
501
+ return res.status(404).json({ error: 'User not found' });
502
+ }
503
+ const user = rows[0];
504
+ res.json({ id: user.id, username: user.username, role: user.role, created_at: user.created_at });
505
+ });
506
+
507
+ app.post('/profile/password', authMiddleware, async (req, res) => {
508
+ if (req.auth.principalType !== 'user') {
509
+ return res.status(400).json({ error: 'Password change is only available for logged-in users' });
510
+ }
511
+ const { oldPassword, newPassword } = req.body || {};
512
+ if (!oldPassword || !newPassword) {
513
+ return res.status(400).json({ error: 'oldPassword and newPassword are required' });
514
+ }
515
+ if (typeof newPassword !== 'string' || newPassword.length < 6) {
516
+ return res.status(400).json({ error: 'New password must be at least 6 characters' });
517
+ }
518
+ const d = await ensureDb();
519
+ const rows = d.query('SELECT id, username, role, password_hash FROM users WHERE id = ?', [req.auth.principalId]);
520
+ if (rows.length === 0) {
521
+ return res.status(404).json({ error: 'User not found' });
522
+ }
523
+ const user = rows[0];
524
+ const ok = await verifyPassword(oldPassword, user.password_hash);
525
+ if (!ok) {
526
+ auditLog(d, {
527
+ principalType: 'user',
528
+ principalId: req.auth.principalId,
529
+ role: user.role,
530
+ action: 'profile_password_change',
531
+ success: false,
532
+ });
533
+ d.save();
534
+ return res.status(400).json({ error: 'Old password is incorrect' });
535
+ }
536
+ const hash = await hashPassword(newPassword);
537
+ d.execute('UPDATE users SET password_hash = ? WHERE id = ?', [hash, user.id]);
538
+ auditLog(d, {
539
+ principalType: 'user',
540
+ principalId: req.auth.principalId,
541
+ role: user.role,
542
+ action: 'profile_password_change',
543
+ success: true,
544
+ });
545
+ d.save();
546
+ res.json({ ok: true });
547
+ });
548
+
549
+ function readDocFile(name) {
550
+ const baseDir = docsDir ?? path.join(__dirname, '../../docs');
551
+ const docPath = path.join(baseDir, name);
552
+ try {
553
+ return fs.readFileSync(docPath, 'utf8');
554
+ } catch (err) {
555
+ return null;
556
+ }
557
+ }
558
+
559
+ app.get('/docs/user', authMiddleware, (req, res) => {
560
+ const content = readDocFile('USER_GUIDE.md');
561
+ if (!content) {
562
+ return res.status(404).json({ error: 'Documentation not available' });
563
+ }
564
+ res.type('text/plain').send(content);
565
+ });
566
+
567
+ app.get('/docs/admin', authMiddleware, (req, res) => {
568
+ if (req.auth.role !== 'admin') {
569
+ return res.status(403).json({ error: 'Admin only' });
570
+ }
571
+ const content = readDocFile('ADMIN_GUIDE.md');
572
+ if (!content) {
573
+ return res.status(404).json({ error: 'Documentation not available' });
574
+ }
575
+ res.type('text/plain').send(content);
576
+ });
577
+
578
+ app.use(express.static(staticDir));
579
+
580
+ return app;
581
+ }
582
+
583
+ // Run as standalone server when this file is executed directly
584
+ const isMain =
585
+ process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
586
+ if (isMain) {
587
+ const app = createApp({});
588
+ const PORT = Number(process.env.PORT) || 4406;
589
+ const HOST = process.env.HOST || '0.0.0.0';
590
+ app.listen(PORT, HOST, () => {
591
+ console.log(`API + Panel listening on http://${HOST}:${PORT}`);
592
+ });
593
+ }
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "scdb-api",
3
+ "version": "1.0.2",
4
+ "main": "index.js",
5
+ "type": "module",
6
+ "scripts": {
7
+ "start": "node index.js"
8
+ },
9
+ "dependencies": {
10
+ "express": "^4.21.0",
11
+ "express-session": "^1.18.0",
12
+ "scdb-core": "1.0.0"
13
+ }
14
+ }