diff --git a/Backend/alembic/versions/__pycache__/add_badges_to_page_content.cpython-312.pyc b/Backend/alembic/versions/__pycache__/add_badges_to_page_content.cpython-312.pyc
new file mode 100644
index 00000000..1c38df1e
Binary files /dev/null and b/Backend/alembic/versions/__pycache__/add_badges_to_page_content.cpython-312.pyc differ
diff --git a/Backend/alembic/versions/__pycache__/cce764ef7a50_add_map_url_to_page_content.cpython-312.pyc b/Backend/alembic/versions/__pycache__/cce764ef7a50_add_map_url_to_page_content.cpython-312.pyc
index dca8999b..0f094e2d 100644
Binary files a/Backend/alembic/versions/__pycache__/cce764ef7a50_add_map_url_to_page_content.cpython-312.pyc and b/Backend/alembic/versions/__pycache__/cce764ef7a50_add_map_url_to_page_content.cpython-312.pyc differ
diff --git a/Backend/alembic/versions/add_badges_to_page_content.py b/Backend/alembic/versions/add_badges_to_page_content.py
new file mode 100644
index 00000000..54f7cb0c
--- /dev/null
+++ b/Backend/alembic/versions/add_badges_to_page_content.py
@@ -0,0 +1,26 @@
+"""add_badges_to_page_content
+
+Revision ID: add_badges_to_page_content
+Revises: cce764ef7a50
+Create Date: 2025-01-14 10:00:00.000000
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+# revision identifiers, used by Alembic.
+revision = 'add_badges_to_page_content'
+down_revision = 'cce764ef7a50'
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ # Add badges column to page_contents table
+ op.add_column('page_contents', sa.Column('badges', sa.Text(), nullable=True))
+
+
+def downgrade() -> None:
+ # Remove badges column from page_contents table
+ op.drop_column('page_contents', 'badges')
+
diff --git a/Backend/src/models/__pycache__/page_content.cpython-312.pyc b/Backend/src/models/__pycache__/page_content.cpython-312.pyc
index 6ba79360..ce674008 100644
Binary files a/Backend/src/models/__pycache__/page_content.cpython-312.pyc and b/Backend/src/models/__pycache__/page_content.cpython-312.pyc differ
diff --git a/Backend/src/models/page_content.py b/Backend/src/models/page_content.py
index 6e382195..31b8304f 100644
--- a/Backend/src/models/page_content.py
+++ b/Backend/src/models/page_content.py
@@ -40,6 +40,7 @@ class PageContent(Base):
map_url = Column(String(1000), nullable=True) # Google Maps embed URL
social_links = Column(Text, nullable=True) # JSON: facebook, twitter, instagram, etc.
footer_links = Column(Text, nullable=True) # JSON: quick links, support links
+ badges = Column(Text, nullable=True) # JSON: array of badges with text and icon
# Home page specific
hero_title = Column(String(500), nullable=True)
diff --git a/Backend/src/routes/__pycache__/page_content_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/page_content_routes.cpython-312.pyc
index d97a9ae9..2e11cc3c 100644
Binary files a/Backend/src/routes/__pycache__/page_content_routes.cpython-312.pyc and b/Backend/src/routes/__pycache__/page_content_routes.cpython-312.pyc differ
diff --git a/Backend/src/routes/__pycache__/system_settings_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/system_settings_routes.cpython-312.pyc
index 197db1e9..694e174c 100644
Binary files a/Backend/src/routes/__pycache__/system_settings_routes.cpython-312.pyc and b/Backend/src/routes/__pycache__/system_settings_routes.cpython-312.pyc differ
diff --git a/Backend/src/routes/page_content_routes.py b/Backend/src/routes/page_content_routes.py
index 0374f7d5..05e1291f 100644
--- a/Backend/src/routes/page_content_routes.py
+++ b/Backend/src/routes/page_content_routes.py
@@ -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,
diff --git a/Backend/src/routes/system_settings_routes.py b/Backend/src/routes/system_settings_routes.py
index e197a835..ea83a785 100644
--- a/Backend/src/routes/system_settings_routes.py
+++ b/Backend/src/routes/system_settings_routes.py
@@ -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"""
+
+
+
+
+
+
+
+
+
✅ SMTP Test Email
+
+
+
+
🎉
+
Email Configuration Test Successful!
+
+
+
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.
+
+
What's next?
+
+
Welcome emails for new user registrations
+
Password reset emails
+
Booking confirmation emails
+
Payment notifications
+
And other system notifications
+
+
+
+
+
+
+ """
+
+ # 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))
+
diff --git a/Backend/src/utils/__pycache__/email_templates.cpython-312.pyc b/Backend/src/utils/__pycache__/email_templates.cpython-312.pyc
index f446400c..6414cf35 100644
Binary files a/Backend/src/utils/__pycache__/email_templates.cpython-312.pyc and b/Backend/src/utils/__pycache__/email_templates.cpython-312.pyc differ
diff --git a/Backend/src/utils/__pycache__/mailer.cpython-312.pyc b/Backend/src/utils/__pycache__/mailer.cpython-312.pyc
index 40b320bd..99b84e65 100644
Binary files a/Backend/src/utils/__pycache__/mailer.cpython-312.pyc and b/Backend/src/utils/__pycache__/mailer.cpython-312.pyc differ
diff --git a/Backend/src/utils/email_templates.py b/Backend/src/utils/email_templates.py
index b70ef0a9..42a4b614 100644
--- a/Backend/src/utils/email_templates.py
+++ b/Backend/src/utils/email_templates.py
@@ -3,10 +3,123 @@ Email templates for various notifications
"""
from datetime import datetime
from typing import Optional
+from ..config.database import SessionLocal
+from ..models.system_settings import SystemSettings
-def get_base_template(content: str, title: str = "Hotel Booking") -> str:
- """Base HTML email template"""
+def _get_company_settings():
+ """Get company settings from database"""
+ try:
+ db = SessionLocal()
+ try:
+ settings = {}
+ setting_keys = [
+ "company_name",
+ "company_tagline",
+ "company_logo_url",
+ "company_phone",
+ "company_email",
+ "company_address",
+ ]
+
+ for key in setting_keys:
+ setting = db.query(SystemSettings).filter(
+ SystemSettings.key == key
+ ).first()
+ if setting and setting.value:
+ settings[key] = setting.value
+ else:
+ settings[key] = None
+
+ return settings
+ finally:
+ db.close()
+ except Exception:
+ return {
+ "company_name": None,
+ "company_tagline": None,
+ "company_logo_url": None,
+ "company_phone": None,
+ "company_email": None,
+ "company_address": None,
+ }
+
+
+def get_base_template(content: str, title: str = "Hotel Booking", client_url: str = "http://localhost:5173") -> str:
+ """Luxury HTML email template with premium company branding"""
+ company_settings = _get_company_settings()
+ company_name = company_settings.get("company_name") or "Hotel Booking"
+ company_tagline = company_settings.get("company_tagline") or "Excellence Redefined"
+ company_logo_url = company_settings.get("company_logo_url")
+ company_phone = company_settings.get("company_phone")
+ company_email = company_settings.get("company_email")
+ company_address = company_settings.get("company_address")
+
+ # Build logo HTML if logo exists
+ logo_html = ""
+ if company_logo_url:
+ # Convert relative URL to absolute if needed
+ if not company_logo_url.startswith('http'):
+ # Try to construct full URL
+ server_url = client_url.replace('://localhost:5173', '').replace('://localhost:3000', '')
+ if not server_url.startswith('http'):
+ server_url = f"http://{server_url}" if ':' not in server_url.split('//')[-1] else server_url
+ full_logo_url = f"{server_url}{company_logo_url}" if company_logo_url.startswith('/') else f"{server_url}/{company_logo_url}"
+ else:
+ full_logo_url = company_logo_url
+
+ logo_html = f'''
+
+
+ {f'
{company_tagline}
' if company_tagline else ''}
+
+ '''
+ else:
+ logo_html = f'''
+
+
{company_name}
+ {f'
{company_tagline}
' if company_tagline else ''}
+
+ '''
+
+ # Build footer contact info
+ footer_contact = ""
+ if company_phone or company_email or company_address:
+ footer_contact = '''
+
+
+ '''
+ if company_phone:
+ footer_contact += f'''
+
+
+
📞 {company_phone}
+
+
+ '''
+ if company_email:
+ footer_contact += f'''
+
- Our team is here to help you with any questions about your stay,
- bookings, or special requests. We're committed to exceeding your expectations.
+ {pageContent?.content || "Our team is here to help you with any questions about your stay, bookings, or special requests. We're committed to exceeding your expectations."}
+ Note: Contact information (phone, email, address) is now managed centrally in Settings → Company Info.
+ These fields will be displayed across the entire application.
+
+ Note: Contact information (phone, email, address) is now managed centrally in Settings → Company Info.
+ These fields will be displayed across the entire application, including the footer.
+