# Security Architecture & Admin Permissions

## Authentication
- **Customers**: no auth required for guest purchases. Optional accounts use Laravel Sanctum (SPA/token).
- **Admins**: session auth + mandatory TOTP 2FA (e.g. `pragmarx/google2fa-laravel`) for any role above Viewer. Login rate-limited (`throttle:login`, 5/min/IP) with account lockout after repeated failures, logged to `login_logs`.

## Roles (spatie/laravel-permission recommended, not hand-rolled)
| Role | Can view secrets | Can edit pricing | Can delete transactions | Can manage admins |
|---|---|---|---|---|
| Super Admin | yes | yes | yes | yes |
| Admin | no (redacted) | yes | no | no |
| Finance Manager | no | yes (pricing only) | no | no |
| API Manager | yes (own scope) | no | no | no |
| Support Agent | no | no | no | no |
| Accountant | no | no | no | no |
| Viewer | no | no | no | no |

Enforce via **Policies** (`TransactionPolicy`, `ApiCredentialPolicy`, etc.), not just UI hiding — every controller action calls `$this->authorize()`. Secret fields (`api_key`, `secret_key`, webhook secrets) are `$hidden` on the model regardless of role; a dedicated "reveal" endpoint requires Super Admin + re-auth + writes an audit log entry.

## Request-level protections
- CSRF: Laravel default, webhook routes explicitly exempted (they use signature verification instead) and nothing else is.
- Rate limiting: `throttle:purchase` (per IP + phone) on guest checkout, `throttle:login` on admin auth, per-provider limiter on outbound VTU calls to avoid tripping upstream limits.
- Input validation: Form Request classes for every mutating endpoint; phone numbers validated against Nigerian MSISDN patterns; amounts re-derived server-side from `data_plans`, never trusted from the client.
- SQLi/XSS: Eloquent/query builder only (no raw string interpolation), Blade auto-escaping, CSP header on admin dashboard.
- IDOR: guest transaction status lookup requires reference **and** phone match (see `DataPurchaseController::status`), not reference alone.

## Webhook security (applies to every gateway/provider webhook)
1. Verify signature against the raw request body (not the re-serialized payload — signatures break if JSON key order changes).
2. Look up `payments.gateway_reference`; if already `verified`, log as duplicate and return 200 without reprocessing.
3. Never trust the webhook's own amount/status field — call the gateway's `verify()` endpoint independently.
4. Log every inbound webhook (valid or not) to `webhook_logs` with secrets redacted.

## Secrets management
- All credential columns use Laravel's `encrypted` cast (AES-256-CBC via `APP_KEY`), `$hidden` on the model, and are excluded from `Log::` calls by a custom log-context redactor listening on `MessageLogged`.
- `.env` holds `APP_KEY`, DB credentials, and cache/queue config only — gateway/provider credentials live in the encrypted DB columns so Super Admin can rotate them without a deploy.

## Fraud/velocity controls
- `fraud_events` table + `FraudService` checks per request: phone-number transaction count in last hour, IP transaction count, amount vs plan's `max_purchase`, and a mismatch check between paid amount and order amount (already enforced in `TransactionService::handlePaymentVerified`).
- Flagged transactions move to `PENDING_REVIEW` service_status instead of being silently blocked, so Support/Finance can clear false positives.

## Idempotency & concurrency
- `transactions.idempotency_key` is unique at the DB level — a duplicate order attempt fails fast rather than relying only on app logic.
- Every state transition in `TransactionService` runs inside `DB::transaction()` with `lockForUpdate()` on the row, preventing two webhook deliveries (or a webhook + a manual retry) from both dispatching a VTU purchase.
