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