This commit is contained in:
Iliyan Angelov
2025-12-04 01:07:34 +02:00
parent 5fb50983a9
commit 3d634b4fce
92 changed files with 9678 additions and 221 deletions

View File

@@ -13,8 +13,15 @@ router = APIRouter(prefix='/favorites', tags=['favorites'])
@router.get('/')
async def get_favorites(current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
role = db.query(Role).filter(Role.id == current_user.role_id).first()
if role and role.name in ['admin', 'staff', 'accountant']:
# PERFORMANCE: Use eager-loaded role relationship if available
if hasattr(current_user, 'role') and current_user.role is not None:
role_name = current_user.role.name
else:
# Fallback: query if relationship wasn't loaded
role = db.query(Role).filter(Role.id == current_user.role_id).first()
role_name = role.name if role else 'customer'
if role_name in ['admin', 'staff', 'accountant']:
raise HTTPException(status_code=403, detail='Admin, staff, and accountant users cannot have favorites')
try:
favorites = db.query(Favorite).filter(Favorite.user_id == current_user.id).order_by(Favorite.created_at.desc()).all()
@@ -35,8 +42,15 @@ async def get_favorites(current_user: User=Depends(get_current_user), db: Sessio
@router.post('/{room_id}')
async def add_favorite(room_id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
role = db.query(Role).filter(Role.id == current_user.role_id).first()
if role and role.name in ['admin', 'staff', 'accountant']:
# PERFORMANCE: Use eager-loaded role relationship if available
if hasattr(current_user, 'role') and current_user.role is not None:
role_name = current_user.role.name
else:
# Fallback: query if relationship wasn't loaded
role = db.query(Role).filter(Role.id == current_user.role_id).first()
role_name = role.name if role else 'customer'
if role_name in ['admin', 'staff', 'accountant']:
raise HTTPException(status_code=403, detail='Admin, staff, and accountant users cannot add favorites')
try:
room = db.query(Room).filter(Room.id == room_id).first()
@@ -58,8 +72,15 @@ async def add_favorite(room_id: int, current_user: User=Depends(get_current_user
@router.delete('/{room_id}')
async def remove_favorite(room_id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
role = db.query(Role).filter(Role.id == current_user.role_id).first()
if role and role.name in ['admin', 'staff', 'accountant']:
# PERFORMANCE: Use eager-loaded role relationship if available
if hasattr(current_user, 'role') and current_user.role is not None:
role_name = current_user.role.name
else:
# Fallback: query if relationship wasn't loaded
role = db.query(Role).filter(Role.id == current_user.role_id).first()
role_name = role.name if role else 'customer'
if role_name in ['admin', 'staff', 'accountant']:
raise HTTPException(status_code=403, detail='Admin, staff, and accountant users cannot remove favorites')
try:
favorite = db.query(Favorite).filter(Favorite.user_id == current_user.id, Favorite.room_id == room_id).first()
@@ -76,8 +97,15 @@ async def remove_favorite(room_id: int, current_user: User=Depends(get_current_u
@router.get('/check/{room_id}')
async def check_favorite(room_id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
role = db.query(Role).filter(Role.id == current_user.role_id).first()
if role and role.name in ['admin', 'staff', 'accountant']:
# PERFORMANCE: Use eager-loaded role relationship if available
if hasattr(current_user, 'role') and current_user.role is not None:
role_name = current_user.role.name
else:
# Fallback: query if relationship wasn't loaded
role = db.query(Role).filter(Role.id == current_user.role_id).first()
role_name = role.name if role else 'customer'
if role_name in ['admin', 'staff', 'accountant']:
return {'status': 'success', 'data': {'isFavorited': False}}
try:
favorite = db.query(Favorite).filter(Favorite.user_id == current_user.id, Favorite.room_id == room_id).first()

View File

@@ -1,7 +1,7 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query
from fastapi import APIRouter, Depends, HTTPException, status, Query, Request
from ...shared.utils.response_helpers import success_response, error_response
from sqlalchemy.orm import Session, joinedload
from sqlalchemy import func
from sqlalchemy import func, and_
from typing import Optional
from ...shared.config.database import get_db
from ...shared.config.logging_config import get_logger
@@ -9,7 +9,9 @@ from ...security.middleware.auth import get_current_user, authorize_roles
from ...auth.models.user import User
from ..models.review import Review, ReviewStatus
from ...rooms.models.room import Room
from ...bookings.models.booking import Booking, BookingStatus
from ..schemas.review import CreateReviewRequest
from ...analytics.services.audit_service import audit_service
logger = get_logger(__name__)
router = APIRouter(prefix='/reviews', tags=['reviews'])
@@ -124,7 +126,11 @@ async def get_all_reviews(status_filter: Optional[str]=Query(None, alias='status
raise HTTPException(status_code=500, detail=str(e))
@router.post('/')
async def create_review(review_data: CreateReviewRequest, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
async def create_review(review_data: CreateReviewRequest, request: Request, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
client_ip = request.client.host if request.client else None
user_agent = request.headers.get('User-Agent')
request_id = getattr(request.state, 'request_id', None)
try:
room_id = review_data.room_id
rating = review_data.rating
@@ -145,6 +151,25 @@ async def create_review(review_data: CreateReviewRequest, current_user: User=Dep
detail=error_response(message='You have already reviewed this room')
)
# BUSINESS RULE: Verify user has actually stayed in this room
# Users can only review rooms they've booked and checked out from
from ...shared.utils.role_helpers import is_admin, is_staff
if not (is_admin(current_user, db) or is_staff(current_user, db)):
# Check if user has a checked-out booking for this room
past_booking = db.query(Booking).filter(
and_(
Booking.user_id == current_user.id,
Booking.room_id == room_id,
Booking.status == BookingStatus.checked_out
)
).first()
if not past_booking:
raise HTTPException(
status_code=403,
detail=error_response(message='You can only review rooms you have stayed in. Please complete a booking and check out before leaving a review.')
)
review = Review(
user_id=current_user.id,
room_id=room_id,
@@ -156,6 +181,28 @@ async def create_review(review_data: CreateReviewRequest, current_user: User=Dep
db.commit()
db.refresh(review)
# SECURITY: Log review creation for audit trail
try:
await audit_service.log_action(
db=db,
action='review_created',
resource_type='review',
user_id=current_user.id,
resource_id=review.id,
ip_address=client_ip,
user_agent=user_agent,
request_id=request_id,
details={
'room_id': review.room_id,
'rating': review.rating,
'status': 'pending',
'comment_length': len(review.comment) if review.comment else 0
},
status='success'
)
except Exception as e:
logger.warning(f'Failed to log review creation audit: {e}')
review_dict = {
'id': review.id,
'user_id': review.user_id,
@@ -181,18 +228,47 @@ async def create_review(review_data: CreateReviewRequest, current_user: User=Dep
)
@router.patch('/{id}/approve', dependencies=[Depends(authorize_roles('admin'))])
async def approve_review(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
async def approve_review(id: int, request: Request, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
client_ip = request.client.host if request.client else None
user_agent = request.headers.get('User-Agent')
request_id = getattr(request.state, 'request_id', None)
try:
review = db.query(Review).filter(Review.id == id).first()
review = db.query(Review).options(joinedload(Review.room)).filter(Review.id == id).first()
if not review:
raise HTTPException(
status_code=404,
detail=error_response(message='Review not found')
)
old_status = review.status.value if hasattr(review.status, 'value') else str(review.status)
review.status = ReviewStatus.approved
db.commit()
db.refresh(review)
# SECURITY: Log review approval for audit trail
try:
await audit_service.log_action(
db=db,
action='review_approved',
resource_type='review',
user_id=current_user.id,
resource_id=review.id,
ip_address=client_ip,
user_agent=user_agent,
request_id=request_id,
details={
'review_user_id': review.user_id,
'room_id': review.room_id,
'room_number': review.room.room_number if review.room else None,
'rating': review.rating,
'old_status': old_status,
'new_status': 'approved'
},
status='success'
)
except Exception as e:
logger.warning(f'Failed to log review approval audit: {e}')
review_dict = {
'id': review.id,
'user_id': review.user_id,
@@ -218,18 +294,47 @@ async def approve_review(id: int, current_user: User=Depends(authorize_roles('ad
)
@router.patch('/{id}/reject', dependencies=[Depends(authorize_roles('admin'))])
async def reject_review(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
async def reject_review(id: int, request: Request, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
client_ip = request.client.host if request.client else None
user_agent = request.headers.get('User-Agent')
request_id = getattr(request.state, 'request_id', None)
try:
review = db.query(Review).filter(Review.id == id).first()
review = db.query(Review).options(joinedload(Review.room)).filter(Review.id == id).first()
if not review:
raise HTTPException(
status_code=404,
detail=error_response(message='Review not found')
)
old_status = review.status.value if hasattr(review.status, 'value') else str(review.status)
review.status = ReviewStatus.rejected
db.commit()
db.refresh(review)
# SECURITY: Log review rejection for audit trail
try:
await audit_service.log_action(
db=db,
action='review_rejected',
resource_type='review',
user_id=current_user.id,
resource_id=review.id,
ip_address=client_ip,
user_agent=user_agent,
request_id=request_id,
details={
'review_user_id': review.user_id,
'room_id': review.room_id,
'room_number': review.room.room_number if review.room else None,
'rating': review.rating,
'old_status': old_status,
'new_status': 'rejected'
},
status='success'
)
except Exception as e:
logger.warning(f'Failed to log review rejection audit: {e}')
review_dict = {
'id': review.id,
'user_id': review.user_id,
@@ -255,13 +360,47 @@ async def reject_review(id: int, current_user: User=Depends(authorize_roles('adm
)
@router.delete('/{id}', dependencies=[Depends(authorize_roles('admin'))])
async def delete_review(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
async def delete_review(id: int, request: Request, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
client_ip = request.client.host if request.client else None
user_agent = request.headers.get('User-Agent')
request_id = getattr(request.state, 'request_id', None)
try:
review = db.query(Review).filter(Review.id == id).first()
review = db.query(Review).options(joinedload(Review.room)).filter(Review.id == id).first()
if not review:
raise HTTPException(status_code=404, detail='Review not found')
# Capture review details before deletion for audit
review_details = {
'review_id': review.id,
'review_user_id': review.user_id,
'room_id': review.room_id,
'room_number': review.room.room_number if review.room else None,
'rating': review.rating,
'status': review.status.value if hasattr(review.status, 'value') else str(review.status),
'comment_length': len(review.comment) if review.comment else 0
}
db.delete(review)
db.commit()
# SECURITY: Log review deletion for audit trail
try:
await audit_service.log_action(
db=db,
action='review_deleted',
resource_type='review',
user_id=current_user.id,
resource_id=id,
ip_address=client_ip,
user_agent=user_agent,
request_id=request_id,
details=review_details,
status='success'
)
except Exception as e:
logger.warning(f'Failed to log review deletion audit: {e}')
return {'status': 'success', 'message': 'Review deleted successfully'}
except HTTPException:
raise