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>
144 lines
3.6 KiB
Go
144 lines
3.6 KiB
Go
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)
|
|
}
|