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:
@@ -0,0 +1,79 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mileusna/useragent"
|
||||
)
|
||||
|
||||
const (
|
||||
unknownDeviceSystem = "unknown"
|
||||
unknownDeviceBrowser = "unknown"
|
||||
)
|
||||
|
||||
func deviceSessionFromMetadata(metadata RequestMetadata) DeviceSession {
|
||||
parsed := useragent.Parse(strings.TrimSpace(metadata.UserAgent))
|
||||
deviceType := "desktop"
|
||||
client := "PC-WEB"
|
||||
if clientHintMobile(metadata.Mobile) || parsed.Mobile || parsed.Tablet {
|
||||
deviceType = "mobile"
|
||||
client = "MOBILE-WEB"
|
||||
}
|
||||
|
||||
system := cleanDeviceLabel(parsed.OS, unknownDeviceSystem)
|
||||
if platform := cleanClientHint(metadata.Platform); platform != "" {
|
||||
system = platform
|
||||
}
|
||||
browser := cleanDeviceLabel(parsed.Name, unknownDeviceBrowser)
|
||||
if parsed.Version != "" && browser != unknownDeviceBrowser {
|
||||
browser += " " + majorVersion(parsed.Version)
|
||||
}
|
||||
|
||||
return DeviceSession{
|
||||
DeviceType: deviceType,
|
||||
System: system,
|
||||
Browser: browser,
|
||||
Client: client,
|
||||
UserAgent: limitMetadata(strings.TrimSpace(metadata.UserAgent), 1024),
|
||||
}
|
||||
}
|
||||
|
||||
func clientHintMobile(value string) bool {
|
||||
value = strings.Trim(strings.TrimSpace(value), `"`)
|
||||
return value == "1" || strings.EqualFold(value, "true") || strings.EqualFold(value, "?1")
|
||||
}
|
||||
|
||||
func cleanClientHint(value string) string {
|
||||
value = strings.Trim(strings.TrimSpace(value), `"`)
|
||||
if value == "" || strings.EqualFold(value, "unknown") {
|
||||
return ""
|
||||
}
|
||||
return limitMetadata(value, 80)
|
||||
}
|
||||
|
||||
func cleanDeviceLabel(value string, fallback string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" || strings.EqualFold(value, "unknown") {
|
||||
return fallback
|
||||
}
|
||||
return limitMetadata(value, 80)
|
||||
}
|
||||
|
||||
func majorVersion(version string) string {
|
||||
version = strings.TrimSpace(version)
|
||||
if index := strings.IndexByte(version, '.'); index >= 0 {
|
||||
version = version[:index]
|
||||
}
|
||||
return limitMetadata(version, 16)
|
||||
}
|
||||
|
||||
func limitMetadata(value string, maxRunes int) string {
|
||||
if maxRunes <= 0 {
|
||||
return ""
|
||||
}
|
||||
runes := []rune(value)
|
||||
if len(runes) <= maxRunes {
|
||||
return value
|
||||
}
|
||||
return string(runes[:maxRunes])
|
||||
}
|
||||
Reference in New Issue
Block a user