Skip to content

Latest commit

 

History

History
296 lines (248 loc) · 13.4 KB

api.md

File metadata and controls

296 lines (248 loc) · 13.4 KB

API reference

Base path: /api/v1. JSON in and out (Content-Type: application/json). The browser reaches the API on its own origin (/api), so cookies are first-party and no CORS is involved. Interactive documentation (Swagger UI) is served at /api/docs when SWAGGER_ENABLED=true (local default; off in production).

Conventions

  • Authentication: server-side session in the HttpOnly cookie CONTESTER_SESSION (PostgreSQL-backed Spring Session). Sessions expire after 7 days of inactivity and can be revoked.

  • CSRF: every state-changing request (POST, PUT, PATCH, DELETE) must send the header X-XSRF-TOKEN with the value of the XSRF-TOKEN cookie. GET /auth/csrf sets the cookie if it is missing. Missing or wrong token: 403.

  • Roles: USER and ADMIN. Everything is deny-by-default; the table below lists who may call each endpoint. "user" = any logged-in account, "public" = no session needed.

  • Pagination: ?page=0&size=20 (size 1–100). Paged responses:

    { "items": [], "page": 0, "size": 20, "totalItems": 0, "totalPages": 0 }
  • Timestamps: ISO-8601 UTC strings (2026-11-10T14:00:00Z).

  • Errors: RFC 9457 problem details, always with a request ID:

    {
      "type": "about:blank",
      "title": "Bad Request",
      "status": 400,
      "detail": "Validation failed",
      "instance": "/api/v1/auth/register",
      "requestId": "6f1c…",
      "errors": [{ "field": "username", "message": "must match ^[A-Za-z0-9_]{3,20}$" }]
    }

    401 no or expired session · 403 authenticated but not allowed, or bad CSRF token · 404 not found or not visible to the caller (other users' submissions, unpublished contests, problems before a contest starts) · 409 conflict (duplicate email/username, idempotency mismatch) · 422 rule violation (e.g. submitting to a contest that has not started) · 429 rate limited (Retry-After header) · 503 judge unavailable.

  • Request IDs: every response carries X-Request-Id; send your own to correlate.

Shared shapes

type Role = 'USER' | 'ADMIN'
type Language = 'python' | 'javascript' | 'java' | 'cpp'
type Difficulty = 'EASY' | 'MEDIUM' | 'HARD'
type ContestStatus = 'UPCOMING' | 'ONGOING' | 'ENDED'
type SubmissionStatus =
  | 'QUEUED' | 'RUNNING'                                   // pending
  | 'ACCEPTED' | 'WRONG_ANSWER' | 'TIME_LIMIT_EXCEEDED'
  | 'MEMORY_LIMIT_EXCEEDED' | 'RUNTIME_ERROR' | 'COMPILATION_ERROR'
  | 'INTERNAL_ERROR'                                       // judge failure, safe to retry

interface CurrentUser {
  id: number; email: string; username: string; role: Role
  firstName: string | null; lastName: string | null; bio: string | null; country: string | null
  emailVerified: boolean; createdAt: string
}

interface PublicProfile {
  username: string; firstName: string | null; lastName: string | null
  bio: string | null; country: string | null; createdAt: string
  stats: { points: number; solved: number; contests: number; submissions: number; acceptanceRate: number | null; rank: number | null }
}

interface ContestSummary {
  id: number; title: string; startsAt: string; endsAt: string; status: ContestStatus
  problemCount: number; participantCount: number; registered: boolean
}

interface ProblemSummary {
  id: number; label: string            // "A", "B", ...
  title: string; points: number; difficulty: Difficulty
  solved: boolean; attempted: boolean  // for the caller
}

interface ContestDetail extends ContestSummary {
  description: string                  // Markdown
  problems: ProblemSummary[]           // [] until the contest starts (admins always see them)
}

interface ProblemDetail {
  id: number; contestId: number; contestTitle: string; contestStatus: ContestStatus
  label: string; title: string; points: number; difficulty: Difficulty
  statement: string; inputSpec: string; outputSpec: string   // Markdown
  timeLimitMs: number; memoryLimitMb: number
  samples: { input: string; output: string }[]
  solved: boolean
}

interface TestResult {
  position: number; sample: boolean; verdict: SubmissionStatus; timeMs: number | null
  // only for sample tests (hidden test data is never returned):
  input?: string; expectedOutput?: string; actualOutput?: string
}

interface Submission {
  id: number; problemId: number; problemLabel: string; problemTitle: string
  contestId: number; username: string
  language: Language; status: SubmissionStatus
  passedCount: number; totalCount: number
  failedTest: number | null            // 1-based position of the first failing test
  timeMs: number | null                // max over executed tests
  compileOutput: string | null         // compiler message for COMPILATION_ERROR
  inContest: boolean                   // submitted while the contest was running
  createdAt: string; judgedAt: string | null
  sourceCode?: string                  // only in GET /submissions/{id}
  results?: TestResult[]               // only in GET /submissions/{id}
}

interface Comment {
  id: number; author: { username: string }; body: string
  createdAt: string; updatedAt: string; canDelete: boolean
  replies: Comment[]                   // one level deep
}

Endpoints

Auth

Method Path Who Body Success Notes
GET /auth/csrf public – 204 Sets XSRF-TOKEN cookie
POST /auth/register public {email, username, password} 201 CurrentUser Logs in; sends verification e-mail. 409 duplicate
POST /auth/login public {email, password} 200 CurrentUser 401 wrong credentials. Rate limited
POST /auth/logout user – 204 Invalidates the session
POST /auth/logout-all user – 204 Invalidates every session of the user
GET /auth/me user – 200 CurrentUser 401 if not logged in
POST /auth/verify-email public {token} 204 Single-use token, 24 h
POST /auth/verify-email/resend user – 204
POST /auth/password/forgot public {email} 202 Always 202 (no account enumeration)
POST /auth/password/reset public {token, password} 204 Single-use, 1 h; revokes all sessions

Rules: e-mail ≤ 254 chars; username ^[A-Za-z0-9_]{3,20}$ (case-insensitive unique); password 8–128 chars with a letter and a digit.

Account (current user)

Method Path Who Body Success
PATCH /me user {username?, firstName?, lastName?, bio?, country?} 200 CurrentUser
POST /me/password user {currentPassword, newPassword} 204 (other sessions revoked)
DELETE /me user {password} 204 (account, submissions, comments deleted)

firstName/lastName ≤ 50, bio ≤ 500, country ISO 3166-1 alpha-2 or null.

Users and leaderboard

Method Path Who Success
GET /users/{username} public 200 PublicProfile
GET /users/{username}/activity?limit=10 public 200 [{problemId, problemTitle, contestId, contestTitle, solvedAt}] (limit 1–50)
GET /leaderboard?page&size&country public 200 Page<{rank, username, country, points, solved}>

Global points = sum of points of every distinct problem the user has solved (in or after a contest). Ties share a rank.

Contests

Method Path Who Success Notes
GET /contests?status&q&page&size public 200 Page<ContestSummary> status = UPCOMING/ONGOING/ENDED; upcoming sorted soonest first, others newest first. Published only
GET /contests/{id} public 200 ContestDetail
PUT /contests/{id}/registration user 200 ContestSummary Idempotent. 422 after the contest ended
DELETE /contests/{id}/registration user 204 Only before the start (422 otherwise)
GET /contests/{id}/standings?page&size public 200 Standings 422 before the contest starts
interface Standings {
  problems: { id: number; label: string; points: number }[]
  rows: Page<{
    rank: number; username: string; country: string | null
    points: number; penaltyMinutes: number
    problems: { problemId: number; solved: boolean; attempts: number; solvedAtMinute: number | null }[]
  }>
}

Scoring: only submissions made while the contest runs count. A problem scores its points on the first accepted submission. Penalty = minutes from the contest start to each first acceptance plus 10 minutes per rejected attempt before it (compilation errors and judge errors are not counted). Rank by points desc, then penalty asc; equal pairs share a rank. Registered participants without submissions appear with 0 points.

Problems, running and submitting

Method Path Who Body Success Notes
GET /problems/{id} public – 200 ProblemDetail 404 before the contest starts (admins excepted)
POST /problems/{id}/run user {language, sourceCode, input} 200 RunResult Runs once on custom input. 10/min per user. 503 when the judge is unreachable
POST /problems/{id}/submissions user {language, sourceCode, clientRequestId} 202 Submission Queued for judging. Replay with the same clientRequestId returns 200 with the original; a different payload returns 409. 6/min per user. While the contest runs the user is registered automatically
GET /submissions/{id} owner, admin – 200 Submission (with sourceCode, results) Poll until status is final
GET /submissions?problemId&contestId&page&size user – 200 Page<Submission> The caller's own submissions, newest first
interface RunResult {
  status: 'OK' | 'COMPILATION_ERROR' | 'RUNTIME_ERROR' | 'TIME_LIMIT_EXCEEDED' | 'MEMORY_LIMIT_EXCEEDED'
  stdout: string; stderr: string; compileOutput: string | null
  exitCode: number | null; timeMs: number | null
}

sourceCode ≤ 64 KB, input ≤ 64 KB. Output is compared line by line after trimming trailing whitespace on each line and trailing blank lines. Judging stops at the first failing test. Time limits apply to CPU time; Java gets 2 × limit + 3 s because Piston compiles Java inside the run stage (judge.md). If the judge stays unavailable for three attempts, the submission ends as INTERNAL_ERROR.

Languages

Method Path Who Success
GET /languages public 200 [{id: Language, name, version, monaco, template}]

monaco is the editor's language mode and template the starter code shown in the editor.

Discussions

Method Path Who Body Success
GET /problems/{id}/comments?page&size public – 200 Page<Comment> (top-level, newest first, replies oldest first)
POST /problems/{id}/comments user (verified e-mail) {body, parentId?} 201 Comment
DELETE /comments/{id} author, admin – 204 (replies are deleted too)

body 1–2000 chars; replies to replies are rejected (422). 5 comments per minute per user.

Administration (ADMIN only)

Method Path Body Success
GET /admin/contests?page&size – 200 Page<AdminContest> (includes unpublished)
POST /admin/contests ContestInput 201 AdminContest
GET /admin/contests/{id} – 200 AdminContest
PUT /admin/contests/{id} ContestInput 200 AdminContest
DELETE /admin/contests/{id} – 204 (problems, tests, submissions deleted)
GET /admin/contests/{id}/problems – 200 AdminProblem[]
POST /admin/contests/{id}/problems ProblemInput 201 AdminProblem
GET /admin/problems/{id} – 200 AdminProblem
PUT /admin/problems/{id} ProblemInput 200 AdminProblem
DELETE /admin/problems/{id} – 204
GET /admin/problems/{id}/tests – 200 TestCase[]
PUT /admin/problems/{id}/tests TestCaseInput[] 200 TestCase[] (replaces all, atomically)
POST /admin/problems/{id}/rejudge – 202 {queued: number}
interface ContestInput {
  title: string                 // 3–120
  description: string           // Markdown, ≤ 10 000
  startsAt: string; endsAt: string   // endsAt > startsAt, duration ≤ 30 days
  published: boolean
}
interface AdminContest extends ContestInput {
  id: number; status: ContestStatus; problemCount: number; participantCount: number
  createdAt: string; updatedAt: string
}
interface ProblemInput {
  title: string                 // 3–120
  statement: string; inputSpec: string; outputSpec: string   // Markdown, ≤ 20 000 each
  timeLimitMs: number           // 250–10 000
  memoryLimitMb: number         // 32–512
  points: number                // 1–10 000
  difficulty: Difficulty
  position: number              // 1-based order in the contest; label = A, B, ...
}
interface AdminProblem extends ProblemInput {
  id: number; contestId: number; label: string; testCount: number; sampleCount: number
  createdAt: string; updatedAt: string
}
interface TestCaseInput { input: string; expectedOutput: string; sample: boolean }   // ≤ 1 MB each, 1–100 tests
interface TestCase extends TestCaseInput { id: number; position: number }

Health and metrics

Path Purpose
/actuator/health/liveness Process is up
/actuator/health/readiness Database reachable
/actuator/health Aggregate including the judge (Piston)
/actuator/prometheus Metrics (internal network only; not routed publicly)