# Analysis: 401 Unauthenticated on PUT /api/companies/{id}

**Scope:** Analysis only — no code changes.  
**Goal:** Explain why the dashboard’s PUT to update a company returns 401 and where the failure occurs.

---

## STEP 1 — API auth requirements (backend)

### Route

- **Path:** `PUT /api/companies/{company}` (resource `update`).
- **Definition:** Registered via `ResourceRegistrar` in `routes/api.php` (line 94):
  - `$registrar = new ResourceRegistrar('company', CompanyController::class, ['only' => ['index', 'store', 'update', 'delete']]);`
  - So `PUT /api/companies/{company}` is the update route.

### Middleware

The route lives inside:

```php
Route::middleware(['auth:sanctum', 'verified', 'nomade.verified', \App\Http\Middleware\LogCompanyUpdateRoute::class])->group(function () {
    // ...
    $registrar = new ResourceRegistrar('company', CompanyController::class, ['only' => ['index', 'store', 'update', 'delete']]);
    // ...
});
```

So **PUT /api/companies/{id}** is protected by:

1. **auth:sanctum** — request must be authenticated (Bearer token or Sanctum cookie).
2. **verified** — user email must be verified.
3. **nomade.verified** — user must be verified by Nomade (EnsureNomadeIsVerified).
4. **LogCompanyUpdateRoute** — logging only.

If **auth:sanctum** fails, Laravel returns **401 Unauthenticated** (no user on the request). The other middlewares run only after authentication.

### How Sanctum is used

- `config/sanctum.php`: no token expiration (`'expiration' => null`); stateful domains are configured via `SANCTUM_STATEFUL_DOMAINS`.
- Backend **Authenticate** middleware logs `has_auth_header` and `has_cookie` (Bearer vs cookie).
- Sanctum accepts **Bearer token** in the `Authorization` header for API requests. So the dashboard is expected to send `Authorization: Bearer <token>`.

**Conclusion:** The backend requires a valid Bearer token (or valid Sanctum cookie). Missing or invalid token → 401.

---

## STEP 2 — Frontend request

### Call chain

1. **UI:** `CompanyDetailPage` (or `AssignExistingCompanyModal`) calls `editCompanyCms(companyFormData, companyId)`.
2. **Context:** `CompanyContext.editCompanyCms` calls `editCompany(repository, company, id!)` (company repository + FormData + id).
3. **Application:** `editCompany` (company application) calls `companyRepo.editCompany(company, id)`.
4. **Repository:** `CompanyRepository.editCompany()` in `nomade-dashboard/src/modules/company/infrastructure/companyRepository.ts`:

```ts
public async editCompany(company: FormData, id: number): Promise<HttpResponseInterface<Company>> {
  try {
    const resp = await this.http.put<Company>(
      `${COMPANY_BASE}/${id}`,
      company,
    );
    return resp;
  } catch (error) {
    return Promise.reject(error);
  }
}
```

- **this.http** is `Http.getInstance()` (singleton).
- **COMPANY_BASE** = `environments.API_PUBLIC_URL + '/companies'` (e.g. `https://api.example.com/api/companies` if `API_PUBLIC_URL` includes `/api`).
- So the request is **PUT** to `{API_PUBLIC_URL}/companies/{id}` with body **FormData**. No explicit headers are passed here.

### How the token is attached

- `this.http` is the shared `Http` wrapper that delegates to `HttpImplementation`.
- `HttpImplementation.put()` (and get/post/patch/delete) calls **`buildRequestHeaders()`** and passes the result as the request `headers` to axios.
- **buildRequestHeaders()** in `nomade-dashboard/src/modules/core/infrastructure/http/HttpImplementation.ts`:

```ts
private async buildRequestHeaders(): Promise<RawAxiosRequestHeaders> {
  const cookieKey = environments.cookies ?? "nomade_token";
  const token = await this.cookies.get(cookieKey);
  const headers: RawAxiosRequestHeaders = {
    Accept: "application/json",
  };
  if (token) {
    headers.Authorization = "Bearer " + token;
  }
  return headers;
}
```

- **Cookie key:** `environments.cookies` comes from `process.env.VITE_COOKIES_USER_TOKEN` (`nomade-dashboard/src/sections/shared/utils/environments/environments.ts`). If that env var is not set, `environments.cookies` is `undefined`, and the code uses the fallback **`"nomade_token"`**.
- **Storage:** Token is read via `AsyncCookiesImplementation`, which uses **js-cookie** (`Cookies.get(key)`).
- So the PUT request **does** get an **Authorization: Bearer &lt;token&gt;** header **only if** a token is found in the cookie under that key.

### Login side (where the token is written)

- In `AuthContext`, after a successful login, the token is stored with:
  - `cookies.set(environments.cookies!, response.data.access_token);`
- So the **same** key (`environments.cookies` = `VITE_COOKIES_USER_TOKEN`) is used for **writing** on login.
- If **VITE_COOKIES_USER_TOKEN** is not set in the dashboard’s `.env`:
  - **Login:** `environments.cookies` is `undefined`; `cookies.set(undefined, token)` will use the string `"undefined"` as the key (js-cookie behavior).
  - **Requests:** `cookieKey = undefined ?? "nomade_token"` → **`"nomade_token"`**. So the client looks for a cookie named **`nomade_token`**, which was never set → **no token**, **no Authorization header** → 401.

### Axios and FormData

- For **FormData**, `HttpImplementation` does **not** set `Content-Type` (so the browser can set `multipart/form-data` with boundary). No issue there.
- The same axios instance and same `buildRequestHeaders()` are used for all methods (get, post, put, patch, delete). So if other authenticated requests work (e.g. GET company), the difference is not the HTTP class design but **this specific request path** (e.g. cookie/key, or token validity at the time of PUT).

**Conclusion:** The PUT request will include `Authorization: Bearer <token>` only when `this.cookies.get(cookieKey)` returns a value. The cookie key is `VITE_COOKIES_USER_TOKEN` or, if unset, `"nomade_token"`. Login uses `environments.cookies!` (so `undefined` when env is missing), which can cause a **write/read key mismatch** and thus missing token on later requests.

---

## STEP 3 — Auth system summary

| Aspect | Implementation |
|--------|----------------|
| **Mechanism** | **Bearer token** in `Authorization` header (not cookie-based auth to the API). |
| **Token storage** | Cookie (js-cookie), key = `VITE_COOKIES_USER_TOKEN` (or fallback `nomade_token` when reading). |
| **Injection** | Every request in `HttpImplementation` calls `buildRequestHeaders()`, which reads the cookie and, if present, sets `Authorization: Bearer <token>`. |
| **Cookies** | No `withCredentials` needed for sending the token to the API: the dashboard reads the cookie in JS and puts the value in the header. The request is cross-origin to the API; the cookie is same-origin to the dashboard. |

So: **Bearer token**, stored in a **cookie** on the dashboard origin, **injected into requests** via `buildRequestHeaders()` in the shared HTTP layer. If the cookie is missing or the key is wrong, the header is missing and the backend returns 401.

---

## STEP 4 — Root cause (why 401 happens)

401 on **PUT /api/companies/{id}** means the backend did not accept the request as authenticated. With the current design, that implies one or more of the following.

### 1. Missing or wrong cookie key (most likely)

- **Missing header:** If `buildRequestHeaders()` does not add `Authorization`, Sanctum has no token and returns 401.
- That happens when `this.cookies.get(cookieKey)` returns nothing.
- **Cause:** Cookie key mismatch between login and requests:
  - **Login:** `cookies.set(environments.cookies!, token)` with `environments.cookies === undefined` → cookie is stored under the key `"undefined"` (or equivalent).
  - **Requests:** `cookieKey = environments.cookies ?? "nomade_token"` → **`"nomade_token"`**.
  - So the client looks for `nomade_token` but the token was stored under `undefined` → no token found → **no Authorization header** → 401.

**Check:** Ensure `VITE_COOKIES_USER_TOKEN` is set in the dashboard’s `.env` (e.g. same value as backend cookie name if any, or a consistent name like `nomade_token`) and that the app was rebuilt so the env is available at runtime.

### 2. Token expired or invalid

- If the header **is** sent but the token is expired or invalid, Sanctum still returns 401.
- Backend has `sanctum.expiration => null`, so tokens do not expire by config; 401 would then be due to invalid/revoked token or wrong format.

**Check:** Backend logs in `Authenticate` middleware: `has_auth_header` and `has_cookie`. If `has_auth_header` is true and you still get 401, the token itself is the problem (validity/revocation).

### 3. Cookie not set or not readable

- Cookie might not be set after login (e.g. login failed or response shape changed).
- Cookie might be cleared (e.g. another tab, logout, or browser clearing).
- Same-site/cross-origin or path/domain settings might prevent the dashboard from reading the cookie (less likely if same origin).

**Check:** In the browser, after login, inspect the cookie for the key you expect (`VITE_COOKIES_USER_TOKEN` or `nomade_token`). Before the PUT, confirm in DevTools that the request has `Authorization: Bearer <value>`.

### 4. Other middlewares (verified / nomade.verified)

- **verified** and **nomade.verified** run **after** auth. If they fail, Laravel typically returns 403 or a custom response, not 401.
- So a plain **401 Unauthenticated** points to **auth:sanctum** failing (no or invalid auth), not to email/nomade verification.

---

## Summary

- **Backend:** PUT `/api/companies/{id}` is protected by `auth:sanctum` (and then `verified`, `nomade.verified`). 401 means “not authenticated”.
- **Frontend:** The PUT is made by `CompanyRepository.editCompany()` using the shared `Http` instance, which uses `buildRequestHeaders()` to add `Authorization: Bearer <token>` from a cookie.
- **Most likely cause:** Cookie key mismatch when `VITE_COOKIES_USER_TOKEN` is unset: login stores the token under `undefined`, while requests look for `nomade_token`, so no token is sent and the request is unauthenticated.
- **Other possibilities:** Token missing (cookie not set or cleared), token invalid/expired, or cookie not readable. Checking backend auth logs and the request headers in the browser will confirm whether the header is sent and whether the problem is missing header vs invalid token.

No code changes were made; this document is analysis only.
