mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
添加代理路由和配置版本的控制器与服务,更新数据库模型,完善API路由
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -26,7 +27,9 @@ func printHelp() {
|
||||
}
|
||||
|
||||
func init() {
|
||||
flag.Parse()
|
||||
if !strings.HasSuffix(os.Args[0], ".test") {
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
if *PrintVersion {
|
||||
fmt.Println(Version)
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"gin-template/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func GetConfigVersions(c *gin.Context) {
|
||||
versions, err := service.ListConfigVersions()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": versions,
|
||||
})
|
||||
}
|
||||
|
||||
func GetActiveConfigVersion(c *gin.Context) {
|
||||
version, err := service.GetActiveConfigVersion()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "当前没有激活版本",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": version,
|
||||
})
|
||||
}
|
||||
|
||||
func PublishConfigVersion(c *gin.Context) {
|
||||
username := c.GetString("username")
|
||||
result, err := service.PublishConfigVersion(username)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": result.Version,
|
||||
})
|
||||
}
|
||||
|
||||
func ActivateConfigVersion(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
version, err := service.ActivateConfigVersion(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": version,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"gin-template/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func GetProxyRoutes(c *gin.Context) {
|
||||
routes, err := service.ListProxyRoutes()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": routes,
|
||||
})
|
||||
}
|
||||
|
||||
func CreateProxyRoute(c *gin.Context) {
|
||||
var input service.ProxyRouteInput
|
||||
if err := json.NewDecoder(c.Request.Body).Decode(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
route, err := service.CreateProxyRoute(input)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": route,
|
||||
})
|
||||
}
|
||||
|
||||
func UpdateProxyRoute(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
var input service.ProxyRouteInput
|
||||
if err = json.NewDecoder(c.Request.Body).Decode(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
route, err := service.UpdateProxyRoute(uint(id), input)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": route,
|
||||
})
|
||||
}
|
||||
|
||||
func DeleteProxyRoute(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err = service.DeleteProxyRoute(uint(id)); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type ConfigVersion struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Version string `json:"version" gorm:"uniqueIndex;size:32;not null"`
|
||||
SnapshotJSON string `json:"snapshot_json" gorm:"type:text;not null"`
|
||||
RenderedConfig string `json:"rendered_config" gorm:"type:text;not null"`
|
||||
Checksum string `json:"checksum" gorm:"size:64;not null"`
|
||||
IsActive bool `json:"is_active" gorm:"not null;default:false;index"`
|
||||
CreatedBy string `json:"created_by" gorm:"size:64;not null"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func ListConfigVersions() (versions []*ConfigVersion, err error) {
|
||||
err = DB.Order("id desc").Find(&versions).Error
|
||||
return versions, err
|
||||
}
|
||||
|
||||
func GetConfigVersionByID(id uint) (*ConfigVersion, error) {
|
||||
version := &ConfigVersion{}
|
||||
err := DB.First(version, id).Error
|
||||
return version, err
|
||||
}
|
||||
|
||||
func GetActiveConfigVersion() (*ConfigVersion, error) {
|
||||
version := &ConfigVersion{}
|
||||
err := DB.Where("is_active = ?", true).Order("id desc").First(version).Error
|
||||
return version, err
|
||||
}
|
||||
@@ -64,6 +64,14 @@ func InitDB() (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = db.AutoMigrate(&ProxyRoute{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = db.AutoMigrate(&ConfigVersion{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = createRootAccountIfNeed()
|
||||
return err
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type ProxyRoute struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Domain string `json:"domain" gorm:"uniqueIndex;size:255;not null"`
|
||||
OriginURL string `json:"origin_url" gorm:"size:2048;not null"`
|
||||
Enabled bool `json:"enabled" gorm:"not null;default:true"`
|
||||
Remark string `json:"remark" gorm:"size:255"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func ListProxyRoutes() (routes []*ProxyRoute, err error) {
|
||||
err = DB.Order("id desc").Find(&routes).Error
|
||||
return routes, err
|
||||
}
|
||||
|
||||
func GetEnabledProxyRoutes() (routes []*ProxyRoute, err error) {
|
||||
err = DB.Where("enabled = ?", true).Order("domain asc").Find(&routes).Error
|
||||
return routes, err
|
||||
}
|
||||
|
||||
func GetProxyRouteByID(id uint) (*ProxyRoute, error) {
|
||||
route := &ProxyRoute{}
|
||||
err := DB.First(route, id).Error
|
||||
return route, err
|
||||
}
|
||||
|
||||
func (route *ProxyRoute) Insert() error {
|
||||
return DB.Create(route).Error
|
||||
}
|
||||
|
||||
func (route *ProxyRoute) Update() error {
|
||||
return DB.Model(route).Select("domain", "origin_url", "enabled", "remark").Updates(route).Error
|
||||
}
|
||||
|
||||
func (route *ProxyRoute) Delete() error {
|
||||
return DB.Delete(route).Error
|
||||
}
|
||||
@@ -62,5 +62,21 @@ func SetApiRouter(router *gin.Engine) {
|
||||
fileRoute.POST("/", middleware.UploadRateLimit(), controller.UploadFile)
|
||||
fileRoute.DELETE("/:id", controller.DeleteFile)
|
||||
}
|
||||
proxyRoute := apiRouter.Group("/proxy-routes")
|
||||
proxyRoute.Use(middleware.AdminAuth())
|
||||
{
|
||||
proxyRoute.GET("/", controller.GetProxyRoutes)
|
||||
proxyRoute.POST("/", controller.CreateProxyRoute)
|
||||
proxyRoute.PUT("/:id", controller.UpdateProxyRoute)
|
||||
proxyRoute.DELETE("/:id", controller.DeleteProxyRoute)
|
||||
}
|
||||
configVersionRoute := apiRouter.Group("/config-versions")
|
||||
configVersionRoute.Use(middleware.AdminAuth())
|
||||
{
|
||||
configVersionRoute.GET("/", controller.GetConfigVersions)
|
||||
configVersionRoute.GET("/active", controller.GetActiveConfigVersion)
|
||||
configVersionRoute.POST("/publish", controller.PublishConfigVersion)
|
||||
configVersionRoute.PUT("/:id/activate", controller.ActivateConfigVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
package router_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"gin-template/common"
|
||||
"gin-template/model"
|
||||
"gin-template/router"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type apiResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
func TestPhase1PublishLifecycle(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
common.RedisEnabled = false
|
||||
setupTestDB(t)
|
||||
|
||||
engine := gin.New()
|
||||
engine.Use(sessions.Sessions("session", cookie.NewStore([]byte("test-secret"))))
|
||||
router.SetApiRouter(engine)
|
||||
|
||||
token := prepareRootToken(t)
|
||||
|
||||
createBody := map[string]any{
|
||||
"domain": "app.example.com",
|
||||
"origin_url": "https://origin-a.internal",
|
||||
"enabled": true,
|
||||
"remark": "primary route",
|
||||
}
|
||||
resp := performJSONRequest(t, engine, token, http.MethodPost, "/api/proxy-routes/", createBody)
|
||||
var createdRoute model.ProxyRoute
|
||||
decodeResponseData(t, resp, &createdRoute)
|
||||
if createdRoute.Domain != "app.example.com" {
|
||||
t.Fatalf("unexpected created route domain: %s", createdRoute.Domain)
|
||||
}
|
||||
|
||||
resp = performJSONRequest(t, engine, token, http.MethodGet, "/api/proxy-routes/", nil)
|
||||
var routes []model.ProxyRoute
|
||||
decodeResponseData(t, resp, &routes)
|
||||
if len(routes) != 1 {
|
||||
t.Fatalf("expected 1 route, got %d", len(routes))
|
||||
}
|
||||
|
||||
resp = performJSONRequest(t, engine, token, http.MethodPost, "/api/config-versions/publish", nil)
|
||||
var version1 model.ConfigVersion
|
||||
decodeResponseData(t, resp, &version1)
|
||||
if !version1.IsActive {
|
||||
t.Fatal("expected published version to be active")
|
||||
}
|
||||
if version1.SnapshotJSON == "" || version1.RenderedConfig == "" || version1.Checksum == "" {
|
||||
t.Fatal("expected published version to contain snapshot, rendered config and checksum")
|
||||
}
|
||||
|
||||
initialSnapshot := version1.SnapshotJSON
|
||||
initialRendered := version1.RenderedConfig
|
||||
|
||||
updateBody := map[string]any{
|
||||
"domain": "app.example.com",
|
||||
"origin_url": "https://origin-b.internal",
|
||||
"enabled": true,
|
||||
"remark": "updated route",
|
||||
}
|
||||
routePath := "/api/proxy-routes/" + toString(createdRoute.ID)
|
||||
resp = performJSONRequest(t, engine, token, http.MethodPut, routePath, updateBody)
|
||||
decodeResponseData(t, resp, &createdRoute)
|
||||
if createdRoute.OriginURL != "https://origin-b.internal" {
|
||||
t.Fatalf("unexpected updated route origin: %s", createdRoute.OriginURL)
|
||||
}
|
||||
|
||||
resp = performJSONRequest(t, engine, token, http.MethodPost, "/api/config-versions/publish", nil)
|
||||
var version2 model.ConfigVersion
|
||||
decodeResponseData(t, resp, &version2)
|
||||
if version2.ID == version1.ID {
|
||||
t.Fatal("expected a new version record")
|
||||
}
|
||||
|
||||
resp = performJSONRequest(t, engine, token, http.MethodGet, "/api/config-versions/", nil)
|
||||
var versions []model.ConfigVersion
|
||||
decodeResponseData(t, resp, &versions)
|
||||
if len(versions) != 2 {
|
||||
t.Fatalf("expected 2 versions, got %d", len(versions))
|
||||
}
|
||||
|
||||
activeResp := performJSONRequest(t, engine, token, http.MethodGet, "/api/config-versions/active", nil)
|
||||
var activeVersion model.ConfigVersion
|
||||
decodeResponseData(t, activeResp, &activeVersion)
|
||||
if activeVersion.ID != version2.ID {
|
||||
t.Fatalf("expected version %d active, got %d", version2.ID, activeVersion.ID)
|
||||
}
|
||||
|
||||
activatePath := "/api/config-versions/" + toString(version1.ID) + "/activate"
|
||||
resp = performJSONRequest(t, engine, token, http.MethodPut, activatePath, nil)
|
||||
decodeResponseData(t, resp, &activeVersion)
|
||||
if activeVersion.ID != version1.ID || !activeVersion.IsActive {
|
||||
t.Fatal("expected version1 to become active after rollback activation")
|
||||
}
|
||||
|
||||
var storedVersion1 model.ConfigVersion
|
||||
if err := model.DB.First(&storedVersion1, version1.ID).Error; err != nil {
|
||||
t.Fatalf("failed to query version1: %v", err)
|
||||
}
|
||||
if storedVersion1.SnapshotJSON != initialSnapshot {
|
||||
t.Fatal("expected version1 snapshot to remain immutable")
|
||||
}
|
||||
if storedVersion1.RenderedConfig != initialRendered {
|
||||
t.Fatal("expected version1 rendered config to remain immutable")
|
||||
}
|
||||
|
||||
deletePath := "/api/proxy-routes/" + toString(createdRoute.ID)
|
||||
resp = performJSONRequest(t, engine, token, http.MethodDelete, deletePath, nil)
|
||||
if !resp.Success {
|
||||
t.Fatalf("expected delete route success, got: %s", resp.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func setupTestDB(t *testing.T) {
|
||||
t.Helper()
|
||||
dbPath := filepath.Join(t.TempDir(), "phase1.db")
|
||||
common.SQLitePath = dbPath
|
||||
if err := model.InitDB(); err != nil {
|
||||
t.Fatalf("failed to init db: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := model.CloseDB(); err != nil {
|
||||
t.Fatalf("failed to close db: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func prepareRootToken(t *testing.T) string {
|
||||
t.Helper()
|
||||
user := &model.User{Username: "root"}
|
||||
if err := user.FillUserByUsername(); err != nil {
|
||||
t.Fatalf("failed to load root user: %v", err)
|
||||
}
|
||||
user.Token = "phase1-test-token"
|
||||
if err := model.DB.Model(user).Update("token", user.Token).Error; err != nil {
|
||||
t.Fatalf("failed to set root token: %v", err)
|
||||
}
|
||||
return user.Token
|
||||
}
|
||||
|
||||
func performJSONRequest(t *testing.T, engine http.Handler, token string, method string, path string, body any) apiResponse {
|
||||
t.Helper()
|
||||
var payload []byte
|
||||
var err error
|
||||
if body != nil {
|
||||
payload, err = json.Marshal(body)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal request body: %v", err)
|
||||
}
|
||||
}
|
||||
req := httptest.NewRequest(method, path, bytes.NewReader(payload))
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
recorder := httptest.NewRecorder()
|
||||
engine.ServeHTTP(recorder, req)
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("unexpected status %d for %s %s: %s", recorder.Code, method, path, recorder.Body.String())
|
||||
}
|
||||
var resp apiResponse
|
||||
if err = json.Unmarshal(recorder.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
if !resp.Success {
|
||||
t.Fatalf("request %s %s failed: %s", method, path, resp.Message)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func decodeResponseData(t *testing.T, resp apiResponse, target any) {
|
||||
t.Helper()
|
||||
if err := json.Unmarshal(resp.Data, target); err != nil {
|
||||
t.Fatalf("failed to decode response data: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func toString(id uint) string {
|
||||
return strconv.FormatUint(uint64(id), 10)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gin-template/model"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ReleaseResult struct {
|
||||
Version *model.ConfigVersion `json:"version"`
|
||||
Routes []*model.ProxyRoute `json:"routes"`
|
||||
}
|
||||
|
||||
func ListConfigVersions() ([]*model.ConfigVersion, error) {
|
||||
return model.ListConfigVersions()
|
||||
}
|
||||
|
||||
func GetActiveConfigVersion() (*model.ConfigVersion, error) {
|
||||
return model.GetActiveConfigVersion()
|
||||
}
|
||||
|
||||
func PublishConfigVersion(createdBy string) (*ReleaseResult, error) {
|
||||
routes, err := model.GetEnabledProxyRoutes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(routes) == 0 {
|
||||
return nil, errors.New("没有可发布的启用规则")
|
||||
}
|
||||
snapshotJSON, err := renderSnapshot(routes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
renderedConfig := renderNginxConfig(routes)
|
||||
version, err := nextVersionNumber(time.Now())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
record := &model.ConfigVersion{
|
||||
Version: version,
|
||||
SnapshotJSON: snapshotJSON,
|
||||
RenderedConfig: renderedConfig,
|
||||
Checksum: checksum(renderedConfig),
|
||||
IsActive: true,
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
err = model.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&model.ConfigVersion{}).Where("is_active = ?", true).Update("is_active", false).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
if isUniqueConstraintError(err) {
|
||||
return nil, errors.New("版本号生成冲突,请重试")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &ReleaseResult{
|
||||
Version: record,
|
||||
Routes: routes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ActivateConfigVersion(id uint) (*model.ConfigVersion, error) {
|
||||
version, err := model.GetConfigVersionByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = model.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&model.ConfigVersion{}).Where("is_active = ?", true).Update("is_active", false).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(version).Update("is_active", true).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
version.IsActive = true
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func renderSnapshot(routes []*model.ProxyRoute) (string, error) {
|
||||
type snapshotRoute struct {
|
||||
Domain string `json:"domain"`
|
||||
OriginURL string `json:"origin_url"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Remark string `json:"remark,omitempty"`
|
||||
}
|
||||
items := make([]snapshotRoute, 0, len(routes))
|
||||
for _, route := range routes {
|
||||
items = append(items, snapshotRoute{
|
||||
Domain: route.Domain,
|
||||
OriginURL: route.OriginURL,
|
||||
Enabled: route.Enabled,
|
||||
Remark: route.Remark,
|
||||
})
|
||||
}
|
||||
data, err := json.Marshal(items)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func renderNginxConfig(routes []*model.ProxyRoute) string {
|
||||
var builder strings.Builder
|
||||
builder.WriteString("# This file is generated by ATSFlare. Do not edit manually.\n")
|
||||
for _, route := range routes {
|
||||
builder.WriteString(fmt.Sprintf("server {\n listen 80;\n server_name %s;\n\n location / {\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_pass %s;\n }\n}\n\n", route.Domain, route.OriginURL))
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func checksum(content string) string {
|
||||
sum := sha256.Sum256([]byte(content))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func nextVersionNumber(now time.Time) (string, error) {
|
||||
prefix := now.Format("20060102")
|
||||
var count int64
|
||||
if err := model.DB.Model(&model.ConfigVersion{}).Where("version LIKE ?", prefix+"-%").Count(&count).Error; err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s-%03d", prefix, count+1), nil
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"gin-template/model"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ProxyRouteInput struct {
|
||||
Domain string `json:"domain"`
|
||||
OriginURL string `json:"origin_url"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
func ListProxyRoutes() ([]*model.ProxyRoute, error) {
|
||||
return model.ListProxyRoutes()
|
||||
}
|
||||
|
||||
func CreateProxyRoute(input ProxyRouteInput) (*model.ProxyRoute, error) {
|
||||
route, err := buildProxyRoute(nil, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = route.Insert(); err != nil {
|
||||
if isUniqueConstraintError(err) {
|
||||
return nil, errors.New("域名已存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return route, nil
|
||||
}
|
||||
|
||||
func UpdateProxyRoute(id uint, input ProxyRouteInput) (*model.ProxyRoute, error) {
|
||||
route, err := model.GetProxyRouteByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
route, err = buildProxyRoute(route, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = route.Update(); err != nil {
|
||||
if isUniqueConstraintError(err) {
|
||||
return nil, errors.New("域名已存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return route, nil
|
||||
}
|
||||
|
||||
func DeleteProxyRoute(id uint) error {
|
||||
route, err := model.GetProxyRouteByID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return route.Delete()
|
||||
}
|
||||
|
||||
func buildProxyRoute(route *model.ProxyRoute, input ProxyRouteInput) (*model.ProxyRoute, error) {
|
||||
domain := strings.ToLower(strings.TrimSpace(input.Domain))
|
||||
originURL := strings.TrimSpace(input.OriginURL)
|
||||
remark := strings.TrimSpace(input.Remark)
|
||||
if domain == "" {
|
||||
return nil, errors.New("域名不能为空")
|
||||
}
|
||||
if strings.Contains(domain, "://") || strings.Contains(domain, "/") {
|
||||
return nil, errors.New("域名格式不合法")
|
||||
}
|
||||
if err := validateOriginURL(originURL); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if route == nil {
|
||||
route = &model.ProxyRoute{}
|
||||
}
|
||||
route.Domain = domain
|
||||
route.OriginURL = originURL
|
||||
route.Enabled = input.Enabled
|
||||
route.Remark = remark
|
||||
return route, nil
|
||||
}
|
||||
|
||||
func validateOriginURL(raw string) error {
|
||||
if raw == "" {
|
||||
return errors.New("源站地址不能为空")
|
||||
}
|
||||
parsed, err := url.ParseRequestURI(raw)
|
||||
if err != nil {
|
||||
return errors.New("源站地址格式不合法")
|
||||
}
|
||||
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
||||
return errors.New("源站地址必须以 http:// 或 https:// 开头")
|
||||
}
|
||||
if parsed.Host == "" {
|
||||
return errors.New("源站地址格式不合法")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isUniqueConstraintError(err error) bool {
|
||||
return err != nil && strings.Contains(strings.ToLower(err.Error()), "unique")
|
||||
}
|
||||
Reference in New Issue
Block a user