Base URL: http://localhost:8081
Authentication: Most endpoints require Authorization: Bearer <JWT>
WebSocket endpoint: /ws (SockJS + STOMP)
This document is intended for frontend integration.
The backend issues a JWT containing:
sub= user idrole=USER- expiry = 24 hours
The JWT filter reads the Authorization header, extracts the user id, and places it into the request as userId. Frontend code should send the token on every protected request.
The Google OAuth success callback is:
GET /auth/oauth-success
This endpoint is handled by the backend after Google login and redirects to:
http://localhost:3000/oauth-success?token=...&userId=...&email=...
The frontend route /oauth-success should read those query parameters and store the token.
Runtime exceptions are converted to HTTP 400:
{
"error": "Room not found"
}Unexpected exceptions are converted to HTTP 500:
{
"error": "Something went wrong"
}Common runtime messages used by this backend include:
User not authenticated via OAuth2Invalid tokenRoom not foundContest not activeUser not in roomInvalid problemWrong PasswordRoom is fullUser already in this roomUser already in another active roomOnly host can start the contestOnly host can end the contestRoom already started or finishedNeed exactly 2 players to startNot enough problems in DBToo many submissions. Please slow down.
Creates a JWT for a user id.
Auth: No
Request: Query parameter
POST /auth/login?userId=1Response:
{
"token": "eyJhbGciOiJIUzI1NiJ9..."
}Notes:
- This endpoint does not validate the user id against the database.
- It only generates a token for the supplied id.
OAuth2 success callback. The backend uses the authenticated Google profile, stores/updates the user, generates a JWT, and redirects to the frontend.
Auth: Handled by OAuth session
Request: No body
Redirect example:
http://localhost:3000/oauth-success?token=JWT_HERE&userId=12&email=user@example.com
Notes:
- If the user already exists by email, only the name is updated.
- New users are saved with provider =
GOOGLE.
All endpoints in this section require a valid JWT unless noted otherwise.
Creates a new room and adds the host as the first participant.
Auth: Yes
Headers:
Authorization: Bearer <JWT>Request: Query parameter
POST /rooms/create?password=1234password is optional. If omitted or blank, the backend generates a 4-digit password.
Response: Room object
{
"id": 101,
"hostUserId": 1,
"password": "1234",
"status": "WAITING",
"startTime": null,
"endTime": null,
"duration": null
}Behavior:
- Creates room with status
WAITING - Creates a
Participantrow for the host with score0
Joins an existing room.
Auth: Yes
Headers:
Authorization: Bearer <JWT>Request: Query parameters
POST /rooms/join?roomId=101&password=1234Response: plain string
Joined successfully
Other possible responses:
Wrong Password
Room is full
User already in this room
User already in another active room
Behavior:
- Room must exist
- Password must match exactly
- Room must have fewer than 2 participants
- A user cannot join multiple
WAITINGorACTIVErooms at the same time
Starts the contest for a room.
Auth: Yes
Headers:
Authorization: Bearer <JWT>Request: Query parameter
POST /rooms/start?roomId=101Response: plain string
Contest started
Behavior:
- Only the host can start the contest
- Room must be in
WAITINGstatus - Exactly 2 participants must be present
- The backend assigns the first 3 problems from the
problemstable to the room - For each participant, it creates
ParticipantProblementries for those assigned problems - Room status becomes
ACTIVE startTimeis set to current timedurationis set to10 minutes
Failure examples:
Only host can start the contest
Room already started or finished
Need exactly 2 players to start
Not enough problems in DB
Submits code for a room problem. The submission is stored first and then sent to RabbitMQ for judging.
Auth: Yes
Headers:
Authorization: Bearer <JWT>Request body: SubmissionRequest
{
"roomId": 101,
"problemId": 11,
"code": "public class Main { ... }",
"languageId": 54
}Important:
The backend fills userId from the JWT, so the frontend does not need to send userId.
Response:
{
"status": "PENDING"
}Behavior:
- Submission is rate limited to 5 submissions per 10 seconds per user
- Room must be
ACTIVE - User must be in the room
- Problem must belong to the room
- A
Submissionrow is created with statusPENDING - Submission id is sent to RabbitMQ queue
judge-queue
Failure examples:
{ "error": "Too many submissions. Please slow down." }Returns the computed room leaderboard.
Auth: Yes
Headers:
Authorization: Bearer <JWT>Response: array of LeaderboardResponse
[
{
"userId": 1,
"solved": 2,
"penalty": 1240,
"lastSolvedTime": 1712001200000,
"rank": 1
},
{
"userId": 2,
"solved": 1,
"penalty": 1760,
"lastSolvedTime": 1712001800000,
"rank": 2
}
]Sorting rules:
- Higher
solvedfirst - Lower
penaltyfirst - Earlier
lastSolvedTimefirst
Penalty formula:
penalty = attemptsPenalty * 600 + timeTakenSecondstimeTakenSeconds = (solvedAt - roomStartTime) / 1000
Ends the contest manually.
Auth: Yes
Headers:
Authorization: Bearer <JWT>Request: Query parameter
POST /rooms/end?roomId=101Response: EndContestResponse
{
"winnerUserId": 1,
"result": "WIN"
}Possible result values:
WINDRAW
Behavior:
- Only the host can end the contest
- Room must be
ACTIVE - Backend computes leaderboard and determines winner
- Room status becomes
FINISHED endTimeis set- A
ContestHistoryentry is stored for each participant
If the contest is tied:
{
"winnerUserId": null,
"result": "DRAW"
}Returns contest statistics for the current user.
Auth: Yes
Headers:
Authorization: Bearer <JWT>Response: map
{
"totalContests": 8,
"wins": 5,
"losses": 2,
"draws": 1,
"totalSolved": 14,
"winRate": 62.5
}Notes:
- Uses the authenticated user id from the JWT
- Reads data from
contest_history
Returns the current user profile summary plus recent contests.
Auth: Yes
Headers:
Authorization: Bearer <JWT>Response:
{
"stats": {
"totalContests": 8,
"wins": 5,
"losses": 2,
"draws": 1,
"totalSolved": 14,
"winRate": 62.5
},
"recentMatches": [
{
"id": 41,
"userId": 1,
"roomId": 101,
"solved": 2,
"penalty": 1240,
"result": "WIN",
"timestamp": 1712002000000
},
{
"id": 40,
"userId": 1,
"roomId": 99,
"solved": 1,
"penalty": 1760,
"result": "LOSS",
"timestamp": 1711999000000
}
]
}Notes:
recentMatchesis the latest 5 records fromcontest_history- Order is newest first
The frontend should connect using SockJS/STOMP to:
/ws
Subscribe to:
/topic/room/{roomId}
Example:
/topic/room/101
The backend pushes a RoomStateResponse object.
{
"roomId": 101,
"leaderboard": [
{
"userId": 1,
"solved": 2,
"penalty": 1240,
"lastSolvedTime": 1712001200000,
"rank": 1
},
{
"userId": 2,
"solved": 1,
"penalty": 1760,
"lastSolvedTime": 1712001800000,
"rank": 2
}
],
"myProblems": [
{
"id": 501,
"userId": 1,
"roomId": 101,
"problemId": 11,
"attempts": 1,
"penalty": 0,
"solved": true,
"solvedAt": 1712001200000
}
],
"opponentProblems": [
{
"id": 502,
"userId": 2,
"roomId": 101,
"problemId": 11,
"attempts": 0,
"penalty": 0,
"solved": false,
"solvedAt": null
}
]
}Important frontend note:
The backend names these fields myProblems and opponentProblems, but it currently fills them based on internal user ordering in the room map. The payload does not explicitly identify which array belongs to the authenticated user. Frontend code should not assume the first list always belongs to the current viewer unless it verifies the user id.
Returned by POST /rooms/create
{
"id": 101,
"hostUserId": 1,
"password": "1234",
"status": "WAITING",
"startTime": null,
"endTime": null,
"duration": null
}{
"userId": 1,
"solved": 2,
"penalty": 1240,
"lastSolvedTime": 1712001200000,
"rank": 1
}{
"winnerUserId": 1,
"result": "WIN"
}{
"id": 41,
"userId": 1,
"roomId": 101,
"solved": 2,
"penalty": 1240,
"result": "WIN",
"timestamp": 1712002000000
}{
"roomId": 101,
"problemId": 11,
"code": "public class Main { ... }",
"languageId": 54
}{
"roomId": 101,
"leaderboard": [],
"myProblems": [],
"opponentProblems": []
}For protected requests:
Authorization: Bearer <JWT>
Content-Type: application/json- User signs in through Google OAuth or receives a manual token from
/auth/login - Frontend stores JWT
- Frontend calls
/rooms/createor/rooms/join - When room reaches 2 participants, host calls
/rooms/start - Frontend subscribes to
/wsand listens on/topic/room/{roomId} - Users submit code through
/rooms/submit - UI updates from websocket messages and leaderboard polling if needed
- Host ends contest with
/rooms/endor backend auto-ends it after 10 minutes
- Use WebSocket for live leaderboard and problem state updates
- Use
/rooms/leaderboardonly if you need a manual refresh fallback
| Method | Path | Auth | Response type |
|---|---|---|---|
| POST | /auth/login |
No | JSON token |
| GET | /auth/oauth-success |
OAuth session | Redirect |
| POST | /rooms/create |
Yes | Room JSON |
| POST | /rooms/join |
Yes | Plain string |
| POST | /rooms/start |
Yes | Plain string |
| POST | /rooms/submit |
Yes | { "status": "PENDING" } |
| GET | /rooms/leaderboard |
Yes | Array of leaderboard objects |
| POST | /rooms/end |
Yes | EndContestResponse |
| GET | /rooms/stats |
Yes | JSON map |
| GET | /rooms/profile |
Yes | JSON map |
| WS | /ws |
No | STOMP connection |
| Topic | /topic/room/{roomId} |
N/A | RoomStateResponse |
await fetch("http://localhost:8081/rooms/submit", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
},
body: JSON.stringify({
roomId: 101,
problemId: 11,
code: sourceCode,
languageId: 54,
}),
});await fetch("http://localhost:8081/rooms/join?roomId=101&password=1234", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
},
});stompClient.subscribe(`/topic/room/${roomId}`, (message) => {
const roomState = JSON.parse(message.body);
console.log(roomState);
});- Contest duration is hardcoded to 10 minutes
- Exactly 2 participants are required to start
- First 3 problems in the database are assigned to every contest
- Submission rate limit is 5 submissions per 10 seconds per user
- Auto-end scheduler checks active rooms every 5 seconds
- Judge execution is done through Judge0
- Results are broadcast to frontend through Redis → WebSocket