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.rs—Userstruct withFromRowderive - Create
src/queries/users.rs:create_user(pool, email, password_hash) -> Userfind_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
argon2crate dependency - Create
src/auth/password.rs:hash_password(password: &str) -> String— Argon2id with recommended paramsverify_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 assession:{token} → { user_id, expires_at }validate_session(redis, token) -> Option<UserId>— lookup in Redis, return user_id if validdestroy_session(redis, token)— delete from Redisrefresh_session(redis, token)— reset TTL to 30 days on each use
5. Auth Middleware
- Create
src/middleware/auth.rs:- Axum extractor
AuthUserthat readsAuthorization: Bearer {token}header (or cookie) - Validates session via Redis
- Returns
401 Unauthorizedif missing or invalid - Populates
AuthUser { user_id: Uuid }for downstream handlers
- Axum extractor
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 RequestswithRetry-Afterheader when exceeded
- Redis-based rate limiter using
8. Input Validation
- Add
validatorcrate - Create request structs with validation derives:
RegisterRequest { email: String, password: String }— email format, password lengthLoginRequest { 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/registerlogin(email, password)— POST to/api/auth/loginlogout()— POST to/api/auth/logoutgetMe()— 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 formroutes/(auth)/register/+page.svelte— registration form
- Create auth guard: redirect to
/loginif not authenticated when accessing(game)routes
Tests
Unit Tests
password::hash_passwordproduces a valid Argon2id hashpassword::verify_passwordreturns true for correct passwordpassword::verify_passwordreturns false for wrong passwordsession::create_sessionstores a key in Redis with correct TTLsession::validate_sessionreturns user_id for valid tokensession::validate_sessionreturns None for expired/invalid tokensession::destroy_sessionremoves the key from RedisRegisterRequestvalidation rejects invalid email formatsRegisterRequestvalidation rejects passwords shorter than 8 charsRegisterRequestvalidation accepts valid input
Integration Tests
POST /api/auth/registerwith valid data → 200, returns token + userPOST /api/auth/registerwith duplicate email → 409POST /api/auth/registerwith invalid email → 422POST /api/auth/registerwith short password → 422POST /api/auth/loginwith correct credentials → 200, returns tokenPOST /api/auth/loginwith wrong password → 401POST /api/auth/loginwith non-existent email → 401GET /api/auth/mewith valid token → 200, returns userGET /api/auth/mewithout token → 401POST /api/auth/logoutdestroys session (subsequent/mereturns 401)- Rate limiter returns 429 after exceeding limit
- Session persists across server restart (stored in Redis, not memory)