security hardening, team invites, granular locking, view counts, board subscriptions, scheduled changelog, mentions, recovery codes, accessibility and hover states

This commit is contained in:
2026-03-21 17:37:01 +02:00
parent f07eddf29e
commit 5ba25fb956
142 changed files with 30397 additions and 2287 deletions

View File

@@ -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})`);
}
}