880 lines
26 KiB
Go
880 lines
26 KiB
Go
package handler
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.piskot.si/SeminarM2/lambdaiot-core/internal/mqtt"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/uuid"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
type Handler struct {
|
|
DB *sql.DB
|
|
JWTSecret string
|
|
MQTTTopic 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"`
|
|
}
|
|
|
|
type Sensor struct {
|
|
ID uuid.UUID `json:"id" db:"id"`
|
|
DeviceID uuid.UUID `json:"device_id" db:"device_id"`
|
|
Name string `json:"name" db:"name"`
|
|
Type string `json:"type" db:"type"`
|
|
DataTypeID int `json:"data_type_id" db:"data_type_id"`
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
|
}
|
|
|
|
type Actor struct {
|
|
ID uuid.UUID `json:"id" db:"id"`
|
|
DeviceID uuid.UUID `json:"device_id" db:"device_id"`
|
|
Name string `json:"name" db:"name"`
|
|
Type string `json:"type" db:"type"`
|
|
DataTypeID int `json:"data_type_id" db:"data_type_id"`
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
|
}
|
|
|
|
type SensorReading struct {
|
|
ID int64 `json:"id" db:"id"`
|
|
SensorID uuid.UUID `json:"sensor_id" db:"sensor_id"`
|
|
Value float64 `json:"value" db:"value"`
|
|
ValueAt time.Time `json:"value_at" db:"value_at"`
|
|
}
|
|
|
|
func (h *Handler) mqttTopic() string {
|
|
if h != nil && h.MQTTTopic != "" {
|
|
return h.MQTTTopic
|
|
}
|
|
if v := os.Getenv("MQTT_TOPIC"); v != "" {
|
|
return v
|
|
}
|
|
return "lambdaiot"
|
|
}
|
|
|
|
func (h *Handler) publishMQTT(c *gin.Context, payload any) bool {
|
|
topic := h.mqttTopic()
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to marshal payload"})
|
|
return false
|
|
}
|
|
if err := mqtt.PublishDefault(topic, body); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Health returns basic service health
|
|
func (h *Handler) Health(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
|
}
|
|
|
|
// Hello returns a simple greeting
|
|
func (h *Handler) Hello(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{"message": "hello from lambdaiot"})
|
|
}
|
|
|
|
// Login issues a JWT token after validating credentials against the database
|
|
func (h *Handler) Login(c *gin.Context) {
|
|
var req struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
if err := c.BindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid json"})
|
|
return
|
|
}
|
|
|
|
// Query user from database
|
|
var userID string
|
|
var passwordHash string
|
|
err := h.DB.QueryRow("SELECT BIN_TO_UUID(id), password_hash FROM users WHERE username = ?", req.Username).Scan(&userID, &passwordHash)
|
|
if err == sql.ErrNoRows {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
|
|
return
|
|
}
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"})
|
|
return
|
|
}
|
|
|
|
// Verify password
|
|
if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.Password)); err != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
|
|
return
|
|
}
|
|
|
|
// Create token
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
|
"sub": req.Username,
|
|
"user_id": userID,
|
|
"exp": time.Now().Add(24 * time.Hour).Unix(),
|
|
})
|
|
signed, err := token.SignedString([]byte(h.JWTSecret))
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not sign token"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"token": signed})
|
|
}
|
|
|
|
func (h *Handler) CreateSensor(c *gin.Context) {
|
|
var req struct {
|
|
DeviceID string `json:"device_id" binding:"required,uuid"`
|
|
Name string `json:"name" binding:"required"`
|
|
Type string `json:"type" binding:"required"`
|
|
DataTypeID int `json:"data_type_id" binding:"required,min=1"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
deviceUUID, err := uuid.Parse(req.DeviceID)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid device id"})
|
|
return
|
|
}
|
|
|
|
// Create sensor in database (schema uses data_type_id)
|
|
sensorID := uuid.New()
|
|
_, err = h.DB.Exec("INSERT INTO sensors (id, device_id, name, type, data_type_id) VALUES (UUID_TO_BIN(?), UUID_TO_BIN(?), ?, ?, ?)", sensorID.String(), deviceUUID.String(), req.Name, req.Type, req.DataTypeID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create sensor"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{"id": sensorID.String()})
|
|
}
|
|
|
|
// GetSensors lists all sensors
|
|
func (h *Handler) GetSensors(c *gin.Context) {
|
|
rows, err := h.DB.Query("SELECT BIN_TO_UUID(id), BIN_TO_UUID(device_id), name, type, data_type_id, created_at, updated_at FROM sensors")
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch sensors"})
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var sensors []Sensor
|
|
for rows.Next() {
|
|
var s Sensor
|
|
if err := rows.Scan(&s.ID, &s.DeviceID, &s.Name, &s.Type, &s.DataTypeID, &s.CreatedAt, &s.UpdatedAt); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to scan sensor"})
|
|
return
|
|
}
|
|
sensors = append(sensors, s)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, sensors)
|
|
}
|
|
|
|
// GetSensor returns a single sensor
|
|
func (h *Handler) GetSensor(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid sensor id"})
|
|
return
|
|
}
|
|
|
|
var s Sensor
|
|
err = h.DB.QueryRow("SELECT BIN_TO_UUID(id), BIN_TO_UUID(device_id), name, type, data_type_id, created_at, updated_at FROM sensors WHERE id = UUID_TO_BIN(?)", id.String()).Scan(&s.ID, &s.DeviceID, &s.Name, &s.Type, &s.DataTypeID, &s.CreatedAt, &s.UpdatedAt)
|
|
if err == sql.ErrNoRows {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "sensor not found"})
|
|
return
|
|
} else if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch sensor"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, s)
|
|
}
|
|
|
|
// UpdateSensor updates sensor fields
|
|
func (h *Handler) UpdateSensor(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid sensor id"})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
DeviceID *string `json:"device_id"`
|
|
Name *string `json:"name"`
|
|
Type *string `json:"type"`
|
|
DataTypeID *int `json:"data_type_id"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
setParts := []string{}
|
|
args := []interface{}{}
|
|
if req.DeviceID != nil {
|
|
devID, err := uuid.Parse(*req.DeviceID)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid device id"})
|
|
return
|
|
}
|
|
setParts = append(setParts, "device_id = UUID_TO_BIN(?)")
|
|
args = append(args, devID.String())
|
|
}
|
|
if req.Name != nil {
|
|
setParts = append(setParts, "name = ?")
|
|
args = append(args, *req.Name)
|
|
}
|
|
if req.Type != nil {
|
|
setParts = append(setParts, "type = ?")
|
|
args = append(args, *req.Type)
|
|
}
|
|
if req.DataTypeID != nil {
|
|
setParts = append(setParts, "data_type_id = ?")
|
|
args = append(args, *req.DataTypeID)
|
|
}
|
|
if len(setParts) == 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"})
|
|
return
|
|
}
|
|
|
|
query := "UPDATE sensors SET " + strings.Join(setParts, ", ") + " WHERE id = UUID_TO_BIN(?)"
|
|
args = append(args, id.String())
|
|
|
|
if _, err := h.DB.Exec(query, args...); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update sensor"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "sensor updated"})
|
|
}
|
|
|
|
// DeleteSensor removes a sensor
|
|
func (h *Handler) DeleteSensor(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid sensor id"})
|
|
return
|
|
}
|
|
if _, err := h.DB.Exec("DELETE FROM sensors WHERE id = UUID_TO_BIN(?)", id.String()); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete sensor"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"message": "sensor deleted"})
|
|
}
|
|
|
|
// TriggerSensor sends an MQTT message requesting a new reading from the sensor.
|
|
// The message payload contains the sensor ID and the requested action (default "read").
|
|
func (h *Handler) TriggerSensor(c *gin.Context) {
|
|
sensorID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid sensor id"})
|
|
return
|
|
}
|
|
|
|
var exists int
|
|
if err := h.DB.QueryRow("SELECT COUNT(1) FROM sensors WHERE id = UUID_TO_BIN(?)", sensorID.String()).Scan(&exists); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to check sensor"})
|
|
return
|
|
}
|
|
if exists == 0 {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "sensor not found"})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Action string `json:"action"`
|
|
}
|
|
if c.Request.ContentLength > 0 {
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
}
|
|
action := req.Action
|
|
if action == "" {
|
|
action = "read"
|
|
}
|
|
|
|
payload := struct {
|
|
Type string `json:"type"`
|
|
SensorID string `json:"sensor_id"`
|
|
Action string `json:"action"`
|
|
RequestedAt string `json:"requested_at"`
|
|
}{
|
|
Type: "sensor_trigger",
|
|
SensorID: sensorID.String(),
|
|
Action: action,
|
|
RequestedAt: time.Now().UTC().Format(time.RFC3339),
|
|
}
|
|
|
|
if !h.publishMQTT(c, payload) {
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusAccepted, gin.H{"status": "published", "topic": h.mqttTopic()})
|
|
}
|
|
|
|
// 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 (h *Handler) Protected(c *gin.Context) {
|
|
if v, ok := c.Get("claims"); ok {
|
|
c.JSON(http.StatusOK, gin.H{"claims": v})
|
|
return
|
|
}
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "no claims"})
|
|
}
|
|
|
|
// Package-level handlers used by tests
|
|
func Health(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
|
}
|
|
|
|
func Hello(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{"message": "hello from lambdaiot"})
|
|
}
|
|
|
|
func Protected(c *gin.Context) {
|
|
if v, ok := c.Get("claims"); ok {
|
|
c.JSON(http.StatusOK, gin.H{"claims": v})
|
|
return
|
|
}
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "no claims"})
|
|
}
|
|
|
|
// Actor CRUD
|
|
func (h *Handler) CreateActor(c *gin.Context) {
|
|
var req struct {
|
|
DeviceID string `json:"device_id" binding:"required,uuid"`
|
|
Name string `json:"name" binding:"required"`
|
|
Type string `json:"type" binding:"required"`
|
|
DataTypeID int `json:"data_type_id" binding:"required,min=1"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
devID, err := uuid.Parse(req.DeviceID)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid device id"})
|
|
return
|
|
}
|
|
actorID := uuid.New()
|
|
if _, err := h.DB.Exec("INSERT INTO actors (id, device_id, name, type, data_type_id) VALUES (UUID_TO_BIN(?), UUID_TO_BIN(?), ?, ?, ?)", actorID.String(), devID.String(), req.Name, req.Type, req.DataTypeID); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create actor"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, gin.H{"id": actorID.String()})
|
|
}
|
|
|
|
func (h *Handler) GetActors(c *gin.Context) {
|
|
rows, err := h.DB.Query("SELECT BIN_TO_UUID(id), BIN_TO_UUID(device_id), name, type, data_type_id, created_at, updated_at FROM actors")
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch actors"})
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
var actors []Actor
|
|
for rows.Next() {
|
|
var a Actor
|
|
if err := rows.Scan(&a.ID, &a.DeviceID, &a.Name, &a.Type, &a.DataTypeID, &a.CreatedAt, &a.UpdatedAt); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to scan actor"})
|
|
return
|
|
}
|
|
actors = append(actors, a)
|
|
}
|
|
c.JSON(http.StatusOK, actors)
|
|
}
|
|
|
|
func (h *Handler) GetActor(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid actor id"})
|
|
return
|
|
}
|
|
var a Actor
|
|
err = h.DB.QueryRow("SELECT BIN_TO_UUID(id), BIN_TO_UUID(device_id), name, type, data_type_id, created_at, updated_at FROM actors WHERE id = UUID_TO_BIN(?)", id.String()).Scan(&a.ID, &a.DeviceID, &a.Name, &a.Type, &a.DataTypeID, &a.CreatedAt, &a.UpdatedAt)
|
|
if err == sql.ErrNoRows {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "actor not found"})
|
|
return
|
|
} else if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch actor"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, a)
|
|
}
|
|
|
|
func (h *Handler) UpdateActor(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid actor id"})
|
|
return
|
|
}
|
|
var req struct {
|
|
DeviceID *string `json:"device_id"`
|
|
Name *string `json:"name"`
|
|
Type *string `json:"type"`
|
|
DataTypeID *int `json:"data_type_id"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
setParts := []string{}
|
|
args := []interface{}{}
|
|
if req.DeviceID != nil {
|
|
devID, err := uuid.Parse(*req.DeviceID)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid device id"})
|
|
return
|
|
}
|
|
setParts = append(setParts, "device_id = UUID_TO_BIN(?)")
|
|
args = append(args, devID.String())
|
|
}
|
|
if req.Name != nil {
|
|
setParts = append(setParts, "name = ?")
|
|
args = append(args, *req.Name)
|
|
}
|
|
if req.Type != nil {
|
|
setParts = append(setParts, "type = ?")
|
|
args = append(args, *req.Type)
|
|
}
|
|
if req.DataTypeID != nil {
|
|
setParts = append(setParts, "data_type_id = ?")
|
|
args = append(args, *req.DataTypeID)
|
|
}
|
|
if len(setParts) == 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"})
|
|
return
|
|
}
|
|
query := "UPDATE actors SET " + strings.Join(setParts, ", ") + " WHERE id = UUID_TO_BIN(?)"
|
|
args = append(args, id.String())
|
|
if _, err := h.DB.Exec(query, args...); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update actor"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"message": "actor updated"})
|
|
}
|
|
|
|
func (h *Handler) DeleteActor(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid actor id"})
|
|
return
|
|
}
|
|
if _, err := h.DB.Exec("DELETE FROM actors WHERE id = UUID_TO_BIN(?)", id.String()); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete actor"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"message": "actor deleted"})
|
|
}
|
|
|
|
// WriteActor publishes a desired action/value for the actor to the MQTT topic.
|
|
func (h *Handler) WriteActor(c *gin.Context) {
|
|
actorID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid actor id"})
|
|
return
|
|
}
|
|
|
|
var exists int
|
|
if err := h.DB.QueryRow("SELECT COUNT(1) FROM actors WHERE id = UUID_TO_BIN(?)", actorID.String()).Scan(&exists); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to check actor"})
|
|
return
|
|
}
|
|
if exists == 0 {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "actor not found"})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Action string `json:"action" binding:"required"`
|
|
Value json.RawMessage `json:"value"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
payload := struct {
|
|
Type string `json:"type"`
|
|
ActorID string `json:"actor_id"`
|
|
Action string `json:"action"`
|
|
Value json.RawMessage `json:"value,omitempty"`
|
|
RequestedAt string `json:"requested_at"`
|
|
}{
|
|
Type: "actor_command",
|
|
ActorID: actorID.String(),
|
|
Action: req.Action,
|
|
Value: req.Value,
|
|
RequestedAt: time.Now().UTC().Format(time.RFC3339),
|
|
}
|
|
|
|
if !h.publishMQTT(c, payload) {
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusAccepted, gin.H{"status": "published", "topic": h.mqttTopic()})
|
|
}
|
|
|
|
// Sensor readings CRUD
|
|
func (h *Handler) CreateSensorReading(c *gin.Context) {
|
|
var req struct {
|
|
SensorID string `json:"sensor_id" binding:"required,uuid"`
|
|
Value float64 `json:"value" binding:"required"`
|
|
ValueAt *string `json:"value_at"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
sensorID, err := uuid.Parse(req.SensorID)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid sensor id"})
|
|
return
|
|
}
|
|
valueAt := time.Now()
|
|
if req.ValueAt != nil && *req.ValueAt != "" {
|
|
if parsed, err := time.Parse(time.RFC3339, *req.ValueAt); err == nil {
|
|
valueAt = parsed
|
|
} else {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid value_at; use RFC3339"})
|
|
return
|
|
}
|
|
}
|
|
res, err := h.DB.Exec("INSERT INTO sensor_readings (sensor_id, value, value_at) VALUES (UUID_TO_BIN(?), ?, ?)", sensorID.String(), req.Value, valueAt)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create sensor reading"})
|
|
return
|
|
}
|
|
id, _ := res.LastInsertId()
|
|
c.JSON(http.StatusCreated, gin.H{"id": id})
|
|
}
|
|
|
|
func (h *Handler) GetSensorReadings(c *gin.Context) {
|
|
limit := 100
|
|
if l := c.Query("limit"); l != "" {
|
|
if n, err := strconv.Atoi(l); err == nil && n > 0 && n <= 1000 {
|
|
limit = n
|
|
}
|
|
}
|
|
page := 0
|
|
if p := c.Query("page"); p != "" {
|
|
if n, err := strconv.Atoi(p); err == nil && n >= 0 {
|
|
page = n
|
|
}
|
|
}
|
|
offset := page * limit
|
|
|
|
var (
|
|
rows *sql.Rows
|
|
err error
|
|
)
|
|
|
|
if sensorIDStr := c.Query("sensor_id"); sensorIDStr != "" {
|
|
sensorID, err := uuid.Parse(sensorIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid sensor id"})
|
|
return
|
|
}
|
|
rows, err = h.DB.Query("SELECT id, BIN_TO_UUID(sensor_id), value, value_at FROM sensor_readings WHERE sensor_id = UUID_TO_BIN(?) ORDER BY id DESC LIMIT ? OFFSET ?", sensorID.String(), limit, offset)
|
|
} else {
|
|
rows, err = h.DB.Query("SELECT id, BIN_TO_UUID(sensor_id), value, value_at FROM sensor_readings ORDER BY id DESC LIMIT ? OFFSET ?", limit, offset)
|
|
}
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch sensor readings"})
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var readings []SensorReading
|
|
for rows.Next() {
|
|
var r SensorReading
|
|
if err := rows.Scan(&r.ID, &r.SensorID, &r.Value, &r.ValueAt); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to scan sensor reading"})
|
|
return
|
|
}
|
|
readings = append(readings, r)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, readings)
|
|
}
|
|
|
|
func (h *Handler) GetSensorReading(c *gin.Context) {
|
|
idStr := c.Param("id")
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid reading id"})
|
|
return
|
|
}
|
|
var r SensorReading
|
|
err = h.DB.QueryRow("SELECT id, BIN_TO_UUID(sensor_id), value, value_at FROM sensor_readings WHERE id = ?", id).Scan(&r.ID, &r.SensorID, &r.Value, &r.ValueAt)
|
|
if err == sql.ErrNoRows {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "sensor reading not found"})
|
|
return
|
|
} else if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch sensor reading"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, r)
|
|
}
|
|
|
|
func (h *Handler) UpdateSensorReading(c *gin.Context) {
|
|
idStr := c.Param("id")
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid reading id"})
|
|
return
|
|
}
|
|
var req struct {
|
|
Value *float64 `json:"value"`
|
|
ValueAt *string `json:"value_at"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
setParts := []string{}
|
|
args := []interface{}{}
|
|
if req.Value != nil {
|
|
setParts = append(setParts, "value = ?")
|
|
args = append(args, *req.Value)
|
|
}
|
|
if req.ValueAt != nil {
|
|
parsed, err := time.Parse(time.RFC3339, *req.ValueAt)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid value_at; use RFC3339"})
|
|
return
|
|
}
|
|
setParts = append(setParts, "value_at = ?")
|
|
args = append(args, parsed)
|
|
}
|
|
if len(setParts) == 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"})
|
|
return
|
|
}
|
|
query := "UPDATE sensor_readings SET " + strings.Join(setParts, ", ") + " WHERE id = ?"
|
|
args = append(args, id)
|
|
if _, err := h.DB.Exec(query, args...); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update sensor reading"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"message": "sensor reading updated"})
|
|
}
|
|
|
|
func (h *Handler) DeleteSensorReading(c *gin.Context) {
|
|
idStr := c.Param("id")
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid reading id"})
|
|
return
|
|
}
|
|
if _, err := h.DB.Exec("DELETE FROM sensor_readings WHERE id = ?", id); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete sensor reading"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"message": "sensor reading deleted"})
|
|
}
|
|
|
|
// Register creates a new user account
|
|
func (h *Handler) Register(c *gin.Context) {
|
|
var req struct {
|
|
Username string `json:"username" binding:"required,min=3,max=50"`
|
|
Password string `json:"password" binding:"required,min=6"`
|
|
Email string `json:"email"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Check if username already exists
|
|
var exists int
|
|
err := h.DB.QueryRow("SELECT COUNT(*) FROM users WHERE username = ?", req.Username).Scan(&exists)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"})
|
|
return
|
|
}
|
|
if exists > 0 {
|
|
c.JSON(http.StatusConflict, gin.H{"error": "username already exists"})
|
|
return
|
|
}
|
|
|
|
// Hash password
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to hash password"})
|
|
return
|
|
}
|
|
|
|
// Insert user
|
|
userID := uuid.New()
|
|
_, err = h.DB.Exec("INSERT INTO users (id, username, password_hash, email) VALUES (UUID_TO_BIN(?), ?, ?, ?)",
|
|
userID.String(), req.Username, string(hashedPassword), req.Email)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create user"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{"id": userID.String(), "username": req.Username})
|
|
}
|