fix gitea workflow path and runner label
Some checks failed
Build docker and publish / build (20.15.1) (push) Failing after 7m57s
Some checks failed
Build docker and publish / build (20.15.1) (push) Failing after 7m57s
This commit is contained in:
parent
a01570b59d
commit
149dfe1ac3
74
doc/api-version-switch-zh.md
Normal file
74
doc/api-version-switch-zh.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# API 版本分流接入指南(`api-header`)
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
- 通过请求头 `api-header` 动态选择不同 Handler(如 `001Handler` / `002Handler`)。
|
||||||
|
- 让旧 App 无需升级仍可走旧逻辑,新 App 通过版本头走新逻辑。
|
||||||
|
|
||||||
|
## 当前规则
|
||||||
|
- 仅识别请求头:`api-header`
|
||||||
|
- 严格版本格式:`x.y.z` 或 `vx.y.z`
|
||||||
|
- 仅当 `api-header > 1.0.0` 时走新逻辑(V2)
|
||||||
|
- 其余情况(缺失/非法/`<=1.0.0`)走旧逻辑(V1)
|
||||||
|
|
||||||
|
相关代码:
|
||||||
|
- 版本解析:`pkg/apiversion/version.go`
|
||||||
|
- 版本注入中间件:`internal/middleware/apiVersionMiddleware.go`
|
||||||
|
- 通用分流器:`internal/middleware/apiVersionSwitchHandler.go`
|
||||||
|
|
||||||
|
## 新接口接入步骤(推荐)
|
||||||
|
|
||||||
|
### 1) 实现两个 Handler
|
||||||
|
```go
|
||||||
|
func FooV1Handler(svcCtx *svc.ServiceContext) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// 旧逻辑(001)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FooV2Handler(svcCtx *svc.ServiceContext) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// 新逻辑(002)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2) 在路由中挂分流器
|
||||||
|
```go
|
||||||
|
group.POST("/foo", middleware.ApiVersionSwitchHandler(
|
||||||
|
foo.FooV1Handler(serverCtx),
|
||||||
|
foo.FooV2Handler(serverCtx),
|
||||||
|
))
|
||||||
|
```
|
||||||
|
|
||||||
|
完成后,无需在业务代码里手写 `api-header` 判断。
|
||||||
|
|
||||||
|
## 客户端调用示例
|
||||||
|
|
||||||
|
### 旧逻辑(V1)
|
||||||
|
```bash
|
||||||
|
curl -X POST 'https://example.com/v1/common/foo' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"x":1}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 新逻辑(V2)
|
||||||
|
```bash
|
||||||
|
curl -X POST 'https://example.com/v1/common/foo' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H 'api-header: 1.0.1' \
|
||||||
|
-d '{"x":1}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试建议(最小集)
|
||||||
|
- 无 `api-header`:命中 V1
|
||||||
|
- `api-header: 1.0.0`:命中 V1
|
||||||
|
- `api-header: 1.0.1`:命中 V2
|
||||||
|
- `api-header: abc`:命中 V1
|
||||||
|
|
||||||
|
可参考测试:
|
||||||
|
- `internal/middleware/apiVersionSwitchHandler_test.go`
|
||||||
|
|
||||||
|
## 适用建议
|
||||||
|
- 差异较小:优先在 V2 中复用现有逻辑,减少重复代码。
|
||||||
|
- 差异较大:拆分 V1/V2 各自逻辑,避免分支污染。
|
||||||
|
- 上线顺序:先发后端分流能力,再逐步让客户端加 `api-header`。
|
||||||
@ -11,6 +11,18 @@ import (
|
|||||||
|
|
||||||
// Check legacy verification code
|
// Check legacy verification code
|
||||||
func CheckCodeLegacyHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
func CheckCodeLegacyHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||||
|
return CheckCodeLegacyV1Handler(svcCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckCodeLegacyV1Handler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||||
|
return checkCodeLegacyHandler(svcCtx, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckCodeLegacyV2Handler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||||
|
return checkCodeLegacyHandler(svcCtx, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCodeLegacyHandler(svcCtx *svc.ServiceContext, consume bool) func(c *gin.Context) {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
var req types.LegacyCheckVerificationCodeRequest
|
var req types.LegacyCheckVerificationCodeRequest
|
||||||
_ = c.ShouldBind(&req)
|
_ = c.ShouldBind(&req)
|
||||||
@ -27,14 +39,9 @@ func CheckCodeLegacyHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
l := commonLogic.NewCheckVerificationCodeLogic(c.Request.Context(), svcCtx)
|
l := commonLogic.NewCheckVerificationCodeLogic(c.Request.Context(), svcCtx)
|
||||||
useLatest := false
|
|
||||||
if value, ok := c.Request.Context().Value(constant.CtxKeyAPIVersionUseLatest).(bool); ok {
|
|
||||||
useLatest = value
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := l.CheckVerificationCodeWithBehavior(normalizedReq, commonLogic.VerifyCodeCheckBehavior{
|
resp, err := l.CheckVerificationCodeWithBehavior(normalizedReq, commonLogic.VerifyCodeCheckBehavior{
|
||||||
Source: "legacy",
|
Source: "legacy",
|
||||||
Consume: useLatest,
|
Consume: consume,
|
||||||
LegacyType3Mapped: legacyType3Mapped,
|
LegacyType3Mapped: legacyType3Mapped,
|
||||||
AllowSceneFallback: constant.ParseVerifyType(normalizedReq.Type) != constant.DeleteAccount,
|
AllowSceneFallback: constant.ParseVerifyType(normalizedReq.Type) != constant.DeleteAccount,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -33,7 +33,10 @@ func newLegacyCheckCodeTestRouter(svcCtx *svc.ServiceContext) *gin.Engine {
|
|||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
router.Use(middleware.ApiVersionMiddleware(svcCtx))
|
router.Use(middleware.ApiVersionMiddleware(svcCtx))
|
||||||
router.POST("/v1/auth/check-code", CheckCodeLegacyHandler(svcCtx))
|
router.POST("/v1/auth/check-code", middleware.ApiVersionSwitchHandler(
|
||||||
|
CheckCodeLegacyV1Handler(svcCtx),
|
||||||
|
CheckCodeLegacyV2Handler(svcCtx),
|
||||||
|
))
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,12 +5,23 @@ import (
|
|||||||
"github.com/perfect-panel/server/internal/logic/common"
|
"github.com/perfect-panel/server/internal/logic/common"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
"github.com/perfect-panel/server/internal/types"
|
"github.com/perfect-panel/server/internal/types"
|
||||||
"github.com/perfect-panel/server/pkg/constant"
|
|
||||||
"github.com/perfect-panel/server/pkg/result"
|
"github.com/perfect-panel/server/pkg/result"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check verification code
|
// Check verification code
|
||||||
func CheckVerificationCodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
func CheckVerificationCodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||||
|
return CheckVerificationCodeV1Handler(svcCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckVerificationCodeV1Handler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||||
|
return checkVerificationCodeHandler(svcCtx, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckVerificationCodeV2Handler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||||
|
return checkVerificationCodeHandler(svcCtx, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkVerificationCodeHandler(svcCtx *svc.ServiceContext, consume bool) func(c *gin.Context) {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
var req types.CheckVerificationCodeRequest
|
var req types.CheckVerificationCodeRequest
|
||||||
_ = c.ShouldBind(&req)
|
_ = c.ShouldBind(&req)
|
||||||
@ -21,14 +32,9 @@ func CheckVerificationCodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Contex
|
|||||||
}
|
}
|
||||||
|
|
||||||
l := common.NewCheckVerificationCodeLogic(c.Request.Context(), svcCtx)
|
l := common.NewCheckVerificationCodeLogic(c.Request.Context(), svcCtx)
|
||||||
useLatest := false
|
|
||||||
if value, ok := c.Request.Context().Value(constant.CtxKeyAPIVersionUseLatest).(bool); ok {
|
|
||||||
useLatest = value
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := l.CheckVerificationCodeWithBehavior(&req, common.VerifyCodeCheckBehavior{
|
resp, err := l.CheckVerificationCodeWithBehavior(&req, common.VerifyCodeCheckBehavior{
|
||||||
Source: "canonical",
|
Source: "canonical",
|
||||||
Consume: useLatest,
|
Consume: consume,
|
||||||
})
|
})
|
||||||
result.HttpResult(c, resp, err)
|
result.HttpResult(c, resp, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,7 +55,10 @@ func newCanonicalCheckCodeTestRouter(svcCtx *svc.ServiceContext) *gin.Engine {
|
|||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
router.Use(middleware.ApiVersionMiddleware(svcCtx))
|
router.Use(middleware.ApiVersionMiddleware(svcCtx))
|
||||||
router.POST("/v1/common/check_verification_code", CheckVerificationCodeHandler(svcCtx))
|
router.POST("/v1/common/check_verification_code", middleware.ApiVersionSwitchHandler(
|
||||||
|
CheckVerificationCodeV1Handler(svcCtx),
|
||||||
|
CheckVerificationCodeV2Handler(svcCtx),
|
||||||
|
))
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,15 +2,14 @@ package user
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/perfect-panel/server/internal/config"
|
commonLogic "github.com/perfect-panel/server/internal/logic/common"
|
||||||
"github.com/perfect-panel/server/internal/logic/public/user"
|
"github.com/perfect-panel/server/internal/logic/public/user"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
|
"github.com/perfect-panel/server/internal/types"
|
||||||
|
"github.com/perfect-panel/server/pkg/authmethod"
|
||||||
"github.com/perfect-panel/server/pkg/constant"
|
"github.com/perfect-panel/server/pkg/constant"
|
||||||
"github.com/perfect-panel/server/pkg/result"
|
"github.com/perfect-panel/server/pkg/result"
|
||||||
"github.com/perfect-panel/server/pkg/xerr"
|
"github.com/perfect-panel/server/pkg/xerr"
|
||||||
@ -36,7 +35,7 @@ func DeleteAccountHandler(serverCtx *svc.ServiceContext) gin.HandlerFunc {
|
|||||||
// 统一处理邮箱格式:转小写并去空格,与发送验证码逻辑保持一致
|
// 统一处理邮箱格式:转小写并去空格,与发送验证码逻辑保持一致
|
||||||
req.Email = strings.ToLower(strings.TrimSpace(req.Email))
|
req.Email = strings.ToLower(strings.TrimSpace(req.Email))
|
||||||
|
|
||||||
// 校验邮箱验证码
|
// 校验邮箱验证码(统一走公共验证码校验器,支持 delete_account/security 场景兼容)
|
||||||
if err := verifyEmailCode(c.Request.Context(), serverCtx, req.Email, req.Code); err != nil {
|
if err := verifyEmailCode(c.Request.Context(), serverCtx, req.Email, req.Code); err != nil {
|
||||||
result.HttpResult(c, nil, err)
|
result.HttpResult(c, nil, err)
|
||||||
return
|
return
|
||||||
@ -48,43 +47,29 @@ func DeleteAccountHandler(serverCtx *svc.ServiceContext) gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CacheKeyPayload 验证码缓存结构
|
|
||||||
type CacheKeyPayload struct {
|
|
||||||
Code string `json:"code"`
|
|
||||||
LastAt int64 `json:"lastAt"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// verifyEmailCode 校验邮箱验证码
|
// verifyEmailCode 校验邮箱验证码
|
||||||
// 支持 DeleteAccount 和 Security 两种场景的验证码
|
// 1. 统一复用 common checker
|
||||||
|
// 2. 强制 consume=true(最终业务操作做一次性消费)
|
||||||
|
// 3. 兼容历史 security 场景验证码(allow fallback)
|
||||||
func verifyEmailCode(ctx context.Context, serverCtx *svc.ServiceContext, email string, code string) error {
|
func verifyEmailCode(ctx context.Context, serverCtx *svc.ServiceContext, email string, code string) error {
|
||||||
// 尝试多种场景的验证码
|
l := commonLogic.NewCheckVerificationCodeLogic(ctx, serverCtx)
|
||||||
scenes := []string{constant.DeleteAccount.String(), constant.Security.String()}
|
resp, err := l.CheckVerificationCodeWithBehavior(&types.CheckVerificationCodeRequest{
|
||||||
var verified bool
|
Method: authmethod.Email,
|
||||||
var cacheKeyUsed string
|
Account: email,
|
||||||
var payload CacheKeyPayload
|
Code: strings.TrimSpace(code),
|
||||||
|
Type: uint8(constant.DeleteAccount),
|
||||||
for _, scene := range scenes {
|
}, commonLogic.VerifyCodeCheckBehavior{
|
||||||
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, scene, email)
|
Source: "delete_account",
|
||||||
value, err := serverCtx.Redis.Get(ctx, cacheKey).Result()
|
Consume: true,
|
||||||
if err != nil || value == "" {
|
AllowSceneFallback: true,
|
||||||
continue
|
})
|
||||||
}
|
if err != nil {
|
||||||
if err := json.Unmarshal([]byte(value), &payload); err != nil {
|
return err
|
||||||
continue
|
|
||||||
}
|
|
||||||
// 检查验证码是否匹配且未过期
|
|
||||||
if payload.Code == code && time.Now().Unix()-payload.LastAt <= serverCtx.Config.VerifyCode.VerifyCodeExpireTime {
|
|
||||||
verified = true
|
|
||||||
cacheKeyUsed = cacheKey
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !verified {
|
if resp == nil || !resp.Status {
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "verification code error or expired")
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "verification code error or expired")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证成功后删除缓存
|
|
||||||
serverCtx.Redis.Del(ctx, cacheKeyUsed)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,14 +5,18 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alicebob/miniredis/v2"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/perfect-panel/server/internal/config"
|
"github.com/perfect-panel/server/internal/config"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
|
"github.com/perfect-panel/server/pkg/constant"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -100,3 +104,89 @@ func TestDeleteAccountHandlerVerifyCodeErrorUsesUnifiedResponse(t *testing.T) {
|
|||||||
t.Fatalf("expected business code 70001, got %d, body=%s", resp.Code, recorder.Body.String())
|
t.Fatalf("expected business code 70001, got %d, body=%s", resp.Code, recorder.Body.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVerifyEmailCode_DeleteAccountSceneConsume(t *testing.T) {
|
||||||
|
miniRedis := miniredis.RunT(t)
|
||||||
|
redisClient := redis.NewClient(&redis.Options{Addr: miniRedis.Addr()})
|
||||||
|
t.Cleanup(func() {
|
||||||
|
redisClient.Close()
|
||||||
|
miniRedis.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
serverCtx := &svc.ServiceContext{
|
||||||
|
Redis: redisClient,
|
||||||
|
Config: config.Config{
|
||||||
|
VerifyCode: config.VerifyCode{VerifyCodeExpireTime: 900},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
email := "delete-account@example.com"
|
||||||
|
code := "112233"
|
||||||
|
cacheKey := seedDeleteSceneCode(t, redisClient, constant.DeleteAccount.String(), email, code)
|
||||||
|
|
||||||
|
err := verifyEmailCode(context.Background(), serverCtx, email, code)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("verifyEmailCode returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := redisClient.Exists(context.Background(), cacheKey).Result()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to check redis key: %v", err)
|
||||||
|
}
|
||||||
|
if exists != 0 {
|
||||||
|
t.Fatalf("expected verification code to be consumed, key still exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyEmailCode_SecurityFallbackConsume(t *testing.T) {
|
||||||
|
miniRedis := miniredis.RunT(t)
|
||||||
|
redisClient := redis.NewClient(&redis.Options{Addr: miniRedis.Addr()})
|
||||||
|
t.Cleanup(func() {
|
||||||
|
redisClient.Close()
|
||||||
|
miniRedis.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
serverCtx := &svc.ServiceContext{
|
||||||
|
Redis: redisClient,
|
||||||
|
Config: config.Config{
|
||||||
|
VerifyCode: config.VerifyCode{VerifyCodeExpireTime: 900},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
email := "security-fallback@example.com"
|
||||||
|
code := "445566"
|
||||||
|
cacheKey := seedDeleteSceneCode(t, redisClient, constant.Security.String(), email, code)
|
||||||
|
|
||||||
|
err := verifyEmailCode(context.Background(), serverCtx, email, code)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("verifyEmailCode fallback returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := redisClient.Exists(context.Background(), cacheKey).Result()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to check redis key: %v", err)
|
||||||
|
}
|
||||||
|
if exists != 0 {
|
||||||
|
t.Fatalf("expected fallback verification code to be consumed, key still exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func seedDeleteSceneCode(t *testing.T, redisClient *redis.Client, scene string, email string, code string) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, scene, email)
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"code": code,
|
||||||
|
"lastAt": time.Now().Unix(),
|
||||||
|
}
|
||||||
|
payloadRaw, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to marshal payload: %v", err)
|
||||||
|
}
|
||||||
|
err = redisClient.Set(context.Background(), cacheKey, payloadRaw, time.Minute*15).Err()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to seed redis payload: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cacheKey
|
||||||
|
}
|
||||||
|
|||||||
@ -636,7 +636,10 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
authGroupRouter.GET("/check/telephone", auth.CheckUserTelephoneHandler(serverCtx))
|
authGroupRouter.GET("/check/telephone", auth.CheckUserTelephoneHandler(serverCtx))
|
||||||
|
|
||||||
// Check legacy verification code
|
// Check legacy verification code
|
||||||
authGroupRouter.POST("/check-code", auth.CheckCodeLegacyHandler(serverCtx))
|
authGroupRouter.POST("/check-code", middleware.ApiVersionSwitchHandler(
|
||||||
|
auth.CheckCodeLegacyV1Handler(serverCtx),
|
||||||
|
auth.CheckCodeLegacyV2Handler(serverCtx),
|
||||||
|
))
|
||||||
|
|
||||||
// User login
|
// User login
|
||||||
authGroupRouter.POST("/login", auth.UserLoginHandler(serverCtx))
|
authGroupRouter.POST("/login", auth.UserLoginHandler(serverCtx))
|
||||||
@ -684,7 +687,10 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
commonGroupRouter.GET("/ads", common.GetAdsHandler(serverCtx))
|
commonGroupRouter.GET("/ads", common.GetAdsHandler(serverCtx))
|
||||||
|
|
||||||
// Check verification code
|
// Check verification code
|
||||||
commonGroupRouter.POST("/check_verification_code", common.CheckVerificationCodeHandler(serverCtx))
|
commonGroupRouter.POST("/check_verification_code", middleware.ApiVersionSwitchHandler(
|
||||||
|
common.CheckVerificationCodeV1Handler(serverCtx),
|
||||||
|
common.CheckVerificationCodeV2Handler(serverCtx),
|
||||||
|
))
|
||||||
|
|
||||||
// Get Client
|
// Get Client
|
||||||
commonGroupRouter.GET("/client", common.GetClientHandler(serverCtx))
|
commonGroupRouter.GET("/client", common.GetClientHandler(serverCtx))
|
||||||
|
|||||||
@ -205,6 +205,9 @@ func resolveVerifyScenes(verifyType constant.VerifyType, allowFallback bool) []s
|
|||||||
}
|
}
|
||||||
return []string{constant.Security.String()}
|
return []string{constant.Security.String()}
|
||||||
case constant.DeleteAccount:
|
case constant.DeleteAccount:
|
||||||
|
if allowFallback {
|
||||||
|
return []string{constant.DeleteAccount.String(), constant.Security.String()}
|
||||||
|
}
|
||||||
return []string{constant.DeleteAccount.String()}
|
return []string{constant.DeleteAccount.String()}
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
23
internal/middleware/apiVersionSwitchHandler.go
Normal file
23
internal/middleware/apiVersionSwitchHandler.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/perfect-panel/server/pkg/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ApiVersionSwitchHandler(legacyHandler gin.HandlerFunc, latestHandler gin.HandlerFunc) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
useLatest, _ := c.Request.Context().Value(constant.CtxKeyAPIVersionUseLatest).(bool)
|
||||||
|
if useLatest && latestHandler != nil {
|
||||||
|
latestHandler(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if legacyHandler != nil {
|
||||||
|
legacyHandler(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.AbortWithStatus(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
50
internal/middleware/apiVersionSwitchHandler_test.go
Normal file
50
internal/middleware/apiVersionSwitchHandler_test.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/perfect-panel/server/pkg/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestApiVersionSwitchHandlerUsesLegacyByDefault(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
r := gin.New()
|
||||||
|
r.GET("/test", ApiVersionSwitchHandler(
|
||||||
|
func(c *gin.Context) { c.String(http.StatusOK, "legacy") },
|
||||||
|
func(c *gin.Context) { c.String(http.StatusOK, "latest") },
|
||||||
|
))
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(resp, req)
|
||||||
|
|
||||||
|
if resp.Code != http.StatusOK || resp.Body.String() != "legacy" {
|
||||||
|
t.Fatalf("expected legacy handler, code=%d body=%s", resp.Code, resp.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApiVersionSwitchHandlerUsesLatestWhenFlagSet(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(func(c *gin.Context) {
|
||||||
|
ctx := context.WithValue(c.Request.Context(), constant.CtxKeyAPIVersionUseLatest, true)
|
||||||
|
c.Request = c.Request.WithContext(ctx)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
r.GET("/test", ApiVersionSwitchHandler(
|
||||||
|
func(c *gin.Context) { c.String(http.StatusOK, "legacy") },
|
||||||
|
func(c *gin.Context) { c.String(http.StatusOK, "latest") },
|
||||||
|
))
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(resp, req)
|
||||||
|
|
||||||
|
if resp.Code != http.StatusOK || resp.Body.String() != "latest" {
|
||||||
|
t.Fatalf("expected latest handler, code=%d body=%s", resp.Code, resp.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user