created tracker api endpoint and created UI interface for trackers
This commit is contained in:
@@ -17,5 +17,7 @@ func AutoMigrate(db *gorm.DB) error {
|
||||
&models.Device{},
|
||||
&models.Record{},
|
||||
&models.UserDevice{},
|
||||
&models.Tracker{},
|
||||
&models.UserTracker{},
|
||||
)
|
||||
}
|
||||
|
||||
44
server/internal/dto/tracker.go
Normal file
44
server/internal/dto/tracker.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package dto
|
||||
|
||||
import "smoop-api/internal/models"
|
||||
|
||||
type TrackerDto struct {
|
||||
GUID string `json:"guid"`
|
||||
Name string `json:"name"`
|
||||
Users []UserDto `json:"users,omitempty"`
|
||||
}
|
||||
|
||||
type TrackerListDto struct {
|
||||
Trackers []TrackerDto `json:"trackers"`
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
type CreateTrackerDto struct {
|
||||
GUID string `json:"guid" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
UserIDs []uint `json:"userIds"`
|
||||
}
|
||||
|
||||
type RenameTrackerDto struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
}
|
||||
|
||||
type SetTrackerUsersDto struct {
|
||||
UserIDs []uint `json:"userIds"`
|
||||
}
|
||||
|
||||
func MapTracker(t models.Tracker) TrackerDto {
|
||||
out := TrackerDto{
|
||||
GUID: t.GUID,
|
||||
Name: t.Name,
|
||||
}
|
||||
if len(t.Users) > 0 {
|
||||
out.Users = make([]UserDto, 0, len(t.Users))
|
||||
for _, u := range t.Users {
|
||||
out.Users = append(out.Users, MapUser(u))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
212
server/internal/handlers/trackers.go
Normal file
212
server/internal/handlers/trackers.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"smoop-api/internal/dto"
|
||||
"smoop-api/internal/models"
|
||||
)
|
||||
|
||||
type TrackersHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewTrackersHandler(db *gorm.DB) *TrackersHandler { return &TrackersHandler{db: db} }
|
||||
|
||||
// GET /trackers — list trackers available for user (admin: all)
|
||||
func (h *TrackersHandler) 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
|
||||
}
|
||||
|
||||
// Default behavior (fallback if middleware not present): use user context
|
||||
isFilter := true
|
||||
var userID uint
|
||||
|
||||
if v, ok := c.Get("filterTrackers"); ok {
|
||||
if b, ok2 := v.(bool); ok2 {
|
||||
isFilter = b
|
||||
}
|
||||
}
|
||||
if v, ok := c.Get("userID"); ok {
|
||||
if id, ok2 := v.(uint); ok2 {
|
||||
userID = id
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to claims if middleware wasn’t applied
|
||||
if _, exists := c.Get("filterTrackers"); !exists {
|
||||
if uc, ok := GetUserContext(c); ok {
|
||||
if uc.Role == models.RoleAdmin {
|
||||
isFilter = false
|
||||
} else {
|
||||
isFilter = true
|
||||
userID = uc.ID
|
||||
}
|
||||
} else {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
total int64
|
||||
list []models.Tracker
|
||||
err error
|
||||
)
|
||||
|
||||
if !isFilter {
|
||||
// Admin: all trackers
|
||||
err = h.db.Model(&models.Tracker{}).Count(&total).Error
|
||||
if err == nil {
|
||||
err = h.db.Preload("Users").Offset(offset).Limit(limit).Find(&list).Error
|
||||
}
|
||||
} else {
|
||||
// Filtered by userID
|
||||
q := h.db.Model(&models.Tracker{}).
|
||||
Joins("JOIN user_trackers ut ON ut.tracker_guid = trackers.guid").
|
||||
Where("ut.user_id = ?", userID)
|
||||
if err = q.Count(&total).Error; err == nil {
|
||||
err = h.db.Preload("Users").
|
||||
Joins("JOIN user_trackers ut ON ut.tracker_guid = trackers.guid").
|
||||
Where("ut.user_id = ?", userID).
|
||||
Offset(offset).Limit(limit).
|
||||
Find(&list).Error
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
out := make([]dto.TrackerDto, 0, len(list))
|
||||
for _, t := range list {
|
||||
out = append(out, dto.MapTracker(t))
|
||||
}
|
||||
c.JSON(http.StatusOK, dto.TrackerListDto{
|
||||
Trackers: out, Offset: offset, Limit: limit, Total: total,
|
||||
})
|
||||
}
|
||||
|
||||
// POST /trackers/create — create tracker; optional initial user assignments
|
||||
func (h *TrackersHandler) Create(c *gin.Context) {
|
||||
var req dto.CreateTrackerDto
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
t := models.Tracker{GUID: req.GUID, Name: req.Name}
|
||||
if err := h.db.Create(&t).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "tracker exists?"})
|
||||
return
|
||||
}
|
||||
// optional users
|
||||
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(&t).Association("Users").Append(&users); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "link failed"})
|
||||
return
|
||||
}
|
||||
}
|
||||
var withUsers models.Tracker
|
||||
if err := h.db.Preload("Users").Where("guid = ?", t.GUID).First(&withUsers).Error; err != nil {
|
||||
c.JSON(http.StatusCreated, dto.TrackerDto{GUID: t.GUID, Name: t.Name})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, dto.MapTracker(withUsers))
|
||||
}
|
||||
|
||||
// POST /trackers/:guid/rename — rename tracker
|
||||
func (h *TrackersHandler) Rename(c *gin.Context) {
|
||||
guid := c.Param("guid")
|
||||
var t models.Tracker
|
||||
if err := h.db.Where("guid = ?", guid).First(&t).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "tracker not found"})
|
||||
return
|
||||
}
|
||||
var req dto.RenameTrackerDto
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
t.Name = req.Name
|
||||
if err := h.db.Save(&t).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "save failed"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, dto.TrackerDto{GUID: t.GUID, Name: t.Name})
|
||||
}
|
||||
|
||||
// POST /trackers/:guid/set_users — replace full user list (admin)
|
||||
func (h *TrackersHandler) SetUsers(c *gin.Context) {
|
||||
guid := c.Param("guid")
|
||||
var t models.Tracker
|
||||
if err := h.db.Where("guid = ?", guid).First(&t).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "tracker not found"})
|
||||
return
|
||||
}
|
||||
var req dto.SetTrackerUsersDto
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
if err := h.db.Model(&t).Association("Users").Clear(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "clear failed"})
|
||||
return
|
||||
}
|
||||
if len(users) > 0 {
|
||||
if err := h.db.Model(&t).Association("Users").Append(&users); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "link failed"})
|
||||
return
|
||||
}
|
||||
}
|
||||
var withUsers models.Tracker
|
||||
_ = h.db.Preload("Users").Where("guid = ?", t.GUID).First(&withUsers).Error
|
||||
c.JSON(http.StatusCreated, dto.MapTracker(withUsers))
|
||||
}
|
||||
|
||||
// local helper (same as devices.go)
|
||||
func (h *TrackersHandler) 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
|
||||
}
|
||||
@@ -35,3 +35,32 @@ func DeviceAccessFilter() gin.HandlerFunc {
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// TrackerAccessFilter middleware sets filtering context for tracker access
|
||||
func TrackerAccessFilter() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userContext, exists := c.Get("user")
|
||||
if !exists {
|
||||
c.JSON(401, gin.H{"error": "unauthorized"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
user, ok := userContext.(handlers.UserContext)
|
||||
if !ok {
|
||||
c.JSON(401, gin.H{"error": "invalid user data"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Set filter flag and user ID in context (mirrors devices)
|
||||
if user.Role == models.RoleAdmin {
|
||||
c.Set("filterTrackers", false) // Admin sees all trackers
|
||||
} else {
|
||||
c.Set("filterTrackers", true) // Regular user needs filtering
|
||||
c.Set("userID", user.ID) // Store user ID for filtering (same key as devices)
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
11
server/internal/models/tracker.go
Normal file
11
server/internal/models/tracker.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Tracker struct {
|
||||
GUID string `gorm:"primaryKey"`
|
||||
Name string `gorm:"size:255;not null"`
|
||||
Users []User `gorm:"many2many:user_trackers;constraint:OnDelete:CASCADE;"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
@@ -10,11 +10,12 @@ const (
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Username string `gorm:"uniqueIndex;size:255;not null"`
|
||||
Password string `gorm:"not null"`
|
||||
Role Role `gorm:"type:varchar(16);not null;default:'user'"`
|
||||
Devices []Device `gorm:"many2many:user_devices;constraint:OnDelete:CASCADE;"`
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Username string `gorm:"uniqueIndex;size:255;not null"`
|
||||
Password string `gorm:"not null"`
|
||||
Role Role `gorm:"type:varchar(16);not null;default:'user'"`
|
||||
Devices []Device `gorm:"many2many:user_devices;constraint:OnDelete:CASCADE;"`
|
||||
Trackers []Tracker `gorm:"many2many:user_trackers;constraint:OnDelete:CASCADE;"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
12
server/internal/models/user_tracker.go
Normal file
12
server/internal/models/user_tracker.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type UserTracker struct {
|
||||
UserID uint `gorm:"primaryKey;index;not null"`
|
||||
TrackerGUID string `gorm:"primaryKey;size:64;index;not null"`
|
||||
CreatedAt time.Time
|
||||
|
||||
User User `gorm:"constraint:OnDelete:CASCADE;"`
|
||||
Tracker Tracker `gorm:"constraint:OnDelete:CASCADE;foreignKey:TrackerGUID;references:GUID"`
|
||||
}
|
||||
@@ -29,6 +29,9 @@ func Build(db *gorm.DB, minio *minio.Client, cfg *config.Config) *gin.Engine {
|
||||
// --- MediaMTX handler
|
||||
mediamtxH := handlers.NewMediaMTXHandler(db, jwtMgr, cfg.MediaMTX)
|
||||
|
||||
/// --- GPS tracker handler
|
||||
trackersH := handlers.NewTrackersHandler(db)
|
||||
|
||||
// --- Public auth
|
||||
r.POST("/auth/signup", authH.SignUp)
|
||||
r.POST("/auth/signin", authH.SignIn)
|
||||
@@ -73,6 +76,10 @@ func Build(db *gorm.DB, minio *minio.Client, cfg *config.Config) *gin.Engine {
|
||||
r.GET("/mediamtx/paths", authMW, adminOnly, mediamtxH.ListPaths)
|
||||
r.POST("/mediamtx/webrtc/kick/:id", authMW, adminOnly, mediamtxH.KickWebRTC)
|
||||
|
||||
r.GET("/trackers", authMW, middleware.TrackerAccessFilter(), trackersH.List)
|
||||
r.POST("/trackers/create", authMW, trackersH.Create)
|
||||
r.POST("/trackers/:guid/rename", authMW, trackersH.Rename)
|
||||
r.POST("/trackers/:guid/set_users", authMW, adminOnly, trackersH.SetUsers)
|
||||
// sensible defaults
|
||||
r.MaxMultipartMemory = 64 << 20 // 64 MiB
|
||||
_ = time.Now() // appease linters
|
||||
|
||||
Reference in New Issue
Block a user