created device config, created new UI elements in device dashboard
This commit is contained in:
@@ -22,5 +22,6 @@ func AutoMigrate(db *gorm.DB) error {
|
||||
&models.DEviceTask{},
|
||||
&models.DeviceCertificate{},
|
||||
&models.RevokedSerial{},
|
||||
&models.DeviceConfig{},
|
||||
)
|
||||
}
|
||||
|
||||
36
server/internal/dto/cert.go
Normal file
36
server/internal/dto/cert.go
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
35
server/internal/dto/device_config.go
Normal file
35
server/internal/dto/device_config.go
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
19
server/internal/models/device_config.go
Normal file
19
server/internal/models/device_config.go
Normal file
@@ -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"`
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user