From 5d7bcddcd141551b9379cade5c6867a300bb21cd Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 21 Aug 2026 15:25:19 -0400 Subject: [PATCH] don't allow users to just mount the prime db i guess for privacy reasons (may undo this someday) --- db.js | 25 +++++++++++++++--- index.js | 38 ++++++++++++++++++++++++--- migrations/008_block_prime_mounts.sql | 3 +++ update-deployed.sh | 36 +++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 migrations/008_block_prime_mounts.sql diff --git a/db.js b/db.js index f9ecd64..183a032 100644 --- a/db.js +++ b/db.js @@ -7,6 +7,7 @@ const { Eta } = require("eta"); const cheerio = require("cheerio"); const db = betterSqlite3("./dbs/0.sqlite"); +const PRIME_DB_ID = 0; db.pragma("journal_mode = WAL"); @@ -587,6 +588,7 @@ function getDbsForStructure(structureId) { FROM structure_dbs INNER JOIN dbs ON structure_dbs.db_id = dbs.id WHERE structure_dbs.structure_id = ? + AND NOT (structure_dbs.db_id = ${PRIME_DB_ID} AND structure_dbs.structure_id != ${PRIME_DB_ID}) ORDER BY structure_dbs.created_at, is_aliased ASC; `, ) @@ -607,11 +609,16 @@ function getDb(dbId) { function getDbForStructure(structureId, dbId) { return db .prepare( - `SELECT * + `SELECT + dbs.*, + structure_dbs.alias, + structure_dbs.structure_id AS alias_struct_id, + dbs.structure_id AS db_struct_id FROM structure_dbs INNER JOIN dbs ON structure_dbs.db_id = dbs.id WHERE structure_dbs.structure_id = ? - AND structure_dbs.db_id = ?; + AND structure_dbs.db_id = ? + AND NOT (structure_dbs.db_id = ${PRIME_DB_ID} AND structure_dbs.structure_id != ${PRIME_DB_ID}); `, ) .get(structureId, dbId); @@ -644,6 +651,9 @@ function createDb(structId, name) { } function attachDb(structId, dbId, alias) { + if (Number(dbId) === PRIME_DB_ID && Number(structId) !== PRIME_DB_ID) { + throw new Error("prime db cannot be attached to other structures"); + } const insertStructureDbStmt = db.prepare(` INSERT INTO structure_dbs (db_id, structure_id, alias) VALUES (?, ?, ?) @@ -698,10 +708,17 @@ function cloneStructure( ).lastInsertRowid; const dbIds = db - .prepare(`SELECT db_id FROM structure_dbs WHERE structure_id = ?;`) + .prepare(`SELECT db_id FROM structure_dbs WHERE structure_id = ? AND db_id != ${PRIME_DB_ID};`) .all(structId); - const toClone = new Set(cloneDbs); + const requestedCloneDbs = Array.isArray(cloneDbs) + ? cloneDbs + : cloneDbs + ? [cloneDbs] + : []; + const toClone = new Set( + requestedCloneDbs.filter((dbId) => Number(dbId) !== PRIME_DB_ID), + ); const toAlias = new Set(); for (let { db_id } of dbIds) { diff --git a/index.js b/index.js index cb8ceea..987711f 100644 --- a/index.js +++ b/index.js @@ -12,11 +12,18 @@ const { match } = require("path-to-regexp"); const bcrypt = require("bcrypt"); const cheerio = require("cheerio"); const webPush = require("web-push"); +const { randomUUID } = require("node:crypto"); const app = express(); const _expressWs = require("express-ws")(app); const bodyParser = require("body-parser"); const model = require("./db"); const PORT = process.env.PORT || 3000; +const SESSION_SECRET = process.env.SESSION_SECRET || randomUUID(); +if (!process.env.SESSION_SECRET) { + console.warn( + "SESSION_SECRET is not set; using an ephemeral secret and invalidating sessions on restart.", + ); +} const db = model.db; const wsRouter = express.Router(); @@ -62,7 +69,7 @@ app.use(fileUpload()); app.use( session({ store: new SQLiteStore({ client: db, expired: { clear: true } }), - secret: "your secret key", + secret: SESSION_SECRET, resave: false, saveUninitialized: true, cookie: { secure: false }, @@ -229,6 +236,26 @@ function currentUserFor(req) { } } +function dbForWorkshopRequest(req, res) { + const appDb = model.getDbForStructure( + req.params.structure_id, + req.params.db_id, + ); + if (!appDb) { + res.status(404).send("Not found"); + return null; + } + const ownDb = String(appDb.db_struct_id) === String(req.params.structure_id); + if ( + !ownDb && + !model.canAccessStructure(req.session.userId, appDb.db_struct_id) + ) { + res.status(404).send("Not found"); + return null; + } + return appDb; +} + function userIdFrom(value) { if (value == null) return null; if (typeof value === "object" && value.id != null) return Number(value.id); @@ -925,6 +952,9 @@ app.post("/workshop/:structure_id/db", (req, res) => { }); app.post("/workshop/:structure_id/db/attach", (req, res) => { + if (Number(req.body.db_id) === 0 && Number(req.params.structure_id) !== 0) { + return res.status(400).send("prime db cannot be attached"); + } model.attachDb(req.params.structure_id, req.body.db_id, req.body.alias); const redirectUrl = `/workshop/${req.params.structure_id}/db/${req.body.db_id}`; return smartRedirect(req, res, redirectUrl); @@ -993,7 +1023,8 @@ app.put("/workshop/:structure_id/route/:route_id", (req, res) => { app.put("/workshop/:structure_id/db/:db_id/library", (req, res) => { try { let dbId = req.params.db_id; - let appDb = model.getDb(dbId); + let appDb = dbForWorkshopRequest(req, res); + if (!appDb) return; model.updateDb({ ...appDb, library: req.body.library }); appDb = model.getDb(dbId); @@ -1018,7 +1049,8 @@ app.put("/workshop/:structure_id/db/:db_id/library", (req, res) => { app.post("/workshop/:structure_id/db/:db_id/repl", (req, res) => { try { let dbId = req.params.db_id; - let appDb = model.getDb(dbId); + let appDb = dbForWorkshopRequest(req, res); + if (!appDb) return; let dbInstance = model.getDbInstance(dbId); let capturedOutput = []; let context = vm.createContext({ diff --git a/migrations/008_block_prime_mounts.sql b/migrations/008_block_prime_mounts.sql new file mode 100644 index 0000000..b307dd7 --- /dev/null +++ b/migrations/008_block_prime_mounts.sql @@ -0,0 +1,3 @@ +DELETE FROM structure_dbs +WHERE db_id = 0 + AND structure_id != 0; diff --git a/update-deployed.sh b/update-deployed.sh index b6ecad2..96c371e 100755 --- a/update-deployed.sh +++ b/update-deployed.sh @@ -70,6 +70,42 @@ else echo "Dependencies unchanged; skipping npm install." fi +if [[ ! -f .env ]]; then + install -m 600 /dev/null .env +else + chmod 600 .env || true +fi + +if ! grep -Eq '^SESSION_SECRET=.+$' .env; then + SESSION_SECRET_VALUE="$( + node -e 'process.stdout.write(require("crypto").randomBytes(48).toString("base64url"))' + )" + if grep -Eq '^SESSION_SECRET=' .env; then + ENV_TMP="$(mktemp)" + awk -v secret="$SESSION_SECRET_VALUE" ' + BEGIN { replaced = 0 } + /^SESSION_SECRET=/ { + if (!replaced) { + print "SESSION_SECRET=" secret + replaced = 1 + } + next + } + { print } + END { + if (!replaced) print "SESSION_SECRET=" secret + } + ' .env >"$ENV_TMP" + cat "$ENV_TMP" >.env + rm -f "$ENV_TMP" + else + printf '\nSESSION_SECRET=%s\n' "$SESSION_SECRET_VALUE" >>.env + fi + echo "Generated SESSION_SECRET in $APP_DIR/.env" +else + echo "SESSION_SECRET already present in $APP_DIR/.env" +fi + PM2_TARGET="" if command -v pm2 >/dev/null 2>&1; then PM2_TARGET="$(pm2 jlist 2>/dev/null | APP_DIR="$APP_DIR" node -e '