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>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user