# SEC1-3 — MFA (TOTP) + recovery codes + service tokens + idle logout

Read `_COMMON.md` first. Do NOT edit `Modules/Core/database/seeders/RolePermissionSeeder.php` or any Clinic file (other agents own them); if you need a new permission, report it. Migration range `2026_11_03_150000`–`150099`. The orchestrator has run `composer require pragmarx/google2fa` (use `PragmaRX\Google2FA\Google2FA`). QR rendering is on the FE (`qrcode` npm package already installed). Spec: SEC1 row of `../master-plan.md` («MFA بتطبيق Authenticator», «خروج تلقائي بعد خمول», tokens) + `../design-gyn-integration.md` security section (grep SEC1).

## Do (BE, Modules/Core)
1. Migration on `users`: `two_factor_secret` text nullable (encrypted cast), `two_factor_recovery_codes` text nullable (encrypted json of HASHED codes), `two_factor_confirmed_at` timestamp nullable.
2. `TwoFactorService`: enable (generate secret, return otpauth URI `otpauth://totp/MoonERP:{email}?secret=…&issuer=MoonERP`), confirm(code) → sets confirmed_at + generates 8 recovery codes (returned ONCE in plain), disable(password+code), verify(user, code) with ±1 window and replay protection (cache the last used timestep per user), useRecoveryCode (one-time).
3. Endpoints (auth:sanctum, new controller `TwoFactorController`, add routes in `Modules/Core/routes/auth.php` inside the authenticated group): `POST auth/2fa/enable`, `POST auth/2fa/confirm`, `POST auth/2fa/disable`, `GET auth/2fa/status`, `POST auth/2fa/recovery-codes` (regenerate, needs current code).
4. Login challenge (`AuthController::login`): when the matched user has confirmed 2FA, do NOT issue the token; return 200 `{ two_factor_required: true, challenge: <signed short-lived token (encrypt user id + expiry 5 min, e.g. Crypt or a cache key)> }`. New `POST auth/2fa/challenge {challenge, code|recovery_code}` → issues the token exactly like login does today (reuse `issueTokenForRequest`). Throttle challenge (5/min).
5. Company policy setting `security.require_2fa_roles` (json list of role names; default []) — a user in such a role who has not enrolled gets `{ two_factor_setup_required: true }` in the login response alongside the token (FE forces the enroll screen). Add the definition to `Modules/Core/database/seeders/SettingDefinitionSeeder.php`.
6. **Service tokens:** `POST auth/service-tokens {name}` (permission core.users.manage or the existing equivalent — read RolePermissionSeeder) mints a NON-expiring token named `service:{name}` for the current user, listed/revoked through the existing sessions API. This unblocks turning on login-token expiry later.
7. Idle logout: setting `security.idle_logout_minutes` (int, default 0 = off) exposed in `GET auth/me` payload (read AuthController::me) — enforcement is FE-side.

## Do (FE, `/home/moonui3/public_html/moon-erp`)
- Profile/security page section (find the existing profile/sessions screen — `features/profile` or similar; add a «التحقق بخطوتين» card): enable → show QR (qrcode lib) + manual secret → confirm code → show recovery codes once with copy/print.
- Login: handle `two_factor_required` → code step (6 digits, or «استخدام كود استرداد»); handle `two_factor_setup_required` → route to enroll.
- Idle logout: a small service listening to user activity; after `idle_logout_minutes` of inactivity show a 60-second countdown dialog then log out (skip when 0).
- ar/en i18n via small targeted edits (another agent is adding keys to the same files) — do NOT bump I18N_VERSION (orchestrator does at deploy). `ng build` green. Consult a Fable agent (model "fable") once for the login code step + enroll card visual design if the existing auth screens don't give a clear pattern.

## Tests
`Modules/Core/tests/Feature/Sec1MfaTest.php`: enable/confirm with a code generated by Google2FA from the secret; login with 2FA returns challenge, challenge+code issues token, wrong code 422, replayed code 422, recovery code works once; disable; service token has no expires_at; require_2fa_roles flag. Run also `Sec1SessionHardeningTest.php`, `SessionApiTest.php` (one pre-existing failure there is known — report if more), `UserApiTest.php`.

## Out of scope
Clinic module, attachments, HistoryService.
