#!/usr/bin/env python3 """ Atlas Academy — Odoo Lead Relay Listens on 127.0.0.1:7793, accepts POST /lead, creates crm.lead in Odoo. Also handles admin actions via _action field for the admin portal. """ import datetime import json import logging import os import smtplib import xmlrpc.client from email.mime.text import MIMEText from http.server import BaseHTTPRequestHandler, HTTPServer logging.basicConfig( level=logging.INFO, format='%(asctime)s [relay] %(levelname)s %(message)s' ) log = logging.getLogger('academy-relay') ODOO_URL = "http://100.93.16.81:8069" ODOO_DB = 'atlas' ODOO_CREDS = [(os.environ['ODOO_RELAY_LOGIN'], os.environ['ODOO_RELAY_PASS'])] UID_FILE = '/opt/atlas/academy/.odoo_uid' # Admin config ADMIN_PASS = os.environ.get('ACADEMY_ADMIN_PASS') # Notification config NOTIFY_SMTP_HOST = os.environ.get('LEAD_SMTP_HOST', 'smtp.gmail.com') NOTIFY_SMTP_PORT = int(os.environ.get('LEAD_SMTP_PORT', '587')) NOTIFY_SMTP_USER = os.environ.get('LEAD_SMTP_USER', 'chaibaarab98@gmail.com') NOTIFY_SMTP_PASS = os.environ.get('LEAD_SMTP_PASS') NOTIFY_SMTP_FROM = os.environ.get('LEAD_SMTP_FROM', 'chaibaarab98@gmail.com') NOTIFY_TO = os.environ.get('LEAD_NOTIFY_TO', 'chaib@atlascorporation.nl') def _notify_operator(name, email, interest, source, lead_id): body = ( f"Nieuwe Academy aanmelding!\n\n" f"Naam: {name}\n" f"E-mail: {email}\n" f"Interesse: {interest or '-'}\n" f"Bron: {source or '-'}\n" f"Lead ID: {lead_id or '-'}\n\n" f"-- Atlas Academy Lead Relay" ) try: msg = MIMEText(body) msg['Subject'] = f'Academy: {name} heeft zich aangemeld' msg['From'] = NOTIFY_SMTP_FROM msg['To'] = NOTIFY_TO s = smtplib.SMTP(NOTIFY_SMTP_HOST, NOTIFY_SMTP_PORT, timeout=10) s.starttls() s.login(NOTIFY_SMTP_USER, NOTIFY_SMTP_PASS) s.send_message(msg) s.quit() log.info('Notification sent to %s for lead %s', NOTIFY_TO, lead_id) except Exception as e: log.warning('Notification email failed: %s', e) def _load_uid(): if os.path.exists(UID_FILE): try: with open(UID_FILE) as f: data = json.load(f) return data.get('uid'), data.get('password') except Exception: pass return None, None def _save_uid(uid, password): with open(UID_FILE, 'w') as f: json.dump({'uid': uid, 'password': password}, f) def _get_odoo_uid(): uid, pw = _load_uid() if uid: return uid, pw common = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/common', allow_none=True) for login, password in ODOO_CREDS: try: uid = common.authenticate(ODOO_DB, login, password, {}) if uid: log.info('Odoo auth OK with %s (uid=%s)', login, uid) _save_uid(uid, password) return uid, password except Exception as e: log.warning('Odoo auth failed for %s: %s', login, e) return None, None def create_lead(name, email, interest, source): uid, pw = _get_odoo_uid() if not uid: log.error('Odoo auth failed for all credentials -- lead not created') return None, 'Odoo authentication failed' try: models = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/object', allow_none=True) lead_id = models.execute_kw( ODOO_DB, uid, pw, 'crm.lead', 'create', [{ 'name': f'Academy Lead: {name}', 'contact_name': name, 'email_from': email, 'description': f'Interesse: {interest} | Bron: {source}', }] ) log.info('Created crm.lead id=%s for %s (%s)', lead_id, name, email) _notify_operator(name, email, interest, source, lead_id) return lead_id, None except Exception as e: if os.path.exists(UID_FILE): os.remove(UID_FILE) log.error('Odoo create_lead error: %s', e) return None, str(e) def _query_leads(limit=100, offset=0): uid, pw = _get_odoo_uid() if not uid: return None, 'Odoo auth failed' try: models = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/object', allow_none=True) ids = models.execute_kw(ODOO_DB, uid, pw, 'crm.lead', 'search', [[('name', 'ilike', 'Academy Lead%')]], {'limit': limit, 'offset': offset}) if not ids: return [], None fields = ['id', 'name', 'contact_name', 'email_from', 'description', 'create_date'] leads = models.execute_kw(ODOO_DB, uid, pw, 'crm.lead', 'read', [ids], {'fields': fields}) return sorted(leads, key=lambda l: l.get('id', 0), reverse=True), None except Exception as e: return None, str(e) def _count_leads(): uid, pw = _get_odoo_uid() if not uid: return None, None, 'Odoo auth failed' try: models = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/object', allow_none=True) total = models.execute_kw(ODOO_DB, uid, pw, 'crm.lead', 'search_count', [[('name', 'ilike', 'Academy Lead%')]]) today_start = datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%d 00:00:00') today = models.execute_kw(ODOO_DB, uid, pw, 'crm.lead', 'search_count', [[('name', 'ilike', 'Academy Lead%'), ('create_date', '>=', today_start)]]) return total, today, None except Exception as e: return None, None, str(e) class Handler(BaseHTTPRequestHandler): def log_message(self, fmt, *args): log.info(fmt, *args) def _send_json(self, code, body): data = json.dumps(body).encode() self.send_response(code) self.send_header('Content-Type', 'application/json') self.send_header('Content-Length', str(len(data))) self.send_header('Access-Control-Allow-Origin', 'https://atlasacademy.nl') self.end_headers() self.wfile.write(data) def do_OPTIONS(self): self.send_response(204) self.send_header('Access-Control-Allow-Origin', 'https://atlasacademy.nl') self.send_header('Access-Control-Allow-Methods', 'POST, OPTIONS') self.send_header('Access-Control-Allow-Headers', 'Content-Type') self.end_headers() def _read_body(self, max_size=8192): length = int(self.headers.get('Content-Length', 0)) if length > max_size: return None, 'payload too large' try: return json.loads(self.rfile.read(length)), None except Exception: return None, 'invalid JSON' def do_GET(self): if self.path == '/health': self._send_json(200, {'ok': True, 'service': 'academy-relay'}) else: self._send_json(404, {'ok': False, 'err': 'not found'}) def do_POST(self): path = self.path.rstrip('/') if path not in ('/lead', '/academy-lead'): self._send_json(404, {'ok': False, 'err': 'not found'}) return body, err = self._read_body() if err: self._send_json(400, {'ok': False, 'err': err}) return # Admin DATA actions are DISABLED on the public endpoint (PII protection). # The gated admin portal (academy_admin.py, behind basic_auth on the atlas-01 hub) # reads Odoo directly via XML-RPC and does NOT use this endpoint for lead data. action = body.get('_action', '') if action: self._send_json(403, {'ok': False, 'err': 'admin actions are not available on the public endpoint'}) return # Default: create lead name = str(body.get('name', '')).strip()[:120] email = str(body.get('email', '')).strip()[:200] interest = str(body.get('interest', '')).strip()[:200] source = str(body.get('source', '')).strip()[:200] if not name or not email: self._send_json(400, {'ok': False, 'err': 'name and email required'}) return lead_id, err = create_lead(name, email, interest, source) if lead_id: self._send_json(200, {'ok': True, 'lead_id': lead_id}) else: log.warning('Returning ok:true despite error (email fallback active): %s', err) self._send_json(200, {'ok': True, 'lead_id': None, 'note': 'queued'}) if __name__ == '__main__': host, port = '127.0.0.1', 7793 server = HTTPServer((host, port), Handler) log.info('Atlas Academy Relay listening on %s:%d', host, port) server.serve_forever()