Files
Nip-05-api/internal/user/service.go
2026-04-29 02:35:00 +00:00

127 lines
3.4 KiB
Go

package user
import (
"context"
"errors"
"time"
)
type Service struct {
repo *Repo
reserved []string
}
func NewService(repo *Repo, reserved []string) *Service {
return &Service{repo: repo, reserved: reserved}
}
func (s *Service) Repo() *Repo { return s.repo }
func (s *Service) Reserved() []string { return s.reserved }
// IsAvailable returns true if no active/in-grace user has the username.
func (s *Service) IsAvailable(ctx context.Context, username string) (bool, error) {
if err := ValidateUsername(username, s.reserved); err != nil {
return false, err
}
u, err := s.repo.GetByUsername(ctx, NormalizeUsername(username))
if errors.Is(err, ErrUserNotFound) {
return true, nil
}
if err != nil {
return false, err
}
_ = u
return false, nil
}
// CreateOrActivate inserts a new active user. Caller is responsible for transactional
// concerns (e.g. payments worker uses this within a tx).
func (s *Service) CreateOrActivate(ctx context.Context, pubkey, username string, sub SubscriptionType, years int, manual bool) (*User, error) {
username = NormalizeUsername(username)
if err := ValidateUsername(username, s.reserved); err != nil {
return nil, err
}
now := time.Now().UTC()
expiresAt := computeExpiry(sub, years, time.Time{}, now)
u := &User{
Pubkey: pubkey,
Username: username,
SubscriptionType: sub,
ExpiresAt: expiresAt,
IsActive: true,
ManualUsername: manual,
}
if err := s.repo.Insert(ctx, u); err != nil {
return nil, err
}
return u, nil
}
// Renew extends an existing user. Used by payments worker and admin extend.
func (s *Service) Renew(ctx context.Context, u *User, sub SubscriptionType, years int) error {
now := time.Now().UTC()
var current time.Time
if u.ExpiresAt != nil {
current = *u.ExpiresAt
}
u.SubscriptionType = sub
u.ExpiresAt = computeExpiry(sub, years, current, now)
u.IsActive = true
u.DeactivatedAt = nil
u.ExpiringReminderSentAt = nil
return s.repo.Update(ctx, u)
}
func (s *Service) SetUsername(ctx context.Context, pubkey, username string) (*User, error) {
username = NormalizeUsername(username)
if err := ValidateUsername(username, s.reserved); err != nil {
return nil, err
}
u, err := s.repo.GetByPubkey(ctx, pubkey)
if err != nil {
return nil, err
}
if existing, err := s.repo.GetByUsername(ctx, username); err == nil && existing.Pubkey != pubkey {
return nil, ErrUsernameTaken
}
u.Username = username
u.ManualUsername = true
if err := s.repo.Update(ctx, u); err != nil {
return nil, err
}
return u, nil
}
func (s *Service) Delete(ctx context.Context, pubkey string) error {
return s.repo.Delete(ctx, pubkey)
}
// computeExpiry returns *time.Time (nil for lifetime).
func computeExpiry(sub SubscriptionType, years int, current time.Time, now time.Time) *time.Time {
if sub == SubLifetime {
return nil
}
var cur time.Time
if !current.IsZero() {
cur = current
}
return YearlyTargetExpiry(&cur, years, now)
}
// YearlyTargetExpiry computes the new expiry for a yearly subscription using
// effective_start = max(now, current_expiry); new_expiry = effective_start + years.
// currentExpiry may be nil or zero for first-time purchases.
func YearlyTargetExpiry(currentExpiry *time.Time, years int, now time.Time) *time.Time {
if years <= 0 {
years = 1
}
base := now
if currentExpiry != nil && currentExpiry.After(base) {
base = *currentExpiry
}
t := base.AddDate(years, 0, 0)
return &t
}