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 } } }