Initial commit: NextGen PPR System
- FastAPI backend with JWT authentication - MySQL database with full schema - Docker Compose orchestration - CSV data import for 43,208 airports and 519,999 aircraft - Complete PPR management API - Modernized replacement for PHP-based system
This commit is contained in:
78
.gitignore
vendored
Normal file
78
.gitignore
vendored
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.sqlite
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env.local
|
||||||
|
.env.development
|
||||||
|
.env.test
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
# Coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
15
02-import-data.sql
Normal file
15
02-import-data.sql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
-- Import airports data from CSV
|
||||||
|
LOAD DATA INFILE '/var/lib/mysql-files/airports_data.csv'
|
||||||
|
INTO TABLE airports
|
||||||
|
FIELDS TERMINATED BY ','
|
||||||
|
OPTIONALLY ENCLOSED BY '"'
|
||||||
|
LINES TERMINATED BY '\n'
|
||||||
|
(icao, iata, name, country);
|
||||||
|
|
||||||
|
-- Import aircraft data from CSV
|
||||||
|
LOAD DATA INFILE '/var/lib/mysql-files/aircraft_data.csv'
|
||||||
|
INTO TABLE aircraft
|
||||||
|
FIELDS TERMINATED BY ','
|
||||||
|
OPTIONALLY ENCLOSED BY '"'
|
||||||
|
LINES TERMINATED BY '\n'
|
||||||
|
(icao24, registration, manufacturer_icao, type_code, manufacturer_name, model);
|
||||||
158
README.md
Normal file
158
README.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# NextGen PPR System
|
||||||
|
|
||||||
|
A modern, containerized Prior Permission Required (PPR) system for aircraft operations management.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- **Backend**: FastAPI with Python 3.11
|
||||||
|
- **Database**: MySQL 8.0
|
||||||
|
- **Cache**: Redis 7
|
||||||
|
- **Container**: Docker & Docker Compose
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🚀 **Modern API**: RESTful API with automatic OpenAPI documentation
|
||||||
|
- 🔐 **Authentication**: JWT-based authentication system
|
||||||
|
- 📊 **Real-time Updates**: WebSocket support for live tower updates
|
||||||
|
- 🗄️ **Self-contained**: Fully dockerized with local database
|
||||||
|
- 🔍 **Documentation**: Auto-generated API docs at `/docs`
|
||||||
|
- 🧪 **Testing**: Comprehensive test suite
|
||||||
|
- 📱 **Mobile Ready**: Responsive design for tower operations
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Docker and Docker Compose installed
|
||||||
|
|
||||||
|
### 1. Start the System
|
||||||
|
```bash
|
||||||
|
cd nextgen
|
||||||
|
./start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Access the Services
|
||||||
|
- **API Documentation**: http://localhost:8001/docs
|
||||||
|
- **API Base URL**: http://localhost:8001/api/v1
|
||||||
|
- **Database**: localhost:3307 (user: ppr_user, password: ppr_password123)
|
||||||
|
|
||||||
|
### 3. Default Login
|
||||||
|
- **Username**: admin
|
||||||
|
- **Password**: admin123
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
- `POST /api/v1/auth/login` - Login and get JWT token
|
||||||
|
|
||||||
|
### PPR Management
|
||||||
|
- `GET /api/v1/pprs` - List PPR records
|
||||||
|
- `POST /api/v1/pprs` - Create new PPR
|
||||||
|
- `GET /api/v1/pprs/{id}` - Get specific PPR
|
||||||
|
- `PUT /api/v1/pprs/{id}` - Update PPR
|
||||||
|
- `PATCH /api/v1/pprs/{id}/status` - Update PPR status
|
||||||
|
- `DELETE /api/v1/pprs/{id}` - Delete PPR
|
||||||
|
|
||||||
|
### Public Endpoints (No Auth Required)
|
||||||
|
- `GET /api/v1/public/arrivals` - Today's arrivals
|
||||||
|
- `GET /api/v1/public/departures` - Today's departures
|
||||||
|
|
||||||
|
### Real-time
|
||||||
|
- `WebSocket /ws/tower-updates` - Live updates for tower operations
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
pip install -r requirements.txt
|
||||||
|
uvicorn app.main:app --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Management
|
||||||
|
```bash
|
||||||
|
# Connect to database
|
||||||
|
docker exec -it ppr_nextgen_db mysql -u ppr_user -p ppr_nextgen
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose logs -f api
|
||||||
|
docker-compose logs -f db
|
||||||
|
|
||||||
|
# Restart services
|
||||||
|
docker-compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
pytest tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Key environment variables (configured in docker-compose.yml):
|
||||||
|
|
||||||
|
- `DB_HOST` - Database host
|
||||||
|
- `DB_USER` - Database username
|
||||||
|
- `DB_PASSWORD` - Database password
|
||||||
|
- `DB_NAME` - Database name
|
||||||
|
- `SECRET_KEY` - JWT secret key
|
||||||
|
- `ACCESS_TOKEN_EXPIRE_MINUTES` - Token expiration time
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
The system uses an improved version of the original schema with:
|
||||||
|
|
||||||
|
- **users** - User authentication
|
||||||
|
- **submitted** - PPR records with status tracking
|
||||||
|
- **journal** - Activity logs with foreign keys
|
||||||
|
- **airports** - Airport reference data
|
||||||
|
- **aircraft** - Aircraft registration database
|
||||||
|
|
||||||
|
## Data Migration
|
||||||
|
|
||||||
|
To migrate data from the old system:
|
||||||
|
|
||||||
|
1. Export data from the old database
|
||||||
|
2. Transform to new schema format
|
||||||
|
3. Import into the new system
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- JWT token authentication
|
||||||
|
- Password hashing with bcrypt
|
||||||
|
- Input validation with Pydantic
|
||||||
|
- SQL injection protection via SQLAlchemy ORM
|
||||||
|
- CORS configuration for frontend integration
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- Database connection pooling
|
||||||
|
- Indexed columns for fast queries
|
||||||
|
- Redis caching (ready for implementation)
|
||||||
|
- Async/await for non-blocking operations
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
Access logs and monitoring:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View API logs
|
||||||
|
docker-compose logs -f api
|
||||||
|
|
||||||
|
# View database logs
|
||||||
|
docker-compose logs -f db
|
||||||
|
|
||||||
|
# System health check
|
||||||
|
curl http://localhost:8001/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stopping the System
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
To remove volumes (database data):
|
||||||
|
```bash
|
||||||
|
docker-compose down -v
|
||||||
|
```
|
||||||
519999
aircraft_data.csv
Normal file
519999
aircraft_data.csv
Normal file
File diff suppressed because it is too large
Load Diff
43208
airports_data_clean.csv
Normal file
43208
airports_data_clean.csv
Normal file
File diff suppressed because it is too large
Load Diff
26
backend/Dockerfile
Normal file
26
backend/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
gcc \
|
||||||
|
default-libmysqlclient-dev \
|
||||||
|
pkg-config \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy requirements first for better caching
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
1
backend/app/__init__.py
Normal file
1
backend/app/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Empty __init__.py files to make Python packages
|
||||||
1
backend/app/api/__init__.py
Normal file
1
backend/app/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Empty __init__.py files to make Python packages
|
||||||
8
backend/app/api/api.py
Normal file
8
backend/app/api/api.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
from app.api.endpoints import auth, pprs, public
|
||||||
|
|
||||||
|
api_router = APIRouter()
|
||||||
|
|
||||||
|
api_router.include_router(auth.router, prefix="/auth", tags=["authentication"])
|
||||||
|
api_router.include_router(pprs.router, prefix="/pprs", tags=["pprs"])
|
||||||
|
api_router.include_router(public.router, prefix="/public", tags=["public"])
|
||||||
47
backend/app/api/deps.py
Normal file
47
backend/app/api/deps.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from typing import Generator
|
||||||
|
from fastapi import Depends, HTTPException, status
|
||||||
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.db.session import SessionLocal
|
||||||
|
from app.core.security import verify_token
|
||||||
|
from app.crud.crud_user import user as crud_user
|
||||||
|
|
||||||
|
security = HTTPBearer()
|
||||||
|
|
||||||
|
|
||||||
|
def get_db() -> Generator:
|
||||||
|
"""Database dependency"""
|
||||||
|
try:
|
||||||
|
db = SessionLocal()
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_current_user(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
credentials: HTTPAuthorizationCredentials = Depends(security)
|
||||||
|
):
|
||||||
|
"""Get current authenticated user"""
|
||||||
|
credentials_exception = HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Could not validate credentials",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
username = verify_token(credentials.credentials)
|
||||||
|
if username is None:
|
||||||
|
raise credentials_exception
|
||||||
|
|
||||||
|
user = crud_user.get_by_username(db, username=username)
|
||||||
|
if user is None:
|
||||||
|
raise credentials_exception
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_active_user(
|
||||||
|
current_user = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Get current active user (for future use if we add user status)"""
|
||||||
|
return current_user
|
||||||
1
backend/app/api/endpoints/__init__.py
Normal file
1
backend/app/api/endpoints/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Empty __init__.py files to make Python packages
|
||||||
41
backend/app/api/endpoints/auth.py
Normal file
41
backend/app/api/endpoints/auth.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.api.deps import get_db
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.core.security import create_access_token
|
||||||
|
from app.crud.crud_user import user as crud_user
|
||||||
|
from app.schemas.ppr import Token
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/login", response_model=Token)
|
||||||
|
async def login_for_access_token(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
form_data: OAuth2PasswordRequestForm = Depends()
|
||||||
|
):
|
||||||
|
"""OAuth2 compatible token login, get an access token for future requests"""
|
||||||
|
user = crud_user.authenticate(
|
||||||
|
db, username=form_data.username, password=form_data.password
|
||||||
|
)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Incorrect username or password",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
access_token_expires = timedelta(minutes=settings.access_token_expire_minutes)
|
||||||
|
access_token = create_access_token(
|
||||||
|
subject=user.username, expires_delta=access_token_expires
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"access_token": access_token, "token_type": "bearer"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/test-token")
|
||||||
|
async def test_token(current_user = Depends(get_db)):
|
||||||
|
"""Test access token"""
|
||||||
|
return current_user
|
||||||
162
backend/app/api/endpoints/pprs.py
Normal file
162
backend/app/api/endpoints/pprs.py
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from datetime import date
|
||||||
|
from app.api.deps import get_db, get_current_active_user
|
||||||
|
from app.crud.crud_ppr import ppr as crud_ppr
|
||||||
|
from app.schemas.ppr import PPR, PPRCreate, PPRUpdate, PPRStatus, PPRStatusUpdate
|
||||||
|
from app.models.ppr import User
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[PPR])
|
||||||
|
async def get_pprs(
|
||||||
|
request: Request,
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100,
|
||||||
|
status: Optional[PPRStatus] = None,
|
||||||
|
date_from: Optional[date] = None,
|
||||||
|
date_to: Optional[date] = None,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
"""Get PPR records with optional filtering"""
|
||||||
|
pprs = crud_ppr.get_multi(
|
||||||
|
db, skip=skip, limit=limit, status=status,
|
||||||
|
date_from=date_from, date_to=date_to
|
||||||
|
)
|
||||||
|
return pprs
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", response_model=PPR)
|
||||||
|
async def create_ppr(
|
||||||
|
request: Request,
|
||||||
|
ppr_in: PPRCreate,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
"""Create a new PPR record"""
|
||||||
|
ppr = crud_ppr.create(db, obj_in=ppr_in, created_by=current_user.username)
|
||||||
|
|
||||||
|
# Send real-time update via WebSocket
|
||||||
|
if hasattr(request.app.state, 'connection_manager'):
|
||||||
|
await request.app.state.connection_manager.broadcast({
|
||||||
|
"type": "ppr_created",
|
||||||
|
"data": {
|
||||||
|
"id": ppr.id,
|
||||||
|
"ac_reg": ppr.ac_reg,
|
||||||
|
"status": ppr.status.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return ppr
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{ppr_id}", response_model=PPR)
|
||||||
|
async def get_ppr(
|
||||||
|
ppr_id: int,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
"""Get a specific PPR record"""
|
||||||
|
ppr = crud_ppr.get(db, ppr_id=ppr_id)
|
||||||
|
if not ppr:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="PPR record not found"
|
||||||
|
)
|
||||||
|
return ppr
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{ppr_id}", response_model=PPR)
|
||||||
|
async def update_ppr(
|
||||||
|
request: Request,
|
||||||
|
ppr_id: int,
|
||||||
|
ppr_in: PPRUpdate,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
"""Update a PPR record"""
|
||||||
|
db_ppr = crud_ppr.get(db, ppr_id=ppr_id)
|
||||||
|
if not db_ppr:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="PPR record not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
ppr = crud_ppr.update(db, db_obj=db_ppr, obj_in=ppr_in)
|
||||||
|
|
||||||
|
# Send real-time update
|
||||||
|
if hasattr(request.app.state, 'connection_manager'):
|
||||||
|
await request.app.state.connection_manager.broadcast({
|
||||||
|
"type": "ppr_updated",
|
||||||
|
"data": {
|
||||||
|
"id": ppr.id,
|
||||||
|
"ac_reg": ppr.ac_reg,
|
||||||
|
"status": ppr.status.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return ppr
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{ppr_id}/status", response_model=PPR)
|
||||||
|
async def update_ppr_status(
|
||||||
|
request: Request,
|
||||||
|
ppr_id: int,
|
||||||
|
status_update: PPRStatusUpdate,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
"""Update PPR status (LANDED, DEPARTED, etc.)"""
|
||||||
|
ppr = crud_ppr.update_status(db, ppr_id=ppr_id, status=status_update.status)
|
||||||
|
if not ppr:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="PPR record not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Log the status change (you might want to create a journal entry here)
|
||||||
|
|
||||||
|
# Send real-time update
|
||||||
|
if hasattr(request.app.state, 'connection_manager'):
|
||||||
|
await request.app.state.connection_manager.broadcast({
|
||||||
|
"type": "status_update",
|
||||||
|
"data": {
|
||||||
|
"id": ppr.id,
|
||||||
|
"ac_reg": ppr.ac_reg,
|
||||||
|
"status": ppr.status.value,
|
||||||
|
"timestamp": ppr.landed_dt.isoformat() if ppr.landed_dt else None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return ppr
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{ppr_id}", response_model=PPR)
|
||||||
|
async def delete_ppr(
|
||||||
|
request: Request,
|
||||||
|
ppr_id: int,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
"""Delete (soft delete) a PPR record"""
|
||||||
|
ppr = crud_ppr.delete(db, ppr_id=ppr_id)
|
||||||
|
if not ppr:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="PPR record not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send real-time update
|
||||||
|
if hasattr(request.app.state, 'connection_manager'):
|
||||||
|
await request.app.state.connection_manager.broadcast({
|
||||||
|
"type": "ppr_deleted",
|
||||||
|
"data": {
|
||||||
|
"id": ppr.id,
|
||||||
|
"ac_reg": ppr.ac_reg
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return ppr
|
||||||
22
backend/app/api/endpoints/public.py
Normal file
22
backend/app/api/endpoints/public.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from typing import List
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.api.deps import get_db
|
||||||
|
from app.crud.crud_ppr import ppr as crud_ppr
|
||||||
|
from app.schemas.ppr import PPR
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/arrivals", response_model=List[PPR])
|
||||||
|
async def get_public_arrivals(db: Session = Depends(get_db)):
|
||||||
|
"""Get today's arrivals for public display"""
|
||||||
|
arrivals = crud_ppr.get_arrivals_today(db)
|
||||||
|
return arrivals
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/departures", response_model=List[PPR])
|
||||||
|
async def get_public_departures(db: Session = Depends(get_db)):
|
||||||
|
"""Get today's departures for public display"""
|
||||||
|
departures = crud_ppr.get_departures_today(db)
|
||||||
|
return departures
|
||||||
1
backend/app/core/__init__.py
Normal file
1
backend/app/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Empty __init__.py files to make Python packages
|
||||||
43
backend/app/core/config.py
Normal file
43
backend/app/core/config.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
# Database settings
|
||||||
|
db_host: str = "db" # Docker service name
|
||||||
|
db_user: str = "ppr_user"
|
||||||
|
db_password: str = "ppr_password123"
|
||||||
|
db_name: str = "ppr_nextgen"
|
||||||
|
db_port: int = 3306
|
||||||
|
|
||||||
|
# Security settings
|
||||||
|
secret_key: str = "your-secret-key-change-this-in-production"
|
||||||
|
algorithm: str = "HS256"
|
||||||
|
access_token_expire_minutes: int = 30
|
||||||
|
|
||||||
|
# Mail settings
|
||||||
|
mail_host: str = "send.one.com"
|
||||||
|
mail_port: int = 465
|
||||||
|
mail_username: str = "noreply@swansea-airport.wales"
|
||||||
|
mail_password: str = "SASAGoForward2155"
|
||||||
|
mail_from: str = "noreply@swansea-airport.wales"
|
||||||
|
mail_from_name: str = "Swansea Airport"
|
||||||
|
|
||||||
|
# Application settings
|
||||||
|
api_v1_str: str = "/api/v1"
|
||||||
|
project_name: str = "Airfield PPR API"
|
||||||
|
base_url: str = "https://pprdev.swansea-airport.wales"
|
||||||
|
|
||||||
|
# Redis settings (for future use)
|
||||||
|
redis_url: Optional[str] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
env_file = ".env"
|
||||||
|
case_sensitive = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def database_url(self) -> str:
|
||||||
|
return f"mysql+pymysql://{self.db_user}:{self.db_password}@{self.db_host}:{self.db_port}/{self.db_name}"
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
47
backend/app/core/security.py
Normal file
47
backend/app/core/security.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional, Union
|
||||||
|
from jose import JWTError, jwt
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
|
||||||
|
def create_access_token(
|
||||||
|
subject: Union[str, int], expires_delta: Optional[timedelta] = None
|
||||||
|
) -> str:
|
||||||
|
"""Create a new access token"""
|
||||||
|
if expires_delta:
|
||||||
|
expire = datetime.utcnow() + expires_delta
|
||||||
|
else:
|
||||||
|
expire = datetime.utcnow() + timedelta(
|
||||||
|
minutes=settings.access_token_expire_minutes
|
||||||
|
)
|
||||||
|
|
||||||
|
to_encode = {"exp": expire, "sub": str(subject)}
|
||||||
|
encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm)
|
||||||
|
return encoded_jwt
|
||||||
|
|
||||||
|
|
||||||
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||||
|
"""Verify a password against its hash"""
|
||||||
|
return pwd_context.verify(plain_password, hashed_password)
|
||||||
|
|
||||||
|
|
||||||
|
def get_password_hash(password: str) -> str:
|
||||||
|
"""Generate password hash"""
|
||||||
|
return pwd_context.hash(password)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_token(token: str) -> Optional[str]:
|
||||||
|
"""Verify JWT token and return username"""
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(
|
||||||
|
token, settings.secret_key, algorithms=[settings.algorithm]
|
||||||
|
)
|
||||||
|
username: str = payload.get("sub")
|
||||||
|
if username is None:
|
||||||
|
return None
|
||||||
|
return username
|
||||||
|
except JWTError:
|
||||||
|
return None
|
||||||
1
backend/app/crud/__init__.py
Normal file
1
backend/app/crud/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Empty __init__.py files to make Python packages
|
||||||
120
backend/app/crud/crud_ppr.py
Normal file
120
backend/app/crud/crud_ppr.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import and_, or_, func, desc
|
||||||
|
from datetime import date, datetime
|
||||||
|
from app.models.ppr import PPRRecord, PPRStatus
|
||||||
|
from app.schemas.ppr import PPRCreate, PPRUpdate
|
||||||
|
|
||||||
|
|
||||||
|
class CRUDPPR:
|
||||||
|
def get(self, db: Session, ppr_id: int) -> Optional[PPRRecord]:
|
||||||
|
return db.query(PPRRecord).filter(PPRRecord.id == ppr_id).first()
|
||||||
|
|
||||||
|
def get_multi(
|
||||||
|
self,
|
||||||
|
db: Session,
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100,
|
||||||
|
status: Optional[PPRStatus] = None,
|
||||||
|
date_from: Optional[date] = None,
|
||||||
|
date_to: Optional[date] = None
|
||||||
|
) -> List[PPRRecord]:
|
||||||
|
query = db.query(PPRRecord)
|
||||||
|
|
||||||
|
if status:
|
||||||
|
query = query.filter(PPRRecord.status == status)
|
||||||
|
|
||||||
|
if date_from:
|
||||||
|
query = query.filter(
|
||||||
|
or_(
|
||||||
|
func.date(PPRRecord.eta) >= date_from,
|
||||||
|
func.date(PPRRecord.etd) >= date_from
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if date_to:
|
||||||
|
query = query.filter(
|
||||||
|
or_(
|
||||||
|
func.date(PPRRecord.eta) <= date_to,
|
||||||
|
func.date(PPRRecord.etd) <= date_to
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return query.order_by(desc(PPRRecord.submitted_dt)).offset(skip).limit(limit).all()
|
||||||
|
|
||||||
|
def get_arrivals_today(self, db: Session) -> List[PPRRecord]:
|
||||||
|
"""Get today's arrivals"""
|
||||||
|
today = date.today()
|
||||||
|
return db.query(PPRRecord).filter(
|
||||||
|
and_(
|
||||||
|
func.date(PPRRecord.eta) == today,
|
||||||
|
PPRRecord.status == PPRStatus.NEW
|
||||||
|
)
|
||||||
|
).order_by(PPRRecord.eta).all()
|
||||||
|
|
||||||
|
def get_departures_today(self, db: Session) -> List[PPRRecord]:
|
||||||
|
"""Get today's departures"""
|
||||||
|
today = date.today()
|
||||||
|
return db.query(PPRRecord).filter(
|
||||||
|
and_(
|
||||||
|
func.date(PPRRecord.etd) == today,
|
||||||
|
PPRRecord.status == PPRStatus.LANDED
|
||||||
|
)
|
||||||
|
).order_by(PPRRecord.etd).all()
|
||||||
|
|
||||||
|
def create(self, db: Session, obj_in: PPRCreate, created_by: str) -> PPRRecord:
|
||||||
|
db_obj = PPRRecord(
|
||||||
|
**obj_in.dict(),
|
||||||
|
created_by=created_by,
|
||||||
|
status=PPRStatus.NEW
|
||||||
|
)
|
||||||
|
db.add(db_obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_obj)
|
||||||
|
return db_obj
|
||||||
|
|
||||||
|
def update(self, db: Session, db_obj: PPRRecord, obj_in: PPRUpdate) -> PPRRecord:
|
||||||
|
update_data = obj_in.dict(exclude_unset=True)
|
||||||
|
for field, value in update_data.items():
|
||||||
|
setattr(db_obj, field, value)
|
||||||
|
|
||||||
|
db.add(db_obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_obj)
|
||||||
|
return db_obj
|
||||||
|
|
||||||
|
def update_status(
|
||||||
|
self,
|
||||||
|
db: Session,
|
||||||
|
ppr_id: int,
|
||||||
|
status: PPRStatus
|
||||||
|
) -> Optional[PPRRecord]:
|
||||||
|
db_obj = self.get(db, ppr_id)
|
||||||
|
if not db_obj:
|
||||||
|
return None
|
||||||
|
|
||||||
|
db_obj.status = status
|
||||||
|
|
||||||
|
# Set timestamps based on status
|
||||||
|
if status == PPRStatus.LANDED:
|
||||||
|
db_obj.landed_dt = datetime.utcnow()
|
||||||
|
elif status == PPRStatus.DEPARTED:
|
||||||
|
db_obj.departed_dt = datetime.utcnow()
|
||||||
|
|
||||||
|
db.add(db_obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_obj)
|
||||||
|
return db_obj
|
||||||
|
|
||||||
|
def delete(self, db: Session, ppr_id: int) -> Optional[PPRRecord]:
|
||||||
|
db_obj = self.get(db, ppr_id)
|
||||||
|
if db_obj:
|
||||||
|
# Soft delete by setting status
|
||||||
|
db_obj.status = PPRStatus.DELETED
|
||||||
|
db.add(db_obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_obj)
|
||||||
|
return db_obj
|
||||||
|
|
||||||
|
|
||||||
|
ppr = CRUDPPR()
|
||||||
39
backend/app/crud/crud_user.py
Normal file
39
backend/app/crud/crud_user.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.models.ppr import User
|
||||||
|
from app.schemas.ppr import UserCreate
|
||||||
|
from app.core.security import get_password_hash, verify_password
|
||||||
|
|
||||||
|
|
||||||
|
class CRUDUser:
|
||||||
|
def get(self, db: Session, user_id: int) -> Optional[User]:
|
||||||
|
return db.query(User).filter(User.id == user_id).first()
|
||||||
|
|
||||||
|
def get_by_username(self, db: Session, username: str) -> Optional[User]:
|
||||||
|
return db.query(User).filter(User.username == username).first()
|
||||||
|
|
||||||
|
def create(self, db: Session, obj_in: UserCreate) -> User:
|
||||||
|
hashed_password = get_password_hash(obj_in.password)
|
||||||
|
db_obj = User(
|
||||||
|
username=obj_in.username,
|
||||||
|
password=hashed_password
|
||||||
|
)
|
||||||
|
db.add(db_obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_obj)
|
||||||
|
return db_obj
|
||||||
|
|
||||||
|
def authenticate(self, db: Session, username: str, password: str) -> Optional[User]:
|
||||||
|
user = self.get_by_username(db, username=username)
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
|
if not verify_password(password, user.password):
|
||||||
|
return None
|
||||||
|
return user
|
||||||
|
|
||||||
|
def is_active(self, user: User) -> bool:
|
||||||
|
# For future use if we add user status
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
user = CRUDUser()
|
||||||
1
backend/app/db/__init__.py
Normal file
1
backend/app/db/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Empty __init__.py files to make Python packages
|
||||||
15
backend/app/db/session.py
Normal file
15
backend/app/db/session.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
engine = create_engine(
|
||||||
|
settings.database_url,
|
||||||
|
pool_pre_ping=True,
|
||||||
|
pool_recycle=300,
|
||||||
|
echo=False, # Set to True for SQL debugging
|
||||||
|
)
|
||||||
|
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
78
backend/app/main.py
Normal file
78
backend/app/main.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
from fastapi import FastAPI, Depends, HTTPException, WebSocket, WebSocketDisconnect
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from typing import List
|
||||||
|
import json
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.api.api import api_router
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title=settings.project_name,
|
||||||
|
openapi_url=f"{settings.api_v1_str}/openapi.json",
|
||||||
|
description="Prior Permission Required (PPR) system API for aircraft operations management",
|
||||||
|
version="2.0.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set up CORS middleware
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"], # Configure this properly for production
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# WebSocket connection manager for real-time updates
|
||||||
|
class ConnectionManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.active_connections: List[WebSocket] = []
|
||||||
|
|
||||||
|
async def connect(self, websocket: WebSocket):
|
||||||
|
await websocket.accept()
|
||||||
|
self.active_connections.append(websocket)
|
||||||
|
|
||||||
|
def disconnect(self, websocket: WebSocket):
|
||||||
|
self.active_connections.remove(websocket)
|
||||||
|
|
||||||
|
async def send_personal_message(self, message: str, websocket: WebSocket):
|
||||||
|
await websocket.send_text(message)
|
||||||
|
|
||||||
|
async def broadcast(self, message: dict):
|
||||||
|
message_str = json.dumps(message)
|
||||||
|
for connection in self.active_connections:
|
||||||
|
try:
|
||||||
|
await connection.send_text(message_str)
|
||||||
|
except:
|
||||||
|
# Connection is dead, remove it
|
||||||
|
self.active_connections.remove(connection)
|
||||||
|
|
||||||
|
manager = ConnectionManager()
|
||||||
|
|
||||||
|
@app.websocket("/ws/tower-updates")
|
||||||
|
async def websocket_endpoint(websocket: WebSocket):
|
||||||
|
await manager.connect(websocket)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
# Keep connection alive
|
||||||
|
data = await websocket.receive_text()
|
||||||
|
# Echo back for heartbeat
|
||||||
|
await websocket.send_text(f"Heartbeat: {data}")
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
manager.disconnect(websocket)
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
return {
|
||||||
|
"message": "Airfield PPR API",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"docs": "/docs"
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
return {"status": "healthy", "timestamp": "2024-01-01T00:00:00Z"}
|
||||||
|
|
||||||
|
# Include API router
|
||||||
|
app.include_router(api_router, prefix=settings.api_v1_str)
|
||||||
|
|
||||||
|
# Make connection manager available to the app
|
||||||
|
app.state.connection_manager = manager
|
||||||
1
backend/app/models/__init__.py
Normal file
1
backend/app/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Empty __init__.py files to make Python packages
|
||||||
88
backend/app/models/ppr.py
Normal file
88
backend/app/models/ppr.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, DateTime, Text, Enum as SQLEnum, BigInteger
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from enum import Enum
|
||||||
|
from app.db.session import Base
|
||||||
|
|
||||||
|
|
||||||
|
class PPRStatus(str, Enum):
|
||||||
|
NEW = "NEW"
|
||||||
|
CONFIRMED = "CONFIRMED"
|
||||||
|
CANCELED = "CANCELED"
|
||||||
|
LANDED = "LANDED"
|
||||||
|
DELETED = "DELETED"
|
||||||
|
DEPARTED = "DEPARTED"
|
||||||
|
|
||||||
|
|
||||||
|
class PPRRecord(Base):
|
||||||
|
__tablename__ = "submitted"
|
||||||
|
|
||||||
|
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||||
|
status = Column(SQLEnum(PPRStatus), nullable=False, default=PPRStatus.NEW)
|
||||||
|
ac_reg = Column(String(16), nullable=False)
|
||||||
|
ac_type = Column(String(32), nullable=False)
|
||||||
|
ac_call = Column(String(16), nullable=True)
|
||||||
|
captain = Column(String(64), nullable=False)
|
||||||
|
fuel = Column(String(16), nullable=True)
|
||||||
|
in_from = Column(String(64), nullable=False)
|
||||||
|
eta = Column(DateTime, nullable=False)
|
||||||
|
pob_in = Column(Integer, nullable=False)
|
||||||
|
out_to = Column(String(64), nullable=True)
|
||||||
|
etd = Column(DateTime, nullable=True)
|
||||||
|
pob_out = Column(Integer, nullable=True)
|
||||||
|
email = Column(String(128), nullable=True)
|
||||||
|
phone = Column(String(16), nullable=True)
|
||||||
|
notes = Column(String(2000), nullable=True)
|
||||||
|
landed_dt = Column(DateTime, nullable=True)
|
||||||
|
departed_dt = Column(DateTime, nullable=True)
|
||||||
|
created_by = Column(String(16), nullable=True)
|
||||||
|
submitted_dt = Column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||||
|
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
username = Column(String(50), nullable=False, unique=True, index=True)
|
||||||
|
password = Column(String(255), nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Journal(Base):
|
||||||
|
__tablename__ = "journal"
|
||||||
|
|
||||||
|
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||||
|
ppr_id = Column(Integer, nullable=False, index=True)
|
||||||
|
entry = Column(Text, nullable=False)
|
||||||
|
user = Column(Text, nullable=False)
|
||||||
|
ip = Column(Text, nullable=False)
|
||||||
|
entry_dt = Column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||||
|
|
||||||
|
|
||||||
|
class Airport(Base):
|
||||||
|
__tablename__ = "airports"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
icao = Column(String(4), nullable=False, unique=True, index=True)
|
||||||
|
iata = Column(String(3), nullable=True, index=True)
|
||||||
|
name = Column(String(255), nullable=False)
|
||||||
|
country = Column(String(100), nullable=False)
|
||||||
|
city = Column(String(100), nullable=True)
|
||||||
|
timezone = Column(String(50), nullable=True)
|
||||||
|
latitude = Column(Text, nullable=True)
|
||||||
|
longitude = Column(Text, nullable=True)
|
||||||
|
elevation = Column(Integer, nullable=True)
|
||||||
|
created_at = Column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||||
|
|
||||||
|
|
||||||
|
class Aircraft(Base):
|
||||||
|
__tablename__ = "aircraft"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
icao24 = Column(String(6), nullable=True)
|
||||||
|
registration = Column(String(25), nullable=True, index=True)
|
||||||
|
manufacturer_icao = Column(String(50), nullable=True)
|
||||||
|
type_code = Column(String(30), nullable=True)
|
||||||
|
manufacturer_name = Column(String(255), nullable=True)
|
||||||
|
model = Column(String(255), nullable=True)
|
||||||
|
clean_reg = Column(String(25), nullable=True, index=True)
|
||||||
|
created_at = Column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||||
|
updated_at = Column(DateTime, nullable=False, server_default=func.current_timestamp())
|
||||||
1
backend/app/schemas/__init__.py
Normal file
1
backend/app/schemas/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Empty __init__.py files to make Python packages
|
||||||
176
backend/app/schemas/ppr.py
Normal file
176
backend/app/schemas/ppr.py
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
from pydantic import BaseModel, EmailStr, validator
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class PPRStatus(str, Enum):
|
||||||
|
NEW = "NEW"
|
||||||
|
CONFIRMED = "CONFIRMED"
|
||||||
|
CANCELED = "CANCELED"
|
||||||
|
LANDED = "LANDED"
|
||||||
|
DELETED = "DELETED"
|
||||||
|
DEPARTED = "DEPARTED"
|
||||||
|
|
||||||
|
|
||||||
|
class PPRBase(BaseModel):
|
||||||
|
ac_reg: str
|
||||||
|
ac_type: str
|
||||||
|
ac_call: Optional[str] = None
|
||||||
|
captain: str
|
||||||
|
fuel: Optional[str] = None
|
||||||
|
in_from: str
|
||||||
|
eta: datetime
|
||||||
|
pob_in: int
|
||||||
|
out_to: Optional[str] = None
|
||||||
|
etd: Optional[datetime] = None
|
||||||
|
pob_out: Optional[int] = None
|
||||||
|
email: Optional[EmailStr] = None
|
||||||
|
phone: Optional[str] = None
|
||||||
|
notes: Optional[str] = None
|
||||||
|
|
||||||
|
@validator('ac_reg')
|
||||||
|
def validate_registration(cls, v):
|
||||||
|
if not v or len(v.strip()) == 0:
|
||||||
|
raise ValueError('Aircraft registration is required')
|
||||||
|
return v.strip().upper()
|
||||||
|
|
||||||
|
@validator('pob_in')
|
||||||
|
def validate_pob_in(cls, v):
|
||||||
|
if v is not None and v < 0:
|
||||||
|
raise ValueError('POB inbound must be non-negative')
|
||||||
|
return v
|
||||||
|
|
||||||
|
@validator('pob_out')
|
||||||
|
def validate_pob_out(cls, v):
|
||||||
|
if v is not None and v < 0:
|
||||||
|
raise ValueError('POB outbound must be non-negative')
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class PPRCreate(PPRBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PPRUpdate(BaseModel):
|
||||||
|
ac_reg: Optional[str] = None
|
||||||
|
ac_type: Optional[str] = None
|
||||||
|
ac_call: Optional[str] = None
|
||||||
|
captain: Optional[str] = None
|
||||||
|
fuel: Optional[str] = None
|
||||||
|
in_from: Optional[str] = None
|
||||||
|
eta: Optional[datetime] = None
|
||||||
|
pob_in: Optional[int] = None
|
||||||
|
out_to: Optional[str] = None
|
||||||
|
etd: Optional[datetime] = None
|
||||||
|
pob_out: Optional[int] = None
|
||||||
|
email: Optional[EmailStr] = None
|
||||||
|
phone: Optional[str] = None
|
||||||
|
notes: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class PPRStatusUpdate(BaseModel):
|
||||||
|
status: PPRStatus
|
||||||
|
|
||||||
|
|
||||||
|
class PPRInDBBase(PPRBase):
|
||||||
|
id: int
|
||||||
|
status: PPRStatus
|
||||||
|
landed_dt: Optional[datetime] = None
|
||||||
|
departed_dt: Optional[datetime] = None
|
||||||
|
created_by: Optional[str] = None
|
||||||
|
submitted_dt: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class PPR(PPRInDBBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PPRInDB(PPRInDBBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# User schemas
|
||||||
|
class UserBase(BaseModel):
|
||||||
|
username: str
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreate(UserBase):
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
class UserInDBBase(UserBase):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class User(UserInDBBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UserInDB(UserInDBBase):
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
# Authentication schemas
|
||||||
|
class Token(BaseModel):
|
||||||
|
access_token: str
|
||||||
|
token_type: str
|
||||||
|
|
||||||
|
|
||||||
|
class TokenData(BaseModel):
|
||||||
|
username: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
# Journal schemas
|
||||||
|
class JournalBase(BaseModel):
|
||||||
|
ppr_id: int
|
||||||
|
entry: str
|
||||||
|
user: str
|
||||||
|
ip: str
|
||||||
|
|
||||||
|
|
||||||
|
class JournalCreate(JournalBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Journal(JournalBase):
|
||||||
|
id: int
|
||||||
|
entry_dt: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
# Airport schemas
|
||||||
|
class AirportBase(BaseModel):
|
||||||
|
icao: str
|
||||||
|
iata: Optional[str] = None
|
||||||
|
name: str
|
||||||
|
country: str
|
||||||
|
|
||||||
|
|
||||||
|
class Airport(AirportBase):
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
# Aircraft schemas
|
||||||
|
class AircraftBase(BaseModel):
|
||||||
|
icao24: Optional[str] = None
|
||||||
|
registration: Optional[str] = None
|
||||||
|
manufacturericao: Optional[str] = None
|
||||||
|
typecode: Optional[str] = None
|
||||||
|
manufacturername: Optional[str] = None
|
||||||
|
model: Optional[str] = None
|
||||||
|
clean_reg: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Aircraft(AircraftBase):
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
17
backend/requirements.txt
Normal file
17
backend/requirements.txt
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
fastapi[all]==0.104.1
|
||||||
|
uvicorn[standard]==0.24.0
|
||||||
|
sqlalchemy==2.0.23
|
||||||
|
alembic==1.12.1
|
||||||
|
pymysql==1.1.0
|
||||||
|
cryptography==41.0.7
|
||||||
|
python-jose[cryptography]==3.3.0
|
||||||
|
passlib[bcrypt]==1.7.4
|
||||||
|
bcrypt==4.0.1
|
||||||
|
python-multipart==0.0.6
|
||||||
|
email-validator==2.1.0
|
||||||
|
pydantic[email]==2.5.0
|
||||||
|
pydantic-settings==2.0.3
|
||||||
|
pytest==7.4.3
|
||||||
|
pytest-asyncio==0.21.1
|
||||||
|
httpx==0.25.2
|
||||||
|
redis==5.0.1
|
||||||
132
backend/schema_dump.sql
Normal file
132
backend/schema_dump.sql
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
/*M!999999\- enable the sandbox mode */
|
||||||
|
-- MariaDB dump 10.19 Distrib 10.6.22-MariaDB, for debian-linux-gnu (x86_64)
|
||||||
|
--
|
||||||
|
-- Host: sasaprod.pattinson.org Database: pprdevdb
|
||||||
|
-- ------------------------------------------------------
|
||||||
|
-- Server version 9.2.0
|
||||||
|
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||||
|
/*!40101 SET NAMES utf8mb4 */;
|
||||||
|
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
|
||||||
|
/*!40103 SET TIME_ZONE='+00:00' */;
|
||||||
|
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||||
|
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||||
|
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||||
|
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `aircraft`
|
||||||
|
--
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `aircraft`;
|
||||||
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
|
/*!40101 SET character_set_client = utf8mb4 */;
|
||||||
|
CREATE TABLE `aircraft` (
|
||||||
|
`icao24` text,
|
||||||
|
`registration` text,
|
||||||
|
`manufacturericao` text,
|
||||||
|
`typecode` text,
|
||||||
|
`manufacturername` text,
|
||||||
|
`model` text,
|
||||||
|
`clean_reg` text GENERATED ALWAYS AS (regexp_replace(`registration`,_utf8mb4'[^a-zA-Z0-9]',_utf8mb4'')) STORED,
|
||||||
|
KEY `reg_idx` (`registration`(8)),
|
||||||
|
KEY `clean_idx` (`clean_reg`(8))
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `airports`
|
||||||
|
--
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `airports`;
|
||||||
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
|
/*!40101 SET character_set_client = utf8mb4 */;
|
||||||
|
CREATE TABLE `airports` (
|
||||||
|
`icao` text NOT NULL,
|
||||||
|
`iata` text,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`country` text NOT NULL,
|
||||||
|
KEY `icao_idx` (`icao`(4)),
|
||||||
|
KEY `iata_idx` (`iata`(3))
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `journal`
|
||||||
|
--
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `journal`;
|
||||||
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
|
/*!40101 SET character_set_client = utf8mb4 */;
|
||||||
|
CREATE TABLE `journal` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`ppr_id` int NOT NULL,
|
||||||
|
`entry` text NOT NULL,
|
||||||
|
`user` text NOT NULL,
|
||||||
|
`ip` text NOT NULL,
|
||||||
|
`entry_dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `id_idx` (`ppr_id`) USING BTREE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=422 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `submitted`
|
||||||
|
--
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `submitted`;
|
||||||
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
|
/*!40101 SET character_set_client = utf8mb4 */;
|
||||||
|
CREATE TABLE `submitted` (
|
||||||
|
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`status` enum('NEW','CONFIRMED','CANCELED','LANDED','DELETED','DEPARTED') CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT 'NEW',
|
||||||
|
`ac_reg` varchar(16) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
|
||||||
|
`ac_type` varchar(32) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
|
||||||
|
`ac_call` varchar(16) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL,
|
||||||
|
`captain` varchar(64) NOT NULL,
|
||||||
|
`fuel` varchar(16) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL,
|
||||||
|
`in_from` varchar(64) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
|
||||||
|
`eta` datetime NOT NULL,
|
||||||
|
`pob_in` int NOT NULL,
|
||||||
|
`out_to` varchar(64) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL,
|
||||||
|
`etd` datetime DEFAULT NULL,
|
||||||
|
`pob_out` int DEFAULT NULL,
|
||||||
|
`email` varchar(128) DEFAULT NULL,
|
||||||
|
`phone` varchar(16) DEFAULT NULL,
|
||||||
|
`notes` varchar(2000) DEFAULT NULL,
|
||||||
|
`landed_dt` datetime DEFAULT NULL,
|
||||||
|
`departed_dt` datetime DEFAULT NULL,
|
||||||
|
`created_by` varchar(16) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL,
|
||||||
|
`submitted_dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY `id` (`id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=271 DEFAULT CHARSET=latin1;
|
||||||
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `users`
|
||||||
|
--
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `users`;
|
||||||
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
|
/*!40101 SET character_set_client = utf8mb4 */;
|
||||||
|
CREATE TABLE `users` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`username` varchar(50) NOT NULL,
|
||||||
|
`password` varchar(255) NOT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `username` (`username`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||||
|
|
||||||
|
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||||
|
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||||
|
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||||
|
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||||
|
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||||
|
|
||||||
|
-- Dump completed on 2025-10-21 15:56:53
|
||||||
18
backend/start.sh
Normal file
18
backend/start.sh
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Startup script for the FastAPI backend
|
||||||
|
echo "Starting Airfield PPR API..."
|
||||||
|
|
||||||
|
# Install dependencies if needed
|
||||||
|
if [ ! -d "venv" ]; then
|
||||||
|
echo "Creating virtual environment..."
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
else
|
||||||
|
source venv/bin/activate
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
echo "Starting FastAPI server on port 8000..."
|
||||||
|
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||||
42
backend/tests/test_main.py
Normal file
42
backend/tests/test_main.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from app.main import app
|
||||||
|
from app.api.deps import get_db
|
||||||
|
from app.db.session import Base
|
||||||
|
|
||||||
|
# Create test database
|
||||||
|
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
|
||||||
|
engine = create_engine(
|
||||||
|
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
||||||
|
)
|
||||||
|
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
def override_get_db():
|
||||||
|
try:
|
||||||
|
db = TestingSessionLocal()
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
app.dependency_overrides[get_db] = override_get_db
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
def test_read_main():
|
||||||
|
response = client.get("/")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "Airfield PPR API" in response.json()["message"]
|
||||||
|
|
||||||
|
def test_health_check():
|
||||||
|
response = client.get("/health")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["status"] == "healthy"
|
||||||
|
|
||||||
|
def test_get_public_arrivals():
|
||||||
|
response = client.get("/api/v1/public/arrivals")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert isinstance(response.json(), list)
|
||||||
65
docker-compose.yml
Normal file
65
docker-compose.yml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# MySQL Database
|
||||||
|
db:
|
||||||
|
image: mysql:8.0
|
||||||
|
container_name: ppr_nextgen_db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: rootpassword123
|
||||||
|
MYSQL_DATABASE: ppr_nextgen
|
||||||
|
MYSQL_USER: ppr_user
|
||||||
|
MYSQL_PASSWORD: ppr_password123
|
||||||
|
ports:
|
||||||
|
- "3307:3306" # Use different port to avoid conflicts
|
||||||
|
volumes:
|
||||||
|
- mysql_data:/var/lib/mysql
|
||||||
|
- ./init_db.sql:/docker-entrypoint-initdb.d/01-schema.sql
|
||||||
|
- ./02-import-data.sql:/docker-entrypoint-initdb.d/02-import-data.sql
|
||||||
|
- ./airports_data_clean.csv:/var/lib/mysql-files/airports_data.csv
|
||||||
|
- ./aircraft_data.csv:/var/lib/mysql-files/aircraft_data.csv
|
||||||
|
networks:
|
||||||
|
- ppr_network
|
||||||
|
|
||||||
|
# FastAPI Backend
|
||||||
|
api:
|
||||||
|
build: ./backend
|
||||||
|
container_name: ppr_nextgen_api
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
DB_HOST: db
|
||||||
|
DB_USER: ppr_user
|
||||||
|
DB_PASSWORD: ppr_password123
|
||||||
|
DB_NAME: ppr_nextgen
|
||||||
|
DB_PORT: 3306
|
||||||
|
SECRET_KEY: super-secret-key-for-nextgen-ppr-system-change-in-production
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES: 30
|
||||||
|
API_V1_STR: /api/v1
|
||||||
|
PROJECT_NAME: "Airfield PPR API NextGen"
|
||||||
|
ports:
|
||||||
|
- "8001:8000" # Use different port to avoid conflicts with existing system
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
volumes:
|
||||||
|
- ./backend:/app
|
||||||
|
networks:
|
||||||
|
- ppr_network
|
||||||
|
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
|
||||||
|
# Redis for caching (optional for now)
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: ppr_nextgen_redis
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "6380:6379" # Use different port
|
||||||
|
networks:
|
||||||
|
- ppr_network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysql_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
ppr_network:
|
||||||
|
driver: bridge
|
||||||
130
init_db.sql
Normal file
130
init_db.sql
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
-- Initialize the NextGen PPR database
|
||||||
|
-- This script creates the tables based on the existing schema but with improvements
|
||||||
|
|
||||||
|
-- Enable foreign key checks
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
|
-- Users table with improved structure
|
||||||
|
CREATE TABLE users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
email VARCHAR(128),
|
||||||
|
full_name VARCHAR(100),
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_username (username),
|
||||||
|
INDEX idx_email (email)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Main PPR submissions table with improvements
|
||||||
|
CREATE TABLE submitted (
|
||||||
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
status ENUM('NEW','CONFIRMED','CANCELED','LANDED','DELETED','DEPARTED') NOT NULL DEFAULT 'NEW',
|
||||||
|
ac_reg VARCHAR(16) NOT NULL,
|
||||||
|
ac_type VARCHAR(32) NOT NULL,
|
||||||
|
ac_call VARCHAR(16) DEFAULT NULL,
|
||||||
|
captain VARCHAR(64) NOT NULL,
|
||||||
|
fuel VARCHAR(16) DEFAULT NULL,
|
||||||
|
in_from VARCHAR(64) NOT NULL,
|
||||||
|
eta DATETIME NOT NULL,
|
||||||
|
pob_in INT NOT NULL,
|
||||||
|
out_to VARCHAR(64) DEFAULT NULL,
|
||||||
|
etd DATETIME DEFAULT NULL,
|
||||||
|
pob_out INT DEFAULT NULL,
|
||||||
|
email VARCHAR(128) DEFAULT NULL,
|
||||||
|
phone VARCHAR(16) DEFAULT NULL,
|
||||||
|
notes TEXT DEFAULT NULL,
|
||||||
|
landed_dt DATETIME DEFAULT NULL,
|
||||||
|
departed_dt DATETIME DEFAULT NULL,
|
||||||
|
created_by VARCHAR(16) DEFAULT NULL,
|
||||||
|
submitted_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
-- Indexes for better performance
|
||||||
|
INDEX idx_status (status),
|
||||||
|
INDEX idx_eta (eta),
|
||||||
|
INDEX idx_etd (etd),
|
||||||
|
INDEX idx_ac_reg (ac_reg),
|
||||||
|
INDEX idx_submitted_dt (submitted_dt),
|
||||||
|
INDEX idx_created_by (created_by)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Activity journal table with foreign key
|
||||||
|
CREATE TABLE journal (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
ppr_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
entry TEXT NOT NULL,
|
||||||
|
user VARCHAR(50) NOT NULL,
|
||||||
|
ip VARCHAR(45) NOT NULL, -- IPv6 compatible
|
||||||
|
entry_dt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
INDEX idx_ppr_id (ppr_id),
|
||||||
|
INDEX idx_entry_dt (entry_dt),
|
||||||
|
INDEX idx_user (user),
|
||||||
|
|
||||||
|
FOREIGN KEY (ppr_id) REFERENCES submitted(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Airports reference table with proper structure
|
||||||
|
CREATE TABLE airports (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
icao VARCHAR(4) NOT NULL,
|
||||||
|
iata VARCHAR(3) DEFAULT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
country VARCHAR(100) NOT NULL,
|
||||||
|
city VARCHAR(100) DEFAULT NULL,
|
||||||
|
timezone VARCHAR(50) DEFAULT NULL,
|
||||||
|
latitude DECIMAL(10, 8) DEFAULT NULL,
|
||||||
|
longitude DECIMAL(11, 8) DEFAULT NULL,
|
||||||
|
elevation INT DEFAULT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
UNIQUE KEY unique_icao (icao),
|
||||||
|
INDEX idx_iata (iata),
|
||||||
|
INDEX idx_country (country),
|
||||||
|
INDEX idx_name (name)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Aircraft reference table with improved structure
|
||||||
|
CREATE TABLE aircraft (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
icao24 VARCHAR(6) DEFAULT NULL,
|
||||||
|
registration VARCHAR(25) DEFAULT NULL,
|
||||||
|
manufacturer_icao VARCHAR(50) DEFAULT NULL,
|
||||||
|
type_code VARCHAR(30) DEFAULT NULL,
|
||||||
|
manufacturer_name VARCHAR(255) DEFAULT NULL,
|
||||||
|
model VARCHAR(255) DEFAULT NULL,
|
||||||
|
clean_reg VARCHAR(25) GENERATED ALWAYS AS (UPPER(REPLACE(REPLACE(registration, '-', ''), ' ', ''))) STORED,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
INDEX idx_registration (registration),
|
||||||
|
INDEX idx_clean_reg (clean_reg),
|
||||||
|
INDEX idx_icao24 (icao24),
|
||||||
|
INDEX idx_type_code (type_code)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Insert default admin user (password: admin123)
|
||||||
|
-- Password hash for 'admin123' using bcrypt
|
||||||
|
INSERT INTO users (username, password, email, full_name) VALUES
|
||||||
|
('admin', '$2b$12$BJOha2yRxkxuHL./BaMfpu2fMDgGMYISuRV2.B1sSklVpRjz3Y4a6', 'admin@ppr.local', 'System Administrator');
|
||||||
|
|
||||||
|
-- Create a view for active PPRs
|
||||||
|
CREATE VIEW active_pprs AS
|
||||||
|
SELECT
|
||||||
|
s.*,
|
||||||
|
af.name as arrival_airport_name,
|
||||||
|
df.name as departure_airport_name,
|
||||||
|
ac.manufacturer_name,
|
||||||
|
ac.model
|
||||||
|
FROM submitted s
|
||||||
|
LEFT JOIN airports af ON s.in_from = af.icao OR s.in_from = af.iata
|
||||||
|
LEFT JOIN airports df ON s.out_to = df.icao OR s.out_to = df.iata
|
||||||
|
LEFT JOIN aircraft ac ON s.ac_reg = ac.registration
|
||||||
|
WHERE s.status != 'DELETED';
|
||||||
|
|
||||||
|
-- Create indexes for the view performance
|
||||||
|
-- ALTER TABLE submitted ADD INDEX idx_in_from (in_from);
|
||||||
|
-- ALTER TABLE submitted ADD INDEX idx_out_to (out_to);
|
||||||
Reference in New Issue
Block a user