Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

CodeJudge

A self-hosted platform for running timed technical assessments.

Build a categorised question bank, assemble timed multiple-choice papers, release them to exactly the students who should sit them, and get scored results with a question-by-question breakdown the moment the clock stops.

Java MySQL Docker Tests License

CodeJudge home page


Contents


What it is

Many colleges, training institutes and small teams still run technical and aptitude assessments by hand: type the paper, print it, invigilate, collect, mark, tally into a spreadsheet, then work out who passed. Every step costs time and every step can lose a mark.

CodeJudge replaces that loop. It is a complete, self-hosted assessment system:

For the department A reusable question bank, a paper builder, batch-based assignment, automatic marking, and cohort analytics with CSV export.
For the candidate A clean exam screen with a visible countdown, answers that save as you go, and a fully marked script returned the instant they submit.

It is not proctoring software β€” there is no browser lockdown, webcam monitoring or tab-switch detection. It handles the assessment, not the invigilation.

Why it might interest you

  • Zero install beyond Docker. No Java, Maven, MySQL or Node on your machine.
  • Genuinely dependency-light. The HTTP server, JSON parser, connection pool and password hashing are all written against the JDK. The only backend dependency is the MySQL driver.
  • No build step for the frontend. Plain HTML, CSS and vanilla JavaScript served from the same jar. No webpack, no npm, no node_modules.
  • Honest about its gaps. See Known limitations.

Screenshots

These are real renders from a running instance, captured by the browser test suite.

Home Features
Home Features
Sign in Sign up
Sign in Sign up

Admin overview

Admin overview

Per-test report β€” distribution, pass rate, question difficulty and ranked results

Admin report


Quick start

Requirements

Docker Desktop (or Docker Engine + Compose v2). Nothing else.

Run it

git clone https://github.com/<your-username>/codejudge.git
cd codejudge

cp .env.example .env        # edit the passwords before anything public
docker compose up --build

First boot takes a few minutes: Maven downloads dependencies inside the build container and MySQL initialises its schema. When it settles, open http://localhost:8080.

First sign-in

Who Where Credentials
Staff / admin http://localhost:8080/#/staff ADMIN_EMAIL / ADMIN_PASSWORD from your .env
Candidates http://localhost:8080/#/register They register themselves

Defaults are admin@codejudge.local / Admin@12345. Change these in .env before first boot β€” the admin account is created once, from those values, against an empty database. Editing .env afterwards does not change an existing admin's password.

Your first test, end to end

  1. Sign in as staff β†’ Questions β†’ New category (four are seeded: Java, SQL, Aptitude, General Programming) β†’ New question. Add a few.
  2. Tests β†’ New test. Set the duration and pass mark.
  3. On the new test row β†’ Questions β†’ tick the ones you want β†’ Save paper.
  4. Assign β†’ Everyone, or pick batches and named candidates.
  5. Publish.
  6. Register a candidate in another browser, sit the paper, submit.
  7. Back in staff β†’ Report on that test.

Everyday commands

docker compose up -d --build    # rebuild and start
docker compose logs -f app      # follow application logs
docker compose restart app      # restart just the app
docker compose down             # stop (data survives)
docker compose down -v          # stop and WIPE the database

Feature tour

Admin

Area What you can do
Overview Candidate / test / question / attempt counts, average-score gauge, latest submissions, top performers, quick actions.
Question bank Create categories; add questions with four options, one correct answer and their own mark value; filter by category; edit and delete.
Tests Create with title, description, duration (1–480 min) and pass mark (0–100%); attach questions from the bank with a running marks total; publish / unpublish; delete.
Batches Group candidates into classes, sections or intakes. Searchable member picker with select-all.
Assignment Per test, choose Everyone or Only who I choose (any mix of batches and named individuals), with a live count of who it reaches.
Candidates Everyone registered, with join dates.
Reports Papers sat, average, highest, lowest, average time, score distribution, pass-rate donut, per-question difficulty (hardest first), full ranked results, CSV export.

Candidate

Area What you get
Tests Only the papers you are entitled to sit. Completed ones show your score; in-progress ones offer Resume.
Exam One question at a time, an answer sheet showing what is left, a ring countdown that turns amber then red, and clear/change answers until you submit.
Result Score dial, pass/fail verdict, rank, time taken, per-topic breakdown, and every question marked with your answer against the correct one.
My results Every attempt, with a score-trend chart over time.
Leaderboard Ranked by marks, then by time taken, with your own row highlighted.

System behaviour worth knowing

  • The clock is server-side. Each attempt gets a deadline in the database and a scheduled task that force-submits it when that deadline passes β€” closing the tab does not stop it. Timers are re-armed on startup, so an app restart mid-exam does not lose the deadline.
  • Answers save as you pick them, so a refresh or a dropped connection costs nothing.
  • One attempt per candidate per test, enforced by a unique database constraint.
  • Correct answers are never sent to the browser during an exam β€” the payload omits them entirely and they only appear once the attempt is submitted.
  • Marking is a single transaction: every answer is compared to the key, per-question marks are written, and the attempt total is finalised atomically.

Tech stack

Layer Choice Why
Language Java 21 The brief: Core Java.
HTTP com.sun.net.httpserver (JDK built-in) No Spring, no servlet container.
Database MySQL 8.4 via JDBC Plain prepared statements, no ORM.
JSON Hand-written parser/writer Avoids Jackson/Gson for a Core Java project.
Pooling Hand-written bounded pool Avoids HikariCP; ~120 lines.
Passwords PBKDF2WithHmacSHA256 (JDK) 120k iterations, per-user salt.
Concurrency ScheduledExecutorService Exam auto-submission and session sweeping.
Build Maven (inside Docker) Single fat jar.
Frontend Vanilla JS + CSS, no build step Served from the same jar.

Only Maven dependency: com.mysql:mysql-connector-j.

Frontend libraries (CDN): Bootstrap Icons, AOS, animate.css, Swiper, ApexCharts. These are presentation only β€” see Known limitations for the air-gapped caveat.

Course-topic coverage

If you are using this as a Core Java project, here is where each topic actually lives:

Topic Where
Collections Framework service/*, DAO result mapping, SessionManager
Streams & lambdas AnalyticsService, LeaderboardService, BatchService
Multithreading worker/AttemptTimerRegistry, util/SessionManager sweeper
JDBC every class in dao/, transactions in AttemptDao.finalizeAttempt
File handling ReportService (CSV generation via java.io)
Exception handling http/Response, http/Router error mapping, DAO SQLException paths
Layered architecture model β†’ dao β†’ service β†’ http

Architecture

                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   Browser  ───────────►│  app  (eclipse-temurin:21-jre) β”‚
   vanilla JS SPA       β”‚                                β”‚
                        β”‚  http/Router  ── small router  β”‚
                        β”‚  service/     ── business rulesβ”‚
                        β”‚  dao/         ── JDBC          β”‚
                        β”‚  worker/      ── exam timers   β”‚
                        β”‚  json/, db/   ── hand-rolled   β”‚
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                        β”‚ JDBC
                                        β–Ό
                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                        β”‚  mysql:8.4                     β”‚
                        β”‚  db/init/*.sql on first boot   β”‚
                        β”‚  SchemaMigrator on every boot  β”‚
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Request flow: Router matches the path, enforces auth and role, calls a service, which validates input and orchestrates one or more DAOs. Services throw Response for expected failures (400/403/404/409); the router maps everything else to a 500 and logs it.

Two background threads:

  • AttemptTimerRegistry β€” one scheduled task per in-progress attempt, firing at its deadline. Re-checks the wall clock before finalising, so a suspended/resumed host cannot end a paper early. Recovers all in-progress attempts on startup.
  • SessionManager sweeper β€” clears idle sessions every five minutes.

Schema migration: db/init/*.sql only runs on a brand-new MySQL volume, so db/SchemaMigrator applies additive changes on every boot instead. Both the pass-mark and the assignment features were added to a live instance this way, with no data loss.

Cache busting: every asset URL carries a build id that changes on each deploy (styles.css?v=…), and the server sends Cache-Control: no-cache, must-revalidate, so a redeploy can never leave a browser on stale CSS or JS.


Database schema

Eleven tables.

users ──┬─< batch_members >── batches ──< test_batches >──┐
        β”‚                                                 β”‚
        β”œβ”€< test_candidates >──────────────────────────────
        β”‚                                                 β–Ό
        └─< attempts >── attempt_answers            tests ──< test_questions >── questions ──> categories
Table Holds
users Accounts, PBKDF2 hash + salt, role (ADMIN / CANDIDATE)
categories Question categories
questions Text, four options, correct option, marks, category
tests Title, duration, pass %, audience (EVERYONE/ASSIGNED), published flag
test_questions Which questions are on a paper, and in what order
batches Cohorts
batch_members Candidates in a cohort
test_batches Papers assigned to whole cohorts
test_candidates Papers assigned to named individuals
attempts One per candidate per test β€” timings, status, score
attempt_answers Per-question answer, correctness and marks awarded

Key constraints: attempts is UNIQUE (test_id, user_id); attempt_answers is UNIQUE (attempt_id, question_id); questions.correct_option is checked to be A–D.


API reference

JSON over HTTP. Authentication is an HttpOnly session cookie set at login (a bearer token is also accepted via Authorization: Bearer <token>).

Access levels: 🟒 public Β· πŸ”΅ any signed-in user Β· πŸ”΄ admin only

Auth

Method Path Purpose
🟒 GET /api/health Liveness probe
🟒 POST /api/auth/register Create a candidate account
🟒 POST /api/auth/login Sign in, sets the session cookie
πŸ”΅ POST /api/auth/logout Invalidate the session
πŸ”΅ GET /api/me Current user
πŸ”΅ GET /api/me/summary Tests taken, average, best, score trend

Candidate

Method Path Purpose
πŸ”΅ GET /api/tests Papers this candidate may sit
πŸ”΅ POST /api/tests/:id/start Start or resume an attempt
πŸ”΅ GET /api/attempts/mine Attempt history
πŸ”΅ GET /api/attempts/:id Live attempt state (for resume)
πŸ”΅ POST /api/attempts/:id/answer Save or clear one answer
πŸ”΅ POST /api/attempts/:id/submit Submit and mark (idempotent)
πŸ”΅ GET /api/attempts/:id/review Full marked script
πŸ”΅ GET /api/tests/:id/leaderboard Ranked results

Admin β€” question bank

Method Path
πŸ”΄ GET POST /api/admin/categories
πŸ”΄ DELETE /api/admin/categories/:id
πŸ”΄ GET POST /api/admin/questions Β Β·Β  ?categoryId= filter on GET
πŸ”΄ PUT DELETE /api/admin/questions/:id

Admin β€” tests

Method Path Purpose
πŸ”΄ GET POST /api/admin/tests List / create
πŸ”΄ GET PUT DELETE /api/admin/tests/:id Read / update / delete
πŸ”΄ GET PUT /api/admin/tests/:id/questions Read / set the paper
πŸ”΄ PUT /api/admin/tests/:id/publish { "published": true }
πŸ”΄ GET PUT /api/admin/tests/:id/audience Who the paper is for
πŸ”΄ GET /api/admin/tests/:id/analytics Full report data
πŸ”΄ GET /api/admin/tests/:id/leaderboard Ranked results
πŸ”΄ GET /api/admin/tests/:id/results.csv CSV download

Admin β€” people

Method Path
πŸ”΄ GET /api/admin/overview
πŸ”΄ GET /api/admin/candidates
πŸ”΄ GET POST /api/admin/batches
πŸ”΄ GET PUT DELETE /api/admin/batches/:id
πŸ”΄ PUT /api/admin/batches/:id/members

Example

# sign in and keep the cookie
curl -c jar.txt -X POST http://localhost:8080/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@codejudge.local","password":"Admin@12345"}'

# add a question
curl -b jar.txt -X POST http://localhost:8080/api/admin/questions \
  -H 'Content-Type: application/json' \
  -d '{"categoryId":1,"text":"Which keyword prevents subclassing?",
       "optionA":"static","optionB":"final","optionC":"sealed","optionD":"private",
       "correctOption":"B","marks":2}'

# restrict a test to two batches
curl -b jar.txt -X PUT http://localhost:8080/api/admin/tests/1/audience \
  -H 'Content-Type: application/json' \
  -d '{"audience":"ASSIGNED","batchIds":[1,2],"candidateIds":[]}'

Error shape β€” every failure returns {"error":"human readable message"} with a sensible status: 400 validation, 401 not signed in, 403 wrong role or not your data, 404 missing, 409 conflict (duplicate email, already-submitted attempt, unassigned test).


Configuration

All configuration is environment variables, so the same jar runs unchanged anywhere.

Variable Default Notes
DB_HOST localhost mysql inside Compose
DB_PORT 3306
DB_NAME codejudge
DB_USER codejudge
DB_PASSWORD codejudge_pw Change it.
APP_PORT 8080 Port the app listens on
ADMIN_EMAIL admin@codejudge.local Bootstrap admin, first boot only
ADMIN_PASSWORD Admin@12345 Change before first boot.

Compose also reads MYSQL_ROOT_PASSWORD, MYSQL_DATABASE, MYSQL_USER and MYSQL_PASSWORD from .env for the database container.

.env is gitignored. .env.example is the template β€” keep real secrets out of the repo.


Testing

Everything runs inside containers; nothing is installed on your machine. Start the app first (docker compose up -d).

Suite Checks Covers
test/e2e_test.py 113 Sign-up, sign-in, question bank, building and publishing tests, sitting papers, marking accuracy, the review report, analytics, CSV export, authorisation, sessions
test/e2e_batches.py 36 Cohorts, assignment, and that restricted papers are refused server-side rather than merely hidden
test/ui_quick.py β€” Renders key screens in headless Chromium, captures screenshots, fails on console errors
# API suites
docker run --rm -i --network codejudge_codejudge-net \
  python:3.12-alpine python - < test/e2e_test.py

docker run --rm -i --network codejudge_codejudge-net \
  python:3.12-alpine python - < test/e2e_batches.py
# Browser suite (writes to test/shots/)
mkdir -p test/shots
MSYS_NO_PATHCONV=1 docker run --rm -i \
  --network codejudge_codejudge-net \
  -v "$(pwd)/test/shots:/shots" \
  -v "$(pwd)/test/ui_quick.py:/ui_quick.py:ro" \
  mcr.microsoft.com/playwright/python:v1.47.0-jammy \
  bash -c "pip install -q playwright==1.47.0; python /ui_quick.py"

MSYS_NO_PATHCONV=1 matters on Windows Git Bash β€” without it MSYS rewrites the container-side paths and the run dies before it starts. Drop it on PowerShell, Linux or macOS.

Chart colour validation

Chart colours are checked computationally rather than by eye (lightness band, chroma floor, colourblind separation, contrast against the real surface):

Role Light (on #FFFFFF) Dark (on #131A2E)
Sequential ramp #A69DFF β†’ #3A31A8 #A69DFF β†’ #5B4EDB
Correct / pass #0E9F6E #23A578
Incorrect / fail #D93F35 #E4604E

The red/green pair sits in the colourblind warn band, which is only acceptable with a second channel β€” so every use of those two ships an icon and a text label, never colour alone.


Deployment

It is two ordinary containers, so it runs anywhere that takes a Dockerfile plus a MySQL database. Set the same variables as .env.example; no code changes needed.

Railway / Render / Fly.io

  1. Point the service at this repo; it builds from the Dockerfile.
  2. Add a managed MySQL instance.
  3. Set DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD, ADMIN_EMAIL, ADMIN_PASSWORD. Set APP_PORT to whatever the platform expects (often $PORT).
  4. Deploy. The schema creates itself on first boot.

On a managed database the db/init/*.sql mount will not run. SchemaMigrator creates the newer tables, but the original ones come from those files β€” so for a fresh managed database, run db/init/01-schema.sql and db/init/02-seed.sql against it once before first boot.

A VPS

git clone https://github.com/<your-username>/codejudge.git
cd codejudge && cp .env.example .env
# edit .env β€” set strong passwords
docker compose up -d --build

Then put Nginx or Caddy in front for TLS. Once you are on HTTPS, consider marking the session cookie Secure in Main.setSessionCookie.

Resource notes

Comfortable on 1–2 GB RAM. The app container is a slim JRE with -XX:MaxRAMPercentage=75. Free tiers work; expect a cold start after idle.


Troubleshooting

The UI looks out of date after an update

Assets are versioned per deployment, so a normal reload should pick up the new build. If you are still on the old one, hard-reload (Ctrl+Shift+R).

A candidate cannot see a test

Check three things: the test is published, it has at least one question, and β€” if its audience is Only who I choose β€” the candidate is in an assigned batch or named directly.

A test will not publish

A paper needs at least one question. A restricted paper also needs at least one batch or candidate assigned, otherwise publishing would reach nobody.

Everyone was signed out

The app restarted. Sessions live in memory by design; signing back in is all that is needed. In-progress exams are unaffected β€” their deadlines live in the database.

Port 8080 is already in use

Set APP_PORT=9090 in .env and docker compose up -d.

I forgot the admin password

The bootstrap admin is only created against an empty database. Either restore from a backup, or docker compose down -v to wipe and start fresh with new .env values. There is no in-app password reset yet.

Build fails downloading Maven dependencies

The build container needs internet on first run. Behind a proxy, pass it through in the Dockerfile build stage.


Security

What is in place

  • Passwords β€” PBKDF2-HMAC-SHA256, per-user 16-byte random salt, 120,000 iterations, constant-time comparison. Never stored or logged in plaintext.
  • Sessions β€” random 256-bit tokens held server-side, sent as HttpOnly, SameSite=Lax cookies so page scripts cannot read them. Eight-hour idle timeout with a background sweeper.
  • Roles β€” every staff route is gated on the server, not merely hidden in the UI. A candidate cannot list the question bank, reach analytics, or read another candidate's attempt or script; those return 403 whatever the browser asks.
  • Assignment β€” restricted papers are filtered out of the candidate's list and refused by the server on a direct link.
  • Exam integrity β€” correct answers are withheld until submission; the deadline is enforced server-side; late answers are rejected; one attempt per candidate per test is a database constraint.
  • Injection β€” every query is a parameterised prepared statement; no SQL is built from user input. All user text is HTML-escaped before rendering.
  • Registration β€” self-registration always produces a candidate. There is no path to create an admin through the API.

Before exposing it publicly

  1. Set strong ADMIN_PASSWORD, DB_PASSWORD and MYSQL_ROOT_PASSWORD in .env.
  2. Put it behind HTTPS.
  3. Consider marking the session cookie Secure (left off so plain-HTTP local dev works).
  4. Do not publish the MySQL port; Compose already keeps it internal.

Found a vulnerability? Please open a private security advisory rather than a public issue.


Known limitations

Being straight about what this does not do, so nobody discovers it mid-exam:

Assessment

  • Multiple choice only β€” four options, one correct answer. No coding questions, no essays, no multi-select, no negative marking.
  • One attempt per candidate per test. For a resit, clone the test.
  • No question randomisation or shuffling.
  • No scheduled open/close windows; a paper is simply published or not.

Accounts

  • No password reset or change in the UI.
  • No two-factor authentication.
  • No rate limiting on sign-in attempts.
  • No bulk candidate import (CSV upload); candidates self-register.
  • Admins can only be created by the bootstrap process.

Operational

  • Sessions are in memory, so a restart signs everyone out and the app does not scale horizontally as-is.
  • No audit log of staff actions.
  • No email β€” no verification, no result notifications.
  • Frontend libraries load from CDN. In an air-gapped deployment the icons, animations and charts degrade, but the app still works; vendor them into src/main/resources/web/ if you need full offline operation.

Not proctoring

  • No browser lockdown, webcam monitoring, screen recording or tab-switch detection.

Cosmetic

  • Navigating away from the admin overview while the gauge is still animating logs a few harmless NaN SVG warnings from ApexCharts' teardown. Nothing renders incorrectly.

Possible next steps

Coding questions with a sandboxed runner Β· question shuffling Β· scheduled exam windows Β· password reset via email Β· bulk candidate import Β· persistent sessions (Redis or a table) Β· audit logging Β· multi-select and negative marking.


Project layout

codejudge/
β”œβ”€β”€ docker-compose.yml          # app + mysql
β”œβ”€β”€ Dockerfile                  # multi-stage: Maven build β†’ slim JRE
β”œβ”€β”€ pom.xml                     # one dependency: mysql-connector-j
β”œβ”€β”€ .env.example                # copy to .env
β”‚
β”œβ”€β”€ db/init/
β”‚   β”œβ”€β”€ 01-schema.sql           # tables, constraints, indexes
β”‚   └── 02-seed.sql             # four starter categories
β”‚
β”œβ”€β”€ docs/screenshots/           # images used by this README
β”‚
β”œβ”€β”€ test/
β”‚   β”œβ”€β”€ e2e_test.py             # 113 API checks
β”‚   β”œβ”€β”€ e2e_batches.py          # 36 assignment checks
β”‚   β”œβ”€β”€ ui_quick.py             # headless browser render
β”‚   └── README.md               # how to run them
β”‚
└── src/main/
    β”œβ”€β”€ java/com/codejudge/
    β”‚   β”œβ”€β”€ Main.java           # bootstrap + all route registration
    β”‚   β”œβ”€β”€ config/             # env-driven configuration
    β”‚   β”œβ”€β”€ db/                 # connection pool, schema migrator
    β”‚   β”œβ”€β”€ json/               # hand-written JSON reader/writer
    β”‚   β”œβ”€β”€ http/               # router, request, response, static files
    β”‚   β”œβ”€β”€ model/              # plain data classes
    β”‚   β”œβ”€β”€ dao/                # JDBC data access
    β”‚   β”œβ”€β”€ service/            # business rules
    β”‚   β”œβ”€β”€ util/               # passwords, sessions, ids
    β”‚   └── worker/             # exam auto-submission timers
    β”‚
    └── resources/web/          # the frontend, served from the jar
        β”œβ”€β”€ index.html
        β”œβ”€β”€ css/                # styles.css (app) Β· site.css (marketing + auth)
        └── js/                 # api Β· ui Β· charts Β· site Β· views-* Β· app

37 Java files across 10 packages Β· 12 JavaScript files Β· no frontend build step.


Contributing

Issues and pull requests are welcome.

# make your change, then:
docker compose up -d --build
docker run --rm -i --network codejudge_codejudge-net python:3.12-alpine python - < test/e2e_test.py
docker run --rm -i --network codejudge_codejudge-net python:3.12-alpine python - < test/e2e_batches.py

A few conventions worth matching:

  • Keep the backend dependency-free beyond the JDBC driver.
  • Business rules belong in service/, SQL in dao/, wiring in Main.
  • Throw Response.error(status, "message") for expected failures; the message is shown to the user, so write it in plain language.
  • Add checks to the relevant suite for anything you change.
  • The frontend has no build step β€” plain JS, and escape user text with UI.esc().

License

MIT. See LICENSE.

Built with Core Java, JDBC, MySQL and Docker.

About

Java and MySQL based technical assessment platform for creating timed tests, managing candidates, automated evaluation, reports, and leaderboards..

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages