118 lines
2.7 KiB
Go
118 lines
2.7 KiB
Go
package invite
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"crypto/rand"
|
||
|
|
"log/slog"
|
||
|
|
"math/big"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
type Service struct {
|
||
|
|
r *Repository
|
||
|
|
codeGroupSize int
|
||
|
|
codeNumGroups int
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewService(r *Repository, codeGroupSize, codeNumGroups int) (*Service, error) {
|
||
|
|
if codeGroupSize == 0 {
|
||
|
|
codeGroupSize = 5
|
||
|
|
}
|
||
|
|
if codeNumGroups == 0 {
|
||
|
|
codeNumGroups = 5
|
||
|
|
}
|
||
|
|
return &Service{r: r, codeGroupSize: codeGroupSize, codeNumGroups: codeNumGroups}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) generateCode() (string, error) {
|
||
|
|
const alphabet = "23456789ABCDEFGHJKMNPQRSTVWXYZ"
|
||
|
|
totalChars := s.codeGroupSize * s.codeNumGroups
|
||
|
|
|
||
|
|
alphabetLen := big.NewInt(int64(len(alphabet)))
|
||
|
|
var builder strings.Builder
|
||
|
|
|
||
|
|
builder.Grow(totalChars + (s.codeNumGroups - 1))
|
||
|
|
|
||
|
|
for i := range totalChars {
|
||
|
|
if i > 0 && i%s.codeGroupSize == 0 {
|
||
|
|
builder.WriteByte('-')
|
||
|
|
}
|
||
|
|
|
||
|
|
randIndex, err := rand.Int(rand.Reader, alphabetLen)
|
||
|
|
if err != nil {
|
||
|
|
slog.Error("generate invite code", slog.String("op", "service.generateCode"), slog.Any("error", err))
|
||
|
|
return "", ErrInternal
|
||
|
|
}
|
||
|
|
|
||
|
|
builder.WriteByte(alphabet[randIndex.Int64()])
|
||
|
|
}
|
||
|
|
|
||
|
|
return builder.String(), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) CreateInvite(ctx context.Context, quota int, expiresAt time.Time) (*Invite, error) {
|
||
|
|
token, err := s.generateCode()
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
return s.r.CreateInvite(ctx, token, quota, expiresAt)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) InviteByToken(ctx context.Context, token string) (*Invite, error) {
|
||
|
|
return s.r.InviteByToken(ctx, token)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) CountInvites(ctx context.Context) (int64, error) {
|
||
|
|
return s.r.CountInvites(ctx)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) Invites(ctx context.Context, limit, offset int64) ([]*Invite, error) {
|
||
|
|
return s.r.Invites(ctx, limit, offset)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) UseInvite(ctx context.Context, token string) (*Invite, error) {
|
||
|
|
tx, err := s.r.Db.BeginTx(ctx, nil)
|
||
|
|
if err != nil {
|
||
|
|
slog.Error("begin transaction", slog.String("op", "service.UseInvite"), slog.String("token", token), slog.Any("error", err))
|
||
|
|
return nil, ErrInternal
|
||
|
|
}
|
||
|
|
defer tx.Rollback()
|
||
|
|
txRepo := s.r.WithTx(tx)
|
||
|
|
|
||
|
|
invite, err := txRepo.InviteByToken(ctx, token)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
if !invite.IsValid() {
|
||
|
|
return nil, ErrInvalid
|
||
|
|
}
|
||
|
|
|
||
|
|
value := *invite
|
||
|
|
cloned := &value
|
||
|
|
|
||
|
|
cloned.Uses += 1
|
||
|
|
if cloned.Uses < cloned.Quota {
|
||
|
|
err := txRepo.UpdateInvite(ctx, cloned)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
err := txRepo.DeleteInvite(ctx, cloned.Token)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
tx.Commit()
|
||
|
|
return cloned, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) DeleteInvite(ctx context.Context, invite *Invite) error {
|
||
|
|
return s.r.DeleteInvite(ctx, invite.Token)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Service) CleanupInvites(ctx context.Context) error {
|
||
|
|
return s.r.CleanupInvites(ctx)
|
||
|
|
}
|