Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Arthurr23/XHealtXperience/llms.txt

Use this file to discover all available pages before exploring further.

XHealtXperience enforces two-factor authentication (2FA) as a first-class security control. The EnsureTwoFactorAuthenticated middleware (registered as two_factor) is applied to every protected route — including tenant clinic routes and the Super Admin panel. Once a user enables 2FA, they must successfully verify their identity on every new login session before accessing any protected resource. Two authentication methods are supported: a TOTP authenticator app (powered by pragmarx/google2fa-laravel) and an Email OTP delivering a 6-digit code directly to the user’s registered email address.

User Model Fields

The following fields on the User model track the full lifecycle of a user’s 2FA configuration:
FieldTypePurpose
two_factor_secretstring|nullEncrypted TOTP secret key. null when using email-only method.
two_factor_confirmed_atdatetime|nullTimestamp of the last successful confirmation. null means 2FA is not active.
two_factor_methodstringActive method: 'app' (TOTP) or 'email'. Defaults to 'app'.
two_factor_email_codestring|nullEncrypted current email OTP code. Never exposed in API responses.
two_factor_email_expires_atdatetime|nullExpiry timestamp for the current email code (valid for 5 minutes).
The helper methods on the model simplify status checks:
// Returns true only when two_factor_confirmed_at is not null
$user->hasTwoFactorEnabled(); // bool

// Decrypts and returns the TOTP secret, or null if not set
$user->twoFactorSecret(); // ?string

Supported Methods

TOTP — Authenticator App

The TOTP method uses pragmarx/google2fa-laravel to generate time-based one-time passwords compatible with any RFC 6238 app, including Google Authenticator, Authy, and 1Password. A unique secret key is generated per user, encrypted at rest in the database, and rendered as a QR code during setup. The QR code label is formatted as {User Name} ({Role}) · {Clinic Name} so it is immediately identifiable in an authenticator app’s list, even if the app truncates the label.

Email OTP

The email method sends a 6-digit code to the user’s registered email address. The code is generated with random_int, stored encrypted in two_factor_email_code, and expires after 5 minutes. Codes are single-use: a successful verification wipes two_factor_email_code and two_factor_email_expires_at from the database immediately.
When a user with the email method arrives at the 2FA challenge screen after login, a fresh code is sent automatically — but only if no valid unexpired code already exists. This prevents a flood of emails if the user refreshes the challenge page.

Setting Up 2FA — TOTP (App) Flow

1

Navigate to the 2FA setup page

The user opens their profile and navigates to the 2FA configuration screen. The frontend calls:
GET /{tenant}/two-factor/setup
# Central (Super Admin):
GET /panel-global/two-factor/setup
The response includes twoFactorEnabled, twoFactorPending, twoFactorMethod, and otpauthUrl (the QR code URL, or null if no secret exists yet).
2

Generate the TOTP secret

The user selects the Authenticator App method and submits the enable form. The server generates a secure random secret key, encrypts it, and saves it to two_factor_secret. The method is set to 'app' and two_factor_confirmed_at is cleared (pending state).
POST /{tenant}/two-factor/enable
# Central (Super Admin):
POST /panel-global/two-factor/enable
The page reloads with a QR code rendered from otpauthUrl. The user scans this code with their authenticator app.
3

Scan the QR code

The user scans the displayed QR code using Google Authenticator or any compatible TOTP app. The app starts generating 6-digit codes that rotate every 30 seconds.
4

Confirm the first code

The user enters the 6-digit code from their app to verify the secret was imported correctly. The server validates the code using Google2FA::verifyKey() with a 1-window drift tolerance.
POST /{tenant}/two-factor/confirm
Body: { "code": "123456" }
On success, two_factor_confirmed_at is set to the current timestamp and the session receives auth.two_factor_verified = true. A two_factor.enabled event is written to the audit log.
5

2FA is now active

The user’s 2FA status is confirmed. From this point forward, every new login session will redirect to the challenge screen before granting access to protected routes.

Setting Up 2FA — Email OTP Flow

1

Select the email method

The user navigates to the 2FA setup page and selects Email. The frontend submits:
POST /{tenant}/two-factor/enable-email
The server sets two_factor_method = 'email' and clears two_factor_confirmed_at. A confirmation code is sent immediately to the user’s registered email address.
2

Confirm the emailed code

The user enters the 6-digit code they received by email:
POST /{tenant}/two-factor/confirm
Body: { "code": "483920" }
The server decrypts the stored code, checks it against the input using constant-time comparison (hash_equals), and verifies it has not expired. On success, two_factor_confirmed_at is set and the audit log records two_factor.enabled.

The Challenge Flow (Post-Login Verification)

After a successful login, if two_factor_confirmed_at is not null, the user has not yet passed the 2FA challenge for this session. Any attempt to visit a protected route triggers the two_factor middleware, which:
  1. Saves the originally-intended URL to session('url.intended').
  2. Redirects the user to the challenge screen.
POST /login  →  (2FA enabled)  →  GET /{tenant}/two-factor/challenge

                              User enters 6-digit code

                          POST /{tenant}/two-factor/challenge

                    session('auth.two_factor_verified') = true

                        redirect → session('url.intended')
                               or → /{tenant}/dashboard
If the user’s preferred method is email, a code is dispatched automatically when they land on the challenge page (provided no valid code exists already). The user can also request a fresh code on demand via:
POST /{tenant}/two-factor/send-code

Disabling 2FA

A user can disable 2FA from their profile settings. The request requires their current password as confirmation:
DELETE /{tenant}/two-factor/disable
Body: { "password": "current_password" }
On success, all 2FA fields are cleared, the session flag is removed, and the event two_factor.disabled is written to the audit log.

Changing the Active Method

Users with an active 2FA setup can switch between app and email. Switching always clears two_factor_confirmed_at, requiring re-confirmation with a code from the new method before it becomes active:
POST /{tenant}/two-factor/method
Body: { "method": "email" }   # or "app"

Cancelling a Pending Setup

If a user starts the setup flow but does not complete the confirmation step, they can cancel and return to the unenrolled state:
POST /{tenant}/two-factor/cancel-setup
This only takes effect if two_factor_confirmed_at is still null (i.e., the setup is genuinely incomplete). If 2FA is already active, this endpoint is a no-op.

Full Route Reference

The following table lists all 2FA-related routes. Tenant routes are prefixed with /{tenant}. The equivalent Super Admin (central) routes are prefixed with /panel-global.
MethodTenant PathCentral PathHandlerDescription
GET/two-factor/challenge/two-factor/challengeshowChallengeRender the post-login code challenge page
POST/two-factor/challenge/two-factor/challengeverifySubmit and verify the challenge code
POST/two-factor/send-code/two-factor/send-codesendCodeSend (or resend) an email OTP on demand
GET/two-factor/setup/panel-global/two-factor/setupshowSetupRender the 2FA management/setup page
POST/two-factor/enable/panel-global/two-factor/enableenableGenerate TOTP secret and begin app setup
POST/two-factor/enable-email/panel-global/two-factor/enable-emailenableEmailSwitch to email method and send confirmation code
POST/two-factor/confirm/panel-global/two-factor/confirmconfirmConfirm the first code to activate 2FA
POST/two-factor/method/panel-global/two-factor/methodsetMethodChange the active method (requires re-confirmation)
DELETE/two-factor/disable/panel-global/two-factor/disabledisableDisable 2FA (requires current password)
POST/two-factor/cancel-setup/panel-global/two-factor/cancel-setupcancelSetupAbort a pending (unconfirmed) setup
The challenge routes (GET/POST /two-factor/challenge and POST /two-factor/send-code) are intentionally registered outside the two_factor middleware group. They live only inside the auth middleware group. If they were placed inside the two_factor group, the middleware would intercept them — detecting an unverified session — and redirect back to the challenge, creating an infinite redirect loop. All other 2FA management routes (setup, enable, confirm, disable) are inside the two_factor group because the user must already have passed the challenge to reach their profile settings.

How the Middleware Decides

The EnsureTwoFactorAuthenticated middleware (two_factor) follows this decision tree on every request:
Request arrives at a protected route

Is a user authenticated?         No  → pass through (let 'auth' handle it)
        ↓ Yes
Does the user have 2FA enabled?  No  → pass through (no code required)
        ↓ Yes
Is auth.two_factor_verified = true in session?   Yes → pass through
        ↓ No
Is the current path in the excluded list?        Yes → pass through (avoid loop)
        ↓ No
→ Save intended URL to session → Redirect to /{tenant}/two-factor/challenge
Excluded paths (never blocked by the middleware):
*/two-factor/challenge
*/logout
*/login
two-factor/challenge   (central Super Admin)
login
logout

Build docs developers (and LLMs) love