mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-11 11:09:01 -05:00
Merge branch 'main' into fix-hungarian-translation-update-260306
This commit is contained in:
@@ -12,18 +12,18 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^5.0.1",
|
||||
"@storybook/addon-a11y": "10.2.14",
|
||||
"@storybook/addon-links": "10.2.14",
|
||||
"@storybook/addon-onboarding": "10.2.14",
|
||||
"@storybook/react-vite": "10.2.14",
|
||||
"@storybook/addon-a11y": "10.2.15",
|
||||
"@storybook/addon-links": "10.2.15",
|
||||
"@storybook/addon-onboarding": "10.2.15",
|
||||
"@storybook/react-vite": "10.2.15",
|
||||
"@typescript-eslint/eslint-plugin": "8.56.1",
|
||||
"@tailwindcss/vite": "4.2.1",
|
||||
"@typescript-eslint/parser": "8.56.1",
|
||||
"@vitejs/plugin-react": "5.1.4",
|
||||
"eslint-plugin-react-refresh": "0.4.26",
|
||||
"eslint-plugin-storybook": "10.2.14",
|
||||
"storybook": "10.2.14",
|
||||
"storybook": "10.2.15",
|
||||
"vite": "7.3.1",
|
||||
"@storybook/addon-docs": "10.2.14"
|
||||
"@storybook/addon-docs": "10.2.15"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZOrganizationTeam = z.object({
|
||||
id: z.string().cuid2(),
|
||||
id: z.cuid2(),
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ const ZCreateProjectAction = z.object({
|
||||
data: ZProjectUpdateInput,
|
||||
});
|
||||
|
||||
export const createProjectAction = authenticatedActionClient.schema(ZCreateProjectAction).action(
|
||||
export const createProjectAction = authenticatedActionClient.inputSchema(ZCreateProjectAction).action(
|
||||
withAuditLogging(
|
||||
"created",
|
||||
"project",
|
||||
@@ -97,7 +97,7 @@ const ZGetOrganizationsForSwitcherAction = z.object({
|
||||
* Called on-demand when user opens the organization switcher.
|
||||
*/
|
||||
export const getOrganizationsForSwitcherAction = authenticatedActionClient
|
||||
.schema(ZGetOrganizationsForSwitcherAction)
|
||||
.inputSchema(ZGetOrganizationsForSwitcherAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
@@ -122,7 +122,7 @@ const ZGetProjectsForSwitcherAction = z.object({
|
||||
* Called on-demand when user opens the project switcher.
|
||||
*/
|
||||
export const getProjectsForSwitcherAction = authenticatedActionClient
|
||||
.schema(ZGetProjectsForSwitcherAction)
|
||||
.inputSchema(ZGetProjectsForSwitcherAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
RocketIcon,
|
||||
UserCircleIcon,
|
||||
UserIcon,
|
||||
WorkflowIcon,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
@@ -114,6 +115,13 @@ export const MainNavigation = ({
|
||||
pathname?.includes("/segments") ||
|
||||
pathname?.includes("/attributes"),
|
||||
},
|
||||
{
|
||||
name: t("common.workflows"),
|
||||
href: `/environments/${environment.id}/workflows`,
|
||||
icon: WorkflowIcon,
|
||||
isActive: pathname?.includes("/workflows"),
|
||||
isHidden: !isFormbricksCloud,
|
||||
},
|
||||
{
|
||||
name: t("common.configuration"),
|
||||
href: `/environments/${environment.id}/workspace/general`,
|
||||
@@ -121,7 +129,7 @@ export const MainNavigation = ({
|
||||
isActive: pathname?.includes("/project"),
|
||||
},
|
||||
],
|
||||
[t, environment.id, pathname]
|
||||
[t, environment.id, pathname, isFormbricksCloud]
|
||||
);
|
||||
|
||||
const dropdownNavigation = [
|
||||
|
||||
@@ -12,7 +12,7 @@ const ZUpdateNotificationSettingsAction = z.object({
|
||||
});
|
||||
|
||||
export const updateNotificationSettingsAction = authenticatedActionClient
|
||||
.schema(ZUpdateNotificationSettingsAction)
|
||||
.inputSchema(ZUpdateNotificationSettingsAction)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
|
||||
@@ -63,7 +63,7 @@ async function handleEmailUpdate({
|
||||
return payload;
|
||||
}
|
||||
|
||||
export const updateUserAction = authenticatedActionClient.schema(ZUserPersonalInfoUpdateInput).action(
|
||||
export const updateUserAction = authenticatedActionClient.inputSchema(ZUserPersonalInfoUpdateInput).action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
"user",
|
||||
|
||||
@@ -17,7 +17,7 @@ const ZUpdateOrganizationNameAction = z.object({
|
||||
});
|
||||
|
||||
export const updateOrganizationNameAction = authenticatedActionClient
|
||||
.schema(ZUpdateOrganizationNameAction)
|
||||
.inputSchema(ZUpdateOrganizationNameAction)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
@@ -55,28 +55,36 @@ const ZDeleteOrganizationAction = z.object({
|
||||
organizationId: ZId,
|
||||
});
|
||||
|
||||
export const deleteOrganizationAction = authenticatedActionClient.schema(ZDeleteOrganizationAction).action(
|
||||
withAuditLogging(
|
||||
"deleted",
|
||||
"organization",
|
||||
async ({ ctx, parsedInput }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
if (!isMultiOrgEnabled) throw new OperationNotAllowedError("Organization deletion disabled");
|
||||
export const deleteOrganizationAction = authenticatedActionClient
|
||||
.inputSchema(ZDeleteOrganizationAction)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"deleted",
|
||||
"organization",
|
||||
async ({
|
||||
ctx,
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: Record<string, any>;
|
||||
}) => {
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
if (!isMultiOrgEnabled) throw new OperationNotAllowedError("Organization deletion disabled");
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner"],
|
||||
},
|
||||
],
|
||||
});
|
||||
ctx.auditLoggingCtx.organizationId = parsedInput.organizationId;
|
||||
const oldObject = await getOrganization(parsedInput.organizationId);
|
||||
ctx.auditLoggingCtx.oldObject = oldObject;
|
||||
return await deleteOrganization(parsedInput.organizationId);
|
||||
}
|
||||
)
|
||||
);
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner"],
|
||||
},
|
||||
],
|
||||
});
|
||||
ctx.auditLoggingCtx.organizationId = parsedInput.organizationId;
|
||||
const oldObject = await getOrganization(parsedInput.organizationId);
|
||||
ctx.auditLoggingCtx.oldObject = oldObject;
|
||||
return await deleteOrganization(parsedInput.organizationId);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@ const ZGetResponsesAction = z.object({
|
||||
});
|
||||
|
||||
export const getResponsesAction = authenticatedActionClient
|
||||
.schema(ZGetResponsesAction)
|
||||
.inputSchema(ZGetResponsesAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
@@ -57,7 +57,7 @@ const ZGetSurveySummaryAction = z.object({
|
||||
});
|
||||
|
||||
export const getSurveySummaryAction = authenticatedActionClient
|
||||
.schema(ZGetSurveySummaryAction)
|
||||
.inputSchema(ZGetSurveySummaryAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
@@ -85,7 +85,7 @@ const ZGetResponseCountAction = z.object({
|
||||
});
|
||||
|
||||
export const getResponseCountAction = authenticatedActionClient
|
||||
.schema(ZGetResponseCountAction)
|
||||
.inputSchema(ZGetResponseCountAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
@@ -110,12 +110,12 @@ export const getResponseCountAction = authenticatedActionClient
|
||||
|
||||
const ZGetDisplaysWithContactAction = z.object({
|
||||
surveyId: ZId,
|
||||
limit: z.number().int().min(1).max(100),
|
||||
offset: z.number().int().nonnegative(),
|
||||
limit: z.int().min(1).max(100),
|
||||
offset: z.int().nonnegative(),
|
||||
});
|
||||
|
||||
export const getDisplaysWithContactAction = authenticatedActionClient
|
||||
.schema(ZGetDisplaysWithContactAction)
|
||||
.inputSchema(ZGetDisplaysWithContactAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
|
||||
@@ -22,7 +22,7 @@ const ZSendEmbedSurveyPreviewEmailAction = z.object({
|
||||
});
|
||||
|
||||
export const sendEmbedSurveyPreviewEmailAction = authenticatedActionClient
|
||||
.schema(ZSendEmbedSurveyPreviewEmailAction)
|
||||
.inputSchema(ZSendEmbedSurveyPreviewEmailAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromSurveyId(parsedInput.surveyId);
|
||||
const organizationLogoUrl = await getOrganizationLogoUrl(organizationId);
|
||||
@@ -69,7 +69,7 @@ const ZResetSurveyAction = z.object({
|
||||
projectId: ZId,
|
||||
});
|
||||
|
||||
export const resetSurveyAction = authenticatedActionClient.schema(ZResetSurveyAction).action(
|
||||
export const resetSurveyAction = authenticatedActionClient.inputSchema(ZResetSurveyAction).action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
"survey",
|
||||
@@ -123,7 +123,7 @@ const ZGetEmailHtmlAction = z.object({
|
||||
});
|
||||
|
||||
export const getEmailHtmlAction = authenticatedActionClient
|
||||
.schema(ZGetEmailHtmlAction)
|
||||
.inputSchema(ZGetEmailHtmlAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
@@ -152,7 +152,7 @@ const ZGeneratePersonalLinksAction = z.object({
|
||||
});
|
||||
|
||||
export const generatePersonalLinksAction = authenticatedActionClient
|
||||
.schema(ZGeneratePersonalLinksAction)
|
||||
.inputSchema(ZGeneratePersonalLinksAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const isContactsEnabled = await getIsContactsEnabled();
|
||||
if (!isContactsEnabled) {
|
||||
@@ -231,7 +231,7 @@ const ZUpdateSingleUseLinksAction = z.object({
|
||||
});
|
||||
|
||||
export const updateSingleUseLinksAction = authenticatedActionClient
|
||||
.schema(ZUpdateSingleUseLinksAction)
|
||||
.inputSchema(ZUpdateSingleUseLinksAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
|
||||
@@ -1095,7 +1095,7 @@ export const getResponsesForSummary = reactCache(
|
||||
[limit, ZOptionalNumber],
|
||||
[offset, ZOptionalNumber],
|
||||
[filterCriteria, ZResponseFilterCriteria.optional()],
|
||||
[cursor, z.string().cuid2().optional()]
|
||||
[cursor, z.cuid2().optional()]
|
||||
);
|
||||
|
||||
const queryLimit = limit ?? RESPONSES_PER_PAGE;
|
||||
|
||||
@@ -28,7 +28,7 @@ const ZGetResponsesDownloadUrlAction = z.object({
|
||||
});
|
||||
|
||||
export const getResponsesDownloadUrlAction = authenticatedActionClient
|
||||
.schema(ZGetResponsesDownloadUrlAction)
|
||||
.inputSchema(ZGetResponsesDownloadUrlAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
@@ -58,7 +58,7 @@ const ZGetSurveyFilterDataAction = z.object({
|
||||
});
|
||||
|
||||
export const getSurveyFilterDataAction = authenticatedActionClient
|
||||
.schema(ZGetSurveyFilterDataAction)
|
||||
.inputSchema(ZGetSurveyFilterDataAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const survey = await getSurvey(parsedInput.surveyId);
|
||||
|
||||
@@ -121,7 +121,7 @@ const checkSurveyFollowUpsPermission = async (organizationId: string): Promise<v
|
||||
}
|
||||
};
|
||||
|
||||
export const updateSurveyAction = authenticatedActionClient.schema(ZSurvey).action(
|
||||
export const updateSurveyAction = authenticatedActionClient.inputSchema(ZSurvey).action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
"survey",
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
"use client";
|
||||
|
||||
import { CheckCircle2, Sparkles } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
|
||||
const FORMBRICKS_HOST = "https://app.formbricks.com";
|
||||
const SURVEY_ID = "cr9r4b2r73x6hlmn5aa2ha44";
|
||||
const ENVIRONMENT_ID = "cmk41i8bi92bdad01svi74dec";
|
||||
|
||||
interface WorkflowsPageProps {
|
||||
userEmail: string;
|
||||
organizationName: string;
|
||||
billingPlan: string;
|
||||
}
|
||||
|
||||
type Step = "prompt" | "followup" | "thankyou";
|
||||
|
||||
export const WorkflowsPage = ({ userEmail, organizationName, billingPlan }: WorkflowsPageProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [step, setStep] = useState<Step>("prompt");
|
||||
const [promptValue, setPromptValue] = useState("");
|
||||
const [detailsValue, setDetailsValue] = useState("");
|
||||
const [responseId, setResponseId] = useState<string | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const handleGenerateWorkflow = async () => {
|
||||
if (promptValue.trim().length < 100 || isSubmitting) return;
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${FORMBRICKS_HOST}/api/v2/client/${ENVIRONMENT_ID}/responses`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
surveyId: SURVEY_ID,
|
||||
finished: false,
|
||||
data: {
|
||||
workflow: promptValue.trim(),
|
||||
useremail: userEmail,
|
||||
orgname: organizationName,
|
||||
billingplan: billingPlan,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setResponseId(json.data?.id ?? null);
|
||||
}
|
||||
|
||||
setStep("followup");
|
||||
} catch {
|
||||
setStep("followup");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitFeedback = async () => {
|
||||
if (isSubmitting) return;
|
||||
setIsSubmitting(true);
|
||||
|
||||
if (responseId) {
|
||||
try {
|
||||
await fetch(`${FORMBRICKS_HOST}/api/v1/client/${ENVIRONMENT_ID}/responses/${responseId}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
finished: true,
|
||||
data: {
|
||||
details: detailsValue.trim(),
|
||||
},
|
||||
}),
|
||||
});
|
||||
} catch {
|
||||
// silently fail
|
||||
}
|
||||
}
|
||||
|
||||
setIsSubmitting(false);
|
||||
setStep("thankyou");
|
||||
};
|
||||
|
||||
const handleSkipFeedback = async () => {
|
||||
if (!responseId) {
|
||||
setStep("thankyou");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await fetch(`${FORMBRICKS_HOST}/api/v1/client/${ENVIRONMENT_ID}/responses/${responseId}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
finished: true,
|
||||
data: {},
|
||||
}),
|
||||
});
|
||||
} catch {
|
||||
// silently fail
|
||||
}
|
||||
|
||||
setStep("thankyou");
|
||||
};
|
||||
|
||||
if (step === "prompt") {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center px-4 pt-[15vh]">
|
||||
<div className="w-full max-w-2xl space-y-8">
|
||||
<div className="space-y-3 text-center">
|
||||
<div className="from-brand-light to-brand-dark mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br shadow-md">
|
||||
<Sparkles className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold tracking-tight text-slate-800">{t("workflows.heading")}</h1>
|
||||
<p className="text-lg text-slate-500">{t("workflows.subheading")}</p>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<textarea
|
||||
value={promptValue}
|
||||
onChange={(e) => setPromptValue(e.target.value)}
|
||||
placeholder={t("workflows.placeholder")}
|
||||
rows={5}
|
||||
className="focus:border-brand-dark focus:ring-brand-light/20 w-full resize-none rounded-xl border border-slate-200 bg-white px-5 py-4 text-base text-slate-800 shadow-sm transition-all placeholder:text-slate-400 focus:outline-none focus:ring-2"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
||||
handleGenerateWorkflow();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="mt-3 flex items-center justify-between">
|
||||
<span
|
||||
className={`text-xs ${promptValue.trim().length >= 100 ? "text-slate-400" : "text-amber-500"}`}>
|
||||
{promptValue.trim().length} / 100
|
||||
</span>
|
||||
<Button
|
||||
onClick={handleGenerateWorkflow}
|
||||
disabled={promptValue.trim().length < 100 || isSubmitting}
|
||||
loading={isSubmitting}
|
||||
size="lg">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
{t("workflows.generate_button")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === "followup") {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center px-4 pt-[15vh]">
|
||||
<div className="w-full max-w-2xl space-y-8">
|
||||
<div className="space-y-3 text-center">
|
||||
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-slate-100">
|
||||
<Sparkles className="text-brand-dark h-6 w-6" />
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold tracking-tight text-slate-800">
|
||||
{t("workflows.coming_soon_title")}
|
||||
</h1>
|
||||
<p className="mx-auto max-w-md text-base text-slate-500">
|
||||
{t("workflows.coming_soon_description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
|
||||
<label className="text-md mb-2 block font-medium text-slate-700">
|
||||
{t("workflows.follow_up_label")}
|
||||
</label>
|
||||
<textarea
|
||||
value={detailsValue}
|
||||
onChange={(e) => setDetailsValue(e.target.value)}
|
||||
placeholder={t("workflows.follow_up_placeholder")}
|
||||
rows={4}
|
||||
className="focus:border-brand-dark focus:ring-brand-light/20 w-full resize-none rounded-lg border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-800 transition-all placeholder:text-slate-400 focus:bg-white focus:outline-none focus:ring-2"
|
||||
/>
|
||||
<div className="mt-4 flex items-center justify-end gap-3">
|
||||
<Button variant="ghost" onClick={handleSkipFeedback} className="text-slate-500">
|
||||
{t("common.skip")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmitFeedback}
|
||||
disabled={!detailsValue.trim() || isSubmitting}
|
||||
loading={isSubmitting}>
|
||||
{t("workflows.submit_button")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center px-4 pt-[15vh]">
|
||||
<div className="w-full max-w-md space-y-6 text-center">
|
||||
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-green-50">
|
||||
<CheckCircle2 className="h-8 w-8 text-green-500" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-slate-800">{t("workflows.thank_you_title")}</h1>
|
||||
<p className="text-base text-slate-500">{t("workflows.thank_you_description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Metadata } from "next";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { WorkflowsPage } from "./components/workflows-page";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Workflows",
|
||||
};
|
||||
|
||||
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||
const params = await props.params;
|
||||
|
||||
if (!IS_FORMBRICKS_CLOUD) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const { session, organization, isBilling } = await getEnvironmentAuth(params.environmentId);
|
||||
|
||||
if (isBilling) {
|
||||
return redirect(`/environments/${params.environmentId}/settings/billing`);
|
||||
}
|
||||
|
||||
const user = await getUser(session.user.id);
|
||||
if (!user) {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkflowsPage
|
||||
userEmail={user.email}
|
||||
organizationName={organization.name}
|
||||
billingPlan={organization.billing.plan}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -21,7 +21,7 @@ const ZCreateOrUpdateIntegrationAction = z.object({
|
||||
});
|
||||
|
||||
export const createOrUpdateIntegrationAction = authenticatedActionClient
|
||||
.schema(ZCreateOrUpdateIntegrationAction)
|
||||
.inputSchema(ZCreateOrUpdateIntegrationAction)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"createdUpdated",
|
||||
@@ -67,7 +67,7 @@ const ZDeleteIntegrationAction = z.object({
|
||||
integrationId: ZId,
|
||||
});
|
||||
|
||||
export const deleteIntegrationAction = authenticatedActionClient.schema(ZDeleteIntegrationAction).action(
|
||||
export const deleteIntegrationAction = authenticatedActionClient.inputSchema(ZDeleteIntegrationAction).action(
|
||||
withAuditLogging(
|
||||
"deleted",
|
||||
"integration",
|
||||
|
||||
@@ -17,7 +17,7 @@ const ZValidateGoogleSheetsConnectionAction = z.object({
|
||||
});
|
||||
|
||||
export const validateGoogleSheetsConnectionAction = authenticatedActionClient
|
||||
.schema(ZValidateGoogleSheetsConnectionAction)
|
||||
.inputSchema(ZValidateGoogleSheetsConnectionAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
@@ -51,7 +51,7 @@ const ZGetSpreadsheetNameByIdAction = z.object({
|
||||
});
|
||||
|
||||
export const getSpreadsheetNameByIdAction = authenticatedActionClient
|
||||
.schema(ZGetSpreadsheetNameByIdAction)
|
||||
.inputSchema(ZGetSpreadsheetNameByIdAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
|
||||
@@ -12,7 +12,7 @@ const ZGetSlackChannelsAction = z.object({
|
||||
});
|
||||
|
||||
export const getSlackChannelsAction = authenticatedActionClient
|
||||
.schema(ZGetSlackChannelsAction)
|
||||
.inputSchema(ZGetSlackChannelsAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
|
||||
@@ -50,7 +50,7 @@ export const GET = withV1ApiWrapper({
|
||||
{
|
||||
environmentId: params.environmentId,
|
||||
url: req.url,
|
||||
validationError: cuidValidation.error.errors[0]?.message,
|
||||
validationError: cuidValidation.error.issues[0]?.message,
|
||||
},
|
||||
"Invalid CUID v1 format detected"
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { DatabaseError } from "@formbricks/types/errors";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
|
||||
export const deleteSurvey = async (surveyId: string) => {
|
||||
validateInputs([surveyId, z.string().cuid2()]);
|
||||
validateInputs([surveyId, z.cuid2()]);
|
||||
|
||||
try {
|
||||
const deletedSurvey = await prisma.survey.delete({
|
||||
|
||||
@@ -101,7 +101,9 @@ describe("verifyRecaptchaToken", () => {
|
||||
},
|
||||
signal: {},
|
||||
};
|
||||
vi.spyOn(global, "AbortController").mockImplementation(() => abortController as any);
|
||||
vi.spyOn(global, "AbortController").mockImplementation(function AbortController() {
|
||||
return abortController as any;
|
||||
});
|
||||
(global.fetch as any).mockImplementation(() => new Promise(() => {}));
|
||||
verifyRecaptchaToken("token", 0.5);
|
||||
vi.advanceTimersByTime(5000);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import cuid2 from "@paralleldrive/cuid2";
|
||||
import * as cuid2 from "@paralleldrive/cuid2";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import * as crypto from "@/lib/crypto";
|
||||
import { generateSurveySingleUseId, validateSurveySingleUseId } from "./singleUseSurveys";
|
||||
@@ -20,10 +20,6 @@ vi.mock("@paralleldrive/cuid2", () => {
|
||||
const isCuidMock = vi.fn();
|
||||
|
||||
return {
|
||||
default: {
|
||||
createId: createIdMock,
|
||||
isCuid: isCuidMock,
|
||||
},
|
||||
createId: createIdMock,
|
||||
isCuid: isCuidMock,
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import cuid2 from "@paralleldrive/cuid2";
|
||||
import { createId, isCuid } from "@paralleldrive/cuid2";
|
||||
import { ENCRYPTION_KEY } from "@/lib/constants";
|
||||
import { symmetricDecrypt, symmetricEncrypt } from "@/lib/crypto";
|
||||
|
||||
// generate encrypted single use id for the survey
|
||||
export const generateSurveySingleUseId = (isEncrypted: boolean): string => {
|
||||
const cuid = cuid2.createId();
|
||||
const cuid = createId();
|
||||
if (!isEncrypted) {
|
||||
return cuid;
|
||||
}
|
||||
@@ -30,7 +30,7 @@ export const validateSurveySingleUseId = (surveySingleUseId: string): string | u
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (cuid2.isCuid(decryptedCuid)) {
|
||||
if (isCuid(decryptedCuid)) {
|
||||
return decryptedCuid;
|
||||
} else {
|
||||
return undefined;
|
||||
|
||||
@@ -14,31 +14,39 @@ const ZCreateOrganizationAction = z.object({
|
||||
organizationName: z.string(),
|
||||
});
|
||||
|
||||
export const createOrganizationAction = authenticatedActionClient.schema(ZCreateOrganizationAction).action(
|
||||
withAuditLogging(
|
||||
"created",
|
||||
"organization",
|
||||
async ({ ctx, parsedInput }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
const hasNoOrganizations = await gethasNoOrganizations();
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
export const createOrganizationAction = authenticatedActionClient
|
||||
.inputSchema(ZCreateOrganizationAction)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"created",
|
||||
"organization",
|
||||
async ({
|
||||
ctx,
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: Record<string, any>;
|
||||
}) => {
|
||||
const hasNoOrganizations = await gethasNoOrganizations();
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
|
||||
if (!hasNoOrganizations && !isMultiOrgEnabled) {
|
||||
throw new OperationNotAllowedError("This action can only be performed on a fresh instance.");
|
||||
if (!hasNoOrganizations && !isMultiOrgEnabled) {
|
||||
throw new OperationNotAllowedError("This action can only be performed on a fresh instance.");
|
||||
}
|
||||
|
||||
const newOrganization = await createOrganization({
|
||||
name: parsedInput.organizationName,
|
||||
});
|
||||
|
||||
await createMembership(newOrganization.id, ctx.user.id, {
|
||||
role: "owner",
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = newOrganization.id;
|
||||
ctx.auditLoggingCtx.newObject = newOrganization;
|
||||
|
||||
return newOrganization;
|
||||
}
|
||||
|
||||
const newOrganization = await createOrganization({
|
||||
name: parsedInput.organizationName,
|
||||
});
|
||||
|
||||
await createMembership(newOrganization.id, ctx.user.id, {
|
||||
role: "owner",
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = newOrganization.id;
|
||||
ctx.auditLoggingCtx.newObject = newOrganization;
|
||||
|
||||
return newOrganization;
|
||||
}
|
||||
)
|
||||
);
|
||||
)
|
||||
);
|
||||
|
||||
@@ -369,6 +369,7 @@ checksums:
|
||||
common/show_response_count: 609e5dc7c074d57e711a728fa2f8eb79
|
||||
common/shown: 63e4ffb245c05e04b636446c3dbdd8df
|
||||
common/size: 227fadeeff951e041ff42031a11a4626
|
||||
common/skip: b7f28dfa2f58b80b149bb82b392d0291
|
||||
common/skipped: d496f0f667e1b4364b954db71335d4ef
|
||||
common/skips: 99de7579122a3fa6ec5e2a47f3fd8b34
|
||||
common/some_files_failed_to_upload: a0e26efeb29ae905257ecf93b112dff0
|
||||
@@ -438,6 +439,7 @@ checksums:
|
||||
common/website_survey: 17513d25a07b6361768a15ec622b021b
|
||||
common/weeks: 545de30df4f44d3f6d1d344af6a10815
|
||||
common/welcome_card: 76081ebd5b2e35da9b0f080323704ae7
|
||||
common/workflows: b0c9c8615a9ba7d9cb73e767290a7f72
|
||||
common/workspace_configuration: d0a5812d6a97d7724d565b1017c34387
|
||||
common/workspace_created_successfully: bf401ae83da954f1db48724e2a8e40f1
|
||||
common/workspace_creation_description: aea2f480ba0c54c5cabac72c9c900ddf
|
||||
@@ -3107,3 +3109,14 @@ checksums:
|
||||
templates/usability_question_9_headline: 5850229e97ae97698ce90b330ea49682
|
||||
templates/usability_rating_description: 8c4f3818fe830ae544611f816265f1a1
|
||||
templates/usability_score_name: 5cbf1172d24dfcb17d979dff6dfdf7e2
|
||||
workflows/coming_soon_description: 1e0621d287924d84fb539afab7372b23
|
||||
workflows/coming_soon_title: d79be80559c70c828cf20811d2ed5039
|
||||
workflows/follow_up_label: 8cafe669370271035aeac8e8cab0f123
|
||||
workflows/follow_up_placeholder: 0c26f9e4f82429acb2ac7525a3e8f24e
|
||||
workflows/generate_button: b194b6172a49af8374a19dd2cf39cfdc
|
||||
workflows/heading: a98a6b14d3e955f38cc16386df9a4111
|
||||
workflows/placeholder: 0d24da3af3b860b8f943c83efdeef227
|
||||
workflows/subheading: ebf5e3b3aeb85e13e843358cc5476f42
|
||||
workflows/submit_button: 7a062f2de02ce60b1d73e510ff1ca094
|
||||
workflows/thank_you_description: 842579609c6bf16a1d6c57a333fd5125
|
||||
workflows/thank_you_title: 07edd8c50685a52c0969d711df26d768
|
||||
|
||||
@@ -159,7 +159,7 @@ export const BREVO_LIST_ID = env.BREVO_LIST_ID;
|
||||
export const UNSPLASH_ACCESS_KEY = env.UNSPLASH_ACCESS_KEY;
|
||||
export const UNSPLASH_ALLOWED_DOMAINS = ["api.unsplash.com"];
|
||||
|
||||
export const STRIPE_API_VERSION = "2024-06-20";
|
||||
export const STRIPE_API_VERSION = "2026-02-25.clover";
|
||||
|
||||
// Maximum number of attribute classes allowed:
|
||||
export const MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT = 150;
|
||||
|
||||
@@ -71,8 +71,8 @@ export const getDisplaysBySurveyIdWithContact = reactCache(
|
||||
async (surveyId: string, limit?: number, offset?: number): Promise<TDisplayWithContact[]> => {
|
||||
validateInputs(
|
||||
[surveyId, ZId],
|
||||
[limit, z.number().int().min(1).optional()],
|
||||
[offset, z.number().int().nonnegative().optional()]
|
||||
[limit, z.int().min(1).optional()],
|
||||
[offset, z.int().nonnegative().optional()]
|
||||
);
|
||||
|
||||
try {
|
||||
|
||||
@@ -14,7 +14,7 @@ export const env = createEnv({
|
||||
CRON_SECRET: z.string().optional(),
|
||||
BREVO_API_KEY: z.string().optional(),
|
||||
BREVO_LIST_ID: z.string().optional(),
|
||||
DATABASE_URL: z.string().url(),
|
||||
DATABASE_URL: z.url(),
|
||||
DEBUG: z.enum(["1", "0"]).optional(),
|
||||
AUTH_DEFAULT_TEAM_ID: z.string().optional(),
|
||||
AUTH_SKIP_INVITE_FOR_SSO: z.enum(["1", "0"]).optional(),
|
||||
@@ -23,7 +23,7 @@ export const env = createEnv({
|
||||
EMAIL_VERIFICATION_DISABLED: z.enum(["1", "0"]).optional(),
|
||||
ENCRYPTION_KEY: z.string(),
|
||||
ENTERPRISE_LICENSE_KEY: z.string().optional(),
|
||||
ENVIRONMENT: z.enum(["production", "staging"]).default("production"),
|
||||
ENVIRONMENT: z.enum(["production", "staging"]).prefault("production"),
|
||||
GITHUB_ID: z.string().optional(),
|
||||
GITHUB_SECRET: z.string().optional(),
|
||||
GOOGLE_CLIENT_ID: z.string().optional(),
|
||||
@@ -31,21 +31,20 @@ export const env = createEnv({
|
||||
GOOGLE_SHEETS_CLIENT_ID: z.string().optional(),
|
||||
GOOGLE_SHEETS_CLIENT_SECRET: z.string().optional(),
|
||||
GOOGLE_SHEETS_REDIRECT_URL: z.string().optional(),
|
||||
HTTP_PROXY: z.string().url().optional(),
|
||||
HTTPS_PROXY: z.string().url().optional(),
|
||||
HTTP_PROXY: z.url().optional(),
|
||||
HTTPS_PROXY: z.url().optional(),
|
||||
IMPRINT_URL: z
|
||||
.string()
|
||||
.url()
|
||||
.optional()
|
||||
.or(z.string().refine((str) => str === "")),
|
||||
IMPRINT_ADDRESS: z.string().optional(),
|
||||
INVITE_DISABLED: z.enum(["1", "0"]).optional(),
|
||||
CHATWOOT_WEBSITE_TOKEN: z.string().optional(),
|
||||
CHATWOOT_BASE_URL: z.string().url().optional(),
|
||||
CHATWOOT_BASE_URL: z.url().optional(),
|
||||
IS_FORMBRICKS_CLOUD: z.enum(["1", "0"]).optional(),
|
||||
LOG_LEVEL: z.enum(["debug", "info", "warn", "error", "fatal"]).optional(),
|
||||
MAIL_FROM: z.string().email().optional(),
|
||||
NEXTAUTH_URL: z.string().url().optional(),
|
||||
MAIL_FROM: z.email().optional(),
|
||||
NEXTAUTH_URL: z.url().optional(),
|
||||
NEXTAUTH_SECRET: z.string().optional(),
|
||||
MAIL_FROM_NAME: z.string().optional(),
|
||||
NOTION_OAUTH_CLIENT_ID: z.string().optional(),
|
||||
@@ -58,10 +57,9 @@ export const env = createEnv({
|
||||
REDIS_URL:
|
||||
process.env.NODE_ENV === "test"
|
||||
? z.string().optional()
|
||||
: z.string().url("REDIS_URL is required for caching, rate limiting, and audit logging"),
|
||||
: z.url("REDIS_URL is required for caching, rate limiting, and audit logging"),
|
||||
PASSWORD_RESET_DISABLED: z.enum(["1", "0"]).optional(),
|
||||
PRIVACY_URL: z
|
||||
.string()
|
||||
.url()
|
||||
.optional()
|
||||
.or(z.string().refine((str) => str === "")),
|
||||
@@ -86,7 +84,6 @@ export const env = createEnv({
|
||||
STRIPE_SECRET_KEY: z.string().optional(),
|
||||
STRIPE_WEBHOOK_SECRET: z.string().optional(),
|
||||
PUBLIC_URL: z
|
||||
.string()
|
||||
.url()
|
||||
.refine(
|
||||
(url) => {
|
||||
@@ -98,12 +95,11 @@ export const env = createEnv({
|
||||
}
|
||||
},
|
||||
{
|
||||
message: "PUBLIC_URL must be a valid URL with a proper host (e.g., https://example.com)",
|
||||
error: "PUBLIC_URL must be a valid URL with a proper host (e.g., https://example.com)",
|
||||
}
|
||||
)
|
||||
.optional(),
|
||||
TERMS_URL: z
|
||||
.string()
|
||||
.url()
|
||||
.optional()
|
||||
.or(z.string().refine((str) => str === "")),
|
||||
@@ -112,7 +108,7 @@ export const env = createEnv({
|
||||
RECAPTCHA_SITE_KEY: z.string().optional(),
|
||||
RECAPTCHA_SECRET_KEY: z.string().optional(),
|
||||
VERCEL_URL: z.string().optional(),
|
||||
WEBAPP_URL: z.string().url().optional(),
|
||||
WEBAPP_URL: z.url().optional(),
|
||||
UNSPLASH_ACCESS_KEY: z.string().optional(),
|
||||
|
||||
NODE_ENV: z.enum(["development", "production", "test"]).optional(),
|
||||
|
||||
@@ -267,7 +267,7 @@ export const getResponses = reactCache(
|
||||
[limit, ZOptionalNumber],
|
||||
[offset, ZOptionalNumber],
|
||||
[filterCriteria, ZResponseFilterCriteria.optional()],
|
||||
[cursor, z.string().cuid2().optional()]
|
||||
[cursor, z.cuid2().optional()]
|
||||
);
|
||||
|
||||
limit = limit ?? RESPONSES_PER_PAGE;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
getOrganizationByEnvironmentId,
|
||||
subscribeOrganizationMembersToSurveyResponses,
|
||||
} from "@/lib/organization/service";
|
||||
import { TriggerUpdate } from "@/modules/survey/editor/types/survey-trigger";
|
||||
import { getActionClasses } from "../actionClass/service";
|
||||
import { ITEMS_PER_PAGE } from "../constants";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
@@ -22,15 +23,6 @@ import {
|
||||
validateMediaAndPrepareBlocks,
|
||||
} from "./utils";
|
||||
|
||||
interface TriggerUpdate {
|
||||
create?: Array<{ actionClassId: string }>;
|
||||
deleteMany?: {
|
||||
actionClassId: {
|
||||
in: string[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const selectSurvey = {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
@@ -114,19 +106,32 @@ export const selectSurvey = {
|
||||
slug: true,
|
||||
} satisfies Prisma.SurveySelect;
|
||||
|
||||
const getTriggerIds = (triggers: TSurvey["triggers"]): string[] | null => {
|
||||
if (!triggers) return null;
|
||||
if (!Array.isArray(triggers)) {
|
||||
throw new InvalidInputError("Invalid trigger id");
|
||||
}
|
||||
|
||||
return triggers.map((trigger) => {
|
||||
const actionClassId = trigger?.actionClass?.id;
|
||||
if (typeof actionClassId !== "string") {
|
||||
throw new InvalidInputError("Invalid trigger id");
|
||||
}
|
||||
return actionClassId;
|
||||
});
|
||||
};
|
||||
|
||||
export const checkTriggersValidity = (triggers: TSurvey["triggers"], actionClasses: ActionClass[]) => {
|
||||
if (!triggers) return;
|
||||
const triggerIds = getTriggerIds(triggers);
|
||||
if (!triggerIds) return;
|
||||
|
||||
// check if all the triggers are valid
|
||||
triggers.forEach((trigger) => {
|
||||
if (!actionClasses.find((actionClass) => actionClass.id === trigger.actionClass.id)) {
|
||||
triggerIds.forEach((triggerId) => {
|
||||
if (!actionClasses.find((actionClass) => actionClass.id === triggerId)) {
|
||||
throw new InvalidInputError("Invalid trigger id");
|
||||
}
|
||||
});
|
||||
|
||||
// check if all the triggers are unique
|
||||
const triggerIds = triggers.map((trigger) => trigger.actionClass.id);
|
||||
|
||||
if (new Set(triggerIds).size !== triggerIds.length) {
|
||||
throw new InvalidInputError("Duplicate trigger id");
|
||||
}
|
||||
@@ -137,36 +142,33 @@ export const handleTriggerUpdates = (
|
||||
currentTriggers: TSurvey["triggers"],
|
||||
actionClasses: ActionClass[]
|
||||
) => {
|
||||
if (!updatedTriggers) return {};
|
||||
const updatedTriggerIds = getTriggerIds(updatedTriggers);
|
||||
if (!updatedTriggerIds) return {};
|
||||
|
||||
checkTriggersValidity(updatedTriggers, actionClasses);
|
||||
|
||||
const currentTriggerIds = currentTriggers.map((trigger) => trigger.actionClass.id);
|
||||
const updatedTriggerIds = updatedTriggers.map((trigger) => trigger.actionClass.id);
|
||||
const currentTriggerIds = getTriggerIds(currentTriggers) ?? [];
|
||||
|
||||
// added triggers are triggers that are not in the current triggers and are there in the new triggers
|
||||
const addedTriggers = updatedTriggers.filter(
|
||||
(trigger) => !currentTriggerIds.includes(trigger.actionClass.id)
|
||||
);
|
||||
const addedTriggerIds = updatedTriggerIds.filter((triggerId) => !currentTriggerIds.includes(triggerId));
|
||||
|
||||
// deleted triggers are triggers that are not in the new triggers and are there in the current triggers
|
||||
const deletedTriggers = currentTriggers.filter(
|
||||
(trigger) => !updatedTriggerIds.includes(trigger.actionClass.id)
|
||||
);
|
||||
const deletedTriggerIds = currentTriggerIds.filter((triggerId) => !updatedTriggerIds.includes(triggerId));
|
||||
|
||||
// Construct the triggers update object
|
||||
const triggersUpdate: TriggerUpdate = {};
|
||||
|
||||
if (addedTriggers.length > 0) {
|
||||
triggersUpdate.create = addedTriggers.map((trigger) => ({
|
||||
actionClassId: trigger.actionClass.id,
|
||||
if (addedTriggerIds.length > 0) {
|
||||
triggersUpdate.create = addedTriggerIds.map((triggerId) => ({
|
||||
actionClassId: triggerId,
|
||||
}));
|
||||
}
|
||||
|
||||
if (deletedTriggers.length > 0) {
|
||||
if (deletedTriggerIds.length > 0) {
|
||||
// disconnect the public triggers from the survey
|
||||
triggersUpdate.deleteMany = {
|
||||
actionClassId: {
|
||||
in: deletedTriggers.map((trigger) => trigger.actionClass.id),
|
||||
in: deletedTriggerIds,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -600,21 +602,16 @@ export const createSurvey = async (
|
||||
);
|
||||
|
||||
try {
|
||||
const { createdBy, ...restSurveyBody } = parsedSurveyBody;
|
||||
|
||||
// empty languages array
|
||||
if (!restSurveyBody.languages?.length) {
|
||||
delete restSurveyBody.languages;
|
||||
}
|
||||
|
||||
const { createdBy, languages, ...restSurveyBody } = parsedSurveyBody;
|
||||
const actionClasses = await getActionClasses(parsedEnvironmentId);
|
||||
|
||||
// @ts-expect-error
|
||||
let data: Omit<Prisma.SurveyCreateInput, "environment"> = {
|
||||
...restSurveyBody,
|
||||
// TODO: Create with attributeFilters
|
||||
// @ts-expect-error - languages would be undefined in case of empty array
|
||||
languages: languages?.length ? languages : undefined,
|
||||
triggers: restSurveyBody.triggers
|
||||
? handleTriggerUpdates(restSurveyBody.triggers, [], actionClasses)
|
||||
? // @ts-expect-error - triggers' createdAt and updatedAt are actually dates
|
||||
handleTriggerUpdates(restSurveyBody.triggers, [], actionClasses)
|
||||
: undefined,
|
||||
attributeFilters: undefined,
|
||||
};
|
||||
@@ -783,15 +780,13 @@ export const loadNewSegmentInSurvey = async (surveyId: string, newSegmentId: str
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Fix this, this happens because the survey type "web" is no longer in the zod types but its required in the schema for migration
|
||||
// @ts-expect-error
|
||||
const modifiedSurvey: TSurvey = {
|
||||
...prismaSurvey, // Properties from prismaSurvey
|
||||
const modifiedSurvey = {
|
||||
...prismaSurvey,
|
||||
segment: surveySegment,
|
||||
customHeadScriptsMode: prismaSurvey.customHeadScriptsMode,
|
||||
};
|
||||
|
||||
return modifiedSurvey;
|
||||
return modifiedSurvey as TSurvey;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
|
||||
@@ -52,7 +52,7 @@ export const getUser = reactCache(async (id: string): Promise<TUser | null> => {
|
||||
});
|
||||
|
||||
export const getUserByEmail = reactCache(async (email: string): Promise<TUser | null> => {
|
||||
validateInputs([email, z.string().email()]);
|
||||
validateInputs([email, z.email()]);
|
||||
|
||||
try {
|
||||
const user = await prisma.user.findFirst({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import cuid2 from "@paralleldrive/cuid2";
|
||||
import * as cuid2 from "@paralleldrive/cuid2";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import * as crypto from "@/lib/crypto";
|
||||
import { env } from "@/lib/env";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import cuid2 from "@paralleldrive/cuid2";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { symmetricEncrypt } from "@/lib/crypto";
|
||||
import { env } from "@/lib/env";
|
||||
|
||||
// generate encrypted single use id for the survey
|
||||
export const generateSurveySingleUseId = (isEncrypted: boolean): string => {
|
||||
const cuid = cuid2.createId();
|
||||
const cuid = createId();
|
||||
if (!isEncrypted) {
|
||||
return cuid;
|
||||
}
|
||||
|
||||
@@ -393,6 +393,7 @@
|
||||
"show_response_count": "Antwortanzahl anzeigen",
|
||||
"shown": "Angezeigt",
|
||||
"size": "Größe",
|
||||
"skip": "Überspringen",
|
||||
"skipped": "Übersprungen",
|
||||
"skips": "Übersprungen",
|
||||
"some_files_failed_to_upload": "Einige Dateien konnten nicht hochgeladen werden",
|
||||
@@ -462,6 +463,7 @@
|
||||
"website_survey": "Website-Umfrage",
|
||||
"weeks": "Wochen",
|
||||
"welcome_card": "Willkommenskarte",
|
||||
"workflows": "Workflows",
|
||||
"workspace_configuration": "Projektkonfiguration",
|
||||
"workspace_created_successfully": "Projekt erfolgreich erstellt",
|
||||
"workspace_creation_description": "Organisieren Sie Umfragen in Projekten für eine bessere Zugriffskontrolle.",
|
||||
@@ -3259,5 +3261,18 @@
|
||||
"usability_question_9_headline": "Ich fühlte mich beim Benutzen des Systems sicher.",
|
||||
"usability_rating_description": "Bewerte die wahrgenommene Benutzerfreundlichkeit, indem du die Nutzer bittest, ihre Erfahrung mit deinem Produkt mittels eines standardisierten 10-Fragen-Fragebogens zu bewerten.",
|
||||
"usability_score_name": "System Usability Score Survey (SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "Danke, dass du deine Workflow-Idee mit uns geteilt hast! Wir arbeiten gerade an diesem Feature und dein Feedback hilft uns dabei, genau das zu entwickeln, was du brauchst.",
|
||||
"coming_soon_title": "Wir sind fast da!",
|
||||
"follow_up_label": "Gibt es noch etwas, das du hinzufügen möchtest?",
|
||||
"follow_up_placeholder": "Welche spezifischen Aufgaben möchtest du automatisieren? Gibt es Tools oder Integrationen, die du gerne einbinden würdest?",
|
||||
"generate_button": "Workflow generieren",
|
||||
"heading": "Welchen Workflow möchtest du erstellen?",
|
||||
"placeholder": "Beschreibe den Workflow, den du generieren möchtest...",
|
||||
"subheading": "Generiere deinen Workflow in Sekunden.",
|
||||
"submit_button": "Details hinzufügen",
|
||||
"thank_you_description": "Dein Input hilft uns dabei, das Workflows-Feature zu entwickeln, das du wirklich brauchst. Wir halten dich über unsere Fortschritte auf dem Laufenden.",
|
||||
"thank_you_title": "Danke für dein Feedback!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,6 +393,7 @@
|
||||
"show_response_count": "Show response count",
|
||||
"shown": "Shown",
|
||||
"size": "Size",
|
||||
"skip": "Skip",
|
||||
"skipped": "Skipped",
|
||||
"skips": "Skips",
|
||||
"some_files_failed_to_upload": "Some files failed to upload",
|
||||
@@ -462,6 +463,7 @@
|
||||
"website_survey": "Website Survey",
|
||||
"weeks": "weeks",
|
||||
"welcome_card": "Welcome card",
|
||||
"workflows": "Workflows",
|
||||
"workspace_configuration": "Workspace Configuration",
|
||||
"workspace_created_successfully": "Workspace created successfully",
|
||||
"workspace_creation_description": "Organize surveys in workspaces for better access control.",
|
||||
@@ -3259,5 +3261,18 @@
|
||||
"usability_question_9_headline": "I felt confident while using the system.",
|
||||
"usability_rating_description": "Measure perceived usability by asking users to rate their experience with your product using a standardized 10-question survey.",
|
||||
"usability_score_name": "System Usability Score (SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "Thank you for sharing your workflow idea with us! We are currently designing this feature and your feedback will help us build exactly what you need.",
|
||||
"coming_soon_title": "We are almost there!",
|
||||
"follow_up_label": "Is there anything else you'd like to add?",
|
||||
"follow_up_placeholder": "What specific tasks would you like to automate? Any tools or integrations you'd want included?",
|
||||
"generate_button": "Generate workflow",
|
||||
"heading": "What workflow do you want to create?",
|
||||
"placeholder": "Describe the workflow you want to generate...",
|
||||
"subheading": "Generate your workflow in seconds.",
|
||||
"submit_button": "Add details",
|
||||
"thank_you_description": "Your input helps us build the Workflows feature you actually need. We'll keep you posted on our progress.",
|
||||
"thank_you_title": "Thank you for your feedback!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,6 +393,7 @@
|
||||
"show_response_count": "Mostrar recuento de respuestas",
|
||||
"shown": "Mostrado",
|
||||
"size": "Tamaño",
|
||||
"skip": "Omitir",
|
||||
"skipped": "Omitido",
|
||||
"skips": "Omisiones",
|
||||
"some_files_failed_to_upload": "Algunos archivos no se han podido subir",
|
||||
@@ -462,6 +463,7 @@
|
||||
"website_survey": "Encuesta de sitio web",
|
||||
"weeks": "semanas",
|
||||
"welcome_card": "Tarjeta de bienvenida",
|
||||
"workflows": "Flujos de trabajo",
|
||||
"workspace_configuration": "Configuración del proyecto",
|
||||
"workspace_created_successfully": "Proyecto creado correctamente",
|
||||
"workspace_creation_description": "Organiza las encuestas en proyectos para un mejor control de acceso.",
|
||||
@@ -3259,5 +3261,18 @@
|
||||
"usability_question_9_headline": "Me sentí seguro mientras usaba el sistema.",
|
||||
"usability_rating_description": "Mide la usabilidad percibida pidiendo a los usuarios que valoren su experiencia con tu producto mediante una encuesta estandarizada de 10 preguntas.",
|
||||
"usability_score_name": "Puntuación de usabilidad del sistema (SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "¡Gracias por compartir tu idea de flujo de trabajo con nosotros! Actualmente estamos diseñando esta funcionalidad y tus comentarios nos ayudarán a construir exactamente lo que necesitas.",
|
||||
"coming_soon_title": "¡Ya casi estamos!",
|
||||
"follow_up_label": "¿Hay algo más que te gustaría añadir?",
|
||||
"follow_up_placeholder": "¿Qué tareas específicas te gustaría automatizar? ¿Alguna herramienta o integración que quisieras incluir?",
|
||||
"generate_button": "Generar flujo de trabajo",
|
||||
"heading": "¿Qué flujo de trabajo quieres crear?",
|
||||
"placeholder": "Describe el flujo de trabajo que quieres generar...",
|
||||
"subheading": "Genera tu flujo de trabajo en segundos.",
|
||||
"submit_button": "Añadir detalles",
|
||||
"thank_you_description": "Tu aportación nos ayuda a construir la funcionalidad de flujos de trabajo que realmente necesitas. Te mantendremos informado sobre nuestro progreso.",
|
||||
"thank_you_title": "¡Gracias por tus comentarios!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,6 +393,7 @@
|
||||
"show_response_count": "Afficher le nombre de réponses",
|
||||
"shown": "Montré",
|
||||
"size": "Taille",
|
||||
"skip": "Ignorer",
|
||||
"skipped": "Passé",
|
||||
"skips": "Sauter",
|
||||
"some_files_failed_to_upload": "Certains fichiers n'ont pas pu être téléchargés",
|
||||
@@ -462,6 +463,7 @@
|
||||
"website_survey": "Sondage de site web",
|
||||
"weeks": "semaines",
|
||||
"welcome_card": "Carte de bienvenue",
|
||||
"workflows": "Workflows",
|
||||
"workspace_configuration": "Configuration du projet",
|
||||
"workspace_created_successfully": "Projet créé avec succès",
|
||||
"workspace_creation_description": "Organisez les enquêtes dans des projets pour un meilleur contrôle d'accès.",
|
||||
@@ -3259,5 +3261,18 @@
|
||||
"usability_question_9_headline": "Je me suis senti confiant en utilisant le système.",
|
||||
"usability_rating_description": "Mesurez la convivialité perçue en demandant aux utilisateurs d'évaluer leur expérience avec votre produit via un sondage standardisé de 10 questions.",
|
||||
"usability_score_name": "Score d'Utilisabilité du Système (SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "Merci d'avoir partagé votre idée de workflow avec nous ! Nous concevons actuellement cette fonctionnalité et vos retours nous aideront à créer exactement ce dont vous avez besoin.",
|
||||
"coming_soon_title": "Nous y sommes presque !",
|
||||
"follow_up_label": "Y a-t-il autre chose que vous aimeriez ajouter ?",
|
||||
"follow_up_placeholder": "Quelles tâches spécifiques aimeriez-vous automatiser ? Des outils ou intégrations que vous souhaiteriez inclure ?",
|
||||
"generate_button": "Générer le workflow",
|
||||
"heading": "Quel workflow souhaitez-vous créer ?",
|
||||
"placeholder": "Décrivez le workflow que vous souhaitez générer...",
|
||||
"subheading": "Générez votre workflow en quelques secondes.",
|
||||
"submit_button": "Ajouter des détails",
|
||||
"thank_you_description": "Votre contribution nous aide à créer la fonctionnalité Workflows dont vous avez réellement besoin. Nous vous tiendrons informé de nos progrès.",
|
||||
"thank_you_title": "Merci pour vos retours !"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,6 +393,7 @@
|
||||
"show_response_count": "Válaszok számának megjelenítése",
|
||||
"shown": "Megjelenítve",
|
||||
"size": "Méret",
|
||||
"skip": "Kihagyás",
|
||||
"skipped": "Kihagyva",
|
||||
"skips": "Kihagyja",
|
||||
"some_files_failed_to_upload": "Néhány fájlt nem sikerült feltölteni",
|
||||
@@ -462,6 +463,7 @@
|
||||
"website_survey": "Webhely kérdőív",
|
||||
"weeks": "hét",
|
||||
"welcome_card": "Üdvözlő kártya",
|
||||
"workflows": "Munkafolyamatok",
|
||||
"workspace_configuration": "Munkaterület beállítása",
|
||||
"workspace_created_successfully": "A munkaterület sikeresen létrehozva",
|
||||
"workspace_creation_description": "Kérdőívek munkaterületekre szervezése a jobb hozzáférés-vezérlés érdekében.",
|
||||
@@ -3259,5 +3261,18 @@
|
||||
"usability_question_9_headline": "Magabiztosnak éreztem magam a rendszer használata során.",
|
||||
"usability_rating_description": "Az érzékelt használhatóság mérése arra kérve a felhasználókat, hogy értékeljék a termékkel kapcsolatos tapasztalataikat egy szabványosított, 10 kérdésből álló kérdőív használatával.",
|
||||
"usability_score_name": "Rendszer-használhatósági pontszám (SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "Köszönjük, hogy megosztotta velünk a munkafolyamat ötletét! Jelenleg ezen a funkción dolgozunk, és a visszajelzése segít nekünk pontosan azt megépíteni, amire szüksége van.",
|
||||
"coming_soon_title": "Majdnem kész vagyunk!",
|
||||
"follow_up_label": "Van még valami, amit hozzá szeretne tenni?",
|
||||
"follow_up_placeholder": "Milyen konkrét feladatokat szeretne automatizálni? Vannak olyan eszközök vagy integrációk, amelyeket szeretne belevenni?",
|
||||
"generate_button": "Munkafolyamat generálása",
|
||||
"heading": "Milyen munkafolyamatot szeretne létrehozni?",
|
||||
"placeholder": "Írja le a munkafolyamatot, amelyet generálni szeretne...",
|
||||
"subheading": "Generálja le a munkafolyamatát másodpercek alatt.",
|
||||
"submit_button": "Részletek hozzáadása",
|
||||
"thank_you_description": "A visszajelzése segít nekünk megépíteni azt a munkafolyamatok funkciót, amelyre tényleg szüksége van. Folyamatosan tájékoztatjuk az előrehaladásról.",
|
||||
"thank_you_title": "Köszönjük a visszajelzését!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,6 +393,7 @@
|
||||
"show_response_count": "回答数を表示",
|
||||
"shown": "表示済み",
|
||||
"size": "サイズ",
|
||||
"skip": "スキップ",
|
||||
"skipped": "スキップ済み",
|
||||
"skips": "スキップ数",
|
||||
"some_files_failed_to_upload": "一部のファイルのアップロードに失敗しました",
|
||||
@@ -462,6 +463,7 @@
|
||||
"website_survey": "ウェブサイトフォーム",
|
||||
"weeks": "週間",
|
||||
"welcome_card": "ウェルカムカード",
|
||||
"workflows": "ワークフロー",
|
||||
"workspace_configuration": "ワークスペース設定",
|
||||
"workspace_created_successfully": "ワークスペースが正常に作成されました",
|
||||
"workspace_creation_description": "アクセス制御を改善するために、フォームをワークスペースで整理します。",
|
||||
@@ -3259,5 +3261,18 @@
|
||||
"usability_question_9_headline": "システムを使っている間、自信がありました。",
|
||||
"usability_rating_description": "標準化された10の質問アンケートを使用して、製品に対するユーザーの体験を評価し、知覚された使いやすさを測定する。",
|
||||
"usability_score_name": "システムユーザビリティスコア(SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "ワークフローのアイデアを共有していただきありがとうございます!現在この機能を設計中で、あなたのフィードバックは私たちが必要とされる機能を構築するのに役立ちます。",
|
||||
"coming_soon_title": "もうすぐ完成です!",
|
||||
"follow_up_label": "他に追加したいことはありますか?",
|
||||
"follow_up_placeholder": "どのような作業を自動化したいですか?含めたいツールや連携機能はありますか?",
|
||||
"generate_button": "ワークフローを生成",
|
||||
"heading": "どのようなワークフローを作成しますか?",
|
||||
"placeholder": "生成したいワークフローを説明してください...",
|
||||
"subheading": "数秒でワークフローを生成します。",
|
||||
"submit_button": "詳細を追加",
|
||||
"thank_you_description": "あなたの意見は、実際に必要とされるワークフロー機能の構築に役立ちます。進捗状況をお知らせします。",
|
||||
"thank_you_title": "フィードバックありがとうございます!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,6 +393,7 @@
|
||||
"show_response_count": "Toon het aantal reacties",
|
||||
"shown": "Getoond",
|
||||
"size": "Maat",
|
||||
"skip": "Overslaan",
|
||||
"skipped": "Overgeslagen",
|
||||
"skips": "Overslaan",
|
||||
"some_files_failed_to_upload": "Sommige bestanden konden niet worden geüpload",
|
||||
@@ -462,6 +463,7 @@
|
||||
"website_survey": "Website-enquête",
|
||||
"weeks": "weken",
|
||||
"welcome_card": "Welkomstkaart",
|
||||
"workflows": "Workflows",
|
||||
"workspace_configuration": "Werkruimte-configuratie",
|
||||
"workspace_created_successfully": "Project succesvol aangemaakt",
|
||||
"workspace_creation_description": "Organiseer enquêtes in werkruimtes voor beter toegangsbeheer.",
|
||||
@@ -3259,5 +3261,18 @@
|
||||
"usability_question_9_headline": "Ik voelde me zelfverzekerd tijdens het gebruik van het systeem.",
|
||||
"usability_rating_description": "Meet de waargenomen bruikbaarheid door gebruikers te vragen hun ervaring met uw product te beoordelen met behulp van een gestandaardiseerde enquête met tien vragen.",
|
||||
"usability_score_name": "Systeembruikbaarheidsscore (SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "Bedankt voor het delen van je workflow-idee met ons! We zijn momenteel bezig met het ontwerpen van deze functie en jouw feedback helpt ons om precies te bouwen wat je nodig hebt.",
|
||||
"coming_soon_title": "We zijn er bijna!",
|
||||
"follow_up_label": "Is er nog iets dat je wilt toevoegen?",
|
||||
"follow_up_placeholder": "Welke specifieke taken wil je automatiseren? Zijn er tools of integraties die je graag zou willen zien?",
|
||||
"generate_button": "Genereer workflow",
|
||||
"heading": "Welke workflow wil je maken?",
|
||||
"placeholder": "Beschrijf de workflow die je wilt genereren...",
|
||||
"subheading": "Genereer je workflow in enkele seconden.",
|
||||
"submit_button": "Voeg details toe",
|
||||
"thank_you_description": "Jouw input helpt ons om de Workflows-functie te bouwen die je echt nodig hebt. We houden je op de hoogte van onze voortgang.",
|
||||
"thank_you_title": "Bedankt voor je feedback!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,6 +393,7 @@
|
||||
"show_response_count": "Mostrar contagem de respostas",
|
||||
"shown": "mostrado",
|
||||
"size": "Tamanho",
|
||||
"skip": "Pular",
|
||||
"skipped": "Pulou",
|
||||
"skips": "Pula",
|
||||
"some_files_failed_to_upload": "Alguns arquivos falharam ao enviar",
|
||||
@@ -462,6 +463,7 @@
|
||||
"website_survey": "Pesquisa de Site",
|
||||
"weeks": "semanas",
|
||||
"welcome_card": "Cartão de boas-vindas",
|
||||
"workflows": "Fluxos de trabalho",
|
||||
"workspace_configuration": "Configuração do projeto",
|
||||
"workspace_created_successfully": "Projeto criado com sucesso",
|
||||
"workspace_creation_description": "Organize pesquisas em projetos para melhor controle de acesso.",
|
||||
@@ -3259,5 +3261,18 @@
|
||||
"usability_question_9_headline": "Me senti confiante ao usar o sistema.",
|
||||
"usability_rating_description": "Meça a usabilidade percebida perguntando aos usuários para avaliar sua experiência com seu produto usando uma pesquisa padronizada de 10 perguntas.",
|
||||
"usability_score_name": "Pontuação de Usabilidade do Sistema (SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "Obrigado por compartilhar sua ideia de fluxo de trabalho conosco! Estamos atualmente projetando este recurso e seu feedback nos ajudará a construir exatamente o que você precisa.",
|
||||
"coming_soon_title": "Estamos quase lá!",
|
||||
"follow_up_label": "Há algo mais que você gostaria de adicionar?",
|
||||
"follow_up_placeholder": "Quais tarefas específicas você gostaria de automatizar? Alguma ferramenta ou integração que você gostaria de incluir?",
|
||||
"generate_button": "Gerar fluxo de trabalho",
|
||||
"heading": "Qual fluxo de trabalho você quer criar?",
|
||||
"placeholder": "Descreva o fluxo de trabalho que você quer gerar...",
|
||||
"subheading": "Gere seu fluxo de trabalho em segundos.",
|
||||
"submit_button": "Adicionar detalhes",
|
||||
"thank_you_description": "Sua contribuição nos ajuda a construir o recurso de Fluxos de trabalho que você realmente precisa. Manteremos você informado sobre nosso progresso.",
|
||||
"thank_you_title": "Obrigado pelo seu feedback!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,6 +393,7 @@
|
||||
"show_response_count": "Mostrar contagem de respostas",
|
||||
"shown": "Mostrado",
|
||||
"size": "Tamanho",
|
||||
"skip": "Saltar",
|
||||
"skipped": "Ignorado",
|
||||
"skips": "Saltos",
|
||||
"some_files_failed_to_upload": "Alguns ficheiros falharam ao carregar",
|
||||
@@ -462,6 +463,7 @@
|
||||
"website_survey": "Inquérito do Website",
|
||||
"weeks": "semanas",
|
||||
"welcome_card": "Cartão de boas-vindas",
|
||||
"workflows": "Fluxos de trabalho",
|
||||
"workspace_configuration": "Configuração do projeto",
|
||||
"workspace_created_successfully": "Projeto criado com sucesso",
|
||||
"workspace_creation_description": "Organize inquéritos em projetos para melhor controlo de acesso.",
|
||||
@@ -3259,5 +3261,18 @@
|
||||
"usability_question_9_headline": "Eu senti-me confiante ao usar o sistema.",
|
||||
"usability_rating_description": "Meça a usabilidade percebida ao solicitar que os utilizadores avaliem a sua experiência com o seu produto usando um questionário padronizado de 10 perguntas.",
|
||||
"usability_score_name": "System Usability Score (SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "Obrigado por partilhar a sua ideia de fluxo de trabalho connosco! Estamos atualmente a desenhar esta funcionalidade e o seu feedback vai ajudar-nos a construir exatamente o que precisa.",
|
||||
"coming_soon_title": "Estamos quase lá!",
|
||||
"follow_up_label": "Há mais alguma coisa que gostaria de acrescentar?",
|
||||
"follow_up_placeholder": "Que tarefas específicas gostaria de automatizar? Alguma ferramenta ou integração que gostaria de incluir?",
|
||||
"generate_button": "Gerar fluxo de trabalho",
|
||||
"heading": "Que fluxo de trabalho quer criar?",
|
||||
"placeholder": "Descreva o fluxo de trabalho que quer gerar...",
|
||||
"subheading": "Gere o seu fluxo de trabalho em segundos.",
|
||||
"submit_button": "Adicionar detalhes",
|
||||
"thank_you_description": "A sua contribuição ajuda-nos a construir a funcionalidade de Fluxos de trabalho de que realmente precisa. Vamos mantê-lo informado sobre o nosso progresso.",
|
||||
"thank_you_title": "Obrigado pelo seu feedback!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,6 +393,7 @@
|
||||
"show_response_count": "Afișează numărul de răspunsuri",
|
||||
"shown": "Afișat",
|
||||
"size": "Mărime",
|
||||
"skip": "Omite",
|
||||
"skipped": "Sărit",
|
||||
"skips": "Salturi",
|
||||
"some_files_failed_to_upload": "Unele fișiere nu au reușit să se încarce",
|
||||
@@ -462,6 +463,7 @@
|
||||
"website_survey": "Chestionar despre site",
|
||||
"weeks": "săptămâni",
|
||||
"welcome_card": "Card de bun venit",
|
||||
"workflows": "Workflows",
|
||||
"workspace_configuration": "Configurare workspace",
|
||||
"workspace_created_successfully": "Spațiul de lucru a fost creat cu succes",
|
||||
"workspace_creation_description": "Organizează sondajele în workspaces pentru un control mai bun al accesului.",
|
||||
@@ -3259,5 +3261,18 @@
|
||||
"usability_question_9_headline": "M-am simțit încrezător în timp ce utilizam sistemul.",
|
||||
"usability_rating_description": "Măsurați uzabilitatea percepută cerând utilizatorilor să își evalueze experiența cu produsul dumneavoastră folosind un chestionar standardizat din 10 întrebări.",
|
||||
"usability_score_name": "Scor de Uzabilitate al Sistemului (SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "Îți mulțumim că ai împărtășit cu noi ideea ta de workflow! În prezent, lucrăm la această funcționalitate, iar feedback-ul tău ne ajută să construim exact ce ai nevoie.",
|
||||
"coming_soon_title": "Suntem aproape gata!",
|
||||
"follow_up_label": "Mai este ceva ce ai vrea să adaugi?",
|
||||
"follow_up_placeholder": "Ce sarcini specifice ai vrea să automatizezi? Există instrumente sau integrări pe care le-ai dori incluse?",
|
||||
"generate_button": "Generează workflow",
|
||||
"heading": "Ce workflow vrei să creezi?",
|
||||
"placeholder": "Descrie workflow-ul pe care vrei să-l generezi...",
|
||||
"subheading": "Generează-ți workflow-ul în câteva secunde.",
|
||||
"submit_button": "Adaugă detalii",
|
||||
"thank_you_description": "Contribuția ta ne ajută să construim funcția Workflows de care chiar ai nevoie. Te vom ține la curent cu progresul nostru.",
|
||||
"thank_you_title": "Îți mulțumim pentru feedback!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,6 +393,7 @@
|
||||
"show_response_count": "Показать количество ответов",
|
||||
"shown": "Показано",
|
||||
"size": "Размер",
|
||||
"skip": "Пропустить",
|
||||
"skipped": "Пропущено",
|
||||
"skips": "Пропуски",
|
||||
"some_files_failed_to_upload": "Не удалось загрузить некоторые файлы",
|
||||
@@ -462,6 +463,7 @@
|
||||
"website_survey": "Опрос сайта",
|
||||
"weeks": "недели",
|
||||
"welcome_card": "Приветственная карточка",
|
||||
"workflows": "Воркфлоу",
|
||||
"workspace_configuration": "Настройка рабочего пространства",
|
||||
"workspace_created_successfully": "Рабочий проект успешно создан",
|
||||
"workspace_creation_description": "Организуйте опросы в рабочих пространствах для лучшего контроля доступа.",
|
||||
@@ -3259,5 +3261,18 @@
|
||||
"usability_question_9_headline": "Я чувствовал себя уверенно, используя систему.",
|
||||
"usability_rating_description": "Оцените воспринимаемую удобство, попросив пользователей оценить свой опыт работы с вашим продуктом с помощью стандартизированного опроса из 10 вопросов.",
|
||||
"usability_score_name": "Индекс удобства системы (SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "Спасибо, что поделился своей идеей воркфлоу с нами! Сейчас мы разрабатываем эту функцию, и твой отзыв поможет нам сделать именно то, что тебе нужно.",
|
||||
"coming_soon_title": "Мы почти готовы!",
|
||||
"follow_up_label": "Хочешь что-то ещё добавить?",
|
||||
"follow_up_placeholder": "Какие задачи ты хочешь автоматизировать? Какие инструменты или интеграции тебе нужны?",
|
||||
"generate_button": "Сгенерировать воркфлоу",
|
||||
"heading": "Какой воркфлоу ты хочешь создать?",
|
||||
"placeholder": "Опиши воркфлоу, который хочешь сгенерировать...",
|
||||
"subheading": "Сгенерируй свой воркфлоу за секунды.",
|
||||
"submit_button": "Добавить детали",
|
||||
"thank_you_description": "Твои идеи помогают нам создавать воркфлоу, которые действительно нужны. Мы будем держать тебя в курсе нашего прогресса.",
|
||||
"thank_you_title": "Спасибо за твой отзыв!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,6 +393,7 @@
|
||||
"show_response_count": "Visa antal svar",
|
||||
"shown": "Visad",
|
||||
"size": "Storlek",
|
||||
"skip": "Hoppa över",
|
||||
"skipped": "Överhoppad",
|
||||
"skips": "Överhoppningar",
|
||||
"some_files_failed_to_upload": "Några filer misslyckades att laddas upp",
|
||||
@@ -462,6 +463,7 @@
|
||||
"website_survey": "Webbplatsenkät",
|
||||
"weeks": "veckor",
|
||||
"welcome_card": "Välkomstkort",
|
||||
"workflows": "Arbetsflöden",
|
||||
"workspace_configuration": "Arbetsytans konfiguration",
|
||||
"workspace_created_successfully": "Arbetsytan har skapats",
|
||||
"workspace_creation_description": "Organisera enkäter i arbetsytor för bättre åtkomstkontroll.",
|
||||
@@ -3259,5 +3261,18 @@
|
||||
"usability_question_9_headline": "Jag kände mig trygg när jag använde systemet.",
|
||||
"usability_rating_description": "Mät upplevd användbarhet genom att be användare betygsätta sin upplevelse med din produkt med en standardiserad 10-frågors enkät.",
|
||||
"usability_score_name": "System Usability Score (SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "Tack för att du delade din arbetsflödesidé med oss! Vi håller just nu på att designa den här funktionen och din feedback hjälper oss att bygga precis det du behöver.",
|
||||
"coming_soon_title": "Vi är nästan där!",
|
||||
"follow_up_label": "Är det något mer du vill lägga till?",
|
||||
"follow_up_placeholder": "Vilka specifika uppgifter vill du automatisera? Finns det några verktyg eller integrationer du vill ha med?",
|
||||
"generate_button": "Skapa arbetsflöde",
|
||||
"heading": "Vilket arbetsflöde vill du skapa?",
|
||||
"placeholder": "Beskriv arbetsflödet du vill skapa...",
|
||||
"subheading": "Skapa ditt arbetsflöde på några sekunder.",
|
||||
"submit_button": "Lägg till detaljer",
|
||||
"thank_you_description": "Din input hjälper oss att bygga arbetsflödesfunktionen du faktiskt behöver. Vi håller dig uppdaterad om hur det går.",
|
||||
"thank_you_title": "Tack för din feedback!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,6 +393,7 @@
|
||||
"show_response_count": "显示 响应 计数",
|
||||
"shown": "显示",
|
||||
"size": "尺寸",
|
||||
"skip": "跳过",
|
||||
"skipped": "跳过",
|
||||
"skips": "跳过",
|
||||
"some_files_failed_to_upload": "某些文件上传失败",
|
||||
@@ -462,6 +463,7 @@
|
||||
"website_survey": "网站 调查",
|
||||
"weeks": "周",
|
||||
"welcome_card": "欢迎 卡片",
|
||||
"workflows": "工作流",
|
||||
"workspace_configuration": "工作区配置",
|
||||
"workspace_created_successfully": "工作区创建成功",
|
||||
"workspace_creation_description": "在工作区中组织调查,以便更好地进行访问控制。",
|
||||
@@ -3259,5 +3261,18 @@
|
||||
"usability_question_9_headline": "使用 系统 时 我 感到 自信。",
|
||||
"usability_rating_description": "通过要求用户使用标准化的 10 问 调查 来 评价 他们对您产品的体验,以 测量 感知 的 可用性。",
|
||||
"usability_score_name": "系统 可用性 得分 ( SUS )"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "感谢你与我们分享你的工作流想法!我们目前正在设计这个功能,你的反馈将帮助我们打造真正适合你的工具。",
|
||||
"coming_soon_title": "我们快完成啦!",
|
||||
"follow_up_label": "你还有其他想补充的吗?",
|
||||
"follow_up_placeholder": "你希望自动化哪些具体任务?有没有想要集成的工具或服务?",
|
||||
"generate_button": "生成工作流",
|
||||
"heading": "你想创建什么样的工作流?",
|
||||
"placeholder": "描述一下你想生成的工作流……",
|
||||
"subheading": "几秒钟生成你的工作流。",
|
||||
"submit_button": "补充细节",
|
||||
"thank_you_description": "你的意见帮助我们打造真正适合你的工作流功能。我们会及时向你汇报进展。",
|
||||
"thank_you_title": "感谢你的反馈!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,6 +393,7 @@
|
||||
"show_response_count": "顯示回應數",
|
||||
"shown": "已顯示",
|
||||
"size": "大小",
|
||||
"skip": "略過",
|
||||
"skipped": "已跳過",
|
||||
"skips": "跳過次數",
|
||||
"some_files_failed_to_upload": "部分檔案上傳失敗",
|
||||
@@ -462,6 +463,7 @@
|
||||
"website_survey": "網站問卷",
|
||||
"weeks": "週",
|
||||
"welcome_card": "歡迎卡片",
|
||||
"workflows": "工作流程",
|
||||
"workspace_configuration": "工作區設定",
|
||||
"workspace_created_successfully": "工作區已成功建立",
|
||||
"workspace_creation_description": "將問卷組織在工作區中,以便更好地控管存取權限。",
|
||||
@@ -3259,5 +3261,18 @@
|
||||
"usability_question_9_headline": "使用 系統 時,我 感到 有 信心。",
|
||||
"usability_rating_description": "透過使用標準化的 十個問題 問卷,要求使用者評估他們對 您 產品的使用體驗,來衡量感知的 可用性。",
|
||||
"usability_score_name": "系統 可用性 分數 (SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "感謝你和我們分享你的工作流程想法!我們目前正在設計這個功能,你的回饋將幫助我們打造真正符合你需求的工具。",
|
||||
"coming_soon_title": "快完成囉!",
|
||||
"follow_up_label": "還有什麼想補充的嗎?",
|
||||
"follow_up_placeholder": "你想自動化哪些具體任務?有沒有想要整合的工具或服務?",
|
||||
"generate_button": "產生工作流程",
|
||||
"heading": "你想建立什麼樣的工作流程?",
|
||||
"placeholder": "請描述你想產生的工作流程⋯⋯",
|
||||
"subheading": "幾秒鐘就能產生你的工作流程。",
|
||||
"submit_button": "補充細節",
|
||||
"thank_you_description": "你的意見幫助我們打造真正符合你需求的工作流程功能。我們會隨時更新進度給你。",
|
||||
"thank_you_title": "感謝你的回饋!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,17 @@ const ZCreateTagAction = z.object({
|
||||
tagName: z.string(),
|
||||
});
|
||||
|
||||
export const createTagAction = authenticatedActionClient.schema(ZCreateTagAction).action(
|
||||
export const createTagAction = authenticatedActionClient.inputSchema(ZCreateTagAction).action(
|
||||
withAuditLogging(
|
||||
"created",
|
||||
"tag",
|
||||
async ({ parsedInput, ctx }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
async ({
|
||||
parsedInput,
|
||||
ctx,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: z.infer<typeof ZCreateTagAction>;
|
||||
}) => {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
@@ -65,103 +71,125 @@ const ZCreateTagToResponseAction = z.object({
|
||||
tagId: ZId,
|
||||
});
|
||||
|
||||
export const createTagToResponseAction = authenticatedActionClient.schema(ZCreateTagToResponseAction).action(
|
||||
withAuditLogging(
|
||||
"addedToResponse",
|
||||
"tag",
|
||||
async ({ parsedInput, ctx }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
const responseEnvironmentId = await getEnvironmentIdFromResponseId(parsedInput.responseId);
|
||||
const tagEnvironment = await getTag(parsedInput.tagId);
|
||||
export const createTagToResponseAction = authenticatedActionClient
|
||||
.inputSchema(ZCreateTagToResponseAction)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"addedToResponse",
|
||||
"tag",
|
||||
async ({
|
||||
parsedInput,
|
||||
ctx,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: z.infer<typeof ZCreateTagToResponseAction>;
|
||||
}) => {
|
||||
const responseEnvironmentId = await getEnvironmentIdFromResponseId(parsedInput.responseId);
|
||||
const tagEnvironment = await getTag(parsedInput.tagId);
|
||||
|
||||
if (!responseEnvironmentId || !tagEnvironment) {
|
||||
throw new Error("Environment not found");
|
||||
if (!responseEnvironmentId || !tagEnvironment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
|
||||
if (responseEnvironmentId !== tagEnvironment.environmentId) {
|
||||
throw new Error("Response and tag are not in the same environment");
|
||||
}
|
||||
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(responseEnvironmentId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromEnvironmentId(responseEnvironmentId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
});
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
ctx.auditLoggingCtx.tagId = parsedInput.tagId;
|
||||
const result = await addTagToRespone(parsedInput.responseId, parsedInput.tagId);
|
||||
ctx.auditLoggingCtx.newObject = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (responseEnvironmentId !== tagEnvironment.environmentId) {
|
||||
throw new Error("Response and tag are not in the same environment");
|
||||
}
|
||||
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(responseEnvironmentId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromEnvironmentId(responseEnvironmentId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
});
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
ctx.auditLoggingCtx.tagId = parsedInput.tagId;
|
||||
const result = await addTagToRespone(parsedInput.responseId, parsedInput.tagId);
|
||||
ctx.auditLoggingCtx.newObject = result;
|
||||
return result;
|
||||
}
|
||||
)
|
||||
);
|
||||
)
|
||||
);
|
||||
|
||||
const ZDeleteTagOnResponseAction = z.object({
|
||||
responseId: ZId,
|
||||
tagId: ZId,
|
||||
});
|
||||
|
||||
export const deleteTagOnResponseAction = authenticatedActionClient.schema(ZDeleteTagOnResponseAction).action(
|
||||
withAuditLogging(
|
||||
"removedFromResponse",
|
||||
"tag",
|
||||
async ({ parsedInput, ctx }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
const responseEnvironmentId = await getEnvironmentIdFromResponseId(parsedInput.responseId);
|
||||
const tagEnvironment = await getTag(parsedInput.tagId);
|
||||
const organizationId = await getOrganizationIdFromResponseId(parsedInput.responseId);
|
||||
if (!responseEnvironmentId || !tagEnvironment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
export const deleteTagOnResponseAction = authenticatedActionClient
|
||||
.inputSchema(ZDeleteTagOnResponseAction)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"removedFromResponse",
|
||||
"tag",
|
||||
async ({
|
||||
parsedInput,
|
||||
ctx,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: z.infer<typeof ZDeleteTagOnResponseAction>;
|
||||
}) => {
|
||||
const responseEnvironmentId = await getEnvironmentIdFromResponseId(parsedInput.responseId);
|
||||
const tagEnvironment = await getTag(parsedInput.tagId);
|
||||
const organizationId = await getOrganizationIdFromResponseId(parsedInput.responseId);
|
||||
if (!responseEnvironmentId || !tagEnvironment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
|
||||
if (responseEnvironmentId !== tagEnvironment.environmentId) {
|
||||
throw new Error("Response and tag are not in the same environment");
|
||||
}
|
||||
if (responseEnvironmentId !== tagEnvironment.environmentId) {
|
||||
throw new Error("Response and tag are not in the same environment");
|
||||
}
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromEnvironmentId(responseEnvironmentId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
});
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
ctx.auditLoggingCtx.tagId = parsedInput.tagId;
|
||||
const result = await deleteTagOnResponse(parsedInput.responseId, parsedInput.tagId);
|
||||
ctx.auditLoggingCtx.oldObject = result;
|
||||
return result;
|
||||
}
|
||||
)
|
||||
);
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromEnvironmentId(responseEnvironmentId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
});
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
ctx.auditLoggingCtx.tagId = parsedInput.tagId;
|
||||
const result = await deleteTagOnResponse(parsedInput.responseId, parsedInput.tagId);
|
||||
ctx.auditLoggingCtx.oldObject = result;
|
||||
return result;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const ZDeleteResponseAction = z.object({
|
||||
responseId: ZId,
|
||||
decrementQuotas: z.boolean().default(false),
|
||||
decrementQuotas: z.boolean().prefault(false),
|
||||
});
|
||||
|
||||
export const deleteResponseAction = authenticatedActionClient.schema(ZDeleteResponseAction).action(
|
||||
export const deleteResponseAction = authenticatedActionClient.inputSchema(ZDeleteResponseAction).action(
|
||||
withAuditLogging(
|
||||
"deleted",
|
||||
"response",
|
||||
async ({ parsedInput, ctx }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
async ({
|
||||
parsedInput,
|
||||
ctx,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: z.infer<typeof ZDeleteResponseAction>;
|
||||
}) => {
|
||||
const organizationId = await getOrganizationIdFromResponseId(parsedInput.responseId);
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
@@ -192,7 +220,7 @@ const ZGetResponseAction = z.object({
|
||||
});
|
||||
|
||||
export const getResponseAction = authenticatedActionClient
|
||||
.schema(ZGetResponseAction)
|
||||
.inputSchema(ZGetResponseAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const ZOverallHealthStatus = z
|
||||
.object({
|
||||
main_database: z.boolean().openapi({
|
||||
description: "Main database connection status - true if database is reachable and running",
|
||||
example: true,
|
||||
}),
|
||||
cache_database: z.boolean().openapi({
|
||||
description: "Cache database connection status - true if cache database is reachable and running",
|
||||
example: true,
|
||||
}),
|
||||
main_database: z
|
||||
.boolean()
|
||||
.meta({
|
||||
example: true,
|
||||
})
|
||||
.describe("Main database connection status - true if database is reachable and running"),
|
||||
cache_database: z
|
||||
.boolean()
|
||||
.meta({
|
||||
example: true,
|
||||
})
|
||||
.describe("Cache database connection status - true if cache database is reachable and running"),
|
||||
})
|
||||
.openapi({
|
||||
.meta({
|
||||
title: "Health Check Response",
|
||||
description: "Health check status for critical application dependencies",
|
||||
});
|
||||
})
|
||||
.describe("Health check status for critical application dependencies");
|
||||
|
||||
export type OverallHealthStatus = z.infer<typeof ZOverallHealthStatus>;
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
import { ZContactAttributeKey } from "@formbricks/database/zod/contact-attribute-keys";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const ZContactAttributeKeyIdSchema = z
|
||||
.string()
|
||||
.cuid2()
|
||||
.openapi({
|
||||
ref: "contactAttributeKeyId",
|
||||
description: "The ID of the contact attribute key",
|
||||
.meta({
|
||||
id: "contactAttributeKeyId",
|
||||
param: {
|
||||
name: "id",
|
||||
in: "path",
|
||||
},
|
||||
});
|
||||
})
|
||||
.describe("The ID of the contact attribute key");
|
||||
|
||||
export const ZContactAttributeKeyUpdateSchema = ZContactAttributeKey.pick({
|
||||
name: true,
|
||||
description: true,
|
||||
}).openapi({
|
||||
ref: "contactAttributeKeyUpdate",
|
||||
}).meta({
|
||||
id: "contactAttributeKeyUpdate",
|
||||
description: "A contact attribute key to update. Key cannot be changed.",
|
||||
});
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export const getContactAttributeKeysEndpoint: ZodOpenApiOperationObject = {
|
||||
description: "Gets contact attribute keys from the database.",
|
||||
tags: ["Management API - Contact Attribute Keys"],
|
||||
requestParams: {
|
||||
query: ZGetContactAttributeKeysFilter.sourceType(),
|
||||
query: ZGetContactAttributeKeysFilter,
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
|
||||
@@ -17,7 +17,7 @@ export const GET = async (request: NextRequest) =>
|
||||
authenticatedApiClient({
|
||||
request,
|
||||
schemas: {
|
||||
query: ZGetContactAttributeKeysFilter.sourceType(),
|
||||
query: ZGetContactAttributeKeysFilter,
|
||||
},
|
||||
handler: async ({ authentication, parsedInput }) => {
|
||||
const { query } = parsedInput;
|
||||
@@ -49,7 +49,7 @@ export const POST = async (request: NextRequest) =>
|
||||
authenticatedApiClient({
|
||||
request,
|
||||
schemas: {
|
||||
body: ZContactAttributeKeyInput.sourceType(),
|
||||
body: ZContactAttributeKeyInput,
|
||||
},
|
||||
handler: async ({ authentication, parsedInput, auditLog }) => {
|
||||
const { body } = parsedInput;
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
import { ZContactAttributeKey } from "@formbricks/database/zod/contact-attribute-keys";
|
||||
import { isSafeIdentifier } from "@/lib/utils/safe-identifier";
|
||||
import { ZGetFilter } from "@/modules/api/v2/types/api-filter";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const ZGetContactAttributeKeysFilter = ZGetFilter.extend({
|
||||
environmentId: z.string().cuid2().optional().describe("The environment ID to filter by"),
|
||||
environmentId: z.cuid2().optional().describe("The environment ID to filter by"),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@@ -37,15 +34,15 @@ export const ZContactAttributeKeyInput = ZContactAttributeKey.pick({
|
||||
// Enforce safe identifier format for key
|
||||
if (!isSafeIdentifier(data.key)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
code: "custom",
|
||||
message:
|
||||
"Key must be a safe identifier: only lowercase letters, numbers, and underscores, and must start with a letter",
|
||||
path: ["key"],
|
||||
});
|
||||
}
|
||||
})
|
||||
.openapi({
|
||||
ref: "contactAttributeKeyInput",
|
||||
.meta({
|
||||
id: "contactAttributeKeyInput",
|
||||
description: "Input data for creating or updating a contact attribute",
|
||||
});
|
||||
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
import { ZResponse } from "@formbricks/database/zod/responses";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const ZResponseIdSchema = z
|
||||
.string()
|
||||
.cuid2()
|
||||
.openapi({
|
||||
ref: "responseId",
|
||||
description: "The ID of the response",
|
||||
.meta({
|
||||
id: "responseId",
|
||||
param: {
|
||||
name: "id",
|
||||
in: "path",
|
||||
},
|
||||
});
|
||||
})
|
||||
.describe("The ID of the response");
|
||||
|
||||
export const ZResponseUpdateSchema = ZResponse.omit({
|
||||
id: true,
|
||||
surveyId: true,
|
||||
}).openapi({
|
||||
ref: "responseUpdate",
|
||||
}).meta({
|
||||
id: "responseUpdate",
|
||||
description: "A response to update.",
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ export const getResponsesEndpoint: ZodOpenApiOperationObject = {
|
||||
summary: "Get responses",
|
||||
description: "Gets responses from the database.",
|
||||
requestParams: {
|
||||
query: ZGetResponsesFilter.sourceType(),
|
||||
query: ZGetResponsesFilter,
|
||||
},
|
||||
tags: ["Management API - Responses"],
|
||||
responses: {
|
||||
|
||||
@@ -19,7 +19,7 @@ export const GET = async (request: NextRequest) =>
|
||||
authenticatedApiClient({
|
||||
request,
|
||||
schemas: {
|
||||
query: ZGetResponsesFilter.sourceType(),
|
||||
query: ZGetResponsesFilter,
|
||||
},
|
||||
handler: async ({ authentication, parsedInput }) => {
|
||||
const { query } = parsedInput;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ZResponse } from "@formbricks/database/zod/responses";
|
||||
import { ZGetFilter } from "@/modules/api/v2/types/api-filter";
|
||||
|
||||
export const ZGetResponsesFilter = ZGetFilter.extend({
|
||||
surveyId: z.string().cuid2().optional(),
|
||||
surveyId: z.cuid2().optional(),
|
||||
contactId: z.string().optional(),
|
||||
}).refine(
|
||||
(data) => {
|
||||
|
||||
@@ -23,7 +23,7 @@ export const getPersonalizedSurveyLink: ZodOpenApiOperationObject = {
|
||||
schema: makePartialSchema(
|
||||
z.object({
|
||||
data: z.object({
|
||||
surveyUrl: z.string().url(),
|
||||
surveyUrl: z.url(),
|
||||
expiresAt: z
|
||||
.string()
|
||||
.nullable()
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const ZContactLinkParams = z.object({
|
||||
surveyId: z
|
||||
.string()
|
||||
.cuid2()
|
||||
.openapi({
|
||||
description: "The ID of the survey",
|
||||
.meta({
|
||||
param: { name: "surveyId", in: "path" },
|
||||
}),
|
||||
})
|
||||
.describe("The ID of the survey"),
|
||||
contactId: z
|
||||
.string()
|
||||
.cuid2()
|
||||
.openapi({
|
||||
description: "The ID of the contact",
|
||||
.meta({
|
||||
param: { name: "contactId", in: "path" },
|
||||
}),
|
||||
})
|
||||
.describe("The ID of the contact"),
|
||||
});
|
||||
|
||||
export const ZContactLinkQuery = z.object({
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
import { ZGetFilter } from "@/modules/api/v2/types/api-filter";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const ZContactLinksBySegmentParams = z.object({
|
||||
surveyId: z
|
||||
.string()
|
||||
.cuid2()
|
||||
.openapi({
|
||||
description: "The ID of the survey",
|
||||
.meta({
|
||||
param: { name: "surveyId", in: "path" },
|
||||
}),
|
||||
})
|
||||
.describe("The ID of the survey"),
|
||||
segmentId: z
|
||||
.string()
|
||||
.cuid2()
|
||||
.openapi({
|
||||
description: "The ID of the segment",
|
||||
.meta({
|
||||
param: { name: "segmentId", in: "path" },
|
||||
}),
|
||||
})
|
||||
.describe("The ID of the segment"),
|
||||
});
|
||||
|
||||
export const ZContactLinksBySegmentQuery = ZGetFilter.pick({
|
||||
@@ -30,7 +25,7 @@ export const ZContactLinksBySegmentQuery = ZGetFilter.pick({
|
||||
.min(1)
|
||||
.max(365)
|
||||
.nullish()
|
||||
.default(null)
|
||||
.prefault(null)
|
||||
.describe("Number of days until the generated JWT expires. If not provided, there is no expiration."),
|
||||
attributeKeys: z
|
||||
.string()
|
||||
@@ -52,7 +47,7 @@ export type TContactWithAttributes = {
|
||||
|
||||
export const ZContactLinkResponse = z.object({
|
||||
contactId: z.string().describe("The ID of the contact"),
|
||||
surveyUrl: z.string().url().describe("Personalized survey link"),
|
||||
surveyUrl: z.url().describe("Personalized survey link"),
|
||||
expiresAt: z.string().nullable().describe("The date and time the link expires, null if no expiration"),
|
||||
attributes: z.record(z.string(), z.string()).describe("The attributes of the contact"),
|
||||
});
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const surveyIdSchema = z
|
||||
.string()
|
||||
.cuid2()
|
||||
.openapi({
|
||||
ref: "surveyId",
|
||||
description: "The ID of the survey",
|
||||
.meta({
|
||||
id: "surveyId",
|
||||
param: {
|
||||
name: "id",
|
||||
in: "path",
|
||||
},
|
||||
});
|
||||
})
|
||||
.describe("The ID of the survey");
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
import { ZSurveyWithoutQuestionType } from "@formbricks/database/zod/surveys";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const ZGetSurveysFilter = z
|
||||
.object({
|
||||
limit: z.coerce.number().positive().min(1).max(100).optional().default(10),
|
||||
skip: z.coerce.number().nonnegative().optional().default(0),
|
||||
sortBy: z.enum(["createdAt", "updatedAt"]).optional().default("createdAt"),
|
||||
order: z.enum(["asc", "desc"]).optional().default("desc"),
|
||||
limit: z.coerce.number().positive().min(1).max(100).optional().prefault(10),
|
||||
skip: z.coerce.number().nonnegative().optional().prefault(0),
|
||||
sortBy: z.enum(["createdAt", "updatedAt"]).optional().prefault("createdAt"),
|
||||
order: z.enum(["asc", "desc"]).optional().prefault("desc"),
|
||||
startDate: z.coerce.date().optional(),
|
||||
endDate: z.coerce.date().optional(),
|
||||
surveyType: z.enum(["link", "app"]).optional(),
|
||||
@@ -23,7 +20,7 @@ export const ZGetSurveysFilter = z
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "startDate must be before endDate",
|
||||
error: "startDate must be before endDate",
|
||||
}
|
||||
);
|
||||
|
||||
@@ -69,8 +66,8 @@ export const ZSurveyInput = ZSurveyWithoutQuestionType.pick({
|
||||
inlineTriggers: true,
|
||||
displayPercentage: true,
|
||||
})
|
||||
.openapi({
|
||||
ref: "surveyInput",
|
||||
.meta({
|
||||
id: "surveyInput",
|
||||
description: "A survey input object for creating or updating surveys",
|
||||
});
|
||||
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
import { ZWebhook } from "@formbricks/database/zod/webhooks";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const ZWebhookIdSchema = z
|
||||
.string()
|
||||
.cuid2()
|
||||
.openapi({
|
||||
ref: "webhookId",
|
||||
description: "The ID of the webhook",
|
||||
.meta({
|
||||
id: "webhookId",
|
||||
param: {
|
||||
name: "id",
|
||||
in: "path",
|
||||
},
|
||||
});
|
||||
})
|
||||
.describe("The ID of the webhook");
|
||||
|
||||
export const ZWebhookUpdateSchema = ZWebhook.omit({
|
||||
id: true,
|
||||
@@ -22,7 +18,7 @@ export const ZWebhookUpdateSchema = ZWebhook.omit({
|
||||
updatedAt: true,
|
||||
environmentId: true,
|
||||
secret: true,
|
||||
}).openapi({
|
||||
ref: "webhookUpdate",
|
||||
}).meta({
|
||||
id: "webhookUpdate",
|
||||
description: "A webhook to update.",
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ export const getWebhooksEndpoint: ZodOpenApiOperationObject = {
|
||||
summary: "Get webhooks",
|
||||
description: "Gets webhooks from the database.",
|
||||
requestParams: {
|
||||
query: ZGetWebhooksFilter.sourceType(),
|
||||
query: ZGetWebhooksFilter,
|
||||
},
|
||||
tags: ["Management API - Webhooks"],
|
||||
responses: {
|
||||
|
||||
@@ -11,7 +11,7 @@ export const GET = async (request: NextRequest) =>
|
||||
authenticatedApiClient({
|
||||
request,
|
||||
schemas: {
|
||||
query: ZGetWebhooksFilter.sourceType(),
|
||||
query: ZGetWebhooksFilter,
|
||||
},
|
||||
handler: async ({ authentication, parsedInput }) => {
|
||||
const { query } = parsedInput;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ZWebhook } from "@formbricks/database/zod/webhooks";
|
||||
import { ZGetFilter } from "@/modules/api/v2/types/api-filter";
|
||||
|
||||
export const ZGetWebhooksFilter = ZGetFilter.extend({
|
||||
surveyIds: z.array(z.string().cuid2()).optional(),
|
||||
surveyIds: z.array(z.cuid2()).optional(),
|
||||
}).refine(
|
||||
(data) => {
|
||||
if (data.startDate && data.endDate && data.startDate > data.endDate) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as yaml from "yaml";
|
||||
import { z } from "zod";
|
||||
import { createDocument, extendZodWithOpenApi } from "zod-openapi";
|
||||
import { createDocument } from "zod-openapi";
|
||||
import { ZApiKeyData } from "@formbricks/database/zod/api-keys";
|
||||
import { ZContact } from "@formbricks/database/zod/contact";
|
||||
import { ZContactAttributeKey } from "@formbricks/database/zod/contact-attribute-keys";
|
||||
@@ -27,8 +26,6 @@ import { rolePaths } from "@/modules/api/v2/roles/lib/openapi";
|
||||
import { bulkContactPaths } from "@/modules/ee/contacts/api/v2/management/contacts/bulk/lib/openapi";
|
||||
import { contactPaths } from "@/modules/ee/contacts/api/v2/management/contacts/lib/openapi";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
const document = createDocument({
|
||||
openapi: "3.1.0",
|
||||
info: {
|
||||
|
||||
@@ -14,7 +14,7 @@ export const getProjectTeamsEndpoint: ZodOpenApiOperationObject = {
|
||||
summary: "Get project teams",
|
||||
description: "Gets projectTeams from the database.",
|
||||
requestParams: {
|
||||
query: ZGetProjectTeamsFilter.sourceType(),
|
||||
query: ZGetProjectTeamsFilter,
|
||||
path: z.object({
|
||||
organizationId: ZOrganizationIdSchema,
|
||||
}),
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function GET(request: Request, props: { params: Promise<{ organizat
|
||||
return authenticatedApiClient({
|
||||
request,
|
||||
schemas: {
|
||||
query: ZGetProjectTeamsFilter.sourceType(),
|
||||
query: ZGetProjectTeamsFilter,
|
||||
params: z.object({ organizationId: ZOrganizationIdSchema }),
|
||||
},
|
||||
externalParams: props.params,
|
||||
|
||||
@@ -3,8 +3,8 @@ import { ZProjectTeam } from "@formbricks/database/zod/project-teams";
|
||||
import { ZGetFilter } from "@/modules/api/v2/types/api-filter";
|
||||
|
||||
export const ZGetProjectTeamsFilter = ZGetFilter.extend({
|
||||
teamId: z.string().cuid2().optional(),
|
||||
projectId: z.string().cuid2().optional(),
|
||||
teamId: z.cuid2().optional(),
|
||||
projectId: z.cuid2().optional(),
|
||||
}).refine(
|
||||
(data) => {
|
||||
if (data.startDate && data.endDate && data.startDate > data.endDate) {
|
||||
@@ -28,8 +28,8 @@ export const ZProjectTeamInput = ZProjectTeam.pick({
|
||||
export type TProjectTeamInput = z.infer<typeof ZProjectTeamInput>;
|
||||
|
||||
export const ZGetProjectTeamUpdateFilter = z.object({
|
||||
teamId: z.string().cuid2(),
|
||||
projectId: z.string().cuid2(),
|
||||
teamId: z.cuid2(),
|
||||
projectId: z.cuid2(),
|
||||
});
|
||||
|
||||
export const ZProjectZTeamUpdateSchema = ZProjectTeam.pick({
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
import { ZTeam } from "@formbricks/database/zod/teams";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const ZTeamIdSchema = z
|
||||
.string()
|
||||
.cuid2()
|
||||
.openapi({
|
||||
ref: "teamId",
|
||||
description: "The ID of the team",
|
||||
.meta({
|
||||
id: "teamId",
|
||||
param: {
|
||||
name: "id",
|
||||
in: "path",
|
||||
},
|
||||
});
|
||||
})
|
||||
.describe("The ID of the team");
|
||||
|
||||
export const ZTeamUpdateSchema = ZTeam.omit({
|
||||
id: true,
|
||||
|
||||
@@ -21,7 +21,7 @@ export const getTeamsEndpoint: ZodOpenApiOperationObject = {
|
||||
path: z.object({
|
||||
organizationId: ZOrganizationIdSchema,
|
||||
}),
|
||||
query: ZGetTeamsFilter.sourceType(),
|
||||
query: ZGetTeamsFilter,
|
||||
},
|
||||
tags: ["Organizations API - Teams"],
|
||||
responses: {
|
||||
|
||||
@@ -16,7 +16,7 @@ export const GET = async (request: NextRequest, props: { params: Promise<{ organ
|
||||
authenticatedApiClient({
|
||||
request,
|
||||
schemas: {
|
||||
query: ZGetTeamsFilter.sourceType(),
|
||||
query: ZGetTeamsFilter,
|
||||
params: z.object({ organizationId: ZOrganizationIdSchema }),
|
||||
},
|
||||
externalParams: props.params,
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const ZOrganizationIdSchema = z
|
||||
.string()
|
||||
.cuid2()
|
||||
.openapi({
|
||||
ref: "organizationId",
|
||||
description: "The ID of the organization",
|
||||
.meta({
|
||||
id: "organizationId",
|
||||
param: {
|
||||
name: "organizationId",
|
||||
in: "path",
|
||||
},
|
||||
});
|
||||
})
|
||||
.describe("The ID of the organization");
|
||||
|
||||
@@ -17,7 +17,7 @@ export const getUsersEndpoint: ZodOpenApiOperationObject = {
|
||||
path: z.object({
|
||||
organizationId: ZOrganizationIdSchema,
|
||||
}),
|
||||
query: ZGetUsersFilter.sourceType(),
|
||||
query: ZGetUsersFilter,
|
||||
},
|
||||
tags: ["Organizations API - Users"],
|
||||
responses: {
|
||||
|
||||
@@ -24,7 +24,7 @@ export const GET = async (request: NextRequest, props: { params: Promise<{ organ
|
||||
authenticatedApiClient({
|
||||
request,
|
||||
schemas: {
|
||||
query: ZGetUsersFilter.sourceType(),
|
||||
query: ZGetUsersFilter,
|
||||
params: z.object({ organizationId: ZOrganizationIdSchema }),
|
||||
},
|
||||
externalParams: props.params,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZGetFilter = z.object({
|
||||
limit: z.coerce.number().min(1).max(250).optional().default(50).describe("Number of items to return"),
|
||||
skip: z.coerce.number().min(0).optional().default(0).describe("Number of items to skip"),
|
||||
sortBy: z.enum(["createdAt", "updatedAt"]).optional().default("createdAt").describe("Sort by field"),
|
||||
order: z.enum(["asc", "desc"]).optional().default("desc").describe("Sort order"),
|
||||
limit: z.coerce.number().min(1).max(250).optional().prefault(50).describe("Number of items to return"),
|
||||
skip: z.coerce.number().min(0).optional().prefault(0).describe("Number of items to skip"),
|
||||
sortBy: z.enum(["createdAt", "updatedAt"]).optional().prefault("createdAt").describe("Sort by field"),
|
||||
order: z.enum(["asc", "desc"]).optional().prefault("desc").describe("Sort order"),
|
||||
startDate: z.coerce.date().optional().describe("Start date"),
|
||||
endDate: z.coerce.date().optional().describe("End date"),
|
||||
filterDateField: z.enum(["createdAt", "updatedAt"]).optional().describe("Date field to filter by"),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export function responseWithMetaSchema<T extends z.ZodTypeAny>(contentSchema: T) {
|
||||
export function responseWithMetaSchema<T extends z.ZodType>(contentSchema: T) {
|
||||
return z.object({
|
||||
data: z.array(contentSchema).optional(),
|
||||
meta: z
|
||||
|
||||
@@ -7,11 +7,16 @@ import { getUserByEmail } from "@/lib/user/service";
|
||||
import { actionClient } from "@/lib/utils/action-client";
|
||||
|
||||
const ZCreateEmailTokenAction = z.object({
|
||||
email: z.string().min(5).max(255).email({ message: "Invalid email" }),
|
||||
email: z
|
||||
.email({
|
||||
error: "Invalid email",
|
||||
})
|
||||
.min(5)
|
||||
.max(255),
|
||||
});
|
||||
|
||||
export const createEmailTokenAction = actionClient
|
||||
.schema(ZCreateEmailTokenAction)
|
||||
.inputSchema(ZCreateEmailTokenAction)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const user = await getUserByEmail(parsedInput.email);
|
||||
if (!user) {
|
||||
|
||||
@@ -33,7 +33,7 @@ vi.mock("@/modules/email", () => ({
|
||||
|
||||
vi.mock("@/lib/utils/action-client", () => ({
|
||||
actionClient: {
|
||||
schema: vi.fn().mockReturnThis(),
|
||||
inputSchema: vi.fn().mockReturnThis(),
|
||||
action: vi.fn((fn) => fn),
|
||||
},
|
||||
}));
|
||||
@@ -50,7 +50,7 @@ describe("forgotPasswordAction", () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -15,7 +15,7 @@ const ZForgotPasswordAction = z.object({
|
||||
});
|
||||
|
||||
export const forgotPasswordAction = actionClient
|
||||
.schema(ZForgotPasswordAction)
|
||||
.inputSchema(ZForgotPasswordAction)
|
||||
.action(async ({ parsedInput }) => {
|
||||
await applyIPRateLimit(rateLimitConfigs.auth.forgotPassword);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Button } from "@/modules/ui/components/button";
|
||||
import { FormControl, FormError, FormField, FormItem } from "@/modules/ui/components/form";
|
||||
|
||||
const ZForgotPasswordForm = z.object({
|
||||
email: z.string().email(),
|
||||
email: z.email(),
|
||||
});
|
||||
|
||||
type TForgotPasswordForm = z.infer<typeof ZForgotPasswordForm>;
|
||||
|
||||
@@ -16,7 +16,7 @@ const ZResetPasswordAction = z.object({
|
||||
password: ZUserPassword,
|
||||
});
|
||||
|
||||
export const resetPasswordAction = actionClient.schema(ZResetPasswordAction).action(
|
||||
export const resetPasswordAction = actionClient.inputSchema(ZResetPasswordAction).action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
"user",
|
||||
|
||||
@@ -2,87 +2,50 @@ import { Authenticator } from "@otplib/core";
|
||||
import type { AuthenticatorOptions } from "@otplib/core/authenticator";
|
||||
import { createDigest, createRandomBytes } from "@otplib/plugin-crypto";
|
||||
import { keyDecoder, keyEncoder } from "@otplib/plugin-thirty-two";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { totpAuthenticatorCheck } from "./totp";
|
||||
|
||||
vi.mock("@otplib/core");
|
||||
vi.mock("@otplib/plugin-crypto");
|
||||
vi.mock("@otplib/plugin-thirty-two");
|
||||
const createAuthenticator = (opts: Partial<AuthenticatorOptions> = {}) =>
|
||||
new Authenticator({
|
||||
createDigest,
|
||||
createRandomBytes,
|
||||
keyDecoder,
|
||||
keyEncoder,
|
||||
...opts,
|
||||
});
|
||||
|
||||
describe("totpAuthenticatorCheck", () => {
|
||||
const token = "123456";
|
||||
const secret = "JBSWY3DPEHPK3PXP";
|
||||
const opts: Partial<AuthenticatorOptions> = { window: [1, 0] };
|
||||
const fixedEpoch = 1_700_000_000_000;
|
||||
|
||||
test("should check a TOTP token with a base32-encoded secret", () => {
|
||||
const checkMock = vi.fn().mockReturnValue(true);
|
||||
(Authenticator as unknown as vi.Mock).mockImplementation(() => ({
|
||||
check: checkMock,
|
||||
}));
|
||||
|
||||
const result = totpAuthenticatorCheck(token, secret, opts);
|
||||
|
||||
expect(Authenticator).toHaveBeenCalledWith({
|
||||
createDigest,
|
||||
createRandomBytes,
|
||||
keyDecoder,
|
||||
keyEncoder,
|
||||
window: [1, 0],
|
||||
});
|
||||
expect(checkMock).toHaveBeenCalledWith(token, secret);
|
||||
const token = createAuthenticator({ epoch: fixedEpoch }).generate(secret);
|
||||
const result = totpAuthenticatorCheck(token, secret, { epoch: fixedEpoch, window: [1, 0] });
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should use default window if none is provided", () => {
|
||||
const checkMock = vi.fn().mockReturnValue(true);
|
||||
(Authenticator as unknown as vi.Mock).mockImplementation(() => ({
|
||||
check: checkMock,
|
||||
}));
|
||||
|
||||
const result = totpAuthenticatorCheck(token, secret);
|
||||
|
||||
expect(Authenticator).toHaveBeenCalledWith({
|
||||
createDigest,
|
||||
createRandomBytes,
|
||||
keyDecoder,
|
||||
keyEncoder,
|
||||
window: [1, 0],
|
||||
});
|
||||
expect(checkMock).toHaveBeenCalledWith(token, secret);
|
||||
// Generate a token for one time-step in the past and verify it at current epoch.
|
||||
// Default window is [1, 0], so previous-step tokens are accepted.
|
||||
const token = createAuthenticator({ epoch: fixedEpoch }).generate(secret);
|
||||
const result = totpAuthenticatorCheck(token, secret, { epoch: fixedEpoch + 30_000 });
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should throw an error for invalid token format", () => {
|
||||
(Authenticator as unknown as vi.Mock).mockImplementation(() => ({
|
||||
check: () => {
|
||||
throw new Error("Invalid token format");
|
||||
},
|
||||
}));
|
||||
|
||||
expect(() => {
|
||||
totpAuthenticatorCheck("invalidToken", secret);
|
||||
}).toThrow("Invalid token format");
|
||||
test("should return false for invalid token format", () => {
|
||||
const result = totpAuthenticatorCheck("invalidToken", secret);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("should throw an error for invalid secret format", () => {
|
||||
(Authenticator as unknown as vi.Mock).mockImplementation(() => ({
|
||||
check: () => {
|
||||
throw new Error("Invalid secret format");
|
||||
},
|
||||
}));
|
||||
|
||||
expect(() => {
|
||||
totpAuthenticatorCheck(token, "invalidSecret");
|
||||
}).toThrow("Invalid secret format");
|
||||
test("should return false for invalid secret format", () => {
|
||||
const token = createAuthenticator({ epoch: fixedEpoch }).generate(secret);
|
||||
const result = totpAuthenticatorCheck(token, "invalidSecret", { epoch: fixedEpoch });
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false if token verification fails", () => {
|
||||
const checkMock = vi.fn().mockReturnValue(false);
|
||||
(Authenticator as unknown as vi.Mock).mockImplementation(() => ({
|
||||
check: checkMock,
|
||||
}));
|
||||
|
||||
const result = totpAuthenticatorCheck(token, secret);
|
||||
const token = createAuthenticator({ epoch: fixedEpoch }).generate(secret);
|
||||
const result = totpAuthenticatorCheck(token, secret, { epoch: fixedEpoch + 60_000 });
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,11 +21,15 @@ import { FormControl, FormError, FormField, FormItem } from "@/modules/ui/compon
|
||||
import { PasswordInput } from "@/modules/ui/components/password-input";
|
||||
|
||||
const ZLoginForm = z.object({
|
||||
email: z.string().email(),
|
||||
email: z.email(),
|
||||
password: z
|
||||
.string()
|
||||
.min(8, { message: "Password must be at least 8 characters long" })
|
||||
.max(128, { message: "Password must be 128 characters or less" }),
|
||||
.min(8, {
|
||||
error: "Password must be at least 8 characters long",
|
||||
})
|
||||
.max(128, {
|
||||
error: "Password must be 128 characters or less",
|
||||
}),
|
||||
totpCode: z.string().optional(),
|
||||
backupCode: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -177,7 +177,7 @@ async function handlePostUserCreation(
|
||||
}
|
||||
}
|
||||
|
||||
export const createUserAction = actionClient.schema(ZCreateUserAction).action(
|
||||
export const createUserAction = actionClient.inputSchema(ZCreateUserAction).action(
|
||||
withAuditLogging(
|
||||
"created",
|
||||
"user",
|
||||
|
||||
@@ -24,7 +24,7 @@ import { PasswordChecks } from "./password-checks";
|
||||
|
||||
const ZSignupInput = z.object({
|
||||
name: ZUserName,
|
||||
email: z.string().email(),
|
||||
email: z.email(),
|
||||
password: ZUserPassword,
|
||||
});
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ vi.mock("@/modules/ee/audit-logs/lib/handler", () => ({
|
||||
|
||||
vi.mock("@/lib/utils/action-client", () => ({
|
||||
actionClient: {
|
||||
schema: vi.fn().mockReturnThis(),
|
||||
inputSchema: vi.fn().mockReturnThis(),
|
||||
action: vi.fn((fn) => fn),
|
||||
},
|
||||
}));
|
||||
@@ -66,7 +66,7 @@ describe("resendVerificationEmailAction", () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -15,7 +15,7 @@ const ZResendVerificationEmailAction = z.object({
|
||||
email: ZUserEmail,
|
||||
});
|
||||
|
||||
export const resendVerificationEmailAction = actionClient.schema(ZResendVerificationEmailAction).action(
|
||||
export const resendVerificationEmailAction = actionClient.inputSchema(ZResendVerificationEmailAction).action(
|
||||
withAuditLogging(
|
||||
"verificationEmailSent",
|
||||
"user",
|
||||
|
||||
@@ -8,7 +8,7 @@ import { updateBrevoCustomer } from "@/modules/auth/lib/brevo";
|
||||
import { getUser, updateUser } from "@/modules/auth/lib/user";
|
||||
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
||||
|
||||
export const verifyEmailChangeAction = actionClient.schema(z.object({ token: z.string() })).action(
|
||||
export const verifyEmailChangeAction = actionClient.inputSchema(z.object({ token: z.string() })).action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
"user",
|
||||
|
||||
@@ -2,9 +2,9 @@ import { z } from "zod";
|
||||
|
||||
export const ZRateLimitConfig = z.object({
|
||||
/** Rate limit window in seconds */
|
||||
interval: z.number().int().positive().describe("Rate limit window in seconds"),
|
||||
interval: z.int().positive().describe("Rate limit window in seconds"),
|
||||
/** Maximum allowed requests per interval */
|
||||
allowedPerInterval: z.number().int().positive().describe("Maximum allowed requests per interval"),
|
||||
allowedPerInterval: z.int().positive().describe("Maximum allowed requests per interval"),
|
||||
/** Namespace for grouping rate limit per feature */
|
||||
namespace: z.string().min(1).describe("Namespace for grouping rate limit per feature"),
|
||||
});
|
||||
|
||||
@@ -73,12 +73,12 @@ export const ZAuditLogEventSchema = z.object({
|
||||
type: ZAuditTarget,
|
||||
}),
|
||||
status: ZAuditStatus,
|
||||
timestamp: z.string().datetime(),
|
||||
timestamp: z.iso.datetime(),
|
||||
organizationId: z.string(),
|
||||
ipAddress: z.string().optional(), // Not using the .ip() here because if we don't enabled it we want to put UNKNOWN_DATA string, to keep the same pattern as the other fields
|
||||
changes: z.record(z.any()).optional(),
|
||||
changes: z.record(z.string(), z.any()).optional(),
|
||||
eventId: z.string().optional(),
|
||||
apiUrl: z.string().url().optional(),
|
||||
apiUrl: z.url().optional(),
|
||||
});
|
||||
|
||||
export type TAuditLogEvent = z.infer<typeof ZAuditLogEventSchema>;
|
||||
|
||||
@@ -16,14 +16,20 @@ import { isSubscriptionCancelled } from "@/modules/ee/billing/api/lib/is-subscri
|
||||
|
||||
const ZUpgradePlanAction = z.object({
|
||||
environmentId: ZId,
|
||||
priceLookupKey: z.nativeEnum(STRIPE_PRICE_LOOKUP_KEYS),
|
||||
priceLookupKey: z.enum(STRIPE_PRICE_LOOKUP_KEYS),
|
||||
});
|
||||
|
||||
export const upgradePlanAction = authenticatedActionClient.schema(ZUpgradePlanAction).action(
|
||||
export const upgradePlanAction = authenticatedActionClient.inputSchema(ZUpgradePlanAction).action(
|
||||
withAuditLogging(
|
||||
"subscriptionUpdated",
|
||||
"organization",
|
||||
async ({ ctx, parsedInput }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
async ({
|
||||
ctx,
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: z.infer<typeof ZUpgradePlanAction>;
|
||||
}) => {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
@@ -53,49 +59,57 @@ const ZManageSubscriptionAction = z.object({
|
||||
environmentId: ZId,
|
||||
});
|
||||
|
||||
export const manageSubscriptionAction = authenticatedActionClient.schema(ZManageSubscriptionAction).action(
|
||||
withAuditLogging(
|
||||
"subscriptionAccessed",
|
||||
"organization",
|
||||
async ({ ctx, parsedInput }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager", "billing"],
|
||||
},
|
||||
],
|
||||
});
|
||||
export const manageSubscriptionAction = authenticatedActionClient
|
||||
.inputSchema(ZManageSubscriptionAction)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"subscriptionAccessed",
|
||||
"organization",
|
||||
async ({
|
||||
ctx,
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: Record<string, any>;
|
||||
}) => {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager", "billing"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const organization = await getOrganization(organizationId);
|
||||
if (!organization) {
|
||||
throw new ResourceNotFoundError("organization", organizationId);
|
||||
const organization = await getOrganization(organizationId);
|
||||
if (!organization) {
|
||||
throw new ResourceNotFoundError("organization", organizationId);
|
||||
}
|
||||
|
||||
if (!organization.billing.stripeCustomerId) {
|
||||
throw new AuthorizationError("You do not have an associated Stripe CustomerId");
|
||||
}
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
const result = await createCustomerPortalSession(
|
||||
organization.billing.stripeCustomerId,
|
||||
`${WEBAPP_URL}/environments/${parsedInput.environmentId}/settings/billing`
|
||||
);
|
||||
ctx.auditLoggingCtx.newObject = { portalSession: result };
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!organization.billing.stripeCustomerId) {
|
||||
throw new AuthorizationError("You do not have an associated Stripe CustomerId");
|
||||
}
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
const result = await createCustomerPortalSession(
|
||||
organization.billing.stripeCustomerId,
|
||||
`${WEBAPP_URL}/environments/${parsedInput.environmentId}/settings/billing`
|
||||
);
|
||||
ctx.auditLoggingCtx.newObject = { portalSession: result };
|
||||
return result;
|
||||
}
|
||||
)
|
||||
);
|
||||
)
|
||||
);
|
||||
|
||||
const ZIsSubscriptionCancelledAction = z.object({
|
||||
organizationId: ZId,
|
||||
});
|
||||
|
||||
export const isSubscriptionCancelledAction = authenticatedActionClient
|
||||
.schema(ZIsSubscriptionCancelledAction)
|
||||
.inputSchema(ZIsSubscriptionCancelledAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import Stripe from "stripe";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { BILLING_LIMITS, PROJECT_FEATURE_KEYS, STRIPE_API_VERSION } from "@/lib/constants";
|
||||
import { env } from "@/lib/env";
|
||||
import { BILLING_LIMITS, PROJECT_FEATURE_KEYS } from "@/lib/constants";
|
||||
import { getOrganization, updateOrganization } from "@/lib/organization/service";
|
||||
|
||||
const stripe = new Stripe(env.STRIPE_SECRET_KEY!, {
|
||||
apiVersion: STRIPE_API_VERSION,
|
||||
});
|
||||
import { getStripeClient } from "./stripe-client";
|
||||
|
||||
export const handleCheckoutSessionCompleted = async (event: Stripe.Event) => {
|
||||
const stripe = getStripeClient();
|
||||
const checkoutSession = event.data.object as Stripe.Checkout.Session;
|
||||
if (!checkoutSession.metadata?.organizationId)
|
||||
throw new ResourceNotFoundError("No organizationId found in checkout session", checkoutSession.id);
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import Stripe from "stripe";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { STRIPE_API_VERSION, STRIPE_PRICE_LOOKUP_KEYS, WEBAPP_URL } from "@/lib/constants";
|
||||
import { env } from "@/lib/env";
|
||||
import { STRIPE_PRICE_LOOKUP_KEYS, WEBAPP_URL } from "@/lib/constants";
|
||||
import { getOrganization } from "@/lib/organization/service";
|
||||
|
||||
const stripe = new Stripe(env.STRIPE_SECRET_KEY!, {
|
||||
apiVersion: STRIPE_API_VERSION,
|
||||
});
|
||||
import { getStripeClient } from "./stripe-client";
|
||||
|
||||
export const createSubscription = async (
|
||||
organizationId: string,
|
||||
@@ -14,6 +9,7 @@ export const createSubscription = async (
|
||||
priceLookupKey: STRIPE_PRICE_LOOKUP_KEYS
|
||||
) => {
|
||||
try {
|
||||
const stripe = getStripeClient();
|
||||
const organization = await getOrganization(organizationId);
|
||||
if (!organization) throw new Error("Organization not found.");
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ import { getOrganization, updateOrganization } from "@/lib/organization/service"
|
||||
export const handleInvoiceFinalized = async (event: Stripe.Event) => {
|
||||
const invoice = event.data.object as Stripe.Invoice;
|
||||
|
||||
const subscriptionId = invoice.subscription as string;
|
||||
const subscription = invoice.parent?.subscription_details?.subscription;
|
||||
const subscriptionId = typeof subscription === "string" ? subscription : subscription?.id;
|
||||
if (!subscriptionId) {
|
||||
logger.warn({ invoiceId: invoice.id }, "Invoice finalized without subscription ID");
|
||||
return { status: 400, message: "No subscription ID found in invoice" };
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import Stripe from "stripe";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { STRIPE_API_VERSION } from "@/lib/constants";
|
||||
import { env } from "@/lib/env";
|
||||
import { getOrganization } from "@/lib/organization/service";
|
||||
|
||||
const stripe = new Stripe(env.STRIPE_SECRET_KEY!, {
|
||||
apiVersion: STRIPE_API_VERSION,
|
||||
});
|
||||
import { getStripeClient } from "./stripe-client";
|
||||
|
||||
export const isSubscriptionCancelled = async (
|
||||
organizationId: string
|
||||
@@ -15,6 +9,7 @@ export const isSubscriptionCancelled = async (
|
||||
date: Date | null;
|
||||
}> => {
|
||||
try {
|
||||
const stripe = getStripeClient();
|
||||
const organization = await getOrganization(organizationId);
|
||||
if (!organization) throw new Error("Team not found.");
|
||||
let isNewTeam =
|
||||
@@ -34,9 +29,10 @@ export const isSubscriptionCancelled = async (
|
||||
|
||||
for (const subscription of subscriptions.data) {
|
||||
if (subscription.cancel_at_period_end) {
|
||||
const periodEndTimestamp = subscription.cancel_at ?? subscription.items.data[0]?.current_period_end;
|
||||
return {
|
||||
cancelled: true,
|
||||
date: new Date(subscription.current_period_end * 1000),
|
||||
date: periodEndTimestamp ? new Date(periodEndTimestamp * 1000) : null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
21
apps/web/modules/ee/billing/api/lib/stripe-client.ts
Normal file
21
apps/web/modules/ee/billing/api/lib/stripe-client.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import Stripe from "stripe";
|
||||
import { STRIPE_API_VERSION } from "@/lib/constants";
|
||||
import { env } from "@/lib/env";
|
||||
|
||||
export const getStripeClient = () => {
|
||||
if (!env.STRIPE_SECRET_KEY) {
|
||||
throw new Error("Stripe is not enabled; STRIPE_SECRET_KEY is not set.");
|
||||
}
|
||||
|
||||
return new Stripe(env.STRIPE_SECRET_KEY, {
|
||||
apiVersion: STRIPE_API_VERSION,
|
||||
});
|
||||
};
|
||||
|
||||
export const getStripeWebhookSecret = () => {
|
||||
if (!env.STRIPE_WEBHOOK_SECRET) {
|
||||
throw new Error("Stripe webhook is not enabled; STRIPE_WEBHOOK_SECRET is not set.");
|
||||
}
|
||||
|
||||
return env.STRIPE_WEBHOOK_SECRET;
|
||||
};
|
||||
@@ -1,20 +1,24 @@
|
||||
import Stripe from "stripe";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { STRIPE_API_VERSION } from "@/lib/constants";
|
||||
import { env } from "@/lib/env";
|
||||
import { handleCheckoutSessionCompleted } from "@/modules/ee/billing/api/lib/checkout-session-completed";
|
||||
import { handleInvoiceFinalized } from "@/modules/ee/billing/api/lib/invoice-finalized";
|
||||
import { handleSubscriptionDeleted } from "@/modules/ee/billing/api/lib/subscription-deleted";
|
||||
|
||||
const stripe = new Stripe(env.STRIPE_SECRET_KEY!, {
|
||||
apiVersion: STRIPE_API_VERSION,
|
||||
});
|
||||
|
||||
const webhookSecret: string = env.STRIPE_WEBHOOK_SECRET!;
|
||||
import { getStripeClient, getStripeWebhookSecret } from "./stripe-client";
|
||||
|
||||
export const webhookHandler = async (requestBody: string, stripeSignature: string) => {
|
||||
let stripe: Stripe;
|
||||
let webhookSecret: string;
|
||||
let event: Stripe.Event;
|
||||
|
||||
try {
|
||||
stripe = getStripeClient();
|
||||
webhookSecret = getStripeWebhookSecret();
|
||||
} catch (err: unknown) {
|
||||
logger.error(err, "Error getting Stripe client or webhook secret");
|
||||
logger.warn("Stripe webhook skipped: Stripe is not configured");
|
||||
return { status: 503, message: "Stripe webhook is not configured" };
|
||||
}
|
||||
|
||||
try {
|
||||
event = stripe.webhooks.constructEvent(requestBody, stripeSignature, webhookSecret);
|
||||
} catch (err) {
|
||||
|
||||
@@ -15,7 +15,7 @@ const ZGeneratePersonalSurveyLinkAction = z.object({
|
||||
});
|
||||
|
||||
export const generatePersonalSurveyLinkAction = authenticatedActionClient
|
||||
.schema(ZGeneratePersonalSurveyLinkAction)
|
||||
.inputSchema(ZGeneratePersonalSurveyLinkAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromContactId(parsedInput.contactId);
|
||||
const projectId = await getProjectIdFromContactId(parsedInput.contactId);
|
||||
|
||||
@@ -23,12 +23,12 @@ import {
|
||||
|
||||
const ZGetContactsAction = z.object({
|
||||
environmentId: ZId,
|
||||
offset: z.number().int().nonnegative(),
|
||||
offset: z.int().nonnegative(),
|
||||
searchValue: z.string().optional(),
|
||||
});
|
||||
|
||||
export const getContactsAction = authenticatedActionClient
|
||||
.schema(ZGetContactsAction)
|
||||
.inputSchema(ZGetContactsAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
@@ -53,7 +53,7 @@ const ZContactDeleteAction = z.object({
|
||||
contactId: ZId,
|
||||
});
|
||||
|
||||
export const deleteContactAction = authenticatedActionClient.schema(ZContactDeleteAction).action(
|
||||
export const deleteContactAction = authenticatedActionClient.inputSchema(ZContactDeleteAction).action(
|
||||
withAuditLogging(
|
||||
"deleted",
|
||||
"contact",
|
||||
@@ -95,46 +95,54 @@ const ZCreateContactsFromCSV = z.object({
|
||||
attributeMap: ZContactCSVAttributeMap,
|
||||
});
|
||||
|
||||
export const createContactsFromCSVAction = authenticatedActionClient.schema(ZCreateContactsFromCSV).action(
|
||||
withAuditLogging(
|
||||
"createdFromCSV",
|
||||
"contact",
|
||||
async ({ ctx, parsedInput }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
});
|
||||
export const createContactsFromCSVAction = authenticatedActionClient
|
||||
.inputSchema(ZCreateContactsFromCSV)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"createdFromCSV",
|
||||
"contact",
|
||||
async ({
|
||||
ctx,
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: Record<string, any>;
|
||||
}) => {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
const result = await createContactsFromCSV(
|
||||
parsedInput.csvData,
|
||||
parsedInput.environmentId,
|
||||
parsedInput.duplicateContactsAction,
|
||||
parsedInput.attributeMap
|
||||
);
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
const result = await createContactsFromCSV(
|
||||
parsedInput.csvData,
|
||||
parsedInput.environmentId,
|
||||
parsedInput.duplicateContactsAction,
|
||||
parsedInput.attributeMap
|
||||
);
|
||||
|
||||
if ("contacts" in result) {
|
||||
ctx.auditLoggingCtx.newObject = {
|
||||
contacts: result.contacts,
|
||||
};
|
||||
if ("contacts" in result) {
|
||||
ctx.auditLoggingCtx.newObject = {
|
||||
contacts: result.contacts,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
)
|
||||
);
|
||||
)
|
||||
);
|
||||
|
||||
const ZUpdateContactAttributesAction = z.object({
|
||||
contactId: ZId,
|
||||
@@ -143,7 +151,7 @@ const ZUpdateContactAttributesAction = z.object({
|
||||
|
||||
export type TUpdateContactAttributesAction = z.infer<typeof ZUpdateContactAttributesAction>;
|
||||
export const updateContactAttributesAction = authenticatedActionClient
|
||||
.schema(ZUpdateContactAttributesAction)
|
||||
.inputSchema(ZUpdateContactAttributesAction)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
|
||||
@@ -64,7 +64,7 @@ export const POST = withV1ApiWrapper({
|
||||
{
|
||||
environmentId: params.environmentId,
|
||||
url: req.url,
|
||||
validationError: cuidValidation.error.errors[0]?.message,
|
||||
validationError: cuidValidation.error.issues[0]?.message,
|
||||
},
|
||||
"Invalid CUID v1 format detected"
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user