mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-20 19:48:52 -05:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5673f4049b | |||
| f0967c2e23 |
+2
-2
@@ -2,7 +2,7 @@
|
||||
|
||||
import { z } from "zod";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { InvalidInputError, OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { OperationNotAllowedError, ResourceNotFoundError, UnknownError } from "@formbricks/types/errors";
|
||||
import { getEmailTemplateHtml } from "@/app/(app)/workspaces/[workspaceId]/surveys/[surveyId]/(analysis)/summary/lib/emailTemplate";
|
||||
import { capturePostHogEvent } from "@/lib/posthog";
|
||||
import { getSurvey, updateSurvey } from "@/lib/survey/service";
|
||||
@@ -176,7 +176,7 @@ export const generatePersonalLinksAction = authenticatedActionClient
|
||||
);
|
||||
|
||||
if (!contactsResult || contactsResult.length === 0) {
|
||||
throw new InvalidInputError("No contacts found for the selected segment");
|
||||
throw new UnknownError("No contacts found for the selected segment");
|
||||
}
|
||||
|
||||
capturePostHogEvent(
|
||||
|
||||
@@ -103,6 +103,7 @@ describe("getWorkspaceStateData", () => {
|
||||
id: workspaceId,
|
||||
appSetupCompleted: true,
|
||||
workspaceSettings: {
|
||||
id: workspaceId,
|
||||
recontactDays: 30,
|
||||
clickOutsideClose: true,
|
||||
overlay: "none",
|
||||
@@ -111,7 +112,14 @@ describe("getWorkspaceStateData", () => {
|
||||
styling: { allowStyleOverwrite: false },
|
||||
},
|
||||
},
|
||||
surveys: mockWorkspaceData.surveys,
|
||||
// `survey.name` is replaced with a back-compat placeholder; segment was
|
||||
// null in the mock so the sanitized segment stays null.
|
||||
surveys: [
|
||||
{
|
||||
...mockWorkspaceData.surveys[0],
|
||||
name: "[deprecated] survey name omitted from public API - will be removed soon",
|
||||
},
|
||||
],
|
||||
actionClasses: mockWorkspaceData.actionClasses,
|
||||
});
|
||||
|
||||
@@ -211,6 +219,7 @@ describe("getWorkspaceStateData", () => {
|
||||
const result = await getWorkspaceStateData(workspaceId);
|
||||
|
||||
expect(result.workspace.workspaceSettings).toEqual({
|
||||
id: workspaceId,
|
||||
recontactDays: 14,
|
||||
clickOutsideClose: false,
|
||||
overlay: "dark",
|
||||
|
||||
@@ -42,6 +42,7 @@ export const getWorkspaceStateData = async (workspaceId: string): Promise<Worksp
|
||||
where: { id: workspaceId },
|
||||
select: {
|
||||
id: true,
|
||||
legacyEnvironmentId: true,
|
||||
appSetupCompleted: true,
|
||||
recontactDays: true,
|
||||
clickOutsideClose: true,
|
||||
@@ -72,7 +73,9 @@ export const getWorkspaceStateData = async (workspaceId: string): Promise<Worksp
|
||||
select: {
|
||||
id: true,
|
||||
welcomeCard: true,
|
||||
// name intentionally omitted — internal label not needed by the SDK
|
||||
// `name` deliberately not selected — internal label not needed by the
|
||||
// SDK and replaced with a fixed placeholder below so older SDKs that
|
||||
// decoded `Survey.name` as a required field keep working.
|
||||
questions: true,
|
||||
blocks: true,
|
||||
variables: true,
|
||||
@@ -99,9 +102,9 @@ export const getWorkspaceStateData = async (workspaceId: string): Promise<Worksp
|
||||
styling: true,
|
||||
status: true,
|
||||
recaptcha: true,
|
||||
// Fetch only what's needed to compute the minimal segment shape.
|
||||
// Titles, descriptions, and filter conditions are evaluated server-side
|
||||
// and must not be sent to the browser.
|
||||
// Only need to know if any filters exist so we can compute
|
||||
// `hasFilters`. Real filter values, segment title/description, and
|
||||
// surveys-list relation are never exposed to clients.
|
||||
segment: {
|
||||
select: {
|
||||
id: true,
|
||||
@@ -135,17 +138,46 @@ export const getWorkspaceStateData = async (workspaceId: string): Promise<Worksp
|
||||
throw new ResourceNotFoundError("workspace", workspaceId);
|
||||
}
|
||||
|
||||
// Transform surveys using the shared utility, then replace the segment with
|
||||
// the minimal public shape (id + hasFilters). We null out segment before
|
||||
// calling transformPrismaSurvey because that function expects a surveys[]
|
||||
// relation on the segment object (used by the management API), which we
|
||||
// intentionally don't fetch here.
|
||||
// Backwards-compat response shape for SDKs from before PR #7931. Those
|
||||
// clients decoded `survey.name` and the full `segment` object as required
|
||||
// fields, so the response must still carry that shape — but every field
|
||||
// that could leak sensitive targeting data is replaced with a placeholder.
|
||||
// The actual segment-membership check happens server-side (segment IDs in
|
||||
// POST /user); SDKs only inspect `filters.length` / `hasFilters` locally.
|
||||
//
|
||||
// `environmentId` mirrors `legacyEnvironmentId ?? workspace.id`, matching
|
||||
// the `/me` endpoints' pattern so migrated workspaces keep returning the
|
||||
// original env ID older clients persisted.
|
||||
const legacyOrCurrentId = workspaceData.legacyEnvironmentId ?? workspaceData.id;
|
||||
const placeholderDate = new Date(0);
|
||||
const placeholderFilter = {
|
||||
id: "placeholder",
|
||||
connector: null,
|
||||
resource: {
|
||||
id: "placeholder",
|
||||
root: { type: "device", deviceType: "phone" },
|
||||
value: "deprecated",
|
||||
qualifier: { operator: "equals" },
|
||||
},
|
||||
};
|
||||
|
||||
const transformedSurveys = workspaceData.surveys.map((survey) => {
|
||||
const minimalSegment = survey.segment
|
||||
const realHasFilters =
|
||||
Array.isArray(survey.segment?.filters) && (survey.segment.filters as unknown[]).length > 0;
|
||||
|
||||
const sanitizedSegment = survey.segment
|
||||
? {
|
||||
id: survey.segment.id,
|
||||
hasFilters:
|
||||
Array.isArray(survey.segment.filters) && (survey.segment.filters as unknown[]).length > 0,
|
||||
title: "[deprecated] segment title omitted from public API - will be removed soon",
|
||||
description: null,
|
||||
isPrivate: true,
|
||||
filters: realHasFilters ? [placeholderFilter] : [],
|
||||
environmentId: legacyOrCurrentId,
|
||||
workspaceId: legacyOrCurrentId,
|
||||
createdAt: placeholderDate,
|
||||
updatedAt: placeholderDate,
|
||||
surveys: [],
|
||||
hasFilters: realHasFilters,
|
||||
}
|
||||
: null;
|
||||
|
||||
@@ -155,7 +187,11 @@ export const getWorkspaceStateData = async (workspaceId: string): Promise<Worksp
|
||||
segment: null,
|
||||
});
|
||||
|
||||
return { ...transformed, segment: minimalSegment };
|
||||
return {
|
||||
...transformed,
|
||||
name: "[deprecated] survey name omitted from public API - will be removed soon",
|
||||
segment: sanitizedSegment,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -163,6 +199,7 @@ export const getWorkspaceStateData = async (workspaceId: string): Promise<Worksp
|
||||
id: workspaceData.id,
|
||||
appSetupCompleted: workspaceData.appSetupCompleted,
|
||||
workspaceSettings: {
|
||||
id: workspaceData.id,
|
||||
recontactDays: workspaceData.recontactDays,
|
||||
clickOutsideClose: workspaceData.clickOutsideClose,
|
||||
overlay: workspaceData.overlay,
|
||||
@@ -171,7 +208,11 @@ export const getWorkspaceStateData = async (workspaceId: string): Promise<Worksp
|
||||
styling: resolveStorageUrlsInObject(workspaceData.styling),
|
||||
},
|
||||
},
|
||||
surveys: resolveStorageUrlsInObject(transformedSurveys),
|
||||
// The runtime shape carries extra back-compat fields (placeholder
|
||||
// segment, `hasFilters`, mirrored `environmentId`) that aren't part of
|
||||
// the modern `TJsWorkspaceStateSurvey`. Cast through unknown — this is
|
||||
// intentional and only this endpoint's response widens the type.
|
||||
surveys: resolveStorageUrlsInObject(transformedSurveys) as unknown as TJsWorkspaceStateSurvey[],
|
||||
actionClasses: workspaceData.actionClasses,
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { z } from "zod";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { InvalidInputError, OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { ZSegmentCreateInput, ZSegmentFilters, ZSegmentUpdateInput } from "@formbricks/types/segment";
|
||||
import { getOrganization } from "@/lib/organization/service";
|
||||
import { capturePostHogEvent } from "@/lib/posthog";
|
||||
@@ -49,7 +49,7 @@ export const createSegmentAction = authenticatedActionClient.inputSchema(ZSegmen
|
||||
const surveyWorkspaceId = await getWorkspaceIdFromSurveyId(parsedInput.surveyId);
|
||||
|
||||
if (surveyWorkspaceId !== parsedInput.workspaceId) {
|
||||
throw new InvalidInputError("Survey and segment are not in the same workspace");
|
||||
throw new Error("Survey and segment are not in the same workspace");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ export const createSegmentAction = authenticatedActionClient.inputSchema(ZSegmen
|
||||
if (!parsedFilters.success) {
|
||||
const errMsg =
|
||||
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message || "Invalid filters";
|
||||
throw new InvalidInputError(errMsg);
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
||||
const segment = await createSegment(parsedInput);
|
||||
@@ -139,7 +139,7 @@ export const updateSegmentAction = authenticatedActionClient.inputSchema(ZUpdate
|
||||
if (!parsedFilters.success) {
|
||||
const errMsg =
|
||||
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message || "Invalid filters";
|
||||
throw new InvalidInputError(errMsg);
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
||||
await checkForRecursiveSegmentFilter(parsedFilters.data, parsedInput.segmentId);
|
||||
@@ -169,7 +169,7 @@ export const loadNewSegmentAction = authenticatedActionClient
|
||||
const segmentWorkspaceId = await getWorkspaceIdFromSegmentId(parsedInput.segmentId);
|
||||
|
||||
if (surveyWorkspaceId !== segmentWorkspaceId) {
|
||||
throw new InvalidInputError("Segment and survey are not in the same workspace");
|
||||
throw new Error("Segment and survey are not in the same workspace");
|
||||
}
|
||||
|
||||
const organizationId = await getOrganizationIdFromSurveyId(parsedInput.surveyId);
|
||||
|
||||
@@ -92,6 +92,26 @@ This function allows rendering values dynamically.
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Render a Kubernetes EnvVar from chart env maps.
|
||||
Scalar values become quoted string values. Map values are rendered as EnvVar fields,
|
||||
which keeps advanced forms such as valueFrom supported.
|
||||
*/}}
|
||||
{{- define "formbricks.envVarValue" -}}
|
||||
{{- $value := .value -}}
|
||||
{{- if kindIs "map" $value -}}
|
||||
{{- include "formbricks.tplvalues.render" (dict "value" $value "context" .context) -}}
|
||||
{{- else if kindIs "invalid" $value -}}
|
||||
value: ""
|
||||
{{- else -}}
|
||||
value: {{ include "formbricks.tplvalues.render" (dict "value" (toString $value) "context" .context) | trim | quote }}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "formbricks.envVar" -}}
|
||||
- name: {{ include "formbricks.tplvalues.render" (dict "value" .name "context" .context) }}
|
||||
{{- include "formbricks.envVarValue" (dict "value" .value "context" .context) | nindent 2 }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Allow the release namespace to be overridden.
|
||||
|
||||
@@ -97,12 +97,7 @@ spec:
|
||||
{{- end }}
|
||||
env:
|
||||
{{- range $key, $value := .Values.cube.env }}
|
||||
- name: {{ include "formbricks.tplvalues.render" ( dict "value" $key "context" $ ) }}
|
||||
{{- if kindIs "string" $value }}
|
||||
value: {{ include "formbricks.tplvalues.render" ( dict "value" $value "context" $ ) | quote }}
|
||||
{{- else }}
|
||||
{{- toYaml $value | nindent 14 }}
|
||||
{{- end }}
|
||||
{{- include "formbricks.envVar" (dict "name" $key "value" $value "context" $) | nindent 12 }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: cube-config
|
||||
|
||||
@@ -136,12 +136,7 @@ spec:
|
||||
value: "http://{{ include "formbricks.hubname" . }}:8080"
|
||||
{{- end }}
|
||||
{{- range $key, $value := .Values.deployment.env }}
|
||||
- name: {{ include "formbricks.tplvalues.render" ( dict "value" $key "context" $ ) }}
|
||||
{{- if kindIs "string" $value }}
|
||||
value: {{ include "formbricks.tplvalues.render" ( dict "value" $value "context" $ ) | quote }}
|
||||
{{- else }}
|
||||
{{- toYaml $value | nindent 14 }}
|
||||
{{- end }}
|
||||
{{- include "formbricks.envVar" (dict "name" $key "value" $value "context" $) | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- if .Values.deployment.resources }}
|
||||
resources:
|
||||
|
||||
@@ -73,8 +73,7 @@ spec:
|
||||
{{- include "formbricks.hubEmbeddingEnv" (dict "root" $ "env" .Values.hub.env) | nindent 12 }}
|
||||
{{- range $key, $value := .Values.hub.env }}
|
||||
{{- if not (and $.Values.hub.embeddings.enabled (include "formbricks.hubEmbeddingEnvManaged" (dict "key" $key))) }}
|
||||
- name: {{ $key }}
|
||||
value: {{ $value | quote }}
|
||||
{{- include "formbricks.envVar" (dict "name" $key "value" $value "context" $) | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .Values.hub.resources }}
|
||||
|
||||
@@ -129,8 +129,7 @@ spec:
|
||||
{{- end }}
|
||||
{{- range $key, $value := .Values.hub.embeddings.env }}
|
||||
{{- if not (or (and $.Values.hub.embeddings.auth.enabled (eq $key "API_KEY")) (and (or $.Values.hub.embeddings.huggingFace.existingSecret $.Values.hub.embeddings.huggingFace.token) (eq $key "HF_TOKEN"))) }}
|
||||
- name: {{ $key }}
|
||||
value: {{ $value | quote }}
|
||||
{{- include "formbricks.envVar" (dict "name" $key "value" $value "context" $) | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -90,14 +90,12 @@ spec:
|
||||
{{- include "formbricks.hubEmbeddingEnv" (dict "root" $ "env" $workerEnv) | nindent 12 }}
|
||||
{{- range $key, $value := .Values.hub.env }}
|
||||
{{- if and (not (hasKey $.Values.hub.worker.env $key)) (not (and $.Values.hub.embeddings.enabled (include "formbricks.hubEmbeddingEnvManaged" (dict "key" $key)))) }}
|
||||
- name: {{ $key }}
|
||||
value: {{ $value | quote }}
|
||||
{{- include "formbricks.envVar" (dict "name" $key "value" $value "context" $) | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $key, $value := .Values.hub.worker.env }}
|
||||
{{- if not (and $.Values.hub.embeddings.enabled (include "formbricks.hubEmbeddingEnvManaged" (dict "key" $key))) }}
|
||||
- name: {{ $key }}
|
||||
value: {{ $value | quote }}
|
||||
{{- include "formbricks.envVar" (dict "name" $key "value" $value "context" $) | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -82,12 +82,7 @@ spec:
|
||||
{{- end }}
|
||||
env:
|
||||
{{- range $key, $value := .Values.deployment.env }}
|
||||
- name: {{ include "formbricks.tplvalues.render" ( dict "value" $key "context" $ ) }}
|
||||
{{- if kindIs "string" $value }}
|
||||
value: {{ include "formbricks.tplvalues.render" ( dict "value" $value "context" $ ) | quote }}
|
||||
{{- else }}
|
||||
{{- toYaml $value | nindent 14 }}
|
||||
{{- end }}
|
||||
{{- include "formbricks.envVar" (dict "name" $key "value" $value "context" $) | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- if .Values.migration.resources }}
|
||||
resources:
|
||||
|
||||
+6
-13
@@ -1,14 +1,13 @@
|
||||
import { z } from "zod";
|
||||
import { ZActionClass } from "./action-classes";
|
||||
import { ZId } from "./common";
|
||||
import { ZJsWorkspaceStateSegment } from "./segment";
|
||||
import { ZUploadFileConfig } from "./storage";
|
||||
import { ZSurveyBase, surveyRefinement } from "./surveys/types";
|
||||
import { ZWorkspace } from "./workspace";
|
||||
|
||||
export const ZJsWorkspaceStateSurvey = ZSurveyBase.pick({
|
||||
id: true,
|
||||
// name intentionally omitted — internal label, not needed by SDK
|
||||
name: true,
|
||||
welcomeCard: true,
|
||||
questions: true,
|
||||
blocks: true,
|
||||
@@ -20,7 +19,7 @@ export const ZJsWorkspaceStateSurvey = ZSurveyBase.pick({
|
||||
autoClose: true,
|
||||
styling: true,
|
||||
status: true,
|
||||
// segment intentionally omitted from pick — replaced with minimal shape below
|
||||
segment: true,
|
||||
recontactDays: true,
|
||||
displayLimit: true,
|
||||
displayOption: true,
|
||||
@@ -32,16 +31,9 @@ export const ZJsWorkspaceStateSurvey = ZSurveyBase.pick({
|
||||
isBackButtonHidden: true,
|
||||
isAutoProgressingEnabled: true,
|
||||
recaptcha: true,
|
||||
})
|
||||
.extend({
|
||||
// Only expose what the SDK needs: segment ID for membership check + whether any filters exist.
|
||||
// Full filter logic (titles, descriptions, conditions) is evaluated server-side and must not
|
||||
// be sent to the browser to avoid leaking sensitive targeting data.
|
||||
segment: ZJsWorkspaceStateSegment.nullable(),
|
||||
})
|
||||
.superRefine((survey, ctx) => {
|
||||
surveyRefinement(survey as z.infer<typeof ZSurveyBase>, ctx);
|
||||
});
|
||||
}).superRefine((survey, ctx) => {
|
||||
surveyRefinement(survey as z.infer<typeof ZSurveyBase>, ctx);
|
||||
});
|
||||
|
||||
export type TJsWorkspaceStateSurvey = z.infer<typeof ZJsWorkspaceStateSurvey>;
|
||||
|
||||
@@ -56,6 +48,7 @@ export const ZJsWorkspaceStateActionClass = ZActionClass.pick({
|
||||
export type TJsWorkspaceStateActionClass = z.infer<typeof ZJsWorkspaceStateActionClass>;
|
||||
|
||||
export const ZJsWorkspaceStateWorkspaceSetting = ZWorkspace.pick({
|
||||
id: true,
|
||||
recontactDays: true,
|
||||
clickOutsideClose: true,
|
||||
overlay: true,
|
||||
|
||||
Reference in New Issue
Block a user