# MVNexus — Full Application Audit Report

**Audit date:** 2026-05-18  
**Auditor role:** Senior QA, security review, product audit, principal full-stack  
**Method:** Codebase inspection, `route:list`, migrations, automated tests, lint/build, grep for dead auth and quality markers. No feature development beyond safe fixes documented in §10.

---

## 1. Executive summary

MVNexus is a **modular Laravel 13 + React 19 SPA** help desk with **OTP / magic-link / invite authentication** (Sanctum cookie sessions), ticketing, communication (replies, notes, attachments), SLA, notifications, search/filters, settings, dashboard, audit logs, and Reverb realtime.

**Verified strengths**

- Core API surface is large and coherent (~90+ `api/v1` routes).
- Identity flows have dedicated feature tests (OTP, magic link, invites, sessions).
- Ticketing, SLA policies, dashboard, audit, notifications, and search have backend tests.
- Frontend type-check, ESLint (0 warnings), production build, and 50 Vitest unit tests pass.
- PHP test suite: **71/71 passing** after audit fixes.
- No Microsoft/OAuth/AuthCallback code in `app/` or `resources/js/src`.
- Arabic/English JSON namespaces are paired; no EN/AR key mismatches detected.

**Verified gaps (production-relevant)**

- **Documentation contradicts product:** `docs/ARCHITECTURE.md` and `docs/DATABASE_SCHEMA.md` describe Microsoft Entra OAuth; runtime auth is OTP-only.
- **Misleading admin UX:** “Admin Users” shows **agent performance** from dashboard API, not user management; no `/users` CRUD API exists.
- **Dead frontend API constants:** `USERS`, `COMPANIES`, `STATUSES`, `PRIORITIES`, `ATTACHMENTS.UPLOAD`, `TICKETS.UPDATE_STATUS`, etc. are defined but unused or have no backend route.
- **Session management API exists; UI does not** (`listSessions` / `revokeSession` in `authService` only).
- **SSO scaffold remains:** `sso_identities` table + `SsoIdentity` model; no OAuth implementation.
- **Missing `config/cors.php`:** relies on framework defaults; risky for credentialed SPA + broadcasting in production.
- **Categories module has no HTTP API;** admin categories page is read-only via ticket form options.
- **Playwright E2E not executed** in this audit (`test:e2e` script exists but was not run).

**Overall readiness score: 64 / 100**

| Environment | Verdict |
|-------------|---------|
| **UAT** | **Conditional yes** — core ticket flows testable; document auth/CORS/realtime deps; fix misleading admin labels first. |
| **Production** | **No** — address critical blockers (CORS, docs/auth alignment, admin honesty, session edge cases, E2E, attachment policy). |

---

## 2. Commands executed and results

| Command | Result |
|---------|--------|
| `composer dump-autoload` | OK |
| `php artisan optimize:clear` | OK |
| `php artisan route:list` | OK (~90 API routes) |
| `php artisan migrate:status` | All 41 migrations **Ran** |
| `php artisan test` | **71 passed** (was 70/71 before fixes) |
| `vendor/bin/pint --test --dirty` | **Passed** |
| `npm run type-check` | **Passed** |
| `npm run lint` | **Passed** (after fixes) |
| `npm run build` | **Passed** |
| `npm run test -- --run` | **50 passed** (16 files) |
| `npm run test:e2e` | **Not run** |

**Grep searches (application source, excluding vendor/node_modules):**

| Pattern | Finding |
|---------|---------|
| microsoft / entra / azure / oauth / AuthCallbackPage / auth_token | **None in app or frontend src** |
| localStorage | **Locale, theme, device fingerprint only** — no auth token storage |
| console.log | **None in `resources/js/src`** |
| TODO / FIXME (app) | `NotificationService` Slack/SMS stubs; no auth TODOs |

---

## 3. Module readiness table

| Module | Status | Notes |
|--------|--------|-------|
| Identity / OTP | **Active** | Rate limits, domain check, trusted device, Mailgun jobs |
| Magic links | **Active** | Throttled verify; tests present |
| Admin invites | **Active** | API + AdminInvitesPage |
| Auth sessions (API) | **Partial** | Backend complete; **no frontend UI** |
| Trusted devices | **Partial** | Backend via fingerprint hash; login checkbox only |
| Ticketing | **Active** | CRUD, assign, my-tickets, statistics |
| Categories / Subcategories | **Partial** | Data layer only; exposed via `ticket-form-options` |
| Communication (replies/notes) | **Active** | Attachments via reply multipart; no standalone upload route |
| Attachments | **Partial** | Download/delete only at `/attachments/{id}` |
| SLA | **Active** | Policies + per-ticket tracking; scheduled `MonitorSlaComplianceJob` |
| Notifications | **Active** | In-app + email; Slack/SMS TODO in code |
| Audit logs | **Active** | API + AdminAuditLogsPage (partial i18n) |
| Dashboard | **Active** | Overview + metrics; realtime refresh |
| Search / Saved filters | **Active** | POST search, quick filters, filter CRUD |
| Settings | **Active** | Definitions + scoped values; resolver tested |
| Realtime | **Partial** | Broadcast listeners; depends on Reverb + Echo config |
| Health checks | **Active** | `/health/*`, `/up` |
| SSO / Microsoft | **Dead** | Schema + model only; docs still describe OAuth |
| Categories `RoutingService` | **Dead** | No references |
| `SlaMetEvent` | **Dead** | Never dispatched |
| Rate limiter `auth` | **Dead** | Registered, never applied to routes |

---

## 4. Page readiness table

| Page | Route | Status | Priority fixes |
|------|-------|--------|----------------|
| Login | `/login` | **OK** | — |
| Magic link | `/auth/magic` | **OK** | Add explicit error UI for invalid/expired token |
| Accept invite | `/auth/accept-invite` | **Partial** | Missing UI when `email`/`token` query params absent |
| Dashboard | `/dashboard` | **OK** | — |
| Tickets list | `/tickets` | **OK** | — |
| Create ticket | `/tickets/create` | **OK** | — |
| Ticket detail | `/tickets/:id` | **OK** | Reply edit/delete UI limited vs API |
| Search | `/search` | **OK** | Invalid HTML nesting in loading state (test warning) |
| Settings | `/settings` | **OK** | Sub-routes in `ROUTES` not all wired |
| Profile | `/profile` | **Partial** | No sessions UI; hardcoded `—` fallback |
| Notifications | `/notifications` | **OK** | — |
| Notification preferences | `/notifications/preferences` | **OK** | — |
| Admin users | `/admin/users` | **Partial** | **Misleading:** agent performance table, not users |
| Admin invites | `/admin/invites` | **Partial** | No list loading/error on initial fetch |
| Admin departments | `/admin/departments` | **Partial** | Read-only list derived from tickets; English table headers |
| Admin categories | `/admin/categories` | **Partial** | Read-only; no CRUD |
| Admin SLA policies | `/settings/sla` | **Partial** | Read-only list; hardcoded `min` suffix |
| Admin audit logs | `/admin/audit-logs` | **Partial** | Much hardcoded English |
| 404 | `*` | **Partial** | Now uses i18n `NotFoundPage` (audit fix) |
| Error boundary | global | **Partial** | Hardcoded English (keys added, component not wired to i18n) |

**Unwired `ROUTES` constants (dead navigation targets):**  
`LOGOUT`, `TICKET_EDIT`, `SETTINGS_GENERAL`, `SETTINGS_DEPARTMENTS`, `SETTINGS_CATEGORIES`, `SETTINGS_STATUSES`, `SETTINGS_PRIORITIES`, `SETTINGS_NOTIFICATIONS`, `ADMIN`, `ADMIN_COMPANIES`, `ADMIN_ROLES`.

---

## 5. API contract matrix

Base: `/api/v1`. Frontend uses Sanctum CSRF + `withCredentials` (see `resources/js/src/shared/utils/api.ts`).

| Frontend service / call | Endpoint | Backend exists? | Method | Payload | Response | Status |
|-------------------------|----------|-----------------|--------|---------|----------|--------|
| authService.requestOtp | POST `/auth/request-otp` | Yes | Yes | Yes | Yes | **OK** |
| authService.verifyOtp | POST `/auth/verify-otp` | Yes | Yes | Yes | Yes | **OK** |
| authService.requestMagicLink | POST `/auth/request-magic-link` | Yes | Yes | Yes | Yes | **OK** |
| authService.verifyMagicLink | POST `/auth/verify-magic-link` | Yes | Yes | Yes | Yes | **OK** |
| authService.acceptInvite | POST `/auth/accept-invite` | Yes | Yes | Yes | Yes | **OK** |
| authService.fetchMe / logout | GET/POST `/auth/me`, `/auth/logout` | Yes | Yes | Yes | Yes | **OK** |
| authService.listSessions | GET `/auth/sessions` | Yes | Yes | — | Yes | **Unused UI** |
| ticketService.* | `/tickets`, replies, notes | Yes | Yes | Yes | Yes | **OK** |
| slaService | GET `/tickets/{id}/sla` | Yes | Yes | — | Yes | **OK** |
| departmentSupportService | `/departments/{id}/agents`, `ticket-form-options` | Yes | Yes | Yes | Yes | **OK** |
| dashboardService | GET `/dashboard/overview` | Yes | Yes | — | Yes | **OK** |
| adminService.fetchAgentPerformance | GET `/dashboard/agent-performance` | Yes | Yes | — | Yes | **OK** (mislabeled page) |
| adminService.fetchDepartmentsFromTickets | GET `/tickets?per_page=100` | Yes | Yes | — | Yes | **Partial** (hacky dept list) |
| adminService.fetchSlaPolicies | GET `/sla-policies` | Yes | Yes | — | Yes | **OK** |
| inviteService | `/admin/invites` | Yes | Yes | Yes | Yes | **OK** |
| searchService | `/search/*`, `/filters/*` | Yes | Yes | Yes | Yes | **OK** |
| settingsService | `/settings/*`, `/setting-definitions/*` | Yes | Yes | Yes | Yes | **OK** |
| notificationService | `/notifications/*`, `/notification-preferences/*` | Yes | Yes | Yes | Yes | **OK** |
| Attachments download URL | GET `/attachments/{id}/download` | Yes | GET | — | File stream | **OK** |
| API_ENDPOINTS.ATTACHMENTS.UPLOAD | POST `/attachments` | **No** | — | — | — | **Dead** (upload via reply POST multipart) |
| API_ENDPOINTS.USERS.* | `/users` | **No** | — | — | — | **Dead** |
| API_ENDPOINTS.COMPANIES.* | `/companies` | **No** | — | — | — | **Dead** |
| API_ENDPOINTS.CATEGORIES.* | `/categories` | **No** | — | — | — | **Dead** |
| API_ENDPOINTS.STATUSES.* | `/statuses` | **No** | — | — | — | **Dead** |
| API_ENDPOINTS.PRIORITIES.* | `/priorities` | **No** | — | — | — | **Dead** |
| API_ENDPOINTS.DEPARTMENTS.LIST/CRUD | `/departments` | **No** | — | — | — | **Dead** (only agents + form-options) |
| API_ENDPOINTS.TICKETS.UPDATE_STATUS | — | **No** | — | — | — | **Dead** (use PUT ticket) |
| API_ENDPOINTS.TICKETS.STATS | `/tickets/stats` | **No** | — | — | — | **Dead** (use `/tickets/statistics/{departmentId}`) |

---

## 6. Translation audit

**Namespaces (en/ar):** `common`, `auth`, `tickets`, `dashboard`, `search`, `settings`, `admin`, `notifications`, `realtime`, `profile`.

| Check | Result |
|-------|--------|
| Missing keys (en vs ar) | **None** across JSON files |
| `t()` usage count | ~324 distinct keys referenced |
| Hardcoded English (high visibility) | Router/ProtectedRoute loaders (**fixed**), ErrorBoundary, OfflineBanner, AdminUsers/Departments table headers, AdminAuditLogsPage, attachment validation strings in `attachments.ts` |
| Hardcoded Arabic | Login language toggle labels only (intentional) |
| RTL | `LocaleProvider` sets `dir`; layout uses `text-align: start` in many places — **good** |
| Dark mode | `ThemeProvider` + CSS variables — **present**; not exhaustively tested per page |
| Unused JSON keys | Likely many (no automated prune run); recommend i18n-scanner in CI |

**Audit fix:** Added `common:errors.*` and wired loading/not-found strings in `Router.tsx` and `ProtectedRoute.tsx`.

---

## 7. Dead code / cleanup plan

| Item | Action | Risk |
|------|--------|------|
| `SsoIdentity` model + `sso_identities` migration | **Needs confirmation** before drop | Medium — demo seeder may use |
| `docs/ARCHITECTURE.md` Microsoft OAuth sections | **Update or archive** | Low |
| `API_ENDPOINTS` dead groups | **Remove or implement APIs** | Low if grep-clean first |
| `Modal` component (unused) | Safe to delete | Low |
| `Categories\RoutingService` | Safe to delete | Low |
| `SlaMetEvent` | Wire listener or delete | Low |
| `config/modules.php` name drift | Align with real module folders | Low |
| `PROFESSIONAL_ICONS_UPDATE.md`, `LOGO_INTEGRATION.md` at repo root | Move to docs or delete | Low |
| Rate limiter `auth` | Remove or apply to routes | Low |
| `public/build` in git | Confirm deploy strategy | Ops |

---

## 8. Security findings

| ID | Severity | Finding | Risk | Recommended fix | Files |
|----|----------|---------|------|-----------------|-------|
| SEC-01 | **High** | No published `config/cors.php` | Credentialed SPA may fail or be over-permissive by default | Publish and restrict origins, credentials, paths | Add `config/cors.php`, `.env` |
| SEC-02 | **High** | Docs describe OAuth; app uses OTP | Operators may misconfigure Entra instead of Mailgun/OTP | Rewrite architecture auth section | `docs/ARCHITECTURE.md` |
| SEC-03 | **Medium** | OTP verify without session middleware could 500 | **Fixed in audit:** `hasSession()` guards | `AuthLoginService.php`, `AuthSessionService.php` |
| SEC-04 | **Medium** | `sso_identities` stores `access_token` / `refresh_token` | Unused attack surface if table populated | Drop or encrypt at rest; no OAuth until designed | Migration, `SsoIdentity.php` |
| SEC-05 | **Medium** | Attachment delete: owner OR super_admin only | Agents cannot remove malicious uploads on others' replies | Policy-based delete aligned with ticket permissions | `AttachmentController.php` |
| SEC-06 | **Medium** | Attachment download uses ticket `view` policy | **OK** if policy enforces department isolation | Verify `TicketPolicy` tests | `AttachmentController.php`, policies |
| SEC-07 | **Low** | Broadcast channels authorize ticket/department/user | **OK** in `routes/channels.php` | Add channel auth tests | `routes/channels.php` |
| SEC-08 | **Low** | OTP rate limits: IP + email + throttle middleware | **OK** | Monitor in production | `OtpService.php`, `AppServiceProvider.php` |
| SEC-09 | **Low** | No `auth_token` in localStorage | **Good** | Keep cookie-only | `authService.ts`, `api.ts` |
| SEC-10 | **Low** | Magic link reuse | Covered by tests (partial) | Ensure one-time token in `MagicLinkService` | Identity module tests |
| SEC-11 | **Info** | Settings sensitive values | Confirm masking in API resources | Review `SettingResource` / resolver | Settings module |
| SEC-12 | **Info** | `TrustProxies` at `*` | Ensure correct in production behind LB | `bootstrap/app.php` |

---

## 9. Test coverage findings

| Area | Test files | Assessment |
|------|------------|------------|
| **Backend total** | 18 test classes, **71 tests** | Good for auth/tickets/SLA/dashboard/audit/notifications |
| Identity | 4 feature files | Strong |
| Ticketing | 2 files | Core API covered |
| Communication | 0 dedicated | **Gap** — replies/notes/attachments |
| Search | 0 | **Gap** |
| Categories | 0 | **Gap** |
| Settings | 1 unit | Resolver only |
| Realtime | 1 unit | Broadcast shape only |
| **Frontend unit** | 50 tests / 16 files | Login, tickets, search, dashboard, notifications |
| **E2E Playwright** | Not run | **Gap** |
| Arabic/RTL | 0 | **Gap** |
| Authorization negative paths | Partial | Expand 403 tests per module |

**Recommended test plan (next sprint)**

1. Communication feature tests: reply with attachment, download 403, note 403.  
2. Search + saved filter integration tests.  
3. Frontend: Profile sessions UI + tests when built.  
4. Playwright: login → create ticket → reply → notification smoke.  
5. Channel authorization feature test with `Broadcast::fake()`.

---

## 10. Fixed issues during audit

| Fix | Files |
|-----|-------|
| Guard `session()` when session store absent on API login/logout | `AuthLoginService.php`, `AuthSessionService.php` |
| PHP test `test_otp_verify_starts_session_without_stateful_origin_headers` now passes | (above) |
| ESLint: `handleRequestMagicLink` hook ordering/deps | `LoginPage.tsx` |
| i18n loading + 404 page | `common.json` (en/ar), `Router.tsx`, `ProtectedRoute.tsx` |

**Post-fix verification:** `php artisan test` 71/71, `npm run lint`, `npm run type-check`, `npm run build`, `npm run test -- --run` all pass.

---

## 11. Remaining critical blockers

1. **Production CORS configuration** not explicitly defined.  
2. **Architecture/documentation** still describes Microsoft SSO.  
3. **Admin Users page** misrepresents functionality (agent performance).  
4. **No E2E proof** of full flows in this audit.  
5. **Attachment authorization** on delete may be too narrow or inconsistent with team workflows.

---

## 12. High priority fixes

1. Publish `config/cors.php` + document `SANCTUM_STATEFUL_DOMAINS` / `SESSION_DOMAIN` / `REVERB_*`.  
2. Rewrite auth sections in `docs/ARCHITECTURE.md`, `docs/DATABASE_SCHEMA.md`, `docs/TESTING_GUIDE.md`.  
3. Rename/rebuild Admin Users page or add real user admin API.  
4. Build Profile → **Sessions** UI using existing session APIs.  
5. Prune or implement dead `API_ENDPOINTS` constants.  
6. Internationalize `AdminAuditLogsPage`, admin table headers, `ErrorBoundary`, `OfflineBanner`.  
7. Run and fix Playwright E2E suite in CI.

---

## 13. Medium priority fixes

1. Accept-invite: validate query params before showing form.  
2. Admin invites: loading/error for invite list.  
3. Implement categories/statuses/priorities admin APIs **or** remove settings route constants.  
4. Remove or implement SSO table/model.  
5. Add `config/cors` + health check to deployment runbook verification.  
6. Communication module automated tests.  
7. Fix SearchPage invalid `<motion.div>` inside `<p>` (hydration warning in tests).

---

## 14. Low priority cleanup

1. Delete unused `Modal` component.  
2. Remove `RoutingService`, `SlaMetEvent`, unused rate limiter `auth`.  
3. Align `config/modules.php` with actual modules.  
4. i18n unused key scan.  
5. Consolidate markdown files at repo root into `docs/`.  
6. Add dedicated `NotFoundPage` with `AppLayout` + navigation home.

---

## 15. Recommended next sprint plan

| Week | Focus |
|------|-------|
| 1 | CORS, Sanctum domains, doc auth rewrite, Admin Users honesty, sessions UI |
| 2 | Communication + attachment tests; attachment delete policy review |
| 3 | Playwright E2E + CI; Arabic smoke on login/tickets |
| 4 | Dead code removal (SSO scaffold decision); API_ENDPOINTS cleanup |

---

## 16. Final production readiness verdict

| Criterion | Met? |
|-----------|------|
| Automated backend tests green | Yes (71/71) |
| Frontend build + unit tests green | Yes |
| Auth model matches docs | **No** |
| Admin UX matches capabilities | **No** |
| API contract hygiene | **Partial** |
| Security hardening (CORS, SSO table) | **Partial** |
| E2E validation | **Not verified** |

**Verdict:** Suitable for **internal UAT** on core ticketing and OTP login **after** CORS/session env validation and stakeholder acknowledgment of admin/read-only limitations. **Not approved for production** until critical blockers in §11 are resolved and E2E passes in CI.

---

## Appendix A — Backend inventory (summary)

**Modules:** Identity, Ticketing, Communication, Sla, Notifications, Audit, Dashboard, Search, Settings, Realtime (+ Shared).

**Scheduled jobs:** `MonitorSlaComplianceJob` every minute (`routes/console.php`).

**Migrations:** 41 (including `login_otps`, `auth_sessions`, `auth_magic_links`, `user_invites`, `sso_identities`).

**Seeders:** `DatabaseSeeder`, `RolePermissionSeeder`, `SuperAdminSeeder`, `DemoDataSeeder`, `TicketingDataSeeder`, `CommunicationDataSeeder`, `SlaDataSeeder`, `NotificationTemplateSeeder`, `SettingDefinitionSeeder`.

---

## Appendix B — Frontend inventory (summary)

**Pages:** 18 lazy-loaded routes in `Router.tsx`.  
**Services:** 10 under `features/*/services`.  
**Realtime hooks:** `useDashboardRealtime`, `useNotificationRealtime`, `useTicketRealtime`, `useTicketPresence`.  
**Auth storage:** Cookie session only; device fingerprint in `localStorage` (not secret).

---

*End of report.*
