214 lines
5.3 KiB
Go
214 lines
5.3 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"filefast/backend/internal/model"
|
|
"filefast/backend/internal/store"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type DeviceService struct {
|
|
store *store.MemoryStore
|
|
presenceStore devicePresenceStore
|
|
sessionStore deviceSessionStore
|
|
presenceTTL time.Duration
|
|
sessionTTL time.Duration
|
|
}
|
|
|
|
type devicePresenceStore interface {
|
|
SetDevicePresence(context.Context, string, bool, time.Time, time.Duration) error
|
|
GetDevicePresence(context.Context, []string) (map[string]bool, error)
|
|
}
|
|
|
|
type deviceSessionStore interface {
|
|
SaveDeviceSession(context.Context, model.DeviceSession, time.Duration) error
|
|
ValidateDeviceSession(context.Context, string, string) (bool, error)
|
|
}
|
|
|
|
type RegisterDeviceInput struct {
|
|
DeviceID string `json:"device_id"`
|
|
Name string `json:"name" binding:"required"`
|
|
Type string `json:"type" binding:"required"`
|
|
NetworkGroupKey string `json:"network_group_key"`
|
|
PublicIPHash string `json:"public_ip_hash"`
|
|
}
|
|
|
|
func NewDeviceService(store *store.MemoryStore, presenceStore devicePresenceStore, sessionStore deviceSessionStore) *DeviceService {
|
|
return &DeviceService{
|
|
store: store,
|
|
presenceStore: presenceStore,
|
|
sessionStore: sessionStore,
|
|
presenceTTL: 45 * time.Second,
|
|
sessionTTL: 30 * 24 * time.Hour,
|
|
}
|
|
}
|
|
|
|
func (s *DeviceService) Register(input RegisterDeviceInput, userAgent, claimedToken string) (model.Device, model.DeviceSession) {
|
|
now := time.Now()
|
|
id := strings.TrimSpace(input.DeviceID)
|
|
if id == "" {
|
|
id = uuid.NewString()
|
|
}
|
|
|
|
device, exists := s.store.GetDevice(id)
|
|
if exists && !s.ValidateSession(id, strings.TrimSpace(claimedToken)) {
|
|
id = uuid.NewString()
|
|
exists = false
|
|
}
|
|
if !exists {
|
|
device = model.Device{
|
|
ID: id,
|
|
CreatedAt: now,
|
|
}
|
|
}
|
|
|
|
device.Name = input.Name
|
|
device.Type = input.Type
|
|
device.UserAgent = userAgent
|
|
device.NetworkGroupKey = input.NetworkGroupKey
|
|
device.PublicIPHash = input.PublicIPHash
|
|
device.LastSeenAt = now
|
|
device.IsOnline = true
|
|
|
|
device = s.store.UpsertDevice(device)
|
|
session := s.issueSession(device.ID)
|
|
s.syncPresence(device)
|
|
return device, session
|
|
}
|
|
|
|
func (s *DeviceService) Heartbeat(deviceID string) (model.Device, bool) {
|
|
device, ok := s.store.GetDevice(deviceID)
|
|
if !ok {
|
|
return model.Device{}, false
|
|
}
|
|
|
|
device.LastSeenAt = time.Now()
|
|
device.IsOnline = true
|
|
device = s.store.UpsertDevice(device)
|
|
s.syncPresence(device)
|
|
return device, true
|
|
}
|
|
|
|
func (s *DeviceService) SetOnline(deviceID string, online bool) (model.Device, bool) {
|
|
device, ok := s.store.GetDevice(deviceID)
|
|
if !ok {
|
|
return model.Device{}, false
|
|
}
|
|
|
|
device.IsOnline = online
|
|
device.LastSeenAt = time.Now()
|
|
device = s.store.UpsertDevice(device)
|
|
s.syncPresence(device)
|
|
return device, true
|
|
}
|
|
|
|
func (s *DeviceService) ListCandidates(currentDeviceID string) []model.Device {
|
|
current, _ := s.store.GetDevice(currentDeviceID)
|
|
currentNetworkGroupKey := strings.TrimSpace(current.NetworkGroupKey)
|
|
if currentNetworkGroupKey == "" {
|
|
return []model.Device{}
|
|
}
|
|
|
|
devices := s.store.ListDevices()
|
|
s.applyPresence(devices)
|
|
candidates := make([]model.Device, 0, len(devices))
|
|
|
|
for _, device := range devices {
|
|
if device.ID == currentDeviceID || !device.IsOnline {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(device.NetworkGroupKey) != currentNetworkGroupKey {
|
|
continue
|
|
}
|
|
candidates = append(candidates, device)
|
|
}
|
|
|
|
sort.SliceStable(candidates, func(i, j int) bool {
|
|
return candidates[i].LastSeenAt.After(candidates[j].LastSeenAt)
|
|
})
|
|
|
|
return candidates
|
|
}
|
|
|
|
func (s *DeviceService) ValidateSession(deviceID, token string) bool {
|
|
deviceID = strings.TrimSpace(deviceID)
|
|
token = strings.TrimSpace(token)
|
|
if deviceID == "" || token == "" {
|
|
return false
|
|
}
|
|
|
|
if s.store.ValidateDeviceSession(deviceID, token) {
|
|
return true
|
|
}
|
|
|
|
if s.sessionStore == nil {
|
|
return false
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
ok, err := s.sessionStore.ValidateDeviceSession(ctx, deviceID, token)
|
|
return err == nil && ok
|
|
}
|
|
|
|
func (s *DeviceService) issueSession(deviceID string) model.DeviceSession {
|
|
session := model.DeviceSession{
|
|
DeviceID: deviceID,
|
|
Token: uuid.NewString(),
|
|
CreatedAt: time.Now(),
|
|
ExpiresAt: time.Now().Add(s.sessionTTL),
|
|
}
|
|
session = s.store.SaveDeviceSession(session)
|
|
|
|
if s.sessionStore != nil {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
_ = s.sessionStore.SaveDeviceSession(ctx, session, s.sessionTTL)
|
|
}
|
|
|
|
return session
|
|
}
|
|
|
|
func (s *DeviceService) syncPresence(device model.Device) {
|
|
if s.presenceStore == nil {
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
_ = s.presenceStore.SetDevicePresence(ctx, device.ID, device.IsOnline, device.LastSeenAt, s.presenceTTL)
|
|
}
|
|
|
|
func (s *DeviceService) applyPresence(devices []model.Device) {
|
|
if s.presenceStore == nil || len(devices) == 0 {
|
|
return
|
|
}
|
|
|
|
deviceIDs := make([]string, 0, len(devices))
|
|
for _, device := range devices {
|
|
if device.ID != "" {
|
|
deviceIDs = append(deviceIDs, device.ID)
|
|
}
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
statuses, err := s.presenceStore.GetDevicePresence(ctx, deviceIDs)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
for index := range devices {
|
|
if online, ok := statuses[devices[index].ID]; ok {
|
|
devices[index].IsOnline = online
|
|
}
|
|
}
|
|
}
|