From 1ced76c44deae546533a7625311de212e27ee6e2 Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Mon, 6 Oct 2025 12:42:29 +0530 Subject: [PATCH] chore: added expirationDays param support in personal link api (#6578) Co-authored-by: pandeymangg --- .../contacts/[contactId]/lib/openapi.ts | 12 ++++- .../contacts/[contactId]/route.ts | 24 +++++++-- .../contacts/[contactId]/types/survey.ts | 11 ++++ .../contact-links/lib/utils.test.ts | 51 +++++++++++++++++++ .../[surveyId]/contact-links/lib/utils.ts | 5 ++ .../segments/[segmentId]/route.ts | 7 ++- docs/api-v2-reference/openapi.yml | 16 ++++++ 7 files changed, 117 insertions(+), 9 deletions(-) create mode 100644 apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/lib/utils.test.ts create mode 100644 apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/lib/utils.ts diff --git a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/lib/openapi.ts b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/lib/openapi.ts index 96c2049fa5..78ef76a2aa 100644 --- a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/lib/openapi.ts +++ b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/lib/openapi.ts @@ -1,7 +1,10 @@ -import { ZContactLinkParams } from "@/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/types/survey"; -import { makePartialSchema } from "@/modules/api/v2/types/openapi-response"; import { z } from "zod"; import { ZodOpenApiOperationObject } from "zod-openapi"; +import { + ZContactLinkParams, + ZContactLinkQuery, +} from "@/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/types/survey"; +import { makePartialSchema } from "@/modules/api/v2/types/openapi-response"; export const getPersonalizedSurveyLink: ZodOpenApiOperationObject = { operationId: "getPersonalizedSurveyLink", @@ -9,6 +12,7 @@ export const getPersonalizedSurveyLink: ZodOpenApiOperationObject = { description: "Retrieves a personalized link for a specific survey.", requestParams: { path: ZContactLinkParams, + query: ZContactLinkQuery, }, tags: ["Management API - Surveys - Contact Links"], responses: { @@ -20,6 +24,10 @@ export const getPersonalizedSurveyLink: ZodOpenApiOperationObject = { z.object({ data: z.object({ surveyUrl: z.string().url(), + expiresAt: z + .string() + .nullable() + .describe("The date and time the link expires, null if no expiration"), }), }) ), diff --git a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/route.ts b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/route.ts index 8bf733ecfa..c4f319df93 100644 --- a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/route.ts +++ b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/route.ts @@ -8,7 +8,9 @@ import { getSurvey } from "@/modules/api/v2/management/surveys/[surveyId]/contac import { TContactLinkParams, ZContactLinkParams, + ZContactLinkQuery, } from "@/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/types/survey"; +import { calculateExpirationDate } from "@/modules/api/v2/management/surveys/[surveyId]/contact-links/lib/utils"; import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { getContactSurveyLink } from "@/modules/ee/contacts/lib/contact-survey-link"; import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils"; @@ -19,9 +21,10 @@ export const GET = async (request: Request, props: { params: Promise { - const { params } = parsedInput; + const { params, query } = parsedInput; if (!params) { return handleApiError(request, { @@ -92,12 +95,27 @@ export const GET = async (request: Request, props: { params: Promise; +export type TContactLinkQuery = z.infer; diff --git a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/lib/utils.test.ts b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/lib/utils.test.ts new file mode 100644 index 0000000000..0101adbbe4 --- /dev/null +++ b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/lib/utils.test.ts @@ -0,0 +1,51 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { calculateExpirationDate } from "./utils"; + +describe("calculateExpirationDate", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + test("calculates expiration date for positive days", () => { + const baseDate = new Date("2024-01-15T12:00:00.000Z"); + vi.setSystemTime(baseDate); + + const result = calculateExpirationDate(7); + const expectedDate = new Date("2024-01-22T12:00:00.000Z"); + + expect(result).toBe(expectedDate.toISOString()); + }); + + test("handles zero expiration days", () => { + const baseDate = new Date("2024-01-15T12:00:00.000Z"); + vi.setSystemTime(baseDate); + + const result = calculateExpirationDate(0); + + expect(result).toBe(baseDate.toISOString()); + }); + + test("handles negative expiration days", () => { + const baseDate = new Date("2024-01-15T12:00:00.000Z"); + vi.setSystemTime(baseDate); + + const result = calculateExpirationDate(-5); + const expectedDate = new Date("2024-01-10T12:00:00.000Z"); + + expect(result).toBe(expectedDate.toISOString()); + }); + + test("returns valid ISO string format", () => { + const baseDate = new Date("2024-01-15T12:00:00.000Z"); + vi.setSystemTime(baseDate); + + const result = calculateExpirationDate(10); + const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; + + expect(result).toMatch(isoRegex); + }); +}); diff --git a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/lib/utils.ts b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/lib/utils.ts new file mode 100644 index 0000000000..675ee6cb31 --- /dev/null +++ b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/lib/utils.ts @@ -0,0 +1,5 @@ +export const calculateExpirationDate = (expirationDays: number) => { + const expirationDate = new Date(); + expirationDate.setDate(expirationDate.getDate() + expirationDays); + return expirationDate.toISOString(); +}; diff --git a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/route.ts b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/route.ts index 2e67aa9bd1..5da2a91e14 100644 --- a/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/route.ts +++ b/apps/web/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/route.ts @@ -1,7 +1,9 @@ +import { logger } from "@formbricks/logger"; import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client"; import { responses } from "@/modules/api/v2/lib/response"; import { handleApiError } from "@/modules/api/v2/lib/utils"; import { getEnvironmentId } from "@/modules/api/v2/management/lib/helper"; +import { calculateExpirationDate } from "@/modules/api/v2/management/surveys/[surveyId]/contact-links/lib/utils"; import { getContactsInSegment } from "@/modules/api/v2/management/surveys/[surveyId]/contact-links/segments/[segmentId]/lib/contact"; import { ZContactLinksBySegmentParams, @@ -11,7 +13,6 @@ import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error"; import { getContactSurveyLink } from "@/modules/ee/contacts/lib/contact-survey-link"; import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils"; import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils"; -import { logger } from "@formbricks/logger"; export const GET = async ( request: Request, @@ -76,9 +77,7 @@ export const GET = async ( // Calculate expiration date based on expirationDays let expiresAt: string | null = null; if (query?.expirationDays) { - const expirationDate = new Date(); - expirationDate.setDate(expirationDate.getDate() + query.expirationDays); - expiresAt = expirationDate.toISOString(); + expiresAt = calculateExpirationDate(query.expirationDays); } // Generate survey links for each contact diff --git a/docs/api-v2-reference/openapi.yml b/docs/api-v2-reference/openapi.yml index 1cca3e2d34..92e5e3803d 100644 --- a/docs/api-v2-reference/openapi.yml +++ b/docs/api-v2-reference/openapi.yml @@ -2017,6 +2017,17 @@ paths: type: string description: The ID of the contact required: true + - in: query + name: expirationDays + description: Number of days until the generated JWT expires. If not provided, + there is no expiration. + schema: + type: + - number + - undefined + minimum: 1 + description: Number of days until the generated JWT expires. If not provided, + there is no expiration. responses: "200": description: Personalized survey link retrieved successfully. @@ -2031,6 +2042,11 @@ paths: surveyUrl: type: string format: uri + expiresAt: + type: string + format: date-time + nullable: true + description: The date and time the link expires, null if no expiration required: - surveyUrl /surveys/{surveyId}/contact-links/segments/{segmentId}: