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,187 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"img_infinite_canvas/internal/domain/design"
|
||||
)
|
||||
|
||||
type MemoryStore struct {
|
||||
mu sync.RWMutex
|
||||
users map[string]User
|
||||
codes map[string]VerificationCode
|
||||
}
|
||||
|
||||
func NewMemoryStore() *MemoryStore {
|
||||
return &MemoryStore{
|
||||
users: make(map[string]User),
|
||||
codes: make(map[string]VerificationCode),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MemoryStore) FindUserByID(ctx context.Context, id string) (User, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
user, ok := s.users[strings.TrimSpace(id)]
|
||||
if !ok {
|
||||
return User{}, design.ErrNotFound
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) FindUserByPhone(ctx context.Context, countryCode string, phone string) (User, error) {
|
||||
return s.find(ctx, func(user User) bool {
|
||||
return user.CountryCode == normalizeCountryCode(countryCode) && user.Phone == normalizePhone(phone)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MemoryStore) FindUserByEmail(ctx context.Context, email string) (User, error) {
|
||||
email = normalizeEmail(email)
|
||||
return s.find(ctx, func(user User) bool {
|
||||
return normalizeEmail(user.Email) == email
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MemoryStore) FindUserByGoogleSubject(ctx context.Context, subject string) (User, error) {
|
||||
subject = strings.TrimSpace(subject)
|
||||
return s.find(ctx, func(user User) bool {
|
||||
return user.GoogleSubject == subject
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MemoryStore) FindUserByWechat(ctx context.Context, openID string, unionID string) (User, error) {
|
||||
openID = strings.TrimSpace(openID)
|
||||
unionID = strings.TrimSpace(unionID)
|
||||
return s.find(ctx, func(user User) bool {
|
||||
if unionID != "" && user.WechatUnionID == unionID {
|
||||
return true
|
||||
}
|
||||
return openID != "" && user.WechatOpenID == openID
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MemoryStore) UpsertUser(ctx context.Context, user User) (User, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
user.ID = normalizeUUID(user.ID)
|
||||
user.Email = normalizeEmail(user.Email)
|
||||
user.Phone = normalizePhone(user.Phone)
|
||||
user.CountryCode = normalizeCountryCode(user.CountryCode)
|
||||
user.Name = strings.TrimSpace(user.Name)
|
||||
user.AvatarURL = strings.TrimSpace(user.AvatarURL)
|
||||
user.Status = activeStatus(user.Status)
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.users[user.ID] = user
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) UpdateUserProfile(ctx context.Context, userID string, patch ProfilePatch) (User, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
userID = normalizeUUID(userID)
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
user, ok := s.users[userID]
|
||||
if !ok {
|
||||
return User{}, design.ErrNotFound
|
||||
}
|
||||
if patch.Name != nil {
|
||||
user.Name = strings.TrimSpace(*patch.Name)
|
||||
}
|
||||
if patch.AvatarURL != nil {
|
||||
user.AvatarURL = strings.TrimSpace(*patch.AvatarURL)
|
||||
}
|
||||
s.users[user.ID] = user
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) CreateVerificationCode(ctx context.Context, code VerificationCode) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.codes[code.ID] = code
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) FindActiveVerificationCode(ctx context.Context, region string, channel string, target string, purpose string) (VerificationCode, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return VerificationCode{}, err
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
var latest VerificationCode
|
||||
for _, code := range s.codes {
|
||||
if code.Region != region || code.Channel != channel || code.Target != target || code.Purpose != purpose {
|
||||
continue
|
||||
}
|
||||
if code.ConsumedAt != nil {
|
||||
continue
|
||||
}
|
||||
if latest.ID == "" || code.ExpiresAt.After(latest.ExpiresAt) {
|
||||
latest = code
|
||||
}
|
||||
}
|
||||
if latest.ID == "" {
|
||||
return VerificationCode{}, design.ErrNotFound
|
||||
}
|
||||
return latest, nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) MarkVerificationCodeConsumed(ctx context.Context, id string, consumedAt time.Time) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
code, ok := s.codes[id]
|
||||
if !ok {
|
||||
return design.ErrNotFound
|
||||
}
|
||||
code.ConsumedAt = &consumedAt
|
||||
s.codes[id] = code
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) IncrementVerificationCodeAttempts(ctx context.Context, id string) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
code, ok := s.codes[id]
|
||||
if !ok {
|
||||
return design.ErrNotFound
|
||||
}
|
||||
code.Attempts++
|
||||
s.codes[id] = code
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) find(ctx context.Context, match func(User) bool) (User, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for _, user := range s.users {
|
||||
if match(user) {
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
return User{}, design.ErrNotFound
|
||||
}
|
||||
Reference in New Issue
Block a user