99 lines
2.8 KiB
Go
99 lines
2.8 KiB
Go
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
|
|
}
|