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
@@ -0,0 +1,62 @@
package auth
import (
"strings"
"testing"
)
func TestDeviceSessionFromMetadata(t *testing.T) {
tests := []struct {
name string
metadata RequestMetadata
deviceType string
system string
browser string
client string
}{
{
name: "unknown desktop fallback",
metadata: RequestMetadata{},
deviceType: "desktop",
system: "unknown",
browser: "unknown",
client: "PC-WEB",
},
{
name: "desktop chrome",
metadata: RequestMetadata{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/126.0.0.0 Safari/537.36",
Platform: `"Windows"`,
Mobile: "?0",
},
deviceType: "desktop",
system: "Windows",
browser: "Chrome 126",
client: "PC-WEB",
},
{
name: "mobile safari from client hints",
metadata: RequestMetadata{
UserAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 Version/18.0 Mobile/15E148 Safari/604.1",
Platform: `"iOS"`,
Mobile: "?1",
},
deviceType: "mobile",
system: "iOS",
browser: "Safari 18",
client: "MOBILE-WEB",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
session := deviceSessionFromMetadata(test.metadata)
if session.DeviceType != test.deviceType || session.System != test.system || session.Client != test.client {
t.Fatalf("unexpected device metadata %#v", session)
}
if !strings.HasPrefix(session.Browser, test.browser) {
t.Fatalf("expected browser %q, got %q", test.browser, session.Browser)
}
})
}
}