# 7-Point MCP Verification — Foundations + Users Management Module Per `claude-project-prompt.md`'s ``, run after this implementation batch (plan: `imperative-toasting-tome.md`). ## 1. Architecture consistency `backend/` is API-only (`routes/api.php`, versioned under `/v1`) — no server-rendered views for app data. `frontend/` is a Vite React SPA consuming it exclusively over `/api/v1/*` and `/sanctum/*`. Business logic lives in `app/Actions/*` (Login, VerifyTwoFactor, CreateUser, UpdateUser, DeleteUser, AssignRole), not in controllers. Controllers stay thin (validate via Form Requests → call an Action → fire an event → return a Resource). ## 2. Mobile-first compliance `MobileShell` (`frontend/src/layouts/shell/MobileShell.tsx`) — top bar + bottom nav — and `DesktopShell` were built together in the same pass (task 7), not desktop-first with mobile bolted on. `AppShell` picks between them via `useIsMobile()` at a single decision point. Verified in-browser both directions: resizing/reloading at 375×812 renders `MobileShell`, at 1280×720 renders `DesktopShell`, with identical data and functionality (login, Users CRUD, dashboard widgets) in both. ## 3. Separate mobile/desktop view integrity - `layouts/shell/{MobileShell,DesktopShell}.tsx` — distinct files, shared only via the `Outlet` contract. - `features/users/{UsersListPage.tsx, UsersListPage.mobile.tsx}` — desktop uses a sortable/paginated `DataTable`; mobile uses a card list. Both share `useUsersRealtime()`, `usersApi.ts`, `UserFormModal`, `AssignRoleModal` — logic shared, markup separate, per the spec's explicit instruction. - `UserFormModal` renders as a centered `Modal` on desktop and a bottom `Sheet` on mobile (same `UserFormFields` inside either). ## 4. Security compliance - **CSRF**: Sanctum SPA cookie auth + `X-XSRF-TOKEN`; verified a stale token gets a 419, a fresh one succeeds. - **Rate limiting**: login throttled 5/min/IP+identifier (verified: 6th attempt in a minute returns "Too many login attempts"); general API throttle 60/min (`RateLimiter::for('api', ...)`). - **No user enumeration**: invalid login always returns the same generic message. - **2FA**: TOTP (`pragmarx/google2fa`) + 10 single-use hashed recovery codes; verified full browser flow (enable → QR → confirm → recovery-code login) and that a used recovery code is consumed. - **RBAC enforcement, 4 layers**: route middleware, `UserPolicy` (view/create/update/delete), API Resource `can_edit`/`can_delete` flags, frontend `usePermission()` nav/button gating. Verified a `user`-role account cannot see the Users nav item or any gated buttons; a `super-admin` can. - **Self-delete guard**: `UserPolicy::delete` blocks it — **and this caught a real bug**: the `Gate::before` super-admin bypass initially overrode that guard, letting the Super Admin soft-delete their own account. Fixed by special-casing self-delete inside `Gate::before` itself (`app/Providers/AppServiceProvider.php`) so it applies even to the bypass; re-verified 403 after the fix, account restored. - **Secure headers**: `SecurityHeaders` middleware (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, HSTS-on-HTTPS) appended to the `api` middleware group; verified present on successful `/api/v1/*` responses. - **CORS**: `supports_credentials: true`, `allowed_origins` restricted to `FRONTEND_URL` (no wildcard). - **Audit logging**: covers login, logout, 2FA enable/verify/disable, and every Users CRUD + role-assign action — verified via the dashboard's Recent Activity feed and directly via `/api/v1/dashboard/activity`. - **Secrets**: DB/MQTT/Super-Admin credentials all via `.env`, generated with `openssl rand`, never hardcoded; Mosquitto `passwd`/`credentials.env` git-ignored. - **File-upload hardening**: `FileUploadValidator` scaffolded (mime-sniffing, size cap, randomized filenames) ahead of any actual upload feature. ## 5. RBAC/permissions compliance Roles `super-admin`/`admin`/`manager`/`user` seeded with `{module}.{action}`-named permissions. Verified via tinker that the seeded Super Admin holds all 8 permissions and the `Gate::before` bypass grants even an undefined permission. Verified via the Users module UI that role reassignment (manager → admin) immediately changes the permission set returned by the API and reflected in the UI. MQTT topic ACLs add a fifth enforcement layer (role-scoped topics — see point 6). ## 6. Real-time/MQTT compliance Topic taxonomy (`private/user/{id}/notifications`, `role/{role}/alerts`, `module/{module}/events`, `broadcast/system`) enforced via Mosquitto's `acl.conf`. Verified end-to-end: - `mosquitto_pub`/`sub` round-trip on both 1883 (MQTT) and confirmed 9001 (WebSocket) is reachable and used by `mqtt.js` in the browser. - ACL denial verified directly: a `role_user` credential subscribes successfully (SUBACK) but receives zero messages published to `module/users/events`, which it isn't authorized for. - Full Users CRUD cycle (create/update/assign-role/delete) each published a distinct MQTT event, captured live via `mosquitto_sub`. - Browser-side: creating/editing/deleting a user through the UI produced a live toast and an up-to-date list/dashboard with **zero manual refresh**, via `useUsersRealtime()`/`useMqttTopic` invalidating the relevant react-query cache. - Reconnect/token-refresh logic (`mqttClient.ts`) re-fetches a new broker credential on `close` before mqtt.js's automatic reconnect retries. - **Documented limitation, not silently dropped**: per-role static Mosquitto credentials give role-level topic isolation only, not per-user; true per-user isolation needs a JWT-validating auth plugin (e.g. `mosquitto-go-auth`), deferred as an explicit follow-up per the approved plan. ## 7. Database portability All migrations use the Eloquent schema builder exclusively — no raw SQL anywhere. `users` adds a `public_id` UUID (used for route-model binding and all API-facing IDs, never the numeric PK), soft deletes, and a self-referential `created_by` FK. `audit_logs` is append-only (`UPDATED_AT = null`, no soft deletes) with a polymorphic `auditable` relation. `spatie/laravel-permission` and Sanctum's own migrations are vendor-maintained and already Postgres-portable. Verified `migrate:fresh` succeeds cleanly against SQLite; nothing in the schema (indexes, FKs, JSON casts) is SQLite-specific. --- ## Known follow-ups (flagged, not silently dropped) - Per-user MQTT topic isolation (JWT-based Mosquitto ACLs) — deferred per plan. - Production TLS/reverse-proxy for the API and MQTT WebSocket listener — out of scope for this LXC-dev-focused pass, required before any non-local exposure. - Frontend bundle exceeds Vite's 500KB chunk-size warning threshold (mqtt.js + react-query + axios); worth code-splitting later, not a functional issue now.