Merge branch 'develop' into incidents-feat-modal
This commit is contained in:
commit
f4a5bd65ad
25 files changed
+214
-441
No files matched your search
@@ -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 = ({
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
key={notification._id}
|
||||
key={notification.id}
|
||||
width="100%"
|
||||
>
|
||||
<Typography
|
||||
@@ -106,7 +106,7 @@ const NotificationConfig = ({
|
||||
name="Trash2"
|
||||
size={20}
|
||||
onClick={() => {
|
||||
handleDelete(notification._id);
|
||||
handleDelete(notification.id);
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -100,7 +100,7 @@ const Notifications = () => {
|
||||
<Typography variant="h1">{t("notifications.createTitle")}</Typography>
|
||||
<DataTable
|
||||
config={{
|
||||
onRowClick: (row) => navigate(`/notifications/${row._id}`),
|
||||
onRowClick: (row) => navigate(`/notifications/${row.id}`),
|
||||
rowSX: {
|
||||
cursor: "pointer",
|
||||
"&:hover td": {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
@@ -219,6 +207,7 @@ export const initializeServices = async ({
|
||||
|
||||
const notificationsService = new NotificationsService(
|
||||
notificationsRepository,
|
||||
monitorsRepository,
|
||||
webhookProvider,
|
||||
emailProvider,
|
||||
slackProvider,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -39,4 +39,5 @@ export interface IMonitorsRepository {
|
||||
// other
|
||||
findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise<MonitorsSummary>;
|
||||
findGroupsByTeamId(teamId: string): Promise<string[]>;
|
||||
removeNotificationFromMonitors(notificationId: string): Promise<void>;
|
||||
}
|
||||
@@ -189,6 +189,10 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
return groups.sort();
|
||||
};
|
||||
|
||||
removeNotificationFromMonitors = async (notificationId: string): Promise<void> => {
|
||||
await MonitorModel.updateMany({ notifications: notificationId }, { $pull: { notifications: notificationId } });
|
||||
};
|
||||
|
||||
private mapDocuments = (documents: MonitorDocument[]): Monitor[] => {
|
||||
if (!documents?.length) {
|
||||
return [];
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import type { Notification } from "@/types/index.js";
|
||||
export interface INotificationsRepository {
|
||||
// create
|
||||
create(notificationData: Partial<Notification>): Promise<Notification>;
|
||||
// fetch
|
||||
findById(id: string, teamId: string): Promise<Notification>;
|
||||
findNotificationsByIds(ids: string[]): Promise<Notification[]>;
|
||||
findByTeamId(teamId: string): Promise<Notification[]>;
|
||||
// update
|
||||
updateById(id: string, teamId: string, updateData: Partial<Notification>): Promise<Notification>;
|
||||
// delete
|
||||
deleteById(id: string, teamId: string): Promise<Notification>;
|
||||
}
|
||||
@@ -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<Notification>) => {
|
||||
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<Notification> => {
|
||||
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<Notification[]> => {
|
||||
const documents = await NotificationModel.find({ teamId });
|
||||
return this.mapDocuments(documents);
|
||||
};
|
||||
|
||||
updateById = async (id: string, teamId: string, patch: Partial<Notification>): Promise<Notification> => {
|
||||
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<Notification> => {
|
||||
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;
|
||||
@@ -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;
|
||||
@@ -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<Notification>) => Promise<Notification>;
|
||||
findById: (id: string, teamId: string) => Promise<Notification>;
|
||||
findNotificationsByTeamId: (teamId: string) => Promise<Notification[]>;
|
||||
updateById(id: string, teamId: string, updateData: Partial<Notification>): Promise<Notification>;
|
||||
deleteById: (id: string, teamId: string) => Promise<Notification>;
|
||||
handleNotifications: (
|
||||
monitor: Monitor,
|
||||
monitorStatusResponse: MonitorStatusResponse,
|
||||
prevStatus: boolean | undefined,
|
||||
statusChanged: boolean
|
||||
) => Promise<boolean>;
|
||||
|
||||
sendTestNotification: (notification: Notification) => Promise<boolean>;
|
||||
testAllNotifications: (notificationIds: string[]) => Promise<boolean>;
|
||||
}
|
||||
@@ -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<Notification>): Promise<Notification> => {
|
||||
return await this.notificationsRepository.create(notificationData);
|
||||
};
|
||||
|
||||
findById = async (id: string, teamId: string): Promise<Notification> => {
|
||||
return await this.notificationsRepository.findById(id, teamId);
|
||||
};
|
||||
|
||||
findNotificationsByTeamId = async (teamId: string): Promise<Notification[]> => {
|
||||
return await this.notificationsRepository.findByTeamId(teamId);
|
||||
};
|
||||
|
||||
updateById = async (id: string, teamId: string, updateData: Partial<Notification>): Promise<Notification> => {
|
||||
return await this.notificationsRepository.updateById(id, teamId, updateData);
|
||||
};
|
||||
|
||||
deleteById = async (id: string, teamId: string): Promise<Notification> => {
|
||||
const deleted = await this.notificationsRepository.deleteById(id, teamId);
|
||||
await this.monitorsRepository.removeNotificationFromMonitors(id);
|
||||
return deleted;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user