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,133 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type googleTokenInfo struct {
|
||||
Audience string `json:"aud"`
|
||||
Subject string `json:"sub"`
|
||||
Email string `json:"email"`
|
||||
EmailVerified string `json:"email_verified"`
|
||||
Name string `json:"name"`
|
||||
Error string `json:"error"`
|
||||
ErrorDesc string `json:"error_description"`
|
||||
}
|
||||
|
||||
type wechatTokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
OpenID string `json:"openid"`
|
||||
UnionID string `json:"unionid"`
|
||||
Scope string `json:"scope"`
|
||||
ErrCode int64 `json:"errcode"`
|
||||
ErrMsg string `json:"errmsg"`
|
||||
}
|
||||
|
||||
func (s *Service) verifyGoogleIDToken(ctx context.Context, idToken string) (GoogleIdentity, error) {
|
||||
idToken = strings.TrimSpace(idToken)
|
||||
if idToken == "" {
|
||||
return GoogleIdentity{}, fmt.Errorf("%w: idToken is required", ErrInvalidCredentials)
|
||||
}
|
||||
endpoint := strings.TrimSpace(s.cfg.GoogleTokenInfo)
|
||||
if endpoint == "" || strings.TrimSpace(s.cfg.GoogleClientID) == "" {
|
||||
return GoogleIdentity{}, ErrProviderNotReady
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("id_token", idToken)
|
||||
requestURL := endpoint + "?" + values.Encode()
|
||||
timeout := s.cfg.GoogleTimeout
|
||||
if timeout <= 0 {
|
||||
timeout = 8 * time.Second
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
||||
if err != nil {
|
||||
return GoogleIdentity{}, err
|
||||
}
|
||||
client := &http.Client{Timeout: timeout}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return GoogleIdentity{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var info googleTokenInfo
|
||||
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
||||
return GoogleIdentity{}, err
|
||||
}
|
||||
if resp.StatusCode >= 400 || info.Error != "" {
|
||||
if info.ErrorDesc != "" {
|
||||
return GoogleIdentity{}, fmt.Errorf("%w: %s", ErrInvalidCredentials, info.ErrorDesc)
|
||||
}
|
||||
return GoogleIdentity{}, ErrInvalidCredentials
|
||||
}
|
||||
if info.Audience != strings.TrimSpace(s.cfg.GoogleClientID) {
|
||||
return GoogleIdentity{}, fmt.Errorf("%w: google audience mismatch", ErrInvalidCredentials)
|
||||
}
|
||||
if info.Subject == "" || info.Email == "" {
|
||||
return GoogleIdentity{}, fmt.Errorf("%w: google identity is incomplete", ErrInvalidCredentials)
|
||||
}
|
||||
if info.EmailVerified != "" && !strings.EqualFold(info.EmailVerified, "true") {
|
||||
return GoogleIdentity{}, fmt.Errorf("%w: google email is not verified", ErrInvalidCredentials)
|
||||
}
|
||||
return GoogleIdentity{
|
||||
Subject: info.Subject,
|
||||
Email: normalizeEmail(info.Email),
|
||||
Name: strings.TrimSpace(info.Name),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) exchangeWechatCodeHTTP(ctx context.Context, code string) (WechatIdentity, error) {
|
||||
code = strings.TrimSpace(code)
|
||||
if code == "" {
|
||||
return WechatIdentity{}, fmt.Errorf("%w: code is required", ErrInvalidCredentials)
|
||||
}
|
||||
if strings.TrimSpace(s.cfg.WechatAppID) == "" || strings.TrimSpace(s.cfg.WechatAppSecret) == "" {
|
||||
return WechatIdentity{}, ErrProviderNotReady
|
||||
}
|
||||
endpoint := strings.TrimSpace(s.cfg.WechatTokenURL)
|
||||
if endpoint == "" {
|
||||
return WechatIdentity{}, ErrProviderNotReady
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("appid", s.cfg.WechatAppID)
|
||||
values.Set("secret", s.cfg.WechatAppSecret)
|
||||
values.Set("code", code)
|
||||
values.Set("grant_type", "authorization_code")
|
||||
requestURL := endpoint + "?" + values.Encode()
|
||||
timeout := s.cfg.WechatTimeout
|
||||
if timeout <= 0 {
|
||||
timeout = 8 * time.Second
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
||||
if err != nil {
|
||||
return WechatIdentity{}, err
|
||||
}
|
||||
client := &http.Client{Timeout: timeout}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return WechatIdentity{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var token wechatTokenResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&token); err != nil {
|
||||
return WechatIdentity{}, err
|
||||
}
|
||||
if resp.StatusCode >= 400 || token.ErrCode != 0 {
|
||||
if token.ErrMsg != "" {
|
||||
return WechatIdentity{}, fmt.Errorf("%w: %s", ErrInvalidCredentials, token.ErrMsg)
|
||||
}
|
||||
return WechatIdentity{}, ErrInvalidCredentials
|
||||
}
|
||||
if token.OpenID == "" {
|
||||
return WechatIdentity{}, fmt.Errorf("%w: wechat identity is incomplete", ErrInvalidCredentials)
|
||||
}
|
||||
return WechatIdentity{
|
||||
OpenID: strings.TrimSpace(token.OpenID),
|
||||
UnionID: strings.TrimSpace(token.UnionID),
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user