From f85612d9fd225477c445696f3cd588e5d9f72534 Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 16 Sep 2025 00:30:38 -0400 Subject: [PATCH 01/13] feat(notifications): access policy bypass notification --- .../access-approval-request-service.ts | 16 +++++++++++++++- .../services/notification/notification-types.ts | 3 ++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index 008b61919..da87978fd 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -777,6 +777,20 @@ export const accessApprovalRequestServiceFactory = ({ .map((appUser) => appUser.email) .filter((email): email is string => !!email); + const approvalPath = `/projects/secret-management/${project.id}/approval`; + const approvalUrl = `${cfg.SITE_URL}${approvalPath}`; + + await notificationService.createUserNotifications( + approverUsersForEmail.map((approver) => ({ + userId: approver.id, + orgId: actorOrgId, + type: NotificationType.ACCESS_POLICY_BYPASSED, + title: "Secret Access Policy Bypassed", + body: `**${actingUser.firstName} ${actingUser.lastName}** (${actingUser.email}) has accessed a secret in **${policy.secretPath || "/"}** in the **${environment?.name || permissionEnvironment}** environment for project **${project.name}** without obtaining the required approval.`, + link: approvalPath + })) + ); + if (recipientEmails.length > 0) { await smtpService.sendMail({ recipients: recipientEmails, @@ -788,7 +802,7 @@ export const accessApprovalRequestServiceFactory = ({ bypassReason: bypassReason || "No reason provided", secretPath: policy.secretPath || "/", environment: environment?.name || permissionEnvironment, - approvalUrl: `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval`, + approvalUrl, requestType: "access" }, template: SmtpTemplates.AccessSecretRequestBypassed diff --git a/backend/src/services/notification/notification-types.ts b/backend/src/services/notification/notification-types.ts index 30bc87244..76ffc2ea1 100644 --- a/backend/src/services/notification/notification-types.ts +++ b/backend/src/services/notification/notification-types.ts @@ -1,6 +1,7 @@ export enum NotificationType { ACCESS_APPROVAL_REQUEST = "access-approval-request", - ACCESS_APPROVAL_REQUEST_UPDATED = "access-approval-request-updated" + ACCESS_APPROVAL_REQUEST_UPDATED = "access-approval-request-updated", + ACCESS_POLICY_BYPASSED = "access-policy-bypassed" } export interface TCreateUserNotificationDTO { From 11029be2ed7bb36a0b0b1ea928149a22bd3b02fa Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 16 Sep 2025 00:44:25 -0400 Subject: [PATCH 02/13] feat(notifications): secret change request notification --- .../secret-approval-request-fns.ts | 17 ++++++++++++++++- .../secret-approval-request-service.ts | 11 ++++++++--- backend/src/server/routes/index.ts | 3 ++- .../services/notification/notification-types.ts | 3 ++- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-fns.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-fns.ts index dfe425b8e..d96fb2e53 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-fns.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-fns.ts @@ -1,5 +1,7 @@ import { TSecretApprovalRequests } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; +import { TNotificationServiceFactory } from "@app/services/notification/notification-service"; +import { NotificationType } from "@app/services/notification/notification-types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; @@ -11,6 +13,7 @@ type TSendApprovalEmails = { smtpService: Pick; projectId: string; secretApprovalRequest: TSecretApprovalRequests; + notificationService: Pick; }; export const sendApprovalEmailsFn = async ({ @@ -18,7 +21,8 @@ export const sendApprovalEmailsFn = async ({ projectDAL, smtpService, projectId, - secretApprovalRequest + secretApprovalRequest, + notificationService }: TSendApprovalEmails) => { const cfg = getConfig(); @@ -26,6 +30,17 @@ export const sendApprovalEmailsFn = async ({ const project = await projectDAL.findProjectWithOrg(projectId); + await notificationService.createUserNotifications( + policy.userApprovers.map((approver) => ({ + userId: approver.userId, + orgId: project.orgId, + type: NotificationType.SECRET_CHANGE_REQUEST, + title: "Secret Change Request", + body: `You have a new secret change request pending your review for the project **${project.name}** in the organization **${project.organization.name}**.`, + link: `/projects/secret-management/${project.id}/approval?requestId=${secretApprovalRequest.id}` + })) + ); + // now we need to go through each of the reviewers and print out all the commits that they need to approve for await (const reviewerUser of policy.userApprovers) { await smtpService.sendMail({ diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index 17b7d8347..7e052baa5 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -28,6 +28,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal"; +import { TNotificationServiceFactory } from "@app/services/notification/notification-service"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; @@ -140,6 +141,7 @@ type TSecretApprovalRequestServiceFactoryDep = { projectMicrosoftTeamsConfigDAL: Pick; microsoftTeamsService: Pick; folderCommitService: Pick; + notificationService: Pick; }; export type TSecretApprovalRequestServiceFactory = ReturnType; @@ -172,7 +174,8 @@ export const secretApprovalRequestServiceFactory = ({ resourceMetadataDAL, projectMicrosoftTeamsConfigDAL, microsoftTeamsService, - folderCommitService + folderCommitService, + notificationService }: TSecretApprovalRequestServiceFactoryDep) => { const requestCount = async ({ projectId, @@ -1446,7 +1449,8 @@ export const secretApprovalRequestServiceFactory = ({ secretApprovalPolicyDAL, secretApprovalRequest, smtpService, - projectId + projectId, + notificationService }); return secretApprovalRequest; @@ -1813,7 +1817,8 @@ export const secretApprovalRequestServiceFactory = ({ secretApprovalPolicyDAL, secretApprovalRequest, smtpService, - projectId + projectId, + notificationService }); return secretApprovalRequest; }; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index b9dc65676..4de76665a 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1363,7 +1363,8 @@ export const registerRoutes = async ( resourceMetadataDAL, projectMicrosoftTeamsConfigDAL, microsoftTeamsService, - folderCommitService + folderCommitService, + notificationService }); const secretService = secretServiceFactory({ diff --git a/backend/src/services/notification/notification-types.ts b/backend/src/services/notification/notification-types.ts index 76ffc2ea1..f12f73792 100644 --- a/backend/src/services/notification/notification-types.ts +++ b/backend/src/services/notification/notification-types.ts @@ -1,7 +1,8 @@ export enum NotificationType { ACCESS_APPROVAL_REQUEST = "access-approval-request", ACCESS_APPROVAL_REQUEST_UPDATED = "access-approval-request-updated", - ACCESS_POLICY_BYPASSED = "access-policy-bypassed" + ACCESS_POLICY_BYPASSED = "access-policy-bypassed", + SECRET_CHANGE_REQUEST = "secret-change-request" } export interface TCreateUserNotificationDTO { From 65babae41a3a60dc5b8aa88aa7fc847b5cb7f84d Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 16 Sep 2025 00:54:56 -0400 Subject: [PATCH 03/13] feat(notifications): add notification update posthog telemetry to track when notifications are clicked / read. also increased popup z index --- backend/src/server/routes/v1/notification-router.ts | 12 ++++++++++++ backend/src/services/telemetry/telemetry-types.ts | 12 +++++++++++- .../components/NavBar/NotificationDropdown.tsx | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/backend/src/server/routes/v1/notification-router.ts b/backend/src/server/routes/v1/notification-router.ts index 5f72b88d6..955a1174e 100644 --- a/backend/src/server/routes/v1/notification-router.ts +++ b/backend/src/server/routes/v1/notification-router.ts @@ -3,8 +3,10 @@ import { z } from "zod"; import { UserNotificationsSchema } from "@app/db/schemas/user-notifications"; import { UnauthorizedError } from "@app/lib/errors"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; export const registerNotificationRouter = async (server: FastifyZodProvider) => { server.route({ @@ -97,6 +99,16 @@ export const registerNotificationRouter = async (server: FastifyZodProvider) => ...req.body }); + await server.services.telemetry.sendPostHogEvents({ + event: PostHogEventTypes.NotificationUpdated, + distinctId: getTelemetryDistinctId(req), + organizationId: req.permission.orgId, + properties: { + notificationId: req.params.notificationId, + ...req.body + } + }); + return { notification }; } }); diff --git a/backend/src/services/telemetry/telemetry-types.ts b/backend/src/services/telemetry/telemetry-types.ts index a43a2f746..de466614a 100644 --- a/backend/src/services/telemetry/telemetry-types.ts +++ b/backend/src/services/telemetry/telemetry-types.ts @@ -32,7 +32,8 @@ export enum PostHogEventTypes { IssueSshHostHostCert = "Issue SSH Host Host Certificate", SignCert = "Sign PKI Certificate", IssueCert = "Issue PKI Certificate", - InvalidateCache = "Invalidate Cache" + InvalidateCache = "Invalidate Cache", + NotificationUpdated = "Notification Updated" } export type TSecretModifiedEvent = { @@ -232,6 +233,14 @@ export type TInvalidateCacheEvent = { }; }; +export type TNotificationUpdatedEvent = { + event: PostHogEventTypes.NotificationUpdated; + properties: { + notificationId: string; + isRead?: boolean; + }; +}; + export type TPostHogEvent = { distinctId: string; organizationId?: string } & ( | TSecretModifiedEvent | TAdminInitEvent @@ -251,4 +260,5 @@ export type TPostHogEvent = { distinctId: string; organizationId?: string } & ( | TSignCertificateEvent | TIssueCertificateEvent | TInvalidateCacheEvent + | TNotificationUpdatedEvent ); diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/NotificationDropdown.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/NotificationDropdown.tsx index a5e9166d9..6b6381ac1 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/NotificationDropdown.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/NotificationDropdown.tsx @@ -46,7 +46,7 @@ export const NotificationDropdown = () => {
From 1ea0bc281472940f5c41b9de49c2779f539e5d76 Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 16 Sep 2025 01:05:07 -0400 Subject: [PATCH 04/13] feat(notifications): change policy bypass notification --- .../secret-approval-request-service.ts | 12 ++++++++++++ .../src/services/notification/notification-types.ts | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index 7e052baa5..b19143b76 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -29,6 +29,7 @@ import { KmsDataKey } from "@app/services/kms/kms-types"; import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service"; import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal"; import { TNotificationServiceFactory } from "@app/services/notification/notification-service"; +import { NotificationType } from "@app/services/notification/notification-types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; @@ -1038,6 +1039,17 @@ export const secretApprovalRequestServiceFactory = ({ } }); + await notificationService.createUserNotifications( + approverUsers.map((approver) => ({ + userId: approver.id, + orgId: project.orgId, + type: NotificationType.SECRET_CHANGE_POLICY_BYPASSED, + title: "Secret Change Policy Bypassed", + body: `**${requestedByUser.firstName} ${requestedByUser.lastName}** (${requestedByUser.email}) has merged a secret to **${policy.secretPath}** in the **${env.name}** environment for project **${project.name}** without obtaining the required approval.`, + link: `/projects/secret-management/${project.id}/approval` + })) + ); + await smtpService.sendMail({ recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!), subjectLine: "Infisical Secret Change Policy Bypassed", diff --git a/backend/src/services/notification/notification-types.ts b/backend/src/services/notification/notification-types.ts index f12f73792..d62fbb07c 100644 --- a/backend/src/services/notification/notification-types.ts +++ b/backend/src/services/notification/notification-types.ts @@ -2,7 +2,8 @@ export enum NotificationType { ACCESS_APPROVAL_REQUEST = "access-approval-request", ACCESS_APPROVAL_REQUEST_UPDATED = "access-approval-request-updated", ACCESS_POLICY_BYPASSED = "access-policy-bypassed", - SECRET_CHANGE_REQUEST = "secret-change-request" + SECRET_CHANGE_REQUEST = "secret-change-request", + SECRET_CHANGE_POLICY_BYPASSED = "secret-change-policy-bypassed" } export interface TCreateUserNotificationDTO { From 66d2ecf28964a77339a57736358e374fe0beeb0e Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 16 Sep 2025 01:12:26 -0400 Subject: [PATCH 05/13] feat(notifications): secret rotation failed notification --- .../secret-rotation-v2-queue.ts | 23 +++++++++++++++---- backend/src/server/routes/index.ts | 3 ++- .../notification/notification-types.ts | 3 ++- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-queue.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-queue.ts index 50c81f444..f653802b6 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-queue.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-queue.ts @@ -17,6 +17,8 @@ import { import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; +import { TNotificationServiceFactory } from "@app/services/notification/notification-service"; +import { NotificationType } from "@app/services/notification/notification-types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; @@ -28,6 +30,7 @@ type TSecretRotationV2QueueServiceFactoryDep = { smtpService: Pick; projectMembershipDAL: Pick; projectDAL: Pick; + notificationService: Pick; }; export const secretRotationV2QueueServiceFactory = async ({ @@ -36,7 +39,8 @@ export const secretRotationV2QueueServiceFactory = async ({ secretRotationV2Service, projectMembershipDAL, projectDAL, - smtpService + smtpService, + notificationService }: TSecretRotationV2QueueServiceFactoryDep) => { const appCfg = getConfig(); @@ -152,6 +156,19 @@ export const secretRotationV2QueueServiceFactory = async ({ const rotationType = SECRET_ROTATION_NAME_MAP[type as SecretRotation]; + const rotationPath = `/projects/secret-management/${projectId}/secrets/${environment.slug}`; + + await notificationService.createUserNotifications( + projectAdmins.map((admin) => ({ + userId: admin.userId, + orgId: project.orgId, + type: NotificationType.SECRET_ROTATION_FAILED, + title: "Secret Rotation Failed", + body: `Your **${rotationType}** rotation **${rotationName}** failed to rotate.`, + link: rotationPath + })) + ); + await smtpService.sendMail({ recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean), template: SmtpTemplates.SecretRotationFailed, @@ -165,9 +182,7 @@ export const secretRotationV2QueueServiceFactory = async ({ secretPath: folder.path, environment: environment.name, projectName: project.name, - rotationUrl: encodeURI( - `${appCfg.SITE_URL}/projects/secret-management/${projectId}/secrets/${environment.slug}` - ) + rotationUrl: encodeURI(`${appCfg.SITE_URL}${rotationPath}`) } }); } catch (error) { diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 4de76665a..11cf65d15 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -2017,7 +2017,8 @@ export const registerRoutes = async ( queueService, projectDAL, projectMembershipDAL, - smtpService + smtpService, + notificationService }); const secretScanningV2Queue = await secretScanningV2QueueServiceFactory({ diff --git a/backend/src/services/notification/notification-types.ts b/backend/src/services/notification/notification-types.ts index d62fbb07c..800e412d5 100644 --- a/backend/src/services/notification/notification-types.ts +++ b/backend/src/services/notification/notification-types.ts @@ -3,7 +3,8 @@ export enum NotificationType { ACCESS_APPROVAL_REQUEST_UPDATED = "access-approval-request-updated", ACCESS_POLICY_BYPASSED = "access-policy-bypassed", SECRET_CHANGE_REQUEST = "secret-change-request", - SECRET_CHANGE_POLICY_BYPASSED = "secret-change-policy-bypassed" + SECRET_CHANGE_POLICY_BYPASSED = "secret-change-policy-bypassed", + SECRET_ROTATION_FAILED = "secret-rotation-failed" } export interface TCreateUserNotificationDTO { From 4431fe687d5a3cce7bb5f444dd57dc5e59a50239 Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 16 Sep 2025 01:27:28 -0400 Subject: [PATCH 06/13] feat(notifications): secret scan alert notifications --- .../secret-scanning-v2-queue.ts | 36 ++++++++++++++++--- backend/src/server/routes/index.ts | 3 +- .../notification/notification-types.ts | 4 ++- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-queue.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-queue.ts index 8621b039b..406c25e03 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-queue.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-queue.ts @@ -21,6 +21,8 @@ import { decryptAppConnection } from "@app/services/app-connection/app-connectio import { TAppConnection } from "@app/services/app-connection/app-connection-types"; import { ActorType } from "@app/services/auth/auth-type"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TNotificationServiceFactory } from "@app/services/notification/notification-service"; +import { NotificationType } from "@app/services/notification/notification-types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; @@ -52,6 +54,7 @@ type TSecretRotationV2QueueServiceFactoryDep = { appConnectionDAL: Pick; auditLogService: Pick; keyStore: Pick; + notificationService: Pick; }; export type TSecretScanningV2QueueServiceFactory = Awaited>; @@ -65,7 +68,8 @@ export const secretScanningV2QueueServiceFactory = async ({ kmsService, auditLogService, keyStore, - appConnectionDAL + appConnectionDAL, + notificationService }: TSecretRotationV2QueueServiceFactoryDep) => { const queueDataSourceFullScan = async ( dataSource: TSecretScanningDataSourceWithConnection, @@ -592,16 +596,38 @@ export const secretScanningV2QueueServiceFactory = async ({ const timestamp = new Date().toISOString(); + const subjectLine = + payload.status === SecretScanningScanStatus.Completed + ? "Incident Alert: Secret(s) Leaked" + : `Secret Scanning Failed`; + + await notificationService.createUserNotifications( + recipients.map((member) => ({ + userId: member.userId, + orgId: project.orgId, + type: + payload.status === SecretScanningScanStatus.Completed + ? NotificationType.SECRET_SCANNING_SECRETS_DETECTED + : NotificationType.SECRET_SCANNING_SCAN_FAILED, + title: subjectLine, + body: + payload.status === SecretScanningScanStatus.Completed + ? `Uncovered **${payload.numberOfSecrets}** secret(s) ${payload.isDiffScan ? " from a recent commit to" : " in"} **${resourceName}**.` + : `Encountered an error while attempting to scan the resource **${resourceName}**: ${payload.errorMessage}`, + link: + payload.status === SecretScanningScanStatus.Completed + ? `/projects/secret-scanning/${projectId}/findings?search=scanId:${payload.scanId}` + : `/projects/secret-scanning/${projectId}/data-sources/${dataSource.type}/${dataSource.id}` + })) + ); + await smtpService.sendMail({ recipients: recipients.map((member) => member.user.email!).filter(Boolean), template: payload.status === SecretScanningScanStatus.Completed ? SmtpTemplates.SecretScanningV2SecretsDetected : SmtpTemplates.SecretScanningV2ScanFailed, - subjectLine: - payload.status === SecretScanningScanStatus.Completed - ? "Incident Alert: Secret(s) Leaked" - : `Secret Scanning Failed`, + subjectLine, substitutions: payload.status === SecretScanningScanStatus.Completed ? { diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 11cf65d15..c88874499 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -2030,7 +2030,8 @@ export const registerRoutes = async ( smtpService, kmsService, keyStore, - appConnectionDAL + appConnectionDAL, + notificationService }); const secretScanningV2Service = secretScanningV2ServiceFactory({ diff --git a/backend/src/services/notification/notification-types.ts b/backend/src/services/notification/notification-types.ts index 800e412d5..e33bafcb9 100644 --- a/backend/src/services/notification/notification-types.ts +++ b/backend/src/services/notification/notification-types.ts @@ -4,7 +4,9 @@ export enum NotificationType { ACCESS_POLICY_BYPASSED = "access-policy-bypassed", SECRET_CHANGE_REQUEST = "secret-change-request", SECRET_CHANGE_POLICY_BYPASSED = "secret-change-policy-bypassed", - SECRET_ROTATION_FAILED = "secret-rotation-failed" + SECRET_ROTATION_FAILED = "secret-rotation-failed", + SECRET_SCANNING_SECRETS_DETECTED = "secret-scanning-secrets-detected", + SECRET_SCANNING_SCAN_FAILED = "secret-scanning-scan-failed" } export interface TCreateUserNotificationDTO { From 55eceb725ccab6f4b87248c48da07669079e2d48 Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 16 Sep 2025 01:36:53 -0400 Subject: [PATCH 07/13] feat(notifications): login service notifications --- backend/src/server/routes/index.ts | 3 +- .../src/services/auth/auth-login-service.ts | 28 ++++++++++++++++++- .../notification/notification-types.ts | 4 ++- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index c88874499..f47048878 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -773,7 +773,8 @@ export const registerRoutes = async ( orgDAL, totpService, orgMembershipDAL, - auditLogService + auditLogService, + notificationService }); const passwordService = authPaswordServiceFactory({ tokenService, diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index ede9e29e3..2a680b9b8 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -14,6 +14,8 @@ import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; +import { TNotificationServiceFactory } from "../notification/notification-service"; +import { NotificationType } from "../notification/notification-types"; import { TOrgDALFactory } from "../org/org-dal"; import { getDefaultOrgMembershipRole } from "../org/org-role-fns"; import { TOrgMembershipDALFactory } from "../org-membership/org-membership-dal"; @@ -47,6 +49,7 @@ type TAuthLoginServiceFactoryDep = { totpService: Pick; auditLogService: Pick; orgMembershipDAL: TOrgMembershipDALFactory; + notificationService: Pick; }; export type TAuthLoginFactory = ReturnType; @@ -57,7 +60,8 @@ export const authLoginServiceFactory = ({ orgDAL, orgMembershipDAL, totpService, - auditLogService + auditLogService, + notificationService }: TAuthLoginServiceFactoryDep) => { /* * Private @@ -71,6 +75,16 @@ export const authLoginServiceFactory = ({ if (!isDeviceSeen) { const newDeviceList = devices.concat([{ ip, userAgent }]); await userDAL.updateById(user.id, { devices: JSON.stringify(newDeviceList) }, tx); + + await notificationService.createUserNotifications([ + { + userId: user.id, + type: NotificationType.LOGIN_FROM_NEW_DEVICE, + title: "Login From New Device", + body: `A new device with IP **${ip}** and User Agent **${userAgent}** has logged into your account.` + } + ]); + if (user.email) { await smtpService.sendMail({ template: SmtpTemplates.NewDeviceJoin, @@ -563,6 +577,18 @@ export const authLoginServiceFactory = ({ .filter(Boolean) as string[]; if (adminEmails.length > 0) { + await notificationService.createUserNotifications( + orgAdmins + .filter((admin) => admin.user.id !== user.id) + .map((admin) => ({ + userId: admin.user.id, + orgId: organizationId, + type: NotificationType.ADMIN_SSO_BYPASS, + title: "Security Alert: Admin SSO Bypass", + body: `The org admin **${user.email}** has bypassed enforced SSO login.` + })) + ); + await smtpService.sendMail({ recipients: adminEmails, subjectLine: "Security Alert: Admin SSO Bypass", diff --git a/backend/src/services/notification/notification-types.ts b/backend/src/services/notification/notification-types.ts index e33bafcb9..3377afbc9 100644 --- a/backend/src/services/notification/notification-types.ts +++ b/backend/src/services/notification/notification-types.ts @@ -6,7 +6,9 @@ export enum NotificationType { SECRET_CHANGE_POLICY_BYPASSED = "secret-change-policy-bypassed", SECRET_ROTATION_FAILED = "secret-rotation-failed", SECRET_SCANNING_SECRETS_DETECTED = "secret-scanning-secrets-detected", - SECRET_SCANNING_SCAN_FAILED = "secret-scanning-scan-failed" + SECRET_SCANNING_SCAN_FAILED = "secret-scanning-scan-failed", + LOGIN_FROM_NEW_DEVICE = "login-from-new-device", + ADMIN_SSO_BYPASS = "admin-sso-bypass" } export interface TCreateUserNotificationDTO { From bf8c33f119de616040f3cc15f35166134558706f Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 16 Sep 2025 01:46:45 -0400 Subject: [PATCH 08/13] feat(notifications): import status update notifications --- backend/src/queue/queue-service.ts | 2 + backend/src/server/routes/index.ts | 3 +- .../external-migration-queue.ts | 41 ++++++++++++++++++- .../external-migration-service.ts | 4 ++ .../notification/notification-types.ts | 5 ++- 5 files changed, 51 insertions(+), 4 deletions(-) diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index 5a7c92f22..f2f63f57a 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -231,6 +231,8 @@ export type TQueueJobTypes = { [QueueName.ImportSecretsFromExternalSource]: { name: QueueJobs.ImportSecretsFromExternalSource; payload: { + orgId: string; + actorId: string; actorEmail: string; importType: ExternalPlatforms; data: { diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index f47048878..eff862f78 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1815,7 +1815,8 @@ export const registerRoutes = async ( secretV2BridgeService, resourceMetadataDAL, folderCommitService, - folderVersionDAL + folderVersionDAL, + notificationService }); const migrationService = externalMigrationServiceFactory({ diff --git a/backend/src/services/external-migration/external-migration-queue.ts b/backend/src/services/external-migration/external-migration-queue.ts index b4974d2ae..770cbbe3a 100644 --- a/backend/src/services/external-migration/external-migration-queue.ts +++ b/backend/src/services/external-migration/external-migration-queue.ts @@ -5,6 +5,8 @@ import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service"; import { TKmsServiceFactory } from "../kms/kms-service"; +import { TNotificationServiceFactory } from "../notification/notification-service"; +import { NotificationType } from "../notification/notification-types"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectServiceFactory } from "../project/project-service"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; @@ -42,6 +44,7 @@ export type TExternalMigrationQueueFactoryDep = { folderVersionDAL: Pick; resourceMetadataDAL: Pick; + notificationService: Pick; }; export type TExternalMigrationQueueFactory = ReturnType; @@ -62,9 +65,12 @@ export const externalMigrationQueueFactory = ({ folderDAL, folderCommitService, folderVersionDAL, - resourceMetadataDAL + resourceMetadataDAL, + notificationService }: TExternalMigrationQueueFactoryDep) => { const startImport = async (dto: { + orgId: string; + actorId: string; actorEmail: string; importType: ExternalPlatforms; data: { @@ -87,9 +93,19 @@ export const externalMigrationQueueFactory = ({ }; queueService.start(QueueName.ImportSecretsFromExternalSource, async (job) => { - const { data, actorEmail, importType } = job.data; + const { data, actorEmail, importType, actorId, orgId } = job.data; try { + await notificationService.createUserNotifications([ + { + userId: actorId, + orgId, + type: NotificationType.IMPORT_STARTED, + title: "Import Started", + body: `An import from **${importType}** to Infisical has been started.` + } + ]); + await smtpService.sendMail({ recipients: [actorEmail], subjectLine: "Infisical import started", @@ -137,6 +153,16 @@ export const externalMigrationQueueFactory = ({ ); } + await notificationService.createUserNotifications([ + { + userId: actorId, + orgId, + type: NotificationType.IMPORT_SUCCESSFUL, + title: "Import Successful", + body: `An import from **${importType}** to Infisical has successfully completed.` + } + ]); + await smtpService.sendMail({ recipients: [actorEmail], subjectLine: "Infisical import successful", @@ -146,6 +172,17 @@ export const externalMigrationQueueFactory = ({ template: SmtpTemplates.ExternalImportSuccessful }); } catch (err) { + await notificationService.createUserNotifications([ + { + userId: actorId, + orgId, + type: NotificationType.IMPORT_FAILED, + title: "Import Failed", + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + body: `An import from **${importType}** to Infisical has failed: ${(err as any)?.message || "Unknown error"}.` + } + ]); + await smtpService.sendMail({ recipients: [job.data.actorEmail], subjectLine: "Infisical import failed", diff --git a/backend/src/services/external-migration/external-migration-service.ts b/backend/src/services/external-migration/external-migration-service.ts index 73fac00b9..e801b607e 100644 --- a/backend/src/services/external-migration/external-migration-service.ts +++ b/backend/src/services/external-migration/external-migration-service.ts @@ -73,6 +73,8 @@ export const externalMigrationServiceFactory = ({ const encrypted = crypto.encryption().symmetric().encryptWithRootEncryptionKey(stringifiedJson); await externalMigrationQueue.startImport({ + actorId: user.id, + orgId: actorOrgId, actorEmail: user.email!, importType: ExternalPlatforms.EnvKey, data: { @@ -131,6 +133,8 @@ export const externalMigrationServiceFactory = ({ const encrypted = crypto.encryption().symmetric().encryptWithRootEncryptionKey(stringifiedJson); await externalMigrationQueue.startImport({ + actorId: user.id, + orgId: actorOrgId, actorEmail: user.email!, importType: ExternalPlatforms.Vault, data: { diff --git a/backend/src/services/notification/notification-types.ts b/backend/src/services/notification/notification-types.ts index 3377afbc9..5743118a6 100644 --- a/backend/src/services/notification/notification-types.ts +++ b/backend/src/services/notification/notification-types.ts @@ -8,7 +8,10 @@ export enum NotificationType { SECRET_SCANNING_SECRETS_DETECTED = "secret-scanning-secrets-detected", SECRET_SCANNING_SCAN_FAILED = "secret-scanning-scan-failed", LOGIN_FROM_NEW_DEVICE = "login-from-new-device", - ADMIN_SSO_BYPASS = "admin-sso-bypass" + ADMIN_SSO_BYPASS = "admin-sso-bypass", + IMPORT_STARTED = "import-started", + IMPORT_SUCCESSFUL = "import-successful", + IMPORT_FAILED = "import-failed" } export interface TCreateUserNotificationDTO { From 7568ad859dcd4dcd59bed92bde090b141867761d Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 16 Sep 2025 01:58:41 -0400 Subject: [PATCH 09/13] feat(notification): direct project access alert notification --- backend/src/server/routes/index.ts | 3 +- .../notification/notification-types.ts | 3 +- .../services/org-admin/org-admin-service.ts | 45 ++++++++++++++----- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index eff862f78..c0e7827c2 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -888,7 +888,8 @@ export const registerRoutes = async ( projectDAL, permissionService, projectUserMembershipRoleDAL, - projectMembershipDAL + projectMembershipDAL, + notificationService }); const rateLimitService = rateLimitServiceFactory({ diff --git a/backend/src/services/notification/notification-types.ts b/backend/src/services/notification/notification-types.ts index 5743118a6..0d0cf1f15 100644 --- a/backend/src/services/notification/notification-types.ts +++ b/backend/src/services/notification/notification-types.ts @@ -11,7 +11,8 @@ export enum NotificationType { ADMIN_SSO_BYPASS = "admin-sso-bypass", IMPORT_STARTED = "import-started", IMPORT_SUCCESSFUL = "import-successful", - IMPORT_FAILED = "import-failed" + IMPORT_FAILED = "import-failed", + DIRECT_PROJECT_ACCESS_ISSUED_TO_ADMIN = "direct-project-access-issued-to-admin" } export interface TCreateUserNotificationDTO { diff --git a/backend/src/services/org-admin/org-admin-service.ts b/backend/src/services/org-admin/org-admin-service.ts index 23005e52b..9c8b574a8 100644 --- a/backend/src/services/org-admin/org-admin-service.ts +++ b/backend/src/services/org-admin/org-admin-service.ts @@ -5,6 +5,8 @@ import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/ import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { TNotificationServiceFactory } from "../notification/notification-service"; +import { NotificationType } from "../notification/notification-types"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; @@ -20,6 +22,7 @@ type TOrgAdminServiceFactoryDep = { >; projectUserMembershipRoleDAL: Pick; smtpService: Pick; + notificationService: Pick; }; export type TOrgAdminServiceFactory = ReturnType; @@ -29,7 +32,8 @@ export const orgAdminServiceFactory = ({ projectDAL, projectMembershipDAL, projectUserMembershipRoleDAL, - smtpService + smtpService, + notificationService }: TOrgAdminServiceFactoryDep) => { const listOrgProjects = async ({ actor, @@ -137,16 +141,35 @@ export const orgAdminServiceFactory = ({ .map((el) => el.user.email!) .filter(Boolean); - if (filteredProjectMembers.length) { - await smtpService.sendMail({ - template: SmtpTemplates.OrgAdminProjectDirectAccess, - recipients: filteredProjectMembers, - subjectLine: "Organization Admin Project Direct Access Issued", - substitutions: { - projectName: project.name, - email: projectMembers.find((el) => el.userId === actorId)?.user?.username - } - }); + const actorEmail = projectMembers.find((el) => el.userId === actorId)?.user?.username; + + if (actorEmail) { + await notificationService.createUserNotifications( + projectMembers + .filter( + (member) => + member.roles.some((role) => role.role === ProjectMembershipRole.Admin) && member.userId !== actorId + ) + .map((member) => ({ + userId: member.userId, + orgId: project.orgId, + type: NotificationType.DIRECT_PROJECT_ACCESS_ISSUED_TO_ADMIN, + title: "Direct Project Access Issued", + body: `The organization admin **${actorEmail}** has self-issued direct access to the project **${project.name}**.` + })) + ); + + if (filteredProjectMembers.length) { + await smtpService.sendMail({ + template: SmtpTemplates.OrgAdminProjectDirectAccess, + recipients: filteredProjectMembers, + subjectLine: "Organization Admin Project Direct Access Issued", + substitutions: { + projectName: project.name, + email: actorEmail + } + }); + } } return { isExistingMember: false, membership: updatedMembership }; }; From f9ca58c8b96754b8b32fe083408b9629dcae0dcd Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 16 Sep 2025 02:06:49 -0400 Subject: [PATCH 10/13] feat(notifications): project access request notification --- backend/src/server/routes/index.ts | 3 ++- .../notification/notification-types.ts | 3 ++- .../src/services/project/project-service.ts | 23 +++++++++++++++++-- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index c0e7827c2..73fb1c8c1 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1231,7 +1231,8 @@ export const registerRoutes = async ( projectTemplateService, groupProjectDAL, smtpService, - reminderService + reminderService, + notificationService }); const projectEnvService = projectEnvServiceFactory({ diff --git a/backend/src/services/notification/notification-types.ts b/backend/src/services/notification/notification-types.ts index 0d0cf1f15..a6727cde9 100644 --- a/backend/src/services/notification/notification-types.ts +++ b/backend/src/services/notification/notification-types.ts @@ -12,7 +12,8 @@ export enum NotificationType { IMPORT_STARTED = "import-started", IMPORT_SUCCESSFUL = "import-successful", IMPORT_FAILED = "import-failed", - DIRECT_PROJECT_ACCESS_ISSUED_TO_ADMIN = "direct-project-access-issued-to-admin" + DIRECT_PROJECT_ACCESS_ISSUED_TO_ADMIN = "direct-project-access-issued-to-admin", + PROJECT_ACCESS_REQUEST = "project-access-request" } export interface TCreateUserNotificationDTO { diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 57539f002..8dbba2b4f 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -57,6 +57,8 @@ import { TKmsServiceFactory } from "../kms/kms-service"; import { validateMicrosoftTeamsChannelsSchema } from "../microsoft-teams/microsoft-teams-fns"; import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal"; import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal"; +import { TNotificationServiceFactory } from "../notification/notification-service"; +import { NotificationType } from "../notification/notification-types"; import { TOrgDALFactory } from "../org/org-dal"; import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal"; import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal"; @@ -183,6 +185,7 @@ type TProjectServiceFactoryDep = { >; projectTemplateService: TProjectTemplateServiceFactory; reminderService: Pick; + notificationService: Pick; }; export type TProjectServiceFactory = ReturnType; @@ -227,7 +230,8 @@ export const projectServiceFactory = ({ projectTemplateService, groupProjectDAL, smtpService, - reminderService + reminderService, + notificationService }: TProjectServiceFactoryDep) => { /* * Create workspace. Make user the admin @@ -1924,6 +1928,21 @@ export const projectServiceFactory = ({ projectTypeUrl = "cert-management"; } + const callbackPath = `/projects/${projectTypeUrl}/${project.id}/access-management?selectedTab=members&requesterEmail=${userDetails.email}`; + + await notificationService.createUserNotifications( + projectMembers + .filter((member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin)) + .map((member) => ({ + userId: member.userId, + orgId: project.orgId, + type: NotificationType.PROJECT_ACCESS_REQUEST, + title: "Project Access Request", + body: `**${userDetails.firstName} ${userDetails.lastName}** (${userDetails.email}) has requested access to the project **${project.name}**.`, + link: callbackPath + })) + ); + await smtpService.sendMail({ template: SmtpTemplates.ProjectAccessRequest, recipients: filteredProjectMembers, @@ -1934,7 +1953,7 @@ export const projectServiceFactory = ({ projectName: project?.name, orgName: org?.name, note: comment, - callback_url: `${appCfg.SITE_URL}/projects/${projectTypeUrl}/${project.id}/access-management?selectedTab=members&requesterEmail=${userDetails.email}` + callback_url: `${appCfg.SITE_URL}${callbackPath}` } }); }; From 747248b7a4d3d064b40ac29d27a030ba427b49ab Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 16 Sep 2025 02:10:50 -0400 Subject: [PATCH 11/13] feat(notifications): project invite notification --- backend/src/server/routes/index.ts | 3 ++- .../services/notification/notification-types.ts | 3 ++- .../project-membership-service.ts | 16 +++++++++++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 73fb1c8c1..45054e0ec 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -928,7 +928,8 @@ export const registerRoutes = async ( projectRoleDAL, groupProjectDAL, secretReminderRecipientsDAL, - licenseService + licenseService, + notificationService }); const projectUserAdditionalPrivilegeService = projectUserAdditionalPrivilegeServiceFactory({ permissionService, diff --git a/backend/src/services/notification/notification-types.ts b/backend/src/services/notification/notification-types.ts index a6727cde9..82ea1f553 100644 --- a/backend/src/services/notification/notification-types.ts +++ b/backend/src/services/notification/notification-types.ts @@ -13,7 +13,8 @@ export enum NotificationType { IMPORT_SUCCESSFUL = "import-successful", IMPORT_FAILED = "import-failed", DIRECT_PROJECT_ACCESS_ISSUED_TO_ADMIN = "direct-project-access-issued-to-admin", - PROJECT_ACCESS_REQUEST = "project-access-request" + PROJECT_ACCESS_REQUEST = "project-access-request", + PROJECT_INVITATION = "project-invitation" } export interface TCreateUserNotificationDTO { diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index 7cf665141..991bf65d1 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -18,6 +18,8 @@ import { ms } from "@app/lib/ms"; import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal"; import { ActorType } from "../auth/auth-type"; import { TGroupProjectDALFactory } from "../group-project/group-project-dal"; +import { TNotificationServiceFactory } from "../notification/notification-service"; +import { NotificationType } from "../notification/notification-types"; import { TOrgDALFactory } from "../org/org-dal"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; @@ -56,6 +58,7 @@ type TProjectMembershipServiceFactoryDep = { projectUserAdditionalPrivilegeDAL: Pick; secretReminderRecipientsDAL: Pick; groupProjectDAL: TGroupProjectDALFactory; + notificationService: Pick; }; export type TProjectMembershipServiceFactory = ReturnType; @@ -74,7 +77,8 @@ export const projectMembershipServiceFactory = ({ projectDAL, projectKeyDAL, secretReminderRecipientsDAL, - licenseService + licenseService, + notificationService }: TProjectMembershipServiceFactoryDep) => { const getProjectMemberships = async ({ actorId, @@ -236,6 +240,16 @@ export const projectMembershipServiceFactory = ({ }); if (sendEmails) { + await notificationService.createUserNotifications( + orgMembers.map((member) => ({ + userId: member.userId, + orgId: project.orgId, + type: NotificationType.PROJECT_INVITATION, + title: "Project Invitation", + body: `You've been invited to join the project **${project.name}**.` + })) + ); + const appCfg = getConfig(); await smtpService.sendMail({ template: SmtpTemplates.WorkspaceInvite, From 888ca40a58df20dce313f978a8de3cf841e5a7f1 Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 16 Sep 2025 02:24:41 -0400 Subject: [PATCH 12/13] feat(notifications): sync failed notification + ui tweaks --- backend/src/server/routes/index.ts | 3 ++- .../notification/notification-types.ts | 3 ++- .../services/secret-sync/secret-sync-queue.ts | 21 +++++++++++++++++-- .../components/NavBar/Notification.tsx | 6 +++++- .../NavBar/NotificationDropdown.tsx | 2 +- 5 files changed, 29 insertions(+), 6 deletions(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 45054e0ec..019310e1e 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1148,7 +1148,8 @@ export const registerRoutes = async ( appConnectionDAL, licenseService, gatewayService, - gatewayV2Service + gatewayV2Service, + notificationService }); const secretQueueService = secretQueueFactory({ diff --git a/backend/src/services/notification/notification-types.ts b/backend/src/services/notification/notification-types.ts index 82ea1f553..a3657c680 100644 --- a/backend/src/services/notification/notification-types.ts +++ b/backend/src/services/notification/notification-types.ts @@ -14,7 +14,8 @@ export enum NotificationType { IMPORT_FAILED = "import-failed", DIRECT_PROJECT_ACCESS_ISSUED_TO_ADMIN = "direct-project-access-issued-to-admin", PROJECT_ACCESS_REQUEST = "project-access-request", - PROJECT_INVITATION = "project-invitation" + PROJECT_INVITATION = "project-invitation", + SECRET_SYNC_FAILED = "secret-sync-failed" } export interface TCreateUserNotificationDTO { diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index a31bb202d..63faf3b03 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -61,6 +61,8 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal"; import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service"; +import { TNotificationServiceFactory } from "../notification/notification-service"; +import { NotificationType } from "../notification/notification-types"; export type TSecretSyncQueueFactory = ReturnType; @@ -100,6 +102,7 @@ type TSecretSyncQueueFactoryDep = { licenseService: Pick; gatewayService: Pick; gatewayV2Service: Pick; + notificationService: Pick; }; type SecretSyncActionJob = Job< @@ -142,7 +145,8 @@ export const secretSyncQueueFactory = ({ folderCommitService, licenseService, gatewayService, - gatewayV2Service + gatewayV2Service, + notificationService }: TSecretSyncQueueFactoryDep) => { const appCfg = getConfig(); @@ -898,6 +902,19 @@ export const secretSyncQueueFactory = ({ break; } + const syncPath = `/projects/secret-management/${projectId}/integrations/secret-syncs/${destination}/${secretSync.id}`; + + await notificationService.createUserNotifications( + projectAdmins.map((admin) => ({ + userId: admin.userId, + orgId: project.orgId, + type: NotificationType.SECRET_SYNC_FAILED, + title: `Secret Sync Failed to ${actionLabel} Secrets`, + body: `Your **${syncDestination}** sync **${name}** failed to complete${failureMessage ? `: \`${failureMessage}\`` : ""}`, + link: syncPath + })) + ); + await smtpService.sendMail({ recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean), template: SmtpTemplates.SecretSyncFailed, @@ -910,7 +927,7 @@ export const secretSyncQueueFactory = ({ secretPath: folder?.path, environment: environment?.name, projectName: project.name, - syncUrl: `${appCfg.SITE_URL}/projects/secret-management/${projectId}/integrations/secret-syncs/${destination}/${secretSync.id}` + syncUrl: `${appCfg.SITE_URL}${syncPath}` } }); }; diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Notification.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Notification.tsx index 59e6861fc..29fd25415 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/Notification.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Notification.tsx @@ -26,7 +26,11 @@ export const Notification = ({ notification, onDelete }: Props) => { {!notification.isRead && ( )} - {notification.title}} delayDuration={300}> + {notification.title}} + delayDuration={300} + className="z-[1000]" + > {notification.title} diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/NotificationDropdown.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/NotificationDropdown.tsx index 6b6381ac1..69a56d5a8 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/NotificationDropdown.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/NotificationDropdown.tsx @@ -46,7 +46,7 @@ export const NotificationDropdown = () => {
From ea1e8e38e506a0e67b552d70bc274b5423ec1c46 Mon Sep 17 00:00:00 2001 From: x032205 Date: Tue, 16 Sep 2025 02:37:25 -0400 Subject: [PATCH 13/13] Addressed greptile review --- .../services/org-admin/org-admin-service.ts | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/backend/src/services/org-admin/org-admin-service.ts b/backend/src/services/org-admin/org-admin-service.ts index 9c8b574a8..995afadc1 100644 --- a/backend/src/services/org-admin/org-admin-service.ts +++ b/backend/src/services/org-admin/org-admin-service.ts @@ -134,35 +134,27 @@ export const orgAdminServiceFactory = ({ }); const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId); - const filteredProjectMembers = projectMembers - .filter( - (member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin) && member.userId !== actorId - ) - .map((el) => el.user.email!) - .filter(Boolean); - + const projectAdmins = projectMembers.filter( + (member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin) && member.userId !== actorId + ); + const mappedProjectAdmins = projectAdmins.map((el) => el.user.email!).filter(Boolean); const actorEmail = projectMembers.find((el) => el.userId === actorId)?.user?.username; if (actorEmail) { await notificationService.createUserNotifications( - projectMembers - .filter( - (member) => - member.roles.some((role) => role.role === ProjectMembershipRole.Admin) && member.userId !== actorId - ) - .map((member) => ({ - userId: member.userId, - orgId: project.orgId, - type: NotificationType.DIRECT_PROJECT_ACCESS_ISSUED_TO_ADMIN, - title: "Direct Project Access Issued", - body: `The organization admin **${actorEmail}** has self-issued direct access to the project **${project.name}**.` - })) + projectAdmins.map((member) => ({ + userId: member.userId, + orgId: project.orgId, + type: NotificationType.DIRECT_PROJECT_ACCESS_ISSUED_TO_ADMIN, + title: "Direct Project Access Issued", + body: `The organization admin **${actorEmail}** has self-issued direct access to the project **${project.name}**.` + })) ); - if (filteredProjectMembers.length) { + if (mappedProjectAdmins.length) { await smtpService.sendMail({ template: SmtpTemplates.OrgAdminProjectDirectAccess, - recipients: filteredProjectMembers, + recipients: mappedProjectAdmins, subjectLine: "Organization Admin Project Direct Access Issued", substitutions: { projectName: project.name,