package internalhttp import ( "context" "fmt" "log/slog" "net/http" "time" "galaxy/user/internal/logging" "galaxy/user/internal/service/accountdeletion" "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/service/shared" "galaxy/user/internal/telemetry" "github.com/gin-gonic/gin" "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" "go.opentelemetry.io/otel/attribute" ) const internalHTTPServiceName = "galaxy-user-internal" type errorResponse struct { Error errorBody `json:"error"` } type errorBody struct { Code string `json:"code"` Message string `json:"message"` } type resolveByEmailRequest struct { Email string `json:"email"` } type resolveByEmailResponse struct { Kind string `json:"kind"` UserID string `json:"user_id,omitempty"` BlockReasonCode string `json:"block_reason_code,omitempty"` } type existsByUserIDResponse struct { Exists bool `json:"exists"` } type ensureByEmailRequest struct { Email string `json:"email"` RegistrationContext *ensureRegistrationContextDTO `json:"registration_context"` } type ensureRegistrationContextDTO struct { PreferredLanguage string `json:"preferred_language"` TimeZone string `json:"time_zone"` } type ensureByEmailResponse struct { Outcome string `json:"outcome"` UserID string `json:"user_id,omitempty"` BlockReasonCode string `json:"block_reason_code,omitempty"` } type blockByUserIDRequest struct { ReasonCode string `json:"reason_code"` } type blockByEmailRequest struct { Email string `json:"email"` ReasonCode string `json:"reason_code"` } type blockResponse struct { Outcome string `json:"outcome"` UserID string `json:"user_id,omitempty"` } type getMyAccountResponse struct { Account selfservice.AccountView `json:"account"` } type updateMyProfileRequest struct { DisplayName string `json:"display_name"` } type updateMySettingsRequest struct { PreferredLanguage string `json:"preferred_language"` TimeZone string `json:"time_zone"` } type syncDeclaredCountryRequest struct { DeclaredCountry string `json:"declared_country"` } type syncDeclaredCountryResponse struct { UserID string `json:"user_id"` DeclaredCountry string `json:"declared_country"` UpdatedAt time.Time `json:"updated_at"` } type actorDTO struct { Type string `json:"type"` ID string `json:"id,omitempty"` } type grantEntitlementRequest struct { PlanCode string `json:"plan_code"` Source string `json:"source"` ReasonCode string `json:"reason_code"` Actor actorDTO `json:"actor"` StartsAt string `json:"starts_at"` EndsAt string `json:"ends_at,omitempty"` } type extendEntitlementRequest struct { Source string `json:"source"` ReasonCode string `json:"reason_code"` Actor actorDTO `json:"actor"` EndsAt string `json:"ends_at"` } type revokeEntitlementRequest struct { Source string `json:"source"` ReasonCode string `json:"reason_code"` Actor actorDTO `json:"actor"` } type applySanctionRequest struct { SanctionCode string `json:"sanction_code"` Scope string `json:"scope"` ReasonCode string `json:"reason_code"` Actor actorDTO `json:"actor"` AppliedAt string `json:"applied_at"` ExpiresAt string `json:"expires_at,omitempty"` } type removeSanctionRequest struct { SanctionCode string `json:"sanction_code"` ReasonCode string `json:"reason_code"` Actor actorDTO `json:"actor"` } type setLimitRequest struct { LimitCode string `json:"limit_code"` Value int `json:"value"` ReasonCode string `json:"reason_code"` Actor actorDTO `json:"actor"` AppliedAt string `json:"applied_at"` ExpiresAt string `json:"expires_at,omitempty"` } type removeLimitRequest struct { LimitCode string `json:"limit_code"` ReasonCode string `json:"reason_code"` Actor actorDTO `json:"actor"` } type deleteUserRequest struct { ReasonCode string `json:"reason_code"` Actor actorDTO `json:"actor"` } type deleteUserResponse struct { UserID string `json:"user_id"` DeletedAt time.Time `json:"deleted_at"` } type entitlementSnapshotResponse struct { PlanCode string `json:"plan_code"` IsPaid bool `json:"is_paid"` Source string `json:"source"` Actor actorDTO `json:"actor"` ReasonCode string `json:"reason_code"` StartsAt time.Time `json:"starts_at"` EndsAt *time.Time `json:"ends_at,omitempty"` UpdatedAt time.Time `json:"updated_at"` } type entitlementCommandResponse struct { UserID string `json:"user_id"` Entitlement entitlementSnapshotResponse `json:"entitlement"` } func newHandlerWithConfig(cfg Config, deps Dependencies) (http.Handler, error) { if err := cfg.Validate(); err != nil { return nil, err } normalizedDeps, err := normalizeDependencies(deps) if err != nil { return nil, err } configureGinModeOnce.Do(func() { gin.SetMode(gin.ReleaseMode) }) engine := gin.New() engine.Use(newOTelMiddleware(normalizedDeps.Telemetry)) engine.Use(withObservability(normalizedDeps.Logger, normalizedDeps.Telemetry)) engine.POST("/api/v1/internal/user-resolutions/by-email", handleResolveByEmail(normalizedDeps.ResolveByEmail, cfg.RequestTimeout)) engine.GET("/api/v1/internal/users/:user_id/exists", handleExistsByUserID(normalizedDeps.ExistsByUserID, cfg.RequestTimeout)) engine.POST("/api/v1/internal/users/ensure-by-email", handleEnsureByEmail(normalizedDeps.EnsureByEmail, cfg.RequestTimeout)) engine.POST("/api/v1/internal/users/:user_id/block", handleBlockByUserID(normalizedDeps.BlockByUserID, cfg.RequestTimeout)) engine.POST("/api/v1/internal/user-blocks/by-email", handleBlockByEmail(normalizedDeps.BlockByEmail, cfg.RequestTimeout)) engine.GET("/api/v1/internal/users/:user_id/account", handleGetMyAccount(normalizedDeps.GetMyAccount, cfg.RequestTimeout)) engine.POST("/api/v1/internal/users/:user_id/profile", handleUpdateMyProfile(normalizedDeps.UpdateMyProfile, cfg.RequestTimeout)) engine.POST("/api/v1/internal/users/:user_id/settings", handleUpdateMySettings(normalizedDeps.UpdateMySettings, cfg.RequestTimeout)) engine.GET("/api/v1/internal/users/:user_id", handleGetUserByID(normalizedDeps.GetUserByID, cfg.RequestTimeout)) engine.POST("/api/v1/internal/user-lookups/by-email", handleGetUserByEmail(normalizedDeps.GetUserByEmail, cfg.RequestTimeout)) engine.POST("/api/v1/internal/user-lookups/by-user-name", handleGetUserByUserName(normalizedDeps.GetUserByUserName, cfg.RequestTimeout)) engine.GET("/api/v1/internal/users", handleListUsers(normalizedDeps.ListUsers, cfg.RequestTimeout)) engine.GET("/api/v1/internal/users/:user_id/eligibility", handleGetUserEligibility(normalizedDeps.GetUserEligibility, cfg.RequestTimeout)) engine.POST("/api/v1/internal/users/:user_id/declared-country/sync", handleSyncDeclaredCountry(normalizedDeps.SyncDeclaredCountry, cfg.RequestTimeout)) engine.POST("/api/v1/internal/users/:user_id/entitlements/grant", handleGrantEntitlement(normalizedDeps.GrantEntitlement, cfg.RequestTimeout)) engine.POST("/api/v1/internal/users/:user_id/entitlements/extend", handleExtendEntitlement(normalizedDeps.ExtendEntitlement, cfg.RequestTimeout)) engine.POST("/api/v1/internal/users/:user_id/entitlements/revoke", handleRevokeEntitlement(normalizedDeps.RevokeEntitlement, cfg.RequestTimeout)) engine.POST("/api/v1/internal/users/:user_id/sanctions/apply", handleApplySanction(normalizedDeps.ApplySanction, cfg.RequestTimeout)) engine.POST("/api/v1/internal/users/:user_id/sanctions/remove", handleRemoveSanction(normalizedDeps.RemoveSanction, cfg.RequestTimeout)) engine.POST("/api/v1/internal/users/:user_id/limits/set", handleSetLimit(normalizedDeps.SetLimit, cfg.RequestTimeout)) engine.POST("/api/v1/internal/users/:user_id/limits/remove", handleRemoveLimit(normalizedDeps.RemoveLimit, cfg.RequestTimeout)) engine.POST("/api/v1/internal/users/:user_id/delete", handleDeleteUser(normalizedDeps.DeleteUser, cfg.RequestTimeout)) return engine, nil } func handleResolveByEmail(useCase ResolveByEmailUseCase, timeout time.Duration) gin.HandlerFunc { return func(c *gin.Context) { var request resolveByEmailRequest if err := decodeJSONRequest(c.Request, &request); err != nil { abortWithProjection(c, shared.ProjectInternalError(shared.InvalidRequest(err.Error()))) return } callCtx, cancel := context.WithTimeout(c.Request.Context(), timeout) defer cancel() result, err := useCase.Execute(callCtx, authdirectory.ResolveByEmailInput{ Email: request.Email, }) if err != nil { abortWithProjection(c, shared.ProjectInternalError(err)) return } c.JSON(http.StatusOK, resolveByEmailResponse{ Kind: result.Kind, UserID: result.UserID, BlockReasonCode: result.BlockReasonCode, }) } } func handleExistsByUserID(useCase ExistsByUserIDUseCase, timeout time.Duration) gin.HandlerFunc { return func(c *gin.Context) { callCtx, cancel := context.WithTimeout(c.Request.Context(), timeout) defer cancel() result, err := useCase.Execute(callCtx, authdirectory.ExistsByUserIDInput{ UserID: c.Param("user_id"), }) if err != nil { abortWithProjection(c, shared.ProjectInternalError(err)) return } c.JSON(http.StatusOK, existsByUserIDResponse{Exists: result.Exists}) } } func handleEnsureByEmail(useCase EnsureByEmailUseCase, timeout time.Duration) gin.HandlerFunc { return func(c *gin.Context) { var request ensureByEmailRequest if err := decodeJSONRequest(c.Request, &request); err != nil { abortWithProjection(c, shared.ProjectInternalError(shared.InvalidRequest(err.Error()))) return } if request.RegistrationContext == nil { abortWithProjection(c, shared.ProjectInternalError(shared.InvalidRequest("registration_context must be present"))) return } var registrationContext *authdirectory.RegistrationContext registrationContext = &authdirectory.RegistrationContext{ PreferredLanguage: request.RegistrationContext.PreferredLanguage, TimeZone: request.RegistrationContext.TimeZone, } callCtx, cancel := context.WithTimeout(c.Request.Context(), timeout) defer cancel() result, err := useCase.Execute(callCtx, authdirectory.EnsureByEmailInput{ Email: request.Email, RegistrationContext: registrationContext, }) if err != nil { abortWithProjection(c, shared.ProjectInternalError(err)) return } c.JSON(http.StatusOK, ensureByEmailResponse{ Outcome: result.Outcome, UserID: result.UserID, BlockReasonCode: result.BlockReasonCode, }) } } func handleBlockByUserID(useCase BlockByUserIDUseCase, timeout time.Duration) gin.HandlerFunc { return func(c *gin.Context) { var request blockByUserIDRequest if err := decodeJSONRequest(c.Request, &request); err != nil { abortWithProjection(c, shared.ProjectInternalError(shared.InvalidRequest(err.Error()))) return } callCtx, cancel := context.WithTimeout(c.Request.Context(), timeout) defer cancel() result, err := useCase.Execute(callCtx, authdirectory.BlockByUserIDInput{ UserID: c.Param("user_id"), ReasonCode: request.ReasonCode, }) if err != nil { abortWithProjection(c, shared.ProjectInternalError(err)) return } c.JSON(http.StatusOK, blockResponse{ Outcome: result.Outcome, UserID: result.UserID, }) } } func handleBlockByEmail(useCase BlockByEmailUseCase, timeout time.Duration) gin.HandlerFunc { return func(c *gin.Context) { var request blockByEmailRequest if err := decodeJSONRequest(c.Request, &request); err != nil { abortWithProjection(c, shared.ProjectInternalError(shared.InvalidRequest(err.Error()))) return } callCtx, cancel := context.WithTimeout(c.Request.Context(), timeout) defer cancel() result, err := useCase.Execute(callCtx, authdirectory.BlockByEmailInput{ Email: request.Email, ReasonCode: request.ReasonCode, }) if err != nil { abortWithProjection(c, shared.ProjectInternalError(err)) return } c.JSON(http.StatusOK, blockResponse{ Outcome: result.Outcome, UserID: result.UserID, }) } } func handleGetMyAccount(useCase GetMyAccountUseCase, timeout time.Duration) gin.HandlerFunc { return func(c *gin.Context) { callCtx, cancel := context.WithTimeout(c.Request.Context(), timeout) defer cancel() result, err := useCase.Execute(callCtx, selfservice.GetMyAccountInput{ UserID: c.Param("user_id"), }) if err != nil { abortWithProjection(c, shared.ProjectInternalError(err)) return } c.JSON(http.StatusOK, getMyAccountResponse{ Account: result.Account, }) } } func handleUpdateMyProfile(useCase UpdateMyProfileUseCase, timeout time.Duration) gin.HandlerFunc { return func(c *gin.Context) { var request updateMyProfileRequest if err := decodeJSONRequest(c.Request, &request); err != nil { abortWithProjection(c, shared.ProjectInternalError(shared.InvalidRequest(err.Error()))) return } callCtx, cancel := context.WithTimeout(c.Request.Context(), timeout) defer cancel() result, err := useCase.Execute(callCtx, selfservice.UpdateMyProfileInput{ UserID: c.Param("user_id"), DisplayName: request.DisplayName, }) if err != nil { abortWithProjection(c, shared.ProjectInternalError(err)) return } c.JSON(http.StatusOK, getMyAccountResponse{ Account: result.Account, }) } } func handleUpdateMySettings(useCase UpdateMySettingsUseCase, timeout time.Duration) gin.HandlerFunc { return func(c *gin.Context) { var request updateMySettingsRequest if err := decodeJSONRequest(c.Request, &request); err != nil { abortWithProjection(c, shared.ProjectInternalError(shared.InvalidRequest(err.Error()))) return } callCtx, cancel := context.WithTimeout(c.Request.Context(), timeout) defer cancel() result, err := useCase.Execute(callCtx, selfservice.UpdateMySettingsInput{ UserID: c.Param("user_id"), PreferredLanguage: request.PreferredLanguage, TimeZone: request.TimeZone, }) if err != nil { abortWithProjection(c, shared.ProjectInternalError(err)) return } c.JSON(http.StatusOK, getMyAccountResponse{ Account: result.Account, }) } } func handleGetUserEligibility(useCase GetUserEligibilityUseCase, timeout time.Duration) gin.HandlerFunc { return func(c *gin.Context) { callCtx, cancel := context.WithTimeout(c.Request.Context(), timeout) defer cancel() result, err := useCase.Execute(callCtx, lobbyeligibility.GetUserEligibilityInput{ UserID: c.Param("user_id"), }) if err != nil { abortWithProjection(c, shared.ProjectInternalError(err)) return } c.JSON(http.StatusOK, result) } } func handleSyncDeclaredCountry(useCase SyncDeclaredCountryUseCase, timeout time.Duration) gin.HandlerFunc { return func(c *gin.Context) { var request syncDeclaredCountryRequest if err := decodeJSONRequest(c.Request, &request); err != nil { abortWithProjection(c, shared.ProjectInternalError(shared.InvalidRequest(err.Error()))) return } callCtx, cancel := context.WithTimeout(c.Request.Context(), timeout) defer cancel() result, err := useCase.Execute(callCtx, geosync.SyncDeclaredCountryInput{ UserID: c.Param("user_id"), DeclaredCountry: request.DeclaredCountry, }) if err != nil { abortWithProjection(c, shared.ProjectInternalError(err)) return } c.JSON(http.StatusOK, syncDeclaredCountryResponse{ UserID: result.UserID, DeclaredCountry: result.DeclaredCountry, UpdatedAt: result.UpdatedAt.UTC(), }) } } func handleGrantEntitlement(useCase GrantEntitlementUseCase, timeout time.Duration) gin.HandlerFunc { return func(c *gin.Context) { var request grantEntitlementRequest if err := decodeJSONRequest(c.Request, &request); err != nil { abortWithProjection(c, shared.ProjectInternalError(shared.InvalidRequest(err.Error()))) return } callCtx, cancel := context.WithTimeout(c.Request.Context(), timeout) defer cancel() result, err := useCase.Execute(callCtx, entitlementsvc.GrantInput{ UserID: c.Param("user_id"), PlanCode: request.PlanCode, Source: request.Source, ReasonCode: request.ReasonCode, Actor: entitlementsvc.ActorInput{ Type: request.Actor.Type, ID: request.Actor.ID, }, StartsAt: request.StartsAt, EndsAt: request.EndsAt, }) if err != nil { abortWithProjection(c, shared.ProjectInternalError(err)) return } c.JSON(http.StatusOK, entitlementCommandResponseFromResult(result)) } } func handleExtendEntitlement(useCase ExtendEntitlementUseCase, timeout time.Duration) gin.HandlerFunc { return func(c *gin.Context) { var request extendEntitlementRequest if err := decodeJSONRequest(c.Request, &request); err != nil { abortWithProjection(c, shared.ProjectInternalError(shared.InvalidRequest(err.Error()))) return } callCtx, cancel := context.WithTimeout(c.Request.Context(), timeout) defer cancel() result, err := useCase.Execute(callCtx, entitlementsvc.ExtendInput{ UserID: c.Param("user_id"), Source: request.Source, ReasonCode: request.ReasonCode, Actor: entitlementsvc.ActorInput{ Type: request.Actor.Type, ID: request.Actor.ID, }, EndsAt: request.EndsAt, }) if err != nil { abortWithProjection(c, shared.ProjectInternalError(err)) return } c.JSON(http.StatusOK, entitlementCommandResponseFromResult(result)) } } func handleRevokeEntitlement(useCase RevokeEntitlementUseCase, timeout time.Duration) gin.HandlerFunc { return func(c *gin.Context) { var request revokeEntitlementRequest if err := decodeJSONRequest(c.Request, &request); err != nil { abortWithProjection(c, shared.ProjectInternalError(shared.InvalidRequest(err.Error()))) return } callCtx, cancel := context.WithTimeout(c.Request.Context(), timeout) defer cancel() result, err := useCase.Execute(callCtx, entitlementsvc.RevokeInput{ UserID: c.Param("user_id"), Source: request.Source, ReasonCode: request.ReasonCode, Actor: entitlementsvc.ActorInput{ Type: request.Actor.Type, ID: request.Actor.ID, }, }) if err != nil { abortWithProjection(c, shared.ProjectInternalError(err)) return } c.JSON(http.StatusOK, entitlementCommandResponseFromResult(result)) } } func handleApplySanction(useCase ApplySanctionUseCase, timeout time.Duration) gin.HandlerFunc { return func(c *gin.Context) { var request applySanctionRequest if err := decodeJSONRequest(c.Request, &request); err != nil { abortWithProjection(c, shared.ProjectInternalError(shared.InvalidRequest(err.Error()))) return } callCtx, cancel := context.WithTimeout(c.Request.Context(), timeout) defer cancel() result, err := useCase.Execute(callCtx, policysvc.ApplySanctionInput{ UserID: c.Param("user_id"), SanctionCode: request.SanctionCode, Scope: request.Scope, ReasonCode: request.ReasonCode, Actor: policysvc.ActorInput{ Type: request.Actor.Type, ID: request.Actor.ID, }, AppliedAt: request.AppliedAt, ExpiresAt: request.ExpiresAt, }) if err != nil { abortWithProjection(c, shared.ProjectInternalError(err)) return } c.JSON(http.StatusOK, result) } } func handleRemoveSanction(useCase RemoveSanctionUseCase, timeout time.Duration) gin.HandlerFunc { return func(c *gin.Context) { var request removeSanctionRequest if err := decodeJSONRequest(c.Request, &request); err != nil { abortWithProjection(c, shared.ProjectInternalError(shared.InvalidRequest(err.Error()))) return } callCtx, cancel := context.WithTimeout(c.Request.Context(), timeout) defer cancel() result, err := useCase.Execute(callCtx, policysvc.RemoveSanctionInput{ UserID: c.Param("user_id"), SanctionCode: request.SanctionCode, ReasonCode: request.ReasonCode, Actor: policysvc.ActorInput{ Type: request.Actor.Type, ID: request.Actor.ID, }, }) if err != nil { abortWithProjection(c, shared.ProjectInternalError(err)) return } c.JSON(http.StatusOK, result) } } func handleSetLimit(useCase SetLimitUseCase, timeout time.Duration) gin.HandlerFunc { return func(c *gin.Context) { var request setLimitRequest if err := decodeJSONRequest(c.Request, &request); err != nil { abortWithProjection(c, shared.ProjectInternalError(shared.InvalidRequest(err.Error()))) return } callCtx, cancel := context.WithTimeout(c.Request.Context(), timeout) defer cancel() result, err := useCase.Execute(callCtx, policysvc.SetLimitInput{ UserID: c.Param("user_id"), LimitCode: request.LimitCode, Value: request.Value, ReasonCode: request.ReasonCode, Actor: policysvc.ActorInput{ Type: request.Actor.Type, ID: request.Actor.ID, }, AppliedAt: request.AppliedAt, ExpiresAt: request.ExpiresAt, }) if err != nil { abortWithProjection(c, shared.ProjectInternalError(err)) return } c.JSON(http.StatusOK, result) } } func handleRemoveLimit(useCase RemoveLimitUseCase, timeout time.Duration) gin.HandlerFunc { return func(c *gin.Context) { var request removeLimitRequest if err := decodeJSONRequest(c.Request, &request); err != nil { abortWithProjection(c, shared.ProjectInternalError(shared.InvalidRequest(err.Error()))) return } callCtx, cancel := context.WithTimeout(c.Request.Context(), timeout) defer cancel() result, err := useCase.Execute(callCtx, policysvc.RemoveLimitInput{ UserID: c.Param("user_id"), LimitCode: request.LimitCode, ReasonCode: request.ReasonCode, Actor: policysvc.ActorInput{ Type: request.Actor.Type, ID: request.Actor.ID, }, }) if err != nil { abortWithProjection(c, shared.ProjectInternalError(err)) return } c.JSON(http.StatusOK, result) } } func handleDeleteUser(useCase DeleteUserUseCase, timeout time.Duration) gin.HandlerFunc { return func(c *gin.Context) { var request deleteUserRequest if err := decodeJSONRequest(c.Request, &request); err != nil { abortWithProjection(c, shared.ProjectInternalError(shared.InvalidRequest(err.Error()))) return } callCtx, cancel := context.WithTimeout(c.Request.Context(), timeout) defer cancel() result, err := useCase.Execute(callCtx, accountdeletion.Input{ UserID: c.Param("user_id"), ReasonCode: request.ReasonCode, Actor: accountdeletion.ActorInput{ Type: request.Actor.Type, ID: request.Actor.ID, }, }) if err != nil { abortWithProjection(c, shared.ProjectInternalError(err)) return } c.JSON(http.StatusOK, deleteUserResponse{ UserID: result.UserID, DeletedAt: result.DeletedAt.UTC(), }) } } func normalizeDependencies(deps Dependencies) (Dependencies, error) { switch { case deps.ResolveByEmail == nil: return Dependencies{}, fmt.Errorf("resolve-by-email use case must not be nil") case deps.EnsureByEmail == nil: return Dependencies{}, fmt.Errorf("ensure-by-email use case must not be nil") case deps.ExistsByUserID == nil: return Dependencies{}, fmt.Errorf("exists-by-user-id use case must not be nil") case deps.BlockByUserID == nil: return Dependencies{}, fmt.Errorf("block-by-user-id use case must not be nil") case deps.BlockByEmail == nil: return Dependencies{}, fmt.Errorf("block-by-email use case must not be nil") case deps.GetMyAccount == nil: return Dependencies{}, fmt.Errorf("get-my-account use case must not be nil") case deps.UpdateMyProfile == nil: return Dependencies{}, fmt.Errorf("update-my-profile use case must not be nil") case deps.UpdateMySettings == nil: return Dependencies{}, fmt.Errorf("update-my-settings use case must not be nil") case deps.GetUserByID == nil: return Dependencies{}, fmt.Errorf("get-user-by-id use case must not be nil") case deps.GetUserByEmail == nil: return Dependencies{}, fmt.Errorf("get-user-by-email use case must not be nil") case deps.GetUserByUserName == nil: return Dependencies{}, fmt.Errorf("get-user-by-user-name use case must not be nil") case deps.ListUsers == nil: return Dependencies{}, fmt.Errorf("list-users use case must not be nil") case deps.GetUserEligibility == nil: return Dependencies{}, fmt.Errorf("get-user-eligibility use case must not be nil") case deps.SyncDeclaredCountry == nil: return Dependencies{}, fmt.Errorf("sync-declared-country use case must not be nil") case deps.GrantEntitlement == nil: return Dependencies{}, fmt.Errorf("grant-entitlement use case must not be nil") case deps.ExtendEntitlement == nil: return Dependencies{}, fmt.Errorf("extend-entitlement use case must not be nil") case deps.RevokeEntitlement == nil: return Dependencies{}, fmt.Errorf("revoke-entitlement use case must not be nil") case deps.ApplySanction == nil: return Dependencies{}, fmt.Errorf("apply-sanction use case must not be nil") case deps.RemoveSanction == nil: return Dependencies{}, fmt.Errorf("remove-sanction use case must not be nil") case deps.SetLimit == nil: return Dependencies{}, fmt.Errorf("set-limit use case must not be nil") case deps.RemoveLimit == nil: return Dependencies{}, fmt.Errorf("remove-limit use case must not be nil") case deps.DeleteUser == nil: return Dependencies{}, fmt.Errorf("delete-user use case must not be nil") default: if deps.Logger == nil { deps.Logger = slog.Default() } return deps, nil } } func entitlementCommandResponseFromResult(result entitlementsvc.CommandResult) entitlementCommandResponse { response := entitlementCommandResponse{ UserID: result.UserID, Entitlement: entitlementSnapshotResponse{ PlanCode: string(result.Entitlement.PlanCode), IsPaid: result.Entitlement.IsPaid, Source: result.Entitlement.Source.String(), Actor: actorDTO{Type: result.Entitlement.Actor.Type.String(), ID: result.Entitlement.Actor.ID.String()}, ReasonCode: result.Entitlement.ReasonCode.String(), StartsAt: result.Entitlement.StartsAt.UTC(), UpdatedAt: result.Entitlement.UpdatedAt.UTC(), }, } if result.Entitlement.EndsAt != nil { value := result.Entitlement.EndsAt.UTC() response.Entitlement.EndsAt = &value } return response } func newOTelMiddleware(runtime *telemetry.Runtime) gin.HandlerFunc { options := []otelgin.Option{} if runtime != nil { options = append( options, otelgin.WithTracerProvider(runtime.TracerProvider()), otelgin.WithMeterProvider(runtime.MeterProvider()), ) } return otelgin.Middleware(internalHTTPServiceName, options...) } func withObservability(logger *slog.Logger, metrics *telemetry.Runtime) gin.HandlerFunc { if logger == nil { logger = slog.Default() } return func(c *gin.Context) { startedAt := time.Now() c.Next() statusCode := c.Writer.Status() route := c.FullPath() if route == "" { route = "unmatched" } errorCode, _ := c.Get(internalErrorCodeContextKey) errorCodeValue, _ := errorCode.(string) outcome := outcomeFromStatusCode(statusCode) duration := time.Since(startedAt) attrs := []any{ "transport", "http", "route", route, "method", c.Request.Method, "status_code", statusCode, "duration_ms", float64(duration.Microseconds()) / 1000, "edge_outcome", string(outcome), } if errorCodeValue != "" { attrs = append(attrs, "error_code", errorCodeValue) } attrs = append(attrs, logging.TraceAttrsFromContext(c.Request.Context())...) metricAttrs := []attribute.KeyValue{ attribute.String("route", route), attribute.String("method", c.Request.Method), attribute.String("edge_outcome", string(outcome)), } if errorCodeValue != "" { metricAttrs = append(metricAttrs, attribute.String("error_code", errorCodeValue)) } metrics.RecordInternalHTTPRequest(c.Request.Context(), metricAttrs, duration) switch outcome { case edgeOutcomeSuccess: logger.InfoContext(c.Request.Context(), "internal request completed", attrs...) case edgeOutcomeFailed: logger.ErrorContext(c.Request.Context(), "internal request failed", attrs...) default: logger.WarnContext(c.Request.Context(), "internal request rejected", attrs...) } } } type edgeOutcome string const ( edgeOutcomeSuccess edgeOutcome = "success" edgeOutcomeRejected edgeOutcome = "rejected" edgeOutcomeFailed edgeOutcome = "failed" ) func outcomeFromStatusCode(statusCode int) edgeOutcome { switch { case statusCode >= 500: return edgeOutcomeFailed case statusCode >= 400: return edgeOutcomeRejected default: return edgeOutcomeSuccess } }