feat: almost done with most ts stuff

This commit is contained in:
=
2025-10-10 00:09:46 +05:30
parent 44c9575a3d
commit c2490380f0
28 changed files with 551 additions and 1432 deletions
@@ -1,285 +0,0 @@
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import { requestContext } from "@fastify/request-context";
import { ActionProjectType, ProjectMembershipRole, ProjectType, TableName, TProjects } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import {
ProjectPermissionActions,
ProjectPermissionSet,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { TUserDALFactory } from "../user/user-dal";
import { TProjectRoleDALFactory } from "./project-role-dal";
import { getPredefinedRoles } from "./project-role-fns";
import {
ProjectRoleServiceIdentifierType,
TCreateRoleDTO,
TDeleteRoleDTO,
TGetRoleDetailsDTO,
TListRolesDTO,
TUpdateRoleDTO
} from "./project-role-types";
type TProjectRoleServiceFactoryDep = {
projectRoleDAL: TProjectRoleDALFactory;
identityDAL: Pick<TIdentityDALFactory, "findById">;
userDAL: Pick<TUserDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findProjectById">;
permissionService: Pick<
TPermissionServiceFactory,
"getProjectPermission" | "getUserProjectPermission" | "invalidateProjectPermissionCache"
>;
identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory;
projectUserMembershipRoleDAL: TProjectUserMembershipRoleDALFactory;
};
export type TProjectRoleServiceFactory = ReturnType<typeof projectRoleServiceFactory>;
const unpackPermissions = (permissions: unknown) =>
UnpackedPermissionSchema.array().parse(
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
);
export const projectRoleServiceFactory = ({
projectRoleDAL,
permissionService,
identityProjectMembershipRoleDAL,
projectUserMembershipRoleDAL,
projectDAL,
identityDAL,
userDAL
}: TProjectRoleServiceFactoryDep) => {
const createRole = async ({ data, actor, actorId, actorAuthMethod, actorOrgId, filter }: TCreateRoleDTO) => {
let projectId = "";
if (filter.type === ProjectRoleServiceIdentifierType.SLUG) {
const project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: "Project not found" });
projectId = project.id;
} else {
projectId = filter.projectId;
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Role);
const existingRole = await projectRoleDAL.findOne({ slug: data.slug, projectId });
if (existingRole) {
throw new BadRequestError({ name: "Create Role", message: "Project role with same slug already exists" });
}
validateHandlebarTemplate("Project Role Create", JSON.stringify(data.permissions || []), {
allowedExpressions: (val) => val.includes("identity.")
});
const role = await projectRoleDAL.create({
...data,
projectId
});
return { ...role, permissions: unpackPermissions(role.permissions) };
};
const getRoleBySlug = async ({
actor,
actorId,
actorAuthMethod,
actorOrgId,
roleSlug,
filter
}: TGetRoleDetailsDTO) => {
let project: TProjects;
if (filter.type === ProjectRoleServiceIdentifierType.SLUG) {
project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId);
} else {
project = await projectDAL.findProjectById(filter.projectId);
}
if (!project) throw new NotFoundError({ message: "Project not found" });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: project.id,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
if (roleSlug !== "custom" && Object.values(ProjectMembershipRole).includes(roleSlug as ProjectMembershipRole)) {
const [predefinedRole] = getPredefinedRoles({
projectId: project.id,
projectType: project.type as ProjectType,
roleFilter: roleSlug as ProjectMembershipRole
});
if (!predefinedRole) throw new NotFoundError({ message: `Default role with slug '${roleSlug}' not found` });
return { ...predefinedRole, permissions: UnpackedPermissionSchema.array().parse(predefinedRole.permissions) };
}
const customRole = await projectRoleDAL.findOne({ slug: roleSlug, projectId: project.id });
if (!customRole) throw new NotFoundError({ message: `Project role with slug '${roleSlug}' not found` });
return { ...customRole, permissions: unpackPermissions(customRole.permissions) };
};
const updateRole = async ({ roleId, actorOrgId, actorAuthMethod, actorId, actor, data }: TUpdateRoleDTO) => {
const projectRole = await projectRoleDAL.findById(roleId);
if (!projectRole) throw new NotFoundError({ message: "Project role not found", name: "Delete role" });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: projectRole.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Role);
if (data?.slug) {
const existingRole = await projectRoleDAL.findOne({ slug: data.slug, projectId: projectRole.projectId });
if (existingRole && existingRole.id !== roleId)
throw new BadRequestError({ name: "Update Role", message: "Project role with the same slug already exists" });
}
validateHandlebarTemplate("Project Role Update", JSON.stringify(data.permissions || []), {
allowedExpressions: (val) => val.includes("identity.")
});
const updatedRole = await projectRoleDAL.updateById(projectRole.id, {
...data,
permissions: data.permissions ? data.permissions : undefined
});
if (!updatedRole) throw new NotFoundError({ message: "Project role not found", name: "Update role" });
await permissionService.invalidateProjectPermissionCache(projectRole.projectId);
return { ...updatedRole, permissions: unpackPermissions(updatedRole.permissions) };
};
const deleteRole = async ({ actor, actorId, actorAuthMethod, actorOrgId, roleId }: TDeleteRoleDTO) => {
const projectRole = await projectRoleDAL.findById(roleId);
if (!projectRole) throw new NotFoundError({ message: "Project role not found", name: "Delete role" });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: projectRole.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Role);
const identityRole = await identityProjectMembershipRoleDAL.findOne({ customRoleId: roleId });
const projectUserRole = await projectUserMembershipRoleDAL.findOne({ customRoleId: roleId });
if (identityRole) {
throw new BadRequestError({
message: "The role is assigned to one or more identities. Make sure to unassign them before deleting the role.",
name: "Delete role"
});
}
if (projectUserRole) {
throw new BadRequestError({
message: "The role is assigned to one or more users. Make sure to unassign them before deleting the role.",
name: "Delete role"
});
}
const deletedRole = await projectRoleDAL.deleteById(roleId);
if (!deletedRole) throw new NotFoundError({ message: "Project role not found", name: "Delete role" });
await permissionService.invalidateProjectPermissionCache(projectRole.projectId);
return { ...deletedRole, permissions: unpackPermissions(deletedRole.permissions) };
};
const listRoles = async ({ actorOrgId, actorAuthMethod, actorId, actor, filter }: TListRolesDTO) => {
let project: TProjects;
if (filter.type === ProjectRoleServiceIdentifierType.SLUG) {
project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId);
} else {
project = await projectDAL.findProjectById(filter.projectId);
}
if (!project) throw new BadRequestError({ message: "Project not found" });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: project.id,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
const customRoles = await projectRoleDAL.find(
{ projectId: project.id },
{ sort: [[`${TableName.ProjectRoles}.slug` as "slug", "asc"]] }
);
const roles = [
...getPredefinedRoles({ projectId: project.id, projectType: project.type as ProjectType }),
...(customRoles || [])
];
return roles;
};
const getUserPermission = async (
userId: string,
projectId: string,
actorAuthMethod: ActorAuthMethod,
actorOrgId: string | undefined
) => {
const { permission, memberships } = await permissionService.getProjectPermission({
actor: ActorType.USER,
actorId: userId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
// just to satisfy ts
const assumedPrivilegeDetailsCtx = requestContext.get("assumedPrivilegeDetails");
const isAssumingPrivilege = assumedPrivilegeDetailsCtx?.projectId === projectId;
const assumedPrivilegeDetails = isAssumingPrivilege
? {
actorId: assumedPrivilegeDetailsCtx?.actorId,
actorType: assumedPrivilegeDetailsCtx?.actorType,
actorName: "",
actorEmail: ""
}
: undefined;
if (assumedPrivilegeDetails?.actorType === ActorType.IDENTITY) {
const identityDetails = await identityDAL.findById(assumedPrivilegeDetails.actorId);
if (!identityDetails)
throw new NotFoundError({ message: `Identity with ID ${assumedPrivilegeDetails.actorId} not found` });
assumedPrivilegeDetails.actorName = identityDetails.name;
} else if (assumedPrivilegeDetails?.actorType === ActorType.USER) {
const userDetails = await userDAL.findById(assumedPrivilegeDetails?.actorId);
if (!userDetails)
throw new NotFoundError({ message: `User with ID ${assumedPrivilegeDetails.actorId} not found` });
assumedPrivilegeDetails.actorName = `${userDetails?.firstName} ${userDetails?.lastName || ""}`;
assumedPrivilegeDetails.actorEmail = userDetails?.email || "";
}
return { permissions: packRules(permission.rules), memberships, assumedPrivilegeDetails };
};
return { createRole, updateRole, deleteRole, listRoles, getUserPermission, getRoleBySlug };
};