198 lines
6.1 KiB
Go
198 lines
6.1 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"time"
|
|
|
|
"galaxy/backend/internal/server/httperr"
|
|
"galaxy/backend/internal/telemetry"
|
|
"galaxy/backend/internal/user"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// timestampLayout matches the format used by other backend handlers
|
|
// (The implementation deviceSession serialisation). UTC, millisecond precision.
|
|
const timestampLayout = "2006-01-02T15:04:05.000Z07:00"
|
|
|
|
// respondAccountError maps user-package sentinels to the standard
|
|
// JSON error envelope. Unknown errors land on a 500.
|
|
func respondAccountError(c *gin.Context, logger *zap.Logger, op string, ctx context.Context, err error) {
|
|
switch {
|
|
case errors.Is(err, user.ErrAccountNotFound):
|
|
httperr.Abort(c, http.StatusNotFound, httperr.CodeNotFound, "account not found")
|
|
case errors.Is(err, user.ErrInvalidInput),
|
|
errors.Is(err, user.ErrInvalidActor):
|
|
httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, err.Error())
|
|
case errors.Is(err, user.ErrInvalidTier):
|
|
httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "tier is not supported")
|
|
case errors.Is(err, user.ErrInvalidSanctionCode):
|
|
httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "sanction_code is not supported")
|
|
default:
|
|
logger.Error(op+" failed",
|
|
append(telemetry.TraceFieldsFromContext(ctx), zap.Error(err))...,
|
|
)
|
|
httperr.Abort(c, http.StatusInternalServerError, httperr.CodeInternalError, "service error")
|
|
}
|
|
}
|
|
|
|
// accountResponseToWire renders the Account aggregate into the
|
|
// `AccountResponse` shape declared in openapi.yaml.
|
|
func accountResponseToWire(account user.Account) accountResponseWire {
|
|
return accountResponseWire{Account: accountToWire(account)}
|
|
}
|
|
|
|
func accountToWire(account user.Account) accountWire {
|
|
out := accountWire{
|
|
UserID: account.UserID.String(),
|
|
Email: account.Email,
|
|
UserName: account.UserName,
|
|
DisplayName: account.DisplayName,
|
|
PreferredLanguage: account.PreferredLanguage,
|
|
TimeZone: account.TimeZone,
|
|
DeclaredCountry: account.DeclaredCountry,
|
|
Entitlement: entitlementSnapshotToWire(account.Entitlement),
|
|
ActiveSanctions: activeSanctionsToWire(account.ActiveSanctions),
|
|
ActiveLimits: activeLimitsToWire(account.ActiveLimits),
|
|
CreatedAt: account.CreatedAt.UTC().Format(timestampLayout),
|
|
UpdatedAt: account.UpdatedAt.UTC().Format(timestampLayout),
|
|
}
|
|
return out
|
|
}
|
|
|
|
func entitlementSnapshotToWire(snap user.EntitlementSnapshot) entitlementSnapshotWire {
|
|
out := entitlementSnapshotWire{
|
|
PlanCode: snap.Tier,
|
|
IsPaid: snap.IsPaid,
|
|
Source: snap.Source,
|
|
Actor: actorRefToWire(snap.Actor),
|
|
ReasonCode: snap.ReasonCode,
|
|
StartsAt: snap.StartsAt.UTC().Format(timestampLayout),
|
|
MaxRegisteredRaceNames: snap.MaxRegisteredRaceNames,
|
|
UpdatedAt: snap.UpdatedAt.UTC().Format(timestampLayout),
|
|
}
|
|
if snap.EndsAt != nil {
|
|
formatted := snap.EndsAt.UTC().Format(timestampLayout)
|
|
out.EndsAt = &formatted
|
|
}
|
|
return out
|
|
}
|
|
|
|
func activeSanctionsToWire(items []user.ActiveSanction) []activeSanctionWire {
|
|
out := make([]activeSanctionWire, 0, len(items))
|
|
for _, s := range items {
|
|
entry := activeSanctionWire{
|
|
SanctionCode: s.SanctionCode,
|
|
Scope: s.Scope,
|
|
ReasonCode: s.ReasonCode,
|
|
Actor: actorRefToWire(s.Actor),
|
|
AppliedAt: s.AppliedAt.UTC().Format(timestampLayout),
|
|
}
|
|
if s.ExpiresAt != nil {
|
|
formatted := s.ExpiresAt.UTC().Format(timestampLayout)
|
|
entry.ExpiresAt = &formatted
|
|
}
|
|
out = append(out, entry)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func activeLimitsToWire(items []user.ActiveLimit) []activeLimitWire {
|
|
out := make([]activeLimitWire, 0, len(items))
|
|
for _, l := range items {
|
|
entry := activeLimitWire{
|
|
LimitCode: l.LimitCode,
|
|
Value: l.Value,
|
|
ReasonCode: l.ReasonCode,
|
|
Actor: actorRefToWire(l.Actor),
|
|
AppliedAt: l.AppliedAt.UTC().Format(timestampLayout),
|
|
}
|
|
if l.ExpiresAt != nil {
|
|
formatted := l.ExpiresAt.UTC().Format(timestampLayout)
|
|
entry.ExpiresAt = &formatted
|
|
}
|
|
out = append(out, entry)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func actorRefToWire(actor user.ActorRef) actorRefWire {
|
|
return actorRefWire{Type: actor.Type, ID: actor.ID}
|
|
}
|
|
|
|
// parseTimePtr converts a wire timestamp pointer into a time.Time
|
|
// pointer. A nil or empty input yields nil. Invalid timestamps return
|
|
// an error that handlers map to ErrInvalidInput.
|
|
func parseTimePtr(raw *string) (*time.Time, error) {
|
|
if raw == nil {
|
|
return nil, nil
|
|
}
|
|
if *raw == "" {
|
|
return nil, nil
|
|
}
|
|
t, err := time.Parse(time.RFC3339Nano, *raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &t, nil
|
|
}
|
|
|
|
// accountResponseWire mirrors `AccountResponse` in openapi.yaml.
|
|
type accountResponseWire struct {
|
|
Account accountWire `json:"account"`
|
|
}
|
|
|
|
// accountWire mirrors `Account` in openapi.yaml.
|
|
type accountWire struct {
|
|
UserID string `json:"user_id"`
|
|
Email string `json:"email"`
|
|
UserName string `json:"user_name"`
|
|
DisplayName string `json:"display_name,omitempty"`
|
|
PreferredLanguage string `json:"preferred_language"`
|
|
TimeZone string `json:"time_zone"`
|
|
DeclaredCountry string `json:"declared_country,omitempty"`
|
|
Entitlement entitlementSnapshotWire `json:"entitlement"`
|
|
ActiveSanctions []activeSanctionWire `json:"active_sanctions"`
|
|
ActiveLimits []activeLimitWire `json:"active_limits"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
type entitlementSnapshotWire struct {
|
|
PlanCode string `json:"plan_code"`
|
|
IsPaid bool `json:"is_paid"`
|
|
Source string `json:"source"`
|
|
Actor actorRefWire `json:"actor"`
|
|
ReasonCode string `json:"reason_code"`
|
|
StartsAt string `json:"starts_at"`
|
|
EndsAt *string `json:"ends_at,omitempty"`
|
|
MaxRegisteredRaceNames int32 `json:"max_registered_race_names"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
type activeSanctionWire struct {
|
|
SanctionCode string `json:"sanction_code"`
|
|
Scope string `json:"scope"`
|
|
ReasonCode string `json:"reason_code"`
|
|
Actor actorRefWire `json:"actor"`
|
|
AppliedAt string `json:"applied_at"`
|
|
ExpiresAt *string `json:"expires_at,omitempty"`
|
|
}
|
|
|
|
type activeLimitWire struct {
|
|
LimitCode string `json:"limit_code"`
|
|
Value int32 `json:"value"`
|
|
ReasonCode string `json:"reason_code"`
|
|
Actor actorRefWire `json:"actor"`
|
|
AppliedAt string `json:"applied_at"`
|
|
ExpiresAt *string `json:"expires_at,omitempty"`
|
|
}
|
|
|
|
type actorRefWire struct {
|
|
Type string `json:"type"`
|
|
ID string `json:"id,omitempty"`
|
|
}
|