greend-server 1.0.3 → 1.0.5
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 +25 -5
- package/package.json +1 -2
- package/server.js +86 -16
- package/data/.gitkeep +0 -0
package/.env.example
CHANGED
|
@@ -5,20 +5,40 @@
|
|
|
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.
|
|
15
21
|
SESSION_SECRET=change-this-to-a-long-random-string
|
|
16
22
|
|
|
23
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
# Session cookie security
|
|
25
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
# Controls whether session cookies require a secure (HTTPS) transport.
|
|
27
|
+
# false — required for local HTTP setups (Electron desktop, dev, LAN installs)
|
|
28
|
+
# true — required when greend-server is behind HTTPS
|
|
29
|
+
# auto — derives from NODE_ENV (production → true, anything else → false)
|
|
30
|
+
#
|
|
31
|
+
# Common mistake: leaving this unset while NODE_ENV=production on a plain HTTP
|
|
32
|
+
# server causes a login loop — login succeeds server-side but the Secure cookie
|
|
33
|
+
# is never accepted by the client.
|
|
34
|
+
SESSION_COOKIE_SECURE=false
|
|
35
|
+
|
|
17
36
|
# Origins allowed to make cross-origin requests (comma-separated).
|
|
18
37
|
# The GreenD desktop app connects from app://localhost.
|
|
19
38
|
# If you also access GreenD from a browser, add that origin too.
|
|
20
39
|
# Example: CORS_ORIGIN=app://localhost,https://greend.yourcompany.com
|
|
21
40
|
CORS_ORIGIN=app://localhost
|
|
22
41
|
|
|
23
|
-
#
|
|
42
|
+
# Node environment. Does NOT control cookie security when SESSION_COOKIE_SECURE
|
|
43
|
+
# is set explicitly. Still affects other Express/library defaults.
|
|
24
44
|
NODE_ENV=production
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "greend-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "GreenD ESG backend — self-hosted Express.js API server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
"files": [
|
|
34
34
|
"server.js",
|
|
35
35
|
"bin/",
|
|
36
|
-
"data/.gitkeep",
|
|
37
36
|
".env.example"
|
|
38
37
|
],
|
|
39
38
|
"publishConfig": { "access": "public" }
|
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,50 @@ 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
|
+
|
|
51
|
+
function resolveCookieSecure() {
|
|
52
|
+
const raw = (process.env.SESSION_COOKIE_SECURE ?? 'auto').toLowerCase().trim();
|
|
53
|
+
if (raw === 'true') return true;
|
|
54
|
+
if (raw === 'false') return false;
|
|
55
|
+
// 'auto' or unrecognised: derive from NODE_ENV for backward compatibility
|
|
56
|
+
return process.env.NODE_ENV === 'production';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const SESSION_COOKIE_OPTIONS = {
|
|
60
|
+
httpOnly: true,
|
|
61
|
+
secure: resolveCookieSecure(),
|
|
62
|
+
sameSite: 'lax',
|
|
63
|
+
path: '/',
|
|
64
|
+
maxAge: 8 * 60 * 60 * 1000,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const CLEAR_SESSION_COOKIE_OPTIONS = {
|
|
68
|
+
httpOnly: SESSION_COOKIE_OPTIONS.httpOnly,
|
|
69
|
+
secure: SESSION_COOKIE_OPTIONS.secure,
|
|
70
|
+
sameSite: SESSION_COOKIE_OPTIONS.sameSite,
|
|
71
|
+
path: SESSION_COOKIE_OPTIONS.path,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (!SESSION_COOKIE_OPTIONS.secure && process.env.NODE_ENV === 'production') {
|
|
75
|
+
console.warn('[greend-server] WARNING: Session cookies are insecure (SESSION_COOKIE_SECURE=false) while NODE_ENV=production. Acceptable only for local HTTP deployments.');
|
|
76
|
+
}
|
|
35
77
|
|
|
36
78
|
// ---------------------------------------------------------------------------
|
|
37
79
|
// File-backed session store — persists sessions to DATA_DIR/sessions.json so
|
|
@@ -110,12 +152,7 @@ app.use(session({
|
|
|
110
152
|
secret: process.env.SESSION_SECRET || 'greend-dev-secret-change-in-production',
|
|
111
153
|
resave: false,
|
|
112
154
|
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
|
-
},
|
|
155
|
+
cookie: SESSION_COOKIE_OPTIONS,
|
|
119
156
|
}));
|
|
120
157
|
|
|
121
158
|
// Generic helpers for simple single-file read/write
|
|
@@ -260,13 +297,35 @@ app.post('/api/auth/login', async (req, res) => {
|
|
|
260
297
|
const valid = await bcrypt.compare(password, user.passwordHash);
|
|
261
298
|
if (!valid) return res.status(401).json({ error: 'Invalid username or password' });
|
|
262
299
|
|
|
263
|
-
req.session.userId = user.id;
|
|
264
300
|
const { passwordHash, ...safeUser } = user;
|
|
265
|
-
|
|
301
|
+
req.session.regenerate((err) => {
|
|
302
|
+
if (err) {
|
|
303
|
+
console.error('[auth] failed to regenerate session:', err.message);
|
|
304
|
+
return res.status(500).json({ error: 'Failed to start a new session' });
|
|
305
|
+
}
|
|
306
|
+
req.session.userId = user.id;
|
|
307
|
+
res.json(safeUser);
|
|
308
|
+
});
|
|
266
309
|
});
|
|
267
310
|
|
|
268
311
|
app.post('/api/auth/logout', (req, res) => {
|
|
269
|
-
|
|
312
|
+
const finalize = () => {
|
|
313
|
+
res.clearCookie(SESSION_COOKIE_NAME, CLEAR_SESSION_COOKIE_OPTIONS);
|
|
314
|
+
res.sendStatus(204);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
if (!req.session) {
|
|
318
|
+
finalize();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
req.session.destroy((err) => {
|
|
323
|
+
if (err) {
|
|
324
|
+
console.error('[auth] failed to destroy session:', err.message);
|
|
325
|
+
return res.status(500).json({ error: 'Failed to close the session' });
|
|
326
|
+
}
|
|
327
|
+
finalize();
|
|
328
|
+
});
|
|
270
329
|
});
|
|
271
330
|
|
|
272
331
|
app.put('/api/auth/password', requireAuth, async (req, res) => {
|
|
@@ -289,7 +348,11 @@ app.put('/api/auth/password', requireAuth, async (req, res) => {
|
|
|
289
348
|
app.get('/api/auth/me', requireAuth, async (req, res) => {
|
|
290
349
|
const users = await readUsers();
|
|
291
350
|
const user = users.find(u => u.id === req.session.userId);
|
|
292
|
-
if (!user)
|
|
351
|
+
if (!user) {
|
|
352
|
+
req.session.destroy(() => {});
|
|
353
|
+
res.clearCookie(SESSION_COOKIE_NAME, CLEAR_SESSION_COOKIE_OPTIONS);
|
|
354
|
+
return res.status(401).json({ error: 'Session invalid' });
|
|
355
|
+
}
|
|
293
356
|
const { passwordHash, ...safeUser } = user;
|
|
294
357
|
res.json(safeUser);
|
|
295
358
|
});
|
|
@@ -419,8 +482,15 @@ async function seedDefaultAdmin() {
|
|
|
419
482
|
const PORT = Number(process.env.PORT) || 3001;
|
|
420
483
|
const HOST = process.env.HOST || '0.0.0.0';
|
|
421
484
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
485
|
+
mkdir(DATA_DIR, { recursive: true })
|
|
486
|
+
.then(() => seedDefaultAdmin())
|
|
487
|
+
.then(() => {
|
|
488
|
+
console.log(`GreenD data directory: ${DATA_DIR}`);
|
|
489
|
+
app.listen(PORT, HOST, () =>
|
|
490
|
+
console.log(`GreenD server running on http://${HOST}:${PORT}`)
|
|
491
|
+
);
|
|
492
|
+
})
|
|
493
|
+
.catch((err) => {
|
|
494
|
+
console.error('Failed to initialize GreenD server:', err.message);
|
|
495
|
+
process.exit(1);
|
|
496
|
+
});
|
package/data/.gitkeep
DELETED
|
File without changes
|