- Create apply-migrations.ts and jobs-worker.ts scripts (package.json references) - Convert badge leaderboard from $queryRawUnsafe to $queryRaw with Prisma.sql templates - Fix OAuth email binding: add oauth_require_link site setting, skip 2FA-protected accounts - Add per-user 2FA rate limiting (5/30s) to prevent TOTP brute-force - Add structured JSON logger with levels (debug/info/warn/error) - Split 341-line HomePage into GuestView + UserView components - Add OpenAPI v3.1 spec at /api/openapi.json - Add LOG_LEVEL env var, regenerate Prisma client - Add mysql2 dependency for migration scripts - All 58 tests pass, typecheck clean
85 lines
2.8 KiB
TypeScript
85 lines
2.8 KiB
TypeScript
import { Cron } from "croner";
|
|
import { env } from "../src/env";
|
|
import { prisma } from "../src/lib/prisma";
|
|
|
|
async function backupEmulatorJar(): Promise<void> {
|
|
if (!env.EMULATOR_JAR_PATH || !env.EMULATOR_BACKUP_DIR) return;
|
|
|
|
const { copyFileSync, mkdirSync, readdirSync, unlinkSync, existsSync } = await import("node:fs");
|
|
const { resolve } = await import("node:path");
|
|
|
|
const timestamp = new Date().toISOString().slice(0, 19).replace(/[T:]/g, "-");
|
|
const backupFile = resolve(env.EMULATOR_BACKUP_DIR, `emulator-${timestamp}.jar`);
|
|
|
|
if (!existsSync(env.EMULATOR_BACKUP_DIR)) {
|
|
mkdirSync(env.EMULATOR_BACKUP_DIR, { recursive: true });
|
|
}
|
|
|
|
try {
|
|
copyFileSync(env.EMULATOR_JAR_PATH, backupFile);
|
|
console.log(`[jobs] Backed up emulator JAR to ${backupFile}`);
|
|
|
|
// Rotate: keep only the N newest
|
|
const keep = env.EMULATOR_BACKUP_KEEP ?? 7;
|
|
const files = readdirSync(env.EMULATOR_BACKUP_DIR)
|
|
.filter((f) => f.startsWith("emulator-") && f.endsWith(".jar"))
|
|
.sort()
|
|
.reverse();
|
|
|
|
for (let i = keep; i < files.length; i++) {
|
|
unlinkSync(resolve(env.EMULATOR_BACKUP_DIR, files[i]));
|
|
console.log(`[jobs] Rotated out old backup: ${files[i]}`);
|
|
}
|
|
} catch (err) {
|
|
console.error("[jobs] JAR backup failed:", err);
|
|
}
|
|
}
|
|
|
|
async function cleanupOldLogs(): Promise<void> {
|
|
try {
|
|
const cutoff = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
|
await prisma.websiteLoginLogs.deleteMany({ where: { createdAt: { lt: cutoff } } });
|
|
console.log("[jobs] Cleaned up login logs older than 30 days");
|
|
} catch (err) {
|
|
console.error("[jobs] Log cleanup failed:", err);
|
|
}
|
|
}
|
|
|
|
async function cleanupOldSessions(): Promise<void> {
|
|
try {
|
|
const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
|
await prisma.passwordReset.deleteMany({ where: { createdAt: { lt: cutoff } } });
|
|
console.log("[jobs] Cleaned up expired password reset tokens");
|
|
} catch (err) {
|
|
console.error("[jobs] Session cleanup failed:", err);
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
console.log("[jobs] Worker started");
|
|
|
|
// JAR backup — daily at 03:00
|
|
if (env.EMULATOR_JAR_PATH && env.EMULATOR_BACKUP_DIR) {
|
|
new Cron("0 3 * * *", () => {
|
|
backupEmulatorJar().catch((e) => console.error("[jobs] Backup error:", e));
|
|
});
|
|
console.log("[jobs] Scheduled: emulator JAR backup (daily 03:00)");
|
|
}
|
|
|
|
// Log cleanup — daily at 04:00
|
|
new Cron("0 4 * * *", () => {
|
|
Promise.all([cleanupOldLogs(), cleanupOldSessions()]).catch((e) =>
|
|
console.error("[jobs] Cleanup error:", e),
|
|
);
|
|
});
|
|
console.log("[jobs] Scheduled: old data cleanup (daily 04:00)");
|
|
|
|
// Run once on startup
|
|
await Promise.all([backupEmulatorJar(), cleanupOldLogs(), cleanupOldSessions()]);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error("[jobs] Fatal:", err);
|
|
process.exit(1);
|
|
});
|