diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 3ffedf98d..bd2778141 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -10,6 +10,7 @@ import { import { PROJECTS } from "@app/lib/api-docs"; 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 { integrationAuthPubSchema } from "../sanitizedSchemas"; import { sanitizedServiceTokenSchema } from "../v2/service-token-router"; @@ -137,10 +138,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const workspace = await server.services.project.getAProject({ + filterType: ProjectFilterType.ID, + filter: req.params.workspaceId, actorId: req.permission.id, actor: req.permission.type, - actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId + actorOrgId: req.permission.orgId }); return { workspace }; } @@ -189,10 +191,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const workspace = await server.services.project.deleteProject({ + filterType: ProjectFilterType.ID, + filter: req.params.workspaceId, actorId: req.permission.id, actor: req.permission.type, - actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId + actorOrgId: req.permission.orgId }); return { workspace }; } @@ -253,17 +256,18 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const workspace = await server.services.project.updateProject({ - actorId: req.permission.id, - actor: req.permission.type, - actorOrgId: req.permission.orgId, - projectId: req.params.workspaceId, + filterType: ProjectFilterType.ID, + filter: req.params.workspaceId, update: { name: req.body.name, autoCapitalization: req.body.autoCapitalization - } + }, + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId }); return { workspace diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index bf9ecef85..8f3d91c62 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -8,6 +8,7 @@ import { authRateLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; 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 { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; const projectWithEnv = ProjectsSchema.merge( @@ -178,6 +179,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } }); + /* Delete a project by slug */ server.route({ method: "DELETE", url: "/:slug", @@ -186,21 +188,25 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { slug: slugSchema.describe("The slug of the project to delete.") }), response: { - 200: z.void() + 200: ProjectsSchema } }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - await server.services.project.deleteProjectBySlug({ + const project = await server.services.project.deleteProject({ + filterType: ProjectFilterType.SLUG, + filter: req.params.slug, actorId: req.permission.id, actorOrgId: req.permission.orgId, - actor: req.permission.type, - slug: req.params.slug + actor: req.permission.type }); + + return project; } }); + /* Get a project by slug */ server.route({ method: "GET", url: "/:slug", @@ -214,11 +220,47 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const project = await server.services.project.getProjectBySlug({ + const project = await server.services.project.getAProject({ + filter: req.params.slug, + filterType: ProjectFilterType.SLUG, actorId: req.permission.id, actorOrgId: req.permission.orgId, + actor: req.permission.type + }); + + return project; + } + }); + + /* Update a project by slug */ + server.route({ + method: "PATCH", + url: "/:slug", + schema: { + params: z.object({ + slug: slugSchema.describe("The slug of the project to update.") + }), + body: z.object({ + name: z.string().trim().optional().describe("The new name of the project."), + autoCapitalization: z.boolean().optional().describe("The new auto-capitalization setting.") + }), + response: { + 200: ProjectsSchema + } + }, + + onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const project = await server.services.project.updateProject({ + filterType: ProjectFilterType.SLUG, + filter: req.params.slug, + update: { + name: req.body.name, + autoCapitalization: req.body.autoCapitalization + }, + actorId: req.permission.id, actor: req.permission.type, - slug: req.params.slug + actorOrgId: req.permission.orgId }); return project; diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts index 20302b53e..d629b1d0f 100644 --- a/backend/src/services/project/project-dal.ts +++ b/backend/src/services/project/project-dal.ts @@ -5,6 +5,8 @@ import { ProjectsSchema, ProjectUpgradeStatus, ProjectVersion, TableName, TProje import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; +import { ProjectFilterType } from "./project-types"; + export type TProjectDALFactory = ReturnType; export const projectDALFactory = (db: TDbClient) => { @@ -139,7 +141,7 @@ export const projectDALFactory = (db: TDbClient) => { { column: `${TableName.Project}.name`, order: "asc" }, { column: `${TableName.Environment}.position`, order: "asc" } ]); - return sqlNestRelationships({ + const project = sqlNestRelationships({ data: workspaces, key: "id", parentMapper: ({ _id, ...el }) => ({ _id, ...ProjectsSchema.parse(el) }), @@ -155,6 +157,12 @@ export const projectDALFactory = (db: TDbClient) => { } ] })?.[0]; + + if (!project) { + throw new BadRequestError({ message: "Project not found" }); + } + + return project; } catch (error) { throw new DatabaseError({ error, name: "Find all projects" }); } @@ -177,7 +185,7 @@ export const projectDALFactory = (db: TDbClient) => { { column: `${TableName.Project}.name`, order: "asc" }, { column: `${TableName.Environment}.position`, order: "asc" } ]); - return sqlNestRelationships({ + const project = sqlNestRelationships({ data: projects, key: "id", parentMapper: ({ _id, ...el }) => ({ _id, ...ProjectsSchema.parse(el) }), @@ -193,11 +201,31 @@ export const projectDALFactory = (db: TDbClient) => { } ] })?.[0]; + + if (!project) { + throw new BadRequestError({ message: "Project not found" }); + } + + return project; } catch (error) { throw new DatabaseError({ error, name: "Find project by slug" }); } }; + const findProjectByFilter = async (filter: string, type: ProjectFilterType) => { + try { + if (type === ProjectFilterType.ID) { + return await findProjectById(filter); + } + if (type === ProjectFilterType.SLUG) { + return await findProjectBySlug(filter); + } + throw new BadRequestError({ message: "Invalid filter type" }); + } catch (error) { + throw new DatabaseError({ error, name: `Failed to find project by ${type}` }); + } + }; + const checkProjectUpgradeStatus = async (projectId: string) => { const project = await projectOrm.findById(projectId); const upgradeInProgress = @@ -217,6 +245,7 @@ export const projectDALFactory = (db: TDbClient) => { findAllProjectsByIdentity, findProjectGhostUser, findProjectById, + findProjectByFilter, findProjectBySlug, checkProjectUpgradeStatus }; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 4717cca11..4c647f391 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -32,11 +32,11 @@ import { assignWorkspaceKeysToMembers, createProjectKey } from "./project-fns"; import { TProjectQueueFactory } from "./project-queue"; import { TCreateProjectDTO, - TDeleteProjectBySlugDTO, TDeleteProjectDTO, - TGetProjectBySlugDTO, TGetProjectDTO, + TToggleProjectAutoCapitalizationDTO, TUpdateProjectDTO, + TUpdateProjectNameDTO, TUpgradeProjectDTO } from "./project-types"; @@ -312,27 +312,8 @@ export const projectServiceFactory = ({ return results; }; - 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.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 userDAL.deleteById(projectGhostUser.id, tx); - } - - return project; - }); - - return deletedProject; - }; - - const deleteProjectBySlug = async ({ actor, actorId, actorOrgId, slug }: TDeleteProjectBySlugDTO) => { - const project = await projectDAL.findOne({ slug }); + const deleteProject = async ({ actor, actorId, actorOrgId, filter, filterType }: TDeleteProjectDTO) => { + const project = await projectDAL.findProjectByFilter(filter, filterType); const { permission } = await permissionService.getProjectPermission(actor, actorId, project.id, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project); @@ -357,22 +338,20 @@ export const projectServiceFactory = ({ return workspaces; }; - const getAProject = async ({ actorId, actorOrgId, projectId, actor }: TGetProjectDTO) => { - await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); - return projectDAL.findProjectById(projectId); - }; + const getAProject = async ({ actorId, actorOrgId, filter, filterType, actor }: TGetProjectDTO) => { + const project = await projectDAL.findProjectByFilter(filter, filterType); - 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); + const updateProject = async ({ actor, actorId, actorOrgId, update, filter, filterType }: TUpdateProjectDTO) => { + const project = await projectDAL.findProjectByFilter(filter, filterType); + + const { permission } = await permissionService.getProjectPermission(actor, actorId, project.id, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); - const updatedProject = await projectDAL.updateById(projectId, { + const updatedProject = await projectDAL.updateById(project.id, { name: update.name, autoCapitalization: update.autoCapitalization }); @@ -385,7 +364,7 @@ export const projectServiceFactory = ({ actorId, actorOrgId, autoCapitalization - }: TGetProjectDTO & { autoCapitalization: boolean }) => { + }: TToggleProjectAutoCapitalizationDTO) => { const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); @@ -393,7 +372,7 @@ export const projectServiceFactory = ({ return updatedProject; }; - const updateName = async ({ projectId, actor, actorId, actorOrgId, name }: TGetProjectDTO & { name: string }) => { + const updateName = async ({ projectId, actor, actorId, actorOrgId, name }: TUpdateProjectNameDTO) => { const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId, actorOrgId); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); @@ -446,10 +425,8 @@ 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 2dabeb07c..f43930c47 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -3,6 +3,11 @@ import { TProjectPermission } from "@app/lib/types"; import { ActorType } from "../auth/auth-type"; +export enum ProjectFilterType { + ID = "id", + SLUG = "slug" +} + export type TCreateProjectDTO = { actor: ActorType; actorId: string; @@ -12,13 +17,6 @@ export type TCreateProjectDTO = { slug?: string; }; -export type TDeleteProjectDTO = { - actor: ActorType; - actorId: string; - actorOrgId?: string; - projectId: string; -}; - export type TDeleteProjectBySlugDTO = { slug: string; actor: ActorType; @@ -27,25 +25,33 @@ export type TDeleteProjectBySlugDTO = { }; export type TGetProjectDTO = { - actor: ActorType; - actorId: string; - actorOrgId?: string; - projectId: string; -}; + filter: string; + filterType: ProjectFilterType; +} & Omit; -export type TGetProjectBySlugDTO = { - slug: string; - actor: ActorType; - actorId: string; - actorOrgId?: string; -}; +export type TToggleProjectAutoCapitalizationDTO = { + autoCapitalization: boolean; +} & TProjectPermission; +export type TUpdateProjectNameDTO = { + name: string; +} & TProjectPermission; export type TUpdateProjectDTO = { + filter: string; + filterType: ProjectFilterType; update: { name?: string; autoCapitalization?: boolean; }; -} & TProjectPermission; +} & Omit; + +export type TDeleteProjectDTO = { + filter: string; + filterType: ProjectFilterType; + actor: ActorType; + actorId: string; + actorOrgId?: string; +} & Omit; export type TUpgradeProjectDTO = { userPrivateKey: string;