From b18f7b957d8c70ddef9d02fe4976e252e59fe93a Mon Sep 17 00:00:00 2001 From: = Date: Wed, 2 Apr 2025 22:23:02 +0530 Subject: [PATCH] feat: added backend logic for project access, search project endpoint, send mail for org admin project access direct --- ...50313124706_add-privilege-upgrade-field.ts | 1 + backend/src/db/schemas/models.ts | 5 + .../ee/services/audit-log/audit-log-types.ts | 14 ++- backend/src/lib/validator/validate-string.ts | 6 +- backend/src/server/routes/index.ts | 4 +- .../src/server/routes/v1/project-router.ts | 107 +++++++++++++++++- .../services/org-admin/org-admin-service.ts | 27 ++++- backend/src/services/org/org-service.ts | 2 +- backend/src/services/project/project-dal.ts | 82 +++++++++++++- .../src/services/project/project-service.ts | 98 +++++++++++++++- backend/src/services/project/project-types.ts | 24 +++- backend/src/services/smtp/smtp-service.ts | 4 +- .../accessApprovalRequest.handlebars | 2 +- .../orgAdminProjectGrantAccess.handlebars | 16 +++ .../smtp/templates/projectAccess.handlebars | 26 +++++ 15 files changed, 397 insertions(+), 21 deletions(-) create mode 100644 backend/src/services/smtp/templates/orgAdminProjectGrantAccess.handlebars create mode 100644 backend/src/services/smtp/templates/projectAccess.handlebars diff --git a/backend/src/db/migrations/20250313124706_add-privilege-upgrade-field.ts b/backend/src/db/migrations/20250313124706_add-privilege-upgrade-field.ts index 9823f4d8e..5a16666af 100644 --- a/backend/src/db/migrations/20250313124706_add-privilege-upgrade-field.ts +++ b/backend/src/db/migrations/20250313124706_add-privilege-upgrade-field.ts @@ -1,4 +1,5 @@ import { Knex } from "knex"; + import { TableName } from "../schemas"; export async function up(knex: Knex): Promise { diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index e626443c3..f58cbe400 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -233,3 +233,8 @@ export enum ActionProjectType { // project operations that happen on all types Any = "any" } + +export enum SortDirection { + ASC = "asc", + DESC = "desc" +} diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 5e7df9444..509d6754e 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -285,7 +285,9 @@ export enum EventType { KMIP_OPERATION_ACTIVATE = "kmip-operation-activate", KMIP_OPERATION_REVOKE = "kmip-operation-revoke", KMIP_OPERATION_LOCATE = "kmip-operation-locate", - KMIP_OPERATION_REGISTER = "kmip-operation-register" + KMIP_OPERATION_REGISTER = "kmip-operation-register", + + PROJECT_ACCESS_REQUEST = "project-access-request" } export const filterableSecretEvents: EventType[] = [ @@ -2277,6 +2279,15 @@ interface KmipOperationRegisterEvent { }; } +interface ProjectAccessRequestEvent { + type: EventType.PROJECT_ACCESS_REQUEST; + metadata: { + projectId: string; + requesterId: string; + requesterEmail: string; + }; +} + interface SetupKmipEvent { type: EventType.SETUP_KMIP; metadata: { @@ -2511,5 +2522,6 @@ export type Event = | KmipOperationRevokeEvent | KmipOperationLocateEvent | KmipOperationRegisterEvent + | ProjectAccessRequestEvent | CreateSecretRequestEvent | SecretApprovalRequestReview; diff --git a/backend/src/lib/validator/validate-string.ts b/backend/src/lib/validator/validate-string.ts index 57bc052f8..3c67be7c1 100644 --- a/backend/src/lib/validator/validate-string.ts +++ b/backend/src/lib/validator/validate-string.ts @@ -36,7 +36,8 @@ export enum CharacterType { DoubleQuote = "doubleQuote", // " Comma = "comma", // , Semicolon = "semicolon", // ; - Exclamation = "exclamation" // ! + Exclamation = "exclamation", // ! + Fullstop = "fullStop" // ! } /** @@ -81,7 +82,8 @@ export const characterValidator = (allowedCharacters: CharacterType[]) => { [CharacterType.DoubleQuote]: '\\"', [CharacterType.Comma]: ",", [CharacterType.Semicolon]: ";", - [CharacterType.Exclamation]: "!" + [CharacterType.Exclamation]: "!", + [CharacterType.Fullstop]: "." }; // Combine patterns from allowed characters diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index dac88791f..977a3b921 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -662,6 +662,7 @@ export const registerRoutes = async ( }); const orgAdminService = orgAdminServiceFactory({ + smtpService, projectDAL, permissionService, projectUserMembershipRoleDAL, @@ -964,7 +965,8 @@ export const registerRoutes = async ( projectSlackConfigDAL, slackIntegrationDAL, projectTemplateService, - groupProjectDAL + groupProjectDAL, + smtpService }); const projectEnvService = projectEnvServiceFactory({ diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index b44e93a66..1cff95a8f 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -8,15 +8,17 @@ import { ProjectSlackConfigsSchema, ProjectType, SecretFoldersSchema, + SortDirection, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { PROJECTS } from "@app/lib/api-docs"; +import { CharacterType, characterValidator } from "@app/lib/validator/validate-string"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { AuthMode } from "@app/services/auth/auth-type"; -import { ProjectFilterType } from "@app/services/project/project-types"; +import { ActorType, AuthMode } from "@app/services/auth/auth-type"; +import { ProjectFilterType, SearchProjectSortBy } from "@app/services/project/project-types"; import { validateSlackChannelsField } from "@app/services/slack/slack-auth-validators"; import { integrationAuthPubSchema, SanitizedProjectSchema } from "../sanitizedSchemas"; @@ -704,4 +706,105 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { return environmentsFolders; } }); + + server.route({ + method: "POST", + url: "/search", + config: { + rateLimit: readLimit + }, + schema: { + body: z.object({ + limit: z.number().default(100), + offset: z.number().default(0), + type: z.nativeEnum(ProjectType).optional(), + orderBy: z.nativeEnum(SearchProjectSortBy).optional().default(SearchProjectSortBy.NAME), + orderDirection: z.nativeEnum(SortDirection).optional().default(SortDirection.ASC), + name: z + .string() + .trim() + .refine((val) => characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen])(val), { + message: "Invalid pattern: only alphanumeric characters, spaces, - are allowed." + }) + .optional() + }), + response: { + 200: z.object({ + projects: SanitizedProjectSchema.extend({ isMember: z.boolean() }).array(), + totalCount: z.number() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const { docs: projects, totalCount } = await server.services.project.searchProjects({ + permission: req.permission, + ...req.body + }); + + return { projects, totalCount }; + } + }); + + server.route({ + method: "POST", + url: "/:workspaceId/project-access", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + workspaceId: z.string().trim() + }), + body: z.object({ + comment: z + .string() + .trim() + .refine( + (val) => + characterValidator([ + CharacterType.AlphaNumeric, + CharacterType.Hyphen, + CharacterType.Comma, + CharacterType.Fullstop, + CharacterType.Spaces, + CharacterType.Exclamation + ])(val), + { + message: "Invalid pattern: only alphanumeric characters, spaces, - are allowed." + } + ) + }), + response: { + 200: z.object({ + message: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + await server.services.project.requestProjectAccess({ + permission: req.permission, + comment: req.body.comment, + projectId: req.params.workspaceId + }); + + if (req.auth.actor === ActorType.USER) { + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.params.workspaceId, + event: { + type: EventType.PROJECT_ACCESS_REQUEST, + metadata: { + projectId: req.params.workspaceId, + requesterEmail: req.auth.user.email || req.auth.user.username, + requesterId: req.auth.userId + } + } + }); + } + + return { message: "Project access request has been send to project admins" }; + } + }); }; diff --git a/backend/src/services/org-admin/org-admin-service.ts b/backend/src/services/org-admin/org-admin-service.ts index c9f792978..15ad09e46 100644 --- a/backend/src/services/org-admin/org-admin-service.ts +++ b/backend/src/services/org-admin/org-admin-service.ts @@ -12,17 +12,22 @@ import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; +import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TUserDALFactory } from "../user/user-dal"; import { TAccessProjectDTO, TListOrgProjectsDTO } from "./org-admin-types"; type TOrgAdminServiceFactoryDep = { permissionService: Pick; projectDAL: Pick; - projectMembershipDAL: Pick; + projectMembershipDAL: Pick< + TProjectMembershipDALFactory, + "findOne" | "create" | "transaction" | "delete" | "findAllProjectMembers" + >; projectKeyDAL: Pick; projectBotDAL: Pick; userDAL: Pick; projectUserMembershipRoleDAL: Pick; + smtpService: Pick; }; export type TOrgAdminServiceFactory = ReturnType; @@ -34,7 +39,8 @@ export const orgAdminServiceFactory = ({ projectKeyDAL, projectBotDAL, userDAL, - projectUserMembershipRoleDAL + projectUserMembershipRoleDAL, + smtpService }: TOrgAdminServiceFactoryDep) => { const listOrgProjects = async ({ actor, @@ -184,6 +190,23 @@ export const orgAdminServiceFactory = ({ ); return newProjectMembership; }); + + 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!); + + 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 + } + }); return { isExistingMember: false, membership: updatedMembership }; }; diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 1868871f3..98b35f68f 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -231,7 +231,7 @@ export const orgServiceFactory = ({ const findAllWorkspaces = async ({ actor, actorId, orgId, type }: TFindAllWorkspacesDTO) => { if (actor === ActorType.USER) { - const workspaces = await projectDAL.findAllProjects(actorId, orgId, type || "all"); + const workspaces = await projectDAL.findUserProjects(actorId, orgId, type || "all"); return workspaces; } diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts index 896e1b858..43f1d57e4 100644 --- a/backend/src/services/project/project-dal.ts +++ b/backend/src/services/project/project-dal.ts @@ -6,20 +6,23 @@ import { ProjectType, ProjectUpgradeStatus, ProjectVersion, + SortDirection, TableName, + TProjects, TProjectsUpdate } from "@app/db/schemas"; import { BadRequestError, DatabaseError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; -import { Filter, ProjectFilterType } from "./project-types"; +import { ActorType } from "../auth/auth-type"; +import { Filter, ProjectFilterType, SearchProjectSortBy } from "./project-types"; export type TProjectDALFactory = ReturnType; export const projectDALFactory = (db: TDbClient) => { const projectOrm = ormify(db, TableName.Project); - const findAllProjects = async (userId: string, orgId: string, projectType: ProjectType | "all") => { + const findUserProjects = async (userId: string, orgId: string, projectType: ProjectType | "all") => { try { const workspaces = await db .replicaNode()(TableName.ProjectMembership) @@ -352,9 +355,79 @@ export const projectDALFactory = (db: TDbClient) => { } }; + const searchProjects = async (dto: { + orgId: string; + actor: ActorType; + actorId: string; + type?: ProjectType; + limit?: number; + offset?: number; + name?: string; + sortBy?: SearchProjectSortBy; + sortDir?: SortDirection; + }) => { + const { limit = 20, offset = 0, sortBy = SearchProjectSortBy.NAME, sortDir = SortDirection.ASC } = dto; + + const userMembershipSubquery = db(TableName.ProjectMembership).where({ userId: dto.actorId }).select("projectId"); + const groups = db(TableName.UserGroupMembership).where({ userId: dto.actorId }).select("groupId"); + const groupMembershipSubquery = db(TableName.GroupProjectMembership).whereIn("groupId", groups).select("projectId"); + + const identityMembershipSubQuery = db(TableName.IdentityProjectMembership) + .where({ identityId: dto.actorId }) + .select("projectId"); + + // Get the SQL strings for the subqueries + const userMembershipSql = userMembershipSubquery.toQuery(); + const groupMembershipSql = groupMembershipSubquery.toQuery(); + const identityMembershipSql = identityMembershipSubQuery.toQuery(); + + const query = db + .replicaNode()(TableName.Project) + .where(`${TableName.Project}.orgId`, dto.orgId) + .select(selectAllTableCols(TableName.Project)) + .select(db.raw("COUNT(*) OVER() AS count")) + .select<(TProjects & { isMember: boolean; count: number })[]>( + dto.actor === ActorType.USER + ? db.raw( + ` + CASE + WHEN ${TableName.Project}.id IN (?) THEN TRUE + WHEN ${TableName.Project}.id IN (?) THEN TRUE + ELSE FALSE + END as "isMember" + `, + [db.raw(userMembershipSql), db.raw(groupMembershipSql)] + ) + : db.raw( + ` + CASE + WHEN ${TableName.Project}.id IN (?) THEN TRUE + ELSE FALSE + END as "isMember" + `, + [db.raw(identityMembershipSql)] + ) + ) + .limit(limit) + .offset(offset); + if (sortBy === SearchProjectSortBy.NAME) { + void query.orderBy([{ column: `${TableName.Project}.name`, order: sortDir }]); + } + + if (dto.type) { + void query.where(`${TableName.Project}.type`, dto.type); + } + if (dto.name) { + void query.whereILike(`${TableName.Project}.name`, `%${dto.name}%`); + } + const docs = await query; + + return { docs, totalCount: Number(docs?.[0]?.count ?? 0) }; + }; + return { ...projectOrm, - findAllProjects, + findUserProjects, setProjectUpgradeStatus, findAllProjectsByIdentity, findProjectGhostUser, @@ -363,6 +436,7 @@ export const projectDALFactory = (db: TDbClient) => { findProjectBySlug, findProjectWithOrg, checkProjectUpgradeStatus, - getProjectFromSplitId + getProjectFromSplitId, + searchProjects }; }; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index dbef33c10..58e3f9b54 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -23,6 +23,7 @@ import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-cer import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal"; import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal"; import { TKeyStoreFactory } from "@app/keystore/keystore"; +import { getConfig } from "@app/lib/config/env"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; @@ -57,6 +58,7 @@ import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secr import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal"; import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal"; +import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TUserDALFactory } from "../user/user-dal"; import { TProjectDALFactory } from "./project-dal"; import { assignWorkspaceKeysToMembers, createProjectKey } from "./project-fns"; @@ -76,6 +78,8 @@ import { TListProjectSshCertificatesDTO, TListProjectSshCertificateTemplatesDTO, TLoadProjectKmsBackupDTO, + TProjectAccessRequestDTO, + TSearchProjectsDTO, TToggleProjectAutoCapitalizationDTO, TUpdateAuditLogsRetentionDTO, TUpdateProjectDTO, @@ -106,7 +110,10 @@ type TProjectServiceFactoryDep = { identityProjectDAL: TIdentityProjectDALFactory; identityProjectMembershipRoleDAL: Pick; projectKeyDAL: Pick; - projectMembershipDAL: Pick; + projectMembershipDAL: Pick< + TProjectMembershipDALFactory, + "create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers" + >; groupProjectDAL: Pick; projectSlackConfigDAL: Pick; slackIntegrationDAL: Pick; @@ -123,6 +130,7 @@ type TProjectServiceFactoryDep = { orgService: Pick; licenseService: Pick; queueService: Pick; + smtpService: Pick; orgDAL: Pick; keyStore: Pick; @@ -177,7 +185,8 @@ export const projectServiceFactory = ({ projectSlackConfigDAL, slackIntegrationDAL, projectTemplateService, - groupProjectDAL + groupProjectDAL, + smtpService }: TProjectServiceFactoryDep) => { /* * Create workspace. Make user the admin @@ -506,7 +515,7 @@ export const projectServiceFactory = ({ actorOrgId, type = ProjectType.SecretManager }: TListProjectsDTO) => { - const workspaces = await projectDAL.findAllProjects(actorId, actorOrgId, type); + const workspaces = await projectDAL.findUserProjects(actorId, actorOrgId, type); if (includeRoles) { const { permission } = await permissionService.getUserOrgPermission( @@ -1339,6 +1348,85 @@ export const projectServiceFactory = ({ }); }; + const searchProjects = async ({ + name, + offset, + permission, + limit, + type, + orderBy, + orderDirection + }: TSearchProjectsDTO) => { + // check user belong to org + await permissionService.getOrgPermission( + permission.type, + permission.id, + permission.orgId, + permission.authMethod, + permission.orgId + ); + + return projectDAL.searchProjects({ + limit, + offset, + name, + type, + orgId: permission.orgId, + actor: permission.type, + actorId: permission.id, + sortBy: orderBy, + sortDir: orderDirection + }); + }; + + const requestProjectAccess = async ({ permission, comment, projectId }: TProjectAccessRequestDTO) => { + // check user belong to org + await permissionService.getOrgPermission( + permission.type, + permission.id, + permission.orgId, + permission.authMethod, + permission.orgId + ); + + const projectMember = await permissionService + .getProjectPermission({ + actor: permission.type, + actorId: permission.id, + projectId, + actionProjectType: ActionProjectType.Any, + actorAuthMethod: permission.authMethod, + actorOrgId: permission.orgId + }) + .catch(() => { + return null; + }); + if (projectMember) throw new BadRequestError({ message: "User already has access to the project" }); + + const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId); + const filteredProjectMembers = projectMembers + .filter((member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin)) + .map((el) => el.user.email!); + const org = await orgDAL.findOne({ id: permission.orgId }); + const project = await projectDAL.findById(projectId); + const userDetails = await userDAL.findById(permission.id); + const appCfg = getConfig(); + + await smtpService.sendMail({ + template: SmtpTemplates.ProjectAccessRequest, + recipients: filteredProjectMembers, + subjectLine: "Project Access Request", + substitutions: { + requesterName: `${userDetails.firstName} ${userDetails.lastName}`, + requesterEmail: userDetails.email, + projectName: project?.name, + orgName: org?.name, + note: comment, + callback_url: `${appCfg.SITE_URL}/${project.type}/${project.id}/access-management?selectedTab=members&requesterEmail=${userDetails.email}` + } + }); + }; + return { createProject, deleteProject, @@ -1364,6 +1452,8 @@ export const projectServiceFactory = ({ loadProjectKmsBackup, getProjectKmsKeys, getProjectSlackConfig, - updateProjectSlackConfig + updateProjectSlackConfig, + requestProjectAccess, + searchProjects }; }; diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 5ccf33d23..ce6e76485 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -1,7 +1,7 @@ import { Knex } from "knex"; -import { ProjectType, TProjectKeys } from "@app/db/schemas"; -import { TProjectPermission } from "@app/lib/types"; +import { ProjectType, SortDirection, TProjectKeys } from "@app/db/schemas"; +import { OrgServiceActor, TProjectPermission } from "@app/lib/types"; import { ActorAuthMethod, ActorType } from "../auth/auth-type"; @@ -158,3 +158,23 @@ export type TUpdateProjectSlackConfig = { isSecretRequestNotificationEnabled: boolean; secretRequestChannels: string; } & TProjectPermission; + +export enum SearchProjectSortBy { + NAME = "name" +} + +export type TSearchProjectsDTO = { + permission: OrgServiceActor; + name?: string; + type?: ProjectType; + limit?: number; + offset?: number; + orderBy?: SearchProjectSortBy; + orderDirection?: SortDirection; +}; + +export type TProjectAccessRequestDTO = { + permission: OrgServiceActor; + projectId: string; + comment: string; +}; diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index 68e0ecd22..81416eae8 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -40,7 +40,9 @@ export enum SmtpTemplates { ExternalImportSuccessful = "externalImportSuccessful.handlebars", ExternalImportFailed = "externalImportFailed.handlebars", ExternalImportStarted = "externalImportStarted.handlebars", - SecretRequestCompleted = "secretRequestCompleted.handlebars" + SecretRequestCompleted = "secretRequestCompleted.handlebars", + ProjectAccessRequest = "projectAccess.handlebars", + OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars" } export enum SmtpHost { diff --git a/backend/src/services/smtp/templates/accessApprovalRequest.handlebars b/backend/src/services/smtp/templates/accessApprovalRequest.handlebars index 3c0811a1c..ef11957a7 100644 --- a/backend/src/services/smtp/templates/accessApprovalRequest.handlebars +++ b/backend/src/services/smtp/templates/accessApprovalRequest.handlebars @@ -49,4 +49,4 @@ {{emailFooter}} - \ No newline at end of file + diff --git a/backend/src/services/smtp/templates/orgAdminProjectGrantAccess.handlebars b/backend/src/services/smtp/templates/orgAdminProjectGrantAccess.handlebars new file mode 100644 index 000000000..ef8c6e6b4 --- /dev/null +++ b/backend/src/services/smtp/templates/orgAdminProjectGrantAccess.handlebars @@ -0,0 +1,16 @@ + + + + + + Organization admin issued direct access to project + + + +

Infisical

+

The organization admin {{email}} has granted direct access to the project "{{projectName}}".

+ + {{emailFooter}} + + + diff --git a/backend/src/services/smtp/templates/projectAccess.handlebars b/backend/src/services/smtp/templates/projectAccess.handlebars new file mode 100644 index 000000000..5ff1ca7ec --- /dev/null +++ b/backend/src/services/smtp/templates/projectAccess.handlebars @@ -0,0 +1,26 @@ + + + + + + Project Access Request + + + +

Infisical

+

You have a new project access request!

+
    +
  • Requester Name: "{{requesterName}}"
  • +
  • Requester Email: "{{requesterEmail}}"
  • +
  • Project Name: "{{projectName}}"
  • +
  • Organization Name: "{{orgName}}"
  • +
  • User Note: "{{note}}"
  • +
+

+ Please click on the link below to grant access +

+ Grant Access + {{emailFooter}} + + +