feat: backend service
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
package backendclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SendEmailCodeInput is the public REST and adapter payload used to
|
||||
// request a login code for a single e-mail address.
|
||||
type SendEmailCodeInput struct {
|
||||
Email string `json:"email"`
|
||||
PreferredLanguage string `json:"-"`
|
||||
}
|
||||
|
||||
// SendEmailCodeResult is the public REST and adapter payload returned
|
||||
// after backend creates a login challenge.
|
||||
type SendEmailCodeResult struct {
|
||||
ChallengeID string `json:"challenge_id"`
|
||||
}
|
||||
|
||||
// ConfirmEmailCodeInput is the public REST and adapter payload used to
|
||||
// complete a previously issued login challenge.
|
||||
type ConfirmEmailCodeInput struct {
|
||||
ChallengeID string `json:"challenge_id"`
|
||||
Code string `json:"code"`
|
||||
ClientPublicKey string `json:"client_public_key"`
|
||||
TimeZone string `json:"time_zone"`
|
||||
}
|
||||
|
||||
// ConfirmEmailCodeResult is the public REST and adapter payload
|
||||
// returned after backend creates a device session.
|
||||
type ConfirmEmailCodeResult struct {
|
||||
DeviceSessionID string `json:"device_session_id"`
|
||||
}
|
||||
|
||||
// AuthError lets a public REST handler project a stable error envelope
|
||||
// without re-deriving backend semantics. StatusCode is the HTTP status
|
||||
// the gateway should return; Code and Message form the JSON envelope.
|
||||
type AuthError struct {
|
||||
StatusCode int
|
||||
Code string
|
||||
Message string
|
||||
}
|
||||
|
||||
// Error returns a readable representation of the projected auth error.
|
||||
func (e *AuthError) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("backendclient auth error: status=%d code=%s message=%s", e.StatusCode, e.Code, e.Message)
|
||||
}
|
||||
|
||||
// SendEmailCode delegates the public send-email-code route to backend.
|
||||
func (c *RESTClient) SendEmailCode(ctx context.Context, input SendEmailCodeInput) (SendEmailCodeResult, error) {
|
||||
if strings.TrimSpace(input.Email) == "" {
|
||||
return SendEmailCodeResult{}, errors.New("backendclient: send email code: email must not be empty")
|
||||
}
|
||||
body, status, err := c.doWithHeaders(ctx, http.MethodPost, c.baseURL+"/api/v1/public/auth/send-email-code", "", input, map[string]string{
|
||||
"Accept-Language": resolvePreferredLanguage(input.PreferredLanguage),
|
||||
})
|
||||
if err != nil {
|
||||
return SendEmailCodeResult{}, fmt.Errorf("backendclient: send email code: %w", err)
|
||||
}
|
||||
switch {
|
||||
case status == http.StatusOK:
|
||||
var result SendEmailCodeResult
|
||||
if err := decodeStrictJSON(body, &result); err != nil {
|
||||
return SendEmailCodeResult{}, fmt.Errorf("backendclient: send email code: decode success response: %w", err)
|
||||
}
|
||||
if strings.TrimSpace(result.ChallengeID) == "" {
|
||||
return SendEmailCodeResult{}, errors.New("backendclient: send email code: challenge_id must not be empty")
|
||||
}
|
||||
return result, nil
|
||||
case status >= 400 && status <= 599:
|
||||
authErr, decodeErr := decodeAuthError(status, body)
|
||||
if decodeErr != nil {
|
||||
return SendEmailCodeResult{}, fmt.Errorf("backendclient: send email code: %w", decodeErr)
|
||||
}
|
||||
return SendEmailCodeResult{}, authErr
|
||||
default:
|
||||
return SendEmailCodeResult{}, fmt.Errorf("backendclient: send email code: unexpected HTTP status %d", status)
|
||||
}
|
||||
}
|
||||
|
||||
// ConfirmEmailCode delegates the public confirm-email-code route to
|
||||
// backend.
|
||||
func (c *RESTClient) ConfirmEmailCode(ctx context.Context, input ConfirmEmailCodeInput) (ConfirmEmailCodeResult, error) {
|
||||
if strings.TrimSpace(input.ChallengeID) == "" {
|
||||
return ConfirmEmailCodeResult{}, errors.New("backendclient: confirm email code: challenge_id must not be empty")
|
||||
}
|
||||
body, status, err := c.doWithHeaders(ctx, http.MethodPost, c.baseURL+"/api/v1/public/auth/confirm-email-code", "", input, nil)
|
||||
if err != nil {
|
||||
return ConfirmEmailCodeResult{}, fmt.Errorf("backendclient: confirm email code: %w", err)
|
||||
}
|
||||
switch {
|
||||
case status == http.StatusOK:
|
||||
var result ConfirmEmailCodeResult
|
||||
if err := decodeStrictJSON(body, &result); err != nil {
|
||||
return ConfirmEmailCodeResult{}, fmt.Errorf("backendclient: confirm email code: decode success response: %w", err)
|
||||
}
|
||||
if strings.TrimSpace(result.DeviceSessionID) == "" {
|
||||
return ConfirmEmailCodeResult{}, errors.New("backendclient: confirm email code: device_session_id must not be empty")
|
||||
}
|
||||
return result, nil
|
||||
case status >= 400 && status <= 599:
|
||||
authErr, decodeErr := decodeAuthError(status, body)
|
||||
if decodeErr != nil {
|
||||
return ConfirmEmailCodeResult{}, fmt.Errorf("backendclient: confirm email code: %w", decodeErr)
|
||||
}
|
||||
return ConfirmEmailCodeResult{}, authErr
|
||||
default:
|
||||
return ConfirmEmailCodeResult{}, fmt.Errorf("backendclient: confirm email code: unexpected HTTP status %d", status)
|
||||
}
|
||||
}
|
||||
|
||||
// resolvePreferredLanguage returns a non-empty Accept-Language value or
|
||||
// the empty string when input is unset; downstream HTTP request helpers
|
||||
// drop the header on empty values.
|
||||
func resolvePreferredLanguage(preferred string) string {
|
||||
return strings.TrimSpace(preferred)
|
||||
}
|
||||
|
||||
type authErrorEnvelope struct {
|
||||
Error *authErrorBody `json:"error"`
|
||||
}
|
||||
|
||||
type authErrorBody struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func decodeAuthError(statusCode int, payload []byte) (*AuthError, error) {
|
||||
var envelope authErrorEnvelope
|
||||
if err := decodeStrictJSON(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 &AuthError{
|
||||
StatusCode: statusCode,
|
||||
Code: envelope.Error.Code,
|
||||
Message: envelope.Error.Message,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user