From c51a17993ef7834c07651411f157c133fc02bb10 Mon Sep 17 00:00:00 2001 From: Florian Beisel Date: Sun, 21 Jan 2024 14:54:34 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Refactor=20basic=20server=20stru?= =?UTF-8?q?cture?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These changes refactor the router handling into its own package to keep main.go clean. Also API handlers are here refactored to their corresponding files. --- api/auth_handlers.go | 37 +++++++++ api/handlers.go | 161 --------------------------------------- api/hostname_handlers.go | 137 +++++++++++++++++++++++++++++++++ api/rule_handlers.go | 38 +++++++++ main.go | 51 +------------ router/router.go | 73 ++++++++++++++++++ 6 files changed, 288 insertions(+), 209 deletions(-) create mode 100644 api/auth_handlers.go delete mode 100644 api/handlers.go create mode 100644 api/hostname_handlers.go create mode 100644 api/rule_handlers.go create mode 100644 router/router.go diff --git a/api/auth_handlers.go b/api/auth_handlers.go new file mode 100644 index 0000000..87c4450 --- /dev/null +++ b/api/auth_handlers.go @@ -0,0 +1,37 @@ +// Copyright 2024 Florian Beisel +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// Helloworld godoc +// +// @Summary Check your authentication +// @Schemes +// @Description Checks whether the user is successfully authenticated +// @ID hello +// @Tags Authentication +// @Accept json +// @Produce json +// @Security Bearer +// @Success 200 {string} Helloworld +// @Router /hello [get] +func Helloworld(g *gin.Context) { + g.JSON(http.StatusOK, "helloworld") +} diff --git a/api/handlers.go b/api/handlers.go deleted file mode 100644 index 250dea7..0000000 --- a/api/handlers.go +++ /dev/null @@ -1,161 +0,0 @@ -package api - -import ( - "errors" - "net/http" - "strings" - - "git.beisel.it/florian/hostname-service/db" - "git.beisel.it/florian/hostname-service/models" - "git.beisel.it/florian/hostname-service/rules" - "github.com/gin-gonic/gin" -) - -func getHostnameRuleByCategory(category string) (rules.HostnameRule, error) { - switch category { - case "notebook": - return &rules.NotebookRule{}, nil - // ... other categories - default: - return nil, errors.New("unknown category") - } -} - -// CreateOrUpdateHostname handles the hostname generation -func CreateOrUpdateHostname(c *gin.Context, isUpdate bool) { - category := c.Param("category") - var oldHostname string - if isUpdate { - oldHostname = c.Param("oldhostname") - } - - var params map[string]interface{} - if err := c.BindJSON(¶ms); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "invalid parameters"}) - return - } - - rule, err := getHostnameRuleByCategory(category) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - hostname, paramsJSON, err := rule.Generate(params) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - if isUpdate { - err = rule.Update(category, oldHostname, hostname, paramsJSON) - if err != nil { - if strings.Contains(err.Error(), "does not exist") { - c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) - return - } else { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - } - } else { - err = rule.Insert(category, hostname, paramsJSON) - } - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Error processing hostname"}) - return - } - - response := models.SimpleHostnameResponse{Hostname: hostname} - c.JSON(http.StatusOK, response) -} - -// @Summary Delete a hostname from the database -// @Description List all details for a given category -// @ID delete-hostnames-by-category-and-name -// @Produce json -// @Param category path string true "Category of the hostname" -// @Param hostname path string true "Hostname to delete" -// @Success 200 {object} models.SimpleHostnameResponse "Hostname" -// @Security Bearer -// @Tags Manipulate existing Hostnames -// @Router /{category}/{hostname} [delete] -func DeleteHostname(c *gin.Context) { - category := c.Param("category") - hostname := c.Param("hostname") - - err := db.DeleteHostname(category, hostname) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - response := models.SimpleHostnameResponse{Hostname: hostname} - c.JSON(http.StatusOK, response) -} - -// @Summary Return a list of hosts and their details filtered by category -// @Description List all details for a given category -// @ID list-hostnames-by-category -// @Produce json -// @Param category path string true "Category of the hostname" -// @Success 200 {array} models.Hostname "An array of responses" -// @Security Bearer -// @Tags Querying Hostnames -// @Router /{category} [get] -func ListHostnamesByCategory(c *gin.Context) { - category := c.Param("category") - - hostnames, err := db.GetHostnamesByCategory(category) - if err != nil { - if strings.Contains(err.Error(), "no rows found") { - c.JSON(http.StatusNotFound, gin.H{"error": "hostname not found"}) - } - c.JSON(http.StatusInternalServerError, gin.H{"error": "Error retrieving hostnames"}) - return - } - - c.JSON(http.StatusOK, hostnames) -} - -// @Summary Return a single hostname by Category and Name -// @Description Return details for a single hostname identified by its category -// @ID get-hostname-by-category-and-name -// @Produce json -// @Param category path string true "Category of the hostname" -// @Param hostname path string true "Category of the hostname" -// @Success 200 {object} models.Hostname "A single response object" -// @Security Bearer -// @Tags Querying Hostnames -// @Router /{category}/{hostname} [get] -func GetHostnameByCategoryAndName(c *gin.Context) { - category := c.Param("category") - hostname := c.Param("hostname") - - hostinfo, err := db.GetHostnameByCategoryAndName(category, hostname) - if err != nil { - if strings.Contains(err.Error(), "no rows found") { - c.JSON(http.StatusNotFound, gin.H{"error": "hostname not found"}) - return - } - c.JSON(http.StatusInternalServerError, gin.H{"error": "Error retrieving hostname"}) - return - } - - c.JSON(http.StatusOK, hostinfo) -} - -// Helloworld godoc -// -// @Summary Check your authentication -// @Schemes -// @Description Checks whether the user is successfully authenticated -// @ID hello -// @Tags Authentication -// @Accept json -// @Produce json -// @Security Bearer -// @Success 200 {string} Helloworld -// @Router /hello [get] -func Helloworld(g *gin.Context) { - g.JSON(http.StatusOK, "helloworld") -} diff --git a/api/hostname_handlers.go b/api/hostname_handlers.go new file mode 100644 index 0000000..bf30b24 --- /dev/null +++ b/api/hostname_handlers.go @@ -0,0 +1,137 @@ +// Copyright 2024 Florian Beisel +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + "strings" + + "git.beisel.it/florian/hostname-service/db" + "git.beisel.it/florian/hostname-service/models" + "github.com/gin-gonic/gin" +) + +// @Summary Return a single hostname by Category and Name +// @Description Return details for a single hostname identified by its category +// @ID get-hostname-by-category-and-name +// @Produce json +// @Param category path string true "Category of the hostname" +// @Param hostname path string true "Category of the hostname" +// @Success 200 {object} models.Hostname "A single response object" +// @Security Bearer +// @Tags Querying Hostnames +// @Router /{category}/{hostname} [get] +func GetHostnameByCategoryAndName(c *gin.Context) { + category := c.Param("category") + hostname := c.Param("hostname") + + hostinfo, err := db.GetHostnameByCategoryAndName(category, hostname) + if err != nil { + if strings.Contains(err.Error(), "no rows found") { + c.JSON(http.StatusNotFound, gin.H{"error": "hostname not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error retrieving hostname"}) + return + } + + c.JSON(http.StatusOK, hostinfo) +} + +// @Summary Return a list of hosts and their details filtered by category +// @Description List all details for a given category +// @ID list-hostnames-by-category +// @Produce json +// @Param category path string true "Category of the hostname" +// @Success 200 {array} models.Hostname "An array of responses" +// @Security Bearer +// @Tags Querying Hostnames +// @Router /{category} [get] +func ListHostnamesByCategory(c *gin.Context) { + category := c.Param("category") + + hostnames, err := db.GetHostnamesByCategory(category) + if err != nil { + if strings.Contains(err.Error(), "no rows found") { + c.JSON(http.StatusNotFound, gin.H{"error": "hostname not found"}) + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error retrieving hostnames"}) + return + } + + c.JSON(http.StatusOK, hostnames) +} + +// @Summary Delete a hostname from the database +// @Description List all details for a given category +// @ID delete-hostnames-by-category-and-name +// @Produce json +// @Param category path string true "Category of the hostname" +// @Param hostname path string true "Hostname to delete" +// @Success 200 {object} models.SimpleHostnameResponse "Hostname" +// @Security Bearer +// @Tags Manipulate existing Hostnames +// @Router /{category}/{hostname} [delete] +func DeleteHostname(c *gin.Context) { + category := c.Param("category") + hostname := c.Param("hostname") + + err := db.DeleteHostname(category, hostname) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + response := models.SimpleHostnameResponse{Hostname: hostname} + c.JSON(http.StatusOK, response) +} + +func CreateOrUpdateHostname(c *gin.Context, isUpdate bool) { + category := c.Param("category") + var oldHostname string + if isUpdate { + oldHostname = c.Param("oldhostname") + } + + var params map[string]interface{} + if err := c.BindJSON(¶ms); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid parameters"}) + return + } + + rule, err := getHostnameRuleByCategory(category) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + var hostname string + if isUpdate { + hostname, err = rule.Update(category, oldHostname, params) + } else { + hostname, err = rule.Insert(category, params) + } + + if err != nil { + if isUpdate && strings.Contains(err.Error(), "does not exist") { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + } else { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } + return + } + + response := models.SimpleHostnameResponse{Hostname: hostname} + c.JSON(http.StatusOK, response) +} diff --git a/api/rule_handlers.go b/api/rule_handlers.go new file mode 100644 index 0000000..d7b9d92 --- /dev/null +++ b/api/rule_handlers.go @@ -0,0 +1,38 @@ +// Copyright 2024 Florian Beisel +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "errors" + "net/http" + + "git.beisel.it/florian/hostname-service/rules" + "github.com/gin-gonic/gin" +) + +func ListAvailableRules(c *gin.Context) { + descriptions := make(map[string]string) + for category, descriptor := range rules.RulesRegistry { + descriptions[category] = descriptor.Description + } + c.JSON(http.StatusOK, descriptions) +} + +func getHostnameRuleByCategory(category string) (rules.HostnameRule, error) { + if descriptor, exists := rules.RulesRegistry[category]; exists { + return descriptor.Factory(), nil + } + return nil, errors.New("unknown category") +} diff --git a/main.go b/main.go index 8a528c4..9aff3d0 100644 --- a/main.go +++ b/main.go @@ -19,15 +19,9 @@ package main import ( "log" - "git.beisel.it/florian/hostname-service/api" - "git.beisel.it/florian/hostname-service/auth" "git.beisel.it/florian/hostname-service/config" "git.beisel.it/florian/hostname-service/db" - "git.beisel.it/florian/hostname-service/docs" - "git.beisel.it/florian/hostname-service/middleware" - "github.com/gin-gonic/gin" - swaggerFiles "github.com/swaggo/files" - ginSwagger "github.com/swaggo/gin-swagger" + "git.beisel.it/florian/hostname-service/router" ) // @title Hostname Service API @@ -54,48 +48,9 @@ func main() { log.Fatalf("Failed to load configuration: %v", err) } - gin.SetMode(gin.DebugMode) - db.Init(config.GlobalConfig.DatabaseFile) - router := gin.Default() - - docs.SwaggerInfo.Host = "localhost:8080" - docs.SwaggerInfo.BasePath = "/api/v1" - - v1 := router.Group("/api/v1") - { - // public routes - v1.POST("/login", auth.LoginHandler) - - // Protected Routes - authenticated := v1.Group("/").Use(middleware.Authenticate()) - { - authenticated.GET("/hello", api.Helloworld) - - // Create Host - authenticated.POST("/:category", func(c *gin.Context) { - api.CreateOrUpdateHostname(c, false) - }) - - // Get Host Details - authenticated.GET("/:category/:hostname", api.GetHostnameByCategoryAndName) - - // Update Host - authenticated.PUT("/:category/:oldhostname", func(c *gin.Context) { - api.CreateOrUpdateHostname(c, true) - }) - - // Delete Host - authenticated.DELETE("/:category/:hostname", api.DeleteHostname) - - // List Hostnames - authenticated.GET("/:category", api.ListHostnamesByCategory) - } - } - - // Swagger endpoint - router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) - + router := router.New() router.Run(":8080") + } diff --git a/router/router.go b/router/router.go new file mode 100644 index 0000000..9532c97 --- /dev/null +++ b/router/router.go @@ -0,0 +1,73 @@ +// Copyright 2024 Florian Beisel +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package router + +import ( + "git.beisel.it/florian/hostname-service/api" + "git.beisel.it/florian/hostname-service/auth" + "git.beisel.it/florian/hostname-service/docs" + "git.beisel.it/florian/hostname-service/middleware" + "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" + // ... other necessary imports ... +) + +func New() *gin.Engine { + gin.SetMode(gin.DebugMode) + router := gin.Default() + + docs.SwaggerInfo.Host = "localhost:8080" + docs.SwaggerInfo.BasePath = "/api/v1" + + v1 := router.Group("/api/v1") + { + // public routes + v1.POST("/login", auth.LoginHandler) + + // Protected Routes + authenticated := v1.Group("/").Use(middleware.Authenticate()) + { + authenticated.GET("/hello", api.Helloworld) + + // Create Host + authenticated.POST("/:category", func(c *gin.Context) { + api.CreateOrUpdateHostname(c, false) + }) + + // Get Host Details + authenticated.GET("/:category/:hostname", api.GetHostnameByCategoryAndName) + + // Update Host + authenticated.PUT("/:category/:oldhostname", func(c *gin.Context) { + api.CreateOrUpdateHostname(c, true) + }) + + // Delete Host + authenticated.DELETE("/:category/:hostname", api.DeleteHostname) + + // List Hostnames + authenticated.GET("/:category", api.ListHostnamesByCategory) + + // List available Rules + authenticated.GET("/api/rules", api.ListAvailableRules) + } + } + + // Swagger endpoint + router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + + return router +}