Files
root 44406b72db Initial commit: img-infinite-canvas AI design workbench MVP
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>
2026-07-07 23:15:37 +08:00

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
}