feat(auth): add device session tracking with configurable limits

Track authenticated sessions per device (parsing user-agent for desktop
vs mobile via mileusna/useragent), enforce configurable concurrent device
limits (Auth.DeviceLimits), and surface device status and "remove other
devices" management in the account dialog. Adds device/session context to
the auth module stores and exposes limits through the API.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-11 01:01:54 +08:00
parent 875fff825c
commit 58f11302fe
32 changed files with 1783 additions and 253 deletions
+38 -3
View File
@@ -190,15 +190,50 @@ Updates account display name and avatar URL. The frontend uploads avatar files t
`GET /api/account/devices`
Returns the current local web session. This version does not maintain a multi-device session registry yet, so the current device is read-only and cannot be removed.
Returns every non-revoked, non-expired web session for the authenticated account. The current session is listed first, followed by most recently active sessions.
```json
{
"devices": [
{
"id": "62dc9af7-e44a-4f47-a0bc-b3408d17c33f",
"online": true,
"current": true,
"deviceType": "desktop",
"system": "macOS",
"browser": "Chrome 126",
"client": "PC-WEB",
"lastSeenAt": "2026-07-11T09:00:00Z"
}
],
"limits": {
"desktop": 2,
"mobile": 1
}
}
```
`limits` is sourced from `Auth.DeviceLimits.Desktop` and `Auth.DeviceLimits.Mobile` in service configuration. It is returned with the device list so clients display the effective policy without hard-coded counts. Defaults are 2 desktop web sessions and 1 mobile web session.
Each signed access and refresh token carries an opaque random session ID. Bearer authentication validates that server-side session before accepting the token, so revoked sessions stop working immediately. Device metadata is derived from request headers for display only and is never an authorization signal. A session is reported online when it is current or was active during the previous five minutes.
`lastSeenAt` is an RFC3339 UTC instant. Clients must render it in the browser's local timezone rather than displaying the UTC wire representation directly.
Valid legacy access tokens without a session ID are migrated to a revocable server-side session on first use after their user subject is verified. This preserves existing logins during rollout.
`DELETE /api/account/devices`
Removes non-current devices when a future server-side session registry exists. In the current local-only implementation, it returns `{"removed": 0}`.
Atomically revokes every active session for the account except the current session. Returns the number of sessions revoked:
```json
{
"removed": 2
}
```
`POST /api/account/logout`
Records a logout request server-side and returns `{"removed": 0}`. The frontend then clears its locally stored session.
Revokes the current server-side session and returns `{"removed": 1}` when it was active. The frontend then clears its locally stored token. Other device sessions remain signed in.
## Projects