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

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