feat(v3): restructure project + add complete frontend

- Restructure: move backend from new_project/ to backend/
- Add full React/TypeScript frontend (37 pages, 17 services, 16 type defs, 11 query hooks)
- Add docs/ with SRS specs, user stories, and workflow documentation
- Update .gitignore for new directory layout

Workflows implemented:
  WF1 User Signup, WF2 Placement Test, WF3 Exam Configuration,
  WF4 General English Exam, WF5 Course Generation,
  WF6 Entity Student Onboarding, AI Course Generation,
  Adaptive Learning Engine UI, White-Label Branding, Score Release

Made-with: Cursor
This commit is contained in:
Yamen Ahmad
2026-04-10 17:26:42 +04:00
commit 907a5c0e92
331 changed files with 23511 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})