Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a game data persistence system by creating a Mongoose schema and REST API endpoints for managing game save data. The implementation follows the existing patterns established by the users API but has several critical security vulnerabilities that must be addressed before merging.
Changes:
- Created
gameDataSchemawith fields for save management (saveId, saveVersion, userId, gameId, completedLevels, lastUpdated) - Implemented POST endpoint for creating new game save data
- Implemented GET and PATCH endpoints for retrieving and updating game data by saveId
- Updated dependencies (Chakra UI, Clerk, emotion packages, framer-motion)
- Removed .env.local.example file
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| src/database/gameDataSchema.ts | Defines Mongoose schema for game save data with unique saveId identifier |
| src/app/api/gameData/route.ts | POST endpoint for creating new game save records |
| src/app/api/gameData/[saveId]/route.ts | GET and PATCH endpoints for retrieving and updating game saves by ID |
| package.json | Dependency updates for Chakra UI, Clerk, and new UI packages |
| package-lock.json | Lock file updates corresponding to package.json changes |
| .env.local.example | Removed environment variable template file |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export async function GET(_req: Request, { params }: { params: { saveId: string } }) { | ||
| await connectDB(); | ||
|
|
||
| const data = await GameData.findOne({ saveId: params.saveId }).lean(); | ||
| if (!data) { | ||
| return NextResponse.json({ error: "Game data not found" }, { status: 404 }); | ||
| } | ||
| return NextResponse.json(data, { status: 200 }); |
There was a problem hiding this comment.
Missing authentication/authorization check. This endpoint allows anyone to read any game data by saveId without verifying the user's identity or ownership. You should use auth() from '@clerk/nextjs/server' to verify the authenticated user and check that they own the game data being accessed (compare authenticated userId with the userId field in the retrieved data).
| export async function PATCH(req: NextRequest, { params }: { params: { saveId: string } }) { | ||
| try { | ||
| await connectDB(); | ||
| const changes = await req.json(); | ||
| if (!changes || typeof changes !== "object" || Array.isArray(changes)) { | ||
| return NextResponse.json({ error: "Invalid patch" }, { status: 400 }); | ||
| } | ||
|
|
||
| const updated = await GameData.findOneAndUpdate( | ||
| { saveId: params.saveId }, | ||
| { $set: changes }, | ||
| { new: true, runValidators: true }, | ||
| ).lean(); | ||
| if (!updated) { | ||
| return NextResponse.json({ error: "Save not found" }, { status: 404 }); | ||
| } | ||
| return NextResponse.json(updated, { status: 200 }); |
There was a problem hiding this comment.
Missing authentication/authorization check. This endpoint allows anyone to update any game data by saveId without verifying the user's identity or ownership. You should use auth() from '@clerk/nextjs/server' to verify the authenticated user and check that they own the game data being modified (compare authenticated userId with the userId field in the existing data).
| gameId: { type: String, required: true }, | ||
| completedLevels: { type: [Number], default: [] }, | ||
| }); | ||
|
|
There was a problem hiding this comment.
Consider adding an index on saveId field. Since saveId is marked as unique and is used for queries in the API endpoints, adding an explicit index will improve query performance. Add: gameDataSchema.index({ saveId: 1 }); after the schema definition.
| gameDataSchema.index({ saveId: 1 }); |
| "@clerk/nextjs": "^6.37.3", | ||
| "@emotion/react": "^11.14.0", | ||
| "@emotion/styled": "^11.14.1", | ||
| "framer-motion": "^12.34.0", |
There was a problem hiding this comment.
Added dependencies (@emotion/styled, framer-motion) that are not used in this PR. These packages add significant bundle size but aren't referenced in any of the new files. If these dependencies are for future work, consider adding them in the PR where they're actually used to keep PRs focused and make it easier to track when dependencies were introduced and why.
| "framer-motion": "^12.34.0", |
| import mongoose, { Schema } from "mongoose"; | ||
|
|
||
| const gameDataSchema = new Schema({ | ||
| saveId: { type: String, required: true, unique: true }, |
There was a problem hiding this comment.
Missing validation for saveId format. The saveId field is used as a unique identifier but has no format validation. Consider adding validation (e.g., regex pattern, length constraints) to ensure saveIds follow a consistent format and prevent potential issues with malformed IDs.
| saveId: { type: String, required: true, unique: true }, | |
| saveId: { | |
| type: String, | |
| required: true, | |
| unique: true, | |
| minlength: 1, | |
| maxlength: 128, | |
| match: [/^[A-Za-z0-9_-]+$/, "Invalid saveId format"], | |
| }, |
| try { | ||
| await connectDB(); | ||
|
|
||
| const body = await req.json(); |
There was a problem hiding this comment.
Missing input validation for the request body. The endpoint should validate that required fields (saveId, saveVersion, userId, gameVersion, gameId) are present and have the correct types before attempting to create the document. This prevents unclear database validation errors from being exposed to the client.
| const body = await req.json(); | |
| const body = await req.json(); | |
| // Basic input validation for required fields before hitting the database | |
| if (body === null || typeof body !== "object" || Array.isArray(body)) { | |
| return NextResponse.json( | |
| { error: "Invalid request body: expected a JSON object." }, | |
| { status: 400 } | |
| ); | |
| } | |
| const { | |
| saveId, | |
| saveVersion, | |
| userId, | |
| gameVersion, | |
| gameId, | |
| } = body as Record<string, unknown>; | |
| const isNonEmptyString = (value: unknown) => | |
| typeof value === "string" && value.trim().length > 0; | |
| if ( | |
| !isNonEmptyString(saveId) || | |
| !isNonEmptyString(saveVersion) || | |
| !isNonEmptyString(userId) || | |
| !isNonEmptyString(gameVersion) || | |
| !isNonEmptyString(gameId) | |
| ) { | |
| return NextResponse.json( | |
| { | |
| error: | |
| "Invalid request body: required fields saveId, saveVersion, userId, gameVersion, and gameId must be non-empty strings.", | |
| }, | |
| { status: 400 } | |
| ); | |
| } |
| await connectDB(); | ||
|
|
||
| const data = await GameData.findOne({ saveId: params.saveId }).lean(); | ||
| if (!data) { | ||
| return NextResponse.json({ error: "Game data not found" }, { status: 404 }); | ||
| } | ||
| return NextResponse.json(data, { status: 200 }); |
There was a problem hiding this comment.
Missing error handling. The GET handler lacks a try-catch block to handle potential database connection errors or query failures. If the database query throws an exception, it will result in an unhandled error. Wrap the function body in a try-catch block and return an appropriate error response with status 500, consistent with other GET endpoints in the codebase.
| await connectDB(); | |
| const data = await GameData.findOne({ saveId: params.saveId }).lean(); | |
| if (!data) { | |
| return NextResponse.json({ error: "Game data not found" }, { status: 404 }); | |
| } | |
| return NextResponse.json(data, { status: 200 }); | |
| try { | |
| await connectDB(); | |
| const data = await GameData.findOne({ saveId: params.saveId }).lean(); | |
| if (!data) { | |
| return NextResponse.json({ error: "Game data not found" }, { status: 404 }); | |
| } | |
| return NextResponse.json(data, { status: 200 }); | |
| } catch (err: any) { | |
| console.error("Server error", err); | |
| return NextResponse.json({ error: err.message }, { status: 500 }); | |
| } |
| const updated = await GameData.findOneAndUpdate( | ||
| { saveId: params.saveId }, | ||
| { $set: changes }, | ||
| { new: true, runValidators: true }, |
There was a problem hiding this comment.
Security vulnerability: Users can modify protected fields. The PATCH endpoint allows modification of any field including userId, saveId, and other critical identifiers. You should either whitelist which fields can be updated (e.g., only completedLevels, lastUpdated), or blacklist critical fields that should never be modified by users (userId, saveId, gameId).
| completedLevels: { type: [Number], default: [] }, | ||
| }); | ||
|
|
||
| const GameData = mongoose.models.GameData || mongoose.model("GameData", gameDataSchema); |
There was a problem hiding this comment.
Schema should specify a collection name. Following the pattern in userSchema.ts which explicitly specifies the collection name as the third argument to mongoose.model(), this schema should also specify the collection name (e.g., "gamedata" or "gamesaves") to ensure it maps to the correct collection in MongoDB Atlas.
| const GameData = mongoose.models.GameData || mongoose.model("GameData", gameDataSchema); | |
| const GameData = mongoose.models.GameData || mongoose.model("GameData", gameDataSchema, "gamedata"); |
| gameVersion: { type: String, required: true }, | ||
| gameId: { type: String, required: true }, | ||
| completedLevels: { type: [Number], default: [] }, | ||
| }); |
There was a problem hiding this comment.
Consider adding timestamps option to schema. Adding { timestamps: true } as the second argument to the Schema constructor would automatically create createdAt and updatedAt fields, which is useful for tracking when game data was created versus when it was last modified. This is distinct from the lastUpdated field which appears to track game progress updates.
| }); | |
| }, { timestamps: true }); |
emi-a-dinh
left a comment
There was a problem hiding this comment.
Looks good, all api endpoints work.
Developer: VINCENT LE
Closes #K-24
Pull Request Summary
Created the gameDataSchema alongside endpoint routes for the api, tested it in Postman and verified it works.
Modifications
/database/gameDataSchema.ts. - created
/api/gameData/[saveId]/route.ts - created
/api/gameData/route.ts - created
Testing Considerations
POST request - Requires "lastUpdated" otherwise error when testing with Postman
Pull Request Checklist
Screenshots/Screencast