refactor: migrate templates and static files to self-contained modules
Templates Migration: - Migrate admin templates to modules (tenancy, billing, monitoring, marketplace, etc.) - Migrate vendor templates to modules (tenancy, billing, orders, messaging, etc.) - Migrate storefront templates to modules (catalog, customers, orders, cart, checkout, cms) - Migrate public templates to modules (billing, marketplace, cms) - Keep shared templates in app/templates/ (base.html, errors/, partials/, macros/) - Migrate letzshop partials to marketplace module Static Files Migration: - Migrate admin JS to modules: tenancy (23 files), core (5 files), monitoring (1 file) - Migrate vendor JS to modules: tenancy (4 files), core (2 files) - Migrate shared JS: vendor-selector.js to core, media-picker.js to cms - Migrate storefront JS: storefront-layout.js to core - Keep framework JS in static/ (api-client, utils, money, icons, log-config, lib/) - Update all template references to use module_static paths Naming Consistency: - Rename static/platform/ to static/public/ - Rename app/templates/platform/ to app/templates/public/ - Update all extends and static references Documentation: - Update module-system.md with shared templates documentation - Update frontend-structure.md with new module JS organization Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,11 +2,16 @@
|
||||
"""
|
||||
Core module API routes.
|
||||
|
||||
Admin routes:
|
||||
- /dashboard/* - Admin dashboard and statistics
|
||||
- /settings/* - Platform settings management
|
||||
|
||||
Vendor routes:
|
||||
- /dashboard/* - Dashboard statistics
|
||||
- /settings/* - Vendor settings management
|
||||
"""
|
||||
|
||||
from .admin import admin_router
|
||||
from .vendor import vendor_router
|
||||
|
||||
__all__ = ["vendor_router"]
|
||||
__all__ = ["admin_router", "vendor_router"]
|
||||
|
||||
19
app/modules/core/routes/api/admin.py
Normal file
19
app/modules/core/routes/api/admin.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# app/modules/core/routes/api/admin.py
|
||||
"""
|
||||
Core module admin API routes.
|
||||
|
||||
Aggregates all admin core routes:
|
||||
- /dashboard/* - Admin dashboard and statistics
|
||||
- /settings/* - Platform settings management
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .admin_dashboard import admin_dashboard_router
|
||||
from .admin_settings import admin_settings_router
|
||||
|
||||
admin_router = APIRouter()
|
||||
|
||||
# Aggregate all core admin routes
|
||||
admin_router.include_router(admin_dashboard_router, tags=["admin-dashboard"])
|
||||
admin_router.include_router(admin_settings_router, tags=["admin-settings"])
|
||||
127
app/modules/core/routes/api/admin_dashboard.py
Normal file
127
app/modules/core/routes/api/admin_dashboard.py
Normal file
@@ -0,0 +1,127 @@
|
||||
# app/modules/core/routes/api/admin_dashboard.py
|
||||
"""
|
||||
Admin dashboard and statistics endpoints.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.modules.tenancy.services.admin_service import admin_service
|
||||
from app.modules.analytics.services.stats_service import stats_service
|
||||
from models.schema.auth import UserContext
|
||||
from app.modules.analytics.schemas import (
|
||||
AdminDashboardResponse,
|
||||
ImportStatsResponse,
|
||||
MarketplaceStatsResponse,
|
||||
OrderStatsBasicResponse,
|
||||
PlatformStatsResponse,
|
||||
ProductStatsResponse,
|
||||
StatsResponse,
|
||||
UserStatsResponse,
|
||||
VendorStatsResponse,
|
||||
)
|
||||
|
||||
admin_dashboard_router = APIRouter(prefix="/dashboard")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@admin_dashboard_router.get("", response_model=AdminDashboardResponse)
|
||||
def get_admin_dashboard(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Get admin dashboard with platform statistics (Admin only)."""
|
||||
user_stats = stats_service.get_user_statistics(db)
|
||||
vendor_stats = stats_service.get_vendor_statistics(db)
|
||||
|
||||
return AdminDashboardResponse(
|
||||
platform={
|
||||
"name": "Multi-Tenant Ecommerce Platform",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
users=UserStatsResponse(**user_stats),
|
||||
vendors=VendorStatsResponse(
|
||||
total=vendor_stats.get("total", vendor_stats.get("total_vendors", 0)),
|
||||
verified=vendor_stats.get(
|
||||
"verified", vendor_stats.get("verified_vendors", 0)
|
||||
),
|
||||
pending=vendor_stats.get("pending", vendor_stats.get("pending_vendors", 0)),
|
||||
inactive=vendor_stats.get(
|
||||
"inactive", vendor_stats.get("inactive_vendors", 0)
|
||||
),
|
||||
),
|
||||
recent_vendors=admin_service.get_recent_vendors(db, limit=5),
|
||||
recent_imports=admin_service.get_recent_import_jobs(db, limit=10),
|
||||
)
|
||||
|
||||
|
||||
@admin_dashboard_router.get("/stats", response_model=StatsResponse)
|
||||
def get_comprehensive_stats(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Get comprehensive platform statistics (Admin only)."""
|
||||
stats_data = stats_service.get_comprehensive_stats(db=db)
|
||||
|
||||
return StatsResponse(
|
||||
total_products=stats_data["total_products"],
|
||||
unique_brands=stats_data["unique_brands"],
|
||||
unique_categories=stats_data["unique_categories"],
|
||||
unique_marketplaces=stats_data["unique_marketplaces"],
|
||||
unique_vendors=stats_data["unique_vendors"],
|
||||
total_inventory_entries=stats_data["total_inventory_entries"],
|
||||
total_inventory_quantity=stats_data["total_inventory_quantity"],
|
||||
)
|
||||
|
||||
|
||||
@admin_dashboard_router.get("/stats/marketplace", response_model=list[MarketplaceStatsResponse])
|
||||
def get_marketplace_stats(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Get statistics broken down by marketplace (Admin only)."""
|
||||
marketplace_stats = stats_service.get_marketplace_breakdown_stats(db=db)
|
||||
|
||||
return [
|
||||
MarketplaceStatsResponse(
|
||||
marketplace=stat["marketplace"],
|
||||
total_products=stat["total_products"],
|
||||
unique_vendors=stat["unique_vendors"],
|
||||
unique_brands=stat["unique_brands"],
|
||||
)
|
||||
for stat in marketplace_stats
|
||||
]
|
||||
|
||||
|
||||
@admin_dashboard_router.get("/stats/platform", response_model=PlatformStatsResponse)
|
||||
def get_platform_statistics(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Get comprehensive platform statistics (Admin only)."""
|
||||
user_stats = stats_service.get_user_statistics(db)
|
||||
vendor_stats = stats_service.get_vendor_statistics(db)
|
||||
product_stats = stats_service.get_product_statistics(db)
|
||||
order_stats = stats_service.get_order_statistics(db)
|
||||
import_stats = stats_service.get_import_statistics(db)
|
||||
|
||||
return PlatformStatsResponse(
|
||||
users=UserStatsResponse(**user_stats),
|
||||
vendors=VendorStatsResponse(
|
||||
total=vendor_stats.get("total", vendor_stats.get("total_vendors", 0)),
|
||||
verified=vendor_stats.get(
|
||||
"verified", vendor_stats.get("verified_vendors", 0)
|
||||
),
|
||||
pending=vendor_stats.get("pending", vendor_stats.get("pending_vendors", 0)),
|
||||
inactive=vendor_stats.get(
|
||||
"inactive", vendor_stats.get("inactive_vendors", 0)
|
||||
),
|
||||
),
|
||||
products=ProductStatsResponse(**product_stats),
|
||||
orders=OrderStatsBasicResponse(**order_stats),
|
||||
imports=ImportStatsResponse(**import_stats),
|
||||
)
|
||||
716
app/modules/core/routes/api/admin_settings.py
Normal file
716
app/modules/core/routes/api/admin_settings.py
Normal file
@@ -0,0 +1,716 @@
|
||||
# app/modules/core/routes/api/admin_settings.py
|
||||
"""
|
||||
Platform settings management endpoints.
|
||||
|
||||
Provides endpoints for:
|
||||
- Viewing all platform settings
|
||||
- Creating/updating settings
|
||||
- Managing configuration by category
|
||||
- Email configuration status and testing
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.config import settings as app_settings
|
||||
from app.core.database import get_db
|
||||
from app.exceptions import ResourceNotFoundException
|
||||
from app.modules.tenancy.exceptions import ConfirmationRequiredException
|
||||
from app.modules.monitoring.services.admin_audit_service import admin_audit_service
|
||||
from app.modules.core.services.admin_settings_service import admin_settings_service
|
||||
from models.schema.auth import UserContext
|
||||
from models.schema.admin import (
|
||||
AdminSettingCreate,
|
||||
AdminSettingDefaultResponse,
|
||||
AdminSettingListResponse,
|
||||
AdminSettingResponse,
|
||||
AdminSettingUpdate,
|
||||
PublicDisplaySettingsResponse,
|
||||
RowsPerPageResponse,
|
||||
RowsPerPageUpdateResponse,
|
||||
)
|
||||
|
||||
admin_settings_router = APIRouter(prefix="/settings")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@admin_settings_router.get("", response_model=AdminSettingListResponse)
|
||||
def get_all_settings(
|
||||
category: str | None = Query(None, description="Filter by category"),
|
||||
is_public: bool | None = Query(None, description="Filter by public flag"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Get all platform settings.
|
||||
|
||||
Can be filtered by category (system, security, marketplace, notifications)
|
||||
and by public flag (settings that can be exposed to frontend).
|
||||
"""
|
||||
settings = admin_settings_service.get_all_settings(db, category, is_public)
|
||||
|
||||
return AdminSettingListResponse(
|
||||
settings=settings, total=len(settings), category=category
|
||||
)
|
||||
|
||||
|
||||
@admin_settings_router.get("/categories")
|
||||
def get_setting_categories(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Get list of all setting categories."""
|
||||
# This could be enhanced to return counts per category
|
||||
return {
|
||||
"categories": [
|
||||
"system",
|
||||
"security",
|
||||
"marketplace",
|
||||
"notifications",
|
||||
"integrations",
|
||||
"payments",
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@admin_settings_router.get("/{key}", response_model=AdminSettingResponse | AdminSettingDefaultResponse)
|
||||
def get_setting(
|
||||
key: str,
|
||||
default: str | None = Query(None, description="Default value if setting not found"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> AdminSettingResponse | AdminSettingDefaultResponse:
|
||||
"""Get specific setting by key.
|
||||
|
||||
If `default` is provided and the setting doesn't exist, returns a response
|
||||
with the default value instead of 404.
|
||||
"""
|
||||
setting = admin_settings_service.get_setting_by_key(db, key)
|
||||
|
||||
if not setting:
|
||||
if default is not None:
|
||||
# Return default value without creating the setting
|
||||
return AdminSettingDefaultResponse(key=key, value=default, exists=False)
|
||||
raise ResourceNotFoundException(resource_type="Setting", identifier=key)
|
||||
|
||||
return AdminSettingResponse.model_validate(setting)
|
||||
|
||||
|
||||
@admin_settings_router.post("", response_model=AdminSettingResponse)
|
||||
def create_setting(
|
||||
setting_data: AdminSettingCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Create new platform setting.
|
||||
|
||||
Setting keys should be lowercase with underscores (e.g., max_vendors_allowed).
|
||||
"""
|
||||
result = admin_settings_service.create_setting(
|
||||
db=db, setting_data=setting_data, admin_user_id=current_admin.id
|
||||
)
|
||||
|
||||
# Log action
|
||||
admin_audit_service.log_action(
|
||||
db=db,
|
||||
admin_user_id=current_admin.id,
|
||||
action="create_setting",
|
||||
target_type="setting",
|
||||
target_id=setting_data.key,
|
||||
details={
|
||||
"category": setting_data.category,
|
||||
"value_type": setting_data.value_type,
|
||||
},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@admin_settings_router.put("/{key}", response_model=AdminSettingResponse)
|
||||
def update_setting(
|
||||
key: str,
|
||||
update_data: AdminSettingUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Update existing setting value."""
|
||||
old_value = admin_settings_service.get_setting_value(db, key)
|
||||
|
||||
result = admin_settings_service.update_setting(
|
||||
db=db, key=key, update_data=update_data, admin_user_id=current_admin.id
|
||||
)
|
||||
|
||||
# Log action
|
||||
admin_audit_service.log_action(
|
||||
db=db,
|
||||
admin_user_id=current_admin.id,
|
||||
action="update_setting",
|
||||
target_type="setting",
|
||||
target_id=key,
|
||||
details={"old_value": str(old_value), "new_value": update_data.value},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@admin_settings_router.post("/upsert", response_model=AdminSettingResponse)
|
||||
def upsert_setting(
|
||||
setting_data: AdminSettingCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Create or update setting (upsert).
|
||||
|
||||
If setting exists, updates its value. If not, creates new setting.
|
||||
"""
|
||||
result = admin_settings_service.upsert_setting(
|
||||
db=db, setting_data=setting_data, admin_user_id=current_admin.id
|
||||
)
|
||||
|
||||
# Log action
|
||||
admin_audit_service.log_action(
|
||||
db=db,
|
||||
admin_user_id=current_admin.id,
|
||||
action="upsert_setting",
|
||||
target_type="setting",
|
||||
target_id=setting_data.key,
|
||||
details={"category": setting_data.category},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CONVENIENCE ENDPOINTS FOR COMMON SETTINGS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@admin_settings_router.get("/display/rows-per-page", response_model=RowsPerPageResponse)
|
||||
def get_rows_per_page(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> RowsPerPageResponse:
|
||||
"""Get the platform-wide rows per page setting."""
|
||||
value = admin_settings_service.get_setting_value(db, "rows_per_page", default="20")
|
||||
return RowsPerPageResponse(rows_per_page=int(value))
|
||||
|
||||
|
||||
@admin_settings_router.put("/display/rows-per-page", response_model=RowsPerPageUpdateResponse)
|
||||
def set_rows_per_page(
|
||||
rows: int = Query(..., ge=10, le=100, description="Rows per page (10-100)"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> RowsPerPageUpdateResponse:
|
||||
"""
|
||||
Set the platform-wide rows per page setting.
|
||||
|
||||
Valid values: 10, 20, 50, 100
|
||||
"""
|
||||
valid_values = [10, 20, 50, 100]
|
||||
if rows not in valid_values:
|
||||
# Round to nearest valid value
|
||||
rows = min(valid_values, key=lambda x: abs(x - rows))
|
||||
|
||||
setting_data = AdminSettingCreate(
|
||||
key="rows_per_page",
|
||||
value=str(rows),
|
||||
value_type="integer",
|
||||
category="display",
|
||||
description="Default number of rows per page in admin tables",
|
||||
is_public=True,
|
||||
)
|
||||
|
||||
admin_settings_service.upsert_setting(
|
||||
db=db, setting_data=setting_data, admin_user_id=current_admin.id
|
||||
)
|
||||
|
||||
admin_audit_service.log_action(
|
||||
db=db,
|
||||
admin_user_id=current_admin.id,
|
||||
action="update_setting",
|
||||
target_type="setting",
|
||||
target_id="rows_per_page",
|
||||
details={"value": rows},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return RowsPerPageUpdateResponse(
|
||||
rows_per_page=rows, message="Rows per page setting updated"
|
||||
)
|
||||
|
||||
|
||||
@admin_settings_router.get("/display/public", response_model=PublicDisplaySettingsResponse)
|
||||
def get_public_display_settings(
|
||||
db: Session = Depends(get_db),
|
||||
) -> PublicDisplaySettingsResponse:
|
||||
"""
|
||||
Get public display settings (no auth required).
|
||||
|
||||
Returns settings that can be used by frontend without admin auth.
|
||||
"""
|
||||
rows_per_page = admin_settings_service.get_setting_value(
|
||||
db, "rows_per_page", default="20"
|
||||
)
|
||||
|
||||
return PublicDisplaySettingsResponse(rows_per_page=int(rows_per_page))
|
||||
|
||||
|
||||
@admin_settings_router.delete("/{key}")
|
||||
def delete_setting(
|
||||
key: str,
|
||||
confirm: bool = Query(False, description="Must be true to confirm deletion"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Delete platform setting.
|
||||
|
||||
Requires confirmation parameter.
|
||||
WARNING: Deleting settings may affect platform functionality.
|
||||
"""
|
||||
if not confirm:
|
||||
raise ConfirmationRequiredException(
|
||||
operation="delete_setting",
|
||||
message="Deletion requires confirmation parameter: confirm=true",
|
||||
)
|
||||
|
||||
message = admin_settings_service.delete_setting(
|
||||
db=db, key=key, admin_user_id=current_admin.id
|
||||
)
|
||||
|
||||
# Log action
|
||||
admin_audit_service.log_action(
|
||||
db=db,
|
||||
admin_user_id=current_admin.id,
|
||||
action="delete_setting",
|
||||
target_type="setting",
|
||||
target_id=key,
|
||||
details={},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return {"message": message}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# EMAIL CONFIGURATION ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
# Email setting keys stored in admin_settings table
|
||||
EMAIL_SETTING_KEYS = {
|
||||
"email_provider": "smtp",
|
||||
"email_from_address": "",
|
||||
"email_from_name": "",
|
||||
"email_reply_to": "",
|
||||
"smtp_host": "",
|
||||
"smtp_port": "587",
|
||||
"smtp_user": "",
|
||||
"smtp_password": "",
|
||||
"smtp_use_tls": "true",
|
||||
"smtp_use_ssl": "false",
|
||||
"sendgrid_api_key": "",
|
||||
"mailgun_api_key": "",
|
||||
"mailgun_domain": "",
|
||||
"aws_access_key_id": "",
|
||||
"aws_secret_access_key": "",
|
||||
"aws_region": "eu-west-1",
|
||||
"email_enabled": "true",
|
||||
"email_debug": "false",
|
||||
}
|
||||
|
||||
|
||||
def get_email_setting(db: Session, key: str) -> str | None:
|
||||
"""Get email setting from database, returns None if not set."""
|
||||
setting = admin_settings_service.get_setting_by_key(db, key)
|
||||
return setting.value if setting else None
|
||||
|
||||
|
||||
def get_effective_email_config(db: Session) -> dict:
|
||||
"""
|
||||
Get effective email configuration.
|
||||
|
||||
Priority: Database settings > Environment variables
|
||||
"""
|
||||
config = {}
|
||||
|
||||
# Provider
|
||||
db_provider = get_email_setting(db, "email_provider")
|
||||
config["provider"] = db_provider if db_provider else app_settings.email_provider
|
||||
|
||||
# From settings
|
||||
db_from_email = get_email_setting(db, "email_from_address")
|
||||
config["from_email"] = db_from_email if db_from_email else app_settings.email_from_address
|
||||
|
||||
db_from_name = get_email_setting(db, "email_from_name")
|
||||
config["from_name"] = db_from_name if db_from_name else app_settings.email_from_name
|
||||
|
||||
db_reply_to = get_email_setting(db, "email_reply_to")
|
||||
config["reply_to"] = db_reply_to if db_reply_to else app_settings.email_reply_to
|
||||
|
||||
# SMTP settings
|
||||
db_smtp_host = get_email_setting(db, "smtp_host")
|
||||
config["smtp_host"] = db_smtp_host if db_smtp_host else app_settings.smtp_host
|
||||
|
||||
db_smtp_port = get_email_setting(db, "smtp_port")
|
||||
config["smtp_port"] = int(db_smtp_port) if db_smtp_port else app_settings.smtp_port
|
||||
|
||||
db_smtp_user = get_email_setting(db, "smtp_user")
|
||||
config["smtp_user"] = db_smtp_user if db_smtp_user else app_settings.smtp_user
|
||||
|
||||
db_smtp_password = get_email_setting(db, "smtp_password")
|
||||
config["smtp_password"] = db_smtp_password if db_smtp_password else app_settings.smtp_password
|
||||
|
||||
db_smtp_use_tls = get_email_setting(db, "smtp_use_tls")
|
||||
config["smtp_use_tls"] = db_smtp_use_tls.lower() in ("true", "1", "yes") if db_smtp_use_tls else app_settings.smtp_use_tls
|
||||
|
||||
db_smtp_use_ssl = get_email_setting(db, "smtp_use_ssl")
|
||||
config["smtp_use_ssl"] = db_smtp_use_ssl.lower() in ("true", "1", "yes") if db_smtp_use_ssl else app_settings.smtp_use_ssl
|
||||
|
||||
# SendGrid
|
||||
db_sendgrid_key = get_email_setting(db, "sendgrid_api_key")
|
||||
config["sendgrid_api_key"] = db_sendgrid_key if db_sendgrid_key else app_settings.sendgrid_api_key
|
||||
|
||||
# Mailgun
|
||||
db_mailgun_key = get_email_setting(db, "mailgun_api_key")
|
||||
config["mailgun_api_key"] = db_mailgun_key if db_mailgun_key else app_settings.mailgun_api_key
|
||||
|
||||
db_mailgun_domain = get_email_setting(db, "mailgun_domain")
|
||||
config["mailgun_domain"] = db_mailgun_domain if db_mailgun_domain else app_settings.mailgun_domain
|
||||
|
||||
# AWS SES
|
||||
db_aws_key = get_email_setting(db, "aws_access_key_id")
|
||||
config["aws_access_key_id"] = db_aws_key if db_aws_key else app_settings.aws_access_key_id
|
||||
|
||||
db_aws_secret = get_email_setting(db, "aws_secret_access_key")
|
||||
config["aws_secret_access_key"] = db_aws_secret if db_aws_secret else app_settings.aws_secret_access_key
|
||||
|
||||
db_aws_region = get_email_setting(db, "aws_region")
|
||||
config["aws_region"] = db_aws_region if db_aws_region else app_settings.aws_region
|
||||
|
||||
# Behavior
|
||||
db_enabled = get_email_setting(db, "email_enabled")
|
||||
config["enabled"] = db_enabled.lower() in ("true", "1", "yes") if db_enabled else app_settings.email_enabled
|
||||
|
||||
db_debug = get_email_setting(db, "email_debug")
|
||||
config["debug"] = db_debug.lower() in ("true", "1", "yes") if db_debug else app_settings.email_debug
|
||||
|
||||
# Track source for each field (DB override or .env)
|
||||
config["_sources"] = {}
|
||||
for key in ["provider", "from_email", "from_name", "smtp_host", "smtp_port"]:
|
||||
db_key = "email_provider" if key == "provider" else ("email_from_address" if key == "from_email" else ("email_from_name" if key == "from_name" else key))
|
||||
config["_sources"][key] = "database" if get_email_setting(db, db_key) else "env"
|
||||
|
||||
return config
|
||||
|
||||
|
||||
class EmailStatusResponse(BaseModel):
|
||||
"""Platform email configuration status."""
|
||||
|
||||
provider: str
|
||||
from_email: str
|
||||
from_name: str
|
||||
reply_to: str | None = None
|
||||
smtp_host: str | None = None
|
||||
smtp_port: int | None = None
|
||||
smtp_user: str | None = None
|
||||
mailgun_domain: str | None = None
|
||||
aws_region: str | None = None
|
||||
debug: bool
|
||||
enabled: bool
|
||||
is_configured: bool
|
||||
has_db_overrides: bool = False
|
||||
|
||||
|
||||
class EmailSettingsUpdate(BaseModel):
|
||||
"""Update email settings."""
|
||||
|
||||
provider: str | None = None
|
||||
from_email: EmailStr | None = None
|
||||
from_name: str | None = None
|
||||
reply_to: EmailStr | None = None
|
||||
# SMTP
|
||||
smtp_host: str | None = None
|
||||
smtp_port: int | None = None
|
||||
smtp_user: str | None = None
|
||||
smtp_password: str | None = None
|
||||
smtp_use_tls: bool | None = None
|
||||
smtp_use_ssl: bool | None = None
|
||||
# SendGrid
|
||||
sendgrid_api_key: str | None = None
|
||||
# Mailgun
|
||||
mailgun_api_key: str | None = None
|
||||
mailgun_domain: str | None = None
|
||||
# AWS SES
|
||||
aws_access_key_id: str | None = None
|
||||
aws_secret_access_key: str | None = None
|
||||
aws_region: str | None = None
|
||||
# Behavior
|
||||
enabled: bool | None = None
|
||||
debug: bool | None = None
|
||||
|
||||
|
||||
class TestEmailRequest(BaseModel):
|
||||
"""Request body for test email."""
|
||||
|
||||
to_email: EmailStr
|
||||
|
||||
|
||||
class TestEmailResponse(BaseModel):
|
||||
"""Response for test email."""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
|
||||
|
||||
@admin_settings_router.get("/email/status", response_model=EmailStatusResponse)
|
||||
def get_email_status(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> EmailStatusResponse:
|
||||
"""
|
||||
Get platform email configuration status.
|
||||
|
||||
Returns the effective email configuration (DB overrides > .env).
|
||||
Sensitive values (passwords, API keys) are NOT exposed.
|
||||
"""
|
||||
config = get_effective_email_config(db)
|
||||
provider = config["provider"].lower()
|
||||
|
||||
# Determine if email is configured based on provider
|
||||
is_configured = False
|
||||
if provider == "smtp":
|
||||
is_configured = bool(config["smtp_host"] and config["smtp_host"] != "localhost")
|
||||
elif provider == "sendgrid":
|
||||
is_configured = bool(config["sendgrid_api_key"])
|
||||
elif provider == "mailgun":
|
||||
is_configured = bool(config["mailgun_api_key"] and config["mailgun_domain"])
|
||||
elif provider == "ses":
|
||||
is_configured = bool(config["aws_access_key_id"] and config["aws_secret_access_key"])
|
||||
|
||||
# Check if any DB overrides exist
|
||||
has_db_overrides = any(v == "database" for v in config["_sources"].values())
|
||||
|
||||
return EmailStatusResponse(
|
||||
provider=provider,
|
||||
from_email=config["from_email"],
|
||||
from_name=config["from_name"],
|
||||
reply_to=config["reply_to"] or None,
|
||||
smtp_host=config["smtp_host"] if provider == "smtp" else None,
|
||||
smtp_port=config["smtp_port"] if provider == "smtp" else None,
|
||||
smtp_user=config["smtp_user"] if provider == "smtp" else None,
|
||||
mailgun_domain=config["mailgun_domain"] if provider == "mailgun" else None,
|
||||
aws_region=config["aws_region"] if provider == "ses" else None,
|
||||
debug=config["debug"],
|
||||
enabled=config["enabled"],
|
||||
is_configured=is_configured,
|
||||
has_db_overrides=has_db_overrides,
|
||||
)
|
||||
|
||||
|
||||
@admin_settings_router.put("/email/settings")
|
||||
def update_email_settings(
|
||||
settings_update: EmailSettingsUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Update platform email settings.
|
||||
|
||||
Settings are stored in the database and override .env values.
|
||||
Only non-null values are updated.
|
||||
"""
|
||||
from models.schema.admin import AdminSettingCreate
|
||||
|
||||
updated_keys = []
|
||||
|
||||
# Map request fields to database keys
|
||||
field_mappings = {
|
||||
"provider": ("email_provider", "string"),
|
||||
"from_email": ("email_from_address", "string"),
|
||||
"from_name": ("email_from_name", "string"),
|
||||
"reply_to": ("email_reply_to", "string"),
|
||||
"smtp_host": ("smtp_host", "string"),
|
||||
"smtp_port": ("smtp_port", "integer"),
|
||||
"smtp_user": ("smtp_user", "string"),
|
||||
"smtp_password": ("smtp_password", "string"),
|
||||
"smtp_use_tls": ("smtp_use_tls", "boolean"),
|
||||
"smtp_use_ssl": ("smtp_use_ssl", "boolean"),
|
||||
"sendgrid_api_key": ("sendgrid_api_key", "string"),
|
||||
"mailgun_api_key": ("mailgun_api_key", "string"),
|
||||
"mailgun_domain": ("mailgun_domain", "string"),
|
||||
"aws_access_key_id": ("aws_access_key_id", "string"),
|
||||
"aws_secret_access_key": ("aws_secret_access_key", "string"),
|
||||
"aws_region": ("aws_region", "string"),
|
||||
"enabled": ("email_enabled", "boolean"),
|
||||
"debug": ("email_debug", "boolean"),
|
||||
}
|
||||
|
||||
# Sensitive fields that should be marked as encrypted
|
||||
sensitive_keys = {
|
||||
"smtp_password", "sendgrid_api_key", "mailgun_api_key",
|
||||
"aws_access_key_id", "aws_secret_access_key"
|
||||
}
|
||||
|
||||
for field, (db_key, value_type) in field_mappings.items():
|
||||
value = getattr(settings_update, field, None)
|
||||
if value is not None:
|
||||
# Convert value to string for storage
|
||||
if value_type == "boolean":
|
||||
str_value = "true" if value else "false"
|
||||
elif value_type == "integer":
|
||||
str_value = str(value)
|
||||
else:
|
||||
str_value = str(value)
|
||||
|
||||
# Create or update setting
|
||||
setting_data = AdminSettingCreate(
|
||||
key=db_key,
|
||||
value=str_value,
|
||||
value_type=value_type,
|
||||
category="email",
|
||||
description=f"Email setting: {field}",
|
||||
is_encrypted=db_key in sensitive_keys,
|
||||
is_public=False,
|
||||
)
|
||||
|
||||
admin_settings_service.upsert_setting(db, setting_data, current_admin.id)
|
||||
updated_keys.append(field)
|
||||
|
||||
# Log action
|
||||
admin_audit_service.log_action(
|
||||
db=db,
|
||||
admin_user_id=current_admin.id,
|
||||
action="update_email_settings",
|
||||
target_type="email_settings",
|
||||
target_id="platform",
|
||||
details={"updated_keys": updated_keys},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Email settings updated by admin {current_admin.id}: {updated_keys}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Updated {len(updated_keys)} email setting(s)",
|
||||
"updated_keys": updated_keys,
|
||||
}
|
||||
|
||||
|
||||
@admin_settings_router.delete("/email/settings")
|
||||
def reset_email_settings(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Reset email settings to use .env values.
|
||||
|
||||
Deletes all email settings from the database, reverting to .env configuration.
|
||||
"""
|
||||
deleted_count = 0
|
||||
|
||||
for key in EMAIL_SETTING_KEYS:
|
||||
setting = admin_settings_service.get_setting_by_key(db, key)
|
||||
if setting:
|
||||
# Use service method for deletion (API-002 compliance)
|
||||
admin_settings_service.delete_setting(db, key, current_admin.id)
|
||||
deleted_count += 1
|
||||
|
||||
# Log action
|
||||
admin_audit_service.log_action(
|
||||
db=db,
|
||||
admin_user_id=current_admin.id,
|
||||
action="reset_email_settings",
|
||||
target_type="email_settings",
|
||||
target_id="platform",
|
||||
details={"deleted_count": deleted_count},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Email settings reset by admin {current_admin.id}, deleted {deleted_count} settings")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Reset {deleted_count} email setting(s) to .env defaults",
|
||||
}
|
||||
|
||||
|
||||
@admin_settings_router.post("/email/test", response_model=TestEmailResponse)
|
||||
def send_test_email(
|
||||
request: TestEmailRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
) -> TestEmailResponse:
|
||||
"""
|
||||
Send a test email using the platform email configuration.
|
||||
|
||||
This tests the email provider configuration from environment variables.
|
||||
"""
|
||||
from app.modules.messaging.services.email_service import EmailService
|
||||
|
||||
try:
|
||||
email_service = EmailService(db)
|
||||
|
||||
# Send test email using platform configuration
|
||||
email_log = email_service.send_raw(
|
||||
to_email=request.to_email,
|
||||
to_name=None,
|
||||
subject="Wizamart Platform - Test Email",
|
||||
body_html="""
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; padding: 20px;">
|
||||
<h2 style="color: #6b46c1;">Test Email from Wizamart</h2>
|
||||
<p>This is a test email to verify your platform email configuration.</p>
|
||||
<p>If you received this email, your email settings are working correctly!</p>
|
||||
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 20px 0;">
|
||||
<p style="color: #6b7280; font-size: 12px;">
|
||||
Provider: {provider}<br>
|
||||
From: {from_email}
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
""".format(
|
||||
provider=app_settings.email_provider,
|
||||
from_email=app_settings.email_from_address,
|
||||
),
|
||||
body_text=f"Test email from Wizamart platform.\n\nProvider: {app_settings.email_provider}\nFrom: {app_settings.email_from_address}",
|
||||
is_platform_email=True,
|
||||
)
|
||||
|
||||
# Check if email was actually sent (send_raw returns EmailLog, not boolean)
|
||||
if email_log.status == "sent":
|
||||
# Log action
|
||||
admin_audit_service.log_action(
|
||||
db=db,
|
||||
admin_user_id=current_admin.id,
|
||||
action="send_test_email",
|
||||
target_type="email",
|
||||
target_id=request.to_email,
|
||||
details={"provider": app_settings.email_provider},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return TestEmailResponse(
|
||||
success=True,
|
||||
message=f"Test email sent to {request.to_email}",
|
||||
)
|
||||
else:
|
||||
return TestEmailResponse(
|
||||
success=False,
|
||||
message=email_log.error_message or "Failed to send test email. Check server logs for details.",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send test email: {e}")
|
||||
return TestEmailResponse(
|
||||
success=False,
|
||||
message=f"Error sending test email: {str(e)}",
|
||||
)
|
||||
190
app/modules/core/routes/api/public.py
Normal file
190
app/modules/core/routes/api/public.py
Normal file
@@ -0,0 +1,190 @@
|
||||
# app/modules/core/routes/api/public.py
|
||||
"""
|
||||
Public language API endpoints.
|
||||
|
||||
Handles:
|
||||
- Setting language preference via cookie
|
||||
- Getting current language info
|
||||
- Listing available languages
|
||||
|
||||
All endpoints are public (no authentication required).
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Request, Response
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.utils.i18n import (
|
||||
DEFAULT_LANGUAGE,
|
||||
LANGUAGE_FLAGS,
|
||||
LANGUAGE_NAMES,
|
||||
LANGUAGE_NAMES_EN,
|
||||
SUPPORTED_LANGUAGES,
|
||||
get_language_info,
|
||||
)
|
||||
from middleware.language import LANGUAGE_COOKIE_NAME, set_language_cookie
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/language")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Schemas
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class SetLanguageRequest(BaseModel):
|
||||
"""Request body for setting language preference."""
|
||||
|
||||
language: str = Field(
|
||||
...,
|
||||
description="Language code (en, fr, de, lb)",
|
||||
min_length=2,
|
||||
max_length=5,
|
||||
)
|
||||
|
||||
|
||||
class SetLanguageResponse(BaseModel):
|
||||
"""Response after setting language preference."""
|
||||
|
||||
success: bool
|
||||
language: str
|
||||
message: str
|
||||
|
||||
|
||||
class LanguageInfo(BaseModel):
|
||||
"""Information about a single language."""
|
||||
|
||||
code: str
|
||||
name: str
|
||||
name_en: str
|
||||
flag: str
|
||||
|
||||
|
||||
class LanguageListResponse(BaseModel):
|
||||
"""Response listing all available languages."""
|
||||
|
||||
languages: list[LanguageInfo]
|
||||
current: str
|
||||
default: str
|
||||
|
||||
|
||||
class CurrentLanguageResponse(BaseModel):
|
||||
"""Response with current language information."""
|
||||
|
||||
code: str
|
||||
name: str
|
||||
name_en: str
|
||||
flag: str
|
||||
source: str # Where the language was determined from (cookie, browser, default)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Endpoints
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.post("/set", response_model=SetLanguageResponse) # public
|
||||
async def set_language(
|
||||
request: Request,
|
||||
response: Response,
|
||||
body: SetLanguageRequest,
|
||||
) -> SetLanguageResponse:
|
||||
"""
|
||||
Set the user's language preference.
|
||||
|
||||
This sets a cookie that will be used for subsequent requests.
|
||||
The page should be reloaded after calling this endpoint.
|
||||
"""
|
||||
language = body.language.lower()
|
||||
|
||||
if language not in SUPPORTED_LANGUAGES:
|
||||
return SetLanguageResponse(
|
||||
success=False,
|
||||
language=language,
|
||||
message=f"Unsupported language: {language}. Supported: {', '.join(SUPPORTED_LANGUAGES)}",
|
||||
)
|
||||
|
||||
# Set language cookie
|
||||
set_language_cookie(response, language)
|
||||
|
||||
logger.info(f"Language preference set to: {language}")
|
||||
|
||||
return SetLanguageResponse(
|
||||
success=True,
|
||||
language=language,
|
||||
message=f"Language set to {LANGUAGE_NAMES.get(language, language)}",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/current", response_model=CurrentLanguageResponse) # public
|
||||
async def get_current_language(request: Request) -> CurrentLanguageResponse:
|
||||
"""
|
||||
Get the current language for this request.
|
||||
|
||||
Returns information about the detected language and where it came from.
|
||||
"""
|
||||
# Get language from request state (set by middleware)
|
||||
language = getattr(request.state, "language", DEFAULT_LANGUAGE)
|
||||
language_info = getattr(request.state, "language_info", {})
|
||||
|
||||
# Determine source
|
||||
source = "default"
|
||||
if language_info.get("cookie"):
|
||||
source = "cookie"
|
||||
elif language_info.get("browser"):
|
||||
source = "browser"
|
||||
|
||||
info = get_language_info(language)
|
||||
|
||||
return CurrentLanguageResponse(
|
||||
code=info["code"],
|
||||
name=info["name"],
|
||||
name_en=info["name_en"],
|
||||
flag=info["flag"],
|
||||
source=source,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/list", response_model=LanguageListResponse) # public
|
||||
async def list_languages(request: Request) -> LanguageListResponse:
|
||||
"""
|
||||
List all available languages.
|
||||
|
||||
Returns all supported languages with their display names.
|
||||
"""
|
||||
current = getattr(request.state, "language", DEFAULT_LANGUAGE)
|
||||
|
||||
languages = [
|
||||
LanguageInfo(
|
||||
code=code,
|
||||
name=LANGUAGE_NAMES.get(code, code),
|
||||
name_en=LANGUAGE_NAMES_EN.get(code, code),
|
||||
flag=LANGUAGE_FLAGS.get(code, ""),
|
||||
)
|
||||
for code in SUPPORTED_LANGUAGES
|
||||
]
|
||||
|
||||
return LanguageListResponse(
|
||||
languages=languages,
|
||||
current=current,
|
||||
default=DEFAULT_LANGUAGE,
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/clear") # public
|
||||
async def clear_language(response: Response) -> SetLanguageResponse:
|
||||
"""
|
||||
Clear the language preference cookie.
|
||||
|
||||
After clearing, the language will be determined from browser settings or defaults.
|
||||
"""
|
||||
response.delete_cookie(key=LANGUAGE_COOKIE_NAME)
|
||||
|
||||
return SetLanguageResponse(
|
||||
success=True,
|
||||
language=DEFAULT_LANGUAGE,
|
||||
message="Language preference cleared. Using browser/default language.",
|
||||
)
|
||||
@@ -13,9 +13,9 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from app.exceptions import VendorNotActiveException
|
||||
from app.services.stats_service import stats_service
|
||||
from app.services.vendor_service import vendor_service
|
||||
from app.modules.tenancy.exceptions import VendorNotActiveException
|
||||
from app.modules.analytics.services.stats_service import stats_service
|
||||
from app.modules.tenancy.services.vendor_service import vendor_service
|
||||
from models.schema.auth import UserContext
|
||||
from app.modules.analytics.schemas import (
|
||||
VendorCustomerStats,
|
||||
|
||||
@@ -14,8 +14,8 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from app.services.platform_settings_service import platform_settings_service
|
||||
from app.services.vendor_service import vendor_service
|
||||
from app.modules.core.services.platform_settings_service import platform_settings_service
|
||||
from app.modules.tenancy.services.vendor_service import vendor_service
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
vendor_settings_router = APIRouter(prefix="/settings")
|
||||
|
||||
Reference in New Issue
Block a user