# Routing overview

Source: https://docs.qiick.io/externa/routing

Externa exposes a **session-cookie** HTTP surface for the Inertia SPA and same-origin JSON endpoints, plus a separate **stateless Public CMS API** under `/api/v1` (Bearer API keys or anonymous `public` role).

> **Route files**
> Entry: `routes/web.php` (session/Inertia). API: `routes/api.php` → `/api/v1/*`. Domain modules: `admin.php`, `settings.php`, `collections.php`, `ai.php`. Console/schedule: `routes/console.php`. Health: `/up` via `bootstrap/app.php`.

## Composition (`routes/web.php`)

1. **`/` (`home`)** — redirects to `dashboard` if authenticated, otherwise `login`.
2. **Authenticated shell** (`auth` + `verified`):
   - `GET /dashboard` — also requires `permission:can-show-dashboard`
   - Notification JSON helpers: `notifications`, `notifications/unread-count`, `notifications/read`
3. **`require`** sibling files:
   - `admin.php` — users, groups, roles, permissions, activity logs, files
   - `settings.php` — account / profile / 2FA settings pages
   - `collections.php` — collections, fields, items
   - `ai.php` — AI webhook + authenticated AI UI/API

Fortify registers login/register/password/2FA routes separately (prefix empty, `web` middleware).

## Middleware layers

Configured in `bootstrap/app.php`:

### Routing registration

```php
->withRouting(
    web: __DIR__.'/../routes/web.php',
    api: __DIR__.'/../routes/api.php',
    commands: __DIR__.'/../routes/console.php',
    health: '/up',
    apiPrefix: 'api',
)
```

- **`web`** — Inertia SPA, Fortify, admin/collections/AI session routes (cookies, CSRF).
- **`api`** — Public CMS API (`/api/v1/...`), no session; `ResolveApiAccess` + collection permission guard. See [Public CMS API](https://docs.qiick.io/externa/public-cms-api).

### Aliases

| Alias              | Class                     | Use                                          |
| ------------------ | ------------------------- | -------------------------------------------- |
| `permission`       | `EnsureUserHasPermission` | `permission:{PermissionEnum value}`          |
| `can.manage.files` | `EnsureCanManageFiles`    | File-manager route → file permission mapping |

### Appended to `web`

| Middleware                         | Role                       |
| ---------------------------------- | -------------------------- |
| `HandleAppearance`                 | Appearance cookie / theme  |
| `HandleInertiaRequests`            | Shared Inertia props       |
| `AddLinkHeadersForPreloadedAssets` | Asset preload link headers |

### Cookie / CSRF exceptions

- Cookies **not** encrypted: `appearance`, `sidebar_state`, `locale`
- CSRF **except**: `ai/webhooks/collection-import`, `api/graphql`, and bare `graphql` (no `/graphql` route is registered — clients use `/api/graphql`)

### Typical stacks by area

| Area                          | Middleware                                                                                                                  |
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| Dashboard / notifications     | `auth`, `verified` (+ permission on dashboard)                                                                              |
| Admin resources               | `auth`, `verified`, `permission:…` per action                                                                               |
| Files                         | `auth`, `verified`, `can.manage.files`                                                                                      |
| Collections                   | `auth`, `verified`, `permission:can-*-collections` (bulk via FormRequest — see [Collections API](https://docs.qiick.io/externa/collections-api)) |
| Project / appearance settings | `auth`, `verified`, `permission:can-manage-project-settings`                                                                |
| AI authenticated              | `auth`, `verified`, `permission:can-use-ai` (+ extra on some routes)                                                        |
| AI webhook                    | `throttle:30,1` + HMAC `X-AI-Webhook-Signature` over raw body (no session; 401 on fail from 1.2.2)                          |
| Public CMS API / GraphQL      | `throttle:api` + `ResolveApiAccess` (no session/CSRF)                                                                       |

## Client surfaces

Clients are expected to be:

1. The **Inertia React** frontend (same origin, session cookie, CSRF).
2. Server-side jobs / tools using the same auth context.
3. The **collection import webhook** with HMAC over the raw body (not a user session).
4. **External sites / apps** via the [Public CMS API](https://docs.qiick.io/externa/public-cms-api) (`/api/v1`, anonymous `public` role and/or Bearer `ek_…` API keys).

Treat JSON from `/files/*`, `/ai/*`, and session `/collections/*` as **first-party SPA APIs**. Use `/api/v1` for headless integrations.

## Inertia shared props

`HandleInertiaRequests::share()` merges:

| Prop                         | Source / meaning                                                                        |
| ---------------------------- | --------------------------------------------------------------------------------------- |
| `name`                       | `config('app.name')`                                                                    |
| `auth.user`                  | Current user model or `null`                                                            |
| `auth.permissions`           | Effective permission **name list** from `EffectivePermissionResolver::permissionsFor()` |
| `auth.roleNames`             | Effective role names (direct + via groups)                                              |
| `auth.isSuperAdmin`          | Whether effective roles include `super-admin`                                           |
| `sidebarOpen`                | From `sidebar_state` cookie (`true` when missing)                                       |
| `collectionLocales`          | Project settings `content_locales` (seeded from `config/collections.php`)               |
| `collectionLocaleMeta`       | `{ code, name, flag }[]` for enabled content locales                                    |
| `defaultContentLocale`       | Project settings default content locale                                                 |
| `notifications.unread_count` | Count of unread database notifications for the user                                     |

Frontend permission checks use `useCan()` against `auth.permissions` / `auth.isSuperAdmin` (see [Effective permissions](https://docs.qiick.io/externa/permissions)).

## Wayfinder

Named Laravel routes and controller actions are exposed to TypeScript via **Laravel Wayfinder** (`@laravel/vite-plugin-wayfinder`). Prefer generated helpers under `resources/js/routes` / actions instead of hard-coding path strings in React.

## Deep-link route ordering

Static path segments are registered **before** parameterized catch-alls so reserved names are not captured as IDs:

- Files: `/files/list`, `/files/upload`, … before `GET /files/{folder?}`
- AI: `/ai/status`, `/ai/conversations`, … before `GET /ai/{conversation}` (UUID)

## Admin UI deep-links (drawers / panels)

List screens sync selected drawers into the query string (Inertia `replace` + `preserveState`) so a URL can reopen the same surface:

| Surface           | Query                         | Behavior                                                               |
| ----------------- | ----------------------------- | ---------------------------------------------------------------------- |
| Collections index | `?edit={id}` / `?new=1`       | Opens create/edit collection drawer                                    |
| Users / Groups    | `?edit={id}` / `?new=1`       | Opens user or group drawer                                             |
| Collection fields | `?field={id}` / `?newField=1` | Opens edit field drawer or add-field type picker                       |
| Files             | `?file={id}`                  | Opens the file detail panel (fetches by id if not on the current page) |

Closing the drawer/panel removes the param without dropping other filters (search, trash, tags, …). Nested field-type multi-step beyond `newField` is out of scope for this sync.

Item **full-page** edit uses a separate [`return`](https://docs.qiick.io/externa/collection-items#return-to-list-filters-and-page) query to restore list filters — not a drawer deep-link.

## Next

| Surface                    | Docs                                          |
| -------------------------- | --------------------------------------------- |
| Admin + files JSON         | [Admin API](https://docs.qiick.io/externa/admin-api)               |
| Collections (session)      | [Collections API](https://docs.qiick.io/externa/collections-api)   |
| Public CMS API (`/api/v1`) | [Public CMS API](https://docs.qiick.io/externa/public-cms-api)     |
| GraphQL                    | [GraphQL](https://docs.qiick.io/externa/graphql)                   |
| AI                         | [AI API](https://docs.qiick.io/externa/ai-api)                     |
| Project settings           | [Project settings](https://docs.qiick.io/externa/project-settings) |
