diff --git a/client/src/Components/v1/NotificationConfig/index.jsx b/client/src/Components/v1/NotificationConfig/index.jsx index d76fd9fb6..f1ed30ad1 100644 --- a/client/src/Components/v1/NotificationConfig/index.jsx +++ b/client/src/Components/v1/NotificationConfig/index.jsx @@ -31,7 +31,7 @@ const NotificationConfig = ({ setMonitor((prev) => { return { ...prev, - notifications: value.map((notification) => notification._id), + notifications: value.map((notification) => notification.id), }; }); }; @@ -39,14 +39,14 @@ const NotificationConfig = ({ // Handlers const handleDelete = (id) => { const updatedNotifications = selectedNotifications.filter( - (notification) => notification._id !== id + (notification) => notification.id !== id ); setSelectedNotifications(updatedNotifications); setMonitor((prev) => { return { ...prev, - notifications: updatedNotifications.map((notification) => notification._id), + notifications: updatedNotifications.map((notification) => notification.id), }; }); }; @@ -57,7 +57,7 @@ const NotificationConfig = ({ useEffect(() => { if (setNotifications) { const toSet = setNotifications.map((notification) => { - return notifications.find((n) => n._id === notification); + return notifications.find((n) => n.id === notification); }); setSelectedNotifications(toSet); } @@ -94,7 +94,7 @@ const NotificationConfig = ({ { - handleDelete(notification._id); + handleDelete(notification.id); }} style={{ cursor: "pointer" }} /> diff --git a/client/src/Hooks/useNotifications.js b/client/src/Hooks/useNotifications.js index 5f2058b6a..6c5919fac 100644 --- a/client/src/Hooks/useNotifications.js +++ b/client/src/Hooks/useNotifications.js @@ -102,7 +102,7 @@ const useGetNotificationById = (id, setNotification) => { const notificationData = { address: notification?.address, notificationName: notification?.notificationName, - type: NOTIFICATION_TYPES.find((type) => type.value === notification?.type)?._id, + type: NOTIFICATION_TYPES.find((type) => type.value === notification?.type)?.id, }; setNotification(notificationData); diff --git a/client/src/Pages/Notifications/components/ActionMenu.jsx b/client/src/Pages/Notifications/components/ActionMenu.jsx index 52516a737..a3ccc4bbb 100644 --- a/client/src/Pages/Notifications/components/ActionMenu.jsx +++ b/client/src/Pages/Notifications/components/ActionMenu.jsx @@ -32,13 +32,13 @@ const ActionMenu = ({ notification, onDelete }) => { const handleRemove = (e) => { e.stopPropagation(); - onDelete(notification._id); + onDelete(notification.id); handleClose(); }; const handleConfigure = (e) => { e.stopPropagation(); - navigate(`/notifications/${notification._id}`); + navigate(`/notifications/${notification.id}`); handleClose(); }; diff --git a/client/src/Pages/Notifications/create/index.jsx b/client/src/Pages/Notifications/create/index.jsx index 11bf2a95d..14b72ab61 100644 --- a/client/src/Pages/Notifications/create/index.jsx +++ b/client/src/Pages/Notifications/create/index.jsx @@ -56,7 +56,7 @@ const CreateNotifications = () => { const [notification, setNotification] = useState({ notificationName: "", address: "", - type: NOTIFICATION_TYPES[0]._id, + type: NOTIFICATION_TYPES[0].id, }); const [errors, setErrors] = useState({}); const { t } = useTranslation(); @@ -66,7 +66,7 @@ const CreateNotifications = () => { const [isOpen, setIsOpen] = useState(false); const getNotificationTypeValue = (typeId) => { - return NOTIFICATION_TYPES.find((type) => type._id === typeId)?.value || "email"; + return NOTIFICATION_TYPES.find((type) => type.id === typeId)?.value || "email"; }; const extractError = (error, field) => diff --git a/client/src/Pages/Notifications/index.jsx b/client/src/Pages/Notifications/index.jsx index 8ebdb22ec..70ed3ef96 100644 --- a/client/src/Pages/Notifications/index.jsx +++ b/client/src/Pages/Notifications/index.jsx @@ -100,7 +100,7 @@ const Notifications = () => { {t("notifications.createTitle")} navigate(`/notifications/${row._id}`), + onRowClick: (row) => navigate(`/notifications/${row.id}`), rowSX: { cursor: "pointer", "&:hover td": { diff --git a/client/src/Pages/Notifications/utils.js b/client/src/Pages/Notifications/utils.js index 2a3d32f4d..9ad422101 100644 --- a/client/src/Pages/Notifications/utils.js +++ b/client/src/Pages/Notifications/utils.js @@ -1,10 +1,10 @@ export const NOTIFICATION_TYPES = [ - { _id: 1, name: "E-mail", value: "email" }, - { _id: 2, name: "Slack", value: "slack" }, - { _id: 3, name: "PagerDuty", value: "pager_duty" }, - { _id: 4, name: "Webhook", value: "webhook" }, - { _id: 5, name: "Discord", value: "discord" }, - { _id: 6, name: "Matrix", value: "matrix" }, + { id: 1, name: "E-mail", value: "email" }, + { id: 2, name: "Slack", value: "slack" }, + { id: 3, name: "PagerDuty", value: "pager_duty" }, + { id: 4, name: "Webhook", value: "webhook" }, + { id: 5, name: "Discord", value: "discord" }, + { id: 6, name: "Matrix", value: "matrix" }, ]; export const TITLE_MAP = { diff --git a/client/src/Pages/Settings/index.jsx b/client/src/Pages/Settings/index.jsx index 54d6d4209..621640d8a 100644 --- a/client/src/Pages/Settings/index.jsx +++ b/client/src/Pages/Settings/index.jsx @@ -157,19 +157,40 @@ const Settings = () => { }; const handleSave = () => { - // Validate - const { error } = settingsValidation.validate(settingsData.settings, { + // Validate] + + const toSubmit = { + checkTTL: settingsData.settings.checkTTL, + pagespeedApiKey: settingsData.settings.pagespeedApiKey, + language: settingsData.settings.language, + timezone: settingsData.settings.timezone, + systemEmailHost: settingsData.settings.systemEmailHost, + systemEmailPort: settingsData.settings.systemEmailPort, + systemEmailSecure: settingsData.settings.systemEmailSecure, + systemEmailPool: settingsData.settings.systemEmailPool, + systemEmailAddress: settingsData.settings.systemEmailAddress, + systemEmailPassword: settingsData.settings.systemEmailPassword, + systemEmailUser: settingsData.settings.systemEmailUser, + systemEmailConnectionHost: settingsData.settings.systemEmailConnectionHost, + systemEmailTLSServername: settingsData.settings.systemEmailTLSServername, + systemEmailIgnoreTLS: settingsData.settings.systemEmailIgnoreTLS, + systemEmailRequireTLS: settingsData.settings.systemEmailRequireTLS, + systemEmailRejectUnauthorized: settingsData.settings.systemEmailRejectUnauthorized, + showURL: settingsData.settings.showURL, + globalThresholds: settingsData.settings.globalThresholds, + }; + + const { error } = settingsValidation.validate(toSubmit, { abortEarly: false, }); if (!error || error.details.length === 0) { setErrors({}); - saveSettings(settingsData?.settings); + saveSettings(toSubmit); } else { const newErrors = {}; error.details.forEach((err) => { newErrors[err.path[0]] = err.message; }); - setErrors(newErrors); } }; diff --git a/server/src/config/controllers.ts b/server/src/config/controllers.ts index d72e4f76f..3b81d27db 100644 --- a/server/src/config/controllers.ts +++ b/server/src/config/controllers.ts @@ -37,7 +37,7 @@ export const initializeControllers = (services: InitializedServices): Initialize queueController: new QueueController(services.jobQueue), logController: new LogController(services.logger), statusPageController: new StatusPageController(services.db), - notificationController: new NotificationController(services.notificationsService, services.db, services.monitorsRepository), + notificationController: new NotificationController(services.notificationsService, services.monitorsRepository), diagnosticController: new DiagnosticController(services.diagnosticService), incidentController: new IncidentController(services.incidentService), }; diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 8cfcb400b..cdee721b2 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -59,13 +59,9 @@ import RecoveryToken from "../db/models/RecoveryToken.js"; import AppSettings from "../db/models/AppSettings.js"; import Incident from "../db/models/Incident.js"; -import InviteModule from "../db/modules/inviteModule.js"; import StatusPageModule from "../db/modules/statusPageModule.js"; -import UserModule from "../db/modules/userModule.js"; import MaintenanceWindowModule from "../db/modules/maintenanceWindowModule.js"; import NotificationModule from "../db/modules/notificationModule.js"; -import RecoveryModule from "../db/modules/recoveryModule.js"; -import SettingsModule from "../db/modules/settingsModule.js"; import IncidentModule from "../db/modules/incidentModule.js"; // repositories @@ -77,7 +73,6 @@ import { MongoUsersRepository, MongoInvitesRepository, MongoRecoveryTokensRepository, - MongoSettingsRepository, MongoNotificationsRepository, MongoIncidentRepository, MongoTeamsRepository, @@ -133,31 +128,25 @@ export const initializeServices = async ({ logger, envSettings, settingsService, + settingsRepository, }: { logger: ILogger; envSettings: EnvConfig; settingsService: any; + settingsRepository: ISettingsRepository; }): Promise => { // Create DB - const inviteModule = new InviteModule({ InviteToken, crypto }); const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, AppSettings }); - const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean }); const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow }); const notificationModule = new NotificationModule({ Notification: NotificationModel, Monitor }); - const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto }); - const settingsModule = new SettingsModule({ AppSettings }); const incidentModule = new IncidentModule({ logger, Incident, Monitor, User }); const db = new MongoDB({ logger, envSettings, - inviteModule, statusPageModule, - userModule, maintenanceWindowModule, notificationModule, - recoveryModule, - settingsModule, incidentModule, }); @@ -171,7 +160,6 @@ export const initializeServices = async ({ const usersRepository = new MongoUsersRepository(); const invitesRepository = new MongoInvitesRepository(); const recoveryTokensRepository = new MongoRecoveryTokensRepository(); - const settingsRepository = new MongoSettingsRepository(); const notificationsRepository = new MongoNotificationsRepository(); const incidentsRepository = new MongoIncidentRepository(); const teamsRepository = new MongoTeamsRepository(); @@ -219,6 +207,7 @@ export const initializeServices = async ({ const notificationsService = new NotificationsService( notificationsRepository, + monitorsRepository, webhookProvider, emailProvider, slackProvider, diff --git a/server/src/controllers/notificationController.ts b/server/src/controllers/notificationController.ts index 1569f0986..1aa10a82c 100644 --- a/server/src/controllers/notificationController.ts +++ b/server/src/controllers/notificationController.ts @@ -5,17 +5,16 @@ import { createNotificationBodyValidation } from "@/validation/joi.js"; import { AppError } from "@/utils/AppError.js"; import { IMonitorsRepository } from "@/repositories/index.js"; import { INotificationsService } from "@/service/index.js"; +import { requireTeamId } from "./controllerUtils.js"; const SERVICE_NAME = "NotificationController"; class NotificationController { static SERVICE_NAME = SERVICE_NAME; - private db: any; private notificationsService: INotificationsService; private monitorsRepository: IMonitorsRepository; - constructor(notificationsService: INotificationsService, db: any, monitorsRepository: IMonitorsRepository) { + constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) { this.notificationsService = notificationsService; - this.db = db; this.monitorsRepository = monitorsRepository; } @@ -61,7 +60,7 @@ class NotificationController { body.userId = userId; body.teamId = teamId; - const notification = await this.db.notificationModule.createNotification(body); + const notification = await this.notificationsService.createNotification(body); return res.status(200).json({ success: true, msg: "Notification created successfully", @@ -79,7 +78,7 @@ class NotificationController { throw new AppError({ message: "Team ID is required", status: 400 }); } - const notifications = await this.db.notificationModule.getNotificationsByTeamId(teamId); + const notifications = await this.notificationsService.findNotificationsByTeamId(teamId); return res.status(200).json({ success: true, @@ -98,12 +97,12 @@ class NotificationController { throw new AppError({ message: "Team ID is required", status: 400 }); } - const notification = await this.db.notificationModule.getNotificationById(req.params.id); - if (!notification.teamId.equals(teamId)) { - throw new AppError({ message: "Unauthorized", status: 403 }); + const notificationId = req.params.id; + if (!notificationId) { + throw new AppError({ message: "Notification ID is required", status: 400 }); } - await this.db.notificationModule.deleteNotificationById(req.params.id); + await this.notificationsService.deleteById(notificationId, teamId); return res.status(200).json({ success: true, msg: "Notification deleted successfully", @@ -115,16 +114,14 @@ class NotificationController { getNotificationById = async (req: Request, res: Response, next: NextFunction) => { try { - const notification = await this.db.notificationModule.getNotificationById(req.params.id); - - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); + const teamId = requireTeamId(req.user?.teamId); + const notificationId = req.params.id; + if (!notificationId) { + throw new AppError({ message: "Notification ID is required", status: 400 }); } - if (!notification.teamId.equals(teamId)) { - throw new AppError({ message: "Unauthorized", status: 403 }); - } + const notification = await this.notificationsService.findById(notificationId, teamId); + return res.status(200).json({ success: true, msg: "Notification fetched successfully", @@ -141,18 +138,12 @@ class NotificationController { abortEarly: false, }); - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); + const teamId = requireTeamId(req.user?.teamId); + const notificationId = req.params.id; + if (!notificationId) { + throw new AppError({ message: "Notification ID is required", status: 400 }); } - - const notification = await this.db.notificationModule.getNotificationById(req.params.id); - - if (!notification.teamId.equals(teamId)) { - throw new AppError({ message: "Unauthorized", status: 403 }); - } - - const editedNotification = await this.db.notificationModule.editNotification(req.params.id, req.body); + const editedNotification = await this.notificationsService.updateById(notificationId, teamId, req.body); return res.status(200).json({ success: true, msg: "Notification updated successfully", diff --git a/server/src/controllers/settingsController.ts b/server/src/controllers/settingsController.ts index 5d3f6ed0e..0a01daa0c 100644 --- a/server/src/controllers/settingsController.ts +++ b/server/src/controllers/settingsController.ts @@ -22,7 +22,7 @@ class SettingsController { buildAppSettings = (dbSettings: any) => { const sanitizedSettings = { ...dbSettings }; delete sanitizedSettings.version; - + delete sanitizedSettings.jwtSecret; const returnSettings = { pagespeedKeySet: false, emailPasswordSet: false, @@ -59,7 +59,7 @@ class SettingsController { updateAppSettings = async (req: Request, res: Response, next: NextFunction) => { await updateAppSettingsBodyValidation.validateAsync(req.body); - const updatedSettings = await this.db.settingsModule.updateAppSettings(req.body); + const updatedSettings = await this.settingsService.updateDbSettings(req.body); const returnSettings = this.buildAppSettings(updatedSettings); return res.status(200).json({ success: true, diff --git a/server/src/db/MongoDB.js b/server/src/db/MongoDB.js index a0c580f1d..eacd2383e 100755 --- a/server/src/db/MongoDB.js +++ b/server/src/db/MongoDB.js @@ -4,26 +4,11 @@ import { runMigrations } from "./migration/index.js"; class MongoDB { static SERVICE_NAME = "MongoDB"; - constructor({ - logger, - envSettings, - inviteModule, - statusPageModule, - userModule, - maintenanceWindowModule, - notificationModule, - recoveryModule, - settingsModule, - incidentModule, - }) { + constructor({ logger, envSettings, statusPageModule, maintenanceWindowModule, notificationModule, incidentModule }) { this.logger = logger; this.envSettings = envSettings; - this.userModule = userModule; - this.inviteModule = inviteModule; - this.recoveryModule = recoveryModule; this.maintenanceWindowModule = maintenanceWindowModule; this.notificationModule = notificationModule; - this.settingsModule = settingsModule; this.statusPageModule = statusPageModule; this.incidentModule = incidentModule; } diff --git a/server/src/db/modules/inviteModule.js b/server/src/db/modules/inviteModule.js deleted file mode 100755 index e3438dc3b..000000000 --- a/server/src/db/modules/inviteModule.js +++ /dev/null @@ -1,57 +0,0 @@ -import { AppError } from "@/utils/AppError.js"; - -const SERVICE_NAME = "inviteModule"; - -class InviteModule { - constructor({ InviteToken, crypto }) { - this.InviteToken = InviteToken; - this.crypto = crypto; - } - - requestInviteToken = async (userData) => { - try { - await this.InviteToken.deleteMany({ email: userData.email }); - userData.token = this.crypto.randomBytes(32).toString("hex"); - let inviteToken = new this.InviteToken(userData); - await inviteToken.save(); - return inviteToken; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "requestInviteToken"; - throw error; - } - }; - - getInviteToken = async (token) => { - try { - const invite = await this.InviteToken.findOne({ - token, - }); - if (invite === null) { - throw new AppError({ message: "Invite token not found", status: 404 }); - } - return invite; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getInviteToken"; - throw error; - } - }; - getInviteTokenAndDelete = async (token) => { - try { - const invite = await this.InviteToken.findOneAndDelete({ - token, - }); - if (invite === null) { - throw new Error("Invite not found"); - } - return invite; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getInviteTokenAndDelete"; - throw error; - } - }; -} - -export default InviteModule; diff --git a/server/src/db/modules/recoveryModule.js b/server/src/db/modules/recoveryModule.js deleted file mode 100755 index b2f9296a6..000000000 --- a/server/src/db/modules/recoveryModule.js +++ /dev/null @@ -1,79 +0,0 @@ -const SERVICE_NAME = "recoveryModule"; - -class RecoveryModule { - constructor({ User, RecoveryToken, crypto }) { - this.User = User; - this.RecoveryToken = RecoveryToken; - this.crypto = crypto; - } - - requestRecoveryToken = async (email) => { - try { - // Delete any existing tokens - await this.RecoveryToken.deleteMany({ email }); - let recoveryToken = new this.RecoveryToken({ - email, - token: this.crypto.randomBytes(32).toString("hex"), - }); - await recoveryToken.save(); - return recoveryToken; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "requestRecoveryToken"; - throw error; - } - }; - validateRecoveryToken = async (candidateToken) => { - try { - const recoveryToken = await this.RecoveryToken.findOne({ - token: candidateToken, - }); - if (recoveryToken !== null) { - return recoveryToken; - } else { - throw new Error("Recovery token not found"); - } - } catch (error) { - error.service = SERVICE_NAME; - error.method = "validateRecoveryToken"; - throw error; - } - }; - - resetPassword = async (password, candidateToken) => { - try { - const newPassword = password; - - // Validate token again - const recoveryToken = await this.validateRecoveryToken(candidateToken); - const user = await this.User.findOne({ email: recoveryToken.email }); - - if (user === null) { - throw new Error("User not found"); - } - - const match = await user.comparePassword(newPassword); - - if (match === true) { - throw new Error("Password cannot be the same as the old password"); - } - - user.password = newPassword; - await user.save(); - await this.RecoveryToken.deleteMany({ email: recoveryToken.email }); - // Fetch the user again without the password - const userWithoutPassword = await this.User.findOne({ - email: recoveryToken.email, - }) - .select("-password") - .select("-profileImage"); - return userWithoutPassword; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "resetPassword"; - throw error; - } - }; -} - -export default RecoveryModule; diff --git a/server/src/db/modules/settingsModule.js b/server/src/db/modules/settingsModule.js deleted file mode 100755 index 37b851bf2..000000000 --- a/server/src/db/modules/settingsModule.js +++ /dev/null @@ -1,36 +0,0 @@ -// import AppSettings from "../../models/AppSettings.js"; -const SERVICE_NAME = "SettingsModule"; - -class SettingsModule { - constructor({ AppSettings }) { - this.AppSettings = AppSettings; - } - - updateAppSettings = async (newSettings) => { - try { - const update = { $set: { ...newSettings } }; - - if (newSettings.pagespeedApiKey === "") { - update.$unset = { pagespeedApiKey: "" }; - delete update.$set.pagespeedApiKey; - } - - if (newSettings.systemEmailPassword === "") { - update.$unset = { systemEmailPassword: "" }; - delete update.$set.systemEmailPassword; - } - - await this.AppSettings.findOneAndUpdate({}, update, { - upsert: true, - }); - const settings = await this.AppSettings.findOne().select("-__v -_id -createdAt -updatedAt -singleton").lean(); - return settings; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "updateAppSettings"; - throw error; - } - }; -} - -export default SettingsModule; diff --git a/server/src/db/modules/userModule.js b/server/src/db/modules/userModule.js deleted file mode 100755 index 0051702b6..000000000 --- a/server/src/db/modules/userModule.js +++ /dev/null @@ -1,170 +0,0 @@ -const SERVICE_NAME = "userModule"; -const DUPLICATE_KEY_CODE = 11000; // MongoDB error code for duplicate key - -class UserModule { - constructor({ User, Team, GenerateAvatarImage, ParseBoolean }) { - this.User = User; - this.Team = Team; - this.GenerateAvatarImage = GenerateAvatarImage; - this.ParseBoolean = ParseBoolean; - } - - checkSuperadmin = async () => { - try { - const superAdmin = await this.User.findOne({ role: "superadmin" }); - if (superAdmin !== null) { - return true; - } - return false; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "checkSuperadmin"; - throw error; - } - }; - - insertUser = async (userData, imageFile) => { - try { - if (imageFile) { - // 1. Save the full size image - userData.profileImage = { - data: imageFile.buffer, - contentType: imageFile.mimetype, - }; - - // 2. Get the avatar sized image - const avatar = await this.GenerateAvatarImage(imageFile); - userData.avatarImage = avatar; - } - - // Handle creating team if superadmin - if (userData.role.includes("superadmin")) { - const team = new this.Team({ - email: userData.email, - }); - userData.teamId = team._id; - userData.checkTTL = 60 * 60 * 24 * 30; - await team.save(); - } - - const newUser = new this.User(userData); - await newUser.save(); - return await this.User.findOne({ _id: newUser._id }).select("-password").select("-profileImage"); // .select() doesn't work with create, need to save then find - } catch (error) { - if (error.code === DUPLICATE_KEY_CODE) { - error.message = "User already exists"; - } - error.service = SERVICE_NAME; - error.method = "insertUser"; - throw error; - } - }; - getUserByEmail = async (email) => { - try { - // Need the password to be able to compare, removed .select() - // We can strip the hash before returning the user - const user = await this.User.findOne({ email: email }).select("-profileImage"); - if (!user) { - throw new Error("User not found"); - } - return user; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getUserByEmail"; - throw error; - } - }; - - updateUser = async ({ userId, user, file }) => { - if (!userId) { - throw new Error("No user in request"); - } - - try { - const candidateUser = { ...user }; - - if (this.ParseBoolean(candidateUser.deleteProfileImage) === true) { - candidateUser.profileImage = null; - candidateUser.avatarImage = null; - } else if (file) { - // 1. Save the full size image - candidateUser.profileImage = { - data: file.buffer, - contentType: file.mimetype, - }; - - // 2. Get the avatar sized image - const avatar = await this.GenerateAvatarImage(file); - candidateUser.avatarImage = avatar; - } - - const updatedUser = await this.User.findByIdAndUpdate( - userId, - candidateUser, - { new: true } // Returns updated user instead of pre-update user - ) - .select("-password") - .select("-profileImage"); - return updatedUser; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "updateUser"; - throw error; - } - }; - deleteUser = async (userId) => { - try { - const deletedUser = await this.User.findByIdAndDelete(userId); - if (!deletedUser) { - throw new Error("User not found"); - } - return deletedUser; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteUser"; - throw error; - } - }; - - getAllUsers = async () => { - try { - const users = await this.User.find().select("-password").select("-profileImage"); - return users; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getAllUsers"; - throw error; - } - }; - - getUserById = async (roles, userId) => { - try { - if (!roles.includes("superadmin")) { - throw new Error("User is not a superadmin"); - } - - const user = await this.User.findById(userId).select("-password").select("-profileImage"); - if (!user) { - throw new Error("User not found"); - } - - return user; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getUserById"; - throw error; - } - }; - - editUserById = async (userId, user) => { - try { - await this.User.findByIdAndUpdate(userId, user, { new: true }).select("-password").select("-profileImage"); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "editUserById"; - throw error; - } - }; -} - -export default UserModule; diff --git a/server/src/index.ts b/server/src/index.ts index b74404228..ca6cea2ba 100755 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -9,7 +9,7 @@ import { runMigrations } from "./db/migration/index.js"; import Logger, { ILogger } from "@/utils/logger.js"; import SettingsService from "@/service/system/settingsService.js"; -import AppSettings from "./db/models/AppSettings.js"; +import { MongoSettingsRepository } from "./repositories/index.js"; const SERVICE_NAME = "Server"; let logger: ILogger; @@ -22,14 +22,16 @@ const startApp = async () => { const frontendPath = path.join(__dirname, "..", "public"); // Create services - const settingsService = new SettingsService(AppSettings); + const settingsRepository = new MongoSettingsRepository(); + const settingsService = new SettingsService(settingsRepository); + const envSettings = settingsService.loadSettings(); // Create logger logger = new Logger({ envSettings }); // Initialize services - const services = await initializeServices({ logger, envSettings, settingsService }); + const services = await initializeServices({ logger, envSettings, settingsService, settingsRepository }); await runMigrations(logger); diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index f29cf9d15..8ead5a688 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -39,4 +39,5 @@ export interface IMonitorsRepository { // other findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise; findGroupsByTeamId(teamId: string): Promise; + removeNotificationFromMonitors(notificationId: string): Promise; } diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 17b9ce243..ad7e8577d 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -189,6 +189,10 @@ class MongoMonitorsRepository implements IMonitorsRepository { return groups.sort(); }; + removeNotificationFromMonitors = async (notificationId: string): Promise => { + await MonitorModel.updateMany({ notifications: notificationId }, { $pull: { notifications: notificationId } }); + }; + private mapDocuments = (documents: MonitorDocument[]): Monitor[] => { if (!documents?.length) { return []; diff --git a/server/src/repositories/notifications/INotificationsRepository.ts b/server/src/repositories/notifications/INotificationsRepository.ts index 8bb65769a..0236c3a89 100644 --- a/server/src/repositories/notifications/INotificationsRepository.ts +++ b/server/src/repositories/notifications/INotificationsRepository.ts @@ -1,8 +1,13 @@ import type { Notification } from "@/types/index.js"; export interface INotificationsRepository { // create + create(notificationData: Partial): Promise; // fetch + findById(id: string, teamId: string): Promise; findNotificationsByIds(ids: string[]): Promise; + findByTeamId(teamId: string): Promise; // update + updateById(id: string, teamId: string, updateData: Partial): Promise; // delete + deleteById(id: string, teamId: string): Promise; } diff --git a/server/src/repositories/notifications/MongoNotificationsRepository.ts b/server/src/repositories/notifications/MongoNotificationsRepository.ts index 01a514f1c..730072d8c 100644 --- a/server/src/repositories/notifications/MongoNotificationsRepository.ts +++ b/server/src/repositories/notifications/MongoNotificationsRepository.ts @@ -2,6 +2,8 @@ import mongoose from "mongoose"; import { NotificationModel, type NotificationDocument } from "@/db/models/index.js"; import { INotificationsRepository } from "@/repositories/index.js"; import type { Notification } from "@/types/index.js"; +import { AppError } from "@/utils/AppError.js"; +import { not } from "joi"; class MongoNotificationsRepository implements INotificationsRepository { private mapDocuments = (documents: NotificationDocument[]): Notification[] => { @@ -36,11 +38,61 @@ class MongoNotificationsRepository implements INotificationsRepository { }; }; + create = async (notificationData: Partial) => { + const notification = await NotificationModel.create({ ...notificationData }); + if (!notification) { + throw new AppError({ message: "Failed to create notification", status: 500 }); + } + return this.toEntity(notification); + }; + + findById = async (id: string, teamId: string): Promise => { + const notification = await NotificationModel.findOne({ + _id: new mongoose.Types.ObjectId(id), + teamId: new mongoose.Types.ObjectId(teamId), + }); + if (!notification) { + throw new AppError({ message: "Notification not found", status: 404 }); + } + return this.toEntity(notification); + }; + findNotificationsByIds = async (ids: string[]) => { const mongoIds = ids.map((id) => new mongoose.Types.ObjectId(id)); const documents = await NotificationModel.find({ _id: { $in: mongoIds } }); return this.mapDocuments(documents); }; + + findByTeamId = async (teamId: string): Promise => { + const documents = await NotificationModel.find({ teamId }); + return this.mapDocuments(documents); + }; + + updateById = async (id: string, teamId: string, patch: Partial): Promise => { + const notification = await NotificationModel.findOneAndUpdate( + { + _id: new mongoose.Types.ObjectId(id), + teamId: new mongoose.Types.ObjectId(teamId), + }, + { $set: patch }, + { new: true, runValidators: true } + ); + if (!notification) { + throw new AppError({ message: "Notification not found or could not be updated", status: 404 }); + } + return this.toEntity(notification); + }; + + deleteById = async (id: string, teamId: string): Promise => { + const deleted = await NotificationModel.findOneAndDelete({ + _id: new mongoose.Types.ObjectId(id), + teamId: new mongoose.Types.ObjectId(teamId), + }); + if (!deleted) { + throw new AppError({ message: "Notification not found or could not be deleted", status: 404 }); + } + return this.toEntity(deleted); + }; } export default MongoNotificationsRepository; diff --git a/server/src/repositories/settings/ISettingsRepository.ts b/server/src/repositories/settings/ISettingsRepository.ts index 111a92a55..578d7f40d 100644 --- a/server/src/repositories/settings/ISettingsRepository.ts +++ b/server/src/repositories/settings/ISettingsRepository.ts @@ -1,8 +1,11 @@ import type { Settings } from "@/types/index.js"; export interface ISettingsRepository { // create + create(settings: Partial): Promise; // fetch + findSingleton(): Promise; // update update(settings: Partial): Promise; // delete + deleteLegacy: () => Promise; } diff --git a/server/src/repositories/settings/MongoSettingsRepository.ts b/server/src/repositories/settings/MongoSettingsRepository.ts index 2cc700525..064993fa3 100644 --- a/server/src/repositories/settings/MongoSettingsRepository.ts +++ b/server/src/repositories/settings/MongoSettingsRepository.ts @@ -1,7 +1,8 @@ import mongoose from "mongoose"; import { ISettingsRepository } from "@/repositories/settings/ISettingsRepository.js"; import type { Settings } from "@/types/index.js"; -import { AppSettingsModel, type AppSettingsDocument } from "@/db/models/AppSettings.js"; +import { AppSettingsModel, type AppSettingsDocument } from "@/db/models/index.js"; +import { SERVFAIL } from "dns"; class MongoSettingsRepository implements ISettingsRepository { private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { @@ -46,6 +47,19 @@ class MongoSettingsRepository implements ISettingsRepository { }; }; + create = async (settings: Partial) => { + const newSettings = await AppSettingsModel.create(settings); + return this.toEntity(newSettings); + }; + + findSingleton = async () => { + let settings = await AppSettingsModel.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean(); + if (!settings) { + return null; + } + return this.toEntity(settings); + }; + update = async (settings: Partial) => { const update: Record = { $set: { ...settings } }; @@ -71,6 +85,11 @@ class MongoSettingsRepository implements ISettingsRepository { return this.toEntity(updatedSettings); }; + + deleteLegacy = async () => { + const res = await AppSettingsModel.deleteMany({ version: { $exists: false } }); + return res.deletedCount > 0; + }; } export default MongoSettingsRepository; diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index 7ca52945e..4e0c9d842 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -1,14 +1,20 @@ import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification } from "@/types/index.js"; import { shouldSendHardwareAlert } from "@/service/infrastructure/notificationProviders/utils.js"; -import { INotificationsRepository } from "@/repositories/index.js"; +import { IMonitorsRepository, INotificationsRepository } from "@/repositories/index.js"; import { INotificationProvider } from "./notificationProviders/INotificationProvider.js"; export interface INotificationsService { + createNotification: (notificationData: Partial) => Promise; + findById: (id: string, teamId: string) => Promise; + findNotificationsByTeamId: (teamId: string) => Promise; + updateById(id: string, teamId: string, updateData: Partial): Promise; + deleteById: (id: string, teamId: string) => Promise; handleNotifications: ( monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, prevStatus: boolean | undefined, statusChanged: boolean ) => Promise; + sendTestNotification: (notification: Notification) => Promise; testAllNotifications: (notificationIds: string[]) => Promise; } @@ -19,6 +25,7 @@ export class NotificationsService implements INotificationsService { static SERVICE_NAME = SERVICE_NAME; private notificationsRepository: INotificationsRepository; + private monitorsRepository: IMonitorsRepository; private webhookProvider: INotificationProvider; private emailProvider: INotificationProvider; private slackProvider: INotificationProvider; @@ -29,6 +36,7 @@ export class NotificationsService implements INotificationsService { constructor( notificationsRepository: INotificationsRepository, + monitorsRepository: IMonitorsRepository, webhookProvider: INotificationProvider, emailProvider: INotificationProvider, slackProvider: INotificationProvider, @@ -38,6 +46,7 @@ export class NotificationsService implements INotificationsService { logger: any ) { this.notificationsRepository = notificationsRepository; + this.monitorsRepository = monitorsRepository; this.webhookProvider = webhookProvider; this.emailProvider = emailProvider; this.slackProvider = slackProvider; @@ -147,4 +156,26 @@ export class NotificationsService implements INotificationsService { } return true; }; + + createNotification = async (notificationData: Partial): Promise => { + return await this.notificationsRepository.create(notificationData); + }; + + findById = async (id: string, teamId: string): Promise => { + return await this.notificationsRepository.findById(id, teamId); + }; + + findNotificationsByTeamId = async (teamId: string): Promise => { + return await this.notificationsRepository.findByTeamId(teamId); + }; + + updateById = async (id: string, teamId: string, updateData: Partial): Promise => { + return await this.notificationsRepository.updateById(id, teamId, updateData); + }; + + deleteById = async (id: string, teamId: string): Promise => { + const deleted = await this.notificationsRepository.deleteById(id, teamId); + await this.monitorsRepository.removeNotificationFromMonitors(id); + return deleted; + }; } diff --git a/server/src/service/system/settingsService.ts b/server/src/service/system/settingsService.ts index 15f86995d..a0db6a140 100755 --- a/server/src/service/system/settingsService.ts +++ b/server/src/service/system/settingsService.ts @@ -1,3 +1,7 @@ +import { ISettingsRepository } from "@/repositories/index.js"; +import { Settings } from "@/types/index.js"; +import { AppError } from "@/utils/AppError.js"; + const SERVICE_NAME = "SettingsService"; export type EnvConfig = { @@ -26,16 +30,16 @@ export interface ISettingsService { readonly serviceName: string; loadSettings(): EnvConfig; getSettings(): EnvConfig; - getDBSettings(): Promise>; + getDBSettings(): Promise; } class SettingsService implements ISettingsService { static SERVICE_NAME = "SettingsService"; - private AppSettings: any; private settings: EnvConfig; + private settingsRepository: ISettingsRepository; - constructor(AppSettings: any) { - this.AppSettings = AppSettings; + constructor(settingsRepository: ISettingsRepository) { + this.settingsRepository = settingsRepository; this.settings = { ...envConfig }; } @@ -54,18 +58,26 @@ class SettingsService implements ISettingsService { return this.settings; } - async getDBSettings() { - // Remove any old settings - await this.AppSettings.deleteMany({ version: { $exists: false } }); + updateDbSettings = async (newSettings: Partial) => { + return await this.settingsRepository.update(newSettings); + }; - let settings = await this.AppSettings.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean(); + getDBSettings = async () => { + // Remove any old settings + await this.settingsRepository.deleteLegacy(); + + let settings = await this.settingsRepository.findSingleton(); if (settings === null) { - await this.AppSettings.create({}); - settings = await this.AppSettings.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean(); + await this.settingsRepository.create({}); + settings = await this.settingsRepository.findSingleton(); + } + + if (!settings) { + throw new AppError({ message: "Settings not found", status: 500 }); } return settings; - } + }; } export default SettingsService;