From 81677943c4d034cd804bc321247651e5e22c8f95 Mon Sep 17 00:00:00 2001 From: Kristjan Komlosi Date: Sat, 10 Jan 2026 14:20:38 +0100 Subject: [PATCH] feat: enhance Dockerfile for SQLite support and update main.go for MySQL connection; add sensor creation endpoint and improve wait-for-mqtt script --- Dockerfile | 7 +++++-- cmd/server/main.go | 30 +++++++++++++-------------- internal/handler/handlers.go | 29 +++++++++++++++++++++------ readme.md | 39 +++++++++++++++++++++++++++--------- scripts/wait-for-mqtt.sh | 19 +++++++++++++++++- 5 files changed, 89 insertions(+), 35 deletions(-) diff --git a/Dockerfile b/Dockerfile index f1637b5..7d654d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,16 +2,19 @@ FROM golang:1.21-alpine AS builder WORKDIR /src +# sqlite3 driver requires CGO; install build deps +RUN apk add --no-cache build-base sqlite-dev + COPY go.mod go.sum ./ RUN go mod download COPY . . -RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ +RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \ go build -ldflags "-s -w" -o /app/server ./cmd/server # Final image FROM alpine:3.18 -RUN apk add --no-cache ca-certificates netcat-openbsd curl +RUN apk add --no-cache ca-certificates netcat-openbsd curl sqlite-libs COPY --from=builder /app/server /usr/local/bin/server COPY scripts/wait-for-mqtt.sh /usr/local/bin/wait-for-mqtt.sh RUN chmod +x /usr/local/bin/wait-for-mqtt.sh diff --git a/cmd/server/main.go b/cmd/server/main.go index eba9dd9..e040a82 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -29,16 +29,16 @@ func main() { log.Fatalf("failed to load config: %v", err) } - // connect to database + // connect to MySQL 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) + sqlDB, err := sql.Open("mysql", dsn) if err != nil { log.Fatalf("failed to connect to database: %v", err) } - defer db.Close() + defer sqlDB.Close() // test connection - if err := db.Ping(); err != nil { + if err := sqlDB.Ping(); err != nil { log.Fatalf("failed to ping database: %v", err) } log.Println("connected to database") @@ -55,7 +55,7 @@ func main() { // connect to MQTT broker (best-effort) var mq *mqttclient.Client // initialize sqlite for state messages - var db *storage.DB + var stateDB *storage.DB { dbPath := os.Getenv("SQLITE_PATH") if dbPath == "" { @@ -65,8 +65,8 @@ func main() { if err != nil { log.Printf("warning: sqlite init failed: %v", err) } else { - db = dbInit - storage.SetDefault(db) + stateDB = dbInit + storage.SetDefault(stateDB) } } if cfg.MQTT.Broker != "" { @@ -108,7 +108,7 @@ func main() { r.Use(gin.Recovery()) r.Use(middleware.GinLogger()) - h := &handler.Handler{DB: db, JWTSecret: cfg.Server.JWTSecret} + h := &handler.Handler{DB: sqlDB, JWTSecret: cfg.Server.JWTSecret} // Public routes r.GET("/health", h.Health) @@ -122,6 +122,9 @@ func main() { auth := r.Group("/") auth.Use(middleware.AuthMiddleware(cfg.Server.JWTSecret)) auth.GET("/protected", h.Protected) + auth.GET("/mqttping", h.MQTTPing) + auth.GET("/messages", handler.GetMessages) + auth.POST("/sensors", h.CreateSensor) // Device CRUD routes auth.POST("/devices", h.CreateDevice) @@ -152,15 +155,10 @@ func main() { if mq != nil { mq.Close() } - if db != nil { - db.Close() + if stateDB != nil { + stateDB.Close() } log.Println("server exiting") } -func getJWTSecret() string { - if s := os.Getenv("JWT_SECRET"); s != "" { - return s - } - return "secret" -} +// removed unused getJWTSecret helper; configuration provides JWTSecret diff --git a/internal/handler/handlers.go b/internal/handler/handlers.go index ddb8c7f..c44f8dd 100644 --- a/internal/handler/handlers.go +++ b/internal/handler/handlers.go @@ -68,10 +68,10 @@ func (h *Handler) Login(c *gin.Context) { 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"` - Data_Type string `json:"data_type" binding:"required"` + 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()}) @@ -84,9 +84,9 @@ func (h *Handler) CreateSensor(c *gin.Context) { return } - // Create sensor in database + // 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) VALUES (UUID_TO_BIN(?), UUID_TO_BIN(?), ?, ?, ?)", sensorID.String(), deviceUUID.String(), req.Name, req.Type, req.Data_Type) + _, 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 @@ -246,6 +246,23 @@ func (h *Handler) Protected(c *gin.Context) { 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"}) +} + // GetMessages returns the last 100 state messages with optional pagination // query parameter: ?page=N (0-based) func GetMessages(c *gin.Context) { diff --git a/readme.md b/readme.md index 594da57..a178222 100644 --- a/readme.md +++ b/readme.md @@ -6,12 +6,12 @@ Repository: https://git.piskot.si/SeminarM2/lambdaiot-core ## Features ✅ -- Simple HTTP server with two endpoints: - - `GET /health` — basic health check - - `GET /hello` — example greeting endpoint -- Tests for handlers using `httptest` -- Multi-stage `Dockerfile` for small production images -- `Makefile` with common tasks (build, run, test, docker-build) +- HTTP API with health, greeting, auth, device CRUD, sensor creation, MQTT ping, and stored message retrieval +- JWT-based auth middleware with demo login (`admin`/`password`) +- MQTT client with startup publish and best-effort subscription to persist `state:` topics into SQLite +- SQLite sidecar (file) for recent MQTT state messages +- Multi-stage `Dockerfile` (CGO-enabled for sqlite3) and `Makefile` for common tasks +- `test/docker-compose.yml` spins up MySQL, Mosquitto, phpMyAdmin, and the server for local integration --- @@ -42,16 +42,35 @@ Then run it: docker run -p 8080:8080 lambdaiot-core:latest ``` +Integration stack (MySQL + Mosquitto + server): + +```bash +cd test +docker compose up -d --build +``` + +The compose file seeds the database from `ai-improved.sql` and exposes: +- API on http://localhost:8080 +- MySQL on localhost:3306 (root/rootpass) +- Mosquitto on localhost:1883 +- phpMyAdmin on http://localhost:8081 + ### Endpoints -- `http://localhost:8080/health` — returns `{ "status": "ok" }` -- `http://localhost:8080/hello` — returns a small greeting JSON -- `POST http://localhost:8080/login` — demo login, JSON body: `{ "username": "admin", "password": "password" }`, returns `{ "token": "..." }` -- `GET http://localhost:8080/protected` — protected endpoint requiring `Authorization: Bearer ` header +- `GET /health` — returns `{ "status": "ok" }` +- `GET /hello` — greeting JSON +- `POST /login` — demo login, body `{ "username": "admin", "password": "password" }`, returns `{ "token": "..." }` +- `GET /protected` — JWT required +- `GET /devices` — list devices (public) / `POST /devices`, `GET/PUT/DELETE /devices/:id` (JWT) +- `POST /sensors` — create sensor (JWT) +- `GET /mqttping` — publish timestamp to MQTT default topic (JWT) +- `GET /messages` — last stored MQTT state messages from SQLite (JWT) ### Environment - `JWT_SECRET` — (optional) secret used to sign tokens; defaults to `secret` for local/dev use +- `MQTT_*` — configure broker, client ID, topic, username/password +- `DB_*` — MySQL connection overrides; see `config.toml` defaults in `internal/config` --- diff --git a/scripts/wait-for-mqtt.sh b/scripts/wait-for-mqtt.sh index dbfddcc..239ebdc 100644 --- a/scripts/wait-for-mqtt.sh +++ b/scripts/wait-for-mqtt.sh @@ -18,5 +18,22 @@ while ! nc -z "$HOST" "$PORT"; do sleep 1 done -echo "Broker $HOST:$PORT is up, starting server" +echo "Broker $HOST:$PORT is up" + +# Optionally wait for MySQL if DB_HOST/DB_PORT are set +DB_HOST=${DB_HOST:-} +DB_PORT=${DB_PORT:-} +if [ -n "$DB_HOST" ]; then + if [ -z "$DB_PORT" ]; then + DB_PORT=3306 + fi + echo "Waiting for MySQL $DB_HOST:$DB_PORT..." + while ! nc -z "$DB_HOST" "$DB_PORT"; do + echo "mysql not ready, retrying..." + sleep 1 + done + echo "MySQL $DB_HOST:$DB_PORT is up" +fi + +echo "Starting server" exec /usr/local/bin/server