diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 8cfcb400b..664194d82 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(); 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/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/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;