feat: implement /platforms/ URL prefix routing strategy
- Update middleware to use /platforms/{code}/ prefix for dev routing
- Change DEFAULT_PLATFORM_CODE from 'oms' to 'main'
- Add 'main' platform for main marketing site (wizamart.lu)
- Remove hardcoded /oms and /loyalty routes from main.py
- Update platform_pages.py homepage to handle vendor landing pages
URL structure:
- localhost:9999/ → Main marketing site ('main' platform)
- localhost:9999/platforms/oms/ → OMS platform
- localhost:9999/platforms/loyalty/ → Loyalty platform
- oms.lu/ → OMS platform (production)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
431
alembic/versions/z6g7h8i9j0k1_add_main_platform.py
Normal file
431
alembic/versions/z6g7h8i9j0k1_add_main_platform.py
Normal file
@@ -0,0 +1,431 @@
|
||||
"""add main platform for marketing site
|
||||
|
||||
Revision ID: z6g7h8i9j0k1
|
||||
Revises: z5f6g7h8i9j0
|
||||
Create Date: 2026-01-19 14:00:00.000000
|
||||
|
||||
This migration adds the 'main' platform for the main marketing site:
|
||||
1. Inserts main platform record (wizamart.lu)
|
||||
2. Creates platform marketing pages (home, about, faq, pricing, contact)
|
||||
|
||||
The 'main' platform serves as the marketing homepage at:
|
||||
- Development: localhost:9999/ (no /platforms/ prefix)
|
||||
- Production: wizamart.lu/
|
||||
|
||||
All other platforms are accessed via:
|
||||
- Development: localhost:9999/platforms/{code}/
|
||||
- Production: {code}.lu or custom domain
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "z6g7h8i9j0k1"
|
||||
down_revision: Union[str, None] = "z5f6g7h8i9j0"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# =========================================================================
|
||||
# 1. Insert Main Marketing platform
|
||||
# =========================================================================
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
INSERT INTO platforms (code, name, description, domain, path_prefix, default_language,
|
||||
supported_languages, is_active, is_public, theme_config, settings,
|
||||
created_at, updated_at)
|
||||
VALUES ('main', 'Wizamart', 'Main marketing site showcasing all Wizamart platforms',
|
||||
'wizamart.lu', NULL, 'fr', '["fr", "de", "en"]', true, true,
|
||||
'{"primary_color": "#2563EB", "secondary_color": "#3B82F6"}',
|
||||
'{"is_marketing_site": true}',
|
||||
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
""")
|
||||
)
|
||||
|
||||
# Get the Main platform ID
|
||||
result = conn.execute(sa.text("SELECT id FROM platforms WHERE code = 'main'"))
|
||||
main_platform_id = result.fetchone()[0]
|
||||
|
||||
# =========================================================================
|
||||
# 2. Create platform marketing pages (is_platform_page=True)
|
||||
# =========================================================================
|
||||
platform_pages = [
|
||||
{
|
||||
"slug": "home",
|
||||
"title": "Wizamart - E-commerce Solutions for Luxembourg",
|
||||
"content": """<div class="hero-section">
|
||||
<h1>Build Your Business with Wizamart</h1>
|
||||
<p class="lead">All-in-one e-commerce, loyalty, and business solutions for Luxembourg merchants</p>
|
||||
</div>
|
||||
|
||||
<div class="platforms-showcase">
|
||||
<h2>Our Platforms</h2>
|
||||
|
||||
<div class="platform-card">
|
||||
<h3>Wizamart OMS</h3>
|
||||
<p>Order Management System for multi-channel selling. Manage orders, inventory, and fulfillment from one dashboard.</p>
|
||||
<a href="/platforms/oms/" class="btn">Learn More</a>
|
||||
</div>
|
||||
|
||||
<div class="platform-card">
|
||||
<h3>Loyalty+</h3>
|
||||
<p>Customer loyalty platform to reward your customers and increase retention. Points, rewards, and member tiers.</p>
|
||||
<a href="/platforms/loyalty/" class="btn">Learn More</a>
|
||||
</div>
|
||||
|
||||
<div class="platform-card">
|
||||
<h3>Site Builder</h3>
|
||||
<p>Create beautiful websites for your local business. No coding required.</p>
|
||||
<span class="badge">Coming Soon</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="why-wizamart">
|
||||
<h2>Why Choose Wizamart?</h2>
|
||||
<ul>
|
||||
<li><strong>Made for Luxembourg:</strong> Built specifically for Luxembourg businesses with local payment methods, languages, and compliance.</li>
|
||||
<li><strong>All-in-One:</strong> Use our platforms together or separately - they integrate seamlessly.</li>
|
||||
<li><strong>Local Support:</strong> Real support from real people in Luxembourg.</li>
|
||||
</ul>
|
||||
</div>""",
|
||||
"meta_description": "Wizamart offers e-commerce, loyalty, and business solutions for Luxembourg merchants. OMS, Loyalty+, and Site Builder platforms.",
|
||||
"show_in_header": False,
|
||||
"show_in_footer": False,
|
||||
"display_order": 0,
|
||||
},
|
||||
{
|
||||
"slug": "about",
|
||||
"title": "About Wizamart",
|
||||
"content": """<div class="about-page">
|
||||
<h1>About Wizamart</h1>
|
||||
|
||||
<div class="mission">
|
||||
<h2>Our Mission</h2>
|
||||
<p>We're building the tools Luxembourg businesses need to thrive in the digital economy. From order management to customer loyalty, we provide the infrastructure that powers local commerce.</p>
|
||||
</div>
|
||||
|
||||
<div class="story">
|
||||
<h2>Our Story</h2>
|
||||
<p>Wizamart was founded with a simple idea: Luxembourg businesses deserve world-class e-commerce tools that understand their unique needs. Local languages, local payment methods, local compliance - built in from the start.</p>
|
||||
</div>
|
||||
|
||||
<div class="team">
|
||||
<h2>Our Team</h2>
|
||||
<p>We're a team of developers, designers, and business experts based in Luxembourg. We understand the local market because we're part of it.</p>
|
||||
</div>
|
||||
|
||||
<div class="values">
|
||||
<h2>Our Values</h2>
|
||||
<ul>
|
||||
<li><strong>Simplicity:</strong> Powerful tools that are easy to use</li>
|
||||
<li><strong>Reliability:</strong> Your business depends on us - we take that seriously</li>
|
||||
<li><strong>Local First:</strong> Built for Luxembourg, by Luxembourg</li>
|
||||
<li><strong>Innovation:</strong> Always improving, always evolving</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>""",
|
||||
"meta_description": "Learn about Wizamart, the Luxembourg-based company building e-commerce and business solutions for local merchants.",
|
||||
"show_in_header": True,
|
||||
"show_in_footer": True,
|
||||
"display_order": 1,
|
||||
},
|
||||
{
|
||||
"slug": "pricing",
|
||||
"title": "Pricing - Wizamart",
|
||||
"content": """<div class="pricing-page">
|
||||
<h1>Choose Your Platform</h1>
|
||||
<p class="lead">Each platform has its own pricing. Choose the tools your business needs.</p>
|
||||
|
||||
<div class="platform-pricing">
|
||||
<div class="platform-pricing-card">
|
||||
<h3>Wizamart OMS</h3>
|
||||
<p>Order Management System</p>
|
||||
<div class="price-range">From €49/month</div>
|
||||
<ul>
|
||||
<li>Multi-channel order management</li>
|
||||
<li>Inventory tracking</li>
|
||||
<li>Shipping integrations</li>
|
||||
<li>Analytics dashboard</li>
|
||||
</ul>
|
||||
<a href="/platforms/oms/pricing" class="btn">View OMS Pricing</a>
|
||||
</div>
|
||||
|
||||
<div class="platform-pricing-card">
|
||||
<h3>Loyalty+</h3>
|
||||
<p>Customer Loyalty Platform</p>
|
||||
<div class="price-range">From €49/month</div>
|
||||
<ul>
|
||||
<li>Points & rewards system</li>
|
||||
<li>Member tiers</li>
|
||||
<li>Analytics & insights</li>
|
||||
<li>POS integrations</li>
|
||||
</ul>
|
||||
<a href="/platforms/loyalty/pricing" class="btn">View Loyalty+ Pricing</a>
|
||||
</div>
|
||||
|
||||
<div class="platform-pricing-card">
|
||||
<h3>Bundle & Save</h3>
|
||||
<p>Use multiple platforms together</p>
|
||||
<div class="price-range">Save up to 20%</div>
|
||||
<ul>
|
||||
<li>Seamless integration</li>
|
||||
<li>Unified dashboard</li>
|
||||
<li>Single invoice</li>
|
||||
<li>Priority support</li>
|
||||
</ul>
|
||||
<a href="/contact" class="btn">Contact Sales</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>""",
|
||||
"meta_description": "Wizamart pricing for OMS, Loyalty+, and bundled solutions. Plans starting at €49/month.",
|
||||
"show_in_header": True,
|
||||
"show_in_footer": True,
|
||||
"display_order": 2,
|
||||
},
|
||||
{
|
||||
"slug": "faq",
|
||||
"title": "FAQ - Frequently Asked Questions",
|
||||
"content": """<div class="faq-page">
|
||||
<h1>Frequently Asked Questions</h1>
|
||||
|
||||
<div class="faq-section">
|
||||
<h2>General</h2>
|
||||
|
||||
<div class="faq-item">
|
||||
<h3>What is Wizamart?</h3>
|
||||
<p>Wizamart is a suite of business tools for Luxembourg merchants, including order management (OMS), customer loyalty (Loyalty+), and website building (Site Builder).</p>
|
||||
</div>
|
||||
|
||||
<div class="faq-item">
|
||||
<h3>Do I need to use all platforms?</h3>
|
||||
<p>No! Each platform works independently. Use one, two, or all three - whatever fits your business needs.</p>
|
||||
</div>
|
||||
|
||||
<div class="faq-item">
|
||||
<h3>What languages are supported?</h3>
|
||||
<p>All platforms support French, German, and English - the three main languages of Luxembourg.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="faq-section">
|
||||
<h2>Billing & Pricing</h2>
|
||||
|
||||
<div class="faq-item">
|
||||
<h3>Is there a free trial?</h3>
|
||||
<p>Yes! All platforms offer a 14-day free trial with no credit card required.</p>
|
||||
</div>
|
||||
|
||||
<div class="faq-item">
|
||||
<h3>What payment methods do you accept?</h3>
|
||||
<p>We accept credit cards, SEPA direct debit, and bank transfers.</p>
|
||||
</div>
|
||||
|
||||
<div class="faq-item">
|
||||
<h3>Can I cancel anytime?</h3>
|
||||
<p>Yes, you can cancel your subscription at any time. No long-term contracts required.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="faq-section">
|
||||
<h2>Support</h2>
|
||||
|
||||
<div class="faq-item">
|
||||
<h3>How do I get help?</h3>
|
||||
<p>All plans include email support. Professional and Business plans include priority support with faster response times.</p>
|
||||
</div>
|
||||
|
||||
<div class="faq-item">
|
||||
<h3>Do you offer onboarding?</h3>
|
||||
<p>Yes! We offer guided onboarding for all new customers to help you get started quickly.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>""",
|
||||
"meta_description": "Frequently asked questions about Wizamart platforms, pricing, billing, and support.",
|
||||
"show_in_header": True,
|
||||
"show_in_footer": True,
|
||||
"display_order": 3,
|
||||
},
|
||||
{
|
||||
"slug": "contact",
|
||||
"title": "Contact Us - Wizamart",
|
||||
"content": """<div class="contact-page">
|
||||
<h1>Contact Us</h1>
|
||||
<p class="lead">We'd love to hear from you. Get in touch with our team.</p>
|
||||
|
||||
<div class="contact-options">
|
||||
<div class="contact-card">
|
||||
<h3>Sales</h3>
|
||||
<p>Interested in our platforms? Let's talk about how we can help your business.</p>
|
||||
<p><a href="mailto:sales@wizamart.lu">sales@wizamart.lu</a></p>
|
||||
</div>
|
||||
|
||||
<div class="contact-card">
|
||||
<h3>Support</h3>
|
||||
<p>Already a customer? Our support team is here to help.</p>
|
||||
<p><a href="mailto:support@wizamart.lu">support@wizamart.lu</a></p>
|
||||
</div>
|
||||
|
||||
<div class="contact-card">
|
||||
<h3>General Inquiries</h3>
|
||||
<p>For everything else, reach out to our general inbox.</p>
|
||||
<p><a href="mailto:hello@wizamart.lu">hello@wizamart.lu</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="address">
|
||||
<h3>Office</h3>
|
||||
<p>Wizamart S.à r.l.<br>
|
||||
Luxembourg City<br>
|
||||
Luxembourg</p>
|
||||
</div>
|
||||
</div>""",
|
||||
"meta_description": "Contact Wizamart for sales, support, or general inquiries. We're here to help your Luxembourg business succeed.",
|
||||
"show_in_header": True,
|
||||
"show_in_footer": True,
|
||||
"display_order": 4,
|
||||
},
|
||||
{
|
||||
"slug": "terms",
|
||||
"title": "Terms of Service - Wizamart",
|
||||
"content": """<div class="terms-page">
|
||||
<h1>Terms of Service</h1>
|
||||
<p class="last-updated">Last updated: January 2026</p>
|
||||
|
||||
<p>These Terms of Service govern your use of Wizamart platforms and services.</p>
|
||||
|
||||
<h2>1. Acceptance of Terms</h2>
|
||||
<p>By accessing or using our services, you agree to be bound by these Terms.</p>
|
||||
|
||||
<h2>2. Services</h2>
|
||||
<p>Wizamart provides e-commerce and business management tools including order management, loyalty programs, and website building services.</p>
|
||||
|
||||
<h2>3. Account Registration</h2>
|
||||
<p>You must provide accurate information when creating an account and keep your login credentials secure.</p>
|
||||
|
||||
<h2>4. Fees and Payment</h2>
|
||||
<p>Subscription fees are billed in advance on a monthly or annual basis. Prices are listed in EUR and include applicable VAT for Luxembourg customers.</p>
|
||||
|
||||
<h2>5. Data Protection</h2>
|
||||
<p>We process personal data in accordance with our Privacy Policy and applicable data protection laws including GDPR.</p>
|
||||
|
||||
<h2>6. Limitation of Liability</h2>
|
||||
<p>Our liability is limited to the amount paid for services in the 12 months preceding any claim.</p>
|
||||
|
||||
<h2>7. Governing Law</h2>
|
||||
<p>These Terms are governed by Luxembourg law. Disputes shall be resolved in Luxembourg courts.</p>
|
||||
|
||||
<h2>8. Contact</h2>
|
||||
<p>For questions about these Terms, contact us at legal@wizamart.lu</p>
|
||||
</div>""",
|
||||
"meta_description": "Wizamart Terms of Service. Read the terms and conditions for using our e-commerce and business platforms.",
|
||||
"show_in_header": False,
|
||||
"show_in_footer": True,
|
||||
"show_in_legal": True,
|
||||
"display_order": 10,
|
||||
},
|
||||
{
|
||||
"slug": "privacy",
|
||||
"title": "Privacy Policy - Wizamart",
|
||||
"content": """<div class="privacy-page">
|
||||
<h1>Privacy Policy</h1>
|
||||
<p class="last-updated">Last updated: January 2026</p>
|
||||
|
||||
<h2>Introduction</h2>
|
||||
<p>Wizamart S.à r.l. ("we", "us") is committed to protecting your privacy. This policy explains how we collect, use, and protect your personal data.</p>
|
||||
|
||||
<h2>Data Controller</h2>
|
||||
<p>Wizamart S.à r.l.<br>Luxembourg City, Luxembourg<br>Email: privacy@wizamart.lu</p>
|
||||
|
||||
<h2>Data We Collect</h2>
|
||||
<ul>
|
||||
<li>Account information (name, email, company details)</li>
|
||||
<li>Usage data (how you use our platforms)</li>
|
||||
<li>Payment information (processed by our payment providers)</li>
|
||||
<li>Support communications</li>
|
||||
</ul>
|
||||
|
||||
<h2>How We Use Your Data</h2>
|
||||
<ul>
|
||||
<li>To provide and improve our services</li>
|
||||
<li>To process payments and billing</li>
|
||||
<li>To communicate with you about your account</li>
|
||||
<li>To send marketing communications (with your consent)</li>
|
||||
</ul>
|
||||
|
||||
<h2>Your Rights</h2>
|
||||
<p>Under GDPR, you have the right to:</p>
|
||||
<ul>
|
||||
<li>Access your personal data</li>
|
||||
<li>Rectify inaccurate data</li>
|
||||
<li>Request deletion of your data</li>
|
||||
<li>Data portability</li>
|
||||
<li>Object to processing</li>
|
||||
</ul>
|
||||
|
||||
<h2>Contact</h2>
|
||||
<p>To exercise your rights or ask questions, contact our Data Protection Officer at privacy@wizamart.lu</p>
|
||||
</div>""",
|
||||
"meta_description": "Wizamart Privacy Policy. Learn how we collect, use, and protect your personal data in compliance with GDPR.",
|
||||
"show_in_header": False,
|
||||
"show_in_footer": True,
|
||||
"show_in_legal": True,
|
||||
"display_order": 11,
|
||||
},
|
||||
]
|
||||
|
||||
for page in platform_pages:
|
||||
show_in_legal = page.get("show_in_legal", False)
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
INSERT INTO content_pages (platform_id, vendor_id, slug, title, content, content_format,
|
||||
meta_description, is_published, is_platform_page,
|
||||
show_in_header, show_in_footer, show_in_legal, display_order,
|
||||
created_at, updated_at)
|
||||
VALUES (:platform_id, NULL, :slug, :title, :content, 'html',
|
||||
:meta_description, true, true,
|
||||
:show_in_header, :show_in_footer, :show_in_legal, :display_order,
|
||||
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
"""),
|
||||
{
|
||||
"platform_id": main_platform_id,
|
||||
"slug": page["slug"],
|
||||
"title": page["title"],
|
||||
"content": page["content"],
|
||||
"meta_description": page["meta_description"],
|
||||
"show_in_header": page["show_in_header"],
|
||||
"show_in_footer": page["show_in_footer"],
|
||||
"show_in_legal": show_in_legal,
|
||||
"display_order": page["display_order"],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# Get the Main platform ID
|
||||
result = conn.execute(sa.text("SELECT id FROM platforms WHERE code = 'main'"))
|
||||
row = result.fetchone()
|
||||
if row:
|
||||
main_platform_id = row[0]
|
||||
|
||||
# Delete all content pages for main platform
|
||||
conn.execute(
|
||||
sa.text("DELETE FROM content_pages WHERE platform_id = :platform_id"),
|
||||
{"platform_id": main_platform_id}
|
||||
)
|
||||
|
||||
# Delete vendor_platforms entries for main (if any)
|
||||
conn.execute(
|
||||
sa.text("DELETE FROM vendor_platforms WHERE platform_id = :platform_id"),
|
||||
{"platform_id": main_platform_id}
|
||||
)
|
||||
|
||||
# Delete main platform
|
||||
conn.execute(sa.text("DELETE FROM platforms WHERE code = 'main'"))
|
||||
@@ -90,16 +90,102 @@ async def homepage(
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Platform marketing homepage.
|
||||
Homepage handler.
|
||||
|
||||
Displays:
|
||||
- Hero section with value proposition
|
||||
- Pricing tier cards
|
||||
- Add-ons section
|
||||
- Letzshop vendor finder
|
||||
- Call to action for signup
|
||||
Handles two scenarios:
|
||||
1. Vendor on custom domain (vendor.com) → Show vendor landing page or redirect to shop
|
||||
2. Platform marketing site → Show platform homepage from CMS or default template
|
||||
|
||||
URL routing:
|
||||
- localhost:9999/ → Main marketing site ('main' platform)
|
||||
- localhost:9999/platforms/oms/ → OMS platform (middleware rewrites to /)
|
||||
- oms.lu/ → OMS platform (domain-based)
|
||||
- shop.mycompany.com/ → Vendor landing page (custom domain)
|
||||
"""
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
# Get platform and vendor from middleware
|
||||
platform = getattr(request.state, "platform", None)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
# Scenario 1: Vendor detected (custom domain like vendor.com)
|
||||
if vendor:
|
||||
logger.debug(f"[HOMEPAGE] Vendor detected: {vendor.subdomain}")
|
||||
|
||||
# Get platform_id (use platform from context or default to 1 for OMS)
|
||||
platform_id = platform.id if platform else 1
|
||||
|
||||
# Try to find vendor landing page (slug='landing' or 'home')
|
||||
landing_page = content_page_service.get_page_for_vendor(
|
||||
db, platform_id=platform_id, slug="landing", vendor_id=vendor.id, include_unpublished=False
|
||||
)
|
||||
|
||||
if not landing_page:
|
||||
landing_page = content_page_service.get_page_for_vendor(
|
||||
db, platform_id=platform_id, slug="home", vendor_id=vendor.id, include_unpublished=False
|
||||
)
|
||||
|
||||
if landing_page:
|
||||
# Render landing page with selected template
|
||||
from app.routes.shop_pages import get_shop_context
|
||||
|
||||
template_name = landing_page.template or "default"
|
||||
template_path = f"vendor/landing-{template_name}.html"
|
||||
|
||||
logger.info(f"[HOMEPAGE] Rendering vendor landing page: {template_path}")
|
||||
return templates.TemplateResponse(
|
||||
template_path, get_shop_context(request, db=db, page=landing_page)
|
||||
)
|
||||
|
||||
# No landing page - redirect to shop
|
||||
vendor_context = getattr(request.state, "vendor_context", None)
|
||||
access_method = (
|
||||
vendor_context.get("detection_method", "unknown")
|
||||
if vendor_context
|
||||
else "unknown"
|
||||
)
|
||||
|
||||
if access_method == "path":
|
||||
full_prefix = (
|
||||
vendor_context.get("full_prefix", "/vendor/")
|
||||
if vendor_context
|
||||
else "/vendor/"
|
||||
)
|
||||
return RedirectResponse(
|
||||
url=f"{full_prefix}{vendor.subdomain}/shop/", status_code=302
|
||||
)
|
||||
# Domain/subdomain - redirect to /shop/
|
||||
return RedirectResponse(url="/shop/", status_code=302)
|
||||
|
||||
# Scenario 2: Platform marketing site (no vendor)
|
||||
# Try to load platform homepage from CMS
|
||||
platform_id = platform.id if platform else 1
|
||||
|
||||
cms_homepage = content_page_service.get_platform_page(
|
||||
db, platform_id=platform_id, slug="home", include_unpublished=False
|
||||
)
|
||||
|
||||
if not cms_homepage:
|
||||
cms_homepage = content_page_service.get_platform_page(
|
||||
db, platform_id=platform_id, slug="platform_homepage", include_unpublished=False
|
||||
)
|
||||
|
||||
if cms_homepage:
|
||||
# Use CMS-based homepage with template selection
|
||||
context = get_platform_context(request, db)
|
||||
context["page"] = cms_homepage
|
||||
context["platform"] = platform
|
||||
|
||||
template_name = cms_homepage.template or "default"
|
||||
template_path = f"platform/homepage-{template_name}.html"
|
||||
|
||||
logger.info(f"[HOMEPAGE] Rendering CMS homepage with template: {template_path}")
|
||||
return templates.TemplateResponse(template_path, context)
|
||||
|
||||
# Fallback: Default wizamart homepage (no CMS content)
|
||||
logger.info("[HOMEPAGE] No CMS homepage found, using default wizamart template")
|
||||
context = get_platform_context(request, db)
|
||||
context["platform"] = platform
|
||||
|
||||
# Fetch tiers for display (use API service internally)
|
||||
from models.database.subscription import TIER_LIMITS, TierCode
|
||||
|
||||
402
main.py
402
main.py
@@ -373,320 +373,29 @@ async def vendor_root_path(
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PLATFORM PUBLIC PAGES (Platform Homepage, About, FAQ, etc.)
|
||||
# PLATFORM ROUTING (via PlatformContextMiddleware)
|
||||
#
|
||||
# The PlatformContextMiddleware handles all platform routing by rewriting paths:
|
||||
# - /platforms/oms/ → rewrites to / (served by platform_pages router)
|
||||
# - /platforms/oms/pricing → rewrites to /pricing (served by platform_pages router)
|
||||
# - /platforms/loyalty/ → rewrites to / (served by platform_pages router)
|
||||
#
|
||||
# The middleware also sets request.state.platform so handlers know which
|
||||
# platform's content to serve. All platform page routes are defined in
|
||||
# app/routes/platform_pages.py which is included above.
|
||||
#
|
||||
# URL Structure (Development - localhost:9999):
|
||||
# - / → Main marketing site ('main' platform)
|
||||
# - /about → Main marketing site about page
|
||||
# - /platforms/oms/ → OMS platform homepage
|
||||
# - /platforms/oms/pricing → OMS platform pricing page
|
||||
# - /platforms/loyalty/ → Loyalty platform homepage
|
||||
#
|
||||
# URL Structure (Production - domain-based):
|
||||
# - wizamart.lu/ → Main marketing site
|
||||
# - oms.lu/ → OMS platform homepage
|
||||
# - loyalty.lu/ → Loyalty platform homepage
|
||||
# ============================================================================
|
||||
logger.info("Registering platform public page routes:")
|
||||
logger.info(" - / (platform homepage)")
|
||||
logger.info(" - /{platform_code}/ (platform-prefixed homepage for dev mode)")
|
||||
logger.info(" - /{platform_code}/{slug} (platform-prefixed content pages)")
|
||||
logger.info(" - /{slug} (platform content pages: /about, /faq, /terms, /contact)")
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def platform_homepage(request: Request, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Platform homepage at localhost:8000 or platform.com
|
||||
|
||||
Uses multi-platform CMS with three-tier resolution:
|
||||
1. Platform marketing pages (is_platform_page=True)
|
||||
2. Vendor default pages (fallback)
|
||||
3. Vendor override pages
|
||||
|
||||
Falls back to default static template if not found.
|
||||
"""
|
||||
from app.services.content_page_service import content_page_service
|
||||
|
||||
logger.debug("[PLATFORM] Homepage requested")
|
||||
|
||||
# Get platform from middleware (multi-platform support)
|
||||
platform = getattr(request.state, "platform", None)
|
||||
|
||||
if platform:
|
||||
# Try to load platform homepage from CMS (platform marketing page)
|
||||
homepage = content_page_service.get_platform_page(
|
||||
db,
|
||||
platform_id=platform.id,
|
||||
slug="home",
|
||||
include_unpublished=False,
|
||||
)
|
||||
|
||||
# Also try platform_homepage slug for backwards compatibility
|
||||
if not homepage:
|
||||
homepage = content_page_service.get_platform_page(
|
||||
db,
|
||||
platform_id=platform.id,
|
||||
slug="platform_homepage",
|
||||
include_unpublished=False,
|
||||
)
|
||||
|
||||
# Load header and footer navigation (platform marketing pages)
|
||||
header_pages = content_page_service.list_platform_pages(
|
||||
db, platform_id=platform.id, header_only=True, include_unpublished=False
|
||||
)
|
||||
|
||||
footer_pages = content_page_service.list_platform_pages(
|
||||
db, platform_id=platform.id, footer_only=True, include_unpublished=False
|
||||
)
|
||||
else:
|
||||
# Fallback for when no platform context (shouldn't happen normally)
|
||||
homepage = None
|
||||
header_pages = []
|
||||
footer_pages = []
|
||||
|
||||
# Get language from request state and build i18n context
|
||||
language = getattr(request.state, "language", "fr")
|
||||
i18n_globals = get_jinja2_globals(language)
|
||||
|
||||
if homepage:
|
||||
# Use template selection from CMS
|
||||
template_name = homepage.template or "default"
|
||||
template_path = f"platform/homepage-{template_name}.html"
|
||||
|
||||
logger.info(f"[PLATFORM] Rendering CMS homepage with template: {template_path}")
|
||||
|
||||
context = {
|
||||
"request": request,
|
||||
"page": homepage,
|
||||
"header_pages": header_pages,
|
||||
"footer_pages": footer_pages,
|
||||
}
|
||||
context.update(i18n_globals)
|
||||
|
||||
return templates.TemplateResponse(template_path, context)
|
||||
|
||||
# Fallback to default static template
|
||||
logger.info("[PLATFORM] No CMS homepage found, using default template")
|
||||
|
||||
context = {
|
||||
"request": request,
|
||||
"header_pages": header_pages,
|
||||
"footer_pages": footer_pages,
|
||||
}
|
||||
context.update(i18n_globals)
|
||||
|
||||
return templates.TemplateResponse("platform/homepage-default.html", context)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PLATFORM-PREFIXED ROUTES (Development mode: /oms/, /loyalty/, etc.)
|
||||
# These routes handle path-based platform routing in development.
|
||||
# In production, domain-based routing means these won't be needed.
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def _serve_platform_homepage(request: Request, platform_code: str, db: Session):
|
||||
"""Helper function to serve platform homepage."""
|
||||
from app.services.content_page_service import content_page_service
|
||||
from models.database.platform import Platform
|
||||
|
||||
# Get platform from middleware or query directly
|
||||
platform = getattr(request.state, "platform", None)
|
||||
if not platform:
|
||||
platform = db.query(Platform).filter(
|
||||
Platform.code == platform_code.lower(),
|
||||
Platform.is_active == True
|
||||
).first()
|
||||
|
||||
if not platform:
|
||||
raise HTTPException(status_code=404, detail=f"Platform not found: {platform_code}")
|
||||
|
||||
# Load platform homepage from CMS
|
||||
homepage = content_page_service.get_platform_page(
|
||||
db, platform_id=platform.id, slug="home", include_unpublished=False
|
||||
)
|
||||
if not homepage:
|
||||
homepage = content_page_service.get_platform_page(
|
||||
db, platform_id=platform.id, slug="platform_homepage", include_unpublished=False
|
||||
)
|
||||
|
||||
# Load navigation
|
||||
header_pages = content_page_service.list_platform_pages(
|
||||
db, platform_id=platform.id, header_only=True, include_unpublished=False
|
||||
)
|
||||
footer_pages = content_page_service.list_platform_pages(
|
||||
db, platform_id=platform.id, footer_only=True, include_unpublished=False
|
||||
)
|
||||
|
||||
# Build context
|
||||
language = getattr(request.state, "language", "fr")
|
||||
i18n_globals = get_jinja2_globals(language)
|
||||
|
||||
if homepage:
|
||||
template_name = homepage.template or "default"
|
||||
template_path = f"platform/homepage-{template_name}.html"
|
||||
logger.info(f"[PLATFORM] Rendering {platform.code} homepage: {template_path}")
|
||||
|
||||
context = {
|
||||
"request": request,
|
||||
"page": homepage,
|
||||
"platform": platform,
|
||||
"header_pages": header_pages,
|
||||
"footer_pages": footer_pages,
|
||||
}
|
||||
context.update(i18n_globals)
|
||||
return templates.TemplateResponse(template_path, context)
|
||||
|
||||
# Fallback
|
||||
logger.info(f"[PLATFORM] No CMS homepage for {platform.code}, using default")
|
||||
context = {
|
||||
"request": request,
|
||||
"platform": platform,
|
||||
"header_pages": header_pages,
|
||||
"footer_pages": footer_pages,
|
||||
}
|
||||
context.update(i18n_globals)
|
||||
return templates.TemplateResponse("platform/homepage-default.html", context)
|
||||
|
||||
|
||||
async def _serve_platform_content_page(request: Request, platform_code: str, slug: str, db: Session):
|
||||
"""Helper function to serve platform content page."""
|
||||
from app.services.content_page_service import content_page_service
|
||||
from models.database.platform import Platform
|
||||
|
||||
# Get platform
|
||||
platform = getattr(request.state, "platform", None)
|
||||
if not platform:
|
||||
platform = db.query(Platform).filter(
|
||||
Platform.code == platform_code.lower(),
|
||||
Platform.is_active == True
|
||||
).first()
|
||||
|
||||
if not platform:
|
||||
raise HTTPException(status_code=404, detail=f"Platform not found: {platform_code}")
|
||||
|
||||
# Load content page
|
||||
page = content_page_service.get_platform_page(
|
||||
db, platform_id=platform.id, slug=slug, include_unpublished=False
|
||||
)
|
||||
if not page:
|
||||
logger.warning(f"[PLATFORM] Page not found: /{platform_code}/{slug}")
|
||||
raise HTTPException(status_code=404, detail=f"Page not found: {slug}")
|
||||
|
||||
# Load navigation
|
||||
header_pages = content_page_service.list_platform_pages(
|
||||
db, platform_id=platform.id, header_only=True, include_unpublished=False
|
||||
)
|
||||
footer_pages = content_page_service.list_platform_pages(
|
||||
db, platform_id=platform.id, footer_only=True, include_unpublished=False
|
||||
)
|
||||
|
||||
logger.info(f"[PLATFORM] Rendering {platform.code} page: {page.title} (/{slug})")
|
||||
|
||||
language = getattr(request.state, "language", "fr")
|
||||
i18n_globals = get_jinja2_globals(language)
|
||||
|
||||
context = {
|
||||
"request": request,
|
||||
"page": page,
|
||||
"platform": platform,
|
||||
"header_pages": header_pages,
|
||||
"footer_pages": footer_pages,
|
||||
}
|
||||
context.update(i18n_globals)
|
||||
return templates.TemplateResponse("platform/content-page.html", context)
|
||||
|
||||
|
||||
# Explicit routes for OMS platform
|
||||
@app.get("/oms", response_class=RedirectResponse, include_in_schema=False)
|
||||
async def oms_redirect():
|
||||
"""Redirect /oms to /oms/"""
|
||||
return RedirectResponse(url="/oms/", status_code=307)
|
||||
|
||||
|
||||
@app.get("/oms/", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def oms_homepage(request: Request, db: Session = Depends(get_db)):
|
||||
"""OMS platform homepage"""
|
||||
return await _serve_platform_homepage(request, "oms", db)
|
||||
|
||||
|
||||
@app.get("/oms/{slug}", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def oms_content_page(request: Request, slug: str, db: Session = Depends(get_db)):
|
||||
"""OMS platform content pages"""
|
||||
return await _serve_platform_content_page(request, "oms", slug, db)
|
||||
|
||||
|
||||
# Explicit routes for Loyalty platform
|
||||
@app.get("/loyalty", response_class=RedirectResponse, include_in_schema=False)
|
||||
async def loyalty_redirect():
|
||||
"""Redirect /loyalty to /loyalty/"""
|
||||
return RedirectResponse(url="/loyalty/", status_code=307)
|
||||
|
||||
|
||||
@app.get("/loyalty/", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def loyalty_homepage(request: Request, db: Session = Depends(get_db)):
|
||||
"""Loyalty platform homepage"""
|
||||
return await _serve_platform_homepage(request, "loyalty", db)
|
||||
|
||||
|
||||
@app.get("/loyalty/{slug}", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def loyalty_content_page(request: Request, slug: str, db: Session = Depends(get_db)):
|
||||
"""Loyalty platform content pages"""
|
||||
return await _serve_platform_content_page(request, "loyalty", slug, db)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# GENERIC PLATFORM CONTENT PAGE (must be LAST - catches all /{slug})
|
||||
# ============================================================================
|
||||
|
||||
@app.get("/{slug}", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def platform_content_page(
|
||||
request: Request, slug: str, db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Platform content pages: /about, /faq, /terms, /contact, etc.
|
||||
|
||||
Uses multi-platform CMS with three-tier resolution.
|
||||
Returns 404 if page not found.
|
||||
|
||||
This route MUST be defined LAST to avoid conflicts with other routes.
|
||||
"""
|
||||
from app.services.content_page_service import content_page_service
|
||||
|
||||
logger.debug(f"[PLATFORM] Content page requested: /{slug}")
|
||||
|
||||
# Get platform from middleware (multi-platform support)
|
||||
platform = getattr(request.state, "platform", None)
|
||||
|
||||
if not platform:
|
||||
logger.warning(f"[PLATFORM] No platform context for content page: {slug}")
|
||||
raise HTTPException(status_code=404, detail=f"Page not found: {slug}")
|
||||
|
||||
# Load platform marketing page from CMS
|
||||
page = content_page_service.get_platform_page(
|
||||
db,
|
||||
platform_id=platform.id,
|
||||
slug=slug,
|
||||
include_unpublished=False,
|
||||
)
|
||||
|
||||
if not page:
|
||||
logger.warning(f"[PLATFORM] Content page not found: {slug}")
|
||||
raise HTTPException(status_code=404, detail=f"Page not found: {slug}")
|
||||
|
||||
# Load header and footer navigation (platform marketing pages)
|
||||
header_pages = content_page_service.list_platform_pages(
|
||||
db, platform_id=platform.id, header_only=True, include_unpublished=False
|
||||
)
|
||||
|
||||
footer_pages = content_page_service.list_platform_pages(
|
||||
db, platform_id=platform.id, footer_only=True, include_unpublished=False
|
||||
)
|
||||
|
||||
logger.info(f"[PLATFORM] Rendering content page: {page.title} (/{slug})")
|
||||
|
||||
# Get language from request state and build i18n context
|
||||
language = getattr(request.state, "language", "fr")
|
||||
i18n_globals = get_jinja2_globals(language)
|
||||
|
||||
context = {
|
||||
"request": request,
|
||||
"page": page,
|
||||
"header_pages": header_pages,
|
||||
"footer_pages": footer_pages,
|
||||
}
|
||||
context.update(i18n_globals)
|
||||
|
||||
return templates.TemplateResponse("platform/content-page.html", context)
|
||||
|
||||
|
||||
logger.info("=" * 80)
|
||||
@@ -701,73 +410,6 @@ for route in app.routes:
|
||||
logger.info(f" {methods:<10} {route.path:<60}")
|
||||
logger.info("=" * 80)
|
||||
|
||||
# ============================================================================
|
||||
# API ROUTES (JSON Responses)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
# Public Routes (no authentication required)
|
||||
@app.get("/", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def root(request: Request, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Smart root handler:
|
||||
- If vendor detected (domain/subdomain): Show vendor landing page or redirect to shop
|
||||
- If no vendor (platform root): Redirect to documentation
|
||||
"""
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
platform = getattr(request.state, "platform", None)
|
||||
|
||||
if vendor:
|
||||
# Vendor context detected - serve landing page
|
||||
from app.services.content_page_service import content_page_service
|
||||
|
||||
# Get platform_id (use platform from context or default to 1 for OMS)
|
||||
platform_id = platform.id if platform else 1
|
||||
|
||||
# Try to find landing page (slug='landing' or 'home') with three-tier resolution
|
||||
landing_page = content_page_service.get_page_for_vendor(
|
||||
db, platform_id=platform_id, slug="landing", vendor_id=vendor.id, include_unpublished=False
|
||||
)
|
||||
|
||||
if not landing_page:
|
||||
# Try 'home' slug as fallback
|
||||
landing_page = content_page_service.get_page_for_vendor(
|
||||
db, platform_id=platform_id, slug="home", vendor_id=vendor.id, include_unpublished=False
|
||||
)
|
||||
|
||||
if landing_page:
|
||||
# Render landing page with selected template
|
||||
from app.routes.shop_pages import get_shop_context
|
||||
|
||||
template_name = landing_page.template or "default"
|
||||
template_path = f"vendor/landing-{template_name}.html"
|
||||
|
||||
return templates.TemplateResponse(
|
||||
template_path, get_shop_context(request, db=db, page=landing_page)
|
||||
)
|
||||
# No landing page - redirect to shop
|
||||
vendor_context = getattr(request.state, "vendor_context", None)
|
||||
access_method = (
|
||||
vendor_context.get("detection_method", "unknown")
|
||||
if vendor_context
|
||||
else "unknown"
|
||||
)
|
||||
|
||||
if access_method == "path":
|
||||
full_prefix = (
|
||||
vendor_context.get("full_prefix", "/vendor/")
|
||||
if vendor_context
|
||||
else "/vendor/"
|
||||
)
|
||||
return RedirectResponse(
|
||||
url=f"{full_prefix}{vendor.subdomain}/shop/", status_code=302
|
||||
)
|
||||
# Domain/subdomain
|
||||
return RedirectResponse(url="/shop/", status_code=302)
|
||||
# No vendor - platform root
|
||||
return RedirectResponse(url="/documentation")
|
||||
|
||||
|
||||
@app.get("/documentation", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def documentation():
|
||||
"""Redirect to documentation"""
|
||||
|
||||
@@ -7,7 +7,11 @@ This middleware runs BEFORE VendorContextMiddleware to establish platform contex
|
||||
|
||||
Handles two routing modes:
|
||||
1. Production: Domain-based (oms.lu, loyalty.lu → Platform detection)
|
||||
2. Development: Path-based (localhost:9999/oms/*, localhost:9999/loyalty/* → Platform detection)
|
||||
2. Development: Path-based (localhost:9999/platforms/oms/*, localhost:9999/platforms/loyalty/*)
|
||||
|
||||
URL Structure:
|
||||
- Main marketing site: localhost:9999/ (no platform prefix) → uses 'main' platform
|
||||
- Platform sites: localhost:9999/platforms/{code}/ → uses specific platform
|
||||
|
||||
Also provides platform_clean_path for downstream middleware to use.
|
||||
"""
|
||||
@@ -16,7 +20,7 @@ import logging
|
||||
|
||||
from fastapi import Request
|
||||
from sqlalchemy.orm import Session
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
# Note: We use pure ASGI middleware (not BaseHTTPMiddleware) to enable path rewriting
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.database import get_db
|
||||
@@ -24,8 +28,8 @@ from models.database.platform import Platform
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Default platform code for backward compatibility
|
||||
DEFAULT_PLATFORM_CODE = "oms"
|
||||
# Default platform code for main marketing site
|
||||
DEFAULT_PLATFORM_CODE = "main"
|
||||
|
||||
|
||||
class PlatformContextManager:
|
||||
@@ -38,8 +42,15 @@ class PlatformContextManager:
|
||||
|
||||
Priority order:
|
||||
1. Domain-based (production): oms.lu → platform code "oms"
|
||||
2. Path-based (development): localhost:9999/oms/* → platform code "oms"
|
||||
3. Default: localhost without path prefix → default platform
|
||||
2. Path-based (development): localhost:9999/platforms/oms/* → platform code "oms"
|
||||
3. Default: localhost without /platforms/ prefix → 'main' platform (marketing site)
|
||||
|
||||
URL Structure:
|
||||
- / → Main marketing site ('main' platform)
|
||||
- /about → Main marketing site about page
|
||||
- /platforms/oms/ → OMS platform homepage
|
||||
- /platforms/oms/pricing → OMS platform pricing
|
||||
- /platforms/loyalty/ → Loyalty platform homepage
|
||||
|
||||
Returns dict with platform info or None if not detected.
|
||||
"""
|
||||
@@ -75,42 +86,33 @@ class PlatformContextManager:
|
||||
"original_path": path,
|
||||
}
|
||||
|
||||
# Method 2: Path-based detection (development)
|
||||
# Check for path prefix like /oms/, /loyalty/
|
||||
if path.startswith("/"):
|
||||
path_parts = path[1:].split("/") # Remove leading / and split
|
||||
if path_parts and path_parts[0]:
|
||||
potential_platform_code = path_parts[0].lower()
|
||||
# Check if this could be a platform code (not vendor paths)
|
||||
if potential_platform_code not in [
|
||||
"vendor",
|
||||
"vendors",
|
||||
"admin",
|
||||
"api",
|
||||
"static",
|
||||
"media",
|
||||
"assets",
|
||||
"health",
|
||||
"docs",
|
||||
"redoc",
|
||||
"openapi.json",
|
||||
]:
|
||||
# Method 2: Path-based detection (development) - ONLY for /platforms/ prefix
|
||||
# Check for path prefix like /platforms/oms/, /platforms/loyalty/
|
||||
if path.startswith("/platforms/"):
|
||||
# Extract: /platforms/oms/pricing → code="oms", clean_path="/pricing"
|
||||
path_after_platforms = path[11:] # Remove "/platforms/"
|
||||
parts = path_after_platforms.split("/", 1)
|
||||
platform_code = parts[0].lower()
|
||||
|
||||
if platform_code:
|
||||
clean_path = "/" + parts[1] if len(parts) > 1 and parts[1] else "/"
|
||||
return {
|
||||
"path_prefix": potential_platform_code,
|
||||
"path_prefix": platform_code,
|
||||
"detection_method": "path",
|
||||
"host": host,
|
||||
"original_path": path,
|
||||
"clean_path": "/" + "/".join(path_parts[1:]) if len(path_parts) > 1 else "/",
|
||||
"clean_path": clean_path,
|
||||
}
|
||||
|
||||
# Method 3: Default platform for localhost without prefix
|
||||
# Method 3: Default platform for localhost without /platforms/ prefix
|
||||
# This serves the main marketing site
|
||||
if host_without_port in ["localhost", "127.0.0.1"]:
|
||||
return {
|
||||
"path_prefix": DEFAULT_PLATFORM_CODE,
|
||||
"detection_method": "default",
|
||||
"host": host,
|
||||
"original_path": path,
|
||||
"clean_path": path,
|
||||
"clean_path": path, # No path rewrite for main site
|
||||
}
|
||||
|
||||
return None
|
||||
@@ -248,54 +250,73 @@ class PlatformContextManager:
|
||||
return False
|
||||
|
||||
|
||||
class PlatformContextMiddleware(BaseHTTPMiddleware):
|
||||
class PlatformContextMiddleware:
|
||||
"""
|
||||
Middleware to inject platform context into request state.
|
||||
ASGI Middleware to inject platform context and rewrite URL paths.
|
||||
|
||||
This middleware:
|
||||
1. Detects platform from domain (production) or path prefix (development)
|
||||
2. Rewrites the URL path to remove platform prefix for routing
|
||||
3. Stores platform info in request state for handlers
|
||||
|
||||
Runs BEFORE VendorContextMiddleware to establish platform context.
|
||||
|
||||
Sets:
|
||||
request.state.platform: Platform object
|
||||
request.state.platform_context: Detection metadata
|
||||
request.state.platform_clean_path: Path without platform prefix
|
||||
Sets in scope['state']:
|
||||
platform: Platform object
|
||||
platform_context: Detection metadata
|
||||
platform_clean_path: Path without platform prefix
|
||||
platform_original_path: Original path before rewrite
|
||||
"""
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
"""
|
||||
Detect and inject platform context.
|
||||
"""
|
||||
# Skip platform detection for static files
|
||||
if PlatformContextManager.is_static_file_request(request):
|
||||
logger.debug(
|
||||
f"[PLATFORM] Skipping platform detection for static file: {request.url.path}"
|
||||
)
|
||||
request.state.platform = None
|
||||
request.state.platform_context = None
|
||||
request.state.platform_clean_path = request.url.path
|
||||
return await call_next(request)
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
# Skip platform detection for system endpoints
|
||||
if request.url.path in ["/health", "/docs", "/redoc", "/openapi.json"]:
|
||||
logger.debug(
|
||||
f"[PLATFORM] Skipping platform detection for system path: {request.url.path}"
|
||||
)
|
||||
request.state.platform = None
|
||||
request.state.platform_context = None
|
||||
request.state.platform_clean_path = request.url.path
|
||||
return await call_next(request)
|
||||
async def __call__(self, scope, receive, send):
|
||||
"""ASGI interface - allows path rewriting before routing."""
|
||||
if scope["type"] != "http":
|
||||
await self.app(scope, receive, send)
|
||||
return
|
||||
|
||||
# Admin requests are global (no platform context)
|
||||
if PlatformContextManager.is_admin_request(request):
|
||||
logger.debug(
|
||||
f"[PLATFORM] Admin request - no platform context: {request.url.path}"
|
||||
)
|
||||
request.state.platform = None
|
||||
request.state.platform_context = None
|
||||
request.state.platform_clean_path = request.url.path
|
||||
return await call_next(request)
|
||||
# Initialize state dict if not present
|
||||
if "state" not in scope:
|
||||
scope["state"] = {}
|
||||
|
||||
path = scope["path"]
|
||||
host = ""
|
||||
for header_name, header_value in scope.get("headers", []):
|
||||
if header_name == b"host":
|
||||
host = header_value.decode("utf-8")
|
||||
break
|
||||
|
||||
# Skip for static files
|
||||
if self._is_static_file(path):
|
||||
scope["state"]["platform"] = None
|
||||
scope["state"]["platform_context"] = None
|
||||
scope["state"]["platform_clean_path"] = path
|
||||
scope["state"]["platform_original_path"] = path
|
||||
await self.app(scope, receive, send)
|
||||
return
|
||||
|
||||
# Skip for system endpoints
|
||||
if path in ["/health", "/docs", "/redoc", "/openapi.json"]:
|
||||
scope["state"]["platform"] = None
|
||||
scope["state"]["platform_context"] = None
|
||||
scope["state"]["platform_clean_path"] = path
|
||||
scope["state"]["platform_original_path"] = path
|
||||
await self.app(scope, receive, send)
|
||||
return
|
||||
|
||||
# Skip for admin requests
|
||||
if self._is_admin_request(path, host):
|
||||
scope["state"]["platform"] = None
|
||||
scope["state"]["platform_context"] = None
|
||||
scope["state"]["platform_clean_path"] = path
|
||||
scope["state"]["platform_original_path"] = path
|
||||
await self.app(scope, receive, send)
|
||||
return
|
||||
|
||||
# Detect platform context
|
||||
platform_context = PlatformContextManager.detect_platform_context(request)
|
||||
platform_context = self._detect_platform_context(path, host)
|
||||
|
||||
if platform_context:
|
||||
db_gen = get_db()
|
||||
@@ -306,52 +327,123 @@ class PlatformContextMiddleware(BaseHTTPMiddleware):
|
||||
)
|
||||
|
||||
if platform:
|
||||
request.state.platform = platform
|
||||
request.state.platform_context = platform_context
|
||||
request.state.platform_clean_path = PlatformContextManager.extract_clean_path(
|
||||
request, platform_context
|
||||
)
|
||||
clean_path = platform_context.get("clean_path", path)
|
||||
|
||||
# Store in scope state
|
||||
scope["state"]["platform"] = platform
|
||||
scope["state"]["platform_context"] = platform_context
|
||||
scope["state"]["platform_clean_path"] = clean_path
|
||||
scope["state"]["platform_original_path"] = path
|
||||
|
||||
# REWRITE THE PATH for routing
|
||||
# This is the key: FastAPI will route based on this rewritten path
|
||||
if platform_context.get("detection_method") == "path":
|
||||
scope["path"] = clean_path
|
||||
# Also update raw_path if present
|
||||
if "raw_path" in scope:
|
||||
scope["raw_path"] = clean_path.encode("utf-8")
|
||||
|
||||
logger.debug(
|
||||
"[PLATFORM_CONTEXT] Platform detected",
|
||||
extra={
|
||||
"platform_id": platform.id,
|
||||
"platform_code": platform.code,
|
||||
"platform_name": platform.name,
|
||||
"detection_method": platform_context.get("detection_method"),
|
||||
"original_path": request.url.path,
|
||||
"clean_path": request.state.platform_clean_path,
|
||||
},
|
||||
f"[PLATFORM] Detected: {platform.code}, "
|
||||
f"original={path}, routed={scope['path']}"
|
||||
)
|
||||
else:
|
||||
# Platform code detected but not found in database
|
||||
# This could be a vendor path like /vendors/...
|
||||
logger.debug(
|
||||
"[PLATFORM] Platform code not found, may be vendor path",
|
||||
extra={
|
||||
"context": platform_context,
|
||||
"detection_method": platform_context.get("detection_method"),
|
||||
},
|
||||
)
|
||||
request.state.platform = None
|
||||
request.state.platform_context = None
|
||||
request.state.platform_clean_path = request.url.path
|
||||
# Platform code not found in database
|
||||
scope["state"]["platform"] = None
|
||||
scope["state"]["platform_context"] = None
|
||||
scope["state"]["platform_clean_path"] = path
|
||||
scope["state"]["platform_original_path"] = path
|
||||
finally:
|
||||
db.close()
|
||||
else:
|
||||
logger.debug(
|
||||
"[PLATFORM] No platform context detected",
|
||||
extra={
|
||||
"path": request.url.path,
|
||||
"host": request.headers.get("host", ""),
|
||||
},
|
||||
)
|
||||
request.state.platform = None
|
||||
request.state.platform_context = None
|
||||
request.state.platform_clean_path = request.url.path
|
||||
scope["state"]["platform"] = None
|
||||
scope["state"]["platform_context"] = None
|
||||
scope["state"]["platform_clean_path"] = path
|
||||
scope["state"]["platform_original_path"] = path
|
||||
|
||||
# Continue to next middleware
|
||||
return await call_next(request)
|
||||
await self.app(scope, receive, send)
|
||||
|
||||
def _detect_platform_context(self, path: str, host: str) -> dict | None:
|
||||
"""
|
||||
Detect platform from path or host.
|
||||
|
||||
URL Structure:
|
||||
- / → Main marketing site ('main' platform)
|
||||
- /about → Main marketing site about page
|
||||
- /platforms/oms/ → OMS platform homepage
|
||||
- /platforms/oms/pricing → OMS platform pricing
|
||||
- /platforms/loyalty/ → Loyalty platform homepage
|
||||
"""
|
||||
host_without_port = host.split(":")[0] if ":" in host else host
|
||||
|
||||
# Method 1: Domain-based (production)
|
||||
if host_without_port and host_without_port not in ["localhost", "127.0.0.1"]:
|
||||
if "." in host_without_port:
|
||||
parts = host_without_port.split(".")
|
||||
if len(parts) == 2: # Root domain like oms.lu
|
||||
return {
|
||||
"domain": host_without_port,
|
||||
"detection_method": "domain",
|
||||
"host": host,
|
||||
"original_path": path,
|
||||
"clean_path": path, # No path rewrite for domain-based
|
||||
}
|
||||
|
||||
# Method 2: Path-based (development) - ONLY for /platforms/ prefix
|
||||
if path.startswith("/platforms/"):
|
||||
# Extract: /platforms/oms/pricing → code="oms", clean_path="/pricing"
|
||||
path_after_platforms = path[11:] # Remove "/platforms/"
|
||||
parts = path_after_platforms.split("/", 1)
|
||||
platform_code = parts[0].lower()
|
||||
|
||||
if platform_code:
|
||||
clean_path = "/" + parts[1] if len(parts) > 1 and parts[1] else "/"
|
||||
return {
|
||||
"path_prefix": platform_code,
|
||||
"detection_method": "path",
|
||||
"host": host,
|
||||
"original_path": path,
|
||||
"clean_path": clean_path,
|
||||
}
|
||||
|
||||
# Method 3: Default for localhost - serves main marketing site
|
||||
if host_without_port in ["localhost", "127.0.0.1"]:
|
||||
return {
|
||||
"path_prefix": DEFAULT_PLATFORM_CODE,
|
||||
"detection_method": "default",
|
||||
"host": host,
|
||||
"original_path": path,
|
||||
"clean_path": path, # No path rewrite for main site
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
def _is_static_file(self, path: str) -> bool:
|
||||
"""Check if path is for static files."""
|
||||
path_lower = path.lower()
|
||||
static_extensions = (
|
||||
".ico", ".css", ".js", ".png", ".jpg", ".jpeg", ".gif", ".svg",
|
||||
".woff", ".woff2", ".ttf", ".eot", ".webp", ".map", ".json",
|
||||
".xml", ".txt", ".pdf", ".webmanifest",
|
||||
)
|
||||
static_paths = ("/static/", "/media/", "/assets/", "/.well-known/")
|
||||
|
||||
if path_lower.endswith(static_extensions):
|
||||
return True
|
||||
if any(path_lower.startswith(p) for p in static_paths):
|
||||
return True
|
||||
if "favicon.ico" in path_lower:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _is_admin_request(self, path: str, host: str) -> bool:
|
||||
"""Check if request is for admin interface."""
|
||||
host_without_port = host.split(":")[0] if ":" in host else host
|
||||
if host_without_port.startswith("admin."):
|
||||
return True
|
||||
if path.startswith("/admin"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_current_platform(request: Request) -> Platform | None:
|
||||
|
||||
Reference in New Issue
Block a user