remove settingsModule

This commit is contained in:
Alex Holliday
2026-01-21 13:01:31 -08:00
parent b0da1f4259
commit fdc3983328
11 changed files with 56 additions and 389 deletions
+2 -14
View File
@@ -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<InitializedServices> => {
// 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();
+2 -2
View File
@@ -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,
+1 -16
View File
@@ -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;
}
-57
View File
@@ -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;
-79
View File
@@ -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;
-36
View File
@@ -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;
-170
View File
@@ -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;
+5 -3
View File
@@ -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);
@@ -1,8 +1,11 @@
import type { Settings } from "@/types/index.js";
export interface ISettingsRepository {
// create
create(settings: Partial<Settings>): Promise<Settings>;
// fetch
findSingleton(): Promise<Settings | null>;
// update
update(settings: Partial<Settings>): Promise<Settings>;
// delete
deleteLegacy: () => Promise<boolean>;
}
@@ -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<Settings>) => {
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<Settings>) => {
const update: Record<string, any> = { $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;
+23 -11
View File
@@ -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<Record<string, any>>;
getDBSettings(): Promise<Settings>;
}
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<Settings>) => {
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;