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),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Platform marketing homepage.
|
Homepage handler.
|
||||||
|
|
||||||
Displays:
|
Handles two scenarios:
|
||||||
- Hero section with value proposition
|
1. Vendor on custom domain (vendor.com) → Show vendor landing page or redirect to shop
|
||||||
- Pricing tier cards
|
2. Platform marketing site → Show platform homepage from CMS or default template
|
||||||
- Add-ons section
|
|
||||||
- Letzshop vendor finder
|
URL routing:
|
||||||
- Call to action for signup
|
- 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 = get_platform_context(request, db)
|
||||||
|
context["platform"] = platform
|
||||||
|
|
||||||
# Fetch tiers for display (use API service internally)
|
# Fetch tiers for display (use API service internally)
|
||||||
from models.database.subscription import TIER_LIMITS, TierCode
|
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)
|
logger.info("=" * 80)
|
||||||
@@ -701,73 +410,6 @@ for route in app.routes:
|
|||||||
logger.info(f" {methods:<10} {route.path:<60}")
|
logger.info(f" {methods:<10} {route.path:<60}")
|
||||||
logger.info("=" * 80)
|
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)
|
@app.get("/documentation", response_class=HTMLResponse, include_in_schema=False)
|
||||||
async def documentation():
|
async def documentation():
|
||||||
"""Redirect to documentation"""
|
"""Redirect to documentation"""
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ This middleware runs BEFORE VendorContextMiddleware to establish platform contex
|
|||||||
|
|
||||||
Handles two routing modes:
|
Handles two routing modes:
|
||||||
1. Production: Domain-based (oms.lu, loyalty.lu → Platform detection)
|
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.
|
Also provides platform_clean_path for downstream middleware to use.
|
||||||
"""
|
"""
|
||||||
@@ -16,7 +20,7 @@ import logging
|
|||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from sqlalchemy.orm import Session
|
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.config import settings
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
@@ -24,8 +28,8 @@ from models.database.platform import Platform
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Default platform code for backward compatibility
|
# Default platform code for main marketing site
|
||||||
DEFAULT_PLATFORM_CODE = "oms"
|
DEFAULT_PLATFORM_CODE = "main"
|
||||||
|
|
||||||
|
|
||||||
class PlatformContextManager:
|
class PlatformContextManager:
|
||||||
@@ -38,8 +42,15 @@ class PlatformContextManager:
|
|||||||
|
|
||||||
Priority order:
|
Priority order:
|
||||||
1. Domain-based (production): oms.lu → platform code "oms"
|
1. Domain-based (production): oms.lu → platform code "oms"
|
||||||
2. Path-based (development): localhost:9999/oms/* → platform code "oms"
|
2. Path-based (development): localhost:9999/platforms/oms/* → platform code "oms"
|
||||||
3. Default: localhost without path prefix → default platform
|
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.
|
Returns dict with platform info or None if not detected.
|
||||||
"""
|
"""
|
||||||
@@ -75,42 +86,33 @@ class PlatformContextManager:
|
|||||||
"original_path": path,
|
"original_path": path,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Method 2: Path-based detection (development)
|
# Method 2: Path-based detection (development) - ONLY for /platforms/ prefix
|
||||||
# Check for path prefix like /oms/, /loyalty/
|
# Check for path prefix like /platforms/oms/, /platforms/loyalty/
|
||||||
if path.startswith("/"):
|
if path.startswith("/platforms/"):
|
||||||
path_parts = path[1:].split("/") # Remove leading / and split
|
# Extract: /platforms/oms/pricing → code="oms", clean_path="/pricing"
|
||||||
if path_parts and path_parts[0]:
|
path_after_platforms = path[11:] # Remove "/platforms/"
|
||||||
potential_platform_code = path_parts[0].lower()
|
parts = path_after_platforms.split("/", 1)
|
||||||
# Check if this could be a platform code (not vendor paths)
|
platform_code = parts[0].lower()
|
||||||
if potential_platform_code not in [
|
|
||||||
"vendor",
|
|
||||||
"vendors",
|
|
||||||
"admin",
|
|
||||||
"api",
|
|
||||||
"static",
|
|
||||||
"media",
|
|
||||||
"assets",
|
|
||||||
"health",
|
|
||||||
"docs",
|
|
||||||
"redoc",
|
|
||||||
"openapi.json",
|
|
||||||
]:
|
|
||||||
return {
|
|
||||||
"path_prefix": potential_platform_code,
|
|
||||||
"detection_method": "path",
|
|
||||||
"host": host,
|
|
||||||
"original_path": path,
|
|
||||||
"clean_path": "/" + "/".join(path_parts[1:]) if len(path_parts) > 1 else "/",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Method 3: Default platform for localhost without prefix
|
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 platform for localhost without /platforms/ prefix
|
||||||
|
# This serves the main marketing site
|
||||||
if host_without_port in ["localhost", "127.0.0.1"]:
|
if host_without_port in ["localhost", "127.0.0.1"]:
|
||||||
return {
|
return {
|
||||||
"path_prefix": DEFAULT_PLATFORM_CODE,
|
"path_prefix": DEFAULT_PLATFORM_CODE,
|
||||||
"detection_method": "default",
|
"detection_method": "default",
|
||||||
"host": host,
|
"host": host,
|
||||||
"original_path": path,
|
"original_path": path,
|
||||||
"clean_path": path,
|
"clean_path": path, # No path rewrite for main site
|
||||||
}
|
}
|
||||||
|
|
||||||
return None
|
return None
|
||||||
@@ -248,54 +250,73 @@ class PlatformContextManager:
|
|||||||
return False
|
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.
|
Runs BEFORE VendorContextMiddleware to establish platform context.
|
||||||
|
|
||||||
Sets:
|
Sets in scope['state']:
|
||||||
request.state.platform: Platform object
|
platform: Platform object
|
||||||
request.state.platform_context: Detection metadata
|
platform_context: Detection metadata
|
||||||
request.state.platform_clean_path: Path without platform prefix
|
platform_clean_path: Path without platform prefix
|
||||||
|
platform_original_path: Original path before rewrite
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def dispatch(self, request: Request, call_next):
|
def __init__(self, app):
|
||||||
"""
|
self.app = app
|
||||||
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)
|
|
||||||
|
|
||||||
# Skip platform detection for system endpoints
|
async def __call__(self, scope, receive, send):
|
||||||
if request.url.path in ["/health", "/docs", "/redoc", "/openapi.json"]:
|
"""ASGI interface - allows path rewriting before routing."""
|
||||||
logger.debug(
|
if scope["type"] != "http":
|
||||||
f"[PLATFORM] Skipping platform detection for system path: {request.url.path}"
|
await self.app(scope, receive, send)
|
||||||
)
|
return
|
||||||
request.state.platform = None
|
|
||||||
request.state.platform_context = None
|
|
||||||
request.state.platform_clean_path = request.url.path
|
|
||||||
return await call_next(request)
|
|
||||||
|
|
||||||
# Admin requests are global (no platform context)
|
# Initialize state dict if not present
|
||||||
if PlatformContextManager.is_admin_request(request):
|
if "state" not in scope:
|
||||||
logger.debug(
|
scope["state"] = {}
|
||||||
f"[PLATFORM] Admin request - no platform context: {request.url.path}"
|
|
||||||
)
|
path = scope["path"]
|
||||||
request.state.platform = None
|
host = ""
|
||||||
request.state.platform_context = None
|
for header_name, header_value in scope.get("headers", []):
|
||||||
request.state.platform_clean_path = request.url.path
|
if header_name == b"host":
|
||||||
return await call_next(request)
|
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
|
# Detect platform context
|
||||||
platform_context = PlatformContextManager.detect_platform_context(request)
|
platform_context = self._detect_platform_context(path, host)
|
||||||
|
|
||||||
if platform_context:
|
if platform_context:
|
||||||
db_gen = get_db()
|
db_gen = get_db()
|
||||||
@@ -306,52 +327,123 @@ class PlatformContextMiddleware(BaseHTTPMiddleware):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if platform:
|
if platform:
|
||||||
request.state.platform = platform
|
clean_path = platform_context.get("clean_path", path)
|
||||||
request.state.platform_context = platform_context
|
|
||||||
request.state.platform_clean_path = PlatformContextManager.extract_clean_path(
|
# Store in scope state
|
||||||
request, platform_context
|
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(
|
logger.debug(
|
||||||
"[PLATFORM_CONTEXT] Platform detected",
|
f"[PLATFORM] Detected: {platform.code}, "
|
||||||
extra={
|
f"original={path}, routed={scope['path']}"
|
||||||
"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,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Platform code detected but not found in database
|
# Platform code not found in database
|
||||||
# This could be a vendor path like /vendors/...
|
scope["state"]["platform"] = None
|
||||||
logger.debug(
|
scope["state"]["platform_context"] = None
|
||||||
"[PLATFORM] Platform code not found, may be vendor path",
|
scope["state"]["platform_clean_path"] = path
|
||||||
extra={
|
scope["state"]["platform_original_path"] = path
|
||||||
"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
|
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
else:
|
else:
|
||||||
logger.debug(
|
scope["state"]["platform"] = None
|
||||||
"[PLATFORM] No platform context detected",
|
scope["state"]["platform_context"] = None
|
||||||
extra={
|
scope["state"]["platform_clean_path"] = path
|
||||||
"path": request.url.path,
|
scope["state"]["platform_original_path"] = path
|
||||||
"host": request.headers.get("host", ""),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
request.state.platform = None
|
|
||||||
request.state.platform_context = None
|
|
||||||
request.state.platform_clean_path = request.url.path
|
|
||||||
|
|
||||||
# Continue to next middleware
|
await self.app(scope, receive, send)
|
||||||
return await call_next(request)
|
|
||||||
|
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:
|
def get_current_platform(request: Request) -> Platform | None:
|
||||||
|
|||||||
Reference in New Issue
Block a user