Skip to main content

webscheduler-auth-rbac

WebScheduler authentication, authorization (RBAC), session management, route filters, and auth hardening (inactivity monitor, session ping, failed-login lockout). Use whenever you're touching login/logout flows, role checks, permissions, session reads/writes, route filter configuration, the `/auth` controller, or anything involving `xs_users.role`, `xs_user_roles`, or `session()->get('user')`. Triggers on phrases like "auth", "login", "permission", "role", "RBAC", "session", "filter", "admin only", "provider can", "staff cannot", "logged in", "inactivity", "lockout", or any work in `app/Controllers/Auth.php` or `app/Filters/`.

Informations de source

Dépôt
niloc95/xscheduler_ci4
Dernière activité de la source
24 juillet 2026 à 12:05
Langue détectée de SKILL.md
anglais
Étoiles
1
Forks
0

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
webscheduler-auth-rbac
description
WebScheduler authentication, authorization (RBAC), session management, route filters, and auth hardening (inactivity monitor, session ping, failed-login lockout). Use whenever you're touching login/logout flows, role checks, permissions, session reads/writes, route filter configuration, the `/auth` controller, or anything involving `xs_users.role`, `xs_user_roles`, or `session()->get('user')`. Triggers on phrases like "auth", "login", "permission", "role", "RBAC", "session", "filter", "admin only", "provider can", "staff cannot", "logged in", "inactivity", "lockout", or any work in `app/Controllers/Auth.php` or `app/Filters/`.
# WebScheduler — Authentication & Authorization Contract ## Operational Roles - `admin` - `provider` - `staff` - `customer` ## Multi-Role Storage Model | Layer | Storage | Role | | --- | --- | --- | | Compatibility primary | `xs_users.role` | Legacy single-role fallback | | Authoritative membership | `xs_user_roles` | Real role membership set | `xs_user_roles` is the **authoritative** source. `xs_users.role` is the compatibility primary role only. Backfilled from `xs_users.role` in migration `2026-04-08-000001_CreateUserRolesTable.php`. ## Session Contract at Login Set by auth flow: - `role` — primary compatibility role - `roles` — full authoritative role array from `UserModel::getRolesForUser()` - `active_role` — highest privilege role for UI context **Read role membership from `roles` first. Use `role` only as fallback.** ## Canonical RBAC Pattern Use this shape everywhere: ```php $userRoles = $user['roles'] ?? [$user['role'] ?? '']; $hasAccess = !empty(array_intersect($requiredRoles, $userRoles)); ``` **Never make authorization decisions from `xs_users.role` alone.** **Fixed (2026-05-20):** `UserModel::canManageUser()` and `canViewUser()` previously used `$user['role'] === 'x'` comparisons — now use `UserModel::getRolesForUser(int $userId)` with `in_array(..., true)`. Any new authorization logic in `UserModel` must follow the same pattern. ## Session Write Contract (v140+ Mandatory) Any mid-session update to session user payload **must** preserve existing role context. **Required pattern:** ```php $currentUser = session()->get('user') ?? []; session()->set('user', array_merge($currentUser, [ // only fields being updated ])); ``` Only the **initial login path** may write a full user array from scratch. A direct `session()->set('user', [...])` overwrite mid-session is a regression and will be caught by the pre-merge grep check (see `rules` skill). ## Route Filter Contract Primary filters: - `setup` - `auth` — session only; redirects HTML callers to login - `role:admin` / `role:admin,provider` / `role:admin,provider,staff` — session only - `api_auth` — session **or** Bearer token; always JSON - `api_auth:admin,provider` — same, plus a role requirement **Any route intended to be externally callable must use `api_auth`, not `auth` or `role:`.** The session-only filters 401 a valid API token. See `api-contract` §Authentication for the token model and the header-beats-session precedence rule. ## API Token Identity A Bearer-token request has no session. `ApiAuthFilter` resolves the `xs_users` row the key is bound to, loads its authoritative `xs_user_roles`, and populates `App\Services\ApiIdentity` with **the same user array shape this contract's "Session Contract at Login" defines** — `role`, `roles`, `active_role` included. Consequence: the Canonical RBAC Pattern below is the *only* pattern needed. There is no separate token authorization path. The active-role hierarchy (`admin > provider > staff`) lives in exactly one place — `resolve_active_role()` in `app/Helpers/permissions_helper.php`, called by both `Auth::login()` and `ApiIdentity::setFromApiKey()`. Do not reimplement it inline. Read the acting identity via `current_identity_user()` / `current_user_id()` (token first, session second), never `session()->get('user_id')` directly, in any code reachable from an API route. ## Auth Hardening Contract (added 2026-05-12) ### Inactivity Warning Modal File: `resources/js/modules/auth/inactivity-monitor.js` - **Wall-clock based (2026-06-12 rewrite).** The monitor never trusts timer ticks — browsers throttle/suspend `setTimeout`/`setInterval` in background tabs and across machine sleep, which previously let the modal show a fake 5:00 countdown long after the server session had expired. All state is derived from `Date.now()` timestamps: `lastActivityAt` (updated on mouse / keyboard / scroll / touch events) and a warning **deadline** (`lastActivityAt + SESSION_MS`). - Idle state is re-evaluated every 30 s (`CHECK_INTERVAL_MS`) **and immediately on `visibilitychange` / `focus` / `pageshow`** so waking a laptop or returning to a backgrounded tab recomputes the truth at once: - elapsed ≥ 120 min → session window fully passed → redirect to `/auth/login` (no fake countdown). - elapsed ≥ 115 min (`SESSION_MS − WARNING_MS`) → show the modal; the countdown displays `deadline − now`, so throttled ticks can't stretch it. - otherwise → maybe fire a keepalive (below). - **Keepalive (drift fix):** the server expires the session 2 h after the last *HTTP request*, but client activity (mousemove/scroll) sends no requests. While the user has been active within the last 15 min, the monitor calls `GET /auth/ping` every 15 min (`KEEPALIVE_MS`) to slide the server window so the client model stays truthful. Idle users get no keepalive — that's the point of inactivity logout. - **Cross-tab sync:** `lastActivityAt` and the keepalive timestamp are shared via `localStorage` keys `xs-last-activity-at` / `xs-last-keepalive-at` (writes throttled to 5 s). Activity or "Stay Logged In" in one tab retracts a stale warning in another at the next check. - While the modal is visible, activity events are **ignored** — only an explicit "Stay Logged In" (server ping) may extend the session; mouse movement toward the button must not. - **"Stay Logged In"** pauses the countdown, calls `GET /auth/ping`, and only redirects to `/auth/login` after retryable failures are exhausted. - Retry semantics mirror other background auth surfaces: tolerate one transient `401` / network failure, wait 3 seconds, then retry once before treating the session as expired. - Because the modal is appended to `document.body` instead of `#spa-content`, SPA cleanup must remove the modal and clear any pending retry/countdown timers on `spa:leaving`; the next `initInactivityMonitor()` call recreates it. - **"Log Out"** → redirects to `/auth/logout`. - Countdown deadline passes → redirects to `/auth/login`. - Exported as `initInactivityMonitor()`, called from `initializeComponents()` in `app.js`. Init counts as both activity and keepalive (the page load / SPA navigation was itself a server request). - SPA-safe: removes listeners (activity + wake) and body-level modal state on `spa:leaving` to prevent double-binding and stale closures. ### Session Ping Endpoint - Route: `GET /auth/ping` → `Auth::ping()` (filter: `auth`) - Returns `{"ok": true, "expires_in": 7200}` for logged-in sessions; `401` otherwise. - Touching the session triggers CI4's `$timeToUpdate` (900s) regeneration, sliding the session cookie expiry. ### Failed-Login Lockout - CI4 Throttler (Token Bucket) — already in vendor, no new dependencies. - Guard is the **first statement** in `Auth::attemptLogin()`. - Limit: 5 attempts per 15 minutes (900s) per IP, keyed as `login_{md5(ip)}`. - On lockout: - AJAX → HTTP 429 + JSON - Form submit → redirect with `lockout_error` + `lockout_wait` flash - On successful login: `$throttler->remove($ipKey)` resets the bucket. - Login view renders `lockout_error` flash as a lock-icon error block with wait time in minutes. ## User Management Multi-Role Rules - Form model uses `roles[]` checkboxes. - Persist role membership to `xs_user_roles`. - Keep `xs_users.role` as compatibility primary role. - User list and APIs must render/return authoritative `roles[]`. ## Customer vs User Domain Split - `xs_users` — internal login users (admin / provider / staff / customer accounts) - `xs_customers` — booking customers (people who book appointments) - `xs_appointments.customer_id` → `xs_customers.id` These are **two distinct identity tables**. Do NOT conflate them. New logic should never use the deprecated appointment `user_id` linkage. ## Staff Scope Contract Canonical source: `ProviderStaffModel::getProvidersForStaff($userId, 'active')` When extracting provider IDs: ```php array_column($rows, 'id') ``` **Preferred appointment context key:** `provider_id` ## Provider Scope Contract (Customer Visibility) Use `CustomerAppointmentService` for all provider/staff customer scoping: - `resolveCustomerIdsForProvider(int $providerUserId)` - `resolveCustomerIdsForStaff(int $staffUserId)` - `getProviderIdsForStaff(int $staffUserId)` These methods enforce the rule that staff users only see customers belonging to providers they're assigned to via `xs_provider_staff_assignments`. Do NOT roll your own scoping query in controllers. ## Auth Email Channel Password reset emails use `NotificationCatalog::BUSINESS_ID_DEFAULT` (value `1`) as `$businessId`. `Auth::sendResetEmail()` renders the view, then calls `MailerService::send()`. Auth has no knowledge of SMTP configuration. See the `notifications` skill for the full `MailerService` transport contract. ## Profile Surface Notes - `/profile` is a live account surface backed by `App\Services\ProfilePageService`. Do not reintroduce placeholder summary cards or fake recent activity. - Profile mutations must preserve session role context via `array_merge` and write audit-log events for `user_updated`, `password_changed`, `profile_photo_updated`, and `notification_preferences_updated`. - Provider and staff notification preferences edited from `/profile` must persist to `xs_users.notify_on_appointments`. ## Pre-Merge Grep Checks (Auth-Related) Always run these before merging auth-related changes: ```bash # Detect accidental single-role auth checks rg "\$user\['role'\]\s*===|\$currentUser\['role'\]\s*==" app/ resources/ # Detect direct provider-role SQL assumptions rg "role\s*=\s*'provider'|WHERE\s+role\s*=\s*'provider'" app/ # Detect direct session user overwrites without merge rg "session\(\)->set\('user',\s*\[" app/ ``` Any result must be reviewed and justified or fixed.
Voir sur GitHub