generated from bisco/codex-bootstrap
Merge branch 'docs/initial-architecture' into develop
This commit is contained in:
315
docs/api-contract.md
Normal file
315
docs/api-contract.md
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
# API Contract
|
||||||
|
|
||||||
|
This document proposes the initial REST API for AzioneLab. Endpoint names are intentionally explicit and small-project friendly.
|
||||||
|
|
||||||
|
All examples use JSON unless noted otherwise.
|
||||||
|
|
||||||
|
## Public Content
|
||||||
|
|
||||||
|
### List Shows
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/shows/
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns published shows.
|
||||||
|
|
||||||
|
Response `200 OK`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "The Open Stage",
|
||||||
|
"slug": "the-open-stage",
|
||||||
|
"summary": "A contemporary theatre performance.",
|
||||||
|
"poster_image": "https://example.org/media/shows/open-stage.jpg"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Show Detail
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/shows/{slug}/
|
||||||
|
```
|
||||||
|
|
||||||
|
Response `200 OK`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "The Open Stage",
|
||||||
|
"slug": "the-open-stage",
|
||||||
|
"summary": "A contemporary theatre performance.",
|
||||||
|
"description": "Full public show description.",
|
||||||
|
"poster_image": "https://example.org/media/shows/open-stage.jpg",
|
||||||
|
"performances": [
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"starts_at": "2026-05-15T20:30:00+02:00",
|
||||||
|
"venue": {
|
||||||
|
"name": "AzioneLab Theatre",
|
||||||
|
"city": "Rome"
|
||||||
|
},
|
||||||
|
"booking_enabled": true,
|
||||||
|
"available_seats": 24
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Status codes:
|
||||||
|
|
||||||
|
- `200 OK`: show found;
|
||||||
|
- `404 Not Found`: show does not exist or is not published.
|
||||||
|
|
||||||
|
### List Performances
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/performances/
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional filters:
|
||||||
|
|
||||||
|
- `show`: show slug;
|
||||||
|
- `from`: start date/time lower bound.
|
||||||
|
|
||||||
|
Response `200 OK`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"show": {
|
||||||
|
"title": "The Open Stage",
|
||||||
|
"slug": "the-open-stage"
|
||||||
|
},
|
||||||
|
"venue": {
|
||||||
|
"name": "AzioneLab Theatre",
|
||||||
|
"city": "Rome"
|
||||||
|
},
|
||||||
|
"starts_at": "2026-05-15T20:30:00+02:00",
|
||||||
|
"booking_enabled": true,
|
||||||
|
"available_seats": 24
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Detail
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/performances/{id}/
|
||||||
|
```
|
||||||
|
|
||||||
|
Response `200 OK`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"show": {
|
||||||
|
"title": "The Open Stage",
|
||||||
|
"slug": "the-open-stage",
|
||||||
|
"summary": "A contemporary theatre performance."
|
||||||
|
},
|
||||||
|
"venue": {
|
||||||
|
"name": "AzioneLab Theatre",
|
||||||
|
"address": "Via Example 10",
|
||||||
|
"city": "Rome"
|
||||||
|
},
|
||||||
|
"starts_at": "2026-05-15T20:30:00+02:00",
|
||||||
|
"booking_enabled": true,
|
||||||
|
"available_seats": 24
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Status codes:
|
||||||
|
|
||||||
|
- `200 OK`: performance found;
|
||||||
|
- `404 Not Found`: performance does not exist or is not public.
|
||||||
|
|
||||||
|
## Booking
|
||||||
|
|
||||||
|
### Create Reservation
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/performances/{id}/reservations/
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates a pending reservation and sends a confirmation email.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Maria Rossi",
|
||||||
|
"email": "maria.rossi@example.org",
|
||||||
|
"phone": "+390600000000",
|
||||||
|
"party_size": 2,
|
||||||
|
"notes": "We will arrive a few minutes early."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response `201 Created`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 123,
|
||||||
|
"status": "pending",
|
||||||
|
"performance": 10,
|
||||||
|
"party_size": 2,
|
||||||
|
"message": "Reservation created. Please check your email to confirm it."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Status codes:
|
||||||
|
|
||||||
|
- `201 Created`: pending reservation created;
|
||||||
|
- `400 Bad Request`: invalid input;
|
||||||
|
- `404 Not Found`: performance does not exist or is not public;
|
||||||
|
- `409 Conflict`: booking is closed or capacity is already unavailable.
|
||||||
|
|
||||||
|
Validation rules:
|
||||||
|
|
||||||
|
- `name` is required;
|
||||||
|
- `email` must be a valid email address;
|
||||||
|
- `party_size` must be a positive integer;
|
||||||
|
- public clients must not set reservation status;
|
||||||
|
- the backend must validate booking availability server-side.
|
||||||
|
|
||||||
|
### Confirm Reservation
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/reservations/confirm/
|
||||||
|
```
|
||||||
|
|
||||||
|
Confirms a pending reservation using the token from the email link.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "opaque-confirmation-token"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response `200 OK`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"reservation_id": 123,
|
||||||
|
"status": "confirmed",
|
||||||
|
"party_size": 2,
|
||||||
|
"qr_code_url": "https://example.org/api/reservations/123/qr-code/"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Status codes:
|
||||||
|
|
||||||
|
- `200 OK`: reservation confirmed;
|
||||||
|
- `400 Bad Request`: token is missing or malformed;
|
||||||
|
- `404 Not Found`: token is unknown;
|
||||||
|
- `409 Conflict`: token already used, reservation already handled, or no capacity remains;
|
||||||
|
- `410 Gone`: token expired.
|
||||||
|
|
||||||
|
### Retrieve QR Code
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/reservations/{id}/qr-code/
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns the generated QR code for a confirmed reservation. Access must be protected by a valid QR token, signed URL, or equivalent control so that reservation IDs are not enough to retrieve QR codes.
|
||||||
|
|
||||||
|
Response `200 OK`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"reservation_id": 123,
|
||||||
|
"qr_code_image": "data:image/png;base64,...",
|
||||||
|
"printable": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Status codes:
|
||||||
|
|
||||||
|
- `200 OK`: QR code available;
|
||||||
|
- `403 Forbidden`: caller is not allowed to access the QR code;
|
||||||
|
- `404 Not Found`: reservation not found;
|
||||||
|
- `409 Conflict`: reservation is not confirmed.
|
||||||
|
|
||||||
|
## Check-In
|
||||||
|
|
||||||
|
### Verify QR Code
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/check-ins/verify/
|
||||||
|
```
|
||||||
|
|
||||||
|
Validates a QR token and records check-in. This endpoint is for authenticated staff or an authenticated scanning interface.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "opaque-check-in-token"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response `200 OK`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "checked_in",
|
||||||
|
"reservation_id": 123,
|
||||||
|
"performance_id": 10,
|
||||||
|
"party_size": 2,
|
||||||
|
"checked_in_at": "2026-05-15T19:55:00+02:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Status codes:
|
||||||
|
|
||||||
|
- `200 OK`: reservation checked in;
|
||||||
|
- `400 Bad Request`: token is missing or malformed;
|
||||||
|
- `401 Unauthorized`: staff authentication is missing;
|
||||||
|
- `403 Forbidden`: authenticated user cannot perform check-in;
|
||||||
|
- `404 Not Found`: token is unknown;
|
||||||
|
- `409 Conflict`: reservation is not confirmed or was already checked in;
|
||||||
|
- `410 Gone`: token is expired.
|
||||||
|
|
||||||
|
The response should include only the minimum information staff need to admit the party.
|
||||||
|
|
||||||
|
## Administration
|
||||||
|
|
||||||
|
The initial administration API is Django admin.
|
||||||
|
|
||||||
|
Admin paths:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /admin/
|
||||||
|
```
|
||||||
|
|
||||||
|
Admin users can manage shows, venues, performances, reservations, reservation tokens, and check-ins according to staff permissions.
|
||||||
|
|
||||||
|
Status codes:
|
||||||
|
|
||||||
|
- `302 Found`: unauthenticated browser redirected to admin login;
|
||||||
|
- `200 OK`: authenticated admin page;
|
||||||
|
- `403 Forbidden`: authenticated user lacks the required permission.
|
||||||
|
|
||||||
|
## Error Format
|
||||||
|
|
||||||
|
Use Django REST Framework's standard validation error format unless a project-specific envelope is introduced later.
|
||||||
|
|
||||||
|
Example `400 Bad Request`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": ["Enter a valid email address."],
|
||||||
|
"party_size": ["Ensure this value is greater than or equal to 1."]
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -1,13 +1,156 @@
|
|||||||
# Architecture
|
# Architecture
|
||||||
|
|
||||||
Describe the project architecture here.
|
AzioneLab is a public website for a theatre company with a small booking system for performances.
|
||||||
|
|
||||||
Include:
|
The architecture is intentionally simple: one Django backend, one Angular frontend, one PostgreSQL database, and nginx as the public reverse proxy. There is no Celery, Redis, message broker, or separate worker service at this stage.
|
||||||
|
|
||||||
- main components;
|
## Components
|
||||||
- runtime dependencies;
|
|
||||||
- data flow;
|
### Public frontend
|
||||||
- persistence;
|
|
||||||
- external integrations;
|
The public frontend is an Angular application using Angular Material.
|
||||||
- deployment topology;
|
|
||||||
- relevant ADRs.
|
Responsibilities:
|
||||||
|
|
||||||
|
- render descriptive pages for the theatre company;
|
||||||
|
- render the public list of shows;
|
||||||
|
- render public show and performance detail pages;
|
||||||
|
- provide the booking form for a selected performance;
|
||||||
|
- show reservation submission, confirmation, and check-in feedback states;
|
||||||
|
- call the backend through REST APIs.
|
||||||
|
|
||||||
|
The frontend must not calculate authoritative availability. It may display availability returned by the backend, but the backend remains responsible for final capacity validation.
|
||||||
|
|
||||||
|
### Backend API
|
||||||
|
|
||||||
|
The backend is a Django 5.2 LTS application using Django REST Framework.
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- expose public read APIs for shows, venues, and performances;
|
||||||
|
- expose public booking and reservation confirmation APIs;
|
||||||
|
- expose an authenticated check-in verification API;
|
||||||
|
- provide Django admin or an equivalent authenticated administration area;
|
||||||
|
- store reservations, tokens, and check-in state;
|
||||||
|
- calculate performance availability server-side;
|
||||||
|
- send reservation confirmation emails;
|
||||||
|
- generate QR codes after reservation confirmation.
|
||||||
|
|
||||||
|
The backend runs with gunicorn in production.
|
||||||
|
|
||||||
|
### Administration area
|
||||||
|
|
||||||
|
The first administration area should use Django admin unless a custom Angular admin becomes necessary later.
|
||||||
|
|
||||||
|
Administrators can manage:
|
||||||
|
|
||||||
|
- shows;
|
||||||
|
- venues;
|
||||||
|
- performances;
|
||||||
|
- total room capacity;
|
||||||
|
- manually occupied seats;
|
||||||
|
- optional additional seats available during booking;
|
||||||
|
- reservation status and check-in records when operationally necessary.
|
||||||
|
|
||||||
|
Admin functionality must require authenticated staff access.
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
PostgreSQL is the system of record.
|
||||||
|
|
||||||
|
It stores:
|
||||||
|
|
||||||
|
- show and venue content;
|
||||||
|
- performance scheduling and capacity configuration;
|
||||||
|
- reservations and explicit reservation status;
|
||||||
|
- reservation tokens used for confirmation and QR verification;
|
||||||
|
- check-in records.
|
||||||
|
|
||||||
|
Capacity checks must happen inside database transactions to avoid overbooking when multiple users book at the same time.
|
||||||
|
|
||||||
|
### Email
|
||||||
|
|
||||||
|
The backend sends transactional emails for:
|
||||||
|
|
||||||
|
- reservation confirmation link after booking submission;
|
||||||
|
- optional confirmation success email containing the QR code or QR verification link.
|
||||||
|
|
||||||
|
Email delivery can use Django's email backend configuration. No asynchronous email worker is required initially; failures should be logged without exposing tokens or personal data.
|
||||||
|
|
||||||
|
### QR code generation
|
||||||
|
|
||||||
|
The backend generates QR codes using a small Python library such as `qrcode` or `segno`.
|
||||||
|
|
||||||
|
QR codes must contain only an opaque token or verification URL. They must not contain names, email addresses, phone numbers, notes, or other personal data.
|
||||||
|
|
||||||
|
### nginx
|
||||||
|
|
||||||
|
nginx is the public entry point.
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- terminate HTTP traffic, and TLS when configured;
|
||||||
|
- serve the built Angular static assets;
|
||||||
|
- reverse proxy API and admin requests to gunicorn;
|
||||||
|
- serve static and media files according to the deployment configuration.
|
||||||
|
|
||||||
|
## Runtime Dependencies
|
||||||
|
|
||||||
|
Required runtime dependencies:
|
||||||
|
|
||||||
|
- Python;
|
||||||
|
- Django 5.2 LTS;
|
||||||
|
- Django REST Framework;
|
||||||
|
- gunicorn;
|
||||||
|
- PostgreSQL;
|
||||||
|
- Angular;
|
||||||
|
- Angular Material;
|
||||||
|
- nginx;
|
||||||
|
- Docker Compose;
|
||||||
|
- a Python QR code library such as `qrcode` or `segno`;
|
||||||
|
- an SMTP-compatible email provider or relay.
|
||||||
|
|
||||||
|
Not included at this stage:
|
||||||
|
|
||||||
|
- Celery;
|
||||||
|
- Redis;
|
||||||
|
- background workers;
|
||||||
|
- separate search, cache, or queue services.
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
1. A visitor opens the website through nginx.
|
||||||
|
2. nginx serves the Angular frontend.
|
||||||
|
3. The frontend calls public backend API endpoints for shows and performances.
|
||||||
|
4. The visitor submits a booking form for a specific performance.
|
||||||
|
5. The backend validates input, checks capacity server-side, creates a pending reservation, creates a confirmation token, and sends a confirmation email.
|
||||||
|
6. The visitor opens the confirmation link.
|
||||||
|
7. The backend validates the token, confirms the reservation if capacity is still available, and generates a QR code token or QR code image.
|
||||||
|
8. The visitor presents the QR code at the venue using a smartphone or printed copy.
|
||||||
|
9. Staff scans the QR code.
|
||||||
|
10. The backend validates the token and records a check-in if the reservation is confirmed and not already checked in.
|
||||||
|
|
||||||
|
## Deployment Topology
|
||||||
|
|
||||||
|
The initial deployment uses Docker Compose with these services:
|
||||||
|
|
||||||
|
- `nginx`: public reverse proxy and static frontend server;
|
||||||
|
- `frontend`: Angular build stage or static asset build source;
|
||||||
|
- `backend`: Django application served by gunicorn;
|
||||||
|
- `db`: PostgreSQL database.
|
||||||
|
|
||||||
|
Only nginx should be publicly exposed. The backend and database should be reachable only on the internal Compose network.
|
||||||
|
|
||||||
|
## Architectural Constraints
|
||||||
|
|
||||||
|
- Keep the booking workflow synchronous and explicit.
|
||||||
|
- Keep all capacity validation on the backend.
|
||||||
|
- Store reservation status explicitly.
|
||||||
|
- Use opaque, random, non-guessable tokens.
|
||||||
|
- Do not place personal data in QR codes.
|
||||||
|
- Avoid optional infrastructure until the project needs it.
|
||||||
|
- Prefer Django admin for internal management before building custom admin UI.
|
||||||
|
|
||||||
|
## Relevant ADRs
|
||||||
|
|
||||||
|
No ADRs are recorded yet. The technology stack and initial constraints are documented here from the project request.
|
||||||
|
|||||||
136
docs/booking-flow.md
Normal file
136
docs/booking-flow.md
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
# Booking Flow
|
||||||
|
|
||||||
|
This document describes the full public booking, confirmation, QR generation, and entrance check-in flow.
|
||||||
|
|
||||||
|
## 1. Public Discovery
|
||||||
|
|
||||||
|
1. A visitor opens the public website.
|
||||||
|
2. The Angular frontend requests published shows and upcoming performances from the backend.
|
||||||
|
3. The visitor opens a show detail page.
|
||||||
|
4. The frontend displays upcoming public performances and availability returned by the backend.
|
||||||
|
|
||||||
|
Availability shown to visitors is informational. The backend recalculates availability when reservation state changes.
|
||||||
|
|
||||||
|
## 2. Booking Submission
|
||||||
|
|
||||||
|
1. The visitor selects a performance.
|
||||||
|
2. The frontend displays the booking form for that performance.
|
||||||
|
3. The visitor enters contact details and party size.
|
||||||
|
4. The frontend submits the booking request to the backend.
|
||||||
|
5. The backend validates:
|
||||||
|
- required fields;
|
||||||
|
- email format;
|
||||||
|
- positive party size;
|
||||||
|
- performance exists and booking is open;
|
||||||
|
- requested seats do not exceed currently available seats.
|
||||||
|
6. The backend creates a `pending` reservation.
|
||||||
|
7. The backend creates a random opaque confirmation token.
|
||||||
|
8. The backend sends an email with a confirmation link.
|
||||||
|
9. The frontend tells the visitor to check their email.
|
||||||
|
|
||||||
|
The reservation is not confirmed at this stage.
|
||||||
|
|
||||||
|
## 3. Email Confirmation
|
||||||
|
|
||||||
|
1. The visitor opens the confirmation link from the email.
|
||||||
|
2. The frontend or backend submits the confirmation token to the confirmation endpoint.
|
||||||
|
3. The backend validates that the token:
|
||||||
|
- exists;
|
||||||
|
- has the `confirmation` purpose;
|
||||||
|
- belongs to a pending reservation;
|
||||||
|
- has not expired;
|
||||||
|
- has not already been used.
|
||||||
|
4. The backend starts a database transaction.
|
||||||
|
5. The backend locks the related performance row.
|
||||||
|
6. The backend recalculates confirmed reservations for the performance.
|
||||||
|
7. The backend confirms the reservation only if enough seats remain.
|
||||||
|
8. The backend marks the confirmation token as used.
|
||||||
|
9. The backend creates a QR verification token.
|
||||||
|
10. The backend generates a QR code containing the opaque QR token or a verification URL.
|
||||||
|
11. The backend returns or sends the QR code to the visitor.
|
||||||
|
|
||||||
|
If there is no longer enough capacity, the backend must not confirm the reservation.
|
||||||
|
|
||||||
|
## 4. QR Code Delivery
|
||||||
|
|
||||||
|
After confirmation, the visitor receives access to a QR code.
|
||||||
|
|
||||||
|
The QR code may be delivered as:
|
||||||
|
|
||||||
|
- an image displayed on the confirmation page;
|
||||||
|
- an image or link in a confirmation email;
|
||||||
|
- a printable page linked from the confirmation result.
|
||||||
|
|
||||||
|
The QR code must be usable from a smartphone screen or printed copy.
|
||||||
|
|
||||||
|
The QR code must contain:
|
||||||
|
|
||||||
|
- an opaque check-in token; or
|
||||||
|
- a verification URL containing an opaque token.
|
||||||
|
|
||||||
|
The QR code must not contain:
|
||||||
|
|
||||||
|
- visitor name;
|
||||||
|
- email address;
|
||||||
|
- phone number;
|
||||||
|
- notes;
|
||||||
|
- party size if avoidable;
|
||||||
|
- any other personal data.
|
||||||
|
|
||||||
|
## 5. Entrance Check-In
|
||||||
|
|
||||||
|
1. Staff opens the authenticated check-in interface.
|
||||||
|
2. Staff scans the visitor's QR code.
|
||||||
|
3. The frontend submits the scanned token to the backend.
|
||||||
|
4. The backend validates:
|
||||||
|
- staff authentication and permission;
|
||||||
|
- token exists and has the `check_in` purpose;
|
||||||
|
- reservation exists;
|
||||||
|
- reservation is confirmed;
|
||||||
|
- token is valid for the performance check-in window if such a window is configured;
|
||||||
|
- reservation has not already been checked in.
|
||||||
|
5. The backend creates a `CheckIn` record.
|
||||||
|
6. The backend returns a successful check-in response.
|
||||||
|
7. Staff admits the visitor party.
|
||||||
|
|
||||||
|
## Duplicate Check-In
|
||||||
|
|
||||||
|
If the same QR code is scanned again:
|
||||||
|
|
||||||
|
1. The backend detects an existing check-in for the reservation.
|
||||||
|
2. The backend returns `409 Conflict`.
|
||||||
|
3. The backend does not create a second check-in.
|
||||||
|
|
||||||
|
The response should be clear enough for staff to understand that the reservation was already used.
|
||||||
|
|
||||||
|
## Capacity Handling
|
||||||
|
|
||||||
|
Capacity is calculated as:
|
||||||
|
|
||||||
|
```text
|
||||||
|
room capacity + additional seats - manually occupied seats - confirmed reservations
|
||||||
|
```
|
||||||
|
|
||||||
|
Only confirmed reservations consume capacity. Pending reservations represent interest, not a guaranteed seat.
|
||||||
|
|
||||||
|
To avoid overbooking, final confirmation must be transactional:
|
||||||
|
|
||||||
|
1. lock the performance;
|
||||||
|
2. recalculate confirmed seats;
|
||||||
|
3. compare availability with requested party size;
|
||||||
|
4. confirm only when seats are available.
|
||||||
|
|
||||||
|
## Failure States
|
||||||
|
|
||||||
|
Expected failure states include:
|
||||||
|
|
||||||
|
- invalid booking input;
|
||||||
|
- booking disabled for the performance;
|
||||||
|
- no capacity remaining;
|
||||||
|
- confirmation token expired;
|
||||||
|
- confirmation token already used;
|
||||||
|
- QR token invalid;
|
||||||
|
- reservation not confirmed;
|
||||||
|
- reservation already checked in.
|
||||||
|
|
||||||
|
Each failure should return a clear status code and a concise user-facing message.
|
||||||
@@ -1,15 +1,167 @@
|
|||||||
# Deployment
|
# Deployment
|
||||||
|
|
||||||
Describe how this project is deployed.
|
AzioneLab should deploy with a simple Docker Compose topology:
|
||||||
|
|
||||||
Include:
|
- `nginx`: public reverse proxy and static frontend server;
|
||||||
|
- `frontend`: Angular build source or build stage for static assets;
|
||||||
|
- `backend`: Django 5.2 LTS application served by gunicorn;
|
||||||
|
- `db`: PostgreSQL database.
|
||||||
|
|
||||||
- environments;
|
Only nginx should expose public ports. The backend and database should stay on the internal Compose network.
|
||||||
- Docker/Compose usage;
|
|
||||||
- required configuration;
|
## Services
|
||||||
- secrets handling;
|
|
||||||
- exposed ports;
|
### nginx
|
||||||
- volumes;
|
|
||||||
- networks;
|
Responsibilities:
|
||||||
- deployment commands;
|
|
||||||
- rollback procedure.
|
- listen on public HTTP and HTTPS ports;
|
||||||
|
- serve built Angular files;
|
||||||
|
- proxy `/api/` and `/admin/` requests to the backend;
|
||||||
|
- serve static and media files according to the selected storage layout;
|
||||||
|
- apply request size and timeout limits appropriate for booking and admin usage.
|
||||||
|
|
||||||
|
Public ports:
|
||||||
|
|
||||||
|
- `80` for HTTP;
|
||||||
|
- `443` for HTTPS in production.
|
||||||
|
|
||||||
|
### frontend
|
||||||
|
|
||||||
|
The frontend is an Angular application using Angular Material.
|
||||||
|
|
||||||
|
Deployment options:
|
||||||
|
|
||||||
|
- build the Angular app in a Docker build stage and copy static files into the nginx image;
|
||||||
|
- or run a one-shot build container that writes static files to a shared volume consumed by nginx.
|
||||||
|
|
||||||
|
The first option is preferred for a simple production deployment because nginx can serve immutable built assets without a long-running Node process.
|
||||||
|
|
||||||
|
### backend
|
||||||
|
|
||||||
|
The backend is a Django application served by gunicorn.
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- REST API;
|
||||||
|
- Django admin;
|
||||||
|
- booking, confirmation, QR generation, and check-in logic;
|
||||||
|
- transactional capacity validation;
|
||||||
|
- email sending.
|
||||||
|
|
||||||
|
The backend should run database migrations before or during deployment through an explicit operational command, not as hidden startup magic unless that choice is documented later.
|
||||||
|
|
||||||
|
### db
|
||||||
|
|
||||||
|
PostgreSQL is the only database service.
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- persistent application data;
|
||||||
|
- reservation and check-in state;
|
||||||
|
- transactional capacity enforcement.
|
||||||
|
|
||||||
|
Use a named Docker volume for database data.
|
||||||
|
|
||||||
|
## Networks
|
||||||
|
|
||||||
|
Recommended Compose networks:
|
||||||
|
|
||||||
|
- `public`: nginx-facing network when needed;
|
||||||
|
- `internal`: private network for nginx, backend, and db communication.
|
||||||
|
|
||||||
|
The database should not be published to the host in production.
|
||||||
|
|
||||||
|
## Volumes
|
||||||
|
|
||||||
|
Recommended volumes:
|
||||||
|
|
||||||
|
- `postgres_data`: PostgreSQL data directory;
|
||||||
|
- `media`: uploaded media and generated QR assets if stored on disk;
|
||||||
|
- `static`: collected Django static files if served by nginx from a shared volume.
|
||||||
|
|
||||||
|
Generated QR codes may also be generated on demand instead of stored as files. If stored, they must not reveal personal data and access must remain controlled.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Required backend configuration:
|
||||||
|
|
||||||
|
- `DJANGO_SECRET_KEY`;
|
||||||
|
- `DJANGO_ALLOWED_HOSTS`;
|
||||||
|
- `DJANGO_CSRF_TRUSTED_ORIGINS`;
|
||||||
|
- `DATABASE_URL` or equivalent database settings;
|
||||||
|
- email host, port, username, password, TLS settings, and sender address;
|
||||||
|
- public site URL used to build confirmation and QR verification links.
|
||||||
|
|
||||||
|
Required database configuration:
|
||||||
|
|
||||||
|
- database name;
|
||||||
|
- database user;
|
||||||
|
- database password;
|
||||||
|
- data volume path.
|
||||||
|
|
||||||
|
Required nginx configuration:
|
||||||
|
|
||||||
|
- upstream backend service name and port;
|
||||||
|
- static frontend root;
|
||||||
|
- proxy rules for `/api/` and `/admin/`;
|
||||||
|
- TLS certificate paths for production.
|
||||||
|
|
||||||
|
Secrets must be provided through deployment-managed environment variables, Docker secrets, or another secret manager. Do not commit real secret values.
|
||||||
|
|
||||||
|
## Example Request Routing
|
||||||
|
|
||||||
|
```text
|
||||||
|
Visitor browser
|
||||||
|
-> nginx
|
||||||
|
-> Angular static files
|
||||||
|
-> /api/ requests proxied to backend:gunicorn
|
||||||
|
-> /admin/ requests proxied to backend:gunicorn
|
||||||
|
Backend
|
||||||
|
-> PostgreSQL
|
||||||
|
-> SMTP provider
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment Commands
|
||||||
|
|
||||||
|
The exact commands will be finalized when application code and Compose files are added.
|
||||||
|
|
||||||
|
Expected production-style flow:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose build
|
||||||
|
docker compose run --rm backend python manage.py migrate
|
||||||
|
docker compose run --rm backend python manage.py collectstatic --noinput
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected validation commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose config
|
||||||
|
docker compose run --rm backend python manage.py check --deploy
|
||||||
|
docker compose run --rm backend python manage.py test
|
||||||
|
```
|
||||||
|
|
||||||
|
The repository does not yet define the canonical Docker-based test command.
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
Rollback should be designed around immutable images and database backups.
|
||||||
|
|
||||||
|
Basic rollback steps:
|
||||||
|
|
||||||
|
1. identify the previous known-good image tags or Git commit;
|
||||||
|
2. stop the current Compose stack;
|
||||||
|
3. deploy the previous image tags or commit;
|
||||||
|
4. restore the database from backup only if a migration or data change requires it;
|
||||||
|
5. run smoke checks for public pages, booking creation, confirmation, and check-in.
|
||||||
|
|
||||||
|
Database rollback needs special care once migrations exist. Down migrations or backup restore procedures should be documented before production use.
|
||||||
|
|
||||||
|
## Operational Notes
|
||||||
|
|
||||||
|
- Configure database backups before accepting real bookings.
|
||||||
|
- Monitor backend errors, email delivery failures, and check-in failures.
|
||||||
|
- Keep container images explicitly versioned; do not use `latest` tags.
|
||||||
|
- Keep the system small until operational needs justify additional services.
|
||||||
|
|||||||
186
docs/domain-model.md
Normal file
186
docs/domain-model.md
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
# Domain Model
|
||||||
|
|
||||||
|
This document describes the core domain concepts for AzioneLab's theatre website and booking system.
|
||||||
|
|
||||||
|
## Show
|
||||||
|
|
||||||
|
A show is a theatrical production presented by the company.
|
||||||
|
|
||||||
|
Suggested fields:
|
||||||
|
|
||||||
|
- `id`: internal identifier;
|
||||||
|
- `title`: public show title;
|
||||||
|
- `slug`: stable public URL identifier;
|
||||||
|
- `summary`: short public description;
|
||||||
|
- `description`: full public description;
|
||||||
|
- `poster_image`: optional public image;
|
||||||
|
- `is_published`: controls public visibility;
|
||||||
|
- `created_at`: creation timestamp;
|
||||||
|
- `updated_at`: last update timestamp.
|
||||||
|
|
||||||
|
Relationships:
|
||||||
|
|
||||||
|
- one show can have many performances.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- unpublished shows are not listed publicly;
|
||||||
|
- a public show detail page may include only published upcoming performances.
|
||||||
|
|
||||||
|
## Venue
|
||||||
|
|
||||||
|
A venue is the place where a performance happens.
|
||||||
|
|
||||||
|
Suggested fields:
|
||||||
|
|
||||||
|
- `id`: internal identifier;
|
||||||
|
- `name`: public venue name;
|
||||||
|
- `slug`: stable public URL identifier;
|
||||||
|
- `address`: public address;
|
||||||
|
- `city`: venue city;
|
||||||
|
- `notes`: optional public or internal venue notes;
|
||||||
|
- `created_at`: creation timestamp;
|
||||||
|
- `updated_at`: last update timestamp.
|
||||||
|
|
||||||
|
Relationships:
|
||||||
|
|
||||||
|
- one venue can host many performances.
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
A performance is a scheduled presentation of one show at one venue.
|
||||||
|
|
||||||
|
Suggested fields:
|
||||||
|
|
||||||
|
- `id`: internal identifier;
|
||||||
|
- `show`: required reference to `Show`;
|
||||||
|
- `venue`: required reference to `Venue`;
|
||||||
|
- `starts_at`: performance date and time;
|
||||||
|
- `room_capacity`: configured total room capacity;
|
||||||
|
- `manually_occupied_seats`: seats unavailable because they are reserved outside the public booking system;
|
||||||
|
- `additional_seats`: optional extra seats made available during booking;
|
||||||
|
- `is_booking_enabled`: controls whether public booking is open;
|
||||||
|
- `created_at`: creation timestamp;
|
||||||
|
- `updated_at`: last update timestamp.
|
||||||
|
|
||||||
|
Relationships:
|
||||||
|
|
||||||
|
- each performance belongs to one show;
|
||||||
|
- each performance belongs to one venue;
|
||||||
|
- one performance can have many reservations.
|
||||||
|
|
||||||
|
Availability formula:
|
||||||
|
|
||||||
|
```text
|
||||||
|
available seats =
|
||||||
|
room capacity
|
||||||
|
+ additional seats
|
||||||
|
- manually occupied seats
|
||||||
|
- confirmed reservations
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- capacity values must not be negative;
|
||||||
|
- `manually_occupied_seats` must not exceed `room_capacity + additional_seats`;
|
||||||
|
- only confirmed reservations reduce public availability;
|
||||||
|
- pending reservations do not guarantee a seat until confirmation;
|
||||||
|
- final capacity validation must happen server-side when confirming a reservation;
|
||||||
|
- changes to capacity configuration must preserve existing confirmed reservations.
|
||||||
|
|
||||||
|
## Reservation
|
||||||
|
|
||||||
|
A reservation is a booking request for a specific performance.
|
||||||
|
|
||||||
|
Suggested fields:
|
||||||
|
|
||||||
|
- `id`: internal identifier;
|
||||||
|
- `performance`: required reference to `Performance`;
|
||||||
|
- `status`: explicit status such as `pending`, `confirmed`, `cancelled`, or `expired`;
|
||||||
|
- `name`: reservation contact name;
|
||||||
|
- `email`: reservation contact email;
|
||||||
|
- `phone`: optional reservation contact phone;
|
||||||
|
- `party_size`: number of requested seats;
|
||||||
|
- `notes`: optional visitor note;
|
||||||
|
- `confirmed_at`: timestamp set when the reservation is confirmed;
|
||||||
|
- `qr_code_generated_at`: timestamp set when the QR code is generated;
|
||||||
|
- `created_at`: creation timestamp;
|
||||||
|
- `updated_at`: last update timestamp.
|
||||||
|
|
||||||
|
Relationships:
|
||||||
|
|
||||||
|
- each reservation belongs to one performance;
|
||||||
|
- one reservation can have one or more reservation tokens for different purposes;
|
||||||
|
- one confirmed reservation can have at most one successful check-in.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- a new reservation starts as `pending`;
|
||||||
|
- a reservation becomes `confirmed` only through a valid confirmation token;
|
||||||
|
- a confirmed reservation receives a QR code;
|
||||||
|
- `party_size` must be positive;
|
||||||
|
- the backend must reject confirmation if the requested seats would exceed availability;
|
||||||
|
- personal data must never be stored in QR codes.
|
||||||
|
|
||||||
|
## ReservationToken
|
||||||
|
|
||||||
|
A reservation token is an opaque token used for confirmation or QR verification.
|
||||||
|
|
||||||
|
Suggested fields:
|
||||||
|
|
||||||
|
- `id`: internal identifier;
|
||||||
|
- `reservation`: required reference to `Reservation`;
|
||||||
|
- `purpose`: token purpose, such as `confirmation` or `check_in`;
|
||||||
|
- `token_hash`: server-side hash of the opaque token;
|
||||||
|
- `expires_at`: optional expiration timestamp;
|
||||||
|
- `used_at`: timestamp set when a one-time token is consumed;
|
||||||
|
- `created_at`: creation timestamp.
|
||||||
|
|
||||||
|
Relationships:
|
||||||
|
|
||||||
|
- each token belongs to one reservation.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- raw tokens must be random, non-guessable, and generated with a cryptographically secure generator;
|
||||||
|
- store only a hash of the token when practical;
|
||||||
|
- confirmation tokens should be single use;
|
||||||
|
- QR tokens may remain valid until the performance check-in window closes;
|
||||||
|
- tokens must not encode personal data.
|
||||||
|
|
||||||
|
## CheckIn
|
||||||
|
|
||||||
|
A check-in records entrance validation for a confirmed reservation.
|
||||||
|
|
||||||
|
Suggested fields:
|
||||||
|
|
||||||
|
- `id`: internal identifier;
|
||||||
|
- `reservation`: required unique reference to `Reservation`;
|
||||||
|
- `checked_in_at`: timestamp of successful check-in;
|
||||||
|
- `checked_in_by`: optional authenticated staff user reference;
|
||||||
|
- `source`: optional source such as `qr_scan` or `manual`;
|
||||||
|
- `created_at`: creation timestamp.
|
||||||
|
|
||||||
|
Relationships:
|
||||||
|
|
||||||
|
- each check-in belongs to one reservation;
|
||||||
|
- a reservation can have at most one successful check-in.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- only confirmed reservations can be checked in;
|
||||||
|
- a reservation cannot be checked in twice;
|
||||||
|
- failed check-in attempts should return a clear status without changing successful check-in state;
|
||||||
|
- check-in must not expose unnecessary personal data to scanning clients.
|
||||||
|
|
||||||
|
## Anti-Overbooking Rule
|
||||||
|
|
||||||
|
The backend must enforce capacity inside a transaction when confirming reservations.
|
||||||
|
|
||||||
|
Recommended approach:
|
||||||
|
|
||||||
|
- lock the relevant `Performance` row during confirmation;
|
||||||
|
- count confirmed seats for that performance;
|
||||||
|
- compare requested seats with available seats;
|
||||||
|
- confirm only if enough seats remain;
|
||||||
|
- otherwise leave the reservation pending or mark it as expired/rejected according to the future product decision.
|
||||||
151
docs/security-notes.md
Normal file
151
docs/security-notes.md
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# Security Notes
|
||||||
|
|
||||||
|
This document records security assumptions and controls for AzioneLab's initial architecture.
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
|
||||||
|
- The public website is readable by anonymous visitors.
|
||||||
|
- Booking endpoints are public but must validate input strictly.
|
||||||
|
- Administration and check-in functions require authenticated staff users.
|
||||||
|
- HTTPS is expected in production.
|
||||||
|
- PostgreSQL is reachable only from trusted application containers.
|
||||||
|
- Email is sent through a configured SMTP provider or relay.
|
||||||
|
|
||||||
|
## Personal Data
|
||||||
|
|
||||||
|
Reservations contain personal data such as name, email address, optional phone number, and optional notes.
|
||||||
|
|
||||||
|
Controls:
|
||||||
|
|
||||||
|
- collect only data required to manage the reservation;
|
||||||
|
- do not expose reservation personal data through public APIs;
|
||||||
|
- do not include personal data in QR codes;
|
||||||
|
- avoid logging request bodies from booking and confirmation endpoints;
|
||||||
|
- avoid logging raw tokens;
|
||||||
|
- restrict admin access to staff users who need it.
|
||||||
|
|
||||||
|
## Token Handling
|
||||||
|
|
||||||
|
Reservation tokens are used for email confirmation and QR verification.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- generate tokens with a cryptographically secure random generator;
|
||||||
|
- make tokens opaque and non-guessable;
|
||||||
|
- do not encode personal data in tokens;
|
||||||
|
- store token hashes when practical;
|
||||||
|
- treat raw tokens as secrets;
|
||||||
|
- mark one-time confirmation tokens as used after successful confirmation;
|
||||||
|
- expire confirmation tokens after a reasonable period;
|
||||||
|
- keep QR tokens valid only for the intended performance and check-in period where practical.
|
||||||
|
|
||||||
|
## QR Codes
|
||||||
|
|
||||||
|
QR codes must contain only:
|
||||||
|
|
||||||
|
- an opaque verification token; or
|
||||||
|
- a verification URL containing an opaque token.
|
||||||
|
|
||||||
|
QR codes must not contain:
|
||||||
|
|
||||||
|
- name;
|
||||||
|
- email;
|
||||||
|
- phone;
|
||||||
|
- notes;
|
||||||
|
- reservation metadata that identifies the visitor.
|
||||||
|
|
||||||
|
The check-in endpoint resolves the token server-side and returns only the minimum information staff need.
|
||||||
|
|
||||||
|
## Authentication and Authorization
|
||||||
|
|
||||||
|
Required controls:
|
||||||
|
|
||||||
|
- Django admin requires authenticated staff accounts;
|
||||||
|
- check-in verification requires authenticated staff or an authenticated scanning client;
|
||||||
|
- staff permissions should separate content management from operational check-in when practical;
|
||||||
|
- public APIs must not allow clients to set protected fields such as reservation status, token values, or check-in state.
|
||||||
|
|
||||||
|
## Input Validation
|
||||||
|
|
||||||
|
Public booking endpoints must validate:
|
||||||
|
|
||||||
|
- required fields;
|
||||||
|
- email format;
|
||||||
|
- maximum field lengths;
|
||||||
|
- positive party size;
|
||||||
|
- booking status for the selected performance;
|
||||||
|
- capacity availability;
|
||||||
|
- unexpected fields.
|
||||||
|
|
||||||
|
Validation must happen server-side even when the frontend also validates input.
|
||||||
|
|
||||||
|
## Anti-Overbooking
|
||||||
|
|
||||||
|
The backend must enforce capacity server-side.
|
||||||
|
|
||||||
|
Capacity calculation:
|
||||||
|
|
||||||
|
```text
|
||||||
|
available seats =
|
||||||
|
room capacity
|
||||||
|
+ additional seats
|
||||||
|
- manually occupied seats
|
||||||
|
- confirmed reservations
|
||||||
|
```
|
||||||
|
|
||||||
|
Controls:
|
||||||
|
|
||||||
|
- use database transactions for confirmation;
|
||||||
|
- lock the performance row or use an equivalent consistency control;
|
||||||
|
- recalculate availability inside the transaction;
|
||||||
|
- confirm the reservation only when enough seats remain;
|
||||||
|
- ensure duplicate confirmation requests are idempotent or rejected safely;
|
||||||
|
- do not rely on frontend availability values.
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
|
||||||
|
Secrets must not be committed to the repository.
|
||||||
|
|
||||||
|
Expected secret configuration:
|
||||||
|
|
||||||
|
- Django `SECRET_KEY`;
|
||||||
|
- database password;
|
||||||
|
- SMTP credentials;
|
||||||
|
- TLS private keys or certificate automation credentials, if used.
|
||||||
|
|
||||||
|
Use environment variables, Docker secrets, or deployment-managed secret injection. Documentation and example configuration should use placeholders only.
|
||||||
|
|
||||||
|
## Deployment Security
|
||||||
|
|
||||||
|
Deployment should follow least privilege:
|
||||||
|
|
||||||
|
- expose only nginx publicly;
|
||||||
|
- keep backend and database on an internal Docker network;
|
||||||
|
- avoid privileged containers;
|
||||||
|
- use explicit image tags rather than `latest`;
|
||||||
|
- persist PostgreSQL data in a named volume;
|
||||||
|
- configure TLS for production;
|
||||||
|
- serve static and media files without exposing private files.
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
Logs should help diagnose operational issues without exposing sensitive data.
|
||||||
|
|
||||||
|
Do not log:
|
||||||
|
|
||||||
|
- raw confirmation tokens;
|
||||||
|
- raw QR tokens;
|
||||||
|
- full booking payloads;
|
||||||
|
- passwords;
|
||||||
|
- session cookies;
|
||||||
|
- authorization headers;
|
||||||
|
- SMTP credentials.
|
||||||
|
|
||||||
|
## Residual Risks
|
||||||
|
|
||||||
|
Initial residual risks:
|
||||||
|
|
||||||
|
- synchronous email can make booking responses depend on SMTP availability;
|
||||||
|
- QR codes can be copied, so duplicate check-in prevention must be reliable;
|
||||||
|
- staff account compromise would expose admin and check-in functionality;
|
||||||
|
- retention and deletion rules for personal data still need a project policy.
|
||||||
@@ -1,16 +1,13 @@
|
|||||||
# Security
|
# Security
|
||||||
|
|
||||||
Describe security assumptions and controls.
|
AzioneLab security assumptions and controls are documented in [security-notes.md](security-notes.md).
|
||||||
|
|
||||||
Include:
|
The initial security model covers:
|
||||||
|
|
||||||
- authentication;
|
- public website access;
|
||||||
- authorization;
|
- authenticated administration and check-in;
|
||||||
- network exposure;
|
- reservation privacy;
|
||||||
- TLS/certificates;
|
- opaque token handling;
|
||||||
- secrets management;
|
- QR code privacy;
|
||||||
- logging of sensitive data;
|
- server-side capacity validation;
|
||||||
- container privileges;
|
- deployment and logging assumptions.
|
||||||
- filesystem permissions;
|
|
||||||
- dependency management;
|
|
||||||
- relevant ADRs.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user