# PlayerAuth Module — Architecture Map

> **Last updated:** 2026-06-10
> **Status:** Living document — incrementally updated as new information is discovered

---

## 1. Purpose & Responsibilities

The PlayerAuth module handles **authentication for the player-facing mobile API**. It is a small, focused module providing:

- Phone + password login
- OTP-based passwordless login (request + verify)
- Token refresh (revoke old, issue new)
- Logout (token revocation)
- Token lifecycle management (creation, expiry, max per player)

It does **NOT** manage:
- Player registration (PlayerAffairs handles that)
- API business logic (PlayerApi handles that)
- Employee/staff authentication (Auth module handles that)
- Token validation on each request (PlayerApiAuthMiddleware handles that)

---

## 2. Directory & File Structure

```
app/Modules/PlayerAuth/
├── bootstrap.php                              # Permission registration (player_auth.manage_tokens)
├── ApiRoutes.php                              # 5 API auth routes
├── Controllers/
│   └── Api/
│       └── PlayerTokenController.php          # Login, OTP, refresh, logout
└── Services/
    └── PlayerAuthService.php                  # Auth logic: find player, verify, token CRUD
```

---

## 3. Database Schema (Production — Source of Truth)

### 3.1 `player_tokens` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | int unsigned | NO | PRI | auto_increment |
| player_id | int unsigned | NO | MUL | FK to players |
| token | varchar(64) | NO | UNI | 32-byte hex token (64 chars) |
| name | varchar(100) | NO | | Default: 'mobile' |
| device_info | varchar(255) | YES | | Device description from client |
| push_token | varchar(500) | YES | | Push notification token (future) |
| last_used_at | datetime | YES | | Updated by middleware on each request |
| expires_at | datetime | YES | MUL | Token expiry (default: 90 days) |
| is_revoked | tinyint(1) | NO | | Default: 0 |
| created_at | datetime | NO | | |

### 3.2 Auth-related columns on `players` table (owned by PlayerAffairs)

| Column | Type | Purpose |
|--------|------|---------|
| phone | varchar(30) | Login identifier |
| password_hash | varchar(255) | bcrypt hash for password login |
| otp_code | varchar(6) | Current OTP (cleared after use) |
| otp_expires_at | datetime | OTP valid until (5 minutes) |
| last_login_at | datetime | Updated on successful token creation |

---

## 4. Routes (5 total — all API, no web routes)

| Method | Path | Handler | Middleware | Notes |
|--------|------|---------|-----------|-------|
| POST | /api/v1/player/auth/login | PlayerTokenController@login | cors | Phone + password |
| POST | /api/v1/player/auth/otp/request | PlayerTokenController@otpRequest | cors | Request OTP SMS |
| POST | /api/v1/player/auth/otp/verify | PlayerTokenController@otpVerify | cors | Verify OTP + issue token |
| POST | /api/v1/player/auth/refresh | PlayerTokenController@refresh | cors, player_auth | Rotate token |
| POST | /api/v1/player/auth/logout | PlayerTokenController@logout | cors, player_auth | Revoke token |

**Key design**: Login and OTP routes do NOT require `player_auth` middleware (since the player doesn't have a token yet). Refresh and logout DO require it.

---

## 5. Events Dispatched

None. PlayerAuth does not dispatch any events.

---

## 6. Events Consumed

None. PlayerAuth does not listen to any events.

---

## 7. Cross-Module Dependencies

### 7.1 PlayerAuth IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| (None) | Only uses core framework classes (App, Database, ApiController) |

### 7.2 Other modules that depend on PlayerAuth:

| Module | Dependency Type |
|--------|----------------|
| PlayerApi (all endpoints) | Requires valid token issued by PlayerAuth |
| PlayerApiAuthMiddleware | Reads `player_tokens` table created/managed by PlayerAuth |

### 7.3 Shared state:

| Resource | Written By | Read By |
|----------|-----------|---------|
| `player_tokens` table | PlayerAuth (create, revoke) | PlayerApiAuthMiddleware (validate) |
| `players.password_hash` | (manual/admin setup) | PlayerAuth (verify) |
| `players.otp_code`, `players.otp_expires_at` | PlayerAuth (generate, clear) | PlayerAuth (verify) |
| `players.last_login_at` | PlayerAuth (on token creation) | Profile display |

---

## 8. Permissions

| Key | Description (Arabic) |
|-----|---------------------|
| player_auth.manage_tokens | Manage player tokens (admin permission, not used in current routes) |

Note: This permission exists in the registry but is not currently enforced on any route. The auth endpoints are public (login/OTP) or self-service (refresh/logout via `player_auth` middleware).

---

## 9. Core Business Flows

### 9.1 Phone + Password Login

```
1. POST /api/v1/player/auth/login {phone, password, device_info?}
2. findPlayerByPhone(phone) → SELECT from players WHERE phone=? AND is_archived=0
3. verifyPassword → password_verify(input, password_hash)
4. If invalid: 401 "رقم الهاتف أو كلمة المرور غير صحيحة"
5. createToken(player_id, 'mobile', device_info):
   a. Check active token count vs max (5)
   b. If at max: revoke oldest (by last_used_at)
   c. Generate: bin2hex(random_bytes(32)) → 64-char token
   d. Set expires_at: now + 90 days (configurable: api.token.player_ttl_hours)
   e. INSERT into player_tokens
   f. UPDATE players SET last_login_at = NOW()
6. Return {token, expires_at, player: {id, name, phone}}
```

### 9.2 OTP Login Flow

```
1. POST /api/v1/player/auth/otp/request {phone}
   a. findPlayerByPhone(phone)
   b. If not found: return success anyway (prevent enumeration)
   c. generateOtp(player_id):
      - Generate 6-digit random code (000000-999999)
      - Store in players.otp_code, set otp_expires_at = now + 5 minutes
   d. TODO: Send via SMS (not yet integrated)
   e. Return {sent: true}

2. POST /api/v1/player/auth/otp/verify {phone, otp, device_info?}
   a. findPlayerByPhone(phone)
   b. verifyOtp: check otp_code matches AND otp_expires_at > now
   c. Clear OTP fields (otp_code=null, otp_expires_at=null)
   d. createToken (same as password login flow)
   e. Return {token, expires_at, player: {id, name, phone}}
```

### 9.3 Token Refresh

```
1. POST /api/v1/player/auth/refresh (requires valid token in Authorization header)
2. Get current token from header, get player from middleware
3. Revoke current token (is_revoked = 1)
4. Create new token with same parameters
5. Return {token, expires_at}
```

### 9.4 Logout

```
1. POST /api/v1/player/auth/logout (requires valid token)
2. Revoke the bearer token used in the request
3. Return {logged_out: true}
```

---

## 10. Token Security Model

| Property | Value | Configurable |
|----------|-------|-------------|
| Token format | 64-char hex (32 random bytes) | No |
| TTL | 90 days (2160 hours) | Yes: `api.token.player_ttl_hours` |
| Max tokens per player | 5 | Yes: `api.token.max_per_player` |
| Eviction policy | Revoke oldest by last_used_at | No |
| Storage | Plain text in DB | No (no hashing) |
| OTP TTL | 5 minutes | No (hardcoded) |
| OTP format | 6 digits zero-padded | No |
| Password hashing | PHP password_verify (bcrypt) | No |

---

## 11. High-Risk Areas

### 11.1 Token Stored in Plain Text
- Tokens are stored as-is in `player_tokens.token` column
- If DB is compromised, all tokens are immediately usable
- Standard practice is to hash tokens (store hash, compare hash)
- Mitigation: tokens are 256-bit random, DB access implies full compromise anyway

### 11.2 OTP SMS Not Integrated
- `generateOtp()` stores the code but no SMS is actually sent
- The TODO comment in the controller confirms this
- Until integrated, OTP login is non-functional in production
- The OTP is stored in plain text on the players table

### 11.3 No Rate Limiting
- Login endpoint has no brute-force protection
- OTP request has no rate limiting (could spam SMS once integrated)
- No account lockout after failed attempts
- Mitigation needed: implement rate limiting per IP/phone

### 11.4 Phone Enumeration (Partially Mitigated)
- OTP request: returns `{sent: true}` even if phone not found (good)
- Login: returns specific "phone or password incorrect" error (somewhat protected)
- No timing attack mitigation (short-circuit on missing player)

### 11.5 Token Max Limit Eviction
- When a player hits 5 active tokens, the oldest by `last_used_at` is revoked
- This could unexpectedly log out a device the player is still using
- No notification to the player when a token is evicted

---

## 12. Known Patterns & Gotchas

1. **No web routes**: This module is entirely API — no admin UI for token management despite the registered permission
2. **Shared players table**: Auth fields (password_hash, otp_code, otp_expires_at, last_login_at) live on the `players` table alongside profile data — no separate auth table
3. **Middleware lives outside module**: `PlayerApiAuthMiddleware` is at `app/Middleware/`, not inside PlayerAuth module — it reads `player_tokens` and sets `App::currentPlayer()`
4. **Token validation not in this module**: The middleware (not PlayerAuth) is responsible for checking token validity on each request — PlayerAuth only creates/revokes
5. **Config path**: Token TTL and max are read from `config('api.token')` — if config file is missing, defaults to 2160 hours / 5 tokens
6. **CORS middleware**: Auth routes use only `cors` (no `player_auth`) — important for login/OTP endpoints to be accessible without a token
7. **Player lookup uses `name_ar`**: The `findPlayerByPhone` query selects `name_ar` but the actual column is `full_name_ar` — potential mismatch (SELECT may return null for that alias unless there's a DB alias/view)

---

## Appendix: Files to Check Before Modifying PlayerAuth

1. `app/Middleware/PlayerApiAuthMiddleware.php` — Token validation logic, reads player_tokens
2. `app/Modules/PlayerApi/ApiRoutes.php` — All endpoints depend on player_auth middleware
3. `app/Core/Router.php:109` — Maps 'player_auth' middleware name to PlayerApiAuthMiddleware class
4. `config/api.php` — Token TTL and max_per_player configuration
5. `app/Core/App.php` — `setCurrentPlayer()` / `currentPlayer()` methods used by middleware and controllers
