Merge pull request #4535 from Infisical/expand-notification-coverage

feat(notifications): expand notification coverage
This commit is contained in:
x032205
2025-09-16 13:46:13 -04:00
committed by GitHub
19 changed files with 323 additions and 52 deletions

View File

@@ -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

View File

@@ -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<TSmtpService, "sendMail">;
projectId: string;
secretApprovalRequest: TSecretApprovalRequests;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
};
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({

View File

@@ -28,6 +28,8 @@ 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 { 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";
@@ -140,6 +142,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
};
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
@@ -172,7 +175,8 @@ export const secretApprovalRequestServiceFactory = ({
resourceMetadataDAL,
projectMicrosoftTeamsConfigDAL,
microsoftTeamsService,
folderCommitService
folderCommitService,
notificationService
}: TSecretApprovalRequestServiceFactoryDep) => {
const requestCount = async ({
projectId,
@@ -1035,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",
@@ -1446,7 +1461,8 @@ export const secretApprovalRequestServiceFactory = ({
secretApprovalPolicyDAL,
secretApprovalRequest,
smtpService,
projectId
projectId,
notificationService
});
return secretApprovalRequest;
@@ -1813,7 +1829,8 @@ export const secretApprovalRequestServiceFactory = ({
secretApprovalPolicyDAL,
secretApprovalRequest,
smtpService,
projectId
projectId,
notificationService
});
return secretApprovalRequest;
};

View File

@@ -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<TSmtpService, "sendMail">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
projectDAL: Pick<TProjectDALFactory, "findById">;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
};
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) {

View File

@@ -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<TAppConnectionDALFactory, "updateById">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "getItem">;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
};
export type TSecretScanningV2QueueServiceFactory = Awaited<ReturnType<typeof secretScanningV2QueueServiceFactory>>;
@@ -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
? {

View File

@@ -231,6 +231,8 @@ export type TQueueJobTypes = {
[QueueName.ImportSecretsFromExternalSource]: {
name: QueueJobs.ImportSecretsFromExternalSource;
payload: {
orgId: string;
actorId: string;
actorEmail: string;
importType: ExternalPlatforms;
data: {

View File

@@ -773,7 +773,8 @@ export const registerRoutes = async (
orgDAL,
totpService,
orgMembershipDAL,
auditLogService
auditLogService,
notificationService
});
const passwordService = authPaswordServiceFactory({
tokenService,
@@ -887,7 +888,8 @@ export const registerRoutes = async (
projectDAL,
permissionService,
projectUserMembershipRoleDAL,
projectMembershipDAL
projectMembershipDAL,
notificationService
});
const rateLimitService = rateLimitServiceFactory({
@@ -926,7 +928,8 @@ export const registerRoutes = async (
projectRoleDAL,
groupProjectDAL,
secretReminderRecipientsDAL,
licenseService
licenseService,
notificationService
});
const projectUserAdditionalPrivilegeService = projectUserAdditionalPrivilegeServiceFactory({
permissionService,
@@ -1145,7 +1148,8 @@ export const registerRoutes = async (
appConnectionDAL,
licenseService,
gatewayService,
gatewayV2Service
gatewayV2Service,
notificationService
});
const secretQueueService = secretQueueFactory({
@@ -1229,7 +1233,8 @@ export const registerRoutes = async (
projectTemplateService,
groupProjectDAL,
smtpService,
reminderService
reminderService,
notificationService
});
const projectEnvService = projectEnvServiceFactory({
@@ -1363,7 +1368,8 @@ export const registerRoutes = async (
resourceMetadataDAL,
projectMicrosoftTeamsConfigDAL,
microsoftTeamsService,
folderCommitService
folderCommitService,
notificationService
});
const secretService = secretServiceFactory({
@@ -1814,7 +1820,8 @@ export const registerRoutes = async (
secretV2BridgeService,
resourceMetadataDAL,
folderCommitService,
folderVersionDAL
folderVersionDAL,
notificationService
});
const migrationService = externalMigrationServiceFactory({
@@ -2017,7 +2024,8 @@ export const registerRoutes = async (
queueService,
projectDAL,
projectMembershipDAL,
smtpService
smtpService,
notificationService
});
const secretScanningV2Queue = await secretScanningV2QueueServiceFactory({
@@ -2029,7 +2037,8 @@ export const registerRoutes = async (
smtpService,
kmsService,
keyStore,
appConnectionDAL
appConnectionDAL,
notificationService
});
const secretScanningV2Service = secretScanningV2ServiceFactory({

View File

@@ -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 };
}
});

View File

@@ -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<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
orgMembershipDAL: TOrgMembershipDALFactory;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
};
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
@@ -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",

View File

@@ -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<TSecretFolderVersionDALFactory, "create">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
};
export type TExternalMigrationQueueFactory = ReturnType<typeof externalMigrationQueueFactory>;
@@ -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",

View File

@@ -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: {

View File

@@ -1,6 +1,21 @@
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",
SECRET_CHANGE_REQUEST = "secret-change-request",
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",
LOGIN_FROM_NEW_DEVICE = "login-from-new-device",
ADMIN_SSO_BYPASS = "admin-sso-bypass",
IMPORT_STARTED = "import-started",
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_INVITATION = "project-invitation",
SECRET_SYNC_FAILED = "secret-sync-failed"
}
export interface TCreateUserNotificationDTO {

View File

@@ -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<TProjectUserMembershipRoleDALFactory, "create" | "delete">;
smtpService: Pick<TSmtpService, "sendMail">;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
};
export type TOrgAdminServiceFactory = ReturnType<typeof orgAdminServiceFactory>;
@@ -29,7 +32,8 @@ export const orgAdminServiceFactory = ({
projectDAL,
projectMembershipDAL,
projectUserMembershipRoleDAL,
smtpService
smtpService,
notificationService
}: TOrgAdminServiceFactoryDep) => {
const listOrgProjects = async ({
actor,
@@ -130,23 +134,34 @@ 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 (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
}
});
if (actorEmail) {
await notificationService.createUserNotifications(
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 (mappedProjectAdmins.length) {
await smtpService.sendMail({
template: SmtpTemplates.OrgAdminProjectDirectAccess,
recipients: mappedProjectAdmins,
subjectLine: "Organization Admin Project Direct Access Issued",
substitutions: {
projectName: project.name,
email: actorEmail
}
});
}
}
return { isExistingMember: false, membership: updatedMembership };
};

View File

@@ -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<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
secretReminderRecipientsDAL: Pick<TSecretReminderRecipientsDALFactory, "delete">;
groupProjectDAL: TGroupProjectDALFactory;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
};
export type TProjectMembershipServiceFactory = ReturnType<typeof projectMembershipServiceFactory>;
@@ -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,

View File

@@ -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<TReminderServiceFactory, "deleteReminderBySecretId">;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
};
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
@@ -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}`
}
});
};

View File

@@ -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<typeof secretSyncQueueFactory>;
@@ -100,6 +102,7 @@ type TSecretSyncQueueFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
};
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}`
}
});
};

View File

@@ -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
);

View File

@@ -26,7 +26,11 @@ export const Notification = ({ notification, onDelete }: Props) => {
{!notification.isRead && (
<FontAwesomeIcon icon={faCircle} className="mt-1.5 size-2 text-yellow-400" />
)}
<Tooltip content={<Markdown>{notification.title}</Markdown>} delayDuration={300}>
<Tooltip
content={<Markdown>{notification.title}</Markdown>}
delayDuration={300}
className="z-[1000]"
>
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium leading-5 text-mineshaft-100">
<Markdown components={{ p: "span" }}>{notification.title}</Markdown>
</span>

View File

@@ -46,7 +46,7 @@ export const NotificationDropdown = () => {
<DropdownMenuContent
align="end"
side="bottom"
className="mt-3 flex h-[550px] w-[400px] overflow-hidden rounded-lg"
className="z-[999] mt-3 flex h-[550px] w-[400px] overflow-hidden rounded-lg"
>
<div className="flex w-full flex-col">
<div className="flex items-center justify-between border-b border-mineshaft-500 px-3 py-2">