package usersvc import ( "context" "encoding/json" "fmt" "io" "net/http" "strings" "time" app "knowfoolery/backend/services/game-session-service/internal/application/session" sharederrors "knowfoolery/backend/shared/domain/errors" ) // Client implements User Service HTTP integration. type Client struct { baseURL string httpClient *http.Client } // NewClient creates a user-service client. func NewClient(baseURL string, timeout time.Duration) *Client { if timeout <= 0 { timeout = 3 * time.Second } return &Client{ baseURL: strings.TrimRight(baseURL, "/"), httpClient: &http.Client{ Timeout: timeout, }, } } type responseEnvelope[T any] struct { Success bool `json:"success"` Data T `json:"data"` } type errorEnvelope struct { Error bool `json:"error"` Code string `json:"code"` Message string `json:"message"` Details string `json:"details"` } type userPayload struct { ID string `json:"id"` DisplayName string `json:"display_name"` EmailVerified bool `json:"email_verified"` } // GetUserProfile fetches a user profile and validates required fields. func (c *Client) GetUserProfile( ctx context.Context, userID, bearerToken string, ) (*app.UserProfile, error) { path := fmt.Sprintf("/users/%s", strings.TrimSpace(userID)) req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil) if err != nil { return nil, sharederrors.Wrap(sharederrors.CodeInternal, "build upstream request", err) } if token := strings.TrimSpace(bearerToken); token != "" { req.Header.Set("Authorization", "Bearer "+token) } resp, err := c.httpClient.Do(req) if err != nil { return nil, sharederrors.Wrap(sharederrors.CodeInternal, "user-service request failed", err) } defer func() { _ = resp.Body.Close() }() raw, err := io.ReadAll(resp.Body) if err != nil { return nil, sharederrors.Wrap(sharederrors.CodeInternal, "read user-service response", err) } if resp.StatusCode < 200 || resp.StatusCode >= 300 { var e errorEnvelope if jsonErr := json.Unmarshal(raw, &e); jsonErr == nil && strings.TrimSpace(e.Code) != "" { return nil, sharederrors.New(sharederrors.ErrorCode(e.Code), e.Message) } if resp.StatusCode == http.StatusNotFound { return nil, sharederrors.New(sharederrors.CodeUserNotFound, "user not found") } if resp.StatusCode == http.StatusUnauthorized { return nil, sharederrors.New(sharederrors.CodeUnauthorized, "unauthorized") } if resp.StatusCode == http.StatusForbidden { return nil, sharederrors.New(sharederrors.CodeForbidden, "forbidden") } return nil, sharederrors.New(sharederrors.CodeInternal, "user-service upstream error") } var out responseEnvelope[userPayload] if err := json.Unmarshal(raw, &out); err != nil { return nil, sharederrors.Wrap(sharederrors.CodeInternal, "decode user-service response", err) } return &app.UserProfile{ ID: out.Data.ID, DisplayName: out.Data.DisplayName, EmailVerified: out.Data.EmailVerified, }, nil } var _ app.UserClient = (*Client)(nil)