diff --git a/package-lock.json b/package-lock.json index 09ace0fd..0287519e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3621,6 +3621,11 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" + }, "node_modules/css-in-js-utils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", @@ -10534,6 +10539,7 @@ "class-variance-authority": "^0.6.1", "clsx": "^1.2.1", "cmdk": "^0.2.0", + "crypto-js": "^4.1.1", "lucide-react": "^0.259.0", "next": "13.4.9", "next-auth": "^4.22.1", diff --git a/packages/app/package.json b/packages/app/package.json index a462c99c..0f886def 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -50,6 +50,7 @@ "class-variance-authority": "^0.6.1", "clsx": "^1.2.1", "cmdk": "^0.2.0", + "crypto-js": "^4.1.1", "lucide-react": "^0.259.0", "next": "13.4.9", "next-auth": "^4.22.1", diff --git a/packages/app/prisma/schema.prisma b/packages/app/prisma/schema.prisma index 711bf35c..7bfb0057 100644 --- a/packages/app/prisma/schema.prisma +++ b/packages/app/prisma/schema.prisma @@ -37,6 +37,10 @@ model User { topLanguages String[] languagesMap Json? + signToken String @default("") + signTokenValidity DateTime @default(dbgenerated("NOW() - interval '1 year'")) + stamp String @default("") + averageAccuracy Decimal @default(0) @db.Decimal(5, 2) averageCpm Decimal @default(0) @db.Decimal(6, 2) diff --git a/packages/app/src/app/race/_components/race/game-multiplayer.tsx b/packages/app/src/app/race/_components/race/game-multiplayer.tsx index 41294f4e..1fe1474f 100644 --- a/packages/app/src/app/race/_components/race/game-multiplayer.tsx +++ b/packages/app/src/app/race/_components/race/game-multiplayer.tsx @@ -5,8 +5,9 @@ import { GameStateUpdatePayload } from "@code-racer/wss/src/events/server-to-cli import { useRouter } from "next/navigation"; import React, { useEffect, useRef, useState } from "react"; import { io } from "socket.io-client"; -import { saveUserResultAction } from "../../actions"; +import { getUserTokenAndStamp, saveUserResultAction } from "../../actions"; import { getSnippetById } from "../../(play)/loaders"; +import CryptoJS from "crypto-js"; // utils import { calculateAccuracy, calculateCPM, noopKeys } from "./utils"; @@ -389,27 +390,46 @@ function changeTimeStamps(e: any) { return; } - if (user) { - saveUserResultAction({ - raceParticipantId: participantId, - timeTaken, - errors: totalErrors, - cpm: calculateCPM(code.length - 1, timeTaken), - accuracy: calculateAccuracy(code.length - 1, totalErrors), - snippetId: snippet.id, + getUserTokenAndStamp() + .then((result) => { + const tokenAndStamp = result; + const data = { + timeTaken, + errors: totalErrors, + cpm: calculateCPM(code.length - 1, timeTaken), + accuracy: calculateAccuracy(code.length - 1, totalErrors), + snippetId: snippet.id, + stamp: tokenAndStamp!["stamp"], + }; + const jsonData = JSON.stringify(data); + const hashedData = CryptoJS.HmacSHA256( + jsonData, + tokenAndStamp!["key"] + ).toString(); + if (user) { + saveUserResultAction({ + raceParticipantId: participantId, + timeTaken, + errors: totalErrors, + cpm: calculateCPM(code.length - 1, timeTaken), + accuracy: calculateAccuracy(code.length - 1, totalErrors), + snippetId: snippet.id, + hash: hashedData, + }) + .then((result) => { + if (!result) { + return router.refresh(); + } + router.push(`/result?resultId=${result.id}`); + }) + .catch((error) => { + catchError(error); + }); + } else { + router.push(`/result?snippetId=${snippet.id}`); + } }) - .then((result) => { - if (!result) { - return router.refresh(); - } - router.push(`/result?resultId=${result.id}`); - }) - .catch((error) => { - catchError(error); - }); - } else { - router.push(`/result?snippetId=${snippet.id}`); - } + .catch((error) => catchError(error)); setSubmittingResults(false); } diff --git a/packages/app/src/app/race/_components/race/race-practice.tsx b/packages/app/src/app/race/_components/race/race-practice.tsx index 1acbe6fb..f5fceb5c 100644 --- a/packages/app/src/app/race/_components/race/race-practice.tsx +++ b/packages/app/src/app/race/_components/race/race-practice.tsx @@ -2,7 +2,8 @@ import { useRouter } from "next/navigation"; import React, { useEffect, useRef, useState } from "react"; -import { saveUserResultAction } from "../../actions"; +import { getUserTokenAndStamp, saveUserResultAction } from "../../actions"; +import CryptoJS from "crypto-js"; // utils import { calculateAccuracy, calculateCPM, noopKeys } from "./utils"; @@ -96,22 +97,40 @@ export default function RacePractice({ user, snippet }: RacePracticeProps) { }, ]) ); - - if (user) { - saveUserResultAction({ - timeTaken, - errors: totalErrors, - cpm: calculateCPM(code.length - 1, timeTaken), - accuracy: calculateAccuracy(code.length - 1, totalErrors), - snippetId: snippet.id, + + getUserTokenAndStamp() + .then((result) => { + const tokenAndStamp = result; + const data = { + timeTaken, + errors: totalErrors, + cpm: calculateCPM(code.length - 1, timeTaken), + accuracy: calculateAccuracy(code.length - 1, totalErrors), + snippetId: snippet.id, + stamp: tokenAndStamp!["stamp"], + }; + const jsonData = JSON.stringify(data); + const hashedData = CryptoJS.HmacSHA256(jsonData, tokenAndStamp!["key"]).toString(); + + if (user) { + saveUserResultAction({ + timeTaken, + errors: totalErrors, + cpm: calculateCPM(code.length - 1, timeTaken), + accuracy: calculateAccuracy(code.length - 1, totalErrors), + snippetId: snippet.id, + hash: hashedData, + }) + .then((result) => { + router.push(`/result?resultId=${result.id}`); + }) + .catch((error) => catchError(error)); + } else { + router.push(`/result?snippetId=${snippet.id}`); + } }) - .then((result) => { - router.push(`/result?resultId=${result.id}`); - }) - .catch((error) => catchError(error)); - } else { - router.push(`/result?snippetId=${snippet.id}`); - } + .catch((error) => catchError(error)); + } }); diff --git a/packages/app/src/app/race/actions.ts b/packages/app/src/app/race/actions.ts index b72d5d83..b3f159dd 100644 --- a/packages/app/src/app/race/actions.ts +++ b/packages/app/src/app/race/actions.ts @@ -6,6 +6,49 @@ import { UnauthorizedError } from "@/lib/exceptions/custom-hooks"; import { prisma } from "@/lib/prisma"; import { getCurrentUser } from "@/lib/session"; import { validatedCallback } from "@/lib/validatedCallback"; +import CryptoJS from "crypto-js"; + +export const getUserTokenAndStamp = async () => { + const user = await getCurrentUser(); + + if (!user) + return { + key: "deFau1tk3y", + stamp: "11011011", + }; + + const userData = await prisma.user.findUnique({ + where: { id: user.id }, + }); + + if (!userData) return; + + let signToken = userData.signToken; + let stamp = userData.stamp; + + if (userData!.signTokenValidity.getTime() < Date.now()) { + stamp = Math.random().toString(36).substring(2, 7); + signToken = Math.random().toString(36).substring(2, 22); + + await prisma.$transaction(async (tx) => { + await tx.user.update({ + where: { + id: user.id, + }, + data: { + stamp: stamp, + signToken: signToken, + signTokenValidity: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7), + }, + }); + }); + } + + return { + key: signToken, + stamp: stamp, + }; +}; export const saveUserResultAction = validatedCallback({ inputValidation: z.object({ @@ -15,11 +58,47 @@ export const saveUserResultAction = validatedCallback({ cpm: z.number(), accuracy: z.number().min(0).max(100), snippetId: z.string(), + hash: z.string(), }), callback: async (input) => { const user = await getCurrentUser(); - if (!user) throw new UnauthorizedError(); + if (!user) { + // verify hash: + const tokenAndStamp = await getUserTokenAndStamp(); + const data = { + timeTaken: input.timeTaken, + errors: input.errors, + cpm: input.cpm, + accuracy: input.accuracy, + snippetId: input.snippetId, + stamp: tokenAndStamp!["stamp"], + }; + const jsonData = JSON.stringify(data); + const hashedData = CryptoJS.HmacSHA256( + jsonData, + tokenAndStamp!["key"] + ).toString(); + + if (hashedData != input.hash.toString()) { + return "Invalid Request: Tampered Data"; + } else { + return { + takenTime: input.timeTaken.toString(), + errorCount: input.errors, + cpm: input.cpm, + accuracy: new Prisma.Decimal(input.accuracy), + snippetId: input.snippetId, + RaceParticipant: input.raceParticipantId + ? { + connect: { + id: input.raceParticipantId, + }, + } + : undefined, + }; + } + } const userData = await prisma.user.findUnique({ where: { id: user.id }, @@ -64,6 +143,28 @@ export const saveUserResultAction = validatedCallback({ .sort((a, b) => languagesMap[b] - languagesMap[a]) .splice(0, 3); + // verify hash: + const tokenAndStamp = await getUserTokenAndStamp(); + const data = { + timeTaken: input.timeTaken, + errors: input.errors, + cpm: input.cpm, + accuracy: input.accuracy, + snippetId: input.snippetId, + stamp: tokenAndStamp!["stamp"], + }; + const jsonData = JSON.stringify(data); + const hashedData = CryptoJS.HmacSHA256( + jsonData, + tokenAndStamp!["key"] + ).toString(); + + const stamp = Math.random().toString(36).substring(2, 7); + + if (hashedData != input.hash.toString()) { + return "Invalid Request: Tampered Data"; + } + return await prisma.$transaction(async (tx) => { const result = await tx.result.create({ data: { @@ -102,6 +203,7 @@ export const saveUserResultAction = validatedCallback({ averageCpm: avgValues._avg.cpm ?? 0, languagesMap: JSON.stringify(languagesMap), topLanguages: topLanguages, + stamp: stamp, }, }); diff --git a/packages/app/src/app/terms/page.tsx b/packages/app/src/app/terms/page.tsx index 282f176a..dcdd90f9 100644 --- a/packages/app/src/app/terms/page.tsx +++ b/packages/app/src/app/terms/page.tsx @@ -7,7 +7,7 @@ const page = () => {
Effective Date: [Date]
-Welcome to CodeRacer! These Terms of Service ("Terms") constitute a legal agreement between you and CodeRacer. Please read these Terms carefully before using our platform, which is accessible at https://code-racer-eight.vercel.app/. By using CodeRacer, you agree to be bound by these Terms.
+Welcome to CodeRacer! These Terms of Service ({`"Terms"`}) constitute a legal agreement between you and CodeRacer. Please read these Terms carefully before using our platform, which is accessible at https://code-racer-eight.vercel.app/. By using CodeRacer, you agree to be bound by these Terms.
@@ -45,7 +45,7 @@ const page = () => {
- 6.1. Disclaimer: CodeRacer is provided "as is," and we make no warranties or representations about the accuracy or reliability of the platform. Your use of CodeRacer is at your own risk. + 6.1. Disclaimer: CodeRacer is provided {`"as is,"`} and we make no warranties or representations about the accuracy or reliability of the platform. Your use of CodeRacer is at your own risk.