Files
SingBox-Gopanel/internal/service/session.go
CN-JS-HuiBai b3435e5ef8
Some checks failed
build / build (api, amd64, linux) (push) Has been cancelled
build / build (api, arm64, linux) (push) Has been cancelled
build / build (api.exe, amd64, windows) (push) Has been cancelled
基本功能已初步完善
2026-04-17 20:41:47 +08:00

251 lines
6.0 KiB
Go

package service
import (
"crypto/sha256"
"encoding/hex"
"sort"
"strconv"
"time"
"xboard-go/internal/database"
"xboard-go/internal/model"
"github.com/google/uuid"
)
const sessionTTL = 7 * 24 * time.Hour
type SessionRecord struct {
ID string `json:"id"`
UserID int `json:"user_id"`
TokenHash string `json:"token_hash"`
Name string `json:"name"`
UserAgent string `json:"user_agent"`
IP string `json:"ip"`
CreatedAt int64 `json:"created_at"`
LastUsedAt int64 `json:"last_used_at"`
ExpiresAt int64 `json:"expires_at"`
}
func TrackSession(userID int, token, ip, userAgent string) SessionRecord {
now := time.Now().Unix()
tokenHash := hashToken(token)
list := activeSessions(userID, now)
for i := range list {
if list[i].TokenHash == tokenHash {
list[i].IP = firstNonEmpty(ip, list[i].IP)
list[i].UserAgent = firstNonEmpty(userAgent, list[i].UserAgent)
list[i].LastUsedAt = now
saveSessions(userID, list)
syncUserSessionOnlineState(userID, list[i].IP, now)
return list[i]
}
}
record := SessionRecord{
ID: uuid.NewString(),
UserID: userID,
TokenHash: tokenHash,
Name: "Web Session",
UserAgent: userAgent,
IP: ip,
CreatedAt: now,
LastUsedAt: now,
ExpiresAt: now + int64(sessionTTL.Seconds()),
}
list = append(list, record)
saveSessions(userID, list)
syncUserSessionOnlineState(userID, record.IP, now)
return record
}
func GetUserSessions(userID int, currentToken string) []SessionRecord {
now := time.Now().Unix()
list := activeSessions(userID, now)
if currentToken != "" {
currentHash := hashToken(currentToken)
found := false
for i := range list {
if list[i].TokenHash == currentHash {
found = true
break
}
}
if !found {
list = append(list, SessionRecord{
ID: uuid.NewString(),
UserID: userID,
TokenHash: currentHash,
Name: "Web Session",
CreatedAt: now,
LastUsedAt: now,
ExpiresAt: now + int64(sessionTTL.Seconds()),
})
saveSessions(userID, list)
}
}
return activeSessions(userID, now)
}
func RemoveUserSession(userID int, sessionID string) bool {
now := time.Now().Unix()
list := activeSessions(userID, now)
next := make([]SessionRecord, 0, len(list))
removed := false
for _, session := range list {
if session.ID != sessionID {
next = append(next, session)
continue
}
removed = true
ttl := time.Until(time.Unix(session.ExpiresAt, 0))
if ttl < time.Hour {
ttl = time.Hour
}
_ = database.CacheSet(revokedTokenKey(session.TokenHash), true, ttl)
}
if removed {
saveSessions(userID, next)
}
return removed
}
func IsSessionTokenRevoked(token string) bool {
if token == "" {
return false
}
_, ok := database.CacheGetJSON[bool](revokedTokenKey(hashToken(token)))
return ok
}
func StoreQuickLoginToken(userID int, ttl time.Duration) string {
verify := uuid.NewString()
_ = database.CacheSet(quickLoginKey(verify), userID, ttl)
return verify
}
func ResolveQuickLoginToken(verify string) (int, bool) {
userID, ok := database.CacheGetJSON[int](quickLoginKey(verify))
if ok {
_ = database.CacheDelete(quickLoginKey(verify))
}
return userID, ok
}
func StoreEmailVerifyCode(email, code string, ttl time.Duration) {
_ = database.CacheSet(emailVerifyKey(email), code, ttl)
}
func CheckEmailVerifyCode(email, code string) bool {
savedCode, ok := database.CacheGetJSON[string](emailVerifyKey(email))
if !ok || savedCode == "" || savedCode != code {
return false
}
_ = database.CacheDelete(emailVerifyKey(email))
return true
}
func activeSessions(userID int, now int64) []SessionRecord {
sessions, ok := database.CacheGetJSON[[]SessionRecord](userSessionsKey(userID))
if !ok || len(sessions) == 0 {
return []SessionRecord{}
}
filtered := make([]SessionRecord, 0, len(sessions))
for _, session := range sessions {
if session.ExpiresAt > 0 && session.ExpiresAt <= now {
continue
}
filtered = append(filtered, session)
}
sort.SliceStable(filtered, func(i, j int) bool {
return filtered[i].LastUsedAt > filtered[j].LastUsedAt
})
if len(filtered) != len(sessions) {
saveSessions(userID, filtered)
}
return filtered
}
func saveSessions(userID int, sessions []SessionRecord) {
sort.SliceStable(sessions, func(i, j int) bool {
return sessions[i].LastUsedAt > sessions[j].LastUsedAt
})
_ = database.CacheSet(userSessionsKey(userID), sessions, sessionTTL)
}
func syncUserSessionOnlineState(userID int, ip string, now int64) {
updates := map[string]any{
"last_online_at": time.Unix(now, 0),
"updated_at": now,
}
if sessions := activeSessions(userID, now); len(sessions) > 0 {
updates["online_count"] = len(sessions)
}
_ = database.DB.Model(&model.User{}).Where("id = ?", userID).Updates(updates).Error
ip = firstNonEmpty(ip)
if ip == "" {
return
}
record := model.UserOnlineDevice{
UserID: userID,
NodeID: 0,
IP: ip,
FirstSeenAt: now,
LastSeenAt: now,
ExpiresAt: now + int64(sessionTTL.Seconds()),
CreatedAt: now,
UpdatedAt: now,
}
var existing model.UserOnlineDevice
if err := database.DB.Where("user_id = ? AND node_id = ? AND ip = ?", userID, 0, ip).First(&existing).Error; err == nil {
_ = database.DB.Model(&model.UserOnlineDevice{}).Where("id = ?", existing.ID).Updates(map[string]any{
"last_seen_at": now,
"expires_at": record.ExpiresAt,
"updated_at": now,
}).Error
return
}
_ = database.DB.Create(&record).Error
}
func hashToken(token string) string {
sum := sha256.Sum256([]byte(token))
return hex.EncodeToString(sum[:])
}
func userSessionsKey(userID int) string {
return "session:user:" + strconv.Itoa(userID)
}
func revokedTokenKey(tokenHash string) string {
return "session:revoked:" + tokenHash
}
func quickLoginKey(verify string) string {
return "session:quick-login:" + verify
}
func emailVerifyKey(email string) string {
return "session:email-code:" + email
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if value != "" {
return value
}
}
return ""
}