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 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, and audit logs are stored.
9
- # Use an absolute path outside the npm package so data survives upgrades.
10
- # Example: DATA_DIR=/opt/greend/data
11
- DATA_DIR=./data
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
- # Set to 'production' to enable secure cookie flags.
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",
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
- : join(__dirname, 'data');
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
- res.json(safeUser);
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
- req.session.destroy(() => res.sendStatus(204));
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) return res.status(401).json({ error: 'Session invalid' });
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
- seedDefaultAdmin().then(() => {
423
- app.listen(PORT, HOST, () =>
424
- console.log(`GreenD server running on http://${HOST}:${PORT}`)
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