feat: user service

This commit is contained in:
Ilia Denisov
2026-04-10 19:05:02 +02:00
committed by GitHub
parent 710bad712e
commit 23ffcb7535
140 changed files with 33418 additions and 952 deletions
+178 -21
View File
@@ -3,10 +3,15 @@ info:
title: Galaxy User Service Internal REST API
version: v1
description: |
This specification documents the planned trusted internal REST contract of
This specification documents the trusted internal REST contract of
`galaxy/user`.
The current runtime is implemented as an internal-only HTTP service backed
by Redis.
Scope:
- regular-user state only; system-admin identity belongs to future
`Admin Service`
- auth-facing user resolution, ensure, existence, and subject blocking
- gateway-facing authenticated account reads and self-service mutations
- lobby-facing eligibility snapshots
@@ -14,14 +19,25 @@ info:
- admin/internal reads, filtered listing, and explicit mutation commands
This specification is internal REST only. It intentionally does not
describe public edge transport, gateway gRPC, or any future asynchronous
event payloads.
describe public edge transport, gateway gRPC, or the auxiliary async
event contracts documented in `README.md` and `docs/flows.md`.
The auth-facing paths listed under `AuthIntegration` are already reserved
by `Auth / Session Service` and their route shapes must remain stable.
Current transport rules:
- request bodies are strict JSON only
- unknown fields are rejected
- trailing JSON input is rejected
- error responses use `{ "error": { "code", "message" } }`
- stable error codes are `invalid_request`, `conflict`,
`subject_not_found`, `internal_error`, and `service_unavailable`
servers:
- url: http://localhost:8091
description: Example local internal listener for User Service.
description: Default local internal listener for User Service.
tags:
- name: AuthIntegration
description: Trusted auth-facing user ownership and block-policy endpoints.
description: Trusted auth-facing user ownership and block-policy endpoints with frozen route shapes reserved by `Auth / Session Service`.
- name: MyAccount
description: Gateway-facing authenticated account queries and self-service mutations.
- name: LobbyIntegration
@@ -88,9 +104,16 @@ paths:
user when registration is allowed, or returns a blocked outcome when
policy denies the flow.
`registration_context` is create-only. Implementations must ignore it
for existing users and must not overwrite settings of an already
existing account.
`registration_context` is required on the current auth-to-user call.
Its frozen shape is `preferred_language` plus `time_zone`. The
registration context is create-only. Implementations must ignore it for
existing users and must not overwrite settings of an already existing
account.
During the current rollout `Auth / Session Service` sends temporary
`preferred_language="en"` and forwards the public confirm `time_zone`.
Gateway-side geoip language derivation is a later rollout and is not
part of the current source-of-truth contract.
requestBody:
required: true
content:
@@ -193,6 +216,10 @@ paths:
- MyAccount
operationId: updateMyProfile
summary: Update self-service profile fields
description: |
`race_name` uniqueness is enforced through a canonical reservation
policy that is case-insensitive, rejects the frozen anti-fraud
confusable pairs, and preserves the original stored casing.
parameters:
- $ref: "#/components/parameters/UserIDPath"
requestBody:
@@ -279,6 +306,14 @@ paths:
- GeoIntegration
operationId: syncDeclaredCountry
summary: Synchronize the current effective declared country
description: |
Applies the latest effective declared country chosen by
`Geo Profile Service`.
`declared_country` must be a known uppercase ISO 3166-1 alpha-2
country code. When the supplied value is already stored on the user
account, the command is a no-op and returns the existing
`updated_at` unchanged.
parameters:
- $ref: "#/components/parameters/UserIDPath"
requestBody:
@@ -330,7 +365,7 @@ paths:
tags:
- AdminUsers
operationId: getUserByEmail
summary: Read one user by normalized e-mail
summary: Read one user by exact-after-trim e-mail
requestBody:
required: true
content:
@@ -385,6 +420,15 @@ paths:
- AdminUsers
operationId: listUsers
summary: List users with deterministic pagination and rich filters
description: |
Returns full user account aggregates ordered by `created_at desc`, then
`user_id desc`.
All supplied query filters combine with logical `AND`.
`page_token` is opaque and bound to the normalized filter set that
produced it. Malformed or filter-mismatched tokens return
`400 invalid_request`.
parameters:
- $ref: "#/components/parameters/PageSize"
- $ref: "#/components/parameters/PageToken"
@@ -457,6 +501,9 @@ paths:
- AdminUsers
operationId: grantEntitlement
summary: Grant a new entitlement period
description: |
Grants a current paid entitlement when the current effective state is
`free`.
parameters:
- $ref: "#/components/parameters/UserIDPath"
requestBody:
@@ -488,6 +535,8 @@ paths:
- AdminUsers
operationId: extendEntitlement
summary: Extend the current entitlement period
description: |
Extends the current finite paid entitlement.
parameters:
- $ref: "#/components/parameters/UserIDPath"
requestBody:
@@ -519,6 +568,8 @@ paths:
- AdminUsers
operationId: revokeEntitlement
summary: Revoke the effective paid entitlement
description: |
Revokes the current effective paid entitlement.
parameters:
- $ref: "#/components/parameters/UserIDPath"
requestBody:
@@ -550,6 +601,8 @@ paths:
- AdminUsers
operationId: applySanction
summary: Apply one sanction record
description: |
Applies one new active sanction record.
parameters:
- $ref: "#/components/parameters/UserIDPath"
requestBody:
@@ -581,6 +634,8 @@ paths:
- AdminUsers
operationId: removeSanction
summary: Remove one active sanction record
description: |
Removes the current active sanction for one `sanction_code`.
parameters:
- $ref: "#/components/parameters/UserIDPath"
requestBody:
@@ -612,6 +667,9 @@ paths:
- AdminUsers
operationId: setLimit
summary: Set one active user-specific limit record
description: |
Creates one new active limit or replaces the current active record of
the same `limit_code`.
parameters:
- $ref: "#/components/parameters/UserIDPath"
requestBody:
@@ -643,6 +701,8 @@ paths:
- AdminUsers
operationId: removeLimit
summary: Remove one active user-specific limit record
description: |
Removes the current active user-specific limit for one `limit_code`.
parameters:
- $ref: "#/components/parameters/UserIDPath"
requestBody:
@@ -689,7 +749,7 @@ components:
PageToken:
name: page_token
in: query
description: Opaque deterministic pagination cursor returned by the previous page.
description: Opaque deterministic pagination cursor returned by the previous page and bound to the normalized filter set that produced it. Malformed or filter-mismatched tokens return `400 invalid_request`.
schema:
type: string
schemas:
@@ -700,27 +760,40 @@ components:
Email:
type: string
format: email
description: Normalized login and contact e-mail address.
description: |
Login and contact e-mail address. The service trims surrounding
whitespace and validates the value structurally, then treats the
trimmed value as the exact stored and lookup value. The service does
not lowercase or otherwise canonicalize e-mail before storage or exact
lookup.
RaceName:
type: string
description: |
Stored race name preserving the user-selected casing after successful
uniqueness checks.
uniqueness checks. Uniqueness is enforced against a canonical
reservation key rather than exact string equality only.
minLength: 1
maxLength: 64
LanguageTag:
type: string
description: BCP 47 language tag.
description: |
BCP 47 language tag. User Service validates semantic correctness on
auth-driven creation and stores the canonical tag form.
minLength: 1
maxLength: 32
TimeZoneName:
type: string
description: IANA time zone name.
description: |
IANA time zone name. User Service validates semantic correctness on
auth-driven creation and stores the trimmed caller value without
additional alias canonicalization.
minLength: 1
maxLength: 128
CountryCode:
type: string
description: ISO 3166-1 alpha-2 country code.
description: |
ISO 3166-1 alpha-2 country code in uppercase ASCII form. The geo sync
command additionally rejects well-formed but unknown region codes.
pattern: "^[A-Z]{2}$"
UserResolutionKind:
type: string
@@ -756,12 +829,13 @@ components:
- profile_update_block
LimitCode:
type: string
description: |
Current supported user-specific limit codes. Retired legacy codes may
still exist in stored history for backward compatibility, but they are
not part of this write or read contract.
enum:
- max_owned_private_games
- max_active_private_games
- max_pending_public_applications
- max_pending_private_join_requests
- max_pending_private_invites_sent
- max_active_game_memberships
ActorRef:
type: object
@@ -777,6 +851,12 @@ components:
description: Optional stable actor identifier.
RegistrationContext:
type: object
description: |
Frozen create-only initialization context used by the current
auth-facing ensure-by-email contract. `preferred_language` is
semantically validated as BCP 47 and stored in canonical tag form on
create. `time_zone` is semantically validated as an IANA time zone
name and stored after trim without additional alias canonicalization.
additionalProperties: false
required:
- preferred_language
@@ -786,8 +866,10 @@ components:
$ref: "#/components/schemas/LanguageTag"
description: |
Create-only initial preferred language. During the current rollout
phase `Auth / Session Service` sends a temporary `"en"` default
until gateway geoip-based language derivation is deployed.
`Auth / Session Service` sends a temporary `"en"` default and
forwards `time_zone`. Gateway-side geoip derivation is not part of
the current source-of-truth contract. Future derived values must
remain valid BCP 47 tags.
time_zone:
$ref: "#/components/schemas/TimeZoneName"
description: Create-only initial IANA time zone name.
@@ -825,6 +907,7 @@ components:
additionalProperties: false
required:
- email
- registration_context
properties:
email:
$ref: "#/components/schemas/Email"
@@ -840,6 +923,11 @@ components:
$ref: "#/components/schemas/EnsureUserOutcome"
user_id:
$ref: "#/components/schemas/UserID"
description: |
Present for `existing` and `created`. A `created` outcome returns
the durable newly materialized `user_id` created together with an
initial generated `player-<shortid>` race name and free
entitlement snapshot.
block_reason_code:
type: string
description: Present only for `outcome=blocked`.
@@ -874,6 +962,12 @@ components:
$ref: "#/components/schemas/UserID"
EntitlementSnapshot:
type: object
description: |
Materialized current effective entitlement snapshot.
The current snapshot is read-optimized and repaired lazily when a
finite paid state has already reached `ends_at`, so callers do not
observe stale paid/free state.
additionalProperties: false
required:
- plan_code
@@ -927,6 +1021,9 @@ components:
ActiveLimit:
type: object
additionalProperties: false
description: |
Current supported active user-specific limit override. Retired legacy
limit codes are ignored on reads and are not returned.
required:
- limit_code
- value
@@ -948,6 +1045,32 @@ components:
expires_at:
type: string
format: date-time
EffectiveLimit:
type: object
additionalProperties: false
description: |
Materialized numeric quota after the frozen `free` or `paid` default
catalog is combined with any active user-specific override for the same
`limit_code`.
`max_owned_private_games` is meaningful only while the current
entitlement is paid and is omitted from free effective limits.
`max_active_game_memberships` applies only to public games.
`max_pending_public_applications` stores the total public-games budget.
`Game Lobby` subtracts current active public memberships from this
value and clamps at `0` to derive remaining pending-application
headroom.
required:
- limit_code
- value
properties:
limit_code:
$ref: "#/components/schemas/LimitCode"
value:
type: integer
minimum: 0
AccountView:
type: object
additionalProperties: false
@@ -1002,6 +1125,10 @@ components:
UpdateMyProfileRequest:
type: object
additionalProperties: false
description: |
The current implementation accepts only `race_name` here. Attempts to
mutate `email` or `declared_country` are rejected as `400
invalid_request` through strict unknown-field handling.
required:
- race_name
properties:
@@ -1044,6 +1171,8 @@ components:
required:
- exists
- user_id
- active_sanctions
- effective_limits
- markers
properties:
exists:
@@ -1051,20 +1180,30 @@ components:
user_id:
$ref: "#/components/schemas/UserID"
entitlement:
description: |
Current effective entitlement snapshot. Omitted when `exists=false`.
$ref: "#/components/schemas/EntitlementSnapshot"
active_sanctions:
type: array
items:
$ref: "#/components/schemas/ActiveSanction"
effective_limits:
description: |
Materialized effective quotas for the current supported lobby
catalog. Unknown users return an empty array. Free users omit
`max_owned_private_games`.
type: array
items:
$ref: "#/components/schemas/ActiveLimit"
$ref: "#/components/schemas/EffectiveLimit"
markers:
$ref: "#/components/schemas/EligibilityMarkers"
SyncDeclaredCountryRequest:
type: object
additionalProperties: false
description: |
Synchronizes the latest effective declared country selected by
`Geo Profile Service`. Repeating the current stored value is accepted
as a no-op.
required:
- declared_country
properties:
@@ -1085,6 +1224,9 @@ components:
updated_at:
type: string
format: date-time
description: |
Effective account mutation timestamp. Same-value no-op syncs return
the existing stored timestamp unchanged.
UserAdminView:
allOf:
- $ref: "#/components/schemas/AccountView"
@@ -1127,6 +1269,12 @@ components:
GrantEntitlementRequest:
type: object
additionalProperties: false
description: |
Grants one current paid entitlement.
`plan_code=free` is invalid here. `starts_at` may be current or past,
but not future. Finite paid plans require `ends_at`, while
`paid_lifetime` forbids it.
required:
- plan_code
- source
@@ -1148,9 +1296,13 @@ components:
ends_at:
type: string
format: date-time
description: Required for `paid_monthly` and `paid_yearly`; omitted for `paid_lifetime`.
ExtendEntitlementRequest:
type: object
additionalProperties: false
description: |
Extends the current finite paid entitlement by appending one new paid
history segment.
required:
- source
- reason_code
@@ -1169,6 +1321,9 @@ components:
RevokeEntitlementRequest:
type: object
additionalProperties: false
description: |
Revokes the current effective paid entitlement and materializes a new
`free` snapshot immediately.
required:
- source
- reason_code
@@ -1183,6 +1338,8 @@ components:
EntitlementCommandResponse:
type: object
additionalProperties: false
description: Resulting current effective entitlement snapshot after one
successful trusted entitlement command.
required:
- user_id
- entitlement