feat: game lobby service
This commit is contained in:
@@ -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")
|
||||
Reference in New Issue
Block a user