feat: initial backend codebase — EnCoach v3

Complete Odoo 19 backend with 25 custom addons:
- encoach_core: user/entity/role management
- encoach_api: REST API + JWT auth
- encoach_ai: OpenAI integration, AI settings, generation
- encoach_ai_course: AI-powered English & IELTS course generation
- encoach_exam_template/session: exam creation, structures, sessions
- encoach_scoring: AI auto-grading + manual approval
- encoach_vector: pgvector RAG integration
- encoach_adaptive: adaptive learning engine
- encoach_placement: placement testing
- encoach_taxonomy/resources: content taxonomy & resource management
- Plus 14 more modules for courses, branding, portal, etc.

Includes docs: user guide, generation report, developer workflow.

Made-with: Cursor
This commit is contained in:
Yamen Ahmad
2026-04-11 15:44:20 +04:00
commit 982d4bca30
371 changed files with 35211 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
from . import captcha_service
from . import otp_service

View File

@@ -0,0 +1,33 @@
import requests
from odoo import api, SUPERUSER_ID
class CaptchaService:
def __init__(self, env):
self.env = env
def verify(self, token):
"""Verify CAPTCHA token against configured provider."""
ICP = self.env['ir.config_parameter'].sudo()
secret_key = ICP.get_param('encoach.captcha_secret_key', '')
provider = ICP.get_param('encoach.captcha_provider', 'recaptcha')
if provider == 'recaptcha':
url = 'https://www.google.com/recaptcha/api/siteverify'
elif provider == 'hcaptcha':
url = 'https://hcaptcha.com/siteverify'
elif provider == 'turnstile':
url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'
else:
return False
try:
resp = requests.post(url, data={
'secret': secret_key,
'response': token,
}, timeout=10)
result = resp.json()
return result.get('success', False)
except Exception:
return False

View File

@@ -0,0 +1,59 @@
import hashlib
import random
import string
from datetime import timedelta
from odoo import fields
class OtpService:
def __init__(self, env):
self.env = env
def _hash_otp(self, otp_code):
return hashlib.sha256(otp_code.encode()).hexdigest()
def generate(self, email):
"""Create a 6-digit OTP, store its SHA-256 hash, return plaintext."""
otp_code = ''.join(random.choices(string.digits, k=6))
otp_hash = self._hash_otp(otp_code)
expires_at = fields.Datetime.now() + timedelta(minutes=15)
self.env['encoach.otp'].sudo().create({
'email': email,
'otp_hash': otp_hash,
'expires_at': expires_at,
})
return otp_code
def verify(self, email, otp_code):
"""Find unexpired, unused record and check SHA-256 hash match."""
otp_hash = self._hash_otp(otp_code)
now = fields.Datetime.now()
record = self.env['encoach.otp'].sudo().search([
('email', '=', email),
('otp_hash', '=', otp_hash),
('used', '=', False),
('expires_at', '>=', now),
], limit=1, order='created_at desc')
if record:
record.write({'used': True})
return True
return False
def can_resend(self, email):
"""Check if resend_count < 3 for the latest OTP."""
record = self.env['encoach.otp'].sudo().search([
('email', '=', email),
], limit=1, order='created_at desc')
return record and record.resend_count < 3
def mark_resend(self, email):
"""Increment resend_count on the latest OTP for this email."""
record = self.env['encoach.otp'].sudo().search([
('email', '=', email),
], limit=1, order='created_at desc')
if record:
record.write({'resend_count': record.resend_count + 1})