58f11302fe
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>
80 lines
2.0 KiB
Go
80 lines
2.0 KiB
Go
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])
|
|
}
|