Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#648 invalid race post request #723

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions packages/app/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
62 changes: 41 additions & 21 deletions packages/app/src/app/race/_components/race/game-multiplayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
}
Expand Down
51 changes: 35 additions & 16 deletions packages/app/src/app/race/_components/race/race-practice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't you think it has to be called on the server?
Otherwise I could get the result of it (tokenAndStamp) in the network tab
and then make a call to saveUserResultAction with invalid cpm amount

.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));

}
});

Expand Down
104 changes: 103 additions & 1 deletion packages/app/src/app/race/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need transaction in here?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed for data migration part. To introduce new token field into database. As well as to update token every 7 days to strengthen the security

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({
Expand All @@ -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 },
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -102,6 +203,7 @@ export const saveUserResultAction = validatedCallback({
averageCpm: avgValues._avg.cpm ?? 0,
languagesMap: JSON.stringify(languagesMap),
topLanguages: topLanguages,
stamp: stamp,
},
});

Expand Down
5 changes: 3 additions & 2 deletions packages/app/src/app/terms/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const page = () => {
<h1>CodeRacer - Terms of Service</h1>
<p><strong>Effective Date:</strong> [Date]</p>

<p>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 <a href="https://code-racer-eight.vercel.app/">https://code-racer-eight.vercel.app/</a>. By using CodeRacer, you agree to be bound by these Terms.</p>
<p>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 <a href="https://code-racer-eight.vercel.app/">https://code-racer-eight.vercel.app/</a>. By using CodeRacer, you agree to be bound by these Terms.</p>

<h2>1. User Accounts</h2>
<p>
Expand Down Expand Up @@ -45,7 +45,7 @@ const page = () => {

<h2>6. Limitation of Liability</h2>
<p>
<strong>6.1. Disclaimer:</strong> 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.
<strong>6.1. Disclaimer:</strong> 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.
</p>

<h2>7. Changes to Terms</h2>
Expand All @@ -58,6 +58,7 @@ const page = () => {
<strong>8.1. Questions:</strong> If you have any questions or concerns about these Terms, please contact us at <a href="mailto:[email protected]">[email protected]</a>.
</p>
</div>
</>
);
};

Expand Down