This commit is contained in:
Iliyan Angelov
2025-11-21 10:55:05 +02:00
parent 722997bb19
commit 4ab7546de0
53 changed files with 3091 additions and 56 deletions

View File

@@ -91,10 +91,8 @@ async def health_check(db: Session=Depends(get_db)):
async def metrics():
return {'status': 'success', 'service': settings.APP_NAME, 'version': settings.APP_VERSION, 'environment': settings.ENVIRONMENT, 'timestamp': datetime.utcnow().isoformat()}
app.include_router(auth_routes.router, prefix='/api')
app.include_router(privacy_routes.router, prefix='/api')
app.include_router(auth_routes.router, prefix=settings.API_V1_PREFIX)
app.include_router(privacy_routes.router, prefix=settings.API_V1_PREFIX)
from .routes import room_routes, booking_routes, payment_routes, invoice_routes, banner_routes, favorite_routes, service_routes, service_booking_routes, promotion_routes, report_routes, review_routes, user_routes, audit_routes, admin_privacy_routes, system_settings_routes, contact_routes, page_content_routes, home_routes, about_routes, contact_content_routes, footer_routes, chat_routes
from .routes import room_routes, booking_routes, payment_routes, invoice_routes, banner_routes, favorite_routes, service_routes, service_booking_routes, promotion_routes, report_routes, review_routes, user_routes, audit_routes, admin_privacy_routes, system_settings_routes, contact_routes, page_content_routes, home_routes, about_routes, contact_content_routes, footer_routes, chat_routes, privacy_routes, terms_routes, refunds_routes, cancellation_routes, accessibility_routes, faq_routes
app.include_router(room_routes.router, prefix='/api')
app.include_router(booking_routes.router, prefix='/api')
app.include_router(payment_routes.router, prefix='/api')
@@ -115,6 +113,12 @@ app.include_router(home_routes.router, prefix='/api')
app.include_router(about_routes.router, prefix='/api')
app.include_router(contact_content_routes.router, prefix='/api')
app.include_router(footer_routes.router, prefix='/api')
app.include_router(privacy_routes.router, prefix='/api')
app.include_router(terms_routes.router, prefix='/api')
app.include_router(refunds_routes.router, prefix='/api')
app.include_router(cancellation_routes.router, prefix='/api')
app.include_router(accessibility_routes.router, prefix='/api')
app.include_router(faq_routes.router, prefix='/api')
app.include_router(chat_routes.router, prefix='/api')
app.include_router(room_routes.router, prefix=settings.API_V1_PREFIX)
app.include_router(booking_routes.router, prefix=settings.API_V1_PREFIX)
@@ -136,6 +140,12 @@ app.include_router(home_routes.router, prefix=settings.API_V1_PREFIX)
app.include_router(about_routes.router, prefix=settings.API_V1_PREFIX)
app.include_router(contact_content_routes.router, prefix=settings.API_V1_PREFIX)
app.include_router(footer_routes.router, prefix=settings.API_V1_PREFIX)
app.include_router(privacy_routes.router, prefix=settings.API_V1_PREFIX)
app.include_router(terms_routes.router, prefix=settings.API_V1_PREFIX)
app.include_router(refunds_routes.router, prefix=settings.API_V1_PREFIX)
app.include_router(cancellation_routes.router, prefix=settings.API_V1_PREFIX)
app.include_router(accessibility_routes.router, prefix=settings.API_V1_PREFIX)
app.include_router(faq_routes.router, prefix=settings.API_V1_PREFIX)
app.include_router(chat_routes.router, prefix=settings.API_V1_PREFIX)
app.include_router(page_content_routes.router, prefix='/api')
app.include_router(page_content_routes.router, prefix=settings.API_V1_PREFIX)

View File

@@ -10,11 +10,17 @@ class PageType(str, enum.Enum):
ABOUT = 'about'
FOOTER = 'footer'
SEO = 'seo'
PRIVACY = 'privacy'
TERMS = 'terms'
REFUNDS = 'refunds'
CANCELLATION = 'cancellation'
ACCESSIBILITY = 'accessibility'
FAQ = 'faq'
class PageContent(Base):
__tablename__ = 'page_contents'
id = Column(Integer, primary_key=True, index=True)
page_type = Column(SQLEnum(PageType), nullable=False, unique=True, index=True)
page_type = Column(SQLEnum(PageType, values_callable=lambda x: [e.value for e in x], native_enum=False, length=50), nullable=False, unique=True, index=True)
title = Column(String(500), nullable=True)
subtitle = Column(String(1000), nullable=True)
description = Column(Text, nullable=True)

View File

@@ -0,0 +1,44 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
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='/accessibility', tags=['accessibility'])
def serialize_page_content(content: PageContent) -> dict:
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,
'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_accessibility_content(db: Session=Depends(get_db)):
try:
content = db.query(PageContent).filter(PageContent.page_type == PageType.ACCESSIBILITY).first()
if not content:
return {'status': 'success', 'data': {'page_content': None, 'is_active': False}}
if not content.is_active:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Accessibility page is currently disabled')
content_dict = serialize_page_content(content)
return {'status': 'success', 'data': {'page_content': content_dict}}
except HTTPException:
raise
except Exception as e:
logger.error(f'Error fetching accessibility content: {str(e)}', exc_info=True)
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching accessibility content: {str(e)}')

View File

@@ -0,0 +1,44 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
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='/cancellation', tags=['cancellation'])
def serialize_page_content(content: PageContent) -> dict:
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,
'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_cancellation_content(db: Session=Depends(get_db)):
try:
content = db.query(PageContent).filter(PageContent.page_type == PageType.CANCELLATION).first()
if not content:
return {'status': 'success', 'data': {'page_content': None, 'is_active': False}}
if not content.is_active:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Cancellation policy page is currently disabled')
content_dict = serialize_page_content(content)
return {'status': 'success', 'data': {'page_content': content_dict}}
except HTTPException:
raise
except Exception as e:
logger.error(f'Error fetching cancellation content: {str(e)}', exc_info=True)
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching cancellation content: {str(e)}')

View File

@@ -0,0 +1,44 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
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='/faq', tags=['faq'])
def serialize_page_content(content: PageContent) -> dict:
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,
'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_faq_content(db: Session=Depends(get_db)):
try:
content = db.query(PageContent).filter(PageContent.page_type == PageType.FAQ).first()
if not content:
return {'status': 'success', 'data': {'page_content': None, 'is_active': False}}
if not content.is_active:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='FAQ page is currently disabled')
content_dict = serialize_page_content(content)
return {'status': 'success', 'data': {'page_content': content_dict}}
except HTTPException:
raise
except Exception as e:
logger.error(f'Error fetching FAQ content: {str(e)}', exc_info=True)
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching FAQ content: {str(e)}')

View File

@@ -1,39 +1,110 @@
from fastapi import APIRouter, Depends, Request, Response, status
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
import json
from datetime import datetime
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 ..models.page_content import PageContent, PageType
from ..services.privacy_admin_service import privacy_admin_service
from ..schemas.privacy import CookieConsent, CookieConsentResponse, UpdateCookieConsentRequest, CookieCategoryPreferences
from ..schemas.admin_privacy import PublicPrivacyConfigResponse
logger = get_logger(__name__)
router = APIRouter(prefix='/privacy', tags=['privacy'])
@router.get('/cookie-consent', response_model=CookieConsentResponse, status_code=status.HTTP_200_OK)
async def get_cookie_consent(request: Request) -> CookieConsentResponse:
raw_cookie = request.cookies.get(COOKIE_CONSENT_COOKIE_NAME)
consent = _parse_consent_cookie(raw_cookie)
consent.categories.necessary = True
return CookieConsentResponse(data=consent)
def serialize_page_content(content: PageContent) -> dict:
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,
'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.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
categories.necessary = True
consent = CookieConsent(categories=categories, has_decided=True)
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('/')
async def get_privacy_content(db: Session=Depends(get_db)):
try:
content = db.query(PageContent).filter(PageContent.page_type == PageType.PRIVACY).first()
if not content:
return {'status': 'success', 'data': {'page_content': None, 'is_active': False}}
if not content.is_active:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Privacy policy page is currently disabled')
content_dict = serialize_page_content(content)
return {'status': 'success', 'data': {'page_content': content_dict}}
except HTTPException:
raise
except Exception as e:
logger.error(f'Error fetching privacy content: {str(e)}', exc_info=True)
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching privacy content: {str(e)}')
@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)
@router.get('/cookie-consent', response_model=CookieConsentResponse)
async def get_cookie_consent(db: Session=Depends(get_db)):
"""
Get the default cookie consent structure.
Note: Actual consent is stored client-side (localStorage), this endpoint provides the default structure.
"""
try:
# Return default consent structure
# The actual consent state is managed client-side
consent = CookieConsent(
version=1,
updated_at=datetime.utcnow(),
has_decided=False,
categories=CookieCategoryPreferences(
necessary=True, # Always true
analytics=False,
marketing=False,
preferences=False
)
)
return CookieConsentResponse(status='success', data=consent)
except Exception as e:
logger.error(f'Error fetching cookie consent: {str(e)}', exc_info=True)
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching cookie consent: {str(e)}')
@router.post('/cookie-consent', response_model=CookieConsentResponse)
async def update_cookie_consent(payload: UpdateCookieConsentRequest, db: Session=Depends(get_db)):
"""
Update cookie consent preferences.
Note: This endpoint acknowledges the consent update. Actual consent is stored client-side.
"""
try:
# Create updated consent structure
consent = CookieConsent(
version=1,
updated_at=datetime.utcnow(),
has_decided=True,
categories=CookieCategoryPreferences(
necessary=True, # Always true
analytics=payload.analytics if payload.analytics is not None else False,
marketing=payload.marketing if payload.marketing is not None else False,
preferences=payload.preferences if payload.preferences is not None else False
)
)
return CookieConsentResponse(status='success', data=consent)
except Exception as e:
logger.error(f'Error updating cookie consent: {str(e)}', exc_info=True)
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error updating cookie consent: {str(e)}')
@router.get('/config', response_model=PublicPrivacyConfigResponse)
async def get_public_privacy_config(db: Session=Depends(get_db)):
"""
Get public privacy configuration including cookie policy settings and integration configs.
This endpoint is public and does not require authentication.
"""
try:
config = privacy_admin_service.get_public_privacy_config(db)
return PublicPrivacyConfigResponse(status='success', data=config)
except Exception as e:
logger.error(f'Error fetching public privacy config: {str(e)}', exc_info=True)
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching public privacy config: {str(e)}')

View File

@@ -0,0 +1,45 @@
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='/refunds', tags=['refunds'])
def serialize_page_content(content: PageContent) -> dict:
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,
'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_refunds_content(db: Session=Depends(get_db)):
try:
content = db.query(PageContent).filter(PageContent.page_type == PageType.REFUNDS).first()
if not content:
return {'status': 'success', 'data': {'page_content': None, 'is_active': False}}
if not content.is_active:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Refunds policy page is currently disabled')
content_dict = serialize_page_content(content)
return {'status': 'success', 'data': {'page_content': content_dict}}
except HTTPException:
raise
except Exception as e:
logger.error(f'Error fetching refunds content: {str(e)}', exc_info=True)
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching refunds content: {str(e)}')

View File

@@ -0,0 +1,45 @@
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='/terms', tags=['terms'])
def serialize_page_content(content: PageContent) -> dict:
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,
'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_terms_content(db: Session=Depends(get_db)):
try:
content = db.query(PageContent).filter(PageContent.page_type == PageType.TERMS).first()
if not content:
return {'status': 'success', 'data': {'page_content': None, 'is_active': False}}
if not content.is_active:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Terms & Conditions page is currently disabled')
content_dict = serialize_page_content(content)
return {'status': 'success', 'data': {'page_content': content_dict}}
except HTTPException:
raise
except Exception as e:
logger.error(f'Error fetching terms content: {str(e)}', exc_info=True)
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching terms content: {str(e)}')