From 8f6b1efea0919c21b9b45727c222680467c787a22cb24d34eae0a14fe8579a08 Mon Sep 17 00:00:00 2001 From: Daan Selen Date: Wed, 29 Apr 2026 13:25:38 +0200 Subject: [PATCH] feat: add services and separate further --- internal/server/api/api.go | 3 + .../api/{routes/types.go => assets/body.go} | 4 +- internal/server/api/assets/response.go | 45 ++++++++++ internal/server/api/middleware/middleware.go | 6 +- internal/server/api/response/response.go | 11 --- internal/server/api/routes/api.go | 90 +------------------ internal/server/api/routes/ctrl.go | 36 ++++++++ internal/server/api/routes/file.go | 62 ++++--------- internal/server/api/routes/key.go | 48 ++++++++++ internal/server/database/functions.go | 2 +- internal/server/service/ctrlservice.go | 21 +++++ internal/server/service/fileservice.go | 74 +++++++++++++++ internal/server/service/keyservice.go | 55 ++++++++++++ internal/server/watchdog/executor.go | 7 +- internal/server/watchdog/scanner.go | 12 +-- internal/shared/utility/utility.go | 10 --- 16 files changed, 318 insertions(+), 168 deletions(-) rename internal/server/api/{routes/types.go => assets/body.go} (76%) create mode 100644 internal/server/api/assets/response.go delete mode 100644 internal/server/api/response/response.go create mode 100644 internal/server/api/routes/ctrl.go create mode 100644 internal/server/api/routes/key.go create mode 100644 internal/server/service/ctrlservice.go create mode 100644 internal/server/service/fileservice.go create mode 100644 internal/server/service/keyservice.go diff --git a/internal/server/api/api.go b/internal/server/api/api.go index bd06f04..132bf8e 100644 --- a/internal/server/api/api.go +++ b/internal/server/api/api.go @@ -31,6 +31,9 @@ func Kickoff(logger *slog.Logger, env bootstrap.Environment, db *gorm.DB) { api := r.Group("/api") routes.RegisterApiRoutes(api, db) + // also register the 2 api subroutes + routes.RegisterKeyRoutes(api, db) + routes.RegisterCtrlRoutes(api, db) file := r.Group("/file") routes.RegisterFileRoutes(file, env, db) diff --git a/internal/server/api/routes/types.go b/internal/server/api/assets/body.go similarity index 76% rename from internal/server/api/routes/types.go rename to internal/server/api/assets/body.go index 647ad3e..febbf36 100644 --- a/internal/server/api/routes/types.go +++ b/internal/server/api/assets/body.go @@ -1,8 +1,8 @@ -package routes +package assets import "time" -type keyRequestBody struct { +type KeyRequestBody struct { Name string `json:"name"` // post request must contain valid: RFC3339 timestamp ExpiresAt time.Time `json:"expiresAt"` diff --git a/internal/server/api/assets/response.go b/internal/server/api/assets/response.go new file mode 100644 index 0000000..a198aaf --- /dev/null +++ b/internal/server/api/assets/response.go @@ -0,0 +1,45 @@ +package assets + +import ( + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +const ( + OkMes string = "OK" + IntErrMes string = "An internal error occured, contact your administrator" +) + +type BasicResponse struct { + Msg string `json:"msg"` + Data any `json:"data"` +} + +// we swap out the hash for the keycontent +type KeyResponse struct { + ID int `json:"id"` + MetaName string `json:"metaName"` + KeyName string `json:"keyName"` + KeyContent string `json:"keyContent"` + Revoked bool `json:"revoked"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + ExpiresAt time.Time `json:"expiresAt"` +} + +type FileResponse struct { + ID int `json:"id"` + MetaName string `json:"metaName"` + FileName string `json:"fileName"` + MediaType string `json:"mediaType"` + CreatedAt time.Time `json:"createdAt"` + ExpiresAt time.Time `json:"expiresAt"` +} + +func InternalErrorResponse(c *gin.Context) { + c.JSON(http.StatusInternalServerError, BasicResponse{ + Msg: IntErrMes, + }) +} diff --git a/internal/server/api/middleware/middleware.go b/internal/server/api/middleware/middleware.go index c93cf98..24bc078 100644 --- a/internal/server/api/middleware/middleware.go +++ b/internal/server/api/middleware/middleware.go @@ -3,7 +3,7 @@ package middleware import ( "log/slog" "net/http" - "orbits-server/internal/server/api/response" + "orbits-server/internal/server/api/assets" "strings" "time" @@ -42,7 +42,7 @@ func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { authorizationHeader := c.GetHeader("Authorization") if len(authorizationHeader) == 0 { - c.AbortWithStatusJSON(http.StatusUnauthorized, response.BasicResponse{ + c.AbortWithStatusJSON(http.StatusUnauthorized, assets.BasicResponse{ Msg: "Authorization header is required", }) return @@ -52,7 +52,7 @@ func AuthMiddleware() gin.HandlerFunc { // The header must be a specific format, 0 being the bearer text and 1 being the token itself, making it 2 pieces total // In the following if statement we verify both parts if the part after Bearer is empty its only 1 part for example if len(headerParts) != 2 || headerParts[0] != "Bearer" { - c.AbortWithStatusJSON(http.StatusUnauthorized, response.BasicResponse{ + c.AbortWithStatusJSON(http.StatusUnauthorized, assets.BasicResponse{ Msg: "Authorization header is invalid", }) return diff --git a/internal/server/api/response/response.go b/internal/server/api/response/response.go deleted file mode 100644 index 31902e8..0000000 --- a/internal/server/api/response/response.go +++ /dev/null @@ -1,11 +0,0 @@ -package response - -const ( - OkMes string = "OK" - IntErrMes string = "An internal error occured, contact your administrator" -) - -type BasicResponse struct { - Msg string `json:"msg"` - Data any `json:"data"` -} diff --git a/internal/server/api/routes/api.go b/internal/server/api/routes/api.go index 5e3c5bf..74b6881 100644 --- a/internal/server/api/routes/api.go +++ b/internal/server/api/routes/api.go @@ -1,102 +1,16 @@ package routes import ( - "log/slog" - "net/http" - "orbits-server/internal/server/api/response" - "orbits-server/internal/server/database" - "orbits-server/internal/shared/security" - "github.com/gin-gonic/gin" "gorm.io/gorm" ) -const ( - accessKeyLen = 32 -) - func RegisterApiRoutes(api *gin.RouterGroup, db *gorm.DB) { - // prefix: api - - // define subroute with key - // /api/key - key := api.Group("/key") - - /* - key.GET("/:key", func(c *gin.Context) { - - }) - */ - - key.POST("/create", func(c *gin.Context) { - var body keyRequestBody - - err := c.ShouldBindBodyWithJSON(&body) - if err != nil { - slog.Error("failed to bind body to json", "error", err) - c.JSON(http.StatusBadRequest, response.BasicResponse{ - Msg: "invalid JSON", - }) - return - } - - keyContent := security.GenerateChars(accessKeyLen) - hash, err := security.HashKey(keyContent) - if err != nil { - slog.Error("failed to generate a hash for the key", "error", err) - c.JSON(http.StatusInternalServerError, response.BasicResponse{ - Msg: response.IntErrMes, - }) - return - } - - keyRecord := database.BuildKeyRecord(hash, body.Name, body.ExpiresAt) - - if err := database.CreateKey(db, &keyRecord); err != nil { - slog.Error("failed to insert key into the database", "error", err) - c.JSON(http.StatusInternalServerError, response.BasicResponse{ - Msg: response.IntErrMes, - }) - return - } - - slog.Info("saved key to database") - - c.JSON(http.StatusCreated, response.BasicResponse{ - Msg: "key has succesfully been created and saved", - Data: keyContent, - }) - }) - - key.GET("/verify", func(c *gin.Context) { + api.GET("/version", func(c *gin.Context) { }) - key.DELETE("/:key", func(c *gin.Context) { - - }) - - // define the control route on the api - // /api/control - ctl := api.Group("/control") - // Display the information on what is going on at the moment - ctl.GET("/command", func(c *gin.Context) { - state, err := database.LatestState(db) - if err != nil { - slog.Error("unable to determine state", "error", err) - c.JSON(http.StatusInternalServerError, response.BasicResponse{ - Msg: response.IntErrMes, - }) - return - } - - c.JSON(http.StatusOK, response.BasicResponse{ - Msg: response.OkMes, - Data: state, - }) - }) - - ctl.PATCH("/command", func(c *gin.Context) { + api.GET("/codename", func(c *gin.Context) { }) } diff --git a/internal/server/api/routes/ctrl.go b/internal/server/api/routes/ctrl.go new file mode 100644 index 0000000..05b0e9b --- /dev/null +++ b/internal/server/api/routes/ctrl.go @@ -0,0 +1,36 @@ +package routes + +import ( + "log/slog" + "net/http" + "orbits-server/internal/server/api/assets" + "orbits-server/internal/server/service" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +func RegisterCtrlRoutes(api *gin.RouterGroup, db *gorm.DB) { + ctrlService := service.NewControlService(db) + // define the control route on the api + // /api/control + ctrl := api.Group("/control") + // Display the information on what is going on at the moment + ctrl.GET("/command", func(c *gin.Context) { + command, err := ctrlService.ListLatestCommand() + if err != nil { + slog.Error("unable to determine state", "error", err) + assets.InternalErrorResponse(c) + return + } + + c.JSON(http.StatusOK, assets.BasicResponse{ + Msg: assets.OkMes, + Data: command, + }) + }) + + ctrl.PATCH("/command", func(c *gin.Context) { + + }) +} diff --git a/internal/server/api/routes/file.go b/internal/server/api/routes/file.go index 8ba2751..6bf71d1 100644 --- a/internal/server/api/routes/file.go +++ b/internal/server/api/routes/file.go @@ -1,12 +1,11 @@ package routes import ( - "errors" "log/slog" "net/http" - "orbits-server/internal/server/api/response" + "orbits-server/internal/server/api/assets" "orbits-server/internal/server/bootstrap" - "orbits-server/internal/server/database" + "orbits-server/internal/server/service" "path/filepath" "github.com/gin-gonic/gin" @@ -14,6 +13,7 @@ import ( ) func RegisterFileRoutes(file *gin.RouterGroup, env bootstrap.Environment, db *gorm.DB) { + fileService := service.NewFileService(db, env) // prefix: file // for example: /file/ @@ -30,7 +30,7 @@ func RegisterFileRoutes(file *gin.RouterGroup, env bootstrap.Environment, db *go fh, err := c.FormFile("file") if err != nil { slog.Debug("no file or file headers provided on the request", "error", err) - c.JSON(http.StatusBadRequest, response.BasicResponse{ + c.JSON(http.StatusBadRequest, assets.BasicResponse{ Msg: "a file is required", }) return @@ -39,35 +39,15 @@ func RegisterFileRoutes(file *gin.RouterGroup, env bootstrap.Environment, db *go stream, err := fh.Open() if err != nil { slog.Error("failed to a reader stream") - c.JSON(http.StatusInternalServerError, response.BasicResponse{ - Msg: response.IntErrMes, - }) + assets.InternalErrorResponse(c) return } defer stream.Close() - fileRecord, err := database.BuildFileRecord(stream, fh.Filename, env.ContentDirectory) + fileRecord, err := fileService.Create(stream, fh.Filename) if err != nil { slog.Error("failed to enroll file to the database", "error", err) - c.JSON(http.StatusInternalServerError, response.BasicResponse{ - Msg: response.IntErrMes, - }) - return - } - - if err := database.CreateFile(db, &fileRecord); err != nil { - if errors.Is(err, gorm.ErrDuplicatedKey) { - slog.Debug("discarding file, its a checksum duplicate", "error", err) - c.JSON(http.StatusConflict, response.BasicResponse{ - Msg: "file already exists", - }) - } else { - // log the failure to the std - slog.Error("failed to insert filedata to the database", "error", err) - c.JSON(http.StatusInternalServerError, response.BasicResponse{ - Msg: response.IntErrMes, - }) - } + assets.InternalErrorResponse(c) return } @@ -76,20 +56,15 @@ func RegisterFileRoutes(file *gin.RouterGroup, env bootstrap.Environment, db *go slog.Error("failed to save to disk, rolling back database", "error", err) // rollback db if the write has failed - err = database.DeleteFileByID(db, fileRecord.ID) - if err != nil { - slog.Error("failed to remove the database record", "error", err) - } + fileService.DeleteByID(fileRecord.ID) // give the response to the client - c.JSON(http.StatusInternalServerError, response.BasicResponse{ - Msg: response.IntErrMes, - }) + assets.InternalErrorResponse(c) return } slog.Info("saved file to local filesystem and database") - c.JSON(http.StatusCreated, response.BasicResponse{ + c.JSON(http.StatusCreated, assets.BasicResponse{ Msg: "file has succesfully been uploaded", Data: fileRecord, }) @@ -98,31 +73,32 @@ func RegisterFileRoutes(file *gin.RouterGroup, env bootstrap.Environment, db *go // delete route file.DELETE("/:filename", func(c *gin.Context) { fileParam := c.Param("filename") - fileRecord, err := database.FindFileByName(db, fileParam) + fileRecord, err := fileService.DeleteByName(fileParam) if err != nil { slog.Error("file not found", "error", err) - c.JSON(http.StatusNotFound, response.BasicResponse{ + c.JSON(http.StatusNotFound, assets.BasicResponse{ Msg: "file was not found", }) return } slog.Info("received a delete request for a file", "file", fileRecord, "filename", fileParam) + c.JSON(http.StatusOK, assets.BasicResponse{ + Msg: "file deleted succesfully", + }) }) // define a route to check what is registered file.GET("/available", func(c *gin.Context) { - files, err := database.ListFiles(db) + files, err := fileService.ListFiles() if err != nil { slog.Error("failed to retrieve available files", "error", err) - c.JSON(http.StatusInternalServerError, response.BasicResponse{ - Msg: response.IntErrMes, - }) + assets.InternalErrorResponse(c) return } - c.JSON(http.StatusOK, response.BasicResponse{ - Msg: response.OkMes, + c.JSON(http.StatusOK, assets.BasicResponse{ + Msg: assets.OkMes, Data: files, }) }) diff --git a/internal/server/api/routes/key.go b/internal/server/api/routes/key.go new file mode 100644 index 0000000..d455d31 --- /dev/null +++ b/internal/server/api/routes/key.go @@ -0,0 +1,48 @@ +package routes + +import ( + "log/slog" + "net/http" + "orbits-server/internal/server/api/assets" + "orbits-server/internal/server/service" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +func RegisterKeyRoutes(api *gin.RouterGroup, db *gorm.DB) { + keyService := service.NewKeyService(db) + // prefix: api + + // define subroute with key + // /api/key + key := api.Group("/key") + + key.POST("/create", func(c *gin.Context) { + var body assets.KeyRequestBody + + if err := c.ShouldBindJSON(&body); err != nil { + slog.Error("failed to bind body to json", "error", err) + assets.InternalErrorResponse(c) + return + } + + keyResponse, err := keyService.Create(body.Name, body.ExpiresAt) + if err != nil { + slog.Error("failed to create key", "error", err) + assets.InternalErrorResponse(c) + return + } + + slog.Info("saved key to database") + + c.JSON(http.StatusCreated, assets.BasicResponse{ + Msg: "key has succesfully been created and saved", + Data: keyResponse, + }) + }) + + key.DELETE("/:key", func(c *gin.Context) { + + }) +} diff --git a/internal/server/database/functions.go b/internal/server/database/functions.go index 85904d9..a2f279e 100644 --- a/internal/server/database/functions.go +++ b/internal/server/database/functions.go @@ -8,7 +8,7 @@ import ( State functions */ -func LatestState(db *gorm.DB) (Command, error) { +func LatestCommand(db *gorm.DB) (Command, error) { var state Command err := db.Last(&state).Error return state, err diff --git a/internal/server/service/ctrlservice.go b/internal/server/service/ctrlservice.go new file mode 100644 index 0000000..8c03b4b --- /dev/null +++ b/internal/server/service/ctrlservice.go @@ -0,0 +1,21 @@ +package service + +import ( + "orbits-server/internal/server/database" + + "gorm.io/gorm" +) + +type CtlService struct { + db *gorm.DB +} + +func NewControlService(db *gorm.DB) *CtlService { + return &CtlService{ + db: db, + } +} + +func (s *CtlService) ListLatestCommand() (database.Command, error) { + return database.LatestCommand(s.db) +} diff --git a/internal/server/service/fileservice.go b/internal/server/service/fileservice.go new file mode 100644 index 0000000..70d33e6 --- /dev/null +++ b/internal/server/service/fileservice.go @@ -0,0 +1,74 @@ +package service + +import ( + "io" + "orbits-server/internal/server/api/assets" + "orbits-server/internal/server/bootstrap" + "orbits-server/internal/server/database" + "os" + + "gorm.io/gorm" +) + +type FileService struct { + db *gorm.DB + env bootstrap.Environment +} + +func NewFileService(db *gorm.DB, env bootstrap.Environment) *FileService { + return &FileService{db: db, env: env} +} + +func (s *FileService) ListFiles() ([]assets.FileResponse, error) { + files, err := database.ListFiles(s.db) + if err != nil { + return nil, err + } + + resp := make([]assets.FileResponse, 0, len(files)) + + for _, f := range files { + resp = append(resp, assets.FileResponse{ + ID: f.ID, + MetaName: f.MetaName, + FileName: f.FileName, + MediaType: string(f.MediaType), + CreatedAt: f.CreatedAt, + ExpiresAt: f.ExpiresAt, + }) + } + + return resp, nil +} + +func (s *FileService) Create(r io.Reader, filename string) (database.File, error) { + fileRecord, err := database.BuildFileRecord(r, filename, s.env.ContentDirectory) + if err != nil { + return database.File{}, err + } + + if err := database.CreateFile(s.db, &fileRecord); err != nil { + return database.File{}, err + } + + return fileRecord, nil +} + +func (s *FileService) DeleteByName(filename string) (database.File, error) { + fileRecord, err := database.FindFileByName(s.db, filename) + if err != nil { + return database.File{}, err + } + + _ = os.Remove(fileRecord.FilePath) + + if err := database.DeleteFileByID(s.db, fileRecord.ID); err != nil { + return database.File{}, err + } + + return fileRecord, nil +} + +func (s *FileService) DeleteByID(id int) { + _ = database.DeleteFileByID(s.db, id) +} diff --git a/internal/server/service/keyservice.go b/internal/server/service/keyservice.go new file mode 100644 index 0000000..8f9a10b --- /dev/null +++ b/internal/server/service/keyservice.go @@ -0,0 +1,55 @@ +package service + +import ( + "orbits-server/internal/server/api/assets" + "orbits-server/internal/server/database" + "orbits-server/internal/shared/security" + "time" + + "gorm.io/gorm" +) + +const ( + accessKeyLen = 32 +) + +type KeyService struct { + db *gorm.DB +} + +func NewKeyService(db *gorm.DB) *KeyService { + return &KeyService{ + db: db, + } +} + +func (s *KeyService) Create(name string, expiresAt time.Time) (assets.KeyResponse, error) { + keyContent := security.GenerateChars(accessKeyLen) + + hash, err := security.HashKey(keyContent) + if err != nil { + return assets.KeyResponse{}, err + } + + keyRecord := database.BuildKeyRecord(hash, name, expiresAt) + + if err := database.CreateKey(s.db, &keyRecord); err != nil { + return assets.KeyResponse{}, err + } + + keyResponse := assets.KeyResponse{ + ID: keyRecord.ID, + MetaName: keyRecord.MetaName, + KeyName: keyRecord.KeyName, + KeyContent: keyContent, + CreatedAt: keyRecord.CreatedAt, + UpdatedAt: keyRecord.UpdatedAt, + ExpiresAt: keyRecord.ExpiresAt, + } + + return keyResponse, nil +} + +func (s *KeyService) DeleteByName(name string) { + +} diff --git a/internal/server/watchdog/executor.go b/internal/server/watchdog/executor.go index be37b8d..fdf6019 100644 --- a/internal/server/watchdog/executor.go +++ b/internal/server/watchdog/executor.go @@ -4,7 +4,6 @@ import ( "log/slog" "orbits-server/internal/server/bootstrap" "orbits-server/internal/server/database" - "orbits-server/internal/shared/utility" "os" "path/filepath" @@ -34,12 +33,12 @@ func applyFS(env bootstrap.Environment, db *gorm.DB, fsOrphans []string) { return } - database.CreateFile(db, &fileData) - os.Rename(fp, fileData.FilePath) + _ = database.CreateFile(db, &fileData) + _ = os.Rename(fp, fileData.FilePath) }() case "strict": - utility.RemoveFile(fp) + _ = os.Remove(fp) case "dry": slog.Debug("dry mode", "file", fp) diff --git a/internal/server/watchdog/scanner.go b/internal/server/watchdog/scanner.go index 163e8c4..629bcdf 100644 --- a/internal/server/watchdog/scanner.go +++ b/internal/server/watchdog/scanner.go @@ -33,13 +33,13 @@ func scanFS(env bootstrap.Environment) (map[string]struct{}, error) { return nil, err } - res := make(map[string]struct{}) + resp := make(map[string]struct{}) for _, f := range fsFiles { full := filepath.Join(env.ContentDirectory, f.Name()) - res[full] = struct{}{} + resp[full] = struct{}{} } - return res, nil + return resp, nil } func scanDB(db *gorm.DB) (map[string]database.File, error) { @@ -48,10 +48,10 @@ func scanDB(db *gorm.DB) (map[string]database.File, error) { return nil, err } - res := make(map[string]database.File) + resp := make(map[string]database.File) for _, f := range files { - res[f.FilePath] = f + resp[f.FilePath] = f } - return res, nil + return resp, nil } diff --git a/internal/shared/utility/utility.go b/internal/shared/utility/utility.go index a3b9204..1af8e8b 100644 --- a/internal/shared/utility/utility.go +++ b/internal/shared/utility/utility.go @@ -2,20 +2,10 @@ package utility import ( "log/slog" - "os" "slices" "strings" ) -func RemoveFile(p string) error { - err := os.Remove(p) - if err != nil { - return err - } - - return nil -} - func CategorizeMediaType(ext string) MediaType { // Lets categorize