diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index 05c77e746..3fbf54b4c 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -230,6 +230,15 @@ export const orgDALFactory = (db: TDbClient) => { } }; + const deleteMembershipByUserId = async (userId: string, orgId: string, tx?: Knex) => { + try { + const [membership] = await (tx || db)(TableName.OrgMembership).where({ userId, orgId }).delete().returning("*"); + return membership; + } catch (error) { + throw new DatabaseError({ error, name: "Delete org membership" }); + } + }; + const findMembership = async ( filter: TFindFilter, { offset, limit, sort, tx }: TFindOpt = {} @@ -275,6 +284,7 @@ export const orgDALFactory = (db: TDbClient) => { createMembership, updateMembershipById, deleteMembershipById, + deleteMembershipByUserId, updateMembership }); }; diff --git a/backend/src/services/project/project-queue.ts b/backend/src/services/project/project-queue.ts index 7bf9f0e51..3a76d9c65 100644 --- a/backend/src/services/project/project-queue.ts +++ b/backend/src/services/project/project-queue.ts @@ -112,6 +112,9 @@ export const projectQueueFactory = ({ await projectDAL.setProjectUpgradeStatus(data.projectId, ProjectUpgradeStatus.InProgress); // Set the status to in progress. This is important to prevent multiple upgrades at the same time. + // eslint-disable-next-line no-promise-executor-return + // await new Promise((resolve) => setTimeout(resolve, 50_000)); + const userPrivateKey = infisicalSymmetricDecrypt({ keyEncoding: data.encryptedPrivateKey.keyEncoding, ciphertext: data.encryptedPrivateKey.encryptedKey, diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 5826e77f0..d5fa45bfa 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -17,6 +17,7 @@ import { TProjectPermission } from "@app/lib/types"; import { ActorType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityProjectDALFactory } from "../identity-project/identity-project-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { TOrgServiceFactory } from "../org/org-service"; import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; @@ -53,6 +54,7 @@ type TProjectServiceFactoryDep = { projectKeyDAL: Pick; projectBotDAL: Pick; projectMembershipDAL: Pick; + orgDAL: Pick; secretBlindIndexDAL: Pick; permissionService: TPermissionServiceFactory; orgService: Pick; @@ -68,6 +70,7 @@ export const projectServiceFactory = ({ permissionService, userDAL, folderDAL, + orgDAL, orgService, identityProjectDAL, projectBotDAL, @@ -287,17 +290,22 @@ export const projectServiceFactory = ({ return results; }; - const findProjectGhostUser = async (projectId: string) => { - const user = await projectMembershipDAL.findProjectGhostUser(projectId); - - return user; - }; - const deleteProject = async ({ actor, actorId, actorOrgId, projectId }: TDeleteProjectDTO) => { const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project); - const deletedProject = await projectDAL.deleteById(projectId); + const deletedProject = await projectDAL.transaction(async (tx) => { + const project = await projectDAL.deleteById(projectId, tx); + const projectGhostUser = await projectMembershipDAL.findProjectGhostUser(projectId).catch(() => null); + + // Delete the org membership for the ghost user if it's found. + if (projectGhostUser) { + await orgDAL.deleteMembershipByUserId(projectGhostUser.id, deletedProject.orgId, tx); + } + + return project; + }); + return deletedProject; }; @@ -389,7 +397,6 @@ export const projectServiceFactory = ({ deleteProject, getProjects, updateProject, - findProjectGhostUser, getProjectUpgradeStatus, getAProject, toggleAutoCapitalization, diff --git a/frontend/src/components/v2/UpgradeOverlay/UpgradeOverlay.tsx b/frontend/src/components/v2/UpgradeOverlay/UpgradeOverlay.tsx new file mode 100644 index 000000000..aea5bb582 --- /dev/null +++ b/frontend/src/components/v2/UpgradeOverlay/UpgradeOverlay.tsx @@ -0,0 +1,46 @@ +import { useRouter } from "next/router"; + +import { Spinner } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { useToggle } from "@app/hooks"; +import { useGetUpgradeProjectStatus } from "@app/hooks/api/workspace/queries"; +import { ProjectVersion } from "@app/hooks/api/workspace/types"; + +export const UpgradeOverlay = () => { + const router = useRouter(); + const { currentWorkspace } = useWorkspace(); + const [isUpgrading, setIsUpgrading] = useToggle(false); + const { isLoading: isUpgradeStatusLoading } = useGetUpgradeProjectStatus({ + projectId: currentWorkspace?.id ?? "", + enabled: currentWorkspace?.version === ProjectVersion.V1, + refetchInterval: 5_000, + onSuccess: (data) => { + if (!data) return; + + if (data.status !== "IN_PROGRESS") { + setIsUpgrading.off(); + } else if (data?.status === "IN_PROGRESS") { + setIsUpgrading.on(); + } + } + }); + + // make sure only to display this on /project routes + if (!currentWorkspace || !router.pathname.includes("/project")) { + return null; + } + + // for non admin this would throw an error + // so no need to render + return !isUpgradeStatusLoading && isUpgrading ? ( // isUpgrading +
+ +
+
Please wait
+ Upgrading your project... +
+
+ ) : ( +
+ ); +}; diff --git a/frontend/src/components/v2/UpgradeOverlay/index.tsx b/frontend/src/components/v2/UpgradeOverlay/index.tsx new file mode 100644 index 000000000..1a74fb6f1 --- /dev/null +++ b/frontend/src/components/v2/UpgradeOverlay/index.tsx @@ -0,0 +1 @@ +export { UpgradeOverlay } from "./UpgradeOverlay"; diff --git a/frontend/src/components/v2/UpgradeProjectAlert/UpgradeProjectAlert.tsx b/frontend/src/components/v2/UpgradeProjectAlert/UpgradeProjectAlert.tsx index ec3bdd9e4..db5eb1179 100644 --- a/frontend/src/components/v2/UpgradeProjectAlert/UpgradeProjectAlert.tsx +++ b/frontend/src/components/v2/UpgradeProjectAlert/UpgradeProjectAlert.tsx @@ -29,7 +29,7 @@ export const UpgradeProjectAlert = ({ project }: UpgradeProjectAlertProps): JSX. refetch: manualProjectStatusRefetch } = useGetUpgradeProjectStatus({ projectId: project.id, - enabled: membership.role === "admin", + enabled: membership.role === "admin" && project.version === ProjectVersion.V1, refetchInterval: 5_000, onSuccess: (data) => { if (membership.role !== "admin") { diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 9dd2d82e7..295369bc4 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -49,6 +49,7 @@ import { SelectItem, UpgradePlanModal } from "@app/components/v2"; +import { UpgradeOverlay } from "@app/components/v2/UpgradeOverlay"; import { OrgPermissionActions, OrgPermissionSubjects, @@ -238,7 +239,7 @@ export const AppLayout = ({ children }: LayoutProps) => { projectId: newProjectId }); } - + // eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected. await new Promise((resolve) => setTimeout(resolve, 2_000)); @@ -261,6 +262,7 @@ export const AppLayout = ({ children }: LayoutProps) => {