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,107 @@
|
||||
-- TravelHub: multi-tenant flight booking schema
|
||||
-- Strategy: shared database, shared schema, tenant_id discriminator on every
|
||||
-- tenant-owned table. Reference data (airports, airlines) is global.
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
|
||||
-- ---------------------------------------------------------------- tenants --
|
||||
CREATE TABLE IF NOT EXISTS tenants (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active'
|
||||
CHECK (status IN ('active', 'suspended')),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- ------------------------------------------------------------------ users --
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
email TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
full_name TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'agent'
|
||||
CHECK (role IN ('admin', 'agent')),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
-- The same email may exist in different tenants; unique only within one.
|
||||
CONSTRAINT users_tenant_email_key UNIQUE (tenant_id, email)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_tenant ON users(tenant_id);
|
||||
|
||||
-- ------------------------------------------------- reference data (global) --
|
||||
CREATE TABLE IF NOT EXISTS airports (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
iata_code CHAR(3) NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
city TEXT NOT NULL,
|
||||
country TEXT NOT NULL,
|
||||
timezone TEXT NOT NULL DEFAULT 'UTC'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS airlines (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
iata_code CHAR(2) NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
logo_url TEXT
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------- flights --
|
||||
-- Inventory is per-tenant: each agency loads its own negotiated fares.
|
||||
CREATE TABLE IF NOT EXISTS flights (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
airline_id UUID NOT NULL REFERENCES airlines(id),
|
||||
flight_number TEXT NOT NULL,
|
||||
origin_airport_id UUID NOT NULL REFERENCES airports(id),
|
||||
destination_airport_id UUID NOT NULL REFERENCES airports(id),
|
||||
departure_time TIMESTAMPTZ NOT NULL,
|
||||
arrival_time TIMESTAMPTZ NOT NULL,
|
||||
aircraft TEXT NOT NULL DEFAULT '',
|
||||
cabin_class TEXT NOT NULL DEFAULT 'economy'
|
||||
CHECK (cabin_class IN ('economy', 'premium_economy', 'business', 'first')),
|
||||
base_price NUMERIC(10,2) NOT NULL CHECK (base_price >= 0),
|
||||
currency CHAR(3) NOT NULL DEFAULT 'USD',
|
||||
seats_total INTEGER NOT NULL CHECK (seats_total > 0),
|
||||
seats_available INTEGER NOT NULL CHECK (seats_available >= 0),
|
||||
status TEXT NOT NULL DEFAULT 'scheduled'
|
||||
CHECK (status IN ('scheduled', 'delayed', 'cancelled')),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CONSTRAINT flights_seats_check CHECK (seats_available <= seats_total),
|
||||
CONSTRAINT flights_times_check CHECK (arrival_time > departure_time)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_flights_search
|
||||
ON flights(tenant_id, origin_airport_id, destination_airport_id, departure_time);
|
||||
|
||||
-- --------------------------------------------------------------- bookings --
|
||||
CREATE TABLE IF NOT EXISTS bookings (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id),
|
||||
flight_id UUID NOT NULL REFERENCES flights(id),
|
||||
booking_reference TEXT NOT NULL,
|
||||
passenger_count INTEGER NOT NULL CHECK (passenger_count > 0),
|
||||
total_amount NUMERIC(10,2) NOT NULL CHECK (total_amount >= 0),
|
||||
currency CHAR(3) NOT NULL DEFAULT 'USD',
|
||||
status TEXT NOT NULL DEFAULT 'confirmed'
|
||||
CHECK (status IN ('confirmed', 'cancelled')),
|
||||
contact_email TEXT NOT NULL,
|
||||
contact_phone TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
-- PNR is unique per tenant, not globally.
|
||||
CONSTRAINT bookings_tenant_ref_key UNIQUE (tenant_id, booking_reference)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_tenant_created ON bookings(tenant_id, created_at DESC);
|
||||
|
||||
-- ------------------------------------------------------------- passengers --
|
||||
CREATE TABLE IF NOT EXISTS passengers (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
booking_id UUID NOT NULL REFERENCES bookings(id) ON DELETE CASCADE,
|
||||
first_name TEXT NOT NULL,
|
||||
last_name TEXT NOT NULL,
|
||||
date_of_birth DATE,
|
||||
passport_number TEXT NOT NULL DEFAULT '',
|
||||
seat_number TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_passengers_booking ON passengers(booking_id);
|
||||
Reference in New Issue
Block a user