feat(channels): 渠道管理 - 可视化排序与别名并修复注册及外观清理 (#444)
This commit is contained in:
@@ -21,6 +21,7 @@ import (
|
||||
)
|
||||
|
||||
func RegisterAuthRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
registerChannelOrderRoutes(r, svc)
|
||||
r.GET("/auth/settings", func(c *gin.Context) {
|
||||
settings, err := svc.PublicAuthSettings()
|
||||
if err != nil {
|
||||
@@ -61,6 +62,9 @@ func RegisterAuthRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
if !available || !enforceRateLimit(c, "email-code:"+c.ClientIP(), policy.Request.EmailCodePerHour, time.Hour) {
|
||||
return
|
||||
}
|
||||
if !enforceRateLimit(c, "registration-email-account:"+passwordResetRateLimitSubject(req.Email), 10, time.Hour) {
|
||||
return
|
||||
}
|
||||
if err := svc.SendRegistrationEmailCode(req.Email); err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
|
||||
43
backend/internal/handler/channel_order.go
Normal file
43
backend/internal/handler/channel_order.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"infinite-canvas/backend/internal/service"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func registerChannelOrderRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
for _, path := range []string{"/admin/channels/order", "/admin/channels/:id/models/order"} {
|
||||
r.GET(path, func(c *gin.Context) {
|
||||
user, err := currentUser(c, svc)
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
items, err := svc.AdminChannelOrder(user, c.Param("id"))
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
ok(c, gin.H{"items": items})
|
||||
})
|
||||
r.PUT(path, func(c *gin.Context) {
|
||||
user, err := currentUser(c, svc)
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 1<<20)
|
||||
var req service.ChannelOrderRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
fail(c, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
if err := svc.SaveAdminChannelOrder(user, c.Param("id"), req); err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
ok(c, gin.H{"saved": true})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -293,6 +293,23 @@ func RegisterFinanceRoutes(r *gin.RouterGroup, svc *service.Service) {
|
||||
r.PATCH("/admin/channels/:id/models/:modelId", func(c *gin.Context) {
|
||||
saveChannelModel(c, svc, c.Param("modelId"))
|
||||
})
|
||||
r.PATCH("/admin/channels/:id/models/:modelId/sort", func(c *gin.Context) {
|
||||
user, err := currentUser(c, svc)
|
||||
if err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
var req service.ChannelModelSortRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
fail(c, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
if err := svc.UpdateAdminChannelModelSort(user, c.Param("id"), c.Param("modelId"), req); err != nil {
|
||||
failService(c, err)
|
||||
return
|
||||
}
|
||||
ok(c, gin.H{"updated": true})
|
||||
})
|
||||
r.DELETE("/admin/channels/:id/models/:modelId", func(c *gin.Context) {
|
||||
user, err := currentUser(c, svc)
|
||||
if err != nil {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"infinite-canvas/backend/internal/service"
|
||||
@@ -27,6 +28,12 @@ func fail(c *gin.Context, status int, err error) {
|
||||
}
|
||||
|
||||
func failService(c *gin.Context, err error) {
|
||||
var cooldown *service.EmailCodeCooldownError
|
||||
if errors.As(err, &cooldown) {
|
||||
c.Header("Retry-After", strconv.Itoa(cooldown.Seconds))
|
||||
fail(c, http.StatusTooManyRequests, cooldown)
|
||||
return
|
||||
}
|
||||
var appErr *service.AppError
|
||||
if errors.As(err, &appErr) && validErrorStatus(appErr.Status) {
|
||||
code := appErr.Code
|
||||
|
||||
@@ -18,6 +18,15 @@ type failureEnvelope struct {
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
func TestFailServiceRegistrationCooldown(t *testing.T) {
|
||||
recorder, context := responseTestContext()
|
||||
failService(context, &service.EmailCodeCooldownError{Seconds: 47})
|
||||
response := decodeFailureEnvelope(t, recorder)
|
||||
if recorder.Code != http.StatusTooManyRequests || response.Code != 429 || recorder.Header().Get("Retry-After") != "47" || !strings.Contains(response.Msg, "47") {
|
||||
t.Fatalf("cooldown response: status=%d header=%s body=%#v", recorder.Code, recorder.Header().Get("Retry-After"), response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailServiceProjectsAppError(t *testing.T) {
|
||||
recorder, context := responseTestContext()
|
||||
err := service.NewAppError(http.StatusTooManyRequests, "请求过于频繁,请稍后重试")
|
||||
|
||||
@@ -152,8 +152,10 @@ func enforceRateLimit(c *gin.Context, key string, limit int, window time.Duratio
|
||||
if allowed {
|
||||
return true
|
||||
}
|
||||
c.Header("Retry-After", "60")
|
||||
fail(c, http.StatusTooManyRequests, errors.New("请求过于频繁,请稍后再试"))
|
||||
wait := runtimeService.RequestRetryAfter(c.Request.Context(), key, window)
|
||||
seconds := max(1, int((wait+time.Second-1)/time.Second))
|
||||
c.Header("Retry-After", strconv.Itoa(seconds))
|
||||
fail(c, http.StatusTooManyRequests, fmt.Errorf("请求次数已达上限,请在 %d 秒后重试", seconds))
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user