feat: mail service
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user