Compare commits
3 Commits
169a774b9c
...
a28d5d1de5
| Author | SHA1 | Date | |
|---|---|---|---|
| a28d5d1de5 | |||
| 502473eee4 | |||
| 183f55c7b3 |
@@ -111,11 +111,9 @@ language_rules:
|
|||||||
function languageSelector(currentLang, enabledLanguages) { ... }
|
function languageSelector(currentLang, enabledLanguages) { ... }
|
||||||
window.languageSelector = languageSelector;
|
window.languageSelector = languageSelector;
|
||||||
pattern:
|
pattern:
|
||||||
file_pattern: "static/shop/js/shop-layout.js"
|
file_patterns:
|
||||||
required_patterns:
|
- "static/shop/js/shop-layout.js"
|
||||||
- "function languageSelector"
|
- "static/vendor/js/init-alpine.js"
|
||||||
- "window.languageSelector"
|
|
||||||
file_pattern: "static/vendor/js/init-alpine.js"
|
|
||||||
required_patterns:
|
required_patterns:
|
||||||
- "function languageSelector"
|
- "function languageSelector"
|
||||||
- "window.languageSelector"
|
- "window.languageSelector"
|
||||||
@@ -247,3 +245,26 @@ language_rules:
|
|||||||
pattern:
|
pattern:
|
||||||
file_pattern: "static/locales/*.json"
|
file_pattern: "static/locales/*.json"
|
||||||
check: "valid_json"
|
check: "valid_json"
|
||||||
|
|
||||||
|
- id: "LANG-011"
|
||||||
|
name: "Use $t() not I18n.t() in HTML templates"
|
||||||
|
severity: "error"
|
||||||
|
description: |
|
||||||
|
In HTML templates, never use I18n.t() directly. It evaluates once
|
||||||
|
and does NOT re-evaluate when translations finish loading async.
|
||||||
|
|
||||||
|
WRONG (non-reactive, shows raw key then updates):
|
||||||
|
<span x-text="I18n.t('module.key')"></span>
|
||||||
|
|
||||||
|
RIGHT (reactive, updates when translations load):
|
||||||
|
<span x-text="$t('module.key')"></span>
|
||||||
|
|
||||||
|
BEST (server-side, zero flash):
|
||||||
|
<span>{{ _('module.key') }}</span>
|
||||||
|
|
||||||
|
Note: I18n.t() is fine in .js files where it's called inside
|
||||||
|
async callbacks after I18n.init() has completed.
|
||||||
|
pattern:
|
||||||
|
file_pattern: "**/*.html"
|
||||||
|
anti_patterns:
|
||||||
|
- "I18n.t("
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from fastapi.responses import HTMLResponse
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.api.deps import get_db, require_menu_access
|
from app.api.deps import get_db, require_menu_access
|
||||||
|
from app.modules.core.utils.page_context import get_admin_context
|
||||||
from app.modules.enums import FrontendType
|
from app.modules.enums import FrontendType
|
||||||
from app.modules.tenancy.models import User
|
from app.modules.tenancy.models import User
|
||||||
from app.templates_config import templates
|
from app.templates_config import templates
|
||||||
@@ -43,10 +44,7 @@ async def admin_loyalty_programs(
|
|||||||
"""
|
"""
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"loyalty/admin/programs.html",
|
"loyalty/admin/programs.html",
|
||||||
{
|
get_admin_context(request, db, current_user),
|
||||||
"request": request,
|
|
||||||
"user": current_user,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -62,10 +60,7 @@ async def admin_loyalty_analytics(
|
|||||||
"""
|
"""
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"loyalty/admin/analytics.html",
|
"loyalty/admin/analytics.html",
|
||||||
{
|
get_admin_context(request, db, current_user),
|
||||||
"request": request,
|
|
||||||
"user": current_user,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -91,11 +86,7 @@ async def admin_loyalty_merchant_detail(
|
|||||||
"""
|
"""
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"loyalty/admin/merchant-detail.html",
|
"loyalty/admin/merchant-detail.html",
|
||||||
{
|
get_admin_context(request, db, current_user, merchant_id=merchant_id),
|
||||||
"request": request,
|
|
||||||
"user": current_user,
|
|
||||||
"merchant_id": merchant_id,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -116,11 +107,7 @@ async def admin_loyalty_program_edit(
|
|||||||
"""
|
"""
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"loyalty/admin/program-edit.html",
|
"loyalty/admin/program-edit.html",
|
||||||
{
|
get_admin_context(request, db, current_user, merchant_id=merchant_id),
|
||||||
"request": request,
|
|
||||||
"user": current_user,
|
|
||||||
"merchant_id": merchant_id,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -141,9 +128,5 @@ async def admin_loyalty_merchant_settings(
|
|||||||
"""
|
"""
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"loyalty/admin/merchant-settings.html",
|
"loyalty/admin/merchant-settings.html",
|
||||||
{
|
get_admin_context(request, db, current_user, merchant_id=merchant_id),
|
||||||
"request": request,
|
|
||||||
"user": current_user,
|
|
||||||
"merchant_id": merchant_id,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,10 +24,8 @@ from app.api.deps import (
|
|||||||
get_db,
|
get_db,
|
||||||
get_resolved_store_code,
|
get_resolved_store_code,
|
||||||
)
|
)
|
||||||
from app.modules.core.services.platform_settings_service import (
|
from app.modules.core.utils.page_context import get_store_context
|
||||||
platform_settings_service,
|
from app.modules.tenancy.models import User
|
||||||
)
|
|
||||||
from app.modules.tenancy.models import Store, User
|
|
||||||
from app.templates_config import templates
|
from app.templates_config import templates
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -42,49 +40,6 @@ ROUTE_CONFIG = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# HELPER: Build Store Context
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
|
|
||||||
def get_store_context(
|
|
||||||
request: Request,
|
|
||||||
db: Session,
|
|
||||||
current_user: User,
|
|
||||||
store_code: str,
|
|
||||||
**extra_context,
|
|
||||||
) -> dict:
|
|
||||||
"""Build template context for store loyalty pages."""
|
|
||||||
# Load store from database
|
|
||||||
store = db.query(Store).filter(Store.subdomain == store_code).first()
|
|
||||||
|
|
||||||
# Get platform defaults
|
|
||||||
platform_config = platform_settings_service.get_storefront_config(db)
|
|
||||||
|
|
||||||
# Resolve with store override
|
|
||||||
storefront_locale = platform_config["locale"]
|
|
||||||
storefront_currency = platform_config["currency"]
|
|
||||||
|
|
||||||
if store and store.storefront_locale:
|
|
||||||
storefront_locale = store.storefront_locale
|
|
||||||
|
|
||||||
context = {
|
|
||||||
"request": request,
|
|
||||||
"user": current_user,
|
|
||||||
"store": store,
|
|
||||||
"store_code": store_code,
|
|
||||||
"storefront_locale": storefront_locale,
|
|
||||||
"storefront_currency": storefront_currency,
|
|
||||||
"dashboard_language": store.dashboard_language if store else "en",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add any extra context
|
|
||||||
if extra_context:
|
|
||||||
context.update(extra_context)
|
|
||||||
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# LOYALTY ROOT (Redirect to Terminal)
|
# LOYALTY ROOT (Redirect to Terminal)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from sqlalchemy.orm import Session
|
|||||||
from app.api.deps import get_current_customer_from_cookie_or_header, get_db
|
from app.api.deps import get_current_customer_from_cookie_or_header, get_db
|
||||||
from app.modules.core.utils.page_context import get_storefront_context
|
from app.modules.core.utils.page_context import get_storefront_context
|
||||||
from app.modules.customers.models import Customer
|
from app.modules.customers.models import Customer
|
||||||
|
from app.modules.loyalty.services import program_service
|
||||||
from app.templates_config import templates
|
from app.templates_config import templates
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -114,7 +115,10 @@ async def loyalty_self_enrollment(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
context = get_storefront_context(request, db=db)
|
store = request.state.store
|
||||||
|
program = program_service.get_active_program_by_store(db, store.id) if store else None
|
||||||
|
|
||||||
|
context = get_storefront_context(request, db=db, program=program)
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"loyalty/storefront/enroll.html",
|
"loyalty/storefront/enroll.html",
|
||||||
context,
|
context,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<img src="{{ store.logo_url }}" alt="{{ store.name }}" class="h-16 w-auto mx-auto mb-4">
|
<img src="{{ store.logo_url }}" alt="{{ store.name }}" class="h-16 w-auto mx-auto mb-4">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ _('loyalty.enrollment.title') }}</h1>
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ _('loyalty.enrollment.title') }}</h1>
|
||||||
<p class="mt-2 text-gray-600 dark:text-gray-400" x-text="I18n.t('loyalty.enrollment.subtitle', {points: program?.points_per_euro || 1})"></p>
|
<p class="mt-2 text-gray-600 dark:text-gray-400">{{ _('loyalty.enrollment.subtitle', points=program.points_per_euro if program else 1) }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading -->
|
<!-- Loading -->
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
class="p-4 text-center text-white"
|
class="p-4 text-center text-white"
|
||||||
:style="'background-color: ' + (program?.card_color || 'var(--color-primary)')">
|
:style="'background-color: ' + (program?.card_color || 'var(--color-primary)')">
|
||||||
<span x-html="$icon('gift', 'w-6 h-6 inline mr-2')"></span>
|
<span x-html="$icon('gift', 'w-6 h-6 inline mr-2')"></span>
|
||||||
<span class="font-semibold" x-text="I18n.t('loyalty.enrollment.welcome_bonus', {points: program?.welcome_bonus_points})"></span>
|
<span class="font-semibold">{{ _('loyalty.enrollment.welcome_bonus', points=program.welcome_bonus_points if program else 0) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form @submit.prevent="submitEnrollment" class="p-6 space-y-4">
|
<form @submit.prevent="submitEnrollment" class="p-6 space-y-4">
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
class="w-full py-3 px-4 text-white font-semibold rounded-lg transition-colors disabled:opacity-50"
|
class="w-full py-3 px-4 text-white font-semibold rounded-lg transition-colors disabled:opacity-50"
|
||||||
:style="'background-color: ' + (program?.card_color || 'var(--color-primary)')">
|
:style="'background-color: ' + (program?.card_color || 'var(--color-primary)')">
|
||||||
<span x-show="enrolling" x-html="$icon('spinner', 'w-5 h-5 inline animate-spin mr-2')"></span>
|
<span x-show="enrolling" x-html="$icon('spinner', 'w-5 h-5 inline animate-spin mr-2')"></span>
|
||||||
<span x-text="enrolling ? I18n.t('loyalty.enrollment.form.joining') : I18n.t('loyalty.enrollment.form.join_button', {points: program?.welcome_bonus_points || 0})"></span>
|
<span x-text="enrolling ? '{{ _('loyalty.enrollment.form.joining') }}' : '{{ _('loyalty.enrollment.form.join_button', points=program.welcome_bonus_points if program else 0) }}'"></span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
// Wrapped in DOMContentLoaded so deferred i18n.js has loaded
|
// Wrapped in DOMContentLoaded so deferred i18n.js has loaded
|
||||||
document.addEventListener('DOMContentLoaded', async function() {
|
document.addEventListener('DOMContentLoaded', async function() {
|
||||||
const modules = {% block i18n_modules %}[]{% endblock %};
|
const modules = {% block i18n_modules %}[]{% endblock %};
|
||||||
await I18n.init('{{ dashboard_language | default("en") }}', modules);
|
await I18n.init('{{ current_language | default("en") }}', modules);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -758,7 +758,7 @@ Then restart Gitea:
|
|||||||
cd ~/gitea && docker compose up -d gitea
|
cd ~/gitea && docker compose up -d gitea
|
||||||
```
|
```
|
||||||
|
|
||||||
### Future: Multi-Tenant Store Routing
|
### Multi-Tenant Store Routing
|
||||||
|
|
||||||
Stores on each platform use two routing modes:
|
Stores on each platform use two routing modes:
|
||||||
|
|
||||||
@@ -769,49 +769,204 @@ Both modes are handled by the `StoreContextMiddleware` which reads the `Host` he
|
|||||||
|
|
||||||
#### Wildcard Subdomains (for store subdomains)
|
#### Wildcard Subdomains (for store subdomains)
|
||||||
|
|
||||||
When stores start using subdomains like `acme.omsflow.lu`, add wildcard blocks:
|
Each non-main platform uses a wildcard Caddy block so any store subdomain (e.g. `acme.omsflow.lu`, `parcelproxy.hostwizard.lu`) is automatically routed without per-store Caddy changes.
|
||||||
|
|
||||||
|
The wildcard blocks use the same origin cert as the platform root domain. The origin cert must include `*.<platform_domain>` — see [Step 21.4](#214-generate-origin-certificates) for how to generate it.
|
||||||
|
|
||||||
```caddy
|
```caddy
|
||||||
*.omsflow.lu {
|
*.omsflow.lu {
|
||||||
|
tls /etc/caddy/certs/omsflow.lu/cert.pem /etc/caddy/certs/omsflow.lu/key.pem
|
||||||
reverse_proxy localhost:8001
|
reverse_proxy localhost:8001
|
||||||
}
|
}
|
||||||
|
|
||||||
*.rewardflow.lu {
|
*.rewardflow.lu {
|
||||||
|
tls /etc/caddy/certs/rewardflow.lu/cert.pem /etc/caddy/certs/rewardflow.lu/key.pem
|
||||||
reverse_proxy localhost:8001
|
reverse_proxy localhost:8001
|
||||||
}
|
}
|
||||||
|
|
||||||
*.wizard.lu {
|
*.hostwizard.lu {
|
||||||
|
tls /etc/caddy/certs/hostwizard.lu/cert.pem /etc/caddy/certs/hostwizard.lu/key.pem
|
||||||
reverse_proxy localhost:8001
|
reverse_proxy localhost:8001
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! warning "Wildcard SSL requires DNS challenge"
|
!!! warning "No wildcard for wizard.lu"
|
||||||
Let's Encrypt cannot issue wildcard certificates via HTTP challenge. Wildcard certs require a **DNS challenge**, which means installing a Caddy DNS provider plugin (e.g. `caddy-dns/cloudflare`) and configuring API credentials for your DNS provider. See [Caddy DNS challenge docs](https://caddyserver.com/docs/automatic-https#dns-challenge).
|
`wizard.lu` cannot use a wildcard block because `git.wizard.lu` is DNS-only (grey cloud in Cloudflare) and uses a Let's Encrypt cert. A wildcard origin cert would conflict. For wizard.lu subdomains, add each one explicitly (same pattern as `api.wizard.lu`, `flower.wizard.lu`).
|
||||||
|
|
||||||
#### Custom Store Domains (for premium stores)
|
**Cloudflare DNS**: Add a wildcard DNS record for each platform:
|
||||||
|
|
||||||
When premium stores bring their own domains (e.g. `acme.lu`), use Caddy's **on-demand TLS**:
|
- `*.omsflow.lu` → A `91.99.65.229` (proxied, orange cloud)
|
||||||
|
- `*.rewardflow.lu` → A `91.99.65.229` (proxied, orange cloud)
|
||||||
|
- `*.hostwizard.lu` → A `91.99.65.229` (proxied, orange cloud)
|
||||||
|
|
||||||
|
With this in place, adding a new store subdomain only requires a database entry (via admin UI) — no DNS or Caddy changes needed.
|
||||||
|
|
||||||
|
#### Runbook: Add a Store Subdomain
|
||||||
|
|
||||||
|
When a merchant creates a store with subdomain `acme` on the OMS platform:
|
||||||
|
|
||||||
|
1. **Database**: Create the store via admin UI — set `subdomain = "acme"` and link to the platform. The `StoreContextMiddleware` will resolve `acme.omsflow.lu` automatically.
|
||||||
|
2. **DNS**: Already covered by the wildcard `*.omsflow.lu` record in Cloudflare.
|
||||||
|
3. **Caddy**: Already covered by the `*.omsflow.lu` block.
|
||||||
|
4. **SSL**: Already covered by the wildcard origin cert.
|
||||||
|
5. **Verify**: `curl -I https://acme.omsflow.lu/`
|
||||||
|
|
||||||
|
No infrastructure changes needed — it's fully self-service.
|
||||||
|
|
||||||
|
#### Runbook: Add a Custom Store Domain
|
||||||
|
|
||||||
|
When a premium store brings their own domain (e.g. `wizamart.com`), the domain must be added to **your** Cloudflare account. This ensures you control SSL, WAF, and DNS — critical since you are responsible for the infrastructure.
|
||||||
|
|
||||||
|
**Step 1: Add domain to Cloudflare**
|
||||||
|
|
||||||
|
1. In [Cloudflare Dashboard](https://dash.cloudflare.com), click **Add a site** > enter `wizamart.com`
|
||||||
|
2. Select the Free plan
|
||||||
|
3. Cloudflare assigns NS records — give these to the store owner to update at their registrar
|
||||||
|
4. Wait for NS propagation (can take up to 24h, usually minutes)
|
||||||
|
|
||||||
|
**Step 2: Configure DNS in Cloudflare**
|
||||||
|
|
||||||
|
Add A records (proxied, orange cloud):
|
||||||
|
|
||||||
|
| Type | Name | Content | Proxy |
|
||||||
|
|------|------|---------|-------|
|
||||||
|
| A | `wizamart.com` | `91.99.65.229` | Proxied |
|
||||||
|
| A | `www` | `91.99.65.229` | Proxied |
|
||||||
|
|
||||||
|
**Step 3: Generate origin certificate**
|
||||||
|
|
||||||
|
1. In Cloudflare: **SSL/TLS** > **Origin Server** > **Create Certificate**
|
||||||
|
2. Hostnames: `wizamart.com, www.wizamart.com`
|
||||||
|
3. Validity: 15 years
|
||||||
|
4. Download cert and key (key shown only once!)
|
||||||
|
|
||||||
|
**Step 4: Install cert on server**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /etc/caddy/certs/wizamart.com
|
||||||
|
sudo nano /etc/caddy/certs/wizamart.com/cert.pem # paste certificate
|
||||||
|
sudo nano /etc/caddy/certs/wizamart.com/key.pem # paste private key
|
||||||
|
sudo chown -R caddy:caddy /etc/caddy/certs/wizamart.com
|
||||||
|
sudo chmod 600 /etc/caddy/certs/wizamart.com/key.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 5: Add to Caddyfile**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nano /etc/caddy/Caddyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
```caddy
|
```caddy
|
||||||
https:// {
|
# ─── Custom store domain: wizamart.com ────────────────────────
|
||||||
tls {
|
wizamart.com {
|
||||||
on_demand
|
tls /etc/caddy/certs/wizamart.com/cert.pem /etc/caddy/certs/wizamart.com/key.pem
|
||||||
|
reverse_proxy localhost:8001
|
||||||
}
|
}
|
||||||
|
|
||||||
|
www.wizamart.com {
|
||||||
|
tls /etc/caddy/certs/wizamart.com/cert.pem /etc/caddy/certs/wizamart.com/key.pem
|
||||||
|
redir https://wizamart.com{uri} permanent
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 6: Reload Caddy**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl reload caddy
|
||||||
|
sudo systemctl status caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 7: Configure Cloudflare settings**
|
||||||
|
|
||||||
|
In Cloudflare dashboard for `wizamart.com`:
|
||||||
|
|
||||||
|
| Setting | Location | Value |
|
||||||
|
|---|---|---|
|
||||||
|
| SSL mode | SSL/TLS > Overview | Full (Strict) |
|
||||||
|
| Always Use HTTPS | SSL/TLS > Edge Certificates | On |
|
||||||
|
| Bot Fight Mode | Security > Settings | On |
|
||||||
|
|
||||||
|
**Step 8: Register domain in database**
|
||||||
|
|
||||||
|
Via admin UI: create a `StoreDomain` record linking `wizamart.com` to the store and platform.
|
||||||
|
|
||||||
|
**Step 9: Verify**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -I https://wizamart.com/
|
||||||
|
```
|
||||||
|
|
||||||
|
The `StoreContextMiddleware` will detect `wizamart.com` as a custom domain, look it up in the `store_domains` table, and route to the correct store.
|
||||||
|
|
||||||
|
#### Runbook: Add a New Platform Domain
|
||||||
|
|
||||||
|
When adding an entirely new platform (e.g. `newplatform.lu`):
|
||||||
|
|
||||||
|
**Step 1: Cloudflare**
|
||||||
|
|
||||||
|
1. Add `newplatform.lu` as a site in Cloudflare
|
||||||
|
2. Configure NS at registrar
|
||||||
|
3. Add DNS records (all proxied):
|
||||||
|
- `newplatform.lu` → A `91.99.65.229`
|
||||||
|
- `www.newplatform.lu` → A `91.99.65.229`
|
||||||
|
- `*.newplatform.lu` → A `91.99.65.229` (for store subdomains)
|
||||||
|
|
||||||
|
**Step 2: Generate origin certificate**
|
||||||
|
|
||||||
|
In Cloudflare: **SSL/TLS** > **Origin Server** > **Create Certificate**
|
||||||
|
|
||||||
|
Hostnames: `newplatform.lu, www.newplatform.lu, *.newplatform.lu`
|
||||||
|
|
||||||
|
**Step 3: Install cert and update Caddyfile**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /etc/caddy/certs/newplatform.lu
|
||||||
|
sudo nano /etc/caddy/certs/newplatform.lu/cert.pem
|
||||||
|
sudo nano /etc/caddy/certs/newplatform.lu/key.pem
|
||||||
|
sudo chown -R caddy:caddy /etc/caddy/certs/newplatform.lu
|
||||||
|
sudo chmod 600 /etc/caddy/certs/newplatform.lu/key.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to Caddyfile:
|
||||||
|
|
||||||
|
```caddy
|
||||||
|
# ─── Platform: NewPlatform (newplatform.lu) ───────────────────
|
||||||
|
newplatform.lu {
|
||||||
|
tls /etc/caddy/certs/newplatform.lu/cert.pem /etc/caddy/certs/newplatform.lu/key.pem
|
||||||
|
reverse_proxy localhost:8001
|
||||||
|
}
|
||||||
|
|
||||||
|
www.newplatform.lu {
|
||||||
|
tls /etc/caddy/certs/newplatform.lu/cert.pem /etc/caddy/certs/newplatform.lu/key.pem
|
||||||
|
redir https://newplatform.lu{uri} permanent
|
||||||
|
}
|
||||||
|
|
||||||
|
*.newplatform.lu {
|
||||||
|
tls /etc/caddy/certs/newplatform.lu/cert.pem /etc/caddy/certs/newplatform.lu/key.pem
|
||||||
reverse_proxy localhost:8001
|
reverse_proxy localhost:8001
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
On-demand TLS auto-provisions SSL certificates when a new domain connects. Add an `ask` endpoint to validate that the domain is registered in the `store_domains` table, preventing abuse:
|
```bash
|
||||||
|
sudo systemctl reload caddy
|
||||||
```caddy
|
|
||||||
tls {
|
|
||||||
on_demand
|
|
||||||
ask http://localhost:8001/api/v1/internal/verify-domain
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! note "Not needed yet"
|
**Step 4: Cloudflare settings**
|
||||||
Wildcard subdomains and custom domains are future work. The current Caddyfile handles all platform root domains and service subdomains.
|
|
||||||
|
Same as other platforms — see [Step 21.6](#216-cloudflare-settings-per-domain).
|
||||||
|
|
||||||
|
**Step 5: Database**
|
||||||
|
|
||||||
|
Create the platform record via `init_production.py` or admin UI with `domain = "newplatform.lu"`.
|
||||||
|
|
||||||
|
**Step 6: Verify**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -I https://newplatform.lu/
|
||||||
|
curl -I https://teststore.newplatform.lu/
|
||||||
|
```
|
||||||
|
|
||||||
## Step 15: Gitea Actions Runner
|
## Step 15: Gitea Actions Runner
|
||||||
|
|
||||||
@@ -2007,15 +2162,15 @@ Disable DNSSEC at the registrar before switching NS — re-enable later via Clou
|
|||||||
Cloudflare Origin Certificates (free, 15-year validity) avoid ACME challenge issues when traffic is proxied:
|
Cloudflare Origin Certificates (free, 15-year validity) avoid ACME challenge issues when traffic is proxied:
|
||||||
|
|
||||||
1. In Cloudflare: **SSL/TLS** > **Origin Server** > **Create Certificate**
|
1. In Cloudflare: **SSL/TLS** > **Origin Server** > **Create Certificate**
|
||||||
2. Generate for each domain with **specific subdomains** (not wildcards):
|
2. Generate for each domain:
|
||||||
- `wizard.lu`: `wizard.lu, api.wizard.lu, www.wizard.lu, flower.wizard.lu, grafana.wizard.lu`
|
- `wizard.lu`: `wizard.lu, api.wizard.lu, www.wizard.lu, flower.wizard.lu, grafana.wizard.lu` (**specific subdomains, no wildcard**)
|
||||||
- `omsflow.lu`: `omsflow.lu, www.omsflow.lu`
|
- `omsflow.lu`: `omsflow.lu, www.omsflow.lu, *.omsflow.lu` (wildcard for store subdomains)
|
||||||
- `rewardflow.lu`: `rewardflow.lu, www.rewardflow.lu`
|
- `rewardflow.lu`: `rewardflow.lu, www.rewardflow.lu, *.rewardflow.lu` (wildcard for store subdomains)
|
||||||
- `hostwizard.lu`: `hostwizard.lu, www.hostwizard.lu`
|
- `hostwizard.lu`: `hostwizard.lu, www.hostwizard.lu, *.hostwizard.lu` (wildcard for store subdomains)
|
||||||
3. Download the certificate and private key (private key is shown only once)
|
3. Download the certificate and private key (private key is shown only once)
|
||||||
|
|
||||||
!!! warning "Do NOT use wildcard origin certs for wizard.lu"
|
!!! warning "Do NOT use wildcard origin certs for wizard.lu"
|
||||||
A `*.wizard.lu` wildcard cert will match `git.wizard.lu`, which needs a Let's Encrypt cert (DNS-only, not proxied through Cloudflare). Use specific subdomains instead.
|
A `*.wizard.lu` wildcard cert will match `git.wizard.lu`, which is DNS-only (grey cloud) and uses a Let's Encrypt cert. A wildcard origin cert would conflict. Use specific subdomains instead. For new wizard.lu subdomains, add them explicitly to this cert and to the Caddyfile.
|
||||||
|
|
||||||
Install on the server:
|
Install on the server:
|
||||||
|
|
||||||
@@ -2090,6 +2245,22 @@ www.hostwizard.lu {
|
|||||||
redir https://hostwizard.lu{uri} permanent
|
redir https://hostwizard.lu{uri} permanent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ─── Store subdomains (wildcard — all except wizard.lu) ──────
|
||||||
|
*.omsflow.lu {
|
||||||
|
tls /etc/caddy/certs/omsflow.lu/cert.pem /etc/caddy/certs/omsflow.lu/key.pem
|
||||||
|
reverse_proxy localhost:8001
|
||||||
|
}
|
||||||
|
|
||||||
|
*.rewardflow.lu {
|
||||||
|
tls /etc/caddy/certs/rewardflow.lu/cert.pem /etc/caddy/certs/rewardflow.lu/key.pem
|
||||||
|
reverse_proxy localhost:8001
|
||||||
|
}
|
||||||
|
|
||||||
|
*.hostwizard.lu {
|
||||||
|
tls /etc/caddy/certs/hostwizard.lu/cert.pem /etc/caddy/certs/hostwizard.lu/key.pem
|
||||||
|
reverse_proxy localhost:8001
|
||||||
|
}
|
||||||
|
|
||||||
# ─── Services (wizard.lu origin cert) ───────────────────────
|
# ─── Services (wizard.lu origin cert) ───────────────────────
|
||||||
api.wizard.lu {
|
api.wizard.lu {
|
||||||
tls /etc/caddy/certs/wizard.lu/cert.pem /etc/caddy/certs/wizard.lu/key.pem
|
tls /etc/caddy/certs/wizard.lu/cert.pem /etc/caddy/certs/wizard.lu/key.pem
|
||||||
|
|||||||
@@ -153,6 +153,19 @@ DEMO_COMPANIES = [
|
|||||||
"business_address": "12 Rue du Web, Differdange, L-4501, Luxembourg",
|
"business_address": "12 Rue du Web, Differdange, L-4501, Luxembourg",
|
||||||
"tax_number": "LU45678901",
|
"tax_number": "LU45678901",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "WizaMart S.à r.l.",
|
||||||
|
"description": "Online marketplace for everyday essentials and home goods",
|
||||||
|
"owner_email": "alex.owner@wizamart.com",
|
||||||
|
"owner_password": "password123", # noqa: SEC001
|
||||||
|
"owner_first_name": "Alex",
|
||||||
|
"owner_last_name": "Dupont",
|
||||||
|
"contact_email": "info@wizamart.com",
|
||||||
|
"contact_phone": "+352 567 890 123",
|
||||||
|
"website": "https://www.wizamart.com",
|
||||||
|
"business_address": "88 Route d'Arlon, Strassen, L-8008, Luxembourg",
|
||||||
|
"tax_number": "LU56789012",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Demo store configurations (linked to merchants by index)
|
# Demo store configurations (linked to merchants by index)
|
||||||
@@ -245,6 +258,17 @@ DEMO_STORES = [
|
|||||||
"theme_preset": "modern",
|
"theme_preset": "modern",
|
||||||
"custom_domain": None,
|
"custom_domain": None,
|
||||||
},
|
},
|
||||||
|
# WizaMart store (OMS platform, custom domain)
|
||||||
|
{
|
||||||
|
"merchant_index": 4, # WizaMart
|
||||||
|
"store_code": "WIZAMART",
|
||||||
|
"name": "WizaMart",
|
||||||
|
"subdomain": "wizamart",
|
||||||
|
"description": "Online marketplace for everyday essentials and home goods",
|
||||||
|
"theme_preset": "modern",
|
||||||
|
"custom_domain": "wizamart.com",
|
||||||
|
"custom_domain_platform": "oms",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Demo subscriptions (linked to merchants by index)
|
# Demo subscriptions (linked to merchants by index)
|
||||||
@@ -258,6 +282,8 @@ DEMO_SUBSCRIPTIONS = [
|
|||||||
{"merchant_index": 2, "platform_code": "oms", "tier_code": "business", "trial_days": 0},
|
{"merchant_index": 2, "platform_code": "oms", "tier_code": "business", "trial_days": 0},
|
||||||
# LuxWeb Agency: Hosting (professional, active)
|
# LuxWeb Agency: Hosting (professional, active)
|
||||||
{"merchant_index": 3, "platform_code": "hosting", "tier_code": "professional", "trial_days": 0},
|
{"merchant_index": 3, "platform_code": "hosting", "tier_code": "professional", "trial_days": 0},
|
||||||
|
# WizaMart: OMS (essential, trial)
|
||||||
|
{"merchant_index": 4, "platform_code": "oms", "tier_code": "essential", "trial_days": 30},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Demo team members (linked to merchants by index, assigned to stores by store_code)
|
# Demo team members (linked to merchants by index, assigned to stores by store_code)
|
||||||
|
|||||||
@@ -3758,6 +3758,19 @@ class ArchitectureValidator:
|
|||||||
context=line.strip()[:80],
|
context=line.strip()[:80],
|
||||||
suggestion="Use single quotes: x-data='func({{ data|tojson }})'",
|
suggestion="Use single quotes: x-data='func({{ data|tojson }})'",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# LANG-011: I18n.t() in templates (should use $t() or server-side _())
|
||||||
|
if "I18n.t(" in line:
|
||||||
|
self._add_violation(
|
||||||
|
rule_id="LANG-011",
|
||||||
|
rule_name="Use $t() not I18n.t() in HTML templates",
|
||||||
|
severity=Severity.ERROR,
|
||||||
|
file_path=file_path,
|
||||||
|
line_number=i,
|
||||||
|
message="I18n.t() is non-reactive in templates. Use $t() (reactive) or _() (server-side)",
|
||||||
|
context=line.strip()[:80],
|
||||||
|
suggestion="Replace I18n.t('key') with $t('key') or {{ _('key') }}",
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # Skip files that can't be read
|
pass # Skip files that can't be read
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user