// Package handlers implements the TravelHub JSON API. // // Handlers never read a tenant id from the URL, query string, or body. They // take it from the authenticated identity, which the Basic auth middleware // resolved from the credentials. That keeps "which tenant am I acting as" // impossible for a client to influence. package handlers import ( "encoding/json" "errors" "log/slog" "net/http" "strconv" "github.com/travelswitch/travelhub/backend/internal/auth" "github.com/travelswitch/travelhub/backend/internal/repository" ) // Handler carries the dependencies shared by every endpoint. type Handler struct { Repo *repository.Repository Logger *slog.Logger } // New builds a Handler. func New(repo *repository.Repository, logger *slog.Logger) *Handler { return &Handler{Repo: repo, Logger: logger} } // page is the envelope for every list endpoint. type page[T any] struct { Items []T `json:"items"` Total int `json:"total"` Limit int `json:"limit"` Offset int `json:"offset"` } func writeJSON(w http.ResponseWriter, status int, body any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if body != nil { json.NewEncoder(w).Encode(body) } } func writeError(w http.ResponseWriter, status int, message string) { writeJSON(w, status, map[string]string{"error": message}) } // fail maps a repository error onto a status code, logging only the ones that // represent a server fault. func (h *Handler) fail(w http.ResponseWriter, err error, context string) { switch { case errors.Is(err, repository.ErrNotFound): writeError(w, http.StatusNotFound, "not found") case errors.Is(err, repository.ErrNoSeats): writeError(w, http.StatusConflict, "not enough seats available on this flight") default: h.Logger.Error(context, "error", err) writeError(w, http.StatusInternalServerError, "internal server error") } } // identity pulls the authenticated caller out of the request, replying 401 if // the route was somehow mounted without the auth middleware. func (h *Handler) identity(w http.ResponseWriter, r *http.Request) (auth.Identity, bool) { id, err := auth.FromContext(r.Context()) if err != nil { writeError(w, http.StatusUnauthorized, "authentication required") return auth.Identity{}, false } return id, true } // intParam reads a bounded integer query parameter. func intParam(r *http.Request, name string, fallback, min, max int) int { raw := r.URL.Query().Get(name) if raw == "" { return fallback } v, err := strconv.Atoi(raw) if err != nil || v < min { return fallback } if v > max { return max } return v } // Health reports liveness. It is the only endpoint that needs neither // credentials nor a tenant. func (h *Handler) Health(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) }