system settings

This commit is contained in:
Alex Holliday committed 2025-11-19 00:00:08 +00:00
1 parent 986e8b926a
commit f40667615e
14 files changed
+244 -1

No files matched your search

Whitespace-only changes.
@@ -0,0 +1,41 @@
import { Request, Response, NextFunction } from "express";
import ApiError from "@/utils/ApiError.js";
import SettingsService from "@/services/system/SettingsService.js";
export interface ISettingsController {
get: (req: Request, res: Response, next: NextFunction) => void;
update: (req: Request, res: Response, next: NextFunction) => void;
}
class SettingsController implements ISettingsController {
private settingsService: SettingsService;
constructor(settingsService: SettingsService) {
this.settingsService = settingsService;
}
get = async (req: Request, res: Response, next: NextFunction) => {
try {
const settings = await this.settingsService.get();
res.status(200).json({
message: "Settings retrieved successfully",
data: settings,
});
} catch (error) {
next(error);
}
};
update = async (req: Request, res: Response, next: NextFunction) => {
try {
const updatedSettings = await this.settingsService.update(req.body);
res.status(200).json({
message: "Settings updated successfully",
data: updatedSettings,
});
} catch (error) {
next(error);
}
};
}
export default SettingsController;
+1
View File
@@ -13,3 +13,4 @@ export { default as DiagnosticController } from "./DiagnosticController.js";
export { default as RecoveryController } from "./RecoveryController.js";
export { default as ProfileController } from "./ProfileController.js";
export { default as IncidentsController } from "./IncidentsController.js";
export { default as SettingsController } from "./SettingsController.js";
+3
View File
@@ -56,3 +56,6 @@ export type { IRecoveryToken } from "./recovery/RecoveryToken.js";
export { Incident, ResolutionTypes } from "./incidents/Incident.js";
export type { IIncident, ResolutionType } from "./incidents/Incident.js";
export { SystemSettings } from "./system/SystemSettings.js";
export type { ISystemSettings } from "./system/SystemSettings.js";
@@ -0,0 +1,45 @@
import mongoose, { Schema, Document } from "mongoose";
export interface ISystemSettings extends Document {
systemEmailHost?: string;
systemEmailPort?: number;
systemEmailAddress?: string;
systemEmailPassword?: string;
systemEmailUser?: string;
systemEmailConnectionHost?: string;
systemEmailTLSServername?: string;
systemEmailSecure: boolean;
systemEmailPool: boolean;
systemEmailIgnoreTLS: boolean;
systemEmailRequireTLS: boolean;
systemEmailRejectUnauthorized: boolean;
}
const SystemSettingsSchema = new Schema<ISystemSettings>(
{
_id: { type: String, default: "global", immutable: true },
systemEmailHost: { type: String },
systemEmailPort: { type: Number },
systemEmailAddress: { type: String },
systemEmailPassword: { type: String },
systemEmailUser: { type: String },
systemEmailConnectionHost: { type: String, default: "localhost" },
systemEmailTLSServername: { type: String },
systemEmailSecure: { type: Boolean, default: false },
systemEmailPool: { type: Boolean, default: false },
systemEmailIgnoreTLS: { type: Boolean, default: false },
systemEmailRequireTLS: { type: Boolean, default: false },
systemEmailRejectUnauthorized: { type: Boolean, default: true },
},
{
timestamps: true,
collection: "system_settings",
}
);
SystemSettingsSchema.index({ _id: 1 }, { unique: true });
export const SystemSettings = mongoose.model<ISystemSettings>(
"SystemSettings",
SystemSettingsSchema
);
+5
View File
@@ -14,6 +14,7 @@ import {
RecoveryController,
ProfileController,
IncidentsController,
SettingsController,
} from "@/controllers/index.js";
export const initControllers = (services: any) => {
@@ -65,5 +66,9 @@ export const initControllers = (services: any) => {
services.monitorService
);
controllers.settingsController = new SettingsController(
services.settingsService
);
return controllers;
};
+3
View File
@@ -15,6 +15,7 @@ import {
RecoveryRoutes,
ProfileRoutes,
IncidentsRoutes,
SettingsRoutes,
} from "@/routes/index.js";
import { errorHandler } from "@/middleware/ErrorHandler.js";
export const initRoutes = (controllers: any, app: Express) => {
@@ -44,6 +45,7 @@ export const initRoutes = (controllers: any, app: Express) => {
const recoveryRoutes = new RecoveryRoutes(controllers.recoveryController);
const profileRoutes = new ProfileRoutes(controllers.profileController);
const incidentsRoutes = new IncidentsRoutes(controllers.incidentsController);
const settingsRoutes = new SettingsRoutes(controllers.settingsController);
app.use("/api/v1/auth", authRoutes.getRouter());
app.use("/api/v1/invite", inviteRoutes.getRouter());
@@ -63,5 +65,6 @@ export const initRoutes = (controllers: any, app: Express) => {
app.use("/api/v1/recovery", recoveryRoutes.getRouter());
app.use("/api/v1/profile", profileRoutes.getRouter());
app.use("/api/v1/incidents", incidentsRoutes.getRouter());
app.use("/api/v1/settings", settingsRoutes.getRouter());
app.use(errorHandler);
};
+4
View File
@@ -27,6 +27,7 @@ import {
DiagnosticService,
RecoveryService,
IncidentService,
SettingsService,
} from "@/services/index.js";
export const initServices = async () => {
@@ -63,6 +64,8 @@ export const initServices = async () => {
const statusPageService = new StatusPageService();
const diagnosticService = new DiagnosticService(jobQueue);
const recoveryService = new RecoveryService();
const settingsService = new SettingsService();
const services = {
checkService,
inviteService,
@@ -89,6 +92,7 @@ export const initServices = async () => {
diagnosticService,
recoveryService,
incidentService,
settingsService,
};
Object.values(services).forEach((service) => {
-1
View File
@@ -3,7 +3,6 @@ import { DiagnosticController } from "@/controllers/index.js";
import { verifyToken } from "@/middleware/VerifyToken.js";
import { verifyOrgPermission } from "@/middleware/VerifyPermission.js";
import { addUserContext } from "@/middleware/AddUserContext.js";
import { FeatureFlags } from "@/utils/FeatureFlags.js";
import { PERMISSIONS } from "@/services/business/AuthService.js";
class DiagnosticRoutes {
+1
View File
@@ -15,3 +15,4 @@ export { default as DiagnosticRoutes } from "./diagnostic.js";
export { default as RecoveryRoutes } from "./recovery.js";
export { default as ProfileRoutes } from "./profile.js";
export { default as IncidentsRoutes } from "./incidents.js";
export { default as SettingsRoutes } from "./settings.js";
+44
View File
@@ -0,0 +1,44 @@
import { Router } from "express";
import { SettingsController } from "@/controllers/index.js";
import { verifyToken } from "@/middleware/VerifyToken.js";
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 { verify } from "node:crypto";
class SettingsRoutes {
private router;
private controller;
constructor(settingsController: SettingsController) {
this.router = Router();
this.controller = settingsController;
this.initRoutes();
}
initRoutes = () => {
this.router.get(
"/",
verifyToken,
addUserContext,
verifyTeamPermission([PERMISSIONS.master]),
this.controller.get
);
this.router.patch(
"/",
verifyToken,
addUserContext,
verifyTeamPermission([PERMISSIONS.master]),
validateBody(systemSettingsSchema),
this.controller.update
);
};
getRouter() {
return this.router;
}
}
export default SettingsRoutes;
+1
View File
@@ -22,5 +22,6 @@ export { default as JobQueue } from "@/services/infrastructure/JobQueue.js";
export { default as JobGenerator } from "@/services/infrastructure/JobGenerator.js";
export { default as ServiceRegistry } from "@/services/system/ServiceRegistry.js";
export { default as SettingsService } from "@/services/system/SettingsService.js";
export * from "@/services/infrastructure/NotificationServices/index.js";
@@ -0,0 +1,53 @@
import { ISystemSettings, SystemSettings } from "@/db/models/index.js";
import ApiError from "@/utils/ApiError.js";
const SERVICE_NAME = "SettingsService";
export interface ISettingsService {
get: () => Promise<ISystemSettings>;
update: (settings: Partial<ISystemSettings>) => Promise<ISystemSettings>;
}
class SettingsService implements ISettingsService {
public SERVICE_NAME: string;
constructor() {
this.SERVICE_NAME = SERVICE_NAME;
}
get = async (): Promise<ISystemSettings> => {
let settings = await SystemSettings.findById("global");
if (!settings) {
settings = await SystemSettings.create({ _id: "global" });
}
if (!settings) {
throw new ApiError("Unable to load system settings", 500);
}
return settings;
};
update = async (
payload: Partial<ISystemSettings>
): Promise<ISystemSettings> => {
const { _id, createdAt, updatedAt, ...updates } = payload as Record<
string,
unknown
>;
const settings = await SystemSettings.findOneAndUpdate(
{ _id: "global" },
{ $set: updates },
{ new: true, upsert: true, setDefaultsOnInsert: true }
);
if (!settings) {
throw new ApiError("Unable to update system settings", 500);
}
return settings;
};
}
export default SettingsService;
+43
View File
@@ -532,3 +532,46 @@ export const getIncidentsQuerySchema = z.object({
export const patchIncidentsBodySchema = z.object({
resolutionNote: z.string().max(200).optional(),
});
export const systemSettingsSchema = z
.object({
systemEmailHost: z.string().trim().min(1).max(255).optional(),
systemEmailPort: z
.union([z.number().int(), z.string().regex(/^\d+$/)])
.optional()
.transform((val) => (typeof val === "string" ? Number(val) : val))
.refine(
(val) =>
val === undefined ||
(Number.isInteger(val) && val > 0 && val < 65536),
{
message: "Port must be between 1 and 65535",
}
),
systemEmailAddress: z.email().optional(),
systemEmailPassword: z.string().optional(),
systemEmailUser: z.string().trim().max(255).optional(),
systemEmailConnectionHost: z.string().trim().max(255).optional(),
systemEmailTLSServername: z.string().trim().max(255).optional(),
systemEmailSecure: z
.union([z.boolean(), z.string()])
.optional()
.transform((val) => (typeof val === "string" ? val === "true" : val)),
systemEmailPool: z
.union([z.boolean(), z.string()])
.optional()
.transform((val) => (typeof val === "string" ? val === "true" : val)),
systemEmailIgnoreTLS: z
.union([z.boolean(), z.string()])
.optional()
.transform((val) => (typeof val === "string" ? val === "true" : val)),
systemEmailRequireTLS: z
.union([z.boolean(), z.string()])
.optional()
.transform((val) => (typeof val === "string" ? val === "true" : val)),
systemEmailRejectUnauthorized: z
.union([z.boolean(), z.string()])
.optional()
.transform((val) => (typeof val === "string" ? val === "true" : val)),
})
.strip();