// Package internalhttp exposes the trusted internal HTTP API used by auth, // gateway self-service, and internal administrative workflows. package internalhttp import ( "context" "errors" "fmt" "log/slog" "net" "net/http" "sync" "time" "galaxy/user/internal/service/adminusers" "galaxy/user/internal/service/authdirectory" "galaxy/user/internal/service/entitlementsvc" "galaxy/user/internal/service/geosync" "galaxy/user/internal/service/lobbyeligibility" "galaxy/user/internal/service/policysvc" "galaxy/user/internal/service/selfservice" "galaxy/user/internal/telemetry" ) const jsonContentType = "application/json; charset=utf-8" var configureGinModeOnce sync.Once // ResolveByEmailUseCase describes the auth-facing resolve-by-email service // consumed by the HTTP transport layer. type ResolveByEmailUseCase interface { // Execute resolves one e-mail subject without creating any account. Execute(ctx context.Context, input authdirectory.ResolveByEmailInput) (authdirectory.ResolveByEmailResult, error) } // EnsureByEmailUseCase describes the auth-facing ensure-by-email service // consumed by the HTTP transport layer. type EnsureByEmailUseCase interface { // Execute returns an existing user, creates a new one, or reports a blocked // outcome for one e-mail subject. Execute(ctx context.Context, input authdirectory.EnsureByEmailInput) (authdirectory.EnsureByEmailResult, error) } // ExistsByUserIDUseCase describes the auth-facing exists-by-user-id service // consumed by the HTTP transport layer. type ExistsByUserIDUseCase interface { // Execute reports whether one stable user identifier exists. Execute(ctx context.Context, input authdirectory.ExistsByUserIDInput) (authdirectory.ExistsByUserIDResult, error) } // BlockByUserIDUseCase describes the auth-facing block-by-user-id service // consumed by the HTTP transport layer. type BlockByUserIDUseCase interface { // Execute blocks one account addressed by stable user identifier. Execute(ctx context.Context, input authdirectory.BlockByUserIDInput) (authdirectory.BlockResult, error) } // BlockByEmailUseCase describes the auth-facing block-by-email service // consumed by the HTTP transport layer. type BlockByEmailUseCase interface { // Execute blocks one exact normalized e-mail subject. Execute(ctx context.Context, input authdirectory.BlockByEmailInput) (authdirectory.BlockResult, error) } // GetMyAccountUseCase describes the self-service account-read use case // consumed by the HTTP transport layer. type GetMyAccountUseCase interface { // Execute returns the authenticated account aggregate for one user. Execute(ctx context.Context, input selfservice.GetMyAccountInput) (selfservice.GetMyAccountResult, error) } // UpdateMyProfileUseCase describes the self-service profile-mutation use case // consumed by the HTTP transport layer. type UpdateMyProfileUseCase interface { // Execute updates the allowed self-service profile fields for one user. Execute(ctx context.Context, input selfservice.UpdateMyProfileInput) (selfservice.UpdateMyProfileResult, error) } // UpdateMySettingsUseCase describes the self-service settings-mutation use // case consumed by the HTTP transport layer. type UpdateMySettingsUseCase interface { // Execute updates the allowed self-service settings fields for one user. Execute(ctx context.Context, input selfservice.UpdateMySettingsInput) (selfservice.UpdateMySettingsResult, error) } // GetUserByIDUseCase describes the trusted admin exact-read by stable user id // consumed by the HTTP transport layer. type GetUserByIDUseCase interface { // Execute returns the full current account aggregate for one user id. Execute(ctx context.Context, input adminusers.GetUserByIDInput) (adminusers.LookupResult, error) } // GetUserByEmailUseCase describes the trusted admin exact-read by normalized // e-mail consumed by the HTTP transport layer. type GetUserByEmailUseCase interface { // Execute returns the full current account aggregate for one normalized // e-mail address. Execute(ctx context.Context, input adminusers.GetUserByEmailInput) (adminusers.LookupResult, error) } // GetUserByRaceNameUseCase describes the trusted admin exact-read by exact // stored race name consumed by the HTTP transport layer. type GetUserByRaceNameUseCase interface { // Execute returns the full current account aggregate for one exact race // name. Execute(ctx context.Context, input adminusers.GetUserByRaceNameInput) (adminusers.LookupResult, error) } // ListUsersUseCase describes the trusted admin paginated listing use case // consumed by the HTTP transport layer. type ListUsersUseCase interface { // Execute returns one deterministic filtered page of full account // aggregates. Execute(ctx context.Context, input adminusers.ListUsersInput) (adminusers.ListUsersResult, error) } // GetUserEligibilityUseCase describes the trusted lobby-facing eligibility // snapshot use case consumed by the HTTP transport layer. type GetUserEligibilityUseCase interface { // Execute returns one read-optimized lobby eligibility snapshot for one // user. Execute(ctx context.Context, input lobbyeligibility.GetUserEligibilityInput) (lobbyeligibility.GetUserEligibilityResult, error) } // SyncDeclaredCountryUseCase describes the trusted geo-facing declared-country // sync use case consumed by the HTTP transport layer. type SyncDeclaredCountryUseCase interface { // Execute synchronizes the current effective declared country for one user. Execute(ctx context.Context, input geosync.SyncDeclaredCountryInput) (geosync.SyncDeclaredCountryResult, error) } // GrantEntitlementUseCase describes the trusted entitlement-grant use case // consumed by the HTTP transport layer. type GrantEntitlementUseCase interface { // Execute grants a new current paid entitlement for one user. Execute(ctx context.Context, input entitlementsvc.GrantInput) (entitlementsvc.CommandResult, error) } // ExtendEntitlementUseCase describes the trusted entitlement-extend use case // consumed by the HTTP transport layer. type ExtendEntitlementUseCase interface { // Execute extends the current finite paid entitlement for one user. Execute(ctx context.Context, input entitlementsvc.ExtendInput) (entitlementsvc.CommandResult, error) } // RevokeEntitlementUseCase describes the trusted entitlement-revoke use case // consumed by the HTTP transport layer. type RevokeEntitlementUseCase interface { // Execute revokes the current paid entitlement for one user. Execute(ctx context.Context, input entitlementsvc.RevokeInput) (entitlementsvc.CommandResult, error) } // ApplySanctionUseCase describes the trusted sanction-apply use case consumed // by the HTTP transport layer. type ApplySanctionUseCase interface { // Execute applies one new active sanction record. Execute(ctx context.Context, input policysvc.ApplySanctionInput) (policysvc.SanctionCommandResult, error) } // RemoveSanctionUseCase describes the trusted sanction-remove use case // consumed by the HTTP transport layer. type RemoveSanctionUseCase interface { // Execute removes one current active sanction record by code. Execute(ctx context.Context, input policysvc.RemoveSanctionInput) (policysvc.SanctionCommandResult, error) } // SetLimitUseCase describes the trusted limit-set use case consumed by the // HTTP transport layer. type SetLimitUseCase interface { // Execute creates or replaces one current active limit record. Execute(ctx context.Context, input policysvc.SetLimitInput) (policysvc.LimitCommandResult, error) } // RemoveLimitUseCase describes the trusted limit-remove use case consumed by // the HTTP transport layer. type RemoveLimitUseCase interface { // Execute removes one current active limit record by code. Execute(ctx context.Context, input policysvc.RemoveLimitInput) (policysvc.LimitCommandResult, error) } // Config describes the trusted internal HTTP listener owned by the user // service. type Config struct { // Addr stores the TCP listen address. Addr string // ReadHeaderTimeout bounds how long the listener may spend reading request // headers before rejecting the connection. ReadHeaderTimeout time.Duration // ReadTimeout bounds how long the listener may spend reading one request. ReadTimeout time.Duration // IdleTimeout bounds how long keep-alive connections stay open. IdleTimeout time.Duration // RequestTimeout bounds one application-layer request execution. RequestTimeout time.Duration } // Validate reports whether cfg contains a usable internal HTTP listener // configuration. func (cfg Config) Validate() error { switch { case cfg.Addr == "": return errors.New("internal HTTP addr must not be empty") case cfg.ReadHeaderTimeout <= 0: return errors.New("internal HTTP read header timeout must be positive") case cfg.ReadTimeout <= 0: return errors.New("internal HTTP read timeout must be positive") case cfg.IdleTimeout <= 0: return errors.New("internal HTTP idle timeout must be positive") case cfg.RequestTimeout <= 0: return errors.New("internal HTTP request timeout must be positive") default: return nil } } // Dependencies describes the collaborators used by the trusted internal HTTP // transport layer. type Dependencies struct { // ResolveByEmail executes the auth-facing resolve-by-email use case. ResolveByEmail ResolveByEmailUseCase // EnsureByEmail executes the auth-facing ensure-by-email use case. EnsureByEmail EnsureByEmailUseCase // ExistsByUserID executes the auth-facing exists-by-user-id use case. ExistsByUserID ExistsByUserIDUseCase // BlockByUserID executes the auth-facing block-by-user-id use case. BlockByUserID BlockByUserIDUseCase // BlockByEmail executes the auth-facing block-by-email use case. BlockByEmail BlockByEmailUseCase // GetMyAccount executes the self-service authenticated account-read use // case. GetMyAccount GetMyAccountUseCase // UpdateMyProfile executes the self-service profile-mutation use case. UpdateMyProfile UpdateMyProfileUseCase // UpdateMySettings executes the self-service settings-mutation use case. UpdateMySettings UpdateMySettingsUseCase // GetUserByID executes the trusted admin exact-read by stable user id. GetUserByID GetUserByIDUseCase // GetUserByEmail executes the trusted admin exact-read by normalized // e-mail. GetUserByEmail GetUserByEmailUseCase // GetUserByRaceName executes the trusted admin exact-read by exact stored // race name. GetUserByRaceName GetUserByRaceNameUseCase // ListUsers executes the trusted admin paginated filtered listing use case. ListUsers ListUsersUseCase // GetUserEligibility executes the trusted lobby-facing eligibility snapshot // read. GetUserEligibility GetUserEligibilityUseCase // SyncDeclaredCountry executes the trusted geo-facing declared-country sync // command. SyncDeclaredCountry SyncDeclaredCountryUseCase // GrantEntitlement executes the trusted entitlement-grant use case. GrantEntitlement GrantEntitlementUseCase // ExtendEntitlement executes the trusted entitlement-extend use case. ExtendEntitlement ExtendEntitlementUseCase // RevokeEntitlement executes the trusted entitlement-revoke use case. RevokeEntitlement RevokeEntitlementUseCase // ApplySanction executes the trusted sanction-apply use case. ApplySanction ApplySanctionUseCase // RemoveSanction executes the trusted sanction-remove use case. RemoveSanction RemoveSanctionUseCase // SetLimit executes the trusted limit-set use case. SetLimit SetLimitUseCase // RemoveLimit executes the trusted limit-remove use case. RemoveLimit RemoveLimitUseCase // Logger writes structured transport logs. When nil, the default logger is // used. Logger *slog.Logger // Telemetry records OpenTelemetry spans and low-cardinality HTTP metrics. Telemetry *telemetry.Runtime } // Server owns the trusted internal HTTP listener exposed by the user service. type Server struct { cfg Config handler http.Handler logger *slog.Logger stateMu sync.RWMutex server *http.Server listener net.Listener } // NewServer constructs one trusted internal HTTP server for cfg and deps. func NewServer(cfg Config, deps Dependencies) (*Server, error) { if err := cfg.Validate(); err != nil { return nil, fmt.Errorf("new internal HTTP server: %w", err) } handler, err := newHandlerWithConfig(cfg, deps) if err != nil { return nil, fmt.Errorf("new internal HTTP server: %w", err) } logger := deps.Logger if logger == nil { logger = slog.Default() } return &Server{ cfg: cfg, handler: handler, logger: logger, }, nil } // Run binds the configured listener and serves the trusted internal HTTP // surface until ctx is cancelled or Shutdown closes the server. func (server *Server) Run(ctx context.Context) error { if ctx == nil { return errors.New("run internal HTTP server: nil context") } if err := ctx.Err(); err != nil { return err } listener, err := net.Listen("tcp", server.cfg.Addr) if err != nil { return fmt.Errorf("run internal HTTP server: listen on %q: %w", server.cfg.Addr, err) } httpServer := &http.Server{ Handler: server.handler, ReadHeaderTimeout: server.cfg.ReadHeaderTimeout, ReadTimeout: server.cfg.ReadTimeout, IdleTimeout: server.cfg.IdleTimeout, } server.stateMu.Lock() server.server = httpServer server.listener = listener server.stateMu.Unlock() server.logger.Info("internal HTTP server started", "addr", listener.Addr().String()) shutdownDone := make(chan struct{}) go func() { defer close(shutdownDone) <-ctx.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), server.cfg.RequestTimeout) defer cancel() _ = server.Shutdown(shutdownCtx) }() defer func() { server.stateMu.Lock() server.server = nil server.listener = nil server.stateMu.Unlock() <-shutdownDone }() err = httpServer.Serve(listener) switch { case err == nil: return nil case errors.Is(err, http.ErrServerClosed): server.logger.Info("internal HTTP server stopped") return nil default: return fmt.Errorf("run internal HTTP server: serve on %q: %w", server.cfg.Addr, err) } } // Shutdown gracefully stops the internal HTTP server within ctx. func (server *Server) Shutdown(ctx context.Context) error { if ctx == nil { return errors.New("shutdown internal HTTP server: nil context") } server.stateMu.RLock() httpServer := server.server server.stateMu.RUnlock() if httpServer == nil { return nil } if err := httpServer.Shutdown(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) { return fmt.Errorf("shutdown internal HTTP server: %w", err) } return nil }