first push message
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
from . import trial_controller
|
||||
from . import tenant_router
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,80 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tenant Request Routing Pipeline (ដំណើរការ Request)
|
||||
-----------------------------------------------------
|
||||
1. User Request -> company1.domain-name.com
|
||||
2. DNS Resolution -> Load Balancer IP
|
||||
3. Load Balancer -> Route to available Worker
|
||||
4. Worker -> Read database name from subdomain
|
||||
5. Database Router -> Connect to correct tenant database
|
||||
6. Process Request -> Return response
|
||||
|
||||
Steps 1-3 happen OUTSIDE Odoo, at the infrastructure layer:
|
||||
- DNS: wildcard A/CNAME record *.domain-name.com -> Load Balancer IP
|
||||
- Load Balancer (nginx/HAProxy/Traefik) terminates TLS and forwards
|
||||
to one of N Odoo worker processes/pods, e.g.:
|
||||
|
||||
nginx.conf snippet:
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name *.domain-name.com;
|
||||
location / {
|
||||
proxy_pass http://odoo_workers;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
upstream odoo_workers {
|
||||
server worker-1:8069;
|
||||
server worker-2:8069;
|
||||
server worker-3:8069;
|
||||
}
|
||||
|
||||
Steps 4-6 are implemented in Odoo itself via this controller, which reads
|
||||
the subdomain from the Host header and switches `request.session.db`
|
||||
before any other controller/model code executes. Odoo natively supports
|
||||
this via the `dbfilter` config option (odoo.conf):
|
||||
|
||||
dbfilter = ^%h$
|
||||
|
||||
`%h` is replaced by the Host header at request time, so Odoo automatically
|
||||
maps "company1.domain-name.com" -> database "company1" PROVIDED the
|
||||
database name matches the first subdomain label. Since our generated
|
||||
db_name (see saas.database._generate_unique_db_name) IS the first label
|
||||
of the subdomain, `dbfilter = ^%h$` alone satisfies steps 4-5 for the
|
||||
standard Odoo multi-database filter mechanism.
|
||||
|
||||
The explicit controller below is only needed if you want CUSTOM routing
|
||||
logic beyond dbfilter (e.g. custom error pages for suspended/expired
|
||||
tenants, or a non-Odoo-native subdomain naming scheme).
|
||||
"""
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class TenantRouterController(http.Controller):
|
||||
|
||||
@http.route('/saas/tenant-status', type='json', auth='public')
|
||||
def tenant_status(self, **kw):
|
||||
"""Optional health endpoint the Load Balancer / monitoring system
|
||||
can call to verify a tenant database is reachable and active
|
||||
before routing traffic to it (step 3-4 sanity check).
|
||||
"""
|
||||
host = request.httprequest.host.split(':')[0]
|
||||
subdomain_label = host.split('.')[0]
|
||||
|
||||
database = request.env['saas.database'].sudo().search(
|
||||
[('db_name', '=', subdomain_label)], limit=1
|
||||
)
|
||||
if not database:
|
||||
return {'status': 'not_found'}
|
||||
|
||||
subscription = database.trial_request_id.subscription_id
|
||||
if subscription and subscription.expiry_date and subscription.expiry_date < http.fields.Datetime.now():
|
||||
return {'status': 'expired', 'db_name': database.db_name}
|
||||
|
||||
return {
|
||||
'status': 'active' if database.state == 'ready' else database.state,
|
||||
'db_name': database.db_name,
|
||||
'worker_node': database.worker_node,
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class SaasTrialController(http.Controller):
|
||||
"""Flow 1: User Registration & App Selection Flow
|
||||
Handles: domain-name.com/trial
|
||||
"""
|
||||
|
||||
@http.route('/trial', type='http', auth='public', website=True, sitemap=True)
|
||||
def trial_signup_page(self, **kw):
|
||||
"""Step: Choose your Apps
|
||||
Mirrors odoo.com/trial - shows ONLY real installed apps
|
||||
(saas.app is synced from ir.module.module where
|
||||
state='installed' and application=True), grouped by category,
|
||||
exactly like the reference screenshot.
|
||||
"""
|
||||
apps = request.env['saas.app'].sudo().search([('active', '=', True)])
|
||||
|
||||
groups = {}
|
||||
order = []
|
||||
for app in apps:
|
||||
cat_name = app.category_id.name or 'Other'
|
||||
if cat_name not in groups:
|
||||
groups[cat_name] = []
|
||||
order.append(cat_name)
|
||||
groups[cat_name].append(app)
|
||||
|
||||
app_groups = [(cat, groups[cat]) for cat in order]
|
||||
countries = request.env['res.country'].sudo().search([], order='name')
|
||||
languages = request.env['res.lang'].sudo().get_installed()
|
||||
|
||||
return request.render('saas_trial_portal.trial_signup_template', {
|
||||
'app_groups': app_groups,
|
||||
'countries': countries,
|
||||
'languages': languages,
|
||||
})
|
||||
|
||||
@http.route('/trial/submit', type='http', auth='public', website=True, methods=['POST'])
|
||||
def trial_signup_submit(self, **post):
|
||||
import json
|
||||
app_ids_raw = post.get('app_ids_json') or '[]'
|
||||
try:
|
||||
app_ids = [int(a) for a in json.loads(app_ids_raw)]
|
||||
except (ValueError, TypeError):
|
||||
app_ids = []
|
||||
|
||||
trial = request.env['saas.trial.request'].sudo().create({
|
||||
'name': post.get('name'),
|
||||
'company_name': post.get('company_name'),
|
||||
'email': post.get('email'),
|
||||
'phone': post.get('phone'),
|
||||
'country_id': int(post['country_id']) if post.get('country_id') else False,
|
||||
'lang': post.get('lang') or False,
|
||||
'company_size': post.get('company_size') or False,
|
||||
'primary_interest': post.get('primary_interest') or False,
|
||||
'app_ids': [(6, 0, app_ids)],
|
||||
})
|
||||
trial.action_submit_and_send_verification()
|
||||
return request.render('saas_trial_portal.trial_check_email_template', {
|
||||
'email': trial.email,
|
||||
})
|
||||
|
||||
@http.route('/trial/verify', type='http', auth='public', website=True)
|
||||
def trial_verify_email(self, token=None, id=None, **kw):
|
||||
trial = request.env['saas.trial.request'].sudo().browse(int(id)) if id else None
|
||||
if not trial or not trial.exists():
|
||||
return request.render('saas_trial_portal.trial_error_template', {
|
||||
'message': 'Invalid verification link.'
|
||||
})
|
||||
trial.action_verify_email(token)
|
||||
return request.render('saas_trial_portal.trial_activated_template', {
|
||||
'trial': trial,
|
||||
})
|
||||
|
||||
@http.route('/trial/continue', type='http', auth='public', website=True, methods=['POST'])
|
||||
def trial_continue(self, id=None, **kw):
|
||||
trial = request.env['saas.trial.request'].sudo().browse(int(id))
|
||||
database = trial.action_continue_to_provisioning()
|
||||
return request.render('saas_trial_portal.trial_provisioning_template', {
|
||||
'trial': trial,
|
||||
'database': database,
|
||||
})
|
||||
Reference in New Issue
Block a user