fix(backend): harden mongo startup and files upload locales
This commit is contained in:
@@ -22,10 +22,11 @@
|
|||||||
|
|
||||||
## MongoDB
|
## MongoDB
|
||||||
|
|
||||||
- Preferred: set a full `MONGO_URL` (example: `mongodb://user:pass@host:27017/indiekit?authSource=admin`).
|
- 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`.
|
||||||
- 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`.
|
- 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 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`.
|
- For `MongoServerError: Authentication failed`, first verify `MONGO_PASSWORD`, then try `MONGO_AUTH_SOURCE=admin`.
|
||||||
|
|
||||||
## Content paths
|
## Content paths
|
||||||
@@ -66,8 +67,9 @@
|
|||||||
- `start.sh` is intentionally ignored by Git (`.gitignore`) so server secrets are not committed.
|
- `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).
|
- 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 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 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 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 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.
|
- The conversations guard patch prevents `Cannot read properties of undefined (reading 'find')` when the `conversation_items` collection is temporarily unavailable.
|
||||||
+7
-2
@@ -7,6 +7,9 @@ const mongoPort = process.env.MONGO_PORT || "27017";
|
|||||||
const mongoDatabase =
|
const mongoDatabase =
|
||||||
process.env.MONGO_DATABASE || process.env.MONGO_DB || "indiekit";
|
process.env.MONGO_DATABASE || process.env.MONGO_DB || "indiekit";
|
||||||
const mongoAuthSource = process.env.MONGO_AUTH_SOURCE || "admin";
|
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 =
|
const mongoCredentials =
|
||||||
mongoUsername && mongoPassword
|
mongoUsername && mongoPassword
|
||||||
? `${encodeURIComponent(mongoUsername)}:${encodeURIComponent(
|
? `${encodeURIComponent(mongoUsername)}:${encodeURIComponent(
|
||||||
@@ -17,9 +20,11 @@ const mongoQuery =
|
|||||||
mongoCredentials && mongoAuthSource
|
mongoCredentials && mongoAuthSource
|
||||||
? `?authSource=${encodeURIComponent(mongoAuthSource)}`
|
? `?authSource=${encodeURIComponent(mongoAuthSource)}`
|
||||||
: "";
|
: "";
|
||||||
|
const mongoUrlFromParts = `mongodb://${mongoCredentials}${mongoHost}:${mongoPort}/${mongoDatabase}${mongoQuery}`;
|
||||||
const mongoUrl =
|
const mongoUrl =
|
||||||
process.env.MONGO_URL ||
|
hasMongoUrl && (!hasMongoCredentials || preferMongoUrl)
|
||||||
`mongodb://${mongoCredentials}${mongoHost}:${mongoPort}/${mongoDatabase}${mongoQuery}`;
|
? process.env.MONGO_URL
|
||||||
|
: mongoUrlFromParts;
|
||||||
|
|
||||||
const githubUsername = process.env.GITHUB_USERNAME || "svemagie";
|
const githubUsername = process.env.GITHUB_USERNAME || "svemagie";
|
||||||
const githubContentToken =
|
const githubContentToken =
|
||||||
|
|||||||
+2
-2
@@ -4,8 +4,8 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"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",
|
"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-frontend-serviceworker-file.mjs && node scripts/patch-conversations-collection-guards.mjs && node node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.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"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
|
|||||||
@@ -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";
|
import config from "../indiekit.config.mjs";
|
||||||
|
|
||||||
const strictMode =
|
const strictMode = process.env.REQUIRE_MONGO !== "0";
|
||||||
process.env.REQUIRE_MONGO === "1" ||
|
|
||||||
(process.env.REQUIRE_MONGO !== "0" && process.env.NODE_ENV === "production");
|
|
||||||
|
|
||||||
const hasMongoUrl = Boolean(process.env.MONGO_URL);
|
const hasMongoUrl = Boolean(process.env.MONGO_URL);
|
||||||
const mongoUser = process.env.MONGO_USERNAME || process.env.MONGO_USER || "";
|
const mongoUser = process.env.MONGO_USERNAME || process.env.MONGO_USER || "";
|
||||||
@@ -37,6 +35,19 @@ if (!mongodbUrl) {
|
|||||||
process.exit(0);
|
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 });
|
const client = new MongoClient(mongodbUrl, { connectTimeoutMS: 5000 });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -46,6 +57,12 @@ try {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = `[preflight] MongoDB connection failed: ${error.message}`;
|
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) {
|
if (strictMode) {
|
||||||
console.error(message);
|
console.error(message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
+2
-1
@@ -12,7 +12,7 @@ if [ -f .env ]; then
|
|||||||
const parsed = dotenv.parse(fs.readFileSync(".env"));
|
const parsed = dotenv.parse(fs.readFileSync(".env"));
|
||||||
for (const [key, value] of Object.entries(parsed)) {
|
for (const [key, value] of Object.entries(parsed)) {
|
||||||
const safe = String(value).split("\x27").join("\x27\"\x27\"\x27");
|
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
|
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-lightningcss.mjs
|
||||||
/usr/local/bin/node scripts/patch-endpoint-media-scope.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-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-frontend-serviceworker-file.mjs
|
||||||
/usr/local/bin/node scripts/patch-conversations-collection-guards.mjs
|
/usr/local/bin/node scripts/patch-conversations-collection-guards.mjs
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user