Repository: formbricks/formbricks
Pull request #5780: fix: suid bugs
Author: login: gupta-piyush19
Created: 2025-05-13T06:36:43Z | Merged: 2025-05-14T12:34:13Z

## What does this PR do?
fixes suId bugs 

Fixes https://github.com/formbricks/internal/issues/587

<!-- Please provide a screenshots or a loom video for visual changes to speed up reviews
 Loom Video: https://www.loom.com/
-->

## How should this be tested?

<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration -->

- Please test the bugs mentioned in the ticket, or connect with @gupta-piyush19 for testing instructions.
- Test B

## Checklist

<!-- We're starting to get more and more contributions. Please help us making this efficient for all of us and go through this checklist. Please tick off what you did  -->

### Required

- [ ] Filled out the "How to test" section in this PR
- [ ] Read [How we Code at Formbricks](<[https://github.com/formbricks/formbricks/blob/main/CONTRIBUTING.md](https://formbricks.com/docs/contributing/how-we-code)>)
- [ ] Self-reviewed my own code
- [ ] Commented on my code in hard-to-understand bits
- [ ] Ran `pnpm build`
- [ ] Checked for warnings, there are none
- [ ] Removed all `console.logs`
- [ ] Merged the latest changes from main onto my branch with `git pull origin main`
- [ ] My changes don't cause any responsiveness issues
- [ ] First PR at Formbricks? [Please sign the CLA!](https://cla-assistant.io/formbricks/formbricks) Without it we wont be able to merge it 🙏

### Appreciated

- [ ] If a UI change was made: Added a screen recording or screenshots to this PR
- [ ] Updated the Formbricks Docs if changes were necessary


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
	- Added validation to prevent updates to responses that are already marked as finished.
	- Introduced enhanced validation for surveys with single-use IDs, including support for encrypted single-use IDs and improved error messaging for missing or invalid IDs.

- **Improvements**
	- Updated cache revalidation logic to account for single-use IDs, ensuring more accurate cache management for single-use survey responses.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts b/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts
index 60a9bb7052bf..229403066d63 100644
--- a/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts
+++ b/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts
@@ -57,6 +57,10 @@ export const PUT = async (
     return handleDatabaseError(error, request.url, endpoint, responseId);
   }
 
+  if (response.finished) {
+    return responses.badRequestResponse("Response is already finished", undefined, true);
+  }
+
   // get survey to get environmentId
   let survey;
   try {
diff --git a/apps/web/app/api/v2/client/[environmentId]/responses/lib/utils.test.ts b/apps/web/app/api/v2/client/[environmentId]/responses/lib/utils.test.ts
index 62c69f4aa500..b603227056a8 100644
--- a/apps/web/app/api/v2/client/[environmentId]/responses/lib/utils.test.ts
+++ b/apps/web/app/api/v2/client/[environmentId]/responses/lib/utils.test.ts
@@ -3,6 +3,7 @@ import { verifyRecaptchaToken } from "@/app/api/v2/client/[environmentId]/respon
 import { checkSurveyValidity } from "@/app/api/v2/client/[environmentId]/responses/lib/utils";
 import { TResponseInputV2 } from "@/app/api/v2/client/[environmentId]/responses/types/response";
 import { responses } from "@/app/lib/api/response";
+import { symmetricDecrypt } from "@/lib/crypto";
 import { getIsSpamProtectionEnabled } from "@/modules/ee/license-check/lib/utils";
 import { Organization } from "@prisma/client";
 import { beforeEach, describe, expect, test, vi } from "vitest";
@@ -40,6 +41,13 @@ vi.mock("@formbricks/logger", () => ({
   },
 }));
 
+vi.mock("@/lib/crypto", () => ({
+  symmetricDecrypt: vi.fn(),
+}));
+vi.mock("@/lib/constants", () => ({
+  ENCRYPTION_KEY: "test-key",
+}));
+
 const mockSurvey: TSurvey = {
   id: "survey-1",
   createdAt: new Date(),
@@ -206,4 +214,119 @@ describe("checkSurveyValidity", () => {
     const result = await checkSurveyValidity(survey, "env-1", mockResponseInput);
     expect(result).toBeNull();
   });
+
+  test("should return badRequestResponse if singleUse is enabled and singleUseId is missing", async () => {
+    const survey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: false } };
+    const result = await checkSurveyValidity(survey, "env-1", { ...mockResponseInput });
+    expect(result).toBeInstanceOf(Response);
+    expect(result?.status).toBe(400);
+    expect(responses.badRequestResponse).toHaveBeenCalledWith("Missing single use id", {
+      surveyId: survey.id,
+      environmentId: "env-1",
+    });
+  });
+
+  test("should return badRequestResponse if singleUse is enabled and meta.url is missing", async () => {
+    const survey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: false } };
+    const result = await checkSurveyValidity(survey, "env-1", {
+      ...mockResponseInput,
+      singleUseId: "su-1",
+      meta: {},
+    });
+    expect(result).toBeInstanceOf(Response);
+    expect(result?.status).toBe(400);
+    expect(responses.badRequestResponse).toHaveBeenCalledWith("Missing or invalid URL in response metadata", {
+      surveyId: survey.id,
+      environmentId: "env-1",
+    });
+  });
+
+  test("should return badRequestResponse if meta.url is invalid", async () => {
+    const survey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: false } };
+    const result = await checkSurveyValidity(survey, "env-1", {
+      ...mockResponseInput,
+      singleUseId: "su-1",
+      meta: { url: "not-a-url" },
+    });
+    expect(result).toBeInstanceOf(Response);
+    expect(result?.status).toBe(400);
+    expect(responses.badRequestResponse).toHaveBeenCalledWith(
+      "Invalid URL in response metadata",
+      expect.objectContaining({ surveyId: survey.id, environmentId: "env-1" })
+    );
+  });
+
+  test("should return badRequestResponse if suId is missing from url", async () => {
+    const survey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: false } };
+    const url = "https://example.com/?foo=bar";
+    const result = await checkSurveyValidity(survey, "env-1", {
+      ...mockResponseInput,
+      singleUseId: "su-1",
+      meta: { url },
+    });
+    expect(result).toBeInstanceOf(Response);
+    expect(result?.status).toBe(400);
+    expect(responses.badRequestResponse).toHaveBeenCalledWith("Missing single use id", {
+      surveyId: survey.id,
+      environmentId: "env-1",
+    });
+  });
+
+  test("should return badRequestResponse if isEncrypted and decrypted suId does not match singleUseId", async () => {
+    const survey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: true } };
+    const url = "https://example.com/?suId=encrypted-id";
+    vi.mocked(symmetricDecrypt).mockReturnValue("decrypted-id");
+    const resultEncryptedMismatch = await checkSurveyValidity(survey, "env-1", {
+      ...mockResponseInput,
+      singleUseId: "su-1",
+      meta: { url },
+    });
+    expect(symmetricDecrypt).toHaveBeenCalledWith("encrypted-id", "test-key");
+    expect(resultEncryptedMismatch).toBeInstanceOf(Response);
+    expect(resultEncryptedMismatch?.status).toBe(400);
+    expect(responses.badRequestResponse).toHaveBeenCalledWith("Invalid single use id", {
+      surveyId: survey.id,
+      environmentId: "env-1",
+    });
+  });
+
+  test("should return badRequestResponse if not encrypted and suId does not match singleUseId", async () => {
+    const survey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: false } };
+    const url = "https://example.com/?suId=su-2";
+    const result = await checkSurveyValidity(survey, "env-1", {
+      ...mockResponseInput,
+      singleUseId: "su-1",
+      meta: { url },
+    });
+    expect(result).toBeInstanceOf(Response);
+    expect(result?.status).toBe(400);
+    expect(responses.badRequestResponse).toHaveBeenCalledWith("Invalid single use id", {
+      surveyId: survey.id,
+      environmentId: "env-1",
+    });
+  });
+
+  test("should return null if singleUse is enabled, not encrypted, and suId matches singleUseId", async () => {
+    const survey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: false } };
+    const url = "https://example.com/?suId=su-1";
+    const result = await checkSurveyValidity(survey, "env-1", {
+      ...mockResponseInput,
+      singleUseId: "su-1",
+      meta: { url },
+    });
+    expect(result).toBeNull();
+  });
+
+  test("should return null if singleUse is enabled, encrypted, and decrypted suId matches singleUseId", async () => {
+    const survey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: true } };
+    const url = "https://example.com/?suId=encrypted-id";
+    vi.mocked(symmetricDecrypt).mockReturnValue("su-1");
+    const _resultEncryptedMatch = await checkSurveyValidity(survey, "env-1", {
+      ...mockResponseInput,
+      singleUseId: "su-1",
+      meta: { url },
+    });
+    expect(symmetricDecrypt).toHaveBeenCalledWith("encrypted-id", "test-key");
+    expect(_resultEncryptedMatch).toBeNull();
+  });
 });
diff --git a/apps/web/app/api/v2/client/[environmentId]/responses/lib/utils.ts b/apps/web/app/api/v2/client/[environmentId]/responses/lib/utils.ts
index 63d0ad6e5eab..6ce24c2ab87b 100644
--- a/apps/web/app/api/v2/client/[environmentId]/responses/lib/utils.ts
+++ b/apps/web/app/api/v2/client/[environmentId]/responses/lib/utils.ts
@@ -2,6 +2,8 @@ import { getOrganizationBillingByEnvironmentId } from "@/app/api/v2/client/[envi
 import { verifyRecaptchaToken } from "@/app/api/v2/client/[environmentId]/responses/lib/recaptcha";
 import { TResponseInputV2 } from "@/app/api/v2/client/[environmentId]/responses/types/response";
 import { responses } from "@/app/lib/api/response";
+import { ENCRYPTION_KEY } from "@/lib/constants";
+import { symmetricDecrypt } from "@/lib/crypto";
 import { getIsSpamProtectionEnabled } from "@/modules/ee/license-check/lib/utils";
 import { logger } from "@formbricks/logger";
 import { TSurvey } from "@formbricks/types/surveys/types";
@@ -24,6 +26,55 @@ export const checkSurveyValidity = async (
     );
   }
 
+  if (survey.singleUse?.enabled) {
+    if (!responseInput.singleUseId) {
+      return responses.badRequestResponse("Missing single use id", {
+        surveyId: survey.id,
+        environmentId,
+      });
+    }
+
+    if (!responseInput.meta?.url) {
+      return responses.badRequestResponse("Missing or invalid URL in response metadata", {
+        surveyId: survey.id,
+        environmentId,
+      });
+    }
+
+    let url;
+    try {
+      url = new URL(responseInput.meta.url);
+    } catch (error) {
+      return responses.badRequestResponse("Invalid URL in response metadata", {
+        surveyId: survey.id,
+        environmentId,
+        error: error.message,
+      });
+    }
+    const suId = url.searchParams.get("suId");
+    if (!suId) {
+      return responses.badRequestResponse("Missing single use id", {
+        surveyId: survey.id,
+        environmentId,
+      });
+    }
+
+    if (survey.singleUse.isEncrypted) {
+      const decryptedSuId = symmetricDecrypt(suId, ENCRYPTION_KEY);
+      if (decryptedSuId !== responseInput.singleUseId) {
+        return responses.badRequestResponse("Invalid single use id", {
+          surveyId: survey.id,
+          environmentId,
+        });
+      }
+    } else if (responseInput.singleUseId !== suId) {
+      return responses.badRequestResponse("Invalid single use id", {
+        surveyId: survey.id,
+        environmentId,
+      });
+    }
+  }
+
   if (survey.recaptcha?.enabled) {
     if (!responseInput.recaptchaToken) {
       logger.error("Missing recaptcha token");
diff --git a/apps/web/lib/response/service.ts b/apps/web/lib/response/service.ts
index 55db15025f5c..5d58fd75d331 100644
--- a/apps/web/lib/response/service.ts
+++ b/apps/web/lib/response/service.ts
@@ -533,6 +533,7 @@ export const updateResponse = async (
       id: response.id,
       contactId: response.contact?.id,
       surveyId: response.surveyId,
+      ...(response.singleUseId ? { singleUseId: response.singleUseId } : {}),
     });
 
     responseNoteCache.revalidate({
diff --git a/apps/web/modules/api/v2/management/responses/[responseId]/lib/response.ts b/apps/web/modules/api/v2/management/responses/[responseId]/lib/response.ts
index 9634ae6b89e0..fe0630361470 100644
--- a/apps/web/modules/api/v2/management/responses/[responseId]/lib/response.ts
+++ b/apps/web/modules/api/v2/management/responses/[responseId]/lib/response.ts
@@ -111,6 +111,7 @@ export const updateResponse = async (
     responseCache.revalidate({
       id: updatedResponse.id,
       surveyId: updatedResponse.surveyId,
+      ...(updatedResponse.singleUseId ? { singleUseId: updatedResponse.singleUseId } : {}),
     });
 
     responseNoteCache.revalidate({
diff --git a/apps/web/modules/api/v2/management/responses/[responseId]/lib/tests/response.test.ts b/apps/web/modules/api/v2/management/responses/[responseId]/lib/tests/response.test.ts
index edd9fb78d60a..98829dd1ee0e 100644
--- a/apps/web/modules/api/v2/management/responses/[responseId]/lib/tests/response.test.ts
+++ b/apps/web/modules/api/v2/management/responses/[responseId]/lib/tests/response.test.ts
@@ -1,4 +1,5 @@
 import { response, responseId, responseInput, survey } from "./__mocks__/response.mock";
+import { responseCache } from "@/lib/response/cache";
 import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
 import { beforeEach, describe, expect, test, vi } from "vitest";
 import { prisma } from "@formbricks/database";
@@ -21,6 +22,16 @@ vi.mock("../utils", () => ({
   findAndDeleteUploadedFilesInResponse: vi.fn(),
 }));
 
+vi.mock("@/lib/response/cache", () => ({
+  responseCache: {
+    revalidate: vi.fn(),
+    tag: {
+      byId: vi.fn(),
+      byResponseId: vi.fn(),
+    },
+  },
+}));
+
 vi.mock("@formbricks/database", () => ({
   prisma: {
     response: {
@@ -175,7 +186,7 @@ describe("Response Lib", () => {
   });
 
   describe("updateResponse", () => {
-    test("update the response and revalidate caches", async () => {
+    test("update the response and revalidate caches including singleUseId", async () => {
       vi.mocked(prisma.response.update).mockResolvedValue(response);
 
       const result = await updateResponse(responseId, responseInput);
@@ -184,12 +195,39 @@ describe("Response Lib", () => {
         data: responseInput,
       });
 
+      expect(responseCache.revalidate).toHaveBeenCalledWith({
+        id: response.id,
+        surveyId: response.surveyId,
+        singleUseId: response.singleUseId,
+      });
+
       expect(result.ok).toBe(true);
       if (result.ok) {
         expect(result.data).toEqual(response);
       }
     });
 
+    test("update the response and revalidate caches", async () => {
+      const responseWithoutSingleUseId = { ...response, singleUseId: null };
+      vi.mocked(prisma.response.update).mockResolvedValue(responseWithoutSingleUseId);
+
+      const result = await updateResponse(responseId, responseInput);
+      expect(prisma.response.update).toHaveBeenCalledWith({
+        where: { id: responseId },
+        data: responseInput,
+      });
+
+      expect(responseCache.revalidate).toHaveBeenCalledWith({
+        id: response.id,
+        surveyId: response.surveyId,
+      });
+
+      expect(result.ok).toBe(true);
+      if (result.ok) {
+        expect(result.data).toEqual(responseWithoutSingleUseId);
+      }
+    });
+
     test("return a not_found error when the response is not found", async () => {
       vi.mocked(prisma.response.update).mockRejectedValue(
         new PrismaClientKnownRequestError("Response not found", {


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>apps/web/app/api/v2/client/[environmentId]/responses/lib/utils.ts (1)</summary><blockquote>

`37-39`: **Consider validating URL more strictly**

The current implementation creates a URL object from `responseInput.meta?.url || ""`, which could lead to unexpected behavior if the URL is empty or malformed.


Consider validating the URL more strictly:

```diff
-const url = new URL(responseInput.meta?.url || "");
-const suId = url.searchParams.get("suId");
-if (!suId) {
+if (!responseInput.meta?.url) {
+  return responses.badRequestResponse("Missing URL in response metadata", {
+    surveyId: survey.id,
+    environmentId,
+  });
+}
+
+try {
+  const url = new URL(responseInput.meta.url);
+  const suId = url.searchParams.get("suId");
+  if (!suId) {
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between dde5a55446a1a351f4f57dbcbccfa9722ea5fa81 and f1096aaa5d03ec624ddba6240819dd9bc279671f.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts` (1 hunks)
* `apps/web/app/api/v2/client/[environmentId]/responses/lib/utils.ts` (2 hunks)
* `apps/web/lib/response/service.ts` (1 hunks)
* `apps/web/modules/api/v2/management/responses/[responseId]/lib/response.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts (1)</summary><blockquote>

<details>
<summary>apps/web/modules/api/v2/lib/response.ts (1)</summary>

* `responses` (319-331)

</details>

</blockquote></details>
<details>
<summary>apps/web/app/api/v2/client/[environmentId]/responses/lib/utils.ts (3)</summary><blockquote>

<details>
<summary>apps/web/modules/api/v2/lib/response.ts (1)</summary>

* `responses` (319-331)

</details>
<details>
<summary>apps/web/lib/crypto.ts (1)</summary>

* `symmetricDecrypt` (78-92)

</details>
<details>
<summary>apps/web/lib/constants.ts (1)</summary>

* `ENCRYPTION_KEY` (20-20)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (6)</summary>

* GitHub Check: Run Unit Tests / Unit Tests
* GitHub Check: Build Formbricks-web / Build Formbricks-web
* GitHub Check: Run E2E Tests / Run E2E Tests
* GitHub Check: Validate Docker Build
* GitHub Check: SonarQube
* GitHub Check: Analyze (javascript)

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts (1)</summary>

`60-62`: **Prevention of modifying finished responses - Good improvement!**

This check appropriately prevents clients from modifying responses that are already marked as finished, maintaining data integrity and preventing potential abuse.

</details>
<details>
<summary>apps/web/lib/response/service.ts (1)</summary>

`536-536`: **Good addition of singleUseId to cache revalidation params**

The conditional inclusion of singleUseId in cache revalidation parameters ensures proper cache invalidation for single-use responses, maintaining consistency with the changes in the other files.

</details>
<details>
<summary>apps/web/modules/api/v2/management/responses/[responseId]/lib/response.ts (1)</summary>

`114-114`: **Proper cache revalidation with singleUseId**

This enhancement correctly includes the singleUseId for cache invalidation when present, ensuring cache consistency for single-use responses.

</details>
<details>
<summary>apps/web/app/api/v2/client/[environmentId]/responses/lib/utils.ts (1)</summary>

`5-6`: **Proper imports for encryption functionality**

The imports for ENCRYPTION_KEY and symmetricDecrypt are correctly added to support the new single-use ID validation logic.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T06:42:28Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This pull request aims to fix issues related to single-use ID (suId) handling in survey responses. Key changes include:
- Conditional inclusion of the "singleUseId" property in updateResponse functions.
- Enhanced validation for single-use IDs in client-side survey utilities, including decryption.
- A new check to prevent updates to responses that are already finished.

### Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

| File | Description |
| ---- | ----------- |
| apps/web/modules/api/v2/management/responses/[responseId]/lib/response.ts | Added conditional spread for "singleUseId" during response cache revalidation. |
| apps/web/lib/response/service.ts | Introduced similar conditional inclusion for "singleUseId" in the service layer. |
| apps/web/app/api/v2/client/[environmentId]/responses/lib/utils.ts | Extended survey validity checks for single-use IDs with encryption support and improved error responses. |
| apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts | Added a pre-update check rejecting modifications on finished responses. |




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T06:42:55Z
user: login: Dhruwang
body: Looks good ✅🚀
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-14T10:45:32Z

Review comments:
diff_hunk: @@ -24,6 +26,39 @@ export const checkSurveyValidity = async (
     );
   }
 
+  if (survey.singleUse?.enabled) {
+    if (!responseInput.singleUseId) {
+      return responses.badRequestResponse("Missing single use id", {
+        surveyId: survey.id,
+        environmentId,
+      });
+    }
+
+    const url = new URL(responseInput.meta?.url || "");
+    const suId = url.searchParams.get("suId");
+    if (!suId) {
+      return responses.badRequestResponse("Missing single use id", {
+        surveyId: survey.id,
+        environmentId,
+      });
+    }
+
+    if (survey.singleUse.isEncrypted) {
+      const decryptedSuId = symmetricDecrypt(suId, ENCRYPTION_KEY);
+      if (decryptedSuId !== responseInput.singleUseId) {
+        return responses.badRequestResponse("Invalid single use id", {
+          surveyId: survey.id,
+          environmentId,
+        });
+      }
+    } else if (responseInput.singleUseId !== suId) {
+      return responses.badRequestResponse("Invalid single use id", {
+        surveyId: survey.id,
+        environmentId,
+      });
+    }
+  }
path: apps/web/app/api/v2/client/[environmentId]/responses/lib/utils.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Comprehensive validation for single-use surveys**

The implementation thoroughly validates single-use IDs for surveys configured with this feature, handling both encrypted and unencrypted scenarios.



A potential improvement would be to add error handling for decryption failures, as symmetricDecrypt might throw exceptions:

```diff
 if (survey.singleUse.isEncrypted) {
-  const decryptedSuId = symmetricDecrypt(suId, ENCRYPTION_KEY);
-  if (decryptedSuId !== responseInput.singleUseId) {
-    return responses.badRequestResponse("Invalid single use id", {
-      surveyId: survey.id,
-      environmentId,
-    });
-  }
+  try {
+    const decryptedSuId = symmetricDecrypt(suId, ENCRYPTION_KEY);
+    if (decryptedSuId !== responseInput.singleUseId) {
+      return responses.badRequestResponse("Invalid single use id", {
+        surveyId: survey.id,
+        environmentId,
+      });
+    }
+  } catch (error) {
+    logger.error({ error, suId }, "Failed to decrypt single use id");
+    return responses.badRequestResponse("Invalid encrypted single use id", {
+      surveyId: survey.id,
+      environmentId,
+    });
+  }
```

Let's verify if there are any other instances of symmetricDecrypt that include try/catch blocks:

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for symmetricDecrypt usage with try/catch blocks
rg -A 5 -B 5 "symmetricDecrypt.*try\s*\{" --type ts
```

Length of output: 53

---

Let's locate where `symmetricDecrypt` is defined and see if it can throw:

---

🏁 Script executed:

```shell
#!/bin/bash
# List all usages of symmetricDecrypt
rg -n "symmetricDecrypt" --type ts

# Find its implementation (export or declaration)
rg -n "export function symmetricDecrypt" -C 5 --type ts
rg -n "function symmetricDecrypt" -C 5 --type ts
```

Length of output: 6741

---

Let’s inspect the `symmetricDecrypt` implementation and its V1/V2 helpers to confirm whether they can throw:

---

🏁 Script executed:

```shell
#!/bin/bash
# Show symmetricDecrypt and its V1/V2 implementations
rg -C 20 "export function symmetricDecrypt" -n apps/web/lib/crypto.ts
rg -C 20 "const symmetricDecryptV1" -n apps/web/lib/crypto.ts
rg -C 20 "const symmetricDecryptV2" -n apps/web/lib/crypto.ts
```

Length of output: 5097

---

We should confirm if `logger` is already imported in this file before suggesting its use:

---

🏁 Script executed:

```shell
#!/bin/bash
# Check top imports in the utils.ts file for logger
rg -n "^import.*logger" -C 3 -- 'apps/web/app/api/v2/client/[environmentId]/responses/lib/utils.ts'
```

Length of output: 486

---



</details>

**Add error handling around symmetricDecrypt in single-use survey validation**

`symmetricDecrypt` can throw on invalid or tampered payloads (V2 rethrows on auth failures; V1 may throw on bad data). Wrap the decryption call in a try/catch, log the error, and return a bad-request response to avoid uncaught exceptions.

- File: apps/web/app/api/v2/client/[environmentId]/responses/lib/utils.ts  
  Location: inside the `if (survey.singleUse.isEncrypted) { … }` block (≈ line 47)

```diff
   if (survey.singleUse.isEncrypted) {
-    const decryptedSuId = symmetricDecrypt(suId, ENCRYPTION_KEY);
-    if (decryptedSuId !== responseInput.singleUseId) {
-      return responses.badRequestResponse("Invalid single use id", {
-        surveyId: survey.id,
-        environmentId,
-      });
-    }
+    try {
+      const decryptedSuId = symmetricDecrypt(suId, ENCRYPTION_KEY);
+      if (decryptedSuId !== responseInput.singleUseId) {
+        return responses.badRequestResponse("Invalid single use id", {
+          surveyId: survey.id,
+          environmentId,
+        });
+      }
+    } catch (error) {
+      logger.error({ error, suId }, "Failed to decrypt single use id");
+      return responses.badRequestResponse("Invalid encrypted single use id", {
+        surveyId: survey.id,
+        environmentId,
+      });
+    }
   }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  if (survey.singleUse?.enabled) {
    if (!responseInput.singleUseId) {
      return responses.badRequestResponse("Missing single use id", {
        surveyId: survey.id,
        environmentId,
      });
    }

    const url = new URL(responseInput.meta?.url || "");
    const suId = url.searchParams.get("suId");
    if (!suId) {
      return responses.badRequestResponse("Missing single use id", {
        surveyId: survey.id,
        environmentId,
      });
    }

    if (survey.singleUse.isEncrypted) {
      try {
        const decryptedSuId = symmetricDecrypt(suId, ENCRYPTION_KEY);
        if (decryptedSuId !== responseInput.singleUseId) {
          return responses.badRequestResponse("Invalid single use id", {
            surveyId: survey.id,
            environmentId,
          });
        }
      } catch (error) {
        logger.error({ error, suId }, "Failed to decrypt single use id");
        return responses.badRequestResponse("Invalid encrypted single use id", {
          surveyId: survey.id,
          environmentId,
        });
      }
    } else if (responseInput.singleUseId !== suId) {
      return responses.badRequestResponse("Invalid single use id", {
        surveyId: survey.id,
        environmentId,
      });
    }
  }
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In apps/web/app/api/v2/client/[environmentId]/responses/lib/utils.ts around
lines 29 to 60, the call to symmetricDecrypt inside the if
(survey.singleUse.isEncrypted) block can throw exceptions on invalid input. To
fix this, wrap the symmetricDecrypt call in a try/catch block, log the caught
error using the existing logger if available, and return a bad-request response
indicating an invalid single use id. This will prevent uncaught exceptions and
provide clearer error handling.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-13T06:42:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,6 +26,39 @@ export const checkSurveyValidity = async (
     );
   }
 
+  if (survey.singleUse?.enabled) {
+    if (!responseInput.singleUseId) {
+      return responses.badRequestResponse("Missing single use id", {
+        surveyId: survey.id,
+        environmentId,
+      });
+    }
+
+    const url = new URL(responseInput.meta?.url || "");
path: apps/web/app/api/v2/client/[environmentId]/responses/lib/utils.ts
user: login: Copilot
body: Constructing a URL with an empty string may throw an exception if responseInput.meta?.url is missing or invalid. Consider validating that a proper URL is available before creating the URL object.
```suggestion
    if (!responseInput.meta?.url) {
      return responses.badRequestResponse("Missing or invalid URL in response metadata", {
        surveyId: survey.id,
        environmentId,
      });
    }

    let url;
    try {
      url = new URL(responseInput.meta.url);
    } catch (error) {
      return responses.badRequestResponse("Invalid URL in response metadata", {
        surveyId: survey.id,
        environmentId,
        error: error.message,
      });
    }
```
created_at: 2025-05-13T06:42:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -111,6 +111,7 @@ export const updateResponse = async (
     responseCache.revalidate({
       id: updatedResponse.id,
       surveyId: updatedResponse.surveyId,
+      ...(updatedResponse.singleUseId && { singleUseId: updatedResponse.singleUseId }),
path: apps/web/modules/api/v2/management/responses/[responseId]/lib/response.ts
user: login: Copilot
body: [nitpick] Consider using a consistent pattern for conditionally adding properties across similar files (e.g., aligning with the ternary pattern used in service.ts) for improved readability.
```suggestion
      ...(updatedResponse.singleUseId ? { singleUseId: updatedResponse.singleUseId } : {}),
```
created_at: 2025-05-13T06:42:55Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-13T06:37:02Z
body: Thank you for following the naming conventions for pull request titles! 🙏
<!-- Sticky Pull Request Commentpr-title-lint-error -->
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-05-13T06:42:24Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

```markdown
## Walkthrough

The changes introduce validation and cache revalidation enhancements related to survey responses with single-use IDs. In the API for updating responses, a check now prevents updates to responses already marked as finished. In survey validation logic, new checks ensure that when a survey is configured for single-use IDs, the provided and URL-extracted IDs match, with support for encrypted IDs. Additionally, cache revalidation logic in two service modules is updated to include the singleUseId property when present, affecting how cache invalidation is handled for single-use responses.

## Suggested labels

`bug`

## Suggested reviewers

- Dhruwang
```

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> - CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> 
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 63cd3a591b53e78354e8c96418fd2068ee18a78b and f829a9124acf0e720031379750886fbe52de996c.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `apps/web/app/api/v2/client/[environmentId]/responses/lib/utils.test.ts` (3 hunks)
* `apps/web/modules/api/v2/management/responses/[responseId]/lib/tests/response.test.ts` (4 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (7)</summary>

* GitHub Check: Build Formbricks-web / Build Formbricks-web
* GitHub Check: Run Unit Tests / Unit Tests
* GitHub Check: Run E2E Tests / Run E2E Tests
* GitHub Check: Run Linters / Linters
* GitHub Check: Validate Docker Build
* GitHub Check: SonarQube
* GitHub Check: Analyze (javascript)

</details>

<details>
<summary>🔇 Additional comments (14)</summary><blockquote>

<details>
<summary>apps/web/modules/api/v2/management/responses/[responseId]/lib/tests/response.test.ts (4)</summary>

`2-2`: **Necessary import for testing cache revalidation**

This import is correctly added to support the new cache revalidation test cases.

---

`25-33`: **Good mock implementation for response cache**

The mock correctly implements the expected interface with `revalidate` and `tag` methods, which is necessary for testing the cache revalidation logic.

---

`189-208`: **Updated test case for response with singleUseId**

The test case has been properly updated to verify that cache revalidation includes the `singleUseId` property when present in the response.

---

`210-229`: **New test case for response without singleUseId**

This test case properly validates that cache revalidation occurs correctly when `singleUseId` is null, ensuring the property is excluded from the revalidation parameters.

</details>
<details>
<summary>apps/web/app/api/v2/client/[environmentId]/responses/lib/utils.test.ts (10)</summary>

`6-6`: **Import for decryption functionality**

The symmetric decryption import is correctly added for testing encrypted singleUseId validation.

---

`44-49`: **Proper mocks for crypto functionality**

These mocks correctly set up the `symmetricDecrypt` function and the `ENCRYPTION_KEY` constant required for testing the encrypted singleUseId validation.

---

`218-227`: **Test for missing singleUseId**

This test correctly verifies that a bad request response is returned when singleUseId is missing for a survey with singleUse enabled.

---

`229-242`: **Test for missing meta.url**

This test properly validates that a bad request response is returned when meta.url is missing, which is required for extracting and validating the suId parameter.

---

`244-257`: **Test for invalid URL format**

This test ensures that improperly formatted URLs are rejected with a bad request response, preventing potential issues with URL parsing.

---

`259-273`: **Test for missing suId query parameter**

This test properly verifies that when the suId query parameter is missing from the URL, a bad request response is returned.

---

`275-291`: **Test for encrypted suId mismatch**

This test correctly validates the encrypted suId flow by mocking the decryption and verifying that mismatched IDs result in a bad request response.

---

`293-307`: **Test for unencrypted suId mismatch**

This test properly ensures that when singleUse is enabled without encryption, the suId parameter must still match the provided singleUseId.

---

`309-318`: **Test for valid unencrypted suId**

This test confirms the happy path when singleUse is enabled without encryption and the suId matches the singleUseId.

---

`320-331`: **Test for valid encrypted suId**

This test properly validates the successful case where an encrypted suId is correctly decrypted and matches the expected singleUseId.

</details>

</blockquote></details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=formbricks/formbricks&utm_content=5780):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-05-13T07:24:02Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=formbricks_formbricks&pullRequest=5780) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=formbricks_formbricks&pullRequest=5780&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=formbricks_formbricks&pullRequest=5780&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=formbricks_formbricks&pullRequest=5780&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [98.5% Coverage on New Code](https://sonarcloud.io/component_measures?id=formbricks_formbricks&pullRequest=5780&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=formbricks_formbricks&pullRequest=5780&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=formbricks_formbricks&pullRequest=5780)


author_association: NONE