# RetroBoard API — Testing Documentation

> Base URL: `http://localhost:8000/api/v1`
> All requests that need auth require: `Authorization: Bearer {access_token}`
> All request bodies are JSON: `Content-Type: application/json`

---

## Quick Start

```bash
# 1. Copy env
cp .env.example .env
# Edit .env with your DB credentials and a real JWT_SECRET

# 2. Create DB and run schema
mysql -u root -p -e "CREATE DATABASE retroboard CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mysql -u root -p retroboard < sql/schema.sql

# 3. Start PHP dev server
php -S localhost:8000 -t public/

# 4. Run the test sequence below
```

---

## 1. Auth

### 1.1 Register (creates org + super_admin user)

```bash
curl -s -X POST http://localhost:8000/api/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "org_name": "Acme Corp",
    "name": "Ion Popescu",
    "email": "ion@acme.ro",
    "password": "SuperSecret123"
  }' | jq
```

**Expected 201:**
```json
{
  "access_token": "eyJ...",
  "refresh_token": "abc123...",
  "token_type": "Bearer",
  "expires_in": 900,
  "user": { "id": "uuid", "name": "Ion Popescu", "email": "ion@acme.ro", "role": "super_admin" },
  "organization": { "id": "uuid", "name": "Acme Corp", "slug": "acme-corp" }
}
```

**Save the tokens:**
```bash
ACCESS_TOKEN="eyJ..."
REFRESH_TOKEN="abc123..."
```

---

### 1.2 Login

```bash
curl -s -X POST http://localhost:8000/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "ion@acme.ro", "password": "SuperSecret123"}' | jq
```

**Expected 200** — same structure as register.

**Error cases:**
```bash
# Wrong password → 401
curl -s -X POST http://localhost:8000/api/v1/auth/login \
  -d '{"email":"ion@acme.ro","password":"wrong"}' -H "Content-Type: application/json"

# Non-existent email → 401 (same message, no enumeration)
curl -s -X POST http://localhost:8000/api/v1/auth/login \
  -d '{"email":"nobody@acme.ro","password":"wrong"}' -H "Content-Type: application/json"
```

---

### 1.3 Refresh Token

```bash
curl -s -X POST http://localhost:8000/api/v1/auth/refresh \
  -H "Content-Type: application/json" \
  -d "{\"refresh_token\": \"$REFRESH_TOKEN\"}" | jq
```

**Expected 200** — new access_token + new refresh_token (old one is revoked).

---

### 1.4 Get Current User

```bash
curl -s http://localhost:8000/api/v1/auth/me \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq
```

---

### 1.5 Logout

```bash
# Logout current device
curl -s -X POST http://localhost:8000/api/v1/auth/logout \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"refresh_token\": \"$REFRESH_TOKEN\"}"

# Logout all devices
curl -s -X POST http://localhost:8000/api/v1/auth/logout \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"all_devices": true}'
```

---

## 2. Teams

### 2.1 Create Team

```bash
curl -s -X POST http://localhost:8000/api/v1/teams \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Frontend Team", "description": "UI/UX and Angular"}' | jq
```

**Expected 201:**
```json
{ "data": { "id": "uuid", "name": "Frontend Team", "description": "...", "created_at": "..." } }
```

```bash
TEAM_ID="<id from response>"
```

---

### 2.2 List Teams

```bash
curl -s http://localhost:8000/api/v1/teams \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq
```

---

### 2.3 Get Team

```bash
curl -s http://localhost:8000/api/v1/teams/$TEAM_ID \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq
```

---

### 2.4 Update Team

```bash
curl -s -X PATCH http://localhost:8000/api/v1/teams/$TEAM_ID \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Frontend & Mobile Team"}' | jq
```

---

### 2.5 Add Member to Team

```bash
# First register a second user (member)
curl -s -X POST http://localhost:8000/api/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "org_name": "Acme Corp",
    "name": "Maria Ionescu",
    "email": "maria@acme.ro",
    "password": "SuperSecret123"
  }' | jq
# Note: this creates a NEW org. In real flow, invite via user_id within same org.
# For testing, use the user_id of a user already in the same org.

MEMBER_USER_ID="<user id>"

curl -s -X POST http://localhost:8000/api/v1/teams/$TEAM_ID/members \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"user_id\": \"$MEMBER_USER_ID\", \"role\": \"member\"}" | jq
```

---

### 2.6 List Team Members

```bash
curl -s http://localhost:8000/api/v1/teams/$TEAM_ID/members \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq
```

---

### 2.7 Remove Member

```bash
curl -s -X DELETE http://localhost:8000/api/v1/teams/$TEAM_ID/members/$MEMBER_USER_ID \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq
```

---

## 3. Templates

### 3.1 List Templates (built-in + org custom)

```bash
curl -s http://localhost:8000/api/v1/templates \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq
```

**Expected:** 4 built-in templates (Start/Stop/Continue, Went Well, Mad/Sad/Glad, 4Ls).

```bash
TEMPLATE_ID="00000000-0000-4000-a000-000000000001"  # Start/Stop/Continue
```

---

### 3.2 Create Custom Template

```bash
curl -s -X POST http://localhost:8000/api/v1/templates \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My Custom Retro",
    "description": "Custom 2-column",
    "columns": [
      {"id": "good", "name": "Good stuff", "color": "#22c55e", "order": 1},
      {"id": "bad",  "name": "Bad stuff",  "color": "#ef4444", "order": 2}
    ]
  }' | jq
```

---

## 4. Sessions

### 4.1 Create Session

```bash
curl -s -X POST http://localhost:8000/api/v1/sessions \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"team_id\": \"$TEAM_ID\",
    \"template_id\": \"$TEMPLATE_ID\",
    \"title\": \"Sprint 42 Retrospective\",
    \"description\": \"End of sprint retro\",
    \"is_anonymous\": false,
    \"votes_per_person\": 3,
    \"scheduled_for\": \"2026-05-01T14:00:00Z\"
  }" | jq
```

**Expected 201:**
```json
{
  "data": {
    "id": "uuid",
    "code": "ABCD1234",
    "title": "Sprint 42 Retrospective",
    "phase": "waiting",
    "join_url": "http://localhost/s/ABCD1234",
    ...
  }
}
```

```bash
SESSION_ID="<id from response>"
```

---

### 4.2 List Sessions

```bash
# All sessions in org
curl -s http://localhost:8000/api/v1/sessions \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq

# Filter by team
curl -s "http://localhost:8000/api/v1/sessions?team_id=$TEAM_ID" \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq

# Filter by phase
curl -s "http://localhost:8000/api/v1/sessions?phase=waiting" \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq
```

---

### 4.3 Get Session (with cards + participants)

```bash
curl -s http://localhost:8000/api/v1/sessions/$SESSION_ID \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq
```

---

### 4.4 Advance Phase: waiting → active

```bash
curl -s -X POST http://localhost:8000/api/v1/sessions/$SESSION_ID/phase \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" | jq
```

**Expected 200:**
```json
{
  "data": {
    "session_id": "uuid",
    "previous_phase": "waiting",
    "current_phase": "active",
    "transitioned_at": "2026-04-25T14:05:00+00:00"
  }
}
```

---

### 4.5 Join Session (as member)

```bash
curl -s -X POST http://localhost:8000/api/v1/sessions/$SESSION_ID/join \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq
```

---

## 5. Cards

### 5.1 Add Card (phase must be active)

```bash
curl -s -X POST http://localhost:8000/api/v1/sessions/$SESSION_ID/cards \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"column_id": "start", "content": "Start doing daily code reviews"}' | jq

curl -s -X POST http://localhost:8000/api/v1/sessions/$SESSION_ID/cards \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"column_id": "stop", "content": "Stop skipping standups"}' | jq

curl -s -X POST http://localhost:8000/api/v1/sessions/$SESSION_ID/cards \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"column_id": "continue", "content": "Continue pair programming sessions"}' | jq
```

```bash
CARD_ID="<id of first card>"
```

---

### 5.2 List Cards

```bash
curl -s http://localhost:8000/api/v1/sessions/$SESSION_ID/cards \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq
```

---

### 5.3 Edit Card

```bash
curl -s -X PATCH http://localhost:8000/api/v1/sessions/$SESSION_ID/cards/$CARD_ID \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"content": "Start doing daily code reviews — 30 min max"}' | jq
```

---

### 5.4 Advance to Voting Phase

```bash
curl -s -X POST http://localhost:8000/api/v1/sessions/$SESSION_ID/phase \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq
# phase: active → voting
```

---

### 5.5 Vote on Card (toggle)

```bash
# Add vote
curl -s -X POST http://localhost:8000/api/v1/sessions/$SESSION_ID/cards/$CARD_ID/vote \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq

# Expected: my_vote: true, votes_count: 1, votes_remaining: 2

# Toggle off (same request)
curl -s -X POST http://localhost:8000/api/v1/sessions/$SESSION_ID/cards/$CARD_ID/vote \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq

# Expected: my_vote: false, votes_count: 0, votes_remaining: 3
```

**Test vote limit:**
```bash
# Vote 3 cards (limit = 3)
# 4th vote should return 422 "No votes remaining"
```

---

### 5.6 Hide Card (manager only)

```bash
curl -s -X PATCH http://localhost:8000/api/v1/sessions/$SESSION_ID/cards/$CARD_ID/hide \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"is_hidden": true}' | jq

# Unhide
curl -s -X PATCH http://localhost:8000/api/v1/sessions/$SESSION_ID/cards/$CARD_ID/hide \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"is_hidden": false}' | jq
```

---

### 5.7 Delete Card

```bash
curl -s -X DELETE http://localhost:8000/api/v1/sessions/$SESSION_ID/cards/$CARD_ID \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq
```

---

## 6. Action Items

### 6.1 Advance to Actions Phase

```bash
# voting → actions
curl -s -X POST http://localhost:8000/api/v1/sessions/$SESSION_ID/phase \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq
```

---

### 6.2 Create Action Item

```bash
USER_ID="<your user id from /auth/me>"

curl -s -X POST http://localhost:8000/api/v1/sessions/$SESSION_ID/actions \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"title\": \"Implement daily code review process\",
    \"description\": \"Max 30 min per day, rotating reviewer\",
    \"owner_id\": \"$USER_ID\",
    \"due_date\": \"2026-05-15\",
    \"card_id\": \"$CARD_ID\"
  }" | jq
```

**Expected 201:**
```json
{
  "data": {
    "id": "uuid",
    "title": "Implement daily code review process",
    "owner_id": "uuid",
    "owner_name": "Ion Popescu",
    "due_date": "2026-05-15",
    "status": "open",
    "created_at": "..."
  }
}
```

```bash
ACTION_ID="<id from response>"
```

---

### 6.3 List Action Items

```bash
curl -s http://localhost:8000/api/v1/sessions/$SESSION_ID/actions \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq
```

---

### 6.4 Update Action Item Status

```bash
curl -s -X PATCH http://localhost:8000/api/v1/actions/$ACTION_ID \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status": "in_progress"}' | jq

curl -s -X PATCH http://localhost:8000/api/v1/actions/$ACTION_ID \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status": "done"}' | jq
```

---

### 6.5 Close Session

```bash
# actions → closed
curl -s -X POST http://localhost:8000/api/v1/sessions/$SESSION_ID/phase \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq
```

---

## 7. Notifications

```bash
# List (paginated)
curl -s "http://localhost:8000/api/v1/notifications?page=1" \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq

# Mark one as read
NOTIF_ID="<notification id>"
curl -s -X PATCH http://localhost:8000/api/v1/notifications/$NOTIF_ID/read \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq

# Mark all as read
curl -s -X POST http://localhost:8000/api/v1/notifications/read-all \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq
```

---

## 8. Error Reference

| HTTP Code | Meaning |
|---|---|
| 200 | OK |
| 201 | Created |
| 204 | No Content |
| 400 | Bad Request |
| 401 | Unauthorized (missing/invalid/expired token) |
| 403 | Forbidden (valid token, insufficient role) |
| 404 | Resource not found |
| 405 | Method not allowed |
| 409 | Conflict (duplicate email, already member, etc.) |
| 422 | Unprocessable (validation error) |
| 500 | Internal server error |
| 503 | Database unavailable |

All errors return:
```json
{ "error": "Human-readable message" }
```

---

## 9. Security Notes

| Mechanism | Implementation |
|---|---|
| Password hashing | `password_hash()` with `PASSWORD_ARGON2ID` (memory: 64MB, time: 4 iterations) |
| JWT signature | HMAC-SHA256 with 64-char secret |
| JWT expiry | Access token: 15 min. Refresh token: 30 days |
| Refresh token storage | Only SHA-256 hash stored in DB, never raw value |
| Token rotation | Every refresh issues new pair, old refresh token revoked immediately |
| User enumeration | Login always takes same time regardless of email existence |
| SQL injection | 100% prepared statements via PDO, zero string interpolation in queries |
| XSS | All user input sanitized with `htmlspecialchars()` before storage |
| GUID IDs | UUID v4 via `random_bytes(16)` — unpredictable, non-sequential |
| Session codes | 8-char alphanumeric from `random_bytes(8)`, no ambiguous characters |
| CORS | Configurable via `CORS_ORIGIN` env variable |
| Security headers | `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy` |
| Org isolation | Every query filters by `org_id` from JWT payload |
| Constant-time comparison | `hash_equals()` for JWT signature verification |

---

## 10. Health Check

```bash
curl -s http://localhost:8000/api/v1/health | jq
# {"status":"ok","timestamp":"2026-04-25T14:00:00+00:00"}
```
