Skip to content

Latest commit

 

History

9 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CodeMetrics Backend

Go + Echo backend for CodeMetrics, a competitive-programming progress tracker. Replaces the previous Node/Express + MongoDB service with a Postgres-backed API.

Stack

  • Echo v4 HTTP framework
  • PostgreSQL via pgx
  • JWT auth (golang-jwt/jwt) with bcrypt password hashing, Google OAuth ("Sign In With Google" ID tokens, verified via go-oidc), and email-based password reset
  • Transactional email (welcome on signup, a login alert with IP/device on every login, password-changed confirmation) sent through the same SMTP mailer as password reset — a no-op logger until SMTP_HOST is configured
  • Per-IP rate limiting (stricter on /api/auth/*)
  • Structured JSON logging (log/slog) with per-request logs
  • Cursor-free page/limit pagination on list endpoints
  • Server-side Codeforces API client with a shared rate limiter (respects CF's ~1req/2s guidance) and a Postgres-backed cache, so per-user stats aren't recomputed or re-fetched from Codeforces on every request

Running locally

cp .env.example .env
docker compose up -d          # starts Postgres on localhost:5433
go run ./cmd/server

The server applies its schema automatically on startup (see internal/db/schema.sql).

API

All responses are JSON with a success boolean.

Auth

Method Path Auth Body / Query
POST /api/auth/register none { "email", "password" }
POST /api/auth/login none { "email", "password" } → returns { token, userId }
POST /api/auth/google none { "credential" } (Google ID token) → returns { token, userId }. Returns 501 until GOOGLE_CLIENT_ID is set.
POST /api/auth/forgot-password none { "email" } → always returns a generic success message
POST /api/auth/reset-password none { "token", "password" }
GET /api/auth/userid JWT ?email=

/api/auth/login, /register, /google, /forgot-password, and /reset-password share a strict rate-limit bucket per IP (default: 5/min) to slow down credential stuffing / brute force.

Password-reset emails go through SMTP (Gmail app password works well for low volume — see .env.example). If SMTP_HOST isn't set, the reset link is logged instead of emailed, so the flow still works end-to-end in development.

Leaderboards

A user can own any number of named leaderboards, each with its own tracked-username list. All endpoints require Authorization: Bearer <token> and act on the authenticated user's own leaderboards (ownership is checked server-side against the JWT's user id — a leaderboard id belonging to someone else 404s rather than exposing another user's data).

Method Path Body / Query
POST /api/leaderboards { "name" }
GET /api/leaderboards { data: [{ id, name, createdAt }] }
PUT /api/leaderboards/:id { "name" }
DELETE /api/leaderboards/:id — (cascades, removing every tracked username on that leaderboard)

Tracked usernames

Scoped to a single leaderboard, which must belong to the authenticated user.

Method Path Body / Query
POST /api/user/add { "username", "leaderboardId" }
POST /api/user/remove { "username", "leaderboardId" }
GET /api/user/fetchusernames ?leaderboardId=&page=1&limit=20{ data, pagination }

Codeforces stats (public, no auth — only ever touches Codeforces' own public data)

Method Path Query Response
GET /api/stats ?usernames=a,b,c (max 10) { data: [Stats, ...] } — see below
GET /api/cf/validate ?username=x { valid: bool }

Each Stats object covers rating (rating/maxRating/minRating, contestCount, bestRank/worstRank, maxRatingUp/maxRatingDown, full ratingHistory with per-contest rank), problem-solving (solvedCount/triedCount/unsolvedCount, averageSubmissionsPerSolved, maxSubmissionsOnProblem, solvedWithOneSubmissionPct, maxAcceptedInADay), and breakdowns by difficulty/tag (levels, problemRatings, tags, plus solvedProblemKeys for computing what two handles have solved in common). This backs the frontend's head-to-head compare page — everything pairwise (common solved, common contests) is derived client-side from two of these payloads rather than a dedicated compare endpoint.

Stats are cached in Postgres (cf_stats_cache) for 10 minutes per handle; outbound calls to Codeforces are serialized behind a shared rate limiter (~1 req/2s) regardless of how many concurrent requests hit these endpoints.

Configuration

See .env.example for all supported environment variables (DB connection, JWT secret/expiry, CORS origin, rate-limit tuning).

Releases

Packages

Contributors

Languages