fix(backend): harden mongo startup and files upload locales
This commit is contained in:
10
README.md
10
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.
|
||||
@@ -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 =
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
93
scripts/patch-endpoint-files-upload-locales.mjs
Normal file
93
scripts/patch-endpoint-files-upload-locales.mjs
Normal file
@@ -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)`,
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user