Files
TS_Proto/backend/internal/handlers/handlers.go
T
nilesh.pandClaude Opus 4.8 4d3eb18a6c Initial commit: TravelHub multi-tenant flight booking platform
Angular + Go + PostgreSQL sample with strict per-tenant isolation.

- Backend: Go API (basic auth, tenant-scoped repository layer, pgx pool)
- Frontend: Angular 22 SPA (search, checkout, bookings, dashboard)
- Database: schema, seed data and demo bookings migrations
- Tests: 311 across Go (integration), Angular (unit) and Playwright (e2e),
  plus a generated evidence report with PDF export
- Security: whitebox VAPT report under security/

Secrets (.env, tokens, DB credentials) are git-ignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-13 11:38:01 +05:30

97 lines
2.8 KiB
Go

// 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"})
}