- 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
147 lines
5.2 KiB
Python
147 lines
5.2 KiB
Python
import logging
|
|
import time
|
|
|
|
import jwt as pyjwt
|
|
|
|
from odoo import http, fields
|
|
from odoo.http import request
|
|
from odoo.exceptions import AccessDenied
|
|
|
|
from .base import (
|
|
_json_response, _error_response, _get_json_body, _get_jwt_secret, validate_token,
|
|
)
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class EncoachAuthController(http.Controller):
|
|
|
|
# ------------------------------------------------------------------
|
|
# POST /api/login
|
|
# ------------------------------------------------------------------
|
|
@http.route('/api/login', type='http', auth='public',
|
|
methods=['POST'], csrf=False)
|
|
def login(self, **kw):
|
|
try:
|
|
body = _get_json_body()
|
|
login = (body.get('login') or body.get('email') or '').strip().lower()
|
|
password = body.get('password', '')
|
|
|
|
if not login or not password:
|
|
return _error_response('login and password are required', 400)
|
|
|
|
# Odoo 19: session.authenticate(env, credential_dict)
|
|
credential = {
|
|
'type': 'password',
|
|
'login': login,
|
|
'password': password,
|
|
}
|
|
try:
|
|
request.session.authenticate(request.env, credential)
|
|
uid = request.session.uid
|
|
if not uid:
|
|
return _error_response('Invalid email or password', 401)
|
|
except AccessDenied:
|
|
return _error_response('Invalid email or password', 401)
|
|
except Exception as auth_err:
|
|
_logger.warning('Auth error for %s: %s', login, auth_err)
|
|
return _error_response('Invalid email or password', 401)
|
|
|
|
user = request.env['res.users'].sudo().browse(uid)
|
|
|
|
# Generate JWT token
|
|
secret = _get_jwt_secret()
|
|
if not secret:
|
|
return _error_response('JWT not configured on server', 500)
|
|
|
|
token = pyjwt.encode(
|
|
{'user_id': user.id, 'exp': int(time.time()) + 86400},
|
|
secret, algorithm='HS256',
|
|
)
|
|
|
|
# Get permissions
|
|
permissions = []
|
|
if hasattr(user, 'get_all_permissions'):
|
|
permissions = user.get_all_permissions().mapped('code')
|
|
|
|
# Update last login
|
|
user.write({'last_login': fields.Datetime.now()})
|
|
|
|
return _json_response({
|
|
'token': token,
|
|
'user': self._user_to_dict(user),
|
|
'permissions': permissions,
|
|
})
|
|
|
|
except Exception as e:
|
|
_logger.exception('login failed')
|
|
return _error_response(str(e), 500)
|
|
|
|
# ------------------------------------------------------------------
|
|
# GET /api/user (returns current authenticated user)
|
|
# ------------------------------------------------------------------
|
|
@http.route('/api/user', type='http', auth='public',
|
|
methods=['GET'], csrf=False)
|
|
def get_current_user(self, **kw):
|
|
try:
|
|
user = validate_token()
|
|
if not user:
|
|
return _error_response('Authentication required', 401)
|
|
|
|
permissions = []
|
|
if hasattr(user, 'get_all_permissions'):
|
|
permissions = user.get_all_permissions().mapped('code')
|
|
|
|
return _json_response({
|
|
'user': self._user_to_dict(user),
|
|
'permissions': permissions,
|
|
})
|
|
|
|
except Exception as e:
|
|
_logger.exception('get_current_user failed')
|
|
return _error_response(str(e), 500)
|
|
|
|
# ------------------------------------------------------------------
|
|
# POST /api/logout
|
|
# ------------------------------------------------------------------
|
|
@http.route('/api/logout', type='http', auth='public',
|
|
methods=['POST'], csrf=False)
|
|
def logout(self, **kw):
|
|
# JWT is stateless — client clears token. Server just returns OK.
|
|
return _json_response({'ok': True})
|
|
|
|
# ------------------------------------------------------------------
|
|
# helpers
|
|
# ------------------------------------------------------------------
|
|
def _user_to_dict(self, user):
|
|
"""Convert res.users to the dict shape the React frontend expects."""
|
|
entities = []
|
|
if hasattr(user, 'entity_ids'):
|
|
entities = [
|
|
{'id': e.id, 'name': e.name, 'role': ''}
|
|
for e in user.entity_ids
|
|
]
|
|
|
|
classrooms = []
|
|
|
|
return {
|
|
'id': user.id,
|
|
'name': user.name or '',
|
|
'email': user.email or '',
|
|
'login': user.login or '',
|
|
'user_type': getattr(user, '_api_user_type', lambda: user.user_type or 'student')()
|
|
if callable(getattr(user, '_api_user_type', None))
|
|
else (user.user_type or 'student'),
|
|
'avatar': bool(getattr(user, 'encoach_avatar', False)),
|
|
'phone': user.phone or '',
|
|
'country': '',
|
|
'timezone': '',
|
|
'bio': '',
|
|
'gender': getattr(user, 'gender', '') or '',
|
|
'student_id': '',
|
|
'is_verified': getattr(user, 'is_verified', False),
|
|
'entities': entities,
|
|
'classrooms': classrooms,
|
|
'expiry_date': '',
|
|
}
|