# Phase 2 Summary — Authentication & Administration

This phase built on Phase 1's architecture and database without
redesigning anything: one additive migration (`0013`), the same MVC
conventions, the same coding standards. Every mechanism described below
was run against a live MySQL 8 database and a real PHP built-in server
during development — not just written and assumed correct.

## What was built

### Authentication
| Feature | Where |
|---|---|
| Login | `AuthController::login()` + `AuthService::attempt()` |
| Logout | `AuthController::logout()` |
| Forgot Password | `AuthController::sendResetLink()` + `PasswordResetService` |
| Reset Password | `AuthController::resetPassword()` + `PasswordResetService::reset()` |
| Remember Me | `AuthService::issueRememberToken()` / `attemptRememberToken()` — selector/validator pattern, one-time-use rotation |
| Email Verification | `EmailVerificationService` — first-time + email-change re-verification share one token table |
| Password Expiration | `PasswordPolicyService::expiryStatus()`, surfaced on the Dashboard and at login |
| Password Policy | `PasswordPolicyService::validateComplexity()` + `isReused()` against `password_histories`, both config-driven |
| Session Management | `App\Core\Session` (Phase 1) + `SessionService` (Phase 2: list/revoke) |
| Multi-session Detection | `SessionService::hasMultipleSessions()` / `exceedsConcurrentLimit()` |
| Two-Factor Auth | `TwoFactorService` — real RFC 6238 TOTP, **verified against the official RFC test vectors**, not force-enabled for any role by default |

### User Management
| Feature | Where |
|---|---|
| Users | `UserController` + `UserModel` + `UserService` |
| Roles | `RoleController` + `RoleModel` |
| Permissions | `PermissionController` + `PermissionModel` (matrix read/write) |
| Departments | `DepartmentController` + `DepartmentModel` |
| Teams | `TeamController` + `TeamModel` (+ membership) |
| Staff | Filterable via Users' role/department filters — no separate table; see Gap Analysis note below |
| User Profile | `ProfileController` + `app/views/profile/show.php` |
| Profile Photo | `UserService::updateAvatar()` + `UploadHelper` (real MIME sniffing, random filename) |
| Preferences | `user_preferences` table, `ProfileController::updatePreferences()` |

### Security
| Feature | Where |
|---|---|
| RBAC | `App\Core\Rbac` (Phase 1), fully exercised: role grants + per-user overrides |
| Permission Middleware | `PermissionMiddleware` (Phase 1), attached to every admin route |
| Route Protection | Every admin route carries `auth` + `permission:{module}.{action}` |
| Activity Logging | `ActivityLogger::record()` called from every write action |
| Login History | `login_attempts` table, surfaced via `ActivityLogController::loginHistory()` |
| Failed Login Tracking | `users.failed_login_count`, incremented in `AuthService::registerFailedAttempt()` |
| Account Lock | `users.locked_until`, set once `config('security.account_lockout')` threshold is hit |
| Password History | `password_histories` table, `PasswordPolicyService::isReused()` |
| Session Timeout | `Session::enforceIdleTimeout()` (Phase 1) |
| CSRF | `CsrfMiddleware` (Phase 1), attached to every state-changing route |
| Audit Trail | `activity_logs` table, four categories matching the Activity Logs page exactly |

### Admin
| Feature | Where |
|---|---|
| Users CRUD | `UserController` (invite/update/deactivate/reactivate/delete) |
| Roles CRUD | `RoleController` (system roles are protected from deletion) |
| Permission Matrix | `PermissionController::matrix()` / `updateRolePermissions()` |
| Assign Roles | Part of `UserController::store()`/`update()` |
| Assign Permissions | `PermissionController::updateRolePermissions()` (role-level); `user_permission_overrides` table exists for future per-user overrides |
| Deactivate Users | `UserController::deactivate()` — also revokes every active session |
| Reset Password (admin) | `UserController::resetPassword()` → `UserService::adminResetPassword()` — sends a real reset link, never a plaintext password |

## What was actually tested (not just written)

- **TOTP correctness**: generated codes matched the official RFC 6238
  test vectors exactly (`287082` at T=1, `081804` at T=37037036) using
  a known ASCII test secret, before ever trusting it with a real one.
- **Account lockout**: scripted 7 consecutive failed logins against a
  real seeded user and confirmed `failed_login_count`/`locked_until`
  updated correctly in MySQL and that a locked account is rejected
  even with the correct password.
- **2FA challenge flow**: full two-step login (password → pending
  session marker → code verification → real session) run end-to-end,
  including a deliberately wrong code being rejected first.
- **Remember-me**: token issued, verified, confirmed one-time-use (a
  second attempt with the same cookie value correctly fails).
- **Full HTTP flow**: `GET /login` → extract live CSRF token from the
  rendered page → `POST /login` with real credentials → session
  cookie persists → `GET /dashboard` succeeds → a real `POST
  /departments` write lands in MySQL with the correct `created_by` →
  a wrong CSRF token is correctly rejected with 419 → a user without
  `user_management.view` is correctly rejected with 403 on `/users`
  while still reaching `/dashboard` with 200.
- **Migrations**: the full 13-migration chain applies cleanly against
  a brand-new database, is idempotent on a second run, and reproduces
  identically via `database/build_migrations.php` regeneration.

## Known simplifications (intentional, documented, not oversights)

- **"Staff"** from the brief is not a separate table — it's the Users
  list filtered by role/department, since every "staff member" is
  already a `users` row with a role. A dedicated staff-scheduling
  concept (shifts, availability) belongs to Phase 3's Appointments
  module, not here.
- **Permission Matrix UI** currently saves at the "manage this whole
  module" granularity per role (matching Phase 1's frontend checkbox
  design), even though the underlying `permissions` table has
  per-action granularity (`view`/`create`/`update`/`delete`/...). The
  API (`PermissionModel::syncRolePermissions()`) already accepts any
  mix of individual permission ids — only the current UI's save button
  computes "all actions in this module" client-side. A future,
  finer-grained UI is a frontend change, not a backend one.
- **Team member picker** in `teams/index.php` takes a raw user ID
  rather than a searchable name autocomplete — flagged inline in that
  view's own markup as a Phase 3 frontend polish item, not a backend
  gap (the API already accepts any valid user id).

## Phase 3 readiness

Nothing here needs to be revisited to build Leads, Customers,
Companies, Appointments, Campaigns, Reports, Email, or AI — they all
plug into the exact same foundation:

1. A new module's routes carry `auth` + a new
   `permission:{module}.{action}` slug. The `permissions` table
   already supports adding new modules — extend
   `config/permissions.php`'s module list and re-run
   `RolePermissionSeeder` (or a targeted follow-up seeder) to grant the
   new module's permissions to existing roles.
2. Every future entity (leads, customers, appointments, ...) already
   has its table in the Phase 1 schema, with `business_line_id`
   scoping ready for query filtering by the logged-in user's
   `Auth::businessLineSlugs()`.
3. `ActivityLogger::record()` and `NotificationService::notify()` are
   ready to be called from any new controller exactly as they already
   are from `UserController`/`DepartmentController`/etc.
4. The sidebar partial (`app/views/partials/sidebar.php`) already
   RBAC-gates the Administration section — add new nav links there
   the same way, gated by the new module's `.view` permission.
5. The `data-ab-table` client-side filtering pattern from Phase 1's
   static frontend has been fully replaced, in every page this phase
   touched, with real server-side search/filter/pagination via
   `Paginator` + AJAX. Phase 3's list pages should follow
   `UserController::index()` / `app/views/users/index.php` as the
   reference implementation for that pattern.
