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>
108 lines
5.4 KiB
SQL
108 lines
5.4 KiB
SQL
-- 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);
|