Generate a session secret instead of refusing to start
Build and Push Docker Image / build-and-push (push) Failing after 20s
Build and Push Docker Image / build-and-push (push) Failing after 20s
setup.sh starts the production server without SESSION_SECRET, so the new "refuse to start" check stopped the app after an update. When the variable is unset in production the server now makes a random secret once and keeps it in DATA_DIR/.session-secret (mode 600, git-ignored). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f274bc9c8a
commit
31fb3f958d
3 files changed
+24
-7
No files matched your search
@@ -11,3 +11,5 @@ coverage/
|
|||||||
.api.log
|
.api.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env
|
.env
|
||||||
|
.session-secret
|
||||||
|
server/data/backups/
|
||||||
@@ -46,7 +46,7 @@ http://localhost:5173. Dev mode is much slower, so don't use it for real users.
|
|||||||
|
|
||||||
| Variable | What it turns on |
|
| Variable | What it turns on |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `SESSION_SECRET` | **Required in production.** A long random string; the server refuses to start without it. |
|
| `SESSION_SECRET` | A long random string that signs login cookies. Optional: if unset in production, the server makes one and keeps it in `DATA_DIR/.session-secret`. |
|
||||||
| `ADMIN_EMAIL` | The one admin account. |
|
| `ADMIN_EMAIL` | The one admin account. |
|
||||||
| `ESV_API_KEY`, `API_BIBLE_KEY` | ESV, NLT, CSB and NASB in the Reader. |
|
| `ESV_API_KEY`, `API_BIBLE_KEY` | ESV, NLT, CSB and NASB in the Reader. |
|
||||||
| `IQBIBLE_API_KEY` | IQ Bible: more commentaries, lexicons (BDB, LSJ, Abbott-Smith), places, topics, people, textual notes, Gospel parallels, daily verse and Spurgeon devotional, plan calendar export, more audio narrations, and background for podcast prompts. Hidden when unset. |
|
| `IQBIBLE_API_KEY` | IQ Bible: more commentaries, lexicons (BDB, LSJ, Abbott-Smith), places, topics, people, textual notes, Gospel parallels, daily verse and Spurgeon devotional, plan calendar export, more audio narrations, and background for podcast prompts. Hidden when unset. |
|
||||||
|
|||||||
+21
-6
@@ -2,11 +2,12 @@ import express from 'express';
|
|||||||
import compression from 'compression';
|
import compression from 'compression';
|
||||||
import session from 'express-session';
|
import session from 'express-session';
|
||||||
import QRCode from 'qrcode';
|
import QRCode from 'qrcode';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID, randomBytes } from 'crypto';
|
||||||
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { dirname, join } from 'path';
|
import { dirname, join } from 'path';
|
||||||
import {
|
import {
|
||||||
initDb, getAllProjects, getProject, getProjectEdited, upsertProject, deleteProject,
|
initDb, dataDir, getAllProjects, getProject, getProjectEdited, upsertProject, deleteProject,
|
||||||
countUsers, createUser, getUserByEmail, getUserById, claimOrphanProjects,
|
countUsers, createUser, getUserByEmail, getUserById, claimOrphanProjects,
|
||||||
enableTotp, disableTotp, setBackupCodeHashes, setPodcastName,
|
enableTotp, disableTotp, setBackupCodeHashes, setPodcastName,
|
||||||
getShareToken, setShareToken, clearShareToken, getProjectByShareToken,
|
getShareToken, setShareToken, clearShareToken, getProjectByShareToken,
|
||||||
@@ -50,9 +51,23 @@ const app = express();
|
|||||||
const PORT = process.env.PORT || 3001;
|
const PORT = process.env.PORT || 3001;
|
||||||
const isProd = process.env.NODE_ENV === 'production';
|
const isProd = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
if (isProd && !process.env.SESSION_SECRET) {
|
// Session cookies are signed with SESSION_SECRET. When it isn't set (setup.sh doesn't set one), a random secret
|
||||||
console.error('SESSION_SECRET is not set. Set it to a long random string before running in production.');
|
// is made once and kept in DATA_DIR, so it survives restarts and nobody has to configure anything.
|
||||||
process.exit(1);
|
function sessionSecret() {
|
||||||
|
if (process.env.SESSION_SECRET) return process.env.SESSION_SECRET;
|
||||||
|
if (!isProd) return 'dev-only-secret-change-me';
|
||||||
|
const file = join(dataDir(), '.session-secret');
|
||||||
|
try {
|
||||||
|
if (existsSync(file)) return readFileSync(file, 'utf8').trim();
|
||||||
|
mkdirSync(dataDir(), { recursive: true });
|
||||||
|
const made = randomBytes(48).toString('hex');
|
||||||
|
writeFileSync(file, made, { mode: 0o600 });
|
||||||
|
console.log(`SESSION_SECRET is not set; generated one and saved it to ${file}.`);
|
||||||
|
return made;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Could not read or save a session secret; set SESSION_SECRET instead.', err.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trust the reverse proxy (needed for secure cookies to work behind nginx/etc).
|
// Trust the reverse proxy (needed for secure cookies to work behind nginx/etc).
|
||||||
@@ -98,7 +113,7 @@ app.use(compression());
|
|||||||
app.use(express.json({ limit: '10mb' }));
|
app.use(express.json({ limit: '10mb' }));
|
||||||
app.use(session({
|
app.use(session({
|
||||||
store: new SqliteSessionStore(),
|
store: new SqliteSessionStore(),
|
||||||
secret: process.env.SESSION_SECRET || 'dev-only-secret-change-me',
|
secret: sessionSecret(),
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
rolling: true,
|
rolling: true,
|
||||||
|
|||||||
Reference in new issue
Block a user