Data flow
This page traces the main things a student does, request by request. Each flow is backed by the controllers and services described on the components page.
Login
Login is Google OAuth. The interesting parts are that the backend checks enrollment before issuing a token, refuses a second concurrent session, and hands the token back in the redirect URL.
sequenceDiagram
participant C as Client
participant O as OAuthController
participant G as Google
participant DB as login_sessions (DB)
C->>O: GET /login
O-->>C: 302 to Google
C->>G: authenticate
G-->>C: 302 /callback?code
C->>O: GET /callback?code
O->>G: exchange code, get user info
G-->>O: email, name
O->>DB: enrolled in, or TA of, any course?
alt not enrolled
O-->>C: 302 ...?error=not_enrolled
else already has an active session
O-->>C: 302 ...?error=session_exists
else ok
O->>DB: insert login_sessions row, get token
O-->>C: 302 ...?name&email&api_token
end
A few details worth knowing:
- The OAuth request pins
hd=sjsu.edu, so only SJSU accounts are offered. - Membership is checked with
CourseAccessService.coursesFor: enrolled as a student, or assigned as the course’s TA. An account that is neither is turned away witherror=not_enrolled. A TA gets&role=taon the redirect so the client can label practice mode; the server never reads it back. - One active session per student is enforced by
tokenStore.hasActiveSession. A second login while one is active getserror=session_exists. - Issuing the token is the
login_sessionsinsert itself. If that write fails, login fails, because a token with no row could never resolve to a user. - Web and desktop use the same flow. The desktop app passes an
app_callback(alocalhostURL) and astatevalue, and the backend redirects the token there instead of to/.
Opening a problem
Once logged in, the client asks for the problems in the currently active lab, then fetches a specific problem’s statement.
sequenceDiagram
participant C as Client
participant P as ProblemController
participant I as StudentIdentityService
participant S as ProblemService
participant R as Problem git repo
C->>P: GET /api/problems/lab (Bearer token)
P->>I: resolve identity from token
I-->>P: email
P->>S: list active-lab problems for this student
S-->>P: problem list
P-->>C: problems
C->>P: GET /api/problems/{course}/section/{s}/lab/{l}/{slug}
P->>S: access check + read statement
S->>R: read index.html, problem.css, assets
S-->>P: statement content
P-->>C: HTML + CSS
ProblemService enforces access on every read through CourseAccessService: the caller must be a member (enrolled student, or the course’s TA), in the right section, the lab must be active (the TA is exempt from this — they may do any lab at any time), and the problem must belong to that lab. It reads statement files from the problem git repo and guards against path traversal. Relative image URLs in the statement are rewritten to point at the asset endpoint.
Running and submitting code
run and submit share most of their path. The difference is what the judge grades against and whether the result is saved.
sequenceDiagram
participant C as Client
participant CC as CodeController
participant CS as CodeService
participant J as JudgeService
participant JS as Judge sandbox
participant G as GitService
participant R as Student git repo
C->>CC: POST /api/code/run or /submit (Bearer token)
CC->>CS: request (identity resolved from token)
CS->>CS: check enrollment + lab window
CS->>CS: claim per-student lock
CS->>J: run/submit (problem, pool path, language, source)
J->>JS: HTTP JSON to judge
JS-->>J: per-testcase verdicts
J-->>CS: result
alt submit
CS->>G: save code + result together
G->>R: write files, git commit
end
CS-->>CC: response
CC-->>C: test output or verdict
Things that matter here:
- Identity comes from the token. Any
studentEmailin the request body is ignored. CodeServiceholds a per-student lock (an atomic claim keyed by the resolved email) shared between run and submit, so a fast double-click cannot fire two judge runs at once.- The lab window is checked with
CourseAccessService.labDenialReason. If the lab has not started or has ended, a student’s request is rejected before reaching the judge; the course’s TA is not held to the window. runsends the sample tests plus any custom inputs the student typed, and saves nothing.submitgrades against all tests (sample and hidden). It calls the judge first, then saves the code and the result JSON together undersubmissions/so they share one timestamp. If the judge call fails, the student sees a generic error and nothing is graded.- The language string from the course or request is mapped to a file extension (for the saved file) and to a judge language code (for the sandbox).
Autosave
While a student edits, the client autosaves in the background about once a minute, and again when the lab ends.
sequenceDiagram
participant C as Client
participant A as AutosaveController
participant G as GitService
participant R as Student git repo
loop about every 60s and on lab end
C->>A: POST /api/autosave (code + location)
A->>A: resolve identity, validate enrollment + active lab
A->>G: write autosaved-solution.<ext>, commit
G->>R: file + git commit
A-->>C: 202 accepted (or 401/403/404)
end
If the client gets a 401 back, the session is gone and it stops the autosave loop.
Proctoring (lockdown)
During a lab the client watches for focus loss, fullscreen exit, copy, paste, and devtools, and heartbeats. These events are queued on the client and drained to the backend, which appends them to a CSV. The CSV is committed to git when the session ends.
Every event is a LockdownViolation(kind, timestampMs, detail). ViolationKind in data/src/commonMain/kotlin/data/Lockdown.kt is the single source of truth for which events are banner-worthy: ALERT shows the student a banner and is logged, INFO is logged for audit only.
| Kind | Severity | Source |
|---|---|---|
LockdownStarted |
INFO | First event of every session. Confirms lockdown engaged |
LockdownEnded |
INFO | Final client event. Confirms the student ended the lab |
FocusLoss |
ALERT | Window blur, desktop and web |
FocusGained |
INFO | Recovery: window regained focus |
FullscreenExit |
ALERT | Web fullscreen released |
TabHidden |
ALERT | Web only. visibilitychange to hidden |
TabVisible |
INFO | Recovery: tab became visible again |
PasteFromOutside |
ALERT | Clipboard content did not match the last copy made in the editor |
CopyFromEditor |
INFO | Ctrl/Cmd+C or +X inside the editor. Logged, not alerted |
ContextMenu |
ALERT | Right-click suppressed |
DevToolsAttempt |
ALERT | F12, Ctrl-Shift-I, Cmd-Q, Alt-F4 and similar |
ClipboardEscape |
ALERT | Clipboard scrubbed on focus loss |
WindowRestored |
ALERT | Desktop only. Minimize was blocked and the window forced back to fullscreen |
Heartbeat |
INFO | 10-second tick, tagged active or idle |
HeartbeatGap |
INFO | Wall-clock gap over 1.5x the interval |
SessionSummary |
INFO | One event when lockdown stops |
LoggedOut |
INFO | Recorded by the server, not the client: session ended by explicit logout or TTL expiry |
SessionSummary.detail is one space-separated line, built in LockdownEventService:
durationMs=… outMs=… focusLosses=… tabHidden=… copiesFromEditor=…
pastesExternal=… fullscreenExits=… navAttempts=… maxHeartbeatGapMs=…
Those are the questions a lab asks after the fact: how long the student was out of the app, how often they switched away, whether they pasted foreign content, whether they tried to leave fullscreen or open devtools, and whether the heartbeat has suspicious gaps.
sequenceDiagram
participant L as Lockdown controller (client)
participant AC as ActivityController
participant AL as ActivityLogService
participant G as GitService
participant R as Student git repo
L->>AC: POST /api/activity/event?problem=slug
AC->>AL: record event
AL->>G: append CSV row (no commit yet)
G->>R: write row
Note over L,R: on session end
L->>AC: POST /api/activity/commit
AC->>AL: commit
AL->>G: git add + commit
G->>R: commit
Session end also commits the log through a different path: when ApiTokenStore ends a session it publishes a LogoutEvent, and LogoutActivityLogHook commits the activity log in response. That listener runs synchronously before the session is marked logged out, so if the commit fails the logout is blocked rather than silently losing the log.