50 lines
6.7 KiB
Markdown
50 lines
6.7 KiB
Markdown
# 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 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.
|