# Authentication and customer authorization

## Boundary

The platform owns authorization even when authentication is delegated later. An upstream identity system may prove who the user is, but it cannot bypass the local menu and vehicle Scope checks.

Current providers:

- `local`: username/password login and opaque server session.
- `legacy-token`: existing static operational Bearer tokens, retained for emergency access and migration.
- `disabled`: local development only.

The API exposes `IdentityAdapter` as the future signed-token or introspection boundary. Yudao Cloud / RuoYi Sys integration must validate a short-lived signed credential and map `(auth_provider, external_subject)` to `platform_user`. Never trust unsigned identity headers from a reverse proxy.

## Roles and menus

- `admin` receives all platform menus and can manage customer accounts.
- `customer` receives only explicitly granted menu keys from the fixed customer catalog: `monitor`, `vehicles`, `tracks`, `statistics`.
- The server rejects every unspecified customer API route. The web navigation and route guard are usability layers, not the authorization boundary.

Shared vehicle APIs used by more than one customer menu are allowed when the account owns any relevant menu, but every data query still receives the same VIN Scope.

## Vehicle Scope

`platform_user_vehicle` is the authoritative active-grant projection. `platform_user_vehicle_grant_history` retains every grant interval, including repeated grants of the same vehicle to the same customer. Customer principals carry a bounded list of active VIN grants with `validFrom/validTo`. Service queries inject the VIN list as `scopeVins`; historical services additionally apply the grant time boundary.

Explicit VINs outside the grant set return `403 VEHICLE_PERMISSION_DENIED`. A missing vehicle grant is fail-closed and produces an empty collection. Plate-number resolution is performed inside the same VIN Scope.

Track, location-history and RAW queries are clamped to the exact active `valid_from` instant. A request ending at or before that instant returns `403 HISTORY_BEFORE_AUTHORIZATION`. Daily mileage is a natural-day aggregate, so a grant beginning after local midnight starts at the next complete `Asia/Shanghai` day; this prevents the first visible row from including pre-grant mileage. Missing grant timestamps return `403 HISTORY_SCOPE_UNAVAILABLE` rather than falling back to unrestricted history.

Saving an unchanged vehicle assignment preserves its original start time. Removing a vehicle closes the active history interval, and assigning it again creates a new interval. The current projection is deleted on removal so existing sessions fail closed after their short cache window.

## Sessions and password policy

- Passwords are stored with bcrypt cost 12.
- Password length is 10-128 characters and must contain at least three of: lowercase, uppercase, number, symbol.
- Five consecutive failures lock the account for 15 minutes.
- Login creates a cryptographically random opaque token; only SHA-256 is stored in MySQL.
- Default session lifetime is 12 hours (`AUTH_SESSION_TTL_HOURS`).
- The browser stores the token in `sessionStorage`, not persistent storage.
- Disabling an account or resetting its password revokes all active sessions immediately.
- Successful/failed logins and account permission changes are written to `platform_auth_audit`.

Session principals are cached in the API for at most 30 seconds to keep realtime polling inexpensive. Account disable and password reset revoke the database session immediately; permission updates invalidate local cache entries and become visible across multiple API instances within the cache window.

## Bootstrap

The first local administrator is created only when no administrator exists:

```text
BOOTSTRAP_ADMIN_USERNAME=admin
BOOTSTRAP_ADMIN_PASSWORD=<strong one-time bootstrap secret>
AUTH_SESSION_TTL_HOURS=12
```

The bootstrap password does not overwrite an existing administrator. Keep the environment file root-owned and mode `0600`. After an administrator changes credentials through the account password dialog, remove the bootstrap password from the environment.

## APIs

```text
POST /api/v2/auth/login
POST /api/v2/auth/logout
PUT  /api/v2/auth/password
GET  /api/v2/session

GET  /api/v2/admin/users
POST /api/v2/admin/users
PUT  /api/v2/admin/users/{id}
```

Customer creation/update accepts display name, optional external customer/tenant references, account status, menu keys and VINs. Username is immutable after creation. Password reset and account disable invalidate previous sessions.

## External identity migration

1. Add a concrete `IdentityAdapter` for the upstream issuer using JWKS signature validation or server-side token introspection.
2. Match the validated issuer subject to `platform_user.auth_provider` and `platform_user.external_subject`.
3. Keep menu and VIN grants in the platform initially, or sync them into the same local projection with source/version metadata.
4. Run local and external providers in parallel during migration; do not remove the operational token until external login and rollback are proven.
5. Preserve `subjectId`, `customerRef`, `tenantRef`, `menuKeys`, `vehicleCount` and `authProvider` in the session contract so the web application does not depend on a Yudao/RuoYi-specific payload.
