Added frontend

This commit is contained in:
2026-04-03 11:51:36 +02:00
parent 12678f8241
commit 29ee01afba
30 changed files with 1 additions and 337 deletions
@@ -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
}
+179
View File
@@ -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
}
+9
View File
@@ -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",
})
}
+288
View File
@@ -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)
}
+591
View File
@@ -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)
}
+99
View File
@@ -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)
}
}
+100
View File
@@ -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)
}
+12
View File
@@ -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()
}
}
+5
View File
@@ -0,0 +1,5 @@
package models
type APIError struct {
Message string `json:"message"`
}
+47
View File
@@ -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"`
}
+24
View File
@@ -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
}
+70
View File
@@ -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))
}