Added sqlite database
This commit is contained in:
Generated
+12
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="db.sqlite" uuid="01e48c0a-92f2-41f7-94e8-3dee9d9f5a2a">
|
||||||
|
<driver-ref>sqlite.xerial</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/config/db.sqlite</jdbc-url>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+7
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="SqlDialectMappings">
|
||||||
|
<file url="file://$PROJECT_DIR$/app/repository/site_repository.go" dialect="GenericSQL" />
|
||||||
|
<file url="PROJECT" dialect="SQLite" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"quay/app/models"
|
||||||
|
"quay/app/repository"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SiteHandler struct {
|
||||||
|
Repo repository.SiteRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSiteHandler(repo repository.SiteRepository) *SiteHandler {
|
||||||
|
return &SiteHandler{Repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSites godoc
|
||||||
|
// @Summary Get all sites
|
||||||
|
// @Description Get a list of all sites
|
||||||
|
// @Tags Sites
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.GetAllSitesResponse
|
||||||
|
// @Failure 500 {object} models.APIError
|
||||||
|
// @Router /sites [get]
|
||||||
|
func (h *SiteHandler) GetSites(c fiber.Ctx) error {
|
||||||
|
sites, err := h.Repo.ListSites()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error listing sites: ", err)
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{
|
||||||
|
Message: "Unexpected error while listing sites",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if sites == nil {
|
||||||
|
sites = []models.Site{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(models.GetAllSitesResponse{
|
||||||
|
Sites: sites,
|
||||||
|
Total: len(sites),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSite godoc
|
||||||
|
// @Summary Get site by ID
|
||||||
|
// @Description Get a single site by its ID
|
||||||
|
// @Tags Sites
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Site ID"
|
||||||
|
// @Success 200 {object} models.Site
|
||||||
|
// @Failure 404 {object} models.APIError
|
||||||
|
// @Failure 500 {object} models.APIError
|
||||||
|
// @Router /sites/{id} [get]
|
||||||
|
func (h *SiteHandler) GetSite(c fiber.Ctx) error {
|
||||||
|
id := c.Params("id")
|
||||||
|
|
||||||
|
site, err := h.Repo.GetSite(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(&models.APIError{
|
||||||
|
Message: "Site not found",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
log.Println("Message getting site: ", err)
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{
|
||||||
|
Message: "Unexpected error while getting site",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if site == nil {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(&models.APIError{
|
||||||
|
Message: "Site not found",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(site)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateIncomingSite(site *models.Site) error {
|
||||||
|
if site == nil {
|
||||||
|
return errors.New("site is required")
|
||||||
|
}
|
||||||
|
if site.GitServer == "" {
|
||||||
|
return errors.New("git server required")
|
||||||
|
}
|
||||||
|
if site.Owner == "" {
|
||||||
|
return errors.New("owner required")
|
||||||
|
}
|
||||||
|
if site.Repository == "" {
|
||||||
|
return errors.New("repository required")
|
||||||
|
}
|
||||||
|
if site.Branch == "" {
|
||||||
|
return errors.New("branch required")
|
||||||
|
}
|
||||||
|
if site.Domain == "" {
|
||||||
|
return errors.New("domain required")
|
||||||
|
}
|
||||||
|
if site.NotFoundFile == "" {
|
||||||
|
site.NotFoundFile = "404.html"
|
||||||
|
}
|
||||||
|
if site.ForwardRules == nil {
|
||||||
|
site.ForwardRules = []models.ForwardRule{}
|
||||||
|
}
|
||||||
|
if site.ForwardRules != nil {
|
||||||
|
for _, r := range site.ForwardRules {
|
||||||
|
if r.Source == "" {
|
||||||
|
return errors.New("forward rule source is required")
|
||||||
|
}
|
||||||
|
if r.Destination == "" {
|
||||||
|
return errors.New("forward rule destination is required")
|
||||||
|
}
|
||||||
|
if r.StatusCode < 300 || r.StatusCode > 399 {
|
||||||
|
return errors.New("forward rule status code must be between 300 and 399")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if site.CustomHeaders == nil {
|
||||||
|
site.CustomHeaders = []models.CustomHeaders{}
|
||||||
|
}
|
||||||
|
if site.CustomHeaders != nil {
|
||||||
|
for _, h := range site.CustomHeaders {
|
||||||
|
if h.Source == "" {
|
||||||
|
return errors.New("custom header source required")
|
||||||
|
}
|
||||||
|
if h.Headers == nil {
|
||||||
|
return errors.New("custom header is required")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostSite godoc
|
||||||
|
// @Summary Create a new site
|
||||||
|
// @Description Create a new site with the provided details
|
||||||
|
// @Tags Sites
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param site body models.Site true "Site details"
|
||||||
|
// @Success 200 {object} models.Site
|
||||||
|
// @Failure 400 {object} models.APIError
|
||||||
|
// @Failure 500 {object} models.APIError
|
||||||
|
// @Router /sites [post]
|
||||||
|
func (h *SiteHandler) PostSite(c fiber.Ctx) error {
|
||||||
|
var site models.Site
|
||||||
|
|
||||||
|
if err := c.Bind().Body(&site); err != nil {
|
||||||
|
log.Println("Error parsing body: ", err)
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{
|
||||||
|
Message: "Invalid request body",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateIncomingSite(&site); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{
|
||||||
|
Message: "Invalid request body: " + err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.Repo.CreateSite(&site); err != nil {
|
||||||
|
log.Println("Error creating site: ", err)
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{
|
||||||
|
Message: "Unexpected error while creating site",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(site)
|
||||||
|
}
|
||||||
+21
-15
@@ -1,6 +1,7 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"quay/app/github"
|
"quay/app/github"
|
||||||
"quay/app/models"
|
"quay/app/models"
|
||||||
@@ -8,22 +9,28 @@ import (
|
|||||||
"quay/internal/envconfig"
|
"quay/internal/envconfig"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"crypto/subtle"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewUpdateSiteHandler(cfg *config.Config, envCfg *envconfig.EnvConfig) fiber.Handler {
|
type UpdateSiteHandler struct {
|
||||||
return func(c fiber.Ctx) error {
|
Cfg *config.Config
|
||||||
|
EnvCfg *envconfig.EnvConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUpdateSiteHandler(cfg *config.Config, envCfg *envconfig.EnvConfig) *UpdateSiteHandler {
|
||||||
|
return &UpdateSiteHandler{Cfg: cfg, EnvCfg: envCfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UpdateSiteHandler) PostUpdate(c fiber.Ctx) error {
|
||||||
sitename := c.Query("site")
|
sitename := c.Query("site")
|
||||||
if sitename == "" {
|
if sitename == "" {
|
||||||
return c.Status(400).JSON(models.APIError{
|
return c.Status(400).JSON(models.APIError{
|
||||||
Error: "Missing 'site' query parameter",
|
Message: "Missing 'site' query parameter",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var siteConfig *config.SiteConfig
|
var siteConfig *config.SiteConfig
|
||||||
for _, site := range cfg.Sites {
|
for _, site := range h.Cfg.Sites {
|
||||||
if site.Name == sitename {
|
if site.Name == sitename {
|
||||||
siteConfig = &site
|
siteConfig = &site
|
||||||
break
|
break
|
||||||
@@ -32,14 +39,14 @@ func NewUpdateSiteHandler(cfg *config.Config, envCfg *envconfig.EnvConfig) fiber
|
|||||||
|
|
||||||
if siteConfig == nil {
|
if siteConfig == nil {
|
||||||
return c.Status(404).JSON(models.APIError{
|
return c.Status(404).JSON(models.APIError{
|
||||||
Error: "Site not found",
|
Message: "Site not found",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
deployToken := siteConfig.DeployToken
|
deployToken := siteConfig.DeployToken
|
||||||
if deployToken == "" {
|
if deployToken == "" {
|
||||||
return c.Status(500).JSON(models.APIError{
|
return c.Status(500).JSON(models.APIError{
|
||||||
Error: "Deploy token not configured for this site",
|
Message: "Deploy token not configured for this site",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,20 +56,20 @@ func NewUpdateSiteHandler(cfg *config.Config, envCfg *envconfig.EnvConfig) fiber
|
|||||||
}
|
}
|
||||||
if providedToken == "" {
|
if providedToken == "" {
|
||||||
return c.Status(401).JSON(models.APIError{
|
return c.Status(401).JSON(models.APIError{
|
||||||
Error: "Missing Authorization header",
|
Message: "Missing Authorization header",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if subtle.ConstantTimeCompare([]byte(providedToken), []byte(deployToken)) != 1 {
|
if subtle.ConstantTimeCompare([]byte(providedToken), []byte(deployToken)) != 1 {
|
||||||
return c.Status(403).JSON(models.APIError{
|
return c.Status(403).JSON(models.APIError{
|
||||||
Error: "Invalid deploy token",
|
Message: "Invalid deploy token",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
sitePath := filepath.Join(envCfg.StoragePath, siteConfig.Name)
|
sitePath := filepath.Join(h.EnvCfg.StoragePath, siteConfig.Name)
|
||||||
if _, err := filepath.Abs(sitePath); err != nil {
|
if _, err := filepath.Abs(sitePath); err != nil {
|
||||||
return c.Status(500).JSON(models.APIError{
|
return c.Status(500).JSON(models.APIError{
|
||||||
Error: "Failed to resolve site path",
|
Message: "Failed to resolve site path",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,16 +77,15 @@ func NewUpdateSiteHandler(cfg *config.Config, envCfg *envconfig.EnvConfig) fiber
|
|||||||
siteConfig.Owner,
|
siteConfig.Owner,
|
||||||
siteConfig.Repo,
|
siteConfig.Repo,
|
||||||
siteConfig.Branch,
|
siteConfig.Branch,
|
||||||
envCfg.GithubPat,
|
h.EnvCfg.GithubPat,
|
||||||
sitePath,
|
sitePath,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(500).JSON(models.APIError{
|
return c.Status(500).JSON(models.APIError{
|
||||||
Error: "Failed to deploy site: " + err.Error(),
|
Message: "Failed to deploy site: " + err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.SendStatus(201)
|
return c.SendStatus(201)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
type APIError struct {
|
type APIError struct {
|
||||||
Error string `json:"error"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type ForwardRule struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
Destination string `json:"destination"`
|
||||||
|
StatusCode int `json:"status_code"`
|
||||||
|
Regex bool `json:"regex"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Header struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomHeaders struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
Headers []Header `json:"headers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Site struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
GitServer string `json:"git_server"`
|
||||||
|
Owner string `json:"owner"`
|
||||||
|
Repository string `json:"repository"`
|
||||||
|
Branch string `json:"branch"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
DeployToken string `json:"deploy_token"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Spa bool `json:"spa"`
|
||||||
|
NotFoundFile string `json:"not_found_file"`
|
||||||
|
ForwardRules []ForwardRule `json:"forward_rules"`
|
||||||
|
CustomHeaders []CustomHeaders `json:"custom_headers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetAllSitesResponse struct {
|
||||||
|
Sites []Site `json:"sites"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import "quay/app/models"
|
||||||
|
|
||||||
|
type SiteRepository interface {
|
||||||
|
GetSite(id string) (*models.Site, error)
|
||||||
|
ListSites() ([]models.Site, error)
|
||||||
|
CreateSite(s *models.Site) error
|
||||||
|
UpdateSite(s *models.Site) error
|
||||||
|
DeleteSite(id string) error
|
||||||
|
GetForwardRule(id string) (*models.ForwardRule, error)
|
||||||
|
CreateForwardRule(siteID string, fr *models.ForwardRule) error
|
||||||
|
UpdateForwardRule(fr *models.ForwardRule) error
|
||||||
|
DeleteForwardRule(id string) error
|
||||||
|
GetCustomHeaders(id string) (*models.CustomHeaders, error)
|
||||||
|
CreateCustomHeaders(siteID string, ch *models.CustomHeaders) error
|
||||||
|
UpdateCustomHeaders(ch *models.CustomHeaders) error
|
||||||
|
DeleteCustomHeaders(id string) error
|
||||||
|
}
|
||||||
+13
-2
@@ -1,20 +1,31 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"log"
|
"log"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"quay/app/handlers"
|
"quay/app/handlers"
|
||||||
"quay/internal/config"
|
"quay/internal/config"
|
||||||
|
"quay/internal/database"
|
||||||
"quay/internal/envconfig"
|
"quay/internal/envconfig"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Register(app *fiber.App, cfg *config.Config, envCfg *envconfig.EnvConfig) {
|
func Register(app *fiber.App, cfg *config.Config, envCfg *envconfig.EnvConfig, db *sql.DB) {
|
||||||
|
siteRepository := database.NewSQLiteSiteRepository(db)
|
||||||
|
|
||||||
|
updateSiteHandler := handlers.NewUpdateSiteHandler(cfg, envCfg)
|
||||||
|
siteHandler := handlers.NewSiteHandler(siteRepository)
|
||||||
|
|
||||||
api := app.Group("/api")
|
api := app.Group("/api")
|
||||||
api.Get("/health", handlers.HealthCheck)
|
api.Get("/health", handlers.HealthCheck)
|
||||||
|
|
||||||
api.Post("/update", handlers.NewUpdateSiteHandler(cfg, envCfg))
|
api.Post("/update", updateSiteHandler.PostUpdate)
|
||||||
|
|
||||||
|
api.Get("/sites", siteHandler.GetSites)
|
||||||
|
api.Get("/sites/:id", siteHandler.GetSite)
|
||||||
|
api.Post("/sites", siteHandler.PostSite)
|
||||||
|
|
||||||
storagePath, err := filepath.Abs(envCfg.StoragePath)
|
storagePath, err := filepath.Abs(envCfg.StoragePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ require (
|
|||||||
github.com/klauspost/compress v1.18.4 // indirect
|
github.com/klauspost/compress v1.18.4 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.38 // indirect
|
||||||
github.com/philhofer/fwd v1.2.0 // indirect
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
github.com/tinylib/msgp v1.6.3 // indirect
|
github.com/tinylib/msgp v1.6.3 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
|
|||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
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/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||||
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
|
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
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,
|
||||||
|
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,411 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"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) 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 FROM custom_headers WHERE id = ?`, id)
|
||||||
|
|
||||||
|
var ch models.CustomHeaders
|
||||||
|
if err := row.Scan(&ch.ID, &ch.Source); 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
|
||||||
|
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=? WHERE id=?`, ch.Source, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 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
|
||||||
|
if err := rows.Scan(&ch.ID, &ch.Source); err != nil {
|
||||||
|
rows.Close()
|
||||||
|
return nil, fmt.Errorf("list custom headers scan: %w", err)
|
||||||
|
}
|
||||||
|
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) VALUES (?, ?, ?)`,
|
||||||
|
ch.ID, siteID, ch.Source,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ type EnvConfig struct {
|
|||||||
ConfigDir string
|
ConfigDir string
|
||||||
GithubPat string
|
GithubPat string
|
||||||
StoragePath string
|
StoragePath string
|
||||||
|
DatabasePath string
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load() EnvConfig {
|
func Load() EnvConfig {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"log"
|
"log"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"quay/app/routes"
|
"quay/app/routes"
|
||||||
"quay/internal/config"
|
"quay/internal/config"
|
||||||
|
"quay/internal/database"
|
||||||
"quay/internal/envconfig"
|
"quay/internal/envconfig"
|
||||||
"quay/internal/fiberconfig"
|
"quay/internal/fiberconfig"
|
||||||
|
|
||||||
@@ -23,10 +25,28 @@ func main() {
|
|||||||
panic("Failed to load config: " + err.Error())
|
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()
|
app := fiber.New()
|
||||||
|
|
||||||
fiberconfig.Setup(app)
|
fiberconfig.Setup(app)
|
||||||
routes.Register(app, cfg, &envCfg)
|
routes.Register(app, cfg, &envCfg, db)
|
||||||
|
|
||||||
log.Fatal(app.Listen(":" + envCfg.Port))
|
log.Fatal(app.Listen(":" + envCfg.Port))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: oklch(1.0000 0 0);
|
||||||
|
--foreground: oklch(0.3211 0 0);
|
||||||
|
--card: oklch(1.0000 0 0);
|
||||||
|
--card-foreground: oklch(0.3211 0 0);
|
||||||
|
--popover: oklch(1.0000 0 0);
|
||||||
|
--popover-foreground: oklch(0.3211 0 0);
|
||||||
|
--primary: oklch(0.6225 0.2041 259.9027);
|
||||||
|
--primary-foreground: oklch(1.0000 0 0);
|
||||||
|
--secondary: oklch(0.9665 0.0045 258.3247);
|
||||||
|
--secondary-foreground: oklch(0.4419 0.0375 257.2811);
|
||||||
|
--muted: oklch(0.9846 0.0017 247.8389);
|
||||||
|
--muted-foreground: oklch(0.5471 0.0321 263.2921);
|
||||||
|
--accent: oklch(0.9510 0.0267 237.5723);
|
||||||
|
--accent-foreground: oklch(0.3742 0.1844 263.9420);
|
||||||
|
--destructive: oklch(0.6496 0.2362 26.9032);
|
||||||
|
--destructive-foreground: oklch(1.0000 0 0);
|
||||||
|
--border: oklch(0.9271 0.0075 260.7315);
|
||||||
|
--input: oklch(0.9271 0.0075 260.7315);
|
||||||
|
--ring: oklch(0.6225 0.2041 259.9027);
|
||||||
|
--chart-1: oklch(0.6225 0.2041 259.9027);
|
||||||
|
--chart-2: oklch(0.5469 0.2507 262.8085);
|
||||||
|
--chart-3: oklch(0.4902 0.2693 263.7106);
|
||||||
|
--chart-4: oklch(0.4234 0.2370 263.9162);
|
||||||
|
--chart-5: oklch(0.3742 0.1844 263.9420);
|
||||||
|
--sidebar: oklch(0.9846 0.0017 247.8389);
|
||||||
|
--sidebar-foreground: oklch(0.3211 0 0);
|
||||||
|
--sidebar-primary: oklch(0.6225 0.2041 259.9027);
|
||||||
|
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||||
|
--sidebar-accent: oklch(0.9510 0.0267 237.5723);
|
||||||
|
--sidebar-accent-foreground: oklch(0.3742 0.1844 263.9420);
|
||||||
|
--sidebar-border: oklch(0.9271 0.0075 260.7315);
|
||||||
|
--sidebar-ring: oklch(0.6225 0.2041 259.9027);
|
||||||
|
--font-sans: Inter, sans-serif;
|
||||||
|
--font-serif: Source Serif 4, serif;
|
||||||
|
--font-mono: JetBrains Mono, monospace;
|
||||||
|
--radius: 1.15rem;
|
||||||
|
--shadow-x: 0;
|
||||||
|
--shadow-y: 1px;
|
||||||
|
--shadow-blur: 3px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0.1;
|
||||||
|
--shadow-color: oklch(0 0 0);
|
||||||
|
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||||
|
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||||
|
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||||
|
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||||
|
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
|
||||||
|
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
|
||||||
|
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
|
||||||
|
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
|
||||||
|
--tracking-normal: 0em;
|
||||||
|
--spacing: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.2046 0 0);
|
||||||
|
--foreground: oklch(0.9219 0 0);
|
||||||
|
--card: oklch(0.2686 0 0);
|
||||||
|
--card-foreground: oklch(0.9219 0 0);
|
||||||
|
--popover: oklch(0.2686 0 0);
|
||||||
|
--popover-foreground: oklch(0.9219 0 0);
|
||||||
|
--primary: oklch(0.6225 0.2041 259.9027);
|
||||||
|
--primary-foreground: oklch(1.0000 0 0);
|
||||||
|
--secondary: oklch(0.2686 0 0);
|
||||||
|
--secondary-foreground: oklch(0.9219 0 0);
|
||||||
|
--muted: oklch(0.2393 0 0);
|
||||||
|
--muted-foreground: oklch(0.7155 0 0);
|
||||||
|
--accent: oklch(0.5802 0.1915 259.7416);
|
||||||
|
--accent-foreground: oklch(0.8820 0.0588 253.9688);
|
||||||
|
--destructive: oklch(0.6496 0.2362 26.9032);
|
||||||
|
--destructive-foreground: oklch(1.0000 0 0);
|
||||||
|
--border: oklch(0.3715 0 0);
|
||||||
|
--input: oklch(0.3715 0 0);
|
||||||
|
--ring: oklch(0.6225 0.2041 259.9027);
|
||||||
|
--chart-1: oklch(0.7122 0.1526 254.9868);
|
||||||
|
--chart-2: oklch(0.6225 0.2041 259.9027);
|
||||||
|
--chart-3: oklch(0.5739 0.2334 262.7735);
|
||||||
|
--chart-4: oklch(0.6225 0.2041 259.9027);
|
||||||
|
--chart-5: oklch(0.6225 0.2041 259.9027);
|
||||||
|
--sidebar: oklch(0.2046 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.9219 0 0);
|
||||||
|
--sidebar-primary: oklch(0.6225 0.2041 259.9027);
|
||||||
|
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||||
|
--sidebar-accent: oklch(0.6225 0.2041 259.9027);
|
||||||
|
--sidebar-accent-foreground: oklch(0.8820 0.0588 253.9688);
|
||||||
|
--sidebar-border: oklch(0.3715 0 0);
|
||||||
|
--sidebar-ring: oklch(0.6225 0.2041 259.9027);
|
||||||
|
--font-sans: Inter, sans-serif;
|
||||||
|
--font-serif: Source Serif 4, serif;
|
||||||
|
--font-mono: JetBrains Mono, monospace;
|
||||||
|
--radius: 1.15rem;
|
||||||
|
--shadow-x: 0;
|
||||||
|
--shadow-y: 1px;
|
||||||
|
--shadow-blur: 3px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0.1;
|
||||||
|
--shadow-color: oklch(0 0 0);
|
||||||
|
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||||
|
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
|
||||||
|
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||||
|
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
|
||||||
|
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
|
||||||
|
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
|
||||||
|
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
|
||||||
|
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
|
||||||
|
--font-sans: var(--font-sans);
|
||||||
|
--font-mono: var(--font-mono);
|
||||||
|
--font-serif: var(--font-serif);
|
||||||
|
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
|
||||||
|
--shadow-2xs: var(--shadow-2xs);
|
||||||
|
--shadow-xs: var(--shadow-xs);
|
||||||
|
--shadow-sm: var(--shadow-sm);
|
||||||
|
--shadow: var(--shadow);
|
||||||
|
--shadow-md: var(--shadow-md);
|
||||||
|
--shadow-lg: var(--shadow-lg);
|
||||||
|
--shadow-xl: var(--shadow-xl);
|
||||||
|
--shadow-2xl: var(--shadow-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user