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