This commit is contained in:
Iliyan Angelov
2025-11-28 14:36:37 +02:00
parent 312f85530c
commit b5698b6018
12 changed files with 191 additions and 35 deletions

View File

@@ -17,11 +17,12 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
# Consider moving to nonces/hashes in future for stricter policy
security_headers['Content-Security-Policy'] = (
"default-src 'self'; "
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://js.stripe.com; "
"style-src 'self' 'unsafe-inline'; "
"img-src 'self' data: https:; "
"font-src 'self' data:; "
"connect-src 'self' https:; "
"connect-src 'self' https: https://js.stripe.com https://hooks.stripe.com; "
"frame-src 'self' https://js.stripe.com https://hooks.stripe.com; "
"base-uri 'self'; "
"form-action 'self'; "
"frame-ancestors 'none'; "

View File

@@ -1,6 +1,7 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query
from ..utils.response_helpers import success_response
from sqlalchemy.orm import Session
from ..utils.response_helpers import success_response, error_response
from sqlalchemy.orm import Session, joinedload
from sqlalchemy import func
from typing import Optional
from ..config.database import get_db
from ..config.logging_config import get_logger
@@ -21,20 +22,49 @@ async def get_room_reviews(
db: Session = Depends(get_db)
):
try:
query = db.query(Review).filter(Review.room_id == room_id, Review.status == ReviewStatus.approved)
# Calculate average rating and total reviews for approved reviews
stats = db.query(
func.avg(Review.rating).label('average_rating'),
func.count(Review.id).label('total_reviews')
).filter(
Review.room_id == room_id,
Review.status == ReviewStatus.approved
).first()
average_rating = round(float(stats.average_rating or 0), 1) if stats and stats.average_rating else 0
total_reviews = stats.total_reviews or 0 if stats else 0
query = db.query(Review).options(
joinedload(Review.user)
).filter(Review.room_id == room_id, Review.status == ReviewStatus.approved)
total = query.count()
offset = (page - 1) * limit
reviews = query.order_by(Review.created_at.desc()).offset(offset).limit(limit).all()
result = []
for review in reviews:
review_dict = {'id': review.id, 'user_id': review.user_id, 'room_id': review.room_id, 'rating': review.rating, 'comment': review.comment, 'status': review.status.value if isinstance(review.status, ReviewStatus) else review.status, 'created_at': review.created_at.isoformat() if review.created_at else None}
review_dict = {
'id': review.id,
'user_id': review.user_id,
'room_id': review.room_id,
'rating': review.rating,
'comment': review.comment,
'status': review.status.value if isinstance(review.status, ReviewStatus) else review.status,
'created_at': review.created_at.isoformat() if review.created_at else None
}
if review.user:
review_dict['user'] = {'id': review.user.id, 'full_name': review.user.full_name, 'email': review.user.email}
review_dict['user'] = {
'id': review.user.id,
'full_name': review.user.full_name,
'name': review.user.full_name, # Add name alias for compatibility
'email': review.user.email
}
result.append(review_dict)
return {
'status': 'success',
'data': {
'reviews': result,
'average_rating': average_rating,
'total_reviews': total_reviews,
'pagination': {
'total': total,
'page': page,
@@ -51,7 +81,10 @@ async def get_room_reviews(
@router.get('/', dependencies=[Depends(authorize_roles('admin'))])
async def get_all_reviews(status_filter: Optional[str]=Query(None, alias='status'), page: int=Query(1, ge=1), limit: int=Query(10, ge=1, le=100), current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
try:
query = db.query(Review)
query = db.query(Review).options(
joinedload(Review.user),
joinedload(Review.room).joinedload(Room.room_type)
)
if status_filter:
try:
query = query.filter(Review.status == ReviewStatus(status_filter))
@@ -64,9 +97,25 @@ async def get_all_reviews(status_filter: Optional[str]=Query(None, alias='status
for review in reviews:
review_dict = {'id': review.id, 'user_id': review.user_id, 'room_id': review.room_id, 'rating': review.rating, 'comment': review.comment, 'status': review.status.value if isinstance(review.status, ReviewStatus) else review.status, 'created_at': review.created_at.isoformat() if review.created_at else None}
if review.user:
review_dict['user'] = {'id': review.user.id, 'full_name': review.user.full_name, 'email': review.user.email, 'phone': review.user.phone}
review_dict['user'] = {
'id': review.user.id,
'full_name': review.user.full_name,
'name': review.user.full_name, # Add name alias for frontend compatibility
'email': review.user.email,
'phone': review.user.phone
}
if review.room:
review_dict['room'] = {'id': review.room.id, 'room_number': review.room.room_number}
room_dict = {
'id': review.room.id,
'room_number': review.room.room_number
}
# Include room type if available
if review.room.room_type:
room_dict['room_type'] = {
'id': review.room.room_type.id,
'name': review.room.room_type.name
}
review_dict['room'] = room_dict
result.append(review_dict)
return {'status': 'success', 'data': {'reviews': result, 'pagination': {'total': total, 'page': page, 'limit': limit, 'totalPages': (total + limit - 1) // limit}}}
except Exception as e:
@@ -79,58 +128,131 @@ async def create_review(review_data: CreateReviewRequest, current_user: User=Dep
try:
room_id = review_data.room_id
rating = review_data.rating
comment = review_data.comment
# Ensure comment is a string (not None) since the model requires it
comment = review_data.comment if review_data.comment else ''
room = db.query(Room).filter(Room.id == room_id).first()
if not room:
raise HTTPException(status_code=404, detail='Room not found')
raise HTTPException(
status_code=404,
detail=error_response(message='Room not found')
)
existing = db.query(Review).filter(Review.user_id == current_user.id, Review.room_id == room_id).first()
if existing:
raise HTTPException(status_code=400, detail='You have already reviewed this room')
review = Review(user_id=current_user.id, room_id=room_id, rating=rating, comment=comment, status=ReviewStatus.pending)
raise HTTPException(
status_code=400,
detail=error_response(message='You have already reviewed this room')
)
review = Review(
user_id=current_user.id,
room_id=room_id,
rating=rating,
comment=comment or '', # Ensure comment is a string
status=ReviewStatus.pending
)
db.add(review)
db.commit()
db.refresh(review)
return {'status': 'success', 'message': 'Review submitted successfully and is pending approval', 'data': {'review': review}}
review_dict = {
'id': review.id,
'user_id': review.user_id,
'room_id': review.room_id,
'rating': review.rating,
'comment': review.comment,
'status': review.status.value if isinstance(review.status, ReviewStatus) else review.status,
'created_at': review.created_at.isoformat() if review.created_at else None
}
return success_response(
data={'review': review_dict},
message='Review submitted successfully and is pending approval'
)
except HTTPException:
raise
except Exception as e:
db.rollback()
logger.error(f'Error creating review: {str(e)}', exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
raise HTTPException(
status_code=500,
detail=error_response(message='An error occurred while creating the review')
)
@router.put('/{id}/approve', dependencies=[Depends(authorize_roles('admin'))])
@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)):
try:
review = db.query(Review).filter(Review.id == id).first()
if not review:
raise HTTPException(status_code=404, detail='Review not found')
raise HTTPException(
status_code=404,
detail=error_response(message='Review not found')
)
review.status = ReviewStatus.approved
db.commit()
db.refresh(review)
return {'status': 'success', 'message': 'Review approved successfully', 'data': {'review': review}}
review_dict = {
'id': review.id,
'user_id': review.user_id,
'room_id': review.room_id,
'rating': review.rating,
'comment': review.comment,
'status': review.status.value if isinstance(review.status, ReviewStatus) else review.status,
'created_at': review.created_at.isoformat() if review.created_at else None
}
return success_response(
data={'review': review_dict},
message='Review approved successfully'
)
except HTTPException:
raise
except Exception as e:
db.rollback()
logger.error(f'Error approving review: {str(e)}', exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
raise HTTPException(
status_code=500,
detail=error_response(message='An error occurred while approving the review')
)
@router.put('/{id}/reject', dependencies=[Depends(authorize_roles('admin'))])
@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)):
try:
review = db.query(Review).filter(Review.id == id).first()
if not review:
raise HTTPException(status_code=404, detail='Review not found')
raise HTTPException(
status_code=404,
detail=error_response(message='Review not found')
)
review.status = ReviewStatus.rejected
db.commit()
db.refresh(review)
return {'status': 'success', 'message': 'Review rejected successfully', 'data': {'review': review}}
review_dict = {
'id': review.id,
'user_id': review.user_id,
'room_id': review.room_id,
'rating': review.rating,
'comment': review.comment,
'status': review.status.value if isinstance(review.status, ReviewStatus) else review.status,
'created_at': review.created_at.isoformat() if review.created_at else None
}
return success_response(
data={'review': review_dict},
message='Review rejected successfully'
)
except HTTPException:
raise
except Exception as e:
db.rollback()
logger.error(f'Error rejecting review: {str(e)}', exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
raise HTTPException(
status_code=500,
detail=error_response(message='An error occurred while rejecting the review')
)
@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)):

View File

@@ -9,7 +9,7 @@ class CreateReviewRequest(BaseModel):
"""Schema for creating a review."""
room_id: int = Field(..., gt=0, description="Room ID")
rating: int = Field(..., ge=1, le=5, description="Rating from 1 to 5")
comment: Optional[str] = Field(None, max_length=2000, description="Review comment")
comment: str = Field(..., min_length=1, max_length=2000, description="Review comment")
model_config = {
"json_schema_extra": {