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

134 lines
4.1 KiB
Go

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
}