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://sjsu.cs30.app (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 the api_token query param on the OAuth redirect (see Login flow below). Endpoints that require it declare @RequestHeader("Authorization", required = false) and resolve identity server-side via StudentIdentityService.resolve(authHeader) — a missing or invalid header returns 401 Unauthorized with an empty body unless noted otherwise. No endpoint trusts an identity value sent by the client (a studentEmail field 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.
  • Students and TAs: the student-facing endpoints serve two kinds of member, resolved per request by CourseAccessService: a student enrolled in a course (course_students) and the course’s TA (Course.taEmail). A student is held to the lab window (ScheduledLab.isActive); the TA is not — they may open, run, submit and autosave against any lab of their course at any time, so they can try a lab exactly as a student would. TA work is saved under the TA’s own email and is never synced to Canvas. Where an endpoint below says “the student”, read “the student or the course’s TA”. The environment gates (IpWhitelistFilter, KioskGateFilter) still apply to the TA — practice happens from a lab workstation, not from home.
  • Content type: JSON request/response bodies unless noted (GET .../assets/** returns raw file bytes with a probed Content-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=. When the account is the TA of a course, &role=ta is added — informational only (the client uses it to label practice mode); every server-side decision re-derives the role from Course.taEmail on each request. On failure, redirects with ?error=<code> instead:

error value Meaning
no_code Google didn’t return a code param.
not_enrolled The Google account is neither enrolled in nor the TA of any course (CourseAccessService.coursesFor).
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. For the course’s TA: every lab of the course, regardless of window. 404 if neither enrolled in nor the TA of 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 neither enrolled nor the TA.

GET /api/labs/{courseId}/lab/{labNumber}/remaining

Response 200: { "remainingMs": 1234 } — milliseconds until endDateTime, clamped to ≥ 0. For the course’s TA, remainingMs is null: they are not held to the window, so there is no countdown to show (the editor hides the timer chip instead of reading a past lab as “Time’s up”). 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 — for the course’s TA, the problems of every lab of the course. 404 if neither enrolled in nor the TA of 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 (membership/section/lab-window checks happen in ProblemService.getProblemContent, via CourseAccessService; the TA is not held to the lab window).

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 caller is neither enrolled in that course nor its TA → 403 if the lab isn’t currently active (lab.isActive; the TA is never held to this) → 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 a caller who is neither enrolled nor the TA).


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 — no HTTP surface

There is no /api/courses endpoint. CourseController was deleted in commit 8fb7d40 (“remove dead code”); course management has no HTTP API at all.

Course CRUD happens only through :cli, which calls CourseService and CourseRepository in-process — the CLI and backend ship in the same jar, so addcourse, addlab, addstudent and the rest reach the database directly rather than over HTTP. Reaching the server’s port grants no course-management access.

One exception: addproblem now uploads a problem ZIP to POST /api/ta/problems/upload (documented under TA flows) rather than writing to the problem git repo directly. The server handles git on the caller’s behalf.

The Canvas commands (course2canvas, submissions2canvas) are the other exception: they read a lab’s plan and its students’ best submissions through GET /api/admin/canvas/lab and GET /api/admin/canvas/lab/submissions (documented under Admin), authenticated with the CLI token, so they can run off the server; submissions2canvas also settles a course fragment through GET /api/admin/canvas/courses. All three are read-only.

Earlier versions of this document described an unauthenticated /api/courses CRUD surface and warned that network exposure was all that protected it. That surface no longer exists, so that warning no longer applies. The Course model itself is unchanged (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 }.


Health

GET /health

HealthController. No auth. Returns {"status":"ok"}. Liveness only — it does not check the database, the judge, or Docker.


Judge capacity

GET /api/code/queue-status

CodeController. Requires a student Bearer token. Returns the judge’s current load snapshot: { inFlight, maxQueueSize, maxWorkers }.

Authenticated deliberately: the data itself is not sensitive, but leaving it open would be an inconsistent gap in an otherwise fully-authenticated API and would hand out the judge’s exact capacity thresholds to anyone.


TA flows

A separate identity track from students. TA routes resolve through TaIdentityService, not StudentIdentityService, and carry their own OAuth round-trip and token. A student token is not valid on a TA route and vice versa. Every route below takes Authorization: Bearer <ta-token> and returns 401 when it is missing or invalid.

The TA in the student app. To do a lab (as opposed to monitoring one), the TA does not use these routes at all: they log into the student app through the ordinary GET /login, get an ordinary login_sessions token, and use the student endpoints above. What differs is only what those endpoints let them do — every lab of their course, at any time (see Students and TAs under Conventions). The student-app login is subject to the same one-active-session rule as a student’s, independently of the TA dashboard session, so the dashboard and the student app can be open at once. The TA’s own student-app session never appears in the dashboard’s student lists, which are built from course.students.

GET /ta/login

TaOAuthController. Browser redirect (302) to Google, the TA counterpart of GET /login. Uses google.ta-redirect-uri, which defaults to google.redirect-uri with /callback swapped for /ta/callback.

GET /ta/callback

Completes the TA OAuth round-trip and issues a TA Bearer token.

POST /api/ta/logout

Ends the TA session.

GET /api/ta/check-session

TA counterpart of POST /api/check-session — confirms the token is still valid and refreshes it.

GET /api/ta/sections

Course sections this TA is assigned to.

GET /api/ta/labs

Labs visible to this TA, across their sections.

GET /api/ta/labs/{labId}/students

Enrolled students for one lab, with per-student status.

GET /api/ta/labs/{labId}/health

Health report for one lab — the TA-scoped equivalent of /api/admin/lab-health.

GET /api/ta/stats

Aggregate counts across the TA’s sections.

GET /api/ta/sessions

Currently-active student sessions (login_sessions rows whose loggedOutAt is null).

DELETE /api/ta/sessions/{token}

Force-ends one student’s session, identified by its token (the login_sessions primary key). This is the “kick a student” control — use it when a student is stuck logged in on a device they no longer hold.

GET /api/ta/activity/{courseId}/{studentEmail}

One student’s lockdown/activity log. Optional ?sinceMs=<epoch-millis> (default 0) returns only events after that instant.

Note this is the one TA route that takes a student email in the path. That is a lookup target, not an identity claim — the caller’s own identity still comes from the TA token, and access is bounded by the TA’s course assignment.

POST /api/ta/problems/upload

TaProblemController. Upload a problem ZIP into the server’s problem pool git repo. Content type: multipart/form-data. The TA must own the target course (same ownership check as every other TA route).

Form parts:

Part Type Description
file file The problem ZIP. Must contain exactly one top-level directory (the problem name).
courseCode string Course code, e.g. CS-200
year int Course year
semester string e.g. Fall

Success 200: { "success": true, "problemName": "<name>" }

Error responses: 400 (empty or corrupt ZIP, blank problemGitRepo, ZIP with ≠ 1 top-level dir), 401 (bad/expired token), 403 (course not owned by this TA), 404 (course not found), 500 (Docker conversion failed, bt upgrade failed, git commit failed).

The server extracts the ZIP, runs problem2html via Docker to render the statement to HTML, optionally runs bt upgrade if bapctools is installed (non-fatal if absent — see bt.path in the configuration reference), and commits to the course’s problemGitRepo git repo.


Admin

GET /api/admin/lab-health

LabHealthController. Query params courseId (string) and labNumber (int). Requires a TA Bearer token and additionally checks that the TA is assigned to that course — a valid token for a course the TA does not own returns 403 Forbidden. Returns a LabHealthReport.

Despite the /api/admin base path this is authenticated with the TA dashboard’s browser session, not the admin dashboard session AdminController uses, and not the CLI token the two endpoints below use.

GET /api/admin/canvas/courses

CanvasSyncController. The cs30 courses a query fits, as an array, so submissions2canvas can settle a fragment and then ask for its lab exactly. Query params, all optional: code (matched case-insensitively as a substring of the course code, an exact code winning outright), year (int, exact), semester (substring), section (int, exact), and active=true to keep only courses that have not ended — the same set the other commands suggest when a lookup misses. No params at all is every course the caller may see.

Auth: the CLI token, as for /lab below. The search only ever covers what the token may read — every course for an ADMIN token, only the sections the TA is assigned to for a TA token — so a TA is never shown another course. PROFESSOR tokens are refused (403).

Success 200, sorted by code, year, semester, section:

[ { "code": "CS30", "year": 2026, "semester": "Spring", "section": 1 } ]

An empty array when nothing fits. Whether none or several is a problem is the CLI’s call, so this never 404s; it is the CLI that lists the candidates or the active courses. Errors: 401/403 as above.

GET /api/admin/canvas/lab

CanvasSyncController. The lab as the CLI’s course2canvas and submissions2canvas need it: window, problems, roster. Query params: code, year (int), semester, section (int), lab (int).

Auth: the CLI token (Authorization: Bearer <cli token>, resolved by CliTokenService), not a browser session. An ADMIN token may read any course; a TA token only the section it is assigned to (Course.taEmail) — any other section, including one that does not exist, is 403, so nothing about other courses is revealed. PROFESSOR tokens are refused (403); nothing issues one today.

Success 200:

{ "courseCode": "CS30", "section": 1, "labNumber": 1,
  "startDateTime": "2026-02-10T10:00:00", "endDateTime": "2026-02-10T11:15:00",
  "problems": [ { "name": "babyshark", "note": "Bonus problems" } ],
  "studentEmails": [ "a@sjsu.edu" ] }

Times are the stored UTC wall-clock values. The server’s repo path is deliberately not included.

Errors: 401 {"error": "Valid CLI token required"}; 403 {"error": "..."} as above; 404 {"error": "Course not found: ..."} or {"error": "Lab 9 not found in ..."}.

GET /api/admin/canvas/lab/submissions

CanvasSyncController. Every enrolled student’s best submission for one problem of the lab, read from the student repo on the server. Query params: the five above plus problem. Same auth as /lab.

Success 200: an array with one entry per student that has a best submission (the others are simply absent):

[ { "email": "a@sjsu.edu",
    "submission": { "highestPassed": 7, "total": 10, "fileName": "submission-2026-07-27T21-39-23.py",
                    "code": "...", "submittedAt": "2026-07-27T21-39-23" } } ]

404 when the course or lab is missing, or when problem is not one of the lab’s problems — checked before any file is read, since the name would otherwise become part of a path.

Both are exempt from the IP allowlist and the kiosk gate by default (/api/admin/ is in both exempt lists), which is what makes them usable from off campus; an operator who narrows cs30.allowed-ips.exempt-paths gates them too.