Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Feature 01: Authentication

Overview

User registration, login, logout, and session management. This is the gateway to every other feature — no game actions are possible without an authenticated user. Sessions are stored in Redis with opaque tokens. Passwords are hashed with Argon2id.

Dependencies

  • Feature 00 (Project Foundation)

Technical Tasks

1. Users Table Migration

  • Create migration 0002_create_users.sql:
    CREATE TABLE users (
        id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        email           TEXT UNIQUE NOT NULL,
        password_hash   TEXT NOT NULL,
        patron_tier     SMALLINT DEFAULT 0,
        patron_expires  TIMESTAMPTZ,
        character_slots SMALLINT DEFAULT 2,
        bank_slots      SMALLINT DEFAULT 50,
        marketplace_slots SMALLINT DEFAULT 10,
        created_at      TIMESTAMPTZ DEFAULT now(),
        last_login      TIMESTAMPTZ
    );
    

2. User Model & Queries (crates/db)

  • Create src/models/user.rsUser struct with FromRow derive
  • Create src/queries/users.rs:
    • create_user(pool, email, password_hash) -> User
    • find_user_by_email(pool, email) -> Option<User>
    • find_user_by_id(pool, id) -> Option<User>
    • update_last_login(pool, id)

3. Password Hashing (crates/api)

  • Add argon2 crate dependency
  • Create src/auth/password.rs:
    • hash_password(password: &str) -> String — Argon2id with recommended params
    • verify_password(password: &str, hash: &str) -> bool

4. Session Management

  • Create src/auth/session.rs:
    • create_session(redis, user_id) -> String — generate random 32-byte token (hex-encoded), store in Redis with 30-day TTL as session:{token} → { user_id, expires_at }
    • validate_session(redis, token) -> Option<UserId> — lookup in Redis, return user_id if valid
    • destroy_session(redis, token) — delete from Redis
    • refresh_session(redis, token) — reset TTL to 30 days on each use

5. Auth Middleware

  • Create src/middleware/auth.rs:
    • Axum extractor AuthUser that reads Authorization: Bearer {token} header (or cookie)
    • Validates session via Redis
    • Returns 401 Unauthorized if missing or invalid
    • Populates AuthUser { user_id: Uuid } for downstream handlers

6. Auth Routes (crates/api)

  • Create src/routes/auth.rs:
    • POST /api/auth/register:
      • Validate: email format, password length (8-128 chars)
      • Check email not already taken (409 Conflict)
      • Hash password
      • Create user record
      • Create session
      • Return { token, user: { id, email } }
    • POST /api/auth/login:
      • Find user by email (401 if not found)
      • Verify password (401 if wrong)
      • Update last_login
      • Create session
      • Return { token, user: { id, email } }
    • POST /api/auth/logout:
      • Requires auth middleware
      • Destroy session
      • Return 204 No Content
    • GET /api/auth/me:
      • Requires auth middleware
      • Return current user info { id, email, patron_tier, character_slots }

7. Rate Limiting

  • Create src/middleware/rate_limit.rs:
    • Redis-based rate limiter using INCR + EXPIRE
    • Per-IP rate limiting for auth endpoints (10 req/min for register, 20 req/min for login)
    • Per-user rate limiting for authenticated endpoints (100 req/min default)
    • Return 429 Too Many Requests with Retry-After header when exceeded

8. Input Validation

  • Add validator crate
  • Create request structs with validation derives:
    • RegisterRequest { email: String, password: String } — email format, password length
    • LoginRequest { email: String, password: String }
  • Create ValidatedJson<T> Axum extractor that validates before passing to handler

9. Client Auth (client/)

  • Create lib/api/auth.ts:
    • register(email, password) — POST to /api/auth/register
    • login(email, password) — POST to /api/auth/login
    • logout() — POST to /api/auth/logout
    • getMe() — GET to /api/auth/me
  • Create lib/stores/auth.ts:
    • Svelte store holding current user + token
    • Persist token to localStorage
    • Auto-inject token into all API requests via the base fetch wrapper
  • Create auth pages:
    • routes/(auth)/login/+page.svelte — login form
    • routes/(auth)/register/+page.svelte — registration form
  • Create auth guard: redirect to /login if not authenticated when accessing (game) routes

Tests

Unit Tests

  • password::hash_password produces a valid Argon2id hash
  • password::verify_password returns true for correct password
  • password::verify_password returns false for wrong password
  • session::create_session stores a key in Redis with correct TTL
  • session::validate_session returns user_id for valid token
  • session::validate_session returns None for expired/invalid token
  • session::destroy_session removes the key from Redis
  • RegisterRequest validation rejects invalid email formats
  • RegisterRequest validation rejects passwords shorter than 8 chars
  • RegisterRequest validation accepts valid input

Integration Tests

  • POST /api/auth/register with valid data → 200, returns token + user
  • POST /api/auth/register with duplicate email → 409
  • POST /api/auth/register with invalid email → 422
  • POST /api/auth/register with short password → 422
  • POST /api/auth/login with correct credentials → 200, returns token
  • POST /api/auth/login with wrong password → 401
  • POST /api/auth/login with non-existent email → 401
  • GET /api/auth/me with valid token → 200, returns user
  • GET /api/auth/me without token → 401
  • POST /api/auth/logout destroys session (subsequent /me returns 401)
  • Rate limiter returns 429 after exceeding limit
  • Session persists across server restart (stored in Redis, not memory)