🎨 Refactor basic server structure

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.
This commit is contained in:
Florian Beisel 2024-01-21 14:54:34 +01:00
parent a4748d7619
commit c51a17993e
Signed by: florian
GPG Key ID: 79ECA2E54996FF4D
6 changed files with 288 additions and 209 deletions

37
api/auth_handlers.go Normal file
View File

@ -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")
}

View File

@ -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(&params); 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")
}

137
api/hostname_handlers.go Normal file
View File

@ -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(&params); 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)
}

38
api/rule_handlers.go Normal file
View File

@ -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")
}

51
main.go
View File

@ -19,15 +19,9 @@ package main
import ( import (
"log" "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/config"
"git.beisel.it/florian/hostname-service/db" "git.beisel.it/florian/hostname-service/db"
"git.beisel.it/florian/hostname-service/docs" "git.beisel.it/florian/hostname-service/router"
"git.beisel.it/florian/hostname-service/middleware"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
) )
// @title Hostname Service API // @title Hostname Service API
@ -54,48 +48,9 @@ func main() {
log.Fatalf("Failed to load configuration: %v", err) log.Fatalf("Failed to load configuration: %v", err)
} }
gin.SetMode(gin.DebugMode)
db.Init(config.GlobalConfig.DatabaseFile) db.Init(config.GlobalConfig.DatabaseFile)
router := gin.Default() router := router.New()
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.Run(":8080") router.Run(":8080")
} }

73
router/router.go Normal file
View File

@ -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
}