package httpapi import ( "crypto/rand" "encoding/hex" "encoding/json" "net/http" "slices" "strings" "juegoland/games2/contracts/shared/go" "juegoland/games2/internal/domain/catalog" "juegoland/games2/internal/server" ) type Handler struct { service *server.Service } type gameView = shared.GameCard type response[T any] struct { RequestID string `json:"requestId"` Data T `json:"data"` } type catalogData struct { Items []gameView `json:"items"` NextCursor *string `json:"nextCursor"` } func NewHandler(service *server.Service) http.Handler { h := &Handler{service: service} mux := http.NewServeMux() mux.HandleFunc("GET /live", h.live) mux.HandleFunc("GET /ready", h.ready) mux.HandleFunc("GET /api/v2/bootstrap", h.bootstrap) mux.HandleFunc("GET /api/v2/catalog", h.catalog) return mux } func (h *Handler) live(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "live"}) } func (h *Handler) ready(w http.ResponseWriter, _ *http.Request) { // This only asserts that the bootstrap server can serve its current // in-memory catalog. PostgreSQL and games are not enabled in this phase. writeJSON(w, http.StatusOK, map[string]string{"status": "ready", "phase": "foundation"}) } func (h *Handler) bootstrap(w http.ResponseWriter, _ *http.Request) { id, ok := newRequestID(w) if !ok { return } writeJSON(w, http.StatusOK, response[server.Bootstrap]{RequestID: id, Data: h.service.Bootstrap()}) } func (h *Handler) catalog(w http.ResponseWriter, r *http.Request) { id, ok := newRequestID(w) if !ok { return } locale := selectLocale(r.Header.Get("Accept-Language")) games := h.service.PublishedGames() items := make([]gameView, 0, len(games)) languages := make(map[string]bool) for _, game := range games { item := toGameView(game, locale) items = append(items, item) languages[string(item.Locale)] = true } if len(languages) == 0 { languages[locale] = true } usedLanguages := make([]string, 0, len(languages)) for language := range languages { usedLanguages = append(usedLanguages, language) } slices.Sort(usedLanguages) w.Header().Set("Content-Language", strings.Join(usedLanguages, ", ")) w.Header().Set("Vary", "Accept-Language") writeJSON(w, http.StatusOK, response[catalogData]{ RequestID: id, Data: catalogData{Items: items, NextCursor: nil}, }) } func toGameView(game catalog.Game, locale string) gameView { copy, usedLocale := game.Text(locale) return gameView{ ID: game.ID, Version: game.Version, Title: copy.Title, Summary: copy.Summary, Locale: shared.Locale(usedLocale), MinPlayers: int64(game.MinPlayers), MaxPlayers: int64(game.MaxPlayers), } } func newRequestID(w http.ResponseWriter) (string, bool) { var bytes [16]byte if _, err := rand.Read(bytes[:]); err != nil { http.Error(w, "Unable to create request ID", http.StatusInternalServerError) return "", false } bytes[6] = (bytes[6] & 0x0f) | 0x40 bytes[8] = (bytes[8] & 0x3f) | 0x80 var encoded [36]byte hex.Encode(encoded[0:8], bytes[0:4]) encoded[8] = '-' hex.Encode(encoded[9:13], bytes[4:6]) encoded[13] = '-' hex.Encode(encoded[14:18], bytes[6:8]) encoded[18] = '-' hex.Encode(encoded[19:23], bytes[8:10]) encoded[23] = '-' hex.Encode(encoded[24:36], bytes[10:16]) return string(encoded[:]), true } func writeJSON(w http.ResponseWriter, status int, body any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Cache-Control", "no-store") w.Header().Set("X-Content-Type-Options", "nosniff") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(body) }