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.

Every clinic in XHealtXperience has its own isolated security configuration stored in a single tenant_settings row within the clinic’s tenant database. This means a policy change in one clinic never affects another. The TenantSetting model exposes these knobs to clinic administrators, while the application automatically reads them at runtime to enforce session expiry, lockout policies, and visitor check-in access.

The TenantSetting model

App\Models\TenantSetting is a standard Eloquent model that maps to the tenant_settings table inside the tenant database. The table always contains at most one row, accessed via TenantSetting::first() or TenantSetting::firstOrCreate([]).
class TenantSetting extends Model
{
    protected $table = 'tenant_settings';

    protected $fillable = [
        'inactivity_timeout_minutes',
        'max_login_attempts',
        'visitor_checkin_token',
    ];
}

Full schema

The tenant_settings table is built up across several tenant migrations:
ColumnTypeDefaultDescription
idbigintautoPrimary key
inactivity_timeout_minutesinteger15Minutes of inactivity before automatic session logout
max_login_attemptsunsignedTinyInteger3Failed login attempts allowed before account lockout
monto_penalizacion_cancelaciondecimal(10,2)0.00Late-cancellation fee (< 24 h before appointment)
contador_pacientesunsignedInteger0Atomic counter for generating sequential patient codes
visitor_checkin_tokenstring(64)nullToken authorizing the public visitor check-in page
Only inactivity_timeout_minutes, max_login_attempts, and visitor_checkin_token are in the model’s $fillable array. monto_penalizacion_cancelacion and contador_pacientes are managed by the application and are not exposed through mass assignment.

Configurable security settings

Inactivity timeout

inactivity_timeout_minutes controls how long an authenticated staff session may sit idle before the check_inactivity middleware automatically logs it out. The CheckInactivity middleware reads this value on every authenticated request:
// app/Http/Middleware/CheckInactivity.php
$config = TenantSetting::first();
$minutosPermitidos = $config?->inactivity_timeout_minutes ?? 15;

$ultimaActividad = session('ultima_actividad');

if ($ultimaActividad && now()->diffInMinutes($ultimaActividad) >= $minutosPermitidos) {
    Auth::guard('web')->logout();
    $request->session()->invalidate();
    // Redirect to /{tenantId}/login
}
When the timeout fires on an Inertia (XHR) request, the middleware returns a 409 response with an X-Inertia-Location header pointing to the tenant login page, triggering a full page reload on the client without a broken UI state.
The tenant_settings object (including inactivity_timeout_minutes) is shared with every Inertia page via HandleInertiaRequests::share(), so the React frontend can display a countdown or warning banner before the session expires — no extra API call needed.

Login lockout

max_login_attempts sets the threshold of consecutive failed login attempts before a staff user account is locked. Once locked, the user cannot log in until an Administrador Clinica unlocks them manually via PATCH /{tenantId}/usuarios/{usuario}/desbloquear.

Visitor check-in token

The visitor_checkin_token is a 40-character random string that authorizes a public, unauthenticated check-in page. This page is designed for a reception-area tablet where patients can confirm their arrival without any staff member needing to hand over a logged-in device.

How it works

The token is embedded in the public check-in URL:
/{tenantId}/citas/checkin/visitante/{token}
When a request arrives at this URL, VisitorCheckInController::verificarToken() compares the URL token against the stored value using hash_equals() (timing-safe comparison):
private function verificarToken(string $token): void
{
    $settings = TenantSetting::first();

    if (
        ! $settings ||
        ! $settings->visitor_checkin_token ||
        ! hash_equals($settings->visitor_checkin_token, $token)
    ) {
        abort(403, 'Enlace de check-in inválido o revocado.');
    }
}
If the token is missing from the database or does not match, the request is rejected with a 403. The three check-in sub-routes that use this protection are:
MethodPathDescription
GET/{tenantId}/citas/checkin/visitante/{token}Renders the check-in UI
GET/{tenantId}/citas/checkin/visitante/{token}/buscarSearches today’s appointments
POST/{tenantId}/citas/checkin/visitante/{token}/citas/{cita}Confirms patient check-in

Generating a QR code

Once visitor_checkin_token is populated, the Administrador Clinica can generate a QR code pointing to the full check-in URL and display it at the reception desk. The frontend reads the token from the tenant_settings prop shared by Inertia.

Regenerating the token

POST /{tenantId}/citas/checkin/visitante/regenerar
This route is restricted to Administrador Clinica. It replaces the stored token with a new 40-character random string using Str::random(40):
public function regenerarToken()
{
    $settings = TenantSetting::first() ?? TenantSetting::create([]);
    $settings->update(['visitor_checkin_token' => Str::random(40)]);

    return back()->with('message', 'Se generó un nuevo enlace de check-in. El anterior quedó revocado.');
}
The old token is immediately invalid — any tablet or printed QR code bearing the previous URL will receive a 403. This is the intended revocation mechanism if a device is lost or stolen.
Regenerating the token instantly breaks all existing QR codes printed or displayed in the clinic. Remember to update signage and tablets after regeneration.

The contador_pacientes field

contador_pacientes is managed automatically by the application and should never be edited manually. It is an atomic incrementing counter used to generate unique sequential patient codes in the format {CODIGO_CLINICA}{counter_padded} (e.g. STL00001, STL00002). Editing this value directly would create duplicate or non-sequential patient codes.

Updating security settings

The security settings form is available exclusively to users with the Administrador Clinica role.
POST /{tenantId}/configuracion/seguridad
The TenantSettingController::update() method validates the input and calls firstOrCreate([]) to ensure the settings row always exists before updating it:
public function update(Request $request): RedirectResponse
{
    $request->validate([
        'inactivity_timeout_minutes' => 'required|integer|min:1|max:1440',
        'max_login_attempts'         => 'required|integer|min:1|max:10',
    ]);

    $setting = TenantSetting::firstOrCreate([]);

    $setting->update([
        'inactivity_timeout_minutes' => $request->inactivity_timeout_minutes,
        'max_login_attempts'         => $request->max_login_attempts,
    ]);

    return back()->with('status', 'Configuración de seguridad actualizada correctamente.');
}

Security settings fields

inactivity_timeout_minutes
integer
required
Number of minutes of inactivity before a logged-in staff session is automatically terminated and the user is redirected to the clinic login page.
  • Minimum: 1
  • Maximum: 1440 (24 hours)
  • Default: 15
  • Enforced by: check_inactivity middleware on every authenticated tenant route
max_login_attempts
integer
required
Maximum number of consecutive failed login attempts before a user account is locked. The lockout is lifted only by an Administrador Clinica using the unlock route.
  • Minimum: 1
  • Maximum: 10
  • Default: 3

How settings reach the frontend

HandleInertiaRequests::share() reads TenantSetting::first() on every request made within an initialized tenant context and injects the result into the global Inertia props:
if (tenancy()->initialized) {
    $settings = \App\Models\TenantSetting::first();
    $tenantSettings = [
        'inactivity_timeout_minutes' => $settings?->inactivity_timeout_minutes ?? 15,
        'max_login_attempts'         => $settings?->max_login_attempts ?? 3,
        'visitor_checkin_token'      => $settings?->visitor_checkin_token,
    ];
}

return [
    // ...
    'tenant_settings' => $tenantSettings,
];
Every React page that receives the Inertia shared props has access to tenant_settings.inactivity_timeout_minutes and can use it to drive client-side idle timers, countdown banners, or QR code generation without any additional API requests.
The visitor_checkin_token is also included in the shared props so the Administrador Clinica dashboard can display the current check-in URL and offer a one-click regeneration button without a separate fetch.

Build docs developers (and LLMs) love