251 lines
6.0 KiB
Go
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 ""
|
|
}
|