Merge remote-tracking branch 'origin/master' into internal

This commit is contained in:
2026-03-19 01:55:01 -07:00
101 changed files with 6910 additions and 330 deletions
+109
View File
@@ -0,0 +1,109 @@
package captcha
import (
"context"
"fmt"
"strings"
"time"
"github.com/mojocn/base64Captcha"
"github.com/redis/go-redis/v9"
)
type localService struct {
redis *redis.Client
driver base64Captcha.Driver
}
func newLocalService(redisClient *redis.Client) Service {
// Configure captcha driver - alphanumeric with visual effects (letters + numbers)
driver := base64Captcha.NewDriverString(
80, // height
240, // width
20, // noise count (more interference)
base64Captcha.OptionShowSlimeLine|base64Captcha.OptionShowSineLine, // show curved lines
5, // length (5 characters)
"abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", // source (exclude confusing chars)
nil, // bg color (use default)
nil, // fonts (use default)
nil, // fonts storage (use default)
)
return &localService{
redis: redisClient,
driver: driver,
}
}
func (s *localService) Generate(ctx context.Context) (id string, image string, err error) {
// Generate captcha
captcha := base64Captcha.NewCaptcha(s.driver, &redisStore{
redis: s.redis,
ctx: ctx,
})
id, b64s, answer, err := captcha.Generate()
if err != nil {
return "", "", err
}
// Store answer in Redis with 5 minute expiration
key := fmt.Sprintf("captcha:%s", id)
err = s.redis.Set(ctx, key, answer, 5*time.Minute).Err()
if err != nil {
return "", "", err
}
return id, b64s, nil
}
func (s *localService) Verify(ctx context.Context, id string, code string, ip string) (bool, error) {
if id == "" || code == "" {
return false, nil
}
key := fmt.Sprintf("captcha:%s", id)
// Get answer from Redis
answer, err := s.redis.Get(ctx, key).Result()
if err != nil {
return false, err
}
// Delete captcha after verification (one-time use)
s.redis.Del(ctx, key)
// Verify code (case-insensitive)
return strings.EqualFold(answer, code), nil
}
func (s *localService) GetType() CaptchaType {
return CaptchaTypeLocal
}
// redisStore implements base64Captcha.Store interface
type redisStore struct {
redis *redis.Client
ctx context.Context
}
func (r *redisStore) Set(id string, value string) error {
key := fmt.Sprintf("captcha:%s", id)
return r.redis.Set(r.ctx, key, value, 5*time.Minute).Err()
}
func (r *redisStore) Get(id string, clear bool) string {
key := fmt.Sprintf("captcha:%s", id)
val, err := r.redis.Get(r.ctx, key).Result()
if err != nil {
return ""
}
if clear {
r.redis.Del(r.ctx, key)
}
return val
}
func (r *redisStore) Verify(id, answer string, clear bool) bool {
v := r.Get(id, clear)
return strings.EqualFold(v, answer)
}
+49
View File
@@ -0,0 +1,49 @@
package captcha
import (
"context"
"github.com/redis/go-redis/v9"
)
type CaptchaType string
const (
CaptchaTypeLocal CaptchaType = "local"
CaptchaTypeTurnstile CaptchaType = "turnstile"
)
// Service defines the captcha service interface
type Service interface {
// Generate generates a new captcha
// For local captcha: returns id and base64 image
// For turnstile: returns empty strings
Generate(ctx context.Context) (id string, image string, err error)
// Verify verifies the captcha
// For local captcha: token is captcha id, code is user input
// For turnstile: token is cf-turnstile-response, code is ignored
Verify(ctx context.Context, token string, code string, ip string) (bool, error)
// GetType returns the captcha type
GetType() CaptchaType
}
// Config holds the configuration for captcha service
type Config struct {
Type CaptchaType
RedisClient *redis.Client
TurnstileSecret string
}
// NewService creates a new captcha service based on the config
func NewService(config Config) Service {
switch config.Type {
case CaptchaTypeTurnstile:
return newTurnstileService(config.TurnstileSecret)
case CaptchaTypeLocal:
fallthrough
default:
return newLocalService(config.RedisClient)
}
}
+37
View File
@@ -0,0 +1,37 @@
package captcha
import (
"context"
"github.com/perfect-panel/server/pkg/turnstile"
)
type turnstileService struct {
service turnstile.Service
}
func newTurnstileService(secret string) Service {
return &turnstileService{
service: turnstile.New(turnstile.Config{
Secret: secret,
}),
}
}
func (s *turnstileService) Generate(ctx context.Context) (id string, image string, err error) {
// Turnstile doesn't need server-side generation
return "", "", nil
}
func (s *turnstileService) Verify(ctx context.Context, token string, code string, ip string) (bool, error) {
if token == "" {
return false, nil
}
// Verify with Cloudflare Turnstile
return s.service.Verify(ctx, token, ip)
}
func (s *turnstileService) GetType() CaptchaType {
return CaptchaTypeTurnstile
}
+3 -1
View File
@@ -56,7 +56,9 @@ func (s *service) verify(ctx context.Context, secret string, token string, ip st
_ = writer.WriteField("idempotency_key", key)
}
_ = writer.Close()
client := &http.Client{}
client := &http.Client{
Timeout: 5 * time.Second,
}
req, _ := http.NewRequest("POST", s.url, body)
req.Header.Set("Content-Type", writer.FormDataContentType())
firstResult, err := client.Do(req)