checks retention routes

This commit is contained in:
Alex Holliday committed 2025-11-27 23:37:29 +00:00
1 parent ce97e201c9
commit 08887bbc58
6 files changed
+104 -3

No files matched your search

@@ -9,6 +9,11 @@ export interface ISettingsController {
res: Response,
next: NextFunction
) => void;
updateRetentionPolicy: (
req: Request,
res: Response,
next: NextFunction
) => void;
testTransport: (req: Request, res: Response, next: NextFunction) => void;
}
@@ -53,6 +58,23 @@ class SettingsController implements ISettingsController {
}
};
updateRetentionPolicy = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const retentionDays = req.body.checksRetentionDays;
const updatedRetentionPolicy =
await this.settingsService.updateRetentionPolicy(retentionDays);
res.status(200).json({
message: `Retention policy set to ${updatedRetentionPolicy} days`,
});
} catch (error) {
next(error);
}
};
testTransport = async (req: Request, res: Response, next: NextFunction) => {
try {
const testResult = await this.notificationService.testEmailTransport(
@@ -13,6 +13,7 @@ export interface ISystemSettings extends Document {
systemEmailIgnoreTLS: boolean;
systemEmailRequireTLS: boolean;
systemEmailRejectUnauthorized: boolean;
dataRetentionDays: number;
}
const SystemSettingsSchema = new Schema<ISystemSettings>(
@@ -30,6 +31,7 @@ const SystemSettingsSchema = new Schema<ISystemSettings>(
systemEmailIgnoreTLS: { type: Boolean, default: false },
systemEmailRequireTLS: { type: Boolean, default: false },
systemEmailRejectUnauthorized: { type: Boolean, default: true },
dataRetentionDays: { type: Number, default: 90 },
},
{
timestamps: true,
+13 -1
View File
@@ -5,8 +5,12 @@ import { verifyTeamPermission } from "@/middleware/VerifyPermission.js";
import { addUserContext } from "@/middleware/AddUserContext.js";
import { PERMISSIONS } from "@/services/business/AuthService.js";
import { validateBody } from "@/middleware/validation.js";
import { systemSettingsSchema } from "@/validation/index.js";
import {
systemSettingsSchema,
updateRetentionPolicySchema,
} from "@/validation/index.js";
import { verify } from "node:crypto";
import { FeatureFlags } from "@/utils/FeatureFlags.js";
class SettingsRoutes {
private router;
@@ -34,6 +38,14 @@ class SettingsRoutes {
validateBody(systemSettingsSchema),
this.controller.updateEmailSettings
);
this.router.patch(
"/retention",
verifyToken,
addUserContext,
verifyTeamPermission(FeatureFlags.getSetRetentionPermission()),
validateBody(updateRetentionPolicySchema),
this.controller.updateRetentionPolicy
);
this.router.post(
"/test-transport",
verifyToken,
@@ -1,4 +1,5 @@
import { ISystemSettings, SystemSettings } from "@/db/models/index.js";
import mongoose from "mongoose";
import ApiError from "@/utils/ApiError.js";
import { EmailService } from "@/services/index.js";
const SERVICE_NAME = "SettingsService";
@@ -7,6 +8,7 @@ export interface ISettingsService {
updateEmailSettings: (
settings: Partial<ISystemSettings>
) => Promise<ISystemSettings>;
updateRetentionPolicy: (retentionDays: number) => Promise<number>;
}
class SettingsService implements ISettingsService {
@@ -93,6 +95,59 @@ class SettingsService implements ISettingsService {
return settings;
};
updateRetentionPolicy = async (retentionDays: number) => {
const settings = await SystemSettings.findOneAndUpdate(
{ _id: "global" },
{ $set: { checksRetentionDays: retentionDays } },
{ new: true, upsert: true, setDefaultsOnInsert: true }
);
if (!settings) {
throw new ApiError("Unable to update system settings", 500);
}
const db = mongoose.connection.db;
if (!db) {
throw new ApiError("Database not initialized", 500);
}
const seconds = Math.floor(retentionDays * 24 * 60 * 60);
const cmdResult = await db.command({
collMod: "checks",
expireAfterSeconds: seconds,
});
if (!cmdResult || (cmdResult as any).ok !== 1) {
throw new ApiError("Failed to apply TTL to checks collection", 500);
}
const list = (await db.command({
listCollections: 1,
filter: { name: "checks" },
})) as unknown as {
cursor?: {
firstBatch?: Array<{
options?: { expireAfterSeconds?: number | string };
}>;
};
ok?: 1 | 0;
};
const expireOption =
list?.cursor?.firstBatch?.[0]?.options?.expireAfterSeconds;
const appliedSeconds =
typeof expireOption === "number" ? expireOption : Number(expireOption);
if (!appliedSeconds || appliedSeconds !== seconds) {
throw new ApiError(
"TTL verification failed: expireAfterSeconds not applied",
500
);
}
return retentionDays;
};
}
export default SettingsService;
+5 -2
View File
@@ -11,8 +11,11 @@ const FeatureFlags = {
mode: CURRENT_MODE,
isSaaS,
isSelfHosted,
getDiagnosticPermission(): string {
return isSaaS() ? PERMISSIONS.master : PERMISSIONS.diagnostic.read;
getDiagnosticPermission(): string[] {
return isSaaS() ? [PERMISSIONS.master] : [PERMISSIONS.diagnostic.read];
},
getSetRetentionPermission(): string[] {
return isSaaS() ? [PERMISSIONS.master] : [PERMISSIONS.checks.write];
},
};
+7
View File
@@ -646,3 +646,10 @@ export const systemSettingsSchema = z
.transform((val) => (typeof val === "string" ? val === "true" : val)),
})
.strip();
export const updateRetentionPolicySchema = z.object({
checksRetentionDays: z
.number()
.min(1, "Retention days must be at least 1")
.max(365, "Retention days cannot exceed 365 days"),
});