From 9473de22120c36a2e4e45beadcab067ef4a97fce Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 23 Feb 2024 01:34:15 +0100 Subject: [PATCH] Draft --- .../src/server/routes/v2/project-router.ts | 55 +++++++++++++++++++ backend/src/services/project/project-dal.ts | 39 +++++++++++++ .../src/services/project/project-service.ts | 31 +++++++++++ backend/src/services/project/project-types.ts | 14 +++++ 4 files changed, 139 insertions(+) diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index 208edba7b..bf9ecef85 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -17,6 +17,14 @@ const projectWithEnv = ProjectsSchema.merge( }) ); +const slugSchema = z + .string() + .min(5) + .max(36) + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }); + export const registerProjectRouter = async (server: FastifyZodProvider) => { /* Get project key */ server.route({ @@ -169,4 +177,51 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { return { project }; } }); + + server.route({ + method: "DELETE", + url: "/:slug", + schema: { + params: z.object({ + slug: slugSchema.describe("The slug of the project to delete.") + }), + response: { + 200: z.void() + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), + + handler: async (req) => { + await server.services.project.deleteProjectBySlug({ + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actor: req.permission.type, + slug: req.params.slug + }); + } + }); + + server.route({ + method: "GET", + url: "/:slug", + schema: { + params: z.object({ + slug: slugSchema.describe("The slug of the project to get.") + }), + response: { + 200: projectWithEnv + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const project = await server.services.project.getProjectBySlug({ + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actor: req.permission.type, + slug: req.params.slug + }); + + return project; + } + }); }; diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts index 7d0826e12..20302b53e 100644 --- a/backend/src/services/project/project-dal.ts +++ b/backend/src/services/project/project-dal.ts @@ -160,6 +160,44 @@ export const projectDALFactory = (db: TDbClient) => { } }; + const findProjectBySlug = async (slug: string) => { + try { + const projects = await db(TableName.ProjectMembership) + .where(`${TableName.Project}.slug`, slug) + .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) + .join(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) + .select( + selectAllTableCols(TableName.Project), + db.ref("id").withSchema(TableName.Project).as("_id"), + db.ref("id").withSchema(TableName.Environment).as("envId"), + db.ref("slug").withSchema(TableName.Environment).as("envSlug"), + db.ref("name").withSchema(TableName.Environment).as("envName") + ) + .orderBy([ + { column: `${TableName.Project}.name`, order: "asc" }, + { column: `${TableName.Environment}.position`, order: "asc" } + ]); + return sqlNestRelationships({ + data: projects, + key: "id", + parentMapper: ({ _id, ...el }) => ({ _id, ...ProjectsSchema.parse(el) }), + childrenMapper: [ + { + key: "envId", + label: "environments" as const, + mapper: ({ envId, envSlug, envName: name }) => ({ + id: envId, + slug: envSlug, + name + }) + } + ] + })?.[0]; + } catch (error) { + throw new DatabaseError({ error, name: "Find project by slug" }); + } + }; + const checkProjectUpgradeStatus = async (projectId: string) => { const project = await projectOrm.findById(projectId); const upgradeInProgress = @@ -179,6 +217,7 @@ export const projectDALFactory = (db: TDbClient) => { findAllProjectsByIdentity, findProjectGhostUser, findProjectById, + findProjectBySlug, checkProjectUpgradeStatus }; }; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 873a7d36f..4717cca11 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -32,7 +32,9 @@ import { assignWorkspaceKeysToMembers, createProjectKey } from "./project-fns"; import { TProjectQueueFactory } from "./project-queue"; import { TCreateProjectDTO, + TDeleteProjectBySlugDTO, TDeleteProjectDTO, + TGetProjectBySlugDTO, TGetProjectDTO, TUpdateProjectDTO, TUpgradeProjectDTO @@ -329,6 +331,27 @@ export const projectServiceFactory = ({ return deletedProject; }; + const deleteProjectBySlug = async ({ actor, actorId, actorOrgId, slug }: TDeleteProjectBySlugDTO) => { + const project = await projectDAL.findOne({ slug }); + + const { permission } = await permissionService.getProjectPermission(actor, actorId, project.id, actorOrgId); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project); + + const deletedProject = await projectDAL.transaction(async (tx) => { + const delProject = await projectDAL.deleteById(project.id, tx); + const projectGhostUser = await projectMembershipDAL.findProjectGhostUser(project.id).catch(() => null); + + // Delete the org membership for the ghost user if it's found. + if (projectGhostUser) { + await userDAL.deleteById(projectGhostUser.id, tx); + } + + return delProject; + }); + + return deletedProject; + }; + const getProjects = async (actorId: string) => { const workspaces = await projectDAL.findAllProjects(actorId); return workspaces; @@ -339,6 +362,12 @@ export const projectServiceFactory = ({ return projectDAL.findProjectById(projectId); }; + const getProjectBySlug = async ({ actorId, actorOrgId, slug, actor }: TGetProjectBySlugDTO) => { + const project = await projectDAL.findProjectBySlug(slug); + await permissionService.getProjectPermission(actor, actorId, project.id, actorOrgId); + return project; + }; + const updateProject = async ({ projectId, actor, actorId, actorOrgId, update }: TUpdateProjectDTO) => { const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); @@ -417,8 +446,10 @@ export const projectServiceFactory = ({ deleteProject, getProjects, updateProject, + deleteProjectBySlug, getProjectUpgradeStatus, getAProject, + getProjectBySlug, toggleAutoCapitalization, updateName, upgradeProject diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 3843450c2..2dabeb07c 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -19,6 +19,13 @@ export type TDeleteProjectDTO = { projectId: string; }; +export type TDeleteProjectBySlugDTO = { + slug: string; + actor: ActorType; + actorId: string; + actorOrgId?: string; +}; + export type TGetProjectDTO = { actor: ActorType; actorId: string; @@ -26,6 +33,13 @@ export type TGetProjectDTO = { projectId: string; }; +export type TGetProjectBySlugDTO = { + slug: string; + actor: ActorType; + actorId: string; + actorOrgId?: string; +}; + export type TUpdateProjectDTO = { update: { name?: string;