From 7a61f4da53ae003f7a8e7afe984fab8bd4b096a8 Mon Sep 17 00:00:00 2001 From: svemagie <869694+svemagie@users.noreply.github.com> Date: Sun, 8 Mar 2026 01:51:18 +0100 Subject: [PATCH] fix(backend): harden mongo startup and files upload locales --- README.md | 10 +- indiekit.config.mjs | 9 +- package.json | 4 +- .../patch-endpoint-files-upload-locales.mjs | 93 +++++++++++++++++++ scripts/preflight-mongo-connection.mjs | 23 ++++- start.example.sh | 3 +- 6 files changed, 130 insertions(+), 12 deletions(-) create mode 100644 scripts/patch-endpoint-files-upload-locales.mjs diff --git a/README.md b/README.md index 3a99f626..1cb5d31b 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,11 @@ ## MongoDB -- Preferred: set a full `MONGO_URL` (example: `mongodb://user:pass@host:27017/indiekit?authSource=admin`). -- If `MONGO_URL` is not set, set `MONGO_USERNAME` and `MONGO_PASSWORD` explicitly; config builds the URL from `MONGO_USERNAME`, `MONGO_PASSWORD`, `MONGO_HOST`, `MONGO_PORT`, `MONGO_DATABASE`, `MONGO_AUTH_SOURCE`. +- Preferred: set `MONGO_USERNAME` and `MONGO_PASSWORD` explicitly; config builds the URL from `MONGO_USERNAME`, `MONGO_PASSWORD`, `MONGO_HOST`, `MONGO_PORT`, `MONGO_DATABASE`, `MONGO_AUTH_SOURCE`. +- You can still use a full `MONGO_URL` (example: `mongodb://user:pass@host:27017/indiekit?authSource=admin`). +- If both `MONGO_URL` and `MONGO_USERNAME`/`MONGO_PASSWORD` are set, decomposed credentials take precedence by default to avoid stale URL mismatches. Set `MONGO_PREFER_URL=1` to force `MONGO_URL` precedence. - Startup scripts now fail fast when `MONGO_URL` is absent and `MONGO_USERNAME` is missing, to avoid silent auth mismatches. -- Startup now runs `scripts/preflight-mongo-connection.mjs` before boot. In `NODE_ENV=production` this is strict and aborts start on Mongo auth/connect failures. +- Startup now runs `scripts/preflight-mongo-connection.mjs` before boot. Preflight is strict by default and aborts start on Mongo auth/connect failures; set `REQUIRE_MONGO=0` to bypass strict mode intentionally. - For `MongoServerError: Authentication failed`, first verify `MONGO_PASSWORD`, then try `MONGO_AUTH_SOURCE=admin`. ## Content paths @@ -66,8 +67,9 @@ - `start.sh` is intentionally ignored by Git (`.gitignore`) so server secrets are not committed. - Use `start.example.sh` as the tracked template and keep real credentials in environment variables (or `.env` on the server). - Startup scripts parse `.env` with the `dotenv` parser (not shell `source`), so values containing spaces are handled safely. -- Startup scripts run preflight + patch helpers before boot (`scripts/preflight-mongo-connection.mjs`, `scripts/patch-lightningcss.mjs`, `scripts/patch-endpoint-media-scope.mjs`, `scripts/patch-endpoint-files-upload-route.mjs`, `scripts/patch-frontend-serviceworker-file.mjs`, `scripts/patch-conversations-collection-guards.mjs`). +- Startup scripts run preflight + patch helpers before boot (`scripts/preflight-mongo-connection.mjs`, `scripts/patch-lightningcss.mjs`, `scripts/patch-endpoint-media-scope.mjs`, `scripts/patch-endpoint-files-upload-route.mjs`, `scripts/patch-endpoint-files-upload-locales.mjs`, `scripts/patch-frontend-serviceworker-file.mjs`, `scripts/patch-conversations-collection-guards.mjs`). - The media scope patch fixes a known upstream issue where file uploads can fail if the token scope is `create update delete` without explicit `media`. - The files upload route patch fixes browser multi-upload by posting to `/files/upload` (session-authenticated) instead of direct `/media` calls without bearer token. +- The files upload locale patch adds missing `files.upload.dropText`/`files.upload.browse`/`files.upload.submitMultiple` labels in endpoint locale files so UI text does not render raw translation keys. - The frontend serviceworker patch ensures `@indiekit/frontend/lib/serviceworker.js` exists at runtime to avoid ENOENT in the offline/service worker route. - The conversations guard patch prevents `Cannot read properties of undefined (reading 'find')` when the `conversation_items` collection is temporarily unavailable. \ No newline at end of file diff --git a/indiekit.config.mjs b/indiekit.config.mjs index 9bce4d68..5b89544a 100644 --- a/indiekit.config.mjs +++ b/indiekit.config.mjs @@ -7,6 +7,9 @@ const mongoPort = process.env.MONGO_PORT || "27017"; const mongoDatabase = process.env.MONGO_DATABASE || process.env.MONGO_DB || "indiekit"; const mongoAuthSource = process.env.MONGO_AUTH_SOURCE || "admin"; +const hasMongoUrl = Boolean(process.env.MONGO_URL); +const hasMongoCredentials = Boolean(mongoUsername && mongoPassword); +const preferMongoUrl = process.env.MONGO_PREFER_URL === "1"; const mongoCredentials = mongoUsername && mongoPassword ? `${encodeURIComponent(mongoUsername)}:${encodeURIComponent( @@ -17,9 +20,11 @@ const mongoQuery = mongoCredentials && mongoAuthSource ? `?authSource=${encodeURIComponent(mongoAuthSource)}` : ""; +const mongoUrlFromParts = `mongodb://${mongoCredentials}${mongoHost}:${mongoPort}/${mongoDatabase}${mongoQuery}`; const mongoUrl = - process.env.MONGO_URL || - `mongodb://${mongoCredentials}${mongoHost}:${mongoPort}/${mongoDatabase}${mongoQuery}`; + hasMongoUrl && (!hasMongoCredentials || preferMongoUrl) + ? process.env.MONGO_URL + : mongoUrlFromParts; const githubUsername = process.env.GITHUB_USERNAME || "svemagie"; const githubContentToken = diff --git a/package.json b/package.json index 9f96dc78..5125c923 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "", "main": "index.js", "scripts": { - "postinstall": "node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-conversations-collection-guards.mjs", - "serve": "node scripts/preflight-mongo-connection.mjs && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-conversations-collection-guards.mjs && node node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs", + "postinstall": "node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-conversations-collection-guards.mjs", + "serve": "node scripts/preflight-mongo-connection.mjs && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-conversations-collection-guards.mjs && node node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], diff --git a/scripts/patch-endpoint-files-upload-locales.mjs b/scripts/patch-endpoint-files-upload-locales.mjs new file mode 100644 index 00000000..dbf2e366 --- /dev/null +++ b/scripts/patch-endpoint-files-upload-locales.mjs @@ -0,0 +1,93 @@ +import { access, readdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; + +const localeDirCandidates = [ + "node_modules/@indiekit/endpoint-files/locales", + "node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-files/locales", +]; + +const defaultLabels = { + dropText: "Drag files here or", + browse: "Browse files", + submitMultiple: "Upload files", +}; + +const localeLabels = { + de: { + dropText: "Dateien hierher ziehen oder", + browse: "Dateien auswaehlen", + submitMultiple: "Dateien hochladen", + }, +}; + +async function exists(filePath) { + try { + await access(filePath); + return true; + } catch { + return false; + } +} + +let checkedDirs = 0; +let checkedFiles = 0; +let patchedFiles = 0; + +for (const localeDir of localeDirCandidates) { + if (!(await exists(localeDir))) { + continue; + } + + checkedDirs += 1; + const files = (await readdir(localeDir)).filter((file) => file.endsWith(".json")); + + for (const fileName of files) { + const filePath = path.join(localeDir, fileName); + const source = await readFile(filePath, "utf8"); + let json; + + try { + json = JSON.parse(source); + } catch { + continue; + } + + checkedFiles += 1; + + if (!json.files || typeof json.files !== "object") { + json.files = {}; + } + + if (!json.files.upload || typeof json.files.upload !== "object") { + json.files.upload = {}; + } + + const locale = fileName.replace(/\.json$/, ""); + const labels = localeLabels[locale] || defaultLabels; + + let changed = false; + for (const [key, value] of Object.entries(labels)) { + if (!json.files.upload[key]) { + json.files.upload[key] = value; + changed = true; + } + } + + if (!changed) { + continue; + } + + await writeFile(filePath, `${JSON.stringify(json, null, 2)}\n`, "utf8"); + patchedFiles += 1; + } +} + +if (checkedDirs === 0) { + console.log("[postinstall] No endpoint-files locale directories found"); +} else if (patchedFiles === 0) { + console.log("[postinstall] endpoint-files upload locale keys already patched"); +} else { + console.log( + `[postinstall] Patched endpoint-files upload locale keys in ${patchedFiles}/${checkedFiles} locale file(s)`, + ); +} diff --git a/scripts/preflight-mongo-connection.mjs b/scripts/preflight-mongo-connection.mjs index 58250c3c..aabdfb07 100644 --- a/scripts/preflight-mongo-connection.mjs +++ b/scripts/preflight-mongo-connection.mjs @@ -2,9 +2,7 @@ import { MongoClient } from "mongodb"; import config from "../indiekit.config.mjs"; -const strictMode = - process.env.REQUIRE_MONGO === "1" || - (process.env.REQUIRE_MONGO !== "0" && process.env.NODE_ENV === "production"); +const strictMode = process.env.REQUIRE_MONGO !== "0"; const hasMongoUrl = Boolean(process.env.MONGO_URL); const mongoUser = process.env.MONGO_USERNAME || process.env.MONGO_USER || ""; @@ -37,6 +35,19 @@ if (!mongodbUrl) { process.exit(0); } +try { + const parsedUrl = new URL(mongodbUrl); + const database = parsedUrl.pathname.replace(/^\//, "") || "(default)"; + const authSource = parsedUrl.searchParams.get("authSource") || "(default)"; + const username = parsedUrl.username ? decodeURIComponent(parsedUrl.username) : "(none)"; + + console.log( + `[preflight] Mongo target ${parsedUrl.hostname}:${parsedUrl.port || "27017"}/${database} user=${username} authSource=${authSource}`, + ); +} catch { + // Keep preflight behavior unchanged if URL parsing fails. +} + const client = new MongoClient(mongodbUrl, { connectTimeoutMS: 5000 }); try { @@ -46,6 +57,12 @@ try { } catch (error) { const message = `[preflight] MongoDB connection failed: ${error.message}`; + if (hasMongoUrl && mongoUser && hasMongoPassword) { + console.warn( + "[preflight] Both MONGO_URL and MONGO_USERNAME/MONGO_PASSWORD are set. Effective precedence follows indiekit.config.mjs.", + ); + } + if (strictMode) { console.error(message); process.exit(1); diff --git a/start.example.sh b/start.example.sh index 257a0331..4f154c46 100644 --- a/start.example.sh +++ b/start.example.sh @@ -12,7 +12,7 @@ if [ -f .env ]; then const parsed = dotenv.parse(fs.readFileSync(".env")); for (const [key, value] of Object.entries(parsed)) { const safe = String(value).split("\x27").join("\x27\"\x27\"\x27"); - process.stdout.write(`export ${key}=\x27${safe}\x27\\n`); + process.stdout.write(`export ${key}=\x27${safe}\x27\n`); } ')" fi @@ -41,6 +41,7 @@ export NODE_ENV="${NODE_ENV:-production}" /usr/local/bin/node scripts/patch-lightningcss.mjs /usr/local/bin/node scripts/patch-endpoint-media-scope.mjs /usr/local/bin/node scripts/patch-endpoint-files-upload-route.mjs +/usr/local/bin/node scripts/patch-endpoint-files-upload-locales.mjs /usr/local/bin/node scripts/patch-frontend-serviceworker-file.mjs /usr/local/bin/node scripts/patch-conversations-collection-guards.mjs