generated from VLADIMIR/template
add tickets api
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"evening_detective_server/internal/services/participation_service"
|
||||
"evening_detective_server/internal/services/scenarios_service"
|
||||
"evening_detective_server/internal/services/team_service"
|
||||
"evening_detective_server/internal/services/ticket_service"
|
||||
"evening_detective_server/internal/services/ui_service"
|
||||
"evening_detective_server/internal/services/users_service"
|
||||
proto "evening_detective_server/proto"
|
||||
@@ -28,6 +29,7 @@ type server struct {
|
||||
answerService *answer_service.AnswerService
|
||||
reviewService *game_review_service.Service
|
||||
teamService *team_service.TeamService
|
||||
ticketService *ticket_service.Service
|
||||
participationService *participation_service.ParticipationService
|
||||
screenRepo *screen_sessions_repo.ScreenSessionsRepo
|
||||
mapRepo *game_map_repo.Repo
|
||||
@@ -43,6 +45,7 @@ func NewServer(
|
||||
answerService *answer_service.AnswerService,
|
||||
reviewService *game_review_service.Service,
|
||||
teamService *team_service.TeamService,
|
||||
ticketService *ticket_service.Service,
|
||||
participationService *participation_service.ParticipationService,
|
||||
screenRepo *screen_sessions_repo.ScreenSessionsRepo,
|
||||
mapRepo *game_map_repo.Repo,
|
||||
@@ -57,6 +60,7 @@ func NewServer(
|
||||
answerService: answerService,
|
||||
reviewService: reviewService,
|
||||
teamService: teamService,
|
||||
ticketService: ticketService,
|
||||
participationService: participationService,
|
||||
screenRepo: screenRepo,
|
||||
mapRepo: mapRepo,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"evening_detective_server/internal/modules/processor_jwt"
|
||||
"evening_detective_server/internal/modules/roles"
|
||||
"evening_detective_server/proto"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func (s *server) IssueGameTickets(ctx context.Context, req *proto.IssueGameTicketsReq) (*proto.IssueGameTicketsRsp, error) {
|
||||
claims, ok := ctx.Value("claims").(*processor_jwt.JWTClaims)
|
||||
if !ok || claims == nil {
|
||||
return nil, status.Error(codes.Unauthenticated, "необходима авторизация")
|
||||
}
|
||||
if !roles.HasRole(claims, roles.Organizer) && !roles.HasRole(claims, roles.Author) {
|
||||
return nil, status.Error(codes.PermissionDenied, "permission denied")
|
||||
}
|
||||
if req == nil || req.GameId <= 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор игры")
|
||||
}
|
||||
|
||||
tickets, err := s.ticketService.Issue(ctx, int(req.GameId), int(req.Count))
|
||||
if err != nil {
|
||||
return &proto.IssueGameTicketsRsp{Error: err.Error()}, nil
|
||||
}
|
||||
|
||||
result := make([]*proto.Ticket, 0, len(tickets))
|
||||
for _, ticket := range tickets {
|
||||
result = append(result, &proto.Ticket{Id: int32(ticket.ID), Url: ticket.URL})
|
||||
}
|
||||
return &proto.IssueGameTicketsRsp{Tickets: result}, nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package tickets_repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type Repo struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
func New(pool *pgxpool.Pool) *Repo {
|
||||
return &Repo{pool: pool}
|
||||
}
|
||||
|
||||
func (r *Repo) CreateBatch(ctx context.Context, gameID int, tokenHashes [][]byte) ([]int, error) {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
ids := make([]int, 0, len(tokenHashes))
|
||||
for _, tokenHash := range tokenHashes {
|
||||
var id int
|
||||
if err := tx.QueryRow(ctx,
|
||||
`INSERT INTO tickets (game_id, token_hash) VALUES ($1, $2) RETURNING id`,
|
||||
gameID, tokenHash,
|
||||
).Scan(&id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package ticket_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"evening_detective_server/internal/repos/tickets_repo"
|
||||
)
|
||||
|
||||
const maxTicketsPerRequest = 100
|
||||
|
||||
var (
|
||||
ErrInvalidTicketCount = errors.New("количество билетов должно быть от 1 до 100")
|
||||
ErrInvalidBaseURL = errors.New("необходимо настроить APP_BASE_URL — публичный адрес приложения")
|
||||
)
|
||||
|
||||
type Ticket struct {
|
||||
ID int
|
||||
URL string
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
repo *tickets_repo.Repo
|
||||
appBaseURL string
|
||||
}
|
||||
|
||||
func New(repo *tickets_repo.Repo, appBaseURL string) *Service {
|
||||
return &Service{repo: repo, appBaseURL: appBaseURL}
|
||||
}
|
||||
|
||||
func (s *Service) 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 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 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
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package ticket_service
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNewToken(t *testing.T) {
|
||||
first, err := newToken()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := newToken()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if first == second || len(first) != 43 || len(second) != 43 {
|
||||
t.Fatalf("invalid tokens: %q %q", first, second)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user