Creating admin interface

This commit is contained in:
James Pattinson
2025-10-21 20:23:58 +00:00
parent f580d0fbf7
commit 28af669993
6 changed files with 1276 additions and 19 deletions

View File

@@ -216,10 +216,67 @@ The API sends real-time updates via WebSocket for:
- **Username:** `admin`
- **Password:** `admin123`
## Journal System
All PPR changes are automatically logged in a journal system for audit trail purposes.
### Get PPR Journal Entries
**Endpoint:** `GET /api/v1/pprs/{ppr_id}/journal`
**Response:**
```json
[
{
"id": 1,
"ppr_id": 3,
"entry": "PPR created for G-ADMIN",
"user": "admin",
"ip": "172.23.0.1",
"entry_dt": "2025-10-21T20:01:01"
},
{
"id": 2,
"ppr_id": 3,
"entry": "captain changed from 'Test Admin' to 'Updated Admin User'",
"user": "admin",
"ip": "172.23.0.1",
"entry_dt": "2025-10-21T20:01:17"
}
]
```
### Automatic Journal Logging
The system automatically logs:
- PPR creation
- All field changes with old and new values
- Status changes
- User and IP address for each change
## Web Interfaces
### Public Arrivals/Departures Board
- **URL:** http://localhost:8082
- **Features:** Real-time arrivals and departures display
- **Authentication:** None required
### Admin Interface
- **URL:** http://localhost:8082/admin.html
- **Features:**
- Complete PPR management (CRUD operations)
- Advanced filtering by status, date range
- Inline editing with modal interface
- Journal/audit trail viewing
- Quick status updates (Confirm, Land, Depart, Cancel)
- New PPR entry creation
- **Authentication:** Username/password prompt (uses API token)
## Development URLs
- API Base: http://localhost:8001/api/v1
- Public Web Interface: http://localhost:8082
- Admin Interface: http://localhost:8082/admin.html
- API Documentation: http://localhost:8001/docs
- Database: localhost:3307 (MySQL)

View File

@@ -4,8 +4,10 @@ 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.crud.crud_journal import journal as crud_journal
from app.schemas.ppr import PPR, PPRCreate, PPRUpdate, PPRStatus, PPRStatusUpdate, Journal
from app.models.ppr import User
from app.core.utils import get_client_ip
router = APIRouter()
@@ -37,7 +39,8 @@ async def create_ppr(
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)
client_ip = get_client_ip(request)
ppr = crud_ppr.create(db, obj_in=ppr_in, created_by=current_user.username, user_ip=client_ip)
# Send real-time update via WebSocket
if hasattr(request.app.state, 'connection_manager'):
@@ -85,7 +88,8 @@ async def update_ppr(
detail="PPR record not found"
)
ppr = crud_ppr.update(db, db_obj=db_ppr, obj_in=ppr_in)
client_ip = get_client_ip(request)
ppr = crud_ppr.update(db, db_obj=db_ppr, obj_in=ppr_in, user=current_user.username, user_ip=client_ip)
# Send real-time update
if hasattr(request.app.state, 'connection_manager'):
@@ -117,8 +121,8 @@ async def patch_ppr(
detail="PPR record not found"
)
# For PATCH, we only update fields that are explicitly provided (not None)
ppr = crud_ppr.update(db, db_obj=db_ppr, obj_in=ppr_in)
client_ip = get_client_ip(request)
ppr = crud_ppr.update(db, db_obj=db_ppr, obj_in=ppr_in, user=current_user.username, user_ip=client_ip)
# Send real-time update
if hasattr(request.app.state, 'connection_manager'):
@@ -143,15 +147,20 @@ async def update_ppr_status(
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)
client_ip = get_client_ip(request)
ppr = crud_ppr.update_status(
db,
ppr_id=ppr_id,
status=status_update.status,
user=current_user.username,
user_ip=client_ip
)
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({
@@ -175,7 +184,8 @@ async def delete_ppr(
current_user: User = Depends(get_current_active_user)
):
"""Delete (soft delete) a PPR record"""
ppr = crud_ppr.delete(db, ppr_id=ppr_id)
client_ip = get_client_ip(request)
ppr = crud_ppr.delete(db, ppr_id=ppr_id, user=current_user.username, user_ip=client_ip)
if not ppr:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -192,4 +202,22 @@ async def delete_ppr(
}
})
return ppr
return ppr
@router.get("/{ppr_id}/journal", response_model=List[Journal])
async def get_ppr_journal(
ppr_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user)
):
"""Get journal entries for a specific PPR"""
# Verify PPR exists
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 crud_journal.get_by_ppr_id(db, ppr_id=ppr_id)

14
backend/app/core/utils.py Normal file
View File

@@ -0,0 +1,14 @@
from fastapi import Request
def get_client_ip(request: Request) -> str:
"""Extract client IP address from request"""
forwarded = request.headers.get("X-Forwarded-For")
if forwarded:
return forwarded.split(",")[0].strip()
real_ip = request.headers.get("X-Real-IP")
if real_ip:
return real_ip
return request.client.host if request.client else "127.0.0.1"

View File

@@ -0,0 +1,35 @@
from typing import List
from sqlalchemy.orm import Session
from app.models.ppr import Journal
from app.schemas.ppr import JournalCreate
class CRUDJournal:
def create(self, db: Session, obj_in: JournalCreate) -> Journal:
db_obj = Journal(**obj_in.dict())
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def get_by_ppr_id(self, db: Session, ppr_id: int) -> List[Journal]:
return db.query(Journal).filter(Journal.ppr_id == ppr_id).order_by(Journal.entry_dt.desc()).all()
def log_change(
self,
db: Session,
ppr_id: int,
entry: str,
user: str,
ip: str
) -> Journal:
journal_in = JournalCreate(
ppr_id=ppr_id,
entry=entry,
user=user,
ip=ip
)
return self.create(db, journal_in)
journal = CRUDJournal()

View File

@@ -4,6 +4,7 @@ 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
from app.crud.crud_journal import journal as crud_journal
class CRUDPPR:
@@ -62,7 +63,7 @@ class CRUDPPR:
)
).order_by(PPRRecord.etd).all()
def create(self, db: Session, obj_in: PPRCreate, created_by: str) -> PPRRecord:
def create(self, db: Session, obj_in: PPRCreate, created_by: str, user_ip: str = "127.0.0.1") -> PPRRecord:
db_obj = PPRRecord(
**obj_in.dict(),
created_by=created_by,
@@ -71,28 +72,52 @@ class CRUDPPR:
db.add(db_obj)
db.commit()
db.refresh(db_obj)
# Log creation in journal
crud_journal.log_change(
db,
db_obj.id,
f"PPR created for {db_obj.ac_reg}",
created_by,
user_ip
)
return db_obj
def update(self, db: Session, db_obj: PPRRecord, obj_in: PPRUpdate) -> PPRRecord:
def update(self, db: Session, db_obj: PPRRecord, obj_in: PPRUpdate, user: str = "system", user_ip: str = "127.0.0.1") -> PPRRecord:
update_data = obj_in.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(db_obj, field, value)
changes = []
for field, value in update_data.items():
old_value = getattr(db_obj, field)
if old_value != value:
changes.append(f"{field} changed from '{old_value}' to '{value}'")
setattr(db_obj, field, value)
if changes:
db.add(db_obj)
db.commit()
db.refresh(db_obj)
# Log changes in journal
for change in changes:
crud_journal.log_change(db, db_obj.id, change, user, user_ip)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update_status(
self,
db: Session,
ppr_id: int,
status: PPRStatus
status: PPRStatus,
user: str = "system",
user_ip: str = "127.0.0.1"
) -> Optional[PPRRecord]:
db_obj = self.get(db, ppr_id)
if not db_obj:
return None
old_status = db_obj.status
db_obj.status = status
# Set timestamps based on status
@@ -104,16 +129,36 @@ class CRUDPPR:
db.add(db_obj)
db.commit()
db.refresh(db_obj)
# Log status change in journal
crud_journal.log_change(
db,
db_obj.id,
f"Status changed from {old_status.value} to {status.value}",
user,
user_ip
)
return db_obj
def delete(self, db: Session, ppr_id: int) -> Optional[PPRRecord]:
def delete(self, db: Session, ppr_id: int, user: str = "system", user_ip: str = "127.0.0.1") -> Optional[PPRRecord]:
db_obj = self.get(db, ppr_id)
if db_obj:
old_status = db_obj.status
# Soft delete by setting status
db_obj.status = PPRStatus.DELETED
db.add(db_obj)
db.commit()
db.refresh(db_obj)
# Log the deletion in journal
crud_journal.log_change(
db,
db_obj.id,
f"PPR marked as DELETED (was {old_status.value})",
user,
user_ip
)
return db_obj

1078
web/admin.html Normal file

File diff suppressed because it is too large Load Diff