diff --git a/README.md b/README.md index b03f5dc..5e5ffe0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Laterna +# đŸ›‹ī¸ Laterna (Server) ## 🛜 API Documentation @@ -40,26 +40,26 @@ Authorization: #### Create a new controller ```bash $ curl -X POST http://your-server.com/api/v1/controllers \ - -H "Authorization: $TOKEN" + -H "Authorization: $TOKEN" ``` -Respons with the newly assigned ID +â„šī¸ Response with the newly assigned ID. The ID will always be the next available one #### Delete an controller ```bash $ curl -X DELETE localhost:8080/api/v1/controllers \ - -H "Authorization: $TOKEN" \ - -d '{"ID": 1}' + -H "Authorization: $TOKEN" \ + -d '{"ID": 1}' ``` #### Get the current color of an controller ```bash $ curl -X GET localhost:8080/api/v1/colors/1 \ - -H "Authorization: $TOKEN" + -H "Authorization: $TOKEN" ``` #### Set the color of an controller ```bash $ curl -X PUT localhost:8080/api/v1/colors/1 \ - -H "Authorization:$TOKEN" \ - -d '{"Color": "#C2C342"}' + -H "Authorization:$TOKEN" \ + -d '{"Color": "#C2C342"}' ``` diff --git a/cmd/internal/db/connection.go b/cmd/internal/db/connection.go index 44a45f1..d79c560 100644 --- a/cmd/internal/db/connection.go +++ b/cmd/internal/db/connection.go @@ -31,7 +31,7 @@ func ConnectDB() { func InitDB() { _, err := DB.Exec(` CREATE TABLE IF NOT EXISTS controllers ( - id TEXT PRIMARY KEY, + id INTEGER PRIMARY KEY, token_hash TEXT, color TEXT, updated_at DATETIME diff --git a/cmd/internal/db/queries.go b/cmd/internal/db/queries.go index 1505cff..8308fa5 100644 --- a/cmd/internal/db/queries.go +++ b/cmd/internal/db/queries.go @@ -9,15 +9,31 @@ import ( "golang.org/x/crypto/bcrypt" ) -func CreateController(id int) string { - token, _ := utils.GenerateToken() - hash, _ := utils.HashToken(token) - - _, err := DB.Exec("INSERT INTO controllers (id, token_hash) VALUES (?, ?)", id, hash) +func CreateController() (int64, error) { + result, err := DB.Exec("INSERT INTO controllers (color, updated_at) VALUES (?, CURRENT_TIMESTAMP)", "#FFFFF") if err != nil { - logger.DBLogger.Fatalf("Error creating controller with ID %v: %s",id, err) + logger.DBLogger.Printf("Error creating controller: %v", err) + return 0, err } + + id, err := result.LastInsertId() + if err != nil { + logger.DBLogger.Printf("Error fetching controller ID: %v", err) + return 0, err + } + return id, nil +} +func CreateAdmin() string { + token, err := utils.GenerateToken() + if err != nil { + logger.DBLogger.Fatalf("Error generating admin token: %v", err) + } + token_hash, err := utils.HashToken(token) + if err != nil { + logger.DBLogger.Fatalf("Error hashing admin token: %v", err) + } + DB.Exec("INSERT INTO controllers (id, token_hash) VALUES (0, ?)", token_hash) return token } diff --git a/cmd/internal/http/server.go b/cmd/internal/http/server.go index 6fa59d8..524a89d 100644 --- a/cmd/internal/http/server.go +++ b/cmd/internal/http/server.go @@ -12,10 +12,11 @@ import ( func StartHTTPServer() { router := http.NewServeMux() - routes.RegisterRoutes(router) + routes.RegisterColorRoutes(router) + routes.RegisterControllerRoutes(router) if !db.ControllerExists(0) { - adminToken := db.CreateController(0) + adminToken := db.CreateAdmin() fmt.Println("IMPORTANT") fmt.Println("- - - - - - - - - - - - - - - - - - ") fmt.Println("ADMIN TOKEN:") diff --git a/cmd/internal/middleware/auth.go b/cmd/internal/middleware/auth.go new file mode 100644 index 0000000..18a43f0 --- /dev/null +++ b/cmd/internal/middleware/auth.go @@ -0,0 +1,19 @@ +package middleware + +import ( + "net/http" + + "github.com/siestaw/laterna/server/cmd/internal/db" + "github.com/siestaw/laterna/server/cmd/utils" +) + +func WithAdminAuth(handlerFunc http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("Authorization") + if !db.IsAdmin(token) { + utils.HTTPResponseHandler(w, r, http.StatusUnauthorized, "Invalid token") + return + } + handlerFunc(w, r) + } +} \ No newline at end of file diff --git a/cmd/internal/models/models.go b/cmd/internal/models/models.go index 5229f06..c8675a8 100644 --- a/cmd/internal/models/models.go +++ b/cmd/internal/models/models.go @@ -1,6 +1,8 @@ package models -import "time" +import ( + "time" +) type HTTPConfig struct { AdminToken string `json:"adminToken"` @@ -15,10 +17,10 @@ type Config struct { VerboseLogging bool `json:"verboseLogging"` } -type HTTPError struct { +type HTTPResponse struct { Timestamp string `json:"timestamp"` Status int `json:"status"` - Error string `json:"error"` + Text string `json:"text"` Message string `json:"message"` Path string `json:"path"` } @@ -32,3 +34,7 @@ type LampState struct { type LampUpdateRequest struct { Color string `json:"color"` } + +type ControllerRequests struct { + ID int `json:"ID"` +} \ No newline at end of file diff --git a/cmd/internal/routes/routes.go b/cmd/internal/routes/colors.go similarity index 52% rename from cmd/internal/routes/routes.go rename to cmd/internal/routes/colors.go index 1d83026..e7fc676 100644 --- a/cmd/internal/routes/routes.go +++ b/cmd/internal/routes/colors.go @@ -8,79 +8,78 @@ import ( "github.com/siestaw/laterna/server/cmd/internal/config" "github.com/siestaw/laterna/server/cmd/internal/db" "github.com/siestaw/laterna/server/cmd/internal/logger" + "github.com/siestaw/laterna/server/cmd/internal/middleware" "github.com/siestaw/laterna/server/cmd/internal/models" "github.com/siestaw/laterna/server/cmd/utils" ) -func RegisterRoutes(mux *http.ServeMux) { - mux.HandleFunc("GET /api/v1/id/{ID}", getCurrent) - mux.HandleFunc("PUT /api/v1/id/{ID}", setCurrent) - - mux.HandleFunc("GET /api/v1/controllers", getControllers) - mux.HandleFunc("PUT /api/v1/controllers", setControllers) - mux.HandleFunc("DELETE /api/v1/controllers/{id}", deleteController) +func RegisterColorRoutes(mux *http.ServeMux) { + mux.HandleFunc("GET /api/v1/colors/{ID}", middleware.WithAdminAuth(getCurrent)) + mux.HandleFunc("PUT /api/v1/colors/{ID}", middleware.WithAdminAuth(setCurrent)) +// mux.HandleFunc("WS /api/v1/ws/colors/{ID}", setCurrentWebsocket) } func getCurrent(w http.ResponseWriter, r *http.Request) { - token := r.Header.Get("Authorization") - if !db.IsAdmin(token) { - utils.HTTPErrorHandling(w, r, http.StatusUnauthorized, "Invalid token") - return - } + defer r.Body.Close() idStr := r.PathValue("ID") id, err := utils.IDtoInt(idStr) if err != nil { - utils.HTTPErrorHandling(w, r, http.StatusBadRequest, err.Error()) + utils.HTTPResponseHandler(w, r, http.StatusBadRequest, err.Error()) + return + } + + if !db.ControllerExists(id) { + utils.HTTPResponseHandler(w, r, http.StatusBadRequest, "Lamp does not exist") return } state, err := db.ViewColor(id) if err != nil { - utils.HTTPErrorHandling(w, r, http.StatusBadRequest, "Lamp not found") + utils.HTTPResponseHandler(w, r, http.StatusBadRequest, err.Error()) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(state) - defer r.Body.Close() } func setCurrent(w http.ResponseWriter, r *http.Request) { - token := r.Header.Get("Authorization") - if !db.IsAdmin(token) { - utils.HTTPErrorHandling(w, r, http.StatusUnauthorized, "Invalid token") - return - } + defer r.Body.Close() idStr := r.PathValue("ID") id, err := utils.IDtoInt(idStr) if err != nil { - utils.HTTPErrorHandling(w, r, http.StatusBadRequest, err.Error()) + utils.HTTPResponseHandler(w, r, http.StatusBadRequest, err.Error()) return } + if !db.ControllerExists(id) { + utils.HTTPResponseHandler(w, r, http.StatusBadRequest, "Lamp does not exist") + return + } + currentState, err := db.ViewColor(id) if err != nil { - utils.HTTPErrorHandling(w, r, http.StatusInternalServerError, "Failed to get current lamp color") + utils.HTTPResponseHandler(w, r, http.StatusInternalServerError, "Failed to get current lamp color") return } if time.Since(currentState.UpdatedAt).Seconds() < config.AppConfig.HTTP.Cooldown { - utils.HTTPErrorHandling(w, r, http.StatusTooManyRequests, "Slow down!") + utils.HTTPResponseHandler(w, r, http.StatusTooManyRequests, "Slow down!") return } var req models.LampUpdateRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { logger.HTTPLogger.Print(err) - utils.HTTPErrorHandling(w, r, http.StatusBadRequest, "Invalid JSON") + utils.HTTPResponseHandler(w, r, http.StatusBadRequest, "Invalid JSON") return } err = db.SetColor(id, req.Color) if err != nil { logger.HTTPLogger.Printf("Could not update lamp %d: %s", id, err) - utils.HTTPErrorHandling(w, r, http.StatusInternalServerError, err.Error()) + utils.HTTPResponseHandler(w, r, http.StatusInternalServerError, err.Error()) return } @@ -90,17 +89,5 @@ func setCurrent(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(updatedState) - defer r.Body.Close() -} - -func getControllers(w http.ResponseWriter, r *http.Request) { - return -} - -func setControllers(w http.ResponseWriter, r *http.Request) { - return -} - -func deleteController(w http.ResponseWriter, r *http.Request) { - return + logger.HTTPLogger.Printf("Lamp %d updated to color %s", id, req.Color) } \ No newline at end of file diff --git a/cmd/internal/routes/controllers.go b/cmd/internal/routes/controllers.go new file mode 100644 index 0000000..5fd85e5 --- /dev/null +++ b/cmd/internal/routes/controllers.go @@ -0,0 +1,47 @@ +package routes + +import ( + "encoding/json" + "net/http" + + "github.com/siestaw/laterna/server/cmd/internal/db" + "github.com/siestaw/laterna/server/cmd/internal/logger" + "github.com/siestaw/laterna/server/cmd/internal/middleware" + "github.com/siestaw/laterna/server/cmd/internal/models" + "github.com/siestaw/laterna/server/cmd/utils" +) + +func RegisterControllerRoutes(mux *http.ServeMux) { + mux.HandleFunc("POST /api/v1/controllers", middleware.WithAdminAuth(createController)) + mux.HandleFunc("DELETE /api/v1/controllers", middleware.WithAdminAuth(deleteController)) +} + +func createController(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + ID, err := db.CreateController() + if err != nil { + utils.HTTPResponseHandler(w, r, http.StatusInternalServerError, "An error occured. Check the server logs for more information") + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]int64{"ID": ID}) +} + +func deleteController(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + var req models.ControllerRequests + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + logger.HTTPLogger.Print(err) + utils.HTTPResponseHandler(w, r, http.StatusBadRequest, "Invalid JSON") + return + } + if !db.ControllerExists(req.ID) || req.ID <= 0 { + utils.HTTPResponseHandler(w, r, http.StatusBadRequest,"Invalid ID") + return + } + if db.DeleteController(req.ID) != nil { + utils.HTTPResponseHandler(w, r, http.StatusInternalServerError,"An error occured. Check the server logs for more information") + return + } + utils.HTTPResponseHandler(w, r, http.StatusOK, "Success") +} \ No newline at end of file diff --git a/cmd/utils/utils.go b/cmd/utils/utils.go index 7f6d580..34eba8d 100644 --- a/cmd/utils/utils.go +++ b/cmd/utils/utils.go @@ -31,7 +31,6 @@ func IsValidHexColor(color string) bool { return match } - func GenerateToken() (string, error) { bytes := make([]byte, 32) if _, err := rand.Read(bytes); err != nil { @@ -50,12 +49,12 @@ func ValidateToken(providedToken string, storedHash string) bool { return err == nil } -func HTTPErrorHandling(w http.ResponseWriter, r *http.Request, status int, message string) { +func HTTPResponseHandler(w http.ResponseWriter, r *http.Request, status int, message string) { timestamp := time.Now().Format("2006-01-02_15-04-05") - errResp := models.HTTPError{ + errResp := models.HTTPResponse{ Timestamp: timestamp, Status: status, - Error: http.StatusText(status), + Text: http.StatusText(status), Message: message, Path: r.URL.Path, } diff --git a/config.json.example b/config.json.example index 46dbbcb..53f6dc1 100644 --- a/config.json.example +++ b/config.json.example @@ -1,6 +1,6 @@ { "fileLogging": false, - "verboseLogging": true, // // not fully implemented yet + "verboseLogging": true, "http": { "port": 8080, "cooldown": 5