diff --git a/app/handlers/site_relations.go b/app/handlers/site_relations.go new file mode 100644 index 0000000..02f0f46 --- /dev/null +++ b/app/handlers/site_relations.go @@ -0,0 +1,408 @@ +package handlers + +import ( + "database/sql" + "errors" + "log" + "quay/app/models" + + "github.com/gofiber/fiber/v3" +) + +func validateForwardRule(rule *models.ForwardRule) error { + if rule == nil { + return errors.New("forward rule is required") + } + if rule.Source == "" { + return errors.New("forward rule source is required") + } + if rule.Destination == "" { + return errors.New("forward rule destination is required") + } + if rule.StatusCode < 300 || rule.StatusCode > 399 { + return errors.New("forward rule status code must be between 300 and 399") + } + return nil +} + +func validateHeader(header *models.Header) error { + if header == nil { + return errors.New("header is required") + } + if header.Key == "" { + return errors.New("header key is required") + } + if header.Value == "" { + return errors.New("header value is required") + } + return nil +} + +func validateCustomHeaders(customHeaders *models.CustomHeaders) error { + if customHeaders == nil { + return errors.New("custom headers are required") + } + if customHeaders.Source == "" { + return errors.New("custom header source required") + } + if customHeaders.Headers == nil { + customHeaders.Headers = []models.Header{} + } + for i := range customHeaders.Headers { + if err := validateHeader(&customHeaders.Headers[i]); err != nil { + return err + } + } + return nil +} + +func (h *SiteHandler) GetSiteForwardRules(c fiber.Ctx) error { + siteID := c.Params("id") + + site, err := h.Repo.GetSite(siteID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Site not found"}) + } + log.Println("Error getting site forward rules: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while listing forward rules"}) + } + + if site.ForwardRules == nil { + site.ForwardRules = []models.ForwardRule{} + } + return c.JSON(site.ForwardRules) +} + +func (h *SiteHandler) GetForwardRule(c fiber.Ctx) error { + id := c.Params("id") + + rule, err := h.Repo.GetForwardRule(id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Forward rule not found"}) + } + log.Println("Error getting forward rule: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while getting forward rule"}) + } + + return c.JSON(rule) +} + +func (h *SiteHandler) PostForwardRule(c fiber.Ctx) error { + siteID := c.Params("id") + + if _, err := h.Repo.GetSite(siteID); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Site not found"}) + } + log.Println("Error checking site before creating forward rule: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while creating forward rule"}) + } + + var rule models.ForwardRule + if err := c.Bind().Body(&rule); err != nil { + log.Println("Error parsing body: ", err) + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body"}) + } + + if err := validateForwardRule(&rule); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body: " + err.Error()}) + } + + if err := h.Repo.CreateForwardRule(siteID, &rule); err != nil { + log.Println("Error creating forward rule: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while creating forward rule"}) + } + + return c.JSON(rule) +} + +func (h *SiteHandler) PutForwardRule(c fiber.Ctx) error { + id := c.Params("id") + + if _, err := h.Repo.GetForwardRule(id); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Forward rule not found"}) + } + log.Println("Error checking forward rule before update: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while updating forward rule"}) + } + + var rule models.ForwardRule + if err := c.Bind().Body(&rule); err != nil { + log.Println("Error parsing body: ", err) + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body"}) + } + + if err := validateForwardRule(&rule); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body: " + err.Error()}) + } + + rule.ID = id + if err := h.Repo.UpdateForwardRule(&rule); err != nil { + log.Println("Error updating forward rule: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while updating forward rule"}) + } + + updated, err := h.Repo.GetForwardRule(id) + if err != nil { + log.Println("Error getting updated forward rule: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Forward rule was updated but could not be retrieved"}) + } + + return c.JSON(updated) +} + +func (h *SiteHandler) DeleteForwardRule(c fiber.Ctx) error { + id := c.Params("id") + + if _, err := h.Repo.GetForwardRule(id); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Forward rule not found"}) + } + log.Println("Error checking forward rule before delete: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting forward rule"}) + } + + if err := h.Repo.DeleteForwardRule(id); err != nil { + log.Println("Error deleting forward rule: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting forward rule"}) + } + + return c.SendStatus(fiber.StatusNoContent) +} + +func (h *SiteHandler) GetSiteCustomHeaders(c fiber.Ctx) error { + siteID := c.Params("id") + + site, err := h.Repo.GetSite(siteID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Site not found"}) + } + log.Println("Error getting site custom headers: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while listing custom headers"}) + } + + if site.CustomHeaders == nil { + site.CustomHeaders = []models.CustomHeaders{} + } + return c.JSON(site.CustomHeaders) +} + +func (h *SiteHandler) GetCustomHeaders(c fiber.Ctx) error { + id := c.Params("id") + + customHeaders, err := h.Repo.GetCustomHeaders(id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Custom headers not found"}) + } + log.Println("Error getting custom headers: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while getting custom headers"}) + } + + return c.JSON(customHeaders) +} + +func (h *SiteHandler) PostCustomHeaders(c fiber.Ctx) error { + siteID := c.Params("id") + + if _, err := h.Repo.GetSite(siteID); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Site not found"}) + } + log.Println("Error checking site before creating custom headers: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while creating custom headers"}) + } + + var customHeaders models.CustomHeaders + if err := c.Bind().Body(&customHeaders); err != nil { + log.Println("Error parsing body: ", err) + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body"}) + } + + if err := validateCustomHeaders(&customHeaders); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body: " + err.Error()}) + } + + if err := h.Repo.CreateCustomHeaders(siteID, &customHeaders); err != nil { + log.Println("Error creating custom headers: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while creating custom headers"}) + } + + return c.JSON(customHeaders) +} + +func (h *SiteHandler) PutCustomHeaders(c fiber.Ctx) error { + id := c.Params("id") + + if _, err := h.Repo.GetCustomHeaders(id); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Custom headers not found"}) + } + log.Println("Error checking custom headers before update: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while updating custom headers"}) + } + + var customHeaders models.CustomHeaders + if err := c.Bind().Body(&customHeaders); err != nil { + log.Println("Error parsing body: ", err) + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body"}) + } + + if err := validateCustomHeaders(&customHeaders); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body: " + err.Error()}) + } + + customHeaders.ID = id + if err := h.Repo.UpdateCustomHeaders(&customHeaders); err != nil { + log.Println("Error updating custom headers: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while updating custom headers"}) + } + + updated, err := h.Repo.GetCustomHeaders(id) + if err != nil { + log.Println("Error getting updated custom headers: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Custom headers were updated but could not be retrieved"}) + } + + return c.JSON(updated) +} + +func (h *SiteHandler) DeleteCustomHeaders(c fiber.Ctx) error { + id := c.Params("id") + + if _, err := h.Repo.GetCustomHeaders(id); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Custom headers not found"}) + } + log.Println("Error checking custom headers before delete: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting custom headers"}) + } + + if err := h.Repo.DeleteCustomHeaders(id); err != nil { + log.Println("Error deleting custom headers: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting custom headers"}) + } + + return c.SendStatus(fiber.StatusNoContent) +} + +func (h *SiteHandler) GetCustomHeaderHeaders(c fiber.Ctx) error { + customHeaderID := c.Params("id") + + customHeaders, err := h.Repo.GetCustomHeaders(customHeaderID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Custom headers not found"}) + } + log.Println("Error listing headers: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while listing headers"}) + } + + if customHeaders.Headers == nil { + customHeaders.Headers = []models.Header{} + } + return c.JSON(customHeaders.Headers) +} + +func (h *SiteHandler) GetHeader(c fiber.Ctx) error { + id := c.Params("id") + + header, err := h.Repo.GetHeader(id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Header not found"}) + } + log.Println("Error getting header: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while getting header"}) + } + + return c.JSON(header) +} + +func (h *SiteHandler) PostHeader(c fiber.Ctx) error { + customHeaderID := c.Params("id") + + if _, err := h.Repo.GetCustomHeaders(customHeaderID); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Custom headers not found"}) + } + log.Println("Error checking custom headers before creating header: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while creating header"}) + } + + var header models.Header + if err := c.Bind().Body(&header); err != nil { + log.Println("Error parsing body: ", err) + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body"}) + } + + if err := validateHeader(&header); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body: " + err.Error()}) + } + + if err := h.Repo.CreateHeader(customHeaderID, &header); err != nil { + log.Println("Error creating header: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while creating header"}) + } + + return c.JSON(header) +} + +func (h *SiteHandler) PutHeader(c fiber.Ctx) error { + id := c.Params("id") + + if _, err := h.Repo.GetHeader(id); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Header not found"}) + } + log.Println("Error checking header before update: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while updating header"}) + } + + var header models.Header + if err := c.Bind().Body(&header); err != nil { + log.Println("Error parsing body: ", err) + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body"}) + } + + if err := validateHeader(&header); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body: " + err.Error()}) + } + + header.ID = id + if err := h.Repo.UpdateHeader(&header); err != nil { + log.Println("Error updating header: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while updating header"}) + } + + updated, err := h.Repo.GetHeader(id) + if err != nil { + log.Println("Error getting updated header: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Header was updated but could not be retrieved"}) + } + + return c.JSON(updated) +} + +func (h *SiteHandler) DeleteHeader(c fiber.Ctx) error { + id := c.Params("id") + + if _, err := h.Repo.GetHeader(id); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Header not found"}) + } + log.Println("Error checking header before delete: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting header"}) + } + + if err := h.Repo.DeleteHeader(id); err != nil { + log.Println("Error deleting header: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting header"}) + } + + return c.SendStatus(fiber.StatusNoContent) +} diff --git a/app/repository/site_repository.go b/app/repository/site_repository.go index 6687309..d20554f 100644 --- a/app/repository/site_repository.go +++ b/app/repository/site_repository.go @@ -16,4 +16,8 @@ type SiteRepository interface { CreateCustomHeaders(siteID string, ch *models.CustomHeaders) error UpdateCustomHeaders(ch *models.CustomHeaders) error DeleteCustomHeaders(id string) error + GetHeader(id string) (*models.Header, error) + CreateHeader(customHeaderID string, h *models.Header) error + UpdateHeader(h *models.Header) error + DeleteHeader(id string) error } diff --git a/app/routes/routes.go b/app/routes/routes.go index ebf1279..480bfac 100644 --- a/app/routes/routes.go +++ b/app/routes/routes.go @@ -29,6 +29,24 @@ func Register(app *fiber.App, cfg *config.Config, envCfg *envconfig.EnvConfig, d api.Put("/sites/:id", siteHandler.PutSite) api.Delete("/sites/:id", siteHandler.DeleteSite) + api.Get("/sites/:id/forward-rules", siteHandler.GetSiteForwardRules) + api.Post("/sites/:id/forward-rules", siteHandler.PostForwardRule) + api.Get("/forward-rules/:id", siteHandler.GetForwardRule) + api.Put("/forward-rules/:id", siteHandler.PutForwardRule) + api.Delete("/forward-rules/:id", siteHandler.DeleteForwardRule) + + api.Get("/sites/:id/custom-headers", siteHandler.GetSiteCustomHeaders) + api.Post("/sites/:id/custom-headers", siteHandler.PostCustomHeaders) + api.Get("/custom-headers/:id", siteHandler.GetCustomHeaders) + api.Put("/custom-headers/:id", siteHandler.PutCustomHeaders) + api.Delete("/custom-headers/:id", siteHandler.DeleteCustomHeaders) + + api.Get("/custom-headers/:id/headers", siteHandler.GetCustomHeaderHeaders) + api.Post("/custom-headers/:id/headers", siteHandler.PostHeader) + api.Get("/headers/:id", siteHandler.GetHeader) + api.Put("/headers/:id", siteHandler.PutHeader) + api.Delete("/headers/:id", siteHandler.DeleteHeader) + storagePath, err := filepath.Abs(envCfg.StoragePath) if err != nil { log.Fatalf("Failed to resolve storage path: %v", err) diff --git a/internal/database/site_sqlite.go b/internal/database/site_sqlite.go index 684dd93..d4e5667 100644 --- a/internal/database/site_sqlite.go +++ b/internal/database/site_sqlite.go @@ -261,6 +261,46 @@ func (r *SQLiteSiteRepository) DeleteCustomHeaders(id string) error { return nil } +// Headers + +func (r *SQLiteSiteRepository) GetHeader(id string) (*models.Header, error) { + row := r.db.QueryRow(`SELECT id, key, value FROM headers WHERE id = ?`, id) + + var h models.Header + if err := row.Scan(&h.ID, &h.Key, &h.Value); err != nil { + return nil, fmt.Errorf("get header: %w", err) + } + return &h, nil +} + +func (r *SQLiteSiteRepository) CreateHeader(customHeaderID string, h *models.Header) error { + h.ID = uuid.NewString() + _, err := r.db.Exec( + `INSERT INTO headers (id, custom_header_id, key, value) VALUES (?, ?, ?, ?)`, + h.ID, customHeaderID, h.Key, h.Value, + ) + if err != nil { + return fmt.Errorf("create header: %w", err) + } + return nil +} + +func (r *SQLiteSiteRepository) UpdateHeader(h *models.Header) error { + _, err := r.db.Exec(`UPDATE headers SET key=?, value=? WHERE id=?`, h.Key, h.Value, h.ID) + if err != nil { + return fmt.Errorf("update header: %w", err) + } + return nil +} + +func (r *SQLiteSiteRepository) DeleteHeader(id string) error { + _, err := r.db.Exec(`DELETE FROM headers WHERE id = ?`, id) + if err != nil { + return fmt.Errorf("delete header: %w", err) + } + return nil +} + // Helpers type scanner interface {