diff --git a/server/db/mongo/modules/inviteModule.js b/server/db/mongo/modules/inviteModule.js index f5c960697..89d8ad8d7 100755 --- a/server/db/mongo/modules/inviteModule.js +++ b/server/db/mongo/modules/inviteModule.js @@ -1,7 +1,7 @@ import InviteToken from "../../models/InviteToken.js"; import crypto from "crypto"; -import ServiceRegistry from "../../../service/serviceRegistry.js"; -import StringService from "../../../service/stringService.js"; +import ServiceRegistry from "../../../service/system/serviceRegistry.js"; +import StringService from "../../../service/system/stringService.js"; const SERVICE_NAME = "inviteModule"; /** diff --git a/server/db/mongo/modules/monitorModule.js b/server/db/mongo/modules/monitorModule.js index 3192b711c..18cb3c63f 100755 --- a/server/db/mongo/modules/monitorModule.js +++ b/server/db/mongo/modules/monitorModule.js @@ -4,8 +4,8 @@ import Check from "../../models/Check.js"; import PageSpeedCheck from "../../models/PageSpeedCheck.js"; import HardwareCheck from "../../models/HardwareCheck.js"; import { NormalizeData, NormalizeDataUptimeDetails } from "../../../utils/dataUtils.js"; -import ServiceRegistry from "../../../service/serviceRegistry.js"; -import StringService from "../../../service/stringService.js"; +import ServiceRegistry from "../../../service/system/serviceRegistry.js"; +import StringService from "../../../service/system/stringService.js"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; diff --git a/server/db/mongo/modules/recoveryModule.js b/server/db/mongo/modules/recoveryModule.js index 3ea18cd8a..2678f49c4 100755 --- a/server/db/mongo/modules/recoveryModule.js +++ b/server/db/mongo/modules/recoveryModule.js @@ -1,7 +1,8 @@ import UserModel from "../../models/User.js"; import RecoveryToken from "../../models/RecoveryToken.js"; -import serviceRegistry from "../../../service/serviceRegistry.js"; -import StringService from "../../../service/stringService.js"; +import crypto from "crypto"; +import serviceRegistry from "../../../service/system/serviceRegistry.js"; +import StringService from "../../../service/system/stringService.js"; const SERVICE_NAME = "recoveryModule"; @@ -53,7 +54,7 @@ const resetPassword = async (password, candidateToken) => { const newPassword = password; // Validate token again - const recoveryToken = await validateRecoveryToken(req, res); + const recoveryToken = await validateRecoveryToken(candidateToken); const user = await UserModel.findOne({ email: recoveryToken.email }); if (user === null) { diff --git a/server/db/mongo/modules/statusPageModule.js b/server/db/mongo/modules/statusPageModule.js index c126d913f..757fdd266 100755 --- a/server/db/mongo/modules/statusPageModule.js +++ b/server/db/mongo/modules/statusPageModule.js @@ -1,7 +1,7 @@ import StatusPage from "../../models/StatusPage.js"; import { NormalizeData } from "../../../utils/dataUtils.js"; -import ServiceRegistry from "../../../service/serviceRegistry.js"; -import StringService from "../../../service/stringService.js"; +import ServiceRegistry from "../../../service/system/serviceRegistry.js"; +import StringService from "../../../service/system/stringService.js"; const SERVICE_NAME = "statusPageModule"; diff --git a/server/db/mongo/modules/userModule.js b/server/db/mongo/modules/userModule.js index a1f056157..974f2d2ed 100755 --- a/server/db/mongo/modules/userModule.js +++ b/server/db/mongo/modules/userModule.js @@ -4,8 +4,8 @@ import { GenerateAvatarImage } from "../../../utils/imageProcessing.js"; const DUPLICATE_KEY_CODE = 11000; // MongoDB error code for duplicate key import { ParseBoolean } from "../../../utils/utils.js"; -import ServiceRegistry from "../../../service/serviceRegistry.js"; -import StringService from "../../../service/stringService.js"; +import ServiceRegistry from "../../../service/system/serviceRegistry.js"; +import StringService from "../../../service/system/stringService.js"; const SERVICE_NAME = "userModule"; const checkSuperadmin = async () => { diff --git a/server/index.js b/server/index.js index e20858208..996eae4d1 100755 --- a/server/index.js +++ b/server/index.js @@ -47,58 +47,58 @@ import DiagnosticRoutes from "./routes/diagnosticRoute.js"; import DiagnosticController from "./controllers/diagnosticController.js"; //JobQueue service and dependencies -import JobQueue from "./service/JobQueue/JobQueue.js"; -import JobQueueHelper from "./service/JobQueue/JobQueueHelper.js"; +import JobQueue from "./service/infrastructure/JobQueue/JobQueue.js"; +import JobQueueHelper from "./service/infrastructure/JobQueue/JobQueueHelper.js"; import { Queue, Worker } from "bullmq"; -import PulseQueue from "./service/PulseQueue/PulseQueue.js"; -import PulseQueueHelper from "./service/PulseQueue/PulseQueueHelper.js"; +import PulseQueue from "./service/infrastructure/PulseQueue/PulseQueue.js"; +import PulseQueueHelper from "./service/infrastructure/PulseQueue/PulseQueueHelper.js"; -import SuperSimpleQueue from "./service/SuperSimpleQueue/SuperSimpleQueue.js"; -import SuperSimpleQueueHelper from "./service/SuperSimpleQueue/SuperSimpleQueueHelper.js"; +import SuperSimpleQueue from "./service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; +import SuperSimpleQueueHelper from "./service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; -import UserService from "./service/userService.js"; +import UserService from "./service/business/userService.js"; //Network service and dependencies -import NetworkService from "./service/networkService.js"; +import NetworkService from "./service/infrastructure/networkService.js"; import axios from "axios"; import ping from "ping"; import http from "http"; import Docker from "dockerode"; import net from "net"; // Email service and dependencies -import EmailService from "./service/emailService.js"; +import EmailService from "./service/infrastructure/emailService.js"; import nodemailer from "nodemailer"; import pkg from "handlebars"; const { compile } = pkg; import mjml2html from "mjml"; // Settings Service and dependencies -import SettingsService from "./service/settingsService.js"; +import SettingsService from "./service/system/settingsService.js"; import AppSettings from "./db/models/AppSettings.js"; // Status Service and dependencies -import StatusService from "./service/statusService.js"; +import StatusService from "./service/infrastructure/statusService.js"; // Notification Service and dependencies -import NotificationService from "./service/notificationService.js"; -import NotificationUtils from "./service/notificationUtils.js"; +import NotificationService from "./service/infrastructure/notificationService.js"; +import NotificationUtils from "./service/infrastructure/notificationUtils.js"; // Buffer Service and dependencies -import BufferService from "./service/bufferService.js"; +import BufferService from "./service/infrastructure/bufferService.js"; // Service Registry -import ServiceRegistry from "./service/serviceRegistry.js"; +import ServiceRegistry from "./service/system/serviceRegistry.js"; import MongoDB from "./db/mongo/MongoDB.js"; // Redis Service and dependencies import IORedis from "ioredis"; -import RedisService from "./service/redisService.js"; +import RedisService from "./service/data/redisService.js"; -import TranslationService from "./service/translationService.js"; +import TranslationService from "./service/system/translationService.js"; import languageMiddleware from "./middleware/languageMiddleware.js"; -import StringService from "./service/stringService.js"; +import StringService from "./service/system/stringService.js"; const SERVICE_NAME = "Server"; const SHUTDOWN_TIMEOUT = 1000; diff --git a/server/middleware/handleErrors.js b/server/middleware/handleErrors.js index f04df1a08..1efce6f67 100755 --- a/server/middleware/handleErrors.js +++ b/server/middleware/handleErrors.js @@ -1,6 +1,6 @@ import logger from "../utils/logger.js"; -import ServiceRegistry from "../service/serviceRegistry.js"; -import StringService from "../service/stringService.js"; +import ServiceRegistry from "../service/system/serviceRegistry.js"; +import StringService from "../service/system/stringService.js"; const handleErrors = (error, req, res, next) => { console.log("ERROR", error); diff --git a/server/middleware/isAllowed.js b/server/middleware/isAllowed.js index 27fb875ed..5d61974be 100755 --- a/server/middleware/isAllowed.js +++ b/server/middleware/isAllowed.js @@ -1,9 +1,9 @@ import jwt from "jsonwebtoken"; const TOKEN_PREFIX = "Bearer "; const SERVICE_NAME = "allowedRoles"; -import ServiceRegistry from "../service/serviceRegistry.js"; -import StringService from "../service/stringService.js"; -import SettingsService from "../service/settingsService.js"; +import ServiceRegistry from "../service/system/serviceRegistry.js"; +import StringService from "../service/system/stringService.js"; +import SettingsService from "../service/system/settingsService.js"; const isAllowed = (allowedRoles) => { return (req, res, next) => { diff --git a/server/middleware/verifyJWT.js b/server/middleware/verifyJWT.js index 46611c0dd..d01db0a97 100755 --- a/server/middleware/verifyJWT.js +++ b/server/middleware/verifyJWT.js @@ -1,7 +1,7 @@ import jwt from "jsonwebtoken"; -import ServiceRegistry from "../service/serviceRegistry.js"; -import SettingsService from "../service/settingsService.js"; -import StringService from "../service/stringService.js"; +import ServiceRegistry from "../service/system/serviceRegistry.js"; +import SettingsService from "../service/system/settingsService.js"; +import StringService from "../service/system/stringService.js"; const SERVICE_NAME = "verifyJWT"; const TOKEN_PREFIX = "Bearer "; diff --git a/server/middleware/verifyOwnership.js b/server/middleware/verifyOwnership.js index 2040d54ee..890c40b24 100755 --- a/server/middleware/verifyOwnership.js +++ b/server/middleware/verifyOwnership.js @@ -1,6 +1,6 @@ import logger from "../utils/logger.js"; -import ServiceRegistry from "../service/serviceRegistry.js"; -import StringService from "../service/stringService.js"; +import ServiceRegistry from "../service/system/serviceRegistry.js"; +import StringService from "../service/system/stringService.js"; import { ObjectId } from "mongodb"; const SERVICE_NAME = "verifyOwnership"; diff --git a/server/service/business/userService.js b/server/service/business/userService.js new file mode 100644 index 000000000..90906552d --- /dev/null +++ b/server/service/business/userService.js @@ -0,0 +1,208 @@ +const SERVICE_NAME = "userService"; +import { createAuthError, createError } from "../../utils/errorUtils.js"; + +class UserService { + static SERVICE_NAME = SERVICE_NAME; + constructor({ db, emailService, settingsService, logger, stringService, jwt }) { + this.db = db; + this.emailService = emailService; + this.settingsService = settingsService; + this.logger = logger; + this.stringService = stringService; + this.jwt = jwt; + } + + issueToken = (payload, appSettings) => { + const tokenTTL = appSettings?.jwtTTL ?? "2h"; + const tokenSecret = appSettings?.jwtSecret; + const payloadData = payload; + return this.jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL }); + }; + + registerUser = async (user, file) => { + // Create a new user + // If superAdmin exists, a token should be attached to all further register requests + const superAdminExists = await this.db.checkSuperadmin(); + if (superAdminExists) { + const invitedUser = await this.db.getInviteTokenAndDelete(user.inviteToken); + user.role = invitedUser.role; + user.teamId = invitedUser.teamId; + } else { + // This is the first account, create JWT secret to use if one is not supplied by env + const jwtSecret = crypto.randomBytes(64).toString("hex"); + await this.db.updateAppSettings({ jwtSecret }); + } + + const newUser = await this.db.insertUser({ ...user }, file); + + this.logger.debug({ + message: "New user created", + service: SERVICE_NAME, + method: "registerUser", + details: newUser._id, + }); + + const userForToken = { ...newUser._doc }; + delete userForToken.profileImage; + delete userForToken.avatarImage; + + const appSettings = await this.settingsService.getSettings(); + + const token = this.issueToken(userForToken, appSettings); + + try { + const html = await this.emailService.buildEmail("welcomeEmailTemplate", { + name: newUser.firstName, + }); + this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error) => { + this.logger.warn({ + message: error.message, + service: SERVICE_NAME, + method: "registerUser", + stack: error.stack, + }); + }); + } catch (error) { + this.logger.warn({ + message: error.message, + service: SERVICE_NAME, + method: "registerUser", + stack: error.stack, + }); + } + + return { user: newUser, token }; + }; + + loginUser = async (email, password) => { + // Check if user exists + const user = await this.db.getUserByEmail(email); + // Compare password + const match = await user.comparePassword(password); + if (match !== true) { + throw createAuthError(this.stringService.authIncorrectPassword); + } + + // Remove password from user object. Should this be abstracted to DB layer? + const userWithoutPassword = { ...user._doc }; + delete userWithoutPassword.password; + delete userWithoutPassword.avatarImage; + + // Happy path, return token + const appSettings = await this.settingsService.getSettings(); + const token = this.issueToken(userWithoutPassword, appSettings); + // reset avatar image + userWithoutPassword.avatarImage = user.avatarImage; + return { user: userWithoutPassword, token }; + }; + + editUser = async (updates, file, currentUser) => { + // Change Password check + if (updates?.password && updates?.newPassword) { + // Get user's email + // Add user email to body for DB operation + updates.email = currentUser.email; + // Get user + const user = await this.db.getUserByEmail(currentUser.email); + // Compare passwords + const match = await user.comparePassword(updates?.password); + // If not a match, throw a 403 + // 403 instead of 401 to avoid triggering axios interceptor + if (!match) { + throw createError(this.stringService.authIncorrectPassword, 403); + } + // If a match, update the password + updates.password = updates.newPassword; + } + + const updatedUser = await this.db.updateUser({ userId: currentUser?._id, user: updates, file: file }); + return updatedUser; + }; + + checkSuperadminExists = async () => { + const superAdminExists = await this.db.checkSuperadmin(); + return superAdminExists; + }; + + requestRecovery = async (email) => { + const user = await this.db.getUserByEmail(email); + const recoveryToken = await this.db.requestRecoveryToken(email); + const name = user.firstName; + const { clientHost } = this.settingsService.getSettings(); + const url = `${clientHost}/set-new-password/${recoveryToken.token}`; + + const html = await this.emailService.buildEmail("passwordResetTemplate", { + name, + email, + url, + }); + const msgId = await this.emailService.sendEmail(email, "Checkmate Password Reset", html); + return msgId; + }; + + validateRecovery = async (recoveryToken) => { + await this.db.validateRecoveryToken(recoveryToken); + }; + + resetPassword = async (password, recoveryToken) => { + const user = await this.db.resetPassword(password, recoveryToken); + const appSettings = await this.settingsService.getSettings(); + const token = this.issueToken(user._doc, appSettings); + return { user, token }; + }; + + deleteUser = async (user) => { + const email = user?.email; + if (!email) { + throw new Error("No email in request"); + } + + const teamId = user?.teamId; + const userId = user?._id; + + if (!teamId) { + throw new Error("No team ID in request"); + } + + if (!userId) { + throw new Error("No user ID in request"); + } + + const roles = user?.role; + if (roles.includes("demo")) { + throw new Error("Demo user cannot be deleted"); + } + + // 1. Find all the monitors associated with the team ID if superadmin + const result = await this.db.getMonitorsByTeamId({ + teamId: teamId, + }); + + if (roles.includes("superadmin")) { + // 2. Remove all jobs, delete checks and alerts + result?.monitors.length > 0 && + (await Promise.all( + result.monitors.map(async (monitor) => { + await this.jobQueue.deleteJob(monitor); + }) + )); + } + // 6. Delete the user by id + await this.db.deleteUser(userId); + }; + + getAllUsers = async () => { + const users = await this.db.getAllUsers(); + return users; + }; + + getUserById = async (roles, userId) => { + const user = await this.db.getUserById(roles, userId); + return user; + }; + + editUserById = async (userId, user) => { + await this.db.editUserById(userId, user); + }; +} +export default UserService; diff --git a/server/service/redisService.js b/server/service/data/redisService.js similarity index 100% rename from server/service/redisService.js rename to server/service/data/redisService.js diff --git a/server/service/JobQueue/JobQueue.js b/server/service/infrastructure/JobQueue/JobQueue.js similarity index 100% rename from server/service/JobQueue/JobQueue.js rename to server/service/infrastructure/JobQueue/JobQueue.js diff --git a/server/service/JobQueue/JobQueueHelper.js b/server/service/infrastructure/JobQueue/JobQueueHelper.js similarity index 100% rename from server/service/JobQueue/JobQueueHelper.js rename to server/service/infrastructure/JobQueue/JobQueueHelper.js diff --git a/server/service/PulseQueue/PulseQueue.js b/server/service/infrastructure/PulseQueue/PulseQueue.js similarity index 100% rename from server/service/PulseQueue/PulseQueue.js rename to server/service/infrastructure/PulseQueue/PulseQueue.js diff --git a/server/service/PulseQueue/PulseQueueHelper.js b/server/service/infrastructure/PulseQueue/PulseQueueHelper.js similarity index 100% rename from server/service/PulseQueue/PulseQueueHelper.js rename to server/service/infrastructure/PulseQueue/PulseQueueHelper.js diff --git a/server/service/SuperSimpleQueue/SuperSimpleQueue.js b/server/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js similarity index 100% rename from server/service/SuperSimpleQueue/SuperSimpleQueue.js rename to server/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js diff --git a/server/service/SuperSimpleQueue/SuperSimpleQueueHelper.js b/server/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js similarity index 100% rename from server/service/SuperSimpleQueue/SuperSimpleQueueHelper.js rename to server/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js diff --git a/server/service/bufferService.js b/server/service/infrastructure/bufferService.js similarity index 100% rename from server/service/bufferService.js rename to server/service/infrastructure/bufferService.js diff --git a/server/service/emailService.js b/server/service/infrastructure/emailService.js similarity index 100% rename from server/service/emailService.js rename to server/service/infrastructure/emailService.js diff --git a/server/service/networkService.js b/server/service/infrastructure/networkService.js similarity index 100% rename from server/service/networkService.js rename to server/service/infrastructure/networkService.js diff --git a/server/service/notificationService.js b/server/service/infrastructure/notificationService.js similarity index 100% rename from server/service/notificationService.js rename to server/service/infrastructure/notificationService.js diff --git a/server/service/notificationUtils.js b/server/service/infrastructure/notificationUtils.js similarity index 100% rename from server/service/notificationUtils.js rename to server/service/infrastructure/notificationUtils.js diff --git a/server/service/statusService.js b/server/service/infrastructure/statusService.js similarity index 98% rename from server/service/statusService.js rename to server/service/infrastructure/statusService.js index 1982bbb30..1c91a407c 100755 --- a/server/service/statusService.js +++ b/server/service/infrastructure/statusService.js @@ -1,5 +1,5 @@ -import MonitorStats from "../db/models/MonitorStats.js"; -import { safelyParseFloat } from "../utils/dataUtils.js"; +import MonitorStats from "../../db/models/MonitorStats.js"; +import { safelyParseFloat } from "../../utils/dataUtils.js"; const SERVICE_NAME = "StatusService"; class StatusService { diff --git a/server/service/serviceRegistry.js b/server/service/system/serviceRegistry.js similarity index 93% rename from server/service/serviceRegistry.js rename to server/service/system/serviceRegistry.js index 69a3b8be5..5f8b3bfe6 100755 --- a/server/service/serviceRegistry.js +++ b/server/service/system/serviceRegistry.js @@ -1,5 +1,5 @@ const SERVICE_NAME = "ServiceRegistry"; -import logger from "../utils/logger.js"; +import logger from "../../utils/logger.js"; class ServiceRegistry { static SERVICE_NAME = SERVICE_NAME; constructor() { diff --git a/server/service/settingsService.js b/server/service/system/settingsService.js similarity index 100% rename from server/service/settingsService.js rename to server/service/system/settingsService.js diff --git a/server/service/stringService.js b/server/service/system/stringService.js similarity index 100% rename from server/service/stringService.js rename to server/service/system/stringService.js diff --git a/server/service/translationService.js b/server/service/system/translationService.js similarity index 100% rename from server/service/translationService.js rename to server/service/system/translationService.js