feat: game lobby service
This commit is contained in:
+99
-25
@@ -13,15 +13,24 @@ requests to this service's internal REST API.
|
||||
|
||||
- opaque regular-user identifiers in `user-*` form
|
||||
- exact-after-trim login e-mail addresses
|
||||
- current race name and editable self-service settings
|
||||
- current entitlement snapshot
|
||||
- active sanctions and active user-specific limits
|
||||
- `user_name` — immutable auto-generated unique platform handle in
|
||||
`player-<suffix>` form
|
||||
- `display_name` — mutable free-text user label validated by
|
||||
`pkg/util/string.go:ValidateTypeName`, not required to be unique, empty by
|
||||
default
|
||||
- editable self-service settings (`preferred_language`, `time_zone`)
|
||||
- current entitlement snapshot including `max_registered_race_names`
|
||||
- active sanctions (including `permanent_block`) and active user-specific
|
||||
limits (including `max_registered_race_names` overrides)
|
||||
- current effective `declared_country`
|
||||
- soft-delete state via `DeleteUser`
|
||||
|
||||
`User Service` is not the source of truth for:
|
||||
|
||||
- system-administrator identity
|
||||
- device sessions, challenges, or client public keys
|
||||
- in-game `race_name` values or their uniqueness — those live in the Game
|
||||
Lobby Race Name Directory
|
||||
- declared-country review workflow or history
|
||||
- edge authentication, request signing, or replay protection
|
||||
|
||||
@@ -48,15 +57,18 @@ The internal REST surface is split into five stable groups:
|
||||
- `GeoIntegration`
|
||||
- synchronize current effective `declared_country`
|
||||
- `AdminUsers`
|
||||
- lookups by `user_id`, exact-after-trim `email`, and exact `race_name`
|
||||
- lookups by `user_id`, exact-after-trim `email`, exact `user_name`, and
|
||||
exact or prefix `display_name`
|
||||
- deterministic filtered listing
|
||||
- explicit entitlement, sanction, and limit commands
|
||||
- `DeleteUser` soft-delete command
|
||||
|
||||
The public authenticated gateway boundary currently exposes exactly three
|
||||
self-service message types:
|
||||
|
||||
- `user.account.get`
|
||||
- `user.profile.update`
|
||||
- `user.profile.update` — payload carries `display_name` only; the prior
|
||||
`race_name` payload field is removed and rejected if present
|
||||
- `user.settings.update`
|
||||
|
||||
Externally these commands use authenticated gRPC plus FlatBuffers payloads.
|
||||
@@ -66,25 +78,35 @@ Internally gateway calls:
|
||||
- `POST /api/v1/internal/users/{user_id}/profile`
|
||||
- `POST /api/v1/internal/users/{user_id}/settings`
|
||||
|
||||
Additional trusted internal operations:
|
||||
|
||||
- `POST /api/v1/internal/users/{user_id}/delete` — soft-delete
|
||||
(`DeleteUser`); intended to be called only by `Admin Service`. Idempotent
|
||||
per `user_id`; a second call after soft-delete returns
|
||||
`404 subject_not_found` for external reads but keeps the deleted record
|
||||
for audit.
|
||||
|
||||
Gateway must derive `user_id` from authenticated session context only. The
|
||||
client payload never carries user identity for this boundary.
|
||||
|
||||
## Identity And Lookup Rules
|
||||
|
||||
- User IDs are opaque stable identifiers generated by `User Service`.
|
||||
- New users receive generated default race names in `player-*` form until the
|
||||
user replaces them.
|
||||
- User IDs are opaque stable identifiers generated by `User Service` and are
|
||||
the only identifier permitted as a foreign key from other models.
|
||||
- Every new user receives an auto-generated `user_name` in `player-<suffix>`
|
||||
form. The suffix is 8 characters drawn from a confusable-free alphanumeric
|
||||
alphabet. `user_name` is immutable after creation; collisions are resolved
|
||||
by retry during create (limit 10 attempts).
|
||||
- `display_name` starts empty for new accounts. Self-service may change it
|
||||
via `UpdateMyProfile`; validation delegates to
|
||||
`pkg/util/string.go:ValidateTypeName`; uniqueness is not enforced.
|
||||
- E-mail semantics are exact-after-trim.
|
||||
- The service trims surrounding whitespace.
|
||||
- The service does not lowercase, canonicalize, or alias-normalize e-mail
|
||||
values.
|
||||
- Exact lookup by e-mail uses the trimmed stored value.
|
||||
- Race-name lookup is exact by stored value.
|
||||
- Race-name uniqueness is not exact-string-only.
|
||||
- Stored casing is preserved.
|
||||
- Uniqueness is enforced by a canonical reservation key.
|
||||
- The canonical policy is case-insensitive and includes the frozen
|
||||
anti-fraud confusable-pair rules used by the race-name policy adapter.
|
||||
- `user_name` lookup is exact by stored value; `display_name` supports exact
|
||||
and prefix lookups.
|
||||
|
||||
## Auth-Facing Contract
|
||||
|
||||
@@ -128,8 +150,10 @@ Auth-facing blocking semantics:
|
||||
|
||||
Self-service reads and writes operate on one shared account aggregate:
|
||||
|
||||
- immutable:
|
||||
- `user_name`
|
||||
- profile:
|
||||
- `race_name`
|
||||
- `display_name`
|
||||
- settings:
|
||||
- `preferred_language`
|
||||
- `time_zone`
|
||||
@@ -144,6 +168,7 @@ Self-service writes return the refreshed full account aggregate.
|
||||
Forbidden self-service mutations:
|
||||
|
||||
- e-mail change
|
||||
- `user_name` change
|
||||
- direct `declared_country` change
|
||||
- direct entitlement mutation
|
||||
- direct sanction mutation
|
||||
@@ -152,14 +177,17 @@ Forbidden self-service mutations:
|
||||
Current write rules:
|
||||
|
||||
- `UpdateMyProfile`
|
||||
- changes only `race_name`
|
||||
- changes only `display_name`
|
||||
- rejects unsupported or unknown fields
|
||||
- returns the current aggregate unchanged on no-op rename
|
||||
- returns the current aggregate unchanged when the incoming value equals
|
||||
the stored one
|
||||
- `UpdateMySettings`
|
||||
- changes only `preferred_language` and `time_zone`
|
||||
- rejects unsupported or unknown fields
|
||||
- active `profile_update_block` sanction blocks both profile and settings
|
||||
writes with `409 conflict`
|
||||
- active `permanent_block` sanction blocks every self-service read and write
|
||||
with `409 conflict` and surfaces in admin reads
|
||||
|
||||
## Validation Rules
|
||||
|
||||
@@ -170,12 +198,25 @@ Current write rules:
|
||||
- keep the trimmed exact value
|
||||
- do not lowercase or canonicalize
|
||||
|
||||
### Race Name
|
||||
### user_name
|
||||
|
||||
- validate non-empty user-facing name
|
||||
- preserve accepted casing in storage and reads
|
||||
- enforce uniqueness through canonical reservation
|
||||
- reject conflicts as `409 conflict`
|
||||
- auto-generated server-side in `player-<suffix>` form
|
||||
- suffix = 8 characters drawn from a confusable-free alphanumeric alphabet
|
||||
- uniqueness enforced at store-layer; conflicts resolved by retry during
|
||||
ensure-by-email (limit 10 attempts)
|
||||
- immutable after creation; any attempt to mutate is a logic error and
|
||||
returns `500 internal_error`
|
||||
|
||||
### display_name
|
||||
|
||||
- validated through `pkg/util/string.go:ValidateTypeName`
|
||||
- empty value is accepted and rendered as no display name in downstream
|
||||
consumers
|
||||
- casing and script preserved as submitted
|
||||
- not required to be unique
|
||||
|
||||
Note: in-game `race_name` values are owned by the Game Lobby Race Name
|
||||
Directory and are not validated, stored, or reserved by `User Service`.
|
||||
|
||||
### preferred_language
|
||||
|
||||
@@ -218,12 +259,17 @@ Supported sanction codes:
|
||||
- `private_game_manage_block`
|
||||
- `game_join_block`
|
||||
- `profile_update_block`
|
||||
- `permanent_block` — terminal state; collapses every `can_*` eligibility
|
||||
marker to `false`; triggers RND cascade release in `Game Lobby` through
|
||||
`user:lifecycle_events`
|
||||
|
||||
Supported user-specific limit codes:
|
||||
|
||||
- `max_owned_private_games`
|
||||
- `max_pending_public_applications`
|
||||
- `max_active_game_memberships`
|
||||
- `max_registered_race_names` — overrides the tariff default for the RND
|
||||
registered-name quota
|
||||
|
||||
Rules:
|
||||
|
||||
@@ -256,6 +302,14 @@ Current markers:
|
||||
- `can_join_game`
|
||||
- `can_update_profile`
|
||||
|
||||
Additional materialized fields:
|
||||
|
||||
- `max_registered_race_names` — tariff-derived quota for the Game Lobby Race
|
||||
Name Directory: `free → 1`, `paid_monthly → 2`, `paid_yearly → 6`,
|
||||
`paid_lifetime → 0` (unlimited marker). A user-specific
|
||||
`max_registered_race_names` limit override, when active, replaces the
|
||||
tariff value.
|
||||
|
||||
## declared_country Ownership Split
|
||||
|
||||
Ownership is intentionally split:
|
||||
@@ -285,7 +339,8 @@ Lookups:
|
||||
|
||||
- by `user_id`
|
||||
- by exact-after-trim `email`
|
||||
- by exact `race_name`
|
||||
- by exact `user_name`
|
||||
- by exact or prefix `display_name`
|
||||
|
||||
Listing rules:
|
||||
|
||||
@@ -302,9 +357,12 @@ Listing filters include:
|
||||
- paid/free state
|
||||
- paid expiry bounds
|
||||
- current `declared_country`
|
||||
- active sanction code
|
||||
- active limit code
|
||||
- active sanction code (including `permanent_block`)
|
||||
- active limit code (including `max_registered_race_names`)
|
||||
- derived eligibility markers
|
||||
- `user_name` exact
|
||||
- `display_name` exact or prefix
|
||||
- `deleted` flag (soft-deleted accounts excluded by default)
|
||||
|
||||
## Domain Events
|
||||
|
||||
@@ -322,6 +380,20 @@ Frozen event types:
|
||||
The current effective declared-country sync remains externally observable as
|
||||
`user.declared_country.changed`.
|
||||
|
||||
### User lifecycle stream
|
||||
|
||||
Separately from the shared domain-events stream, `User Service` publishes to
|
||||
a dedicated Redis stream `user:lifecycle_events` consumed by `Game Lobby`
|
||||
for Race Name Directory cascade release. Event types:
|
||||
|
||||
- `user.lifecycle.permanent_blocked` — emitted when
|
||||
`SanctionCodePermanentBlock` becomes active on a user
|
||||
- `user.lifecycle.deleted` — emitted when `DeleteUser` succeeds
|
||||
|
||||
Event envelopes carry `user_id`, `occurred_at_ms`, mutation source, optional
|
||||
`reason_code`, and actor metadata. Delivery is at-least-once; consumers
|
||||
must be idempotent.
|
||||
|
||||
Event rules:
|
||||
|
||||
- events are post-commit only
|
||||
@@ -377,4 +449,6 @@ gateway `UNAVAILABLE`, not business results.
|
||||
|
||||
- [Internal REST contract](openapi.yaml)
|
||||
- [Service docs index](docs/README.md)
|
||||
- [Stage 21 decisions](docs/stage21-user-name-display-name.md)
|
||||
- [Stage 22 decisions](docs/stage22-permanent-block-delete-user.md)
|
||||
- [System architecture](../ARCHITECTURE.md)
|
||||
|
||||
Reference in New Issue
Block a user