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:
2
custom_addons/encoach_signup/services/__init__.py
Normal file
2
custom_addons/encoach_signup/services/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import captcha_service
|
||||
from . import otp_service
|
||||
33
custom_addons/encoach_signup/services/captcha_service.py
Normal file
33
custom_addons/encoach_signup/services/captcha_service.py
Normal 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
|
||||
59
custom_addons/encoach_signup/services/otp_service.py
Normal file
59
custom_addons/encoach_signup/services/otp_service.py
Normal 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})
|
||||
Reference in New Issue
Block a user