persona-community-5/services/persona-api/internal/adapter/memory/auth_code.go
jordan f5f3229364
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Add components: service/persona-api, worker/media-worker, app-react/creator-ui
2026-02-24 07:40:04 +00:00

88 lines
2.1 KiB
Go

package memory
import (
"context"
"log/slog"
"sync"
"time"
"git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/domain"
"git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/port"
)
// Compile-time interface check.
var _ port.AuthCodeRepository = (*AuthCodeRepository)(nil)
// AuthCodeRepository is an in-memory auth code store for standalone development.
type AuthCodeRepository struct {
mu sync.RWMutex
codes map[string]*domain.AuthCode
}
// NewAuthCodeRepository creates a new in-memory auth code repository.
func NewAuthCodeRepository() *AuthCodeRepository {
return &AuthCodeRepository{
codes: make(map[string]*domain.AuthCode),
}
}
func (r *AuthCodeRepository) Create(_ context.Context, code *domain.AuthCode) error {
r.mu.Lock()
defer r.mu.Unlock()
cp := *code
r.codes[code.ID] = &cp
// In standalone dev mode the code lives only in memory and is lost on restart.
// Always log it so the developer can copy-paste the code from the terminal
// even when NOTIFY_URL is set and an email is also being delivered.
slog.Warn("[DEV] auth code created — use this code to log in",
"email", code.Email,
"purpose", code.Purpose,
"code", code.Code,
"expires_at", code.ExpiresAt.Format("15:04:05"),
)
return nil
}
func (r *AuthCodeRepository) FindValid(_ context.Context, email string, code string, purpose domain.AuthCodePurpose) (*domain.AuthCode, error) {
r.mu.RLock()
defer r.mu.RUnlock()
for _, c := range r.codes {
if c.Email == email && c.Code == code && c.Purpose == purpose && c.IsValid() {
cp := *c
return &cp, nil
}
}
return nil, domain.ErrInvalidAuthCode
}
func (r *AuthCodeRepository) MarkUsed(_ context.Context, id string) error {
r.mu.Lock()
defer r.mu.Unlock()
c, ok := r.codes[id]
if !ok {
return domain.ErrInvalidAuthCode
}
now := time.Now()
c.UsedAt = &now
return nil
}
func (r *AuthCodeRepository) DeleteExpired(_ context.Context) (int, error) {
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
deleted := 0
for id, c := range r.codes {
if now.After(c.ExpiresAt) {
delete(r.codes, id)
deleted++
}
}
return deleted, nil
}