package handlers import ( "fmt" "net/http" "strconv" "strings" "github.com/gin-gonic/gin" "gorm.io/gorm" "smoop-api/internal/dto" "smoop-api/internal/models" ) type DevicesHandler struct { db *gorm.DB } func NewDevicesHandler(db *gorm.DB) *DevicesHandler { return &DevicesHandler{db: db} } func (h *DevicesHandler) List(c *gin.Context) { offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0")) limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50")) if limit <= 0 || limit > 200 { limit = 50 } // Get user context userContext, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } user, ok := userContext.(UserContext) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid user data"}) return } var total int64 var devs []models.Device var err error if user.Role == models.RoleAdmin { // Admin user - show all devices err = h.db.Model(&models.Device{}).Count(&total).Error if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "count query failed: " + err.Error()}) return } err = h.db.Preload("Users").Offset(offset).Limit(limit).Find(&devs).Error } else { err = h.db.Model(&models.Device{}). Joins("INNER JOIN user_devices ON user_devices.id = devices.guid"). Where("user_devices.guid = ?", user.ID). Count(&total).Error if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "count query failed: " + err.Error()}) return } err = h.db.Preload("Users"). Joins("INNER JOIN user_devices ON user_devices.id = devices.guid"). Where("user_devices.guid = ?", user.ID). Offset(offset).Limit(limit). Find(&devs).Error } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed: " + err.Error()}) return } out := make([]dto.DeviceDto, 0, len(devs)) for _, d := range devs { out = append(out, dto.MapDevice(d)) } c.JSON(http.StatusOK, dto.DeviceListDto{Devices: out, Offset: offset, Limit: limit, Total: total}) } func (h *DevicesHandler) Create(c *gin.Context) { var req dto.CreateDeviceDto if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } d := models.Device{GUID: req.GUID, Name: req.Name} if err := h.db.Create(&d).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "device exists?"}) return } // Optional initial user assignments if len(req.UserIDs) > 0 { users, err := h.fetchUsers(req.UserIDs) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := h.db.Model(&d).Association("Users").Append(&users); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "link failed"}) return } } // Return with users var withUsers models.Device if err := h.db.Preload("Users").Where("guid = ?", d.GUID).First(&withUsers).Error; err != nil { c.JSON(http.StatusCreated, dto.DeviceDto{GUID: d.GUID, Name: d.Name}) return } c.JSON(http.StatusCreated, dto.MapDevice(withUsers)) } func (h *DevicesHandler) Rename(c *gin.Context) { guid := c.Param("guid") var d models.Device if err := h.db.Where("guid = ?", guid).First(&d).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "device not found"}) return } var req dto.RenameDeviceDto if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } d.Name = req.Name if err := h.db.Save(&d).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "save failed"}) return } c.JSON(http.StatusCreated, dto.DeviceDto{GUID: d.GUID, Name: d.Name}) } func (h *DevicesHandler) AddToUser(c *gin.Context) { guid := c.Param("guid") var d models.Device if err := h.db.Where("guid = ?", guid).First(&d).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "device not found"}) return } var req dto.EditDeviceToUserRelationDto if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ids := req.UserIDs if req.UserID != 0 { ids = append(ids, req.UserID) } if len(ids) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "userIds or userId required"}) return } users, err := h.fetchUsers(ids) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := h.db.Model(&d).Association("Users").Append(&users); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "link failed"}) return } var withUsers models.Device _ = h.db.Preload("Users").Where("guid = ?", d.GUID).First(&withUsers).Error c.JSON(http.StatusCreated, dto.MapDevice(withUsers)) } // SetUsers replaces the users of a device with the provided list. // Passing an empty list clears all assignments (covers the "no user assigned" case). func (h *DevicesHandler) SetUsers(c *gin.Context) { guid := c.Param("guid") var d models.Device if err := h.db.Where("guid = ?", guid).First(&d).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "device not found"}) return } var req dto.SetDeviceUsersDto if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Load users (if any) users := []models.User{} if len(req.UserIDs) > 0 { found, err := h.fetchUsers(req.UserIDs) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } users = found } // Replace association: Clear() then Append(new) if err := h.db.Model(&d).Association("Users").Clear(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "clear failed"}) return } if len(users) > 0 { if err := h.db.Model(&d).Association("Users").Append(&users); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "link failed"}) return } } var withUsers models.Device _ = h.db.Preload("Users").Where("guid = ?", d.GUID).First(&withUsers).Error c.JSON(http.StatusCreated, dto.MapDevice(withUsers)) } func (h *DevicesHandler) RemoveFromUser(c *gin.Context) { guid := c.Param("guid") var d models.Device if err := h.db.Where("guid = ?", guid).First(&d).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "device not found"}) return } var req dto.RemoveDeviceUsersDto _ = c.ShouldBindJSON(&req) // ignore error; we support query fallback ids := make([]uint, 0, len(req.UserIDs)+1) if req.UserID != 0 { ids = append(ids, req.UserID) } if len(req.UserIDs) > 0 { ids = append(ids, req.UserIDs...) } // query fallback if len(ids) == 0 { if q := strings.TrimSpace(c.Query("userId")); q != "" { if n, err := strconv.Atoi(q); err == nil && n > 0 { ids = append(ids, uint(n)) } } if q := strings.TrimSpace(c.Query("userIds")); q != "" { for _, p := range strings.Split(q, ",") { p = strings.TrimSpace(p) if n, err := strconv.Atoi(p); err == nil && n > 0 { ids = append(ids, uint(n)) } } } } if len(ids) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "userIds or userId required"}) return } users, err := h.fetchUsers(ids) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if len(users) == 0 { c.JSON(http.StatusOK, dto.DeviceDto{GUID: d.GUID, Name: d.Name}) return } if err := h.db.Model(&d).Association("Users").Delete(&users); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "unlink failed"}) return } var withUsers models.Device _ = h.db.Preload("Users").Where("guid = ?", d.GUID).First(&withUsers).Error c.JSON(http.StatusOK, dto.MapDevice(withUsers)) } func (h *DevicesHandler) fetchUsers(ids []uint) ([]models.User, error) { unique := make(map[uint]struct{}, len(ids)) clean := make([]uint, 0, len(ids)) for _, id := range ids { if id != 0 { if _, ok := unique[id]; !ok { unique[id] = struct{}{} clean = append(clean, id) } } } if len(clean) == 0 { return nil, nil } var users []models.User if err := h.db.Find(&users, clean).Error; err != nil { return nil, err } if len(users) != len(clean) { return nil, fmt.Errorf("some users not found") } return users, nil } func (h *DevicesHandler) ListCertsByDevice(c *gin.Context) { guid := c.Param("guid") var d models.Device if err := h.db.Where("guid = ?", guid).First(&d).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "device not found"}) return } var list []models.DeviceCertificate if err := h.db.Where("device_guid = ?", guid).Order("id desc").Find(&list).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"}) return } c.JSON(http.StatusOK, gin.H{"certs": list}) }