diff --git a/gateway/core/controllers/format.go b/gateway/core/controllers/format.go new file mode 100644 index 0000000..019f487 --- /dev/null +++ b/gateway/core/controllers/format.go @@ -0,0 +1,190 @@ +package controllers + +import ( + "net/http" + "strconv" + + "github.com/abaldeweg/warehouse-server/gateway/auth" + "github.com/abaldeweg/warehouse-server/gateway/core/models" + "github.com/abaldeweg/warehouse-server/gateway/core/repository" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// FormatController handles requests related to book formats. +type FormatController struct { + formatRepo *repository.FormatRepository + db *gorm.DB +} + +// NewFormatController instantiates a new FormatController. +func NewFormatController(db *gorm.DB) *FormatController { + return &FormatController{ + formatRepo: repository.NewFormatRepository(db), + db: db, + } +} + +// FindAll retrieves all formats for the authenticated user's branch. +func (fc *FormatController) FindAll(c *gin.Context) { + user, ok := c.Get("user") + if !ok { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"msg": "Unauthorized"}) + return + } + + formats, err := fc.formatRepo.FindAllByBranchID(uint(user.(auth.User).Branch.Id)) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve formats"}) + return + } + + c.JSON(http.StatusOK, formats) +} + +// FindOne retrieves a specific format by ID for the authenticated user's branch. +func (fc *FormatController) FindOne(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"}) + return + } + + format, err := fc.formatRepo.FindOne(uint(id)) + if err != nil { + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "Format not found"}) + return + } + + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve format"}) + return + } + + c.JSON(http.StatusOK, format) +} + +// Create creates a new format for the authenticated user's branch. +func (fc *FormatController) Create(c *gin.Context) { + var format models.Format + if err := c.ShouldBindJSON(&format); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + user, ok := c.Get("user") + if !ok { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"msg": "Unauthorized"}) + return + } + format.BranchID = uint(user.(auth.User).Branch.Id) + + if !format.Validate(fc.db) { + c.JSON(http.StatusBadRequest, gin.H{"error": "Validation failed"}) + return + } + + if err := fc.formatRepo.Create(&format); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create format"}) + return + } + + createdFormat, err := fc.formatRepo.FindOne(format.ID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve created format"}) + return + } + + c.JSON(http.StatusCreated, createdFormat) + +} + +// Update updates an existing format for the authenticated user's branch. +func (fc *FormatController) Update(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"}) + return + } + + existingFormat, err := fc.formatRepo.FindOne(uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Format not found"}) + return + } + + user, ok := c.Get("user") + if !ok { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"msg": "Unauthorized"}) + return + } + + if uint(user.(auth.User).Branch.Id) != existingFormat.BranchID { + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"msg": "Forbidden"}) + return + } + + var format models.Format + if err := c.ShouldBindJSON(&format); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Format has invalid data"}) + return + } + + format.ID = existingFormat.ID + format.BranchID = existingFormat.BranchID + + if !format.Validate(fc.db) { + c.JSON(http.StatusBadRequest, gin.H{"error": "Validation failed"}) + return + } + + if err := fc.formatRepo.Update(&format); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update format"}) + return + } + + updatedFormat, err := fc.formatRepo.FindOne(uint(id)) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve updated format"}) + return + } + + c.JSON(http.StatusOK, updatedFormat) +} + +// Delete deletes a format by ID for the authenticated user's branch. +func (fc *FormatController) Delete(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"}) + return + } + + user, ok := c.Get("user") + if !ok { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"msg": "Unauthorized"}) + return + } + + format, err := fc.formatRepo.FindOne(uint(id)) + if err != nil { + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "Format not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve format"}) + return + } + + if uint(user.(auth.User).Branch.Id) != format.BranchID { + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"msg": "Forbidden"}) + return + } + + if err := fc.formatRepo.Delete(uint(id)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete format"}) + return + } + + c.JSON(http.StatusNoContent, nil) +} diff --git a/gateway/core/database/database.go b/gateway/core/database/database.go index cd18978..5e25031 100644 --- a/gateway/core/database/database.go +++ b/gateway/core/database/database.go @@ -50,6 +50,7 @@ func runMigrations(db *gorm.DB) { &models.Condition{}, &models.Tag{}, &models.Genre{}, + &models.Format{}, ) if err != nil { diff --git a/gateway/core/models/book.go b/gateway/core/models/book.go index 65a823f..e9142df 100644 --- a/gateway/core/models/book.go +++ b/gateway/core/models/book.go @@ -27,12 +27,12 @@ type Book struct { ConditionID uint `json:"condition_id"` Tags []*Tag `json:"tags" gorm:"many2many:book_tag;"` // Reservation []*Reservation `json:"reservations" gorm:"foreignKey:BookID"` - Recommendation bool `json:"recommendations" gorm:"foreignKey:BookID"` - Inventory bool `json:"inventory" gorm:"default:false"` - // Format *Format `json:"format" gorm:"foreignKey:FormatID"` - FormatID uint `json:"format_id" gorm:"not null"` - Subtitle string `json:"subtitle" validate:"max=255"` - Duplicate bool `gorm:"default:false"` + Recommendation bool `json:"recommendations" gorm:"foreignKey:BookID"` + Inventory bool `json:"inventory" gorm:"default:false"` + Format *Format `json:"format" gorm:"foreignKey:FormatID"` + FormatID uint `json:"format_id" gorm:"not null"` + Subtitle string `json:"subtitle" validate:"max=255"` + Duplicate bool `gorm:"default:false"` } // TableName overrides the default table name for Book model. diff --git a/gateway/core/models/format.go b/gateway/core/models/format.go new file mode 100644 index 0000000..294f25a --- /dev/null +++ b/gateway/core/models/format.go @@ -0,0 +1,30 @@ +package models + +import ( + "github.com/go-playground/validator/v10" + "gorm.io/gorm" +) + +// Format represents a book format entity. +type Format struct { + ID uint `json:"id" gorm:"primaryKey;autoIncrement;->"` + Name string `json:"name" validate:"required,min=1,max=255"` + BranchID uint `json:"branch_id" gorm:"index"` + Branch Branch `json:"branch" gorm:"foreignKey:BranchID"` + Books []Book `json:"-" gorm:"foreignKey:FormatID"` +} + +// TableName overrides the default table name for Format model. +func (Format) TableName() string { + return "format" +} + +// Validate validates the Format model based on defined rules. +func (f *Format) Validate(db *gorm.DB) bool { + validate := validator.New() + if err := validate.StructExcept(f, "Branch", "Books"); err != nil { + return false + } + + return true +} diff --git a/gateway/core/repository/format.go b/gateway/core/repository/format.go new file mode 100644 index 0000000..b6aa40b --- /dev/null +++ b/gateway/core/repository/format.go @@ -0,0 +1,48 @@ +package repository + +import ( + "github.com/abaldeweg/warehouse-server/gateway/core/models" + "gorm.io/gorm" +) + +// FormatRepository struct for format repository. +type FormatRepository struct { + db *gorm.DB +} + +// NewFormatRepository creates a new format repository. +func NewFormatRepository(db *gorm.DB) *FormatRepository { + return &FormatRepository{db: db} +} + +// FindAllByBranchID returns all formats for a given branch ID, ordered alphabetically by name. +func (r *FormatRepository) FindAllByBranchID(branchID uint) ([]models.Format, error) { + var formats []models.Format + result := r.db.Preload("Branch").Where("branch_id = ?", branchID).Order("name asc").Find(&formats) + return formats, result.Error +} + +// FindOne returns one format by id and branchID. +func (r *FormatRepository) FindOne(id uint) (models.Format, error) { + var format models.Format + result := r.db.Preload("Branch").Where("id = ?", id).First(&format) + return format, result.Error +} + +// Create creates a new format. +func (r *FormatRepository) Create(format *models.Format) error { + result := r.db.Create(format) + return result.Error +} + +// Update updates a format. +func (r *FormatRepository) Update(format *models.Format) error { + result := r.db.Save(format) + return result.Error +} + +// Delete deletes a format. +func (r *FormatRepository) Delete(id uint) error { + result := r.db.Delete(&models.Format{}, id) + return result.Error +} diff --git a/gateway/openapi.yaml b/gateway/openapi.yaml index 6b2d3aa..7937783 100644 --- a/gateway/openapi.yaml +++ b/gateway/openapi.yaml @@ -657,6 +657,135 @@ paths: description: Genre not found '500': description: Failed to delete genre + /apis/core/1/api/format: + get: + summary: Get all formats for the authenticated user's branch + security: + - bearerAuth: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Format' + '401': + description: Unauthorized + '500': + description: Internal Server Error + /apis/core/1/api/format/new: + post: + summary: Create a new format + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Format' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/Format' + '400': + description: Bad Request or Validation Failed + '401': + description: Unauthorized + '500': + description: Internal Server Error + /apis/core/1/api/format/{id}: + get: + summary: Get a format by ID for the authenticated user's branch + security: + - bearerAuth: [] + parameters: + - in: path + name: id + schema: + type: integer + required: true + description: Format ID + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Format' + '400': + description: Invalid ID + '401': + description: Unauthorized + '403': + description: Forbidden (user does not have access to this format) + '404': + description: Format not found + '500': + description: Internal Server Error + put: + summary: Update an existing format + security: + - bearerAuth: [] + parameters: + - in: path + name: id + schema: + type: integer + required: true + description: Format ID + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Format' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Format' + '400': + description: Bad Request, Invalid ID, or Validation Failed + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: Format not found + '500': + description: Internal Server Error + delete: + summary: Delete a format by ID for the authenticated user's branch + security: + - bearerAuth: [] + parameters: + - in: path + name: id + schema: + type: integer + required: true + description: Format ID + responses: + '204': + description: No Content + '400': + description: Invalid ID + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: Format not found + '500': + description: Internal Server Error components: schemas: @@ -707,3 +836,10 @@ components: properties: name: type: string + Format: + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 255 diff --git a/gateway/router/router.go b/gateway/router/router.go index 29a7c3f..b3732b3 100644 --- a/gateway/router/router.go +++ b/gateway/router/router.go @@ -86,7 +86,7 @@ func Routes() *gin.Engine { apiCoreBranch := apiCore.Group(`/api/branch`) { - apiCoreBranch.Use(func(c *gin.Context) { + apiCoreBranch.Use(func(c *gin.Context) { if !authenticator(c) { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"msg": "Unauthorized"}) return @@ -108,9 +108,9 @@ func Routes() *gin.Engine { }) } - apiCoreCondition := apiCore.Group(`/api/condition`) + apiCoreCondition := apiCore.Group(`/api/condition`) { - apiCoreCondition.Use(func(c *gin.Context) { + apiCoreCondition.Use(func(c *gin.Context) { if !authenticator(c) { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"msg": "Unauthorized"}) return @@ -152,16 +152,48 @@ func Routes() *gin.Engine { apiCoreFormat := apiCore.Group(`/api/format`) { - apiCoreFormat.GET(`/`, handleCoreAPI("/api/format/")) - apiCoreFormat.GET(`/:id`, handleCoreAPIWithId("/api/format")) - apiCoreFormat.POST(`/new`, handleCoreAPI("/api/format/new")) - apiCoreFormat.PUT(`/:id`, handleCoreAPIWithId("/api/format")) - apiCoreFormat.DELETE(`/:id`, handleCoreAPIWithId("/api/format")) + apiCoreFormat.Use(func(c *gin.Context) { + if !authenticator(c) { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"msg": "Unauthorized"}) + return + } + c.Next() + }) + + apiCoreFormat.GET(`/`, RoleMiddleware("ROLE_USER"), func(c *gin.Context) { + fc := controllers.NewFormatController(db) + fc.FindAll(c) + }) + apiCoreFormat.GET(`/:id`, RoleMiddleware("ROLE_USER"), func(c *gin.Context) { + fc := controllers.NewFormatController(db) + fc.FindOne(c) + }) + apiCoreFormat.POST(`/new`, RoleMiddleware("ROLE_ADMIN"), func(c *gin.Context) { + fc := controllers.NewFormatController(db) + fc.Create(c) + }) + apiCoreFormat.PUT(`/:id`, RoleMiddleware("ROLE_ADMIN"), func(c *gin.Context) { + fc := controllers.NewFormatController(db) + fc.Update(c) + }) + apiCoreFormat.DELETE(`/:id`, RoleMiddleware("ROLE_ADMIN"), func(c *gin.Context) { + fc := controllers.NewFormatController(db) + fc.Delete(c) + }) } - apiCoreGenre := apiCore.Group(`/api/genre`) + // apiCoreFormat := apiCore.Group(`/api/format`) + // { + // apiCoreFormat.GET(`/`, handleCoreAPI("/api/format/")) + // apiCoreFormat.GET(`/:id`, handleCoreAPIWithId("/api/format")) + // apiCoreFormat.POST(`/new`, handleCoreAPI("/api/format/new")) + // apiCoreFormat.PUT(`/:id`, handleCoreAPIWithId("/api/format")) + // apiCoreFormat.DELETE(`/:id`, handleCoreAPIWithId("/api/format")) + // } + + apiCoreGenre := apiCore.Group(`/api/genre`) { - apiCoreGenre.Use(func(c *gin.Context) { + apiCoreGenre.Use(func(c *gin.Context) { if !authenticator(c) { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"msg": "Unauthorized"}) return @@ -240,7 +272,7 @@ func Routes() *gin.Engine { apiCoreTag := apiCore.Group(`/api/tag`) { - apiCoreTag.Use(func(c *gin.Context) { + apiCoreTag.Use(func(c *gin.Context) { if !authenticator(c) { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"msg": "Unauthorized"}) return @@ -270,7 +302,7 @@ func Routes() *gin.Engine { }) } - // apiCoreTag := apiCore.Group(`/api/tag`) + // apiCoreTag := apiCore.Group(`/api/tag`) // { // apiCoreTag.GET(`/`, handleCoreAPI("/api/tag/")) // apiCoreTag.GET(`/:id`, handleCoreAPIWithId("/api/tag"))