127 lines
3.4 KiB
Go
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
|
|
}
|