greend-server 1.0.3 → 1.0.4
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/.env.example +10 -4
- package/package.json +1 -1
- package/server.js +73 -16
package/.env.example
CHANGED
|
@@ -5,10 +5,16 @@
|
|
|
5
5
|
PORT=3001
|
|
6
6
|
HOST=0.0.0.0
|
|
7
7
|
|
|
8
|
-
# Directory where all ESG data, user accounts,
|
|
9
|
-
# Use an absolute path outside the
|
|
10
|
-
#
|
|
11
|
-
|
|
8
|
+
# Directory where all ESG data, user accounts, audit logs, and sessions are stored.
|
|
9
|
+
# Use an absolute path outside the app/package directory so data survives upgrades.
|
|
10
|
+
# If omitted, GreenD defaults to an OS-managed shared data location:
|
|
11
|
+
# Windows: %PROGRAMDATA%\GreenD\data
|
|
12
|
+
# macOS: ~/Library/Application Support/GreenD/data
|
|
13
|
+
# Linux: ~/.local/share/GreenD/data
|
|
14
|
+
# Example:
|
|
15
|
+
# DATA_DIR=C:\ProgramData\GreenD\data
|
|
16
|
+
# DATA_DIR=/opt/greend/data
|
|
17
|
+
# DATA_DIR=
|
|
12
18
|
|
|
13
19
|
# REQUIRED in production: set this to a long random string (32+ chars).
|
|
14
20
|
# Never use the default in production.
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -7,6 +7,7 @@ import { readFile, writeFile, mkdir, readdir } from 'fs/promises';
|
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
import { dirname, join } from 'path';
|
|
9
9
|
import path from 'path';
|
|
10
|
+
import os from 'os';
|
|
10
11
|
import { createRequire } from 'module';
|
|
11
12
|
|
|
12
13
|
const require = createRequire(import.meta.url);
|
|
@@ -29,9 +30,37 @@ if (
|
|
|
29
30
|
process.exit(1);
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
function getDefaultDataDir() {
|
|
34
|
+
if (process.platform === 'win32') {
|
|
35
|
+
const programData = process.env.PROGRAMDATA || path.join(process.env.SystemDrive || 'C:', 'ProgramData');
|
|
36
|
+
return path.join(programData, 'GreenD', 'data');
|
|
37
|
+
}
|
|
38
|
+
if (process.platform === 'darwin') {
|
|
39
|
+
return path.join(os.homedir(), 'Library', 'Application Support', 'GreenD', 'data');
|
|
40
|
+
}
|
|
41
|
+
const xdgDataHome = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
|
|
42
|
+
return path.join(xdgDataHome, 'GreenD', 'data');
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
const DATA_DIR = process.env.DATA_DIR
|
|
33
46
|
? path.resolve(process.env.DATA_DIR)
|
|
34
|
-
:
|
|
47
|
+
: getDefaultDataDir();
|
|
48
|
+
|
|
49
|
+
const SESSION_COOKIE_NAME = 'connect.sid';
|
|
50
|
+
const SESSION_COOKIE_OPTIONS = {
|
|
51
|
+
httpOnly: true,
|
|
52
|
+
secure: process.env.NODE_ENV === 'production',
|
|
53
|
+
sameSite: 'lax',
|
|
54
|
+
path: '/',
|
|
55
|
+
maxAge: 8 * 60 * 60 * 1000,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const CLEAR_SESSION_COOKIE_OPTIONS = {
|
|
59
|
+
httpOnly: SESSION_COOKIE_OPTIONS.httpOnly,
|
|
60
|
+
secure: SESSION_COOKIE_OPTIONS.secure,
|
|
61
|
+
sameSite: SESSION_COOKIE_OPTIONS.sameSite,
|
|
62
|
+
path: SESSION_COOKIE_OPTIONS.path,
|
|
63
|
+
};
|
|
35
64
|
|
|
36
65
|
// ---------------------------------------------------------------------------
|
|
37
66
|
// File-backed session store — persists sessions to DATA_DIR/sessions.json so
|
|
@@ -110,12 +139,7 @@ app.use(session({
|
|
|
110
139
|
secret: process.env.SESSION_SECRET || 'greend-dev-secret-change-in-production',
|
|
111
140
|
resave: false,
|
|
112
141
|
saveUninitialized: false,
|
|
113
|
-
cookie:
|
|
114
|
-
httpOnly: true,
|
|
115
|
-
secure: process.env.NODE_ENV === 'production',
|
|
116
|
-
sameSite: 'lax',
|
|
117
|
-
maxAge: 8 * 60 * 60 * 1000, // 8 hours
|
|
118
|
-
},
|
|
142
|
+
cookie: SESSION_COOKIE_OPTIONS,
|
|
119
143
|
}));
|
|
120
144
|
|
|
121
145
|
// Generic helpers for simple single-file read/write
|
|
@@ -260,13 +284,35 @@ app.post('/api/auth/login', async (req, res) => {
|
|
|
260
284
|
const valid = await bcrypt.compare(password, user.passwordHash);
|
|
261
285
|
if (!valid) return res.status(401).json({ error: 'Invalid username or password' });
|
|
262
286
|
|
|
263
|
-
req.session.userId = user.id;
|
|
264
287
|
const { passwordHash, ...safeUser } = user;
|
|
265
|
-
|
|
288
|
+
req.session.regenerate((err) => {
|
|
289
|
+
if (err) {
|
|
290
|
+
console.error('[auth] failed to regenerate session:', err.message);
|
|
291
|
+
return res.status(500).json({ error: 'Failed to start a new session' });
|
|
292
|
+
}
|
|
293
|
+
req.session.userId = user.id;
|
|
294
|
+
res.json(safeUser);
|
|
295
|
+
});
|
|
266
296
|
});
|
|
267
297
|
|
|
268
298
|
app.post('/api/auth/logout', (req, res) => {
|
|
269
|
-
|
|
299
|
+
const finalize = () => {
|
|
300
|
+
res.clearCookie(SESSION_COOKIE_NAME, CLEAR_SESSION_COOKIE_OPTIONS);
|
|
301
|
+
res.sendStatus(204);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
if (!req.session) {
|
|
305
|
+
finalize();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
req.session.destroy((err) => {
|
|
310
|
+
if (err) {
|
|
311
|
+
console.error('[auth] failed to destroy session:', err.message);
|
|
312
|
+
return res.status(500).json({ error: 'Failed to close the session' });
|
|
313
|
+
}
|
|
314
|
+
finalize();
|
|
315
|
+
});
|
|
270
316
|
});
|
|
271
317
|
|
|
272
318
|
app.put('/api/auth/password', requireAuth, async (req, res) => {
|
|
@@ -289,7 +335,11 @@ app.put('/api/auth/password', requireAuth, async (req, res) => {
|
|
|
289
335
|
app.get('/api/auth/me', requireAuth, async (req, res) => {
|
|
290
336
|
const users = await readUsers();
|
|
291
337
|
const user = users.find(u => u.id === req.session.userId);
|
|
292
|
-
if (!user)
|
|
338
|
+
if (!user) {
|
|
339
|
+
req.session.destroy(() => {});
|
|
340
|
+
res.clearCookie(SESSION_COOKIE_NAME, CLEAR_SESSION_COOKIE_OPTIONS);
|
|
341
|
+
return res.status(401).json({ error: 'Session invalid' });
|
|
342
|
+
}
|
|
293
343
|
const { passwordHash, ...safeUser } = user;
|
|
294
344
|
res.json(safeUser);
|
|
295
345
|
});
|
|
@@ -419,8 +469,15 @@ async function seedDefaultAdmin() {
|
|
|
419
469
|
const PORT = Number(process.env.PORT) || 3001;
|
|
420
470
|
const HOST = process.env.HOST || '0.0.0.0';
|
|
421
471
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
472
|
+
mkdir(DATA_DIR, { recursive: true })
|
|
473
|
+
.then(() => seedDefaultAdmin())
|
|
474
|
+
.then(() => {
|
|
475
|
+
console.log(`GreenD data directory: ${DATA_DIR}`);
|
|
476
|
+
app.listen(PORT, HOST, () =>
|
|
477
|
+
console.log(`GreenD server running on http://${HOST}:${PORT}`)
|
|
478
|
+
);
|
|
479
|
+
})
|
|
480
|
+
.catch((err) => {
|
|
481
|
+
console.error('Failed to initialize GreenD server:', err.message);
|
|
482
|
+
process.exit(1);
|
|
483
|
+
});
|