Journaling improvements
This commit is contained in:
@@ -159,7 +159,8 @@ async def cancel_arrival(
|
||||
current_user: User = Depends(get_current_operator_user)
|
||||
):
|
||||
"""Cancel an arrival record"""
|
||||
arrival = crud_arrival.cancel(db, arrival_id=arrival_id)
|
||||
client_ip = get_client_ip(request)
|
||||
arrival = crud_arrival.cancel(db, arrival_id=arrival_id, user=current_user.username, user_ip=client_ip)
|
||||
if not arrival:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
|
||||
@@ -87,7 +87,7 @@ async def create_user(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Username already registered"
|
||||
)
|
||||
user = crud_user.create(db, obj_in=user_in)
|
||||
user = crud_user.create(db, obj_in=user_in, admin_user=current_user.username)
|
||||
return user
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ async def update_user(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
user = crud_user.update(db, db_obj=user, obj_in=user_in)
|
||||
user = crud_user.update(db, db_obj=user, obj_in=user_in, admin_user=current_user.username)
|
||||
return user
|
||||
|
||||
|
||||
@@ -123,5 +123,5 @@ async def change_user_password(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
user = crud_user.change_password(db, db_obj=user, new_password=password_data.password)
|
||||
user = crud_user.change_password(db, db_obj=user, new_password=password_data.password, admin_user=current_user.username)
|
||||
return user
|
||||
@@ -64,7 +64,8 @@ async def create_circuit(
|
||||
detail="Cannot provide both local_flight_id and arrival_id"
|
||||
)
|
||||
|
||||
circuit = crud_circuit.create(db, obj_in=circuit_in)
|
||||
client_ip = get_client_ip(request)
|
||||
circuit = crud_circuit.create(db, obj_in=circuit_in, user=current_user.username, user_ip=client_ip)
|
||||
|
||||
# Send real-time update via WebSocket
|
||||
if hasattr(request.app.state, 'connection_manager'):
|
||||
@@ -116,6 +117,7 @@ async def update_circuit(
|
||||
|
||||
@router.delete("/{circuit_id}")
|
||||
async def delete_circuit(
|
||||
request: Request,
|
||||
circuit_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_operator_user)
|
||||
@@ -127,5 +129,6 @@ async def delete_circuit(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Circuit record not found"
|
||||
)
|
||||
crud_circuit.delete(db, circuit_id=circuit_id)
|
||||
client_ip = get_client_ip(request)
|
||||
crud_circuit.delete(db, circuit_id=circuit_id, user=current_user.username, user_ip=client_ip)
|
||||
return {"detail": "Circuit record deleted"}
|
||||
|
||||
@@ -159,7 +159,8 @@ async def cancel_departure(
|
||||
current_user: User = Depends(get_current_operator_user)
|
||||
):
|
||||
"""Cancel a departure record"""
|
||||
departure = crud_departure.cancel(db, departure_id=departure_id)
|
||||
client_ip = get_client_ip(request)
|
||||
departure = crud_departure.cancel(db, departure_id=departure_id, user=current_user.username, user_ip=client_ip)
|
||||
if not departure:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
|
||||
@@ -4,11 +4,79 @@ from app.api import deps
|
||||
from app.crud.crud_journal import journal
|
||||
from app.models.journal import EntityType
|
||||
from app.schemas.journal import JournalEntryResponse, EntityJournalResponse
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
from datetime import datetime, date
|
||||
|
||||
router = APIRouter(tags=["journal"])
|
||||
|
||||
|
||||
@router.get("/search/all", response_model=List[JournalEntryResponse])
|
||||
async def search_journal(
|
||||
date_from: Optional[date] = None,
|
||||
date_to: Optional[date] = None,
|
||||
entity_type: Optional[str] = None,
|
||||
entity_id: Optional[int] = None,
|
||||
user: Optional[str] = None,
|
||||
limit: int = 500,
|
||||
db: Session = Depends(deps.get_db),
|
||||
current_user = Depends(deps.get_current_user)
|
||||
):
|
||||
"""
|
||||
Search journal entries with optional filters.
|
||||
|
||||
Parameters:
|
||||
- date_from: Filter entries from this date (YYYY-MM-DD)
|
||||
- date_to: Filter entries until this date (YYYY-MM-DD)
|
||||
- entity_type: Filter by entity type (PPR, LOCAL_FLIGHT, ARRIVAL, DEPARTURE, OVERFLIGHT, CIRCUIT, USER)
|
||||
- entity_id: Filter by specific entity ID
|
||||
- user: Filter by user who created the entry
|
||||
- limit: Maximum number of entries to return (default 500, max 5000)
|
||||
|
||||
All filters are optional and can be combined.
|
||||
Returns entries in reverse chronological order (newest first).
|
||||
"""
|
||||
if limit > 5000:
|
||||
limit = 5000
|
||||
|
||||
# Validate entity_type if provided
|
||||
if entity_type:
|
||||
try:
|
||||
EntityType[entity_type.upper()]
|
||||
except KeyError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid entity_type. Must be one of: {', '.join([e.value for e in EntityType])}"
|
||||
)
|
||||
|
||||
entries = journal.search_entries(
|
||||
db,
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
entity_type=entity_type,
|
||||
entity_id=entity_id,
|
||||
user=user,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
@router.get("/user/{username}", response_model=List[JournalEntryResponse])
|
||||
async def get_user_journal(
|
||||
username: str,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(deps.get_db),
|
||||
current_user = Depends(deps.get_current_user)
|
||||
):
|
||||
"""
|
||||
Get all journal entries created by a specific user.
|
||||
|
||||
This endpoint is read-only and returns entries in reverse chronological order.
|
||||
"""
|
||||
entries = journal.get_user_journal(db, username, limit=limit)
|
||||
return entries
|
||||
|
||||
|
||||
@router.get("/{entity_type}/{entity_id}", response_model=EntityJournalResponse)
|
||||
async def get_entity_journal(
|
||||
entity_type: str,
|
||||
@@ -45,19 +113,3 @@ async def get_entity_journal(
|
||||
entries=entries,
|
||||
total_entries=len(entries)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/user/{username}", response_model=List[JournalEntryResponse])
|
||||
async def get_user_journal(
|
||||
username: str,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(deps.get_db),
|
||||
current_user = Depends(deps.get_current_user)
|
||||
):
|
||||
"""
|
||||
Get all journal entries created by a specific user.
|
||||
|
||||
This endpoint is read-only and returns entries in reverse chronological order.
|
||||
"""
|
||||
entries = journal.get_user_journal(db, username, limit=limit)
|
||||
return entries
|
||||
|
||||
@@ -160,7 +160,8 @@ async def cancel_local_flight(
|
||||
current_user: User = Depends(get_current_operator_user)
|
||||
):
|
||||
"""Cancel a local flight record"""
|
||||
flight = crud_local_flight.cancel(db, flight_id=flight_id)
|
||||
client_ip = get_client_ip(request)
|
||||
flight = crud_local_flight.cancel(db, flight_id=flight_id, user=current_user.username, user_ip=client_ip)
|
||||
if not flight:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
|
||||
@@ -56,7 +56,7 @@ async def public_book_local_flight(
|
||||
notes=flight_in.notes,
|
||||
)
|
||||
|
||||
flight = crud_local_flight.create(db, obj_in=flight_create, created_by="PUBLIC_PILOT", submitted_via="PUBLIC")
|
||||
flight = crud_local_flight.create(db, obj_in=flight_create, created_by="PUBLIC_PILOT", submitted_via="PUBLIC", user_ip=request.client.host if request.client else None)
|
||||
|
||||
# Update with submission source and pilot email
|
||||
db.query(type(flight)).filter(type(flight).id == flight.id).update({
|
||||
@@ -98,7 +98,7 @@ async def public_record_circuit(
|
||||
circuit_timestamp=circuit_in.circuit_timestamp,
|
||||
)
|
||||
|
||||
circuit = crud_circuit.create(db, obj_in=circuit_create)
|
||||
circuit = crud_circuit.create(db, obj_in=circuit_create, user="PUBLIC_PILOT", user_ip=request.client.host if request.client else None)
|
||||
|
||||
# Send real-time update via WebSocket
|
||||
if hasattr(request.app.state, 'connection_manager'):
|
||||
@@ -136,7 +136,7 @@ async def public_book_departure(
|
||||
notes=departure_in.notes,
|
||||
)
|
||||
|
||||
departure = crud_departure.create(db, obj_in=departure_create, created_by="PUBLIC_PILOT", submitted_via="PUBLIC")
|
||||
departure = crud_departure.create(db, obj_in=departure_create, created_by="PUBLIC_PILOT", submitted_via="PUBLIC", user_ip=request.client.host if request.client else None)
|
||||
|
||||
# Update with pilot email (submitted_via is already set in create method)
|
||||
db.query(type(departure)).filter(type(departure).id == departure.id).update({
|
||||
@@ -181,7 +181,7 @@ async def public_book_arrival(
|
||||
notes=arrival_in.notes,
|
||||
)
|
||||
|
||||
arrival = crud_arrival.create(db, obj_in=arrival_create, created_by="PUBLIC_PILOT", submitted_via="PUBLIC")
|
||||
arrival = crud_arrival.create(db, obj_in=arrival_create, created_by="PUBLIC_PILOT", submitted_via="PUBLIC", user_ip=request.client.host if request.client else None)
|
||||
|
||||
# Update with pilot email
|
||||
db.query(type(arrival)).filter(type(arrival).id == arrival.id).update({
|
||||
|
||||
@@ -58,7 +58,7 @@ class CRUDArrival:
|
||||
)
|
||||
).order_by(Arrival.created_dt).all()
|
||||
|
||||
def create(self, db: Session, obj_in: ArrivalCreate, created_by: str, submitted_via: str = "ADMIN") -> Arrival:
|
||||
def create(self, db: Session, obj_in: ArrivalCreate, created_by: str, submitted_via: str = "ADMIN", user_ip: Optional[str] = None) -> Arrival:
|
||||
from app.models.arrival import SubmissionSource
|
||||
|
||||
# Set initial status based on submission source
|
||||
@@ -76,6 +76,17 @@ class CRUDArrival:
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
|
||||
# Log creation in journal
|
||||
journal.log_change(
|
||||
db,
|
||||
EntityType.ARRIVAL,
|
||||
db_obj.id,
|
||||
f"Arrival created: {db_obj.registration}",
|
||||
created_by,
|
||||
user_ip
|
||||
)
|
||||
|
||||
return db_obj
|
||||
|
||||
def update(self, db: Session, db_obj: Arrival, obj_in: ArrivalUpdate, user: str = "system", user_ip: Optional[str] = None) -> Arrival:
|
||||
@@ -156,15 +167,27 @@ class CRUDArrival:
|
||||
|
||||
return db_obj
|
||||
|
||||
def cancel(self, db: Session, arrival_id: int) -> Optional[Arrival]:
|
||||
def cancel(self, db: Session, arrival_id: int, user: str = "system", user_ip: Optional[str] = None) -> Optional[Arrival]:
|
||||
db_obj = self.get(db, arrival_id)
|
||||
if not db_obj:
|
||||
return None
|
||||
|
||||
old_status = db_obj.status
|
||||
db_obj.status = ArrivalStatus.CANCELLED
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
|
||||
# Log cancellation in journal
|
||||
journal.log_change(
|
||||
db,
|
||||
EntityType.ARRIVAL,
|
||||
arrival_id,
|
||||
f"Status changed from {old_status.value} to CANCELLED",
|
||||
user,
|
||||
user_ip
|
||||
)
|
||||
|
||||
return db_obj
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ from sqlalchemy import desc
|
||||
from datetime import datetime
|
||||
from app.models.circuit import Circuit
|
||||
from app.schemas.circuit import CircuitCreate, CircuitUpdate
|
||||
from app.models.journal import EntityType
|
||||
from app.crud.crud_journal import journal
|
||||
|
||||
|
||||
class CRUDCircuit:
|
||||
@@ -30,7 +32,7 @@ class CRUDCircuit:
|
||||
) -> List[Circuit]:
|
||||
return db.query(Circuit).order_by(desc(Circuit.created_at)).offset(skip).limit(limit).all()
|
||||
|
||||
def create(self, db: Session, obj_in: CircuitCreate) -> Circuit:
|
||||
def create(self, db: Session, obj_in: CircuitCreate, user: str = "system", user_ip: Optional[str] = None) -> Circuit:
|
||||
db_obj = Circuit(
|
||||
local_flight_id=obj_in.local_flight_id,
|
||||
arrival_id=obj_in.arrival_id,
|
||||
@@ -39,22 +41,74 @@ class CRUDCircuit:
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
|
||||
# Log circuit creation in journal
|
||||
# Use LOCAL_FLIGHT entity type if local_flight_id exists, otherwise ARRIVAL
|
||||
entity_type = EntityType.LOCAL_FLIGHT if obj_in.local_flight_id else EntityType.ARRIVAL
|
||||
entity_id = obj_in.local_flight_id if obj_in.local_flight_id else obj_in.arrival_id
|
||||
|
||||
journal.log_change(
|
||||
db,
|
||||
entity_type,
|
||||
entity_id,
|
||||
f"Circuit recorded at {obj_in.circuit_timestamp.isoformat()}",
|
||||
user,
|
||||
user_ip
|
||||
)
|
||||
|
||||
return db_obj
|
||||
|
||||
def update(self, db: Session, db_obj: Circuit, obj_in: CircuitUpdate) -> Circuit:
|
||||
def update(self, db: Session, db_obj: Circuit, obj_in: CircuitUpdate, user: str = "system", user_ip: Optional[str] = None) -> Circuit:
|
||||
obj_data = obj_in.dict(exclude_unset=True)
|
||||
changes = []
|
||||
|
||||
for field, value in obj_data.items():
|
||||
setattr(db_obj, field, value)
|
||||
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)
|
||||
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
|
||||
# Log changes in journal if any were made
|
||||
if changes:
|
||||
entity_type = EntityType.LOCAL_FLIGHT if db_obj.local_flight_id else EntityType.ARRIVAL
|
||||
entity_id = db_obj.local_flight_id if db_obj.local_flight_id else db_obj.arrival_id
|
||||
|
||||
for change in changes:
|
||||
journal.log_change(
|
||||
db,
|
||||
entity_type,
|
||||
entity_id,
|
||||
f"Circuit: {change}",
|
||||
user,
|
||||
user_ip
|
||||
)
|
||||
|
||||
return db_obj
|
||||
|
||||
def delete(self, db: Session, circuit_id: int) -> bool:
|
||||
def delete(self, db: Session, circuit_id: int, user: str = "system", user_ip: Optional[str] = None) -> bool:
|
||||
circuit = self.get(db, circuit_id)
|
||||
if circuit:
|
||||
# Determine which entity this circuit belongs to
|
||||
entity_type = EntityType.LOCAL_FLIGHT if circuit.local_flight_id else EntityType.ARRIVAL
|
||||
entity_id = circuit.local_flight_id if circuit.local_flight_id else circuit.arrival_id
|
||||
|
||||
db.delete(circuit)
|
||||
db.commit()
|
||||
|
||||
# Log deletion in journal
|
||||
journal.log_change(
|
||||
db,
|
||||
entity_type,
|
||||
entity_id,
|
||||
f"Circuit deleted (recorded at {circuit.circuit_timestamp.isoformat()})",
|
||||
user,
|
||||
user_ip
|
||||
)
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class CRUDDeparture:
|
||||
)
|
||||
).order_by(Departure.created_dt).all()
|
||||
|
||||
def create(self, db: Session, obj_in: DepartureCreate, created_by: str, submitted_via: str = "ADMIN") -> Departure:
|
||||
def create(self, db: Session, obj_in: DepartureCreate, created_by: str, submitted_via: str = "ADMIN", user_ip: Optional[str] = None) -> Departure:
|
||||
from app.models.departure import SubmissionSource
|
||||
|
||||
# Set initial status based on submission source
|
||||
@@ -68,6 +68,17 @@ class CRUDDeparture:
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
|
||||
# Log creation in journal
|
||||
journal.log_change(
|
||||
db,
|
||||
EntityType.DEPARTURE,
|
||||
db_obj.id,
|
||||
f"Departure created: {db_obj.registration}",
|
||||
created_by,
|
||||
user_ip
|
||||
)
|
||||
|
||||
return db_obj
|
||||
|
||||
def update(self, db: Session, db_obj: Departure, obj_in: DepartureUpdate, user: str = "system", user_ip: Optional[str] = None) -> Departure:
|
||||
@@ -150,15 +161,27 @@ class CRUDDeparture:
|
||||
|
||||
return db_obj
|
||||
|
||||
def cancel(self, db: Session, departure_id: int) -> Optional[Departure]:
|
||||
def cancel(self, db: Session, departure_id: int, user: str = "system", user_ip: Optional[str] = None) -> Optional[Departure]:
|
||||
db_obj = self.get(db, departure_id)
|
||||
if not db_obj:
|
||||
return None
|
||||
|
||||
old_status = db_obj.status
|
||||
db_obj.status = DepartureStatus.CANCELLED
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
|
||||
# Log cancellation in journal
|
||||
journal.log_change(
|
||||
db,
|
||||
EntityType.DEPARTURE,
|
||||
departure_id,
|
||||
f"Status changed from {old_status.value} to CANCELLED",
|
||||
user,
|
||||
user_ip
|
||||
)
|
||||
|
||||
return db_obj
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from typing import List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, or_, func, desc
|
||||
from app.models.journal import JournalEntry, EntityType
|
||||
from datetime import datetime
|
||||
from datetime import datetime, date
|
||||
|
||||
|
||||
class CRUDJournal:
|
||||
@@ -58,6 +59,41 @@ class CRUDJournal:
|
||||
JournalEntry.user == user
|
||||
).order_by(JournalEntry.entry_dt.desc()).limit(limit).all()
|
||||
|
||||
def search_entries(
|
||||
self,
|
||||
db: Session,
|
||||
date_from: Optional[date] = None,
|
||||
date_to: Optional[date] = None,
|
||||
entity_type: Optional[str] = None,
|
||||
entity_id: Optional[int] = None,
|
||||
user: Optional[str] = None,
|
||||
limit: int = 500
|
||||
) -> List[JournalEntry]:
|
||||
"""Search journal entries with optional filters."""
|
||||
query = db.query(JournalEntry)
|
||||
|
||||
# Apply date filters
|
||||
if date_from:
|
||||
query = query.filter(func.date(JournalEntry.entry_dt) >= date_from)
|
||||
|
||||
if date_to:
|
||||
query = query.filter(func.date(JournalEntry.entry_dt) <= date_to)
|
||||
|
||||
# Apply entity type filter
|
||||
if entity_type:
|
||||
query = query.filter(JournalEntry.entity_type == entity_type.upper())
|
||||
|
||||
# Apply entity ID filter
|
||||
if entity_id:
|
||||
query = query.filter(JournalEntry.entity_id == entity_id)
|
||||
|
||||
# Apply user filter
|
||||
if user:
|
||||
query = query.filter(JournalEntry.user == user)
|
||||
|
||||
# Order by date descending (newest first) and apply limit
|
||||
return query.order_by(JournalEntry.entry_dt.desc()).limit(limit).all()
|
||||
|
||||
# Convenience methods for backward compatibility with PPR journal
|
||||
def log_ppr_change(
|
||||
self,
|
||||
|
||||
@@ -84,7 +84,7 @@ class CRUDLocalFlight:
|
||||
)
|
||||
).order_by(LocalFlight.created_dt).all()
|
||||
|
||||
def create(self, db: Session, obj_in: LocalFlightCreate, created_by: str, submitted_via: str = "ADMIN") -> LocalFlight:
|
||||
def create(self, db: Session, obj_in: LocalFlightCreate, created_by: str, submitted_via: str = "ADMIN", user_ip: Optional[str] = None) -> LocalFlight:
|
||||
from app.models.local_flight import SubmissionSource
|
||||
|
||||
# Set initial status based on submission source
|
||||
@@ -102,6 +102,17 @@ class CRUDLocalFlight:
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
|
||||
# Log creation in journal
|
||||
journal.log_change(
|
||||
db,
|
||||
EntityType.LOCAL_FLIGHT,
|
||||
db_obj.id,
|
||||
f"Local flight created: {db_obj.registration} ({db_obj.flight_type.value})",
|
||||
created_by,
|
||||
user_ip
|
||||
)
|
||||
|
||||
return db_obj
|
||||
|
||||
def update(self, db: Session, db_obj: LocalFlight, obj_in: LocalFlightUpdate, user: str = "system", user_ip: Optional[str] = None) -> LocalFlight:
|
||||
@@ -201,13 +212,24 @@ class CRUDLocalFlight:
|
||||
|
||||
return db_obj
|
||||
|
||||
def cancel(self, db: Session, flight_id: int) -> Optional[LocalFlight]:
|
||||
def cancel(self, db: Session, flight_id: int, user: str = "system", user_ip: Optional[str] = None) -> Optional[LocalFlight]:
|
||||
db_obj = self.get(db, flight_id)
|
||||
if db_obj:
|
||||
old_status = db_obj.status
|
||||
db_obj.status = LocalFlightStatus.CANCELLED
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
|
||||
# Log cancellation in journal
|
||||
journal.log_change(
|
||||
db,
|
||||
EntityType.LOCAL_FLIGHT,
|
||||
flight_id,
|
||||
f"Status changed from {old_status.value} to CANCELLED",
|
||||
user,
|
||||
user_ip
|
||||
)
|
||||
return db_obj
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ from sqlalchemy.orm import Session
|
||||
from app.models.ppr import User
|
||||
from app.schemas.ppr import UserCreate, UserUpdate
|
||||
from app.core.security import get_password_hash, verify_password
|
||||
from app.models.journal import EntityType
|
||||
from app.crud.crud_journal import journal
|
||||
|
||||
|
||||
class CRUDUser:
|
||||
@@ -15,7 +17,7 @@ class CRUDUser:
|
||||
def get_multi(self, db: Session, skip: int = 0, limit: int = 100) -> List[User]:
|
||||
return db.query(User).offset(skip).limit(limit).all()
|
||||
|
||||
def create(self, db: Session, obj_in: UserCreate) -> User:
|
||||
def create(self, db: Session, obj_in: UserCreate, admin_user: str = "system") -> User:
|
||||
hashed_password = get_password_hash(obj_in.password)
|
||||
db_obj = User(
|
||||
username=obj_in.username,
|
||||
@@ -25,17 +27,46 @@ class CRUDUser:
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
|
||||
# Log user creation in journal
|
||||
journal.log_change(
|
||||
db,
|
||||
EntityType.USER,
|
||||
db_obj.id,
|
||||
f"User created: {obj_in.username} with role {obj_in.role}",
|
||||
admin_user,
|
||||
None
|
||||
)
|
||||
|
||||
return db_obj
|
||||
|
||||
def update(self, db: Session, db_obj: User, obj_in: UserUpdate) -> User:
|
||||
def update(self, db: Session, db_obj: User, obj_in: UserUpdate, admin_user: str = "system") -> User:
|
||||
update_data = obj_in.dict(exclude_unset=True)
|
||||
changes = []
|
||||
if "password" in update_data:
|
||||
update_data["password"] = get_password_hash(update_data["password"])
|
||||
changes.append("password changed")
|
||||
for field, value in update_data.items():
|
||||
setattr(db_obj, field, value)
|
||||
old_value = getattr(db_obj, field)
|
||||
if field == "password" or old_value != value:
|
||||
if field != "password": # Don't log actual password values
|
||||
changes.append(f"{field} changed from '{old_value}' to '{value}'")
|
||||
setattr(db_obj, field, value)
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
|
||||
# Log user update in journal
|
||||
if changes:
|
||||
journal.log_change(
|
||||
db,
|
||||
EntityType.USER,
|
||||
db_obj.id,
|
||||
"; ".join(changes),
|
||||
admin_user,
|
||||
None
|
||||
)
|
||||
|
||||
return db_obj
|
||||
|
||||
def authenticate(self, db: Session, username: str, password: str) -> Optional[User]:
|
||||
@@ -50,13 +81,24 @@ class CRUDUser:
|
||||
# For future use if we add user status
|
||||
return True
|
||||
|
||||
def change_password(self, db: Session, db_obj: User, new_password: str) -> User:
|
||||
def change_password(self, db: Session, db_obj: User, new_password: str, admin_user: str = "system") -> User:
|
||||
"""Change a user's password (typically used by admins to reset another user's password)"""
|
||||
hashed_password = get_password_hash(new_password)
|
||||
db_obj.password = hashed_password
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
|
||||
# Log password change in journal (security audit)
|
||||
journal.log_change(
|
||||
db,
|
||||
EntityType.USER,
|
||||
db_obj.id,
|
||||
f"Password changed by {admin_user}",
|
||||
admin_user,
|
||||
None
|
||||
)
|
||||
|
||||
return db_obj
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ class EntityType(str, PyEnum):
|
||||
ARRIVAL = "ARRIVAL"
|
||||
DEPARTURE = "DEPARTURE"
|
||||
OVERFLIGHT = "OVERFLIGHT"
|
||||
CIRCUIT = "CIRCUIT"
|
||||
USER = "USER"
|
||||
|
||||
|
||||
class JournalEntry(Base):
|
||||
|
||||
Reference in New Issue
Block a user