This commit is contained in:
Iliyan Angelov
2025-11-18 23:35:19 +02:00
parent ab832f851b
commit 2043ac897c
27 changed files with 2947 additions and 323 deletions

View File

@@ -39,6 +39,7 @@ async def get_all_page_contents(
"map_url": content.map_url,
"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,
"hero_title": content.hero_title,
"hero_subtitle": content.hero_subtitle,
"hero_image": content.hero_image,
@@ -97,8 +98,10 @@ async def get_page_content(
"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,
"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,
"hero_title": content.hero_title,
"hero_subtitle": content.hero_subtitle,
"hero_image": content.hero_image,
@@ -141,6 +144,7 @@ async def create_or_update_page_content(
map_url: Optional[str] = None,
social_links: Optional[str] = None,
footer_links: Optional[str] = None,
badges: Optional[str] = None,
hero_title: Optional[str] = None,
hero_subtitle: Optional[str] = None,
hero_image: Optional[str] = None,
@@ -183,6 +187,15 @@ async def create_or_update_page_content(
detail="Invalid JSON in footer_links"
)
if badges:
try:
json.loads(badges)
except json.JSONDecodeError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid JSON in badges"
)
if values:
try:
json.loads(values)
@@ -236,6 +249,8 @@ async def create_or_update_page_content(
existing_content.social_links = social_links
if footer_links is not None:
existing_content.footer_links = footer_links
if badges is not None:
existing_content.badges = badges
if hero_title is not None:
existing_content.hero_title = hero_title
if hero_subtitle is not None:
@@ -286,6 +301,7 @@ async def create_or_update_page_content(
map_url=map_url,
social_links=social_links,
footer_links=footer_links,
badges=badges,
hero_title=hero_title,
hero_subtitle=hero_subtitle,
hero_image=hero_image,
@@ -346,7 +362,7 @@ async def update_page_content(
for key, value in page_data.items():
if hasattr(existing_content, key):
# Handle JSON fields - convert dict/list to JSON string
if key in ["contact_info", "social_links", "footer_links", "values", "features"] and value is not None:
if key in ["contact_info", "social_links", "footer_links", "badges", "values", "features"] and value is not None:
if isinstance(value, str):
# Already a string, validate it's valid JSON
try:
@@ -383,8 +399,10 @@ async def update_page_content(
"og_image": existing_content.og_image,
"canonical_url": existing_content.canonical_url,
"contact_info": json.loads(existing_content.contact_info) if existing_content.contact_info else None,
"map_url": existing_content.map_url,
"social_links": json.loads(existing_content.social_links) if existing_content.social_links else None,
"footer_links": json.loads(existing_content.footer_links) if existing_content.footer_links else None,
"badges": json.loads(existing_content.badges) if existing_content.badges else None,
"hero_title": existing_content.hero_title,
"hero_subtitle": existing_content.hero_subtitle,
"hero_image": existing_content.hero_image,

View File

@@ -1,11 +1,33 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, Request, UploadFile, File
from sqlalchemy.orm import Session
from typing import Optional
from datetime import datetime
from pydantic import BaseModel, EmailStr
from pathlib import Path
import logging
import aiofiles
import uuid
import os
from ..config.database import get_db
from ..middleware.auth import get_current_user, authorize_roles
from ..models.user import User
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://'):
return image_url
if image_url.startswith('/'):
return f"{base_url}{image_url}"
return f"{base_url}/{image_url}"
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/admin/system-settings", tags=["admin-system-settings"])
@@ -300,3 +322,768 @@ async def update_stripe_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",
"smtp_port",
"smtp_user",
"smtp_password",
"smtp_from_email",
"smtp_from_name",
"smtp_use_tls",
]
for key in setting_keys:
setting = db.query(SystemSettings).filter(
SystemSettings.key == key
).first()
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 ""
return "*" * (len(password_value) - 4) + password_value[-4:]
result = {
"smtp_host": smtp_settings.get("smtp_host", ""),
"smtp_port": smtp_settings.get("smtp_port", ""),
"smtp_user": smtp_settings.get("smtp_user", ""),
"smtp_password": "",
"smtp_password_masked": mask_password(smtp_settings.get("smtp_password", "")),
"smtp_from_email": smtp_settings.get("smtp_from_email", ""),
"smtp_from_name": smtp_settings.get("smtp_from_name", ""),
"smtp_use_tls": smtp_settings.get("smtp_use_tls", "true").lower() == "true",
"has_host": bool(smtp_settings.get("smtp_host")),
"has_user": bool(smtp_settings.get("smtp_user")),
"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()
if password_setting:
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()
if any_setting:
result["updated_at"] = any_setting.updated_at.isoformat() if any_setting.updated_at else None
result["updated_by"] = any_setting.updated_by.full_name if any_setting.updated_by else None
else:
result["updated_at"] = None
result["updated_by"] = None
return {
"status": "success",
"data": result
}
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()
smtp_user = smtp_data.get("smtp_user", "").strip()
smtp_password = smtp_data.get("smtp_password", "").strip()
smtp_from_email = smtp_data.get("smtp_from_email", "").strip()
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,
detail="SMTP host cannot be empty"
)
if smtp_port:
try:
port_num = int(smtp_port)
if port_num < 1 or port_num > 65535:
raise HTTPException(
status_code=400,
detail="SMTP port must be between 1 and 65535"
)
except ValueError:
raise HTTPException(
status_code=400,
detail="SMTP port must be a valid number"
)
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
).first()
if setting:
setting.value = value
setting.updated_by_id = current_user.id
else:
setting = SystemSettings(
key=key,
value=value,
description=description,
updated_by_id=current_user.id
)
db.add(setting)
# Update or create settings (only update if value is provided)
if smtp_host:
update_setting(
"smtp_host",
smtp_host,
"SMTP server hostname (e.g., smtp.gmail.com)"
)
if smtp_port:
update_setting(
"smtp_port",
smtp_port,
"SMTP server port (e.g., 587 for STARTTLS, 465 for SSL)"
)
if smtp_user:
update_setting(
"smtp_user",
smtp_user,
"SMTP authentication username/email"
)
if smtp_password:
update_setting(
"smtp_password",
smtp_password,
"SMTP authentication password (stored securely)"
)
if smtp_from_email:
update_setting(
"smtp_from_email",
smtp_from_email,
"Default 'From' email address for outgoing emails"
)
if smtp_from_name:
update_setting(
"smtp_from_name",
smtp_from_name,
"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",
"true" if smtp_use_tls else "false",
"Use TLS/SSL for SMTP connection (true for port 465, false for port 587 with STARTTLS)"
)
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(
SystemSettings.key == key
).first()
if setting:
updated_settings[key] = setting.value
result = {
"smtp_host": updated_settings.get("smtp_host", ""),
"smtp_port": updated_settings.get("smtp_port", ""),
"smtp_user": updated_settings.get("smtp_user", ""),
"smtp_password": smtp_password if smtp_password else "",
"smtp_password_masked": mask_password(updated_settings.get("smtp_password", "")),
"smtp_from_email": updated_settings.get("smtp_from_email", ""),
"smtp_from_name": updated_settings.get("smtp_from_name", ""),
"smtp_use_tls": updated_settings.get("smtp_use_tls", "true").lower() == "true",
"has_host": bool(updated_settings.get("smtp_host")),
"has_user": bool(updated_settings.get("smtp_user")),
"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()
if password_setting:
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
return {
"status": "success",
"message": "SMTP settings updated successfully",
"data": result
}
except HTTPException:
raise
except Exception as e:
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>
"""
# 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()
# Send the test email
await send_email(
to=test_email,
subject="SMTP Test Email - Hotel Booking System",
html=test_html,
text=test_text
)
sent_at = datetime.utcnow()
return {
"status": "success",
"message": f"Test email sent successfully to {test_email}",
"data": {
"recipient": test_email,
"sent_at": sent_at.isoformat()
}
}
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,
detail="SMTP settings are not fully configured. Please configure SMTP Host, Username, and Password in the Email Server settings."
)
elif "authentication failed" in error_msg.lower() or "invalid credentials" in error_msg.lower():
raise HTTPException(
status_code=400,
detail="SMTP authentication failed. Please check your SMTP username and password."
)
elif "connection" in error_msg.lower() or "timeout" in error_msg.lower():
raise HTTPException(
status_code=400,
detail=f"Cannot connect to SMTP server. Please check your SMTP host and port settings. Error: {error_msg}"
)
else:
raise HTTPException(
status_code=500,
detail=f"Failed to send test email: {error_msg}"
)
class UpdateCompanySettingsRequest(BaseModel):
company_name: Optional[str] = None
company_tagline: Optional[str] = None
company_phone: Optional[str] = None
company_email: Optional[str] = None
company_address: Optional[str] = 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",
"company_tagline",
"company_logo_url",
"company_favicon_url",
"company_phone",
"company_email",
"company_address",
]
settings_dict = {}
for key in setting_keys:
setting = db.query(SystemSettings).filter(
SystemSettings.key == key
).first()
if setting:
settings_dict[key] = setting.value
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()
updated_at = None
updated_by = None
if logo_setting:
updated_at = logo_setting.updated_at.isoformat() if logo_setting.updated_at else None
updated_by = logo_setting.updated_by.full_name if logo_setting.updated_by else None
return {
"status": "success",
"data": {
"company_name": settings_dict.get("company_name", ""),
"company_tagline": settings_dict.get("company_tagline", ""),
"company_logo_url": settings_dict.get("company_logo_url", ""),
"company_favicon_url": settings_dict.get("company_favicon_url", ""),
"company_phone": settings_dict.get("company_phone", ""),
"company_email": settings_dict.get("company_email", ""),
"company_address": settings_dict.get("company_address", ""),
"updated_at": updated_at,
"updated_by": updated_by,
}
}
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 = {}
if request_data.company_name is not None:
db_settings["company_name"] = request_data.company_name
if request_data.company_tagline is not None:
db_settings["company_tagline"] = request_data.company_tagline
if request_data.company_phone is not None:
db_settings["company_phone"] = request_data.company_phone
if request_data.company_email is not None:
db_settings["company_email"] = request_data.company_email
if request_data.company_address is not None:
db_settings["company_address"] = request_data.company_address
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,
updated_by_id=current_user.id
)
db.add(setting)
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"]:
setting = db.query(SystemSettings).filter(
SystemSettings.key == key
).first()
if setting:
updated_settings[key] = setting.value
else:
updated_settings[key] = None
# Get updated_at and updated_by
logo_setting = db.query(SystemSettings).filter(
SystemSettings.key == "company_logo_url"
).first()
if not logo_setting:
logo_setting = db.query(SystemSettings).filter(
SystemSettings.key == "company_name"
).first()
updated_at = None
updated_by = None
if logo_setting:
updated_at = logo_setting.updated_at.isoformat() if logo_setting.updated_at else None
updated_by = logo_setting.updated_by.full_name if logo_setting.updated_by else None
return {
"status": "success",
"message": "Company settings updated successfully",
"data": {
"company_name": updated_settings.get("company_name", ""),
"company_tagline": updated_settings.get("company_tagline", ""),
"company_logo_url": updated_settings.get("company_logo_url", ""),
"company_favicon_url": updated_settings.get("company_favicon_url", ""),
"company_phone": updated_settings.get("company_phone", ""),
"company_email": updated_settings.get("company_email", ""),
"company_address": updated_settings.get("company_address", ""),
"updated_at": updated_at,
"updated_by": updated_by,
}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.post("/company/logo")
async def upload_company_logo(
request: Request,
image: UploadFile = File(...),
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
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()
if old_logo_setting and old_logo_setting.value:
old_logo_path = Path(__file__).parent.parent.parent / old_logo_setting.value.lstrip('/')
if old_logo_path.exists() and old_logo_path.is_file():
try:
old_logo_path.unlink()
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()
if logo_setting:
logo_setting.value = image_url
logo_setting.updated_at = datetime.utcnow()
logo_setting.updated_by_id = current_user.id
else:
logo_setting = SystemSettings(
key="company_logo_url",
value=image_url,
updated_by_id=current_user.id
)
db.add(logo_setting)
db.commit()
# Return the image URL
base_url = get_base_url(request)
full_url = normalize_image_url(image_url, base_url)
return {
"status": "success",
"message": "Logo uploaded successfully",
"data": {
"logo_url": image_url,
"full_url": full_url
}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
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,
image: UploadFile = File(...),
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,
detail="File type could not be determined"
)
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(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Favicon must be .ico, .png, or .svg file"
)
# Validate file size (max 500KB)
content = await image.read()
if len(content) > 500 * 1024: # 500KB
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()
if old_favicon_setting and old_favicon_setting.value:
old_favicon_path = Path(__file__).parent.parent.parent / old_favicon_setting.value.lstrip('/')
if old_favicon_path.exists() and old_favicon_path.is_file():
try:
old_favicon_path.unlink()
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"
elif filename_lower.endswith('.svg'):
filename = "favicon.svg"
else:
filename = "favicon.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
favicon_setting = db.query(SystemSettings).filter(
SystemSettings.key == "company_favicon_url"
).first()
if favicon_setting:
favicon_setting.value = image_url
favicon_setting.updated_at = datetime.utcnow()
favicon_setting.updated_by_id = current_user.id
else:
favicon_setting = SystemSettings(
key="company_favicon_url",
value=image_url,
updated_by_id=current_user.id
)
db.add(favicon_setting)
db.commit()
# Return the image URL
base_url = get_base_url(request)
full_url = normalize_image_url(image_url, base_url)
return {
"status": "success",
"message": "Favicon uploaded successfully",
"data": {
"favicon_url": image_url,
"full_url": full_url
}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
logger.error(f"Error uploading favicon: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))