mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-02 11:30:31 -05:00
fix: use ENCRYPTION_KEY in single use surveys (#1094)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
@@ -179,10 +179,9 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
"The .NET Web Framework for Makers. Build production ready, full-stack web applications fast without sweating the small stuff.",
|
||||
href: "https://spark-framework.net",
|
||||
},
|
||||
{
|
||||
{
|
||||
name: "Firecamp",
|
||||
description:
|
||||
"vscode for apis, open-source postman/insomnia alternative",
|
||||
description: "vscode for apis, open-source postman/insomnia alternative",
|
||||
href: "https://firecamp.io",
|
||||
},
|
||||
],
|
||||
|
||||
+13
-6
@@ -1,10 +1,11 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { generateSurveySingleUseId } from "@/app/lib/singleUseSurveys";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { sendEmbedSurveyPreviewEmail } from "@formbricks/lib/emails/emails";
|
||||
import { AuthenticationError } from "@formbricks/types/v1/errors";
|
||||
import { generateSurveySingleUseId } from "@/app/lib/singleUseSurveys";
|
||||
import { canUserAccessSurvey } from "@formbricks/lib/survey/auth";
|
||||
import { AuthenticationError, AuthorizationError } from "@formbricks/types/v1/errors";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
type TSendEmailActionArgs = {
|
||||
to: string;
|
||||
@@ -12,9 +13,15 @@ type TSendEmailActionArgs = {
|
||||
html: string;
|
||||
};
|
||||
|
||||
export async function generateSingleUseIdAction(isEncrypted: boolean): Promise<string> {
|
||||
const singleUseId = generateSurveySingleUseId(isEncrypted);
|
||||
return singleUseId;
|
||||
export async function generateSingleUseIdAction(surveyId: string, isEncrypted: boolean): Promise<string> {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const hasUserSurveyAccess = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
|
||||
if (!hasUserSurveyAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return generateSurveySingleUseId(isEncrypted);
|
||||
}
|
||||
|
||||
export const sendEmailAction = async ({ html, subject, to }: TSendEmailActionArgs) => {
|
||||
|
||||
+1
-10
@@ -40,16 +40,7 @@ export default function LinkSurveyShareButton({
|
||||
}}>
|
||||
<ShareIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
{showLinkModal && isSingleUse ? (
|
||||
<ShareEmbedSurvey
|
||||
survey={survey}
|
||||
open={showLinkModal}
|
||||
setOpen={setShowLinkModal}
|
||||
product={product}
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
profile={profile}
|
||||
/>
|
||||
) : (
|
||||
{showLinkModal && isSingleUse && (
|
||||
<ShareEmbedSurvey
|
||||
survey={survey}
|
||||
open={showLinkModal}
|
||||
|
||||
+4
-3
@@ -20,6 +20,8 @@ export default function LinkSingleUseSurveyModal({ survey, surveyBaseUrl }: Link
|
||||
|
||||
useEffect(() => {
|
||||
fetchSingleUseIds();
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [survey.singleUse?.isEncrypted]);
|
||||
|
||||
const fetchSingleUseIds = async () => {
|
||||
@@ -30,9 +32,8 @@ export default function LinkSingleUseSurveyModal({ survey, surveyBaseUrl }: Link
|
||||
const generateSingleUseIds = async (isEncrypted: boolean) => {
|
||||
const promises = Array(7)
|
||||
.fill(null)
|
||||
.map(() => generateSingleUseIdAction(isEncrypted));
|
||||
const ids = await Promise.all(promises);
|
||||
return ids;
|
||||
.map(() => generateSingleUseIdAction(survey.id, isEncrypted));
|
||||
return await Promise.all(promises);
|
||||
};
|
||||
|
||||
const defaultSurveyUrl = `${surveyBaseUrl}/${survey.id}`;
|
||||
|
||||
+15
-33
@@ -1,12 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
|
||||
import { DatePicker } from "@formbricks/ui/DatePicker";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/Tooltip";
|
||||
import { CheckCircleIcon } from "@heroicons/react/24/solid";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { KeyboardEventHandler, useEffect, useState } from "react";
|
||||
@@ -15,14 +13,12 @@ import toast from "react-hot-toast";
|
||||
interface ResponseOptionsCardProps {
|
||||
localSurvey: TSurvey;
|
||||
setLocalSurvey: (survey: TSurvey | ((TSurvey) => TSurvey)) => void;
|
||||
isEncryptionKeySet: boolean;
|
||||
responseCount: number;
|
||||
}
|
||||
|
||||
export default function ResponseOptionsCard({
|
||||
localSurvey,
|
||||
setLocalSurvey,
|
||||
isEncryptionKeySet,
|
||||
responseCount,
|
||||
}: ResponseOptionsCardProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -44,7 +40,7 @@ export default function ResponseOptionsCard({
|
||||
subheading: "You can only use this link once.",
|
||||
});
|
||||
|
||||
const [singleUseEncryption, setSingleUseEncryption] = useState(isEncryptionKeySet);
|
||||
const [singleUseEncryption, setSingleUseEncryption] = useState(true);
|
||||
const [verifyEmailSurveyDetails, setVerifyEmailSurveyDetails] = useState({
|
||||
name: "",
|
||||
subheading: "",
|
||||
@@ -438,34 +434,20 @@ export default function ResponseOptionsCard({
|
||||
/>
|
||||
<Label htmlFor="headline">URL Encryption</Label>
|
||||
<div>
|
||||
<TooltipProvider delayDuration={50}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="mt-2 flex items-center space-x-1 ">
|
||||
<Switch
|
||||
id="encryption-switch"
|
||||
checked={singleUseEncryption}
|
||||
onCheckedChange={hangleSingleUseEncryptionToggle}
|
||||
disabled={!isEncryptionKeySet}
|
||||
/>
|
||||
<Label htmlFor="encryption-label">
|
||||
<div className="ml-2">
|
||||
<p className="text-sm font-normal text-slate-600">
|
||||
Enable encryption of Single Use Id (suId) in survey URL.
|
||||
</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
{!isEncryptionKeySet && (
|
||||
<TooltipContent side={"top"}>
|
||||
<p className="py-2 text-center text-xs text-slate-500 dark:text-slate-400">
|
||||
FORMBRICKS_ENCRYPTION_KEY needs to be set to enable this feature.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<div className="mt-2 flex items-center space-x-1 ">
|
||||
<Switch
|
||||
id="encryption-switch"
|
||||
checked={singleUseEncryption}
|
||||
onCheckedChange={hangleSingleUseEncryptionToggle}
|
||||
/>
|
||||
<Label htmlFor="encryption-label">
|
||||
<div className="ml-2">
|
||||
<p className="text-sm font-normal text-slate-600">
|
||||
Enable encryption of Single Use Id (suId) in survey URL.
|
||||
</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
-3
@@ -15,7 +15,6 @@ interface SettingsViewProps {
|
||||
setLocalSurvey: (survey: TSurvey) => void;
|
||||
actionClasses: TActionClass[];
|
||||
attributeClasses: TAttributeClass[];
|
||||
isEncryptionKeySet: boolean;
|
||||
responseCount: number;
|
||||
}
|
||||
|
||||
@@ -25,7 +24,6 @@ export default function SettingsView({
|
||||
setLocalSurvey,
|
||||
actionClasses,
|
||||
attributeClasses,
|
||||
isEncryptionKeySet,
|
||||
responseCount,
|
||||
}: SettingsViewProps) {
|
||||
return (
|
||||
@@ -49,7 +47,6 @@ export default function SettingsView({
|
||||
<ResponseOptionsCard
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
isEncryptionKeySet={isEncryptionKeySet}
|
||||
responseCount={responseCount}
|
||||
/>
|
||||
|
||||
|
||||
-3
@@ -20,7 +20,6 @@ interface SurveyEditorProps {
|
||||
environment: TEnvironment;
|
||||
actionClasses: TActionClass[];
|
||||
attributeClasses: TAttributeClass[];
|
||||
isEncryptionKeySet: boolean;
|
||||
responseCount: number;
|
||||
}
|
||||
|
||||
@@ -30,7 +29,6 @@ export default function SurveyEditor({
|
||||
environment,
|
||||
actionClasses,
|
||||
attributeClasses,
|
||||
isEncryptionKeySet,
|
||||
responseCount,
|
||||
}: SurveyEditorProps): JSX.Element {
|
||||
const [activeView, setActiveView] = useState<"questions" | "settings">("questions");
|
||||
@@ -95,7 +93,6 @@ export default function SurveyEditor({
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
actionClasses={actionClasses}
|
||||
attributeClasses={attributeClasses}
|
||||
isEncryptionKeySet={isEncryptionKeySet}
|
||||
responseCount={responseCount}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
export const revalidate = REVALIDATION_INTERVAL;
|
||||
import React from "react";
|
||||
import { FORMBRICKS_ENCRYPTION_KEY, REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import SurveyEditor from "./components/SurveyEditor";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
|
||||
import SurveyEditor from "./components/SurveyEditor";
|
||||
|
||||
export default async function SurveysEditPage({ params }) {
|
||||
const [survey, product, environment, actionClasses, attributeClasses, responseCount] = await Promise.all([
|
||||
@@ -19,7 +18,7 @@ export default async function SurveysEditPage({ params }) {
|
||||
getAttributeClasses(params.environmentId),
|
||||
getResponseCountBySurveyId(params.surveyId),
|
||||
]);
|
||||
const isEncryptionKeySet = !!FORMBRICKS_ENCRYPTION_KEY;
|
||||
|
||||
if (!survey || !environment || !actionClasses || !attributeClasses || !product) {
|
||||
return <ErrorComponent />;
|
||||
}
|
||||
@@ -32,7 +31,6 @@ export default async function SurveysEditPage({ params }) {
|
||||
environment={environment}
|
||||
actionClasses={actionClasses}
|
||||
attributeClasses={attributeClasses}
|
||||
isEncryptionKeySet={isEncryptionKeySet}
|
||||
responseCount={responseCount}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FORMBRICKS_ENCRYPTION_KEY } from "@formbricks/lib/constants";
|
||||
import { decryptAES128, encryptAES128 } from "@formbricks/lib/crypto";
|
||||
import { FORMBRICKS_ENCRYPTION_KEY, ENCRYPTION_KEY } from "@formbricks/lib/constants";
|
||||
import { decryptAES128, symmetricDecrypt, symmetricEncrypt } from "@formbricks/lib/crypto";
|
||||
import cuid2 from "@paralleldrive/cuid2";
|
||||
|
||||
// generate encrypted single use id for the survey
|
||||
@@ -8,20 +8,26 @@ export const generateSurveySingleUseId = (isEncrypted: boolean): string => {
|
||||
if (!isEncrypted) {
|
||||
return cuid;
|
||||
}
|
||||
if (!FORMBRICKS_ENCRYPTION_KEY) {
|
||||
throw new Error("FORMBRICKS_ENCRYPTION_KEY is not defined");
|
||||
}
|
||||
const encryptedCuid = encryptAES128(FORMBRICKS_ENCRYPTION_KEY, cuid);
|
||||
|
||||
const encryptedCuid = symmetricEncrypt(cuid, ENCRYPTION_KEY);
|
||||
return encryptedCuid;
|
||||
};
|
||||
|
||||
// validate the survey single use id
|
||||
export const validateSurveySingleUseId = (surveySingleUseId: string): string | undefined => {
|
||||
if (!FORMBRICKS_ENCRYPTION_KEY) {
|
||||
throw new Error("FORMBRICKS_ENCRYPTION_KEY is not defined");
|
||||
}
|
||||
try {
|
||||
const decryptedCuid = decryptAES128(FORMBRICKS_ENCRYPTION_KEY!, surveySingleUseId);
|
||||
let decryptedCuid: string | null = null;
|
||||
|
||||
if (surveySingleUseId.length === 64) {
|
||||
if (!FORMBRICKS_ENCRYPTION_KEY) {
|
||||
throw new Error("FORMBRICKS_ENCRYPTION_KEY is not defined");
|
||||
}
|
||||
|
||||
decryptedCuid = decryptAES128(FORMBRICKS_ENCRYPTION_KEY!, surveySingleUseId);
|
||||
} else {
|
||||
decryptedCuid = symmetricDecrypt(surveySingleUseId, ENCRYPTION_KEY);
|
||||
}
|
||||
|
||||
if (cuid2.isCuid(decryptedCuid)) {
|
||||
return decryptedCuid;
|
||||
} else {
|
||||
|
||||
@@ -28,17 +28,10 @@ export async function middleware(request: NextRequest) {
|
||||
} catch (_e) {
|
||||
console.log("Rate Limiting IP: ", ip);
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: "Too many requests, Please try after a while!" },
|
||||
{ status: 429 }
|
||||
);
|
||||
return NextResponse.json({ error: "Too many requests, Please try after a while!" }, { status: 429 });
|
||||
}
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ error: "Too many requests, Please try after a while!" },
|
||||
{ status: 429 }
|
||||
);
|
||||
|
||||
return NextResponse.json({ error: "Too many requests, Please try after a while!" }, { status: 429 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user