149 lines
5.3 KiB
Go
149 lines
5.3 KiB
Go
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
|
|
}
|