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,143 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"img_infinite_canvas/internal/domain/design"
|
||||
)
|
||||
|
||||
type CodeCache interface {
|
||||
Get(ctx context.Context, key string, target any) (bool, error)
|
||||
Set(ctx context.Context, key string, value any, ttl time.Duration) error
|
||||
Delete(ctx context.Context, key string) error
|
||||
}
|
||||
|
||||
type CacheCodeStore struct {
|
||||
cache CodeCache
|
||||
ttl time.Duration
|
||||
prefix string
|
||||
}
|
||||
|
||||
type codeIndex struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
func NewCacheCodeStore(cache CodeCache, ttl time.Duration) *CacheCodeStore {
|
||||
if ttl <= 0 {
|
||||
ttl = 10 * time.Minute
|
||||
}
|
||||
return &CacheCodeStore{
|
||||
cache: cache,
|
||||
ttl: ttl,
|
||||
prefix: "auth:code",
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CacheCodeStore) CreateVerificationCode(ctx context.Context, code VerificationCode) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if code.ExpiresAt.IsZero() {
|
||||
code.ExpiresAt = time.Now().Add(s.ttl)
|
||||
}
|
||||
key := s.activeKey(code.Region, code.Channel, code.Target, code.Purpose)
|
||||
ttl := s.remainingTTL(code)
|
||||
if err := s.cache.Set(ctx, key, code, ttl); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.cache.Set(ctx, s.idKey(code.ID), codeIndex{Key: key}, ttl)
|
||||
}
|
||||
|
||||
func (s *CacheCodeStore) FindActiveVerificationCode(ctx context.Context, region string, channel string, target string, purpose string) (VerificationCode, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return VerificationCode{}, err
|
||||
}
|
||||
key := s.activeKey(region, channel, target, purpose)
|
||||
var code VerificationCode
|
||||
found, err := s.cache.Get(ctx, key, &code)
|
||||
if err != nil {
|
||||
return VerificationCode{}, err
|
||||
}
|
||||
if !found || code.ID == "" {
|
||||
return VerificationCode{}, design.ErrNotFound
|
||||
}
|
||||
if code.ConsumedAt != nil || time.Now().After(code.ExpiresAt) {
|
||||
_ = s.cache.Delete(ctx, key)
|
||||
_ = s.cache.Delete(ctx, s.idKey(code.ID))
|
||||
return VerificationCode{}, design.ErrNotFound
|
||||
}
|
||||
return code, nil
|
||||
}
|
||||
|
||||
func (s *CacheCodeStore) MarkVerificationCodeConsumed(ctx context.Context, id string, _ time.Time) error {
|
||||
index, err := s.findIndex(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.cache.Delete(ctx, index.Key); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.cache.Delete(ctx, s.idKey(id))
|
||||
}
|
||||
|
||||
func (s *CacheCodeStore) IncrementVerificationCodeAttempts(ctx context.Context, id string) error {
|
||||
index, err := s.findIndex(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var code VerificationCode
|
||||
found, err := s.cache.Get(ctx, index.Key, &code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !found || code.ID == "" {
|
||||
_ = s.cache.Delete(ctx, s.idKey(id))
|
||||
return design.ErrNotFound
|
||||
}
|
||||
code.Attempts++
|
||||
return s.cache.Set(ctx, index.Key, code, s.remainingTTL(code))
|
||||
}
|
||||
|
||||
func (s *CacheCodeStore) findIndex(ctx context.Context, id string) (codeIndex, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return codeIndex{}, err
|
||||
}
|
||||
var index codeIndex
|
||||
found, err := s.cache.Get(ctx, s.idKey(id), &index)
|
||||
if err != nil {
|
||||
return codeIndex{}, err
|
||||
}
|
||||
if !found || index.Key == "" {
|
||||
return codeIndex{}, design.ErrNotFound
|
||||
}
|
||||
return index, nil
|
||||
}
|
||||
|
||||
func (s *CacheCodeStore) remainingTTL(code VerificationCode) time.Duration {
|
||||
if !code.ExpiresAt.IsZero() {
|
||||
ttl := time.Until(code.ExpiresAt)
|
||||
if ttl > 0 {
|
||||
return ttl
|
||||
}
|
||||
}
|
||||
return s.ttl
|
||||
}
|
||||
|
||||
func (s *CacheCodeStore) activeKey(region string, channel string, target string, purpose string) string {
|
||||
raw := strings.Join([]string{
|
||||
strings.TrimSpace(region),
|
||||
strings.TrimSpace(channel),
|
||||
strings.TrimSpace(target),
|
||||
strings.TrimSpace(purpose),
|
||||
}, "\x00")
|
||||
sum := sha256.Sum256([]byte(raw))
|
||||
return s.prefix + ":active:" + hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func (s *CacheCodeStore) idKey(id string) string {
|
||||
return s.prefix + ":id:" + strings.TrimSpace(id)
|
||||
}
|
||||
Reference in New Issue
Block a user