Files
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

119 lines
3.0 KiB
Go

package handlers
import (
"net/http"
"strings"
"time"
"github.com/google/uuid"
"github.com/travelswitch/travelhub/backend/internal/models"
)
// SearchFlights handles GET /api/flights.
//
// Query parameters: origin, destination (IATA codes), date (YYYY-MM-DD),
// cabin, passengers, sort (price|duration|departure), limit, offset.
func (h *Handler) SearchFlights(w http.ResponseWriter, r *http.Request) {
id, ok := h.identity(w, r)
if !ok {
return
}
q := r.URL.Query()
search := models.FlightSearch{
Origin: strings.ToUpper(strings.TrimSpace(q.Get("origin"))),
Destination: strings.ToUpper(strings.TrimSpace(q.Get("destination"))),
CabinClass: strings.TrimSpace(q.Get("cabin")),
Passengers: intParam(r, "passengers", 1, 1, 9),
SortBy: q.Get("sort"),
Limit: intParam(r, "limit", 25, 1, 100),
Offset: intParam(r, "offset", 0, 0, 100000),
}
if raw := strings.TrimSpace(q.Get("date")); raw != "" {
// Parsed in the server's local zone, which is what an agent selling
// from a desk in that zone means by "flights on the 12th".
d, err := time.ParseInLocation("2006-01-02", raw, time.Local)
if err != nil {
writeError(w, http.StatusBadRequest, "date must be in YYYY-MM-DD format")
return
}
search.DepartDate = &d
}
if search.CabinClass != "" && !validCabin(search.CabinClass) {
writeError(w, http.StatusBadRequest,
"cabin must be one of economy, premium_economy, business, first")
return
}
flights, total, err := h.Repo.SearchFlights(r.Context(), id.Tenant.ID, search)
if err != nil {
h.fail(w, err, "search flights")
return
}
writeJSON(w, http.StatusOK, page[models.Flight]{
Items: flights, Total: total, Limit: search.Limit, Offset: search.Offset,
})
}
// GetFlight handles GET /api/flights/{id}.
func (h *Handler) GetFlight(w http.ResponseWriter, r *http.Request) {
id, ok := h.identity(w, r)
if !ok {
return
}
flightID, err := uuid.Parse(r.PathValue("id"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid flight id")
return
}
flight, err := h.Repo.FlightByID(r.Context(), id.Tenant.ID, flightID)
if err != nil {
h.fail(w, err, "get flight")
return
}
writeJSON(w, http.StatusOK, flight)
}
// ListAirports handles GET /api/airports.
func (h *Handler) ListAirports(w http.ResponseWriter, r *http.Request) {
id, ok := h.identity(w, r)
if !ok {
return
}
airports, err := h.Repo.ListAirports(r.Context(), id.Tenant.ID)
if err != nil {
h.fail(w, err, "list airports")
return
}
writeJSON(w, http.StatusOK, airports)
}
// ListAirlines handles GET /api/airlines.
func (h *Handler) ListAirlines(w http.ResponseWriter, r *http.Request) {
id, ok := h.identity(w, r)
if !ok {
return
}
airlines, err := h.Repo.ListAirlines(r.Context(), id.Tenant.ID)
if err != nil {
h.fail(w, err, "list airlines")
return
}
writeJSON(w, http.StatusOK, airlines)
}
func validCabin(c string) bool {
switch c {
case "economy", "premium_economy", "business", "first":
return true
}
return false
}