security hardening, team invites, granular locking, view counts, board subscriptions, scheduled changelog, mentions, recovery codes, accessibility and hover states
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import cron from "node-cron";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { unlink } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import prisma from "../lib/prisma.js";
|
||||
import { config } from "../config.js";
|
||||
import { cleanExpiredChallenges } from "../routes/passkey.js";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
import { cleanupExpiredTokens } from "../lib/token-blocklist.js";
|
||||
import { getPluginCronJobs } from "../plugins/loader.js";
|
||||
import { cleanupViews } from "../lib/view-tracker.js";
|
||||
|
||||
export function startCronJobs() {
|
||||
// prune old activity events - daily at 3am
|
||||
@@ -38,23 +41,92 @@ export function startCronJobs() {
|
||||
}
|
||||
});
|
||||
|
||||
// clean webauthn challenges - every 10 minutes
|
||||
cron.schedule("*/10 * * * *", () => {
|
||||
// clean webauthn challenges - every minute
|
||||
cron.schedule("* * * * *", () => {
|
||||
cleanExpiredChallenges();
|
||||
});
|
||||
|
||||
// remove failed push subscriptions - daily at 5am
|
||||
cron.schedule("0 5 * * *", async () => {
|
||||
// subscriptions with no associated user get cleaned by cascade
|
||||
// this handles any other stale ones
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - 30);
|
||||
|
||||
const result = await prisma.pushSubscription.deleteMany({
|
||||
where: { createdAt: { lt: cutoff } },
|
||||
// clean expired recovery codes - daily at 3:30am
|
||||
cron.schedule("30 3 * * *", async () => {
|
||||
const result = await prisma.recoveryCode.deleteMany({
|
||||
where: { expiresAt: { lt: new Date() } },
|
||||
});
|
||||
if (result.count > 0) {
|
||||
console.log(`Cleaned ${result.count} old push subscriptions`);
|
||||
console.log(`Cleaned ${result.count} expired recovery codes`);
|
||||
}
|
||||
});
|
||||
|
||||
// clean expired blocked tokens - every 30 minutes
|
||||
cron.schedule("*/30 * * * *", async () => {
|
||||
const count = await cleanupExpiredTokens();
|
||||
if (count > 0) {
|
||||
console.log(`Cleaned ${count} expired blocked tokens`);
|
||||
}
|
||||
});
|
||||
|
||||
// remove push subscriptions with too many failures - daily at 5am
|
||||
cron.schedule("0 5 * * *", async () => {
|
||||
const result = await prisma.pushSubscription.deleteMany({
|
||||
where: { failureCount: { gte: 3 } },
|
||||
});
|
||||
if (result.count > 0) {
|
||||
console.log(`Cleaned ${result.count} failed push subscriptions`);
|
||||
}
|
||||
});
|
||||
|
||||
// clean orphaned attachments (uploaded but never linked) - hourly
|
||||
cron.schedule("30 * * * *", async () => {
|
||||
const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
|
||||
const orphans = await prisma.attachment.findMany({
|
||||
where: {
|
||||
postId: null,
|
||||
commentId: null,
|
||||
createdAt: { lt: cutoff },
|
||||
},
|
||||
select: { id: true, path: true },
|
||||
});
|
||||
|
||||
if (orphans.length === 0) return;
|
||||
|
||||
const ids = orphans.map((a) => a.id);
|
||||
const result = await prisma.attachment.deleteMany({
|
||||
where: { id: { in: ids }, postId: null, commentId: null },
|
||||
});
|
||||
|
||||
if (result.count > 0) {
|
||||
console.log(`Cleaned ${result.count} orphaned attachments`);
|
||||
}
|
||||
|
||||
for (const att of orphans) {
|
||||
try {
|
||||
await unlink(resolve(process.cwd(), "uploads", att.path));
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
|
||||
// clean expired view-tracker entries - every 5 minutes
|
||||
cron.schedule("*/5 * * * *", () => { cleanupViews(); });
|
||||
|
||||
// register plugin-provided cron jobs (min interval: every minute, reject sub-minute)
|
||||
for (const job of getPluginCronJobs()) {
|
||||
if (!cron.validate(job.schedule)) {
|
||||
console.error(`Plugin cron "${job.name}" has invalid schedule: ${job.schedule}, skipping`);
|
||||
continue;
|
||||
}
|
||||
// reject schedules with 6 fields (seconds) to prevent sub-minute execution
|
||||
const fields = job.schedule.trim().split(/\s+/);
|
||||
if (fields.length > 5) {
|
||||
console.error(`Plugin cron "${job.name}" uses sub-minute schedule, skipping`);
|
||||
continue;
|
||||
}
|
||||
cron.schedule(job.schedule, async () => {
|
||||
try {
|
||||
await job.handler();
|
||||
} catch (err) {
|
||||
console.error(`Plugin cron job "${job.name}" failed:`, err);
|
||||
}
|
||||
});
|
||||
console.log(`Registered plugin cron: ${job.name} (${job.schedule})`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user