Local Build and Deploy / deploy (push) Successful in 1m9s
Sentry is opt-in via DSN env vars; logger uses structured pino JSON in prod; badge uploads are normalized to GIF with sharp. Co-authored-by: Cursor <[email protected]>
124 lines
3.2 KiB
TypeScript
124 lines
3.2 KiB
TypeScript
import { Cron } from "croner";
|
|
import { env } from "../src/env";
|
|
import { logger } from "../src/lib/logger";
|
|
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);
|
|
logger.info("Backed up emulator JAR", {
|
|
module: "jobs",
|
|
backupFile,
|
|
});
|
|
|
|
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]));
|
|
logger.info("Rotated out old backup", {
|
|
module: "jobs",
|
|
file: files[i],
|
|
});
|
|
}
|
|
} catch (err) {
|
|
logger.error("JAR backup failed", {
|
|
module: "jobs",
|
|
err: err instanceof Error ? err.message : String(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 } },
|
|
});
|
|
logger.info("Cleaned up login logs older than 30 days", { module: "jobs" });
|
|
} catch (err) {
|
|
logger.error("Log cleanup failed", {
|
|
module: "jobs",
|
|
err: err instanceof Error ? err.message : String(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 } },
|
|
});
|
|
logger.info("Cleaned up expired password reset tokens", {
|
|
module: "jobs",
|
|
});
|
|
} catch (err) {
|
|
logger.error("Session cleanup failed", {
|
|
module: "jobs",
|
|
err: err instanceof Error ? err.message : String(err),
|
|
});
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
logger.info("Worker started", { module: "jobs" });
|
|
|
|
if (env.EMULATOR_JAR_PATH && env.EMULATOR_BACKUP_DIR) {
|
|
new Cron("0 3 * * *", () => {
|
|
backupEmulatorJar().catch((e) =>
|
|
logger.error("Backup error", {
|
|
module: "jobs",
|
|
err: e instanceof Error ? e.message : String(e),
|
|
}),
|
|
);
|
|
});
|
|
logger.info("Scheduled: emulator JAR backup (daily 03:00)", {
|
|
module: "jobs",
|
|
});
|
|
}
|
|
|
|
new Cron("0 4 * * *", () => {
|
|
Promise.all([cleanupOldLogs(), cleanupOldSessions()]).catch((e) =>
|
|
logger.error("Cleanup error", {
|
|
module: "jobs",
|
|
err: e instanceof Error ? e.message : String(e),
|
|
}),
|
|
);
|
|
});
|
|
logger.info("Scheduled: old data cleanup (daily 04:00)", { module: "jobs" });
|
|
|
|
await Promise.all([
|
|
backupEmulatorJar(),
|
|
cleanupOldLogs(),
|
|
cleanupOldSessions(),
|
|
]);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
logger.error("Fatal", {
|
|
module: "jobs",
|
|
err: err instanceof Error ? err.message : String(err),
|
|
});
|
|
process.exit(1);
|
|
});
|