From 630c0bce059f01339060bdaaf13a7aec08c2295c Mon Sep 17 00:00:00 2001 From: Justus Ojala Date: Mon, 15 Sep 2025 13:57:00 +0300 Subject: [PATCH] Add submission key to frontend to prevent duplicate signups --- package-lock.json | 42 +++++++++++++++++++++++++++++---------- package.json | 3 ++- src/models/Signup.ts | 3 ++- src/pages/signup/[id].tsx | 3 +++ 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 87cc204..7f0cd6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,8 @@ "sharp": "^0.30.3", "shortid": "^2.2.16", "styled-components": "^5.3.5", - "swr": "^1.2.2" + "swr": "^1.2.2", + "uuid": "^13.0.0" }, "devDependencies": { "@types/jest": "^27.4.1", @@ -13610,6 +13611,17 @@ "semver": "bin/semver" } }, + "node_modules/testcafe-reporter-dashboard/node_modules/uuid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/testcafe-reporter-json": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/testcafe-reporter-json/-/testcafe-reporter-json-2.2.0.tgz", @@ -14510,13 +14522,16 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "node_modules/uuid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", - "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "dev": true, + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", "bin": { - "uuid": "bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/uvu": { @@ -25397,6 +25412,12 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true + }, + "uuid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", + "dev": true } } }, @@ -25859,10 +25880,9 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "uuid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", - "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", - "dev": true + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==" }, "uvu": { "version": "0.5.3", diff --git a/package.json b/package.json index 44bc95d..e4efcc8 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,8 @@ "sharp": "^0.30.3", "shortid": "^2.2.16", "styled-components": "^5.3.5", - "swr": "^1.2.2" + "swr": "^1.2.2", + "uuid": "^13.0.0" }, "overrides": { "react-mde": { diff --git a/src/models/Signup.ts b/src/models/Signup.ts index efa8d77..0997385 100644 --- a/src/models/Signup.ts +++ b/src/models/Signup.ts @@ -1,7 +1,8 @@ import { OptionTypes } from "@components/Widgets/SignupQuestionsWidget/common"; export interface Signup { - id?: number; + id?: number; // Database id for completed signup + submitKey?: string; // Signup request idempotency key signupForm_id: number; answer: string; } diff --git a/src/pages/signup/[id].tsx b/src/pages/signup/[id].tsx index 6b17c70..a2d7301 100644 --- a/src/pages/signup/[id].tsx +++ b/src/pages/signup/[id].tsx @@ -13,6 +13,7 @@ import PageWrapper from "@views/common/PageWrapper"; import LoadingView from "@views/common/LoadingView"; import noop from "@utils/noop"; import NotFoundPage from "@pages/404"; +import { v4 as uuid } from "uuid"; type InitialProps = { initialForm: SignupForm; @@ -23,6 +24,7 @@ const FORM_URL = `${process.env.NEXT_PUBLIC_API_URL}/signupForm/`; const SignUpPage: NextPage = ({ initialForm }) => { const router = useRouter(); const id = String(initialForm?.id ?? ""); + const submitKey = uuid(); // Submission key, generated on page refresh const URL = `${FORM_URL}${id}/`; const { data: signupForm, error } = useSWR(URL, (url) => axios.get(url).then((res) => res.data), { fallbackData: initialForm }); @@ -43,6 +45,7 @@ const SignUpPage: NextPage = ({ initialForm }) => { const onSubmit = async ({ formData }: ISubmitEvent) => { const payload: Signup = { + submitKey, // This is for preventing duplicate requests; NOT RELATED TO THE SIGNUP ID IN DATABASE signupForm_id: signupForm.id, answer: formData, };