|
|
@ -1,47 +1,40 @@ |
|
|
|
# -*- coding: utf-8 -*- |
|
|
|
############################################################################# |
|
|
|
# |
|
|
|
# Cybrosys Technologies Pvt. Ltd. |
|
|
|
# |
|
|
|
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|
|
|
# Author: Ayana KP (odoo@cybrosys.com) |
|
|
|
# Modified by: Broigm - Improvements in authentication and structure |
|
|
|
# |
|
|
|
# You can modify it under the terms of the GNU LESSER |
|
|
|
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|
|
|
# |
|
|
|
# This program is distributed in the hope that it will be useful, |
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
|
|
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|
|
|
# |
|
|
|
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|
|
|
# (LGPL v3) along with this program. |
|
|
|
# If not, see <http://www.gnu.org/licenses/>. |
|
|
|
# |
|
|
|
############################################################################# |
|
|
|
import json |
|
|
|
import logging |
|
|
|
import base64 |
|
|
|
import ast |
|
|
|
from datetime import datetime, date, timedelta |
|
|
|
from odoo import http |
|
|
|
from odoo import http, fields |
|
|
|
from odoo.http import request |
|
|
|
from werkzeug.exceptions import BadRequest, Unauthorized, NotFound, MethodNotAllowed |
|
|
|
from .jwt_auth import JWTAuthMixin |
|
|
|
|
|
|
|
_logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
|
|
|
class RestApi(http.Controller): |
|
|
|
"""Controlador API REST mejorado con autenticación basada en API Key solamente""" |
|
|
|
class RestApi(http.Controller, JWTAuthMixin): |
|
|
|
"""Controlador API REST mejorado con JWT y filtrado avanzado""" |
|
|
|
|
|
|
|
def _json_response(self, data, status=200): |
|
|
|
"""Genera respuesta JSON estandarizada""" |
|
|
|
response = request.make_response( |
|
|
|
json.dumps(data, ensure_ascii=False, indent=2), |
|
|
|
headers=[('Content-Type', 'application/json')] |
|
|
|
) |
|
|
|
response.status_code = status |
|
|
|
return response |
|
|
|
try: |
|
|
|
response = request.make_response( |
|
|
|
json.dumps(data, ensure_ascii=False, indent=2, default=str), |
|
|
|
headers=[('Content-Type', 'application/json; charset=utf-8')] |
|
|
|
) |
|
|
|
response.status_code = status |
|
|
|
return response |
|
|
|
except Exception as e: |
|
|
|
_logger.error(f"Error creating JSON response: {str(e)}") |
|
|
|
fallback_data = { |
|
|
|
'error': True, |
|
|
|
'message': 'Error interno creando respuesta JSON', |
|
|
|
'status_code': 500 |
|
|
|
} |
|
|
|
return request.make_response( |
|
|
|
json.dumps(fallback_data, indent=2), |
|
|
|
status=500, |
|
|
|
headers=[('Content-Type', 'application/json; charset=utf-8')] |
|
|
|
) |
|
|
|
|
|
|
|
def _error_response(self, message, status=400, error_code=None): |
|
|
|
"""Genera respuesta de error estandarizada""" |
|
|
@ -55,41 +48,6 @@ class RestApi(http.Controller): |
|
|
|
|
|
|
|
return self._json_response(error_data, status) |
|
|
|
|
|
|
|
def _authenticate_api_key(self, api_key): |
|
|
|
""" |
|
|
|
Autentica usando solo la API key y configura la sesión del usuario |
|
|
|
Returns: (success: bool, user_id: int or None, error_message: str or None) |
|
|
|
""" |
|
|
|
if not api_key: |
|
|
|
return False, None, "API Key no proporcionada" |
|
|
|
|
|
|
|
try: |
|
|
|
user = request.env['res.users'].sudo().search([ |
|
|
|
('api_key', '=', api_key), |
|
|
|
('active', '=', True) |
|
|
|
], limit=1) |
|
|
|
|
|
|
|
if not user: |
|
|
|
return False, None, "API Key inválida o usuario inactivo" |
|
|
|
|
|
|
|
# Verificar si la API key no ha expirado (si implementas expiración) |
|
|
|
if hasattr(user, 'api_key_expiry') and user.api_key_expiry: |
|
|
|
if user.api_key_expiry < datetime.now(): |
|
|
|
return False, None, "API Key expirada" |
|
|
|
|
|
|
|
# Configurar la sesión con el usuario autenticado |
|
|
|
request.session.uid = user.id |
|
|
|
request.env.user = user |
|
|
|
|
|
|
|
# Actualizar estadísticas de uso |
|
|
|
user.update_api_key_usage() |
|
|
|
|
|
|
|
return True, user.id, None |
|
|
|
|
|
|
|
except Exception as e: |
|
|
|
_logger.error(f"Error en autenticación API: {str(e)}") |
|
|
|
return False, None, "Error interno de autenticación" |
|
|
|
|
|
|
|
def _serialize_record_values(self, records): |
|
|
|
"""Serializa los valores de los registros para JSON""" |
|
|
|
if not records: |
|
|
@ -99,102 +57,170 @@ class RestApi(http.Controller): |
|
|
|
for record in records: |
|
|
|
serialized_record = {} |
|
|
|
for key, value in record.items(): |
|
|
|
if isinstance(value, (datetime, date)): |
|
|
|
serialized_record[key] = value.isoformat() |
|
|
|
elif isinstance(value, bytes): |
|
|
|
serialized_record[key] = base64.b64encode(value).decode('utf-8') |
|
|
|
elif hasattr(value, '__iter__') and not isinstance(value, (str, dict)): |
|
|
|
try: |
|
|
|
serialized_record[key] = list(value) if value else [] |
|
|
|
except: |
|
|
|
serialized_record[key] = str(value) |
|
|
|
else: |
|
|
|
serialized_record[key] = value |
|
|
|
try: |
|
|
|
if isinstance(value, (datetime, date)): |
|
|
|
serialized_record[key] = value.isoformat() |
|
|
|
elif isinstance(value, bytes): |
|
|
|
serialized_record[key] = base64.b64encode(value).decode('utf-8') |
|
|
|
elif isinstance(value, tuple) and len(value) == 2: |
|
|
|
# Para relaciones Many2one que vienen como (id, name) |
|
|
|
serialized_record[key] = list(value) |
|
|
|
elif hasattr(value, '__iter__') and not isinstance(value, (str, dict, bytes)): |
|
|
|
try: |
|
|
|
serialized_record[key] = list(value) if value else [] |
|
|
|
except: |
|
|
|
serialized_record[key] = str(value) |
|
|
|
else: |
|
|
|
serialized_record[key] = value |
|
|
|
except Exception as e: |
|
|
|
_logger.warning(f"Error serializing field {key}: {str(e)}") |
|
|
|
serialized_record[key] = str(value) if value is not None else None |
|
|
|
|
|
|
|
serialized_records.append(serialized_record) |
|
|
|
|
|
|
|
return serialized_records |
|
|
|
|
|
|
|
def _get_model_config(self, model_name): |
|
|
|
"""Obtiene la configuración de la API para un modelo""" |
|
|
|
model_obj = request.env['ir.model'].sudo().search([('model', '=', model_name)], limit=1) |
|
|
|
if not model_obj: |
|
|
|
return None, "Modelo no encontrado" |
|
|
|
try: |
|
|
|
model_obj = request.env['ir.model'].sudo().search([('model', '=', model_name)], limit=1) |
|
|
|
if not model_obj: |
|
|
|
return None, "Modelo no encontrado" |
|
|
|
|
|
|
|
api_config = request.env['connection.api'].sudo().search([ |
|
|
|
('model_id', '=', model_obj.id), |
|
|
|
('active', '=', True) |
|
|
|
], limit=1) |
|
|
|
api_config = request.env['connection.api'].sudo().search([ |
|
|
|
('model_id', '=', model_obj.id), |
|
|
|
('active', '=', True) |
|
|
|
], limit=1) |
|
|
|
|
|
|
|
if not api_config: |
|
|
|
return None, "Modelo no configurado para API REST" |
|
|
|
if not api_config: |
|
|
|
return None, "Modelo no configurado para API REST" |
|
|
|
|
|
|
|
return api_config, None |
|
|
|
return api_config, None |
|
|
|
except Exception as e: |
|
|
|
_logger.error(f"Error getting model config for {model_name}: {str(e)}") |
|
|
|
return None, f"Error obteniendo configuración del modelo: {str(e)}" |
|
|
|
|
|
|
|
def _parse_request_data(self, method): |
|
|
|
"""Parsea los datos de la request según el método HTTP""" |
|
|
|
"""Parsea los datos de la request con parámetros avanzados""" |
|
|
|
data = {} |
|
|
|
fields = [] |
|
|
|
domain = [] |
|
|
|
limit = None |
|
|
|
offset = None |
|
|
|
order = None |
|
|
|
|
|
|
|
if method == 'GET': |
|
|
|
# Para GET, intentar obtener campos del query string o JSON body |
|
|
|
query_params = dict(request.httprequest.args) |
|
|
|
try: |
|
|
|
if method == 'GET': |
|
|
|
query_params = dict(request.httprequest.args) |
|
|
|
|
|
|
|
try: |
|
|
|
if request.httprequest.data: |
|
|
|
json_data = json.loads(request.httprequest.data) |
|
|
|
data.update(json_data) |
|
|
|
if 'fields' in json_data: |
|
|
|
fields = json_data['fields'] |
|
|
|
except json.JSONDecodeError: |
|
|
|
pass |
|
|
|
|
|
|
|
if not fields and 'fields' in query_params: |
|
|
|
fields = [field.strip() for field in query_params['fields'].split(',')] |
|
|
|
|
|
|
|
elif method in ['POST', 'PUT']: |
|
|
|
try: |
|
|
|
if request.httprequest.data: |
|
|
|
data = json.loads(request.httprequest.data) |
|
|
|
if 'fields' in data: |
|
|
|
fields = data['fields'] |
|
|
|
else: |
|
|
|
return None, None, "No se proporcionaron datos JSON" |
|
|
|
except json.JSONDecodeError: |
|
|
|
return None, None, "JSON inválido" |
|
|
|
# Parsear domain |
|
|
|
if 'domain' in query_params: |
|
|
|
try: |
|
|
|
domain = ast.literal_eval(query_params['domain']) |
|
|
|
if not isinstance(domain, list): |
|
|
|
domain = [] |
|
|
|
except: |
|
|
|
_logger.warning("Invalid domain format, ignoring") |
|
|
|
domain = [] |
|
|
|
|
|
|
|
# Parsear fields |
|
|
|
if 'fields' in query_params: |
|
|
|
fields = [field.strip() for field in query_params['fields'].split(',') if field.strip()] |
|
|
|
|
|
|
|
return data, fields, None |
|
|
|
# Parsear limit |
|
|
|
if 'limit' in query_params: |
|
|
|
try: |
|
|
|
limit = int(query_params['limit']) |
|
|
|
if limit <= 0: |
|
|
|
limit = None |
|
|
|
except: |
|
|
|
pass |
|
|
|
|
|
|
|
# Parsear offset |
|
|
|
if 'offset' in query_params: |
|
|
|
try: |
|
|
|
offset = int(query_params['offset']) |
|
|
|
if offset < 0: |
|
|
|
offset = None |
|
|
|
except: |
|
|
|
pass |
|
|
|
|
|
|
|
# Parsear order |
|
|
|
if 'order' in query_params: |
|
|
|
order = query_params['order'].strip() |
|
|
|
if not order: |
|
|
|
order = None |
|
|
|
|
|
|
|
# También intentar JSON body para GET (opcional) |
|
|
|
try: |
|
|
|
if request.httprequest.data: |
|
|
|
json_data = json.loads(request.httprequest.data.decode('utf-8')) |
|
|
|
data.update(json_data) |
|
|
|
if 'fields' in json_data and not fields: |
|
|
|
fields = json_data['fields'] |
|
|
|
if 'domain' in json_data and not domain: |
|
|
|
domain = json_data['domain'] |
|
|
|
except: |
|
|
|
pass |
|
|
|
|
|
|
|
elif method in ['POST', 'PUT']: |
|
|
|
try: |
|
|
|
if request.httprequest.data: |
|
|
|
data = json.loads(request.httprequest.data.decode('utf-8')) |
|
|
|
if 'fields' in data: |
|
|
|
fields = data['fields'] |
|
|
|
else: |
|
|
|
return None, None, None, None, None, None, "No se proporcionaron datos JSON" |
|
|
|
except (json.JSONDecodeError, UnicodeDecodeError) as e: |
|
|
|
return None, None, None, None, None, None, f"JSON inválido: {str(e)}" |
|
|
|
|
|
|
|
return data, fields, domain, limit, offset, order, None |
|
|
|
except Exception as e: |
|
|
|
_logger.error(f"Error parsing request data: {str(e)}") |
|
|
|
return None, None, None, None, None, None, f"Error procesando datos de la request: {str(e)}" |
|
|
|
|
|
|
|
@http.route(['/api/v1/auth'], type='http', auth='none', methods=['POST'], csrf=False) |
|
|
|
def authenticate(self, **kw): |
|
|
|
"""Endpoint de autenticación que genera API key""" |
|
|
|
"""Endpoint de autenticación que genera JWT token""" |
|
|
|
try: |
|
|
|
data = json.loads(request.httprequest.data or '{}') |
|
|
|
except json.JSONDecodeError: |
|
|
|
if request.httprequest.data: |
|
|
|
data = json.loads(request.httprequest.data.decode('utf-8')) |
|
|
|
else: |
|
|
|
data = {} |
|
|
|
except (json.JSONDecodeError, UnicodeDecodeError): |
|
|
|
return self._error_response("JSON inválido", 400) |
|
|
|
|
|
|
|
username = data.get('username') or request.httprequest.headers.get('username') |
|
|
|
password = data.get('password') or request.httprequest.headers.get('password') |
|
|
|
database = data.get('database') or request.httprequest.headers.get('database', request.env.cr.dbname) |
|
|
|
database = data.get('database') or request.httprequest.headers.get('database') or request.env.cr.dbname |
|
|
|
expires_in = data.get('expires_in_hours', 24) |
|
|
|
|
|
|
|
if not all([username, password]): |
|
|
|
return self._error_response("Username y password son requeridos", 400) |
|
|
|
|
|
|
|
try: |
|
|
|
# Actualizar sesión con la base de datos |
|
|
|
request.session.update(http.get_default_session(), db=database) |
|
|
|
# Validar expires_in |
|
|
|
if not isinstance(expires_in, int) or expires_in < 1 or expires_in > 168: # Max 7 días |
|
|
|
expires_in = 24 |
|
|
|
|
|
|
|
try: |
|
|
|
# Autenticar credenciales |
|
|
|
auth_result = request.session.authenticate( |
|
|
|
database, |
|
|
|
{'login': username, 'password': password, 'type': 'password'} |
|
|
|
) |
|
|
|
credential = {'login': username, 'password': password, 'type': 'password'} |
|
|
|
|
|
|
|
auth_result = request.session.authenticate(database, credential) |
|
|
|
if not auth_result: |
|
|
|
return self._error_response("Credenciales inválidas", 401) |
|
|
|
|
|
|
|
# Generar o recuperar API key |
|
|
|
user = request.env['res.users'].browse(auth_result['uid']) |
|
|
|
api_key = user.generate_api_key() |
|
|
|
uid = auth_result['uid'] |
|
|
|
|
|
|
|
if not uid: |
|
|
|
return self._error_response("Credenciales inválidas", 401) |
|
|
|
|
|
|
|
# Generar JWT token |
|
|
|
user = request.env['res.users'].browse(uid) |
|
|
|
token = self._generate_jwt_token(uid, expires_in) |
|
|
|
|
|
|
|
if not token: |
|
|
|
return self._error_response("Error generando token de acceso", 500) |
|
|
|
|
|
|
|
response_data = { |
|
|
|
"success": True, |
|
|
@ -203,7 +229,9 @@ class RestApi(http.Controller): |
|
|
|
"user_id": user.id, |
|
|
|
"username": user.login, |
|
|
|
"name": user.name, |
|
|
|
"api_key": api_key, |
|
|
|
"access_token": token, |
|
|
|
"token_type": "Bearer", |
|
|
|
"expires_in": expires_in * 3600, # En segundos |
|
|
|
"database": database |
|
|
|
} |
|
|
|
} |
|
|
@ -214,16 +242,72 @@ class RestApi(http.Controller): |
|
|
|
_logger.error(f"Error en autenticación: {str(e)}") |
|
|
|
return self._error_response("Error interno de autenticación", 500) |
|
|
|
|
|
|
|
@http.route(['/api/v1/refresh'], type='http', auth='none', methods=['POST'], csrf=False) |
|
|
|
def refresh_token(self, **kw): |
|
|
|
"""Endpoint para refrescar un JWT token""" |
|
|
|
try: |
|
|
|
success, user_id, error_msg = self._authenticate_request() |
|
|
|
if not success: |
|
|
|
return self._error_response(error_msg, 401, "TOKEN_INVALID") |
|
|
|
|
|
|
|
# Generar nuevo token |
|
|
|
try: |
|
|
|
data = json.loads(request.httprequest.data.decode('utf-8')) if request.httprequest.data else {} |
|
|
|
expires_in = data.get('expires_in_hours', 24) |
|
|
|
if not isinstance(expires_in, int) or expires_in < 1 or expires_in > 168: |
|
|
|
expires_in = 24 |
|
|
|
except: |
|
|
|
expires_in = 24 |
|
|
|
|
|
|
|
new_token = self._generate_jwt_token(user_id, expires_in) |
|
|
|
if not new_token: |
|
|
|
return self._error_response("Error generando nuevo token", 500) |
|
|
|
|
|
|
|
user = request.env['res.users'].browse(user_id) |
|
|
|
response_data = { |
|
|
|
"success": True, |
|
|
|
"message": "Token renovado exitosamente", |
|
|
|
"data": { |
|
|
|
"access_token": new_token, |
|
|
|
"token_type": "Bearer", |
|
|
|
"expires_in": expires_in * 3600, |
|
|
|
"user_id": user.id, |
|
|
|
"username": user.login |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return self._json_response(response_data) |
|
|
|
|
|
|
|
except Exception as e: |
|
|
|
_logger.error(f"Error refreshing token: {str(e)}") |
|
|
|
return self._error_response("Error interno renovando token", 500) |
|
|
|
|
|
|
|
def _authenticate_request(self): |
|
|
|
""" |
|
|
|
Autentica la request usando JWT token |
|
|
|
Returns: (success: bool, user_id: int or None, error_message: str or None) |
|
|
|
""" |
|
|
|
# Buscar token en headers |
|
|
|
auth_header = request.httprequest.headers.get('Authorization') |
|
|
|
if not auth_header: |
|
|
|
# Fallback a headers alternativos para compatibilidad |
|
|
|
token = request.httprequest.headers.get('X-API-Key') or request.httprequest.headers.get('api-key') |
|
|
|
if token: |
|
|
|
auth_header = f"Bearer {token}" |
|
|
|
|
|
|
|
if not auth_header: |
|
|
|
return False, None, "Token de autorización no proporcionado (use Authorization: Bearer <token>)" |
|
|
|
|
|
|
|
return self._validate_jwt_token(auth_header) |
|
|
|
|
|
|
|
@http.route(['/api/v1/<model_name>', '/api/v1/<model_name>/<int:record_id>'], |
|
|
|
type='http', auth='none', methods=['GET', 'POST', 'PUT', 'DELETE'], csrf=False) |
|
|
|
def api_handler(self, model_name, record_id=None, **kw): |
|
|
|
"""Endpoint principal de la API REST""" |
|
|
|
"""Endpoint principal de la API REST con JWT y filtrado avanzado""" |
|
|
|
method = request.httprequest.method |
|
|
|
|
|
|
|
# Autenticación usando API key |
|
|
|
api_key = request.httprequest.headers.get('X-API-Key') or request.httprequest.headers.get('api-key') |
|
|
|
success, user_id, error_msg = self._authenticate_api_key(api_key) |
|
|
|
|
|
|
|
# Autenticación usando JWT |
|
|
|
success, user_id, error_msg = self._authenticate_request() |
|
|
|
if not success: |
|
|
|
return self._error_response(error_msg, 401, "AUTHENTICATION_FAILED") |
|
|
|
|
|
|
@ -243,56 +327,108 @@ class RestApi(http.Controller): |
|
|
|
if not method_permissions.get(method, False): |
|
|
|
return self._error_response(f"Método {method} no permitido para este modelo", 405, "METHOD_NOT_ALLOWED") |
|
|
|
|
|
|
|
# Parsear datos de la request |
|
|
|
data, fields, error_msg = self._parse_request_data(method) |
|
|
|
# Parsear datos de la request con parámetros avanzados |
|
|
|
data, fields, domain, limit, offset, order, error_msg = self._parse_request_data(method) |
|
|
|
if error_msg: |
|
|
|
return self._error_response(error_msg, 400, "INVALID_REQUEST_DATA") |
|
|
|
|
|
|
|
try: |
|
|
|
return self._handle_request(method, api_config.model_id.model, record_id, data, fields) |
|
|
|
return self._handle_request(method, api_config.model_id.model, record_id, data, fields, domain, limit, offset, order, api_config) |
|
|
|
except Exception as e: |
|
|
|
_logger.error(f"Error procesando request {method} para {model_name}: {str(e)}") |
|
|
|
return self._error_response("Error interno del servidor", 500, "INTERNAL_SERVER_ERROR") |
|
|
|
|
|
|
|
def _handle_request(self, method, model_name, record_id, data, fields): |
|
|
|
"""Maneja las diferentes operaciones CRUD""" |
|
|
|
model = request.env[model_name] |
|
|
|
|
|
|
|
if method == 'GET': |
|
|
|
return self._handle_get(model, record_id, fields) |
|
|
|
elif method == 'POST': |
|
|
|
return self._handle_post(model, data, fields) |
|
|
|
elif method == 'PUT': |
|
|
|
return self._handle_put(model, record_id, data, fields) |
|
|
|
elif method == 'DELETE': |
|
|
|
return self._handle_delete(model, record_id) |
|
|
|
|
|
|
|
def _handle_get(self, model, record_id, fields): |
|
|
|
"""Maneja requests GET""" |
|
|
|
def _handle_request(self, method, model_name, record_id, data, fields, domain, limit, offset, order, api_config=None): |
|
|
|
"""Maneja las diferentes operaciones CRUD con parámetros avanzados""" |
|
|
|
try: |
|
|
|
model = request.env[model_name] |
|
|
|
|
|
|
|
if method == 'GET': |
|
|
|
return self._handle_get(model, record_id, fields, domain, limit, offset, order, api_config) |
|
|
|
elif method == 'POST': |
|
|
|
return self._handle_post(model, data, fields) |
|
|
|
elif method == 'PUT': |
|
|
|
return self._handle_put(model, record_id, data, fields) |
|
|
|
elif method == 'DELETE': |
|
|
|
return self._handle_delete(model, record_id) |
|
|
|
except Exception as e: |
|
|
|
_logger.error(f"Error in _handle_request: {str(e)}") |
|
|
|
raise |
|
|
|
|
|
|
|
def _handle_get(self, model, record_id, fields, domain, limit, offset, order, api_config=None): |
|
|
|
"""Maneja requests GET con filtrado avanzado""" |
|
|
|
try: |
|
|
|
# Aplicar límites de la configuración |
|
|
|
max_limit = getattr(api_config, 'max_records_limit', 1000) if api_config else 1000 |
|
|
|
if limit and limit > max_limit: |
|
|
|
limit = max_limit |
|
|
|
|
|
|
|
if record_id: |
|
|
|
# Obtener registro específico |
|
|
|
domain = [('id', '=', record_id)] |
|
|
|
search_domain = [('id', '=', record_id)] |
|
|
|
search_fields = fields if fields else [] |
|
|
|
records = model.search_read(domain=search_domain, fields=search_fields) |
|
|
|
total_count = len(records) |
|
|
|
|
|
|
|
response_data = { |
|
|
|
"success": True, |
|
|
|
"count": len(records), |
|
|
|
"total": total_count, |
|
|
|
"data": self._serialize_record_values(records) |
|
|
|
} |
|
|
|
else: |
|
|
|
# Obtener todos los registros |
|
|
|
domain = [] |
|
|
|
# Obtener registros con filtros |
|
|
|
search_domain = domain if domain else [] |
|
|
|
search_fields = fields if fields else ['id', 'display_name'] |
|
|
|
|
|
|
|
records = model.search_read(domain=domain, fields=search_fields) |
|
|
|
serialized_records = self._serialize_record_values(records) |
|
|
|
# Contar total sin límite para metadatos |
|
|
|
try: |
|
|
|
total_count = model.search_count(search_domain) |
|
|
|
except: |
|
|
|
total_count = None |
|
|
|
|
|
|
|
response_data = { |
|
|
|
"success": True, |
|
|
|
"count": len(serialized_records), |
|
|
|
"data": serialized_records |
|
|
|
} |
|
|
|
# Búsqueda con parámetros |
|
|
|
search_params = { |
|
|
|
'domain': search_domain, |
|
|
|
'fields': search_fields |
|
|
|
} |
|
|
|
|
|
|
|
if limit: |
|
|
|
search_params['limit'] = limit |
|
|
|
if offset: |
|
|
|
search_params['offset'] = offset |
|
|
|
if order: |
|
|
|
search_params['order'] = order |
|
|
|
|
|
|
|
records = model.search_read(**search_params) |
|
|
|
|
|
|
|
response_data = { |
|
|
|
"success": True, |
|
|
|
"count": len(records), |
|
|
|
"data": self._serialize_record_values(records) |
|
|
|
} |
|
|
|
|
|
|
|
# Agregar metadatos de paginación |
|
|
|
if total_count is not None: |
|
|
|
response_data["total"] = total_count |
|
|
|
if offset: |
|
|
|
response_data["offset"] = offset |
|
|
|
if limit: |
|
|
|
response_data["limit"] = limit |
|
|
|
|
|
|
|
# Información de paginación |
|
|
|
if limit and total_count is not None: |
|
|
|
current_offset = offset or 0 |
|
|
|
has_more = (current_offset + limit) < total_count |
|
|
|
response_data["has_more"] = has_more |
|
|
|
if has_more: |
|
|
|
response_data["next_offset"] = current_offset + limit |
|
|
|
|
|
|
|
return self._json_response(response_data) |
|
|
|
|
|
|
|
except Exception as e: |
|
|
|
_logger.error(f"Error en GET: {str(e)}") |
|
|
|
return self._error_response("Error obteniendo registros", 500) |
|
|
|
return self._error_response(f"Error obteniendo registros: {str(e)}", 500) |
|
|
|
|
|
|
|
def _handle_post(self, model, data, fields): |
|
|
|
"""Maneja requests POST (crear)""" |
|
|
@ -305,12 +441,12 @@ class RestApi(http.Controller): |
|
|
|
# Obtener el registro creado con los campos especificados |
|
|
|
search_fields = fields if fields else ['id', 'display_name'] |
|
|
|
record_data = new_record.read(search_fields)[0] |
|
|
|
serialized_record = self._serialize_record_values([record_data]) |
|
|
|
|
|
|
|
response_data = { |
|
|
|
"success": True, |
|
|
|
"message": "Registro creado exitosamente", |
|
|
|
"data": serialized_record[0] if serialized_record else {} |
|
|
|
"count": 1, |
|
|
|
"data": self._serialize_record_values([record_data]) |
|
|
|
} |
|
|
|
|
|
|
|
return self._json_response(response_data, 201) |
|
|
@ -337,12 +473,12 @@ class RestApi(http.Controller): |
|
|
|
# Obtener el registro actualizado |
|
|
|
search_fields = fields if fields else ['id', 'display_name'] |
|
|
|
record_data = record.read(search_fields)[0] |
|
|
|
serialized_record = self._serialize_record_values([record_data]) |
|
|
|
|
|
|
|
response_data = { |
|
|
|
"success": True, |
|
|
|
"message": "Registro actualizado exitosamente", |
|
|
|
"data": serialized_record[0] if serialized_record else {} |
|
|
|
"count": 1, |
|
|
|
"data": self._serialize_record_values([record_data]) |
|
|
|
} |
|
|
|
|
|
|
|
return self._json_response(response_data) |
|
|
@ -384,9 +520,7 @@ class RestApi(http.Controller): |
|
|
|
@http.route(['/api/v1/models'], type='http', auth='none', methods=['GET'], csrf=False) |
|
|
|
def list_available_models(self, **kw): |
|
|
|
"""Endpoint para listar modelos disponibles en la API""" |
|
|
|
api_key = request.httprequest.headers.get('X-API-Key') or request.httprequest.headers.get('api-key') |
|
|
|
success, user_id, error_msg = self._authenticate_api_key(api_key) |
|
|
|
|
|
|
|
success, user_id, error_msg = self._authenticate_request() |
|
|
|
if not success: |
|
|
|
return self._error_response(error_msg, 401) |
|
|
|
|
|
|
@ -398,11 +532,18 @@ class RestApi(http.Controller): |
|
|
|
model_info = { |
|
|
|
"model": config.model_id.model, |
|
|
|
"name": config.model_id.name, |
|
|
|
"description": config.description or f"API REST para el modelo {config.model_id.name}", |
|
|
|
"methods": { |
|
|
|
"GET": config.is_get, |
|
|
|
"POST": config.is_post, |
|
|
|
"PUT": config.is_put, |
|
|
|
"DELETE": config.is_delete |
|
|
|
}, |
|
|
|
"max_records_limit": config.max_records_limit, |
|
|
|
"endpoints": { |
|
|
|
"collection": f"/api/v1/{config.model_id.model}", |
|
|
|
"item": f"/api/v1/{config.model_id.model}/{{id}}", |
|
|
|
"schema": f"/api/v1/schema/{config.model_id.model}" |
|
|
|
} |
|
|
|
} |
|
|
|
models_data.append(model_info) |
|
|
@ -416,6 +557,12 @@ class RestApi(http.Controller): |
|
|
|
"documentation": { |
|
|
|
"swagger_ui": f"{base_url}/api/v1/docs", |
|
|
|
"openapi_spec": f"{base_url}/api/v1/openapi.json" |
|
|
|
}, |
|
|
|
"authentication": { |
|
|
|
"type": "JWT Bearer Token", |
|
|
|
"header": "Authorization: Bearer <token>", |
|
|
|
"auth_endpoint": f"{base_url}/api/v1/auth", |
|
|
|
"refresh_endpoint": f"{base_url}/api/v1/refresh" |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
@ -425,21 +572,86 @@ class RestApi(http.Controller): |
|
|
|
_logger.error(f"Error listando modelos: {str(e)}") |
|
|
|
return self._error_response("Error interno del servidor", 500) |
|
|
|
|
|
|
|
@http.route(['/api/v1/health'], type='http', auth='none', methods=['GET'], csrf=False) |
|
|
|
def health_check(self, **kwargs): |
|
|
|
"""Endpoint de verificación de salud de la API""" |
|
|
|
try: |
|
|
|
# Verificar conexión a BD |
|
|
|
request.env.cr.execute("SELECT 1") |
|
|
|
|
|
|
|
# Verificar configuraciones activas |
|
|
|
active_configs = len(request.env["connection.api"].sudo().search([("active", "=", True)])) |
|
|
|
|
|
|
|
# Verificar configuración JWT |
|
|
|
jwt_secret = request.env['ir.config_parameter'].sudo().get_param('rest_api.jwt_secret') |
|
|
|
|
|
|
|
health_status = { |
|
|
|
"status": "healthy", |
|
|
|
"timestamp": fields.Datetime.now().isoformat(), |
|
|
|
"database": request.env.cr.dbname, |
|
|
|
"active_models": active_configs, |
|
|
|
"version": "2.0.0", |
|
|
|
"auth_method": "JWT Bearer Token", |
|
|
|
"jwt_configured": bool(jwt_secret), |
|
|
|
"features": { |
|
|
|
"dynamic_schemas": True, |
|
|
|
"advanced_filtering": True, |
|
|
|
"jwt_authentication": True, |
|
|
|
"pagination": True, |
|
|
|
"field_selection": True |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return self._json_response(health_status) |
|
|
|
|
|
|
|
except Exception as e: |
|
|
|
error_status = { |
|
|
|
"status": "unhealthy", |
|
|
|
"error": str(e), |
|
|
|
"timestamp": fields.Datetime.now().isoformat() |
|
|
|
} |
|
|
|
return request.make_response( |
|
|
|
json.dumps(error_status), |
|
|
|
status=503, |
|
|
|
headers=[("Content-Type", "application/json; charset=utf-8")] |
|
|
|
) |
|
|
|
|
|
|
|
@http.route(['/api', '/api/'], type='http', auth='none', methods=['GET'], csrf=False) |
|
|
|
def api_root(self, **kw): |
|
|
|
"""Endpoint raíz de la API que redirige a la documentación""" |
|
|
|
base_url = request.env['ir.config_parameter'].sudo().get_param('web.base.url', 'http://localhost:8069') |
|
|
|
|
|
|
|
api_info = { |
|
|
|
"message": "Bienvenido a la REST API de Odoo", |
|
|
|
"version": "1.0.0", |
|
|
|
"documentation": f"{base_url}/api/v1/docs", |
|
|
|
"endpoints": { |
|
|
|
"auth": f"{base_url}/api/v1/auth", |
|
|
|
"models": f"{base_url}/api/v1/models", |
|
|
|
"docs": f"{base_url}/api/v1/docs", |
|
|
|
"openapi": f"{base_url}/api/v1/openapi.json" |
|
|
|
"""Endpoint raíz de la API que proporciona información básica""" |
|
|
|
try: |
|
|
|
base_url = request.env['ir.config_parameter'].sudo().get_param('web.base.url', 'http://localhost:8069') |
|
|
|
|
|
|
|
api_info = { |
|
|
|
"message": "Bienvenido a la REST API de Odoo v2.0", |
|
|
|
"version": "2.0.0", |
|
|
|
"status": "active", |
|
|
|
"documentation": f"{base_url}/api/v1/docs", |
|
|
|
"features": [ |
|
|
|
"JWT Bearer Token Authentication", |
|
|
|
"Dynamic Schema Generation", |
|
|
|
"Advanced Filtering with Domain", |
|
|
|
"Pagination Support", |
|
|
|
"Field Selection", |
|
|
|
"Interactive Swagger Documentation" |
|
|
|
], |
|
|
|
"endpoints": { |
|
|
|
"auth": f"{base_url}/api/v1/auth", |
|
|
|
"refresh": f"{base_url}/api/v1/refresh", |
|
|
|
"models": f"{base_url}/api/v1/models", |
|
|
|
"health": f"{base_url}/api/v1/health", |
|
|
|
"docs": f"{base_url}/api/v1/docs", |
|
|
|
"openapi": f"{base_url}/api/v1/openapi.json" |
|
|
|
}, |
|
|
|
"authentication": { |
|
|
|
"type": "JWT Bearer Token", |
|
|
|
"header": "Authorization", |
|
|
|
"format": "Bearer <token>", |
|
|
|
"expires_in_hours": "configurable (default: 24h)" |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return self._json_response(api_info) |
|
|
|
return self._json_response(api_info) |
|
|
|
except Exception as e: |
|
|
|
_logger.error(f"Error en api_root: {str(e)}") |
|
|
|
return self._error_response("Error interno del servidor", 500) |
|
|
|