package server import ( "context" "net/http" "galaxy/backend/internal/geo" "galaxy/backend/internal/server/handlers" "galaxy/backend/internal/server/httperr" "galaxy/backend/internal/telemetry" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" ) // AdminGeoLister is the narrow contract the admin geo handler needs // from the geo domain. `*geo.Service` satisfies it directly; tests // pass a recording fake. type AdminGeoLister interface { ListUserCounters(ctx context.Context, userID uuid.UUID) ([]geo.CountryCounter, error) } // AdminGeoHandlers groups the admin-side geo-counter handlers under // `/api/v1/admin/geo/*`. type AdminGeoHandlers struct { svc AdminGeoLister logger *zap.Logger } // NewAdminGeoHandlers constructs the handler set. svc may be nil — in // that case every handler returns 501 not_implemented, matching the // pre-Stage-5.8 placeholder behaviour. logger may also be nil; zap.NewNop // is used in that case. func NewAdminGeoHandlers(svc AdminGeoLister, logger *zap.Logger) *AdminGeoHandlers { if logger == nil { logger = zap.NewNop() } return &AdminGeoHandlers{svc: svc, logger: logger.Named("http.admin.geo")} } // adminGeoCountryWire mirrors `GeoCountryCounter` from `openapi.yaml`. type adminGeoCountryWire struct { Country string `json:"country"` Count int64 `json:"count"` LastSeenAt *string `json:"last_seen_at,omitempty"` } // adminGeoListWire mirrors `GeoCountryCounterList` from `openapi.yaml`. type adminGeoListWire struct { UserID string `json:"user_id"` Items []adminGeoCountryWire `json:"items"` } // ListUserCountries handles GET /api/v1/admin/geo/users/{user_id}/countries. func (h *AdminGeoHandlers) ListUserCountries() gin.HandlerFunc { if h.svc == nil { return handlers.NotImplemented("adminGeoListUserCountries") } return func(c *gin.Context) { userID, ok := parseUserIDParam(c) if !ok { return } ctx := c.Request.Context() entries, err := h.svc.ListUserCounters(ctx, userID) if err != nil { h.logger.Error("admin geo list user countries failed", append(telemetry.TraceFieldsFromContext(ctx), zap.String("user_id", userID.String()), zap.Error(err), )..., ) httperr.Abort(c, http.StatusInternalServerError, httperr.CodeInternalError, "service error") return } c.JSON(http.StatusOK, geoCountersToWire(userID, entries)) } } func geoCountersToWire(userID uuid.UUID, entries []geo.CountryCounter) adminGeoListWire { out := adminGeoListWire{ UserID: userID.String(), Items: make([]adminGeoCountryWire, 0, len(entries)), } for _, e := range entries { item := adminGeoCountryWire{ Country: e.Country, Count: e.Count, } if e.LastSeenAt != nil { formatted := e.LastSeenAt.UTC().Format("2006-01-02T15:04:05.000Z07:00") item.LastSeenAt = &formatted } out.Items = append(out.Items, item) } return out }