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

188 lines
4.5 KiB
Go

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
}