Backend API Reference
Reference documentation for every HTTP endpoint exposed by :backend (com.cs30.server.controller.*).
This is a dictionary, not a tutorial — for the auth design (why Bearer tokens, why the 2-minute TTL,
why login_sessions exists), see the login flow.
This document only covers what’s actually implemented and wired today — if an endpoint below stops
matching the code, trust the code.
Base URL in production: https://sjsu1.cs30.app:8443 (see cs30.backend.url in application.properties).
Locally: http://localhost:8080 unless server.port is overridden.
Conventions
- Auth header:
Authorization: Bearer <token>, where<token>comes from theapi_tokenquery param on the OAuth redirect (see Login flow below). Endpoints that require it declare@RequestHeader("Authorization", required = false)and resolve identity server-side viaStudentIdentityService.resolve(authHeader)— a missing or invalid header returns401 Unauthorizedwith an empty body unless noted otherwise. No endpoint trusts an identity value sent by the client (astudentEmailfield or path segment); where one is present in a request body, it’s either ignored or compared against the resolved identity and logged on mismatch. - Content type: JSON request/response bodies unless noted (
GET .../assets/**returns raw file bytes with a probedContent-Type). - Errors: endpoints generally return a 2xx/4xx status with either an empty body or a small JSON object; there’s no unified error envelope across the API.
Login flow
Handled by OAuthController. This is the only part of the API not called directly by the frontend as a
JSON API — /login and /callback are browser redirects (Google OAuth 2.0, hd=sjsu.edu restricted).
GET /login
Starts the OAuth round-trip. Redirects (302) to Google’s consent screen.
| Query param | Required | Purpose |
|---|---|---|
app_callback |
no | Desktop-only. If present, marks this as a desktop login (see below) and is echoed back as the redirect destination after /callback completes. |
state |
no | Opaque value round-tripped back to the caller, appended to the final redirect. |
GET /callback
Google redirects here with ?code=.... On success, redirects (302) to app_callback (desktop) or /
(web) with ?name=&email=&api_token=&state=. On failure, redirects with ?error=<code> instead:
error value |
Meaning |
|---|---|
no_code |
Google didn’t return a code param. |
not_enrolled |
The Google account isn’t enrolled in any course (CourseRepository.findByStudentEmail). |
session_exists |
The student already has an active session (see below) — not a bug, this is the one-active-session-at-a-time invariant working as intended. |
auth_failed |
The Google token/userinfo exchange threw. |
One active session per student. A new login is rejected with error=session_exists while the
student’s existing Bearer token is still within its 2-minute TTL and hasn’t been explicitly logged out.
The only legitimate ways to get a new session while an old one exists: log out first, or wait for the
old one’s TTL to expire (e.g. after a device crash). See the README section linked above for the full
rationale.
Desktop vs. web login is distinguished by whether app_callback was set on /login — this decides
the platform field recorded in login_sessions (ApiTokenStore holds no state of its own — see
the login flow), not a separate endpoint.
ApiTokenStore.generate(), called here, is what inserts the login_sessions row for this login —
required, not best-effort: if the insert fails, the login itself fails (error=auth_failed) rather than
handing out a token with no backing row.
POST /api/logout
Auth: Authorization: Bearer <token> (required, no fallback). Revokes the token — this runs a blocking
pre-logout hook first (records a LoggedOut activity event and commits the activity log to git); if that
fails, the request 500s and the session is left active rather than silently ending. Otherwise 200
with an empty body, regardless of whether the token was valid to begin with (an already-invalid/unknown
token is a no-op, not an error).
POST /api/web-logout
Auth: Authorization header, or ?token=<token> query param (web-only fallback — used by
navigator.sendBeacon on tab/window close, which can’t set custom headers). Same effect and same
failure mode as /api/logout.
POST /api/check-session
Auth: Authorization, optional. Heartbeat endpoint — both desktop and web call this every 60s to keep
the session’s TTL alive. This answers “is the client still connected,” not “is the student active” — the
heartbeat fires on a fixed interval regardless of mouse/keyboard activity, so an idle-but-open tab never
expires this way.
Response 200:
{ "hasActiveSession": true, "email": "jdoe@sjsu.edu" }
hasActiveSession is false and email is null if the token is missing/invalid/expired — this
endpoint never 401s, since the client polls it specifically to detect logout/expiry. If this call is
the one that detects TTL expiry, it runs the same pre-logout hook /api/logout does; on the rare failure
there, this 500s instead of returning its usual body.
Labs
LabController, base path /api/labs. All three require a valid Bearer token (401 if missing).
GET /api/labs/student
Currently-active labs (now between startDateTime/endDateTime) across every course the
authenticated student is enrolled in. 404 if not enrolled in any course.
Response 200: LabResponse[]
{ courseCode, courseId, section, year, semester, labNumber, startDateTime, endDateTime, problemGitRepo }
GET /api/labs/student/all
Same shape as above, but every lab (past/current/future), not just active ones. 404 if not enrolled.
GET /api/labs/{courseId}/lab/{labNumber}/remaining
Response 200: { "remainingMs": 1234 } — milliseconds until endDateTime, clamped to ≥ 0.
Returns remainingMs: 0 (not 404) if the course or lab number doesn’t exist.
Problems
ProblemController, base path /api/problems. All require a valid Bearer token.
GET /api/problems/lab
Problems for the authenticated student’s currently-active labs. 404 if not enrolled in any course;
200 with an empty list if enrolled but no lab is currently active.
Response 200: LabProblemInfo[] — { courseId, courseCode, section, labNumber, slug, title, language }
GET /api/problems/{courseId}/section/{section}/lab/{labNumber}/{slug}
HTML + CSS for one problem statement. 404 if the problem doesn’t exist or the student can’t access it
(enrollment/section/lab checks happen in ProblemService.getProblemContent).
Response 200: { "html": "...", "css": "..." }
GET /api/problems/{courseId}/section/{section}/lab/{labNumber}/{slug}/assets/**
Static asset (image, etc.) referenced by a problem statement’s HTML. The ** suffix is the
repo-relative asset path. Response is the raw file with a probed Content-Type
(application/octet-stream if unrecognized). 404 if the file doesn’t exist or access is denied.
Code execution & submission
CodeController, base path /api/code. All require a valid Bearer token; the studentEmail field in
each request body is accepted for backward compatibility but overridden with the resolved identity
(a mismatch is logged as [identity-mismatch], not rejected).
POST /api/code/run
Runs code against the judge without grading/persisting a submission — used for the editor’s “Run” / custom-input flows.
Request: RunCodeRequest — { courseId, section, labNumber, problemName, studentEmail, code, language?, customStdins: [] }
Response: RunCodeResponse — { success, message, testcases?, compileOutput? }. 200 if success,
else 400.
POST /api/code/submit
Grades code against the problem’s testcases and persists the submission (GitService.saveSubmissionWithResult).
Request: SubmitCodeRequest — { courseId, section, labNumber, problemName, studentEmail, code, language? }
Response: SubmitCodeResponse — { success, message, status?, passed?, total?, maxTimeS?, testcases?, compileOutput?, filePath? }.
status is one of AC, WA, TLE, RTE, MLE, CE. 200 if success, else 400; 401 (with
SubmitCodeResponse(false, "Unauthorized")) if the Bearer token doesn’t resolve.
GET /api/code/submissions
Query params: courseId, section, labNumber, problemName (all required).
Response 200: SubmissionInfo[] — { timestamp, passed, total, maxTimeMs?, status, filePath, code },
scoped to the authenticated student’s own submissions only (the resolved email, not a query param).
Autosave
AutosaveController, base path /api/autosave. This is a different mechanism from SaveType.AUTOSAVE
inside /api/code — it writes a single overwritten autosaved-solution.<ext> file per problem (recovery
snapshot, not history), authored as the student in git rather than the server identity.
POST /api/autosave
Request: AutosaveRequest — { courseId, section, labNumber, problemSlug, code, language }
Auth + validation order: 401 if no valid Bearer token → 404 if courseId doesn’t exist → 403 if
the student isn’t enrolled in that course → 403 if the lab isn’t currently active (lab.isActive) →
500 if the git write itself fails → 202 Accepted on success.
GET /api/autosave/{courseId}/{section}/{labNumber}/{problemSlug}
Returns the student’s last autosaved code for a problem, so the editor can repopulate it on reopen.
Response 200: raw code as a plain string body — "" if no autosave exists yet (never 404 for “not
found”, only for a genuinely missing course or 403 for non-enrollment).
Activity / lockdown logging
ActivityController, base path /api/activity. This is the current, live lockdown-event pipeline.
POST /api/activity/event
Query param: problem (optional, label only). Body: LockdownViolation — { kind, timestampMs, detail? }
(kind is one of the ViolationKind enum values in :data).
401 if no valid Bearer token, else 202 Accepted. Recorded as one CSV row per event under
section_{n}/ActivityLogs/{date}/{email}_{date}_activity.csv in the student’s course git repo
(GitService.appendActivityLog — not committed yet, see below).
POST /api/activity/commit
No body. Commits that day’s activity log CSV(s) to git (GitService.commitActivityLog), called when a
lockdown session ends. 401 if no valid Bearer token, else 202 Accepted.
Also invoked internally (not via this HTTP endpoint) by the pre-logout hook described under Login flow — every logout, explicit or TTL-triggered, commits the activity log the same way this endpoint does, and can fail the logout if that commit fails.
Courses — admin/CLI only, no authentication
CourseController, base path /api/courses. Plain CRUD over CourseRepository with no Bearer check
at all — every route is reachable by anyone who can reach the port. This is a deliberate scope
decision, not an oversight: course management is an instructor/CLI concern (:cli calls these routes
directly, e.g. addcourse/addstudent), never a student-facing flow, so it was excluded from the
student-identity security pass that hardened every other controller. This means network exposure is
the only thing standing between “anyone” and full course CRUD — never expose this port beyond the lab
network / a trusted admin path.
| Method | Path | Body | Response |
|---|---|---|---|
GET |
/api/courses |
— | Course[] |
GET |
/api/courses/{id} |
— | Course or 404 |
POST |
/api/courses |
Course |
Created Course |
PUT |
/api/courses/{id} |
Course |
Updated Course (id forced to path param) or 404 |
DELETE |
/api/courses/{id} |
— | 204 or 404 |
Course shape (backend/src/main/models/Course.kt): { id, code, section, year, semester, startDate,
endDate, language, studentGitRepo, problemGitRepo, students: string[], labs: ScheduledLab[] }, where
ScheduledLab is { id, labNumber, startDateTime, endDateTime, problems: Problem[] } and Problem is
{ id, name, language }.