feat: game lobby service

This commit is contained in:
Ilia Denisov
2026-04-25 23:20:55 +02:00
committed by GitHub
parent 32dc29359a
commit 48b0056b49
336 changed files with 57074 additions and 1418 deletions
+118
View File
@@ -0,0 +1,118 @@
// Package shared defines cross-service primitives used by Game Lobby
// application services: the Actor identity carried into every service
// call, the authorization sentinel errors translated to the
// `forbidden` HTTP code at the transport boundary, and the helpers
// that derive one from a transport layer.
package shared
import (
"errors"
"fmt"
"strings"
)
// ActorKind identifies the caller class of one Lobby service operation.
type ActorKind string
const (
// ActorKindAdmin reports that the caller is Admin Service routed through
// the internal trusted HTTP port. Admin callers are pre-authorized by the
// admin role check that Admin Service performs at the gateway boundary
// before forwarding the request.
ActorKindAdmin ActorKind = "admin"
// ActorKindUser reports that the caller is an authenticated platform user
// routed through Edge Gateway. User callers are identified by the
// `X-User-ID` header injected by Edge Gateway.
ActorKindUser ActorKind = "user"
)
// IsKnown reports whether kind belongs to the frozen actor-kind vocabulary.
func (kind ActorKind) IsKnown() bool {
switch kind {
case ActorKindAdmin, ActorKindUser:
return true
default:
return false
}
}
// Actor identifies the caller of one Lobby service operation. User actors
// carry a non-empty UserID; admin actors carry an empty UserID.
type Actor struct {
// Kind reports the caller class.
Kind ActorKind
// UserID stores the platform user identifier for ActorKindUser callers.
// It must be empty for ActorKindAdmin callers.
UserID string
}
// NewAdminActor returns one Actor that identifies the trusted admin caller.
func NewAdminActor() Actor {
return Actor{Kind: ActorKindAdmin}
}
// NewUserActor returns one Actor that identifies the user caller with userID.
func NewUserActor(userID string) Actor {
return Actor{Kind: ActorKindUser, UserID: userID}
}
// IsAdmin reports whether actor is the trusted admin caller.
func (actor Actor) IsAdmin() bool {
return actor.Kind == ActorKindAdmin
}
// IsUser reports whether actor is an authenticated platform user.
func (actor Actor) IsUser() bool {
return actor.Kind == ActorKindUser
}
// Validate reports whether actor carries a structurally valid identity.
// Admin actors must not carry a user identifier; user actors must carry a
// non-empty trimmed user identifier.
func (actor Actor) Validate() error {
if !actor.Kind.IsKnown() {
return fmt.Errorf("actor kind %q is unsupported", actor.Kind)
}
switch actor.Kind {
case ActorKindAdmin:
if strings.TrimSpace(actor.UserID) != "" {
return fmt.Errorf("admin actor must not carry a user id")
}
case ActorKindUser:
if strings.TrimSpace(actor.UserID) == "" {
return fmt.Errorf("user actor must carry a non-empty user id")
}
if strings.TrimSpace(actor.UserID) != actor.UserID {
return fmt.Errorf("user actor id must not contain surrounding whitespace")
}
}
return nil
}
// ErrForbidden reports that the caller is not authorized for the requested
// operation on the requested resource. The transport layer translates it to
// the HTTP `403 forbidden` error envelope.
var ErrForbidden = errors.New("forbidden")
// ErrEligibilityDenied reports that the User Service eligibility snapshot
// rejected the acting user for the requested operation. It covers both
// "user not found" (Exists=false) and any sanction or marker that
// collapses the relevant `can_*` flag to false. The transport layer
// translates it to the HTTP `422 eligibility_denied` envelope.
var ErrEligibilityDenied = errors.New("eligibility denied")
// ErrServiceUnavailable reports that an upstream synchronous dependency
// (User Service, Game Master, etc.) is unreachable or violated its
// contract. The transport layer translates it to the HTTP
// `503 service_unavailable` envelope.
var ErrServiceUnavailable = errors.New("service unavailable")
// ErrSubjectNotFound reports that the operation references a subject
// (a user, a pending race-name registration, etc.) that is not present
// in the relevant store and is not naturally surfaced through one of
// the domain `ErrNotFound` sentinels. The transport layer translates
// it to the HTTP `404 subject_not_found` envelope.
var ErrSubjectNotFound = errors.New("subject not found")