Creating admin interface
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
14
backend/app/core/utils.py
Normal 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"
|
||||
35
backend/app/crud/crud_journal.py
Normal file
35
backend/app/crud/crud_journal.py
Normal 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()
|
||||
@@ -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
1078
web/admin.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user