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:
nilesh.p
2026-09-13 11:38:01 +05:30
co-authored by Claude Opus 4.8
commit 4d3eb18a6c
129 changed files with 25424 additions and 0 deletions
@@ -0,0 +1,135 @@
package repository
import (
"context"
"github.com/google/uuid"
"github.com/travelswitch/travelhub/backend/internal/models"
)
// ListAirports returns the airports this tenant actually flies to or from,
// which is what the search form's dropdowns should offer. Airports themselves
// are global, but the filter keeps each agency's UI limited to its network.
func (r *Repository) ListAirports(ctx context.Context, tenantID uuid.UUID) ([]models.Airport, error) {
const q = `
SELECT DISTINCT a.id, a.iata_code, a.name, a.city, a.country, a.timezone
FROM airports a
WHERE EXISTS (
SELECT 1 FROM flights f
WHERE f.tenant_id = $1
AND (f.origin_airport_id = a.id OR f.destination_airport_id = a.id)
)
ORDER BY a.city, a.iata_code`
rows, err := r.pool.Query(ctx, q, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
airports := []models.Airport{}
for rows.Next() {
var a models.Airport
if err := rows.Scan(&a.ID, &a.IATACode, &a.Name, &a.City, &a.Country, &a.Timezone); err != nil {
return nil, err
}
airports = append(airports, a)
}
return airports, rows.Err()
}
// ListAirlines returns the carriers present in this tenant's inventory.
func (r *Repository) ListAirlines(ctx context.Context, tenantID uuid.UUID) ([]models.Airline, error) {
const q = `
SELECT DISTINCT al.id, al.iata_code, al.name, COALESCE(al.logo_url, '')
FROM airlines al
JOIN flights f ON f.airline_id = al.id AND f.tenant_id = $1
ORDER BY al.name`
rows, err := r.pool.Query(ctx, q, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
airlines := []models.Airline{}
for rows.Next() {
var a models.Airline
if err := rows.Scan(&a.ID, &a.IATACode, &a.Name, &a.LogoURL); err != nil {
return nil, err
}
airlines = append(airlines, a)
}
return airlines, rows.Err()
}
// DashboardStats aggregates one tenant's trading position.
func (r *Repository) DashboardStats(ctx context.Context, tenantID uuid.UUID) (models.DashboardStats, error) {
var s models.DashboardStats
s.Currency = "USD"
const totalsSQL = `
SELECT COUNT(*),
COUNT(*) FILTER (WHERE status = 'confirmed'),
COUNT(*) FILTER (WHERE status = 'cancelled'),
COALESCE(SUM(total_amount) FILTER (WHERE status = 'confirmed'), 0),
COALESCE(SUM(passenger_count) FILTER (WHERE status = 'confirmed'), 0)
FROM bookings
WHERE tenant_id = $1`
if err := r.pool.QueryRow(ctx, totalsSQL, tenantID).Scan(
&s.TotalBookings, &s.ConfirmedBookings, &s.CancelledBookings,
&s.TotalRevenue, &s.SeatsSold,
); err != nil {
return s, err
}
const stockSQL = `
SELECT COUNT(*) FROM flights
WHERE tenant_id = $1 AND departure_time > now() AND status <> 'cancelled'`
if err := r.pool.QueryRow(ctx, stockSQL, tenantID).Scan(&s.FlightsInStock); err != nil {
return s, err
}
const routesSQL = `
SELECT o.iata_code || ' to ' || d.iata_code AS route,
COUNT(*) AS bookings,
COALESCE(SUM(b.total_amount), 0) AS revenue
FROM bookings b
JOIN flights f ON f.id = b.flight_id
JOIN airports o ON o.id = f.origin_airport_id
JOIN airports d ON d.id = f.destination_airport_id
WHERE b.tenant_id = $1 AND b.status = 'confirmed'
GROUP BY route
ORDER BY revenue DESC
LIMIT 5`
rows, err := r.pool.Query(ctx, routesSQL, tenantID)
if err != nil {
return s, err
}
defer rows.Close()
for rows.Next() {
var entry struct {
Route string `json:"route"`
Bookings int `json:"bookings"`
Revenue float64 `json:"revenue"`
}
if err := rows.Scan(&entry.Route, &entry.Bookings, &entry.Revenue); err != nil {
return s, err
}
s.TopRoutes = append(s.TopRoutes, entry)
}
if err := rows.Err(); err != nil {
return s, err
}
recent, _, err := r.ListBookings(ctx, tenantID, "", 5, 0)
if err != nil {
return s, err
}
s.RecentBookings = recent
return s, nil
}