diff --git a/Common/Server/Services/AlertInternalNoteService.ts b/Common/Server/Services/AlertInternalNoteService.ts index 92006a98eed..d0fa9190a97 100644 --- a/Common/Server/Services/AlertInternalNoteService.ts +++ b/Common/Server/Services/AlertInternalNoteService.ts @@ -1,10 +1,83 @@ +import ObjectID from "../../Types/ObjectID"; import DatabaseService from "./DatabaseService"; import Model from "Common/Models/DatabaseModels/AlertInternalNote"; +import { OnCreate, OnUpdate } from "../Types/Database/Hooks"; +import AlertFeedService from "./AlertFeedService"; +import { AlertFeedEventType } from "../../Models/DatabaseModels/AlertFeed"; +import { Blue500 } from "../../Types/BrandColors"; +import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax"; export class Service extends DatabaseService { public constructor() { super(Model); } + + public override async onCreateSuccess( + _onCreate: OnCreate, + createdItem: Model, + ): Promise { + const userId: ObjectID | null | undefined = + createdItem.createdByUserId || createdItem.createdByUser?.id; + + await AlertFeedService.createAlertFeed({ + alertId: createdItem.alertId!, + projectId: createdItem.projectId!, + alertFeedEventType: AlertFeedEventType.PrivateNote, + displayColor: Blue500, + userId: userId || undefined, + + feedInfoInMarkdown: `**Posted Internal / Private Note** + + ${createdItem.note} + `, + }); + + return createdItem; + } + + public override async onUpdateSuccess( + onUpdate: OnUpdate, + _updatedItemIds: Array, + ): Promise> { + if (onUpdate.updateBy.data.note) { + const updatedItems: Array = await this.findBy({ + query: onUpdate.updateBy.query, + limit: LIMIT_PER_PROJECT, + skip: 0, + props: { + isRoot: true, + }, + select: { + alertId: true, + projectId: true, + note: true, + createdByUserId: true, + createdByUser: { + _id: true, + }, + }, + }); + + const userId: ObjectID | null | undefined = + onUpdate.updateBy.props.userId; + + for (const updatedItem of updatedItems) { + await AlertFeedService.createAlertFeed({ + alertId: updatedItem.alertId!, + projectId: updatedItem.projectId!, + alertFeedEventType: AlertFeedEventType.PrivateNote, + displayColor: Blue500, + userId: userId || undefined, + + feedInfoInMarkdown: `**Updated Internal / Private Note** + + ${updatedItem.note} + `, + }); + } + } + return onUpdate; + } } export default new Service(); diff --git a/Common/Server/Services/AlertOwnerTeamService.ts b/Common/Server/Services/AlertOwnerTeamService.ts index ac9f08f4984..8e6901fcc68 100644 --- a/Common/Server/Services/AlertOwnerTeamService.ts +++ b/Common/Server/Services/AlertOwnerTeamService.ts @@ -1,10 +1,122 @@ +import Team from "../../Models/DatabaseModels/Team"; +import ObjectID from "../../Types/ObjectID"; +import DeleteBy from "../Types/Database/DeleteBy"; +import { OnCreate, OnDelete } from "../Types/Database/Hooks"; import DatabaseService from "./DatabaseService"; import Model from "Common/Models/DatabaseModels/AlertOwnerTeam"; +import TeamService from "./TeamService"; +import AlertFeedService from "./AlertFeedService"; +import { AlertFeedEventType } from "../../Models/DatabaseModels/AlertFeed"; +import { Gray500, Red500 } from "../../Types/BrandColors"; export class Service extends DatabaseService { public constructor() { super(Model); } + + protected override async onBeforeDelete( + deleteBy: DeleteBy, + ): Promise> { + const itemsToDelete: Model[] = await this.findBy({ + query: deleteBy.query, + limit: deleteBy.limit, + skip: deleteBy.skip, + props: { + isRoot: true, + }, + select: { + alertId: true, + projectId: true, + teamId: true, + }, + }); + + return { + carryForward: { + itemsToDelete: itemsToDelete, + }, + deleteBy: deleteBy, + }; + } + + protected override async onDeleteSuccess( + onDelete: OnDelete, + _itemIdsBeforeDelete: Array, + ): Promise> { + const deleteByUserId: ObjectID | undefined = + onDelete.deleteBy.deletedByUser?.id || onDelete.deleteBy.props.userId; + + const itemsToDelete: Model[] = onDelete.carryForward.itemsToDelete; + + for (const item of itemsToDelete) { + const alertId: ObjectID | undefined = item.alertId; + const projectId: ObjectID | undefined = item.projectId; + const teamId: ObjectID | undefined = item.teamId; + + if (alertId && teamId && projectId) { + const team: Team | null = await TeamService.findOneById({ + id: teamId, + select: { + name: true, + }, + props: { + isRoot: true, + }, + }); + + if (team && team.name) { + await AlertFeedService.createAlertFeed({ + alertId: alertId, + projectId: projectId, + alertFeedEventType: AlertFeedEventType.OwnerTeamRemoved, + displayColor: Red500, + feedInfoInMarkdown: `**Team ${team.name}** was removed from the alert as the owner.`, + userId: deleteByUserId || undefined, + }); + } + } + } + + return onDelete; + } + + public override async onCreateSuccess( + onCreate: OnCreate, + createdItem: Model, + ): Promise { + // add alert feed. + + const alertId: ObjectID | undefined = createdItem.alertId; + const projectId: ObjectID | undefined = createdItem.projectId; + const teamId: ObjectID | undefined = createdItem.teamId; + const createdByUserId: ObjectID | undefined = + createdItem.createdByUserId || onCreate.createBy.props.userId; + + if (alertId && teamId && projectId) { + const team: Team | null = await TeamService.findOneById({ + id: teamId, + select: { + name: true, + }, + props: { + isRoot: true, + }, + }); + + if (team && team.name) { + await AlertFeedService.createAlertFeed({ + alertId: alertId, + projectId: projectId, + alertFeedEventType: AlertFeedEventType.OwnerTeamAdded, + displayColor: Gray500, + feedInfoInMarkdown: `**Team ${team.name}** was added to the alert as the owner.`, + userId: createdByUserId || undefined, + }); + } + } + + return createdItem; + } } export default new Service(); diff --git a/Common/Server/Services/AlertOwnerUserService.ts b/Common/Server/Services/AlertOwnerUserService.ts index 55d6be4ac3c..ab0c2920f01 100644 --- a/Common/Server/Services/AlertOwnerUserService.ts +++ b/Common/Server/Services/AlertOwnerUserService.ts @@ -1,10 +1,124 @@ +import User from "../../Models/DatabaseModels/User"; +import ObjectID from "../../Types/ObjectID"; +import DeleteBy from "../Types/Database/DeleteBy"; +import { OnCreate, OnDelete } from "../Types/Database/Hooks"; import DatabaseService from "./DatabaseService"; import Model from "Common/Models/DatabaseModels/AlertOwnerUser"; +import UserService from "./UserService"; +import AlertFeedService from "./AlertFeedService"; +import { AlertFeedEventType } from "../../Models/DatabaseModels/AlertFeed"; +import { Gray500, Red500 } from "../../Types/BrandColors"; export class Service extends DatabaseService { public constructor() { super(Model); } + + protected override async onBeforeDelete( + deleteBy: DeleteBy, + ): Promise> { + const itemsToDelete: Model[] = await this.findBy({ + query: deleteBy.query, + limit: deleteBy.limit, + skip: deleteBy.skip, + props: { + isRoot: true, + }, + select: { + alertId: true, + projectId: true, + userId: true, + }, + }); + + return { + carryForward: { + itemsToDelete: itemsToDelete, + }, + deleteBy: deleteBy, + }; + } + + protected override async onDeleteSuccess( + onDelete: OnDelete, + _itemIdsBeforeDelete: Array, + ): Promise> { + const deleteByUserId: ObjectID | undefined = + onDelete.deleteBy.deletedByUser?.id || onDelete.deleteBy.props.userId; + + const itemsToDelete: Model[] = onDelete.carryForward.itemsToDelete; + + for (const item of itemsToDelete) { + const alertId: ObjectID | undefined = item.alertId; + const projectId: ObjectID | undefined = item.projectId; + const userId: ObjectID | undefined = item.userId; + + if (alertId && userId && projectId) { + const user: User | null = await UserService.findOneById({ + id: userId, + select: { + name: true, + email: true, + }, + props: { + isRoot: true, + }, + }); + + if (user && user.name) { + await AlertFeedService.createAlertFeed({ + alertId: alertId, + projectId: projectId, + alertFeedEventType: AlertFeedEventType.OwnerUserRemoved, + displayColor: Red500, + feedInfoInMarkdown: `**${user.name.toString()}** (${user.email?.toString()}) was removed from the alert as the owner.`, + userId: deleteByUserId || undefined, + }); + } + } + } + + return onDelete; + } + + public override async onCreateSuccess( + onCreate: OnCreate, + createdItem: Model, + ): Promise { + // add alert feed. + + const alertId: ObjectID | undefined = createdItem.alertId; + const projectId: ObjectID | undefined = createdItem.projectId; + const userId: ObjectID | undefined = createdItem.userId; + const createdByUserId: ObjectID | undefined = + createdItem.createdByUserId || onCreate.createBy.props.userId; + + if (alertId && userId && projectId) { + const user: User | null = await UserService.findOneById({ + id: userId, + select: { + name: true, + email: true, + }, + props: { + isRoot: true, + }, + }); + + if (user && user.name) { + await AlertFeedService.createAlertFeed({ + alertId: alertId, + projectId: projectId, + alertFeedEventType: AlertFeedEventType.OwnerUserAdded, + displayColor: Gray500, + feedInfoInMarkdown: `**${user.name.toString()}** (${user.email?.toString()}) was added to the alert as the owner.`, + userId: createdByUserId || undefined, + }); + } + } + + return createdItem; + } } export default new Service(); diff --git a/Common/Server/Services/AlertService.ts b/Common/Server/Services/AlertService.ts index fe31c1c4a58..9e3be40ab29 100644 --- a/Common/Server/Services/AlertService.ts +++ b/Common/Server/Services/AlertService.ts @@ -38,6 +38,9 @@ import Metric, { ServiceType, } from "../../Models/AnalyticsModels/Metric"; import AlertMetricType from "../../Types/Alerts/AlertMetricType"; +import AlertFeedService from "./AlertFeedService"; +import { AlertFeedEventType } from "../../Models/DatabaseModels/AlertFeed"; +import { Gray500, Red500 } from "../../Types/BrandColors"; export class Service extends DatabaseService { public constructor() { @@ -224,6 +227,48 @@ export class Service extends DatabaseService { throw new BadDataException("currentAlertStateId is required"); } + const createdByUserId: ObjectID | undefined | null = + createdItem.createdByUserId || createdItem.createdByUser?.id; + + await AlertFeedService.createAlertFeed({ + alertId: createdItem.id!, + projectId: createdItem.projectId!, + alertFeedEventType: AlertFeedEventType.AlertCreated, + displayColor: Red500, + feedInfoInMarkdown: `**Alert Created**: + + **Alert Title**: + + ${createdItem.title || "No title provided."} + + **Description**: + + ${createdItem.description || "No description provided."} + + `, + userId: createdByUserId || undefined, + }); + + await AlertFeedService.createAlertFeed({ + alertId: createdItem.id!, + projectId: createdItem.projectId!, + alertFeedEventType: AlertFeedEventType.RootCause, + displayColor: Red500, + feedInfoInMarkdown: `**Root Cause** + +${createdItem.rootCause || "No root cause provided."}`, + }); + + await AlertFeedService.createAlertFeed({ + alertId: createdItem.id!, + projectId: createdItem.projectId!, + alertFeedEventType: AlertFeedEventType.RemediationNotes, + displayColor: Red500, + feedInfoInMarkdown: `**Remediation Notes** + +${createdItem.remediationNotes || "No remediation notes provided."}`, + }); + await this.changeAlertState({ projectId: createdItem.projectId, alertId: createdItem.id, @@ -445,6 +490,82 @@ export class Service extends DatabaseService { } } + if (updatedItemIds.length > 0) { + for (const alertId of updatedItemIds) { + if (onUpdate.updateBy.data.title) { + // add alert feed. + const createdByUserId: ObjectID | undefined | null = + onUpdate.updateBy.props.userId; + + await AlertFeedService.createAlertFeed({ + alertId: alertId, + projectId: onUpdate.updateBy.props.tenantId as ObjectID, + alertFeedEventType: AlertFeedEventType.AlertUpdated, + displayColor: Gray500, + feedInfoInMarkdown: `**Alert title was updated.** Here's the new title. + + ${onUpdate.updateBy.data.title || "No title provided."} + `, + userId: createdByUserId || undefined, + }); + } + + if (onUpdate.updateBy.data.rootCause) { + // add alert feed. + const createdByUserId: ObjectID | undefined | null = + onUpdate.updateBy.props.userId; + + await AlertFeedService.createAlertFeed({ + alertId: alertId, + projectId: onUpdate.updateBy.props.tenantId as ObjectID, + alertFeedEventType: AlertFeedEventType.AlertUpdated, + displayColor: Gray500, + feedInfoInMarkdown: `**Alert root cause was updated.** Here's the new root cause. + + ${onUpdate.updateBy.data.rootCause || "No root cause provided."} + `, + userId: createdByUserId || undefined, + }); + } + + if (onUpdate.updateBy.data.description) { + // add alert feed. + const createdByUserId: ObjectID | undefined | null = + onUpdate.updateBy.props.userId; + + await AlertFeedService.createAlertFeed({ + alertId: alertId, + projectId: onUpdate.updateBy.props.tenantId as ObjectID, + alertFeedEventType: AlertFeedEventType.AlertUpdated, + displayColor: Gray500, + feedInfoInMarkdown: `**Alert description was updated.** Here's the new description. + + ${onUpdate.updateBy.data.description || "No description provided."} + `, + userId: createdByUserId || undefined, + }); + } + + if (onUpdate.updateBy.data.remediationNotes) { + // add alert feed. + const createdByUserId: ObjectID | undefined | null = + onUpdate.updateBy.props.userId; + + await AlertFeedService.createAlertFeed({ + alertId: alertId, + projectId: onUpdate.updateBy.props.tenantId as ObjectID, + alertFeedEventType: AlertFeedEventType.AlertUpdated, + displayColor: Gray500, + feedInfoInMarkdown: `**Remediation notes were updated.** Here are the new notes. + + ${onUpdate.updateBy.data.remediationNotes || "No remediation notes provided."} + `, + userId: createdByUserId || undefined, + }); + } + } + } + return onUpdate; } diff --git a/Common/Server/Services/AlertStateTimelineService.ts b/Common/Server/Services/AlertStateTimelineService.ts index e23b3fff096..71ee3d017ed 100644 --- a/Common/Server/Services/AlertStateTimelineService.ts +++ b/Common/Server/Services/AlertStateTimelineService.ts @@ -19,6 +19,8 @@ import { JSONObject } from "../../Types/JSON"; import AlertInternalNote from "../../Models/DatabaseModels/AlertInternalNote"; import AlertInternalNoteService from "./AlertInternalNoteService"; import logger from "../Utils/Logger"; +import AlertFeedService from "./AlertFeedService"; +import { AlertFeedEventType } from "../../Models/DatabaseModels/AlertFeed"; export class Service extends DatabaseService { public constructor() { @@ -164,6 +166,36 @@ export class Service extends DatabaseService { }); } + const alertState: AlertState | null = await AlertStateService.findOneBy({ + query: { + _id: createdItem.alertStateId.toString()!, + }, + props: { + isRoot: true, + }, + select: { + _id: true, + isResolvedState: true, + isAcknowledgedState: true, + isCreatedState: true, + color: true, + name: true, + }, + }); + + const stateName: string = alertState?.name || ""; + + await AlertFeedService.createAlertFeed({ + alertId: createdItem.alertId!, + projectId: createdItem.projectId!, + alertFeedEventType: AlertFeedEventType.AlertStateChanged, + displayColor: alertState?.color, + feedInfoInMarkdown: "**Alert State** changed to **" + stateName + "**", + moreInformationInMarkdown: `**Cause:** +${createdItem.rootCause}`, + userId: createdItem.createdByUserId || onCreate.createBy.props.userId, + }); + await AlertService.updateOneBy({ query: { _id: createdItem.alertId?.toString(), diff --git a/Worker/Jobs/AlertOwners/SendCreatedResourceNotification.ts b/Worker/Jobs/AlertOwners/SendCreatedResourceNotification.ts index 96aecb020b7..26e1bff356b 100644 --- a/Worker/Jobs/AlertOwners/SendCreatedResourceNotification.ts +++ b/Worker/Jobs/AlertOwners/SendCreatedResourceNotification.ts @@ -18,6 +18,9 @@ import Alert from "Common/Models/DatabaseModels/Alert"; import AlertState from "Common/Models/DatabaseModels/AlertState"; import Project from "Common/Models/DatabaseModels/Project"; import User from "Common/Models/DatabaseModels/User"; +import { AlertFeedEventType } from "Common/Models/DatabaseModels/AlertFeed"; +import { Yellow500 } from "Common/Types/BrandColors"; +import AlertFeedService from "Common/Server/Services/AlertFeedService"; RunCron( "AlertOwner:SendCreatedResourceEmail", @@ -63,6 +66,10 @@ RunCron( }); for (const alert of alerts) { + const alertFeedText: string = `**Owner Alert Created Notification Sent**: + Notification sent to owners of this alert because this alert was created.`; + let moreAlertFeedInformationInMarkdown: string = ""; + const alertIdentifiedDate: Date = await AlertService.getAlertIdentifiedDate(alert.id!); @@ -175,11 +182,22 @@ RunCron( eventType: NotificationSettingEventType.SEND_ALERT_CREATED_OWNER_NOTIFICATION, }); + + moreAlertFeedInformationInMarkdown += `**Notified**: ${user.name} (${user.email})\n`; } catch (e) { logger.error("Error in sending alert created resource notification"); logger.error(e); } } + + await AlertFeedService.createAlertFeed({ + alertId: alert.id!, + projectId: alert.projectId!, + alertFeedEventType: AlertFeedEventType.OwnerNotificationSent, + displayColor: Yellow500, + feedInfoInMarkdown: alertFeedText, + moreInformationInMarkdown: moreAlertFeedInformationInMarkdown, + }); } }, ); diff --git a/Worker/Jobs/AlertOwners/SendNotePostedNotification.ts b/Worker/Jobs/AlertOwners/SendNotePostedNotification.ts index a3b7f160dbd..662e7222cc9 100644 --- a/Worker/Jobs/AlertOwners/SendNotePostedNotification.ts +++ b/Worker/Jobs/AlertOwners/SendNotePostedNotification.ts @@ -17,6 +17,9 @@ import Markdown, { MarkdownContentType } from "Common/Server/Types/Markdown"; import Alert from "Common/Models/DatabaseModels/Alert"; import AlertInternalNote from "Common/Models/DatabaseModels/AlertInternalNote"; import User from "Common/Models/DatabaseModels/User"; +import AlertFeedService from "Common/Server/Services/AlertFeedService"; +import { AlertFeedEventType } from "Common/Models/DatabaseModels/AlertFeed"; +import { Blue500 } from "Common/Types/BrandColors"; RunCron( "AlertOwner:SendsNotePostedEmail", @@ -140,6 +143,8 @@ RunCron( vars["isPrivateNote"] = "true"; } + let moreAlertFeedInformationInMarkdown: string = ""; + for (const user of owners) { const emailMessage: EmailEnvelope = { templateType: EmailTemplateType.AlertOwnerNotePosted, @@ -168,7 +173,20 @@ RunCron( eventType: NotificationSettingEventType.SEND_ALERT_NOTE_POSTED_OWNER_NOTIFICATION, }); + + moreAlertFeedInformationInMarkdown += `**Notified:** ${user.name} (${user.email})\n`; } + + const alertFeedText: string = `**Owners Notified because note is posted** Owners have been notified about the new note posted on the alert.`; + + await AlertFeedService.createAlertFeed({ + alertId: alert.id!, + projectId: alert.projectId!, + alertFeedEventType: AlertFeedEventType.OwnerNotificationSent, + displayColor: Blue500, + feedInfoInMarkdown: alertFeedText, + moreInformationInMarkdown: moreAlertFeedInformationInMarkdown, + }); } }, ); diff --git a/Worker/Jobs/AlertOwners/SendStateChangeNotification.ts b/Worker/Jobs/AlertOwners/SendStateChangeNotification.ts index 0cc29091c3d..5fd0cff4eae 100644 --- a/Worker/Jobs/AlertOwners/SendStateChangeNotification.ts +++ b/Worker/Jobs/AlertOwners/SendStateChangeNotification.ts @@ -19,6 +19,9 @@ import Alert from "Common/Models/DatabaseModels/Alert"; import AlertState from "Common/Models/DatabaseModels/AlertState"; import AlertStateTimeline from "Common/Models/DatabaseModels/AlertStateTimeline"; import User from "Common/Models/DatabaseModels/User"; +import AlertFeedService from "Common/Server/Services/AlertFeedService"; +import { AlertFeedEventType } from "Common/Models/DatabaseModels/AlertFeed"; +import { Blue500 } from "Common/Types/BrandColors"; RunCron( "AlertOwner:SendStateChangeEmail", @@ -125,6 +128,8 @@ RunCron( continue; } + let moreAlertFeedInformationInMarkdown: string = ""; + for (const user of owners) { const vars: Dictionary = { alertTitle: alert.title!, @@ -188,7 +193,18 @@ RunCron( eventType: NotificationSettingEventType.SEND_ALERT_STATE_CHANGED_OWNER_NOTIFICATION, }); + + moreAlertFeedInformationInMarkdown += `**Notified:** ${user.name} (${user.email})\n`; } + + await AlertFeedService.createAlertFeed({ + alertId: alert.id!, + projectId: alert.projectId!, + alertFeedEventType: AlertFeedEventType.OwnerNotificationSent, + displayColor: Blue500, + feedInfoInMarkdown: `**Owners have been notified about the state change of the alert.**: Owners have been notified about the state change of the alert because the alert state changed to **${alertState.name}**.`, + moreInformationInMarkdown: moreAlertFeedInformationInMarkdown, + }); } }, );