diff --git a/apps/web/app/api/v2/management/roles/route.ts b/apps/web/app/api/v2/management/roles/route.ts new file mode 100644 index 0000000000..9580752584 --- /dev/null +++ b/apps/web/app/api/v2/management/roles/route.ts @@ -0,0 +1,3 @@ +import { GET } from "@/modules/api/v2/management/roles/route"; + +export { GET }; diff --git a/apps/web/modules/api/v2/management/roles/lib/openapi.ts b/apps/web/modules/api/v2/management/roles/lib/openapi.ts new file mode 100644 index 0000000000..e7e937a924 --- /dev/null +++ b/apps/web/modules/api/v2/management/roles/lib/openapi.ts @@ -0,0 +1,26 @@ +import { z } from "zod"; +import { ZodOpenApiOperationObject, ZodOpenApiPathsObject } from "zod-openapi"; + +export const getRolesEndpoint: ZodOpenApiOperationObject = { + operationId: "getRoles", + summary: "Get roles", + description: "Gets roles from the database.", + requestParams: {}, + tags: ["Management API > Roles"], + responses: { + "200": { + description: "Roles retrieved successfully.", + content: { + "application/json": { + schema: z.array(z.string()), + }, + }, + }, + }, +}; + +export const rolePaths: ZodOpenApiPathsObject = { + "/roles": { + get: getRolesEndpoint, + }, +}; diff --git a/apps/web/modules/api/v2/management/roles/lib/roles.ts b/apps/web/modules/api/v2/management/roles/lib/roles.ts new file mode 100644 index 0000000000..41c022410f --- /dev/null +++ b/apps/web/modules/api/v2/management/roles/lib/roles.ts @@ -0,0 +1,26 @@ +import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; +import { ApiResponse } from "@/modules/api/v2/types/api-success"; +import { prisma } from "@formbricks/database"; +import { Result, err, ok } from "@formbricks/types/error-handlers"; + +export const getRoles = async (): Promise, ApiErrorResponseV2>> => { + try { + // We use a raw query to get all the roles because we can't list enum options with prisma + const results = await prisma.$queryRaw<{ unnest: string }[]>` + SELECT unnest(enum_range(NULL::"OrganizationRole")); + `; + + if (!results) { + // We set internal_server_error because it's an enum and we should always have the roles + return err({ type: "internal_server_error", details: [{ field: "roles", issue: "not found" }] }); + } + + const roles = results.map((row) => row.unnest); + + return ok({ + data: roles, + }); + } catch (error) { + return err({ type: "internal_server_error", details: [{ field: "roles", issue: error.message }] }); + } +}; diff --git a/apps/web/modules/api/v2/management/roles/lib/tests/roles.test.ts b/apps/web/modules/api/v2/management/roles/lib/tests/roles.test.ts new file mode 100644 index 0000000000..c23324382e --- /dev/null +++ b/apps/web/modules/api/v2/management/roles/lib/tests/roles.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it, vi } from "vitest"; +import { prisma } from "@formbricks/database"; +import { getRoles } from "../roles"; + +// Mock prisma with a $queryRaw function +vi.mock("@formbricks/database", () => ({ + prisma: { + $queryRaw: vi.fn(), + }, +})); + +describe("getRoles", () => { + it("returns roles on success", async () => { + (prisma.$queryRaw as any).mockResolvedValueOnce([{ unnest: "ADMIN" }, { unnest: "MEMBER" }]); + + const result = await getRoles(); + expect(result.ok).toBe(true); + + if (result.ok) { + expect(result.data.data).toEqual(["ADMIN", "MEMBER"]); + } + }); + + it("returns error if no results are found", async () => { + (prisma.$queryRaw as any).mockResolvedValueOnce(null); + + const result = await getRoles(); + expect(result.ok).toBe(false); + + if (!result.ok) { + expect(result.error?.type).toBe("internal_server_error"); + } + }); + + it("returns error on exception", async () => { + vi.mocked(prisma.$queryRaw).mockRejectedValueOnce(new Error("Test DB error")); + + const result = await getRoles(); + expect(result.ok).toBe(false); + + if (!result.ok) { + expect(result.error.type).toBe("internal_server_error"); + } + }); +}); diff --git a/apps/web/modules/api/v2/management/roles/route.ts b/apps/web/modules/api/v2/management/roles/route.ts new file mode 100644 index 0000000000..829cbc2fe4 --- /dev/null +++ b/apps/web/modules/api/v2/management/roles/route.ts @@ -0,0 +1,19 @@ +import { responses } from "@/modules/api/v2/lib/response"; +import { handleApiError } from "@/modules/api/v2/lib/utils"; +import { authenticatedApiClient } from "@/modules/api/v2/management/auth/authenticated-api-client"; +import { getRoles } from "@/modules/api/v2/management/roles/lib/roles"; +import { NextRequest } from "next/server"; + +export const GET = async (request: NextRequest) => + authenticatedApiClient({ + request, + handler: async () => { + const res = await getRoles(); + + if (res.ok) { + return responses.successResponse(res.data); + } + + return handleApiError(request, res.error); + }, + }); diff --git a/apps/web/modules/api/v2/openapi-document.ts b/apps/web/modules/api/v2/openapi-document.ts index 18d79a3d5c..d58ee1de0a 100644 --- a/apps/web/modules/api/v2/openapi-document.ts +++ b/apps/web/modules/api/v2/openapi-document.ts @@ -2,6 +2,7 @@ import { contactAttributeKeyPaths } from "@/modules/api/v2/management/contact-at import { contactAttributePaths } from "@/modules/api/v2/management/contact-attributes/lib/openapi"; import { contactPaths } from "@/modules/api/v2/management/contacts/lib/openapi"; import { responsePaths } from "@/modules/api/v2/management/responses/lib/openapi"; +import { rolePaths } from "@/modules/api/v2/management/roles/lib/openapi"; import { surveyPaths } from "@/modules/api/v2/management/surveys/lib/openapi"; import { webhookPaths } from "@/modules/api/v2/management/webhooks/lib/openapi"; import * as yaml from "yaml"; @@ -30,6 +31,7 @@ const document = createDocument({ ...contactAttributeKeyPaths, ...surveyPaths, ...webhookPaths, + ...rolePaths, }, servers: [ { @@ -62,6 +64,10 @@ const document = createDocument({ name: "Management API > Webhooks", description: "Operations for managing webhooks.", }, + { + name: "Management API > Roles", + description: "Operations for managing roles.", + }, ], components: { securitySchemes: { @@ -79,6 +85,7 @@ const document = createDocument({ contactAttributeKey: ZContactAttributeKey, survey: ZSurveyWithoutQuestionType, webhook: ZWebhook, + role: z.array(z.string()), }, }, security: [ diff --git a/apps/web/playwright/api/constants.ts b/apps/web/playwright/api/constants.ts index 904500b1e6..caddcc1ed1 100644 --- a/apps/web/playwright/api/constants.ts +++ b/apps/web/playwright/api/constants.ts @@ -1,3 +1,4 @@ export const RESPONSES_API_URL = `/api/v2/management/responses`; export const SURVEYS_API_URL = `/api/v1/management/surveys`; export const WEBHOOKS_API_URL = `/api/v2/management/webhooks`; +export const ROLES_API_URL = `/api/v2/management/roles`; diff --git a/apps/web/playwright/api/management/role.spec.ts b/apps/web/playwright/api/management/role.spec.ts new file mode 100644 index 0000000000..8c02d0fa90 --- /dev/null +++ b/apps/web/playwright/api/management/role.spec.ts @@ -0,0 +1,29 @@ +import { ROLES_API_URL } from "@/playwright/api/constants"; +import { expect } from "@playwright/test"; +import { logger } from "@formbricks/logger"; +import { test } from "../../lib/fixtures"; +import { loginAndGetApiKey } from "../../lib/utils"; + +test.describe("API Tests for Roles", () => { + test("Retrieve Roles via API", async ({ page, users, request }) => { + let apiKey; + + try { + ({ apiKey } = await loginAndGetApiKey(page, users)); + } catch (error) { + logger.error(error, "Error during login and getting API key"); + throw error; + } + + const response = await request.get(ROLES_API_URL, { + headers: { + "x-api-key": apiKey, + }, + }); + expect(response.ok()).toBe(true); + const responseBody = await response.json(); + + expect(Array.isArray(responseBody.data)).toBe(true); + expect(responseBody.data.length).toBeGreaterThan(0); + }); +}); diff --git a/docs/api-v2-reference/openapi.yml b/docs/api-v2-reference/openapi.yml index 7ef49a39fd..62a01e4d24 100644 --- a/docs/api-v2-reference/openapi.yml +++ b/docs/api-v2-reference/openapi.yml @@ -19,6 +19,10 @@ tags: description: Operations for managing surveys. - name: Management API > Webhooks description: Operations for managing webhooks. + - name: Management API > Roles + description: Operations for managing roles. +security: + - apiKeyAuth: [] paths: /responses/{responseId}: put: @@ -521,8 +525,6 @@ paths: description: Formbricks API Server /responses: get: - security: - - apiKeyAuth: [] operationId: getResponses summary: Get responses description: Gets responses from the database. @@ -546,7 +548,7 @@ paths: name: sortBy schema: type: string - enum: + enum: &a6 - createdAt - updatedAt default: createdAt @@ -554,7 +556,7 @@ paths: name: order schema: type: string - enum: + enum: &a7 - asc - desc default: desc @@ -730,8 +732,6 @@ paths: offset: type: number post: - security: - - apiKeyAuth: [] operationId: createResponse summary: Create a response description: Creates a response in the database. @@ -955,8 +955,6 @@ paths: description: The display ID of the response /responses/{id}: get: - security: - - apiKeyAuth: [] operationId: getResponse summary: Get a response description: Gets a response from the database. @@ -1084,8 +1082,6 @@ paths: - "null" description: The display ID of the response put: - security: - - apiKeyAuth: [] operationId: updateResponse summary: Update a response description: Updates a response in the database. @@ -1296,8 +1292,6 @@ paths: - "null" description: The display ID of the response delete: - security: - - apiKeyAuth: [] operationId: deleteResponse summary: Delete a response description: Deletes a response from the database. @@ -1426,8 +1420,6 @@ paths: description: The display ID of the response /contacts: get: - security: - - apiKeyAuth: [] operationId: getContacts summary: Get contacts description: Gets contacts from the database. @@ -1481,8 +1473,6 @@ paths: items: $ref: "#/components/schemas/contact" post: - security: - - apiKeyAuth: [] operationId: createContact summary: Create a contact description: Creates a contact in the database. @@ -1504,8 +1494,6 @@ paths: $ref: "#/components/schemas/contact" /contacts/{id}: get: - security: - - apiKeyAuth: [] operationId: getContact summary: Get a contact description: Gets a contact from the database. @@ -1525,8 +1513,6 @@ paths: schema: $ref: "#/components/schemas/contact" put: - security: - - apiKeyAuth: [] operationId: updateContact summary: Update a contact description: Updates a contact in the database. @@ -1553,8 +1539,6 @@ paths: schema: $ref: "#/components/schemas/contact" delete: - security: - - apiKeyAuth: [] operationId: deleteContact summary: Delete a contact description: Deletes a contact from the database. @@ -1575,8 +1559,6 @@ paths: $ref: "#/components/schemas/contact" /contact-attributes: get: - security: - - apiKeyAuth: [] operationId: getContactAttributes summary: Get contact attributes description: Gets contact attributes from the database. @@ -1650,8 +1632,6 @@ paths: - contactId - value post: - security: - - apiKeyAuth: [] operationId: createContactAttribute summary: Create a contact attribute description: Creates a contact attribute in the database. @@ -1669,8 +1649,6 @@ paths: description: Contact attribute created successfully. /contact-attributes/{id}: get: - security: - - apiKeyAuth: [] operationId: getContactAttribute summary: Get a contact attribute description: Gets a contact attribute from the database. @@ -1690,8 +1668,6 @@ paths: schema: $ref: "#/components/schemas/contactAttribute" put: - security: - - apiKeyAuth: [] operationId: updateContactAttribute summary: Update a contact attribute description: Updates a contact attribute in the database. @@ -1718,8 +1694,6 @@ paths: schema: $ref: "#/components/schemas/contactAttribute" delete: - security: - - apiKeyAuth: [] operationId: deleteContactAttribute summary: Delete a contact attribute description: Deletes a contact attribute from the database. @@ -1740,8 +1714,6 @@ paths: $ref: "#/components/schemas/contactAttribute" /contact-attribute-keys: get: - security: - - apiKeyAuth: [] operationId: getContactAttributeKeys summary: Get contact attribute keys description: Gets contact attribute keys from the database. @@ -1832,8 +1804,6 @@ paths: - type - environmentId post: - security: - - apiKeyAuth: [] operationId: createContactAttributeKey summary: Create a contact attribute key description: Creates a contact attribute key in the database. @@ -1851,8 +1821,6 @@ paths: description: Contact attribute key created successfully. /contact-attribute-keys/{id}: get: - security: - - apiKeyAuth: [] operationId: getContactAttributeKey summary: Get a contact attribute key description: Gets a contact attribute key from the database. @@ -1872,8 +1840,6 @@ paths: schema: $ref: "#/components/schemas/contactAttributeKey" put: - security: - - apiKeyAuth: [] operationId: updateContactAttributeKey summary: Update a contact attribute key description: Updates a contact attribute key in the database. @@ -1900,8 +1866,6 @@ paths: schema: $ref: "#/components/schemas/contactAttributeKey" delete: - security: - - apiKeyAuth: [] operationId: deleteContactAttributeKey summary: Delete a contact attribute key description: Deletes a contact attribute key from the database. @@ -1922,8 +1886,6 @@ paths: $ref: "#/components/schemas/contactAttributeKey" /surveys: get: - security: - - apiKeyAuth: [] operationId: getSurveys summary: Get surveys description: Gets surveys from the database. @@ -1994,8 +1956,6 @@ paths: items: $ref: "#/components/schemas/survey" post: - security: - - apiKeyAuth: [] operationId: createSurvey summary: Create a survey description: Creates a survey in the database. @@ -2017,8 +1977,6 @@ paths: $ref: "#/components/schemas/survey" /surveys/{id}: get: - security: - - apiKeyAuth: [] operationId: getSurvey summary: Get a survey description: Gets a survey from the database. @@ -2039,8 +1997,6 @@ paths: schema: $ref: "#/components/schemas/survey" put: - security: - - apiKeyAuth: [] operationId: updateSurvey summary: Update a survey description: Updates a survey in the database. @@ -2068,8 +2024,6 @@ paths: schema: $ref: "#/components/schemas/survey" delete: - security: - - apiKeyAuth: [] operationId: deleteSurvey summary: Delete a survey description: Deletes a survey from the database. @@ -2091,8 +2045,6 @@ paths: $ref: "#/components/schemas/survey" /webhooks: get: - security: - - apiKeyAuth: [] operationId: getWebhooks summary: Get webhooks description: Gets webhooks from the database. @@ -2116,17 +2068,13 @@ paths: name: sortBy schema: type: string - enum: - - createdAt - - updatedAt + enum: *a6 default: createdAt - in: query name: order schema: type: string - enum: - - asc - - desc + enum: *a7 default: desc - in: query name: startDate @@ -2182,7 +2130,7 @@ paths: description: The URL of the webhook source: type: string - enum: &a6 + enum: &a8 - user - zapier - make @@ -2195,7 +2143,7 @@ paths: type: array items: type: string - enum: &a7 + enum: &a9 - responseFinished - responseCreated - responseUpdated @@ -2215,8 +2163,6 @@ paths: offset: type: number post: - security: - - apiKeyAuth: [] operationId: createWebhook summary: Create a webhook description: Creates a webhook in the database. @@ -2241,7 +2187,7 @@ paths: description: The URL of the webhook source: type: string - enum: *a6 + enum: *a8 description: The source of the webhook environmentId: type: string @@ -2250,7 +2196,7 @@ paths: type: array items: type: string - enum: *a7 + enum: *a9 description: The triggers of the webhook surveyIds: type: array @@ -2294,7 +2240,7 @@ paths: description: The URL of the webhook source: type: string - enum: *a6 + enum: *a8 description: The source of the webhook environmentId: type: string @@ -2303,7 +2249,7 @@ paths: type: array items: type: string - enum: *a7 + enum: *a9 description: The triggers of the webhook surveyIds: type: array @@ -2312,8 +2258,6 @@ paths: description: "The IDs of the surveys " /webhooks/{webhookId}: get: - security: - - apiKeyAuth: [] operationId: getWebhook summary: Get a webhook description: Gets a webhook from the database. @@ -2356,7 +2300,7 @@ paths: description: The URL of the webhook source: type: string - enum: *a6 + enum: *a8 description: The source of the webhook environmentId: type: string @@ -2365,7 +2309,7 @@ paths: type: array items: type: string - enum: *a7 + enum: *a9 description: The triggers of the webhook surveyIds: type: array @@ -2373,8 +2317,6 @@ paths: type: string description: "The IDs of the surveys " put: - security: - - apiKeyAuth: [] operationId: updateWebhook summary: Update a webhook description: Updates a webhook in the database. @@ -2406,7 +2348,7 @@ paths: description: The URL of the webhook source: type: string - enum: *a6 + enum: *a8 description: The source of the webhook environmentId: type: string @@ -2415,7 +2357,7 @@ paths: type: array items: type: string - enum: *a7 + enum: *a9 description: The triggers of the webhook surveyIds: type: array @@ -2459,7 +2401,7 @@ paths: description: The URL of the webhook source: type: string - enum: *a6 + enum: *a8 description: The source of the webhook environmentId: type: string @@ -2468,7 +2410,7 @@ paths: type: array items: type: string - enum: *a7 + enum: *a9 description: The triggers of the webhook surveyIds: type: array @@ -2476,8 +2418,6 @@ paths: type: string description: "The IDs of the surveys " delete: - security: - - apiKeyAuth: [] operationId: deleteWebhook summary: Delete a webhook description: Deletes a webhook from the database. @@ -2520,7 +2460,7 @@ paths: description: The URL of the webhook source: type: string - enum: *a6 + enum: *a8 description: The source of the webhook environmentId: type: string @@ -2529,13 +2469,29 @@ paths: type: array items: type: string - enum: *a7 + enum: *a9 description: The triggers of the webhook surveyIds: type: array items: type: string description: "The IDs of the surveys " + /roles: + get: + operationId: getRoles + summary: Get roles + description: Gets roles from the database. + tags: + - Management API > Roles + responses: + "200": + description: Roles retrieved successfully. + content: + application/json: + schema: + type: array + items: + type: string components: securitySchemes: apiKeyAuth: @@ -2937,7 +2893,7 @@ components: required: - id - type - default: &a9 [] + default: &a11 [] description: The endings of the survey thankYouCard: type: @@ -3005,7 +2961,7 @@ components: description: Survey variables displayOption: type: string - enum: &a10 + enum: &a12 - displayOnce - displayMultiple - displaySome @@ -3084,7 +3040,7 @@ components: type: - string - "null" - enum: &a12 + enum: &a14 - bottomLeft - bottomRight - topLeft @@ -3239,13 +3195,13 @@ components: properties: linkSurveys: type: string - enum: &a8 + enum: &a10 - casual - straight - simple appSurveys: type: string - enum: *a8 + enum: *a10 required: - linkSurveys - appSurveys @@ -3262,7 +3218,7 @@ components: type: - string - "null" - enum: &a11 + enum: &a13 - animation - color - image @@ -3392,7 +3348,7 @@ components: description: The URL of the webhook source: type: string - enum: *a6 + enum: *a8 description: The source of the webhook environmentId: type: string @@ -3401,7 +3357,7 @@ components: type: array items: type: string - enum: *a7 + enum: *a9 description: The triggers of the webhook surveyIds: type: array @@ -3418,6 +3374,10 @@ components: - environmentId - triggers - surveyIds + role: + type: array + items: + type: string responseId: type: string description: The ID of the response @@ -3563,7 +3523,7 @@ components: required: - id - type - default: *a9 + default: *a11 description: The endings of the survey thankYouCard: type: @@ -3629,7 +3589,7 @@ components: description: Survey variables displayOption: type: string - enum: *a10 + enum: *a12 description: Display options for the survey recontactDays: type: @@ -3889,10 +3849,10 @@ components: properties: linkSurveys: type: string - enum: *a8 + enum: *a10 appSurveys: type: string - enum: *a8 + enum: *a10 required: - linkSurveys - appSurveys @@ -3909,7 +3869,7 @@ components: type: - string - "null" - enum: *a11 + enum: *a13 brightness: type: - number @@ -3942,7 +3902,7 @@ components: type: - string - "null" - enum: *a12 + enum: *a14 clickOutsideClose: type: - boolean