Add project guidance, security/governance/hygiene rules, and product wiki
- CLAUDE.md + AGENTS.md: canonical guiding principles for AI agents and developers (tenant isolation, no RLS, stack, security, conventions, environment gotchas) - SECURITY.md: required security controls (isolation, secrets, auth, input handling, least privilege) with VAPT status - GOVERNANCE.md: how changes are validated before merge — deterministic gates + AI review, risk tiers, human accountability - CONTRIBUTING.md: code hygiene, definition of done, file hygiene - docs/wiki/: product reference (overview, domain model, features, roles & access, API reference, glossary) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
45b5b34680
commit
035711f08c
@@ -0,0 +1,49 @@
|
||||
# Overview
|
||||
|
||||
## What TravelHub is
|
||||
|
||||
TravelHub is a **multi-tenant flight-booking platform** in the travel domain. It
|
||||
serves travel agencies: each agency's staff use it to search flight inventory,
|
||||
sell tickets to travellers, and manage the resulting bookings. It is built as a
|
||||
reference implementation of a secure, multi-tenant full-stack application.
|
||||
|
||||
- **Frontend:** Angular single-page app (the agent console).
|
||||
- **Backend:** Go HTTP API.
|
||||
- **Database:** PostgreSQL.
|
||||
- **Auth:** HTTP Basic, scoped per tenant.
|
||||
|
||||
## The multi-tenant model
|
||||
|
||||
A **tenant** is one travel agency. Many tenants share a single deployment and a
|
||||
single database. The central product promise: **no tenant can see another
|
||||
tenant's data** — not its flights, not its customers, not its revenue, not even
|
||||
whether a given booking exists.
|
||||
|
||||
Storage uses a **shared database, shared schema, `tenant_id` discriminator**:
|
||||
every tenant-owned row carries the id of the tenant that owns it. Reference data
|
||||
that is genuinely global — airports and airlines — is shared and has no tenant.
|
||||
|
||||
Isolation is enforced in the **application layer**: the tenant is derived from
|
||||
who you authenticated as, and every query is scoped to that tenant. (This is a
|
||||
deliberate design choice; PostgreSQL row-level security is intentionally not
|
||||
used — see [CLAUDE.md](../../CLAUDE.md).)
|
||||
|
||||
### What makes isolation visible
|
||||
|
||||
The two seeded demo agencies carry the **same routes at different fares** —
|
||||
Skyline at list price, Globetrotter at a negotiated markup — and different
|
||||
availability. Signing in as each and searching the same route shows different
|
||||
results. That contrast is the product's isolation made tangible, not an
|
||||
accident of the demo data.
|
||||
|
||||
## Who uses it
|
||||
|
||||
Agency staff in two roles — **admin** and **agent** (see
|
||||
[Roles & access](04-roles-and-access.md)). There is no traveller-facing
|
||||
self-service; a traveller is a passenger on a booking an agent makes.
|
||||
|
||||
## What it deliberately is not (yet)
|
||||
|
||||
A booking is a single one-way flight. Round trips, seat maps, ancillaries,
|
||||
payment capture, ticketing, and tenant self-signup are out of scope for this
|
||||
reference build.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Domain model
|
||||
|
||||
## Entities
|
||||
|
||||
| Entity | Scope | Purpose |
|
||||
| ------ | ----- | ------- |
|
||||
| **Tenant** | — | A travel agency. The owner of all tenant-scoped data. |
|
||||
| **User** | per-tenant | Agency staff who sign in (admin or agent). |
|
||||
| **Airport** | global | Reference data (IATA code, city, country, timezone). |
|
||||
| **Airline** | global | Reference data (IATA code, name). |
|
||||
| **Flight** | per-tenant | Sellable inventory: a route on a date at a fare, with seats. |
|
||||
| **Booking** | per-tenant | A confirmed sale against a flight. |
|
||||
| **Passenger** | per-tenant | A traveller on a booking. |
|
||||
|
||||
"Per-tenant" entities carry a `tenant_id` and cascade-delete with the tenant.
|
||||
"Global" entities are shared by all tenants and have no tenant.
|
||||
|
||||
## Relationships
|
||||
|
||||
```
|
||||
Tenant 1───* User
|
||||
Tenant 1───* Flight ───* Booking ───* Passenger
|
||||
│ │
|
||||
Airline, Airport User (the agent who sold it)
|
||||
(global refs)
|
||||
```
|
||||
|
||||
- A **Flight** references a global Airline and two global Airports (origin,
|
||||
destination), but belongs to one tenant — so the same physical route exists as
|
||||
a separate, independently-priced Flight row per tenant.
|
||||
- A **Booking** references a Flight, the User who made it, and has many
|
||||
Passengers.
|
||||
|
||||
## Key fields & rules
|
||||
|
||||
**Tenant** — `slug` (unique), `name`, `status` ∈ {`active`, `suspended`}. A
|
||||
suspended tenant's users cannot sign in, and it is hidden from the public tenant
|
||||
list.
|
||||
|
||||
**User** — `email`, `password_hash` (bcrypt), `full_name`, `role` ∈
|
||||
{`admin`, `agent`}. Email is **unique per tenant, not globally** — the same
|
||||
person can hold accounts at several agencies.
|
||||
|
||||
**Flight** — `flight_number`, `departure_time`/`arrival_time`, `cabin_class` ∈
|
||||
{`economy`, `premium_economy`, `business`, `first`}, `base_price`, `currency`,
|
||||
`seats_total`, `seats_available`, `status` ∈ {`scheduled`, `delayed`,
|
||||
`cancelled`}. Constraints guarantee `seats_available ≤ seats_total` and
|
||||
`arrival_time > departure_time`.
|
||||
|
||||
**Booking** — `booking_reference` (the PNR, **unique per tenant**),
|
||||
`passenger_count`, `total_amount`, `currency`, `status` ∈ {`confirmed`,
|
||||
`cancelled`}, contact email/phone.
|
||||
|
||||
**Passenger** — name, optional date of birth, passport number, seat.
|
||||
|
||||
## Invariants the system must uphold
|
||||
|
||||
- **No overselling.** A booking is created in a transaction that row-locks the
|
||||
flight (`SELECT … FOR UPDATE`) before reading availability, so two concurrent
|
||||
sales of the last seat cannot both succeed — the second gets a 409.
|
||||
- **Seat accounting round-trips.** Confirming a booking decrements
|
||||
`seats_available` by the passenger count; cancelling returns them (guarded so
|
||||
a double-cancel can't credit twice).
|
||||
- **PNR uniqueness is per tenant**, generated from an alphabet that omits
|
||||
easily-confused characters (no I/O/0/1).
|
||||
- **Tenant isolation** on every read and write (see
|
||||
[Overview](01-overview.md) and [SECURITY.md](../../SECURITY.md)).
|
||||
|
||||
The authoritative schema is in
|
||||
[`backend/migrations/001_init.sql`](../../backend/migrations/001_init.sql).
|
||||
@@ -0,0 +1,58 @@
|
||||
# Features
|
||||
|
||||
The agent console, screen by screen. Each maps to a feature area used in the
|
||||
test suite and the evidence report.
|
||||
|
||||
## Sign in
|
||||
|
||||
- The agency picker lists active tenants (the only unauthenticated read).
|
||||
- The agent chooses an agency, then enters credentials. The same email/password
|
||||
is only valid against the agency it was issued for — valid credentials aimed
|
||||
at the wrong agency are rejected.
|
||||
- Demo-account shortcuts fill the form for quick access (see
|
||||
[Roles & access](04-roles-and-access.md)).
|
||||
- Sessions survive a reload (credentials held client-side) but not a sign-out;
|
||||
stale credentials are discarded and the user returned to sign-in.
|
||||
|
||||
## Search flights
|
||||
|
||||
- Filter the agency's inventory by **origin/destination** (IATA), **date**,
|
||||
**cabin**, and **number of travellers**.
|
||||
- Dropdowns offer only the airports the agency actually flies.
|
||||
- Sort by **lowest fare**, **shortest trip**, or **earliest departure**.
|
||||
- Results are paged; each card shows carrier, times (with a +1 for overnight
|
||||
arrivals), duration, fare, cabin, and seats remaining (scarce inventory is
|
||||
flagged). Only future, non-cancelled flights with enough seats appear.
|
||||
|
||||
## Checkout & booking
|
||||
|
||||
- Selecting a flight opens checkout with the fare carried over.
|
||||
- Enter one or more travellers (up to 9) and contact details; the total reprices
|
||||
live as travellers change.
|
||||
- Confirming creates the booking and shows a confirmation with the **PNR**.
|
||||
- Guardrails: the party can't exceed remaining seats; a seat sold while the form
|
||||
was open surfaces as a clear "just sold" message (409), not an oversell.
|
||||
|
||||
## Bookings
|
||||
|
||||
- List every reservation the agency has sold, filterable by status
|
||||
(confirmed/cancelled), newest first.
|
||||
- Open a booking to see its full itinerary, travellers, totals and contact.
|
||||
- **Cancel** a booking (two-step confirm); the seats return to inventory. A
|
||||
cancelled booking still appears in the list, marked cancelled.
|
||||
|
||||
## Dashboard
|
||||
|
||||
- The agency's trading position: total revenue, confirmed/cancelled bookings,
|
||||
seats sold, future flights in stock.
|
||||
- Best-selling routes and the most recent bookings.
|
||||
- All figures are strictly per-agency; a sale moves them and a cancellation
|
||||
moves them back.
|
||||
|
||||
## Cross-cutting
|
||||
|
||||
- **Multi-tenant throughout:** every screen shows only the signed-in agency's
|
||||
data; the sidebar always names the current agency.
|
||||
- **Responsive:** works at desk and phone width; no horizontal scrolling.
|
||||
- **Observable correctness:** the e2e suite captures screenshots and the API
|
||||
request/response behind each of these flows into the evidence report.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Roles & access
|
||||
|
||||
## Authentication
|
||||
|
||||
HTTP **Basic auth**, scoped per tenant. Every API call (except the two public
|
||||
endpoints) carries:
|
||||
|
||||
- an `Authorization: Basic base64(email:password)` header, and
|
||||
- an `X-Tenant` header naming the agency (or the agency is taken from the
|
||||
subdomain).
|
||||
|
||||
The password is verified against the user row **inside the named tenant**, so
|
||||
credentials from one agency never authenticate at another. Passwords are stored
|
||||
as bcrypt hashes.
|
||||
|
||||
> Basic auth replays a reusable credential on every request and the SPA holds it
|
||||
> client-side. That is acceptable for this reference build behind TLS; a public
|
||||
> deployment should move to short-lived tokens. See
|
||||
> [SECURITY.md](../../SECURITY.md).
|
||||
|
||||
## Roles
|
||||
|
||||
| Role | Intended capability |
|
||||
| ---- | ------------------- |
|
||||
| **agent** | Search, book, manage and cancel bookings; view the dashboard. |
|
||||
| **admin** | Everything an agent can do, plus agency administration. |
|
||||
|
||||
Both roles exist on the user record and the API exposes an `admin`/`agent`
|
||||
distinction. **Note:** admin-only functionality is not yet built, so today the
|
||||
two roles are functionally identical — the `RequireAdmin` gate exists but is
|
||||
wired to no route. When admin features are added they must be gated explicitly
|
||||
(see [GOVERNANCE.md](../../GOVERNANCE.md)).
|
||||
|
||||
## Tenant status
|
||||
|
||||
A tenant is `active` or `suspended`. A suspended tenant's users cannot sign in,
|
||||
and the tenant is hidden from the public agency picker.
|
||||
|
||||
## Demo accounts
|
||||
|
||||
Seeded by `002_seed.sql`. Password for all: **`demo1234`**.
|
||||
|
||||
| Agency | Email | Role |
|
||||
| ------ | ----- | ---- |
|
||||
| Skyline Travel | `admin@skyline.test` | admin |
|
||||
| Skyline Travel | `agent@skyline.test` | agent |
|
||||
| Globetrotter Tours | `admin@globetrotter.test` | admin |
|
||||
| Globetrotter Tours | `agent@globetrotter.test` | agent |
|
||||
|
||||
Sign in to each agency and search `JFK → LHR`: the same route is priced
|
||||
differently per tenant — the isolation made visible.
|
||||
|
||||
## Public (unauthenticated) endpoints
|
||||
|
||||
Only two, by necessity:
|
||||
|
||||
- `GET /api/health` — liveness.
|
||||
- `GET /api/tenants` — the agency list for the sign-in picker (returns slug and
|
||||
name only; active tenants only).
|
||||
|
||||
Everything else requires a valid, tenant-scoped credential.
|
||||
@@ -0,0 +1,67 @@
|
||||
# API reference
|
||||
|
||||
Base URL (local): `http://localhost:8080`. All responses are JSON with camelCase
|
||||
fields. All endpoints require `Authorization` (Basic) and `X-Tenant` headers
|
||||
**except** the two marked public.
|
||||
|
||||
| Method | Path | Purpose |
|
||||
| ------ | ---- | ------- |
|
||||
| GET | `/api/health` | Liveness (**public**) |
|
||||
| GET | `/api/tenants` | Agency list for the sign-in picker (**public**) |
|
||||
| GET | `/api/session` | Current user + tenant; validates credentials |
|
||||
| GET | `/api/flights` | Search inventory (see parameters) |
|
||||
| GET | `/api/flights/{id}` | One flight |
|
||||
| GET | `/api/airports` | Airports in this tenant's network |
|
||||
| GET | `/api/airlines` | Carriers in this tenant's inventory |
|
||||
| GET | `/api/bookings` | List bookings (`?status=confirmed|cancelled`) |
|
||||
| POST | `/api/bookings` | Create a booking |
|
||||
| GET | `/api/bookings/{id}` | One booking with passengers |
|
||||
| POST | `/api/bookings/{id}/cancel` | Cancel; returns seats to inventory |
|
||||
| GET | `/api/dashboard` | Agency totals, top routes, recent bookings |
|
||||
|
||||
## Flight search parameters
|
||||
|
||||
`GET /api/flights` accepts: `origin`, `destination` (IATA codes), `date`
|
||||
(`YYYY-MM-DD`), `cabin` (`economy|premium_economy|business|first`), `passengers`
|
||||
(min seats free), `sort` (`price|duration|departure`), `limit`, `offset`.
|
||||
Only future, non-cancelled flights with enough seats are returned.
|
||||
|
||||
```bash
|
||||
curl -u agent@skyline.test:demo1234 -H 'X-Tenant: skyline' \
|
||||
'http://localhost:8080/api/flights?origin=JFK&destination=LHR&sort=price&limit=5'
|
||||
```
|
||||
|
||||
## Create a booking
|
||||
|
||||
`POST /api/bookings`
|
||||
|
||||
```json
|
||||
{
|
||||
"flightId": "uuid",
|
||||
"contactEmail": "ops@example.com",
|
||||
"contactPhone": "+1-555-0100",
|
||||
"passengers": [
|
||||
{ "firstName": "Dana", "lastName": "Whitfield",
|
||||
"dateOfBirth": "1988-04-12", "passportNumber": "X1234567" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Server-controlled (never trust the client for these): the tenant, the fare,
|
||||
the total, and the PNR. Unknown fields are rejected (`400`).
|
||||
|
||||
## Status codes & contracts
|
||||
|
||||
| Code | When |
|
||||
| ---- | ---- |
|
||||
| 200 / 201 | success (201 on create) |
|
||||
| 400 | invalid input (bad date/cabin, malformed JSON, unknown field, validation) |
|
||||
| 401 | missing/invalid credentials, or credentials for the wrong tenant |
|
||||
| 403 | admin-only route refused to an agent |
|
||||
| 404 | not found **or** belongs to another tenant (existence is hidden) |
|
||||
| 409 | not enough seats (oversell prevented) |
|
||||
| 500 | server error (generic message; no internals leaked) |
|
||||
|
||||
The full request/response of each call is captured in the e2e evidence report
|
||||
(with the `Authorization` header redacted). Types are defined in
|
||||
[`backend/internal/models`](../../backend/internal/models/models.go).
|
||||
@@ -0,0 +1,22 @@
|
||||
# Glossary
|
||||
|
||||
| Term | Meaning |
|
||||
| ---- | ------- |
|
||||
| **Tenant** | One travel agency using the platform. The owner of all tenant-scoped data. Identified by a `slug` (e.g. `skyline`). |
|
||||
| **Agency** | The user-facing word for a tenant. |
|
||||
| **Agent** | Agency staff member who searches, books and manages reservations. |
|
||||
| **Admin** | Agency staff member with (intended) administration rights on top of agent capabilities. |
|
||||
| **Tenant isolation** | The guarantee that one tenant cannot see or affect another's data. Enforced in the application layer. |
|
||||
| **PNR / Booking reference** | The 6-character code identifying a booking (e.g. `QX7A2M`). Unique within a tenant. |
|
||||
| **Flight (inventory)** | A sellable route on a date at a fare with a seat count, owned by one tenant. The same physical route is a separate row per tenant. |
|
||||
| **Cabin class** | `economy`, `premium_economy`, `business`, or `first`. |
|
||||
| **Seats available / total** | Remaining and original seats on a flight. Bookings decrement available; cancellations restore it. |
|
||||
| **Oversell** | Selling more seats than exist. Prevented by a row-locked transaction at booking time. |
|
||||
| **IATA code** | Standard airport (3-letter, e.g. `JFK`) or airline (2-letter, e.g. `AA`) code. |
|
||||
| **Reference data** | Global, non-tenant data: airports and airlines, shared by all tenants. |
|
||||
| **X-Tenant** | The HTTP header naming which agency a request is for (or taken from the subdomain). |
|
||||
| **Basic auth** | The authentication scheme: `Authorization: Basic base64(email:password)`, verified within the named tenant. |
|
||||
| **Suspended tenant** | An agency whose users cannot sign in and which is hidden from the agency picker. |
|
||||
| **Evidence report** | The e2e test report showing screenshots and the API request/response for each scenario (`e2e/evidence-report/`). |
|
||||
| **Scenario card** | A structured definition of one test scenario in `qa/scenarios/`; the source a Playwright test is generated from. |
|
||||
| **Binding (test layer)** | A test of the seam between layers — that the UI sends what the API expects and renders what it returns. |
|
||||
@@ -0,0 +1,24 @@
|
||||
# TravelHub product wiki
|
||||
|
||||
The product reference for TravelHub — what it is, how it is modelled, and how it
|
||||
behaves. For engineering conventions see [CLAUDE.md](../../CLAUDE.md); for
|
||||
running and testing see [TESTING.md](../../TESTING.md) and
|
||||
[qa/README.md](../../qa/README.md).
|
||||
|
||||
## Pages
|
||||
|
||||
1. [Overview](01-overview.md) — what TravelHub is and the multi-tenant model
|
||||
2. [Domain model](02-domain-model.md) — entities, relationships, the data model
|
||||
3. [Features](03-features.md) — capabilities, screen by screen
|
||||
4. [Roles & access](04-roles-and-access.md) — admin/agent, auth, demo accounts
|
||||
5. [API reference](05-api-reference.md) — endpoints and contracts
|
||||
6. [Glossary](06-glossary.md) — domain terms
|
||||
|
||||
## In one paragraph
|
||||
|
||||
TravelHub is a multi-tenant flight-booking platform for travel agencies. Several
|
||||
agencies ("tenants") share one deployment and one database, but each sees only
|
||||
its own inventory, customers and revenue. An agency's staff sign in, search the
|
||||
agency's flight inventory, sell tickets, manage and cancel bookings, and see a
|
||||
trading dashboard. It is built as a reference sample: Angular SPA, Go API,
|
||||
PostgreSQL, with strict per-tenant isolation.
|
||||
Reference in New Issue
Block a user