225 lines
6.9 KiB
Go
225 lines
6.9 KiB
Go
package restapi
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
authServiceSendEmailCodePath = "/api/v1/public/auth/send-email-code"
|
|
authServiceConfirmEmailCodePath = "/api/v1/public/auth/confirm-email-code"
|
|
)
|
|
|
|
// HTTPAuthServiceClient implements AuthServiceClient over the Auth / Session
|
|
// Service public HTTP API using strict JSON request and response decoding.
|
|
type HTTPAuthServiceClient struct {
|
|
baseURL string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
type authServiceErrorEnvelope struct {
|
|
Error *authServiceErrorBody `json:"error"`
|
|
}
|
|
|
|
type authServiceErrorBody struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// NewHTTPAuthServiceClient constructs an AuthServiceClient that delegates the
|
|
// gateway public-auth routes to the Auth / Session Service public HTTP API at
|
|
// baseURL. The resulting client relies only on the caller-provided context for
|
|
// cancellation and timeout control.
|
|
func NewHTTPAuthServiceClient(baseURL string) (*HTTPAuthServiceClient, error) {
|
|
transport, ok := http.DefaultTransport.(*http.Transport)
|
|
if !ok {
|
|
return nil, errors.New("new auth service HTTP client: default transport is not *http.Transport")
|
|
}
|
|
|
|
return newHTTPAuthServiceClient(baseURL, &http.Client{
|
|
Transport: transport.Clone(),
|
|
})
|
|
}
|
|
|
|
func newHTTPAuthServiceClient(baseURL string, httpClient *http.Client) (*HTTPAuthServiceClient, error) {
|
|
if httpClient == nil {
|
|
return nil, errors.New("new auth service HTTP client: http client must not be nil")
|
|
}
|
|
|
|
trimmedBaseURL := strings.TrimSpace(baseURL)
|
|
if trimmedBaseURL == "" {
|
|
return nil, errors.New("new auth service HTTP client: base URL must not be empty")
|
|
}
|
|
|
|
parsedBaseURL, err := url.Parse(strings.TrimRight(trimmedBaseURL, "/"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("new auth service HTTP client: parse base URL: %w", err)
|
|
}
|
|
if parsedBaseURL.Scheme == "" || parsedBaseURL.Host == "" {
|
|
return nil, errors.New("new auth service HTTP client: base URL must be absolute")
|
|
}
|
|
|
|
return &HTTPAuthServiceClient{
|
|
baseURL: parsedBaseURL.String(),
|
|
httpClient: httpClient,
|
|
}, nil
|
|
}
|
|
|
|
// Close releases idle HTTP connections owned by the client transport.
|
|
func (c *HTTPAuthServiceClient) Close() error {
|
|
if c == nil || c.httpClient == nil {
|
|
return nil
|
|
}
|
|
|
|
type idleCloser interface {
|
|
CloseIdleConnections()
|
|
}
|
|
|
|
if transport, ok := c.httpClient.Transport.(idleCloser); ok {
|
|
transport.CloseIdleConnections()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SendEmailCode delegates the public send-email-code route to the configured
|
|
// Auth / Session Service public HTTP API.
|
|
func (c *HTTPAuthServiceClient) SendEmailCode(ctx context.Context, input SendEmailCodeInput) (SendEmailCodeResult, error) {
|
|
payload, statusCode, err := c.doJSONRequest(ctx, authServiceSendEmailCodePath, input)
|
|
if err != nil {
|
|
return SendEmailCodeResult{}, fmt.Errorf("send email code via auth service: %w", err)
|
|
}
|
|
|
|
switch {
|
|
case statusCode == http.StatusOK:
|
|
var result SendEmailCodeResult
|
|
if err := decodeStrictJSONPayload(payload, &result); err != nil {
|
|
return SendEmailCodeResult{}, fmt.Errorf("send email code via auth service: decode success response: %w", err)
|
|
}
|
|
if err := validateSendEmailCodeResult(&result); err != nil {
|
|
return SendEmailCodeResult{}, fmt.Errorf("send email code via auth service: %w", err)
|
|
}
|
|
|
|
return result, nil
|
|
case statusCode >= 400 && statusCode <= 599:
|
|
authErr, err := decodeAuthServiceError(statusCode, payload)
|
|
if err != nil {
|
|
return SendEmailCodeResult{}, fmt.Errorf("send email code via auth service: %w", err)
|
|
}
|
|
|
|
return SendEmailCodeResult{}, authErr
|
|
default:
|
|
return SendEmailCodeResult{}, fmt.Errorf("send email code via auth service: unexpected HTTP status %d", statusCode)
|
|
}
|
|
}
|
|
|
|
// ConfirmEmailCode delegates the public confirm-email-code route to the
|
|
// configured Auth / Session Service public HTTP API.
|
|
func (c *HTTPAuthServiceClient) ConfirmEmailCode(ctx context.Context, input ConfirmEmailCodeInput) (ConfirmEmailCodeResult, error) {
|
|
payload, statusCode, err := c.doJSONRequest(ctx, authServiceConfirmEmailCodePath, input)
|
|
if err != nil {
|
|
return ConfirmEmailCodeResult{}, fmt.Errorf("confirm email code via auth service: %w", err)
|
|
}
|
|
|
|
switch {
|
|
case statusCode == http.StatusOK:
|
|
var result ConfirmEmailCodeResult
|
|
if err := decodeStrictJSONPayload(payload, &result); err != nil {
|
|
return ConfirmEmailCodeResult{}, fmt.Errorf("confirm email code via auth service: decode success response: %w", err)
|
|
}
|
|
if err := validateConfirmEmailCodeResult(&result); err != nil {
|
|
return ConfirmEmailCodeResult{}, fmt.Errorf("confirm email code via auth service: %w", err)
|
|
}
|
|
|
|
return result, nil
|
|
case statusCode >= 400 && statusCode <= 599:
|
|
authErr, err := decodeAuthServiceError(statusCode, payload)
|
|
if err != nil {
|
|
return ConfirmEmailCodeResult{}, fmt.Errorf("confirm email code via auth service: %w", err)
|
|
}
|
|
|
|
return ConfirmEmailCodeResult{}, authErr
|
|
default:
|
|
return ConfirmEmailCodeResult{}, fmt.Errorf("confirm email code via auth service: unexpected HTTP status %d", statusCode)
|
|
}
|
|
}
|
|
|
|
func (c *HTTPAuthServiceClient) doJSONRequest(ctx context.Context, path string, requestBody any) ([]byte, int, error) {
|
|
if c == nil || c.httpClient == nil {
|
|
return nil, 0, errors.New("nil client")
|
|
}
|
|
if ctx == nil {
|
|
return nil, 0, errors.New("nil context")
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
payload, err := json.Marshal(requestBody)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("marshal request body: %w", err)
|
|
}
|
|
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(payload))
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("build request: %w", err)
|
|
}
|
|
request.Header.Set("Content-Type", "application/json")
|
|
|
|
response, err := c.httpClient.Do(request)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
responsePayload, err := io.ReadAll(response.Body)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("read response body: %w", err)
|
|
}
|
|
|
|
return responsePayload, response.StatusCode, nil
|
|
}
|
|
|
|
func decodeAuthServiceError(statusCode int, payload []byte) (*AuthServiceError, error) {
|
|
var envelope authServiceErrorEnvelope
|
|
if err := decodeStrictJSONPayload(payload, &envelope); err != nil {
|
|
return nil, fmt.Errorf("decode error response: %w", err)
|
|
}
|
|
if envelope.Error == nil {
|
|
return nil, errors.New("decode error response: missing error object")
|
|
}
|
|
|
|
return &AuthServiceError{
|
|
StatusCode: statusCode,
|
|
Code: envelope.Error.Code,
|
|
Message: envelope.Error.Message,
|
|
}, nil
|
|
}
|
|
|
|
func decodeStrictJSONPayload(payload []byte, target any) error {
|
|
decoder := json.NewDecoder(bytes.NewReader(payload))
|
|
decoder.DisallowUnknownFields()
|
|
|
|
if err := decoder.Decode(target); err != nil {
|
|
return err
|
|
}
|
|
if err := decoder.Decode(&struct{}{}); err != io.EOF {
|
|
if err == nil {
|
|
return errors.New("unexpected trailing JSON input")
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var _ AuthServiceClient = (*HTTPAuthServiceClient)(nil)
|