This commit is contained in:
Iliyan Angelov
2025-11-21 01:20:51 +02:00
parent a38ab4fa82
commit 6f85b8cf17
242 changed files with 7154 additions and 14492 deletions

View File

@@ -1,2 +0,0 @@
# Routes package

View File

@@ -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)}')

View File

@@ -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)

View File

@@ -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))

View File

@@ -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)}')

View File

@@ -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

View 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

View File

@@ -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)}')

View File

@@ -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.')

View File

@@ -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))

View File

@@ -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)}')

View File

@@ -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)}')

View File

@@ -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

View File

@@ -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)

View File

@@ -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))

View File

@@ -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))

View File

@@ -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

View File

@@ -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()

View File

@@ -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))

View File

@@ -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:

View File

@@ -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))