# AUTH-003 — Password Reset Flow
## Implementation Memory

| Field | Value |
|-------|-------|
| Task | AUTH-003 — Password Reset Flow |
| Sprint | Sprint 2 — Authentication & Authorization (FINAL task) |
| Status | ✅ Implementation Complete — Awaiting Human Approval |
| Depends On | AUTH-001 (50e9b19), AUTH-002 (499f8c2), AUTH-004 (94ea225) |

---

## 1. Files Created

| File | Purpose |
|------|---------|
| `app/views/auth/forgot_password.php` | Forgot Password form view |
| `app/views/auth/reset_password.php` | Reset Password form view (with token hidden field + confirm field) |

## 2. Files Modified

| File | Change Summary |
|------|----------------|
| `app/modules/Auth/AuthController.php` | 4 new actions: `showForgotPassword`, `processForgotPassword`, `showResetPassword`, `processResetPassword` |
| `app/modules/Auth/AuthService.php` | 3 public + 5 private methods for token generation, validation, and password update |
| `app/modules/Auth/AuthModel.php` | 2 new queries: `findActiveUserByEmailForReset()` (crew excluded), `updatePasswordHash()` |
| `app/modules/Auth/routes.php` | 4 new routes: GET/POST `/forgot-password`, GET/POST `/reset-password/{token}` |

---

## 3. Key Design Decisions

### Token Storage
- Tokens stored as `storage/cache/reset_token_{64-hex}.json`
- Payload: `{ user_id, email, expires_at }`
- Token NOT stored in payload (filename IS the token — existence = validation)
- `isValidTokenFormat()`: `ctype_xdigit()` + length 64 → prevents path traversal

### Email Simulation
- Reset link written to `storage/logs/reset_links_{date}.log`
- Only the link is written — email address masked as `***`
- No SMTP integration per Sprint 2 scope

### User Enumeration Prevention
- `requestPasswordReset()` always returns the same generic message
- "If your email address is registered, you will receive a reset link shortly."
- Even invalid emails get `success: true` with the generic message

### Crew Exclusion
- `findActiveUserByEmailForReset()` filters `r.name IN ('couple', 'super_admin')`
- Crew passwords are managed via the Couple portal (Sprint 3+)
- View hint text informs users of this constraint

### Security Chain
```
random_bytes(32) → bin2hex() → 64-char hex token
hash_equals($password, $confirm) → constant-time comparison
password_hash($password, PASSWORD_BCRYPT) → new hash
deleteTokenFile() BEFORE returning success → replay prevention
```

### View Location Deviation
- SPRINT2_PLANNING §5 specifies `app/modules/Auth/views/`
- Framework resolves views from `app/views/` (BaseController::render())
- AUTH-001 established the `app/views/auth/` pattern (login.php already there)
- Views created at `app/views/auth/` for framework consistency
- Documented as deviation

---

## 4. Route Registration Summary

| Method | Pattern | Controller Action | Middleware |
|--------|---------|-------------------|-----------|
| GET | `/forgot-password` | `showForgotPassword` | none |
| POST | `/forgot-password` | `processForgotPassword` | `CsrfMiddleware` |
| GET | `/reset-password/{token}` | `showResetPassword` | none |
| POST | `/reset-password/{token}` | `processResetPassword` | `CsrfMiddleware` |

---

## 5. AuthService Method Map

| Method | Visibility | Purpose |
|--------|-----------|---------|
| `requestPasswordReset(email, ip)` | public | Token generation entry point |
| `resetPassword(token, pass, confirm, ip)` | public | Token validation + password update |
| `loadResetToken(token)` | public | Pre-validate token for GET form render |
| `resetTokenFilePath(token)` | private | Resolves cache file path |
| `writeTokenFile(token, payload)` | private | Persists token JSON |
| `readTokenFile(token)` | private | Reads token JSON |
| `deleteTokenFile(token)` | private | Invalidates token |
| `isValidTokenFormat(token)` | private | ctype_xdigit + length=64 guard |
| `writeResetLinkLog(userId, email, link)` | private | Simulated email delivery to log |
