Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: ci

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
backend:
name: Backend / Go
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: interviewhub-backend-go
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: interviewhub-backend-go/go.mod
cache-dependency-path: interviewhub-backend-go/go.sum
- name: Install the supported TypeScript runtime
uses: actions/setup-node@v4
with:
node-version: "22.12"
- name: Verify modules
run: go mod verify
- name: Check formatting
run: test -z "$(gofmt -l .)"
- name: Vet
run: go vet ./...
- name: Test
run: go test -coverprofile=coverage.out ./...
- name: Race detector
run: go test -race ./...
- name: Ensure tests did not modify fixtures
working-directory: .
run: git diff --exit-code
- name: Build
run: go build -trimpath ./cmd/server ./cmd/seed ./cmd/oauth-bind

frontend:
name: Frontend / React
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: interviewhub-lld
env:
PUPPETEER_SKIP_DOWNLOAD: "true"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: interviewhub-lld/package-lock.json
- name: Install locked dependencies
run: npm ci --no-audit --no-fund
- name: Typecheck
run: npm run typecheck
- name: Verify session isolation and revocation queue
run: npm run test:session-boundaries
- name: Check browser smoke script
run: node --check scripts/verify-e2e.mjs
- name: Build
run: npm run build

platform:
name: Platform / Scripts and containers
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Validate shell scripts
run: |
bash -n run-local.sh stop-local.sh interviewhub-backend-go/scripts/seed-db.sh
shellcheck run-local.sh stop-local.sh interviewhub-backend-go/scripts/seed-db.sh
- name: Validate Compose
run: docker compose --file interviewhub-backend-go/docker-compose.yml --project-directory interviewhub-backend-go config --quiet
- name: Build backend image
run: docker build --file interviewhub-backend-go/Dockerfile --tag judgemycode-api:ci interviewhub-backend-go
- name: Build frontend image
run: docker build --file interviewhub-lld/Dockerfile --tag judgemycode-web:ci interviewhub-lld
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Environment variables (keep secrets out of git)
.env
.env.local
.env.*
.env.*.local
*.env
!*.env.example
Expand Down Expand Up @@ -28,6 +29,7 @@ temp_sandboxes/

# Logs and runtime files
*.pid
*.pid.tmp
logs/
*.log
*refresh_tokens.json
Expand Down
43 changes: 43 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
SHELL := /bin/bash
.DEFAULT_GOAL := help

ROOT := $(CURDIR)
BACKEND_DIR := $(ROOT)/interviewhub-backend-go
FRONTEND_DIR := $(ROOT)/interviewhub-lld

.PHONY: help setup dev stop stop-all check test build

help: ## Show available commands
@awk 'BEGIN {FS = ":.*## "; printf "JudgeMyCode development commands:\n"} /^[a-zA-Z_-]+:.*## / {printf " %-10s %s\n", $$1, $$2}' $(MAKEFILE_LIST)

setup: ## Prepare env files, PostgreSQL, migrations, seeds, and dependencies
./run-local.sh --setup-only

dev: ## Start the complete local development stack
./run-local.sh

stop: ## Stop API and frontend; leave PostgreSQL running
./stop-local.sh

stop-all: ## Stop API, frontend, and PostgreSQL without deleting data
./stop-local.sh --dependencies

check: ## Run static checks without producing build artifacts
bash -n run-local.sh stop-local.sh interviewhub-backend-go/scripts/seed-db.sh
@if command -v shellcheck >/dev/null 2>&1; then shellcheck run-local.sh stop-local.sh interviewhub-backend-go/scripts/seed-db.sh; else echo "shellcheck not installed; skipped"; fi
@unformatted="$$(cd "$(BACKEND_DIR)" && gofmt -l $$(find . -name '*.go' -not -path './vendor/*'))"; test -z "$$unformatted" || { printf 'gofmt required:\n%s\n' "$$unformatted"; exit 1; }
cd "$(BACKEND_DIR)" && go vet ./...
cd "$(FRONTEND_DIR)" && npm run typecheck
cd "$(FRONTEND_DIR)" && npm run test:session-boundaries
docker compose --file "$(BACKEND_DIR)/docker-compose.yml" --project-directory "$(BACKEND_DIR)" config --quiet

test: ## Run backend tests, including the race detector
cd "$(BACKEND_DIR)" && go test ./...
cd "$(BACKEND_DIR)" && go test -race ./...

build: ## Build backend and production frontend
mkdir -p "$(ROOT)/.local-run/bin"
cd "$(BACKEND_DIR)" && go build -trimpath -o "$(ROOT)/.local-run/bin/interviewhub-api" ./cmd/server
cd "$(BACKEND_DIR)" && go build -trimpath -o "$(ROOT)/.local-run/bin/interviewhub-seed" ./cmd/seed
cd "$(BACKEND_DIR)" && go build -trimpath -o "$(ROOT)/.local-run/bin/interviewhub-oauth-bind" ./cmd/oauth-bind
cd "$(FRONTEND_DIR)" && npm run build
121 changes: 121 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# JudgeMyCode

JudgeMyCode is a React/Vite interview workspace backed by a Go API and PostgreSQL. The supported local workflow runs PostgreSQL in Docker and runs the API and frontend directly on the host for fast reloads.

## One-command local development

Prerequisites:

- Go 1.25 or newer compatible toolchain
- Node.js 22.12+ and npm
- Docker with Compose v2
- `curl`, `lsof`, and `cksum` (included by default on macOS and most Unix systems)
- For every local execution language: Python 3, a JDK (`javac`/`java`), and a
C++17 compiler (`g++`). Missing toolchains do not prevent the application
from starting; that language reports `RUNTIME_UNAVAILABLE` when used.

From this directory:

```bash
./run-local.sh
```

The command is safe to repeat. It creates missing local environment defaults, starts and waits for PostgreSQL, applies migrations, explicitly loads the local demo users and problem catalog, installs locked frontend dependencies when needed, rebuilds/restarts the API and Vite processes, and waits for both HTTP endpoints. The explicit restart prevents a previous binary from serving stale source or configuration.

Yes: `run-local.sh` starts the database dependency, backend, and frontend. To
start again after stopping—or to rebuild/restart after code changes—run the
same command:

```bash
./run-local.sh
```

Use `./run-local.sh --setup-only` when you want dependencies, migrations,
seeds, and npm packages prepared without starting the API or frontend.

| Service | Local address |
| --- | --- |
| Frontend | `http://127.0.0.1:5173` |
| API | `http://127.0.0.1:5050/api` |
| PostgreSQL | `127.0.0.1:5432` |

Runtime logs are under `logs/`; validated PID files are `backend.pid` and `frontend.pid`.

```bash
./stop-local.sh # API + frontend
./stop-local.sh --dependencies # also stop PostgreSQL; data volume is kept
```

The Makefile exposes the same workflow:

```bash
make setup # dependencies, database, migrations, local seeds
make dev # complete local stack
make stop # application processes only
make stop-all # application + PostgreSQL
make check # shell, Go, TypeScript, and Compose checks
make test # Go tests and race detector
make build # API binary and production frontend bundle
```

## Repository layout

- `interviewhub-backend-go/`: Go API with `internal/httpapi` as the composition/router boundary, embedded SQL migrations, PostgreSQL adapter, and local execution engine.
- `interviewhub-lld/`: React 19, TypeScript, Vite, and Monaco frontend organized around `src/app`, feature modules under `src/features`, and shared infrastructure under `src/shared`.
- `run-local.sh` / `stop-local.sh`: idempotent local lifecycle.
- `.github/workflows/ci.yml`: backend, frontend, script, Compose, and image checks.

Execution is asynchronous: run/submit returns an execution ID, and the client polls the owned execution resource until it reaches `COMPLETED`, `FAILED`, or `TIMED_OUT`.

Workspace autosave uses server revisions instead of timestamps. Every write is
compare-and-swap; a stale browser receives `409 Conflict` and must explicitly
keep its local draft or load the server version. Each open tab also keeps its
own recovery journal, so tabs cannot silently overwrite one another's unsaved
work.

Administrator problem edits also use compare-and-swap with the problem's
`updatedAt` revision. The guarded statement update and starter/test replacement
are one PostgreSQL transaction, and catalog seed/reset operations share the
same mutation lock. A stale editor therefore receives `409 Conflict` instead
of silently replacing another administrator's work.

A plain run executes once with custom standard input. A selected public test
runs with its server-owned input and expected output, “Run all tests” evaluates
all public tests, and submit evaluates public plus hidden tests before writing
submission history. Client-supplied assertions are never trusted.

Submit jobs capture an immutable catalog/test snapshot at queue admission and
persist its SHA-256 `evaluationRevision`. Later administrator edits therefore
cannot change an in-flight grade, and historical assertion values are removed
from submission responses.

Submission history uses owner-scoped keyset pagination (50 rows by default,
100 maximum) and lightweight indexed summaries; source files and execution
output are loaded only through the owner/admin detail endpoint. User progress,
including solved problem IDs, is calculated in one PostgreSQL aggregate rather
than by downloading and scanning a tenant's history in the browser.

Refresh credentials rotate once, are serialized with logout at the database
family boundary, and retain the original absolute expiry. The browser uses a
cross-tab lock plus principal/login-epoch lineage, so concurrent tabs adopt one
replacement and a delayed request can never replay under a newly signed-in
account. Failed logouts are kept as independent per-credential records in a
bounded, deduplicated revocation queue, so concurrent tabs cannot lose one
another's work; the queue is retried at startup, when connectivity returns, on
cross-tab changes, and around later sign-ins. One required browser Web Lock
serializes every credential mutation; unsupported browsers fail closed instead
of relying on an expiring client lease. A separate drain lock keeps old network
revocations from blocking a new login, and each HTTP request carries the bearer
captured with its originating session and body. The API bounds refresh
and logout requests independently by source and hashed opaque credential, and
PostgreSQL family operations use the direct `family_id` index.

AI feedback is optional. Configure an OpenAI-compatible chat-completions endpoint with `AI_API_URL`, `AI_API_KEY`, and `AI_MODEL`; `AI_REQUEST_TIMEOUT` defaults to `20s` and `AI_MAX_OUTPUT_TOKENS` to `700`. With no key/provider configured, the API returns a clear unavailable/configuration error and never fabricates feedback. Staging/production also requires `AI_EXTERNAL_GUARD=true` as an explicit acknowledgement that a distributed quota and spend guard exists upstream.

## Security boundary

`EXECUTION_ENABLED=true` is written only for the explicit local-development workflow. The current runner executes candidate programs on the API host and is not a security sandbox. Never expose it to untrusted users. Production must set `EXECUTION_ENABLED=false` until a dedicated network-disabled judge service with CPU, memory, process, filesystem, output, and wall-time isolation is implemented.

The Compose file intentionally contains PostgreSQL only. Redis and the placeholder worker were removed because no durable queue/worker implementation exists yet.

See [backend documentation](interviewhub-backend-go/README.md), [API documentation](interviewhub-backend-go/API_DOCUMENTATION.md), and the [production architecture contract](interviewhub-backend-go/docs/production-architecture.md).
17 changes: 17 additions & 0 deletions interviewhub-backend-go/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.git
.github
.env
.env.*
data
test
logs
bin
temp_sandboxes
*.pid
*.log
coverage.out
Dockerfile.worker
docker-compose*.yml
README.md
API_DOCUMENTATION.md
docs
25 changes: 18 additions & 7 deletions interviewhub-backend-go/.env.example
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# Copy to .env locally; inject real values through your deployment platform.
PORT=5050
HOST=127.0.0.1
APP_ENV=development
JWT_SECRET=replace-with-a-long-random-secret-at-least-32-characters
ACCESS_TOKEN_TTL=15m
REFRESH_TOKEN_TTL=720h
ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,http://127.0.0.1:5173
# Comma-separated proxy networks allowed to supply X-Forwarded-For. Leave
# empty when clients connect directly; never trust an internet-wide CIDR.
TRUSTED_PROXY_CIDRS=
MAX_REQUEST_BYTES=2097152

# Application Base URLs
Expand Down Expand Up @@ -33,16 +37,23 @@ LINKEDIN_REDIRECT_URL=http://localhost:5050/api/auth/oauth/linkedin/callback
# Local development database and queue settings.
# Host-run API (go run ./cmd/server or ./scripts/seed-db.sh):
DATABASE_URL=postgres://judgemycode:local-dev-password@localhost:5432/judgemycode?sslmode=disable
REDIS_URL=redis://localhost:6379/0
# Inside docker compose, use hostnames `postgres` / `redis` instead of localhost.
# Docker Compose exposes PostgreSQL only on the loopback interface.

EXECUTION_ENABLED=false
EXECUTION_RUNNER=local
EXECUTION_QUEUE_NAME=judge-jobs
EXECUTION_DOCKER_ENABLED=false
EXECUTION_QUEUE_CAPACITY=32
EXECUTION_WORKER_COUNT=2
EXECUTION_TIMEOUT_SEC=6
EXECUTION_MAX_FILE_COUNT=50
EXECUTION_MAX_OUTPUT_BYTES=1048576
EXECUTION_MAX_MEMORY_MB=256
EXECUTION_MAX_PROCESSES=64
EXECUTION_CPU_CORES=1

# Optional OpenAI-compatible chat-completions provider. Leave blank to disable
# AI feedback; the API returns a configuration/unavailable error, never mocks.
AI_API_URL=
AI_API_KEY=
AI_MODEL=
AI_REQUEST_TIMEOUT=20s
AI_MAX_OUTPUT_TOKENS=700
# Required in staging/production when AI is enabled; confirms ingress/provider
# enforces a distributed per-account quota and total spend budget.
AI_EXTERNAL_GUARD=false
16 changes: 0 additions & 16 deletions interviewhub-backend-go/.github/workflows/ci.yml

This file was deleted.

2 changes: 1 addition & 1 deletion interviewhub-backend-go/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Requests follow this path:

- **handlers** validate HTTP requests, enforce HTTP status codes, and translate service errors into the standard response envelope. They do not contain business rules.
- **services** own product rules: identity, progress calculations, workspace ownership, grading, and AI interactions.
- **store** is the persistence boundary. Production uses PostgreSQL exclusively. The in-memory/JSON adapter exists only for unit tests.
- **store** is the persistence boundary. Production uses PostgreSQL exclusively. A fresh in-memory adapter exists only for unit tests; runtime JSON persistence was removed.
- **models** holds transport-safe domain structures shared by layers.

Do not hardcode problem catalogs or user records in the API server. Bootstrap empty databases with `./scripts/seed-db.sh`.
Expand Down
Loading
Loading