generated from VLADIMIR/template
146 lines
3.6 KiB
Go
146 lines
3.6 KiB
Go
package ticket_service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"evening_detective_server/internal/repos/tickets_repo"
|
|
)
|
|
|
|
const maxTicketsPerRequest = 100
|
|
|
|
var (
|
|
ErrInvalidTicketCount = errors.New("количество билетов должно быть от 1 до 100")
|
|
ErrInvalidBaseURL = errors.New("необходимо настроить APP_BASE_URL — публичный адрес приложения")
|
|
ErrTicketNotFound = tickets_repo.ErrTicketNotFound
|
|
ErrTicketUsed = tickets_repo.ErrTicketUsed
|
|
ErrGameFinished = tickets_repo.ErrGameFinished
|
|
)
|
|
|
|
type Ticket struct {
|
|
ID int
|
|
URL string
|
|
}
|
|
|
|
type Page struct {
|
|
ID int
|
|
Status string
|
|
GameName string
|
|
StartAt time.Time
|
|
Location *string
|
|
GameStatus string
|
|
UsedAt *time.Time
|
|
UsedBy *string
|
|
}
|
|
|
|
type TicketService struct {
|
|
repo *tickets_repo.Repo
|
|
appBaseURL string
|
|
}
|
|
|
|
func NewTicketService(repo *tickets_repo.Repo, appBaseURL string) *TicketService {
|
|
return &TicketService{repo: repo, appBaseURL: appBaseURL}
|
|
}
|
|
|
|
func (s *TicketService) Issue(ctx context.Context, gameID, count int) ([]*Ticket, error) {
|
|
if count < 1 || count > maxTicketsPerRequest {
|
|
return nil, ErrInvalidTicketCount
|
|
}
|
|
|
|
base, err := parseBaseURL(s.appBaseURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hashes := make([][]byte, count)
|
|
tokens := make([]string, count)
|
|
for i := range count {
|
|
token, err := newToken()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hash := sha256.Sum256([]byte(token))
|
|
tokens[i] = token
|
|
hashes[i] = hash[:]
|
|
}
|
|
|
|
ids, err := s.repo.CreateBatch(ctx, gameID, hashes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tickets := make([]*Ticket, count)
|
|
for i, id := range ids {
|
|
link := *base
|
|
link.Path = strings.TrimRight(base.Path, "/") + "/tickets/" + tokens[i]
|
|
link.RawPath = ""
|
|
tickets[i] = &Ticket{ID: id, URL: link.String()}
|
|
}
|
|
return tickets, nil
|
|
}
|
|
|
|
func (s *TicketService) Get(ctx context.Context, token string) (*Page, error) {
|
|
hash, err := tokenHash(token)
|
|
if err != nil {
|
|
return nil, ErrTicketNotFound
|
|
}
|
|
info, err := s.repo.GetByHash(ctx, hash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Page{
|
|
ID: info.ID,
|
|
Status: info.Status,
|
|
GameName: info.GameName,
|
|
StartAt: info.StartAt,
|
|
Location: info.Location,
|
|
GameStatus: info.GameStatus,
|
|
UsedAt: info.UsedAt,
|
|
UsedBy: info.UsedBy,
|
|
}, nil
|
|
}
|
|
|
|
func (s *TicketService) Accept(ctx context.Context, token string, userID int) (*Page, error) {
|
|
hash, err := tokenHash(token)
|
|
if err != nil {
|
|
return nil, ErrTicketNotFound
|
|
}
|
|
info, err := s.repo.Accept(ctx, hash, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Page{ID: info.ID, Status: info.Status, GameName: info.GameName, StartAt: info.StartAt, Location: info.Location, GameStatus: info.GameStatus, UsedAt: info.UsedAt, UsedBy: info.UsedBy}, nil
|
|
}
|
|
|
|
func newToken() (string, error) {
|
|
data := make([]byte, 32)
|
|
if _, err := rand.Read(data); err != nil {
|
|
return "", fmt.Errorf("generate ticket token: %w", err)
|
|
}
|
|
return base64.RawURLEncoding.EncodeToString(data), nil
|
|
}
|
|
|
|
func tokenHash(token string) ([]byte, error) {
|
|
data, err := base64.RawURLEncoding.DecodeString(token)
|
|
if err != nil || len(data) != 32 {
|
|
return nil, ErrTicketNotFound
|
|
}
|
|
hash := sha256.Sum256([]byte(token))
|
|
return hash[:], nil
|
|
}
|
|
|
|
func parseBaseURL(value string) (*url.URL, error) {
|
|
base, err := url.Parse(strings.TrimSpace(value))
|
|
if err != nil || base.Host == "" || (base.Scheme != "http" && base.Scheme != "https") || base.User != nil || base.RawQuery != "" || base.Fragment != "" {
|
|
return nil, ErrInvalidBaseURL
|
|
}
|
|
return base, nil
|
|
}
|