Added sqlite database
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"log"
|
||||
"quay/app/models"
|
||||
"quay/app/repository"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
type SiteHandler struct {
|
||||
Repo repository.SiteRepository
|
||||
}
|
||||
|
||||
func NewSiteHandler(repo repository.SiteRepository) *SiteHandler {
|
||||
return &SiteHandler{Repo: repo}
|
||||
}
|
||||
|
||||
// GetSites godoc
|
||||
// @Summary Get all sites
|
||||
// @Description Get a list of all sites
|
||||
// @Tags Sites
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.GetAllSitesResponse
|
||||
// @Failure 500 {object} models.APIError
|
||||
// @Router /sites [get]
|
||||
func (h *SiteHandler) GetSites(c fiber.Ctx) error {
|
||||
sites, err := h.Repo.ListSites()
|
||||
|
||||
if err != nil {
|
||||
log.Println("Error listing sites: ", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{
|
||||
Message: "Unexpected error while listing sites",
|
||||
})
|
||||
}
|
||||
|
||||
if sites == nil {
|
||||
sites = []models.Site{}
|
||||
}
|
||||
|
||||
return c.JSON(models.GetAllSitesResponse{
|
||||
Sites: sites,
|
||||
Total: len(sites),
|
||||
})
|
||||
}
|
||||
|
||||
// GetSite godoc
|
||||
// @Summary Get site by ID
|
||||
// @Description Get a single site by its ID
|
||||
// @Tags Sites
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Site ID"
|
||||
// @Success 200 {object} models.Site
|
||||
// @Failure 404 {object} models.APIError
|
||||
// @Failure 500 {object} models.APIError
|
||||
// @Router /sites/{id} [get]
|
||||
func (h *SiteHandler) GetSite(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
|
||||
site, err := h.Repo.GetSite(id)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(&models.APIError{
|
||||
Message: "Site not found",
|
||||
})
|
||||
}
|
||||
log.Println("Message getting site: ", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{
|
||||
Message: "Unexpected error while getting site",
|
||||
})
|
||||
}
|
||||
|
||||
if site == nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(&models.APIError{
|
||||
Message: "Site not found",
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(site)
|
||||
}
|
||||
|
||||
func validateIncomingSite(site *models.Site) error {
|
||||
if site == nil {
|
||||
return errors.New("site is required")
|
||||
}
|
||||
if site.GitServer == "" {
|
||||
return errors.New("git server required")
|
||||
}
|
||||
if site.Owner == "" {
|
||||
return errors.New("owner required")
|
||||
}
|
||||
if site.Repository == "" {
|
||||
return errors.New("repository required")
|
||||
}
|
||||
if site.Branch == "" {
|
||||
return errors.New("branch required")
|
||||
}
|
||||
if site.Domain == "" {
|
||||
return errors.New("domain required")
|
||||
}
|
||||
if site.NotFoundFile == "" {
|
||||
site.NotFoundFile = "404.html"
|
||||
}
|
||||
if site.ForwardRules == nil {
|
||||
site.ForwardRules = []models.ForwardRule{}
|
||||
}
|
||||
if site.ForwardRules != nil {
|
||||
for _, r := range site.ForwardRules {
|
||||
if r.Source == "" {
|
||||
return errors.New("forward rule source is required")
|
||||
}
|
||||
if r.Destination == "" {
|
||||
return errors.New("forward rule destination is required")
|
||||
}
|
||||
if r.StatusCode < 300 || r.StatusCode > 399 {
|
||||
return errors.New("forward rule status code must be between 300 and 399")
|
||||
}
|
||||
}
|
||||
}
|
||||
if site.CustomHeaders == nil {
|
||||
site.CustomHeaders = []models.CustomHeaders{}
|
||||
}
|
||||
if site.CustomHeaders != nil {
|
||||
for _, h := range site.CustomHeaders {
|
||||
if h.Source == "" {
|
||||
return errors.New("custom header source required")
|
||||
}
|
||||
if h.Headers == nil {
|
||||
return errors.New("custom header is required")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PostSite godoc
|
||||
// @Summary Create a new site
|
||||
// @Description Create a new site with the provided details
|
||||
// @Tags Sites
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param site body models.Site true "Site details"
|
||||
// @Success 200 {object} models.Site
|
||||
// @Failure 400 {object} models.APIError
|
||||
// @Failure 500 {object} models.APIError
|
||||
// @Router /sites [post]
|
||||
func (h *SiteHandler) PostSite(c fiber.Ctx) error {
|
||||
var site models.Site
|
||||
|
||||
if err := c.Bind().Body(&site); err != nil {
|
||||
log.Println("Error parsing body: ", err)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{
|
||||
Message: "Invalid request body",
|
||||
})
|
||||
}
|
||||
|
||||
if err := validateIncomingSite(&site); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{
|
||||
Message: "Invalid request body: " + err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
if err := h.Repo.CreateSite(&site); err != nil {
|
||||
log.Println("Error creating site: ", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{
|
||||
Message: "Unexpected error while creating site",
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(site)
|
||||
}
|
||||
+77
-71
@@ -1,6 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"path/filepath"
|
||||
"quay/app/github"
|
||||
"quay/app/models"
|
||||
@@ -8,78 +9,83 @@ import (
|
||||
"quay/internal/envconfig"
|
||||
"strings"
|
||||
|
||||
"crypto/subtle"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func NewUpdateSiteHandler(cfg *config.Config, envCfg *envconfig.EnvConfig) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
sitename := c.Query("site")
|
||||
if sitename == "" {
|
||||
return c.Status(400).JSON(models.APIError{
|
||||
Error: "Missing 'site' query parameter",
|
||||
})
|
||||
}
|
||||
|
||||
var siteConfig *config.SiteConfig
|
||||
for _, site := range cfg.Sites {
|
||||
if site.Name == sitename {
|
||||
siteConfig = &site
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if siteConfig == nil {
|
||||
return c.Status(404).JSON(models.APIError{
|
||||
Error: "Site not found",
|
||||
})
|
||||
}
|
||||
|
||||
deployToken := siteConfig.DeployToken
|
||||
if deployToken == "" {
|
||||
return c.Status(500).JSON(models.APIError{
|
||||
Error: "Deploy token not configured for this site",
|
||||
})
|
||||
}
|
||||
|
||||
providedToken := c.Get("Authorization")
|
||||
if strings.HasPrefix(providedToken, "Bearer ") {
|
||||
providedToken = strings.TrimPrefix(providedToken, "Bearer ")
|
||||
}
|
||||
if providedToken == "" {
|
||||
return c.Status(401).JSON(models.APIError{
|
||||
Error: "Missing Authorization header",
|
||||
})
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare([]byte(providedToken), []byte(deployToken)) != 1 {
|
||||
return c.Status(403).JSON(models.APIError{
|
||||
Error: "Invalid deploy token",
|
||||
})
|
||||
}
|
||||
|
||||
sitePath := filepath.Join(envCfg.StoragePath, siteConfig.Name)
|
||||
if _, err := filepath.Abs(sitePath); err != nil {
|
||||
return c.Status(500).JSON(models.APIError{
|
||||
Error: "Failed to resolve site path",
|
||||
})
|
||||
}
|
||||
|
||||
err := github.FetchAndDeployBranch(
|
||||
siteConfig.Owner,
|
||||
siteConfig.Repo,
|
||||
siteConfig.Branch,
|
||||
envCfg.GithubPat,
|
||||
sitePath,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(models.APIError{
|
||||
Error: "Failed to deploy site: " + err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.SendStatus(201)
|
||||
}
|
||||
type UpdateSiteHandler struct {
|
||||
Cfg *config.Config
|
||||
EnvCfg *envconfig.EnvConfig
|
||||
}
|
||||
|
||||
func NewUpdateSiteHandler(cfg *config.Config, envCfg *envconfig.EnvConfig) *UpdateSiteHandler {
|
||||
return &UpdateSiteHandler{Cfg: cfg, EnvCfg: envCfg}
|
||||
}
|
||||
|
||||
func (h *UpdateSiteHandler) PostUpdate(c fiber.Ctx) error {
|
||||
sitename := c.Query("site")
|
||||
if sitename == "" {
|
||||
return c.Status(400).JSON(models.APIError{
|
||||
Message: "Missing 'site' query parameter",
|
||||
})
|
||||
}
|
||||
|
||||
var siteConfig *config.SiteConfig
|
||||
for _, site := range h.Cfg.Sites {
|
||||
if site.Name == sitename {
|
||||
siteConfig = &site
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if siteConfig == nil {
|
||||
return c.Status(404).JSON(models.APIError{
|
||||
Message: "Site not found",
|
||||
})
|
||||
}
|
||||
|
||||
deployToken := siteConfig.DeployToken
|
||||
if deployToken == "" {
|
||||
return c.Status(500).JSON(models.APIError{
|
||||
Message: "Deploy token not configured for this site",
|
||||
})
|
||||
}
|
||||
|
||||
providedToken := c.Get("Authorization")
|
||||
if strings.HasPrefix(providedToken, "Bearer ") {
|
||||
providedToken = strings.TrimPrefix(providedToken, "Bearer ")
|
||||
}
|
||||
if providedToken == "" {
|
||||
return c.Status(401).JSON(models.APIError{
|
||||
Message: "Missing Authorization header",
|
||||
})
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare([]byte(providedToken), []byte(deployToken)) != 1 {
|
||||
return c.Status(403).JSON(models.APIError{
|
||||
Message: "Invalid deploy token",
|
||||
})
|
||||
}
|
||||
|
||||
sitePath := filepath.Join(h.EnvCfg.StoragePath, siteConfig.Name)
|
||||
if _, err := filepath.Abs(sitePath); err != nil {
|
||||
return c.Status(500).JSON(models.APIError{
|
||||
Message: "Failed to resolve site path",
|
||||
})
|
||||
}
|
||||
|
||||
err := github.FetchAndDeployBranch(
|
||||
siteConfig.Owner,
|
||||
siteConfig.Repo,
|
||||
siteConfig.Branch,
|
||||
h.EnvCfg.GithubPat,
|
||||
sitePath,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(models.APIError{
|
||||
Message: "Failed to deploy site: " + err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.SendStatus(201)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package models
|
||||
|
||||
type APIError struct {
|
||||
Error string `json:"error"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package models
|
||||
|
||||
type ForwardRule struct {
|
||||
ID string `json:"id"`
|
||||
Source string `json:"source"`
|
||||
Destination string `json:"destination"`
|
||||
StatusCode int `json:"status_code"`
|
||||
Regex bool `json:"regex"`
|
||||
}
|
||||
|
||||
type Header struct {
|
||||
ID string `json:"id"`
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type CustomHeaders struct {
|
||||
ID string `json:"id"`
|
||||
Source string `json:"source"`
|
||||
Headers []Header `json:"headers"`
|
||||
}
|
||||
|
||||
type Site struct {
|
||||
ID string `json:"id"`
|
||||
GitServer string `json:"git_server"`
|
||||
Owner string `json:"owner"`
|
||||
Repository string `json:"repository"`
|
||||
Branch string `json:"branch"`
|
||||
Domain string `json:"domain"`
|
||||
DeployToken string `json:"deploy_token"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Spa bool `json:"spa"`
|
||||
NotFoundFile string `json:"not_found_file"`
|
||||
ForwardRules []ForwardRule `json:"forward_rules"`
|
||||
CustomHeaders []CustomHeaders `json:"custom_headers"`
|
||||
}
|
||||
|
||||
type GetAllSitesResponse struct {
|
||||
Sites []Site `json:"sites"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package repository
|
||||
|
||||
import "quay/app/models"
|
||||
|
||||
type SiteRepository interface {
|
||||
GetSite(id string) (*models.Site, error)
|
||||
ListSites() ([]models.Site, error)
|
||||
CreateSite(s *models.Site) error
|
||||
UpdateSite(s *models.Site) error
|
||||
DeleteSite(id string) error
|
||||
GetForwardRule(id string) (*models.ForwardRule, error)
|
||||
CreateForwardRule(siteID string, fr *models.ForwardRule) error
|
||||
UpdateForwardRule(fr *models.ForwardRule) error
|
||||
DeleteForwardRule(id string) error
|
||||
GetCustomHeaders(id string) (*models.CustomHeaders, error)
|
||||
CreateCustomHeaders(siteID string, ch *models.CustomHeaders) error
|
||||
UpdateCustomHeaders(ch *models.CustomHeaders) error
|
||||
DeleteCustomHeaders(id string) error
|
||||
}
|
||||
+13
-2
@@ -1,20 +1,31 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"quay/app/handlers"
|
||||
"quay/internal/config"
|
||||
"quay/internal/database"
|
||||
"quay/internal/envconfig"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func Register(app *fiber.App, cfg *config.Config, envCfg *envconfig.EnvConfig) {
|
||||
func Register(app *fiber.App, cfg *config.Config, envCfg *envconfig.EnvConfig, db *sql.DB) {
|
||||
siteRepository := database.NewSQLiteSiteRepository(db)
|
||||
|
||||
updateSiteHandler := handlers.NewUpdateSiteHandler(cfg, envCfg)
|
||||
siteHandler := handlers.NewSiteHandler(siteRepository)
|
||||
|
||||
api := app.Group("/api")
|
||||
api.Get("/health", handlers.HealthCheck)
|
||||
|
||||
api.Post("/update", handlers.NewUpdateSiteHandler(cfg, envCfg))
|
||||
api.Post("/update", updateSiteHandler.PostUpdate)
|
||||
|
||||
api.Get("/sites", siteHandler.GetSites)
|
||||
api.Get("/sites/:id", siteHandler.GetSite)
|
||||
api.Post("/sites", siteHandler.PostSite)
|
||||
|
||||
storagePath, err := filepath.Abs(envCfg.StoragePath)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user