Merge current features with security hardening and automatic safe initialization
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import { mkdirSync, existsSync, readFileSync, writeFileSync, chmodSync, chownSync } from 'node:fs';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
const passwordFile = process.env.APP_DATABASE_PASSWORD_FILE;
|
||||
if (!passwordFile) throw new Error('Runtime password file is required.');
|
||||
const directory = passwordFile.slice(0, passwordFile.lastIndexOf('/'));
|
||||
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
||||
const password = existsSync(passwordFile) ? readFileSync(passwordFile, 'utf8').trim() : randomBytes(32).toString('hex');
|
||||
if (!/^[a-f0-9]{64}$/.test(password)) throw new Error('Invalid runtime password file.');
|
||||
if (!existsSync(passwordFile)) writeFileSync(passwordFile, password + '\n', { flag: 'wx', mode: 0o400 });
|
||||
chmodSync(passwordFile, 0o400);
|
||||
chownSync(passwordFile, 1001, 1001);
|
||||
chownSync(directory, 1001, 1001);
|
||||
const ownerUrl = new URL('postgresql://postgres:5432/' + encodeURIComponent(process.env.POSTGRES_DB || 'kita_planer'));
|
||||
ownerUrl.username = process.env.POSTGRES_USER || 'kita_planer';
|
||||
ownerUrl.password = process.env.POSTGRES_PASSWORD || '';
|
||||
if (!ownerUrl.password) throw new Error('Database owner password is required.');
|
||||
const environment = { ...process.env, DATABASE_URL: ownerUrl.toString(), APP_DATABASE_PASSWORD: password };
|
||||
for (const args of [['node_modules/prisma/build/index.js', 'migrate', 'deploy'], ['scripts/provision-runtime-role.mjs']]) {
|
||||
const result = spawnSync(process.execPath, args, { env: environment, stdio: 'inherit' });
|
||||
if (result.status !== 0) process.exit(result.status || 1);
|
||||
}
|
||||
for (const path of ['/app/uploads', '/app/uploads/announcements', '/app/uploads/announcements/announcements', '/app/runtime-cache']) {
|
||||
mkdirSync(path, { recursive: true, mode: 0o700 });
|
||||
chownSync(path, 1001, 1001);
|
||||
}
|
||||
console.log('Database initialized; restricted application access and volumes are ready.');
|
||||
@@ -0,0 +1,42 @@
|
||||
// Runs inside the application container. No external cron service or browser is required.
|
||||
const secret = process.env.NOTDIENST_JOB_SECRET;
|
||||
if (!secret || secret.length < 32) {
|
||||
console.error(
|
||||
"Notdienst-Fristprüfung deaktiviert: NOTDIENST_JOB_SECRET muss mindestens 32 Zeichen haben.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const port = Number(process.env.PORT || 3000);
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65535) process.exit(1);
|
||||
let stopped = false;
|
||||
let timer;
|
||||
async function tick() {
|
||||
const started = Date.now();
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://127.0.0.1:${port}/api/jobs/notdienstalarme`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { authorization: `Bearer ${secret}` },
|
||||
signal: AbortSignal.timeout(50000),
|
||||
},
|
||||
);
|
||||
if (!response.ok)
|
||||
console.error(
|
||||
`Notdienst-Fristprüfung: HTTP ${response.status}; erneuter Versuch in einer Minute.`,
|
||||
);
|
||||
} catch {
|
||||
console.error(
|
||||
"Notdienst-Fristprüfung momentan nicht erreichbar; erneuter Versuch in einer Minute.",
|
||||
);
|
||||
}
|
||||
if (!stopped)
|
||||
timer = setTimeout(tick, Math.max(1000, 60000 - (Date.now() - started)));
|
||||
}
|
||||
for (const signal of ["SIGINT", "SIGTERM"])
|
||||
process.on(signal, () => {
|
||||
stopped = true;
|
||||
clearTimeout(timer);
|
||||
process.exit(0);
|
||||
});
|
||||
timer = setTimeout(tick, 5000);
|
||||
@@ -1,3 +1,7 @@
|
||||
import { cpSync } from 'node:fs';
|
||||
import { cpSync, mkdirSync } from 'node:fs';
|
||||
cpSync('.next/static', '.next/standalone/.next/static', { recursive: true });
|
||||
cpSync('public', '.next/standalone/public', { recursive: true });
|
||||
mkdirSync('.next/standalone/scripts', { recursive: true });
|
||||
cpSync('scripts/start-production.sh', '.next/standalone/scripts/start-production.sh');
|
||||
cpSync('scripts/notdienst-alarm-worker.mjs', '.next/standalone/scripts/notdienst-alarm-worker.mjs');
|
||||
cpSync('scripts/runtime-database-url.mjs', '.next/standalone/scripts/runtime-database-url.mjs');
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
const file = process.env.APP_DATABASE_PASSWORD_FILE;
|
||||
if (!file) throw new Error('Runtime password file is required.');
|
||||
const password = readFileSync(file, 'utf8').trim();
|
||||
if (!/^[a-f0-9]{64}$/.test(password)) throw new Error('Invalid runtime password file.');
|
||||
const url = new URL('postgresql://kita_runtime@postgres:5432/' + encodeURIComponent(process.env.APP_DATABASE_NAME || 'kita_planer'));
|
||||
url.password = password;
|
||||
process.stdout.write(url.toString());
|
||||
@@ -1,4 +1,12 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
# Apply reviewed migrations separately before starting the runtime.
|
||||
cd "$(dirname "$0")/.."
|
||||
if [ -n "${APP_DATABASE_PASSWORD_FILE:-}" ]; then
|
||||
DATABASE_URL="$(node scripts/runtime-database-url.mjs)"
|
||||
export DATABASE_URL
|
||||
fi
|
||||
# Schema changes are applied by the maintenance image before runtime startup.
|
||||
if [ -n "${NOTDIENST_JOB_SECRET:-}" ]; then
|
||||
node scripts/notdienst-alarm-worker.mjs &
|
||||
fi
|
||||
exec node server.js
|
||||
|
||||
Reference in New Issue
Block a user