mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat(bootstrap): zero-config auto-generated secrets on first run
Resolves root cause of #252 (Electron black screen) and #249 (OAuth fail) for users running with zero configuration (no .env needed). New: scripts/bootstrap-env.mjs - Auto-generates JWT_SECRET (64 bytes), STORAGE_ENCRYPTION_KEY (32 bytes), API_KEY_SECRET (32 bytes) if missing or empty - Persists to {DATA_DIR}/server.env — survives restarts, Docker volume remounts, and upgrades without changing secrets - Reads .env from CWD (user overrides), then merges process.env (highest prio) - Logs friendly warnings for missing optional OAuth secrets Updated: run-standalone.mjs + run-next.mjs - Call bootstrapEnv() before spawning server — covers npm + Docker paths Updated: electron/main.js (synchronous inline — CJS cannot await import ESM) - Reads userData/server.env, generates missing secrets with crypto.randomBytes() - Persists back to server.env, sets OMNIROUTE_BOOTSTRAPPED=true New: BootstrapBanner.tsx + page.tsx update - Dismissable amber banner on dashboard home when running in zero-config mode - Shows where server.env is located and how to customize secrets
This commit is contained in:
@@ -383,33 +383,67 @@ function startNextServer() {
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Auto-generate required env vars for Electron ──
|
||||
// In packaged Electron, there is no .env file — JWT_SECRET and
|
||||
// STORAGE_ENCRYPTION_KEY must be generated once and persisted.
|
||||
const envFilePath = path.join(app.getPath("userData"), "electron-env.json");
|
||||
let persistedEnv = {};
|
||||
try {
|
||||
if (fs.existsSync(envFilePath)) {
|
||||
persistedEnv = JSON.parse(fs.readFileSync(envFilePath, "utf8"));
|
||||
// ── Zero-config bootstrap: auto-generate required secrets ─────────────────
|
||||
// Electron uses CJS — cannot dynamically import ESM bootstrap-env.mjs.
|
||||
// This mirrors bootstrap-env.mjs logic synchronously:
|
||||
// 1. Read persisted secrets from userData/server.env
|
||||
// 2. Generate missing secrets with crypto.randomBytes()
|
||||
// 3. Persist back to userData/server.env for future restarts
|
||||
const crypto = require("crypto");
|
||||
const userDataDir = app.getPath("userData");
|
||||
const serverEnvPath = path.join(userDataDir, "server.env");
|
||||
|
||||
// Parse a simple KEY=VALUE file
|
||||
function parseEnvFile(filePath) {
|
||||
if (!fs.existsSync(filePath)) return {};
|
||||
const env = {};
|
||||
for (const line of fs.readFileSync(filePath, "utf8").split(/\r?\n/)) {
|
||||
const t = line.trim();
|
||||
if (!t || t.startsWith("#")) continue;
|
||||
const eq = t.indexOf("=");
|
||||
if (eq < 1) continue;
|
||||
env[t.slice(0, eq).trim()] = t.slice(eq + 1).trim();
|
||||
}
|
||||
} catch {
|
||||
/* ignore read errors */
|
||||
return env;
|
||||
}
|
||||
|
||||
if (!persistedEnv.JWT_SECRET) {
|
||||
persistedEnv.JWT_SECRET = require("crypto").randomBytes(64).toString("hex");
|
||||
}
|
||||
if (!persistedEnv.STORAGE_ENCRYPTION_KEY) {
|
||||
persistedEnv.STORAGE_ENCRYPTION_KEY = require("crypto").randomBytes(32).toString("hex");
|
||||
}
|
||||
if (!persistedEnv.STORAGE_ENCRYPTION_KEY_VERSION) {
|
||||
persistedEnv.STORAGE_ENCRYPTION_KEY_VERSION = "v1";
|
||||
}
|
||||
const persisted = parseEnvFile(serverEnvPath);
|
||||
const serverEnv = { ...process.env, ...persisted };
|
||||
let changed = false;
|
||||
|
||||
try {
|
||||
fs.writeFileSync(envFilePath, JSON.stringify(persistedEnv, null, 2));
|
||||
} catch {
|
||||
/* ignore write errors */
|
||||
if (!serverEnv.JWT_SECRET) {
|
||||
serverEnv.JWT_SECRET = persisted.JWT_SECRET = crypto.randomBytes(64).toString("hex");
|
||||
changed = true;
|
||||
console.log("[Electron] ✨ JWT_SECRET auto-generated");
|
||||
}
|
||||
if (!serverEnv.STORAGE_ENCRYPTION_KEY) {
|
||||
serverEnv.STORAGE_ENCRYPTION_KEY = persisted.STORAGE_ENCRYPTION_KEY = crypto
|
||||
.randomBytes(32)
|
||||
.toString("hex");
|
||||
serverEnv.STORAGE_ENCRYPTION_KEY_VERSION = persisted.STORAGE_ENCRYPTION_KEY_VERSION = "v1";
|
||||
changed = true;
|
||||
console.log("[Electron] ✨ STORAGE_ENCRYPTION_KEY auto-generated");
|
||||
}
|
||||
if (!serverEnv.API_KEY_SECRET) {
|
||||
serverEnv.API_KEY_SECRET = persisted.API_KEY_SECRET = crypto.randomBytes(32).toString("hex");
|
||||
changed = true;
|
||||
console.log("[Electron] ✨ API_KEY_SECRET auto-generated");
|
||||
}
|
||||
if (changed) {
|
||||
serverEnv.OMNIROUTE_BOOTSTRAPPED = "true";
|
||||
try {
|
||||
fs.mkdirSync(userDataDir, { recursive: true });
|
||||
const lines = [
|
||||
"# Auto-generated by OmniRoute bootstrap",
|
||||
"",
|
||||
...Object.entries(persisted).map(([k, v]) => `${k}=${v}`),
|
||||
"",
|
||||
];
|
||||
fs.writeFileSync(serverEnvPath, lines.join("\n"), "utf8");
|
||||
console.log("[Electron] 📁 Secrets persisted to:", serverEnvPath);
|
||||
} catch (e) {
|
||||
console.warn("[Electron] Could not persist secrets:", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[Electron] Starting Next.js server on port", serverPort);
|
||||
@@ -419,8 +453,7 @@ function startNextServer() {
|
||||
nextServer = spawn("node", [serverScript], {
|
||||
cwd: NEXT_SERVER_PATH,
|
||||
env: {
|
||||
...process.env,
|
||||
...persistedEnv,
|
||||
...serverEnv,
|
||||
PORT: String(serverPort),
|
||||
NODE_ENV: "production",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user