From 3f470096ce02e2894d35c91716f070f590b0c719 Mon Sep 17 00:00:00 2001 From: Kaaninchen <124433727+kaaninchen@users.noreply.github.com> Date: Mon, 3 Aug 2026 18:50:22 +0200 Subject: [PATCH] feat: toggle controller state on/off --- Makefile | 4 ++-- README.md | 22 +++++++++++++++----- cmd/internal/db/connection.go | 3 ++- cmd/internal/db/queries.go | 29 ++++++++++++++++++++++++--- cmd/internal/models/models.go | 6 ++++++ cmd/internal/routes/colors.go | 1 + cmd/internal/routes/controllers.go | 32 ++++++++++++++++++++++++++++++ 7 files changed, 86 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index a8edfa3..deab264 100644 --- a/Makefile +++ b/Makefile @@ -10,10 +10,10 @@ build: @echo "๐Ÿ”ง Building binary..." @cp $(CONFIG_FILE) $(BIN_DIR)/ @go build -o $(BIN_DIR)/$(BIN_NAME) $(MAIN_PKG) - @echo "โœ… Build complete: $(BIN_DIR)/$(BIN_NAME)" + @echo "Build complete: $(BIN_DIR)/$(BIN_NAME)" run: - @echo "๐Ÿš€ Running server..." + @echo "Running server..." @go run $(MAIN_PKG) clean: @echo "๐Ÿงน Cleaning up..." diff --git a/README.md b/README.md index 3eadc65..c952a4c 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Laterna requires a config.json file in the root directory. A [example config](ht cp config.json.example config.json ``` -You can leave the json as it is or configure it to your liking, although some configuration options (e.g. `verboseLogging`) aren't fully implemented yet. +You can leave the config as it is or adjust it to your liking. Make sure that your firewall supports connections to the configured port for laterna (default: `8080`), otherwise clients won't be able to connect to the API. You can do so by using ufw on most linux distributions @@ -74,10 +74,11 @@ The token is shown once on the first startup. To regenerate it, run the server w #### ๐ŸŽ›๏ธ Controllers -| Method | Route | Description | Payload | -|--------|----------------|-------------------------------|---------------------| -| POST | `/controllers` | Create a new controller | | -| DELETE | `/controllers` | Delete an existing controller |{ "ID": 1} | +| Method | Route | Description | Payload | +|--------|----------------------------|--------------------------------------|------------| +| POST | `/controllers` | Create a new controller | | +| POST | `/controllers/toggle/{id}` | Toggle an existing controller on/off | | +| DELETE | `/controllers` | Delete an existing controller | { "ID": 1} | @@ -126,6 +127,10 @@ $ http GET localhost:8080/api/v1/colors/1 "Authorization: $TOKEN" $ http PUT localhost:8080/api/v1/colors/1 "Authorization: $TOKEN" Color="#C2C342" ``` +#### Toggle controller on/off +```bash +$ http PUT localhost:8080/api/v1/colors/1 "Authorization: $TOKEN" +``` @@ -170,6 +175,13 @@ $ curl -X PUT localhost:8080/api/v1/colors/1 \ -d '{"Color": "#C2C342"}' ``` +#### Toggle controller on/off + +```bash +$ curl -X POST localhost:8080/api/v1/colors/1 \ + -H "Authorization:$TOKEN" +``` + diff --git a/cmd/internal/db/connection.go b/cmd/internal/db/connection.go index d79c560..61f8e26 100644 --- a/cmd/internal/db/connection.go +++ b/cmd/internal/db/connection.go @@ -34,6 +34,7 @@ func InitDB() { id INTEGER PRIMARY KEY, token_hash TEXT, color TEXT, + active INTEGER DEFAULT 0, updated_at DATETIME ); `) @@ -42,4 +43,4 @@ func InitDB() { logger.DBLogger.Fatalf("An error occured while initializing the database: %v", err) } -} \ No newline at end of file +} diff --git a/cmd/internal/db/queries.go b/cmd/internal/db/queries.go index 0d414e9..d0560df 100644 --- a/cmd/internal/db/queries.go +++ b/cmd/internal/db/queries.go @@ -71,7 +71,7 @@ func ControllerExists(id int) bool { } func GetAllColors() ([]models.LampState, error) { - rows, err := DB.Query("SELECT id, color, updated_at FROM controllers WHERE id > 0") + rows, err := DB.Query("SELECT id, color, active, updated_at FROM controllers WHERE id > 0") if err != nil { return nil, err } @@ -81,7 +81,7 @@ func GetAllColors() ([]models.LampState, error) { for rows.Next() { var state models.LampState - if err := rows.Scan(&state.ID, &state.Color, &state.UpdatedAt); err != nil { + if err := rows.Scan(&state.ID, &state.Color, &state.Active, &state.UpdatedAt); err != nil { return nil, err } controllers = append(controllers, state) @@ -90,7 +90,6 @@ func GetAllColors() ([]models.LampState, error) { if err := rows.Err(); err != nil { return nil, err } - return controllers, nil } @@ -129,3 +128,27 @@ func SetColor(id int, color string) error { logger.DBLogger.Printf("Lamp %d color updated to %s", id, color) return nil } + +func GetControllerState(id int) (bool, error) { + row := DB.QueryRow("SELECT active FROM controllers WHERE id = ?", id) + + var state bool + err := row.Scan(&state) + if err != nil { + logger.DBLogger.Print(err) + return false, err + } + + return state, nil +} + +func SetControllerstate(active bool, id int) error { + _, err := DB.Exec("UPDATE controllers SET active = ? WHERE id = ?", active, id) + if err != nil { + logger.DBLogger.Printf("Failed to update active state for controller %d: %v", id, err) + return err + } + + logger.DBLogger.Printf("Set active state for %d to %t", id, active) + return nil +} diff --git a/cmd/internal/models/models.go b/cmd/internal/models/models.go index 810c094..46730cf 100644 --- a/cmd/internal/models/models.go +++ b/cmd/internal/models/models.go @@ -27,6 +27,7 @@ type HTTPResponse struct { type LampState struct { ID int `json:"id"` Color string `json:"color"` + Active bool `json:"active"` UpdatedAt time.Time `json:"updated_at"` } @@ -45,3 +46,8 @@ type DeleteData struct { type CreateData struct { Created int `json:"created"` } + +type ToggleController struct { + ID int `json:"id"` + Active bool `json:"active"` +} diff --git a/cmd/internal/routes/colors.go b/cmd/internal/routes/colors.go index 8a6a98a..8a6f520 100644 --- a/cmd/internal/routes/colors.go +++ b/cmd/internal/routes/colors.go @@ -24,6 +24,7 @@ func listColors(w http.ResponseWriter, r *http.Request) { colors, err := db.GetAllColors() if err != nil { utils.ErrorResponse(w, http.StatusInternalServerError, err.Error()) + return } utils.SuccessResponse(w, http.StatusOK, colors) } diff --git a/cmd/internal/routes/controllers.go b/cmd/internal/routes/controllers.go index 1551fab..597d355 100644 --- a/cmd/internal/routes/controllers.go +++ b/cmd/internal/routes/controllers.go @@ -13,6 +13,8 @@ import ( func RegisterControllerRoutes(mux *http.ServeMux) { mux.HandleFunc("POST /api/v1/controllers", middleware.WithAdminAuth(createController)) mux.HandleFunc("DELETE /api/v1/controllers", middleware.WithAdminAuth(deleteController)) + + mux.HandleFunc("POST /api/v1/controllers/toggle/{ID}", middleware.WithAdminAuth(toggleControllerState)) } func createController(w http.ResponseWriter, r *http.Request) { @@ -41,3 +43,33 @@ func deleteController(w http.ResponseWriter, r *http.Request) { } utils.SuccessResponse(w, http.StatusOK, models.DeleteData{Deleted: req.ID}) } + +func toggleControllerState(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + idStr := r.PathValue("ID") + id, err := utils.IDtoInt((idStr)) + if err != nil { + utils.ErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + if !db.ControllerExists((id)) { + utils.ErrorResponse(w, http.StatusNotFound, "Controller not found") + return + } + + currentState, err := db.GetControllerState(id) + if err != nil { + utils.ErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + err = db.SetControllerstate(!currentState, id) + if err != nil { + utils.ErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + utils.SuccessResponse(w, http.StatusOK, models.ToggleController{ID: id, Active: !currentState}) +}