generated from VLADIMIR/template
up
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
// Package device_presence_service tracks browser connections in venue memory.
|
||||
package device_presence_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"evening_detective_server/internal/services/game_model"
|
||||
"evening_detective_server/internal/services/screen_service"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalid = errors.New("некорректные данные устройства")
|
||||
ErrDisabled = errors.New("учёт устройств доступен только на площадке")
|
||||
ErrPermission = errors.New("нет доступа к устройствам")
|
||||
ErrUnauthorized = errors.New("требуется авторизация")
|
||||
ErrFull = errors.New("слишком много подключений")
|
||||
)
|
||||
|
||||
const (
|
||||
connectedFor = time.Minute
|
||||
retainedFor = 15 * time.Minute
|
||||
maxConnections = 1024
|
||||
)
|
||||
|
||||
type IGamePresence interface {
|
||||
GetGame(context.Context, int) (*game_model.Game, error)
|
||||
}
|
||||
type ITeamPresence interface {
|
||||
CheckPresence(context.Context, int, string) (int, error)
|
||||
}
|
||||
type IScreenPresence interface {
|
||||
Get(context.Context, int, string) (*screen_service.ScreenSession, error)
|
||||
}
|
||||
|
||||
// Heartbeat contains transient credentials; only the verified connection is retained.
|
||||
type Heartbeat struct {
|
||||
DeviceID string
|
||||
ConnectionID string
|
||||
OrganizerGameID *int
|
||||
TeamID *int
|
||||
Password string
|
||||
ScreenID string
|
||||
ScreenTokenHash string
|
||||
IP string
|
||||
UserAgent string
|
||||
IsAuthenticated bool
|
||||
CanManage bool
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
ID string
|
||||
DeviceID string
|
||||
FirstSeenAt time.Time
|
||||
LastSeenAt time.Time
|
||||
IP string
|
||||
UserAgent string
|
||||
Kind string
|
||||
GameID int
|
||||
TeamID int
|
||||
ScreenID int
|
||||
Connected bool
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
ID string
|
||||
FirstSeenAt time.Time
|
||||
LastSeenAt time.Time
|
||||
Connected bool
|
||||
Connections []Connection
|
||||
}
|
||||
|
||||
type DevicePresenceService struct {
|
||||
enabled bool
|
||||
games IGamePresence
|
||||
teams ITeamPresence
|
||||
screens IScreenPresence
|
||||
now func() time.Time
|
||||
mu sync.Mutex
|
||||
connections map[string]Connection
|
||||
}
|
||||
|
||||
func NewDevicePresenceService(enabled bool, games IGamePresence, teams ITeamPresence, screens IScreenPresence) *DevicePresenceService {
|
||||
return &DevicePresenceService{
|
||||
enabled: enabled,
|
||||
games: games,
|
||||
teams: teams,
|
||||
screens: screens,
|
||||
now: time.Now,
|
||||
connections: make(map[string]Connection),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DevicePresenceService) Heartbeat(ctx context.Context, input Heartbeat) (bool, time.Time, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return false, time.Time{}, err
|
||||
}
|
||||
if !s.enabled {
|
||||
return false, s.now(), nil
|
||||
}
|
||||
connection, err := s.verify(ctx, input)
|
||||
if err != nil {
|
||||
return true, time.Time{}, err
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
now := s.now()
|
||||
s.prune(now)
|
||||
old, exists := s.connections[connection.ID]
|
||||
if exists && old.DeviceID != connection.DeviceID {
|
||||
return true, now, ErrInvalid
|
||||
}
|
||||
if !exists && len(s.connections) >= maxConnections {
|
||||
return true, now, ErrFull
|
||||
}
|
||||
connection.FirstSeenAt = now
|
||||
if exists {
|
||||
connection.FirstSeenAt = old.FirstSeenAt
|
||||
}
|
||||
connection.LastSeenAt = now
|
||||
s.connections[connection.ID] = connection
|
||||
return true, now, nil
|
||||
}
|
||||
|
||||
func (s *DevicePresenceService) List(ctx context.Context, gameID *int, canManage bool) ([]Device, time.Time, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
if !s.enabled {
|
||||
return nil, time.Time{}, ErrDisabled
|
||||
}
|
||||
if !canManage {
|
||||
return nil, time.Time{}, ErrPermission
|
||||
}
|
||||
if gameID != nil && *gameID <= 0 {
|
||||
return nil, time.Time{}, ErrInvalid
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
now := s.now()
|
||||
s.prune(now)
|
||||
devices := make(map[string]*Device)
|
||||
for _, connection := range s.connections {
|
||||
if gameID != nil && connection.GameID != *gameID {
|
||||
continue
|
||||
}
|
||||
connection.Connected = now.Sub(connection.LastSeenAt) < connectedFor
|
||||
device := devices[connection.DeviceID]
|
||||
if device == nil {
|
||||
device = &Device{
|
||||
ID: connection.DeviceID,
|
||||
FirstSeenAt: connection.FirstSeenAt,
|
||||
LastSeenAt: connection.LastSeenAt,
|
||||
}
|
||||
devices[connection.DeviceID] = device
|
||||
}
|
||||
if connection.FirstSeenAt.Before(device.FirstSeenAt) {
|
||||
device.FirstSeenAt = connection.FirstSeenAt
|
||||
}
|
||||
if connection.LastSeenAt.After(device.LastSeenAt) {
|
||||
device.LastSeenAt = connection.LastSeenAt
|
||||
}
|
||||
device.Connected = device.Connected || connection.Connected
|
||||
device.Connections = append(device.Connections, connection)
|
||||
}
|
||||
result := make([]Device, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
sort.Slice(device.Connections, func(i, j int) bool { return device.Connections[i].ID < device.Connections[j].ID })
|
||||
result = append(result, *device)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool { return result[i].ID < result[j].ID })
|
||||
return result, now, nil
|
||||
}
|
||||
|
||||
func (s *DevicePresenceService) prune(now time.Time) {
|
||||
for id, connection := range s.connections {
|
||||
if now.Sub(connection.LastSeenAt) >= retainedFor {
|
||||
delete(s.connections, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DevicePresenceService) verify(ctx context.Context, input Heartbeat) (Connection, error) {
|
||||
deviceID, err := parseUUID(input.DeviceID)
|
||||
if err != nil {
|
||||
return Connection{}, err
|
||||
}
|
||||
connectionID, err := parseUUID(input.ConnectionID)
|
||||
if err != nil {
|
||||
return Connection{}, err
|
||||
}
|
||||
organizer := input.OrganizerGameID != nil
|
||||
team := input.TeamID != nil || input.Password != ""
|
||||
screen := input.ScreenID != "" || input.ScreenTokenHash != ""
|
||||
contexts := 0
|
||||
for _, present := range []bool{organizer, team, screen} {
|
||||
if present {
|
||||
contexts++
|
||||
}
|
||||
}
|
||||
if contexts > 1 || organizer && *input.OrganizerGameID <= 0 || team && (input.TeamID == nil || *input.TeamID <= 0 || input.Password == "") || screen && (input.ScreenID == "" || input.ScreenTokenHash == "") {
|
||||
return Connection{}, ErrInvalid
|
||||
}
|
||||
connection := Connection{
|
||||
ID: connectionID,
|
||||
DeviceID: deviceID,
|
||||
Kind: "browser",
|
||||
IP: input.IP,
|
||||
UserAgent: input.UserAgent,
|
||||
}
|
||||
// Bound untrusted metadata as well as the number of records.
|
||||
if len(connection.UserAgent) > 1024 {
|
||||
connection.UserAgent = connection.UserAgent[:1024]
|
||||
}
|
||||
connection.UserAgent = strings.ToValidUTF8(connection.UserAgent, "")
|
||||
if organizer {
|
||||
if !input.IsAuthenticated {
|
||||
return Connection{}, ErrUnauthorized
|
||||
}
|
||||
if !input.CanManage {
|
||||
return Connection{}, ErrPermission
|
||||
}
|
||||
if _, err := s.games.GetGame(ctx, *input.OrganizerGameID); err != nil {
|
||||
return Connection{}, err
|
||||
}
|
||||
connection.Kind = "organizer"
|
||||
connection.GameID = *input.OrganizerGameID
|
||||
}
|
||||
if team {
|
||||
gameID, err := s.teams.CheckPresence(ctx, *input.TeamID, input.Password)
|
||||
if err != nil {
|
||||
return Connection{}, err
|
||||
}
|
||||
connection.Kind = "team"
|
||||
connection.TeamID = *input.TeamID
|
||||
connection.GameID = gameID
|
||||
}
|
||||
if screen {
|
||||
id, err := strconv.Atoi(input.ScreenID)
|
||||
if err != nil || id <= 0 || id > 2147483647 {
|
||||
return Connection{}, ErrInvalid
|
||||
}
|
||||
session, err := s.screens.Get(ctx, id, input.ScreenTokenHash)
|
||||
if err != nil {
|
||||
return Connection{}, err
|
||||
}
|
||||
connection.Kind = "screen"
|
||||
connection.ScreenID = session.ID
|
||||
if session.GameID != nil {
|
||||
connection.GameID = *session.GameID
|
||||
}
|
||||
}
|
||||
return connection, nil
|
||||
}
|
||||
|
||||
func parseUUID(value string) (string, error) {
|
||||
parsed, err := uuid.Parse(value)
|
||||
if err != nil || len(value) != 36 || parsed == uuid.Nil {
|
||||
return "", ErrInvalid
|
||||
}
|
||||
return parsed.String(), nil
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
package device_presence_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"evening_detective_server/internal/services/game_model"
|
||||
"evening_detective_server/internal/services/game_service"
|
||||
"evening_detective_server/internal/services/screen_service"
|
||||
"evening_detective_server/internal/services/team_service"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type presenceFixture struct{}
|
||||
|
||||
func (presenceFixture) GetGame(_ context.Context, id int) (*game_model.Game, error) {
|
||||
if id != 7 {
|
||||
return nil, game_service.ErrGameNotFound
|
||||
}
|
||||
return &game_model.Game{}, nil
|
||||
}
|
||||
func (presenceFixture) CheckPresence(_ context.Context, id int, password string) (int, error) {
|
||||
if id != 2 || password != "secret" {
|
||||
return 0, team_service.ErrTeamNotFound
|
||||
}
|
||||
return 7, nil
|
||||
}
|
||||
func (presenceFixture) Get(_ context.Context, id int, hash string) (*screen_service.ScreenSession, error) {
|
||||
if id != 3 || hash != "hash" {
|
||||
return nil, screen_service.ErrScreenNotFound
|
||||
}
|
||||
gameID := 7
|
||||
return &screen_service.ScreenSession{ID: 3, GameID: &gameID}, nil
|
||||
}
|
||||
func newFixture() (*DevicePresenceService, Heartbeat) {
|
||||
fixture := presenceFixture{}
|
||||
return NewDevicePresenceService(true, fixture, fixture, fixture), Heartbeat{DeviceID: uuid.NewString(), ConnectionID: uuid.NewString()}
|
||||
}
|
||||
func pointer(id int) *int { return &id }
|
||||
func send(t *testing.T, s *DevicePresenceService, input Heartbeat) {
|
||||
t.Helper()
|
||||
enabled, _, err := s.Heartbeat(context.Background(), input)
|
||||
if !enabled || err != nil {
|
||||
t.Fatalf("heartbeat: %v %v", enabled, err)
|
||||
}
|
||||
}
|
||||
func list(t *testing.T, s *DevicePresenceService, gameID *int) []Device {
|
||||
t.Helper()
|
||||
devices, _, err := s.List(context.Background(), gameID, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return devices
|
||||
}
|
||||
|
||||
func TestRegistrationAndContext(t *testing.T) {
|
||||
s, input := newFixture()
|
||||
now := time.Date(2026, 10, 11, 0, 0, 0, 0, time.UTC)
|
||||
s.now = func() time.Time { return now }
|
||||
input.TeamID, input.Password = pointer(2), "secret"
|
||||
send(t, s, input)
|
||||
now = now.Add(time.Second)
|
||||
input.IP, input.UserAgent = "192.0.2.1", "updated"
|
||||
send(t, s, input)
|
||||
devices := list(t, s, pointer(7))
|
||||
if len(devices) != 1 || len(devices[0].Connections) != 1 {
|
||||
t.Fatal(devices)
|
||||
}
|
||||
connection := devices[0].Connections[0]
|
||||
if connection.IP != input.IP || connection.TeamID != 2 || connection.FirstSeenAt.Equal(connection.LastSeenAt) {
|
||||
t.Fatal(connection)
|
||||
}
|
||||
tab := input
|
||||
tab.ConnectionID = uuid.NewString()
|
||||
tab.TeamID, tab.Password = nil, ""
|
||||
tab.OrganizerGameID, tab.IsAuthenticated, tab.CanManage = pointer(7), true, true
|
||||
send(t, s, tab)
|
||||
if len(list(t, s, nil)[0].Connections) != 2 {
|
||||
t.Fatal("tabs not grouped")
|
||||
}
|
||||
input.TeamID, input.Password = nil, ""
|
||||
input.ScreenID, input.ScreenTokenHash = "3", "hash"
|
||||
send(t, s, input)
|
||||
connection = s.connections[input.ConnectionID]
|
||||
if connection.TeamID != 0 || connection.ScreenID != 3 || connection.GameID != 7 {
|
||||
t.Fatal(connection)
|
||||
}
|
||||
input.ScreenID, input.ScreenTokenHash = "", ""
|
||||
send(t, s, input)
|
||||
connection = s.connections[input.ConnectionID]
|
||||
if connection.TeamID != 0 || connection.ScreenID != 0 || connection.GameID != 0 || connection.Kind != "browser" {
|
||||
t.Fatal(connection)
|
||||
}
|
||||
if len(list(t, s, pointer(7))[0].Connections) != 1 {
|
||||
t.Fatal("filter includes other contexts")
|
||||
}
|
||||
devices[0].Connections[0].GameID = 99
|
||||
if s.connections[input.ConnectionID].GameID != 0 {
|
||||
t.Fatal("list exposes registry memory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeBoundaries(t *testing.T) {
|
||||
s, input := newFixture()
|
||||
start := time.Now()
|
||||
now := start
|
||||
s.now = func() time.Time { return now }
|
||||
send(t, s, input)
|
||||
for _, test := range []struct {
|
||||
age time.Duration
|
||||
exists, connected bool
|
||||
}{
|
||||
{time.Minute - time.Nanosecond, true, true},
|
||||
{time.Minute, true, false},
|
||||
{15*time.Minute - time.Nanosecond, true, false},
|
||||
{15 * time.Minute, false, false},
|
||||
} {
|
||||
now = start.Add(test.age)
|
||||
devices := list(t, s, nil)
|
||||
if (len(devices) > 0) != test.exists {
|
||||
t.Fatalf("retention at %v: %v", test.age, devices)
|
||||
}
|
||||
if test.exists && (devices[0].Connected != test.connected || devices[0].Connections[0].Connected != test.connected) {
|
||||
t.Fatalf("connection at %v", test.age)
|
||||
}
|
||||
}
|
||||
send(t, s, input)
|
||||
now = now.Add(59 * time.Second)
|
||||
tab := input
|
||||
tab.ConnectionID = uuid.NewString()
|
||||
send(t, s, tab)
|
||||
now = now.Add(time.Second)
|
||||
if !list(t, s, nil)[0].Connected {
|
||||
t.Fatal("live tab must keep device connected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapacityAndPruning(t *testing.T) {
|
||||
s, input := newFixture()
|
||||
now := time.Now()
|
||||
s.now = func() time.Time { return now }
|
||||
for range maxConnections {
|
||||
input.ConnectionID = uuid.NewString()
|
||||
send(t, s, input)
|
||||
}
|
||||
send(t, s, input)
|
||||
extra := input
|
||||
extra.ConnectionID = uuid.NewString()
|
||||
_, _, err := s.Heartbeat(context.Background(), extra)
|
||||
if !errors.Is(err, ErrFull) || len(s.connections) != maxConnections {
|
||||
t.Fatal(err)
|
||||
}
|
||||
now = now.Add(retainedFor)
|
||||
send(t, s, extra)
|
||||
if len(s.connections) != 1 {
|
||||
t.Fatal("expired records not pruned on update")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidContextDoesNotOverwrite(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(*Heartbeat)
|
||||
want error
|
||||
}{
|
||||
{"device UUID", func(h *Heartbeat) { h.DeviceID = "invalid" }, ErrInvalid},
|
||||
{"connection UUID", func(h *Heartbeat) { h.ConnectionID = "" }, ErrInvalid},
|
||||
{"nil UUID", func(h *Heartbeat) { h.DeviceID = uuid.Nil.String() }, ErrInvalid},
|
||||
{"device reassignment", func(h *Heartbeat) { h.DeviceID = uuid.NewString() }, ErrInvalid},
|
||||
{"mixed", func(h *Heartbeat) { h.TeamID = pointer(2); h.Password = "secret"; h.OrganizerGameID = pointer(7) }, ErrInvalid},
|
||||
{"password only", func(h *Heartbeat) { h.Password = "secret" }, ErrInvalid},
|
||||
{"team only", func(h *Heartbeat) { h.TeamID = pointer(2) }, ErrInvalid},
|
||||
{"screen only", func(h *Heartbeat) { h.ScreenID = "3" }, ErrInvalid},
|
||||
{"token only", func(h *Heartbeat) { h.ScreenTokenHash = "hash" }, ErrInvalid},
|
||||
{"screen ID", func(h *Heartbeat) { h.ScreenID = "oops"; h.ScreenTokenHash = "hash" }, ErrInvalid},
|
||||
{"negative game", func(h *Heartbeat) { h.OrganizerGameID = pointer(-1) }, ErrInvalid},
|
||||
{"anonymous organizer", func(h *Heartbeat) { h.OrganizerGameID = pointer(7) }, ErrUnauthorized},
|
||||
{"user organizer", func(h *Heartbeat) { h.OrganizerGameID = pointer(7); h.IsAuthenticated = true }, ErrPermission},
|
||||
{"missing game", func(h *Heartbeat) { h.OrganizerGameID = pointer(8); h.IsAuthenticated = true; h.CanManage = true }, game_service.ErrGameNotFound},
|
||||
{"bad team", func(h *Heartbeat) { h.TeamID = pointer(2); h.Password = "bad" }, team_service.ErrTeamNotFound},
|
||||
{"bad screen", func(h *Heartbeat) { h.ScreenID = "3"; h.ScreenTokenHash = "bad" }, screen_service.ErrScreenNotFound},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
s, input := newFixture()
|
||||
send(t, s, input)
|
||||
before := s.connections[input.ConnectionID]
|
||||
test.mutate(&input)
|
||||
_, _, err := s.Heartbeat(context.Background(), input)
|
||||
if !errors.Is(err, test.want) {
|
||||
t.Fatalf("got %v, want %v", err, test.want)
|
||||
}
|
||||
if s.connections[before.ID] != before || len(s.connections) != 1 {
|
||||
t.Fatal("invalid request modified registry")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestModeAndReadAccess(t *testing.T) {
|
||||
s, input := newFixture()
|
||||
if _, _, err := s.List(context.Background(), nil, false); !errors.Is(err, ErrPermission) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err := s.List(context.Background(), pointer(0), true); !errors.Is(err, ErrInvalid) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.enabled = false
|
||||
enabled, now, err := s.Heartbeat(context.Background(), Heartbeat{})
|
||||
if enabled || now.IsZero() || err != nil || len(s.connections) != 0 {
|
||||
t.Fatal("prod registered device")
|
||||
}
|
||||
if _, _, err = s.List(context.Background(), nil, true); !errors.Is(err, ErrDisabled) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
if _, _, err = s.Heartbeat(ctx, input); !errors.Is(err, context.Canceled) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentAccess(t *testing.T) {
|
||||
s, input := newFixture()
|
||||
var wg sync.WaitGroup
|
||||
for range 20 {
|
||||
wg.Go(func() {
|
||||
tab := input
|
||||
tab.ConnectionID = uuid.NewString()
|
||||
for range 50 {
|
||||
send(t, s, tab)
|
||||
list(t, s, nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
if len(list(t, s, nil)[0].Connections) != 20 {
|
||||
t.Fatal("lost connections")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetadataUTF8(t *testing.T) {
|
||||
for _, agent := range []string{strings.Repeat("x", 1023) + "я", "bad\xffagent"} {
|
||||
s, input := newFixture()
|
||||
input.UserAgent = agent
|
||||
send(t, s, input)
|
||||
stored := list(t, s, nil)[0].Connections[0].UserAgent
|
||||
if len(stored) > 1024 || !utf8.ValidString(stored) {
|
||||
t.Fatal("metadata must serialize safely")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -377,7 +377,7 @@ func verifyExportedGameReads(t *testing.T, f serviceFixture, worker *game_export
|
||||
games := game_service.NewGameService(f.games, f.teams, f.scenarios, nil, "", actions_repo.NewActionsRepo(f.pool), applications_repo.NewApplicationsRepo(f.pool))
|
||||
mapRepo := game_map_repo.NewGameMapRepo(f.pool)
|
||||
server := app.NewServer(nil, nil, nil, nil, games, nil, nil, nil, nil, nil, nil,
|
||||
screen_service.NewScreenService(screen_sessions_repo.NewScreenSessionsRepo(f.pool)), nil, nil, map_service.NewMapService(mapRepo), nil, worker, nil, nil, nil)
|
||||
screen_service.NewScreenService(screen_sessions_repo.NewScreenSessionsRepo(f.pool)), nil, nil, map_service.NewMapService(mapRepo), nil, worker, nil, nil, nil, nil)
|
||||
ctx := processor_jwt.ContextWithClaims(context.Background(), &processor_jwt.JWTClaims{UserID: f.userID, Roles: []string{"organizer"}})
|
||||
game, err := server.GetGame(ctx, &proto.GetGameReq{ID: int32(f.gameID)})
|
||||
check(t, err)
|
||||
|
||||
@@ -166,3 +166,18 @@ func (s *TeamService) DownloadTeamQR(ctx context.Context, id int) ([]byte, strin
|
||||
data, err := team_qr_pdf.Generate(cards)
|
||||
return data, filename, err
|
||||
}
|
||||
|
||||
// CheckPresence validates team credentials without loading game progress or a scenario.
|
||||
func (s *TeamService) CheckPresence(ctx context.Context, teamID int, password string) (int, error) {
|
||||
team, err := s.teamsRepo.GetTeamByID(ctx, teamID)
|
||||
if errors.Is(err, teams_repo.ErrTeamNotFound) {
|
||||
return 0, ErrTeamNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if team.Password != password || team.ParticipationStatus == "cancelled" {
|
||||
return 0, ErrTeamNotFound
|
||||
}
|
||||
return team.GameID, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user