Files
SKEEN-Proyecto/odoo-addons/skeen_whatsapp/controllers/auth.py
Consultoría Alcaraz Salazar 281fb34588 Roles ampliados (10) y matriz de permisos por rol
- Roles: admin, médico, recepción, enfermería, cosmetología, psicóloga,
  farmacia, entrenador, marketing + lectura (compatibilidad)
- Matriz de permisos por rol persistida en Odoo (ir.config_parameter)
  y editable en Configuración → Roles (secciones × roles)
- Prioridad: permisos personales del usuario > matriz por rol > defaults
- Protección: admin siempre conserva /configuracion
2026-08-14 08:47:27 +00:00

125 lines
3.6 KiB
Python

# -*- coding: utf-8 -*-
"""Autenticación ligera (token HMAC) para el frontend React de SKEEN.
Token = base64url(payload_json) + "." + firma HMAC-SHA256, sin tabla de sesiones.
El secreto vive en ir.config_parameter 'skeen.frontend.secret' (se genera al primer uso).
"""
import base64
import functools
import hashlib
import hmac
import json
import secrets
import time
from odoo.http import request, Response
from ..roles import ROLE_RANKS
TTL_SECONDS = 60 * 60 * 12 # 12 horas
SECRET_KEY = 'skeen.frontend.secret'
RANK = ROLE_RANKS
def _cors_headers():
return {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}
def _json(data, status=200):
return Response(
json.dumps(data, default=str),
status=status,
mimetype='application/json',
headers=_cors_headers(),
)
def _b64url_encode(raw: bytes) -> str:
return base64.urlsafe_b64encode(raw).rstrip(b'=').decode('ascii')
def _b64url_decode(s: str) -> bytes:
pad = '=' * (-len(s) % 4)
return base64.urlsafe_b64decode(s + pad)
def _get_secret(env) -> str:
ICP = env['ir.config_parameter'].sudo()
secret = ICP.get_param(SECRET_KEY)
if not secret:
secret = secrets.token_urlsafe(48)
ICP.set_param(SECRET_KEY, secret)
return secret
def sign_token(env, user) -> str:
payload = {
'uid': user.id,
'login': user.login,
'role': user.role,
'exp': int(time.time()) + TTL_SECONDS,
}
body = _b64url_encode(json.dumps(payload, separators=(',', ':')).encode('utf-8'))
sig = hmac.new(_get_secret(env).encode('utf-8'), body.encode('ascii'), hashlib.sha256).hexdigest()
return f'{body}.{sig}'
def verify_token(env, token: str):
if not token or '.' not in token:
return None
body, sig = token.rsplit('.', 1)
expected = hmac.new(_get_secret(env).encode('utf-8'), body.encode('ascii'), hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
return None
try:
payload = json.loads(_b64url_decode(body).decode('utf-8'))
except Exception:
return None
if int(payload.get('exp', 0)) < int(time.time()):
return None
return payload
def current_user(env):
"""Devuelve el usuario autenticado (record) o None."""
auth = request.httprequest.headers.get('Authorization', '') or ''
if not auth.lower().startswith('bearer '):
return None
token = auth[7:].strip()
payload = verify_token(env, token)
if not payload:
return None
user = env['skeen.frontend.user'].sudo().browse(payload.get('uid'))
if not user.exists() or not user.active:
return None
return user
def require_role(min_role='lectura'):
"""Decorator: exige token válido y un rol con rango >= min_role.
Deja pasar OPTIONS (preflight CORS) sin autenticación.
"""
required = RANK.get(min_role, 10)
def decorator(func):
@functools.wraps(func)
def wrapper(self, *args, **kw):
if request.httprequest.method == 'OPTIONS':
return _json({}, 200)
user = current_user(request.env)
if not user:
return _json({'status': 'error', 'message': 'No autenticado'}, 401)
if user.role_rank() < required:
return _json({'status': 'error', 'message': 'Sin permiso'}, 403)
request.frontend_user = user
return func(self, *args, **kw)
return wrapper
return decorator