feat: implement device management endpoints and database connection
This commit is contained in:
+30
-5
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"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/config"
|
||||||
"git.piskot.si/SeminarM2/lambdaiot-core/internal/handler"
|
"git.piskot.si/SeminarM2/lambdaiot-core/internal/handler"
|
||||||
@@ -25,6 +27,20 @@ func main() {
|
|||||||
log.Fatalf("failed to load config: %v", err)
|
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
|
// determine address
|
||||||
addr := cfg.Server.Address
|
addr := cfg.Server.Address
|
||||||
if addr == "" {
|
if addr == "" {
|
||||||
@@ -69,18 +85,27 @@ func main() {
|
|||||||
r.Use(gin.Recovery())
|
r.Use(gin.Recovery())
|
||||||
r.Use(middleware.GinLogger())
|
r.Use(middleware.GinLogger())
|
||||||
|
|
||||||
|
h := &handler.Handler{DB: db, JWTSecret: cfg.Server.JWTSecret}
|
||||||
|
|
||||||
// Public routes
|
// Public routes
|
||||||
r.GET("/health", handler.Health)
|
r.GET("/health", h.Health)
|
||||||
r.GET("/hello", handler.Hello)
|
r.GET("/hello", h.Hello)
|
||||||
r.POST("/login", handler.Login)
|
r.POST("/login", h.Login)
|
||||||
|
r.GET("/devices", h.GetDevices)
|
||||||
|
|
||||||
// mqttping endpoint handled by internal/handler
|
// mqttping endpoint handled by internal/handler
|
||||||
r.POST("/mqttping", handler.MQTTPing)
|
r.POST("/mqttping", h.MQTTPing)
|
||||||
|
|
||||||
// Protected routes
|
// Protected routes
|
||||||
auth := r.Group("/")
|
auth := r.Group("/")
|
||||||
auth.Use(middleware.AuthMiddleware(cfg.Server.JWTSecret))
|
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{
|
srv := &http.Server{
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
|
|||||||
@@ -1,25 +1,43 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"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
|
// 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"})
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hello returns a simple greeting
|
// 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"})
|
c.JSON(http.StatusOK, gin.H{"message": "hello from lambda-iot core"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login issues a JWT token for demo purposes
|
// Login issues a JWT token for demo purposes
|
||||||
func Login(c *gin.Context) {
|
func (h *Handler) Login(c *gin.Context) {
|
||||||
var req struct {
|
var req struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
@@ -38,11 +56,7 @@ func Login(c *gin.Context) {
|
|||||||
"sub": req.Username,
|
"sub": req.Username,
|
||||||
"exp": time.Now().Add(time.Hour).Unix(),
|
"exp": time.Now().Add(time.Hour).Unix(),
|
||||||
})
|
})
|
||||||
secret := "secret"
|
signed, err := token.SignedString([]byte(h.JWTSecret))
|
||||||
if s := c.GetHeader("X-JWT-SECRET"); s != "" {
|
|
||||||
secret = s
|
|
||||||
}
|
|
||||||
signed, err := token.SignedString([]byte(secret))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not sign token"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not sign token"})
|
||||||
return
|
return
|
||||||
@@ -50,8 +64,150 @@ func Login(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"token": signed})
|
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
|
// 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 {
|
if v, ok := c.Get("claims"); ok {
|
||||||
c.JSON(http.StatusOK, gin.H{"claims": v})
|
c.JSON(http.StatusOK, gin.H{"claims": v})
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user