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.
Files changed (3) hide show
  1. package/.env.example +10 -4
  2. package/package.json +1 -1
  3. 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, 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "greend-server",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "GreenD ESG backend — self-hosted Express.js API server",
5
5
  "type": "module",
6
6
  "main": "server.js",
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
- : join(__dirname, 'data');
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
- res.json(safeUser);
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
- req.session.destroy(() => res.sendStatus(204));
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) return res.status(401).json({ error: 'Session invalid' });
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
- seedDefaultAdmin().then(() => {
423
- app.listen(PORT, HOST, () =>
424
- console.log(`GreenD server running on http://${HOST}:${PORT}`)
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
+ });