diff --git a/backend/app/handlers/gitserver.go b/backend/app/handlers/gitserver.go new file mode 100644 index 0000000..04c28d5 --- /dev/null +++ b/backend/app/handlers/gitserver.go @@ -0,0 +1,123 @@ +package handlers + +import ( + "database/sql" + "errors" + "log" + "quay/app/models" + "quay/app/repository" + + "github.com/gofiber/fiber/v3" +) + +type GitServerHandler struct { + Repo repository.GitServerRepository +} + +func NewGitServerHandler(repo repository.GitServerRepository) *GitServerHandler { + return &GitServerHandler{Repo: repo} +} + +func (h *GitServerHandler) GetGitServers(c fiber.Ctx) error { + gs, err := h.Repo.ListGitServers() + if err != nil { + log.Println("Error listing gitservers: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while listing git servers"}) + } + if gs == nil { + gs = []models.GitServer{} + } + return c.JSON(gs) +} + +func (h *GitServerHandler) GetGitServer(c fiber.Ctx) error { + id := c.Params("id") + g, err := h.Repo.GetGitServer(id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Git server not found"}) + } + log.Println("Error getting gitserver: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while getting git server"}) + } + if g == nil { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Git server not found"}) + } + return c.JSON(g) +} + +func validateIncomingGitServer(gs *models.GitServer) error { + return models.ValidateGitServer(gs) +} + +func (h *GitServerHandler) PostGitServer(c fiber.Ctx) error { + var gs models.GitServer + if err := c.Bind().Body(&gs); err != nil { + log.Println("Error parsing body: ", err) + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body"}) + } + + if err := validateIncomingGitServer(&gs); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body: " + err.Error()}) + } + + if err := h.Repo.CreateGitServer(&gs); err != nil { + log.Println("Error creating gitserver: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while creating git server"}) + } + + return c.JSON(gs) +} + +func (h *GitServerHandler) PutGitServer(c fiber.Ctx) error { + id := c.Params("id") + if _, err := h.Repo.GetGitServer(id); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Git server not found"}) + } + log.Println("Error checking gitserver before update: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while updating git server"}) + } + + var gs models.GitServer + if err := c.Bind().Body(&gs); err != nil { + log.Println("Error parsing body: ", err) + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body"}) + } + + if err := validateIncomingGitServer(&gs); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body: " + err.Error()}) + } + + gs.ID = id + if err := h.Repo.UpdateGitServer(&gs); err != nil { + log.Println("Error updating gitserver: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while updating git server"}) + } + + updated, err := h.Repo.GetGitServer(id) + if err != nil { + log.Println("Error getting updated gitserver: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Git server was updated but could not be retrieved"}) + } + + return c.JSON(updated) +} + +func (h *GitServerHandler) DeleteGitServer(c fiber.Ctx) error { + id := c.Params("id") + if _, err := h.Repo.GetGitServer(id); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return c.Status(fiber.StatusNotFound).JSON(&models.APIError{Message: "Git server not found"}) + } + log.Println("Error checking gitserver before delete: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting git server"}) + } + + if err := h.Repo.DeleteGitServer(id); err != nil { + log.Println("Error deleting gitserver: ", err) + return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting git server"}) + } + + return c.SendStatus(fiber.StatusNoContent) +} diff --git a/backend/app/models/gitserver.go b/backend/app/models/gitserver.go new file mode 100644 index 0000000..5117fc7 --- /dev/null +++ b/backend/app/models/gitserver.go @@ -0,0 +1,34 @@ +package models + +import "errors" + +type GitServer struct { + ID string `json:"id"` + Name string `json:"name"` + Protocol string `json:"protocol"` + BaseUrl string `json:"baseUrl"` + Type string `json:"type"` + CreatedAt string `json:"created_at"` +} + +func ValidateGitServer(gs *GitServer) error { + if gs.Name == "" { + return errors.New("name is required") + } + if gs.Protocol == "" { + return errors.New("protocol is required") + } + if gs.Protocol != "http" && gs.Protocol != "https" { + return errors.New("protocol must be either 'http' or 'https'") + } + if gs.BaseUrl == "" { + return errors.New("baseUrl is required") + } + if gs.Type == "" { + return errors.New("type is required") + } + if gs.Type != "github" && gs.Type != "gitlab" && gs.Type != "gitea" { + return errors.New("type must be either 'github', 'gitlab' or 'gitea'") + } + return nil +} diff --git a/backend/app/repository/gitserver_repository.go b/backend/app/repository/gitserver_repository.go new file mode 100644 index 0000000..443d4ce --- /dev/null +++ b/backend/app/repository/gitserver_repository.go @@ -0,0 +1,16 @@ +package repository + +import ( + "errors" + "quay/app/models" +) + +var ErrGitServerAlreadyExists = errors.New("gitserver already exists") + +type GitServerRepository interface { + ListGitServers() ([]models.GitServer, error) + GetGitServer(id string) (*models.GitServer, error) + CreateGitServer(gs *models.GitServer) error + UpdateGitServer(gs *models.GitServer) error + DeleteGitServer(id string) error +} diff --git a/backend/app/routes/routes.go b/backend/app/routes/routes.go index ae7bd68..3150b48 100644 --- a/backend/app/routes/routes.go +++ b/backend/app/routes/routes.go @@ -19,10 +19,27 @@ func Register(app *fiber.App, cfg *config.Config, envCfg *envconfig.EnvConfig, d siteRepository := cachedrepo.NewCachedSiteRepository(database.NewSQLiteSiteRepository(db)) deploymentRepository := database.NewSQLiteDeploymentRepository(db) userRepository := database.NewSQLiteUserRepository(db) + gitServerRepository := database.NewSQLiteGitServerRepository(db) + + // Seed default git servers if none exist + if gsList, err := gitServerRepository.ListGitServers(); err != nil { + log.Printf("Warning checking gitservers: %v", err) + } else if len(gsList) == 0 { + defaults := []models.GitServer{ + {Name: "GitHub", Protocol: "https", BaseUrl: "github.com", Type: "github"}, + {Name: "GitLab", Protocol: "https", BaseUrl: "gitlab.com", Type: "gitlab"}, + } + for _, d := range defaults { + if err := gitServerRepository.CreateGitServer(&d); err != nil { + log.Printf("Warning creating default gitserver %s: %v", d.Name, err) + } + } + } siteHandler := handlers.NewSiteHandler(siteRepository) deploySiteHandler := handlers.NewDeploySiteHandler(envCfg, siteRepository, deploymentRepository) userHandler := handlers.NewUserHandler(userRepository) + gitServerHandler := handlers.NewGitServerHandler(gitServerRepository) deploymentsHandler := handlers.NewDeploymentHandler(deploymentRepository) api := app.Group("/api/v1", middleware.APIHostGuard(envCfg.DashboardHost)) @@ -46,6 +63,13 @@ func Register(app *fiber.App, cfg *config.Config, envCfg *envconfig.EnvConfig, d protected.Delete("/sites/:id", siteHandler.DeleteSite) protected.Patch("/sites/:id/enabled", siteHandler.ToggleEnabled) + // Git servers + protected.Get("/gitservers", gitServerHandler.GetGitServers) + protected.Get("/gitservers/:id", gitServerHandler.GetGitServer) + protected.Post("/gitservers", gitServerHandler.PostGitServer) + protected.Put("/gitservers/:id", gitServerHandler.PutGitServer) + protected.Delete("/gitservers/:id", gitServerHandler.DeleteGitServer) + // Forward rules protected.Get("/sites/:id/forward-rules", siteHandler.GetSiteForwardRules) protected.Post("/sites/:id/forward-rules", siteHandler.PostForwardRule) diff --git a/backend/internal/database/gitserver_sqlite.go b/backend/internal/database/gitserver_sqlite.go new file mode 100644 index 0000000..a844b8c --- /dev/null +++ b/backend/internal/database/gitserver_sqlite.go @@ -0,0 +1,104 @@ +package database + +import ( + "database/sql" + "fmt" + "quay/app/models" + "quay/app/repository" + + "github.com/google/uuid" +) + +type SQLiteGitServerRepository struct { + db *sql.DB +} + +func NewSQLiteGitServerRepository(db *sql.DB) *SQLiteGitServerRepository { + return &SQLiteGitServerRepository{db: db} +} + +var _ repository.GitServerRepository = (*SQLiteGitServerRepository)(nil) + +func (r *SQLiteGitServerRepository) ListGitServers() ([]models.GitServer, error) { + rows, err := r.db.Query(`SELECT id, name, protocol, base_url, type, created_at FROM gitservers`) + if err != nil { + return nil, fmt.Errorf("list gitservers: %w", err) + } + + var out []models.GitServer + for rows.Next() { + gs, err := scanGitServer(rows) + if err != nil { + rows.Close() + return nil, fmt.Errorf("list gitservers scan: %w", err) + } + out = append(out, *gs) + } + rows.Close() + if err := rows.Err(); err != nil { + return nil, err + } + return out, nil +} + +func (r *SQLiteGitServerRepository) GetGitServer(id string) (*models.GitServer, error) { + row := r.db.QueryRow(`SELECT id, name, protocol, base_url, type, created_at FROM gitservers WHERE id = ?`, id) + return scanGitServer(row) +} + +func (r *SQLiteGitServerRepository) CreateGitServer(gs *models.GitServer) error { + tx, err := r.db.Begin() + if err != nil { + return fmt.Errorf("create gitserver begin tx: %w", err) + } + defer tx.Rollback() + + gs.ID = uuid.NewString() + + _, err = tx.Exec(`INSERT INTO gitservers (id, name, protocol, base_url, type) VALUES (?, ?, ?, ?, ?)`, + gs.ID, gs.Name, gs.Protocol, gs.BaseUrl, gs.Type) + if err != nil { + if isSQLiteUniqueConstraintError(err) { + return fmt.Errorf("create gitserver insert: %w", repository.ErrGitServerAlreadyExists) + } + return fmt.Errorf("create gitserver insert: %w", err) + } + + return tx.Commit() +} + +func (r *SQLiteGitServerRepository) UpdateGitServer(gs *models.GitServer) error { + tx, err := r.db.Begin() + if err != nil { + return fmt.Errorf("update gitserver begin tx: %w", err) + } + defer tx.Rollback() + + _, err = tx.Exec(`UPDATE gitservers SET name = ?, protocol = ?, base_url = ?, type = ? WHERE id = ?`, + gs.Name, gs.Protocol, gs.BaseUrl, gs.Type, gs.ID) + if err != nil { + if isSQLiteUniqueConstraintError(err) { + return fmt.Errorf("update gitserver: %w", repository.ErrGitServerAlreadyExists) + } + return fmt.Errorf("update gitserver: %w", err) + } + + return tx.Commit() +} + +func (r *SQLiteGitServerRepository) DeleteGitServer(id string) error { + _, err := r.db.Exec(`DELETE FROM gitservers WHERE id = ?`, id) + if err != nil { + return fmt.Errorf("delete gitserver: %w", err) + } + return nil +} + +func scanGitServer(s scanner) (*models.GitServer, error) { + g := new(models.GitServer) + err := s.Scan(&g.ID, &g.Name, &g.Protocol, &g.BaseUrl, &g.Type, &g.CreatedAt) + if err != nil { + return nil, err + } + return g, nil +} diff --git a/backend/internal/database/init_sqlite.go b/backend/internal/database/init_sqlite.go index 4c9e8cd..cd407ca 100644 --- a/backend/internal/database/init_sqlite.go +++ b/backend/internal/database/init_sqlite.go @@ -68,6 +68,16 @@ CREATE TABLE IF NOT EXISTS users ( hashed_password TEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ) +; + +CREATE TABLE IF NOT EXISTS gitservers ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + protocol TEXT NOT NULL, + base_url TEXT NOT NULL, + type TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); `) if err == nil {