Added frontend
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
.env
|
||||
config/config.yaml
|
||||
storage
|
||||
quay
|
||||
config
|
||||
@@ -0,0 +1,309 @@
|
||||
package cachedrepo
|
||||
|
||||
import (
|
||||
"quay/app/models"
|
||||
"quay/app/repository"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type CachedSiteRepository struct {
|
||||
inner repository.SiteRepository
|
||||
mu sync.RWMutex
|
||||
|
||||
sites map[string]*models.Site // id -> site
|
||||
siteList []models.Site // cached ListSites result
|
||||
siteListValid bool
|
||||
|
||||
forwardRules map[string]*models.ForwardRule // id -> rule
|
||||
customHeaders map[string]*models.CustomHeaders // id -> custom headers
|
||||
headers map[string]*models.Header // id -> header
|
||||
}
|
||||
|
||||
func NewCachedSiteRepository(inner repository.SiteRepository) *CachedSiteRepository {
|
||||
return &CachedSiteRepository{
|
||||
inner: inner,
|
||||
sites: make(map[string]*models.Site),
|
||||
forwardRules: make(map[string]*models.ForwardRule),
|
||||
customHeaders: make(map[string]*models.CustomHeaders),
|
||||
headers: make(map[string]*models.Header),
|
||||
}
|
||||
}
|
||||
|
||||
var _ repository.SiteRepository = (*CachedSiteRepository)(nil)
|
||||
|
||||
// Sites
|
||||
|
||||
func (c *CachedSiteRepository) GetSite(id string) (*models.Site, error) {
|
||||
c.mu.RLock()
|
||||
if s, ok := c.sites[id]; ok {
|
||||
c.mu.RUnlock()
|
||||
return s, nil
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
s, err := c.inner.GetSite(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.sites[id] = s
|
||||
c.mu.Unlock()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (c *CachedSiteRepository) GetSiteByDomain(domain string) (*models.Site, error) {
|
||||
c.mu.RLock()
|
||||
if s, ok := c.sites[domain]; ok {
|
||||
c.mu.RUnlock()
|
||||
return s, nil
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
s, err := c.inner.GetSiteByDomain(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.sites[domain] = s
|
||||
c.mu.Unlock()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (c *CachedSiteRepository) ListSites() ([]models.Site, error) {
|
||||
c.mu.RLock()
|
||||
if c.siteListValid {
|
||||
cp := make([]models.Site, len(c.siteList))
|
||||
copy(cp, c.siteList)
|
||||
c.mu.RUnlock()
|
||||
return cp, nil
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
sites, err := c.inner.ListSites()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.siteList = sites
|
||||
c.siteListValid = true
|
||||
for i := range sites {
|
||||
s := sites[i]
|
||||
c.sites[s.ID] = &s
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return sites, nil
|
||||
}
|
||||
|
||||
func (c *CachedSiteRepository) CreateSite(s *models.Site) error {
|
||||
if err := c.inner.CreateSite(s); err != nil {
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.sites[s.ID] = s
|
||||
c.siteListValid = false
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CachedSiteRepository) UpdateSite(s *models.Site) error {
|
||||
if err := c.inner.UpdateSite(s); err != nil {
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.sites[s.ID] = s
|
||||
c.siteListValid = false
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CachedSiteRepository) DeleteSite(id string) error {
|
||||
if err := c.inner.DeleteSite(id); err != nil {
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
delete(c.sites, id)
|
||||
c.siteListValid = false
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Forward Rules
|
||||
|
||||
func (c *CachedSiteRepository) GetForwardRule(id string) (*models.ForwardRule, error) {
|
||||
c.mu.RLock()
|
||||
if fr, ok := c.forwardRules[id]; ok {
|
||||
c.mu.RUnlock()
|
||||
return fr, nil
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
fr, err := c.inner.GetForwardRule(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.forwardRules[id] = fr
|
||||
c.mu.Unlock()
|
||||
return fr, nil
|
||||
}
|
||||
|
||||
func (c *CachedSiteRepository) CreateForwardRule(siteID string, fr *models.ForwardRule) error {
|
||||
if err := c.inner.CreateForwardRule(siteID, fr); err != nil {
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.forwardRules[fr.ID] = fr
|
||||
delete(c.sites, siteID) // site's embedded rules are now stale
|
||||
c.siteListValid = false
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CachedSiteRepository) UpdateForwardRule(fr *models.ForwardRule) error {
|
||||
if err := c.inner.UpdateForwardRule(fr); err != nil {
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.forwardRules[fr.ID] = fr
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CachedSiteRepository) DeleteForwardRule(id string) error {
|
||||
if err := c.inner.DeleteForwardRule(id); err != nil {
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
delete(c.forwardRules, id)
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Custom Headers
|
||||
|
||||
func (c *CachedSiteRepository) GetCustomHeaders(id string) (*models.CustomHeaders, error) {
|
||||
c.mu.RLock()
|
||||
if ch, ok := c.customHeaders[id]; ok {
|
||||
c.mu.RUnlock()
|
||||
return ch, nil
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
ch, err := c.inner.GetCustomHeaders(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.customHeaders[id] = ch
|
||||
c.mu.Unlock()
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func (c *CachedSiteRepository) CreateCustomHeaders(siteID string, ch *models.CustomHeaders) error {
|
||||
if err := c.inner.CreateCustomHeaders(siteID, ch); err != nil {
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.customHeaders[ch.ID] = ch
|
||||
delete(c.sites, siteID)
|
||||
c.siteListValid = false
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CachedSiteRepository) UpdateCustomHeaders(ch *models.CustomHeaders) error {
|
||||
if err := c.inner.UpdateCustomHeaders(ch); err != nil {
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.customHeaders[ch.ID] = ch
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CachedSiteRepository) DeleteCustomHeaders(id string) error {
|
||||
if err := c.inner.DeleteCustomHeaders(id); err != nil {
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
delete(c.customHeaders, id)
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Headers
|
||||
|
||||
func (c *CachedSiteRepository) GetHeader(id string) (*models.Header, error) {
|
||||
c.mu.RLock()
|
||||
if h, ok := c.headers[id]; ok {
|
||||
c.mu.RUnlock()
|
||||
return h, nil
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
h, err := c.inner.GetHeader(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.headers[id] = h
|
||||
c.mu.Unlock()
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (c *CachedSiteRepository) CreateHeader(customHeaderID string, h *models.Header) error {
|
||||
if err := c.inner.CreateHeader(customHeaderID, h); err != nil {
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.headers[h.ID] = h
|
||||
delete(c.customHeaders, customHeaderID)
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CachedSiteRepository) UpdateHeader(h *models.Header) error {
|
||||
if err := c.inner.UpdateHeader(h); err != nil {
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.headers[h.ID] = h
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CachedSiteRepository) DeleteHeader(id string) error {
|
||||
if err := c.inner.DeleteHeader(id); err != nil {
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
delete(c.headers, id)
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Invalidate explicitly evicts all cached data
|
||||
func (c *CachedSiteRepository) Invalidate() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.sites = make(map[string]*models.Site)
|
||||
c.siteList = nil
|
||||
c.siteListValid = false
|
||||
c.forwardRules = make(map[string]*models.ForwardRule)
|
||||
c.customHeaders = make(map[string]*models.CustomHeaders)
|
||||
c.headers = make(map[string]*models.Header)
|
||||
}
|
||||
|
||||
// InvalidateSite evicts a single site and marks the list stale.
|
||||
func (c *CachedSiteRepository) InvalidateSite(id string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
delete(c.sites, id)
|
||||
c.siteListValid = false
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func FetchAndDeployBranch(repoOwner, repoName, branch, pat, destDir string) error {
|
||||
archiveURL := fmt.Sprintf(
|
||||
"https://api.github.com/repos/%s/%s/zipball/%s",
|
||||
repoOwner, repoName, branch,
|
||||
)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, archiveURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+pat)
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetching archive: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("GitHub returned %s for %s", resp.Status, archiveURL)
|
||||
}
|
||||
|
||||
storageRoot := filepath.Dir(destDir)
|
||||
|
||||
tmpZip, err := os.CreateTemp(storageRoot, "ghbranch-*.zip")
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating temp zip: %w", err)
|
||||
}
|
||||
defer os.Remove(tmpZip.Name())
|
||||
|
||||
if _, err = io.Copy(tmpZip, resp.Body); err != nil {
|
||||
tmpZip.Close()
|
||||
return fmt.Errorf("writing zip: %w", err)
|
||||
}
|
||||
tmpZip.Close()
|
||||
|
||||
tmpDir, err := os.MkdirTemp(storageRoot, "ghbranch-unpack-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating temp dir: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
if err = unzip(tmpZip.Name(), tmpDir); err != nil {
|
||||
return fmt.Errorf("unzipping: %w", err)
|
||||
}
|
||||
|
||||
// github wraps everything in one top-level folder ({owner}-{repo}-{sha}/).
|
||||
entries, err := os.ReadDir(tmpDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading temp dir: %w", err)
|
||||
}
|
||||
if len(entries) != 1 || !entries[0].IsDir() {
|
||||
return fmt.Errorf("unexpected archive layout: expected a single root directory")
|
||||
}
|
||||
extractedRoot := filepath.Join(tmpDir, entries[0].Name())
|
||||
|
||||
if err = os.MkdirAll(filepath.Dir(destDir), 0o755); err != nil {
|
||||
return fmt.Errorf("creating destination parent dirs: %w", err)
|
||||
}
|
||||
os.RemoveAll(destDir)
|
||||
|
||||
if err = os.Rename(extractedRoot, destDir); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = copyDir(extractedRoot, destDir); err != nil {
|
||||
return fmt.Errorf("cross-device copy to destination: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// unzip extracts the zip at src into the dest directory,
|
||||
// guarding against zip-slip path traversal attacks.
|
||||
func unzip(src, dest string) error {
|
||||
r, err := zip.OpenReader(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
destClean := filepath.Clean(dest) + string(os.PathSeparator)
|
||||
|
||||
for _, f := range r.File {
|
||||
target := filepath.Join(dest, filepath.Clean("/"+f.Name))
|
||||
if !strings.HasPrefix(target, destClean) {
|
||||
return fmt.Errorf("zip slip attempt: %q resolves outside destination", f.Name)
|
||||
}
|
||||
|
||||
if f.FileInfo().IsDir() {
|
||||
if err = os.MkdirAll(target, f.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err = os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = writeZipEntry(f, target); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeZipEntry(f *zip.File, target string) error {
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
out, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, rc)
|
||||
return err
|
||||
}
|
||||
|
||||
// copyDir recursively copies src to dst, preserving file modes.
|
||||
// Used as a cross-device fallback when os.Rename fails.
|
||||
func copyDir(src, dst string) error {
|
||||
return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(src, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
target := filepath.Join(dst, rel)
|
||||
|
||||
if d.IsDir() {
|
||||
return os.MkdirAll(target, 0o755)
|
||||
}
|
||||
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return copyFile(path, target, info.Mode())
|
||||
})
|
||||
}
|
||||
|
||||
func copyFile(src, dst string, mode fs.FileMode) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, in)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package handlers
|
||||
|
||||
import "github.com/gofiber/fiber/v3"
|
||||
|
||||
func HealthCheck(c fiber.Ctx) error {
|
||||
return c.Status(200).JSON(fiber.Map{
|
||||
"status": "ok",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"log"
|
||||
"quay/app/models"
|
||||
"quay/app/repository"
|
||||
"quay/internal/security"
|
||||
|
||||
"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.CreateSiteResponse
|
||||
// @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(),
|
||||
})
|
||||
}
|
||||
|
||||
rawDeployToken, hashedDeployToken, err := security.CreateDeployToken()
|
||||
|
||||
if err != nil {
|
||||
log.Println("Error creating deploy token: ", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{
|
||||
Message: "Unexpected error while creating deploy token: " + err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
site.DeployToken = hashedDeployToken
|
||||
|
||||
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(&models.CreateSiteResponse{
|
||||
Site: site,
|
||||
RawDeployToken: rawDeployToken,
|
||||
})
|
||||
}
|
||||
|
||||
// PutSite godoc
|
||||
// @Summary Update an existing site
|
||||
// @Description Update an existing site by its ID
|
||||
// @Tags Sites
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Site ID"
|
||||
// @Param site body models.Site true "Updated site details"
|
||||
// @Success 200 {object} models.Site
|
||||
// @Failure 400 {object} models.APIError
|
||||
// @Failure 404 {object} models.APIError
|
||||
// @Failure 500 {object} models.APIError
|
||||
// @Router /sites/{id} [put]
|
||||
func (h *SiteHandler) PutSite(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
|
||||
if _, err := h.Repo.GetSite(id); 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 update: ", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{
|
||||
Message: "Unexpected error while updating site",
|
||||
})
|
||||
}
|
||||
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
site.ID = id
|
||||
if err := h.Repo.UpdateSite(&site); err != nil {
|
||||
log.Println("Error updating site: ", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{
|
||||
Message: "Unexpected error while updating site",
|
||||
})
|
||||
}
|
||||
|
||||
updatedSite, err := h.Repo.GetSite(id)
|
||||
if err != nil {
|
||||
log.Println("Error getting updated site: ", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{
|
||||
Message: "Site was updated but could not be retrieved",
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(updatedSite)
|
||||
}
|
||||
|
||||
// DeleteSite godoc
|
||||
// @Summary Delete a site
|
||||
// @Description Delete a site by its ID
|
||||
// @Tags Sites
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Site ID"
|
||||
// @Success 204
|
||||
// @Failure 404 {object} models.APIError
|
||||
// @Failure 500 {object} models.APIError
|
||||
// @Router /sites/{id} [delete]
|
||||
func (h *SiteHandler) DeleteSite(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
|
||||
if _, err := h.Repo.GetSite(id); 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 delete: ", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{
|
||||
Message: "Unexpected error while deleting site",
|
||||
})
|
||||
}
|
||||
|
||||
if err := h.Repo.DeleteSite(id); err != nil {
|
||||
log.Println("Error deleting site: ", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{
|
||||
Message: "Unexpected error while deleting site",
|
||||
})
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
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
|
||||
}
|
||||
|
||||
// GetSiteForwardRules godoc
|
||||
//
|
||||
// @Summary List forward rules for a site
|
||||
// @Description Returns all forward rules associated with the given site
|
||||
// @Tags Forward-Rules
|
||||
// @Produce json
|
||||
// @Param id path string true "Site ID"
|
||||
// @Success 200 {array} models.ForwardRule "List of forward rules"
|
||||
// @Failure 404 {object} models.APIError "Site not found"
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /sites/{id}/forward-rules [get]
|
||||
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)
|
||||
}
|
||||
|
||||
// GetForwardRule godoc
|
||||
//
|
||||
// @Summary Get a forward rule
|
||||
// @Description Returns a single forward rule by ID
|
||||
// @Tags Forward-Rules
|
||||
// @Produce json
|
||||
// @Param id path string true "Forward Rule ID"
|
||||
// @Success 200 {object} models.ForwardRule "Forward rule"
|
||||
// @Failure 404 {object} models.APIError "Forward rule not found"
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /forward-rules/{id} [get]
|
||||
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)
|
||||
}
|
||||
|
||||
// PostForwardRule godoc
|
||||
//
|
||||
// @Summary Create a forward rule
|
||||
// @Description Creates a new forward rule for the given site
|
||||
// @Tags Forward-Rules
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Site ID"
|
||||
// @Param rule body models.ForwardRule true "Forward rule to create"
|
||||
// @Success 200 {object} models.ForwardRule "Created forward rule"
|
||||
// @Failure 400 {object} models.APIError "Invalid request body"
|
||||
// @Failure 404 {object} models.APIError "Site not found"
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /sites/{id}/forward-rules [post]
|
||||
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)
|
||||
}
|
||||
|
||||
// PutForwardRule godoc
|
||||
//
|
||||
// @Summary Update a forward rule
|
||||
// @Description Replaces an existing forward rule by ID
|
||||
// @Tags Forward-Rules
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Forward Rule ID"
|
||||
// @Param rule body models.ForwardRule true "Updated forward rule"
|
||||
// @Success 200 {object} models.ForwardRule "Updated forward rule"
|
||||
// @Failure 400 {object} models.APIError "Invalid request body"
|
||||
// @Failure 404 {object} models.APIError "Forward rule not found"
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /forward-rules/{id} [put]
|
||||
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)
|
||||
}
|
||||
|
||||
// DeleteForwardRule godoc
|
||||
//
|
||||
// @Summary Delete a forward rule
|
||||
// @Description Deletes a forward rule by ID
|
||||
// @Tags Forward-Rules
|
||||
// @Produce json
|
||||
// @Param id path string true "Forward Rule ID"
|
||||
// @Success 204 "No content"
|
||||
// @Failure 404 {object} models.APIError "Forward rule not found"
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /forward-rules/{id} [delete]
|
||||
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)
|
||||
}
|
||||
|
||||
// GetSiteCustomHeaders godoc
|
||||
//
|
||||
// @Summary List custom header groups for a site
|
||||
// @Description Returns all custom header groups associated with the given site
|
||||
// @Tags Custom-Headers
|
||||
// @Produce json
|
||||
// @Param id path string true "Site ID"
|
||||
// @Success 200 {array} models.CustomHeaders "List of custom header groups"
|
||||
// @Failure 404 {object} models.APIError "Site not found"
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /sites/{id}/custom-headers [get]
|
||||
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)
|
||||
}
|
||||
|
||||
// GetCustomHeaders godoc
|
||||
//
|
||||
// @Summary Get a custom header group
|
||||
// @Description Returns a single custom header group by ID
|
||||
// @Tags Custom-Headers
|
||||
// @Produce json
|
||||
// @Param id path string true "Custom Headers ID"
|
||||
// @Success 200 {object} models.CustomHeaders "Custom header group"
|
||||
// @Failure 404 {object} models.APIError "Custom headers not found"
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /custom-headers/{id} [get]
|
||||
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)
|
||||
}
|
||||
|
||||
// PostCustomHeaders godoc
|
||||
//
|
||||
// @Summary Create a custom header group
|
||||
// @Description Creates a new custom header group for the given site
|
||||
// @Tags Custom-Headers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Site ID"
|
||||
// @Param customHeaders body models.CustomHeaders true "Custom header group to create"
|
||||
// @Success 200 {object} models.CustomHeaders "Created custom header group"
|
||||
// @Failure 400 {object} models.APIError "Invalid request body"
|
||||
// @Failure 404 {object} models.APIError "Site not found"
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /sites/{id}/custom-headers [post]
|
||||
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)
|
||||
}
|
||||
|
||||
// PutCustomHeaders godoc
|
||||
//
|
||||
// @Summary Update a custom header group
|
||||
// @Description Replaces an existing custom header group by ID
|
||||
// @Tags Custom-Headers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Custom Headers ID"
|
||||
// @Param customHeaders body models.CustomHeaders true "Updated custom header group"
|
||||
// @Success 200 {object} models.CustomHeaders "Updated custom header group"
|
||||
// @Failure 400 {object} models.APIError "Invalid request body"
|
||||
// @Failure 404 {object} models.APIError "Custom headers not found"
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /custom-headers/{id} [put]
|
||||
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)
|
||||
}
|
||||
|
||||
// DeleteCustomHeaders godoc
|
||||
//
|
||||
// @Summary Delete a custom header group
|
||||
// @Description Deletes a custom header group by ID
|
||||
// @Tags Custom-Headers
|
||||
// @Produce json
|
||||
// @Param id path string true "Custom Headers ID"
|
||||
// @Success 204 "No content"
|
||||
// @Failure 404 {object} models.APIError "Custom headers not found"
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /custom-headers/{id} [delete]
|
||||
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)
|
||||
}
|
||||
|
||||
// GetCustomHeaderHeaders godoc
|
||||
//
|
||||
// @Summary List headers in a custom header group
|
||||
// @Description Returns all individual headers belonging to the given custom header group
|
||||
// @Tags Custom-Headers
|
||||
// @Produce json
|
||||
// @Param id path string true "Custom Headers ID"
|
||||
// @Success 200 {array} models.Header "List of headers"
|
||||
// @Failure 404 {object} models.APIError "Custom headers not found"
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /custom-headers/{id}/headers [get]
|
||||
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)
|
||||
}
|
||||
|
||||
// GetHeader godoc
|
||||
//
|
||||
// @Summary Get a header
|
||||
// @Description Returns a single header by ID
|
||||
// @Tags Headers
|
||||
// @Produce json
|
||||
// @Param id path string true "Header ID"
|
||||
// @Success 200 {object} models.Header "Header"
|
||||
// @Failure 404 {object} models.APIError "Header not found"
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /headers/{id} [get]
|
||||
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)
|
||||
}
|
||||
|
||||
// PostHeader godoc
|
||||
//
|
||||
// @Summary Create a header
|
||||
// @Description Creates a new header within the given custom header group
|
||||
// @Tags Headers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Custom Headers ID"
|
||||
// @Param header body models.Header true "Header to create"
|
||||
// @Success 200 {object} models.Header "Created header"
|
||||
// @Failure 400 {object} models.APIError "Invalid request body"
|
||||
// @Failure 404 {object} models.APIError "Custom headers not found"
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /custom-headers/{id}/headers [post]
|
||||
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)
|
||||
}
|
||||
|
||||
// PutHeader godoc
|
||||
//
|
||||
// @Summary Update a header
|
||||
// @Description Replaces an existing header by ID
|
||||
// @Tags Headers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Header ID"
|
||||
// @Param header body models.Header true "Updated header"
|
||||
// @Success 200 {object} models.Header "Updated header"
|
||||
// @Failure 400 {object} models.APIError "Invalid request body"
|
||||
// @Failure 404 {object} models.APIError "Header not found"
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /headers/{id} [put]
|
||||
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)
|
||||
}
|
||||
|
||||
// DeleteHeader godoc
|
||||
//
|
||||
// @Summary Delete a header
|
||||
// @Description Deletes a header by ID
|
||||
// @Tags Headers
|
||||
// @Produce json
|
||||
// @Param id path string true "Header ID"
|
||||
// @Success 204 "No content"
|
||||
// @Failure 404 {object} models.APIError "Header not found"
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /headers/{id} [delete]
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"quay/app/repository"
|
||||
"regexp"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func NewStaticHandler(storagePath string, siteRepo repository.SiteRepository) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
domain := c.Hostname()
|
||||
site, err := siteRepo.GetSiteByDomain(domain)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).SendString("Failed to resolve site")
|
||||
}
|
||||
|
||||
if site == nil {
|
||||
return c.Status(fiber.StatusNotFound).SendString("Site not found")
|
||||
}
|
||||
|
||||
if !site.Enabled {
|
||||
return c.Status(fiber.StatusServiceUnavailable).SendString("Site is currently unavailable")
|
||||
}
|
||||
|
||||
urlPath := filepath.Clean(c.Path())
|
||||
|
||||
for _, rule := range site.ForwardRules {
|
||||
if rule.Regex {
|
||||
re, err := regexp.Compile(rule.Source)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
match := re.FindStringSubmatchIndex(urlPath)
|
||||
if match != nil {
|
||||
dest := re.ExpandString([]byte{}, rule.Destination, urlPath, match)
|
||||
return c.Redirect().Status(rule.StatusCode).To(string(dest))
|
||||
}
|
||||
} else if rule.Source == urlPath {
|
||||
return c.Redirect().Status(rule.StatusCode).To(rule.Destination)
|
||||
}
|
||||
}
|
||||
|
||||
for _, customHeader := range site.CustomHeaders {
|
||||
var matched bool
|
||||
if customHeader.Regex {
|
||||
re, err := regexp.Compile(customHeader.Source)
|
||||
if err == nil {
|
||||
matched = re.MatchString(urlPath)
|
||||
}
|
||||
} else {
|
||||
matched, _ = path.Match(customHeader.Source, urlPath)
|
||||
}
|
||||
if matched {
|
||||
for _, header := range customHeader.Headers {
|
||||
c.Set(header.Key, header.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if urlPath == "/" || urlPath == "." {
|
||||
urlPath = "/index.html"
|
||||
}
|
||||
|
||||
basePath := filepath.Join(storagePath, site.ID)
|
||||
filePath := filepath.Join(basePath, urlPath)
|
||||
|
||||
info, err := os.Stat(filePath)
|
||||
if err == nil {
|
||||
if info.IsDir() {
|
||||
indexPath := filepath.Join(filePath, "index.html")
|
||||
if _, err := os.Stat(indexPath); err == nil {
|
||||
return c.SendFile(indexPath)
|
||||
}
|
||||
} else {
|
||||
return c.SendFile(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
if site.Spa {
|
||||
indexPath := filepath.Join(basePath, "index.html")
|
||||
if _, err := os.Stat(indexPath); err == nil {
|
||||
return c.SendFile(indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
if site.NotFoundFile != "" {
|
||||
notFoundPath := filepath.Join(basePath, site.NotFoundFile)
|
||||
if _, err := os.Stat(notFoundPath); err == nil {
|
||||
return c.SendFile(notFoundPath)
|
||||
}
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusNotFound)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"quay/app/github"
|
||||
"quay/app/models"
|
||||
"quay/app/repository"
|
||||
"quay/internal/envconfig"
|
||||
"quay/internal/security"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
type UpdateSiteHandler struct {
|
||||
EnvCfg *envconfig.EnvConfig
|
||||
SiteRepo repository.SiteRepository
|
||||
}
|
||||
|
||||
func NewUpdateSiteHandler(envCfg *envconfig.EnvConfig, siteRepo repository.SiteRepository) *UpdateSiteHandler {
|
||||
return &UpdateSiteHandler{EnvCfg: envCfg, SiteRepo: siteRepo}
|
||||
}
|
||||
|
||||
func (h *UpdateSiteHandler) PostUpdate(c fiber.Ctx) error {
|
||||
siteId := c.Query("site")
|
||||
if siteId == "" {
|
||||
return c.Status(400).JSON(models.APIError{
|
||||
Message: "Missing 'site' query parameter",
|
||||
})
|
||||
}
|
||||
|
||||
site, err := h.SiteRepo.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("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",
|
||||
})
|
||||
}
|
||||
|
||||
deployToken := site.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 security.CompareDeployTokens(providedToken, deployToken) == false {
|
||||
return c.Status(403).JSON(models.APIError{
|
||||
Message: "Invalid deploy token",
|
||||
})
|
||||
}
|
||||
|
||||
sitePath := filepath.Join(h.EnvCfg.StoragePath, site.ID)
|
||||
if _, err := filepath.Abs(sitePath); err != nil {
|
||||
return c.Status(500).JSON(models.APIError{
|
||||
Message: "Failed to resolve site path",
|
||||
})
|
||||
}
|
||||
|
||||
err = github.FetchAndDeployBranch(
|
||||
site.Owner,
|
||||
site.Repository,
|
||||
site.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)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package middleware
|
||||
|
||||
import "github.com/gofiber/fiber/v3"
|
||||
|
||||
func APIHostGuard(allowedHost string) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
if c.Hostname() != allowedHost {
|
||||
return c.Status(fiber.StatusNotFound).SendString("Not Found")
|
||||
}
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package models
|
||||
|
||||
type APIError struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
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"`
|
||||
Regex bool `json:"regex"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type CreateSiteResponse struct {
|
||||
Site Site `json:"site"`
|
||||
RawDeployToken string `json:"raw_deploy_token"`
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package repository
|
||||
|
||||
import "quay/app/models"
|
||||
|
||||
type SiteRepository interface {
|
||||
GetSite(id string) (*models.Site, error)
|
||||
GetSiteByDomain(domain 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
|
||||
GetHeader(id string) (*models.Header, error)
|
||||
CreateHeader(customHeaderID string, h *models.Header) error
|
||||
UpdateHeader(h *models.Header) error
|
||||
DeleteHeader(id string) error
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"quay/app/cachedrepo"
|
||||
"quay/app/handlers"
|
||||
"quay/app/middleware"
|
||||
"quay/app/models"
|
||||
"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, db *sql.DB) {
|
||||
siteRepository := cachedrepo.NewCachedSiteRepository(database.NewSQLiteSiteRepository(db))
|
||||
|
||||
updateSiteHandler := handlers.NewUpdateSiteHandler(envCfg, siteRepository)
|
||||
siteHandler := handlers.NewSiteHandler(siteRepository)
|
||||
|
||||
api := app.Group("/api/v1", middleware.APIHostGuard(envCfg.DashboardHost))
|
||||
api.Get("/health", handlers.HealthCheck)
|
||||
|
||||
api.Post("/update", updateSiteHandler.PostUpdate)
|
||||
|
||||
api.Get("/sites", siteHandler.GetSites)
|
||||
api.Get("/sites/:id", siteHandler.GetSite)
|
||||
api.Post("/sites", siteHandler.PostSite)
|
||||
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)
|
||||
|
||||
api.Use(func(c fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusNotFound).JSON(&models.APIError{
|
||||
Message: "Endpoint not found",
|
||||
})
|
||||
})
|
||||
|
||||
storagePath, err := filepath.Abs(envCfg.StoragePath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to resolve storage path: %v", err)
|
||||
}
|
||||
|
||||
siteMap := make(map[string]config.SiteConfig)
|
||||
for _, site := range cfg.Sites {
|
||||
siteMap[site.Domain] = site
|
||||
}
|
||||
|
||||
app.Use(handlers.NewStaticHandler(storagePath, siteRepository))
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
# Configuration file for Quay
|
||||
# This file defines the global settings and the sites to be deployed.
|
||||
# You can use ${VARIABLE_NAME} to reference environment variables for sensitive information.
|
||||
|
||||
sites:
|
||||
- repo: my-lib-docs
|
||||
owner: yourname
|
||||
branch: gh-pages
|
||||
domain: docs.my-lib.com
|
||||
deploy_token: "${MY_LIB_DEPLOY_TOKEN}"
|
||||
enabled: true
|
||||
not_found_file: 404.html
|
||||
forward_rules:
|
||||
- source: /npm
|
||||
destination: https://www.npmjs.com/package/my-lib
|
||||
status_code: 302
|
||||
- source: ^/docs/v(\d+)$
|
||||
destination: https://v$1.docs.my-lib.com/
|
||||
status_code: 301
|
||||
regex: true
|
||||
- source: ^/blog/(?P<slug>[^/]+)$
|
||||
destination: /posts/${slug}
|
||||
status_code: 302
|
||||
regex: true
|
||||
custom_headers:
|
||||
- source: /json/* # glob (default)
|
||||
headers:
|
||||
Content-Type: "application/json"
|
||||
Cache-Control: "max-age=7200, must-revalidate"
|
||||
- source: ^/api/v\d+/ # regex opt-in
|
||||
regex: true
|
||||
headers:
|
||||
Cache-Control: "no-store"
|
||||
|
||||
- repo: portfolio
|
||||
owner: yourname
|
||||
branch: main
|
||||
domain: yourname.com
|
||||
spa: true
|
||||
deploy_token: "${PORTFOLIO_DEPLOY_TOKEN}"
|
||||
enabled: true
|
||||
|
||||
- repo: other-docs
|
||||
owner: yourname
|
||||
branch: gh-pages
|
||||
domain: other-docs.yourdomain.com
|
||||
deploy_token: "${OTHER_DOCS_DEPLOY_TOKEN}"
|
||||
enabled: true
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,714 @@
|
||||
basePath: /api/v1
|
||||
definitions:
|
||||
models.APIError:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
models.CreateSiteResponse:
|
||||
properties:
|
||||
raw_deploy_token:
|
||||
type: string
|
||||
site:
|
||||
$ref: '#/definitions/models.Site'
|
||||
type: object
|
||||
models.CustomHeaders:
|
||||
properties:
|
||||
headers:
|
||||
items:
|
||||
$ref: '#/definitions/models.Header'
|
||||
type: array
|
||||
id:
|
||||
type: string
|
||||
source:
|
||||
type: string
|
||||
type: object
|
||||
models.ForwardRule:
|
||||
properties:
|
||||
destination:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
regex:
|
||||
type: boolean
|
||||
source:
|
||||
type: string
|
||||
status_code:
|
||||
type: integer
|
||||
type: object
|
||||
models.GetAllSitesResponse:
|
||||
properties:
|
||||
sites:
|
||||
items:
|
||||
$ref: '#/definitions/models.Site'
|
||||
type: array
|
||||
total:
|
||||
type: integer
|
||||
type: object
|
||||
models.Header:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
key:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
type: object
|
||||
models.Site:
|
||||
properties:
|
||||
branch:
|
||||
type: string
|
||||
custom_headers:
|
||||
items:
|
||||
$ref: '#/definitions/models.CustomHeaders'
|
||||
type: array
|
||||
deploy_token:
|
||||
type: string
|
||||
domain:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
forward_rules:
|
||||
items:
|
||||
$ref: '#/definitions/models.ForwardRule'
|
||||
type: array
|
||||
git_server:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
not_found_file:
|
||||
type: string
|
||||
owner:
|
||||
type: string
|
||||
repository:
|
||||
type: string
|
||||
spa:
|
||||
type: boolean
|
||||
type: object
|
||||
host: localhost:4321
|
||||
info:
|
||||
contact: {}
|
||||
description: Self-hosted static site deployment service
|
||||
title: Quay API
|
||||
version: "1.0"
|
||||
paths:
|
||||
/custom-headers/{id}:
|
||||
delete:
|
||||
description: Deletes a custom header group by ID
|
||||
parameters:
|
||||
- description: Custom Headers ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: No content
|
||||
"404":
|
||||
description: Custom headers not found
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
summary: Delete a custom header group
|
||||
tags:
|
||||
- Custom-Headers
|
||||
get:
|
||||
description: Returns a single custom header group by ID
|
||||
parameters:
|
||||
- description: Custom Headers ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Custom header group
|
||||
schema:
|
||||
$ref: '#/definitions/models.CustomHeaders'
|
||||
"404":
|
||||
description: Custom headers not found
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
summary: Get a custom header group
|
||||
tags:
|
||||
- Custom-Headers
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Replaces an existing custom header group by ID
|
||||
parameters:
|
||||
- description: Custom Headers ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: Updated custom header group
|
||||
in: body
|
||||
name: customHeaders
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.CustomHeaders'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Updated custom header group
|
||||
schema:
|
||||
$ref: '#/definitions/models.CustomHeaders'
|
||||
"400":
|
||||
description: Invalid request body
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"404":
|
||||
description: Custom headers not found
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
summary: Update a custom header group
|
||||
tags:
|
||||
- Custom-Headers
|
||||
/custom-headers/{id}/headers:
|
||||
get:
|
||||
description: Returns all individual headers belonging to the given custom header
|
||||
group
|
||||
parameters:
|
||||
- description: Custom Headers ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: List of headers
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/models.Header'
|
||||
type: array
|
||||
"404":
|
||||
description: Custom headers not found
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
summary: List headers in a custom header group
|
||||
tags:
|
||||
- Custom-Headers
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Creates a new header within the given custom header group
|
||||
parameters:
|
||||
- description: Custom Headers ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: Header to create
|
||||
in: body
|
||||
name: header
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.Header'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Created header
|
||||
schema:
|
||||
$ref: '#/definitions/models.Header'
|
||||
"400":
|
||||
description: Invalid request body
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"404":
|
||||
description: Custom headers not found
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
summary: Create a header
|
||||
tags:
|
||||
- Headers
|
||||
/forward-rules/{id}:
|
||||
delete:
|
||||
description: Deletes a forward rule by ID
|
||||
parameters:
|
||||
- description: Forward Rule ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: No content
|
||||
"404":
|
||||
description: Forward rule not found
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
summary: Delete a forward rule
|
||||
tags:
|
||||
- Forward-Rules
|
||||
get:
|
||||
description: Returns a single forward rule by ID
|
||||
parameters:
|
||||
- description: Forward Rule ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Forward rule
|
||||
schema:
|
||||
$ref: '#/definitions/models.ForwardRule'
|
||||
"404":
|
||||
description: Forward rule not found
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
summary: Get a forward rule
|
||||
tags:
|
||||
- Forward-Rules
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Replaces an existing forward rule by ID
|
||||
parameters:
|
||||
- description: Forward Rule ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: Updated forward rule
|
||||
in: body
|
||||
name: rule
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.ForwardRule'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Updated forward rule
|
||||
schema:
|
||||
$ref: '#/definitions/models.ForwardRule'
|
||||
"400":
|
||||
description: Invalid request body
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"404":
|
||||
description: Forward rule not found
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
summary: Update a forward rule
|
||||
tags:
|
||||
- Forward-Rules
|
||||
/headers/{id}:
|
||||
delete:
|
||||
description: Deletes a header by ID
|
||||
parameters:
|
||||
- description: Header ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: No content
|
||||
"404":
|
||||
description: Header not found
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
summary: Delete a header
|
||||
tags:
|
||||
- Headers
|
||||
get:
|
||||
description: Returns a single header by ID
|
||||
parameters:
|
||||
- description: Header ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Header
|
||||
schema:
|
||||
$ref: '#/definitions/models.Header'
|
||||
"404":
|
||||
description: Header not found
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
summary: Get a header
|
||||
tags:
|
||||
- Headers
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Replaces an existing header by ID
|
||||
parameters:
|
||||
- description: Header ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: Updated header
|
||||
in: body
|
||||
name: header
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.Header'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Updated header
|
||||
schema:
|
||||
$ref: '#/definitions/models.Header'
|
||||
"400":
|
||||
description: Invalid request body
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"404":
|
||||
description: Header not found
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
summary: Update a header
|
||||
tags:
|
||||
- Headers
|
||||
/sites:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Get a list of all sites
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/models.GetAllSitesResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
summary: Get all sites
|
||||
tags:
|
||||
- Sites
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Create a new site with the provided details
|
||||
parameters:
|
||||
- description: Site details
|
||||
in: body
|
||||
name: site
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.Site'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/models.CreateSiteResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
summary: Create a new site
|
||||
tags:
|
||||
- Sites
|
||||
/sites/{id}:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Delete a site by its ID
|
||||
parameters:
|
||||
- description: Site ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
summary: Delete a site
|
||||
tags:
|
||||
- Sites
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Get a single site by its ID
|
||||
parameters:
|
||||
- description: Site ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/models.Site'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
summary: Get site by ID
|
||||
tags:
|
||||
- Sites
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Update an existing site by its ID
|
||||
parameters:
|
||||
- description: Site ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: Updated site details
|
||||
in: body
|
||||
name: site
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.Site'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/models.Site'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
summary: Update an existing site
|
||||
tags:
|
||||
- Sites
|
||||
/sites/{id}/custom-headers:
|
||||
get:
|
||||
description: Returns all custom header groups associated with the given site
|
||||
parameters:
|
||||
- description: Site ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: List of custom header groups
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/models.CustomHeaders'
|
||||
type: array
|
||||
"404":
|
||||
description: Site not found
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
summary: List custom header groups for a site
|
||||
tags:
|
||||
- Custom-Headers
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Creates a new custom header group for the given site
|
||||
parameters:
|
||||
- description: Site ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: Custom header group to create
|
||||
in: body
|
||||
name: customHeaders
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.CustomHeaders'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Created custom header group
|
||||
schema:
|
||||
$ref: '#/definitions/models.CustomHeaders'
|
||||
"400":
|
||||
description: Invalid request body
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"404":
|
||||
description: Site not found
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
summary: Create a custom header group
|
||||
tags:
|
||||
- Custom-Headers
|
||||
/sites/{id}/forward-rules:
|
||||
get:
|
||||
description: Returns all forward rules associated with the given site
|
||||
parameters:
|
||||
- description: Site ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: List of forward rules
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/models.ForwardRule'
|
||||
type: array
|
||||
"404":
|
||||
description: Site not found
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
summary: List forward rules for a site
|
||||
tags:
|
||||
- Forward-Rules
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Creates a new forward rule for the given site
|
||||
parameters:
|
||||
- description: Site ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: Forward rule to create
|
||||
in: body
|
||||
name: rule
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.ForwardRule'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Created forward rule
|
||||
schema:
|
||||
$ref: '#/definitions/models.ForwardRule'
|
||||
"400":
|
||||
description: Invalid request body
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"404":
|
||||
description: Site not found
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIError'
|
||||
summary: Create a forward rule
|
||||
tags:
|
||||
- Forward-Rules
|
||||
swagger: "2.0"
|
||||
tags:
|
||||
- description: Manage sites
|
||||
name: Sites
|
||||
- description: Manage redirect rules for a site
|
||||
name: Forward-Rules
|
||||
- description: Manage custom header groups for a site
|
||||
name: Custom-Headers
|
||||
- description: Manage individual headers within a custom header group
|
||||
name: Headers
|
||||
@@ -0,0 +1,41 @@
|
||||
module quay
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/Flussen/swagger-fiber-v3 v1.0.1
|
||||
github.com/gofiber/fiber/v3 v3.1.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mattn/go-sqlite3 v1.14.38
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/gofiber/schema v1.7.0 // indirect
|
||||
github.com/gofiber/utils/v2 v2.0.2 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/swaggo/swag v1.16.4 // indirect
|
||||
github.com/tinylib/msgp v1.6.3 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,94 @@
|
||||
github.com/Flussen/swagger-fiber-v3 v1.0.1 h1:lgR2+ADJRx7Kh4oGidsf790UVwXrgC4I7p/3SAmHimw=
|
||||
github.com/Flussen/swagger-fiber-v3 v1.0.1/go.mod h1:rHViWTgpklVFVsYkWgL8zip4QHJlKwuBax8wY0G3sPw=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
|
||||
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
|
||||
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=
|
||||
github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=
|
||||
github.com/gofiber/schema v1.7.0 h1:yNM+FNRZjyYEli9Ey0AXRBrAY9jTnb+kmGs3lJGPvKg=
|
||||
github.com/gofiber/schema v1.7.0/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=
|
||||
github.com/gofiber/utils/v2 v2.0.2 h1:ShRRssz0F3AhTlAQcuEj54OEDtWF7+HJDwEi/aa6QLI=
|
||||
github.com/gofiber/utils/v2 v2.0.2/go.mod h1:+9Ub4NqQ+IaJoTliq5LfdmOJAA/Hzwf4pXOxOa3RrJ0=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.38 h1:tDUzL85kMvOrvpCt8P64SbGgVFtJB11GPi2AdmITgb4=
|
||||
github.com/mattn/go-sqlite3 v1.14.38/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
|
||||
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
|
||||
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
|
||||
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,55 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
)
|
||||
|
||||
func InitializeSQLite(db *sql.DB) error {
|
||||
_, err := db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS sites (
|
||||
id TEXT PRIMARY KEY,
|
||||
git_server TEXT NOT NULL,
|
||||
owner TEXT NOT NULL,
|
||||
repository TEXT NOT NULL,
|
||||
branch TEXT NOT NULL,
|
||||
domain TEXT NOT NULL,
|
||||
deploy_token TEXT NOT NULL,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
spa INTEGER NOT NULL DEFAULT 0,
|
||||
not_found_file TEXT NOT NULL DEFAULT '404.html'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS forward_rules (
|
||||
id TEXT PRIMARY KEY,
|
||||
site_id TEXT NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
destination TEXT NOT NULL,
|
||||
status_code INTEGER NOT NULL,
|
||||
regex INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS custom_headers (
|
||||
id TEXT PRIMARY KEY,
|
||||
site_id TEXT NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
regex INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS headers (
|
||||
id TEXT PRIMARY KEY,
|
||||
custom_header_id TEXT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
FOREIGN KEY (custom_header_id) REFERENCES custom_headers(id) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
|
||||
if err == nil {
|
||||
log.Println("Database initialized successfully")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"quay/app/models"
|
||||
"quay/app/repository"
|
||||
|
||||
"github.com/google/uuid"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
type SQLiteSiteRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewSQLiteSiteRepository(db *sql.DB) *SQLiteSiteRepository {
|
||||
return &SQLiteSiteRepository{db: db}
|
||||
}
|
||||
|
||||
var _ repository.SiteRepository = (*SQLiteSiteRepository)(nil)
|
||||
|
||||
// Sites
|
||||
|
||||
func (r *SQLiteSiteRepository) GetSite(id string) (*models.Site, error) {
|
||||
row := r.db.QueryRow(`
|
||||
SELECT id, git_server, owner, repository, branch, domain, deploy_token, enabled, not_found_file
|
||||
FROM sites WHERE id = ?`, id)
|
||||
|
||||
s, err := scanSite(row)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get site: %w", err)
|
||||
}
|
||||
|
||||
if err := r.populateSiteRelations(s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (r *SQLiteSiteRepository) GetSiteByDomain(domain string) (*models.Site, error) {
|
||||
row := r.db.QueryRow(`
|
||||
SELECT id, git_server, owner, repository, branch, domain, deploy_token, enabled, not_found_file
|
||||
FROM sites WHERE domain = ?`, domain)
|
||||
|
||||
s, err := scanSite(row)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("get site by domain: %w", err)
|
||||
}
|
||||
|
||||
if err := r.populateSiteRelations(s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (r *SQLiteSiteRepository) ListSites() ([]models.Site, error) {
|
||||
rows, err := r.db.Query(`
|
||||
SELECT id, git_server, owner, repository, branch, domain, deploy_token, enabled, not_found_file
|
||||
FROM sites`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list sites: %w", err)
|
||||
}
|
||||
|
||||
var sites []models.Site
|
||||
for rows.Next() {
|
||||
s, err := scanSite(rows)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return nil, fmt.Errorf("list sites scan: %w", err)
|
||||
}
|
||||
sites = append(sites, *s)
|
||||
}
|
||||
rows.Close()
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range sites {
|
||||
if err := r.populateSiteRelations(&sites[i]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return sites, nil
|
||||
}
|
||||
|
||||
func (r *SQLiteSiteRepository) CreateSite(s *models.Site) error {
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("create site begin tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
s.ID = uuid.NewString()
|
||||
|
||||
_, err = tx.Exec(`
|
||||
INSERT INTO sites (id, git_server, owner, repository, branch, domain, deploy_token, enabled, not_found_file)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
s.ID, s.GitServer, s.Owner, s.Repository, s.Branch,
|
||||
s.Domain, s.DeployToken, s.Enabled, s.NotFoundFile,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create site insert: %w", err)
|
||||
}
|
||||
|
||||
for i := range s.ForwardRules {
|
||||
if err := insertForwardRule(tx, s.ID, &s.ForwardRules[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for i := range s.CustomHeaders {
|
||||
if err := insertCustomHeaders(tx, s.ID, &s.CustomHeaders[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (r *SQLiteSiteRepository) UpdateSite(s *models.Site) error {
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("update site begin tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
_, err = tx.Exec(`
|
||||
UPDATE sites SET git_server=?, owner=?, repository=?, branch=?, domain=?,
|
||||
deploy_token=?, enabled=?, not_found_file=? WHERE id=?`,
|
||||
s.GitServer, s.Owner, s.Repository, s.Branch, s.Domain,
|
||||
s.DeployToken, s.Enabled, s.NotFoundFile, s.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update site: %w", err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`DELETE FROM forward_rules WHERE site_id = ?`, s.ID); err != nil {
|
||||
return fmt.Errorf("update site delete forward rules: %w", err)
|
||||
}
|
||||
for i := range s.ForwardRules {
|
||||
if err := insertForwardRule(tx, s.ID, &s.ForwardRules[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`
|
||||
DELETE FROM headers WHERE custom_header_id IN (
|
||||
SELECT id FROM custom_headers WHERE site_id = ?
|
||||
)`, s.ID); err != nil {
|
||||
return fmt.Errorf("update site delete headers: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(`DELETE FROM custom_headers WHERE site_id = ?`, s.ID); err != nil {
|
||||
return fmt.Errorf("update site delete custom headers: %w", err)
|
||||
}
|
||||
for _, ch := range s.CustomHeaders {
|
||||
if err := insertCustomHeaders(tx, s.ID, &ch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (r *SQLiteSiteRepository) DeleteSite(id string) error {
|
||||
_, err := r.db.Exec(`DELETE FROM sites WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete site: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Forward Rules
|
||||
|
||||
func (r *SQLiteSiteRepository) GetForwardRule(id string) (*models.ForwardRule, error) {
|
||||
row := r.db.QueryRow(`
|
||||
SELECT id, source, destination, status_code, regex
|
||||
FROM forward_rules WHERE id = ?`, id)
|
||||
|
||||
var fr models.ForwardRule
|
||||
var regex int
|
||||
err := row.Scan(&fr.ID, &fr.Source, &fr.Destination, &fr.StatusCode, ®ex)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get forward rule: %w", err)
|
||||
}
|
||||
fr.Regex = regex != 0
|
||||
return &fr, nil
|
||||
}
|
||||
|
||||
func (r *SQLiteSiteRepository) CreateForwardRule(siteID string, fr *models.ForwardRule) error {
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("create forward rule begin tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if err := insertForwardRule(tx, siteID, fr); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (r *SQLiteSiteRepository) UpdateForwardRule(fr *models.ForwardRule) error {
|
||||
_, err := r.db.Exec(`
|
||||
UPDATE forward_rules SET source=?, destination=?, status_code=?, regex=? WHERE id=?`,
|
||||
fr.Source, fr.Destination, fr.StatusCode, fr.Regex, fr.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update forward rule: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SQLiteSiteRepository) DeleteForwardRule(id string) error {
|
||||
_, err := r.db.Exec(`DELETE FROM forward_rules WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete forward rule: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Custom Headers
|
||||
|
||||
func (r *SQLiteSiteRepository) GetCustomHeaders(id string) (*models.CustomHeaders, error) {
|
||||
row := r.db.QueryRow(`SELECT id, source, regex FROM custom_headers WHERE id = ?`, id)
|
||||
|
||||
var ch models.CustomHeaders
|
||||
var regex int
|
||||
if err := row.Scan(&ch.ID, &ch.Source, ®ex); err != nil {
|
||||
return nil, fmt.Errorf("get custom headers: %w", err)
|
||||
}
|
||||
|
||||
headers, err := r.listHeaders(ch.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ch.Headers = headers
|
||||
ch.Regex = regex != 0
|
||||
return &ch, nil
|
||||
}
|
||||
|
||||
func (r *SQLiteSiteRepository) CreateCustomHeaders(siteID string, ch *models.CustomHeaders) error {
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("create custom headers begin tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if err := insertCustomHeaders(tx, siteID, ch); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (r *SQLiteSiteRepository) UpdateCustomHeaders(ch *models.CustomHeaders) error {
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("update custom headers begin tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if _, err := tx.Exec(`UPDATE custom_headers SET source=?, regex=? WHERE id=?`, ch.Source, ch.Regex, ch.ID); err != nil {
|
||||
return fmt.Errorf("update custom headers: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(`DELETE FROM headers WHERE custom_header_id = ?`, ch.ID); err != nil {
|
||||
return fmt.Errorf("update custom headers delete headers: %w", err)
|
||||
}
|
||||
for _, h := range ch.Headers {
|
||||
if err := insertHeader(tx, ch.ID, &h); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (r *SQLiteSiteRepository) DeleteCustomHeaders(id string) error {
|
||||
_, err := r.db.Exec(`DELETE FROM custom_headers WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete custom headers: %w", err)
|
||||
}
|
||||
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 {
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
func scanSite(s scanner) (*models.Site, error) {
|
||||
var site models.Site
|
||||
var enabled int
|
||||
err := s.Scan(
|
||||
&site.ID, &site.GitServer, &site.Owner, &site.Repository,
|
||||
&site.Branch, &site.Domain, &site.DeployToken, &enabled, &site.NotFoundFile,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
site.Enabled = enabled != 0
|
||||
return &site, nil
|
||||
}
|
||||
|
||||
func (r *SQLiteSiteRepository) populateSiteRelations(s *models.Site) error {
|
||||
rules, err := r.listForwardRules(s.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.ForwardRules = rules
|
||||
|
||||
customHeaders, err := r.listCustomHeaders(s.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.CustomHeaders = customHeaders
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SQLiteSiteRepository) listForwardRules(siteID string) ([]models.ForwardRule, error) {
|
||||
rows, err := r.db.Query(`
|
||||
SELECT id, source, destination, status_code, regex
|
||||
FROM forward_rules WHERE site_id = ?`, siteID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list forward rules: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var rules []models.ForwardRule
|
||||
for rows.Next() {
|
||||
var fr models.ForwardRule
|
||||
var regex int
|
||||
if err := rows.Scan(&fr.ID, &fr.Source, &fr.Destination, &fr.StatusCode, ®ex); err != nil {
|
||||
return nil, fmt.Errorf("list forward rules scan: %w", err)
|
||||
}
|
||||
fr.Regex = regex != 0
|
||||
rules = append(rules, fr)
|
||||
}
|
||||
return rules, rows.Err()
|
||||
}
|
||||
|
||||
func (r *SQLiteSiteRepository) listCustomHeaders(siteID string) ([]models.CustomHeaders, error) {
|
||||
rows, err := r.db.Query(`SELECT id, source, regex FROM custom_headers WHERE site_id = ?`, siteID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list custom headers: %w", err)
|
||||
}
|
||||
|
||||
var result []models.CustomHeaders
|
||||
for rows.Next() {
|
||||
var ch models.CustomHeaders
|
||||
var regex int
|
||||
if err := rows.Scan(&ch.ID, &ch.Source, ®ex); err != nil {
|
||||
rows.Close()
|
||||
return nil, fmt.Errorf("list custom headers scan: %w", err)
|
||||
}
|
||||
ch.Regex = regex != 0
|
||||
result = append(result, ch)
|
||||
}
|
||||
rows.Close()
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range result {
|
||||
headers, err := r.listHeaders(result[i].ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[i].Headers = headers
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *SQLiteSiteRepository) listHeaders(customHeaderID string) ([]models.Header, error) {
|
||||
rows, err := r.db.Query(`
|
||||
SELECT id, key, value FROM headers WHERE custom_header_id = ?`, customHeaderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list headers: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var headers []models.Header
|
||||
for rows.Next() {
|
||||
var h models.Header
|
||||
if err := rows.Scan(&h.ID, &h.Key, &h.Value); err != nil {
|
||||
return nil, fmt.Errorf("list headers scan: %w", err)
|
||||
}
|
||||
headers = append(headers, h)
|
||||
}
|
||||
return headers, rows.Err()
|
||||
}
|
||||
|
||||
func insertForwardRule(tx *sql.Tx, siteID string, fr *models.ForwardRule) error {
|
||||
fr.ID = uuid.NewString()
|
||||
_, err := tx.Exec(`
|
||||
INSERT INTO forward_rules (id, site_id, source, destination, status_code, regex)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
fr.ID, siteID, fr.Source, fr.Destination, fr.StatusCode, fr.Regex,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert forward rule: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func insertCustomHeaders(tx *sql.Tx, siteID string, ch *models.CustomHeaders) error {
|
||||
ch.ID = uuid.NewString()
|
||||
_, err := tx.Exec(`
|
||||
INSERT INTO custom_headers (id, site_id, source, regex) VALUES (?, ?, ?, ?)`,
|
||||
ch.ID, siteID, ch.Source, ch.Regex,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert custom headers: %w", err)
|
||||
}
|
||||
for _, h := range ch.Headers {
|
||||
if err := insertHeader(tx, ch.ID, &h); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func insertHeader(tx *sql.Tx, customHeaderID string, h *models.Header) error {
|
||||
h.ID = uuid.NewString()
|
||||
_, err := tx.Exec(`
|
||||
INSERT INTO headers (id, custom_header_id, key, value) VALUES (?, ?, ?, ?)`,
|
||||
h.ID, customHeaderID, h.Key, h.Value,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert header: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func ConnectSQLite(dbPath string) (*sql.DB, error) {
|
||||
db, err := sql.Open("sqlite3", dbPath+"?_busy_timeout=5000&_journal_mode=WAL")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// SQLite doesn't like multiple connections
|
||||
db.SetMaxOpenConns(1)
|
||||
|
||||
if err = db.Ping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package envconfig
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
type EnvConfig struct {
|
||||
Port string
|
||||
ConfigDir string
|
||||
GithubPat string
|
||||
StoragePath string
|
||||
DashboardHost string
|
||||
}
|
||||
|
||||
func Load() EnvConfig {
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "4321"
|
||||
}
|
||||
|
||||
configDir := os.Getenv("CONFIG_DIR")
|
||||
if configDir == "" {
|
||||
configDir = "./"
|
||||
}
|
||||
|
||||
githubPat := os.Getenv("GITHUB_PAT")
|
||||
if githubPat == "" {
|
||||
githubPat = ""
|
||||
}
|
||||
|
||||
storagePath := os.Getenv("STORAGE_PATH")
|
||||
if storagePath == "" {
|
||||
storagePath = "./storage"
|
||||
}
|
||||
|
||||
dashboardHost := os.Getenv("DASHBOARD_HOST")
|
||||
if dashboardHost == "" {
|
||||
dashboardHost = "localhost"
|
||||
}
|
||||
|
||||
return EnvConfig{
|
||||
Port: port,
|
||||
ConfigDir: configDir,
|
||||
GithubPat: githubPat,
|
||||
StoragePath: storagePath,
|
||||
DashboardHost: dashboardHost,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package fiberconfig
|
||||
|
||||
import (
|
||||
"github.com/Flussen/swagger-fiber-v3"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/gofiber/fiber/v3/middleware/logger"
|
||||
"github.com/gofiber/fiber/v3/middleware/static"
|
||||
)
|
||||
|
||||
func Setup(app *fiber.App) {
|
||||
app.Get("/api/docs.json", static.New("./docs/swagger.json"))
|
||||
|
||||
app.Get("/api/swagger/*", swagger.New(swagger.Config{
|
||||
URL: "/api/docs.json",
|
||||
Title: "mcheads.net API Documentation",
|
||||
}))
|
||||
|
||||
app.Use("/api", func(c fiber.Ctx) error {
|
||||
c.Set("Content-Type", "application/json")
|
||||
return c.Next()
|
||||
})
|
||||
|
||||
app.Use(logger.New(logger.Config{
|
||||
Format: "[${time}] ${status} - ${method} ${path} (${latency}) - ${ip}\n",
|
||||
TimeFormat: "2006-01-02 15:04:05",
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func generateToken() (rawToken string, err error) {
|
||||
bytes := make([]byte, 32)
|
||||
if _, err = rand.Read(bytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "quay_" + hex.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
func hashToken(rawToken string) string {
|
||||
sum := sha256.Sum256([]byte(rawToken))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func CreateDeployToken() (rawToken, hashedToken string, err error) {
|
||||
rawToken, err = generateToken()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to generate token: %w", err)
|
||||
}
|
||||
hashedToken = hashToken(rawToken)
|
||||
return rawToken, hashedToken, nil
|
||||
}
|
||||
|
||||
func CompareDeployTokens(incomingRawToken, storedHashedToken string) bool {
|
||||
incomingHash := hashToken(incomingRawToken)
|
||||
|
||||
return subtle.ConstantTimeCompare(
|
||||
[]byte(incomingHash),
|
||||
[]byte(storedHashedToken),
|
||||
) == 1
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"quay/app/routes"
|
||||
"quay/internal/config"
|
||||
"quay/internal/database"
|
||||
"quay/internal/envconfig"
|
||||
"quay/internal/fiberconfig"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
// @title Quay API
|
||||
// @version 1.0
|
||||
// @description Self-hosted static site deployment service
|
||||
// @host localhost:4321
|
||||
// @BasePath /api/v1
|
||||
|
||||
// @tag.name Sites
|
||||
// @tag.description Manage sites
|
||||
|
||||
// @tag.name Forward-Rules
|
||||
// @tag.description Manage redirect rules for a site
|
||||
|
||||
// @tag.name Custom-Headers
|
||||
// @tag.description Manage custom header groups for a site
|
||||
|
||||
// @tag.name Headers
|
||||
// @tag.description Manage individual headers within a custom header group
|
||||
|
||||
func main() {
|
||||
_ = godotenv.Load()
|
||||
|
||||
envCfg := envconfig.Load()
|
||||
|
||||
configFilePath := filepath.Join(envCfg.ConfigDir, "config.yaml")
|
||||
cfg, err := config.Load(configFilePath)
|
||||
if err != nil {
|
||||
panic("Failed to load config: " + err.Error())
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(envCfg.ConfigDir, "db.sqlite")
|
||||
db, err := database.ConnectSQLite(dbPath)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to connect to database:", err)
|
||||
}
|
||||
log.Println("Connected to database")
|
||||
defer func(db *sql.DB) {
|
||||
err := db.Close()
|
||||
if err != nil {
|
||||
log.Println("Failed to close database:", err)
|
||||
}
|
||||
}(db)
|
||||
|
||||
err = database.InitializeSQLite(db)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to initialize database:", err)
|
||||
}
|
||||
|
||||
app := fiber.New()
|
||||
|
||||
fiberconfig.Setup(app)
|
||||
routes.Register(app, cfg, &envCfg, db)
|
||||
|
||||
log.Fatal(app.Listen(":" + envCfg.Port))
|
||||
}
|
||||
Reference in New Issue
Block a user