101%: app-level DDoS guard, PWA, /api/health, API docs, worker JAR backup

Beyond parity — the web-feasible versions of the "host-only" items plus
extras AtomCMS doesn't have:
- App-level abuse/DDoS guard (src/lib/services/abuse-guard.ts): counts
  requests per IP and auto-adds flooders to website_ip_blacklist (enforced
  by the access guard) + fires ddosDetected(). OFF by default, tunable via
  settings. The iptables layer stays host-only; this is the real app-tier
  mitigation. Access guard now also enforces the IP blacklist (cached).
- PWA: a themeable web manifest (src/app/manifest.ts) + a service worker
  (public/sw.js, cache-first assets / network-first pages) registered after
  hydration — the hotel is now installable.
- /api/health: DB + emulator(RCON) + runtime status probe.
- /developers: a public API documentation page covering every REST endpoint
  with its method, path and auth requirement.
- jobs-worker: daily emulator JAR backup (runs host-side in the worker, like
  AtomCMS's backup command) — copies + prunes; no-ops unless EMULATOR_JAR_PATH
  + EMULATOR_BACKUP_DIR are set.

Verified live (prod, amx_test): /api/health ok, manifest + sw served, docs
page renders, normal pages unaffected by the guard. tsc 0, vitest 49/49,
next build 0.
This commit is contained in:
Simo committed 2026-06-29 18:39:23 +02:00
1 parent 4dfe698009
commit 54ec99de6d
11 files changed
+702 -1

No files matched your search

+37
View File
@@ -20,6 +20,8 @@
* worker is the first consumer.
*/
import "dotenv/config";
import { copyFile, mkdir, readdir, stat, unlink } from "node:fs/promises";
import { join } from "node:path";
import { Cron } from "croner";
import { prisma } from "@/lib/prisma";
import { emulatorOffline } from "@/lib/services/alert";
@@ -245,6 +247,40 @@ async function githubUpdateCheck(): Promise<void> {
}
}
// --- (g) emulator: JAR backup — daily --------------------------------------
//
// AtomCMS's backup tooling lives in the scheduler (host-side), which is exactly
// what this worker is — so it CAN do the filesystem copy the Next request tier
// cannot. Copies EMULATOR_JAR_PATH into EMULATOR_BACKUP_DIR with a timestamped
// name, keeping the newest EMULATOR_BACKUP_KEEP (default 7). No-ops cleanly when
// the env paths aren't set (e.g. on a dev box).
async function emulatorBackup(): Promise<void> {
const jar = process.env.EMULATOR_JAR_PATH;
const dir = process.env.EMULATOR_BACKUP_DIR;
if (!jar || !dir) return; // not configured — nothing to do
try {
await stat(jar); // ensure the source exists
await mkdir(dir, { recursive: true });
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
await copyFile(jar, join(dir, `emulator-${stamp}.jar`));
// Prune to the newest N backups.
const keep = Number(process.env.EMULATOR_BACKUP_KEEP ?? "7") || 7;
const files = (await readdir(dir))
.filter((f) => f.startsWith("emulator-") && f.endsWith(".jar"))
.sort()
.reverse();
for (const old of files.slice(keep)) {
await unlink(join(dir, old)).catch(() => {});
}
log("backup", `emulator JAR backed up (${files.length + 1} kept, pruning to ${keep})`);
} catch (e) {
logErr("backup", "emulator backup failed", e);
}
}
// --- scheduler wiring ------------------------------------------------------
const jobs: Cron[] = [
@@ -254,6 +290,7 @@ const jobs: Cron[] = [
new Cron("*/30 * * * * *", { name: "radio-record-songs", protect: true }, recordSongPlay),
new Cron("* * * * *", { name: "radio-auto-dj", protect: true }, autoDj),
new Cron("0 * * * *", { name: "github-update-check", protect: true }, githubUpdateCheck),
new Cron("0 4 * * *", { name: "emulator-backup", protect: true }, emulatorBackup),
];
log("worker", `started — ${jobs.length} scheduled job(s): ${jobs.map((j) => j.name).join(", ")}`);