fix: use ENCRYPTION_KEY in single use surveys (#1094)

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
Anshuman Pandey
2023-10-19 13:47:57 +05:30
committed by GitHub
parent 7ea11133d8
commit 14c354ea36
10 changed files with 60 additions and 89 deletions
@@ -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",
},
],
@@ -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) => {
@@ -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}
@@ -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}`;
@@ -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>
@@ -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}
/>
@@ -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}
/>
</>
+16 -10
View File
@@ -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 {
+2 -9
View File
@@ -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 });
}
}