diff --git a/backend/package-lock.json b/backend/package-lock.json index 389d16475..8bd4a98a0 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -6459,12 +6459,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -8115,9 +8115,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 7cbfd88a0..d768066fa 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -508,12 +508,27 @@ export const SECRET_TAGS = { LIST: { projectId: "The ID of the project to list tags from." }, + GET_TAG_BY_ID: { + projectId: "The ID of the project to get tags from.", + tagId: "The ID of the tag to get details" + }, + GET_TAG_BY_SLUG: { + projectId: "The ID of the project to get tags from.", + tagSlug: "The slug of the tag to get details" + }, CREATE: { projectId: "The ID of the project to create the tag in.", name: "The name of the tag to create.", slug: "The slug of the tag to create.", color: "The color of the tag to create." }, + UPDATE: { + projectId: "The ID of the project to update the tag in.", + tagId: "The ID of the tag to get details", + name: "The name of the tag to update.", + slug: "The slug of the tag to update.", + color: "The color of the tag to update." + }, DELETE: { tagId: "The ID of the tag to delete.", projectId: "The ID of the project to delete the tag from." diff --git a/backend/src/lib/errors/index.ts b/backend/src/lib/errors/index.ts index 18b40acfd..0a7cb8014 100644 --- a/backend/src/lib/errors/index.ts +++ b/backend/src/lib/errors/index.ts @@ -59,6 +59,18 @@ export class BadRequestError extends Error { } } +export class NotFoundError extends Error { + name: string; + + error: unknown; + + constructor({ name, error, message }: { message?: string; name?: string; error?: unknown }) { + super(message ?? "The requested entity is not found"); + this.name = name || "NotFound"; + this.error = error; + } +} + export class DisableRotationErrors extends Error { name: string; diff --git a/backend/src/server/plugins/error-handler.ts b/backend/src/server/plugins/error-handler.ts index c8da4077a..3320c7d87 100644 --- a/backend/src/server/plugins/error-handler.ts +++ b/backend/src/server/plugins/error-handler.ts @@ -6,6 +6,7 @@ import { BadRequestError, DatabaseError, InternalServerError, + NotFoundError, ScimRequestError, UnauthorizedError } from "@app/lib/errors"; @@ -15,6 +16,8 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider req.log.error(error); if (error instanceof BadRequestError) { void res.status(400).send({ statusCode: 400, message: error.message, error: error.name }); + } else if (error instanceof NotFoundError) { + void res.status(404).send({ statusCode: 404, message: error.message, error: error.name }); } else if (error instanceof UnauthorizedError) { void res.status(403).send({ statusCode: 403, message: error.message, error: error.name }); } else if (error instanceof DatabaseError || error instanceof InternalServerError) { diff --git a/backend/src/server/routes/v1/secret-tag-router.ts b/backend/src/server/routes/v1/secret-tag-router.ts index ccbb4572d..ce92409f6 100644 --- a/backend/src/server/routes/v1/secret-tag-router.ts +++ b/backend/src/server/routes/v1/secret-tag-router.ts @@ -36,6 +36,67 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "GET", + url: "/:projectId/tags/:tagId", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + projectId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_ID.projectId), + tagId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_ID.tagId) + }), + response: { + 200: z.object({ + workspaceTag: SecretTagsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const workspaceTag = await server.services.secretTag.getTagById({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.tagId + }); + return { workspaceTag }; + } + }); + + server.route({ + method: "GET", + url: "/:projectId/tags/slug/:tagSlug", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + projectId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_SLUG.projectId), + tagSlug: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_SLUG.tagSlug) + }), + response: { + 200: z.object({ + workspaceTag: SecretTagsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const workspaceTag = await server.services.secretTag.getTagBySlug({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + slug: req.params.tagSlug, + projectId: req.params.projectId + }); + return { workspaceTag }; + } + }); + server.route({ method: "POST", url: "/:projectId/tags", @@ -71,6 +132,42 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "PATCH", + url: "/:projectId/tags/:tagId", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + projectId: z.string().trim().describe(SECRET_TAGS.UPDATE.projectId), + tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId) + }), + body: z.object({ + name: z.string().trim().describe(SECRET_TAGS.UPDATE.name), + slug: z.string().trim().describe(SECRET_TAGS.UPDATE.slug), + color: z.string().trim().describe(SECRET_TAGS.UPDATE.color) + }), + response: { + 200: z.object({ + workspaceTag: SecretTagsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const workspaceTag = await server.services.secretTag.updateTag({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body, + id: req.params.tagId + }); + return { workspaceTag }; + } + }); + server.route({ method: "DELETE", url: "/:projectId/tags/:tagId", diff --git a/backend/src/services/secret-tag/secret-tag-service.ts b/backend/src/services/secret-tag/secret-tag-service.ts index 916e812e6..76b57dc90 100644 --- a/backend/src/services/secret-tag/secret-tag-service.ts +++ b/backend/src/services/secret-tag/secret-tag-service.ts @@ -2,10 +2,17 @@ import { ForbiddenError } from "@casl/ability"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { BadRequestError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TSecretTagDALFactory } from "./secret-tag-dal"; -import { TCreateTagDTO, TDeleteTagDTO, TListProjectTagsDTO } from "./secret-tag-types"; +import { + TCreateTagDTO, + TDeleteTagDTO, + TGetTagByIdDTO, + TGetTagBySlugDTO, + TListProjectTagsDTO, + TUpdateTagDTO +} from "./secret-tag-types"; type TSecretTagServiceFactoryDep = { secretTagDAL: TSecretTagDALFactory; @@ -48,6 +55,28 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe return newTag; }; + const updateTag = async ({ actorId, actor, actorOrgId, actorAuthMethod, id, name, color, slug }: TUpdateTagDTO) => { + const tag = await secretTagDAL.findById(id); + if (!tag) throw new BadRequestError({ message: "Tag doesn't exist" }); + + if (slug) { + const existingTag = await secretTagDAL.findOne({ slug, projectId: tag.projectId }); + if (existingTag && existingTag.id !== tag.id) throw new BadRequestError({ message: "Tag already exist" }); + } + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + tag.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags); + + const updatedTag = await secretTagDAL.updateById(tag.id, { name, color, slug }); + return updatedTag; + }; + const deleteTag = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TDeleteTagDTO) => { const tag = await secretTagDAL.findById(id); if (!tag) throw new BadRequestError({ message: "Tag doesn't exist" }); @@ -65,6 +94,38 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe return deletedTag; }; + const getTagById = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TGetTagByIdDTO) => { + const tag = await secretTagDAL.findById(id); + if (!tag) throw new NotFoundError({ message: "Tag doesn't exist" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + tag.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); + + return tag; + }; + + const getTagBySlug = async ({ actorId, actor, actorOrgId, actorAuthMethod, slug, projectId }: TGetTagBySlugDTO) => { + const tag = await secretTagDAL.findOne({ projectId, slug }); + if (!tag) throw new NotFoundError({ message: "Tag doesn't exist" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + tag.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags); + + return tag; + }; + const getProjectTags = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TListProjectTagsDTO) => { const { permission } = await permissionService.getProjectPermission( actor, @@ -79,5 +140,5 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe return tags; }; - return { createTag, deleteTag, getProjectTags }; + return { createTag, deleteTag, getProjectTags, getTagById, getTagBySlug, updateTag }; }; diff --git a/backend/src/services/secret-tag/secret-tag-types.ts b/backend/src/services/secret-tag/secret-tag-types.ts index d2f027153..f2ace0901 100644 --- a/backend/src/services/secret-tag/secret-tag-types.ts +++ b/backend/src/services/secret-tag/secret-tag-types.ts @@ -6,6 +6,21 @@ export type TCreateTagDTO = { slug: string; } & TProjectPermission; +export type TUpdateTagDTO = { + id: string; + name?: string; + slug?: string; + color?: string; +} & Omit; + +export type TGetTagByIdDTO = { + id: string; +} & Omit; + +export type TGetTagBySlugDTO = { + slug: string; +} & TProjectPermission; + export type TDeleteTagDTO = { id: string; } & Omit; diff --git a/docs/api-reference/endpoints/secret-tags/get-by-id.mdx b/docs/api-reference/endpoints/secret-tags/get-by-id.mdx new file mode 100644 index 000000000..de02fe133 --- /dev/null +++ b/docs/api-reference/endpoints/secret-tags/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get By ID" +openapi: "GET /api/v1/workspace/{projectId}/tags/{tagId}" +--- diff --git a/docs/api-reference/endpoints/secret-tags/get-by-slug.mdx b/docs/api-reference/endpoints/secret-tags/get-by-slug.mdx new file mode 100644 index 000000000..91eab730f --- /dev/null +++ b/docs/api-reference/endpoints/secret-tags/get-by-slug.mdx @@ -0,0 +1,4 @@ +--- +title: "Get By Slug" +openapi: "GET /api/v1/workspace/{projectId}/tags/slug/{tagSlug}" +--- diff --git a/docs/api-reference/endpoints/secret-tags/update.mdx b/docs/api-reference/endpoints/secret-tags/update.mdx new file mode 100644 index 000000000..b9c290db8 --- /dev/null +++ b/docs/api-reference/endpoints/secret-tags/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/workspace/{projectId}/tags/{tagId}" +--- diff --git a/docs/mint.json b/docs/mint.json index 840649d03..698172e67 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -505,7 +505,10 @@ "group": "Secret Tags", "pages": [ "api-reference/endpoints/secret-tags/list", + "api-reference/endpoints/secret-tags/get-by-id", + "api-reference/endpoints/secret-tags/get-by-slug", "api-reference/endpoints/secret-tags/create", + "api-reference/endpoints/secret-tags/update", "api-reference/endpoints/secret-tags/delete" ] },