-
+
Create device
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
- {{ errorMsg }}
-
+
+
+
+
+
{{ errorMsg }}
-
-
-
-
\ No newline at end of file
+
diff --git a/server/internal/db/db.go b/server/internal/db/db.go
index 9c87ae8..712830c 100644
--- a/server/internal/db/db.go
+++ b/server/internal/db/db.go
@@ -22,5 +22,6 @@ func AutoMigrate(db *gorm.DB) error {
&models.DEviceTask{},
&models.DeviceCertificate{},
&models.RevokedSerial{},
+ &models.DeviceConfig{},
)
}
diff --git a/server/internal/dto/cert.go b/server/internal/dto/cert.go
new file mode 100644
index 0000000..3d0e646
--- /dev/null
+++ b/server/internal/dto/cert.go
@@ -0,0 +1,36 @@
+package dto
+
+import (
+ "smoop-api/internal/models"
+ "time"
+)
+
+type DeviceCertDto struct {
+ ID uint `json:"id"`
+ DeviceGUID string `json:"deviceGuid"`
+ SerialHex string `json:"serialHex"`
+ IssuerCN string `json:"issuerCN"`
+ SubjectDN string `json:"subjectDN"`
+ NotBefore time.Time `json:"notBefore"`
+ NotAfter time.Time `json:"notAfter"`
+ CreatedAt time.Time `json:"createdAt"`
+ // PemCert is sensitive/noisy; expose only if you really need it:
+ // PemCert string `json:"pemCert,omitempty"`
+}
+
+type DeviceCertListDto struct {
+ Certs []DeviceCertDto `json:"certs"`
+}
+
+func MapDeviceCert(c models.DeviceCertificate) DeviceCertDto {
+ return DeviceCertDto{
+ ID: c.ID,
+ DeviceGUID: c.DeviceGUID,
+ SerialHex: c.SerialHex,
+ IssuerCN: c.IssuerCN,
+ SubjectDN: c.SubjectDN,
+ NotBefore: c.NotBefore,
+ NotAfter: c.NotAfter,
+ CreatedAt: c.CreatedAt,
+ }
+}
diff --git a/server/internal/dto/device_config.go b/server/internal/dto/device_config.go
new file mode 100644
index 0000000..51e0012
--- /dev/null
+++ b/server/internal/dto/device_config.go
@@ -0,0 +1,35 @@
+package dto
+
+import "smoop-api/internal/models"
+
+type DeviceConfigDto struct {
+ MGuid string `json:"m_guid"`
+ MRecordingDuration int `json:"m_recordingDuration"`
+ MBaseURL string `json:"m_baseUrl"`
+ MPolling int `json:"m_polling"`
+ MJitter int `json:"m_jitter"`
+}
+
+type CreateDeviceConfigDto struct {
+ MRecordingDuration int `json:"m_recordingDuration" binding:"required"`
+ MBaseURL string `json:"m_baseUrl" binding:"required"`
+ MPolling int `json:"m_polling" binding:"required"`
+ MJitter int `json:"m_jitter" binding:"required"`
+}
+
+type UpdateDeviceConfigDto struct {
+ MRecordingDuration *int `json:"m_recordingDuration,omitempty"`
+ MBaseURL *string `json:"m_baseUrl,omitempty"`
+ MPolling *int `json:"m_polling,omitempty"`
+ MJitter *int `json:"m_jitter,omitempty"`
+}
+
+func MapDeviceConfig(cfg models.DeviceConfig) DeviceConfigDto {
+ return DeviceConfigDto{
+ MGuid: cfg.MGuid,
+ MRecordingDuration: cfg.MRecordingDuration,
+ MBaseURL: cfg.MBaseURL,
+ MPolling: cfg.MPolling,
+ MJitter: cfg.MJitter,
+ }
+}
diff --git a/server/internal/handlers/devices.go b/server/internal/handlers/devices.go
index 61c3c62..319981b 100644
--- a/server/internal/handlers/devices.go
+++ b/server/internal/handlers/devices.go
@@ -1,6 +1,7 @@
package handlers
import (
+ "errors"
"fmt"
"net/http"
"strconv"
@@ -302,5 +303,147 @@ func (h *DevicesHandler) ListCertsByDevice(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
return
}
- c.JSON(http.StatusOK, gin.H{"certs": list})
+ 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))
}
diff --git a/server/internal/models/device.go b/server/internal/models/device.go
index 51b4c00..041b300 100644
--- a/server/internal/models/device.go
+++ b/server/internal/models/device.go
@@ -11,4 +11,6 @@ type Device struct {
Certs []DeviceCertificate `gorm:"foreignKey:DeviceGUID;references:GUID;constraint:OnDelete:CASCADE"`
CreatedAt time.Time
UpdatedAt time.Time
+
+ Config *DeviceConfig `gorm:"foreignKey:DeviceGUID;references:GUID;constraint:OnDelete:CASCADE"`
}
diff --git a/server/internal/models/device_config.go b/server/internal/models/device_config.go
new file mode 100644
index 0000000..c8b007e
--- /dev/null
+++ b/server/internal/models/device_config.go
@@ -0,0 +1,19 @@
+package models
+
+import "time"
+
+// One-to-one config bound to a device GUID.
+type DeviceConfig struct {
+ DeviceGUID string `gorm:"primaryKey;size:64"` // 1:1 with Device.GUID
+ // Fields reflect your device JSON keys (m_*)
+ MGuid string `gorm:"size:64;not null"` // duplicate for device FW convenience
+ MRecordingDuration int `gorm:"not null;default:240"`
+ MBaseURL string `gorm:"size:512;not null"`
+ MPolling int `gorm:"not null;default:30"`
+ MJitter int `gorm:"not null;default:10"`
+
+ CreatedAt time.Time
+ UpdatedAt time.Time
+
+ Device Device `gorm:"constraint:OnDelete:CASCADE;foreignKey:DeviceGUID;references:GUID"`
+}
diff --git a/server/internal/router/router.go b/server/internal/router/router.go
index bdd16f9..a7189ce 100644
--- a/server/internal/router/router.go
+++ b/server/internal/router/router.go
@@ -62,6 +62,9 @@ func Build(db *gorm.DB, minio *minio.Client, cfg *config.Config) *gin.Engine {
r.GET("/device/:guid/tasks", authMW, middleware.DeviceAccessFilter(), tasksH.ListDeviceTasks)
r.GET("/device/:guid/certs", authMW, adminOnly, devH.ListCertsByDevice)
r.POST("/certs/revoke", authMW, adminOnly, certsAdminH.Revoke)
+ r.GET("/device/:guid/config", authMW, middleware.DeviceAccessFilter(), devH.GetDeviceConfig)
+ r.POST("/device/:guid/config", authMW, adminOnly, devH.CreateDeviceConfig)
+ r.PUT("/device/:guid/config", authMW, middleware.DeviceAccessFilter(), devH.UpdateDeviceConfig)
r.POST("/records/upload", middleware.MTLSGuardUpload(db), recH.Upload)
r.GET("/records", authMW, recH.List)