295 lines
8.7 KiB
Go
295 lines
8.7 KiB
Go
// Package internalhttp defines the frozen trusted internal HTTP contract used
|
|
// by Mail Service.
|
|
package internalhttp
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"galaxy/mail/internal/domain/common"
|
|
)
|
|
|
|
const (
|
|
// LoginCodeDeliveriesPath is the dedicated trusted route used by
|
|
// Auth / Session Service for auth login-code delivery intake.
|
|
LoginCodeDeliveriesPath = "/api/v1/internal/login-code-deliveries"
|
|
|
|
// IdempotencyKeyHeader is the required header that scopes auth-delivery
|
|
// deduplication.
|
|
IdempotencyKeyHeader = "Idempotency-Key"
|
|
|
|
// ErrorCodeInvalidRequest identifies trusted validation failures.
|
|
ErrorCodeInvalidRequest = "invalid_request"
|
|
|
|
// ErrorCodeInternalError identifies trusted invariant failures.
|
|
ErrorCodeInternalError = "internal_error"
|
|
|
|
// ErrorCodeServiceUnavailable identifies trusted availability failures.
|
|
ErrorCodeServiceUnavailable = "service_unavailable"
|
|
|
|
// ErrorCodeConflict identifies conflicting idempotency replays.
|
|
ErrorCodeConflict = "conflict"
|
|
|
|
jsonMediaType = "application/json"
|
|
)
|
|
|
|
// LoginCodeDeliveryRequest stores the strict JSON body accepted on the frozen
|
|
// auth-delivery route before normalization.
|
|
type LoginCodeDeliveryRequest struct {
|
|
// Email stores the destination e-mail address.
|
|
Email string `json:"email"`
|
|
|
|
// Code stores the exact login code generated by Auth / Session Service.
|
|
Code string `json:"code"`
|
|
|
|
// Locale stores the caller-selected BCP 47 language tag.
|
|
Locale string `json:"locale"`
|
|
}
|
|
|
|
// LoginCodeDeliveryCommand stores the normalized auth-delivery request shape
|
|
// that later Mail Service handlers and services can consume directly.
|
|
type LoginCodeDeliveryCommand struct {
|
|
// IdempotencyKey stores the caller-owned stable deduplication key.
|
|
IdempotencyKey common.IdempotencyKey
|
|
|
|
// Email stores the normalized recipient address.
|
|
Email common.Email
|
|
|
|
// Code stores the exact login code after boundary validation.
|
|
Code string
|
|
|
|
// Locale stores the canonical BCP 47 language tag.
|
|
Locale common.Locale
|
|
}
|
|
|
|
// Validate reports whether command satisfies the frozen auth-delivery
|
|
// contract.
|
|
func (command LoginCodeDeliveryCommand) Validate() error {
|
|
if err := command.IdempotencyKey.Validate(); err != nil {
|
|
return fmt.Errorf("idempotency key: %w", err)
|
|
}
|
|
if err := command.Email.Validate(); err != nil {
|
|
return fmt.Errorf("email: %w", err)
|
|
}
|
|
if strings.TrimSpace(command.Code) == "" {
|
|
return errors.New("code must not be empty")
|
|
}
|
|
if strings.TrimSpace(command.Code) != command.Code {
|
|
return errors.New("code must not contain surrounding whitespace")
|
|
}
|
|
if err := command.Locale.Validate(); err != nil {
|
|
return fmt.Errorf("locale: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Fingerprint returns the stable auth-delivery idempotency fingerprint of
|
|
// command.
|
|
func (command LoginCodeDeliveryCommand) Fingerprint() (string, error) {
|
|
if err := command.Validate(); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
normalized := struct {
|
|
IdempotencyKey string `json:"idempotency_key"`
|
|
Email string `json:"email"`
|
|
Code string `json:"code"`
|
|
Locale string `json:"locale"`
|
|
}{
|
|
IdempotencyKey: command.IdempotencyKey.String(),
|
|
Email: command.Email.String(),
|
|
Code: command.Code,
|
|
Locale: command.Locale.String(),
|
|
}
|
|
|
|
payload, err := json.Marshal(normalized)
|
|
if err != nil {
|
|
return "", fmt.Errorf("marshal login code delivery fingerprint: %w", err)
|
|
}
|
|
|
|
sum := sha256.Sum256(payload)
|
|
|
|
return "sha256:" + hex.EncodeToString(sum[:]), nil
|
|
}
|
|
|
|
// LoginCodeDeliveryOutcome identifies the stable successful auth-delivery
|
|
// intake outcomes.
|
|
type LoginCodeDeliveryOutcome string
|
|
|
|
const (
|
|
// LoginCodeDeliveryOutcomeSent reports durable acceptance into the internal
|
|
// mail-delivery pipeline.
|
|
LoginCodeDeliveryOutcomeSent LoginCodeDeliveryOutcome = "sent"
|
|
|
|
// LoginCodeDeliveryOutcomeSuppressed reports intentional outward delivery
|
|
// suppression while keeping the auth flow success-shaped.
|
|
LoginCodeDeliveryOutcomeSuppressed LoginCodeDeliveryOutcome = "suppressed"
|
|
)
|
|
|
|
// IsKnown reports whether outcome belongs to the frozen auth success surface.
|
|
func (outcome LoginCodeDeliveryOutcome) IsKnown() bool {
|
|
switch outcome {
|
|
case LoginCodeDeliveryOutcomeSent, LoginCodeDeliveryOutcomeSuppressed:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// LoginCodeDeliveryResponse stores the stable successful auth-delivery
|
|
// response body.
|
|
type LoginCodeDeliveryResponse struct {
|
|
// Outcome stores the stable coarse acceptance result.
|
|
Outcome LoginCodeDeliveryOutcome `json:"outcome"`
|
|
}
|
|
|
|
// Validate reports whether response satisfies the frozen success contract.
|
|
func (response LoginCodeDeliveryResponse) Validate() error {
|
|
if !response.Outcome.IsKnown() {
|
|
return fmt.Errorf("login code delivery outcome %q is unsupported", response.Outcome)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ErrorResponse stores the stable trusted error envelope used by Mail Service.
|
|
type ErrorResponse struct {
|
|
// Error stores the stable trusted error body.
|
|
Error ErrorBody `json:"error"`
|
|
}
|
|
|
|
// Validate reports whether response satisfies the frozen trusted error
|
|
// envelope contract.
|
|
func (response ErrorResponse) Validate() error {
|
|
return response.Error.Validate()
|
|
}
|
|
|
|
// ErrorBody stores the stable trusted error shape returned by Mail Service.
|
|
type ErrorBody struct {
|
|
// Code stores the stable machine-readable error code.
|
|
Code string `json:"code"`
|
|
|
|
// Message stores the trusted human-readable error message.
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// Validate reports whether body contains a complete trusted error payload.
|
|
func (body ErrorBody) Validate() error {
|
|
switch {
|
|
case strings.TrimSpace(body.Code) == "":
|
|
return errors.New("error code must not be empty")
|
|
case strings.TrimSpace(body.Code) != body.Code:
|
|
return errors.New("error code must not contain surrounding whitespace")
|
|
case strings.TrimSpace(body.Message) == "":
|
|
return errors.New("error message must not be empty")
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// DecodeLoginCodeDeliveryCommand validates one trusted HTTP request and
|
|
// returns the normalized auth-delivery command shape frozen by Stage 04.
|
|
func DecodeLoginCodeDeliveryCommand(request *http.Request) (LoginCodeDeliveryCommand, error) {
|
|
if request == nil {
|
|
return LoginCodeDeliveryCommand{}, errors.New("login code delivery request must not be nil")
|
|
}
|
|
|
|
if err := validateJSONContentType(request.Header.Get("Content-Type")); err != nil {
|
|
return LoginCodeDeliveryCommand{}, err
|
|
}
|
|
|
|
idempotencyKey, err := parseIdempotencyKey(request.Header.Get(IdempotencyKeyHeader))
|
|
if err != nil {
|
|
return LoginCodeDeliveryCommand{}, err
|
|
}
|
|
|
|
body, err := decodeLoginCodeDeliveryRequest(request.Body)
|
|
if err != nil {
|
|
return LoginCodeDeliveryCommand{}, err
|
|
}
|
|
|
|
command := LoginCodeDeliveryCommand{
|
|
IdempotencyKey: idempotencyKey,
|
|
Email: common.Email(strings.TrimSpace(body.Email)),
|
|
Code: body.Code,
|
|
}
|
|
|
|
locale, err := common.ParseLocale(strings.TrimSpace(body.Locale))
|
|
if err != nil {
|
|
return LoginCodeDeliveryCommand{}, fmt.Errorf("locale: %w", err)
|
|
}
|
|
command.Locale = locale
|
|
|
|
if err := command.Validate(); err != nil {
|
|
return LoginCodeDeliveryCommand{}, err
|
|
}
|
|
|
|
return command, nil
|
|
}
|
|
|
|
func decodeLoginCodeDeliveryRequest(body io.ReadCloser) (LoginCodeDeliveryRequest, error) {
|
|
if body == nil {
|
|
return LoginCodeDeliveryRequest{}, errors.New("request body must not be nil")
|
|
}
|
|
defer body.Close()
|
|
|
|
payload, err := io.ReadAll(body)
|
|
if err != nil {
|
|
return LoginCodeDeliveryRequest{}, fmt.Errorf("read request body: %w", err)
|
|
}
|
|
|
|
decoder := json.NewDecoder(bytes.NewReader(payload))
|
|
decoder.DisallowUnknownFields()
|
|
|
|
var request LoginCodeDeliveryRequest
|
|
if err := decoder.Decode(&request); err != nil {
|
|
return LoginCodeDeliveryRequest{}, fmt.Errorf("decode request body: %w", err)
|
|
}
|
|
if err := decoder.Decode(&struct{}{}); err != io.EOF {
|
|
if err == nil {
|
|
return LoginCodeDeliveryRequest{}, errors.New("decode request body: unexpected trailing JSON input")
|
|
}
|
|
|
|
return LoginCodeDeliveryRequest{}, fmt.Errorf("decode request body: %w", err)
|
|
}
|
|
|
|
return request, nil
|
|
}
|
|
|
|
func parseIdempotencyKey(value string) (common.IdempotencyKey, error) {
|
|
switch {
|
|
case strings.TrimSpace(value) == "":
|
|
return "", errors.New("Idempotency-Key header must not be empty")
|
|
case strings.TrimSpace(value) != value:
|
|
return "", errors.New("Idempotency-Key header must not contain surrounding whitespace")
|
|
default:
|
|
key := common.IdempotencyKey(value)
|
|
if err := key.Validate(); err != nil {
|
|
return "", fmt.Errorf("idempotency key: %w", err)
|
|
}
|
|
|
|
return key, nil
|
|
}
|
|
}
|
|
|
|
func validateJSONContentType(value string) error {
|
|
mediaType, _, err := mime.ParseMediaType(value)
|
|
if err != nil {
|
|
return fmt.Errorf("Content-Type must be %s", jsonMediaType)
|
|
}
|
|
if mediaType != jsonMediaType {
|
|
return fmt.Errorf("Content-Type must be %s", jsonMediaType)
|
|
}
|
|
|
|
return nil
|
|
}
|