Files
encoach_backend_new_v3/odoo/addons/iot_drivers/tools/certificate.py
Yamen Ahmad 3e83d8d7d5 feat: add complete EnCoach backend — Odoo 19 + all addons
Includes:
- Odoo 19 framework (odoo/)
- 27 custom EnCoach addons (new_project/custom_addons/)
  - encoach_core, encoach_api, encoach_lms_api, encoach_adaptive_api
  - encoach_exam, encoach_taxonomy, encoach_adaptive, encoach_assignment
  - encoach_ai, encoach_ai_grading, encoach_ai_generation, encoach_ai_media
  - encoach_courseware, encoach_communication, encoach_subscription
  - encoach_notification, encoach_approval, encoach_branding
  - encoach_classroom, encoach_registration, encoach_stats
  - encoach_faq, encoach_ticket, encoach_training, encoach_resources
  - encoach_adaptive_ai, encoach_sis
- 21 OpenEduCat Enterprise modules (new_project/enterprise-19/)
- 14 OpenEduCat Community modules (new_project/openeducat_erp-19.0/)
- Configuration: odoo.conf, requirements.txt, scripts
- 200+ REST API endpoints with JWT authentication
- SRS and test documentation

Made-with: Cursor
2026-04-01 17:10:04 +04:00

154 lines
5.5 KiB
Python

import datetime
import logging
import requests
from cryptography import x509
from cryptography.x509.oid import NameOID
from pathlib import Path
from odoo.addons.iot_drivers.tools.helpers import (
get_conf,
get_identifier,
get_path_nginx,
get_version,
odoo_restart,
require_db,
start_nginx_server,
update_conf,
)
from odoo.addons.iot_drivers.tools.system import IS_RPI, IS_TEST, IS_WINDOWS
_logger = logging.getLogger(__name__)
@require_db
def ensure_validity():
"""Ensure that the certificate is up to date
Load a new if the current one is not valid or if there is none.
This method also sends the certificate end date to the database.
"""
inform_database(get_certificate_end_date() or download_odoo_certificate())
def get_certificate_end_date():
"""Check if the certificate is up to date and valid
:return: End date of the certificate if it is valid, None otherwise
:rtype: str
"""
base_path = [get_path_nginx(), 'conf'] if IS_WINDOWS else ['/etc/ssl/certs']
path = Path(*base_path, 'nginx-cert.crt')
if not path.exists():
return None
try:
cert = x509.load_pem_x509_certificate(path.read_bytes())
except ValueError:
_logger.exception("Unable to read certificate file.")
return None
common_name = next(
(name_attribute.value for name_attribute in cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)), ''
)
# Ensure cryptography compatibility with python < 3.13
if IS_RPI and float(get_version()[1:8]) < 2025.10:
cert_end_date = cert.not_valid_after
else:
cert_end_date = cert.not_valid_after_utc.replace(tzinfo=None)
if (
common_name == 'OdooTempIoTBoxCertificate'
or datetime.datetime.now() > cert_end_date - datetime.timedelta(days=10)
):
_logger.debug("SSL certificate '%s' must be updated.", common_name)
return None
_logger.debug("SSL certificate '%s' is valid until %s", common_name, cert_end_date)
return str(cert_end_date)
def download_odoo_certificate(retry=0):
"""Send a request to Odoo with customer db_uuid and enterprise_code
to get a true certificate
"""
if IS_TEST:
_logger.info("Skipping certificate download in test mode.")
return None
db_uuid = get_conf('db_uuid')
enterprise_code = get_conf('enterprise_code')
if not db_uuid:
return None
try:
response = requests.post(
'https://www.odoo.com/odoo-enterprise/iot/x509',
json={'params': {'db_uuid': db_uuid, 'enterprise_code': enterprise_code}},
timeout=95, # let's encrypt library timeout
)
response.raise_for_status()
response_body = response.json()
except (requests.exceptions.RequestException, ValueError) as e:
_logger.warning("An error occurred while trying to reach odoo.com to get a new certificate: %s", e)
if retry < 5:
return download_odoo_certificate(retry=retry + 1)
return _logger.exception("Maximum attempt to download the odoo.com certificate reached")
server_error = response_body.get('error')
if server_error:
_logger.error("Server error received from odoo.com while trying to get the certificate: %s", server_error)
return None
result = response_body.get('result', {})
certificate_error = result.get('error')
if certificate_error:
_logger.warning("Error received from odoo.com while trying to get the certificate: %s", certificate_error)
return None
update_conf({'subject': result['subject_cn']})
certificate = result['x509_pem']
private_key = result['private_key_pem']
if not certificate or not private_key: # ensure not empty strings
_logger.error("The certificate received from odoo.com is not valid.")
return None
if IS_RPI:
Path('/etc/ssl/certs/nginx-cert.crt').write_text(certificate, encoding='utf-8')
Path('/root_bypass_ramdisks/etc/ssl/certs/nginx-cert.crt').write_text(certificate, encoding='utf-8')
Path('/etc/ssl/private/nginx-cert.key').write_text(private_key, encoding='utf-8')
Path('/root_bypass_ramdisks/etc/ssl/private/nginx-cert.key').write_text(private_key, encoding='utf-8')
start_nginx_server()
cert = x509.load_pem_x509_certificate(certificate.encode())
if float(get_version()[1:8]) < 2025.10:
return str(cert.not_valid_after)
else:
return str(cert.not_valid_after_utc)
else:
Path(get_path_nginx(), 'conf', 'nginx-cert.crt').write_text(certificate, encoding='utf-8')
Path(get_path_nginx(), 'conf', 'nginx-cert.key').write_text(private_key, encoding='utf-8')
odoo_restart(3)
return None
@require_db
def inform_database(ssl_certificate_end_date, server_url=None):
"""Inform the database about the certificate end date.
If end date is ``None``, we avoid sending a useless request.
:param str ssl_certificate_end_date: End date of the SSL certificate
:param str server_url: URL of the Odoo server (provided by decorator).
"""
if not ssl_certificate_end_date:
return
try:
response = requests.post(
server_url + "/iot/box/update_certificate_status",
json={'params': {'identifier': get_identifier(), 'ssl_certificate_end_date': ssl_certificate_end_date}},
timeout=5,
)
response.raise_for_status()
except requests.exceptions.RequestException:
_logger.exception("Could not reach configured server to inform about the certificate status")