44406b72db
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>
188 lines
4.5 KiB
Go
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
|
|
}
|