feat: 添加环境变量支持并增强系统标题效果验证
feat(security): 支持通过环境变量配置主密钥和JWT密钥 refactor(router): 移除开发便捷路由接口 feat(admin): 添加超级管理员权限检查 feat(titles): 增加系统标题效果参数验证逻辑
This commit is contained in:
parent
8141a47690
commit
1b5a715a22
@ -1,6 +1,9 @@
|
|||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@ -201,10 +204,15 @@ func (h *handler) CreateSystemTitleEffect() core.HandlerFunc {
|
|||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 30115, "同类型效果已存在"))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 30115, "同类型效果已存在"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
sanitized, verr := validateEffectParams(req.EffectType, req.ParamsJSON)
|
||||||
|
if verr != nil {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, verr.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
ef := &model.SystemTitleEffects{
|
ef := &model.SystemTitleEffects{
|
||||||
TitleID: titleID,
|
TitleID: titleID,
|
||||||
EffectType: req.EffectType,
|
EffectType: req.EffectType,
|
||||||
ParamsJSON: req.ParamsJSON,
|
ParamsJSON: sanitized,
|
||||||
StackingStrategy: req.StackingStrategy,
|
StackingStrategy: req.StackingStrategy,
|
||||||
CapValueX1000: req.CapValueX1000,
|
CapValueX1000: req.CapValueX1000,
|
||||||
ScopesJSON: req.ScopesJSON,
|
ScopesJSON: req.ScopesJSON,
|
||||||
@ -262,7 +270,16 @@ func (h *handler) ModifySystemTitleEffect() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
row.EffectType = *req.EffectType
|
row.EffectType = *req.EffectType
|
||||||
}
|
}
|
||||||
if req.ParamsJSON != "" { row.ParamsJSON = req.ParamsJSON }
|
if req.ParamsJSON != "" {
|
||||||
|
et := row.EffectType
|
||||||
|
if req.EffectType != nil { et = *req.EffectType }
|
||||||
|
sanitized, verr := validateEffectParams(et, req.ParamsJSON)
|
||||||
|
if verr != nil {
|
||||||
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, verr.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
row.ParamsJSON = sanitized
|
||||||
|
}
|
||||||
if req.StackingStrategy != nil { row.StackingStrategy = *req.StackingStrategy }
|
if req.StackingStrategy != nil { row.StackingStrategy = *req.StackingStrategy }
|
||||||
if req.CapValueX1000 != nil { row.CapValueX1000 = *req.CapValueX1000 }
|
if req.CapValueX1000 != nil { row.CapValueX1000 = *req.CapValueX1000 }
|
||||||
if req.ScopesJSON != "" { row.ScopesJSON = req.ScopesJSON }
|
if req.ScopesJSON != "" { row.ScopesJSON = req.ScopesJSON }
|
||||||
@ -276,6 +293,89 @@ func (h *handler) ModifySystemTitleEffect() core.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateEffectParams(effectType int32, raw string) (string, error) {
|
||||||
|
dec := json.NewDecoder(bytes.NewBufferString(raw))
|
||||||
|
dec.DisallowUnknownFields()
|
||||||
|
switch effectType {
|
||||||
|
case 1:
|
||||||
|
var p struct {
|
||||||
|
TemplateID int64 `json:"template_id"`
|
||||||
|
Frequency struct {
|
||||||
|
Period string `json:"period"`
|
||||||
|
Times int `json:"times"`
|
||||||
|
} `json:"frequency"`
|
||||||
|
}
|
||||||
|
if err := dec.Decode(&p); err != nil { return "", err }
|
||||||
|
if p.TemplateID < 0 { return "", fmt.Errorf("template_id无效") }
|
||||||
|
if p.Frequency.Times < 1 || p.Frequency.Times > 100 { return "", fmt.Errorf("times范围错误") }
|
||||||
|
if p.Frequency.Period != "day" && p.Frequency.Period != "week" && p.Frequency.Period != "month" { return "", fmt.Errorf("period无效") }
|
||||||
|
b, _ := json.Marshal(p); return string(b), nil
|
||||||
|
case 2:
|
||||||
|
var p struct {
|
||||||
|
DiscountType string `json:"discount_type"`
|
||||||
|
ValueX1000 int32 `json:"value_x1000"`
|
||||||
|
MaxDiscountX1000 int32 `json:"max_discount_x1000"`
|
||||||
|
}
|
||||||
|
if err := dec.Decode(&p); err != nil { return "", err }
|
||||||
|
if p.DiscountType != "percentage" && p.DiscountType != "fixed" { return "", fmt.Errorf("discount_type无效") }
|
||||||
|
if p.ValueX1000 < 0 || p.MaxDiscountX1000 < 0 { return "", fmt.Errorf("数值必须>=0") }
|
||||||
|
b, _ := json.Marshal(p); return string(b), nil
|
||||||
|
case 3:
|
||||||
|
var p struct {
|
||||||
|
MultiplierX1000 int32 `json:"multiplier_x1000"`
|
||||||
|
DailyCapPoints int32 `json:"daily_cap_points"`
|
||||||
|
}
|
||||||
|
if err := dec.Decode(&p); err != nil { return "", err }
|
||||||
|
if p.MultiplierX1000 < 0 || p.DailyCapPoints < 0 { return "", fmt.Errorf("数值必须>=0") }
|
||||||
|
b, _ := json.Marshal(p); return string(b), nil
|
||||||
|
case 4:
|
||||||
|
var p struct {
|
||||||
|
TemplateID int64 `json:"template_id"`
|
||||||
|
Frequency struct {
|
||||||
|
Period string `json:"period"`
|
||||||
|
Times int `json:"times"`
|
||||||
|
} `json:"frequency"`
|
||||||
|
}
|
||||||
|
if err := dec.Decode(&p); err != nil { return "", err }
|
||||||
|
if p.TemplateID < 0 { return "", fmt.Errorf("template_id无效") }
|
||||||
|
if p.Frequency.Times < 1 || p.Frequency.Times > 100 { return "", fmt.Errorf("times范围错误") }
|
||||||
|
if p.Frequency.Period != "week" && p.Frequency.Period != "month" { return "", fmt.Errorf("period无效") }
|
||||||
|
b, _ := json.Marshal(p); return string(b), nil
|
||||||
|
case 5:
|
||||||
|
var p struct {
|
||||||
|
TargetPrizeIDs []int64 `json:"target_prize_ids"`
|
||||||
|
BoostX1000 int32 `json:"boost_x1000"`
|
||||||
|
CapX1000 *int32 `json:"cap_x1000"`
|
||||||
|
}
|
||||||
|
if err := dec.Decode(&p); err != nil { return "", err }
|
||||||
|
if p.BoostX1000 < 0 || p.BoostX1000 > 100000 { return "", fmt.Errorf("boost_x1000范围错误") }
|
||||||
|
if p.CapX1000 != nil && *p.CapX1000 < 0 { return "", fmt.Errorf("cap_x1000必须>=0") }
|
||||||
|
if len(p.TargetPrizeIDs) > 200 { return "", fmt.Errorf("target_prize_ids数量过多") }
|
||||||
|
m := make(map[int64]struct{})
|
||||||
|
out := make([]int64, 0, len(p.TargetPrizeIDs))
|
||||||
|
for _, id := range p.TargetPrizeIDs { if _, ok := m[id]; !ok { m[id] = struct{}{}; out = append(out, id) } }
|
||||||
|
p.TargetPrizeIDs = out
|
||||||
|
b, _ := json.Marshal(p); return string(b), nil
|
||||||
|
case 6:
|
||||||
|
var p struct {
|
||||||
|
TargetPrizeIDs []int64 `json:"target_prize_ids"`
|
||||||
|
ChanceX1000 int32 `json:"chance_x1000"`
|
||||||
|
PeriodCapTimes *int32 `json:"period_cap_times"`
|
||||||
|
}
|
||||||
|
if err := dec.Decode(&p); err != nil { return "", err }
|
||||||
|
if p.ChanceX1000 < 0 || p.ChanceX1000 > 100000 { return "", fmt.Errorf("chance_x1000范围错误") }
|
||||||
|
if p.PeriodCapTimes != nil && *p.PeriodCapTimes < 0 { return "", fmt.Errorf("period_cap_times必须>=0") }
|
||||||
|
if len(p.TargetPrizeIDs) > 200 { return "", fmt.Errorf("target_prize_ids数量过多") }
|
||||||
|
m := make(map[int64]struct{})
|
||||||
|
out := make([]int64, 0, len(p.TargetPrizeIDs))
|
||||||
|
for _, id := range p.TargetPrizeIDs { if _, ok := m[id]; !ok { m[id] = struct{}{}; out = append(out, id) } }
|
||||||
|
p.TargetPrizeIDs = out
|
||||||
|
b, _ := json.Marshal(p); return string(b), nil
|
||||||
|
default:
|
||||||
|
return raw, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *handler) DeleteSystemTitleEffect() core.HandlerFunc {
|
func (h *handler) DeleteSystemTitleEffect() core.HandlerFunc {
|
||||||
return func(ctx core.Context) {
|
return func(ctx core.Context) {
|
||||||
titleID, err := strconv.ParseInt(ctx.Param("title_id"), 10, 64)
|
titleID, err := strconv.ParseInt(ctx.Param("title_id"), 10, 64)
|
||||||
|
|||||||
@ -594,23 +594,27 @@ type addCouponResponse struct {
|
|||||||
// @Router /api/admin/users/{user_id}/coupons/add [post]
|
// @Router /api/admin/users/{user_id}/coupons/add [post]
|
||||||
// @Security LoginVerifyToken
|
// @Security LoginVerifyToken
|
||||||
func (h *handler) AddUserCoupon() core.HandlerFunc {
|
func (h *handler) AddUserCoupon() core.HandlerFunc {
|
||||||
return func(ctx core.Context) {
|
return func(ctx core.Context) {
|
||||||
req := new(addCouponRequest)
|
req := new(addCouponRequest)
|
||||||
rsp := new(addCouponResponse)
|
rsp := new(addCouponResponse)
|
||||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
if err := ctx.ShouldBindJSON(req); err != nil {
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
|
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递用户ID"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := h.user.AddCoupon(ctx.RequestContext(), userID, req.CouponID); err != nil {
|
if ctx.SessionUserInfo().IsSuper != 1 {
|
||||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20109, err.Error()))
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateAdminError, "禁止操作"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rsp.Success = true
|
if err := h.user.AddCoupon(ctx.RequestContext(), userID, req.CouponID); err != nil {
|
||||||
ctx.Payload(rsp)
|
ctx.AbortWithError(core.Error(http.StatusBadRequest, 20109, err.Error()))
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
rsp.Success = true
|
||||||
|
ctx.Payload(rsp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,18 @@
|
|||||||
package interceptor
|
package interceptor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
"bindbox-game/configs"
|
"bindbox-game/configs"
|
||||||
"bindbox-game/internal/code"
|
"bindbox-game/internal/code"
|
||||||
"bindbox-game/internal/pkg/core"
|
"bindbox-game/internal/pkg/core"
|
||||||
"bindbox-game/internal/pkg/jwtoken"
|
"bindbox-game/internal/pkg/jwtoken"
|
||||||
"bindbox-game/internal/pkg/utils"
|
"bindbox-game/internal/pkg/utils"
|
||||||
"bindbox-game/internal/proposal"
|
"bindbox-game/internal/proposal"
|
||||||
"bindbox-game/internal/repository/mysql/dao"
|
"bindbox-game/internal/repository/mysql/dao"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (i *interceptor) AdminTokenAuthVerify(ctx core.Context) (sessionUserInfo proposal.SessionUserInfo, err core.BusinessError) {
|
func (i *interceptor) AdminTokenAuthVerify(ctx core.Context) (sessionUserInfo proposal.SessionUserInfo, err core.BusinessError) {
|
||||||
@ -26,7 +27,9 @@ func (i *interceptor) AdminTokenAuthVerify(ctx core.Context) (sessionUserInfo pr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证 JWT 是否合法
|
// 验证 JWT 是否合法
|
||||||
jwtClaims, jwtErr := jwtoken.New(configs.Get().JWT.AdminSecret).Parse(headerAuthorizationString)
|
secret := configs.Get().JWT.AdminSecret
|
||||||
|
if v := os.Getenv("ADMIN_JWT_SECRET"); v != "" { secret = v }
|
||||||
|
jwtClaims, jwtErr := jwtoken.New(secret).Parse(headerAuthorizationString)
|
||||||
if jwtErr != nil {
|
if jwtErr != nil {
|
||||||
err = core.Error(
|
err = core.Error(
|
||||||
http.StatusUnauthorized,
|
http.StatusUnauthorized,
|
||||||
|
|||||||
@ -49,10 +49,7 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, error) {
|
|||||||
// 管理端非认证接口路由组
|
// 管理端非认证接口路由组
|
||||||
adminNonAuthApiRouter := mux.Group("/api/admin")
|
adminNonAuthApiRouter := mux.Group("/api/admin")
|
||||||
{
|
{
|
||||||
adminNonAuthApiRouter.POST("/login", adminHandler.Login()) // 登录
|
adminNonAuthApiRouter.POST("/login", adminHandler.Login())
|
||||||
// 开发便捷:无认证的初始化接口(仅用于快速配置,生产环境请关闭或加权限)
|
|
||||||
adminNonAuthApiRouter.POST("/system_titles/seed_default", adminHandler.SeedDefaultTitles())
|
|
||||||
adminNonAuthApiRouter.POST("/menu/ensure_titles", adminHandler.EnsureTitlesMenu())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 管理端认证接口路由组
|
// 管理端认证接口路由组
|
||||||
|
|||||||
@ -5,11 +5,16 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"os"
|
||||||
|
|
||||||
"bindbox-game/configs"
|
"bindbox-game/configs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func masterKey() []byte {
|
func masterKey() []byte {
|
||||||
|
if v := os.Getenv("RANDOM_COMMIT_MASTER_KEY"); v != "" {
|
||||||
|
b, err := hex.DecodeString(v)
|
||||||
|
if err == nil && len(b) > 0 { return b }
|
||||||
|
}
|
||||||
s := configs.Get().Random.CommitMasterKey
|
s := configs.Get().Random.CommitMasterKey
|
||||||
if s == "" {
|
if s == "" {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user