Files
NewSmoop/server/internal/handlers/devices.go

450 lines
13 KiB
Go

package handlers
import (
"errors"
"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
}
out := make([]dto.DeviceCertDto, 0, len(list))
for _, it := range list {
out = append(out, dto.MapDeviceCert(it))
}
c.JSON(http.StatusOK, dto.DeviceCertListDto{Certs: out})
}
// GET /device/:guid/config (admin or assigned user — choose policy; here adminOnly for symmetry with certs)
func (h *DevicesHandler) GetDeviceConfig(c *gin.Context) {
guid := c.Param("guid")
// Ensure device exists
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 cfg models.DeviceConfig
if err := h.db.Where("device_guid = ?", guid).First(&cfg).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "config not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
return
}
c.JSON(http.StatusOK, dto.MapDeviceConfig(cfg))
}
// POST /device/:guid/config (create)
func (h *DevicesHandler) CreateDeviceConfig(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
}
// Ensure not exists
var exists int64
_ = h.db.Model(&models.DeviceConfig{}).Where("device_guid = ?", guid).Count(&exists).Error
if exists > 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "config already exists"})
return
}
var req dto.CreateDeviceConfigDto
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
cfg := models.DeviceConfig{
DeviceGUID: guid,
MGuid: guid,
MRecordingDuration: req.MRecordingDuration,
MBaseURL: req.MBaseURL,
MPolling: req.MPolling,
MJitter: req.MJitter,
}
if err := h.db.Create(&cfg).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "create failed"})
return
}
c.JSON(http.StatusCreated, dto.MapDeviceConfig(cfg))
}
// PUT /device/:guid/config (partial update)
func (h *DevicesHandler) UpdateDeviceConfig(c *gin.Context) {
guid := c.Param("guid")
var req dto.UpdateDeviceConfigDto
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var cfg models.DeviceConfig
err := h.db.Where("device_guid = ?", guid).First(&cfg).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// Create-on-update behavior
// m_baseUrl is required to create (NOT NULL constraint in model)
if req.MBaseURL == nil || strings.TrimSpace(*req.MBaseURL) == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "m_baseUrl is required to create config"})
return
}
// Defaults
recDur := 240
if req.MRecordingDuration != nil {
recDur = *req.MRecordingDuration
}
poll := 30
if req.MPolling != nil {
poll = *req.MPolling
}
jitter := 10
if req.MJitter != nil {
jitter = *req.MJitter
}
cfg = models.DeviceConfig{
DeviceGUID: guid,
MGuid: guid,
MRecordingDuration: recDur,
MBaseURL: strings.TrimSpace(*req.MBaseURL),
MPolling: poll,
MJitter: jitter,
}
if err := h.db.Create(&cfg).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "create failed"})
return
}
c.JSON(http.StatusCreated, dto.MapDeviceConfig(cfg))
return
}
// Other DB error
c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
return
}
// Patch only provided fields
if req.MRecordingDuration != nil {
cfg.MRecordingDuration = *req.MRecordingDuration
}
if req.MBaseURL != nil {
cfg.MBaseURL = strings.TrimSpace(*req.MBaseURL)
}
if req.MPolling != nil {
cfg.MPolling = *req.MPolling
}
if req.MJitter != nil {
cfg.MJitter = *req.MJitter
}
if err := h.db.Save(&cfg).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "save failed"})
return
}
c.JSON(http.StatusOK, dto.MapDeviceConfig(cfg))
}