From 803c9703483b73e8dd06f173d2cab89abb2f4b35 Mon Sep 17 00:00:00 2001 From: pankaj <0xpankaj@gmail.com> Date: Mon, 1 Sep 2025 03:10:48 +0545 Subject: [PATCH 1/2] completed end point --- apps/backend/index.ts | 7 +- apps/backend/middleware/admin.ts | 67 ++++++ apps/backend/middleware/user.ts | 58 +++++ apps/backend/package.json | 12 +- apps/backend/routes/admin-contest.ts | 121 +++++++++++ apps/backend/routes/admin.ts | 37 +++- apps/backend/routes/contest.ts | 312 ++++++++++++++++++++++++++- apps/backend/routes/user.ts | 43 +++- apps/backend/utils/otpEmail.ts | 0 9 files changed, 638 insertions(+), 19 deletions(-) create mode 100644 apps/backend/routes/admin-contest.ts create mode 100644 apps/backend/utils/otpEmail.ts diff --git a/apps/backend/index.ts b/apps/backend/index.ts index 31505fd..33ce4b5 100644 --- a/apps/backend/index.ts +++ b/apps/backend/index.ts @@ -1,9 +1,14 @@ import express from "express"; import userRouter from "./routes/user" -import contestRouter from "./routes/contest" import adminRouter from "./routes/admin" +import contestRouter from "./routes/contest" +import cors from "cors" + const app = express(); +app.use(cors()) +app.use(express.json()); + app.use("/user", userRouter); app.use("/admin", adminRouter); diff --git a/apps/backend/middleware/admin.ts b/apps/backend/middleware/admin.ts index e69de29..c87bf51 100644 --- a/apps/backend/middleware/admin.ts +++ b/apps/backend/middleware/admin.ts @@ -0,0 +1,67 @@ +import { client } from "db/client"; +import { type NextFunction, type Request, type Response } from "express" +import jwt from "jsonwebtoken" + + + +interface authenticatedRequest extends Request { + userId?: string, + userRole?: string +} + +export const adminMiddleware = async( + req: authenticatedRequest, + res: Response, + next: NextFunction + ) => { + + try { + const token = req.headers.authorization?.replace("Bearer ", ""); + + if (!token) { + return res.status(401).json({ error: "Missing token"}); + } + + const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any; + + if (!decoded) { + return res.status(401).json({ error: "Invalid token"}); + } + + const user = await client.user.findUnique({ + where: { + id: decoded.userId + }, + select: { + role: true, + id: true + } + }) + + + if (!user || user.role !== "Admin"){ + return res.status(401).json({ error: "Unauthorized"}); + } + + + req.userId = user.id; + req.userRole = user.role; + + + + + next(); + + + + + + + + + } catch (error) { + res.status(401).json({ error: "Invalid token"}); + } + + +} \ No newline at end of file diff --git a/apps/backend/middleware/user.ts b/apps/backend/middleware/user.ts index e69de29..c01e7b7 100644 --- a/apps/backend/middleware/user.ts +++ b/apps/backend/middleware/user.ts @@ -0,0 +1,58 @@ + +import { client } from "db/client" +import { type Request, type Response, type NextFunction } from "express" +import jwt from "jsonwebtoken" + + +export interface authenticatedRequest extends Request { + userId?: string + userRole?: string +} + +export const userMiddleware = async( + req: authenticatedRequest, + res: Response, + next: NextFunction +) => { + try { + + const token = req.headers.authorization?.replace("Bearer ", ""); + + + if (!token) { + return res.status(401).json({ error: "Missing token"}); + } + + const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any; + + const user = await client.user.findUnique({ + where: { + id: decoded.userId + }, + select: { + role: true, + id: true + } + }); + + + if (!user) { + return res.status(401).json({ error: "Unauthorized"}); + } + + + req.userId = user.id; + req.userRole = user.role; + + + + next(); + + + + + } catch (error) { + res.status(401).json({ error: "Unauthorized" }) + } + +} \ No newline at end of file diff --git a/apps/backend/package.json b/apps/backend/package.json index cd7ff4e..3494661 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -9,8 +9,18 @@ "typescript": "^5.0.0" }, "dependencies": { + "@types/bcrypt": "^6.0.0", + "@types/cors": "^2.8.19", "@types/express": "^5.0.3", + "@types/express-rate-limit": "^6.0.2", + "@types/jsonwebtoken": "^9.0.10", + "bcrypt": "^6.0.0", + "cors": "^2.8.5", + "db": "*", "express": "^5.1.0", - "db": "*" + "express-rate-limit": "^8.0.1", + "jsonwebtoken": "^9.0.2", + "nodemailer": "^7.0.6", + "zod": "^4.1.5" } } \ No newline at end of file diff --git a/apps/backend/routes/admin-contest.ts b/apps/backend/routes/admin-contest.ts new file mode 100644 index 0000000..a24151b --- /dev/null +++ b/apps/backend/routes/admin-contest.ts @@ -0,0 +1,121 @@ +import { client } from "db/client"; +import { Router } from "express"; +import { adminMiddleware } from "../middleware/admin"; +import z from "zod" + +const router = Router(); + +// Admin + +router.post("/contest", adminMiddleware, async(req, res ) => { + try { + + const createContestSchema = z.object({ + title: z.string().min(1, "title is missing "), + startTime: z.string().datetime("Invalid datetime format"), + challengeIds: z.array(z.string()).min(1, "one challenge required") + }) + + + const { title, startTime, challengeIds } = createContestSchema.parse(req.body); + + + const challenges = await client.challenge.findMany({ + where: { + id: { + in: challengeIds + } + } + }) + + + if (challenges.length !== challengeIds.length) { + return res.status(404).json({ error: "Challenge not found"}); + } + + const contest = await client.contest.create({ + data: { + title, + startTime: new Date(startTime), + contestToChallengeMapping: { + create: challengeIds.map((challengeId, index ) => ({ + challengeId, + index + })) + } + }, + include: { + contestToChallengeMapping: { + include: { + challenge: true + } + } + } + }) + + res.status(201).json({ + "msg": "Contest created successfully", + "contest": contest + }); + + + + + } catch (error) { + res.status(500).json({ error: "Internal server error"}); + } +}) + + +//create challenge +router.post("/challenge", adminMiddleware, async(req, res) => { + try { + + const createChallengeSchema = z.object({ + title: z.string().min(1, "Title is required"), + notionDocId: z.string().min(1, "Notion doc ID is required"), + maxPoints: z.number().int().positive("Max points must be positive") +}); + +const { title, notionDocId, maxPoints } = createChallengeSchema.parse(req.body); + + + const challenge = await client.challenge.create({ + data: { + title, + notionDocId, + maxPoints + } + }) + + res.status(201).json({ + "msg": "Challenge created successfully", + "challenge": challenge + }); + + + + } catch (error) { + res.status(500).json({ error: "Internal server error"}); + } +}); + + +// getting all challenges +router.get("/challenges", adminMiddleware, async(req, res) => { + const challengs = await client.challenge.findMany({ + orderBy: { + title: "asc" + } + }) + + res.status(200).json({ + "challenges": challengs + }); +}) + + + + + +export default router; diff --git a/apps/backend/routes/admin.ts b/apps/backend/routes/admin.ts index ce6afbb..70ebb16 100644 --- a/apps/backend/routes/admin.ts +++ b/apps/backend/routes/admin.ts @@ -1,15 +1,46 @@ import { Router } from "express"; +import { client } from "db/client"; +import z from "zod" +import bcrypt from "bcrypt" const router = Router(); -router.post("/signup", (req, res) => { +router.post("/signup", async(req, res) => { + const signupSchema = z.object({ + email: z.string().email(), + password: z.string().min(6), + }); -}) + console.log(req.body); + + const { email, password } = signupSchema.parse(req.body); + + console.log(email, password); + + const existingUser = await client.user.findUnique({ + where: { + email: email + } + }) -router.post("/signin", (req, res) => { + if(existingUser) { + return res.status(400).json({ error: "User already exists"}); + } + const hashedPassword = await bcrypt.hash(password, 10); + + const user = await client.user.create({ + data: { + email: email, + password: hashedPassword, + role: "Admin" + } + }) + + res.status(201).json(user); }) + export default router; \ No newline at end of file diff --git a/apps/backend/routes/contest.ts b/apps/backend/routes/contest.ts index d23be12..01a4489 100644 --- a/apps/backend/routes/contest.ts +++ b/apps/backend/routes/contest.ts @@ -1,37 +1,327 @@ +import { client } from "db/client"; import { Router } from "express"; +import rateLimit from "express-rate-limit"; +import z from "zod"; +import { adminMiddleware } from "../middleware/admin"; +import { userMiddleware, type authenticatedRequest } from "../middleware/user"; const router = Router(); + +// GET /contest/active // https://api.devforces.com/contest?offset=10&page=20 -router.get("/active", (req, res) => { - const {offset, page} = req.query; +router.get("/active", async(req, res) => { + + try { + + const offset = parseInt(req.query.offset as string) || 0; + const page = parseInt(req.query.page as string) || 20; + + const contests = await client.contest.findMany({ + include: { + contestToChallengeMapping: { + include: { + challenge: { + select: { + id: true, + title: true, + maxPoints: true + } + } + } + }, + _count: { + select: { + leaderboard: true + } + } + }, + skip: page * offset, + take: offset, + orderBy: { + startTime: "desc" + } + + + + + }); + + + res.status(200).json(contests); + + } catch (error) { + res.status(500).json({ error: "Internal server error"}); + } + }) -router.get("/finished", (req, res) => { - let {offset, page} = req.query; +// GET /contest/finished +router.get("/finished", async(req, res) => { + + try { + + const offset = parseInt(req.query.offset as string) || 0; + const page = parseInt(req.query.page as string) || 20; + + const contests = await client.contest.findMany({ + include: { + contestToChallengeMapping: { + include: { + challenge: { + select: { + id: true, + title: true, + maxPoints: true + } + } + } + }, + _count: { + select: { + leaderboard: true + } + } + }, + skip: offset * page, + take: offset, + orderBy: { + startTime: "desc" + } + }) + + res.json({ + success: true, + contests: contests, + count: contests.length + }); + + + } catch (error) { + res.sendStatus(500).json({ error: "Internal server error"}); + } + + }) +// GET /contest/:contestId // return all the sub challenges and their start times. -router.get("/:contestId", (req, res) => { - const contestId = req.params.contestId +router.get("/:contestId", async(req, res) => { + + try { + const contestId = req.params.contestId + + const contests = await client.contest.findUnique({ + where: { + id: contestId + }, + include: { + contestToChallengeMapping: { + include: { + challenge: { + select: { + id: true, + title: true, + maxPoints: true, + notionDocId: true + } + } + }, + orderBy: { + index: "asc" + } + } + } + }) + + if (!contests) { + return res.status(404).json({ error: "Contest not found"}); + } + + + res.status(200).json(contests); + + + + + + + + } catch (error) { + res.status(500).json({ error: "Internal server error"}); + } + }) -router.get("/:contestId/:challengeId", (req, res) => { - const contestId = req.params.contestId +router.get("/:contestId/:challengeId", async(req, res) => { + + try { + const contestId = req.params.contestId; + const challengeId = req.params.challengeId; + + const contest = await client.contestToChallengeMapping.findFirst({ + where: { + contestId, + challengeId + }, + include: { + challenge: true, + contest: { + select: { + id: true, + title: true, + startTime: true + } + } + } + }) + + if (!contest) { + return res.status(404).json({ error: "Challenge not found"}); + } + + + res.status(200).json(contest); + + + + + } catch (error) { + res.status(500).json({ error: "Internal server error"}); + } }) -router.get("/leaderboard/:contestId", (req, res) => { +// /contest/leaderboard/:contestId +router.get("/leaderboard/:contestId", async(req, res) => { + + try { + const contestId = req.params.contestId + + const learderboard = await client.leaderboard.findMany({ + where: { + contestId + }, + include: { + user: { + select: { + id: true, + email: true + } + } + }, + orderBy: { + rank: "asc" + } + }) + + + if (!learderboard) { + return res.status(404).json({ error: "Leaderboard not found"}); + } + + res.status(200).json(learderboard); + + + } catch (error) { + res.status(500).json({ error: "Internal server error"}); + } + + +}) + + + +// rate limiting +const ratelimit = rateLimit({ + max:20, + message: "too many request to this problem", }) -router.post("/submit/:challengeId", (req, res) => { - // have rate limitting + +// POST OST /contest/submit/:challengeId +router.post("/submit/:challengeId", ratelimit, userMiddleware, async(req: authenticatedRequest, res) => { + // have rate limitting done + // max 20 submissions per problem + try { + + const challengeId = req.params.challengeId; + const userId = req.userId!; + + const submission = req.body.submission + + const contestToChallengeMapping = await client.contestToChallengeMapping.findFirst({ + where: { + challengeId + }, + include: { + challenge: true, + contest: true + } + }) + + + if (!contestToChallengeMapping) { + return res.status(404).json({ error: "Challenge not found"}); + } + + + //checking contest started or not + if (contestToChallengeMapping.contest.startTime > new Date()) { + return res.status(400).json({ error: "Contest not started yet"}); + } + + // maximut 20 submission + const submissionCount = await client.submission.count({ + where: { + userId, + contestToChallengeMappingId: contestToChallengeMapping.id + } + }); + + + + if (submissionCount >= 20) { + return res.status(400).json({ error: "You have already submitted 20 times"}); + } + + const finalSubmission = await client.submission.create({ + data: { + userId, + contestToChallengeMappingId: contestToChallengeMapping.id, + submission, + points: 0 // will update on stage 2 + } + }) + + + res.status(200).json({ + "msg": "Submission created successfully", + "submission": { + id: finalSubmission.id, + createdAt: finalSubmission.createdAt, + remaingSubmissions: 20 - submissionCount + + } + }); + + + + } catch (error) { + res.status(500).json({ error: "Internal server error"}); + } + // forward the request to GPT // store the response in sorted set and the DB }) + + + + + export default router; \ No newline at end of file diff --git a/apps/backend/routes/user.ts b/apps/backend/routes/user.ts index e204b2b..d78f4a5 100644 --- a/apps/backend/routes/user.ts +++ b/apps/backend/routes/user.ts @@ -1,15 +1,52 @@ import { Router } from "express"; import { client } from "db/client"; +import z from "zod"; +import bcrypt from "bcrypt" + + -const router = Router(); -router.post("/signup", (req, res) => { +const router = Router(); +router.get("/health", (req, res) => { + res.status(200).json({ message: req.body }); }) -router.post("/signin", (req, res) => { +router.post("/signup", async(req, res) => { + + + const signupSchema = z.object({ + email: z.string().email(), + password: z.string().min(6), + }); + + console.log(req.body); + + const { email, password } = signupSchema.parse(req.body); + + console.log(email, password); + + const existingUser = await client.user.findUnique({ + where: { + email: email + } + }) + + if(existingUser) { + return res.status(400).json({ error: "User already exists"}); + } + + const hashedPassword = await bcrypt.hash(password, 10); + const user = await client.user.create({ + data: { + email: email, + password: hashedPassword, + role: "User" + } + }) + res.status(201).json(user); }) export default router; \ No newline at end of file diff --git a/apps/backend/utils/otpEmail.ts b/apps/backend/utils/otpEmail.ts new file mode 100644 index 0000000..e69de29 From 92f1a18599c655c98cabe5b7356287c9d49d7d75 Mon Sep 17 00:00:00 2001 From: pankaj <0xpankaj@gmail.com> Date: Mon, 1 Sep 2025 03:27:17 +0545 Subject: [PATCH 2/2] working one email otp and gpt part --- bun.lock | 62 +++++++++++++ .../20250831195956_init/migration.sql | 90 +++++++++++++++++++ .../db/prisma/migrations/migration_lock.toml | 3 + 3 files changed, 155 insertions(+) create mode 100644 packages/db/prisma/migrations/20250831195956_init/migration.sql create mode 100644 packages/db/prisma/migrations/migration_lock.toml diff --git a/bun.lock b/bun.lock index fe528c2..1ea505a 100644 --- a/bun.lock +++ b/bun.lock @@ -12,9 +12,19 @@ "apps/backend": { "name": "backend", "dependencies": { + "@types/bcrypt": "^6.0.0", + "@types/cors": "^2.8.19", "@types/express": "^5.0.3", + "@types/express-rate-limit": "^6.0.2", + "@types/jsonwebtoken": "^9.0.10", + "bcrypt": "^6.0.0", + "cors": "^2.8.5", "db": "*", "express": "^5.1.0", + "express-rate-limit": "^8.0.1", + "jsonwebtoken": "^9.0.2", + "nodemailer": "^7.0.6", + "zod": "^4.1.5", }, "devDependencies": { "@types/bun": "latest", @@ -217,24 +227,34 @@ "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], + "@types/bcrypt": ["@types/bcrypt@6.0.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ=="], + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], "@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="], "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/express": ["@types/express@5.0.3", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="], + "@types/express-rate-limit": ["@types/express-rate-limit@6.0.2", "", { "dependencies": { "express-rate-limit": "*" } }, "sha512-e1xZLOOlxCDvplAGq7rDcXtbdBu2CWRsMjaIu1LVqGxWtKvwr884YE5mPs3IvHeG/OMDhf24oTaqG5T1bV3rBQ=="], + "@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.7", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ=="], "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="], + "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/node": ["@types/node@22.18.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ=="], "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], @@ -303,12 +323,16 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "bcrypt": ["bcrypt@6.0.0", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg=="], + "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -355,6 +379,8 @@ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], @@ -391,6 +417,8 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], "effect": ["effect@3.16.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg=="], @@ -449,6 +477,8 @@ "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], + "express-rate-limit": ["express-rate-limit@8.0.1", "", { "dependencies": { "ip-address": "10.0.1" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-aZVCnybn7TVmxO4BtlmnvX+nuz8qHW124KKJ8dumsBsmv5ZLxE0pYu7S2nwyRBGHHCAzdmnGyrc5U/rksSPO7Q=="], + "exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="], "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], @@ -533,6 +563,8 @@ "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + "ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="], + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], @@ -607,16 +639,36 @@ "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + "jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="], + "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + "jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="], + + "jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], + + "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], + + "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], + + "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="], + + "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], + + "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="], + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -645,8 +697,14 @@ "next": ["next@15.5.2", "", { "dependencies": { "@next/env": "15.5.2", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.2", "@next/swc-darwin-x64": "15.5.2", "@next/swc-linux-arm64-gnu": "15.5.2", "@next/swc-linux-arm64-musl": "15.5.2", "@next/swc-linux-x64-gnu": "15.5.2", "@next/swc-linux-x64-musl": "15.5.2", "@next/swc-win32-arm64-msvc": "15.5.2", "@next/swc-win32-x64-msvc": "15.5.2", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q=="], + "node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + + "nodemailer": ["nodemailer@7.0.6", "", {}, "sha512-F44uVzgwo49xboqbFgBGkRaiMgtoBrBEWCVincJPK9+S9Adkzt/wXCLKbf7dxucmxfTI5gHGB+bEmdyzN6QKjw=="], + "nypm": ["nypm@0.6.1", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.2.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -883,6 +941,8 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "zod": ["zod@4.1.5", "", {}, "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], @@ -903,6 +963,8 @@ "http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + "jsonwebtoken/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "sharp/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], "@typescript-eslint/typescript-estree/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], diff --git a/packages/db/prisma/migrations/20250831195956_init/migration.sql b/packages/db/prisma/migrations/20250831195956_init/migration.sql new file mode 100644 index 0000000..d1f1442 --- /dev/null +++ b/packages/db/prisma/migrations/20250831195956_init/migration.sql @@ -0,0 +1,90 @@ +-- CreateEnum +CREATE TYPE "public"."Role" AS ENUM ('User', 'Admin'); + +-- CreateTable +CREATE TABLE "public"."User" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "role" "public"."Role" NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Contest" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "startTime" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Contest_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."ContestToChallengeMapping" ( + "id" TEXT NOT NULL, + "contestId" TEXT NOT NULL, + "challengeId" TEXT NOT NULL, + "index" INTEGER NOT NULL, + + CONSTRAINT "ContestToChallengeMapping_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Challenge" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "notionDocId" TEXT NOT NULL, + "maxPoints" INTEGER NOT NULL, + + CONSTRAINT "Challenge_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Submission" ( + "id" TEXT NOT NULL, + "submission" TEXT NOT NULL, + "contestToChallengeMappingId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "points" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Submission_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Leaderboard" ( + "id" TEXT NOT NULL, + "contestId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "rank" INTEGER NOT NULL, + + CONSTRAINT "Leaderboard_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "public"."User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "ContestToChallengeMapping_contestId_challengeId_key" ON "public"."ContestToChallengeMapping"("contestId", "challengeId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Leaderboard_contestId_rank_key" ON "public"."Leaderboard"("contestId", "rank"); + +-- AddForeignKey +ALTER TABLE "public"."ContestToChallengeMapping" ADD CONSTRAINT "ContestToChallengeMapping_contestId_fkey" FOREIGN KEY ("contestId") REFERENCES "public"."Contest"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."ContestToChallengeMapping" ADD CONSTRAINT "ContestToChallengeMapping_challengeId_fkey" FOREIGN KEY ("challengeId") REFERENCES "public"."Challenge"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Submission" ADD CONSTRAINT "Submission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Submission" ADD CONSTRAINT "Submission_contestToChallengeMappingId_fkey" FOREIGN KEY ("contestToChallengeMappingId") REFERENCES "public"."ContestToChallengeMapping"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Leaderboard" ADD CONSTRAINT "Leaderboard_contestId_fkey" FOREIGN KEY ("contestId") REFERENCES "public"."Contest"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Leaderboard" ADD CONSTRAINT "Leaderboard_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/packages/db/prisma/migrations/migration_lock.toml b/packages/db/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/packages/db/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql"