Files
galaxy-game/user/README.md
T
2026-04-17 18:39:16 +02:00

381 lines
10 KiB
Markdown

# User Service
`galaxy/user` owns regular-user platform identity and account state.
The service is internal-only. Its source-of-truth transport is trusted
REST/JSON. `Edge Gateway` exposes selected self-service operations externally
through authenticated gRPC with FlatBuffers payloads and transcodes those
requests to this service's internal REST API.
## Scope
`User Service` is the source of truth for:
- 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
- current effective `declared_country`
`User Service` is not the source of truth for:
- system-administrator identity
- device sessions, challenges, or client public keys
- declared-country review workflow or history
- edge authentication, request signing, or replay protection
Administrative reads and writes against regular-user state do not make this
service the owner of administrator identity. Admin identity belongs to the
future `Admin Service`.
## Trusted Surfaces
The internal REST surface is split into five stable groups:
- `AuthIntegration`
- resolve-by-email
- exists-by-user-id
- ensure-by-email
- block-by-user-id
- block-by-email
- `MyAccount`
- get account aggregate
- update profile
- update settings
- `LobbyIntegration`
- read synchronous eligibility snapshot
- `GeoIntegration`
- synchronize current effective `declared_country`
- `AdminUsers`
- lookups by `user_id`, exact-after-trim `email`, and exact `race_name`
- deterministic filtered listing
- explicit entitlement, sanction, and limit commands
The public authenticated gateway boundary currently exposes exactly three
self-service message types:
- `user.account.get`
- `user.profile.update`
- `user.settings.update`
Externally these commands use authenticated gRPC plus FlatBuffers payloads.
Internally gateway calls:
- `GET /api/v1/internal/users/{user_id}/account`
- `POST /api/v1/internal/users/{user_id}/profile`
- `POST /api/v1/internal/users/{user_id}/settings`
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.
- 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.
## Auth-Facing Contract
`Auth / Session Service` depends on the following synchronous user-owned
decisions:
- `resolve-by-email`
- returns `creatable`, `existing`, or `blocked`
- `ensure-by-email`
- returns `created`, `existing`, or `blocked`
- `exists-by-user-id`
- supports trusted session revoke and block flows
- block operations
- support trusted auth-driven user or e-mail blocking flows
`ensure-by-email` rules:
- `registration_context` is required.
- Its frozen shape is:
- `preferred_language`
- `time_zone`
- The registration context is create-only.
- New users store the supplied values after semantic validation.
- Existing users ignore the registration context completely.
- Existing users must not have settings overwritten by a later auth flow.
- The current rollout source of truth is:
- `Auth / Session Service` forwards the preferred-language candidate derived
from public `Accept-Language`
- unsupported or missing public language input falls back to `en`
- `Auth / Session Service` forwards the public confirm `time_zone`
- the create-only registration context remains unchanged for existing users
Auth-facing blocking semantics:
- `blocked` means the auth flow must not create or return a usable session for
that subject.
- `send-email-code` may still remain success-shaped at the auth edge, but
`User Service` remains the source of truth for the blocked decision.
## Self-Service Account Contract
Self-service reads and writes operate on one shared account aggregate:
- profile:
- `race_name`
- settings:
- `preferred_language`
- `time_zone`
- derived current state:
- entitlement snapshot
- active sanctions
- active limits
- current `declared_country`
Self-service writes return the refreshed full account aggregate.
Forbidden self-service mutations:
- e-mail change
- direct `declared_country` change
- direct entitlement mutation
- direct sanction mutation
- direct limit mutation
Current write rules:
- `UpdateMyProfile`
- changes only `race_name`
- rejects unsupported or unknown fields
- returns the current aggregate unchanged on no-op rename
- `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`
## Validation Rules
### E-mail
- trim surrounding whitespace
- validate as structurally valid e-mail
- keep the trimmed exact value
- do not lowercase or canonicalize
### Race Name
- validate non-empty user-facing name
- preserve accepted casing in storage and reads
- enforce uniqueness through canonical reservation
- reject conflicts as `409 conflict`
### preferred_language
- validate as BCP 47 language tag
- store canonical BCP 47 tag form
- current auth-driven create path temporarily uses `"en"` from authsession
### time_zone
- validate as IANA time-zone name
- store trimmed value
- do not apply additional alias canonicalization
## Entitlements
`User Service` owns the current effective entitlement snapshot.
Rules:
- every new user starts with the frozen free entitlement baseline
- explicit admin or later billing commands may:
- grant
- extend
- revoke
- finite paid entitlements are repaired lazily on read when expiry has passed
- downstream services read current entitlement from `User Service`, not from
billing or any write-side source
The shared account aggregate and lobby eligibility snapshot always expose the
current effective entitlement after lazy expiry repair.
## Sanctions And Limits
Sanctions and user-specific limits are explicit command-driven state.
Supported sanction codes:
- `login_block`
- `private_game_create_block`
- `private_game_manage_block`
- `game_join_block`
- `profile_update_block`
Supported user-specific limit codes:
- `max_owned_private_games`
- `max_pending_public_applications`
- `max_active_game_memberships`
Rules:
- active views expose only currently supported codes
- retired legacy limit codes may remain in stored history but are not part of
the active read or write contract
- sanctions and limits are projected into:
- the self-service account aggregate
- admin reads
- lobby eligibility snapshots
## Lobby Eligibility Semantics
`Game Lobby` depends on a synchronous read-optimized eligibility snapshot.
Rules:
- unknown users return `exists=false` rather than `404`
- entitlement state is current and expiry-repaired
- active sanctions are filtered to the lobby-relevant subset
- effective limits are derived from:
- the frozen free or paid default catalog
- plus any active user-specific override
Current markers:
- `can_login`
- `can_create_private_game`
- `can_manage_private_game`
- `can_join_game`
- `can_update_profile`
## declared_country Ownership Split
Ownership is intentionally split:
- `User Service`
- stores only the current effective `declared_country` value
- `Geo Profile Service`
- owns review workflow
- owns decision history
- owns version history and retry state
`User Service` accepts only trusted sync commands from `Geo Profile Service`
for the latest approved effective value.
Sync rules:
- accepted values are uppercase ISO 3166-1 alpha-2 country codes
- syncing the already stored value is a no-op
- a successful change updates the current account record and emits a domain
event
## Admin Read And List Semantics
Trusted admin reads operate on regular-user state only.
Lookups:
- by `user_id`
- by exact-after-trim `email`
- by exact `race_name`
Listing rules:
- deterministic order:
- `created_at desc`
- then `user_id desc`
- all supplied 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`
Listing filters include:
- paid/free state
- paid expiry bounds
- current `declared_country`
- active sanction code
- active limit code
- derived eligibility markers
## Domain Events
`User Service` publishes auxiliary post-commit domain events to the shared
Redis stream configured for domain events.
Frozen event types:
- `user.profile.changed`
- `user.settings.changed`
- `user.entitlement.changed`
- `user.sanction.changed`
- `user.limit.changed`
The current effective declared-country sync remains externally observable as
`user.declared_country.changed`.
Event rules:
- events are post-commit only
- event envelopes carry `user_id`, mutation source, occurrence timestamp, and
optional trace correlation
- event payloads expose the latest committed state relevant to the operation
- profile and settings events use `initialized` for auth-driven creation and
`updated` for later self-service writes
- entitlement events use:
- `initialized`
- `granted`
- `extended`
- `revoked`
- `expired_repaired`
- sanction events use:
- `applied`
- `removed`
- limit events use:
- `set`
- `removed`
## Error Model
The trusted internal REST contract uses strict JSON error envelopes:
```json
{
"error": {
"code": "invalid_request",
"message": "request is invalid"
}
}
```
Stable error codes:
- `invalid_request`
- `conflict`
- `subject_not_found`
- `internal_error`
- `service_unavailable`
Gateway mirrors these business errors on the authenticated `user.*` boundary
as:
- gateway `result_code`
- FlatBuffers error payload carrying the same `code` and `message`
Transport failures, timeouts, and upstream `503` remain transport-level
gateway `UNAVAILABLE`, not business results.
## References
- [Internal REST contract](openapi.yaml)
- [Service docs index](docs/README.md)
- [System architecture](../ARCHITECTURE.md)