6.7 KiB
7-Point MCP Verification — Foundations + Users Management Module
Per claude-project-prompt.md's <mcp_rule>, 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 theOutletcontract.features/users/{UsersListPage.tsx, UsersListPage.mobile.tsx}— desktop uses a sortable/paginatedDataTable; mobile uses a card list. Both shareuseUsersRealtime(),usersApi.ts,UserFormModal,AssignRoleModal— logic shared, markup separate, per the spec's explicit instruction.UserFormModalrenders as a centeredModalon desktop and a bottomSheeton mobile (sameUserFormFieldsinside 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 Resourcecan_edit/can_deleteflags, frontendusePermission()nav/button gating. Verified auser-role account cannot see the Users nav item or any gated buttons; asuper-admincan. - Self-delete guard:
UserPolicy::deleteblocks it — and this caught a real bug: theGate::beforesuper-admin bypass initially overrode that guard, letting the Super Admin soft-delete their own account. Fixed by special-casing self-delete insideGate::beforeitself (app/Providers/AppServiceProvider.php) so it applies even to the bypass; re-verified 403 after the fix, account restored. - Secure headers:
SecurityHeadersmiddleware (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, HSTS-on-HTTPS) appended to theapimiddleware group; verified present on successful/api/v1/*responses. - CORS:
supports_credentials: true,allowed_originsrestricted toFRONTEND_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 withopenssl rand, never hardcoded; Mosquittopasswd/credentials.envgit-ignored. - File-upload hardening:
FileUploadValidatorscaffolded (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/subround-trip on both 1883 (MQTT) and confirmed 9001 (WebSocket) is reachable and used bymqtt.jsin the browser.- ACL denial verified directly: a
role_usercredential subscribes successfully (SUBACK) but receives zero messages published tomodule/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()/useMqttTopicinvalidating the relevant react-query cache. - Reconnect/token-refresh logic (
mqttClient.ts) re-fetches a new broker credential onclosebefore 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.