Tower: upload cetmix_tower_webhook 16.0.1.0.5 (via marketplace)
This commit is contained in:
250
addons/cetmix_tower_webhook/controllers/main.py
Normal file
250
addons/cetmix_tower_webhook/controllers/main.py
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
# Copyright (C) 2025 Cetmix OÜ
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from odoo import http
|
||||||
|
from odoo.http import Response, request
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CetmixTowerWebhookController(http.Controller):
|
||||||
|
"""
|
||||||
|
Handles incoming requests to Tower webhooks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@http.route(
|
||||||
|
["/cetmix_tower_webhooks/<string:endpoint>"],
|
||||||
|
type="http",
|
||||||
|
auth="public",
|
||||||
|
methods=["POST", "GET"],
|
||||||
|
csrf=False,
|
||||||
|
save_session=False,
|
||||||
|
)
|
||||||
|
def cetmix_webhook(self, endpoint, **kwargs):
|
||||||
|
"""
|
||||||
|
Process an incoming webhook request.
|
||||||
|
|
||||||
|
Workflow:
|
||||||
|
1. Extract request headers, body, and HTTP method.
|
||||||
|
2. Match the request against a registered webhook.
|
||||||
|
3. Authenticate the request if required.
|
||||||
|
4. Execute the webhook code.
|
||||||
|
5. Log the request and return the response.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
endpoint (str): The requested webhook endpoint.
|
||||||
|
**kwargs: Additional request parameters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: HTTP JSON response containing the result message.
|
||||||
|
"""
|
||||||
|
# Step 1: Extract request data
|
||||||
|
headers = self._extract_webhook_request_headers()
|
||||||
|
raw_data = self._extract_webhook_request_raw_data()
|
||||||
|
http_method = request.httprequest.method.lower()
|
||||||
|
|
||||||
|
# Step 2. Find webhook
|
||||||
|
webhook = (
|
||||||
|
request.env["cx.tower.webhook"]
|
||||||
|
.sudo()
|
||||||
|
.search(
|
||||||
|
[
|
||||||
|
("endpoint", "=", endpoint),
|
||||||
|
("method", "=", http_method),
|
||||||
|
("active", "=", True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
payload = self._extract_webhook_request_payload(webhook)
|
||||||
|
|
||||||
|
log_model = request.env["cx.tower.webhook.log"].sudo()
|
||||||
|
log_values = log_model._prepare_values(
|
||||||
|
webhook=webhook,
|
||||||
|
endpoint=endpoint,
|
||||||
|
request_method=http_method,
|
||||||
|
request_payload=payload,
|
||||||
|
request_headers=headers,
|
||||||
|
authentication_status="not_required",
|
||||||
|
code_status="skipped",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not webhook:
|
||||||
|
log_values.update(
|
||||||
|
{
|
||||||
|
"authentication_status": "failed",
|
||||||
|
"http_status": 404,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return self._finalize_webhook_response(
|
||||||
|
message="Webhook not found",
|
||||||
|
error_message="Webhook not found",
|
||||||
|
**log_values,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 3. Authenticate
|
||||||
|
auth_status, auth_error, http_auth_code = "success", None, 200
|
||||||
|
if webhook.authenticator_id:
|
||||||
|
if not webhook.authenticator_id.is_ip_allowed(self._get_remote_addr()):
|
||||||
|
auth_status, auth_error, http_auth_code = (
|
||||||
|
"failed",
|
||||||
|
"Address not allowed",
|
||||||
|
403,
|
||||||
|
)
|
||||||
|
log_values.update(
|
||||||
|
{
|
||||||
|
"error_message": auth_error,
|
||||||
|
"http_status": http_auth_code,
|
||||||
|
"authentication_status": auth_status,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return self._finalize_webhook_response(
|
||||||
|
message=auth_error,
|
||||||
|
**log_values,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with request.env.cr.savepoint():
|
||||||
|
auth_result = webhook.authenticator_id.sudo().authenticate(
|
||||||
|
headers=headers,
|
||||||
|
raw_data=raw_data,
|
||||||
|
payload=payload,
|
||||||
|
)
|
||||||
|
if not auth_result.get("allowed"):
|
||||||
|
raise Exception(
|
||||||
|
auth_result.get("message", "Authentication not allowed")
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
auth_status, auth_error, http_auth_code = "failed", str(e), 403
|
||||||
|
else:
|
||||||
|
auth_status = "not_required"
|
||||||
|
|
||||||
|
if auth_status == "failed":
|
||||||
|
# Authentication failed
|
||||||
|
log_values.update(
|
||||||
|
{
|
||||||
|
"error_message": auth_error,
|
||||||
|
"http_status": http_auth_code,
|
||||||
|
"authentication_status": auth_status,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return self._finalize_webhook_response(
|
||||||
|
message=auth_error,
|
||||||
|
**log_values,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 4. Execute webhook code
|
||||||
|
code_status, error_message, http_code, message = "success", None, 200, "OK"
|
||||||
|
try:
|
||||||
|
with request.env.cr.savepoint():
|
||||||
|
code_result = webhook.execute(payload, headers=headers)
|
||||||
|
if code_result.get("exit_code") != 0:
|
||||||
|
raise Exception(code_result.get("message"))
|
||||||
|
message = code_result.get("message") or "OK"
|
||||||
|
except Exception as e:
|
||||||
|
code_status, error_message, http_code, message = "failed", str(e), 500, None
|
||||||
|
|
||||||
|
# Step 5. Update log
|
||||||
|
log_values.update(
|
||||||
|
{
|
||||||
|
"code_status": code_status,
|
||||||
|
"error_message": error_message,
|
||||||
|
"http_status": http_code,
|
||||||
|
"result_message": message,
|
||||||
|
"authentication_status": auth_status,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._finalize_webhook_response(
|
||||||
|
message=message or error_message or "", **log_values
|
||||||
|
)
|
||||||
|
|
||||||
|
def _extract_webhook_request_payload(self, webhook):
|
||||||
|
"""
|
||||||
|
Extract the request payload depending on HTTP method and content type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
webhook (cx.tower.webhook): Webhook record with configuration
|
||||||
|
(may be empty).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Parsed payload as a dictionary. Empty if parsing fails.
|
||||||
|
"""
|
||||||
|
http_method = request.httprequest.method
|
||||||
|
try:
|
||||||
|
if http_method.upper() == "POST":
|
||||||
|
content_type = webhook.content_type if webhook else "json"
|
||||||
|
return self._get_payload_by_content_type(content_type)
|
||||||
|
elif http_method.upper() == "GET":
|
||||||
|
return request.httprequest.args.to_dict(flat=True)
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _get_payload_by_content_type(self, content_type):
|
||||||
|
"""
|
||||||
|
Return the request payload for POST requests according to content type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content_type (str): Payload format, e.g. "json" or "form".
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Parsed payload as a dictionary.
|
||||||
|
"""
|
||||||
|
if content_type == "form":
|
||||||
|
return request.httprequest.form.to_dict(flat=True)
|
||||||
|
data = request.httprequest.data
|
||||||
|
return json.loads(data or "{}") if data else {}
|
||||||
|
|
||||||
|
def _extract_webhook_request_headers(self):
|
||||||
|
"""
|
||||||
|
Extract request headers.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Request headers as a dictionary.
|
||||||
|
"""
|
||||||
|
return dict(request.httprequest.headers)
|
||||||
|
|
||||||
|
def _extract_webhook_request_raw_data(self):
|
||||||
|
"""
|
||||||
|
Return raw request body.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bytes: Raw HTTP request body.
|
||||||
|
"""
|
||||||
|
return request.httprequest.data
|
||||||
|
|
||||||
|
def _finalize_webhook_response(self, message, **kwargs):
|
||||||
|
"""
|
||||||
|
Create a log entry and return final HTTP response.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message (str): Response message text.
|
||||||
|
**kwargs: Log values for `cx.tower.webhook.log`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: HTTP JSON response with message and status code.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with request.env.cr.savepoint():
|
||||||
|
request.env["cx.tower.webhook.log"].sudo().create_from_call(**kwargs)
|
||||||
|
except Exception:
|
||||||
|
# don't break controller if logging fails
|
||||||
|
_logger.error("Failed to create log entry", exc_info=True)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
status=kwargs.get("http_status"),
|
||||||
|
response=json.dumps({"message": message or ""}),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_remote_addr(self):
|
||||||
|
"""
|
||||||
|
Return the remote IP address of the current request.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Remote client IP address.
|
||||||
|
"""
|
||||||
|
return request.httprequest.remote_addr
|
||||||
Reference in New Issue
Block a user