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