feat: user service
This commit is contained in:
+178
-21
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user