## Context

The frontend identifies the current user through `GET /api/auth/me`, which reads the current database row. Protected backend writes use the role stored in the signed login token. `SystemUserAdminService.updateUser` currently changes role, status, and page permissions without increasing `auth_version`; therefore an old token can retain a lower role while the UI renders the newly promoted role.

`phone-api-client.js`, `wecom-api-client.js`, and `system-user-api-client.js` use the shared authentication request helper. `device-api-client.js` uses a separate raw `fetch`, so it does not request the CSRF cookie or send the `X-XSRF-TOKEN` header. `DeviceAssetController` also lacks server-side administrator checks, unlike phone and enterprise-WeChat controllers; frontend route metadata cannot stop a direct API call. Spring Security maps both a missing CSRF token and role denial to a generic 403 response, which conceals the cause during diagnosis.

## Goals / Non-Goals

**Goals:**

- Make a role, status, or page-permission change invalidate the account's existing login token before its new authorization state is shown as usable.
- Preserve the existing fixed roles and server-side authorization checks; a developer remains the highest-privilege role but must reauthenticate after an authorization change.
- Use one authenticated request path for device GET, multipart create/update, and delete operations, including cookies and CSRF headers for every write.
- Enforce the existing administrator-only device rule in the backend for device rows, lookups, and controlled image reads.
- Remove stale browser identity on 401 and reacquire CSRF input if its Cookie is absent.
- Return a safe, actionable distinction between expired authentication, CSRF failure, and ordinary authorization denial.

**Non-Goals:**

- Do not add a database migration, create more developer accounts, weaken CSRF, or change device asset fields, upload validation, or response DTOs.
- Do not add self-service role changes or browser-stored login tokens.
- Do not modify the enterprise-WeChat or phone asset business rules.

## Decisions

### 1. Invalidate on every authorization-state mutation

`SystemUserAdminService.updateUser` will compare the persisted and requested role, status, and normalized page permissions. If any effective authorization value changes, it will increment `auth_version` in the same database update. `AuthTokenFilter` already rejects a token whose version differs from the database value, so the next request with that old cookie returns 401 and the user must sign in again.

`auth_version`（登录票据版本号：服务端用来立即作废旧登录票据的整数） is used because the role embedded in a signed JWT cannot safely be altered in-place. Relying only on `/api/auth/me` would keep the UI and API authorization sources inconsistent; rereading the role from the database on each request would reduce this mismatch but would not reliably revoke sessions after status or page-permission changes.

### 2. Reuse the existing authenticated request helper for multipart safely

Exported `request` in `auth-api-client.js` will remain the single browser request entry point. It will preserve caller headers, request CSRF only for non-GET non-login methods, and omit `Content-Type` when the body is `FormData` so the browser can supply the multipart boundary.

`device-api-client.js` will call this helper for list, lookup, create, update, delete, and image access requests as appropriate. The alternative of disabling CSRF for device endpoints is rejected because it would permit forged writes from a third-party page that can use a user's login cookie.

### 3. Keep safe 401/403 diagnostic classes

The security error handler will retain generic authorization wording for ordinary role/page denial, return a session-expired/login-required message for an invalidated token, and return a refresh-and-retry message for missing or invalid CSRF input. It will not reveal account existence, target roles, permission maps, token values, or internal exception details.

CSRF（跨站请求伪造防护：要求浏览器把服务端发出的随机值同时放在 Cookie 和请求头中） remains mandatory for all non-login writes. Without it, another website could submit a write using the user's ambient login Cookie.

### 4. Make administrator-only device access a backend rule

`PagePermissionService` will expose one reusable administrator requirement that reads the authenticated principal and accepts only `DEVELOPER` and `SUPER_ADMIN`. `DeviceAssetController` will call it before every list, lookup, image-read, create, update, and delete endpoint. This keeps the approved device access model unchanged while making the API enforce it.

Adding a new device entry to the configurable page-permission map was considered but rejected for this change. The approved product behavior is administrator-only; adding `READ`/`EDIT` assignment for ordinary roles would be a product-scope expansion and must be proposed separately.

### 5. Expire the browser view and verify actual CSRF Cookie presence

When the shared request helper receives a 401, it will signal the authentication state to clear the in-memory user and redirect to `/login`; it will not retry a potentially non-idempotent write. The CSRF helper will treat the Cookie as the source of truth: it skips initialization only when `XSRF-TOKEN` is actually present, otherwise it requests `/api/auth/csrf` again.

### 6. Test the real failure paths before declaring the fix

Backend tests will prove that a promoted user token becomes invalid, a fresh developer token can manage users, and a non-developer token remains denied. Browser tests will verify that the first device write initializes CSRF, sends cookies and the token header, preserves multipart boundaries, and shows the distinct safe error messages.

## Risks / Trade-offs

- [An administrator changes their own authorization state] -> Their next request becomes 401 by design; the frontend redirects to login with a clear reauthentication message.
- [A change increments `auth_version` unnecessarily] -> Compare the persisted role, status, and normalized permission JSON before incrementing; no-op edits retain the current session.
- [Setting JSON content type for multipart] -> Detect `FormData` and let the browser add the boundary; otherwise uploads would reach the server as malformed data.
- [Detailed 403 messages disclose security state] -> Restrict messages to three generic client actions: login again, refresh/retry, or no permission.
- [A logged-in ordinary user bypasses the device route] -> Require an administrator in every device controller endpoint, including the opaque image endpoint.
- [The CSRF Cookie disappears while the JavaScript flag remains true] -> Test Cookie absence and reacquisition rather than trusting an in-memory readiness flag.
- [Concurrent account updates] -> Use the existing single-row update path and increment from the persisted version; the current project has no optimistic-lock column, so concurrent administrator updates remain outside this targeted fix.

## Migration Plan

1. Deploy the backend and frontend together; no data migration or schema change is required.
2. Existing sessions continue until a managed account's authorization state changes. That change invalidates its previous session at the next protected request.
3. Verify a device create, multipart device update, device image read, phone write, enterprise-WeChat write, and system-user write using a fresh developer login; verify an ordinary user cannot call any device endpoint.
4. Roll back by reverting the application changes. Existing `auth_version` values and asset data remain intact; already-invalidated sessions simply need a new login.

## Open Questions

None. Reauthentication after a role, status, or page-permission change is the selected security behavior.
