统一单入口 Go 工程与前端依赖目录,增加后台供应商配置、候选审核、计划预算、成交冲正、持仓复盘和站内通知。接入策略版本、Python 回测、可配置 A 股公开行情及 zap 文件轮转日志,补充迁移、契约与验证。
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"git.sundynix.cn/Blizzard/Veyra/internal/apperror"
|
||||
"git.sundynix.cn/Blizzard/Veyra/internal/model"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func bindBacktests(r *gin.Engine, o IdentityOptions) {
|
||||
g := r.Group("/api/admin/v1/backtests", func(c *gin.Context) { authenticate(c, o, "admin") })
|
||||
g.Use(func(c *gin.Context) {
|
||||
if o.Backtests == nil {
|
||||
fail(c, apperror.ErrUnavailable)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
})
|
||||
g.GET("", func(c *gin.Context) {
|
||||
v, e := o.Backtests.List(c.Request.Context(), currentUser(c))
|
||||
if e != nil {
|
||||
fail(c, e)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, v)
|
||||
})
|
||||
g.GET("/:id", func(c *gin.Context) {
|
||||
v, e := o.Backtests.Detail(c.Request.Context(), currentUser(c), c.Param("id"))
|
||||
if e != nil {
|
||||
fail(c, e)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, v)
|
||||
})
|
||||
g.POST("", func(c *gin.Context) {
|
||||
if !sameOrigin(c.Request) {
|
||||
fail(c, apperror.ErrForbidden)
|
||||
return
|
||||
}
|
||||
var input model.BacktestInput
|
||||
if !decodeLimit(c, &input, 256<<10) {
|
||||
return
|
||||
}
|
||||
v, e := o.Backtests.Create(c.Request.Context(), currentUser(c), input, c.Writer.Header().Get("X-Request-ID"))
|
||||
if e != nil {
|
||||
fail(c, e)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusAccepted, v)
|
||||
})
|
||||
g.POST("/from-market", func(c *gin.Context) {
|
||||
if !sameOrigin(c.Request) {
|
||||
fail(c, apperror.ErrForbidden)
|
||||
return
|
||||
}
|
||||
var input struct {
|
||||
StrategyVersionID string `json:"strategy_version_id"`
|
||||
Symbol string `json:"symbol"`
|
||||
Exchange string `json:"exchange"`
|
||||
Start string `json:"start"`
|
||||
End string `json:"end"`
|
||||
InitialCash string `json:"initial_cash"`
|
||||
}
|
||||
if !decode(c, &input) {
|
||||
return
|
||||
}
|
||||
v, e := o.Backtests.CreateMarket(c.Request.Context(), currentUser(c), input.StrategyVersionID, input.Symbol, input.Exchange, input.Start, input.End, input.InitialCash, c.Writer.Header().Get("X-Request-ID"))
|
||||
if e != nil {
|
||||
fail(c, e)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusAccepted, v)
|
||||
})
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"git.sundynix.cn/Blizzard/Veyra/internal/apperror"
|
||||
"git.sundynix.cn/Blizzard/Veyra/internal/model"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func bindCandidates(r *gin.Engine, options IdentityOptions) {
|
||||
for _, admin := range []bool{true, false} {
|
||||
path, audience := "/api/v1", "app"
|
||||
if admin {
|
||||
path, audience = "/api/admin/v1", "admin"
|
||||
}
|
||||
group := r.Group(path, func(c *gin.Context) { authenticate(c, options, audience) })
|
||||
group.Use(func(c *gin.Context) {
|
||||
if options.Candidates == nil {
|
||||
fail(c, apperror.ErrUnavailable)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
})
|
||||
group.GET("/candidates", func(c *gin.Context) {
|
||||
limit := 100
|
||||
if raw := c.Query("limit"); raw != "" {
|
||||
value, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
fail(c, apperror.ErrInvalidInput)
|
||||
return
|
||||
}
|
||||
limit = value
|
||||
}
|
||||
result, err := options.Candidates.List(c.Request.Context(), currentUser(c), admin, c.Query("status"), limit)
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, result)
|
||||
})
|
||||
group.GET("/candidates/:id", func(c *gin.Context) {
|
||||
result, err := options.Candidates.Detail(c.Request.Context(), currentUser(c), c.Param("id"), admin)
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, result)
|
||||
})
|
||||
if !admin {
|
||||
continue
|
||||
}
|
||||
group.Use(func(c *gin.Context) {
|
||||
if !sameOrigin(c.Request) {
|
||||
fail(c, apperror.ErrForbidden)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
})
|
||||
group.POST("/candidates", func(c *gin.Context) {
|
||||
var input model.CandidateDraftInput
|
||||
if !decode(c, &input) {
|
||||
return
|
||||
}
|
||||
result, err := options.Candidates.Create(c.Request.Context(), currentUser(c), "", c.Writer.Header().Get("X-Request-ID"), input, 0)
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusCreated, result)
|
||||
})
|
||||
group.POST("/candidates/:id/revisions", func(c *gin.Context) {
|
||||
var input model.CandidateRevisionInput
|
||||
if !decode(c, &input) {
|
||||
return
|
||||
}
|
||||
result, err := options.Candidates.Create(c.Request.Context(), currentUser(c), c.Param("id"), c.Writer.Header().Get("X-Request-ID"), input.CandidateDraftInput, input.ExpectedVersion)
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusCreated, result)
|
||||
})
|
||||
for endpoint, action := range map[string]string{"submit": "SUBMIT", "publish": "PUBLISH", "return": "RETURN", "withdraw": "WITHDRAW"} {
|
||||
group.POST("/candidates/:id/"+endpoint, func(c *gin.Context) {
|
||||
var input model.CandidateActionInput
|
||||
if !decode(c, &input) {
|
||||
return
|
||||
}
|
||||
result, err := options.Candidates.Transition(c.Request.Context(), currentUser(c), c.Param("id"), action, c.Writer.Header().Get("X-Request-ID"), input)
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,15 @@ type LoginLimiter interface {
|
||||
Login(context.Context, string, string) (bool, error)
|
||||
}
|
||||
type IdentityOptions struct {
|
||||
Market *service.MarketService
|
||||
Backtests *service.BacktestService
|
||||
Auth *service.AuthService
|
||||
Providers *service.ProviderService
|
||||
Candidates *service.CandidateService
|
||||
Plans *service.TradePlanService
|
||||
Fills *service.TradeFillService
|
||||
Notifications *service.NotificationService
|
||||
Strategies *service.StrategyService
|
||||
Accounts *service.AccountService
|
||||
Limiter LoginLimiter
|
||||
DevelopmentLogin bool
|
||||
@@ -209,7 +217,11 @@ func sameOrigin(r *http.Request) bool {
|
||||
}
|
||||
|
||||
func decode(c *gin.Context, value any) bool {
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 16<<10)
|
||||
return decodeLimit(c, value, 16<<10)
|
||||
}
|
||||
|
||||
func decodeLimit(c *gin.Context, value any, limit int64) bool {
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, limit)
|
||||
if !strings.HasPrefix(strings.ToLower(c.GetHeader("Content-Type")), "application/json") {
|
||||
failure(c, http.StatusUnsupportedMediaType, "INVALID_CONTENT_TYPE", "请使用 application/json。", false)
|
||||
return false
|
||||
@@ -237,13 +249,25 @@ func failure(c *gin.Context, status int, code, message string, retryable bool) {
|
||||
func fail(c *gin.Context, err error) {
|
||||
switch {
|
||||
case errors.Is(err, apperror.ErrInvalidInput):
|
||||
failure(c, http.StatusUnprocessableEntity, "INVALID_INPUT", "请检查输入内容;资金最多两位小数,密码为 12–72 字节。", false)
|
||||
failure(c, http.StatusUnprocessableEntity, "INVALID_INPUT", "请检查输入字段、必填项与格式。", false)
|
||||
case errors.Is(err, apperror.ErrUnauthenticated):
|
||||
failure(c, http.StatusUnauthorized, "UNAUTHENTICATED", "登录信息无效或会话已过期。", false)
|
||||
case errors.Is(err, apperror.ErrForbidden):
|
||||
failure(c, http.StatusForbidden, "FORBIDDEN", "无权执行此操作。", false)
|
||||
case errors.Is(err, apperror.ErrInsufficientCash):
|
||||
failure(c, http.StatusConflict, "INSUFFICIENT_CASH", "可用现金不足,请检查其他计划保留的预算。", false)
|
||||
case errors.Is(err, apperror.ErrInsufficientQuantity):
|
||||
failure(c, http.StatusConflict, "INSUFFICIENT_SELLABLE_QUANTITY", "可卖数量不足;请检查模拟可卖时间与剩余持仓。", false)
|
||||
case errors.Is(err, apperror.ErrCandidateUnavailable):
|
||||
failure(c, http.StatusConflict, "CANDIDATE_UNAVAILABLE", "候选已失效或版本已更新,请重新查看依据。", false)
|
||||
case errors.Is(err, apperror.ErrPreviewExpired):
|
||||
failure(c, http.StatusConflict, "PREVIEW_EXPIRED", "预算预览已过期,请重新预览后确认。", false)
|
||||
case errors.Is(err, apperror.ErrChronology):
|
||||
failure(c, http.StatusConflict, "FILL_CHRONOLOGY", "请按成交发生时间顺序录入;更早记录需先纠正后续成交。", false)
|
||||
case errors.Is(err, apperror.ErrDependentFill):
|
||||
failure(c, http.StatusConflict, "DEPENDENT_FILL", "后续成交依赖此记录,请先冲正最新成交。", false)
|
||||
case errors.Is(err, apperror.ErrConflict):
|
||||
failure(c, http.StatusConflict, "CONFLICT", "该登录名或账户类型已存在。", false)
|
||||
failure(c, http.StatusConflict, "CONFLICT", "记录已存在或版本已更新,请刷新后重试。", false)
|
||||
case errors.Is(err, apperror.ErrNotFound):
|
||||
failure(c, http.StatusNotFound, "NOT_FOUND", "记录不存在。", false)
|
||||
default:
|
||||
|
||||
@@ -2,8 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"go.uber.org/zap"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -18,7 +17,7 @@ func TestAuthenticationAndCSRFBoundaries(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
probe := func(context.Context) error { return nil }
|
||||
router := NewRouter(service.NewHealthService(probe, probe), slog.New(slog.NewTextHandler(io.Discard, nil)), IdentityOptions{Auth: auth, DevelopmentLogin: true})
|
||||
router := NewRouter(service.NewHealthService(probe, probe), zap.NewNop(), IdentityOptions{Auth: auth, DevelopmentLogin: true})
|
||||
for _, tc := range []struct {
|
||||
method, path, body, origin string
|
||||
status int
|
||||
@@ -43,7 +42,7 @@ func TestAuthenticationAndCSRFBoundaries(t *testing.T) {
|
||||
|
||||
func TestDevelopmentLoginDisabledByDefault(t *testing.T) {
|
||||
probe := func(context.Context) error { return nil }
|
||||
router := NewRouter(service.NewHealthService(probe, probe), slog.New(slog.NewTextHandler(io.Discard, nil)), IdentityOptions{})
|
||||
router := NewRouter(service.NewHealthService(probe, probe), zap.NewNop(), IdentityOptions{})
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", strings.NewReader(`{}`)))
|
||||
if rec.Code != http.StatusServiceUnavailable {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"git.sundynix.cn/Blizzard/Veyra/internal/apperror"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func bindMarket(r *gin.Engine, o IdentityOptions) {
|
||||
for _, audience := range []string{"admin", "app"} {
|
||||
prefix := "/api/v1"
|
||||
if audience == "admin" {
|
||||
prefix = "/api/admin/v1"
|
||||
}
|
||||
g := r.Group(prefix, func(c *gin.Context) { authenticate(c, o, audience) })
|
||||
g.Use(func(c *gin.Context) {
|
||||
if o.Market == nil {
|
||||
fail(c, apperror.ErrUnavailable)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
})
|
||||
g.GET("/instruments", func(c *gin.Context) {
|
||||
v, e := o.Market.List(c.Request.Context(), currentUser(c), c.Query("q"))
|
||||
if e != nil {
|
||||
fail(c, e)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, v)
|
||||
})
|
||||
g.GET("/market/history", func(c *gin.Context) {
|
||||
v, e := o.Market.History(c.Request.Context(), currentUser(c), c.Query("symbol"), c.Query("exchange"), c.Query("start"), c.Query("end"))
|
||||
if e != nil {
|
||||
fail(c, e)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, v)
|
||||
})
|
||||
if audience == "admin" {
|
||||
g.GET("/market/syncs", func(c *gin.Context) {
|
||||
v, e := o.Market.Tasks(c.Request.Context(), currentUser(c))
|
||||
if e != nil {
|
||||
fail(c, e)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, v)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"git.sundynix.cn/Blizzard/Veyra/internal/apperror"
|
||||
"git.sundynix.cn/Blizzard/Veyra/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func bindNotifications(r *gin.Engine, options IdentityOptions) {
|
||||
for _, admin := range []bool{true, false} {
|
||||
path, audience := "/api/v1", "app"
|
||||
if admin {
|
||||
path, audience = "/api/admin/v1", "admin"
|
||||
}
|
||||
group := r.Group(path, func(c *gin.Context) { authenticate(c, options, audience) })
|
||||
group.Use(func(c *gin.Context) {
|
||||
if options.Notifications == nil {
|
||||
fail(c, apperror.ErrUnavailable)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
})
|
||||
if admin {
|
||||
group.GET("/notification-tasks", func(c *gin.Context) {
|
||||
result, err := options.Notifications.Tasks(c.Request.Context(), currentUser(c))
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, result)
|
||||
})
|
||||
group.GET("/simulation-quotes", func(c *gin.Context) {
|
||||
result, err := options.Notifications.Quotes(c.Request.Context(), currentUser(c))
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, result)
|
||||
})
|
||||
group.POST("/simulation-quotes", func(c *gin.Context) {
|
||||
if !sameOrigin(c.Request) {
|
||||
fail(c, apperror.ErrForbidden)
|
||||
return
|
||||
}
|
||||
var input service.SimulationQuoteInput
|
||||
if !decode(c, &input) {
|
||||
return
|
||||
}
|
||||
result, err := options.Notifications.SaveQuote(c.Request.Context(), currentUser(c), input, c.Writer.Header().Get("X-Request-ID"))
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusCreated, result)
|
||||
})
|
||||
} else {
|
||||
group.GET("/notifications", func(c *gin.Context) {
|
||||
result, err := options.Notifications.List(c.Request.Context(), currentUser(c))
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, result)
|
||||
})
|
||||
group.POST("/notifications/:id/read", func(c *gin.Context) {
|
||||
var input struct{}
|
||||
if !decode(c, &input) {
|
||||
return
|
||||
}
|
||||
if err := options.Notifications.MarkRead(c.Request.Context(), currentUser(c), c.Param("id")); err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, gin.H{"read": true})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"git.sundynix.cn/Blizzard/Veyra/internal/apperror"
|
||||
"git.sundynix.cn/Blizzard/Veyra/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func bindProviders(r *gin.Engine, options IdentityOptions) {
|
||||
admin := r.Group("/api/admin/v1", func(c *gin.Context) { authenticate(c, options, "admin") })
|
||||
admin.Use(func(c *gin.Context) {
|
||||
if options.Providers == nil {
|
||||
fail(c, apperror.ErrUnavailable)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
})
|
||||
admin.GET("/providers", func(c *gin.Context) {
|
||||
list, err := options.Providers.List(c.Request.Context(), currentUser(c))
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, list)
|
||||
})
|
||||
admin.PUT("/providers/:key", func(c *gin.Context) {
|
||||
if !sameOrigin(c.Request) {
|
||||
fail(c, apperror.ErrForbidden)
|
||||
return
|
||||
}
|
||||
var input service.ProviderInput
|
||||
if !decode(c, &input) {
|
||||
return
|
||||
}
|
||||
config, err := options.Providers.Save(c.Request.Context(), currentUser(c), c.Param("key"), c.Writer.Header().Get("X-Request-ID"), input)
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, config)
|
||||
})
|
||||
admin.GET("/audit-events", func(c *gin.Context) {
|
||||
list, err := options.Providers.Audit(c.Request.Context(), currentUser(c))
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, list)
|
||||
})
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"go.uber.org/zap"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"git.sundynix.cn/Blizzard/Veyra/internal/service"
|
||||
)
|
||||
|
||||
func NewRouter(health *service.HealthService, logger *slog.Logger, identity IdentityOptions) http.Handler {
|
||||
func NewRouter(health *service.HealthService, logger *zap.Logger, identity IdentityOptions) http.Handler {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.New()
|
||||
r.Use(func(c *gin.Context) {
|
||||
@@ -29,10 +29,27 @@ func NewRouter(health *service.HealthService, logger *slog.Logger, identity Iden
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
if recover() != nil {
|
||||
logger.Error("request panic", "request_id", requestID)
|
||||
logger.Error("request panic", zap.String("request_id", requestID))
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": gin.H{"code": "INTERNAL_ERROR", "message": "Internal server error", "request_id": requestID}})
|
||||
}
|
||||
logger.Info("http request", "method", c.Request.Method, "route", c.FullPath(), "status", c.Writer.Status(), "duration_ms", time.Since(start).Milliseconds(), "request_id", requestID)
|
||||
route := c.FullPath()
|
||||
if route == "" {
|
||||
route = "/<unmatched>"
|
||||
if c.Request.URL.Path == "/" {
|
||||
route = "/"
|
||||
} else if strings.HasPrefix(c.Request.URL.Path, "/assets/") {
|
||||
route = "/assets/*"
|
||||
}
|
||||
}
|
||||
fields := []zap.Field{zap.String("method", c.Request.Method), zap.String("route", route), zap.Int("status", c.Writer.Status()), zap.Int64("duration_ms", time.Since(start).Milliseconds()), zap.String("request_id", requestID)}
|
||||
switch {
|
||||
case c.Writer.Status() >= 500:
|
||||
logger.Error("HTTP request", fields...)
|
||||
case c.Writer.Status() >= 400:
|
||||
logger.Warn("HTTP request", fields...)
|
||||
default:
|
||||
logger.Info("HTTP request", fields...)
|
||||
}
|
||||
}()
|
||||
c.Next()
|
||||
})
|
||||
@@ -48,6 +65,14 @@ func NewRouter(health *service.HealthService, logger *slog.Logger, identity Iden
|
||||
r.GET("/readyz", check)
|
||||
r.GET("/api/v1/system/health", check)
|
||||
bindIdentity(r, identity)
|
||||
bindProviders(r, identity)
|
||||
bindCandidates(r, identity)
|
||||
bindTradePlans(r, identity)
|
||||
bindTradeFills(r, identity)
|
||||
bindNotifications(r, identity)
|
||||
bindStrategies(r, identity)
|
||||
bindBacktests(r, identity)
|
||||
bindMarket(r, identity)
|
||||
console := adminui.Handler()
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
if !strings.HasPrefix(c.Request.URL.Path, "/api/") && c.Request.URL.Path != "/api" {
|
||||
|
||||
@@ -3,8 +3,7 @@ package handler
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"go.uber.org/zap"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -16,7 +15,7 @@ import (
|
||||
func TestLivenessAndReadinessAreIndependent(t *testing.T) {
|
||||
secretError := "password=secret-internal-detail"
|
||||
health := service.NewHealthService(nil, func(context.Context) error { return errors.New(secretError) })
|
||||
router := NewRouter(health, slog.New(slog.NewTextHandler(io.Discard, nil)), IdentityOptions{})
|
||||
router := NewRouter(health, zap.NewNop(), IdentityOptions{})
|
||||
for _, tc := range []struct {
|
||||
path string
|
||||
status int
|
||||
@@ -37,7 +36,7 @@ func TestLivenessAndReadinessAreIndependent(t *testing.T) {
|
||||
|
||||
func TestReadyWhenBothDependenciesRespond(t *testing.T) {
|
||||
probe := func(context.Context) error { return nil }
|
||||
router := NewRouter(service.NewHealthService(probe, probe), slog.New(slog.NewTextHandler(io.Discard, nil)), IdentityOptions{})
|
||||
router := NewRouter(service.NewHealthService(probe, probe), zap.NewNop(), IdentityOptions{})
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/readyz", nil))
|
||||
if rec.Code != http.StatusOK {
|
||||
@@ -47,7 +46,7 @@ func TestReadyWhenBothDependenciesRespond(t *testing.T) {
|
||||
|
||||
func TestEmbeddedConsoleAndAPIBoundary(t *testing.T) {
|
||||
probe := func(context.Context) error { return nil }
|
||||
router := NewRouter(service.NewHealthService(probe, probe), slog.New(slog.NewTextHandler(io.Discard, nil)), IdentityOptions{})
|
||||
router := NewRouter(service.NewHealthService(probe, probe), zap.NewNop(), IdentityOptions{})
|
||||
for _, path := range []string{"/", "/research/candidates"} {
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"git.sundynix.cn/Blizzard/Veyra/internal/apperror"
|
||||
"git.sundynix.cn/Blizzard/Veyra/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func bindStrategies(r *gin.Engine, options IdentityOptions) {
|
||||
group := r.Group("/api/admin/v1", func(c *gin.Context) { authenticate(c, options, "admin") })
|
||||
group.Use(func(c *gin.Context) {
|
||||
if options.Strategies == nil {
|
||||
fail(c, apperror.ErrUnavailable)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
})
|
||||
group.GET("/strategies", func(c *gin.Context) {
|
||||
result, err := options.Strategies.List(c.Request.Context(), currentUser(c))
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, result)
|
||||
})
|
||||
group.GET("/strategies/:id", func(c *gin.Context) {
|
||||
result, err := options.Strategies.Detail(c.Request.Context(), currentUser(c), c.Param("id"))
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, result)
|
||||
})
|
||||
group.Use(func(c *gin.Context) {
|
||||
if !sameOrigin(c.Request) {
|
||||
fail(c, apperror.ErrForbidden)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
})
|
||||
for _, path := range []string{"/strategies", "/strategies/:id/versions"} {
|
||||
group.POST(path, func(c *gin.Context) {
|
||||
var input service.StrategyInput
|
||||
if !decode(c, &input) {
|
||||
return
|
||||
}
|
||||
result, err := options.Strategies.Save(c.Request.Context(), currentUser(c), c.Param("id"), input, c.Writer.Header().Get("X-Request-ID"))
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusCreated, result)
|
||||
})
|
||||
}
|
||||
group.POST("/strategies/:id/deploy", func(c *gin.Context) {
|
||||
var input struct {
|
||||
VersionID string `json:"version_id"`
|
||||
Status string `json:"status"`
|
||||
ExpectedVersion int64 `json:"expected_version"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
if !decode(c, &input) {
|
||||
return
|
||||
}
|
||||
result, err := options.Strategies.Deploy(c.Request.Context(), currentUser(c), c.Param("id"), input.VersionID, input.Status, input.ExpectedVersion, input.Reason, c.Writer.Header().Get("X-Request-ID"))
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, result)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"git.sundynix.cn/Blizzard/Veyra/internal/apperror"
|
||||
"git.sundynix.cn/Blizzard/Veyra/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func bindTradeFills(r *gin.Engine, options IdentityOptions) {
|
||||
group := r.Group("/api/v1", func(c *gin.Context) { authenticate(c, options, "app") })
|
||||
group.Use(func(c *gin.Context) {
|
||||
if options.Fills == nil {
|
||||
fail(c, apperror.ErrUnavailable)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
})
|
||||
group.POST("/fills", func(c *gin.Context) {
|
||||
var input service.FillInput
|
||||
if !decode(c, &input) {
|
||||
return
|
||||
}
|
||||
result, err := options.Fills.Record(c.Request.Context(), currentUser(c), input, c.Writer.Header().Get("X-Request-ID"))
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusCreated, result)
|
||||
})
|
||||
group.POST("/fills/:id/reverse", func(c *gin.Context) {
|
||||
var input struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
if !decode(c, &input) {
|
||||
return
|
||||
}
|
||||
result, err := options.Fills.Reverse(c.Request.Context(), currentUser(c), c.Param("id"), input.Reason, c.Writer.Header().Get("X-Request-ID"))
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, result)
|
||||
})
|
||||
group.GET("/accounts/:id/fills", func(c *gin.Context) {
|
||||
result, err := options.Fills.List(c.Request.Context(), currentUser(c), c.Param("id"))
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, result)
|
||||
})
|
||||
group.GET("/accounts/:id/portfolio", func(c *gin.Context) {
|
||||
result, err := options.Fills.Portfolio(c.Request.Context(), currentUser(c), c.Param("id"))
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, result)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"git.sundynix.cn/Blizzard/Veyra/internal/apperror"
|
||||
"git.sundynix.cn/Blizzard/Veyra/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func bindTradePlans(r *gin.Engine, options IdentityOptions) {
|
||||
group := r.Group("/api/v1", func(c *gin.Context) { authenticate(c, options, "app") })
|
||||
group.Use(func(c *gin.Context) {
|
||||
if options.Plans == nil {
|
||||
fail(c, apperror.ErrUnavailable)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
})
|
||||
group.POST("/plan-previews", func(c *gin.Context) {
|
||||
var input service.PlanPreviewInput
|
||||
if !decode(c, &input) {
|
||||
return
|
||||
}
|
||||
result, err := options.Plans.Preview(c.Request.Context(), currentUser(c), input)
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusCreated, result)
|
||||
})
|
||||
group.POST("/plan-previews/:id/confirm", func(c *gin.Context) {
|
||||
var input struct{}
|
||||
if !decode(c, &input) {
|
||||
return
|
||||
}
|
||||
result, err := options.Plans.Confirm(c.Request.Context(), currentUser(c), c.Param("id"), c.Writer.Header().Get("X-Request-ID"))
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, result)
|
||||
})
|
||||
group.GET("/plans", func(c *gin.Context) {
|
||||
result, err := options.Plans.List(c.Request.Context(), currentUser(c))
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, result)
|
||||
})
|
||||
group.POST("/plans/:id/cancel", func(c *gin.Context) {
|
||||
var input struct {
|
||||
ExpectedVersion int64 `json:"expected_version"`
|
||||
}
|
||||
if !decode(c, &input) {
|
||||
return
|
||||
}
|
||||
result, err := options.Plans.Cancel(c.Request.Context(), currentUser(c), c.Param("id"), input.ExpectedVersion, c.Writer.Header().Get("X-Request-ID"))
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, result)
|
||||
})
|
||||
group.GET("/accounts/:id/budget", func(c *gin.Context) {
|
||||
result, err := options.Plans.Budget(c.Request.Context(), currentUser(c), c.Param("id"))
|
||||
if err != nil {
|
||||
fail(c, err)
|
||||
return
|
||||
}
|
||||
success(c, http.StatusOK, result)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user