mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 15:26:36 +08:00
refactor(plugins): restructure admin and message_gateway into standard layered sub-packages
This commit is contained in:
@@ -4,7 +4,21 @@
|
||||
// Package cap 提供人机验证中间件
|
||||
package cap
|
||||
|
||||
// HTTP 响应错误文案
|
||||
const (
|
||||
errCapTokenMissing = "验证码验证失败,缺少验证码凭证" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||
errCapTokenInvalidOrExpired = "验证码校验失败或已过期,请重试" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||
errCapNotConfigured = "captcha is not configured"
|
||||
errChallengeGenerateFailed = "生成验证难题失败,请稍后再试"
|
||||
errInvalidRequestParams = "无效的参数"
|
||||
errSolutionVerifyFailed = "校验验证解答失败,请稍后再试"
|
||||
)
|
||||
|
||||
// Redeem 结果码,属于 redeem 响应 JSON 的对外契约取值,禁止改写取值
|
||||
const (
|
||||
redeemErrInvalidToken = "invalid_token"
|
||||
redeemErrNonceStoreFailed = "nonce_store_error"
|
||||
redeemErrAlreadyRedeemed = "already_redeemed"
|
||||
redeemErrSettingsLoad = "settings_load_error"
|
||||
redeemErrTokenStoreFailed = "token_store_error" //nolint:gosec // error code, not hardcoded credentials
|
||||
)
|
||||
|
||||
@@ -6,25 +6,11 @@ package cap
|
||||
import (
|
||||
"Wavelet/pkg/logger"
|
||||
"Wavelet/pkg/response"
|
||||
"Wavelet/plugins/domain/cap/pow"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ChallengeResponse is a local type alias for the pow.ChallengeResponse struct
|
||||
type ChallengeResponse = pow.ChallengeResponse
|
||||
|
||||
type challengeRequest struct {
|
||||
Scope string `json:"scope" form:"scope"`
|
||||
}
|
||||
|
||||
type redeemRequest struct {
|
||||
Token string `json:"token" binding:"required"`
|
||||
Solutions []int `json:"solutions" binding:"required"`
|
||||
Scope string `json:"scope" form:"scope"`
|
||||
}
|
||||
|
||||
// Challenge 生成 PoW 人机验证难题
|
||||
// @Summary 生成人机验证难题
|
||||
// @Description 客户端获取 PoW 难题和签名的 JWT Token,并在后台计算。
|
||||
@@ -45,13 +31,13 @@ func Challenge(c *gin.Context) {
|
||||
|
||||
mgr := GetDefaultManager()
|
||||
if mgr == nil {
|
||||
response.AbortInternal(c, "captcha is not configured")
|
||||
response.AbortInternal(c, errCapNotConfigured)
|
||||
return
|
||||
}
|
||||
resp, err := mgr.Generate(c.Request.Context(), req.Scope)
|
||||
if err != nil {
|
||||
logger.ErrorF(c.Request.Context(), "Generate cap challenge failed: %v", err)
|
||||
response.AbortInternal(c, "生成验证难题失败,请稍后再试")
|
||||
response.AbortInternal(c, errChallengeGenerateFailed)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -72,7 +58,7 @@ func Challenge(c *gin.Context) {
|
||||
func Redeem(c *gin.Context) {
|
||||
var req redeemRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.AbortBadRequest(c, "无效的参数")
|
||||
response.AbortBadRequest(c, errInvalidRequestParams)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -82,13 +68,13 @@ func Redeem(c *gin.Context) {
|
||||
|
||||
mgr := GetDefaultManager()
|
||||
if mgr == nil {
|
||||
response.AbortInternal(c, "captcha is not configured")
|
||||
response.AbortInternal(c, errCapNotConfigured)
|
||||
return
|
||||
}
|
||||
resp, err := mgr.Redeem(c.Request.Context(), req.Token, req.Solutions, req.Scope)
|
||||
if err != nil {
|
||||
logger.ErrorF(c.Request.Context(), "Redeem cap solutions failed: %v", err)
|
||||
response.AbortInternal(c, "校验验证解答失败,请稍后再试")
|
||||
response.AbortInternal(c, errSolutionVerifyFailed)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cap
|
||||
|
||||
import (
|
||||
"Wavelet/plugins/domain/cap/pow"
|
||||
)
|
||||
|
||||
// ChallengeResponse is a local type alias for the pow.ChallengeResponse struct
|
||||
type ChallengeResponse = pow.ChallengeResponse
|
||||
|
||||
// challengeRequest is the CAPTCHA challenge request payload.
|
||||
type challengeRequest struct {
|
||||
Scope string `json:"scope" form:"scope"`
|
||||
}
|
||||
|
||||
// redeemRequest is the CAPTCHA redeem request payload.
|
||||
type redeemRequest struct {
|
||||
Token string `json:"token" binding:"required"`
|
||||
Solutions []int `json:"solutions" binding:"required"`
|
||||
Scope string `json:"scope" form:"scope"`
|
||||
}
|
||||
|
||||
// RedeemResponse is returned to the client on redeem.
|
||||
type RedeemResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Token string `json:"token,omitempty"`
|
||||
Expires int64 `json:"expires,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// configRecord maps the columns selected from the system config table.
|
||||
type configRecord struct {
|
||||
Key string `gorm:"column:key"`
|
||||
Value string `gorm:"column:value"`
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cap
|
||||
|
||||
import (
|
||||
"Wavelet/core"
|
||||
"Wavelet/core/contracts"
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
dbMu sync.RWMutex
|
||||
dbSvc contracts.DBService
|
||||
)
|
||||
|
||||
// setDBService caches the DBService contract used by the persistence layer.
|
||||
func setDBService(s contracts.DBService) {
|
||||
dbMu.Lock()
|
||||
defer dbMu.Unlock()
|
||||
dbSvc = s
|
||||
}
|
||||
|
||||
// getDB resolves a GORM handle, preferring the *core.Context when supplied by callers.
|
||||
func getDB(ctx context.Context) *gorm.DB {
|
||||
if c, ok := ctx.(*core.Context); ok && c != nil {
|
||||
if s, err := core.Inject[contracts.DBService](c); err == nil && s != nil {
|
||||
return s.DB(ctx)
|
||||
}
|
||||
}
|
||||
dbMu.RLock()
|
||||
s := dbSvc
|
||||
dbMu.RUnlock()
|
||||
if s != nil {
|
||||
return s.DB(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadRuntimeSettings reads the CAPTCHA owned rows from the system config table.
|
||||
func loadRuntimeSettings(ctx context.Context) (RuntimeSettings, error) {
|
||||
var records []configRecord
|
||||
db := getDB(ctx)
|
||||
if db == nil {
|
||||
return parseRuntimeSettings(nil), nil
|
||||
}
|
||||
if err := db.Table("w_system_configs").Where("key IN ?", runtimeConfigKeys).Find(&records).Error; err != nil {
|
||||
return RuntimeSettings{}, err
|
||||
}
|
||||
configs := make(map[string]string, len(records))
|
||||
for _, r := range records {
|
||||
configs[r.Key] = r.Value
|
||||
}
|
||||
return parseRuntimeSettings(configs), nil
|
||||
}
|
||||
@@ -4,45 +4,15 @@
|
||||
package cap
|
||||
|
||||
import (
|
||||
"Wavelet/core"
|
||||
"Wavelet/core/contracts"
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/singleflight"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
dbMu sync.RWMutex
|
||||
dbSvc contracts.DBService
|
||||
)
|
||||
|
||||
func setDBService(s contracts.DBService) {
|
||||
dbMu.Lock()
|
||||
defer dbMu.Unlock()
|
||||
dbSvc = s
|
||||
}
|
||||
|
||||
func getDB(ctx context.Context) *gorm.DB {
|
||||
if c, ok := ctx.(*core.Context); ok && c != nil {
|
||||
if s, err := core.Inject[contracts.DBService](c); err == nil && s != nil {
|
||||
return s.DB(ctx)
|
||||
}
|
||||
}
|
||||
dbMu.RLock()
|
||||
s := dbSvc
|
||||
dbMu.RUnlock()
|
||||
if s != nil {
|
||||
return s.DB(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
defaultChallengeCount = 1
|
||||
defaultChallengeSize = 32
|
||||
@@ -165,26 +135,6 @@ func (s *runtimeSettingsStore) current(ctx context.Context) (RuntimeSettings, er
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
func loadRuntimeSettings(ctx context.Context) (RuntimeSettings, error) {
|
||||
type configRecord struct {
|
||||
Key string `gorm:"column:key"`
|
||||
Value string `gorm:"column:value"`
|
||||
}
|
||||
var records []configRecord
|
||||
db := getDB(ctx)
|
||||
if db == nil {
|
||||
return parseRuntimeSettings(nil), nil
|
||||
}
|
||||
if err := db.Table("w_system_configs").Where("key IN ?", runtimeConfigKeys).Find(&records).Error; err != nil {
|
||||
return RuntimeSettings{}, err
|
||||
}
|
||||
configs := make(map[string]string, len(records))
|
||||
for _, r := range records {
|
||||
configs[r.Key] = r.Value
|
||||
}
|
||||
return parseRuntimeSettings(configs), nil
|
||||
}
|
||||
|
||||
func parseRuntimeSettings(configs map[string]string) RuntimeSettings {
|
||||
settings := RuntimeSettings{
|
||||
ChallengeCount: defaultChallengeCount,
|
||||
|
||||
@@ -53,19 +53,11 @@ func (m *Manager) Generate(ctx context.Context, scope string) (*pow.ChallengeRes
|
||||
return pow.GenerateChallenge(m.secret, challengeConfig, scope)
|
||||
}
|
||||
|
||||
// RedeemResponse is returned to the client on redeem.
|
||||
type RedeemResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Token string `json:"token,omitempty"`
|
||||
Expires int64 `json:"expires,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Redeem verifies PoW solutions and returns a one-time redeem token.
|
||||
func (m *Manager) Redeem(ctx context.Context, token string, solutions []int, scope string) (*RedeemResponse, error) {
|
||||
sigHex := pow.JwtSigHex(token)
|
||||
if sigHex == "" {
|
||||
return &RedeemResponse{Success: false, Error: "invalid_token"}, nil
|
||||
return &RedeemResponse{Success: false, Error: redeemErrInvalidToken}, nil
|
||||
}
|
||||
|
||||
nonceKey := "cap:nonce:" + sigHex
|
||||
@@ -83,15 +75,15 @@ func (m *Manager) Redeem(ctx context.Context, token string, solutions []int, sco
|
||||
|
||||
set, err := m.store.SetNX(ctx, nonceKey, "1", nonceTTL)
|
||||
if err != nil {
|
||||
return &RedeemResponse{Success: false, Error: "nonce_store_error"}, err
|
||||
return &RedeemResponse{Success: false, Error: redeemErrNonceStoreFailed}, err
|
||||
}
|
||||
if !set {
|
||||
return &RedeemResponse{Success: false, Error: "already_redeemed"}, nil
|
||||
return &RedeemResponse{Success: false, Error: redeemErrAlreadyRedeemed}, nil
|
||||
}
|
||||
|
||||
settings, err := CurrentSettings(ctx)
|
||||
if err != nil {
|
||||
return &RedeemResponse{Success: false, Error: "settings_load_error"}, err
|
||||
return &RedeemResponse{Success: false, Error: redeemErrSettingsLoad}, err
|
||||
}
|
||||
|
||||
id := pow.RandomHex(redeemTokenIDLength)
|
||||
@@ -104,7 +96,7 @@ func (m *Manager) Redeem(ctx context.Context, token string, solutions []int, sco
|
||||
storeVal := strconv.FormatInt(tokenExpires.UnixNano(), 10) + "|" + scope
|
||||
|
||||
if err := m.store.Set(ctx, tokenKey, storeVal, settings.TokenTTL); err != nil {
|
||||
return &RedeemResponse{Success: false, Error: "token_store_error"}, err
|
||||
return &RedeemResponse{Success: false, Error: redeemErrTokenStoreFailed}, err
|
||||
}
|
||||
|
||||
return &RedeemResponse{
|
||||
Reference in New Issue
Block a user