feat: added backend logic for project access, search project endpoint, send mail for org admin project access direct

This commit is contained in:
=
2025-04-03 23:28:18 +05:30
parent e6349474aa
commit b18f7b957d
15 changed files with 397 additions and 21 deletions
+78 -4
View File
@@ -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<typeof projectDALFactory>;
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
};
};
@@ -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<TIdentityProjectMembershipRoleDALFactory, "create">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne" | "delete">;
projectMembershipDAL: Pick<
TProjectMembershipDALFactory,
"create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers"
>;
groupProjectDAL: Pick<TGroupProjectDALFactory, "delete">;
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "findOne" | "transaction" | "updateById" | "create">;
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById" | "findByIdWithWorkflowIntegrationDetails">;
@@ -123,6 +130,7 @@ type TProjectServiceFactoryDep = {
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
queueService: Pick<TQueueServiceFactory, "stopRepeatableJob">;
smtpService: Pick<TSmtpService, "sendMail">;
orgDAL: Pick<TOrgDALFactory, "findOne">;
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
@@ -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
};
};
+22 -2
View File
@@ -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;
};