feat(captcha): add captcha service interface and implementations
- Add captcha service interface with Generate and Verify methods - Implement local image captcha using base64Captcha library - Implement Cloudflare Turnstile verification wrapper - Support Redis-based captcha storage with 5-minute expiration - Add factory method for creating captcha service instances
This commit is contained in:
parent
fae8787ff4
commit
0dbcff85f1
98
pkg/captcha/local.go
Normal file
98
pkg/captcha/local.go
Normal file
@ -0,0 +1,98 @@
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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
|
||||
driver := base64Captcha.NewDriverDigit(80, 240, 5, 0.7, 80)
|
||||
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
|
||||
return 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 v == answer
|
||||
}
|
||||
49
pkg/captcha/service.go
Normal file
49
pkg/captcha/service.go
Normal 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
pkg/captcha/turnstile.go
Normal file
37
pkg/captcha/turnstile.go
Normal 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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user