44406b72db
Moteva-style AI design workbench replica. Home prompt creates a project; the project page provides an infinite canvas with node drag, zoom/pan, a design chat panel, history replay, image asset upload, and project save/regenerate. - Backend: go-zero, DDD layering, sqlc/pgx, optional PostgreSQL; memory or Redis cache; asynq job queue; MinIO/S3/R2/OSS object storage; sky-valley/pi agent runtime adapter. - Frontend: Next.js App Router + Vite artifact build, TypeScript, i18n, shadcn/ui components, auth (OTP/Turnstile/Google/WeChat). - Deploy: Docker Compose and k3s manifests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
107 lines
2.7 KiB
Go
107 lines
2.7 KiB
Go
package auth
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
const defaultTurnstileSiteVerifyURL = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
|
|
|
|
type TurnstileVerifierOptions struct {
|
|
SecretKey string
|
|
SiteVerifyURL string
|
|
Timeout time.Duration
|
|
HTTPClient *http.Client
|
|
}
|
|
|
|
type TurnstileVerifier struct {
|
|
secretKey string
|
|
siteVerifyURL string
|
|
timeout time.Duration
|
|
client *http.Client
|
|
}
|
|
|
|
type turnstileVerifyRequest struct {
|
|
Secret string `json:"secret"`
|
|
Response string `json:"response"`
|
|
IdempotencyKey string `json:"idempotency_key,omitempty"`
|
|
}
|
|
|
|
type turnstileVerifyResponse struct {
|
|
Success bool `json:"success"`
|
|
ErrorCodes []string `json:"error-codes"`
|
|
}
|
|
|
|
func NewTurnstileVerifier(opts TurnstileVerifierOptions) (*TurnstileVerifier, error) {
|
|
secretKey := strings.TrimSpace(opts.SecretKey)
|
|
if secretKey == "" {
|
|
return nil, ErrProviderNotReady
|
|
}
|
|
siteVerifyURL := strings.TrimSpace(opts.SiteVerifyURL)
|
|
if siteVerifyURL == "" {
|
|
siteVerifyURL = defaultTurnstileSiteVerifyURL
|
|
}
|
|
timeout := opts.Timeout
|
|
if timeout <= 0 {
|
|
timeout = 5 * time.Second
|
|
}
|
|
client := opts.HTTPClient
|
|
if client == nil {
|
|
client = &http.Client{Timeout: timeout}
|
|
}
|
|
return &TurnstileVerifier{
|
|
secretKey: secretKey,
|
|
siteVerifyURL: siteVerifyURL,
|
|
timeout: timeout,
|
|
client: client,
|
|
}, nil
|
|
}
|
|
|
|
func (v *TurnstileVerifier) Verify(ctx context.Context, token string) error {
|
|
token = strings.TrimSpace(token)
|
|
if token == "" {
|
|
return ErrHumanVerificationRequired
|
|
}
|
|
ctx, cancel := context.WithTimeout(ctx, v.timeout)
|
|
defer cancel()
|
|
|
|
body, err := json.Marshal(turnstileVerifyRequest{
|
|
Secret: v.secretKey,
|
|
Response: token,
|
|
IdempotencyKey: uuid.NewString(),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, v.siteVerifyURL, bytes.NewReader(body))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := v.client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: turnstile verification request failed", ErrHumanVerificationInvalid)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var result turnstileVerifyResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return fmt.Errorf("%w: turnstile verification response is invalid", ErrHumanVerificationInvalid)
|
|
}
|
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices || !result.Success {
|
|
if len(result.ErrorCodes) > 0 {
|
|
return fmt.Errorf("%w: %s", ErrHumanVerificationInvalid, strings.Join(result.ErrorCodes, ","))
|
|
}
|
|
return ErrHumanVerificationInvalid
|
|
}
|
|
return nil
|
|
}
|