mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 21:26:04 +00:00
Merge pull request #4535 from Infisical/expand-notification-coverage
feat(notifications): expand notification coverage
This commit is contained in:
@@ -777,6 +777,20 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
.map((appUser) => appUser.email)
|
.map((appUser) => appUser.email)
|
||||||
.filter((email): email is string => !!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) {
|
if (recipientEmails.length > 0) {
|
||||||
await smtpService.sendMail({
|
await smtpService.sendMail({
|
||||||
recipients: recipientEmails,
|
recipients: recipientEmails,
|
||||||
@@ -788,7 +802,7 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
bypassReason: bypassReason || "No reason provided",
|
bypassReason: bypassReason || "No reason provided",
|
||||||
secretPath: policy.secretPath || "/",
|
secretPath: policy.secretPath || "/",
|
||||||
environment: environment?.name || permissionEnvironment,
|
environment: environment?.name || permissionEnvironment,
|
||||||
approvalUrl: `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval`,
|
approvalUrl,
|
||||||
requestType: "access"
|
requestType: "access"
|
||||||
},
|
},
|
||||||
template: SmtpTemplates.AccessSecretRequestBypassed
|
template: SmtpTemplates.AccessSecretRequestBypassed
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { TSecretApprovalRequests } from "@app/db/schemas";
|
import { TSecretApprovalRequests } from "@app/db/schemas";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
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 { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
|
|
||||||
@@ -11,6 +13,7 @@ type TSendApprovalEmails = {
|
|||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
secretApprovalRequest: TSecretApprovalRequests;
|
secretApprovalRequest: TSecretApprovalRequests;
|
||||||
|
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sendApprovalEmailsFn = async ({
|
export const sendApprovalEmailsFn = async ({
|
||||||
@@ -18,7 +21,8 @@ export const sendApprovalEmailsFn = async ({
|
|||||||
projectDAL,
|
projectDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
projectId,
|
projectId,
|
||||||
secretApprovalRequest
|
secretApprovalRequest,
|
||||||
|
notificationService
|
||||||
}: TSendApprovalEmails) => {
|
}: TSendApprovalEmails) => {
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
|
|
||||||
@@ -26,6 +30,17 @@ export const sendApprovalEmailsFn = async ({
|
|||||||
|
|
||||||
const project = await projectDAL.findProjectWithOrg(projectId);
|
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
|
// 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) {
|
for await (const reviewerUser of policy.userApprovers) {
|
||||||
await smtpService.sendMail({
|
await smtpService.sendMail({
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
|||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||||
import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
|
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 { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||||
@@ -140,6 +142,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
|||||||
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||||
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
||||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||||
|
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
|
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
|
||||||
@@ -172,7 +175,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
projectMicrosoftTeamsConfigDAL,
|
projectMicrosoftTeamsConfigDAL,
|
||||||
microsoftTeamsService,
|
microsoftTeamsService,
|
||||||
folderCommitService
|
folderCommitService,
|
||||||
|
notificationService
|
||||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||||
const requestCount = async ({
|
const requestCount = async ({
|
||||||
projectId,
|
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({
|
await smtpService.sendMail({
|
||||||
recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!),
|
recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!),
|
||||||
subjectLine: "Infisical Secret Change Policy Bypassed",
|
subjectLine: "Infisical Secret Change Policy Bypassed",
|
||||||
@@ -1446,7 +1461,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretApprovalPolicyDAL,
|
secretApprovalPolicyDAL,
|
||||||
secretApprovalRequest,
|
secretApprovalRequest,
|
||||||
smtpService,
|
smtpService,
|
||||||
projectId
|
projectId,
|
||||||
|
notificationService
|
||||||
});
|
});
|
||||||
|
|
||||||
return secretApprovalRequest;
|
return secretApprovalRequest;
|
||||||
@@ -1813,7 +1829,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretApprovalPolicyDAL,
|
secretApprovalPolicyDAL,
|
||||||
secretApprovalRequest,
|
secretApprovalRequest,
|
||||||
smtpService,
|
smtpService,
|
||||||
projectId
|
projectId,
|
||||||
|
notificationService
|
||||||
});
|
});
|
||||||
return secretApprovalRequest;
|
return secretApprovalRequest;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
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 { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
@@ -28,6 +30,7 @@ type TSecretRotationV2QueueServiceFactoryDep = {
|
|||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
|
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const secretRotationV2QueueServiceFactory = async ({
|
export const secretRotationV2QueueServiceFactory = async ({
|
||||||
@@ -36,7 +39,8 @@ export const secretRotationV2QueueServiceFactory = async ({
|
|||||||
secretRotationV2Service,
|
secretRotationV2Service,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
smtpService
|
smtpService,
|
||||||
|
notificationService
|
||||||
}: TSecretRotationV2QueueServiceFactoryDep) => {
|
}: TSecretRotationV2QueueServiceFactoryDep) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
@@ -152,6 +156,19 @@ export const secretRotationV2QueueServiceFactory = async ({
|
|||||||
|
|
||||||
const rotationType = SECRET_ROTATION_NAME_MAP[type as SecretRotation];
|
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({
|
await smtpService.sendMail({
|
||||||
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
|
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
|
||||||
template: SmtpTemplates.SecretRotationFailed,
|
template: SmtpTemplates.SecretRotationFailed,
|
||||||
@@ -165,9 +182,7 @@ export const secretRotationV2QueueServiceFactory = async ({
|
|||||||
secretPath: folder.path,
|
secretPath: folder.path,
|
||||||
environment: environment.name,
|
environment: environment.name,
|
||||||
projectName: project.name,
|
projectName: project.name,
|
||||||
rotationUrl: encodeURI(
|
rotationUrl: encodeURI(`${appCfg.SITE_URL}${rotationPath}`)
|
||||||
`${appCfg.SITE_URL}/projects/secret-management/${projectId}/secrets/${environment.slug}`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import { decryptAppConnection } from "@app/services/app-connection/app-connectio
|
|||||||
import { TAppConnection } from "@app/services/app-connection/app-connection-types";
|
import { TAppConnection } from "@app/services/app-connection/app-connection-types";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
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 { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
@@ -52,6 +54,7 @@ type TSecretRotationV2QueueServiceFactoryDep = {
|
|||||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
|
||||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||||
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "getItem">;
|
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "getItem">;
|
||||||
|
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretScanningV2QueueServiceFactory = Awaited<ReturnType<typeof secretScanningV2QueueServiceFactory>>;
|
export type TSecretScanningV2QueueServiceFactory = Awaited<ReturnType<typeof secretScanningV2QueueServiceFactory>>;
|
||||||
@@ -65,7 +68,8 @@ export const secretScanningV2QueueServiceFactory = async ({
|
|||||||
kmsService,
|
kmsService,
|
||||||
auditLogService,
|
auditLogService,
|
||||||
keyStore,
|
keyStore,
|
||||||
appConnectionDAL
|
appConnectionDAL,
|
||||||
|
notificationService
|
||||||
}: TSecretRotationV2QueueServiceFactoryDep) => {
|
}: TSecretRotationV2QueueServiceFactoryDep) => {
|
||||||
const queueDataSourceFullScan = async (
|
const queueDataSourceFullScan = async (
|
||||||
dataSource: TSecretScanningDataSourceWithConnection,
|
dataSource: TSecretScanningDataSourceWithConnection,
|
||||||
@@ -592,16 +596,38 @@ export const secretScanningV2QueueServiceFactory = async ({
|
|||||||
|
|
||||||
const timestamp = new Date().toISOString();
|
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({
|
await smtpService.sendMail({
|
||||||
recipients: recipients.map((member) => member.user.email!).filter(Boolean),
|
recipients: recipients.map((member) => member.user.email!).filter(Boolean),
|
||||||
template:
|
template:
|
||||||
payload.status === SecretScanningScanStatus.Completed
|
payload.status === SecretScanningScanStatus.Completed
|
||||||
? SmtpTemplates.SecretScanningV2SecretsDetected
|
? SmtpTemplates.SecretScanningV2SecretsDetected
|
||||||
: SmtpTemplates.SecretScanningV2ScanFailed,
|
: SmtpTemplates.SecretScanningV2ScanFailed,
|
||||||
subjectLine:
|
subjectLine,
|
||||||
payload.status === SecretScanningScanStatus.Completed
|
|
||||||
? "Incident Alert: Secret(s) Leaked"
|
|
||||||
: `Secret Scanning Failed`,
|
|
||||||
substitutions:
|
substitutions:
|
||||||
payload.status === SecretScanningScanStatus.Completed
|
payload.status === SecretScanningScanStatus.Completed
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -231,6 +231,8 @@ export type TQueueJobTypes = {
|
|||||||
[QueueName.ImportSecretsFromExternalSource]: {
|
[QueueName.ImportSecretsFromExternalSource]: {
|
||||||
name: QueueJobs.ImportSecretsFromExternalSource;
|
name: QueueJobs.ImportSecretsFromExternalSource;
|
||||||
payload: {
|
payload: {
|
||||||
|
orgId: string;
|
||||||
|
actorId: string;
|
||||||
actorEmail: string;
|
actorEmail: string;
|
||||||
importType: ExternalPlatforms;
|
importType: ExternalPlatforms;
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -773,7 +773,8 @@ export const registerRoutes = async (
|
|||||||
orgDAL,
|
orgDAL,
|
||||||
totpService,
|
totpService,
|
||||||
orgMembershipDAL,
|
orgMembershipDAL,
|
||||||
auditLogService
|
auditLogService,
|
||||||
|
notificationService
|
||||||
});
|
});
|
||||||
const passwordService = authPaswordServiceFactory({
|
const passwordService = authPaswordServiceFactory({
|
||||||
tokenService,
|
tokenService,
|
||||||
@@ -887,7 +888,8 @@ export const registerRoutes = async (
|
|||||||
projectDAL,
|
projectDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
projectMembershipDAL
|
projectMembershipDAL,
|
||||||
|
notificationService
|
||||||
});
|
});
|
||||||
|
|
||||||
const rateLimitService = rateLimitServiceFactory({
|
const rateLimitService = rateLimitServiceFactory({
|
||||||
@@ -926,7 +928,8 @@ export const registerRoutes = async (
|
|||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
secretReminderRecipientsDAL,
|
secretReminderRecipientsDAL,
|
||||||
licenseService
|
licenseService,
|
||||||
|
notificationService
|
||||||
});
|
});
|
||||||
const projectUserAdditionalPrivilegeService = projectUserAdditionalPrivilegeServiceFactory({
|
const projectUserAdditionalPrivilegeService = projectUserAdditionalPrivilegeServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
@@ -1145,7 +1148,8 @@ export const registerRoutes = async (
|
|||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
gatewayService,
|
gatewayService,
|
||||||
gatewayV2Service
|
gatewayV2Service,
|
||||||
|
notificationService
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretQueueService = secretQueueFactory({
|
const secretQueueService = secretQueueFactory({
|
||||||
@@ -1229,7 +1233,8 @@ export const registerRoutes = async (
|
|||||||
projectTemplateService,
|
projectTemplateService,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
reminderService
|
reminderService,
|
||||||
|
notificationService
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectEnvService = projectEnvServiceFactory({
|
const projectEnvService = projectEnvServiceFactory({
|
||||||
@@ -1363,7 +1368,8 @@ export const registerRoutes = async (
|
|||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
projectMicrosoftTeamsConfigDAL,
|
projectMicrosoftTeamsConfigDAL,
|
||||||
microsoftTeamsService,
|
microsoftTeamsService,
|
||||||
folderCommitService
|
folderCommitService,
|
||||||
|
notificationService
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretService = secretServiceFactory({
|
const secretService = secretServiceFactory({
|
||||||
@@ -1814,7 +1820,8 @@ export const registerRoutes = async (
|
|||||||
secretV2BridgeService,
|
secretV2BridgeService,
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
folderCommitService,
|
folderCommitService,
|
||||||
folderVersionDAL
|
folderVersionDAL,
|
||||||
|
notificationService
|
||||||
});
|
});
|
||||||
|
|
||||||
const migrationService = externalMigrationServiceFactory({
|
const migrationService = externalMigrationServiceFactory({
|
||||||
@@ -2017,7 +2024,8 @@ export const registerRoutes = async (
|
|||||||
queueService,
|
queueService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
smtpService
|
smtpService,
|
||||||
|
notificationService
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretScanningV2Queue = await secretScanningV2QueueServiceFactory({
|
const secretScanningV2Queue = await secretScanningV2QueueServiceFactory({
|
||||||
@@ -2029,7 +2037,8 @@ export const registerRoutes = async (
|
|||||||
smtpService,
|
smtpService,
|
||||||
kmsService,
|
kmsService,
|
||||||
keyStore,
|
keyStore,
|
||||||
appConnectionDAL
|
appConnectionDAL,
|
||||||
|
notificationService
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretScanningV2Service = secretScanningV2ServiceFactory({
|
const secretScanningV2Service = secretScanningV2ServiceFactory({
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import { z } from "zod";
|
|||||||
import { UserNotificationsSchema } from "@app/db/schemas/user-notifications";
|
import { UserNotificationsSchema } from "@app/db/schemas/user-notifications";
|
||||||
import { UnauthorizedError } from "@app/lib/errors";
|
import { UnauthorizedError } from "@app/lib/errors";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
export const registerNotificationRouter = async (server: FastifyZodProvider) => {
|
export const registerNotificationRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@@ -97,6 +99,16 @@ export const registerNotificationRouter = async (server: FastifyZodProvider) =>
|
|||||||
...req.body
|
...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 };
|
return { notification };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
|||||||
|
|
||||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||||
import { TokenType } from "../auth-token/auth-token-types";
|
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 { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { getDefaultOrgMembershipRole } from "../org/org-role-fns";
|
import { getDefaultOrgMembershipRole } from "../org/org-role-fns";
|
||||||
import { TOrgMembershipDALFactory } from "../org-membership/org-membership-dal";
|
import { TOrgMembershipDALFactory } from "../org-membership/org-membership-dal";
|
||||||
@@ -47,6 +49,7 @@ type TAuthLoginServiceFactoryDep = {
|
|||||||
totpService: Pick<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">;
|
totpService: Pick<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">;
|
||||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||||
orgMembershipDAL: TOrgMembershipDALFactory;
|
orgMembershipDAL: TOrgMembershipDALFactory;
|
||||||
|
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
|
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
|
||||||
@@ -57,7 +60,8 @@ export const authLoginServiceFactory = ({
|
|||||||
orgDAL,
|
orgDAL,
|
||||||
orgMembershipDAL,
|
orgMembershipDAL,
|
||||||
totpService,
|
totpService,
|
||||||
auditLogService
|
auditLogService,
|
||||||
|
notificationService
|
||||||
}: TAuthLoginServiceFactoryDep) => {
|
}: TAuthLoginServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
* Private
|
* Private
|
||||||
@@ -71,6 +75,16 @@ export const authLoginServiceFactory = ({
|
|||||||
if (!isDeviceSeen) {
|
if (!isDeviceSeen) {
|
||||||
const newDeviceList = devices.concat([{ ip, userAgent }]);
|
const newDeviceList = devices.concat([{ ip, userAgent }]);
|
||||||
await userDAL.updateById(user.id, { devices: JSON.stringify(newDeviceList) }, tx);
|
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) {
|
if (user.email) {
|
||||||
await smtpService.sendMail({
|
await smtpService.sendMail({
|
||||||
template: SmtpTemplates.NewDeviceJoin,
|
template: SmtpTemplates.NewDeviceJoin,
|
||||||
@@ -563,6 +577,18 @@ export const authLoginServiceFactory = ({
|
|||||||
.filter(Boolean) as string[];
|
.filter(Boolean) as string[];
|
||||||
|
|
||||||
if (adminEmails.length > 0) {
|
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({
|
await smtpService.sendMail({
|
||||||
recipients: adminEmails,
|
recipients: adminEmails,
|
||||||
subjectLine: "Security Alert: Admin SSO Bypass",
|
subjectLine: "Security Alert: Admin SSO Bypass",
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
|||||||
|
|
||||||
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||||
import { TKmsServiceFactory } from "../kms/kms-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 { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectServiceFactory } from "../project/project-service";
|
import { TProjectServiceFactory } from "../project/project-service";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
@@ -42,6 +44,7 @@ export type TExternalMigrationQueueFactoryDep = {
|
|||||||
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "create">;
|
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "create">;
|
||||||
|
|
||||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||||
|
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TExternalMigrationQueueFactory = ReturnType<typeof externalMigrationQueueFactory>;
|
export type TExternalMigrationQueueFactory = ReturnType<typeof externalMigrationQueueFactory>;
|
||||||
@@ -62,9 +65,12 @@ export const externalMigrationQueueFactory = ({
|
|||||||
folderDAL,
|
folderDAL,
|
||||||
folderCommitService,
|
folderCommitService,
|
||||||
folderVersionDAL,
|
folderVersionDAL,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL,
|
||||||
|
notificationService
|
||||||
}: TExternalMigrationQueueFactoryDep) => {
|
}: TExternalMigrationQueueFactoryDep) => {
|
||||||
const startImport = async (dto: {
|
const startImport = async (dto: {
|
||||||
|
orgId: string;
|
||||||
|
actorId: string;
|
||||||
actorEmail: string;
|
actorEmail: string;
|
||||||
importType: ExternalPlatforms;
|
importType: ExternalPlatforms;
|
||||||
data: {
|
data: {
|
||||||
@@ -87,9 +93,19 @@ export const externalMigrationQueueFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
queueService.start(QueueName.ImportSecretsFromExternalSource, async (job) => {
|
queueService.start(QueueName.ImportSecretsFromExternalSource, async (job) => {
|
||||||
const { data, actorEmail, importType } = job.data;
|
const { data, actorEmail, importType, actorId, orgId } = job.data;
|
||||||
|
|
||||||
try {
|
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({
|
await smtpService.sendMail({
|
||||||
recipients: [actorEmail],
|
recipients: [actorEmail],
|
||||||
subjectLine: "Infisical import started",
|
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({
|
await smtpService.sendMail({
|
||||||
recipients: [actorEmail],
|
recipients: [actorEmail],
|
||||||
subjectLine: "Infisical import successful",
|
subjectLine: "Infisical import successful",
|
||||||
@@ -146,6 +172,17 @@ export const externalMigrationQueueFactory = ({
|
|||||||
template: SmtpTemplates.ExternalImportSuccessful
|
template: SmtpTemplates.ExternalImportSuccessful
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} 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({
|
await smtpService.sendMail({
|
||||||
recipients: [job.data.actorEmail],
|
recipients: [job.data.actorEmail],
|
||||||
subjectLine: "Infisical import failed",
|
subjectLine: "Infisical import failed",
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ export const externalMigrationServiceFactory = ({
|
|||||||
const encrypted = crypto.encryption().symmetric().encryptWithRootEncryptionKey(stringifiedJson);
|
const encrypted = crypto.encryption().symmetric().encryptWithRootEncryptionKey(stringifiedJson);
|
||||||
|
|
||||||
await externalMigrationQueue.startImport({
|
await externalMigrationQueue.startImport({
|
||||||
|
actorId: user.id,
|
||||||
|
orgId: actorOrgId,
|
||||||
actorEmail: user.email!,
|
actorEmail: user.email!,
|
||||||
importType: ExternalPlatforms.EnvKey,
|
importType: ExternalPlatforms.EnvKey,
|
||||||
data: {
|
data: {
|
||||||
@@ -131,6 +133,8 @@ export const externalMigrationServiceFactory = ({
|
|||||||
const encrypted = crypto.encryption().symmetric().encryptWithRootEncryptionKey(stringifiedJson);
|
const encrypted = crypto.encryption().symmetric().encryptWithRootEncryptionKey(stringifiedJson);
|
||||||
|
|
||||||
await externalMigrationQueue.startImport({
|
await externalMigrationQueue.startImport({
|
||||||
|
actorId: user.id,
|
||||||
|
orgId: actorOrgId,
|
||||||
actorEmail: user.email!,
|
actorEmail: user.email!,
|
||||||
importType: ExternalPlatforms.Vault,
|
importType: ExternalPlatforms.Vault,
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -1,6 +1,21 @@
|
|||||||
export enum NotificationType {
|
export enum NotificationType {
|
||||||
ACCESS_APPROVAL_REQUEST = "access-approval-request",
|
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 {
|
export interface TCreateUserNotificationDTO {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/
|
|||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
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 { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||||
@@ -20,6 +22,7 @@ type TOrgAdminServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create" | "delete">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create" | "delete">;
|
||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
|
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOrgAdminServiceFactory = ReturnType<typeof orgAdminServiceFactory>;
|
export type TOrgAdminServiceFactory = ReturnType<typeof orgAdminServiceFactory>;
|
||||||
@@ -29,7 +32,8 @@ export const orgAdminServiceFactory = ({
|
|||||||
projectDAL,
|
projectDAL,
|
||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
smtpService
|
smtpService,
|
||||||
|
notificationService
|
||||||
}: TOrgAdminServiceFactoryDep) => {
|
}: TOrgAdminServiceFactoryDep) => {
|
||||||
const listOrgProjects = async ({
|
const listOrgProjects = async ({
|
||||||
actor,
|
actor,
|
||||||
@@ -130,23 +134,34 @@ export const orgAdminServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
|
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
|
||||||
const filteredProjectMembers = projectMembers
|
const projectAdmins = projectMembers.filter(
|
||||||
.filter(
|
(member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin) && member.userId !== actorId
|
||||||
(member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin) && member.userId !== actorId
|
);
|
||||||
)
|
const mappedProjectAdmins = projectAdmins.map((el) => el.user.email!).filter(Boolean);
|
||||||
.map((el) => el.user.email!)
|
const actorEmail = projectMembers.find((el) => el.userId === actorId)?.user?.username;
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
if (filteredProjectMembers.length) {
|
if (actorEmail) {
|
||||||
await smtpService.sendMail({
|
await notificationService.createUserNotifications(
|
||||||
template: SmtpTemplates.OrgAdminProjectDirectAccess,
|
projectAdmins.map((member) => ({
|
||||||
recipients: filteredProjectMembers,
|
userId: member.userId,
|
||||||
subjectLine: "Organization Admin Project Direct Access Issued",
|
orgId: project.orgId,
|
||||||
substitutions: {
|
type: NotificationType.DIRECT_PROJECT_ACCESS_ISSUED_TO_ADMIN,
|
||||||
projectName: project.name,
|
title: "Direct Project Access Issued",
|
||||||
email: projectMembers.find((el) => el.userId === actorId)?.user?.username
|
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 };
|
return { isExistingMember: false, membership: updatedMembership };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { ms } from "@app/lib/ms";
|
|||||||
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
|
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
|
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 { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||||
@@ -56,6 +58,7 @@ type TProjectMembershipServiceFactoryDep = {
|
|||||||
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||||
secretReminderRecipientsDAL: Pick<TSecretReminderRecipientsDALFactory, "delete">;
|
secretReminderRecipientsDAL: Pick<TSecretReminderRecipientsDALFactory, "delete">;
|
||||||
groupProjectDAL: TGroupProjectDALFactory;
|
groupProjectDAL: TGroupProjectDALFactory;
|
||||||
|
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TProjectMembershipServiceFactory = ReturnType<typeof projectMembershipServiceFactory>;
|
export type TProjectMembershipServiceFactory = ReturnType<typeof projectMembershipServiceFactory>;
|
||||||
@@ -74,7 +77,8 @@ export const projectMembershipServiceFactory = ({
|
|||||||
projectDAL,
|
projectDAL,
|
||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
secretReminderRecipientsDAL,
|
secretReminderRecipientsDAL,
|
||||||
licenseService
|
licenseService,
|
||||||
|
notificationService
|
||||||
}: TProjectMembershipServiceFactoryDep) => {
|
}: TProjectMembershipServiceFactoryDep) => {
|
||||||
const getProjectMemberships = async ({
|
const getProjectMemberships = async ({
|
||||||
actorId,
|
actorId,
|
||||||
@@ -236,6 +240,16 @@ export const projectMembershipServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (sendEmails) {
|
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();
|
const appCfg = getConfig();
|
||||||
await smtpService.sendMail({
|
await smtpService.sendMail({
|
||||||
template: SmtpTemplates.WorkspaceInvite,
|
template: SmtpTemplates.WorkspaceInvite,
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ import { TKmsServiceFactory } from "../kms/kms-service";
|
|||||||
import { validateMicrosoftTeamsChannelsSchema } from "../microsoft-teams/microsoft-teams-fns";
|
import { validateMicrosoftTeamsChannelsSchema } from "../microsoft-teams/microsoft-teams-fns";
|
||||||
import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal";
|
import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal";
|
||||||
import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-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 { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
|
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
|
||||||
import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal";
|
import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal";
|
||||||
@@ -183,6 +185,7 @@ type TProjectServiceFactoryDep = {
|
|||||||
>;
|
>;
|
||||||
projectTemplateService: TProjectTemplateServiceFactory;
|
projectTemplateService: TProjectTemplateServiceFactory;
|
||||||
reminderService: Pick<TReminderServiceFactory, "deleteReminderBySecretId">;
|
reminderService: Pick<TReminderServiceFactory, "deleteReminderBySecretId">;
|
||||||
|
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
|
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
|
||||||
@@ -227,7 +230,8 @@ export const projectServiceFactory = ({
|
|||||||
projectTemplateService,
|
projectTemplateService,
|
||||||
groupProjectDAL,
|
groupProjectDAL,
|
||||||
smtpService,
|
smtpService,
|
||||||
reminderService
|
reminderService,
|
||||||
|
notificationService
|
||||||
}: TProjectServiceFactoryDep) => {
|
}: TProjectServiceFactoryDep) => {
|
||||||
/*
|
/*
|
||||||
* Create workspace. Make user the admin
|
* Create workspace. Make user the admin
|
||||||
@@ -1924,6 +1928,21 @@ export const projectServiceFactory = ({
|
|||||||
projectTypeUrl = "cert-management";
|
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({
|
await smtpService.sendMail({
|
||||||
template: SmtpTemplates.ProjectAccessRequest,
|
template: SmtpTemplates.ProjectAccessRequest,
|
||||||
recipients: filteredProjectMembers,
|
recipients: filteredProjectMembers,
|
||||||
@@ -1934,7 +1953,7 @@ export const projectServiceFactory = ({
|
|||||||
projectName: project?.name,
|
projectName: project?.name,
|
||||||
orgName: org?.name,
|
orgName: org?.name,
|
||||||
note: comment,
|
note: comment,
|
||||||
callback_url: `${appCfg.SITE_URL}/projects/${projectTypeUrl}/${project.id}/access-management?selectedTab=members&requesterEmail=${userDetails.email}`
|
callback_url: `${appCfg.SITE_URL}${callbackPath}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
|||||||
|
|
||||||
import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
|
import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
|
||||||
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
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>;
|
export type TSecretSyncQueueFactory = ReturnType<typeof secretSyncQueueFactory>;
|
||||||
|
|
||||||
@@ -100,6 +102,7 @@ type TSecretSyncQueueFactoryDep = {
|
|||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
|
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
|
||||||
|
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SecretSyncActionJob = Job<
|
type SecretSyncActionJob = Job<
|
||||||
@@ -142,7 +145,8 @@ export const secretSyncQueueFactory = ({
|
|||||||
folderCommitService,
|
folderCommitService,
|
||||||
licenseService,
|
licenseService,
|
||||||
gatewayService,
|
gatewayService,
|
||||||
gatewayV2Service
|
gatewayV2Service,
|
||||||
|
notificationService
|
||||||
}: TSecretSyncQueueFactoryDep) => {
|
}: TSecretSyncQueueFactoryDep) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
@@ -898,6 +902,19 @@ export const secretSyncQueueFactory = ({
|
|||||||
break;
|
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({
|
await smtpService.sendMail({
|
||||||
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
|
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
|
||||||
template: SmtpTemplates.SecretSyncFailed,
|
template: SmtpTemplates.SecretSyncFailed,
|
||||||
@@ -910,7 +927,7 @@ export const secretSyncQueueFactory = ({
|
|||||||
secretPath: folder?.path,
|
secretPath: folder?.path,
|
||||||
environment: environment?.name,
|
environment: environment?.name,
|
||||||
projectName: project.name,
|
projectName: project.name,
|
||||||
syncUrl: `${appCfg.SITE_URL}/projects/secret-management/${projectId}/integrations/secret-syncs/${destination}/${secretSync.id}`
|
syncUrl: `${appCfg.SITE_URL}${syncPath}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export enum PostHogEventTypes {
|
|||||||
IssueSshHostHostCert = "Issue SSH Host Host Certificate",
|
IssueSshHostHostCert = "Issue SSH Host Host Certificate",
|
||||||
SignCert = "Sign PKI Certificate",
|
SignCert = "Sign PKI Certificate",
|
||||||
IssueCert = "Issue PKI Certificate",
|
IssueCert = "Issue PKI Certificate",
|
||||||
InvalidateCache = "Invalidate Cache"
|
InvalidateCache = "Invalidate Cache",
|
||||||
|
NotificationUpdated = "Notification Updated"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TSecretModifiedEvent = {
|
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 } & (
|
export type TPostHogEvent = { distinctId: string; organizationId?: string } & (
|
||||||
| TSecretModifiedEvent
|
| TSecretModifiedEvent
|
||||||
| TAdminInitEvent
|
| TAdminInitEvent
|
||||||
@@ -251,4 +260,5 @@ export type TPostHogEvent = { distinctId: string; organizationId?: string } & (
|
|||||||
| TSignCertificateEvent
|
| TSignCertificateEvent
|
||||||
| TIssueCertificateEvent
|
| TIssueCertificateEvent
|
||||||
| TInvalidateCacheEvent
|
| TInvalidateCacheEvent
|
||||||
|
| TNotificationUpdatedEvent
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,7 +26,11 @@ export const Notification = ({ notification, onDelete }: Props) => {
|
|||||||
{!notification.isRead && (
|
{!notification.isRead && (
|
||||||
<FontAwesomeIcon icon={faCircle} className="mt-1.5 size-2 text-yellow-400" />
|
<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">
|
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium leading-5 text-mineshaft-100">
|
||||||
<Markdown components={{ p: "span" }}>{notification.title}</Markdown>
|
<Markdown components={{ p: "span" }}>{notification.title}</Markdown>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const NotificationDropdown = () => {
|
|||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align="end"
|
align="end"
|
||||||
side="bottom"
|
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 w-full flex-col">
|
||||||
<div className="flex items-center justify-between border-b border-mineshaft-500 px-3 py-2">
|
<div className="flex items-center justify-between border-b border-mineshaft-500 px-3 py-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user