A production-ready REST API backend for multi-player Blackjack card games, built with Rust using Axum, featuring JWT authentication, Argon2id password hashing, role-based access control, rate limiting, structured logging, and comprehensive security hardening.
This project provides a complete backend system for managing multi-player Blackjack games (1-10 players per game) with:
- RESTful API: Versioned endpoints under
/api/v1with comprehensive documentation - JWT Authentication: Secure player authentication per game session
- Security Hardening (M8): Argon2id password hashing (OWASP parameters), RBAC, security headers
- User Management (M7-M8): User registration, login, password changes, account activation/deactivation
- Access Control (M8): Role-based permissions (Creator, Player, Spectator)
- Game Management (M8): Kick players, view participants with roles
- Turn-Based Gameplay (M7): Ordered turns, automatic advancement, smart turn skipping
- Game Invitations (M7): Invite system with configurable timeouts and status tracking
- Rate Limiting: Per-user request throttling using sliding window algorithm
- Real-time Ready: WebSocket blueprint for future real-time notifications
- Observability: Structured logging with tracing, health checks, metrics-ready
- Production-Grade: External configuration, CORS support, graceful error handling
- Multi-player Support: 1-10 players per game with independent state management
- Flexible Gameplay: Dynamic Ace values (1 or 11), ordered card history, bust detection
- Comprehensive Testing: 167 tests passing (unit, integration, API, service)
- Numbered cards (2-9): Face value
- 10, Jack, Queen, King: 10 points each
- Ace: 1 point (can be changed to 11 points at player's discretion)
Phase 1: Game Creation & Enrollment
- Creator creates a game with optional enrollment timeout (default: 300s)
- Players can:
- Browse open games via
/api/v1/games/open - Self-enroll in games with available slots (max 10 players)
- Receive and accept game invitations from enrolled players
- Browse open games via
- Enrollment Period:
- Players can join until timeout expires OR creator manually closes enrollment
- Game accepts players up to maximum capacity (10 players)
- Creator closes enrollment to start the game
- Turn order is randomized when enrollment closes
Phase 2: Turn-Based Gameplay
- Players take ordered turns (enforced by server)
- On each turn, the current player can:
- Draw a card from the shared deck
- Change Ace values (1 ↔ 11 points)
- Stand (end their turn)
- After drawing a card:
- Server validates it's the player's turn (returns 409 if not)
- If player busts (>21), their turn automatically advances
- If player stands, turn advances to next active player
- Auto-finish: Game automatically finishes when all players have stood or busted
- Winner is determined based on highest score ≤21
- Single Winner: Player with highest score ≤21
- Tie: Multiple players with the same highest score ≤21
- No Winner: All players exceeded 21 points (all bust)
This project uses a workspace-based architecture with clear separation of concerns:
rust_blackjack/
├── Cargo.toml # Workspace manifest
├── Dockerfile # Multi-stage Docker build
├── .dockerignore
├── .github/
│ └── workflows/
│ └── ci.yml # CI/CD pipeline
├── crates/
│ ├── blackjack-core/ # Domain logic (Game, Card, Player)
│ │ ├── src/lib.rs
│ │ └── tests/integration_tests.rs
│ ├── blackjack-service/ # Business logic layer
│ │ ├── src/lib.rs
│ │ ├── migrations/ # Future SQLite migrations
│ │ └── tests/service_tests.rs
│ ├── blackjack-api/ # REST API (Axum)
│ │ ├── src/
│ │ │ ├── main.rs # Server entry point
│ │ │ ├── lib.rs
│ │ │ ├── auth.rs # JWT authentication
│ │ │ ├── config.rs # Configuration management
│ │ │ ├── error.rs # Standardized errors
│ │ │ ├── handlers.rs # HTTP request handlers
│ │ │ ├── middleware.rs # Auth, rate limit, deprecation
│ │ │ ├── rate_limiter.rs # Sliding window rate limiter
│ │ │ └── websocket.rs # WebSocket blueprint (future)
│ │ ├── config.toml # Default configuration
│ │ └── tests/api_tests.rs
│ └── blackjack-cli/ # Original CLI version (preserved)
│ └── src/main.rs
├── docs/
│ ├── PRD.md # Product Requirements Document
│ └── postman/ # API testing resources
│ ├── README.md # Testing guide overview
│ ├── Blackjack_API.postman_collection.json
│ ├── Blackjack_API_Local.postman_environment.json
│ ├── POSTMAN_GUIDE.md # Complete Postman tutorial
│ ├── QUICK_REFERENCE.md # Quick reference guide
│ ├── CURL_EXAMPLES.md # cURL command examples
│ ├── API_TESTING_INDEX.md # Complete testing index
│ ├── api_tests.http # VS Code REST Client file
│ └── test_api.ps1 # PowerShell test script
└── README.md # This file
- blackjack-core: Pure domain logic, no external dependencies
- blackjack-service: Orchestration, concurrency, validation
- blackjack-api: HTTP, authentication, rate limiting, serialization
- blackjack-cli: Original terminal-based game (preserved for reference)
- Rust 1.75 or later
- Docker (optional, for containerized deployment)
# Clone the repository
git clone <repository-url>
cd rust_blackjack
# Build the workspace
cargo build --workspace
# Run tests
cargo test --workspace
# Run the API server (development mode)
RUST_LOG=debug cargo run -p blackjack-api
# Run the original CLI game
cargo run -p blackjack-cliThe API server will start on http://127.0.0.1:8080 by default.
Multiple options are available for testing the API endpoints:
Import the pre-configured Postman collection with all endpoints:
- Open Postman
- Click Import → Select files
- Import both:
docs/postman/Blackjack_API.postman_collection.json- Complete collectiondocs/postman/Blackjack_API_Local.postman_environment.json- Environment variables
- Select Blackjack API - Local environment
- Start with Create Game → Login → Draw Card
Features:
- ✅ Automatic token management (JWT saved automatically)
- ✅ Automatic game_id management
- ✅ Pre-configured requests with examples
- ✅ Test scripts with console logging
- ✅ Full documentation in each request
📖 See docs/postman/POSTMAN_GUIDE.md for detailed instructions
Use the .http file for quick testing in VS Code:
- Install the REST Client extension
- Open
docs/postman/api_tests.http - Click "Send Request" above each request
- Modify variables at the top of the file
Run the complete test suite automatically:
# Make sure the server is running first
.\docs\postman\test_api.ps1This script will:
- ✅ Test all endpoints in order
- ✅ Save variables automatically
- ✅ Show detailed colored output
- ✅ Test error scenarios
- ✅ Provide a complete summary
For command-line testing, see docs/postman/CURL_EXAMPLES.md with ready-to-use examples:
# Health check
curl http://localhost:8080/health | jq '.'
# Create game (save the game_id)
curl -X POST http://localhost:8080/api/v1/games \
-H "Content-Type: application/json" \
-d '{"emails":["player1@example.com"]}'
# More examples in docs/postman/CURL_EXAMPLES.md...All testing resources are located in the docs/postman/ directory:
| File | Purpose | Best For |
|---|---|---|
docs/postman/Blackjack_API.postman_collection.json |
Postman collection | Interactive testing, documentation |
docs/postman/Blackjack_API_Local.postman_environment.json |
Postman environment | Variable management |
docs/postman/POSTMAN_GUIDE.md |
Complete Postman guide | Learning the API flow |
docs/postman/api_tests.http |
REST Client file | Quick VS Code testing |
docs/postman/test_api.ps1 |
PowerShell test script | Automated full suite |
docs/postman/CURL_EXAMPLES.md |
cURL examples | Command-line reference |
docs/postman/QUICK_REFERENCE.md |
Quick reference guide | Fast lookup |
docs/postman/API_TESTING_INDEX.md |
Complete testing index | Navigation hub |
- Start the server:
cargo run -p blackjack-api - Health check: Verify server is running
- Create game: Get a
game_id - Login: Get a JWT token
- Play: Draw cards, change Ace values
- Finish: End game and see results
All testing tools follow this same flow with automatic variable management!
Configuration is loaded from crates/blackjack-api/config.toml and can be overridden with environment variables prefixed with BLACKJACK_.
[server]
host = "127.0.0.1"
port = 8080
[cors]
allowed_origins = ["http://localhost:3000"]
[jwt]
secret = "dev-secret-key-change-in-production"
expiration_hours = 24
[rate_limit]
requests_per_minute = 10
[api]
version_deprecation_months = 6Environment variables take precedence over config.toml:
# Server configuration
export BLACKJACK_SERVER_HOST=0.0.0.0
export BLACKJACK_SERVER_PORT=3000
# JWT configuration
export BLACKJACK_JWT_SECRET=your-production-secret-key
export BLACKJACK_JWT_EXPIRATION_HOURS=12
# Rate limiting
export BLACKJACK_RATE_LIMIT_REQUESTS_PER_MINUTE=20
# Logging (uses RUST_LOG standard)
export RUST_LOG=info
# or for detailed debugging:
export RUST_LOG=debugFor local development, copy .env.example to .env:
BLACKJACK_JWT_SECRET=dev-secret-change-in-production
BLACKJACK_SERVER_PORT=8080
RUST_LOG=debughttp://localhost:8080
All API endpoints are versioned under /api/v1.
- Health Checks:
/health,/health/ready - Authentication:
/api/v1/auth/register,/api/v1/auth/login - Game Lifecycle (M7): Create, browse open games, enroll, close enrollment
- Invitations (M7): Create, list pending, accept, decline
- Gameplay (M7): Turn-based draw, stand, game state
- Game Results: Finish game, get results
Basic health check.
Response (200 OK):
{
"status": "healthy",
"uptime_seconds": 3600,
"version": "0.1.0"
}Readiness check for orchestration systems (Kubernetes, etc.).
Response (200 OK):
{
"ready": true,
"checks": {
"memory": "ok",
"config": "loaded",
"future_sqlite": "pending",
"future_metrics": "pending"
}
}All passwords must meet the following complexity requirements:
- Minimum length: 8 characters
- Must contain:
- At least one uppercase letter (A-Z)
- At least one lowercase letter (a-z)
- At least one digit (0-9)
- At least one special character (!@#$%^&*)
Valid password examples:
MyP@ssw0rdSecure#Pass123Test!User2024
Register a new user account. (Milestone 7)
Request:
{
"email": "newplayer@example.com",
"password": "Secure#Pass123"
}Response (200 OK):
{
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"email": "newplayer@example.com",
"message": "User registered successfully"
}Errors:
400- Invalid email format400- Weak password (doesn't meet complexity requirements)- Error includes specific requirements that weren't met
409- Email already registered
Login with existing user credentials. (Milestone 7)
Request:
{
"email": "player1@example.com",
"password": "Secure#Pass123"
}Response (200 OK):
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_in": 86400
}Errors:
401- Invalid credentials (wrong email or password)403- Account inactive (account has been deactivated)404- User not found
Change user password. Requires authentication. (Milestone 8)
Headers:
Authorization: Bearer <jwt_token>
Request:
{
"old_password": "OldSecure#Pass123",
"new_password": "NewSecure#Pass456"
}Response (200 OK):
{
"message": "Password changed successfully"
}Errors:
401- Unauthorized (missing or invalid token)401- Invalid old password400- New password doesn't meet complexity requirements404- User not found
Create a new game with enrollment system. Requires authentication. (Milestone 7)
Headers:
Authorization: Bearer <jwt_token>
Request:
{
"enrollment_timeout_seconds": 300
}Response (200 OK):
{
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"creator_id": "user-uuid",
"message": "Game created successfully",
"player_count": 1,
"enrollment_closes_at": "2026-01-14T12:05:00Z"
}Errors:
401- Unauthorized (missing or invalid token)400- Invalid timeout value
Get list of games accepting enrollment. Requires authentication. (Milestone 7)
Headers:
Authorization: Bearer <jwt_token>
Response (200 OK):
{
"games": [
{
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"creator_id": "user-uuid",
"enrolled_count": 3,
"max_players": 10,
"enrollment_timeout_seconds": 300,
"time_remaining_seconds": 245,
"enrollment_closes_at": "2026-01-14T12:05:00Z"
}
],
"count": 1
}Errors:
401- Unauthorized
Enroll the authenticated user in a game. Requires authentication. (Milestone 7)
Headers:
Authorization: Bearer <jwt_token>
Request:
{
"email": "player2@example.com"
}Response (200 OK):
{
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"email": "player2@example.com",
"message": "Player enrolled successfully",
"enrolled_count": 2
}Errors:
401- Unauthorized404- Game not found409- Game is full (10 players max)410- Enrollment period has closed
Close enrollment and start the game. Only creator can close. (Milestone 7)
Headers:
Authorization: Bearer <jwt_token>
Request:
{}Response (200 OK):
{
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"message": "Enrollment closed, game ready to start",
"turn_order": ["player1@example.com", "player2@example.com", "player3@example.com"],
"player_count": 3
}Errors:
401- Unauthorized403- Only game creator can close enrollment404- Game not found
Kick a player from the game. Only creator can kick players. (Milestone 8)
Headers:
Authorization: Bearer <jwt_token>
Path Parameters:
game_id- The game UUIDplayer_id- The player's user UUID to kick
Response (200 OK):
{
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"player_id": "player-uuid",
"player_email": "player2@example.com",
"message": "Player player2@example.com kicked successfully"
}Errors:
401- Unauthorized403- Only game creator can kick players403- Cannot kick the game creator404- Game not found404- Player not found in game409- Can only kick players during enrollment phase
Get all participants in a game. Requires authentication. (Milestone 8)
Headers:
Authorization: Bearer <jwt_token>
Response (200 OK):
{
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"participants": [
{
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"email": "creator@example.com",
"role": "Creator"
},
{
"user_id": "660e8400-e29b-41d4-a716-446655440001",
"email": "player1@example.com",
"role": "Player"
}
],
"count": 2
}Errors:
401- Unauthorized404- Game not found
Send an invitation to another user to join the game. Requires authentication.
Headers:
Authorization: Bearer <jwt_token>
Request:
{
"invitee_email": "friend@example.com"
}Response (200 OK):
{
"invitation_id": "invite-uuid",
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"inviter_email": "player1@example.com",
"invitee_email": "friend@example.com",
"status": "pending",
"expires_at": "2026-01-14T12:05:00Z",
"message": "Invitation sent successfully"
}Errors:
401- Unauthorized403- You must be enrolled in the game to send invitations404- Game not found410- Enrollment period has closed
Get all pending invitations for the authenticated user. Requires authentication.
Headers:
Authorization: Bearer <jwt_token>
Response (200 OK):
{
"invitations": [
{
"invitation_id": "invite-uuid",
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"inviter_email": "player1@example.com",
"status": "pending",
"expires_at": "2026-01-14T12:05:00Z",
"created_at": "2026-01-14T12:00:00Z"
}
],
"count": 1
}Errors:
401- Unauthorized
Accept a game invitation. Requires authentication.
Headers:
Authorization: Bearer <jwt_token>
Response (200 OK):
{
"message": "Invitation accepted, you are now enrolled in the game",
"game_id": "550e8400-e29b-41d4-a716-446655440000"
}Errors:
401- Unauthorized404- Invitation not found409- Game is full410- Invitation has expired
Decline a game invitation. Requires authentication.
Headers:
Authorization: Bearer <jwt_token>
Response (200 OK):
{
"message": "Invitation declined"
}Errors:
401- Unauthorized404- Invitation not found
Get current game state. Requires authentication.
Headers:
Authorization: Bearer <jwt_token>
Response (200 OK):
{
"game_id": "550e8400-e29b-41d4-a716-446655440000",
"enrollment_open": false,
"current_turn_player": "player2@example.com",
"turn_order": ["player1@example.com", "player2@example.com", "player3@example.com"],
"players": {
"player1@example.com": {
"points": 18,
"state": "Standing",
"cards_history": [
{
"id": "card-uuid-1",
"name": "King",
"value": 10,
"suit": "Hearts"
},
{
"id": "card-uuid-2",
"name": "8",
"value": 8,
"suit": "Diamonds"
}
],
"busted": false
},
"player2@example.com": {
"points": 15,
"state": "Active",
"cards_history": [...],
"busted": false
}
},
"cards_in_deck": 46,
"finished": false
}Errors:
401- Unauthorized (missing or invalid token)404- Game not found
Draw a card for the authenticated player. Turn-based - validates current turn. (Milestone 7)
Headers:
Authorization: Bearer <jwt_token>
Response (200 OK):
{
"card": {
"id": "card-uuid",
"name": "Ace",
"value": 11,
"suit": "Spades"
},
"current_points": 21,
"busted": false,
"cards_remaining": 45,
"cards_history": [
{
"id": "card-uuid-1",
"name": "King",
"value": 10,
"suit": "Hearts"
},
{
"id": "card-uuid-2",
"name": "Ace",
"value": 11,
"suit": "Spades"
}
],
"is_finished": false,
"next_player": "player3@example.com"
}Errors:
401- Unauthorized404- Game or player not found409- Not your turn (NOT_YOUR_TURN) OR enrollment still open (ENROLLMENT_NOT_CLOSED)410- Deck is empty
Stand (end turn without drawing). Turn-based - validates current turn. (Milestone 7)
Headers:
Authorization: Bearer <jwt_token>
Response (200 OK):
{
"message": "Player stood successfully",
"current_points": 18,
"is_finished": false,
"next_player": "player3@example.com"
}Auto-finish Response (200 OK - when all players done):
{
"message": "Player stood successfully",
"current_points": 18,
"is_finished": true,
"winner": "player1@example.com",
"results": {
"winner": "player1@example.com",
"tied_players": [],
"highest_score": 21,
"all_players": {
"player1@example.com": {
"points": 21,
"cards_count": 2,
"busted": false
},
"player2@example.com": {
"points": 18,
"cards_count": 3,
"busted": false
}
}
}
}Errors:
401- Unauthorized404- Game not found409- Not your turn (NOT_YOUR_TURN)410- Enrollment still open
Change an Ace value between 1 and 11. Requires authentication.
Headers:
Authorization: Bearer <jwt_token>
Request:
{
"card_id": "card-uuid",
"as_eleven": false
}Response (200 OK):
{
"points": 11,
"busted": false
}Errors:
401- Unauthorized403- Game already finished404- Game, player, or card not found
Finish the game and calculate results. Requires authentication.
Headers:
Authorization: Bearer <jwt_token>
Response (200 OK):
{
"winner": "player1@example.com",
"tied_players": [],
"highest_score": 21,
"all_players": {
"player1@example.com": {
"points": 21,
"cards_count": 2,
"busted": false
},
"player2@example.com": {
"points": 19,
"cards_count": 3,
"busted": false
}
}
}Errors:
401- Unauthorized404- Game not found409- Game already finished
Get results of a finished game. Requires authentication.
Headers:
Authorization: Bearer <jwt_token>
Response: Same as POST /api/v1/games/:game_id/finish
Errors:
401- Unauthorized404- Game not found409- Game not finished yet
Here's a complete example using curl to play a game:
# 1. Create a new game
GAME_RESPONSE=$(curl -s -X POST http://localhost:8080/api/v1/games \
-H "Content-Type: application/json" \
-d '{
"emails": ["player1@example.com", "player2@example.com"]
}')
GAME_ID=$(echo $GAME_RESPONSE | jq -r '.game_id')
echo "Game created: $GAME_ID"
# 2. Login as player1
TOKEN1=$(curl -s -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d "{
\"email\": \"player1@example.com\",
\"game_id\": \"$GAME_ID\"
}" | jq -r '.token')
echo "Player1 authenticated"
# 3. Login as player2
TOKEN2=$(curl -s -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d "{
\"email\": \"player2@example.com\",
\"game_id\": \"$GAME_ID\"
}" | jq -r '.token')
echo "Player2 authenticated"
# 4. Player1 draws a card
curl -s -X POST "http://localhost:8080/api/v1/games/$GAME_ID/draw" \
-H "Authorization: Bearer $TOKEN1" | jq
# 5. Player1 draws another card
DRAW2=$(curl -s -X POST "http://localhost:8080/api/v1/games/$GAME_ID/draw" \
-H "Authorization: Bearer $TOKEN1")
echo "$DRAW2" | jq
# 6. If player1 got an Ace, change its value
ACE_ID=$(echo "$DRAW2" | jq -r '.card | select(.name == "A") | .id')
if [ "$ACE_ID" != "null" ] && [ "$ACE_ID" != "" ]; then
curl -s -X PUT "http://localhost:8080/api/v1/games/$GAME_ID/ace" \
-H "Authorization: Bearer $TOKEN1" \
-H "Content-Type: application/json" \
-d "{
\"card_id\": \"$ACE_ID\",
\"as_eleven\": false
}" | jq
fi
# 7. Player2 draws cards
curl -s -X POST "http://localhost:8080/api/v1/games/$GAME_ID/draw" \
-H "Authorization: Bearer $TOKEN2" | jq
curl -s -X POST "http://localhost:8080/api/v1/games/$GAME_ID/draw" \
-H "Authorization: Bearer $TOKEN2" | jq
# 8. View game state
curl -s "http://localhost:8080/api/v1/games/$GAME_ID" \
-H "Authorization: Bearer $TOKEN1" | jq
# 9. Finish the game
curl -s -X POST "http://localhost:8080/api/v1/games/$GAME_ID/finish" \
-H "Authorization: Bearer $TOKEN1" | jq
# 10. Get final results
curl -s "http://localhost:8080/api/v1/games/$GAME_ID/results" \
-H "Authorization: Bearer $TOKEN1" | jqStatus: ✅ COMPLETE (January 14, 2026)
The M7 implementation introduces turn-based multiplayer gameplay with user management and game invitations.
Completed (Jan 10, 2026):
- ✅
POST /api/v1/games- Create game with enrollment timeout - ✅
GET /api/v1/games/open- List games in enrollment phase - ✅
POST /api/v1/games/:game_id/enroll- Enroll player in game - ✅
POST /api/v1/games/:game_id/close-enrollment- Close enrollment and initialize turns
Completed (Jan 14, 2026):
- ✅
POST /api/v1/games/:game_id/invitations- Send game invitations - ✅
GET /api/v1/invitations/pending- List pending invitations - ✅
POST /api/v1/invitations/:id/accept- Accept invitation - ✅
POST /api/v1/invitations/:id/decline- Decline invitation - ✅
POST /api/v1/games/:game_id/stand- Stand and advance turn
Completed (Jan 14, 2026):
- ✅ Turn order system with automatic advancement
- ✅ Player state tracking (standing, busted, active)
- ✅ Smart turn skipping for inactive players
- ✅ Auto-finish when all players complete turns
Status: ✅ COMPLETE (January 15, 2026)
The M8 implementation adds enterprise-grade security features and enhanced user management capabilities.
Completed (Jan 15, 2026):
- ✅ Argon2id password hashing (OWASP parameters: 19 MiB memory, 2 iterations)
- ✅ Password complexity validation (8+ chars, uppercase, lowercase, digit, special)
- ✅ Email validation (RFC 5322 compliant)
- ✅ Constant-time password verification (timing attack protection)
- ✅ Security headers middleware (CSP, HSTS, X-Frame-Options, X-Content-Type-Options)
Completed (Jan 15, 2026):
- ✅ Role-Based Access Control (RBAC) with GameRole enum
- ✅ GamePermission system (5 permissions)
- ✅ Permission checking at service layer
- ✅ Participant tracking with roles (Creator, Player, Spectator)
Completed (Jan 15, 2026):
- ✅
POST /api/v1/auth/change-password- Change user password - ✅ Account activation/deactivation
- ✅ Login tracking (last_login timestamp)
- ✅ Enhanced User model with security fields
Completed (Jan 15, 2026):
- ✅
DELETE /api/v1/games/:game_id/players/:player_id- Kick player (creator only) - ✅
GET /api/v1/games/:game_id/participants- List game participants with roles
- Ordered Turns: Players take turns in sequence based on join order
- Turn Validation: Actions restricted to the current player's turn
- Auto-Advance: Turns automatically advance when player stands, busts, or finishes
- Smart Skipping: Turn system skips inactive players (standing/busted)
- User Registration: Create persistent user accounts with email/password
- User Authentication: Secure login system with JWT tokens
- User Profiles: User IDs linked to game sessions
- Creator Tracking: Games track which user created them
- Invitation System: Users can invite others to join games
- Timeout Control: Configurable invitation expiration (default: 5 minutes, max: 1 hour)
- Status Tracking: Pending, Accepted, Declined, Expired states
- Auto-Cleanup: Expired invitations automatically detected
pub struct User {
pub id: Uuid, // Unique user identifier
pub email: String, // Email address (unique)
pub password_hash: String, // Argon2id hashed password (M8)
pub is_active: bool, // Account status (M8)
pub last_login: Option<String>, // Last login timestamp (M8)
pub created_at: Option<String>, // Account creation timestamp
pub stats: Option<UserStats>, // Player statistics
}pub struct GameInvitation {
pub id: Uuid,
pub game_id: Uuid,
pub from_user_id: Uuid, // Who sent the invitation
pub to_user_id: Uuid, // Who receives it
pub status: InvitationStatus,
pub timeout_seconds: u64, // Configurable timeout
pub created_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
}
pub enum InvitationStatus {
Pending,
Accepted,
Declined,
Expired,
}pub enum PlayerState {
Active, // Currently playing
Standing, // Decided to stop drawing
Busted, // Exceeded 21 points
}Game Roles:
pub enum GameRole {
Creator, // User who created the game (all permissions)
Player, // Regular enrolled player (own actions only)
Spectator, // Future: read-only access
}Game Permissions:
InvitePlayers- Invite other users to join (Creator only)KickPlayers- Remove players from game (Creator only)CloseEnrollment- Manually close enrollment (Creator only)FinishGame- Manually finish game (Creator only)ModifySettings- Change game settings (Creator only)
Game Participant:
pub struct GameParticipant {
pub user_id: Uuid,
pub email: String,
pub role: GameRole,
pub joined_at: String,
}Access Control Methods:
// Check if user can perform an action
game.can_user_perform(user_id, permission) -> bool
// Get user's role in the game
game.get_participant_role(user_id) -> Option<GameRole>
// Check if user is the creator
game.is_creator(user_id) -> bool
// Check if user is a participant
game.is_participant(user_id) -> boolThe Game struct now includes:
turn_order: Vec<String>- Ordered list of player emailscurrent_turn_index: usize- Index of current player's turncreator_id: Uuid- User who created the game- Player states tracked via
PlayerStateenum
// Register new user with password validation and Argon2id hashing
user_service.register(email, password) -> Result<Uuid>
// Login with constant-time password verification
user_service.login(email, password) -> Result<User>
// Change password with validation
user_service.change_password(user_id, old_password, new_password) -> Result<()>
// Get user by ID
user_service.get_user(user_id) -> Result<User>// Create invitation with timeout (seconds)
invitation_service.create(from_user_id, to_user_id, game_id, timeout_seconds)
// Accept invitation
invitation_service.accept(invitation_id)
// Decline invitation
invitation_service.decline(invitation_id)
// Get pending invitations for user
invitation_service.get_pending_for_user(user_id)
// Cleanup expired invitations
invitation_service.cleanup_expired()// Create game now requires creator_id
game_service.create_game(emails, creator_id) -> Result<Uuid>
// Stand - player stops drawing cards
game_service.stand(game_id, email) -> Result<()>
// Turn management (automatic)
game.advance_turn() // Move to next active player
game.get_current_player() // Get email of current player
game.can_player_act(email) // Check if player can act now
game.check_auto_finish() // Auto-finish if all doneAdd to crates/blackjack-api/config.toml:
[invitations]
default_timeout_seconds = 300 # 5 minutes default
max_timeout_seconds = 3600 # 1 hour maximumEnvironment Variables:
export BLACKJACK_INVITATIONS_DEFAULT_TIMEOUT_SECONDS=300
export BLACKJACK_INVITATIONS_MAX_TIMEOUT_SECONDS=3600The JWT token claims now include user authentication:
pub struct Claims {
pub sub: String, // Subject (email)
pub exp: usize, // Expiration timestamp
pub user_id: Uuid, // NEW: User ID
pub game_id: Option<Uuid>, // Optional: Game ID (backward compatible)
}✅ Fully Backward Compatible:
- Existing endpoints continue to work unchanged
game_idin JWT claims is nowOptional<Uuid>- Login endpoint accepts both old format (email + game_id) and new format (email + password for user auth)
- Rate limiting updated to use
user_idinstead of game-specific limits
- Core data structures (User, GameInvitation, PlayerState)
- Game turn management logic
- UserService implementation (registration, login, get user)
- InvitationService implementation (create, accept, decline, cleanup)
- GameService updates for turn-based gameplay
- JWT Claims structure with user_id
- Configuration with invitation timeouts
- Error handling for new error types
- Middleware updates (rate limiting with user_id)
- Full workspace compilation
- REST API handlers for user registration/login
- REST API handlers for invitations (create, list, accept, decline)
- REST API handlers for turn-based actions
- Comprehensive tests (25+ tests per PRD)
- Postman collection updates
- Full API documentation
- Hashing Algorithm: Argon2id (OWASP recommended)
- Parameters:
- Memory cost: 19456 KiB (19 MiB)
- Time cost: 2 iterations
- Parallelism: 1 thread
- Random 16-byte salt per hash
- Security Features:
- Constant-time password verification (timing attack protection)
- Email format validation (RFC 5322)
- Password complexity validation
- Account status tracking (
is_activefield) - Last login timestamp tracking
- Automatic turn advancement when player stands or busts
- Game auto-finishes when all players are standing/busted
- Turns skip inactive players automatically
- Server-enforced maximum timeout (1 hour)
- Client can request shorter timeouts
- Expired invitations cleaned up on query
See docs/postman/ARCHITECTURE.md for detailed architecture and implementation overview.
# Build the image
docker build -t blackjack-api:latest .
# Run the container
docker run -p 8080:8080 \
-e BLACKJACK_JWT_SECRET=your-production-secret \
-e RUST_LOG=info \
blackjack-api:latestCreate a docker-compose.yml:
version: '3.8'
services:
api:
build: .
ports:
- "8080:8080"
environment:
- BLACKJACK_JWT_SECRET=${BLACKJACK_JWT_SECRET}
- BLACKJACK_SERVER_HOST=0.0.0.0
- RUST_LOG=info
restart: unless-stoppedRun with:
export BLACKJACK_JWT_SECRET=your-production-secret
docker-compose up -dThe project includes a GitHub Actions workflow (.github/workflows/ci.yml) that runs on every push and pull request:
- Test -
cargo test --workspace - Lint -
cargo clippy -- -D warnings - Format -
cargo fmt --check - Build -
cargo build --release - Docker Build - Multi-stage Docker image build
# Run all checks locally before pushing
cargo test --workspace
cargo clippy -- -D warnings
cargo fmt --check
cargo build --releaseThe API uses tracing for structured, contextual logging:
// Logs include context from instrumentation
tracing::info!(
game_id = %game_id,
player_count = player_count,
"Game created successfully"
);Control log verbosity with RUST_LOG:
error- Only errorswarn- Warnings and errorsinfo- Normal operations (recommended for production)debug- Detailed debugging informationtrace- Very verbose, includes all details
Example:
# Production
RUST_LOG=info cargo run -p blackjack-api
# Development
RUST_LOG=debug cargo run -p blackjack-api
# Specific module debugging
RUST_LOG=blackjack_api::handlers=debug,blackjack_service=info cargo run -p blackjack-apiUse health endpoints for monitoring:
# Basic liveness check
curl http://localhost:8080/health
# Readiness check (for Kubernetes probes)
curl http://localhost:8080/health/readyThe project has comprehensive test coverage across all layers:
# Run all tests
cargo test --workspace
# Run tests with output
cargo test --workspace -- --nocapture
# Run tests for specific crate
cargo test -p blackjack-core
cargo test -p blackjack-service
cargo test -p blackjack-api
# Run specific test
cargo test -p blackjack-api test_config_defaults
# Run doc tests
cargo test --doc- Core (19 tests): Game logic, card deck, player mechanics, winner calculation
- Service (12 tests): Game service, configuration, concurrency, error handling
- API (13 tests): Config loading, error conversion, rate limiting, authentication
- CLI (13 tests): Original CLI game tests
- Doc tests (17): Documentation examples validation
Total: 74 tests covering all critical paths.
Before deploying to production:
- Change JWT Secret: Set strong
BLACKJACK_JWT_SECRETvia environment variable - Configure CORS: Update
allowed_originsin config.toml or via env var - Set Log Level: Use
RUST_LOG=infoorwarnfor production - Enable HTTPS: Use a reverse proxy (nginx, Caddy) with TLS termination
- Set Rate Limits: Adjust
requests_per_minutebased on your needs - Monitor Logs: Integrate with log aggregation (ELK, Datadog, etc.)
- Health Checks: Configure orchestrator to use
/healthand/health/ready - Resource Limits: Set appropriate CPU and memory limits in container orchestrator
- Backup Strategy: Plan for future database backups (when SQLite is integrated)
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}The following features are planned but not yet implemented:
Blueprint is available in crates/blackjack-api/src/websocket.rs:
- Real-time game event broadcasting
- JWT authentication on connection
- Events: card drawn, Ace value changed, game finished
Migration files are prepared in crates/blackjack-service/migrations/:
- Persistent game state across restarts
- Player history and statistics
- Game replay capabilities
- Prometheus
/metricsendpoint - Counters: games created, cards drawn, rate limits hit
- Gauges: active games, total players
- Grafana dashboard templates
- Hot Configuration Reload: Using
notifycrate to watch config.toml - Input Validation: Using
validatorcrate for request validation - Secrets Management: Integration with HashiCorp Vault or AWS Secrets Manager
- API Versioning: Support for
/api/v2alongside/api/v1with deprecation headers - Admin Endpoints: Game management, player statistics, system metrics
See docs/PRD.md for the complete product roadmap.
- Create a feature branch
- Make your changes
- Run tests:
cargo test --workspace - Run clippy:
cargo clippy -- -D warnings - Format code:
cargo fmt - Create a pull request
- Follow Rust naming conventions
- Add doc comments to public APIs
- Include examples in doc comments
- Write tests for new features
- Keep functions focused and small
This project is licensed under the MIT License - see the LICENSE file for details.
Developed as a learning project for building production-ready REST APIs in Rust.
- Built with Axum - Ergonomic web framework
- Uses Tokio - Async runtime for Rust
- Logging with Tracing - Application-level tracing
- JWT with jsonwebtoken - JSON Web Token implementation
Prerequisites:
- Rust 1.75 or higher
- Cargo package manager
Build and run:
# Build the project
cargo build
# Run the game
cargo run
# Run tests
cargo testGameplay Example:
Welcome to the Card Game!
Enter the number of players: 2
Player 1's turn:
Do you want a card? [Y/n]: y
You got the card: 5
Your current points: 5
Do you want a card? [Y/n]: y
You got the card: A
You have an Ace! Do you want it to count as 11 points instead of 1? [Y/n]: y
Your current points: 16
Do you want a card? [Y/n]: n
Player 1 finished with 16 points.
Player 2's turn:
...
==========================
Game Results:
==========================
Player 1 wins with 16 points!
Finished.
- rand 0.9.2: Random card selection
This project is being transformed into a production-ready REST API backend system. See the complete Product Requirements Document (PRD) for detailed information about the planned evolution.
The system will be restructured into a Cargo workspace with multiple crates:
rust_blackjack/
├── crates/
│ ├── blackjack-core/ # Core game logic and domain models
│ ├── blackjack-service/ # Business logic and state management
│ ├── blackjack-api/ # REST API and HTTP handlers
│ └── blackjack-cli/ # Original CLI version (preserved)
├── docs/
│ └── PRD.md # Detailed implementation plan
└── README.md
- ✅ REST API: Versioned endpoints under
/api/v1 - ✅ JWT Authentication: Secure player identification
- ✅ Multi-player Games: Shared game state for 1-10 players per game ID
- ✅ 52-Card Deck: Realistic card deck with 4 suits
- ✅ Card History: Players can view all cards they've drawn
- ✅ Flexible Ace Values: Change Ace values multiple times during gameplay
- ✅ Rate Limiting: Prevent API abuse (configurable req/min)
- ✅ Health Checks:
/healthand/health/readyendpoints - ✅ Structured Logging: Tracing with contextual information
- ✅ External Configuration: TOML config + environment variables
- ✅ CI/CD Pipeline: Automated testing, linting, and Docker builds
- ✅ Production Ready: Docker support, CORS, error handling
See PRD.md for the complete implementation plan:
Completed Milestones:
- ✅ Milestone 1: Workspace Configuration and CI/CD
- ✅ Milestone 2: Core Crate (game logic)
- ✅ Milestone 3: Service Crate (state management)
- ✅ Milestone 4: API Crate (authentication & config)
- ✅ Milestone 5: REST Endpoints & Health Checks
- ✅ Milestone 6: Tests, Documentation & Docker
Completed Milestones (continued): 7. ✅ Milestone 7: Turn-Based Gameplay and User Management (COMPLETE - Jan 14, 2026)
- ✅ Phase 1: Game Enrollment Endpoints
- ✅ Phase 2A: Game Invitation Endpoints
- ✅ Phase 2B: Stand Endpoint
- ✅ Phase 3: PlayerState & Turn Management
- ✅ Phase 4: Additional Tests
- ✅ Milestone 8: Security Hardening and Enhanced User Management (COMPLETE - Jan 15, 2026)
- ✅ Argon2id Password Hashing
- ✅ Password Complexity Validation
- ✅ Role-Based Access Control (RBAC)
- ✅ Security Headers (CSP, HSTS, X-Frame-Options)
- ✅ Account Management (activation, deactivation, login tracking)
- ✅ Password Change Endpoint
- ✅ Kick Player Endpoint
- ✅ Get Participants Endpoint
Planned: 9. ⏳ Milestone 9: SQLite Persistence and Database Layer
Status: 167/167 tests passing | All features complete through M8 | Production-ready API
This project is currently under active development. Contributions are welcome once the backend architecture is established.
This project is licensed under the terms specified in the LICENSE file.
- Product Requirements Document (PRD): Complete technical specification for the backend transformation
- Current Implementation: See
src/main.rsfor the CLI version source code
| Version | Date | Description |
|---|---|---|
| 0.1.0 | 2025-12-23 | Initial CLI implementation with 1-10 player support |
| 1.0.0 | TBD | Backend REST API system (see PRD) |
Current Status: CLI Version Operational | Backend Development Planned
Next Steps: See Phase 1 in PRD