updates
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
# Routes package
|
||||
|
||||
|
||||
Binary file not shown.
BIN
Backend/src/routes/__pycache__/chat_routes.cpython-312.pyc
Normal file
BIN
Backend/src/routes/__pycache__/chat_routes.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,75 +1,23 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
import json
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ..models.page_content import PageContent, PageType
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/about", tags=["about"])
|
||||
|
||||
router = APIRouter(prefix='/about', tags=['about'])
|
||||
|
||||
def serialize_page_content(content: PageContent) -> dict:
|
||||
"""Serialize PageContent model to dictionary"""
|
||||
return {
|
||||
"id": content.id,
|
||||
"page_type": content.page_type.value,
|
||||
"title": content.title,
|
||||
"subtitle": content.subtitle,
|
||||
"description": content.description,
|
||||
"content": content.content,
|
||||
"meta_title": content.meta_title,
|
||||
"meta_description": content.meta_description,
|
||||
"meta_keywords": content.meta_keywords,
|
||||
"og_title": content.og_title,
|
||||
"og_description": content.og_description,
|
||||
"og_image": content.og_image,
|
||||
"canonical_url": content.canonical_url,
|
||||
"story_content": content.story_content,
|
||||
"values": json.loads(content.values) if content.values else None,
|
||||
"features": json.loads(content.features) if content.features else None,
|
||||
"about_hero_image": content.about_hero_image,
|
||||
"mission": content.mission,
|
||||
"vision": content.vision,
|
||||
"team": json.loads(content.team) if content.team else None,
|
||||
"timeline": json.loads(content.timeline) if content.timeline else None,
|
||||
"achievements": json.loads(content.achievements) if content.achievements else None,
|
||||
"is_active": content.is_active,
|
||||
"created_at": content.created_at.isoformat() if content.created_at else None,
|
||||
"updated_at": content.updated_at.isoformat() if content.updated_at else None,
|
||||
}
|
||||
return {'id': content.id, 'page_type': content.page_type.value, 'title': content.title, 'subtitle': content.subtitle, 'description': content.description, 'content': content.content, 'meta_title': content.meta_title, 'meta_description': content.meta_description, 'meta_keywords': content.meta_keywords, 'og_title': content.og_title, 'og_description': content.og_description, 'og_image': content.og_image, 'canonical_url': content.canonical_url, 'story_content': content.story_content, 'values': json.loads(content.values) if content.values else None, 'features': json.loads(content.features) if content.features else None, 'about_hero_image': content.about_hero_image, 'mission': content.mission, 'vision': content.vision, 'team': json.loads(content.team) if content.team else None, 'timeline': json.loads(content.timeline) if content.timeline else None, 'achievements': json.loads(content.achievements) if content.achievements else None, 'is_active': content.is_active, 'created_at': content.created_at.isoformat() if content.created_at else None, 'updated_at': content.updated_at.isoformat() if content.updated_at else None}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_about_content(
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get about page content"""
|
||||
@router.get('/')
|
||||
async def get_about_content(db: Session=Depends(get_db)):
|
||||
try:
|
||||
content = db.query(PageContent).filter(PageContent.page_type == PageType.ABOUT).first()
|
||||
|
||||
if not content:
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"page_content": None
|
||||
}
|
||||
}
|
||||
|
||||
return {'status': 'success', 'data': {'page_content': None}}
|
||||
content_dict = serialize_page_content(content)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"page_content": content_dict
|
||||
}
|
||||
}
|
||||
return {'status': 'success', 'data': {'page_content': content_dict}}
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching about content: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error fetching about content: {str(e)}"
|
||||
)
|
||||
|
||||
logger.error(f'Error fetching about content: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching about content: {str(e)}')
|
||||
@@ -1,120 +1,36 @@
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import authorize_roles
|
||||
from ..models.user import User
|
||||
from ..schemas.admin_privacy import (
|
||||
CookieIntegrationSettings,
|
||||
CookieIntegrationSettingsResponse,
|
||||
CookiePolicySettings,
|
||||
CookiePolicySettingsResponse,
|
||||
)
|
||||
from ..schemas.admin_privacy import CookieIntegrationSettings, CookieIntegrationSettingsResponse, CookiePolicySettings, CookiePolicySettingsResponse
|
||||
from ..services.privacy_admin_service import privacy_admin_service
|
||||
router = APIRouter(prefix='/admin/privacy', tags=['admin-privacy'])
|
||||
|
||||
|
||||
router = APIRouter(prefix="/admin/privacy", tags=["admin-privacy"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"/cookie-policy",
|
||||
response_model=CookiePolicySettingsResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
def get_cookie_policy(
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(authorize_roles("admin")),
|
||||
) -> CookiePolicySettingsResponse:
|
||||
"""
|
||||
Get global cookie policy configuration (admin only).
|
||||
"""
|
||||
@router.get('/cookie-policy', response_model=CookiePolicySettingsResponse, status_code=status.HTTP_200_OK)
|
||||
def get_cookie_policy(db: Session=Depends(get_db), _: User=Depends(authorize_roles('admin'))) -> CookiePolicySettingsResponse:
|
||||
settings = privacy_admin_service.get_policy_settings(db)
|
||||
policy = privacy_admin_service.get_or_create_policy(db)
|
||||
updated_by_name = (
|
||||
policy.updated_by.full_name if getattr(policy, "updated_by", None) else None
|
||||
)
|
||||
updated_by_name = policy.updated_by.full_name if getattr(policy, 'updated_by', None) else None
|
||||
return CookiePolicySettingsResponse(data=settings, updated_at=policy.updated_at, updated_by=updated_by_name)
|
||||
|
||||
return CookiePolicySettingsResponse(
|
||||
data=settings,
|
||||
updated_at=policy.updated_at,
|
||||
updated_by=updated_by_name,
|
||||
)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/cookie-policy",
|
||||
response_model=CookiePolicySettingsResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
def update_cookie_policy(
|
||||
payload: CookiePolicySettings,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
) -> CookiePolicySettingsResponse:
|
||||
"""
|
||||
Update global cookie policy configuration (admin only).
|
||||
"""
|
||||
@router.put('/cookie-policy', response_model=CookiePolicySettingsResponse, status_code=status.HTTP_200_OK)
|
||||
def update_cookie_policy(payload: CookiePolicySettings, db: Session=Depends(get_db), current_user: User=Depends(authorize_roles('admin'))) -> CookiePolicySettingsResponse:
|
||||
policy = privacy_admin_service.update_policy(db, payload, current_user)
|
||||
settings = privacy_admin_service.get_policy_settings(db)
|
||||
updated_by_name = (
|
||||
policy.updated_by.full_name if getattr(policy, "updated_by", None) else None
|
||||
)
|
||||
updated_by_name = policy.updated_by.full_name if getattr(policy, 'updated_by', None) else None
|
||||
return CookiePolicySettingsResponse(data=settings, updated_at=policy.updated_at, updated_by=updated_by_name)
|
||||
|
||||
return CookiePolicySettingsResponse(
|
||||
data=settings,
|
||||
updated_at=policy.updated_at,
|
||||
updated_by=updated_by_name,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/integrations",
|
||||
response_model=CookieIntegrationSettingsResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
def get_cookie_integrations(
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(authorize_roles("admin")),
|
||||
) -> CookieIntegrationSettingsResponse:
|
||||
"""
|
||||
Get IDs for third-party integrations (admin only).
|
||||
"""
|
||||
@router.get('/integrations', response_model=CookieIntegrationSettingsResponse, status_code=status.HTTP_200_OK)
|
||||
def get_cookie_integrations(db: Session=Depends(get_db), _: User=Depends(authorize_roles('admin'))) -> CookieIntegrationSettingsResponse:
|
||||
settings = privacy_admin_service.get_integration_settings(db)
|
||||
cfg = privacy_admin_service.get_or_create_integrations(db)
|
||||
updated_by_name = (
|
||||
cfg.updated_by.full_name if getattr(cfg, "updated_by", None) else None
|
||||
)
|
||||
updated_by_name = cfg.updated_by.full_name if getattr(cfg, 'updated_by', None) else None
|
||||
return CookieIntegrationSettingsResponse(data=settings, updated_at=cfg.updated_at, updated_by=updated_by_name)
|
||||
|
||||
return CookieIntegrationSettingsResponse(
|
||||
data=settings,
|
||||
updated_at=cfg.updated_at,
|
||||
updated_by=updated_by_name,
|
||||
)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/integrations",
|
||||
response_model=CookieIntegrationSettingsResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
def update_cookie_integrations(
|
||||
payload: CookieIntegrationSettings,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
) -> CookieIntegrationSettingsResponse:
|
||||
"""
|
||||
Update IDs for third-party integrations (admin only).
|
||||
"""
|
||||
@router.put('/integrations', response_model=CookieIntegrationSettingsResponse, status_code=status.HTTP_200_OK)
|
||||
def update_cookie_integrations(payload: CookieIntegrationSettings, db: Session=Depends(get_db), current_user: User=Depends(authorize_roles('admin'))) -> CookieIntegrationSettingsResponse:
|
||||
cfg = privacy_admin_service.update_integrations(db, payload, current_user)
|
||||
settings = privacy_admin_service.get_integration_settings(db)
|
||||
updated_by_name = (
|
||||
cfg.updated_by.full_name if getattr(cfg, "updated_by", None) else None
|
||||
)
|
||||
|
||||
return CookieIntegrationSettingsResponse(
|
||||
data=settings,
|
||||
updated_at=cfg.updated_at,
|
||||
updated_by=updated_by_name,
|
||||
)
|
||||
|
||||
|
||||
updated_by_name = cfg.updated_by.full_name if getattr(cfg, 'updated_by', None) else None
|
||||
return CookieIntegrationSettingsResponse(data=settings, updated_at=cfg.updated_at, updated_by=updated_by_name)
|
||||
@@ -3,237 +3,91 @@ from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc, or_, func
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ..models.audit_log import AuditLog
|
||||
router = APIRouter(prefix='/audit-logs', tags=['audit-logs'])
|
||||
|
||||
router = APIRouter(prefix="/audit-logs", tags=["audit-logs"])
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_audit_logs(
|
||||
action: Optional[str] = Query(None, description="Filter by action"),
|
||||
resource_type: Optional[str] = Query(None, description="Filter by resource type"),
|
||||
user_id: Optional[int] = Query(None, description="Filter by user ID"),
|
||||
status_filter: Optional[str] = Query(None, alias="status", description="Filter by status"),
|
||||
search: Optional[str] = Query(None, description="Search in action, resource_type, or details"),
|
||||
start_date: Optional[str] = Query(None, description="Start date (YYYY-MM-DD)"),
|
||||
end_date: Optional[str] = Query(None, description="End date (YYYY-MM-DD)"),
|
||||
page: int = Query(1, ge=1, description="Page number"),
|
||||
limit: int = Query(20, ge=1, le=100, description="Items per page"),
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get audit logs (Admin only)"""
|
||||
@router.get('/')
|
||||
async def get_audit_logs(action: Optional[str]=Query(None, description='Filter by action'), resource_type: Optional[str]=Query(None, description='Filter by resource type'), user_id: Optional[int]=Query(None, description='Filter by user ID'), status_filter: Optional[str]=Query(None, alias='status', description='Filter by status'), search: Optional[str]=Query(None, description='Search in action, resource_type, or details'), start_date: Optional[str]=Query(None, description='Start date (YYYY-MM-DD)'), end_date: Optional[str]=Query(None, description='End date (YYYY-MM-DD)'), page: int=Query(1, ge=1, description='Page number'), limit: int=Query(20, ge=1, le=100, description='Items per page'), current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
query = db.query(AuditLog)
|
||||
|
||||
# Apply filters
|
||||
if action:
|
||||
query = query.filter(AuditLog.action.like(f"%{action}%"))
|
||||
|
||||
query = query.filter(AuditLog.action.like(f'%{action}%'))
|
||||
if resource_type:
|
||||
query = query.filter(AuditLog.resource_type == resource_type)
|
||||
|
||||
if user_id:
|
||||
query = query.filter(AuditLog.user_id == user_id)
|
||||
|
||||
if status_filter:
|
||||
query = query.filter(AuditLog.status == status_filter)
|
||||
|
||||
if search:
|
||||
search_filter = or_(
|
||||
AuditLog.action.like(f"%{search}%"),
|
||||
AuditLog.resource_type.like(f"%{search}%"),
|
||||
AuditLog.ip_address.like(f"%{search}%")
|
||||
)
|
||||
search_filter = or_(AuditLog.action.like(f'%{search}%'), AuditLog.resource_type.like(f'%{search}%'), AuditLog.ip_address.like(f'%{search}%'))
|
||||
query = query.filter(search_filter)
|
||||
|
||||
# Date range filter
|
||||
if start_date:
|
||||
try:
|
||||
start = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
start = datetime.strptime(start_date, '%Y-%m-%d')
|
||||
query = query.filter(AuditLog.created_at >= start)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if end_date:
|
||||
try:
|
||||
end = datetime.strptime(end_date, "%Y-%m-%d")
|
||||
# Set to end of day
|
||||
end = datetime.strptime(end_date, '%Y-%m-%d')
|
||||
end = end.replace(hour=23, minute=59, second=59)
|
||||
query = query.filter(AuditLog.created_at <= end)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Get total count
|
||||
total = query.count()
|
||||
|
||||
# Apply pagination and ordering
|
||||
offset = (page - 1) * limit
|
||||
logs = query.order_by(desc(AuditLog.created_at)).offset(offset).limit(limit).all()
|
||||
|
||||
# Format response
|
||||
result = []
|
||||
for log in logs:
|
||||
log_dict = {
|
||||
"id": log.id,
|
||||
"user_id": log.user_id,
|
||||
"action": log.action,
|
||||
"resource_type": log.resource_type,
|
||||
"resource_id": log.resource_id,
|
||||
"ip_address": log.ip_address,
|
||||
"user_agent": log.user_agent,
|
||||
"request_id": log.request_id,
|
||||
"details": log.details,
|
||||
"status": log.status,
|
||||
"error_message": log.error_message,
|
||||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||||
}
|
||||
|
||||
# Add user info if available
|
||||
log_dict = {'id': log.id, 'user_id': log.user_id, 'action': log.action, 'resource_type': log.resource_type, 'resource_id': log.resource_id, 'ip_address': log.ip_address, 'user_agent': log.user_agent, 'request_id': log.request_id, 'details': log.details, 'status': log.status, 'error_message': log.error_message, 'created_at': log.created_at.isoformat() if log.created_at else None}
|
||||
if log.user:
|
||||
log_dict["user"] = {
|
||||
"id": log.user.id,
|
||||
"full_name": log.user.full_name,
|
||||
"email": log.user.email,
|
||||
}
|
||||
|
||||
log_dict['user'] = {'id': log.user.id, 'full_name': log.user.full_name, 'email': log.user.email}
|
||||
result.append(log_dict)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"logs": result,
|
||||
"pagination": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"totalPages": (total + limit - 1) // limit,
|
||||
},
|
||||
},
|
||||
}
|
||||
return {'status': 'success', 'data': {'logs': result, 'pagination': {'total': total, 'page': page, 'limit': limit, 'totalPages': (total + limit - 1) // limit}}}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_audit_stats(
|
||||
start_date: Optional[str] = Query(None, description="Start date (YYYY-MM-DD)"),
|
||||
end_date: Optional[str] = Query(None, description="End date (YYYY-MM-DD)"),
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get audit log statistics (Admin only)"""
|
||||
@router.get('/stats')
|
||||
async def get_audit_stats(start_date: Optional[str]=Query(None, description='Start date (YYYY-MM-DD)'), end_date: Optional[str]=Query(None, description='End date (YYYY-MM-DD)'), current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
query = db.query(AuditLog)
|
||||
|
||||
# Date range filter
|
||||
if start_date:
|
||||
try:
|
||||
start = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
start = datetime.strptime(start_date, '%Y-%m-%d')
|
||||
query = query.filter(AuditLog.created_at >= start)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if end_date:
|
||||
try:
|
||||
end = datetime.strptime(end_date, "%Y-%m-%d")
|
||||
end = datetime.strptime(end_date, '%Y-%m-%d')
|
||||
end = end.replace(hour=23, minute=59, second=59)
|
||||
query = query.filter(AuditLog.created_at <= end)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Get statistics
|
||||
total_logs = query.count()
|
||||
success_count = query.filter(AuditLog.status == "success").count()
|
||||
failed_count = query.filter(AuditLog.status == "failed").count()
|
||||
error_count = query.filter(AuditLog.status == "error").count()
|
||||
|
||||
# Get top actions
|
||||
top_actions = (
|
||||
db.query(
|
||||
AuditLog.action,
|
||||
func.count(AuditLog.id).label("count")
|
||||
)
|
||||
.group_by(AuditLog.action)
|
||||
.order_by(desc("count"))
|
||||
.limit(10)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Get top resource types
|
||||
top_resource_types = (
|
||||
db.query(
|
||||
AuditLog.resource_type,
|
||||
func.count(AuditLog.id).label("count")
|
||||
)
|
||||
.group_by(AuditLog.resource_type)
|
||||
.order_by(desc("count"))
|
||||
.limit(10)
|
||||
.all()
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"total": total_logs,
|
||||
"by_status": {
|
||||
"success": success_count,
|
||||
"failed": failed_count,
|
||||
"error": error_count,
|
||||
},
|
||||
"top_actions": [{"action": action, "count": count} for action, count in top_actions],
|
||||
"top_resource_types": [{"resource_type": rt, "count": count} for rt, count in top_resource_types],
|
||||
},
|
||||
}
|
||||
success_count = query.filter(AuditLog.status == 'success').count()
|
||||
failed_count = query.filter(AuditLog.status == 'failed').count()
|
||||
error_count = query.filter(AuditLog.status == 'error').count()
|
||||
top_actions = db.query(AuditLog.action, func.count(AuditLog.id).label('count')).group_by(AuditLog.action).order_by(desc('count')).limit(10).all()
|
||||
top_resource_types = db.query(AuditLog.resource_type, func.count(AuditLog.id).label('count')).group_by(AuditLog.resource_type).order_by(desc('count')).limit(10).all()
|
||||
return {'status': 'success', 'data': {'total': total_logs, 'by_status': {'success': success_count, 'failed': failed_count, 'error': error_count}, 'top_actions': [{'action': action, 'count': count} for action, count in top_actions], 'top_resource_types': [{'resource_type': rt, 'count': count} for rt, count in top_resource_types]}}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{id}")
|
||||
async def get_audit_log_by_id(
|
||||
id: int,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get audit log by ID (Admin only)"""
|
||||
@router.get('/{id}')
|
||||
async def get_audit_log_by_id(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
log = db.query(AuditLog).filter(AuditLog.id == id).first()
|
||||
|
||||
if not log:
|
||||
raise HTTPException(status_code=404, detail="Audit log not found")
|
||||
|
||||
log_dict = {
|
||||
"id": log.id,
|
||||
"user_id": log.user_id,
|
||||
"action": log.action,
|
||||
"resource_type": log.resource_type,
|
||||
"resource_id": log.resource_id,
|
||||
"ip_address": log.ip_address,
|
||||
"user_agent": log.user_agent,
|
||||
"request_id": log.request_id,
|
||||
"details": log.details,
|
||||
"status": log.status,
|
||||
"error_message": log.error_message,
|
||||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||||
}
|
||||
|
||||
raise HTTPException(status_code=404, detail='Audit log not found')
|
||||
log_dict = {'id': log.id, 'user_id': log.user_id, 'action': log.action, 'resource_type': log.resource_type, 'resource_id': log.resource_id, 'ip_address': log.ip_address, 'user_agent': log.user_agent, 'request_id': log.request_id, 'details': log.details, 'status': log.status, 'error_message': log.error_message, 'created_at': log.created_at.isoformat() if log.created_at else None}
|
||||
if log.user:
|
||||
log_dict["user"] = {
|
||||
"id": log.user.id,
|
||||
"full_name": log.user.full_name,
|
||||
"email": log.user.email,
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {"log": log_dict}
|
||||
}
|
||||
log_dict['user'] = {'id': log.user.id, 'full_name': log.user.full_name, 'email': log.user.email}
|
||||
return {'status': 'success', 'data': {'log': log_dict}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
@@ -5,518 +5,193 @@ from pathlib import Path
|
||||
import aiofiles
|
||||
import uuid
|
||||
import os
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..services.auth_service import auth_service
|
||||
from ..schemas.auth import (
|
||||
RegisterRequest,
|
||||
LoginRequest,
|
||||
RefreshTokenRequest,
|
||||
ForgotPasswordRequest,
|
||||
ResetPasswordRequest,
|
||||
AuthResponse,
|
||||
TokenResponse,
|
||||
MessageResponse,
|
||||
MFAInitResponse,
|
||||
EnableMFARequest,
|
||||
VerifyMFARequest,
|
||||
MFAStatusResponse
|
||||
)
|
||||
from ..schemas.auth import RegisterRequest, LoginRequest, RefreshTokenRequest, ForgotPasswordRequest, ResetPasswordRequest, AuthResponse, TokenResponse, MessageResponse, MFAInitResponse, EnableMFARequest, VerifyMFARequest, MFAStatusResponse
|
||||
from ..middleware.auth import get_current_user
|
||||
from ..models.user import User
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
router = APIRouter(prefix='/auth', tags=['auth'])
|
||||
|
||||
def get_base_url(request: Request) -> str:
|
||||
"""Get base URL for image normalization"""
|
||||
return os.getenv("SERVER_URL") or f"http://{request.headers.get('host', 'localhost:8000')}"
|
||||
|
||||
return os.getenv('SERVER_URL') or f'http://{request.headers.get('host', 'localhost:8000')}'
|
||||
|
||||
def normalize_image_url(image_url: str, base_url: str) -> str:
|
||||
"""Normalize image URL to absolute URL"""
|
||||
if not image_url:
|
||||
return image_url
|
||||
if image_url.startswith('http://') or image_url.startswith('https://'):
|
||||
return image_url
|
||||
if image_url.startswith('/'):
|
||||
return f"{base_url}{image_url}"
|
||||
return f"{base_url}/{image_url}"
|
||||
return f'{base_url}{image_url}'
|
||||
return f'{base_url}/{image_url}'
|
||||
|
||||
|
||||
@router.post("/register", status_code=status.HTTP_201_CREATED)
|
||||
async def register(
|
||||
request: RegisterRequest,
|
||||
response: Response,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Register new user"""
|
||||
@router.post('/register', status_code=status.HTTP_201_CREATED)
|
||||
async def register(request: RegisterRequest, response: Response, db: Session=Depends(get_db)):
|
||||
try:
|
||||
result = await auth_service.register(
|
||||
db=db,
|
||||
name=request.name,
|
||||
email=request.email,
|
||||
password=request.password,
|
||||
phone=request.phone
|
||||
)
|
||||
|
||||
# Set refresh token as HttpOnly cookie
|
||||
response.set_cookie(
|
||||
key="refreshToken",
|
||||
value=result["refreshToken"],
|
||||
httponly=True,
|
||||
secure=False, # Set to True in production with HTTPS
|
||||
samesite="strict",
|
||||
max_age=7 * 24 * 60 * 60, # 7 days
|
||||
path="/"
|
||||
)
|
||||
|
||||
# Format response to match frontend expectations
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Registration successful",
|
||||
"data": {
|
||||
"token": result["token"],
|
||||
"user": result["user"]
|
||||
}
|
||||
}
|
||||
result = await auth_service.register(db=db, name=request.name, email=request.email, password=request.password, phone=request.phone)
|
||||
response.set_cookie(key='refreshToken', value=result['refreshToken'], httponly=True, secure=False, samesite='strict', max_age=7 * 24 * 60 * 60, path='/')
|
||||
return {'status': 'success', 'message': 'Registration successful', 'data': {'token': result['token'], 'user': result['user']}}
|
||||
except ValueError as e:
|
||||
error_message = str(e)
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={
|
||||
"status": "error",
|
||||
"message": error_message
|
||||
}
|
||||
)
|
||||
return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST, content={'status': 'error', 'message': error_message})
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
async def login(
|
||||
request: LoginRequest,
|
||||
response: Response,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Login user"""
|
||||
@router.post('/login')
|
||||
async def login(request: LoginRequest, response: Response, db: Session=Depends(get_db)):
|
||||
try:
|
||||
result = await auth_service.login(
|
||||
db=db,
|
||||
email=request.email,
|
||||
password=request.password,
|
||||
remember_me=request.rememberMe or False,
|
||||
mfa_token=request.mfaToken
|
||||
)
|
||||
|
||||
# Check if MFA is required
|
||||
if result.get("requires_mfa"):
|
||||
return {
|
||||
"status": "success",
|
||||
"requires_mfa": True,
|
||||
"user_id": result["user_id"]
|
||||
}
|
||||
|
||||
# Set refresh token as HttpOnly cookie
|
||||
result = await auth_service.login(db=db, email=request.email, password=request.password, remember_me=request.rememberMe or False, mfa_token=request.mfaToken)
|
||||
if result.get('requires_mfa'):
|
||||
return {'status': 'success', 'requires_mfa': True, 'user_id': result['user_id']}
|
||||
max_age = 7 * 24 * 60 * 60 if request.rememberMe else 1 * 24 * 60 * 60
|
||||
response.set_cookie(
|
||||
key="refreshToken",
|
||||
value=result["refreshToken"],
|
||||
httponly=True,
|
||||
secure=False, # Set to True in production with HTTPS
|
||||
samesite="strict",
|
||||
max_age=max_age,
|
||||
path="/"
|
||||
)
|
||||
|
||||
# Format response to match frontend expectations
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"token": result["token"],
|
||||
"user": result["user"]
|
||||
}
|
||||
}
|
||||
response.set_cookie(key='refreshToken', value=result['refreshToken'], httponly=True, secure=False, samesite='strict', max_age=max_age, path='/')
|
||||
return {'status': 'success', 'data': {'token': result['token'], 'user': result['user']}}
|
||||
except ValueError as e:
|
||||
error_message = str(e)
|
||||
status_code = status.HTTP_401_UNAUTHORIZED if "Invalid email or password" in error_message or "Invalid MFA token" in error_message else status.HTTP_400_BAD_REQUEST
|
||||
return JSONResponse(
|
||||
status_code=status_code,
|
||||
content={
|
||||
"status": "error",
|
||||
"message": error_message
|
||||
}
|
||||
)
|
||||
status_code = status.HTTP_401_UNAUTHORIZED if 'Invalid email or password' in error_message or 'Invalid MFA token' in error_message else status.HTTP_400_BAD_REQUEST
|
||||
return JSONResponse(status_code=status_code, content={'status': 'error', 'message': error_message})
|
||||
|
||||
|
||||
@router.post("/refresh-token", response_model=TokenResponse)
|
||||
async def refresh_token(
|
||||
refreshToken: str = Cookie(None),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Refresh access token"""
|
||||
@router.post('/refresh-token', response_model=TokenResponse)
|
||||
async def refresh_token(refreshToken: str=Cookie(None), db: Session=Depends(get_db)):
|
||||
if not refreshToken:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Refresh token not found"
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Refresh token not found')
|
||||
try:
|
||||
result = await auth_service.refresh_access_token(db, refreshToken)
|
||||
return result
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=str(e)
|
||||
)
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/logout", response_model=MessageResponse)
|
||||
async def logout(
|
||||
response: Response,
|
||||
refreshToken: str = Cookie(None),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Logout user"""
|
||||
@router.post('/logout', response_model=MessageResponse)
|
||||
async def logout(response: Response, refreshToken: str=Cookie(None), db: Session=Depends(get_db)):
|
||||
if refreshToken:
|
||||
await auth_service.logout(db, refreshToken)
|
||||
response.delete_cookie(key='refreshToken', path='/')
|
||||
return {'status': 'success', 'message': 'Logout successful'}
|
||||
|
||||
# Clear refresh token cookie
|
||||
response.delete_cookie(key="refreshToken", path="/")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Logout successful"
|
||||
}
|
||||
|
||||
|
||||
@router.get("/profile")
|
||||
async def get_profile(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get current user profile"""
|
||||
@router.get('/profile')
|
||||
async def get_profile(current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
try:
|
||||
user = await auth_service.get_profile(db, current_user.id)
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"user": user
|
||||
}
|
||||
}
|
||||
return {'status': 'success', 'data': {'user': user}}
|
||||
except ValueError as e:
|
||||
if "User not found" in str(e):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e)
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
if 'User not found' in str(e):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/profile")
|
||||
async def update_profile(
|
||||
profile_data: dict,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update current user profile"""
|
||||
@router.put('/profile')
|
||||
async def update_profile(profile_data: dict, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
try:
|
||||
user = await auth_service.update_profile(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
full_name=profile_data.get("full_name"),
|
||||
email=profile_data.get("email"),
|
||||
phone_number=profile_data.get("phone_number"),
|
||||
password=profile_data.get("password"),
|
||||
current_password=profile_data.get("currentPassword"),
|
||||
currency=profile_data.get("currency")
|
||||
)
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Profile updated successfully",
|
||||
"data": {
|
||||
"user": user
|
||||
}
|
||||
}
|
||||
user = await auth_service.update_profile(db=db, user_id=current_user.id, full_name=profile_data.get('full_name'), email=profile_data.get('email'), phone_number=profile_data.get('phone_number'), password=profile_data.get('password'), current_password=profile_data.get('currentPassword'), currency=profile_data.get('currency'))
|
||||
return {'status': 'success', 'message': 'Profile updated successfully', 'data': {'user': user}}
|
||||
except ValueError as e:
|
||||
error_message = str(e)
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
if "not found" in error_message.lower():
|
||||
if 'not found' in error_message.lower():
|
||||
status_code = status.HTTP_404_NOT_FOUND
|
||||
raise HTTPException(
|
||||
status_code=status_code,
|
||||
detail=error_message
|
||||
)
|
||||
raise HTTPException(status_code=status_code, detail=error_message)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"An error occurred: {str(e)}"
|
||||
)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'An error occurred: {str(e)}')
|
||||
|
||||
|
||||
@router.post("/forgot-password", response_model=MessageResponse)
|
||||
async def forgot_password(
|
||||
request: ForgotPasswordRequest,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Send password reset link"""
|
||||
@router.post('/forgot-password', response_model=MessageResponse)
|
||||
async def forgot_password(request: ForgotPasswordRequest, db: Session=Depends(get_db)):
|
||||
result = await auth_service.forgot_password(db, request.email)
|
||||
return {
|
||||
"status": "success",
|
||||
"message": result["message"]
|
||||
}
|
||||
return {'status': 'success', 'message': result['message']}
|
||||
|
||||
|
||||
@router.post("/reset-password", response_model=MessageResponse)
|
||||
async def reset_password(
|
||||
request: ResetPasswordRequest,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Reset password with token"""
|
||||
@router.post('/reset-password', response_model=MessageResponse)
|
||||
async def reset_password(request: ResetPasswordRequest, db: Session=Depends(get_db)):
|
||||
try:
|
||||
result = await auth_service.reset_password(
|
||||
db=db,
|
||||
token=request.token,
|
||||
password=request.password
|
||||
)
|
||||
return {
|
||||
"status": "success",
|
||||
"message": result["message"]
|
||||
}
|
||||
result = await auth_service.reset_password(db=db, token=request.token, password=request.password)
|
||||
return {'status': 'success', 'message': result['message']}
|
||||
except ValueError as e:
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
if "User not found" in str(e):
|
||||
if 'User not found' in str(e):
|
||||
status_code = status.HTTP_404_NOT_FOUND
|
||||
raise HTTPException(
|
||||
status_code=status_code,
|
||||
detail=str(e)
|
||||
)
|
||||
|
||||
|
||||
# MFA Routes
|
||||
raise HTTPException(status_code=status_code, detail=str(e))
|
||||
from ..services.mfa_service import mfa_service
|
||||
from ..config.settings import settings
|
||||
|
||||
|
||||
@router.get("/mfa/init")
|
||||
async def init_mfa(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Initialize MFA setup - generate secret and QR code"""
|
||||
@router.get('/mfa/init')
|
||||
async def init_mfa(current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
try:
|
||||
if current_user.mfa_enabled:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="MFA is already enabled"
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='MFA is already enabled')
|
||||
secret = mfa_service.generate_secret()
|
||||
app_name = getattr(settings, 'APP_NAME', 'Hotel Booking')
|
||||
qr_code = mfa_service.generate_qr_code(secret, current_user.email, app_name)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"secret": secret,
|
||||
"qr_code": qr_code
|
||||
}
|
||||
}
|
||||
return {'status': 'success', 'data': {'secret': secret, 'qr_code': qr_code}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error initializing MFA: {str(e)}"
|
||||
)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error initializing MFA: {str(e)}')
|
||||
|
||||
|
||||
@router.post("/mfa/enable")
|
||||
async def enable_mfa(
|
||||
request: EnableMFARequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Enable MFA after verifying token"""
|
||||
@router.post('/mfa/enable')
|
||||
async def enable_mfa(request: EnableMFARequest, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
try:
|
||||
success, backup_codes = mfa_service.enable_mfa(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
secret=request.secret,
|
||||
verification_token=request.verification_token
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "MFA enabled successfully",
|
||||
"data": {
|
||||
"backup_codes": backup_codes
|
||||
}
|
||||
}
|
||||
success, backup_codes = mfa_service.enable_mfa(db=db, user_id=current_user.id, secret=request.secret, verification_token=request.verification_token)
|
||||
return {'status': 'success', 'message': 'MFA enabled successfully', 'data': {'backup_codes': backup_codes}}
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error enabling MFA: {str(e)}"
|
||||
)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error enabling MFA: {str(e)}')
|
||||
|
||||
|
||||
@router.post("/mfa/disable")
|
||||
async def disable_mfa(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Disable MFA"""
|
||||
@router.post('/mfa/disable')
|
||||
async def disable_mfa(current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
try:
|
||||
mfa_service.disable_mfa(db=db, user_id=current_user.id)
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "MFA disabled successfully"
|
||||
}
|
||||
return {'status': 'success', 'message': 'MFA disabled successfully'}
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error disabling MFA: {str(e)}"
|
||||
)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error disabling MFA: {str(e)}')
|
||||
|
||||
|
||||
@router.get("/mfa/status", response_model=MFAStatusResponse)
|
||||
async def get_mfa_status(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get MFA status for current user"""
|
||||
@router.get('/mfa/status', response_model=MFAStatusResponse)
|
||||
async def get_mfa_status(current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
try:
|
||||
status_data = mfa_service.get_mfa_status(db=db, user_id=current_user.id)
|
||||
return status_data
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error getting MFA status: {str(e)}"
|
||||
)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error getting MFA status: {str(e)}')
|
||||
|
||||
|
||||
@router.post("/mfa/regenerate-backup-codes")
|
||||
async def regenerate_backup_codes(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Regenerate backup codes for MFA"""
|
||||
@router.post('/mfa/regenerate-backup-codes')
|
||||
async def regenerate_backup_codes(current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
try:
|
||||
backup_codes = mfa_service.regenerate_backup_codes(db=db, user_id=current_user.id)
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Backup codes regenerated successfully",
|
||||
"data": {
|
||||
"backup_codes": backup_codes
|
||||
}
|
||||
}
|
||||
return {'status': 'success', 'message': 'Backup codes regenerated successfully', 'data': {'backup_codes': backup_codes}}
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error regenerating backup codes: {str(e)}"
|
||||
)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error regenerating backup codes: {str(e)}')
|
||||
|
||||
|
||||
@router.post("/avatar/upload")
|
||||
async def upload_avatar(
|
||||
request: Request,
|
||||
image: UploadFile = File(...),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Upload user avatar"""
|
||||
@router.post('/avatar/upload')
|
||||
async def upload_avatar(request: Request, image: UploadFile=File(...), current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
try:
|
||||
# Validate file type
|
||||
if not image.content_type or not image.content_type.startswith('image/'):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="File must be an image"
|
||||
)
|
||||
|
||||
# Validate file size (max 2MB)
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='File must be an image')
|
||||
content = await image.read()
|
||||
if len(content) > 2 * 1024 * 1024: # 2MB
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Avatar file size must be less than 2MB"
|
||||
)
|
||||
|
||||
# Create uploads directory
|
||||
upload_dir = Path(__file__).parent.parent.parent / "uploads" / "avatars"
|
||||
if len(content) > 2 * 1024 * 1024:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Avatar file size must be less than 2MB')
|
||||
upload_dir = Path(__file__).parent.parent.parent / 'uploads' / 'avatars'
|
||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Delete old avatar if exists
|
||||
if current_user.avatar:
|
||||
old_avatar_path = Path(__file__).parent.parent.parent / current_user.avatar.lstrip('/')
|
||||
if old_avatar_path.exists() and old_avatar_path.is_file():
|
||||
try:
|
||||
old_avatar_path.unlink()
|
||||
except Exception:
|
||||
pass # Ignore deletion errors
|
||||
|
||||
# Generate filename
|
||||
pass
|
||||
ext = Path(image.filename).suffix or '.png'
|
||||
filename = f"avatar-{current_user.id}-{uuid.uuid4()}{ext}"
|
||||
filename = f'avatar-{current_user.id}-{uuid.uuid4()}{ext}'
|
||||
file_path = upload_dir / filename
|
||||
|
||||
# Save file
|
||||
async with aiofiles.open(file_path, 'wb') as f:
|
||||
await f.write(content)
|
||||
|
||||
# Update user avatar
|
||||
image_url = f"/uploads/avatars/{filename}"
|
||||
image_url = f'/uploads/avatars/{filename}'
|
||||
current_user.avatar = image_url
|
||||
db.commit()
|
||||
db.refresh(current_user)
|
||||
|
||||
# Return the image URL
|
||||
base_url = get_base_url(request)
|
||||
full_url = normalize_image_url(image_url, base_url)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"status": "success",
|
||||
"message": "Avatar uploaded successfully",
|
||||
"data": {
|
||||
"avatar_url": image_url,
|
||||
"full_url": full_url,
|
||||
"user": {
|
||||
"id": current_user.id,
|
||||
"name": current_user.full_name,
|
||||
"email": current_user.email,
|
||||
"phone": current_user.phone,
|
||||
"avatar": image_url,
|
||||
"role": current_user.role.name if current_user.role else "customer"
|
||||
}
|
||||
}
|
||||
}
|
||||
return {'success': True, 'status': 'success', 'message': 'Avatar uploaded successfully', 'data': {'avatar_url': image_url, 'full_url': full_url, 'user': {'id': current_user.id, 'name': current_user.full_name, 'email': current_user.email, 'phone': current_user.phone, 'avatar': image_url, 'role': current_user.role.name if current_user.role else 'customer'}}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error uploading avatar: {str(e)}"
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error uploading avatar: {str(e)}')
|
||||
@@ -7,300 +7,144 @@ from pathlib import Path
|
||||
import os
|
||||
import aiofiles
|
||||
import uuid
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ..models.banner import Banner
|
||||
|
||||
router = APIRouter(prefix="/banners", tags=["banners"])
|
||||
|
||||
router = APIRouter(prefix='/banners', tags=['banners'])
|
||||
|
||||
def normalize_image_url(image_url: str, base_url: str) -> str:
|
||||
"""Normalize image URL to absolute URL"""
|
||||
if not image_url:
|
||||
return image_url
|
||||
if image_url.startswith('http://') or image_url.startswith('https://'):
|
||||
return image_url
|
||||
if image_url.startswith('/'):
|
||||
return f"{base_url}{image_url}"
|
||||
return f"{base_url}/{image_url}"
|
||||
|
||||
return f'{base_url}{image_url}'
|
||||
return f'{base_url}/{image_url}'
|
||||
|
||||
def get_base_url(request: Request) -> str:
|
||||
"""Get base URL for image normalization"""
|
||||
return os.getenv("SERVER_URL") or f"http://{request.headers.get('host', 'localhost:3000')}"
|
||||
return os.getenv('SERVER_URL') or f'http://{request.headers.get('host', 'localhost:3000')}'
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_banners(
|
||||
request: Request,
|
||||
position: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get all active banners"""
|
||||
@router.get('/')
|
||||
async def get_banners(request: Request, position: Optional[str]=Query(None), db: Session=Depends(get_db)):
|
||||
try:
|
||||
query = db.query(Banner).filter(Banner.is_active == True)
|
||||
|
||||
# Filter by position
|
||||
if position:
|
||||
query = query.filter(Banner.position == position)
|
||||
|
||||
# Filter by date range
|
||||
now = datetime.utcnow()
|
||||
query = query.filter(
|
||||
or_(
|
||||
Banner.start_date == None,
|
||||
Banner.start_date <= now
|
||||
)
|
||||
).filter(
|
||||
or_(
|
||||
Banner.end_date == None,
|
||||
Banner.end_date >= now
|
||||
)
|
||||
)
|
||||
|
||||
query = query.filter(or_(Banner.start_date == None, Banner.start_date <= now)).filter(or_(Banner.end_date == None, Banner.end_date >= now))
|
||||
banners = query.order_by(Banner.display_order.asc(), Banner.created_at.desc()).all()
|
||||
|
||||
base_url = get_base_url(request)
|
||||
result = []
|
||||
for banner in banners:
|
||||
banner_dict = {
|
||||
"id": banner.id,
|
||||
"title": banner.title,
|
||||
"description": banner.description,
|
||||
"image_url": normalize_image_url(banner.image_url, base_url),
|
||||
"link_url": banner.link_url,
|
||||
"position": banner.position,
|
||||
"display_order": banner.display_order,
|
||||
"is_active": banner.is_active,
|
||||
"start_date": banner.start_date.isoformat() if banner.start_date else None,
|
||||
"end_date": banner.end_date.isoformat() if banner.end_date else None,
|
||||
"created_at": banner.created_at.isoformat() if banner.created_at else None,
|
||||
}
|
||||
banner_dict = {'id': banner.id, 'title': banner.title, 'description': banner.description, 'image_url': normalize_image_url(banner.image_url, base_url), 'link_url': banner.link_url, 'position': banner.position, 'display_order': banner.display_order, 'is_active': banner.is_active, 'start_date': banner.start_date.isoformat() if banner.start_date else None, 'end_date': banner.end_date.isoformat() if banner.end_date else None, 'created_at': banner.created_at.isoformat() if banner.created_at else None}
|
||||
result.append(banner_dict)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {"banners": result}
|
||||
}
|
||||
return {'status': 'success', 'data': {'banners': result}}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{id}")
|
||||
async def get_banner_by_id(
|
||||
id: int,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get banner by ID"""
|
||||
@router.get('/{id}')
|
||||
async def get_banner_by_id(id: int, request: Request, db: Session=Depends(get_db)):
|
||||
try:
|
||||
banner = db.query(Banner).filter(Banner.id == id).first()
|
||||
if not banner:
|
||||
raise HTTPException(status_code=404, detail="Banner not found")
|
||||
|
||||
raise HTTPException(status_code=404, detail='Banner not found')
|
||||
base_url = get_base_url(request)
|
||||
banner_dict = {
|
||||
"id": banner.id,
|
||||
"title": banner.title,
|
||||
"description": banner.description,
|
||||
"image_url": normalize_image_url(banner.image_url, base_url),
|
||||
"link_url": banner.link_url,
|
||||
"position": banner.position,
|
||||
"display_order": banner.display_order,
|
||||
"is_active": banner.is_active,
|
||||
"start_date": banner.start_date.isoformat() if banner.start_date else None,
|
||||
"end_date": banner.end_date.isoformat() if banner.end_date else None,
|
||||
"created_at": banner.created_at.isoformat() if banner.created_at else None,
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {"banner": banner_dict}
|
||||
}
|
||||
banner_dict = {'id': banner.id, 'title': banner.title, 'description': banner.description, 'image_url': normalize_image_url(banner.image_url, base_url), 'link_url': banner.link_url, 'position': banner.position, 'display_order': banner.display_order, 'is_active': banner.is_active, 'start_date': banner.start_date.isoformat() if banner.start_date else None, 'end_date': banner.end_date.isoformat() if banner.end_date else None, 'created_at': banner.created_at.isoformat() if banner.created_at else None}
|
||||
return {'status': 'success', 'data': {'banner': banner_dict}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/", dependencies=[Depends(authorize_roles("admin"))])
|
||||
async def create_banner(
|
||||
banner_data: dict,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create new banner (Admin only)"""
|
||||
@router.post('/', dependencies=[Depends(authorize_roles('admin'))])
|
||||
async def create_banner(banner_data: dict, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
banner = Banner(
|
||||
title=banner_data.get("title"),
|
||||
description=banner_data.get("description"),
|
||||
image_url=banner_data.get("image_url"),
|
||||
link_url=banner_data.get("link"),
|
||||
position=banner_data.get("position", "home"),
|
||||
display_order=banner_data.get("display_order", 0),
|
||||
is_active=True,
|
||||
start_date=datetime.fromisoformat(banner_data["start_date"].replace('Z', '+00:00')) if banner_data.get("start_date") else None,
|
||||
end_date=datetime.fromisoformat(banner_data["end_date"].replace('Z', '+00:00')) if banner_data.get("end_date") else None,
|
||||
)
|
||||
|
||||
banner = Banner(title=banner_data.get('title'), description=banner_data.get('description'), image_url=banner_data.get('image_url'), link_url=banner_data.get('link'), position=banner_data.get('position', 'home'), display_order=banner_data.get('display_order', 0), is_active=True, start_date=datetime.fromisoformat(banner_data['start_date'].replace('Z', '+00:00')) if banner_data.get('start_date') else None, end_date=datetime.fromisoformat(banner_data['end_date'].replace('Z', '+00:00')) if banner_data.get('end_date') else None)
|
||||
db.add(banner)
|
||||
db.commit()
|
||||
db.refresh(banner)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Banner created successfully",
|
||||
"data": {"banner": banner}
|
||||
}
|
||||
return {'status': 'success', 'message': 'Banner created successfully', 'data': {'banner': banner}}
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/{id}", dependencies=[Depends(authorize_roles("admin"))])
|
||||
async def update_banner(
|
||||
id: int,
|
||||
banner_data: dict,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update banner (Admin only)"""
|
||||
@router.put('/{id}', dependencies=[Depends(authorize_roles('admin'))])
|
||||
async def update_banner(id: int, banner_data: dict, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
banner = db.query(Banner).filter(Banner.id == id).first()
|
||||
if not banner:
|
||||
raise HTTPException(status_code=404, detail="Banner not found")
|
||||
|
||||
if "title" in banner_data:
|
||||
banner.title = banner_data["title"]
|
||||
if "description" in banner_data:
|
||||
banner.description = banner_data["description"]
|
||||
if "image_url" in banner_data:
|
||||
banner.image_url = banner_data["image_url"]
|
||||
if "link" in banner_data:
|
||||
banner.link_url = banner_data["link"]
|
||||
if "position" in banner_data:
|
||||
banner.position = banner_data["position"]
|
||||
if "display_order" in banner_data:
|
||||
banner.display_order = banner_data["display_order"]
|
||||
if "is_active" in banner_data:
|
||||
banner.is_active = banner_data["is_active"]
|
||||
if "start_date" in banner_data:
|
||||
banner.start_date = datetime.fromisoformat(banner_data["start_date"].replace('Z', '+00:00')) if banner_data["start_date"] else None
|
||||
if "end_date" in banner_data:
|
||||
banner.end_date = datetime.fromisoformat(banner_data["end_date"].replace('Z', '+00:00')) if banner_data["end_date"] else None
|
||||
|
||||
raise HTTPException(status_code=404, detail='Banner not found')
|
||||
if 'title' in banner_data:
|
||||
banner.title = banner_data['title']
|
||||
if 'description' in banner_data:
|
||||
banner.description = banner_data['description']
|
||||
if 'image_url' in banner_data:
|
||||
banner.image_url = banner_data['image_url']
|
||||
if 'link' in banner_data:
|
||||
banner.link_url = banner_data['link']
|
||||
if 'position' in banner_data:
|
||||
banner.position = banner_data['position']
|
||||
if 'display_order' in banner_data:
|
||||
banner.display_order = banner_data['display_order']
|
||||
if 'is_active' in banner_data:
|
||||
banner.is_active = banner_data['is_active']
|
||||
if 'start_date' in banner_data:
|
||||
banner.start_date = datetime.fromisoformat(banner_data['start_date'].replace('Z', '+00:00')) if banner_data['start_date'] else None
|
||||
if 'end_date' in banner_data:
|
||||
banner.end_date = datetime.fromisoformat(banner_data['end_date'].replace('Z', '+00:00')) if banner_data['end_date'] else None
|
||||
db.commit()
|
||||
db.refresh(banner)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Banner updated successfully",
|
||||
"data": {"banner": banner}
|
||||
}
|
||||
return {'status': 'success', 'message': 'Banner updated successfully', 'data': {'banner': banner}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/{id}", dependencies=[Depends(authorize_roles("admin"))])
|
||||
async def delete_banner(
|
||||
id: int,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Delete banner (Admin only)"""
|
||||
@router.delete('/{id}', dependencies=[Depends(authorize_roles('admin'))])
|
||||
async def delete_banner(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
banner = db.query(Banner).filter(Banner.id == id).first()
|
||||
if not banner:
|
||||
raise HTTPException(status_code=404, detail="Banner not found")
|
||||
|
||||
# Delete image file if it exists and is a local upload
|
||||
raise HTTPException(status_code=404, detail='Banner not found')
|
||||
if banner.image_url and banner.image_url.startswith('/uploads/banners/'):
|
||||
file_path = Path(__file__).parent.parent.parent / "uploads" / "banners" / Path(banner.image_url).name
|
||||
file_path = Path(__file__).parent.parent.parent / 'uploads' / 'banners' / Path(banner.image_url).name
|
||||
if file_path.exists():
|
||||
file_path.unlink()
|
||||
|
||||
db.delete(banner)
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Banner deleted successfully"
|
||||
}
|
||||
return {'status': 'success', 'message': 'Banner deleted successfully'}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/upload", dependencies=[Depends(authorize_roles("admin"))])
|
||||
async def upload_banner_image(
|
||||
request: Request,
|
||||
image: UploadFile = File(...),
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
):
|
||||
"""Upload banner image (Admin only)"""
|
||||
@router.post('/upload', dependencies=[Depends(authorize_roles('admin'))])
|
||||
async def upload_banner_image(request: Request, image: UploadFile=File(...), current_user: User=Depends(authorize_roles('admin'))):
|
||||
try:
|
||||
# Validate file exists
|
||||
if not image:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="No file provided"
|
||||
)
|
||||
|
||||
# Validate file type
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='No file provided')
|
||||
if not image.content_type or not image.content_type.startswith('image/'):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"File must be an image. Received: {image.content_type}"
|
||||
)
|
||||
|
||||
# Validate filename
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f'File must be an image. Received: {image.content_type}')
|
||||
if not image.filename:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Filename is required"
|
||||
)
|
||||
|
||||
# Create uploads directory
|
||||
upload_dir = Path(__file__).parent.parent.parent / "uploads" / "banners"
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Filename is required')
|
||||
upload_dir = Path(__file__).parent.parent.parent / 'uploads' / 'banners'
|
||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Generate filename
|
||||
ext = Path(image.filename).suffix or '.jpg'
|
||||
filename = f"banner-{uuid.uuid4()}{ext}"
|
||||
filename = f'banner-{uuid.uuid4()}{ext}'
|
||||
file_path = upload_dir / filename
|
||||
|
||||
# Save file
|
||||
async with aiofiles.open(file_path, 'wb') as f:
|
||||
content = await image.read()
|
||||
if not content:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="File is empty"
|
||||
)
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='File is empty')
|
||||
await f.write(content)
|
||||
|
||||
# Return the image URL
|
||||
image_url = f"/uploads/banners/{filename}"
|
||||
image_url = f'/uploads/banners/{filename}'
|
||||
base_url = get_base_url(request)
|
||||
full_url = normalize_image_url(image_url, base_url)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"status": "success",
|
||||
"message": "Image uploaded successfully",
|
||||
"data": {
|
||||
"image_url": image_url,
|
||||
"full_url": full_url
|
||||
}
|
||||
}
|
||||
return {'success': True, 'status': 'success', 'message': 'Image uploaded successfully', 'data': {'image_url': image_url, 'full_url': full_url}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
File diff suppressed because it is too large
Load Diff
335
Backend/src/routes/chat_routes.py
Normal file
335
Backend/src/routes/chat_routes.py
Normal file
@@ -0,0 +1,335 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, WebSocket, WebSocketDisconnect
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, or_
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
import json
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import get_current_user, get_current_user_optional
|
||||
from ..models.user import User
|
||||
from ..models.chat import Chat, ChatMessage, ChatStatus
|
||||
from ..models.role import Role
|
||||
router = APIRouter(prefix='/chat', tags=['chat'])
|
||||
|
||||
class ConnectionManager:
|
||||
|
||||
def __init__(self):
|
||||
self.active_connections: dict[int, List[WebSocket]] = {}
|
||||
self.staff_connections: dict[int, WebSocket] = {}
|
||||
self.visitor_connections: dict[int, WebSocket] = {}
|
||||
|
||||
async def connect_chat(self, websocket: WebSocket, chat_id: int, user_type: str):
|
||||
await websocket.accept()
|
||||
if chat_id not in self.active_connections:
|
||||
self.active_connections[chat_id] = []
|
||||
self.active_connections[chat_id].append(websocket)
|
||||
if user_type == 'staff':
|
||||
pass
|
||||
elif user_type == 'visitor':
|
||||
self.visitor_connections[chat_id] = websocket
|
||||
|
||||
def disconnect_chat(self, websocket: WebSocket, chat_id: int):
|
||||
if chat_id in self.active_connections:
|
||||
if websocket in self.active_connections[chat_id]:
|
||||
self.active_connections[chat_id].remove(websocket)
|
||||
if not self.active_connections[chat_id]:
|
||||
del self.active_connections[chat_id]
|
||||
if chat_id in self.visitor_connections and self.visitor_connections[chat_id] == websocket:
|
||||
del self.visitor_connections[chat_id]
|
||||
|
||||
async def send_personal_message(self, message: dict, websocket: WebSocket):
|
||||
try:
|
||||
await websocket.send_json(message)
|
||||
except Exception as e:
|
||||
print(f'Error sending message: {e}')
|
||||
|
||||
async def broadcast_to_chat(self, message: dict, chat_id: int):
|
||||
if chat_id in self.active_connections:
|
||||
disconnected = []
|
||||
for connection in self.active_connections[chat_id]:
|
||||
try:
|
||||
await connection.send_json(message)
|
||||
except Exception as e:
|
||||
print(f'Error broadcasting to connection: {e}')
|
||||
disconnected.append(connection)
|
||||
for conn in disconnected:
|
||||
self.active_connections[chat_id].remove(conn)
|
||||
|
||||
async def notify_staff_new_chat(self, chat_data: dict):
|
||||
disconnected = []
|
||||
for user_id, websocket in self.staff_connections.items():
|
||||
try:
|
||||
await websocket.send_json({'type': 'new_chat', 'data': chat_data})
|
||||
except Exception as e:
|
||||
print(f'Error notifying staff {user_id}: {e}')
|
||||
disconnected.append(user_id)
|
||||
for user_id in disconnected:
|
||||
del self.staff_connections[user_id]
|
||||
|
||||
async def notify_staff_new_message(self, chat_id: int, message_data: dict, chat: Chat):
|
||||
if message_data.get('sender_type') == 'visitor':
|
||||
notification_data = {'type': 'new_message_notification', 'data': {'chat_id': chat_id, 'chat': {'id': chat.id, 'visitor_name': chat.visitor_name, 'visitor_email': chat.visitor_email, 'status': chat.status.value, 'created_at': chat.created_at.isoformat()}, 'message': {'id': message_data.get('id'), 'message': message_data.get('message'), 'sender_type': message_data.get('sender_type'), 'created_at': message_data.get('created_at')}}}
|
||||
disconnected = []
|
||||
for user_id, websocket in self.staff_connections.items():
|
||||
try:
|
||||
await websocket.send_json(notification_data)
|
||||
except Exception as e:
|
||||
print(f'Error notifying staff {user_id}: {e}')
|
||||
disconnected.append(user_id)
|
||||
for user_id in disconnected:
|
||||
del self.staff_connections[user_id]
|
||||
|
||||
def connect_staff(self, user_id: int, websocket: WebSocket):
|
||||
self.staff_connections[user_id] = websocket
|
||||
|
||||
def disconnect_staff(self, user_id: int):
|
||||
if user_id in self.staff_connections:
|
||||
del self.staff_connections[user_id]
|
||||
manager = ConnectionManager()
|
||||
|
||||
@router.post('/create', status_code=status.HTTP_201_CREATED)
|
||||
async def create_chat(visitor_name: Optional[str]=None, visitor_email: Optional[str]=None, visitor_phone: Optional[str]=None, current_user: Optional[User]=Depends(get_current_user_optional), db: Session=Depends(get_db)):
|
||||
if current_user:
|
||||
chat = Chat(visitor_id=current_user.id, visitor_name=current_user.full_name, visitor_email=current_user.email, status=ChatStatus.pending)
|
||||
else:
|
||||
if not visitor_name:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Visitor name is required')
|
||||
if not visitor_email:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Visitor email is required')
|
||||
chat = Chat(visitor_name=visitor_name, visitor_email=visitor_email, status=ChatStatus.pending)
|
||||
db.add(chat)
|
||||
db.commit()
|
||||
db.refresh(chat)
|
||||
chat_data = {'id': chat.id, 'visitor_name': chat.visitor_name, 'visitor_email': chat.visitor_email, 'status': chat.status.value, 'created_at': chat.created_at.isoformat()}
|
||||
await manager.notify_staff_new_chat(chat_data)
|
||||
return {'success': True, 'data': {'id': chat.id, 'visitor_name': chat.visitor_name, 'visitor_email': chat.visitor_email, 'status': chat.status.value, 'created_at': chat.created_at.isoformat()}}
|
||||
|
||||
@router.post('/{chat_id}/accept')
|
||||
async def accept_chat(chat_id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
if current_user.role.name not in ['staff', 'admin']:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Only staff members can accept chats')
|
||||
chat = db.query(Chat).filter(Chat.id == chat_id).first()
|
||||
if not chat:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Chat not found')
|
||||
if chat.status != ChatStatus.pending:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Chat is not pending')
|
||||
chat.staff_id = current_user.id
|
||||
chat.status = ChatStatus.active
|
||||
db.commit()
|
||||
db.refresh(chat)
|
||||
await manager.broadcast_to_chat({'type': 'chat_accepted', 'data': {'staff_name': current_user.full_name, 'staff_id': current_user.id}}, chat_id)
|
||||
return {'success': True, 'data': {'id': chat.id, 'staff_id': chat.staff_id, 'staff_name': current_user.full_name, 'status': chat.status.value}}
|
||||
|
||||
@router.get('/list')
|
||||
async def list_chats(status_filter: Optional[str]=None, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
if current_user.role.name in ['staff', 'admin']:
|
||||
query = db.query(Chat)
|
||||
if status_filter:
|
||||
try:
|
||||
status_enum = ChatStatus(status_filter)
|
||||
query = query.filter(Chat.status == status_enum)
|
||||
except ValueError:
|
||||
pass
|
||||
chats = query.order_by(Chat.created_at.desc()).all()
|
||||
else:
|
||||
chats = db.query(Chat).filter(Chat.visitor_id == current_user.id).order_by(Chat.created_at.desc()).all()
|
||||
return {'success': True, 'data': [{'id': chat.id, 'visitor_id': chat.visitor_id, 'visitor_name': chat.visitor_name, 'visitor_email': chat.visitor_email, 'staff_id': chat.staff_id, 'staff_name': chat.staff.full_name if chat.staff else None, 'status': chat.status.value, 'created_at': chat.created_at.isoformat(), 'updated_at': chat.updated_at.isoformat(), 'message_count': len(chat.messages)} for chat in chats]}
|
||||
|
||||
@router.get('/{chat_id}')
|
||||
async def get_chat(chat_id: int, current_user: Optional[User]=Depends(get_current_user_optional), db: Session=Depends(get_db)):
|
||||
chat = db.query(Chat).filter(Chat.id == chat_id).first()
|
||||
if not chat:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Chat not found')
|
||||
if current_user:
|
||||
if current_user.role.name not in ['staff', 'admin']:
|
||||
if chat.visitor_id != current_user.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You don't have permission to view this chat")
|
||||
return {'success': True, 'data': {'id': chat.id, 'visitor_id': chat.visitor_id, 'visitor_name': chat.visitor_name, 'visitor_email': chat.visitor_email, 'staff_id': chat.staff_id, 'staff_name': chat.staff.full_name if chat.staff else None, 'status': chat.status.value, 'created_at': chat.created_at.isoformat(), 'updated_at': chat.updated_at.isoformat()}}
|
||||
|
||||
@router.get('/{chat_id}/messages')
|
||||
async def get_messages(chat_id: int, current_user: Optional[User]=Depends(get_current_user_optional), db: Session=Depends(get_db)):
|
||||
chat = db.query(Chat).filter(Chat.id == chat_id).first()
|
||||
if not chat:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Chat not found')
|
||||
if current_user:
|
||||
if current_user.role.name not in ['staff', 'admin']:
|
||||
if chat.visitor_id != current_user.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You don't have permission to view this chat")
|
||||
else:
|
||||
pass
|
||||
messages = db.query(ChatMessage).filter(ChatMessage.chat_id == chat_id).order_by(ChatMessage.created_at.asc()).all()
|
||||
return {'success': True, 'data': [{'id': msg.id, 'chat_id': msg.chat_id, 'sender_id': msg.sender_id, 'sender_type': msg.sender_type, 'sender_name': msg.sender.full_name if msg.sender else None, 'message': msg.message, 'is_read': msg.is_read, 'created_at': msg.created_at.isoformat()} for msg in messages]}
|
||||
|
||||
@router.post('/{chat_id}/message')
|
||||
async def send_message(chat_id: int, message: str, current_user: Optional[User]=Depends(get_current_user_optional), db: Session=Depends(get_db)):
|
||||
chat = db.query(Chat).filter(Chat.id == chat_id).first()
|
||||
if not chat:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Chat not found')
|
||||
if chat.status == ChatStatus.closed:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Chat is closed')
|
||||
sender_type = 'visitor'
|
||||
sender_id = None
|
||||
if current_user:
|
||||
if current_user.role.name in ['staff', 'admin']:
|
||||
sender_type = 'staff'
|
||||
sender_id = current_user.id
|
||||
else:
|
||||
sender_type = 'visitor'
|
||||
sender_id = current_user.id
|
||||
if chat.visitor_id != current_user.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You don't have permission to send messages in this chat")
|
||||
else:
|
||||
sender_type = 'visitor'
|
||||
sender_id = None
|
||||
chat_message = ChatMessage(chat_id=chat_id, sender_id=sender_id, sender_type=sender_type, message=message)
|
||||
db.add(chat_message)
|
||||
db.commit()
|
||||
db.refresh(chat_message)
|
||||
message_data = {'type': 'new_message', 'data': {'id': chat_message.id, 'chat_id': chat_message.chat_id, 'sender_id': chat_message.sender_id, 'sender_type': chat_message.sender_type, 'sender_name': chat_message.sender.full_name if chat_message.sender else None, 'message': chat_message.message, 'is_read': chat_message.is_read, 'created_at': chat_message.created_at.isoformat()}}
|
||||
await manager.broadcast_to_chat(message_data, chat_id)
|
||||
if chat_message.sender_type == 'visitor':
|
||||
await manager.notify_staff_new_message(chat_id, message_data['data'], chat)
|
||||
return {'success': True, 'data': {'id': chat_message.id, 'chat_id': chat_message.chat_id, 'sender_type': chat_message.sender_type, 'message': chat_message.message, 'created_at': chat_message.created_at.isoformat()}}
|
||||
|
||||
@router.post('/{chat_id}/close')
|
||||
async def close_chat(chat_id: int, current_user: Optional[User]=Depends(get_current_user_optional), db: Session=Depends(get_db)):
|
||||
chat = db.query(Chat).filter(Chat.id == chat_id).first()
|
||||
if not chat:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Chat not found')
|
||||
if current_user:
|
||||
if current_user.role.name not in ['staff', 'admin']:
|
||||
if chat.visitor_id != current_user.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You don't have permission to close this chat")
|
||||
else:
|
||||
pass
|
||||
chat.status = ChatStatus.closed
|
||||
chat.closed_at = datetime.utcnow()
|
||||
db.commit()
|
||||
await manager.broadcast_to_chat({'type': 'chat_closed', 'data': {'chat_id': chat_id}}, chat_id)
|
||||
return {'success': True, 'data': {'id': chat.id, 'status': chat.status.value}}
|
||||
|
||||
@router.websocket('/ws/{chat_id}')
|
||||
async def websocket_chat(websocket: WebSocket, chat_id: int, user_type: str=None, token: Optional[str]=None):
|
||||
query_params = dict(websocket.query_params)
|
||||
user_type = query_params.get('user_type', 'visitor')
|
||||
token = query_params.get('token')
|
||||
current_user = None
|
||||
if user_type == 'staff' and token:
|
||||
try:
|
||||
from ..middleware.auth import verify_token
|
||||
from ..config.database import get_db
|
||||
payload = verify_token(token)
|
||||
user_id = payload.get('userId')
|
||||
db_gen = get_db()
|
||||
db = next(db_gen)
|
||||
try:
|
||||
current_user = db.query(User).filter(User.id == user_id).first()
|
||||
if not current_user or current_user.role.name not in ['staff', 'admin']:
|
||||
await websocket.close(code=1008, reason='Unauthorized')
|
||||
return
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as e:
|
||||
await websocket.close(code=1008, reason='Invalid token')
|
||||
return
|
||||
await manager.connect_chat(websocket, chat_id, user_type)
|
||||
if user_type == 'staff' and current_user:
|
||||
manager.connect_staff(current_user.id, websocket)
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
message_data = json.loads(data)
|
||||
if message_data.get('type') == 'message':
|
||||
from ..config.database import get_db
|
||||
db_gen = get_db()
|
||||
db = next(db_gen)
|
||||
try:
|
||||
chat = db.query(Chat).filter(Chat.id == chat_id).first()
|
||||
if not chat:
|
||||
continue
|
||||
sender_id = current_user.id if current_user else None
|
||||
sender_type = 'staff' if user_type == 'staff' else 'visitor'
|
||||
chat_message = ChatMessage(chat_id=chat_id, sender_id=sender_id, sender_type=sender_type, message=message_data.get('message', ''))
|
||||
db.add(chat_message)
|
||||
db.commit()
|
||||
db.refresh(chat_message)
|
||||
finally:
|
||||
db.close()
|
||||
message_data = {'type': 'new_message', 'data': {'id': chat_message.id, 'chat_id': chat_message.chat_id, 'sender_id': chat_message.sender_id, 'sender_type': chat_message.sender_type, 'sender_name': chat_message.sender.full_name if chat_message.sender else None, 'message': chat_message.message, 'is_read': chat_message.is_read, 'created_at': chat_message.created_at.isoformat()}}
|
||||
await manager.broadcast_to_chat(message_data, chat_id)
|
||||
if chat_message.sender_type == 'visitor':
|
||||
await manager.notify_staff_new_message(chat_id, message_data['data'], chat)
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect_chat(websocket, chat_id)
|
||||
if user_type == 'staff' and current_user:
|
||||
manager.disconnect_staff(current_user.id)
|
||||
|
||||
@router.websocket('/ws/staff/notifications')
|
||||
async def websocket_staff_notifications(websocket: WebSocket):
|
||||
current_user = None
|
||||
try:
|
||||
await websocket.accept()
|
||||
query_params = dict(websocket.query_params)
|
||||
token = query_params.get('token')
|
||||
if not token:
|
||||
await websocket.close(code=1008, reason='Token required')
|
||||
return
|
||||
try:
|
||||
from ..middleware.auth import verify_token
|
||||
from ..config.database import get_db
|
||||
payload = verify_token(token)
|
||||
user_id = payload.get('userId')
|
||||
if not user_id:
|
||||
await websocket.close(code=1008, reason='Invalid token payload')
|
||||
return
|
||||
db_gen = get_db()
|
||||
db = next(db_gen)
|
||||
try:
|
||||
current_user = db.query(User).filter(User.id == user_id).first()
|
||||
if not current_user:
|
||||
await websocket.close(code=1008, reason='User not found')
|
||||
return
|
||||
role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
if not role or role.name not in ['staff', 'admin']:
|
||||
await websocket.close(code=1008, reason='Unauthorized role')
|
||||
return
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as e:
|
||||
print(f'WebSocket token verification error: {e}')
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
await websocket.close(code=1008, reason=f'Token verification failed: {str(e)}')
|
||||
return
|
||||
manager.connect_staff(current_user.id, websocket)
|
||||
try:
|
||||
await websocket.send_json({'type': 'connected', 'data': {'message': 'WebSocket connected'}})
|
||||
except Exception as e:
|
||||
print(f'Error sending initial message: {e}')
|
||||
while True:
|
||||
try:
|
||||
data = await websocket.receive_text()
|
||||
try:
|
||||
message_data = json.loads(data)
|
||||
if message_data.get('type') == 'ping':
|
||||
await websocket.send_json({'type': 'pong', 'data': 'pong'})
|
||||
except json.JSONDecodeError:
|
||||
await websocket.send_json({'type': 'pong', 'data': 'pong'})
|
||||
except WebSocketDisconnect:
|
||||
print('WebSocket disconnected normally')
|
||||
break
|
||||
except Exception as e:
|
||||
print(f'WebSocket receive error: {e}')
|
||||
break
|
||||
except WebSocketDisconnect:
|
||||
print('WebSocket disconnected')
|
||||
except Exception as e:
|
||||
print(f'WebSocket error: {e}')
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
if current_user:
|
||||
try:
|
||||
manager.disconnect_staff(current_user.id)
|
||||
except:
|
||||
pass
|
||||
@@ -1,68 +1,23 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
import json
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ..models.page_content import PageContent, PageType
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/contact-content", tags=["contact-content"])
|
||||
|
||||
router = APIRouter(prefix='/contact-content', tags=['contact-content'])
|
||||
|
||||
def serialize_page_content(content: PageContent) -> dict:
|
||||
"""Serialize PageContent model to dictionary"""
|
||||
return {
|
||||
"id": content.id,
|
||||
"page_type": content.page_type.value,
|
||||
"title": content.title,
|
||||
"subtitle": content.subtitle,
|
||||
"description": content.description,
|
||||
"content": content.content,
|
||||
"meta_title": content.meta_title,
|
||||
"meta_description": content.meta_description,
|
||||
"meta_keywords": content.meta_keywords,
|
||||
"og_title": content.og_title,
|
||||
"og_description": content.og_description,
|
||||
"og_image": content.og_image,
|
||||
"canonical_url": content.canonical_url,
|
||||
"contact_info": json.loads(content.contact_info) if content.contact_info else None,
|
||||
"map_url": content.map_url,
|
||||
"is_active": content.is_active,
|
||||
"created_at": content.created_at.isoformat() if content.created_at else None,
|
||||
"updated_at": content.updated_at.isoformat() if content.updated_at else None,
|
||||
}
|
||||
return {'id': content.id, 'page_type': content.page_type.value, 'title': content.title, 'subtitle': content.subtitle, 'description': content.description, 'content': content.content, 'meta_title': content.meta_title, 'meta_description': content.meta_description, 'meta_keywords': content.meta_keywords, 'og_title': content.og_title, 'og_description': content.og_description, 'og_image': content.og_image, 'canonical_url': content.canonical_url, 'contact_info': json.loads(content.contact_info) if content.contact_info else None, 'map_url': content.map_url, 'is_active': content.is_active, 'created_at': content.created_at.isoformat() if content.created_at else None, 'updated_at': content.updated_at.isoformat() if content.updated_at else None}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_contact_content(
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get contact page content"""
|
||||
@router.get('/')
|
||||
async def get_contact_content(db: Session=Depends(get_db)):
|
||||
try:
|
||||
content = db.query(PageContent).filter(PageContent.page_type == PageType.CONTACT).first()
|
||||
|
||||
if not content:
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"page_content": None
|
||||
}
|
||||
}
|
||||
|
||||
return {'status': 'success', 'data': {'page_content': None}}
|
||||
content_dict = serialize_page_content(content)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"page_content": content_dict
|
||||
}
|
||||
}
|
||||
return {'status': 'success', 'data': {'page_content': content_dict}}
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching contact content: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error fetching contact content: {str(e)}"
|
||||
)
|
||||
|
||||
logger.error(f'Error fetching contact content: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching contact content: {str(e)}')
|
||||
@@ -3,17 +3,13 @@ from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from typing import Optional
|
||||
import logging
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..models.user import User
|
||||
from ..models.role import Role
|
||||
from ..models.system_settings import SystemSettings
|
||||
from ..utils.mailer import send_email
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/contact", tags=["contact"])
|
||||
|
||||
router = APIRouter(prefix='/contact', tags=['contact'])
|
||||
|
||||
class ContactForm(BaseModel):
|
||||
name: str
|
||||
@@ -22,182 +18,35 @@ class ContactForm(BaseModel):
|
||||
message: str
|
||||
phone: Optional[str] = None
|
||||
|
||||
|
||||
def get_admin_email(db: Session) -> str:
|
||||
"""Get admin email from system settings or find admin user"""
|
||||
# First, try to get from company_email (company settings)
|
||||
company_email_setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "company_email"
|
||||
).first()
|
||||
|
||||
company_email_setting = db.query(SystemSettings).filter(SystemSettings.key == 'company_email').first()
|
||||
if company_email_setting and company_email_setting.value:
|
||||
return company_email_setting.value
|
||||
|
||||
# Second, try to get from admin_email (legacy setting)
|
||||
admin_email_setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "admin_email"
|
||||
).first()
|
||||
|
||||
admin_email_setting = db.query(SystemSettings).filter(SystemSettings.key == 'admin_email').first()
|
||||
if admin_email_setting and admin_email_setting.value:
|
||||
return admin_email_setting.value
|
||||
|
||||
# If not found in settings, find the first admin user
|
||||
admin_role = db.query(Role).filter(Role.name == "admin").first()
|
||||
admin_role = db.query(Role).filter(Role.name == 'admin').first()
|
||||
if admin_role:
|
||||
admin_user = db.query(User).filter(
|
||||
User.role_id == admin_role.id,
|
||||
User.is_active == True
|
||||
).first()
|
||||
|
||||
admin_user = db.query(User).filter(User.role_id == admin_role.id, User.is_active == True).first()
|
||||
if admin_user:
|
||||
return admin_user.email
|
||||
|
||||
# Fallback to SMTP_FROM_EMAIL if configured
|
||||
from ..config.settings import settings
|
||||
if settings.SMTP_FROM_EMAIL:
|
||||
return settings.SMTP_FROM_EMAIL
|
||||
|
||||
# Last resort: raise error
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Admin email not configured. Please set company_email in system settings or ensure an admin user exists."
|
||||
)
|
||||
raise HTTPException(status_code=500, detail='Admin email not configured. Please set company_email in system settings or ensure an admin user exists.')
|
||||
|
||||
|
||||
@router.post("/submit")
|
||||
async def submit_contact_form(
|
||||
contact_data: ContactForm,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Submit contact form and send email to admin"""
|
||||
@router.post('/submit')
|
||||
async def submit_contact_form(contact_data: ContactForm, db: Session=Depends(get_db)):
|
||||
try:
|
||||
# Get admin email
|
||||
admin_email = get_admin_email(db)
|
||||
|
||||
# Create email subject
|
||||
subject = f"Contact Form: {contact_data.subject}"
|
||||
|
||||
# Create email body (HTML)
|
||||
html_body = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {{
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f9f9f9;
|
||||
}}
|
||||
.header {{
|
||||
background: linear-gradient(135deg, #d4af37 0%, #c9a227 100%);
|
||||
color: #0f0f0f;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}}
|
||||
.content {{
|
||||
background-color: #ffffff;
|
||||
padding: 30px;
|
||||
border-radius: 0 0 8px 8px;
|
||||
}}
|
||||
.field {{
|
||||
margin-bottom: 20px;
|
||||
}}
|
||||
.label {{
|
||||
font-weight: bold;
|
||||
color: #d4af37;
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}}
|
||||
.value {{
|
||||
color: #333;
|
||||
padding: 10px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
.footer {{
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #ddd;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h2>New Contact Form Submission</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="field">
|
||||
<span class="label">Name:</span>
|
||||
<div class="value">{contact_data.name}</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="label">Email:</span>
|
||||
<div class="value">{contact_data.email}</div>
|
||||
</div>
|
||||
{f'<div class="field"><span class="label">Phone:</span><div class="value">{contact_data.phone}</div></div>' if contact_data.phone else ''}
|
||||
<div class="field">
|
||||
<span class="label">Subject:</span>
|
||||
<div class="value">{contact_data.subject}</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="label">Message:</span>
|
||||
<div class="value" style="white-space: pre-wrap;">{contact_data.message}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>This email was sent from the hotel booking contact form.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# Create plain text version
|
||||
text_body = f"""
|
||||
New Contact Form Submission
|
||||
|
||||
Name: {contact_data.name}
|
||||
Email: {contact_data.email}
|
||||
{f'Phone: {contact_data.phone}' if contact_data.phone else ''}
|
||||
Subject: {contact_data.subject}
|
||||
|
||||
Message:
|
||||
{contact_data.message}
|
||||
"""
|
||||
|
||||
# Send email to admin
|
||||
await send_email(
|
||||
to=admin_email,
|
||||
subject=subject,
|
||||
html=html_body,
|
||||
text=text_body
|
||||
)
|
||||
|
||||
logger.info(f"Contact form submitted successfully. Email sent to {admin_email}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Thank you for contacting us! We will get back to you soon."
|
||||
}
|
||||
|
||||
subject = f'Contact Form: {contact_data.subject}'
|
||||
html_body = f
|
||||
text_body = f
|
||||
await send_email(to=admin_email, subject=subject, html=html_body, text=text_body)
|
||||
logger.info(f'Contact form submitted successfully. Email sent to {admin_email}')
|
||||
return {'status': 'success', 'message': 'Thank you for contacting us! We will get back to you soon.'}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to submit contact form: {type(e).__name__}: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Failed to submit contact form. Please try again later."
|
||||
)
|
||||
|
||||
logger.error(f'Failed to submit contact form: {type(e).__name__}: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=500, detail='Failed to submit contact form. Please try again later.')
|
||||
@@ -1,7 +1,6 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import get_current_user
|
||||
from ..models.user import User
|
||||
@@ -9,179 +8,74 @@ from ..models.favorite import Favorite
|
||||
from ..models.room import Room
|
||||
from ..models.room_type import RoomType
|
||||
from ..models.review import Review, ReviewStatus
|
||||
router = APIRouter(prefix='/favorites', tags=['favorites'])
|
||||
|
||||
router = APIRouter(prefix="/favorites", tags=["favorites"])
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_favorites(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get user's favorite rooms"""
|
||||
@router.get('/')
|
||||
async def get_favorites(current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
if current_user.role in ['admin', 'staff']:
|
||||
raise HTTPException(status_code=403, detail='Admin and staff users cannot have favorites')
|
||||
try:
|
||||
favorites = db.query(Favorite).filter(
|
||||
Favorite.user_id == current_user.id
|
||||
).order_by(Favorite.created_at.desc()).all()
|
||||
|
||||
favorites = db.query(Favorite).filter(Favorite.user_id == current_user.id).order_by(Favorite.created_at.desc()).all()
|
||||
result = []
|
||||
for favorite in favorites:
|
||||
if not favorite.room:
|
||||
continue
|
||||
|
||||
room = favorite.room
|
||||
|
||||
# Get review stats
|
||||
review_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()
|
||||
|
||||
room_dict = {
|
||||
"id": room.id,
|
||||
"room_type_id": room.room_type_id,
|
||||
"room_number": room.room_number,
|
||||
"floor": room.floor,
|
||||
"status": room.status.value if hasattr(room.status, 'value') else room.status,
|
||||
"price": float(room.price) if room.price else 0.0,
|
||||
"featured": room.featured,
|
||||
"description": room.description,
|
||||
"amenities": room.amenities,
|
||||
"images": room.images or [],
|
||||
"average_rating": round(float(review_stats.average_rating or 0), 1) if review_stats and review_stats.average_rating else None,
|
||||
"total_reviews": review_stats.total_reviews or 0 if review_stats else 0,
|
||||
}
|
||||
|
||||
review_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()
|
||||
room_dict = {'id': room.id, 'room_type_id': room.room_type_id, 'room_number': room.room_number, 'floor': room.floor, 'status': room.status.value if hasattr(room.status, 'value') else room.status, 'price': float(room.price) if room.price else 0.0, 'featured': room.featured, 'description': room.description, 'amenities': room.amenities, 'images': room.images or [], 'average_rating': round(float(review_stats.average_rating or 0), 1) if review_stats and review_stats.average_rating else None, 'total_reviews': review_stats.total_reviews or 0 if review_stats else 0}
|
||||
if room.room_type:
|
||||
room_dict["room_type"] = {
|
||||
"id": room.room_type.id,
|
||||
"name": room.room_type.name,
|
||||
"description": room.room_type.description,
|
||||
"base_price": float(room.room_type.base_price) if room.room_type.base_price else 0.0,
|
||||
"capacity": room.room_type.capacity,
|
||||
"amenities": room.room_type.amenities,
|
||||
}
|
||||
|
||||
favorite_dict = {
|
||||
"id": favorite.id,
|
||||
"user_id": favorite.user_id,
|
||||
"room_id": favorite.room_id,
|
||||
"room": room_dict,
|
||||
"created_at": favorite.created_at.isoformat() if favorite.created_at else None,
|
||||
}
|
||||
|
||||
room_dict['room_type'] = {'id': room.room_type.id, 'name': room.room_type.name, 'description': room.room_type.description, 'base_price': float(room.room_type.base_price) if room.room_type.base_price else 0.0, 'capacity': room.room_type.capacity, 'amenities': room.room_type.amenities}
|
||||
favorite_dict = {'id': favorite.id, 'user_id': favorite.user_id, 'room_id': favorite.room_id, 'room': room_dict, 'created_at': favorite.created_at.isoformat() if favorite.created_at else None}
|
||||
result.append(favorite_dict)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"favorites": result,
|
||||
"total": len(result),
|
||||
}
|
||||
}
|
||||
return {'status': 'success', 'data': {'favorites': result, 'total': len(result)}}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{room_id}")
|
||||
async def add_favorite(
|
||||
room_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Add room to favorites"""
|
||||
@router.post('/{room_id}')
|
||||
async def add_favorite(room_id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
if current_user.role in ['admin', 'staff']:
|
||||
raise HTTPException(status_code=403, detail='Admin and staff users cannot add favorites')
|
||||
try:
|
||||
# Check if room exists
|
||||
room = db.query(Room).filter(Room.id == room_id).first()
|
||||
if not room:
|
||||
raise HTTPException(status_code=404, detail="Room not found")
|
||||
|
||||
# Check if already favorited
|
||||
existing = db.query(Favorite).filter(
|
||||
Favorite.user_id == current_user.id,
|
||||
Favorite.room_id == room_id
|
||||
).first()
|
||||
|
||||
raise HTTPException(status_code=404, detail='Room not found')
|
||||
existing = db.query(Favorite).filter(Favorite.user_id == current_user.id, Favorite.room_id == room_id).first()
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Room already in favorites list"
|
||||
)
|
||||
|
||||
# Create favorite
|
||||
favorite = Favorite(
|
||||
user_id=current_user.id,
|
||||
room_id=room_id
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=400, detail='Room already in favorites list')
|
||||
favorite = Favorite(user_id=current_user.id, room_id=room_id)
|
||||
db.add(favorite)
|
||||
db.commit()
|
||||
db.refresh(favorite)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Added to favorites list",
|
||||
"data": {"favorite": favorite}
|
||||
}
|
||||
return {'status': 'success', 'message': 'Added to favorites list', 'data': {'favorite': favorite}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/{room_id}")
|
||||
async def remove_favorite(
|
||||
room_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Remove room from favorites"""
|
||||
@router.delete('/{room_id}')
|
||||
async def remove_favorite(room_id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
if current_user.role in ['admin', 'staff']:
|
||||
raise HTTPException(status_code=403, detail='Admin and staff users cannot remove favorites')
|
||||
try:
|
||||
favorite = db.query(Favorite).filter(
|
||||
Favorite.user_id == current_user.id,
|
||||
Favorite.room_id == room_id
|
||||
).first()
|
||||
|
||||
favorite = db.query(Favorite).filter(Favorite.user_id == current_user.id, Favorite.room_id == room_id).first()
|
||||
if not favorite:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Room not found in favorites list"
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=404, detail='Room not found in favorites list')
|
||||
db.delete(favorite)
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Removed from favorites list"
|
||||
}
|
||||
return {'status': 'success', 'message': 'Removed from favorites list'}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/check/{room_id}")
|
||||
async def check_favorite(
|
||||
room_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Check if room is favorited by user"""
|
||||
@router.get('/check/{room_id}')
|
||||
async def check_favorite(room_id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
if current_user.role in ['admin', 'staff']:
|
||||
return {'status': 'success', 'data': {'isFavorited': False}}
|
||||
try:
|
||||
favorite = db.query(Favorite).filter(
|
||||
Favorite.user_id == current_user.id,
|
||||
Favorite.room_id == room_id
|
||||
).first()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {"isFavorited": favorite is not None}
|
||||
}
|
||||
favorite = db.query(Favorite).filter(Favorite.user_id == current_user.id, Favorite.room_id == room_id).first()
|
||||
return {'status': 'success', 'data': {'isFavorited': favorite is not None}}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
@@ -1,63 +1,23 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
import json
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ..models.page_content import PageContent, PageType
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/footer", tags=["footer"])
|
||||
|
||||
router = APIRouter(prefix='/footer', tags=['footer'])
|
||||
|
||||
def serialize_page_content(content: PageContent) -> dict:
|
||||
"""Serialize PageContent model to dictionary"""
|
||||
return {
|
||||
"id": content.id,
|
||||
"page_type": content.page_type.value,
|
||||
"title": content.title,
|
||||
"subtitle": content.subtitle,
|
||||
"description": content.description,
|
||||
"content": content.content,
|
||||
"social_links": json.loads(content.social_links) if content.social_links else None,
|
||||
"footer_links": json.loads(content.footer_links) if content.footer_links else None,
|
||||
"badges": json.loads(content.badges) if content.badges else None,
|
||||
"copyright_text": content.copyright_text,
|
||||
"is_active": content.is_active,
|
||||
"created_at": content.created_at.isoformat() if content.created_at else None,
|
||||
"updated_at": content.updated_at.isoformat() if content.updated_at else None,
|
||||
}
|
||||
return {'id': content.id, 'page_type': content.page_type.value, 'title': content.title, 'subtitle': content.subtitle, 'description': content.description, 'content': content.content, 'social_links': json.loads(content.social_links) if content.social_links else None, 'footer_links': json.loads(content.footer_links) if content.footer_links else None, 'badges': json.loads(content.badges) if content.badges else None, 'copyright_text': content.copyright_text, 'is_active': content.is_active, 'created_at': content.created_at.isoformat() if content.created_at else None, 'updated_at': content.updated_at.isoformat() if content.updated_at else None}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_footer_content(
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get footer content"""
|
||||
@router.get('/')
|
||||
async def get_footer_content(db: Session=Depends(get_db)):
|
||||
try:
|
||||
content = db.query(PageContent).filter(PageContent.page_type == PageType.FOOTER).first()
|
||||
|
||||
if not content:
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"page_content": None
|
||||
}
|
||||
}
|
||||
|
||||
return {'status': 'success', 'data': {'page_content': None}}
|
||||
content_dict = serialize_page_content(content)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"page_content": content_dict
|
||||
}
|
||||
}
|
||||
return {'status': 'success', 'data': {'page_content': content_dict}}
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching footer content: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error fetching footer content: {str(e)}"
|
||||
)
|
||||
|
||||
logger.error(f'Error fetching footer content: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching footer content: {str(e)}')
|
||||
@@ -1,110 +1,23 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
import json
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ..models.page_content import PageContent, PageType
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/home", tags=["home"])
|
||||
|
||||
router = APIRouter(prefix='/home', tags=['home'])
|
||||
|
||||
def serialize_page_content(content: PageContent) -> dict:
|
||||
"""Serialize PageContent model to dictionary"""
|
||||
return {
|
||||
"id": content.id,
|
||||
"page_type": content.page_type.value,
|
||||
"title": content.title,
|
||||
"subtitle": content.subtitle,
|
||||
"description": content.description,
|
||||
"content": content.content,
|
||||
"meta_title": content.meta_title,
|
||||
"meta_description": content.meta_description,
|
||||
"meta_keywords": content.meta_keywords,
|
||||
"og_title": content.og_title,
|
||||
"og_description": content.og_description,
|
||||
"og_image": content.og_image,
|
||||
"canonical_url": content.canonical_url,
|
||||
"hero_title": content.hero_title,
|
||||
"hero_subtitle": content.hero_subtitle,
|
||||
"hero_image": content.hero_image,
|
||||
"amenities_section_title": content.amenities_section_title,
|
||||
"amenities_section_subtitle": content.amenities_section_subtitle,
|
||||
"amenities": json.loads(content.amenities) if content.amenities else None,
|
||||
"testimonials_section_title": content.testimonials_section_title,
|
||||
"testimonials_section_subtitle": content.testimonials_section_subtitle,
|
||||
"testimonials": json.loads(content.testimonials) if content.testimonials else None,
|
||||
"gallery_section_title": content.gallery_section_title,
|
||||
"gallery_section_subtitle": content.gallery_section_subtitle,
|
||||
"gallery_images": json.loads(content.gallery_images) if content.gallery_images else None,
|
||||
"luxury_section_title": content.luxury_section_title,
|
||||
"luxury_section_subtitle": content.luxury_section_subtitle,
|
||||
"luxury_section_image": content.luxury_section_image,
|
||||
"luxury_features": json.loads(content.luxury_features) if content.luxury_features else None,
|
||||
"luxury_gallery_section_title": content.luxury_gallery_section_title,
|
||||
"luxury_gallery_section_subtitle": content.luxury_gallery_section_subtitle,
|
||||
"luxury_gallery": json.loads(content.luxury_gallery) if content.luxury_gallery else None,
|
||||
"luxury_testimonials_section_title": content.luxury_testimonials_section_title,
|
||||
"luxury_testimonials_section_subtitle": content.luxury_testimonials_section_subtitle,
|
||||
"luxury_testimonials": json.loads(content.luxury_testimonials) if content.luxury_testimonials else None,
|
||||
"about_preview_title": content.about_preview_title,
|
||||
"about_preview_subtitle": content.about_preview_subtitle,
|
||||
"about_preview_content": content.about_preview_content,
|
||||
"about_preview_image": content.about_preview_image,
|
||||
"stats": json.loads(content.stats) if content.stats else None,
|
||||
"luxury_services_section_title": content.luxury_services_section_title,
|
||||
"luxury_services_section_subtitle": content.luxury_services_section_subtitle,
|
||||
"luxury_services": json.loads(content.luxury_services) if content.luxury_services else None,
|
||||
"luxury_experiences_section_title": content.luxury_experiences_section_title,
|
||||
"luxury_experiences_section_subtitle": content.luxury_experiences_section_subtitle,
|
||||
"luxury_experiences": json.loads(content.luxury_experiences) if content.luxury_experiences else None,
|
||||
"awards_section_title": content.awards_section_title,
|
||||
"awards_section_subtitle": content.awards_section_subtitle,
|
||||
"awards": json.loads(content.awards) if content.awards else None,
|
||||
"cta_title": content.cta_title,
|
||||
"cta_subtitle": content.cta_subtitle,
|
||||
"cta_button_text": content.cta_button_text,
|
||||
"cta_button_link": content.cta_button_link,
|
||||
"cta_image": content.cta_image,
|
||||
"partners_section_title": content.partners_section_title,
|
||||
"partners_section_subtitle": content.partners_section_subtitle,
|
||||
"partners": json.loads(content.partners) if content.partners else None,
|
||||
"is_active": content.is_active,
|
||||
"created_at": content.created_at.isoformat() if content.created_at else None,
|
||||
"updated_at": content.updated_at.isoformat() if content.updated_at else None,
|
||||
}
|
||||
return {'id': content.id, 'page_type': content.page_type.value, 'title': content.title, 'subtitle': content.subtitle, 'description': content.description, 'content': content.content, 'meta_title': content.meta_title, 'meta_description': content.meta_description, 'meta_keywords': content.meta_keywords, 'og_title': content.og_title, 'og_description': content.og_description, 'og_image': content.og_image, 'canonical_url': content.canonical_url, 'hero_title': content.hero_title, 'hero_subtitle': content.hero_subtitle, 'hero_image': content.hero_image, 'amenities_section_title': content.amenities_section_title, 'amenities_section_subtitle': content.amenities_section_subtitle, 'amenities': json.loads(content.amenities) if content.amenities else None, 'testimonials_section_title': content.testimonials_section_title, 'testimonials_section_subtitle': content.testimonials_section_subtitle, 'testimonials': json.loads(content.testimonials) if content.testimonials else None, 'gallery_section_title': content.gallery_section_title, 'gallery_section_subtitle': content.gallery_section_subtitle, 'gallery_images': json.loads(content.gallery_images) if content.gallery_images else None, 'luxury_section_title': content.luxury_section_title, 'luxury_section_subtitle': content.luxury_section_subtitle, 'luxury_section_image': content.luxury_section_image, 'luxury_features': json.loads(content.luxury_features) if content.luxury_features else None, 'luxury_gallery_section_title': content.luxury_gallery_section_title, 'luxury_gallery_section_subtitle': content.luxury_gallery_section_subtitle, 'luxury_gallery': json.loads(content.luxury_gallery) if content.luxury_gallery else None, 'luxury_testimonials_section_title': content.luxury_testimonials_section_title, 'luxury_testimonials_section_subtitle': content.luxury_testimonials_section_subtitle, 'luxury_testimonials': json.loads(content.luxury_testimonials) if content.luxury_testimonials else None, 'about_preview_title': content.about_preview_title, 'about_preview_subtitle': content.about_preview_subtitle, 'about_preview_content': content.about_preview_content, 'about_preview_image': content.about_preview_image, 'stats': json.loads(content.stats) if content.stats else None, 'luxury_services_section_title': content.luxury_services_section_title, 'luxury_services_section_subtitle': content.luxury_services_section_subtitle, 'luxury_services': json.loads(content.luxury_services) if content.luxury_services else None, 'luxury_experiences_section_title': content.luxury_experiences_section_title, 'luxury_experiences_section_subtitle': content.luxury_experiences_section_subtitle, 'luxury_experiences': json.loads(content.luxury_experiences) if content.luxury_experiences else None, 'awards_section_title': content.awards_section_title, 'awards_section_subtitle': content.awards_section_subtitle, 'awards': json.loads(content.awards) if content.awards else None, 'cta_title': content.cta_title, 'cta_subtitle': content.cta_subtitle, 'cta_button_text': content.cta_button_text, 'cta_button_link': content.cta_button_link, 'cta_image': content.cta_image, 'partners_section_title': content.partners_section_title, 'partners_section_subtitle': content.partners_section_subtitle, 'partners': json.loads(content.partners) if content.partners else None, 'is_active': content.is_active, 'created_at': content.created_at.isoformat() if content.created_at else None, 'updated_at': content.updated_at.isoformat() if content.updated_at else None}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_home_content(
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get homepage content"""
|
||||
@router.get('/')
|
||||
async def get_home_content(db: Session=Depends(get_db)):
|
||||
try:
|
||||
content = db.query(PageContent).filter(PageContent.page_type == PageType.HOME).first()
|
||||
|
||||
if not content:
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"page_content": None
|
||||
}
|
||||
}
|
||||
|
||||
return {'status': 'success', 'data': {'page_content': None}}
|
||||
content_dict = serialize_page_content(content)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"page_content": content_dict
|
||||
}
|
||||
}
|
||||
return {'status': 'success', 'data': {'page_content': content_dict}}
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching home content: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error fetching home content: {str(e)}"
|
||||
)
|
||||
|
||||
logger.error(f'Error fetching home content: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching home content: {str(e)}')
|
||||
@@ -2,139 +2,60 @@ from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ..models.invoice import Invoice, InvoiceStatus
|
||||
from ..models.booking import Booking
|
||||
from ..services.invoice_service import InvoiceService
|
||||
router = APIRouter(prefix='/invoices', tags=['invoices'])
|
||||
|
||||
router = APIRouter(prefix="/invoices", tags=["invoices"])
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_invoices(
|
||||
booking_id: Optional[int] = Query(None),
|
||||
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(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get invoices for current user (or all invoices for admin)"""
|
||||
@router.get('/')
|
||||
async def get_invoices(booking_id: Optional[int]=Query(None), 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(get_current_user), db: Session=Depends(get_db)):
|
||||
try:
|
||||
# Admin can see all invoices, users can only see their own
|
||||
user_id = None if current_user.role_id == 1 else current_user.id
|
||||
|
||||
result = InvoiceService.get_invoices(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
booking_id=booking_id,
|
||||
status=status_filter,
|
||||
page=page,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": result
|
||||
}
|
||||
result = InvoiceService.get_invoices(db=db, user_id=user_id, booking_id=booking_id, status=status_filter, page=page, limit=limit)
|
||||
return {'status': 'success', 'data': result}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{id}")
|
||||
async def get_invoice_by_id(
|
||||
id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get invoice by ID"""
|
||||
@router.get('/{id}')
|
||||
async def get_invoice_by_id(id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
try:
|
||||
invoice = InvoiceService.get_invoice(id, db)
|
||||
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Invoice not found")
|
||||
|
||||
# Check access: admin can see all, users can only see their own
|
||||
if current_user.role_id != 1 and invoice["user_id"] != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {"invoice": invoice}
|
||||
}
|
||||
raise HTTPException(status_code=404, detail='Invoice not found')
|
||||
if current_user.role_id != 1 and invoice['user_id'] != current_user.id:
|
||||
raise HTTPException(status_code=403, detail='Forbidden')
|
||||
return {'status': 'success', 'data': {'invoice': invoice}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_invoice(
|
||||
invoice_data: dict,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create a new invoice from a booking (Admin/Staff only)"""
|
||||
@router.post('/')
|
||||
async def create_invoice(invoice_data: dict, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
try:
|
||||
# Only admin/staff can create invoices
|
||||
if current_user.role_id not in [1, 2]:
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
|
||||
booking_id = invoice_data.get("booking_id")
|
||||
raise HTTPException(status_code=403, detail='Forbidden')
|
||||
booking_id = invoice_data.get('booking_id')
|
||||
if not booking_id:
|
||||
raise HTTPException(status_code=400, detail="booking_id is required")
|
||||
|
||||
# Ensure booking_id is an integer
|
||||
raise HTTPException(status_code=400, detail='booking_id is required')
|
||||
try:
|
||||
booking_id = int(booking_id)
|
||||
except (ValueError, TypeError):
|
||||
raise HTTPException(status_code=400, detail="booking_id must be a valid integer")
|
||||
|
||||
# Check if booking exists
|
||||
raise HTTPException(status_code=400, detail='booking_id must be a valid integer')
|
||||
booking = db.query(Booking).filter(Booking.id == booking_id).first()
|
||||
if not booking:
|
||||
raise HTTPException(status_code=404, detail="Booking not found")
|
||||
|
||||
# Prepare invoice kwargs
|
||||
invoice_kwargs = {
|
||||
"company_name": invoice_data.get("company_name"),
|
||||
"company_address": invoice_data.get("company_address"),
|
||||
"company_phone": invoice_data.get("company_phone"),
|
||||
"company_email": invoice_data.get("company_email"),
|
||||
"company_tax_id": invoice_data.get("company_tax_id"),
|
||||
"company_logo_url": invoice_data.get("company_logo_url"),
|
||||
"customer_tax_id": invoice_data.get("customer_tax_id"),
|
||||
"notes": invoice_data.get("notes"),
|
||||
"terms_and_conditions": invoice_data.get("terms_and_conditions"),
|
||||
"payment_instructions": invoice_data.get("payment_instructions"),
|
||||
}
|
||||
|
||||
# Add promotion code to invoice notes if present in booking
|
||||
invoice_notes = invoice_kwargs.get("notes", "")
|
||||
raise HTTPException(status_code=404, detail='Booking not found')
|
||||
invoice_kwargs = {'company_name': invoice_data.get('company_name'), 'company_address': invoice_data.get('company_address'), 'company_phone': invoice_data.get('company_phone'), 'company_email': invoice_data.get('company_email'), 'company_tax_id': invoice_data.get('company_tax_id'), 'company_logo_url': invoice_data.get('company_logo_url'), 'customer_tax_id': invoice_data.get('customer_tax_id'), 'notes': invoice_data.get('notes'), 'terms_and_conditions': invoice_data.get('terms_and_conditions'), 'payment_instructions': invoice_data.get('payment_instructions')}
|
||||
invoice_notes = invoice_kwargs.get('notes', '')
|
||||
if booking.promotion_code:
|
||||
promotion_note = f"Promotion Code: {booking.promotion_code}"
|
||||
invoice_notes = f"{promotion_note}\n{invoice_notes}".strip() if invoice_notes else promotion_note
|
||||
invoice_kwargs["notes"] = invoice_notes
|
||||
|
||||
# Create invoice
|
||||
invoice = InvoiceService.create_invoice_from_booking(
|
||||
booking_id=booking_id,
|
||||
db=db,
|
||||
created_by_id=current_user.id,
|
||||
tax_rate=invoice_data.get("tax_rate", 0.0),
|
||||
discount_amount=invoice_data.get("discount_amount", 0.0),
|
||||
due_days=invoice_data.get("due_days", 30),
|
||||
**invoice_kwargs
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Invoice created successfully",
|
||||
"data": {"invoice": invoice}
|
||||
}
|
||||
promotion_note = f'Promotion Code: {booking.promotion_code}'
|
||||
invoice_notes = f'{promotion_note}\n{invoice_notes}'.strip() if invoice_notes else promotion_note
|
||||
invoice_kwargs['notes'] = invoice_notes
|
||||
invoice = InvoiceService.create_invoice_from_booking(booking_id=booking_id, db=db, created_by_id=current_user.id, tax_rate=invoice_data.get('tax_rate', 0.0), discount_amount=invoice_data.get('discount_amount', 0.0), due_days=invoice_data.get('due_days', 30), **invoice_kwargs)
|
||||
return {'status': 'success', 'message': 'Invoice created successfully', 'data': {'invoice': invoice}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
@@ -142,33 +63,14 @@ async def create_invoice(
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
async def update_invoice(
|
||||
id: int,
|
||||
invoice_data: dict,
|
||||
current_user: User = Depends(authorize_roles("admin", "staff")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update an invoice (Admin/Staff only)"""
|
||||
@router.put('/{id}')
|
||||
async def update_invoice(id: int, invoice_data: dict, current_user: User=Depends(authorize_roles('admin', 'staff')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
invoice = db.query(Invoice).filter(Invoice.id == id).first()
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Invoice not found")
|
||||
|
||||
# Update invoice
|
||||
updated_invoice = InvoiceService.update_invoice(
|
||||
invoice_id=id,
|
||||
db=db,
|
||||
updated_by_id=current_user.id,
|
||||
**invoice_data
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Invoice updated successfully",
|
||||
"data": {"invoice": updated_invoice}
|
||||
}
|
||||
raise HTTPException(status_code=404, detail='Invoice not found')
|
||||
updated_invoice = InvoiceService.update_invoice(invoice_id=id, db=db, updated_by_id=current_user.id, **invoice_data)
|
||||
return {'status': 'success', 'message': 'Invoice updated successfully', 'data': {'invoice': updated_invoice}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
@@ -176,30 +78,12 @@ async def update_invoice(
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{id}/mark-paid")
|
||||
async def mark_invoice_as_paid(
|
||||
id: int,
|
||||
payment_data: dict,
|
||||
current_user: User = Depends(authorize_roles("admin", "staff")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Mark an invoice as paid (Admin/Staff only)"""
|
||||
@router.post('/{id}/mark-paid')
|
||||
async def mark_invoice_as_paid(id: int, payment_data: dict, current_user: User=Depends(authorize_roles('admin', 'staff')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
amount = payment_data.get("amount")
|
||||
|
||||
updated_invoice = InvoiceService.mark_invoice_as_paid(
|
||||
invoice_id=id,
|
||||
db=db,
|
||||
amount=amount,
|
||||
updated_by_id=current_user.id
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Invoice marked as paid successfully",
|
||||
"data": {"invoice": updated_invoice}
|
||||
}
|
||||
amount = payment_data.get('amount')
|
||||
updated_invoice = InvoiceService.mark_invoice_as_paid(invoice_id=id, db=db, amount=amount, updated_by_id=current_user.id)
|
||||
return {'status': 'success', 'message': 'Invoice marked as paid successfully', 'data': {'invoice': updated_invoice}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
@@ -207,61 +91,32 @@ async def mark_invoice_as_paid(
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
async def delete_invoice(
|
||||
id: int,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Delete an invoice (Admin only)"""
|
||||
@router.delete('/{id}')
|
||||
async def delete_invoice(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
invoice = db.query(Invoice).filter(Invoice.id == id).first()
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Invoice not found")
|
||||
|
||||
raise HTTPException(status_code=404, detail='Invoice not found')
|
||||
db.delete(invoice)
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Invoice deleted successfully"
|
||||
}
|
||||
return {'status': 'success', 'message': 'Invoice deleted successfully'}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/booking/{booking_id}")
|
||||
async def get_invoices_by_booking(
|
||||
booking_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get all invoices for a specific booking"""
|
||||
@router.get('/booking/{booking_id}')
|
||||
async def get_invoices_by_booking(booking_id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
try:
|
||||
# Check if booking exists and user has access
|
||||
booking = db.query(Booking).filter(Booking.id == booking_id).first()
|
||||
if not booking:
|
||||
raise HTTPException(status_code=404, detail="Booking not found")
|
||||
|
||||
# Check access: admin can see all, users can only see their own bookings
|
||||
raise HTTPException(status_code=404, detail='Booking not found')
|
||||
if current_user.role_id != 1 and booking.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
|
||||
result = InvoiceService.get_invoices(
|
||||
db=db,
|
||||
booking_id=booking_id
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": result
|
||||
}
|
||||
raise HTTPException(status_code=403, detail='Forbidden')
|
||||
result = InvoiceService.get_invoices(db=db, booking_id=booking_id)
|
||||
return {'status': 'success', 'data': result}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1,111 +1,39 @@
|
||||
from fastapi import APIRouter, Depends, Request, Response, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..config.logging_config import get_logger
|
||||
from ..config.settings import settings
|
||||
from ..middleware.cookie_consent import COOKIE_CONSENT_COOKIE_NAME, _parse_consent_cookie
|
||||
from ..schemas.admin_privacy import PublicPrivacyConfigResponse
|
||||
from ..schemas.privacy import (
|
||||
CookieCategoryPreferences,
|
||||
CookieConsent,
|
||||
CookieConsentResponse,
|
||||
UpdateCookieConsentRequest,
|
||||
)
|
||||
from ..schemas.privacy import CookieCategoryPreferences, CookieConsent, CookieConsentResponse, UpdateCookieConsentRequest
|
||||
from ..services.privacy_admin_service import privacy_admin_service
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter(prefix='/privacy', tags=['privacy'])
|
||||
|
||||
router = APIRouter(prefix="/privacy", tags=["privacy"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"/cookie-consent",
|
||||
response_model=CookieConsentResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
@router.get('/cookie-consent', response_model=CookieConsentResponse, status_code=status.HTTP_200_OK)
|
||||
async def get_cookie_consent(request: Request) -> CookieConsentResponse:
|
||||
"""
|
||||
Return the current cookie consent preferences.
|
||||
Reads from the cookie (if present) or returns default (necessary only).
|
||||
"""
|
||||
raw_cookie = request.cookies.get(COOKIE_CONSENT_COOKIE_NAME)
|
||||
consent = _parse_consent_cookie(raw_cookie)
|
||||
|
||||
# Ensure necessary is always true
|
||||
consent.categories.necessary = True
|
||||
|
||||
return CookieConsentResponse(data=consent)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/cookie-consent",
|
||||
response_model=CookieConsentResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
async def update_cookie_consent(
|
||||
request: UpdateCookieConsentRequest, response: Response
|
||||
) -> CookieConsentResponse:
|
||||
"""
|
||||
Update cookie consent preferences.
|
||||
|
||||
The 'necessary' category is controlled by the server and always true.
|
||||
"""
|
||||
# Build categories from existing cookie (if any) so partial updates work
|
||||
existing_raw = response.headers.get("cookie") # usually empty here
|
||||
# We can't reliably read cookies from the response; rely on defaults.
|
||||
# For the purposes of this API, we always start from defaults and then
|
||||
# override with the request payload.
|
||||
@router.post('/cookie-consent', response_model=CookieConsentResponse, status_code=status.HTTP_200_OK)
|
||||
async def update_cookie_consent(request: UpdateCookieConsentRequest, response: Response) -> CookieConsentResponse:
|
||||
existing_raw = response.headers.get('cookie')
|
||||
categories = CookieCategoryPreferences()
|
||||
|
||||
if request.analytics is not None:
|
||||
categories.analytics = request.analytics
|
||||
if request.marketing is not None:
|
||||
categories.marketing = request.marketing
|
||||
if request.preferences is not None:
|
||||
categories.preferences = request.preferences
|
||||
|
||||
# 'necessary' enforced server-side
|
||||
categories.necessary = True
|
||||
|
||||
consent = CookieConsent(categories=categories, has_decided=True)
|
||||
|
||||
# Persist consent as a secure, HttpOnly cookie
|
||||
response.set_cookie(
|
||||
key=COOKIE_CONSENT_COOKIE_NAME,
|
||||
value=consent.model_dump_json(),
|
||||
httponly=True,
|
||||
secure=settings.is_production,
|
||||
samesite="lax",
|
||||
max_age=365 * 24 * 60 * 60, # 1 year
|
||||
path="/",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Cookie consent updated: analytics=%s, marketing=%s, preferences=%s",
|
||||
consent.categories.analytics,
|
||||
consent.categories.marketing,
|
||||
consent.categories.preferences,
|
||||
)
|
||||
|
||||
response.set_cookie(key=COOKIE_CONSENT_COOKIE_NAME, value=consent.model_dump_json(), httponly=True, secure=settings.is_production, samesite='lax', max_age=365 * 24 * 60 * 60, path='/')
|
||||
logger.info('Cookie consent updated: analytics=%s, marketing=%s, preferences=%s', consent.categories.analytics, consent.categories.marketing, consent.categories.preferences)
|
||||
return CookieConsentResponse(data=consent)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/config",
|
||||
response_model=PublicPrivacyConfigResponse,
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
async def get_public_privacy_config(
|
||||
db: Session = Depends(get_db),
|
||||
) -> PublicPrivacyConfigResponse:
|
||||
"""
|
||||
Public privacy configuration for the frontend:
|
||||
- Global policy flags
|
||||
- Public integration IDs (e.g. GA measurement ID)
|
||||
"""
|
||||
@router.get('/config', response_model=PublicPrivacyConfigResponse, status_code=status.HTTP_200_OK)
|
||||
async def get_public_privacy_config(db: Session=Depends(get_db)) -> PublicPrivacyConfigResponse:
|
||||
config = privacy_admin_service.get_public_privacy_config(db)
|
||||
return PublicPrivacyConfigResponse(data=config)
|
||||
|
||||
|
||||
return PublicPrivacyConfigResponse(data=config)
|
||||
@@ -3,346 +3,158 @@ from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ..models.promotion import Promotion, DiscountType
|
||||
router = APIRouter(prefix='/promotions', tags=['promotions'])
|
||||
|
||||
router = APIRouter(prefix="/promotions", tags=["promotions"])
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_promotions(
|
||||
search: Optional[str] = Query(None),
|
||||
status_filter: Optional[str] = Query(None, alias="status"),
|
||||
type: Optional[str] = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
limit: int = Query(10, ge=1, le=100),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get all promotions with filters"""
|
||||
@router.get('/')
|
||||
async def get_promotions(search: Optional[str]=Query(None), status_filter: Optional[str]=Query(None, alias='status'), type: Optional[str]=Query(None), page: int=Query(1, ge=1), limit: int=Query(10, ge=1, le=100), db: Session=Depends(get_db)):
|
||||
try:
|
||||
query = db.query(Promotion)
|
||||
|
||||
# Filter by search (code or name)
|
||||
if search:
|
||||
query = query.filter(
|
||||
or_(
|
||||
Promotion.code.like(f"%{search}%"),
|
||||
Promotion.name.like(f"%{search}%")
|
||||
)
|
||||
)
|
||||
|
||||
# Filter by status (is_active)
|
||||
query = query.filter(or_(Promotion.code.like(f'%{search}%'), Promotion.name.like(f'%{search}%')))
|
||||
if status_filter:
|
||||
is_active = status_filter == "active"
|
||||
is_active = status_filter == 'active'
|
||||
query = query.filter(Promotion.is_active == is_active)
|
||||
|
||||
# Filter by discount type
|
||||
if type:
|
||||
try:
|
||||
query = query.filter(Promotion.discount_type == DiscountType(type))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
total = query.count()
|
||||
offset = (page - 1) * limit
|
||||
promotions = query.order_by(Promotion.created_at.desc()).offset(offset).limit(limit).all()
|
||||
|
||||
result = []
|
||||
for promo in promotions:
|
||||
promo_dict = {
|
||||
"id": promo.id,
|
||||
"code": promo.code,
|
||||
"name": promo.name,
|
||||
"description": promo.description,
|
||||
"discount_type": promo.discount_type.value if isinstance(promo.discount_type, DiscountType) else promo.discount_type,
|
||||
"discount_value": float(promo.discount_value) if promo.discount_value else 0.0,
|
||||
"min_booking_amount": float(promo.min_booking_amount) if promo.min_booking_amount else None,
|
||||
"max_discount_amount": float(promo.max_discount_amount) if promo.max_discount_amount else None,
|
||||
"start_date": promo.start_date.isoformat() if promo.start_date else None,
|
||||
"end_date": promo.end_date.isoformat() if promo.end_date else None,
|
||||
"usage_limit": promo.usage_limit,
|
||||
"used_count": promo.used_count,
|
||||
"is_active": promo.is_active,
|
||||
"created_at": promo.created_at.isoformat() if promo.created_at else None,
|
||||
}
|
||||
promo_dict = {'id': promo.id, 'code': promo.code, 'name': promo.name, 'description': promo.description, 'discount_type': promo.discount_type.value if isinstance(promo.discount_type, DiscountType) else promo.discount_type, 'discount_value': float(promo.discount_value) if promo.discount_value else 0.0, 'min_booking_amount': float(promo.min_booking_amount) if promo.min_booking_amount else None, 'max_discount_amount': float(promo.max_discount_amount) if promo.max_discount_amount else None, 'start_date': promo.start_date.isoformat() if promo.start_date else None, 'end_date': promo.end_date.isoformat() if promo.end_date else None, 'usage_limit': promo.usage_limit, 'used_count': promo.used_count, 'is_active': promo.is_active, 'created_at': promo.created_at.isoformat() if promo.created_at else None}
|
||||
result.append(promo_dict)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"promotions": result,
|
||||
"pagination": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"totalPages": (total + limit - 1) // limit,
|
||||
},
|
||||
},
|
||||
}
|
||||
return {'status': 'success', 'data': {'promotions': result, 'pagination': {'total': total, 'page': page, 'limit': limit, 'totalPages': (total + limit - 1) // limit}}}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{code}")
|
||||
async def get_promotion_by_code(code: str, db: Session = Depends(get_db)):
|
||||
"""Get promotion by code"""
|
||||
@router.get('/{code}')
|
||||
async def get_promotion_by_code(code: str, db: Session=Depends(get_db)):
|
||||
try:
|
||||
promotion = db.query(Promotion).filter(Promotion.code == code).first()
|
||||
if not promotion:
|
||||
raise HTTPException(status_code=404, detail="Promotion not found")
|
||||
|
||||
promo_dict = {
|
||||
"id": promotion.id,
|
||||
"code": promotion.code,
|
||||
"name": promotion.name,
|
||||
"description": promotion.description,
|
||||
"discount_type": promotion.discount_type.value if isinstance(promotion.discount_type, DiscountType) else promotion.discount_type,
|
||||
"discount_value": float(promotion.discount_value) if promotion.discount_value else 0.0,
|
||||
"min_booking_amount": float(promotion.min_booking_amount) if promotion.min_booking_amount else None,
|
||||
"max_discount_amount": float(promotion.max_discount_amount) if promotion.max_discount_amount else None,
|
||||
"start_date": promotion.start_date.isoformat() if promotion.start_date else None,
|
||||
"end_date": promotion.end_date.isoformat() if promotion.end_date else None,
|
||||
"usage_limit": promotion.usage_limit,
|
||||
"used_count": promotion.used_count,
|
||||
"is_active": promotion.is_active,
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {"promotion": promo_dict}
|
||||
}
|
||||
raise HTTPException(status_code=404, detail='Promotion not found')
|
||||
promo_dict = {'id': promotion.id, 'code': promotion.code, 'name': promotion.name, 'description': promotion.description, 'discount_type': promotion.discount_type.value if isinstance(promotion.discount_type, DiscountType) else promotion.discount_type, 'discount_value': float(promotion.discount_value) if promotion.discount_value else 0.0, 'min_booking_amount': float(promotion.min_booking_amount) if promotion.min_booking_amount else None, 'max_discount_amount': float(promotion.max_discount_amount) if promotion.max_discount_amount else None, 'start_date': promotion.start_date.isoformat() if promotion.start_date else None, 'end_date': promotion.end_date.isoformat() if promotion.end_date else None, 'usage_limit': promotion.usage_limit, 'used_count': promotion.used_count, 'is_active': promotion.is_active}
|
||||
return {'status': 'success', 'data': {'promotion': promo_dict}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/validate")
|
||||
async def validate_promotion(
|
||||
validation_data: dict,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Validate and apply promotion"""
|
||||
@router.post('/validate')
|
||||
async def validate_promotion(validation_data: dict, db: Session=Depends(get_db)):
|
||||
try:
|
||||
code = validation_data.get("code")
|
||||
# Accept both booking_value (from frontend) and booking_amount (for backward compatibility)
|
||||
booking_amount = float(validation_data.get("booking_value") or validation_data.get("booking_amount", 0))
|
||||
|
||||
code = validation_data.get('code')
|
||||
booking_amount = float(validation_data.get('booking_value') or validation_data.get('booking_amount', 0))
|
||||
promotion = db.query(Promotion).filter(Promotion.code == code).first()
|
||||
if not promotion:
|
||||
raise HTTPException(status_code=404, detail="Promotion code not found")
|
||||
|
||||
# Check if promotion is active
|
||||
raise HTTPException(status_code=404, detail='Promotion code not found')
|
||||
if not promotion.is_active:
|
||||
raise HTTPException(status_code=400, detail="Promotion is not active")
|
||||
|
||||
# Check date validity
|
||||
raise HTTPException(status_code=400, detail='Promotion is not active')
|
||||
now = datetime.utcnow()
|
||||
if promotion.start_date and now < promotion.start_date:
|
||||
raise HTTPException(status_code=400, detail="Promotion is not valid at this time")
|
||||
raise HTTPException(status_code=400, detail='Promotion is not valid at this time')
|
||||
if promotion.end_date and now > promotion.end_date:
|
||||
raise HTTPException(status_code=400, detail="Promotion is not valid at this time")
|
||||
|
||||
# Check usage limit
|
||||
raise HTTPException(status_code=400, detail='Promotion is not valid at this time')
|
||||
if promotion.usage_limit and promotion.used_count >= promotion.usage_limit:
|
||||
raise HTTPException(status_code=400, detail="Promotion usage limit reached")
|
||||
|
||||
# Check minimum booking amount
|
||||
raise HTTPException(status_code=400, detail='Promotion usage limit reached')
|
||||
if promotion.min_booking_amount and booking_amount < float(promotion.min_booking_amount):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Minimum booking amount is {promotion.min_booking_amount}"
|
||||
)
|
||||
|
||||
# Calculate discount
|
||||
raise HTTPException(status_code=400, detail=f'Minimum booking amount is {promotion.min_booking_amount}')
|
||||
discount_amount = promotion.calculate_discount(booking_amount)
|
||||
final_amount = booking_amount - discount_amount
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"status": "success",
|
||||
"data": {
|
||||
"promotion": {
|
||||
"id": promotion.id,
|
||||
"code": promotion.code,
|
||||
"name": promotion.name,
|
||||
"description": promotion.description,
|
||||
"discount_type": promotion.discount_type.value if hasattr(promotion.discount_type, 'value') else str(promotion.discount_type),
|
||||
"discount_value": float(promotion.discount_value) if promotion.discount_value else 0,
|
||||
"min_booking_amount": float(promotion.min_booking_amount) if promotion.min_booking_amount else None,
|
||||
"max_discount_amount": float(promotion.max_discount_amount) if promotion.max_discount_amount else None,
|
||||
"start_date": promotion.start_date.isoformat() if promotion.start_date else None,
|
||||
"end_date": promotion.end_date.isoformat() if promotion.end_date else None,
|
||||
"usage_limit": promotion.usage_limit,
|
||||
"used_count": promotion.used_count,
|
||||
"status": "active" if promotion.is_active else "inactive",
|
||||
},
|
||||
"discount": discount_amount,
|
||||
"original_amount": booking_amount,
|
||||
"discount_amount": discount_amount,
|
||||
"final_amount": final_amount,
|
||||
},
|
||||
"message": "Promotion validated successfully"
|
||||
}
|
||||
return {'success': True, 'status': 'success', 'data': {'promotion': {'id': promotion.id, 'code': promotion.code, 'name': promotion.name, 'description': promotion.description, 'discount_type': promotion.discount_type.value if hasattr(promotion.discount_type, 'value') else str(promotion.discount_type), 'discount_value': float(promotion.discount_value) if promotion.discount_value else 0, 'min_booking_amount': float(promotion.min_booking_amount) if promotion.min_booking_amount else None, 'max_discount_amount': float(promotion.max_discount_amount) if promotion.max_discount_amount else None, 'start_date': promotion.start_date.isoformat() if promotion.start_date else None, 'end_date': promotion.end_date.isoformat() if promotion.end_date else None, 'usage_limit': promotion.usage_limit, 'used_count': promotion.used_count, 'status': 'active' if promotion.is_active else 'inactive'}, 'discount': discount_amount, 'original_amount': booking_amount, 'discount_amount': discount_amount, 'final_amount': final_amount}, 'message': 'Promotion validated successfully'}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/", dependencies=[Depends(authorize_roles("admin"))])
|
||||
async def create_promotion(
|
||||
promotion_data: dict,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create new promotion (Admin only)"""
|
||||
@router.post('/', dependencies=[Depends(authorize_roles('admin'))])
|
||||
async def create_promotion(promotion_data: dict, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
code = promotion_data.get("code")
|
||||
|
||||
# Check if code exists
|
||||
code = promotion_data.get('code')
|
||||
existing = db.query(Promotion).filter(Promotion.code == code).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="Promotion code already exists")
|
||||
|
||||
discount_type = promotion_data.get("discount_type")
|
||||
discount_value = float(promotion_data.get("discount_value", 0))
|
||||
|
||||
# Validate discount value
|
||||
if discount_type == "percentage" and discount_value > 100:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Percentage discount cannot exceed 100%"
|
||||
)
|
||||
|
||||
promotion = Promotion(
|
||||
code=code,
|
||||
name=promotion_data.get("name"),
|
||||
description=promotion_data.get("description"),
|
||||
discount_type=DiscountType(discount_type),
|
||||
discount_value=discount_value,
|
||||
min_booking_amount=float(promotion_data["min_booking_amount"]) if promotion_data.get("min_booking_amount") else None,
|
||||
max_discount_amount=float(promotion_data["max_discount_amount"]) if promotion_data.get("max_discount_amount") else None,
|
||||
start_date=datetime.fromisoformat(promotion_data["start_date"].replace('Z', '+00:00')) if promotion_data.get("start_date") else None,
|
||||
end_date=datetime.fromisoformat(promotion_data["end_date"].replace('Z', '+00:00')) if promotion_data.get("end_date") else None,
|
||||
usage_limit=promotion_data.get("usage_limit"),
|
||||
used_count=0,
|
||||
is_active=promotion_data.get("status") == "active" if promotion_data.get("status") else True,
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=400, detail='Promotion code already exists')
|
||||
discount_type = promotion_data.get('discount_type')
|
||||
discount_value = float(promotion_data.get('discount_value', 0))
|
||||
if discount_type == 'percentage' and discount_value > 100:
|
||||
raise HTTPException(status_code=400, detail='Percentage discount cannot exceed 100%')
|
||||
promotion = Promotion(code=code, name=promotion_data.get('name'), description=promotion_data.get('description'), discount_type=DiscountType(discount_type), discount_value=discount_value, min_booking_amount=float(promotion_data['min_booking_amount']) if promotion_data.get('min_booking_amount') else None, max_discount_amount=float(promotion_data['max_discount_amount']) if promotion_data.get('max_discount_amount') else None, start_date=datetime.fromisoformat(promotion_data['start_date'].replace('Z', '+00:00')) if promotion_data.get('start_date') else None, end_date=datetime.fromisoformat(promotion_data['end_date'].replace('Z', '+00:00')) if promotion_data.get('end_date') else None, usage_limit=promotion_data.get('usage_limit'), used_count=0, is_active=promotion_data.get('status') == 'active' if promotion_data.get('status') else True)
|
||||
db.add(promotion)
|
||||
db.commit()
|
||||
db.refresh(promotion)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Promotion created successfully",
|
||||
"data": {"promotion": promotion}
|
||||
}
|
||||
return {'status': 'success', 'message': 'Promotion created successfully', 'data': {'promotion': promotion}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/{id}", dependencies=[Depends(authorize_roles("admin"))])
|
||||
async def update_promotion(
|
||||
id: int,
|
||||
promotion_data: dict,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update promotion (Admin only)"""
|
||||
@router.put('/{id}', dependencies=[Depends(authorize_roles('admin'))])
|
||||
async def update_promotion(id: int, promotion_data: dict, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
promotion = db.query(Promotion).filter(Promotion.id == id).first()
|
||||
if not promotion:
|
||||
raise HTTPException(status_code=404, detail="Promotion not found")
|
||||
|
||||
# Check if new code exists (excluding current)
|
||||
code = promotion_data.get("code")
|
||||
raise HTTPException(status_code=404, detail='Promotion not found')
|
||||
code = promotion_data.get('code')
|
||||
if code and code != promotion.code:
|
||||
existing = db.query(Promotion).filter(
|
||||
Promotion.code == code,
|
||||
Promotion.id != id
|
||||
).first()
|
||||
existing = db.query(Promotion).filter(Promotion.code == code, Promotion.id != id).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="Promotion code already exists")
|
||||
|
||||
# Validate discount value
|
||||
discount_type = promotion_data.get("discount_type", promotion.discount_type.value if isinstance(promotion.discount_type, DiscountType) else promotion.discount_type)
|
||||
discount_value = promotion_data.get("discount_value")
|
||||
raise HTTPException(status_code=400, detail='Promotion code already exists')
|
||||
discount_type = promotion_data.get('discount_type', promotion.discount_type.value if isinstance(promotion.discount_type, DiscountType) else promotion.discount_type)
|
||||
discount_value = promotion_data.get('discount_value')
|
||||
if discount_value is not None:
|
||||
discount_value = float(discount_value)
|
||||
if discount_type == "percentage" and discount_value > 100:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Percentage discount cannot exceed 100%"
|
||||
)
|
||||
|
||||
# Update fields
|
||||
if "code" in promotion_data:
|
||||
promotion.code = promotion_data["code"]
|
||||
if "name" in promotion_data:
|
||||
promotion.name = promotion_data["name"]
|
||||
if "description" in promotion_data:
|
||||
promotion.description = promotion_data["description"]
|
||||
if "discount_type" in promotion_data:
|
||||
promotion.discount_type = DiscountType(promotion_data["discount_type"])
|
||||
if "discount_value" in promotion_data:
|
||||
if discount_type == 'percentage' and discount_value > 100:
|
||||
raise HTTPException(status_code=400, detail='Percentage discount cannot exceed 100%')
|
||||
if 'code' in promotion_data:
|
||||
promotion.code = promotion_data['code']
|
||||
if 'name' in promotion_data:
|
||||
promotion.name = promotion_data['name']
|
||||
if 'description' in promotion_data:
|
||||
promotion.description = promotion_data['description']
|
||||
if 'discount_type' in promotion_data:
|
||||
promotion.discount_type = DiscountType(promotion_data['discount_type'])
|
||||
if 'discount_value' in promotion_data:
|
||||
promotion.discount_value = discount_value
|
||||
if "min_booking_amount" in promotion_data:
|
||||
promotion.min_booking_amount = float(promotion_data["min_booking_amount"]) if promotion_data["min_booking_amount"] else None
|
||||
if "max_discount_amount" in promotion_data:
|
||||
promotion.max_discount_amount = float(promotion_data["max_discount_amount"]) if promotion_data["max_discount_amount"] else None
|
||||
if "start_date" in promotion_data:
|
||||
promotion.start_date = datetime.fromisoformat(promotion_data["start_date"].replace('Z', '+00:00')) if promotion_data["start_date"] else None
|
||||
if "end_date" in promotion_data:
|
||||
promotion.end_date = datetime.fromisoformat(promotion_data["end_date"].replace('Z', '+00:00')) if promotion_data["end_date"] else None
|
||||
if "usage_limit" in promotion_data:
|
||||
promotion.usage_limit = promotion_data["usage_limit"]
|
||||
if "status" in promotion_data:
|
||||
promotion.is_active = promotion_data["status"] == "active"
|
||||
|
||||
if 'min_booking_amount' in promotion_data:
|
||||
promotion.min_booking_amount = float(promotion_data['min_booking_amount']) if promotion_data['min_booking_amount'] else None
|
||||
if 'max_discount_amount' in promotion_data:
|
||||
promotion.max_discount_amount = float(promotion_data['max_discount_amount']) if promotion_data['max_discount_amount'] else None
|
||||
if 'start_date' in promotion_data:
|
||||
promotion.start_date = datetime.fromisoformat(promotion_data['start_date'].replace('Z', '+00:00')) if promotion_data['start_date'] else None
|
||||
if 'end_date' in promotion_data:
|
||||
promotion.end_date = datetime.fromisoformat(promotion_data['end_date'].replace('Z', '+00:00')) if promotion_data['end_date'] else None
|
||||
if 'usage_limit' in promotion_data:
|
||||
promotion.usage_limit = promotion_data['usage_limit']
|
||||
if 'status' in promotion_data:
|
||||
promotion.is_active = promotion_data['status'] == 'active'
|
||||
db.commit()
|
||||
db.refresh(promotion)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Promotion updated successfully",
|
||||
"data": {"promotion": promotion}
|
||||
}
|
||||
return {'status': 'success', 'message': 'Promotion updated successfully', 'data': {'promotion': promotion}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/{id}", dependencies=[Depends(authorize_roles("admin"))])
|
||||
async def delete_promotion(
|
||||
id: int,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Delete promotion (Admin only)"""
|
||||
@router.delete('/{id}', dependencies=[Depends(authorize_roles('admin'))])
|
||||
async def delete_promotion(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
promotion = db.query(Promotion).filter(Promotion.id == id).first()
|
||||
if not promotion:
|
||||
raise HTTPException(status_code=404, detail="Promotion not found")
|
||||
|
||||
raise HTTPException(status_code=404, detail='Promotion not found')
|
||||
db.delete(promotion)
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Promotion deleted successfully"
|
||||
}
|
||||
return {'status': 'success', 'message': 'Promotion deleted successfully'}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
@@ -3,7 +3,6 @@ from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, and_
|
||||
from typing import Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
@@ -12,55 +11,34 @@ from ..models.payment import Payment, PaymentStatus
|
||||
from ..models.room import Room
|
||||
from ..models.service_usage import ServiceUsage
|
||||
from ..models.service import Service
|
||||
router = APIRouter(prefix='/reports', tags=['reports'])
|
||||
|
||||
router = APIRouter(prefix="/reports", tags=["reports"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def get_reports(
|
||||
from_date: Optional[str] = Query(None, alias="from"),
|
||||
to_date: Optional[str] = Query(None, alias="to"),
|
||||
type: Optional[str] = Query(None),
|
||||
current_user: User = Depends(authorize_roles("admin", "staff")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get comprehensive reports (Admin/Staff only)"""
|
||||
@router.get('')
|
||||
async def get_reports(from_date: Optional[str]=Query(None, alias='from'), to_date: Optional[str]=Query(None, alias='to'), type: Optional[str]=Query(None), current_user: User=Depends(authorize_roles('admin', 'staff')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
# Parse dates if provided
|
||||
start_date = None
|
||||
end_date = None
|
||||
if from_date:
|
||||
try:
|
||||
start_date = datetime.strptime(from_date, "%Y-%m-%d")
|
||||
start_date = datetime.strptime(from_date, '%Y-%m-%d')
|
||||
except ValueError:
|
||||
start_date = datetime.fromisoformat(from_date.replace('Z', '+00:00'))
|
||||
if to_date:
|
||||
try:
|
||||
end_date = datetime.strptime(to_date, "%Y-%m-%d")
|
||||
# Set to end of day
|
||||
end_date = datetime.strptime(to_date, '%Y-%m-%d')
|
||||
end_date = end_date.replace(hour=23, minute=59, second=59)
|
||||
except ValueError:
|
||||
end_date = datetime.fromisoformat(to_date.replace('Z', '+00:00'))
|
||||
|
||||
# Base queries
|
||||
booking_query = db.query(Booking)
|
||||
payment_query = db.query(Payment).filter(Payment.payment_status == PaymentStatus.completed)
|
||||
|
||||
# Apply date filters
|
||||
if start_date:
|
||||
booking_query = booking_query.filter(Booking.created_at >= start_date)
|
||||
payment_query = payment_query.filter(Payment.payment_date >= start_date)
|
||||
if end_date:
|
||||
booking_query = booking_query.filter(Booking.created_at <= end_date)
|
||||
payment_query = payment_query.filter(Payment.payment_date <= end_date)
|
||||
|
||||
# Total bookings
|
||||
total_bookings = booking_query.count()
|
||||
|
||||
# Total revenue
|
||||
total_revenue = payment_query.with_entities(func.sum(Payment.amount)).scalar() or 0.0
|
||||
|
||||
# Total customers (unique users with bookings)
|
||||
total_customers = db.query(func.count(func.distinct(Booking.user_id))).scalar() or 0
|
||||
if start_date or end_date:
|
||||
customer_query = db.query(func.count(func.distinct(Booking.user_id)))
|
||||
@@ -69,415 +47,126 @@ async def get_reports(
|
||||
if end_date:
|
||||
customer_query = customer_query.filter(Booking.created_at <= end_date)
|
||||
total_customers = customer_query.scalar() or 0
|
||||
|
||||
# Available rooms
|
||||
available_rooms = db.query(Room).filter(Room.status == "available").count()
|
||||
|
||||
# Occupied rooms (rooms with active bookings)
|
||||
occupied_rooms = db.query(func.count(func.distinct(Booking.room_id))).filter(
|
||||
Booking.status.in_([BookingStatus.confirmed, BookingStatus.checked_in])
|
||||
).scalar() or 0
|
||||
|
||||
# Revenue by date (daily breakdown)
|
||||
available_rooms = db.query(Room).filter(Room.status == 'available').count()
|
||||
occupied_rooms = db.query(func.count(func.distinct(Booking.room_id))).filter(Booking.status.in_([BookingStatus.confirmed, BookingStatus.checked_in])).scalar() or 0
|
||||
revenue_by_date = []
|
||||
if start_date and end_date:
|
||||
daily_revenue_query = db.query(
|
||||
func.date(Payment.payment_date).label('date'),
|
||||
func.sum(Payment.amount).label('revenue'),
|
||||
func.count(func.distinct(Payment.booking_id)).label('bookings')
|
||||
).filter(Payment.payment_status == PaymentStatus.completed)
|
||||
|
||||
daily_revenue_query = db.query(func.date(Payment.payment_date).label('date'), func.sum(Payment.amount).label('revenue'), func.count(func.distinct(Payment.booking_id)).label('bookings')).filter(Payment.payment_status == PaymentStatus.completed)
|
||||
if start_date:
|
||||
daily_revenue_query = daily_revenue_query.filter(Payment.payment_date >= start_date)
|
||||
if end_date:
|
||||
daily_revenue_query = daily_revenue_query.filter(Payment.payment_date <= end_date)
|
||||
|
||||
daily_revenue_query = daily_revenue_query.group_by(
|
||||
func.date(Payment.payment_date)
|
||||
).order_by(func.date(Payment.payment_date))
|
||||
|
||||
daily_revenue_query = daily_revenue_query.group_by(func.date(Payment.payment_date)).order_by(func.date(Payment.payment_date))
|
||||
daily_data = daily_revenue_query.all()
|
||||
revenue_by_date = [
|
||||
{
|
||||
"date": str(date),
|
||||
"revenue": float(revenue or 0),
|
||||
"bookings": int(bookings or 0)
|
||||
}
|
||||
for date, revenue, bookings in daily_data
|
||||
]
|
||||
|
||||
# Bookings by status
|
||||
revenue_by_date = [{'date': str(date), 'revenue': float(revenue or 0), 'bookings': int(bookings or 0)} for date, revenue, bookings in daily_data]
|
||||
bookings_by_status = {}
|
||||
for status in BookingStatus:
|
||||
count = booking_query.filter(Booking.status == status).count()
|
||||
status_name = status.value if hasattr(status, 'value') else str(status)
|
||||
bookings_by_status[status_name] = count
|
||||
|
||||
# Top rooms (by revenue)
|
||||
top_rooms_query = db.query(
|
||||
Room.id,
|
||||
Room.room_number,
|
||||
func.count(Booking.id).label('bookings'),
|
||||
func.sum(Payment.amount).label('revenue')
|
||||
).join(Booking, Room.id == Booking.room_id).join(
|
||||
Payment, Booking.id == Payment.booking_id
|
||||
).filter(Payment.payment_status == PaymentStatus.completed)
|
||||
|
||||
top_rooms_query = db.query(Room.id, Room.room_number, func.count(Booking.id).label('bookings'), func.sum(Payment.amount).label('revenue')).join(Booking, Room.id == Booking.room_id).join(Payment, Booking.id == Payment.booking_id).filter(Payment.payment_status == PaymentStatus.completed)
|
||||
if start_date:
|
||||
top_rooms_query = top_rooms_query.filter(Booking.created_at >= start_date)
|
||||
if end_date:
|
||||
top_rooms_query = top_rooms_query.filter(Booking.created_at <= end_date)
|
||||
|
||||
top_rooms_data = top_rooms_query.group_by(Room.id, Room.room_number).order_by(
|
||||
func.sum(Payment.amount).desc()
|
||||
).limit(10).all()
|
||||
|
||||
top_rooms = [
|
||||
{
|
||||
"room_id": room_id,
|
||||
"room_number": room_number,
|
||||
"bookings": int(bookings or 0),
|
||||
"revenue": float(revenue or 0)
|
||||
}
|
||||
for room_id, room_number, bookings, revenue in top_rooms_data
|
||||
]
|
||||
|
||||
# Service usage statistics
|
||||
service_usage_query = db.query(
|
||||
Service.id,
|
||||
Service.name,
|
||||
func.count(ServiceUsage.id).label('usage_count'),
|
||||
func.sum(ServiceUsage.total_price).label('total_revenue')
|
||||
).join(ServiceUsage, Service.id == ServiceUsage.service_id)
|
||||
|
||||
top_rooms_data = top_rooms_query.group_by(Room.id, Room.room_number).order_by(func.sum(Payment.amount).desc()).limit(10).all()
|
||||
top_rooms = [{'room_id': room_id, 'room_number': room_number, 'bookings': int(bookings or 0), 'revenue': float(revenue or 0)} for room_id, room_number, bookings, revenue in top_rooms_data]
|
||||
service_usage_query = db.query(Service.id, Service.name, func.count(ServiceUsage.id).label('usage_count'), func.sum(ServiceUsage.total_price).label('total_revenue')).join(ServiceUsage, Service.id == ServiceUsage.service_id)
|
||||
if start_date:
|
||||
service_usage_query = service_usage_query.filter(ServiceUsage.usage_date >= start_date)
|
||||
if end_date:
|
||||
service_usage_query = service_usage_query.filter(ServiceUsage.usage_date <= end_date)
|
||||
|
||||
service_usage_data = service_usage_query.group_by(Service.id, Service.name).order_by(
|
||||
func.sum(ServiceUsage.total_price).desc()
|
||||
).limit(10).all()
|
||||
|
||||
service_usage = [
|
||||
{
|
||||
"service_id": service_id,
|
||||
"service_name": service_name,
|
||||
"usage_count": int(usage_count or 0),
|
||||
"total_revenue": float(total_revenue or 0)
|
||||
}
|
||||
for service_id, service_name, usage_count, total_revenue in service_usage_data
|
||||
]
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"success": True,
|
||||
"data": {
|
||||
"total_bookings": total_bookings,
|
||||
"total_revenue": float(total_revenue),
|
||||
"total_customers": int(total_customers),
|
||||
"available_rooms": available_rooms,
|
||||
"occupied_rooms": occupied_rooms,
|
||||
"revenue_by_date": revenue_by_date if revenue_by_date else None,
|
||||
"bookings_by_status": bookings_by_status,
|
||||
"top_rooms": top_rooms if top_rooms else None,
|
||||
"service_usage": service_usage if service_usage else None,
|
||||
}
|
||||
}
|
||||
service_usage_data = service_usage_query.group_by(Service.id, Service.name).order_by(func.sum(ServiceUsage.total_price).desc()).limit(10).all()
|
||||
service_usage = [{'service_id': service_id, 'service_name': service_name, 'usage_count': int(usage_count or 0), 'total_revenue': float(total_revenue or 0)} for service_id, service_name, usage_count, total_revenue in service_usage_data]
|
||||
return {'status': 'success', 'success': True, 'data': {'total_bookings': total_bookings, 'total_revenue': float(total_revenue), 'total_customers': int(total_customers), 'available_rooms': available_rooms, 'occupied_rooms': occupied_rooms, 'revenue_by_date': revenue_by_date if revenue_by_date else None, 'bookings_by_status': bookings_by_status, 'top_rooms': top_rooms if top_rooms else None, 'service_usage': service_usage if service_usage else None}}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/dashboard")
|
||||
async def get_dashboard_stats(
|
||||
current_user: User = Depends(authorize_roles("admin", "staff")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get dashboard statistics (Admin/Staff only)"""
|
||||
@router.get('/dashboard')
|
||||
async def get_dashboard_stats(current_user: User=Depends(authorize_roles('admin', 'staff')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
# Total bookings
|
||||
total_bookings = db.query(Booking).count()
|
||||
|
||||
# Active bookings
|
||||
active_bookings = db.query(Booking).filter(
|
||||
Booking.status.in_([BookingStatus.pending, BookingStatus.confirmed, BookingStatus.checked_in])
|
||||
).count()
|
||||
|
||||
# Total revenue (from completed payments)
|
||||
total_revenue = db.query(func.sum(Payment.amount)).filter(
|
||||
Payment.payment_status == PaymentStatus.completed
|
||||
).scalar() or 0.0
|
||||
|
||||
# Today's revenue
|
||||
active_bookings = db.query(Booking).filter(Booking.status.in_([BookingStatus.pending, BookingStatus.confirmed, BookingStatus.checked_in])).count()
|
||||
total_revenue = db.query(func.sum(Payment.amount)).filter(Payment.payment_status == PaymentStatus.completed).scalar() or 0.0
|
||||
today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
today_revenue = db.query(func.sum(Payment.amount)).filter(
|
||||
and_(
|
||||
Payment.payment_status == PaymentStatus.completed,
|
||||
Payment.payment_date >= today_start
|
||||
)
|
||||
).scalar() or 0.0
|
||||
|
||||
# Total rooms
|
||||
today_revenue = db.query(func.sum(Payment.amount)).filter(and_(Payment.payment_status == PaymentStatus.completed, Payment.payment_date >= today_start)).scalar() or 0.0
|
||||
total_rooms = db.query(Room).count()
|
||||
|
||||
# Available rooms
|
||||
available_rooms = db.query(Room).filter(Room.status == "available").count()
|
||||
|
||||
# Recent bookings (last 7 days)
|
||||
available_rooms = db.query(Room).filter(Room.status == 'available').count()
|
||||
week_ago = datetime.utcnow() - timedelta(days=7)
|
||||
recent_bookings = db.query(Booking).filter(
|
||||
Booking.created_at >= week_ago
|
||||
).count()
|
||||
|
||||
# Pending payments
|
||||
pending_payments = db.query(Payment).filter(
|
||||
Payment.payment_status == PaymentStatus.pending
|
||||
).count()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"total_bookings": total_bookings,
|
||||
"active_bookings": active_bookings,
|
||||
"total_revenue": float(total_revenue),
|
||||
"today_revenue": float(today_revenue),
|
||||
"total_rooms": total_rooms,
|
||||
"available_rooms": available_rooms,
|
||||
"recent_bookings": recent_bookings,
|
||||
"pending_payments": pending_payments,
|
||||
}
|
||||
}
|
||||
recent_bookings = db.query(Booking).filter(Booking.created_at >= week_ago).count()
|
||||
pending_payments = db.query(Payment).filter(Payment.payment_status == PaymentStatus.pending).count()
|
||||
return {'status': 'success', 'data': {'total_bookings': total_bookings, 'active_bookings': active_bookings, 'total_revenue': float(total_revenue), 'today_revenue': float(today_revenue), 'total_rooms': total_rooms, 'available_rooms': available_rooms, 'recent_bookings': recent_bookings, 'pending_payments': pending_payments}}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/customer/dashboard")
|
||||
async def get_customer_dashboard_stats(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get customer dashboard statistics"""
|
||||
@router.get('/customer/dashboard')
|
||||
async def get_customer_dashboard_stats(current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
try:
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Total bookings count for user
|
||||
total_bookings = db.query(Booking).filter(
|
||||
Booking.user_id == current_user.id
|
||||
).count()
|
||||
|
||||
# Total spending (sum of completed payments from user's bookings)
|
||||
user_bookings = db.query(Booking.id).filter(
|
||||
Booking.user_id == current_user.id
|
||||
).subquery()
|
||||
|
||||
total_spending = db.query(func.sum(Payment.amount)).filter(
|
||||
and_(
|
||||
Payment.booking_id.in_(db.query(user_bookings.c.id)),
|
||||
Payment.payment_status == PaymentStatus.completed
|
||||
)
|
||||
).scalar() or 0.0
|
||||
|
||||
# Currently staying (checked_in bookings)
|
||||
total_bookings = db.query(Booking).filter(Booking.user_id == current_user.id).count()
|
||||
user_bookings = db.query(Booking.id).filter(Booking.user_id == current_user.id).subquery()
|
||||
total_spending = db.query(func.sum(Payment.amount)).filter(and_(Payment.booking_id.in_(db.query(user_bookings.c.id)), Payment.payment_status == PaymentStatus.completed)).scalar() or 0.0
|
||||
now = datetime.utcnow()
|
||||
currently_staying = db.query(Booking).filter(
|
||||
and_(
|
||||
Booking.user_id == current_user.id,
|
||||
Booking.status == BookingStatus.checked_in,
|
||||
Booking.check_in_date <= now,
|
||||
Booking.check_out_date >= now
|
||||
)
|
||||
).count()
|
||||
|
||||
# Upcoming bookings (confirmed/pending with check_in_date in future)
|
||||
upcoming_bookings_query = db.query(Booking).filter(
|
||||
and_(
|
||||
Booking.user_id == current_user.id,
|
||||
Booking.status.in_([BookingStatus.confirmed, BookingStatus.pending]),
|
||||
Booking.check_in_date > now
|
||||
)
|
||||
).order_by(Booking.check_in_date.asc()).limit(5).all()
|
||||
|
||||
currently_staying = db.query(Booking).filter(and_(Booking.user_id == current_user.id, Booking.status == BookingStatus.checked_in, Booking.check_in_date <= now, Booking.check_out_date >= now)).count()
|
||||
upcoming_bookings_query = db.query(Booking).filter(and_(Booking.user_id == current_user.id, Booking.status.in_([BookingStatus.confirmed, BookingStatus.pending]), Booking.check_in_date > now)).order_by(Booking.check_in_date.asc()).limit(5).all()
|
||||
upcoming_bookings = []
|
||||
for booking in upcoming_bookings_query:
|
||||
booking_dict = {
|
||||
"id": booking.id,
|
||||
"booking_number": booking.booking_number,
|
||||
"check_in_date": booking.check_in_date.isoformat() if booking.check_in_date else None,
|
||||
"check_out_date": booking.check_out_date.isoformat() if booking.check_out_date else None,
|
||||
"status": booking.status.value if isinstance(booking.status, BookingStatus) else booking.status,
|
||||
"total_price": float(booking.total_price) if booking.total_price else 0.0,
|
||||
}
|
||||
|
||||
booking_dict = {'id': booking.id, 'booking_number': booking.booking_number, 'check_in_date': booking.check_in_date.isoformat() if booking.check_in_date else None, 'check_out_date': booking.check_out_date.isoformat() if booking.check_out_date else None, 'status': booking.status.value if isinstance(booking.status, BookingStatus) else booking.status, 'total_price': float(booking.total_price) if booking.total_price else 0.0}
|
||||
if booking.room:
|
||||
booking_dict["room"] = {
|
||||
"id": booking.room.id,
|
||||
"room_number": booking.room.room_number,
|
||||
"room_type": {
|
||||
"name": booking.room.room_type.name if booking.room.room_type else None
|
||||
}
|
||||
}
|
||||
|
||||
booking_dict['room'] = {'id': booking.room.id, 'room_number': booking.room.room_number, 'room_type': {'name': booking.room.room_type.name if booking.room.room_type else None}}
|
||||
upcoming_bookings.append(booking_dict)
|
||||
|
||||
# Recent activity (last 5 bookings ordered by created_at)
|
||||
recent_bookings_query = db.query(Booking).filter(
|
||||
Booking.user_id == current_user.id
|
||||
).order_by(Booking.created_at.desc()).limit(5).all()
|
||||
|
||||
recent_bookings_query = db.query(Booking).filter(Booking.user_id == current_user.id).order_by(Booking.created_at.desc()).limit(5).all()
|
||||
recent_activity = []
|
||||
for booking in recent_bookings_query:
|
||||
activity_type = None
|
||||
if booking.status == BookingStatus.checked_out:
|
||||
activity_type = "Check-out"
|
||||
activity_type = 'Check-out'
|
||||
elif booking.status == BookingStatus.checked_in:
|
||||
activity_type = "Check-in"
|
||||
activity_type = 'Check-in'
|
||||
elif booking.status == BookingStatus.confirmed:
|
||||
activity_type = "Booking Confirmed"
|
||||
activity_type = 'Booking Confirmed'
|
||||
elif booking.status == BookingStatus.pending:
|
||||
activity_type = "Booking"
|
||||
activity_type = 'Booking'
|
||||
else:
|
||||
activity_type = "Booking"
|
||||
|
||||
activity_dict = {
|
||||
"action": activity_type,
|
||||
"booking_id": booking.id,
|
||||
"booking_number": booking.booking_number,
|
||||
"created_at": booking.created_at.isoformat() if booking.created_at else None,
|
||||
}
|
||||
|
||||
activity_type = 'Booking'
|
||||
activity_dict = {'action': activity_type, 'booking_id': booking.id, 'booking_number': booking.booking_number, 'created_at': booking.created_at.isoformat() if booking.created_at else None}
|
||||
if booking.room:
|
||||
activity_dict["room"] = {
|
||||
"room_number": booking.room.room_number,
|
||||
}
|
||||
|
||||
activity_dict['room'] = {'room_number': booking.room.room_number}
|
||||
recent_activity.append(activity_dict)
|
||||
|
||||
# Calculate percentage change (placeholder - can be enhanced)
|
||||
# For now, compare last month vs this month
|
||||
last_month_start = (now - timedelta(days=30)).replace(day=1, hour=0, minute=0, second=0)
|
||||
last_month_end = now.replace(day=1, hour=0, minute=0, second=0) - timedelta(seconds=1)
|
||||
|
||||
last_month_bookings = db.query(Booking).filter(
|
||||
and_(
|
||||
Booking.user_id == current_user.id,
|
||||
Booking.created_at >= last_month_start,
|
||||
Booking.created_at <= last_month_end
|
||||
)
|
||||
).count()
|
||||
|
||||
this_month_bookings = db.query(Booking).filter(
|
||||
and_(
|
||||
Booking.user_id == current_user.id,
|
||||
Booking.created_at >= now.replace(day=1, hour=0, minute=0, second=0),
|
||||
Booking.created_at <= now
|
||||
)
|
||||
).count()
|
||||
|
||||
last_month_bookings = db.query(Booking).filter(and_(Booking.user_id == current_user.id, Booking.created_at >= last_month_start, Booking.created_at <= last_month_end)).count()
|
||||
this_month_bookings = db.query(Booking).filter(and_(Booking.user_id == current_user.id, Booking.created_at >= now.replace(day=1, hour=0, minute=0, second=0), Booking.created_at <= now)).count()
|
||||
booking_change_percentage = 0
|
||||
if last_month_bookings > 0:
|
||||
booking_change_percentage = ((this_month_bookings - last_month_bookings) / last_month_bookings) * 100
|
||||
|
||||
last_month_spending = db.query(func.sum(Payment.amount)).filter(
|
||||
and_(
|
||||
Payment.booking_id.in_(db.query(user_bookings.c.id)),
|
||||
Payment.payment_status == PaymentStatus.completed,
|
||||
Payment.payment_date >= last_month_start,
|
||||
Payment.payment_date <= last_month_end
|
||||
)
|
||||
).scalar() or 0.0
|
||||
|
||||
this_month_spending = db.query(func.sum(Payment.amount)).filter(
|
||||
and_(
|
||||
Payment.booking_id.in_(db.query(user_bookings.c.id)),
|
||||
Payment.payment_status == PaymentStatus.completed,
|
||||
Payment.payment_date >= now.replace(day=1, hour=0, minute=0, second=0),
|
||||
Payment.payment_date <= now
|
||||
)
|
||||
).scalar() or 0.0
|
||||
|
||||
booking_change_percentage = (this_month_bookings - last_month_bookings) / last_month_bookings * 100
|
||||
last_month_spending = db.query(func.sum(Payment.amount)).filter(and_(Payment.booking_id.in_(db.query(user_bookings.c.id)), Payment.payment_status == PaymentStatus.completed, Payment.payment_date >= last_month_start, Payment.payment_date <= last_month_end)).scalar() or 0.0
|
||||
this_month_spending = db.query(func.sum(Payment.amount)).filter(and_(Payment.booking_id.in_(db.query(user_bookings.c.id)), Payment.payment_status == PaymentStatus.completed, Payment.payment_date >= now.replace(day=1, hour=0, minute=0, second=0), Payment.payment_date <= now)).scalar() or 0.0
|
||||
spending_change_percentage = 0
|
||||
if last_month_spending > 0:
|
||||
spending_change_percentage = ((this_month_spending - last_month_spending) / last_month_spending) * 100
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"success": True,
|
||||
"data": {
|
||||
"total_bookings": total_bookings,
|
||||
"total_spending": float(total_spending),
|
||||
"currently_staying": currently_staying,
|
||||
"upcoming_bookings": upcoming_bookings,
|
||||
"recent_activity": recent_activity,
|
||||
"booking_change_percentage": round(booking_change_percentage, 1),
|
||||
"spending_change_percentage": round(spending_change_percentage, 1),
|
||||
}
|
||||
}
|
||||
spending_change_percentage = (this_month_spending - last_month_spending) / last_month_spending * 100
|
||||
return {'status': 'success', 'success': True, 'data': {'total_bookings': total_bookings, 'total_spending': float(total_spending), 'currently_staying': currently_staying, 'upcoming_bookings': upcoming_bookings, 'recent_activity': recent_activity, 'booking_change_percentage': round(booking_change_percentage, 1), 'spending_change_percentage': round(spending_change_percentage, 1)}}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/revenue")
|
||||
async def get_revenue_report(
|
||||
start_date: Optional[str] = Query(None),
|
||||
end_date: Optional[str] = Query(None),
|
||||
current_user: User = Depends(authorize_roles("admin", "staff")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get revenue report (Admin/Staff only)"""
|
||||
@router.get('/revenue')
|
||||
async def get_revenue_report(start_date: Optional[str]=Query(None), end_date: Optional[str]=Query(None), current_user: User=Depends(authorize_roles('admin', 'staff')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
query = db.query(Payment).filter(
|
||||
Payment.payment_status == PaymentStatus.completed
|
||||
)
|
||||
|
||||
query = db.query(Payment).filter(Payment.payment_status == PaymentStatus.completed)
|
||||
if start_date:
|
||||
start = datetime.fromisoformat(start_date.replace('Z', '+00:00'))
|
||||
query = query.filter(Payment.payment_date >= start)
|
||||
|
||||
if end_date:
|
||||
end = datetime.fromisoformat(end_date.replace('Z', '+00:00'))
|
||||
query = query.filter(Payment.payment_date <= end)
|
||||
|
||||
# Total revenue
|
||||
total_revenue = db.query(func.sum(Payment.amount)).filter(
|
||||
Payment.payment_status == PaymentStatus.completed
|
||||
).scalar() or 0.0
|
||||
|
||||
# Revenue by payment method
|
||||
revenue_by_method = db.query(
|
||||
Payment.payment_method,
|
||||
func.sum(Payment.amount).label('total')
|
||||
).filter(
|
||||
Payment.payment_status == PaymentStatus.completed
|
||||
).group_by(Payment.payment_method).all()
|
||||
|
||||
total_revenue = db.query(func.sum(Payment.amount)).filter(Payment.payment_status == PaymentStatus.completed).scalar() or 0.0
|
||||
revenue_by_method = db.query(Payment.payment_method, func.sum(Payment.amount).label('total')).filter(Payment.payment_status == PaymentStatus.completed).group_by(Payment.payment_method).all()
|
||||
method_breakdown = {}
|
||||
for method, total in revenue_by_method:
|
||||
method_name = method.value if hasattr(method, 'value') else str(method)
|
||||
method_breakdown[method_name] = float(total or 0)
|
||||
|
||||
# Revenue by date (daily breakdown)
|
||||
daily_revenue = db.query(
|
||||
func.date(Payment.payment_date).label('date'),
|
||||
func.sum(Payment.amount).label('total')
|
||||
).filter(
|
||||
Payment.payment_status == PaymentStatus.completed
|
||||
).group_by(func.date(Payment.payment_date)).order_by(func.date(Payment.payment_date).desc()).limit(30).all()
|
||||
|
||||
daily_breakdown = [
|
||||
{
|
||||
"date": date.isoformat() if isinstance(date, datetime) else str(date),
|
||||
"revenue": float(total or 0)
|
||||
}
|
||||
for date, total in daily_revenue
|
||||
]
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"total_revenue": float(total_revenue),
|
||||
"revenue_by_method": method_breakdown,
|
||||
"daily_breakdown": daily_breakdown,
|
||||
}
|
||||
}
|
||||
daily_revenue = db.query(func.date(Payment.payment_date).label('date'), func.sum(Payment.amount).label('total')).filter(Payment.payment_status == PaymentStatus.completed).group_by(func.date(Payment.payment_date)).order_by(func.date(Payment.payment_date).desc()).limit(30).all()
|
||||
daily_breakdown = [{'date': date.isoformat() if isinstance(date, datetime) else str(date), 'revenue': float(total or 0)} for date, total in daily_revenue]
|
||||
return {'status': 'success', 'data': {'total_revenue': float(total_revenue), 'revenue_by_method': method_breakdown, 'daily_breakdown': daily_breakdown}}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
@@ -1,251 +1,117 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ..models.review import Review, ReviewStatus
|
||||
from ..models.room import Room
|
||||
router = APIRouter(prefix='/reviews', tags=['reviews'])
|
||||
|
||||
router = APIRouter(prefix="/reviews", tags=["reviews"])
|
||||
|
||||
|
||||
@router.get("/room/{room_id}")
|
||||
async def get_room_reviews(room_id: int, db: Session = Depends(get_db)):
|
||||
"""Get reviews for a room"""
|
||||
@router.get('/room/{room_id}')
|
||||
async def get_room_reviews(room_id: int, db: Session=Depends(get_db)):
|
||||
try:
|
||||
reviews = db.query(Review).filter(
|
||||
Review.room_id == room_id,
|
||||
Review.status == ReviewStatus.approved
|
||||
).order_by(Review.created_at.desc()).all()
|
||||
|
||||
reviews = db.query(Review).filter(Review.room_id == room_id, Review.status == ReviewStatus.approved).order_by(Review.created_at.desc()).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, 'email': review.user.email}
|
||||
result.append(review_dict)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {"reviews": result}
|
||||
}
|
||||
return {'status': 'success', 'data': {'reviews': result}}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@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)
|
||||
):
|
||||
"""Get all reviews (Admin only)"""
|
||||
@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)
|
||||
|
||||
if status_filter:
|
||||
try:
|
||||
query = query.filter(Review.status == ReviewStatus(status_filter))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
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,
|
||||
"phone": review.user.phone,
|
||||
}
|
||||
|
||||
review_dict['user'] = {'id': review.user.id, 'full_name': review.user.full_name, 'email': review.user.email, 'phone': review.user.phone}
|
||||
if review.room:
|
||||
review_dict["room"] = {
|
||||
"id": review.room.id,
|
||||
"room_number": review.room.room_number,
|
||||
}
|
||||
|
||||
review_dict['room'] = {'id': review.room.id, 'room_number': review.room.room_number}
|
||||
result.append(review_dict)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"reviews": result,
|
||||
"pagination": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"totalPages": (total + limit - 1) // limit,
|
||||
},
|
||||
},
|
||||
}
|
||||
return {'status': 'success', 'data': {'reviews': result, 'pagination': {'total': total, 'page': page, 'limit': limit, 'totalPages': (total + limit - 1) // limit}}}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_review(
|
||||
review_data: dict,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create new review"""
|
||||
@router.post('/')
|
||||
async def create_review(review_data: dict, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
try:
|
||||
room_id = review_data.get("room_id")
|
||||
rating = review_data.get("rating")
|
||||
comment = review_data.get("comment")
|
||||
|
||||
# Check if room exists
|
||||
room_id = review_data.get('room_id')
|
||||
rating = review_data.get('rating')
|
||||
comment = review_data.get('comment')
|
||||
room = db.query(Room).filter(Room.id == room_id).first()
|
||||
if not room:
|
||||
raise HTTPException(status_code=404, detail="Room not found")
|
||||
|
||||
# Check if user already reviewed this room
|
||||
existing = db.query(Review).filter(
|
||||
Review.user_id == current_user.id,
|
||||
Review.room_id == room_id
|
||||
).first()
|
||||
|
||||
raise HTTPException(status_code=404, detail='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"
|
||||
)
|
||||
|
||||
# Create review
|
||||
review = Review(
|
||||
user_id=current_user.id,
|
||||
room_id=room_id,
|
||||
rating=rating,
|
||||
comment=comment,
|
||||
status=ReviewStatus.pending,
|
||||
)
|
||||
|
||||
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)
|
||||
db.add(review)
|
||||
db.commit()
|
||||
db.refresh(review)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Review submitted successfully and is pending approval",
|
||||
"data": {"review": review}
|
||||
}
|
||||
return {'status': 'success', 'message': 'Review submitted successfully and is pending approval', 'data': {'review': review}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/{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)
|
||||
):
|
||||
"""Approve review (Admin only)"""
|
||||
@router.put('/{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='Review not found')
|
||||
review.status = ReviewStatus.approved
|
||||
db.commit()
|
||||
db.refresh(review)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Review approved successfully",
|
||||
"data": {"review": review}
|
||||
}
|
||||
return {'status': 'success', 'message': 'Review approved successfully', 'data': {'review': review}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/{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)
|
||||
):
|
||||
"""Reject review (Admin only)"""
|
||||
@router.put('/{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='Review not found')
|
||||
review.status = ReviewStatus.rejected
|
||||
db.commit()
|
||||
db.refresh(review)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Review rejected successfully",
|
||||
"data": {"review": review}
|
||||
}
|
||||
return {'status': 'success', 'message': 'Review rejected successfully', 'data': {'review': review}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@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)
|
||||
):
|
||||
"""Delete review (Admin only)"""
|
||||
@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)):
|
||||
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='Review not found')
|
||||
db.delete(review)
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Review deleted successfully"
|
||||
}
|
||||
return {'status': 'success', 'message': 'Review deleted successfully'}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,22 +21,18 @@ from ..config.settings import settings
|
||||
|
||||
router = APIRouter(prefix="/service-bookings", tags=["service-bookings"])
|
||||
|
||||
|
||||
def generate_service_booking_number() -> str:
|
||||
"""Generate unique service booking number"""
|
||||
prefix = "SB"
|
||||
timestamp = datetime.utcnow().strftime("%Y%m%d")
|
||||
random_suffix = random.randint(1000, 9999)
|
||||
return f"{prefix}{timestamp}{random_suffix}"
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_service_booking(
|
||||
booking_data: dict,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create a new service booking"""
|
||||
try:
|
||||
services = booking_data.get("services", [])
|
||||
total_amount = float(booking_data.get("total_amount", 0))
|
||||
@@ -48,7 +44,7 @@ async def create_service_booking(
|
||||
if total_amount <= 0:
|
||||
raise HTTPException(status_code=400, detail="Total amount must be greater than 0")
|
||||
|
||||
# Validate services and calculate total
|
||||
|
||||
calculated_total = 0
|
||||
service_items_data = []
|
||||
|
||||
@@ -59,7 +55,7 @@ async def create_service_booking(
|
||||
if not service_id:
|
||||
raise HTTPException(status_code=400, detail="Service ID is required for each item")
|
||||
|
||||
# Check if service exists and is active
|
||||
|
||||
service = db.query(Service).filter(Service.id == service_id).first()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail=f"Service with ID {service_id} not found")
|
||||
@@ -78,17 +74,17 @@ async def create_service_booking(
|
||||
"total_price": item_total
|
||||
})
|
||||
|
||||
# Verify calculated total matches provided total (with small tolerance for floating point)
|
||||
|
||||
if abs(calculated_total - total_amount) > 0.01:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Total amount mismatch. Calculated: {calculated_total}, Provided: {total_amount}"
|
||||
)
|
||||
|
||||
# Generate booking number
|
||||
|
||||
booking_number = generate_service_booking_number()
|
||||
|
||||
# Create service booking
|
||||
|
||||
service_booking = ServiceBooking(
|
||||
booking_number=booking_number,
|
||||
user_id=current_user.id,
|
||||
@@ -98,9 +94,9 @@ async def create_service_booking(
|
||||
)
|
||||
|
||||
db.add(service_booking)
|
||||
db.flush() # Flush to get the ID
|
||||
db.flush()
|
||||
|
||||
# Create service booking items
|
||||
|
||||
for item_data in service_items_data:
|
||||
booking_item = ServiceBookingItem(
|
||||
service_booking_id=service_booking.id,
|
||||
@@ -114,12 +110,12 @@ async def create_service_booking(
|
||||
db.commit()
|
||||
db.refresh(service_booking)
|
||||
|
||||
# Load relationships
|
||||
|
||||
service_booking = db.query(ServiceBooking).options(
|
||||
joinedload(ServiceBooking.service_items).joinedload(ServiceBookingItem.service)
|
||||
).filter(ServiceBooking.id == service_booking.id).first()
|
||||
|
||||
# Format response
|
||||
|
||||
booking_dict = {
|
||||
"id": service_booking.id,
|
||||
"booking_number": service_booking.booking_number,
|
||||
@@ -157,13 +153,11 @@ async def create_service_booking(
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/me")
|
||||
async def get_my_service_bookings(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get all service bookings for current user"""
|
||||
try:
|
||||
bookings = db.query(ServiceBooking).options(
|
||||
joinedload(ServiceBooking.service_items).joinedload(ServiceBookingItem.service)
|
||||
@@ -204,14 +198,12 @@ async def get_my_service_bookings(
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{id}")
|
||||
async def get_service_booking_by_id(
|
||||
id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get service booking by ID"""
|
||||
try:
|
||||
booking = db.query(ServiceBooking).options(
|
||||
joinedload(ServiceBooking.service_items).joinedload(ServiceBookingItem.service)
|
||||
@@ -220,7 +212,7 @@ async def get_service_booking_by_id(
|
||||
if not booking:
|
||||
raise HTTPException(status_code=404, detail="Service booking not found")
|
||||
|
||||
# Check access
|
||||
|
||||
if booking.user_id != current_user.id and current_user.role_id != 1:
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
|
||||
@@ -259,7 +251,6 @@ async def get_service_booking_by_id(
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{id}/payment/stripe/create-intent")
|
||||
async def create_service_stripe_payment_intent(
|
||||
id: int,
|
||||
@@ -267,9 +258,8 @@ async def create_service_stripe_payment_intent(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create Stripe payment intent for service booking"""
|
||||
try:
|
||||
# Check if Stripe is configured
|
||||
|
||||
secret_key = get_stripe_secret_key(db)
|
||||
if not secret_key:
|
||||
secret_key = settings.STRIPE_SECRET_KEY
|
||||
@@ -286,7 +276,7 @@ async def create_service_stripe_payment_intent(
|
||||
if amount <= 0:
|
||||
raise HTTPException(status_code=400, detail="Amount must be greater than 0")
|
||||
|
||||
# Verify service booking exists and user has access
|
||||
|
||||
booking = db.query(ServiceBooking).filter(ServiceBooking.id == id).first()
|
||||
if not booking:
|
||||
raise HTTPException(status_code=404, detail="Service booking not found")
|
||||
@@ -294,22 +284,22 @@ async def create_service_stripe_payment_intent(
|
||||
if booking.user_id != current_user.id and current_user.role_id != 1:
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
|
||||
# Verify amount matches booking total
|
||||
|
||||
if abs(float(booking.total_amount) - amount) > 0.01:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Amount mismatch. Booking total: {booking.total_amount}, Provided: {amount}"
|
||||
)
|
||||
|
||||
# Create payment intent
|
||||
|
||||
intent = StripeService.create_payment_intent(
|
||||
amount=amount,
|
||||
currency=currency,
|
||||
description=f"Service Booking #{booking.booking_number}",
|
||||
description=f"Service Booking
|
||||
db=db
|
||||
)
|
||||
|
||||
# Get publishable key
|
||||
|
||||
publishable_key = get_stripe_publishable_key(db)
|
||||
if not publishable_key:
|
||||
publishable_key = settings.STRIPE_PUBLISHABLE_KEY
|
||||
@@ -333,7 +323,6 @@ async def create_service_stripe_payment_intent(
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{id}/payment/stripe/confirm")
|
||||
async def confirm_service_stripe_payment(
|
||||
id: int,
|
||||
@@ -341,14 +330,13 @@ async def confirm_service_stripe_payment(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Confirm Stripe payment for service booking"""
|
||||
try:
|
||||
payment_intent_id = payment_data.get("payment_intent_id")
|
||||
|
||||
if not payment_intent_id:
|
||||
raise HTTPException(status_code=400, detail="payment_intent_id is required")
|
||||
|
||||
# Verify service booking exists and user has access
|
||||
|
||||
booking = db.query(ServiceBooking).filter(ServiceBooking.id == id).first()
|
||||
if not booking:
|
||||
raise HTTPException(status_code=404, detail="Service booking not found")
|
||||
@@ -356,7 +344,7 @@ async def confirm_service_stripe_payment(
|
||||
if booking.user_id != current_user.id and current_user.role_id != 1:
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
|
||||
# Retrieve and verify payment intent
|
||||
|
||||
intent_data = StripeService.retrieve_payment_intent(payment_intent_id, db)
|
||||
|
||||
if intent_data["status"] != "succeeded":
|
||||
@@ -365,15 +353,15 @@ async def confirm_service_stripe_payment(
|
||||
detail=f"Payment intent status is {intent_data['status']}, expected 'succeeded'"
|
||||
)
|
||||
|
||||
# Verify amount matches
|
||||
amount_paid = intent_data["amount"] / 100 # Convert from cents
|
||||
|
||||
amount_paid = intent_data["amount"] / 100
|
||||
if abs(float(booking.total_amount) - amount_paid) > 0.01:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Payment amount does not match booking total"
|
||||
)
|
||||
|
||||
# Create payment record
|
||||
|
||||
payment = ServicePayment(
|
||||
service_booking_id=booking.id,
|
||||
amount=booking.total_amount,
|
||||
@@ -386,7 +374,7 @@ async def confirm_service_stripe_payment(
|
||||
|
||||
db.add(payment)
|
||||
|
||||
# Update booking status
|
||||
|
||||
booking.status = ServiceBookingStatus.confirmed
|
||||
|
||||
db.commit()
|
||||
|
||||
@@ -2,276 +2,133 @@ from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_
|
||||
from typing import Optional
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ..models.service import Service
|
||||
from ..models.service_usage import ServiceUsage
|
||||
from ..models.booking import Booking, BookingStatus
|
||||
router = APIRouter(prefix='/services', tags=['services'])
|
||||
|
||||
router = APIRouter(prefix="/services", tags=["services"])
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_services(
|
||||
search: Optional[str] = Query(None),
|
||||
status_filter: Optional[str] = Query(None, alias="status"),
|
||||
page: int = Query(1, ge=1),
|
||||
limit: int = Query(10, ge=1, le=100),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get all services with filters"""
|
||||
@router.get('/')
|
||||
async def get_services(search: Optional[str]=Query(None), status_filter: Optional[str]=Query(None, alias='status'), page: int=Query(1, ge=1), limit: int=Query(10, ge=1, le=100), db: Session=Depends(get_db)):
|
||||
try:
|
||||
query = db.query(Service)
|
||||
|
||||
# Filter by search (name or description)
|
||||
if search:
|
||||
query = query.filter(
|
||||
or_(
|
||||
Service.name.like(f"%{search}%"),
|
||||
Service.description.like(f"%{search}%")
|
||||
)
|
||||
)
|
||||
|
||||
# Filter by status (is_active)
|
||||
query = query.filter(or_(Service.name.like(f'%{search}%'), Service.description.like(f'%{search}%')))
|
||||
if status_filter:
|
||||
is_active = status_filter == "active"
|
||||
is_active = status_filter == 'active'
|
||||
query = query.filter(Service.is_active == is_active)
|
||||
|
||||
total = query.count()
|
||||
offset = (page - 1) * limit
|
||||
services = query.order_by(Service.created_at.desc()).offset(offset).limit(limit).all()
|
||||
|
||||
result = []
|
||||
for service in services:
|
||||
service_dict = {
|
||||
"id": service.id,
|
||||
"name": service.name,
|
||||
"description": service.description,
|
||||
"price": float(service.price) if service.price else 0.0,
|
||||
"category": service.category,
|
||||
"is_active": service.is_active,
|
||||
"created_at": service.created_at.isoformat() if service.created_at else None,
|
||||
}
|
||||
service_dict = {'id': service.id, 'name': service.name, 'description': service.description, 'price': float(service.price) if service.price else 0.0, 'category': service.category, 'is_active': service.is_active, 'created_at': service.created_at.isoformat() if service.created_at else None}
|
||||
result.append(service_dict)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"services": result,
|
||||
"pagination": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"totalPages": (total + limit - 1) // limit,
|
||||
},
|
||||
},
|
||||
}
|
||||
return {'status': 'success', 'data': {'services': result, 'pagination': {'total': total, 'page': page, 'limit': limit, 'totalPages': (total + limit - 1) // limit}}}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{id}")
|
||||
async def get_service_by_id(id: int, db: Session = Depends(get_db)):
|
||||
"""Get service by ID"""
|
||||
@router.get('/{id}')
|
||||
async def get_service_by_id(id: int, db: Session=Depends(get_db)):
|
||||
try:
|
||||
service = db.query(Service).filter(Service.id == id).first()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="Service not found")
|
||||
|
||||
service_dict = {
|
||||
"id": service.id,
|
||||
"name": service.name,
|
||||
"description": service.description,
|
||||
"price": float(service.price) if service.price else 0.0,
|
||||
"category": service.category,
|
||||
"is_active": service.is_active,
|
||||
"created_at": service.created_at.isoformat() if service.created_at else None,
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {"service": service_dict}
|
||||
}
|
||||
raise HTTPException(status_code=404, detail='Service not found')
|
||||
service_dict = {'id': service.id, 'name': service.name, 'description': service.description, 'price': float(service.price) if service.price else 0.0, 'category': service.category, 'is_active': service.is_active, 'created_at': service.created_at.isoformat() if service.created_at else None}
|
||||
return {'status': 'success', 'data': {'service': service_dict}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/", dependencies=[Depends(authorize_roles("admin"))])
|
||||
async def create_service(
|
||||
service_data: dict,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create new service (Admin only)"""
|
||||
@router.post('/', dependencies=[Depends(authorize_roles('admin'))])
|
||||
async def create_service(service_data: dict, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
name = service_data.get("name")
|
||||
|
||||
# Check if name exists
|
||||
name = service_data.get('name')
|
||||
existing = db.query(Service).filter(Service.name == name).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="Service name already exists")
|
||||
|
||||
service = Service(
|
||||
name=name,
|
||||
description=service_data.get("description"),
|
||||
price=float(service_data.get("price", 0)),
|
||||
category=service_data.get("category"),
|
||||
is_active=service_data.get("status") == "active" if service_data.get("status") else True,
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=400, detail='Service name already exists')
|
||||
service = Service(name=name, description=service_data.get('description'), price=float(service_data.get('price', 0)), category=service_data.get('category'), is_active=service_data.get('status') == 'active' if service_data.get('status') else True)
|
||||
db.add(service)
|
||||
db.commit()
|
||||
db.refresh(service)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Service created successfully",
|
||||
"data": {"service": service}
|
||||
}
|
||||
return {'status': 'success', 'message': 'Service created successfully', 'data': {'service': service}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/{id}", dependencies=[Depends(authorize_roles("admin"))])
|
||||
async def update_service(
|
||||
id: int,
|
||||
service_data: dict,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update service (Admin only)"""
|
||||
@router.put('/{id}', dependencies=[Depends(authorize_roles('admin'))])
|
||||
async def update_service(id: int, service_data: dict, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
service = db.query(Service).filter(Service.id == id).first()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="Service not found")
|
||||
|
||||
# Check if new name exists (excluding current)
|
||||
name = service_data.get("name")
|
||||
raise HTTPException(status_code=404, detail='Service not found')
|
||||
name = service_data.get('name')
|
||||
if name and name != service.name:
|
||||
existing = db.query(Service).filter(
|
||||
Service.name == name,
|
||||
Service.id != id
|
||||
).first()
|
||||
existing = db.query(Service).filter(Service.name == name, Service.id != id).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="Service name already exists")
|
||||
|
||||
# Update fields
|
||||
if "name" in service_data:
|
||||
service.name = service_data["name"]
|
||||
if "description" in service_data:
|
||||
service.description = service_data["description"]
|
||||
if "price" in service_data:
|
||||
service.price = float(service_data["price"])
|
||||
if "category" in service_data:
|
||||
service.category = service_data["category"]
|
||||
if "status" in service_data:
|
||||
service.is_active = service_data["status"] == "active"
|
||||
|
||||
raise HTTPException(status_code=400, detail='Service name already exists')
|
||||
if 'name' in service_data:
|
||||
service.name = service_data['name']
|
||||
if 'description' in service_data:
|
||||
service.description = service_data['description']
|
||||
if 'price' in service_data:
|
||||
service.price = float(service_data['price'])
|
||||
if 'category' in service_data:
|
||||
service.category = service_data['category']
|
||||
if 'status' in service_data:
|
||||
service.is_active = service_data['status'] == 'active'
|
||||
db.commit()
|
||||
db.refresh(service)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Service updated successfully",
|
||||
"data": {"service": service}
|
||||
}
|
||||
return {'status': 'success', 'message': 'Service updated successfully', 'data': {'service': service}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/{id}", dependencies=[Depends(authorize_roles("admin"))])
|
||||
async def delete_service(
|
||||
id: int,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Delete service (Admin only)"""
|
||||
@router.delete('/{id}', dependencies=[Depends(authorize_roles('admin'))])
|
||||
async def delete_service(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
service = db.query(Service).filter(Service.id == id).first()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="Service not found")
|
||||
|
||||
# Check if service is used in active bookings
|
||||
active_usage = db.query(ServiceUsage).join(Booking).filter(
|
||||
ServiceUsage.service_id == id,
|
||||
Booking.status.in_([BookingStatus.pending, BookingStatus.confirmed, BookingStatus.checked_in])
|
||||
).count()
|
||||
|
||||
raise HTTPException(status_code=404, detail='Service not found')
|
||||
active_usage = db.query(ServiceUsage).join(Booking).filter(ServiceUsage.service_id == id, Booking.status.in_([BookingStatus.pending, BookingStatus.confirmed, BookingStatus.checked_in])).count()
|
||||
if active_usage > 0:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Cannot delete service that is used in active bookings"
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=400, detail='Cannot delete service that is used in active bookings')
|
||||
db.delete(service)
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Service deleted successfully"
|
||||
}
|
||||
return {'status': 'success', 'message': 'Service deleted successfully'}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/use")
|
||||
async def use_service(
|
||||
usage_data: dict,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Add service to booking"""
|
||||
@router.post('/use')
|
||||
async def use_service(usage_data: dict, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
try:
|
||||
booking_id = usage_data.get("booking_id")
|
||||
service_id = usage_data.get("service_id")
|
||||
quantity = usage_data.get("quantity", 1)
|
||||
|
||||
# Check if booking exists
|
||||
booking_id = usage_data.get('booking_id')
|
||||
service_id = usage_data.get('service_id')
|
||||
quantity = usage_data.get('quantity', 1)
|
||||
booking = db.query(Booking).filter(Booking.id == booking_id).first()
|
||||
if not booking:
|
||||
raise HTTPException(status_code=404, detail="Booking not found")
|
||||
|
||||
# Check if service exists and is active
|
||||
raise HTTPException(status_code=404, detail='Booking not found')
|
||||
service = db.query(Service).filter(Service.id == service_id).first()
|
||||
if not service or not service.is_active:
|
||||
raise HTTPException(status_code=404, detail="Service not found or inactive")
|
||||
|
||||
# Calculate total price
|
||||
raise HTTPException(status_code=404, detail='Service not found or inactive')
|
||||
total_price = float(service.price) * quantity
|
||||
|
||||
# Create service usage
|
||||
service_usage = ServiceUsage(
|
||||
booking_id=booking_id,
|
||||
service_id=service_id,
|
||||
quantity=quantity,
|
||||
unit_price=service.price,
|
||||
total_price=total_price,
|
||||
)
|
||||
|
||||
service_usage = ServiceUsage(booking_id=booking_id, service_id=service_id, quantity=quantity, unit_price=service.price, total_price=total_price)
|
||||
db.add(service_usage)
|
||||
db.commit()
|
||||
db.refresh(service_usage)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Service added to booking successfully",
|
||||
"data": {"bookingService": service_usage}
|
||||
}
|
||||
return {'status': 'success', 'message': 'Service added to booking successfully', 'data': {'bookingService': service_usage}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
@@ -16,9 +16,7 @@ from ..models.system_settings import SystemSettings
|
||||
from ..utils.mailer import send_email
|
||||
from ..services.room_service import get_base_url
|
||||
|
||||
|
||||
def normalize_image_url(image_url: str, base_url: str) -> str:
|
||||
"""Normalize image URL to absolute URL"""
|
||||
if not image_url:
|
||||
return image_url
|
||||
if image_url.startswith('http://') or image_url.startswith('https://'):
|
||||
@@ -31,19 +29,17 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/admin/system-settings", tags=["admin-system-settings"])
|
||||
|
||||
|
||||
@router.get("/currency")
|
||||
async def get_platform_currency(
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get platform currency setting (public endpoint for frontend)"""
|
||||
try:
|
||||
setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "platform_currency"
|
||||
).first()
|
||||
|
||||
if not setting:
|
||||
# Default to VND if not set
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
@@ -64,25 +60,23 @@ async def get_platform_currency(
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/currency")
|
||||
async def update_platform_currency(
|
||||
currency_data: dict,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update platform currency (Admin only)"""
|
||||
try:
|
||||
currency = currency_data.get("currency", "").upper()
|
||||
|
||||
# Validate currency code
|
||||
|
||||
if not currency or len(currency) != 3 or not currency.isalpha():
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Invalid currency code. Must be a 3-letter ISO 4217 code (e.g., USD, EUR, VND)"
|
||||
)
|
||||
|
||||
# Get or create setting
|
||||
|
||||
setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "platform_currency"
|
||||
).first()
|
||||
@@ -117,13 +111,11 @@ async def update_platform_currency(
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def get_all_settings(
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get all system settings (Admin only)"""
|
||||
try:
|
||||
settings = db.query(SystemSettings).all()
|
||||
|
||||
@@ -146,13 +138,11 @@ async def get_all_settings(
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/stripe")
|
||||
async def get_stripe_settings(
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get Stripe payment settings (Admin only)"""
|
||||
try:
|
||||
secret_key_setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "stripe_secret_key"
|
||||
@@ -166,7 +156,7 @@ async def get_stripe_settings(
|
||||
SystemSettings.key == "stripe_webhook_secret"
|
||||
).first()
|
||||
|
||||
# Mask secret keys for security (only show last 4 characters)
|
||||
|
||||
def mask_key(key_value: str) -> str:
|
||||
if not key_value or len(key_value) < 4:
|
||||
return ""
|
||||
@@ -206,41 +196,39 @@ async def get_stripe_settings(
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/stripe")
|
||||
async def update_stripe_settings(
|
||||
stripe_data: dict,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update Stripe payment settings (Admin only)"""
|
||||
try:
|
||||
secret_key = stripe_data.get("stripe_secret_key", "").strip()
|
||||
publishable_key = stripe_data.get("stripe_publishable_key", "").strip()
|
||||
webhook_secret = stripe_data.get("stripe_webhook_secret", "").strip()
|
||||
|
||||
# Validate secret key format (should start with sk_)
|
||||
|
||||
if secret_key and not secret_key.startswith("sk_"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Invalid Stripe secret key format. Must start with 'sk_'"
|
||||
)
|
||||
|
||||
# Validate publishable key format (should start with pk_)
|
||||
|
||||
if publishable_key and not publishable_key.startswith("pk_"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Invalid Stripe publishable key format. Must start with 'pk_'"
|
||||
)
|
||||
|
||||
# Validate webhook secret format (should start with whsec_)
|
||||
|
||||
if webhook_secret and not webhook_secret.startswith("whsec_"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Invalid Stripe webhook secret format. Must start with 'whsec_'"
|
||||
)
|
||||
|
||||
# Update or create secret key setting
|
||||
|
||||
if secret_key:
|
||||
setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "stripe_secret_key"
|
||||
@@ -258,7 +246,7 @@ async def update_stripe_settings(
|
||||
)
|
||||
db.add(setting)
|
||||
|
||||
# Update or create publishable key setting
|
||||
|
||||
if publishable_key:
|
||||
setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "stripe_publishable_key"
|
||||
@@ -276,7 +264,7 @@ async def update_stripe_settings(
|
||||
)
|
||||
db.add(setting)
|
||||
|
||||
# Update or create webhook secret setting
|
||||
|
||||
if webhook_secret:
|
||||
setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "stripe_webhook_secret"
|
||||
@@ -296,7 +284,7 @@ async def update_stripe_settings(
|
||||
|
||||
db.commit()
|
||||
|
||||
# Return masked values
|
||||
|
||||
def mask_key(key_value: str) -> str:
|
||||
if not key_value or len(key_value) < 4:
|
||||
return ""
|
||||
@@ -322,13 +310,11 @@ async def update_stripe_settings(
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/paypal")
|
||||
async def get_paypal_settings(
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get PayPal payment settings (Admin only)"""
|
||||
try:
|
||||
client_id_setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "paypal_client_id"
|
||||
@@ -342,7 +328,7 @@ async def get_paypal_settings(
|
||||
SystemSettings.key == "paypal_mode"
|
||||
).first()
|
||||
|
||||
# Mask secret for security (only show last 4 characters)
|
||||
|
||||
def mask_key(key_value: str) -> str:
|
||||
if not key_value or len(key_value) < 4:
|
||||
return ""
|
||||
@@ -378,27 +364,25 @@ async def get_paypal_settings(
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/paypal")
|
||||
async def update_paypal_settings(
|
||||
paypal_data: dict,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update PayPal payment settings (Admin only)"""
|
||||
try:
|
||||
client_id = paypal_data.get("paypal_client_id", "").strip()
|
||||
client_secret = paypal_data.get("paypal_client_secret", "").strip()
|
||||
mode = paypal_data.get("paypal_mode", "sandbox").strip().lower()
|
||||
|
||||
# Validate mode
|
||||
|
||||
if mode and mode not in ["sandbox", "live"]:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Invalid PayPal mode. Must be 'sandbox' or 'live'"
|
||||
)
|
||||
|
||||
# Update or create client ID setting
|
||||
|
||||
if client_id:
|
||||
setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "paypal_client_id"
|
||||
@@ -416,7 +400,7 @@ async def update_paypal_settings(
|
||||
)
|
||||
db.add(setting)
|
||||
|
||||
# Update or create client secret setting
|
||||
|
||||
if client_secret:
|
||||
setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "paypal_client_secret"
|
||||
@@ -434,7 +418,7 @@ async def update_paypal_settings(
|
||||
)
|
||||
db.add(setting)
|
||||
|
||||
# Update or create mode setting
|
||||
|
||||
if mode:
|
||||
setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "paypal_mode"
|
||||
@@ -454,7 +438,7 @@ async def update_paypal_settings(
|
||||
|
||||
db.commit()
|
||||
|
||||
# Return masked values
|
||||
|
||||
def mask_key(key_value: str) -> str:
|
||||
if not key_value or len(key_value) < 4:
|
||||
return ""
|
||||
@@ -478,15 +462,13 @@ async def update_paypal_settings(
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/smtp")
|
||||
async def get_smtp_settings(
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get SMTP email server settings (Admin only)"""
|
||||
try:
|
||||
# Get all SMTP settings
|
||||
|
||||
smtp_settings = {}
|
||||
setting_keys = [
|
||||
"smtp_host",
|
||||
@@ -505,7 +487,7 @@ async def get_smtp_settings(
|
||||
if setting:
|
||||
smtp_settings[key] = setting.value
|
||||
|
||||
# Mask password for security (only show last 4 characters if set)
|
||||
|
||||
def mask_password(password_value: str) -> str:
|
||||
if not password_value or len(password_value) < 4:
|
||||
return ""
|
||||
@@ -525,7 +507,7 @@ async def get_smtp_settings(
|
||||
"has_password": bool(smtp_settings.get("smtp_password")),
|
||||
}
|
||||
|
||||
# Get updated_at and updated_by from any setting (prefer password setting if exists)
|
||||
|
||||
password_setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "smtp_password"
|
||||
).first()
|
||||
@@ -534,7 +516,7 @@ async def get_smtp_settings(
|
||||
result["updated_at"] = password_setting.updated_at.isoformat() if password_setting.updated_at else None
|
||||
result["updated_by"] = password_setting.updated_by.full_name if password_setting.updated_by else None
|
||||
else:
|
||||
# Try to get from any other SMTP setting
|
||||
|
||||
any_setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key.in_(setting_keys)
|
||||
).first()
|
||||
@@ -552,14 +534,12 @@ async def get_smtp_settings(
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/smtp")
|
||||
async def update_smtp_settings(
|
||||
smtp_data: dict,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update SMTP email server settings (Admin only)"""
|
||||
try:
|
||||
smtp_host = smtp_data.get("smtp_host", "").strip()
|
||||
smtp_port = smtp_data.get("smtp_port", "").strip()
|
||||
@@ -569,7 +549,7 @@ async def update_smtp_settings(
|
||||
smtp_from_name = smtp_data.get("smtp_from_name", "").strip()
|
||||
smtp_use_tls = smtp_data.get("smtp_use_tls", True)
|
||||
|
||||
# Validate required fields if provided
|
||||
|
||||
if smtp_host and not smtp_host:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
@@ -591,14 +571,14 @@ async def update_smtp_settings(
|
||||
)
|
||||
|
||||
if smtp_from_email:
|
||||
# Basic email validation
|
||||
|
||||
if "@" not in smtp_from_email or "." not in smtp_from_email.split("@")[1]:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Invalid email address format for 'From Email'"
|
||||
)
|
||||
|
||||
# Helper function to update or create setting
|
||||
|
||||
def update_setting(key: str, value: str, description: str):
|
||||
setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == key
|
||||
@@ -616,7 +596,7 @@ async def update_smtp_settings(
|
||||
)
|
||||
db.add(setting)
|
||||
|
||||
# Update or create settings (only update if value is provided)
|
||||
|
||||
if smtp_host:
|
||||
update_setting(
|
||||
"smtp_host",
|
||||
@@ -659,7 +639,7 @@ async def update_smtp_settings(
|
||||
"Default 'From' name for outgoing emails"
|
||||
)
|
||||
|
||||
# Update TLS setting (convert boolean to string)
|
||||
|
||||
if smtp_use_tls is not None:
|
||||
update_setting(
|
||||
"smtp_use_tls",
|
||||
@@ -669,13 +649,13 @@ async def update_smtp_settings(
|
||||
|
||||
db.commit()
|
||||
|
||||
# Return updated settings with masked password
|
||||
|
||||
def mask_password(password_value: str) -> str:
|
||||
if not password_value or len(password_value) < 4:
|
||||
return ""
|
||||
return "*" * (len(password_value) - 4) + password_value[-4:]
|
||||
|
||||
# Get updated settings
|
||||
|
||||
updated_settings = {}
|
||||
for key in ["smtp_host", "smtp_port", "smtp_user", "smtp_password", "smtp_from_email", "smtp_from_name", "smtp_use_tls"]:
|
||||
setting = db.query(SystemSettings).filter(
|
||||
@@ -698,7 +678,7 @@ async def update_smtp_settings(
|
||||
"has_password": bool(updated_settings.get("smtp_password")),
|
||||
}
|
||||
|
||||
# Get updated_by from password setting if it exists
|
||||
|
||||
password_setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "smtp_password"
|
||||
).first()
|
||||
@@ -717,131 +697,28 @@ async def update_smtp_settings(
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
class TestEmailRequest(BaseModel):
|
||||
email: EmailStr
|
||||
|
||||
|
||||
@router.post("/smtp/test")
|
||||
async def test_smtp_email(
|
||||
request: TestEmailRequest,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Send a test email to verify SMTP settings (Admin only)"""
|
||||
try:
|
||||
test_email = str(request.email)
|
||||
admin_name = str(current_user.full_name or current_user.email or "Admin")
|
||||
timestamp_str = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||
|
||||
# Create test email HTML content
|
||||
test_html = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {{
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}}
|
||||
.header {{
|
||||
background: linear-gradient(135deg, #d4af37 0%, #c9a227 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
border-radius: 10px 10px 0 0;
|
||||
}}
|
||||
.content {{
|
||||
background: #f9f9f9;
|
||||
padding: 30px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 0 0 10px 10px;
|
||||
}}
|
||||
.success-icon {{
|
||||
font-size: 48px;
|
||||
margin-bottom: 20px;
|
||||
}}
|
||||
.info-box {{
|
||||
background: white;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid #d4af37;
|
||||
border-radius: 5px;
|
||||
}}
|
||||
.footer {{
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>✅ SMTP Test Email</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div style="text-align: center;">
|
||||
<div class="success-icon">🎉</div>
|
||||
<h2>Email Configuration Test Successful!</h2>
|
||||
</div>
|
||||
|
||||
<p>This is a test email sent from your Hotel Booking system to verify that the SMTP email settings are configured correctly.</p>
|
||||
|
||||
<div class="info-box">
|
||||
<strong>📧 Test Details:</strong>
|
||||
<ul>
|
||||
<li><strong>Recipient:</strong> {test_email}</li>
|
||||
<li><strong>Sent by:</strong> {admin_name}</li>
|
||||
<li><strong>Time:</strong> {timestamp_str}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p>If you received this email, it means your SMTP server settings are working correctly and the system can send emails through your configured email server.</p>
|
||||
|
||||
<p><strong>What's next?</strong></p>
|
||||
<ul>
|
||||
<li>Welcome emails for new user registrations</li>
|
||||
<li>Password reset emails</li>
|
||||
<li>Booking confirmation emails</li>
|
||||
<li>Payment notifications</li>
|
||||
<li>And other system notifications</li>
|
||||
</ul>
|
||||
|
||||
<div class="footer">
|
||||
<p>This is an automated test email from Hotel Booking System</p>
|
||||
<p>If you did not request this test, please ignore this email.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
test_html = f
|
||||
|
||||
# Plain text version
|
||||
test_text = f"""
|
||||
SMTP Test Email
|
||||
|
||||
This is a test email sent from your Hotel Booking system to verify that the SMTP email settings are configured correctly.
|
||||
|
||||
Test Details:
|
||||
- Recipient: {test_email}
|
||||
- Sent by: {admin_name}
|
||||
- Time: {timestamp_str}
|
||||
|
||||
If you received this email, it means your SMTP server settings are working correctly and the system can send emails through your configured email server.
|
||||
|
||||
This is an automated test email from Hotel Booking System
|
||||
If you did not request this test, please ignore this email.
|
||||
""".strip()
|
||||
test_text = f
|
||||
.strip()
|
||||
|
||||
# Send the test email
|
||||
|
||||
await send_email(
|
||||
to=test_email,
|
||||
subject="SMTP Test Email - Hotel Booking System",
|
||||
@@ -860,13 +737,13 @@ If you did not request this test, please ignore this email.
|
||||
}
|
||||
}
|
||||
except HTTPException:
|
||||
# Re-raise HTTP exceptions (like validation errors from send_email)
|
||||
|
||||
raise
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
logger.error(f"Error sending test email: {type(e).__name__}: {error_msg}", exc_info=True)
|
||||
|
||||
# Provide more user-friendly error messages
|
||||
|
||||
if "SMTP mailer not configured" in error_msg:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
@@ -888,7 +765,6 @@ If you did not request this test, please ignore this email.
|
||||
detail=f"Failed to send test email: {error_msg}"
|
||||
)
|
||||
|
||||
|
||||
class UpdateCompanySettingsRequest(BaseModel):
|
||||
company_name: Optional[str] = None
|
||||
company_tagline: Optional[str] = None
|
||||
@@ -897,12 +773,10 @@ class UpdateCompanySettingsRequest(BaseModel):
|
||||
company_address: Optional[str] = None
|
||||
tax_rate: Optional[float] = None
|
||||
|
||||
|
||||
@router.get("/company")
|
||||
async def get_company_settings(
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get company settings (public endpoint for frontend)"""
|
||||
try:
|
||||
setting_keys = [
|
||||
"company_name",
|
||||
@@ -925,7 +799,7 @@ async def get_company_settings(
|
||||
else:
|
||||
settings_dict[key] = None
|
||||
|
||||
# Get updated_at and updated_by from logo setting if exists
|
||||
|
||||
logo_setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "company_logo_url"
|
||||
).first()
|
||||
@@ -954,14 +828,12 @@ async def get_company_settings(
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/company")
|
||||
async def update_company_settings(
|
||||
request_data: UpdateCompanySettingsRequest,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update company settings (Admin only)"""
|
||||
try:
|
||||
db_settings = {}
|
||||
|
||||
@@ -979,18 +851,18 @@ async def update_company_settings(
|
||||
db_settings["tax_rate"] = str(request_data.tax_rate)
|
||||
|
||||
for key, value in db_settings.items():
|
||||
# Find or create setting
|
||||
|
||||
setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == key
|
||||
).first()
|
||||
|
||||
if setting:
|
||||
# Update existing
|
||||
|
||||
setting.value = value if value else None
|
||||
setting.updated_at = datetime.utcnow()
|
||||
setting.updated_by_id = current_user.id
|
||||
else:
|
||||
# Create new
|
||||
|
||||
setting = SystemSettings(
|
||||
key=key,
|
||||
value=value if value else None,
|
||||
@@ -1000,7 +872,7 @@ async def update_company_settings(
|
||||
|
||||
db.commit()
|
||||
|
||||
# Get updated settings
|
||||
|
||||
updated_settings = {}
|
||||
for key in ["company_name", "company_tagline", "company_logo_url", "company_favicon_url", "company_phone", "company_email", "company_address", "tax_rate"]:
|
||||
setting = db.query(SystemSettings).filter(
|
||||
@@ -1011,7 +883,7 @@ async def update_company_settings(
|
||||
else:
|
||||
updated_settings[key] = None
|
||||
|
||||
# Get updated_at and updated_by
|
||||
|
||||
logo_setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "company_logo_url"
|
||||
).first()
|
||||
@@ -1048,7 +920,6 @@ async def update_company_settings(
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/company/logo")
|
||||
async def upload_company_logo(
|
||||
request: Request,
|
||||
@@ -1056,28 +927,27 @@ async def upload_company_logo(
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Upload company logo (Admin only)"""
|
||||
try:
|
||||
# Validate file type
|
||||
|
||||
if not image.content_type or not image.content_type.startswith('image/'):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="File must be an image"
|
||||
)
|
||||
|
||||
# Validate file size (max 2MB)
|
||||
|
||||
content = await image.read()
|
||||
if len(content) > 2 * 1024 * 1024: # 2MB
|
||||
if len(content) > 2 * 1024 * 1024:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Logo file size must be less than 2MB"
|
||||
)
|
||||
|
||||
# Create uploads directory
|
||||
|
||||
upload_dir = Path(__file__).parent.parent.parent / "uploads" / "company"
|
||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Delete old logo if exists
|
||||
|
||||
old_logo_setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "company_logo_url"
|
||||
).first()
|
||||
@@ -1090,20 +960,20 @@ async def upload_company_logo(
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not delete old logo: {e}")
|
||||
|
||||
# Generate filename
|
||||
|
||||
ext = Path(image.filename).suffix or '.png'
|
||||
# Always use logo.png to ensure we only have one logo
|
||||
|
||||
filename = "logo.png"
|
||||
file_path = upload_dir / filename
|
||||
|
||||
# Save file
|
||||
|
||||
async with aiofiles.open(file_path, 'wb') as f:
|
||||
await f.write(content)
|
||||
|
||||
# Store the URL in system_settings
|
||||
|
||||
image_url = f"/uploads/company/{filename}"
|
||||
|
||||
# Update or create setting
|
||||
|
||||
logo_setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "company_logo_url"
|
||||
).first()
|
||||
@@ -1122,7 +992,7 @@ async def upload_company_logo(
|
||||
|
||||
db.commit()
|
||||
|
||||
# Return the image URL
|
||||
|
||||
base_url = get_base_url(request)
|
||||
full_url = normalize_image_url(image_url, base_url)
|
||||
|
||||
@@ -1142,7 +1012,6 @@ async def upload_company_logo(
|
||||
logger.error(f"Error uploading logo: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/company/favicon")
|
||||
async def upload_company_favicon(
|
||||
request: Request,
|
||||
@@ -1150,9 +1019,8 @@ async def upload_company_favicon(
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Upload company favicon (Admin only)"""
|
||||
try:
|
||||
# Validate file type (favicon can be ico, png, svg)
|
||||
|
||||
if not image.content_type:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -1161,7 +1029,7 @@ async def upload_company_favicon(
|
||||
|
||||
allowed_types = ['image/x-icon', 'image/vnd.microsoft.icon', 'image/png', 'image/svg+xml', 'image/ico']
|
||||
if image.content_type not in allowed_types:
|
||||
# Check filename extension as fallback
|
||||
|
||||
filename_lower = (image.filename or '').lower()
|
||||
if not any(filename_lower.endswith(ext) for ext in ['.ico', '.png', '.svg']):
|
||||
raise HTTPException(
|
||||
@@ -1169,19 +1037,19 @@ async def upload_company_favicon(
|
||||
detail="Favicon must be .ico, .png, or .svg file"
|
||||
)
|
||||
|
||||
# Validate file size (max 500KB)
|
||||
|
||||
content = await image.read()
|
||||
if len(content) > 500 * 1024: # 500KB
|
||||
if len(content) > 500 * 1024:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Favicon file size must be less than 500KB"
|
||||
)
|
||||
|
||||
# Create uploads directory
|
||||
|
||||
upload_dir = Path(__file__).parent.parent.parent / "uploads" / "company"
|
||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Delete old favicon if exists
|
||||
|
||||
old_favicon_setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "company_favicon_url"
|
||||
).first()
|
||||
@@ -1194,7 +1062,7 @@ async def upload_company_favicon(
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not delete old favicon: {e}")
|
||||
|
||||
# Generate filename - preserve extension but use standard name
|
||||
|
||||
filename_lower = (image.filename or '').lower()
|
||||
if filename_lower.endswith('.ico'):
|
||||
filename = "favicon.ico"
|
||||
@@ -1205,14 +1073,14 @@ async def upload_company_favicon(
|
||||
|
||||
file_path = upload_dir / filename
|
||||
|
||||
# Save file
|
||||
|
||||
async with aiofiles.open(file_path, 'wb') as f:
|
||||
await f.write(content)
|
||||
|
||||
# Store the URL in system_settings
|
||||
|
||||
image_url = f"/uploads/company/{filename}"
|
||||
|
||||
# Update or create setting
|
||||
|
||||
favicon_setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "company_favicon_url"
|
||||
).first()
|
||||
@@ -1231,7 +1099,7 @@ async def upload_company_favicon(
|
||||
|
||||
db.commit()
|
||||
|
||||
# Return the image URL
|
||||
|
||||
base_url = get_base_url(request)
|
||||
full_url = normalize_image_url(image_url, base_url)
|
||||
|
||||
@@ -1251,12 +1119,10 @@ async def upload_company_favicon(
|
||||
logger.error(f"Error uploading favicon: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/recaptcha")
|
||||
async def get_recaptcha_settings(
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get reCAPTCHA settings (Public endpoint for frontend)"""
|
||||
try:
|
||||
site_key_setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "recaptcha_site_key"
|
||||
@@ -1284,13 +1150,11 @@ async def get_recaptcha_settings(
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/recaptcha/admin")
|
||||
async def get_recaptcha_settings_admin(
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get reCAPTCHA settings (Admin only - includes secret key)"""
|
||||
try:
|
||||
site_key_setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "recaptcha_site_key"
|
||||
@@ -1304,7 +1168,7 @@ async def get_recaptcha_settings_admin(
|
||||
SystemSettings.key == "recaptcha_enabled"
|
||||
).first()
|
||||
|
||||
# Mask secret for security (only show last 4 characters)
|
||||
|
||||
def mask_key(key_value: str) -> str:
|
||||
if not key_value or len(key_value) < 4:
|
||||
return ""
|
||||
@@ -1340,20 +1204,18 @@ async def get_recaptcha_settings_admin(
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/recaptcha")
|
||||
async def update_recaptcha_settings(
|
||||
recaptcha_data: dict,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update reCAPTCHA settings (Admin only)"""
|
||||
try:
|
||||
site_key = recaptcha_data.get("recaptcha_site_key", "").strip()
|
||||
secret_key = recaptcha_data.get("recaptcha_secret_key", "").strip()
|
||||
enabled = recaptcha_data.get("recaptcha_enabled", False)
|
||||
|
||||
# Update or create site key setting
|
||||
|
||||
if site_key:
|
||||
setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "recaptcha_site_key"
|
||||
@@ -1371,7 +1233,7 @@ async def update_recaptcha_settings(
|
||||
)
|
||||
db.add(setting)
|
||||
|
||||
# Update or create secret key setting
|
||||
|
||||
if secret_key:
|
||||
setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "recaptcha_secret_key"
|
||||
@@ -1389,7 +1251,7 @@ async def update_recaptcha_settings(
|
||||
)
|
||||
db.add(setting)
|
||||
|
||||
# Update or create enabled setting
|
||||
|
||||
setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "recaptcha_enabled"
|
||||
).first()
|
||||
@@ -1408,7 +1270,7 @@ async def update_recaptcha_settings(
|
||||
|
||||
db.commit()
|
||||
|
||||
# Return masked values
|
||||
|
||||
def mask_key(key_value: str) -> str:
|
||||
if not key_value or len(key_value) < 4:
|
||||
return ""
|
||||
@@ -1432,13 +1294,11 @@ async def update_recaptcha_settings(
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/recaptcha/verify")
|
||||
async def verify_recaptcha(
|
||||
verification_data: dict,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Verify reCAPTCHA token (Public endpoint)"""
|
||||
try:
|
||||
token = verification_data.get("token", "").strip()
|
||||
|
||||
@@ -1448,7 +1308,7 @@ async def verify_recaptcha(
|
||||
detail="reCAPTCHA token is required"
|
||||
)
|
||||
|
||||
# Get reCAPTCHA settings
|
||||
|
||||
enabled_setting = db.query(SystemSettings).filter(
|
||||
SystemSettings.key == "recaptcha_enabled"
|
||||
).first()
|
||||
@@ -1457,13 +1317,13 @@ async def verify_recaptcha(
|
||||
SystemSettings.key == "recaptcha_secret_key"
|
||||
).first()
|
||||
|
||||
# Check if reCAPTCHA is enabled
|
||||
|
||||
is_enabled = False
|
||||
if enabled_setting:
|
||||
is_enabled = enabled_setting.value.lower() == "true" if enabled_setting.value else False
|
||||
|
||||
if not is_enabled:
|
||||
# If disabled, always return success
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
@@ -1478,7 +1338,7 @@ async def verify_recaptcha(
|
||||
detail="reCAPTCHA secret key is not configured"
|
||||
)
|
||||
|
||||
# Verify with Google reCAPTCHA API
|
||||
|
||||
import httpx
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
@@ -1498,8 +1358,8 @@ async def verify_recaptcha(
|
||||
"status": "success",
|
||||
"data": {
|
||||
"verified": True,
|
||||
"score": result.get("score"), # For v3
|
||||
"action": result.get("action") # For v3
|
||||
"score": result.get("score"),
|
||||
"action": result.get("action")
|
||||
}
|
||||
}
|
||||
else:
|
||||
|
||||
@@ -3,323 +3,136 @@ from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_
|
||||
from typing import Optional
|
||||
import bcrypt
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ..models.role import Role
|
||||
from ..models.booking import Booking, BookingStatus
|
||||
router = APIRouter(prefix='/users', tags=['users'])
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["users"])
|
||||
|
||||
|
||||
@router.get("/", dependencies=[Depends(authorize_roles("admin"))])
|
||||
async def get_users(
|
||||
search: Optional[str] = Query(None),
|
||||
role: Optional[str] = Query(None),
|
||||
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)
|
||||
):
|
||||
"""Get all users with filters and pagination (Admin only)"""
|
||||
@router.get('/', dependencies=[Depends(authorize_roles('admin'))])
|
||||
async def get_users(search: Optional[str]=Query(None), role: Optional[str]=Query(None), 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(User)
|
||||
|
||||
# Filter by search (full_name, email, phone)
|
||||
if search:
|
||||
query = query.filter(
|
||||
or_(
|
||||
User.full_name.like(f"%{search}%"),
|
||||
User.email.like(f"%{search}%"),
|
||||
User.phone.like(f"%{search}%")
|
||||
)
|
||||
)
|
||||
|
||||
# Filter by role
|
||||
query = query.filter(or_(User.full_name.like(f'%{search}%'), User.email.like(f'%{search}%'), User.phone.like(f'%{search}%')))
|
||||
if role:
|
||||
role_map = {"admin": 1, "staff": 2, "customer": 3}
|
||||
role_map = {'admin': 1, 'staff': 2, 'customer': 3}
|
||||
if role in role_map:
|
||||
query = query.filter(User.role_id == role_map[role])
|
||||
|
||||
# Filter by status
|
||||
if status_filter:
|
||||
is_active = status_filter == "active"
|
||||
is_active = status_filter == 'active'
|
||||
query = query.filter(User.is_active == is_active)
|
||||
|
||||
# Get total count
|
||||
total = query.count()
|
||||
|
||||
# Apply pagination
|
||||
offset = (page - 1) * limit
|
||||
users = query.order_by(User.created_at.desc()).offset(offset).limit(limit).all()
|
||||
|
||||
# Transform users
|
||||
result = []
|
||||
for user in users:
|
||||
user_dict = {
|
||||
"id": user.id,
|
||||
"email": user.email,
|
||||
"full_name": user.full_name,
|
||||
"phone": user.phone,
|
||||
"phone_number": user.phone, # For frontend compatibility
|
||||
"address": user.address,
|
||||
"avatar": user.avatar,
|
||||
"currency": getattr(user, 'currency', 'VND'),
|
||||
"is_active": user.is_active,
|
||||
"status": "active" if user.is_active else "inactive",
|
||||
"role_id": user.role_id,
|
||||
"role": user.role.name if user.role else "customer",
|
||||
"created_at": user.created_at.isoformat() if user.created_at else None,
|
||||
"updated_at": user.updated_at.isoformat() if user.updated_at else None,
|
||||
}
|
||||
user_dict = {'id': user.id, 'email': user.email, 'full_name': user.full_name, 'phone': user.phone, 'phone_number': user.phone, 'address': user.address, 'avatar': user.avatar, 'currency': getattr(user, 'currency', 'VND'), 'is_active': user.is_active, 'status': 'active' if user.is_active else 'inactive', 'role_id': user.role_id, 'role': user.role.name if user.role else 'customer', 'created_at': user.created_at.isoformat() if user.created_at else None, 'updated_at': user.updated_at.isoformat() if user.updated_at else None}
|
||||
result.append(user_dict)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"users": result,
|
||||
"pagination": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"totalPages": (total + limit - 1) // limit,
|
||||
},
|
||||
},
|
||||
}
|
||||
return {'status': 'success', 'data': {'users': result, 'pagination': {'total': total, 'page': page, 'limit': limit, 'totalPages': (total + limit - 1) // limit}}}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{id}", dependencies=[Depends(authorize_roles("admin"))])
|
||||
async def get_user_by_id(
|
||||
id: int,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get user by ID (Admin only)"""
|
||||
@router.get('/{id}', dependencies=[Depends(authorize_roles('admin'))])
|
||||
async def get_user_by_id(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
user = db.query(User).filter(User.id == id).first()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# Get recent bookings
|
||||
bookings = db.query(Booking).filter(
|
||||
Booking.user_id == id
|
||||
).order_by(Booking.created_at.desc()).limit(5).all()
|
||||
|
||||
user_dict = {
|
||||
"id": user.id,
|
||||
"email": user.email,
|
||||
"full_name": user.full_name,
|
||||
"phone": user.phone,
|
||||
"phone_number": user.phone,
|
||||
"address": user.address,
|
||||
"avatar": user.avatar,
|
||||
"currency": getattr(user, 'currency', 'VND'),
|
||||
"is_active": user.is_active,
|
||||
"status": "active" if user.is_active else "inactive",
|
||||
"role_id": user.role_id,
|
||||
"role": user.role.name if user.role else "customer",
|
||||
"created_at": user.created_at.isoformat() if user.created_at else None,
|
||||
"updated_at": user.updated_at.isoformat() if user.updated_at else None,
|
||||
"bookings": [
|
||||
{
|
||||
"id": b.id,
|
||||
"booking_number": b.booking_number,
|
||||
"status": b.status.value if isinstance(b.status, BookingStatus) else b.status,
|
||||
"created_at": b.created_at.isoformat() if b.created_at else None,
|
||||
}
|
||||
for b in bookings
|
||||
],
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {"user": user_dict}
|
||||
}
|
||||
raise HTTPException(status_code=404, detail='User not found')
|
||||
bookings = db.query(Booking).filter(Booking.user_id == id).order_by(Booking.created_at.desc()).limit(5).all()
|
||||
user_dict = {'id': user.id, 'email': user.email, 'full_name': user.full_name, 'phone': user.phone, 'phone_number': user.phone, 'address': user.address, 'avatar': user.avatar, 'currency': getattr(user, 'currency', 'VND'), 'is_active': user.is_active, 'status': 'active' if user.is_active else 'inactive', 'role_id': user.role_id, 'role': user.role.name if user.role else 'customer', 'created_at': user.created_at.isoformat() if user.created_at else None, 'updated_at': user.updated_at.isoformat() if user.updated_at else None, 'bookings': [{'id': b.id, 'booking_number': b.booking_number, 'status': b.status.value if isinstance(b.status, BookingStatus) else b.status, 'created_at': b.created_at.isoformat() if b.created_at else None} for b in bookings]}
|
||||
return {'status': 'success', 'data': {'user': user_dict}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/", dependencies=[Depends(authorize_roles("admin"))])
|
||||
async def create_user(
|
||||
user_data: dict,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create new user (Admin only)"""
|
||||
@router.post('/', dependencies=[Depends(authorize_roles('admin'))])
|
||||
async def create_user(user_data: dict, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
email = user_data.get("email")
|
||||
password = user_data.get("password")
|
||||
full_name = user_data.get("full_name")
|
||||
phone_number = user_data.get("phone_number")
|
||||
role = user_data.get("role", "customer")
|
||||
status = user_data.get("status", "active")
|
||||
|
||||
# Map role string to role_id
|
||||
role_map = {"admin": 1, "staff": 2, "customer": 3}
|
||||
email = user_data.get('email')
|
||||
password = user_data.get('password')
|
||||
full_name = user_data.get('full_name')
|
||||
phone_number = user_data.get('phone_number')
|
||||
role = user_data.get('role', 'customer')
|
||||
status = user_data.get('status', 'active')
|
||||
role_map = {'admin': 1, 'staff': 2, 'customer': 3}
|
||||
role_id = role_map.get(role, 3)
|
||||
|
||||
# Check if email exists
|
||||
existing = db.query(User).filter(User.email == email).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="Email already exists")
|
||||
|
||||
# Hash password
|
||||
raise HTTPException(status_code=400, detail='Email already exists')
|
||||
password_bytes = password.encode('utf-8')
|
||||
salt = bcrypt.gensalt()
|
||||
hashed_password = bcrypt.hashpw(password_bytes, salt).decode('utf-8')
|
||||
|
||||
# Create user
|
||||
user = User(
|
||||
email=email,
|
||||
password=hashed_password,
|
||||
full_name=full_name,
|
||||
phone=phone_number,
|
||||
role_id=role_id,
|
||||
is_active=status == "active",
|
||||
)
|
||||
|
||||
user = User(email=email, password=hashed_password, full_name=full_name, phone=phone_number, role_id=role_id, is_active=status == 'active')
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
# Remove password from response
|
||||
user_dict = {
|
||||
"id": user.id,
|
||||
"email": user.email,
|
||||
"full_name": user.full_name,
|
||||
"phone": user.phone,
|
||||
"phone_number": user.phone,
|
||||
"currency": getattr(user, 'currency', 'VND'),
|
||||
"role_id": user.role_id,
|
||||
"is_active": user.is_active,
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "User created successfully",
|
||||
"data": {"user": user_dict}
|
||||
}
|
||||
user_dict = {'id': user.id, 'email': user.email, 'full_name': user.full_name, 'phone': user.phone, 'phone_number': user.phone, 'currency': getattr(user, 'currency', 'VND'), 'role_id': user.role_id, 'is_active': user.is_active}
|
||||
return {'status': 'success', 'message': 'User created successfully', 'data': {'user': user_dict}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
async def update_user(
|
||||
id: int,
|
||||
user_data: dict,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update user"""
|
||||
@router.put('/{id}')
|
||||
async def update_user(id: int, user_data: dict, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
try:
|
||||
# Users can only update themselves unless they're admin
|
||||
if current_user.role_id != 1 and current_user.id != id:
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
|
||||
raise HTTPException(status_code=403, detail='Forbidden')
|
||||
user = db.query(User).filter(User.id == id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# Check if email is being changed and if it's taken
|
||||
email = user_data.get("email")
|
||||
raise HTTPException(status_code=404, detail='User not found')
|
||||
email = user_data.get('email')
|
||||
if email and email != user.email:
|
||||
existing = db.query(User).filter(User.email == email).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="Email already exists")
|
||||
|
||||
# Map role string to role_id (only admin can change role)
|
||||
role_map = {"admin": 1, "staff": 2, "customer": 3}
|
||||
|
||||
# Update fields
|
||||
if "full_name" in user_data:
|
||||
user.full_name = user_data["full_name"]
|
||||
if "email" in user_data and current_user.role_id == 1:
|
||||
user.email = user_data["email"]
|
||||
if "phone_number" in user_data:
|
||||
user.phone = user_data["phone_number"]
|
||||
if "role" in user_data and current_user.role_id == 1:
|
||||
user.role_id = role_map.get(user_data["role"], 3)
|
||||
if "status" in user_data and current_user.role_id == 1:
|
||||
user.is_active = user_data["status"] == "active"
|
||||
if "currency" in user_data:
|
||||
currency = user_data["currency"]
|
||||
raise HTTPException(status_code=400, detail='Email already exists')
|
||||
role_map = {'admin': 1, 'staff': 2, 'customer': 3}
|
||||
if 'full_name' in user_data:
|
||||
user.full_name = user_data['full_name']
|
||||
if 'email' in user_data and current_user.role_id == 1:
|
||||
user.email = user_data['email']
|
||||
if 'phone_number' in user_data:
|
||||
user.phone = user_data['phone_number']
|
||||
if 'role' in user_data and current_user.role_id == 1:
|
||||
user.role_id = role_map.get(user_data['role'], 3)
|
||||
if 'status' in user_data and current_user.role_id == 1:
|
||||
user.is_active = user_data['status'] == 'active'
|
||||
if 'currency' in user_data:
|
||||
currency = user_data['currency']
|
||||
if len(currency) == 3 and currency.isalpha():
|
||||
user.currency = currency.upper()
|
||||
if "password" in user_data:
|
||||
password_bytes = user_data["password"].encode('utf-8')
|
||||
if 'password' in user_data:
|
||||
password_bytes = user_data['password'].encode('utf-8')
|
||||
salt = bcrypt.gensalt()
|
||||
user.password = bcrypt.hashpw(password_bytes, salt).decode('utf-8')
|
||||
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
# Remove password from response
|
||||
user_dict = {
|
||||
"id": user.id,
|
||||
"email": user.email,
|
||||
"full_name": user.full_name,
|
||||
"phone": user.phone,
|
||||
"phone_number": user.phone,
|
||||
"currency": getattr(user, 'currency', 'VND'),
|
||||
"role_id": user.role_id,
|
||||
"is_active": user.is_active,
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "User updated successfully",
|
||||
"data": {"user": user_dict}
|
||||
}
|
||||
user_dict = {'id': user.id, 'email': user.email, 'full_name': user.full_name, 'phone': user.phone, 'phone_number': user.phone, 'currency': getattr(user, 'currency', 'VND'), 'role_id': user.role_id, 'is_active': user.is_active}
|
||||
return {'status': 'success', 'message': 'User updated successfully', 'data': {'user': user_dict}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/{id}", dependencies=[Depends(authorize_roles("admin"))])
|
||||
async def delete_user(
|
||||
id: int,
|
||||
current_user: User = Depends(authorize_roles("admin")),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Delete user (Admin only)"""
|
||||
@router.delete('/{id}', dependencies=[Depends(authorize_roles('admin'))])
|
||||
async def delete_user(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||
try:
|
||||
user = db.query(User).filter(User.id == id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# Check if user has active bookings
|
||||
active_bookings = db.query(Booking).filter(
|
||||
Booking.user_id == id,
|
||||
Booking.status.in_([BookingStatus.pending, BookingStatus.confirmed, BookingStatus.checked_in])
|
||||
).count()
|
||||
|
||||
raise HTTPException(status_code=404, detail='User not found')
|
||||
active_bookings = db.query(Booking).filter(Booking.user_id == id, Booking.status.in_([BookingStatus.pending, BookingStatus.confirmed, BookingStatus.checked_in])).count()
|
||||
if active_bookings > 0:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Cannot delete user with active bookings"
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=400, detail='Cannot delete user with active bookings')
|
||||
db.delete(user)
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "User deleted successfully"
|
||||
}
|
||||
return {'status': 'success', 'message': 'User deleted successfully'}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
Reference in New Issue
Block a user