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 }