package backendclient import ( "context" "errors" "fmt" "net/http" "strings" "galaxy/gateway/internal/downstream" usermodel "galaxy/model/user" "galaxy/transcoder" ) const ( userCommandResultCodeOK = "ok" defaultUserErrorCode = "internal_error" ) var stableUserErrorMessages = map[string]string{ "invalid_request": "request is invalid", "subject_not_found": "subject not found", "conflict": "request conflicts with current state", defaultUserErrorCode: "internal server error", } // ExecuteUserCommand routes one authenticated user-surface command into // backend's `/api/v1/user/*` endpoints. The function is registered for // the message types listed in `galaxy/model/user`. func (c *RESTClient) ExecuteUserCommand(ctx context.Context, command downstream.AuthenticatedCommand) (downstream.UnaryResult, error) { if c == nil || c.httpClient == nil { return downstream.UnaryResult{}, errors.New("backendclient: execute user command: nil client") } if ctx == nil { return downstream.UnaryResult{}, errors.New("backendclient: execute user command: nil context") } if err := ctx.Err(); err != nil { return downstream.UnaryResult{}, err } if strings.TrimSpace(command.UserID) == "" { return downstream.UnaryResult{}, errors.New("backendclient: execute user command: user_id must not be empty") } switch command.MessageType { case usermodel.MessageTypeGetMyAccount: if _, err := transcoder.PayloadToGetMyAccountRequest(command.PayloadBytes); err != nil { return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute user command %q: %w", command.MessageType, err) } return c.executeUserAccountGet(ctx, command.UserID) case usermodel.MessageTypeUpdateMyProfile: req, err := transcoder.PayloadToUpdateMyProfileRequest(command.PayloadBytes) if err != nil { return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute user command %q: %w", command.MessageType, err) } return c.executeUserAccountUpdateProfile(ctx, command.UserID, req) case usermodel.MessageTypeUpdateMySettings: req, err := transcoder.PayloadToUpdateMySettingsRequest(command.PayloadBytes) if err != nil { return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute user command %q: %w", command.MessageType, err) } return c.executeUserAccountUpdateSettings(ctx, command.UserID, req) default: return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute user command: unsupported message type %q", command.MessageType) } } func (c *RESTClient) executeUserAccountGet(ctx context.Context, userID string) (downstream.UnaryResult, error) { body, status, err := c.do(ctx, http.MethodGet, c.baseURL+"/api/v1/user/account", userID, nil) if err != nil { return downstream.UnaryResult{}, fmt.Errorf("execute user.account.get: %w", err) } return projectUserResponse(status, body) } func (c *RESTClient) executeUserAccountUpdateProfile(ctx context.Context, userID string, req *usermodel.UpdateMyProfileRequest) (downstream.UnaryResult, error) { body, status, err := c.do(ctx, http.MethodPatch, c.baseURL+"/api/v1/user/account/profile", userID, req) if err != nil { return downstream.UnaryResult{}, fmt.Errorf("execute user.profile.update: %w", err) } return projectUserResponse(status, body) } func (c *RESTClient) executeUserAccountUpdateSettings(ctx context.Context, userID string, req *usermodel.UpdateMySettingsRequest) (downstream.UnaryResult, error) { body, status, err := c.do(ctx, http.MethodPatch, c.baseURL+"/api/v1/user/account/settings", userID, req) if err != nil { return downstream.UnaryResult{}, fmt.Errorf("execute user.settings.update: %w", err) } return projectUserResponse(status, body) } func projectUserResponse(statusCode int, payload []byte) (downstream.UnaryResult, error) { switch { case statusCode == http.StatusOK: var response usermodel.AccountResponse if err := decodeStrictJSON(payload, &response); err != nil { return downstream.UnaryResult{}, fmt.Errorf("decode success response: %w", err) } payloadBytes, err := transcoder.AccountResponseToPayload(&response) if err != nil { return downstream.UnaryResult{}, fmt.Errorf("encode success response payload: %w", err) } return downstream.UnaryResult{ ResultCode: userCommandResultCodeOK, PayloadBytes: payloadBytes, }, nil case statusCode == http.StatusServiceUnavailable: return downstream.UnaryResult{}, downstream.ErrDownstreamUnavailable case statusCode >= 400 && statusCode <= 599: errResp, err := decodeUserError(statusCode, payload) if err != nil { return downstream.UnaryResult{}, fmt.Errorf("decode error response: %w", err) } payloadBytes, err := transcoder.ErrorResponseToPayload(errResp) if err != nil { return downstream.UnaryResult{}, fmt.Errorf("encode error response payload: %w", err) } return downstream.UnaryResult{ ResultCode: errResp.Error.Code, PayloadBytes: payloadBytes, }, nil default: return downstream.UnaryResult{}, fmt.Errorf("unexpected HTTP status %d", statusCode) } } func decodeUserError(statusCode int, payload []byte) (*usermodel.ErrorResponse, error) { var response usermodel.ErrorResponse if err := decodeStrictJSON(payload, &response); err != nil { return nil, err } response.Error.Code = normalizeUserErrorCode(statusCode, response.Error.Code) response.Error.Message = normalizeUserErrorMessage(response.Error.Code, response.Error.Message) if strings.TrimSpace(response.Error.Code) == "" { return nil, errors.New("missing error code") } if strings.TrimSpace(response.Error.Message) == "" { return nil, errors.New("missing error message") } return &response, nil } func normalizeUserErrorCode(statusCode int, code string) string { if trimmed := strings.TrimSpace(code); trimmed != "" { return trimmed } switch statusCode { case http.StatusBadRequest: return "invalid_request" case http.StatusNotFound: return "subject_not_found" case http.StatusConflict: return "conflict" default: return defaultUserErrorCode } } func normalizeUserErrorMessage(code, message string) string { if trimmed := strings.TrimSpace(message); trimmed != "" { return trimmed } if stable, ok := stableUserErrorMessages[code]; ok { return stable } return stableUserErrorMessages[defaultUserErrorCode] }