From 2878d6a59be9c7f1d25703c7033b459e570e7827 Mon Sep 17 00:00:00 2001 From: Kristjan Komlosi Date: Sun, 4 Jan 2026 13:32:57 +0100 Subject: [PATCH] feat: implement device management endpoints and database connection --- cmd/server/main.go | 35 ++++++- internal/handler/handlers.go | 174 +++++++++++++++++++++++++++++++++-- 2 files changed, 195 insertions(+), 14 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 08f74f0..028a72e 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "database/sql" "fmt" "log" "net/http" @@ -11,6 +12,7 @@ import ( "time" "github.com/gin-gonic/gin" + _ "github.com/go-sql-driver/mysql" "git.piskot.si/SeminarM2/lambdaiot-core/internal/config" "git.piskot.si/SeminarM2/lambdaiot-core/internal/handler" @@ -25,6 +27,20 @@ func main() { log.Fatalf("failed to load config: %v", err) } + // connect to database + dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true", cfg.Database.User, cfg.Database.Password, cfg.Database.Host, cfg.Database.Port, cfg.Database.Name) + db, err := sql.Open("mysql", dsn) + if err != nil { + log.Fatalf("failed to connect to database: %v", err) + } + defer db.Close() + + // test connection + if err := db.Ping(); err != nil { + log.Fatalf("failed to ping database: %v", err) + } + log.Println("connected to database") + // determine address addr := cfg.Server.Address if addr == "" { @@ -69,18 +85,27 @@ func main() { r.Use(gin.Recovery()) r.Use(middleware.GinLogger()) + h := &handler.Handler{DB: db, JWTSecret: cfg.Server.JWTSecret} + // Public routes - r.GET("/health", handler.Health) - r.GET("/hello", handler.Hello) - r.POST("/login", handler.Login) + r.GET("/health", h.Health) + r.GET("/hello", h.Hello) + r.POST("/login", h.Login) + r.GET("/devices", h.GetDevices) // mqttping endpoint handled by internal/handler - r.POST("/mqttping", handler.MQTTPing) + r.POST("/mqttping", h.MQTTPing) // Protected routes auth := r.Group("/") auth.Use(middleware.AuthMiddleware(cfg.Server.JWTSecret)) - auth.GET("/protected", handler.Protected) + auth.GET("/protected", h.Protected) + + // Device CRUD routes + auth.POST("/devices", h.CreateDevice) + auth.GET("/devices/:id", h.GetDevice) + auth.PUT("/devices/:id", h.UpdateDevice) + auth.DELETE("/devices/:id", h.DeleteDevice) srv := &http.Server{ Addr: addr, diff --git a/internal/handler/handlers.go b/internal/handler/handlers.go index 4ad5989..ed7d61f 100644 --- a/internal/handler/handlers.go +++ b/internal/handler/handlers.go @@ -1,25 +1,43 @@ package handler import ( + "database/sql" "net/http" + "strings" "time" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" ) +type Handler struct { + DB *sql.DB + JWTSecret string +} + +type Device struct { + ID uuid.UUID `json:"id" db:"id"` + Name string `json:"name" db:"name"` + Description string `json:"description" db:"description"` + Location string `json:"location" db:"location"` + StatusID int `json:"status_id" db:"status_id"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + // Health returns basic service health -func Health(c *gin.Context) { +func (h *Handler) Health(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) } // Hello returns a simple greeting -func Hello(c *gin.Context) { +func (h *Handler) Hello(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "hello from lambda-iot core"}) } // Login issues a JWT token for demo purposes -func Login(c *gin.Context) { +func (h *Handler) Login(c *gin.Context) { var req struct { Username string `json:"username"` Password string `json:"password"` @@ -38,11 +56,7 @@ func Login(c *gin.Context) { "sub": req.Username, "exp": time.Now().Add(time.Hour).Unix(), }) - secret := "secret" - if s := c.GetHeader("X-JWT-SECRET"); s != "" { - secret = s - } - signed, err := token.SignedString([]byte(secret)) + signed, err := token.SignedString([]byte(h.JWTSecret)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "could not sign token"}) return @@ -50,8 +64,150 @@ func Login(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"token": signed}) } +// CreateDevice creates a new device +func (h *Handler) CreateDevice(c *gin.Context) { + var req struct { + Name string `json:"name" binding:"required"` + Description string `json:"description" binding:"required"` + Location string `json:"location" binding:"required"` + StatusID int `json:"status_id" binding:"required,min=1,max=4"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + id := uuid.New() + _, err := h.DB.Exec("INSERT INTO devices (id, name, description, location, status_id) VALUES (UUID_TO_BIN(?), ?, ?, ?, ?)", id.String(), req.Name, req.Description, req.Location, req.StatusID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create device"}) + return + } + + c.JSON(http.StatusCreated, gin.H{"id": id.String()}) +} + +// GetDevices retrieves all devices +func (h *Handler) GetDevices(c *gin.Context) { + rows, err := h.DB.Query("SELECT BIN_TO_UUID(id), name, description, location, status_id, created_at, updated_at FROM devices") + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch devices"}) + return + } + defer rows.Close() + + var devices []Device + for rows.Next() { + var d Device + err := rows.Scan(&d.ID, &d.Name, &d.Description, &d.Location, &d.StatusID, &d.CreatedAt, &d.UpdatedAt) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to scan device"}) + return + } + devices = append(devices, d) + } + + c.JSON(http.StatusOK, devices) +} + +// GetDevice retrieves a single device by ID +func (h *Handler) GetDevice(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid device id"}) + return + } + + var d Device + err = h.DB.QueryRow("SELECT BIN_TO_UUID(id), name, description, location, status_id, created_at, updated_at FROM devices WHERE id = UUID_TO_BIN(?)", id.String()).Scan(&d.ID, &d.Name, &d.Description, &d.Location, &d.StatusID, &d.CreatedAt, &d.UpdatedAt) + if err == sql.ErrNoRows { + c.JSON(http.StatusNotFound, gin.H{"error": "device not found"}) + return + } else if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch device"}) + return + } + + c.JSON(http.StatusOK, d) +} + +// UpdateDevice updates a device +func (h *Handler) UpdateDevice(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid device id"}) + return + } + + var req struct { + Name *string `json:"name"` + Description *string `json:"description"` + Location *string `json:"location"` + StatusID *int `json:"status_id"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Build update query dynamically + setParts := []string{} + args := []interface{}{} + if req.Name != nil { + setParts = append(setParts, "name = ?") + args = append(args, *req.Name) + } + if req.Description != nil { + setParts = append(setParts, "description = ?") + args = append(args, *req.Description) + } + if req.Location != nil { + setParts = append(setParts, "location = ?") + args = append(args, *req.Location) + } + if req.StatusID != nil { + setParts = append(setParts, "status_id = ?") + args = append(args, *req.StatusID) + } + if len(setParts) == 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"}) + return + } + + query := "UPDATE devices SET " + strings.Join(setParts, ", ") + " WHERE id = UUID_TO_BIN(?)" + args = append(args, id.String()) + + _, err = h.DB.Exec(query, args...) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update device"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "device updated"}) +} + +// DeleteDevice deletes a device +func (h *Handler) DeleteDevice(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid device id"}) + return + } + + _, err = h.DB.Exec("DELETE FROM devices WHERE id = UUID_TO_BIN(?)", id.String()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete device"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "device deleted"}) +} + // Protected requires a valid JWT and returns the token claims -func Protected(c *gin.Context) { +func (h *Handler) Protected(c *gin.Context) { if v, ok := c.Get("claims"); ok { c.JSON(http.StatusOK, gin.H{"claims": v}) return