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
This commit is contained in:
@@ -15,15 +15,12 @@ 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 = {
|
||||
'lectura': 10,
|
||||
'medico': 50,
|
||||
'recepcion': 50,
|
||||
'admin': 100,
|
||||
}
|
||||
RANK = ROLE_RANKS
|
||||
|
||||
|
||||
def _cors_headers():
|
||||
|
||||
@@ -6,6 +6,7 @@ import base64
|
||||
import json
|
||||
from datetime import date, datetime, timedelta
|
||||
from .auth import sign_token, current_user, require_role, verify_token
|
||||
from ..roles import ROLE_SELECTION, ROLE_KEYS, MENU_ROLES_KEY, get_menu_roles
|
||||
|
||||
|
||||
def json_response(data, status=200):
|
||||
@@ -100,7 +101,7 @@ class SkeenFrontendController(http.Controller):
|
||||
return json_response({'status': 'error', 'message': 'login y name son obligatorios'}, 400)
|
||||
if len(password) < 8:
|
||||
return json_response({'status': 'error', 'message': 'La contraseña debe tener al menos 8 caracteres'}, 400)
|
||||
if role not in ('admin', 'recepcion', 'medico', 'lectura'):
|
||||
if role not in ROLE_KEYS:
|
||||
return json_response({'status': 'error', 'message': 'Rol inválido'}, 400)
|
||||
User = request.env['skeen.frontend.user'].sudo()
|
||||
if User.search([('login', '=', login)], limit=1):
|
||||
@@ -123,7 +124,7 @@ class SkeenFrontendController(http.Controller):
|
||||
if 'name' in data:
|
||||
vals['name'] = data['name']
|
||||
if 'role' in data:
|
||||
if data['role'] not in ('admin', 'recepcion', 'medico', 'lectura'):
|
||||
if data['role'] not in ROLE_KEYS:
|
||||
return json_response({'status': 'error', 'message': 'Rol inválido'}, 400)
|
||||
vals['role'] = data['role']
|
||||
if 'active' in data:
|
||||
@@ -159,6 +160,43 @@ class SkeenFrontendController(http.Controller):
|
||||
except Exception as e:
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
# ============================================================
|
||||
# Permisos de menú por rol (matriz dinámica)
|
||||
# ============================================================
|
||||
@http.route('/skeen/frontend/v1/roles/permissions', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
|
||||
@require_role('lectura')
|
||||
def roles_permissions_get(self, **kw):
|
||||
try:
|
||||
return json_response({
|
||||
'status': 'success',
|
||||
'matrix': get_menu_roles(request.env),
|
||||
'roles': [{'key': k, 'label': l} for k, l in ROLE_SELECTION],
|
||||
})
|
||||
except Exception as e:
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
@http.route('/skeen/frontend/v1/roles/permissions', type='http', auth='none', methods=['PUT', 'OPTIONS'], csrf=False)
|
||||
@require_role('admin')
|
||||
def roles_permissions_put(self, **kw):
|
||||
try:
|
||||
data = _parse_json_body()
|
||||
matrix = data.get('matrix')
|
||||
if not isinstance(matrix, dict):
|
||||
return json_response({'status': 'error', 'message': 'matrix debe ser un objeto {href: [roles]}'}, 400)
|
||||
clean = {}
|
||||
for href, roles in matrix.items():
|
||||
if not isinstance(href, str):
|
||||
return json_response({'status': 'error', 'message': 'Las llaves de matrix deben ser strings (href)'}, 400)
|
||||
if not isinstance(roles, list) or any(not isinstance(r, str) or r not in ROLE_KEYS for r in roles):
|
||||
return json_response({'status': 'error', 'message': f'Roles inválidos para {href}'}, 400)
|
||||
clean[href] = list(dict.fromkeys(roles))
|
||||
if 'admin' not in clean.get('/configuracion', []):
|
||||
return json_response({'status': 'error', 'message': "No se puede quitar '/configuracion' al rol admin"}, 400)
|
||||
request.env['ir.config_parameter'].sudo().set_param(MENU_ROLES_KEY, json.dumps(clean))
|
||||
return json_response({'status': 'success', 'matrix': clean})
|
||||
except Exception as e:
|
||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||
|
||||
# ============================================================
|
||||
# Dashboard
|
||||
# ============================================================
|
||||
|
||||
@@ -5,6 +5,8 @@ import json
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from ..roles import ROLE_SELECTION, ROLE_RANKS
|
||||
|
||||
try:
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
except Exception: # pragma: no cover
|
||||
@@ -21,12 +23,7 @@ class SkeenFrontendUser(models.Model):
|
||||
login = fields.Char(string='Usuario', required=True, index=True)
|
||||
name = fields.Char(string='Nombre', required=True)
|
||||
password_hash = fields.Char(string='Hash de contraseña', required=True)
|
||||
role = fields.Selection([
|
||||
('admin', 'Administrador'),
|
||||
('recepcion', 'Recepción'),
|
||||
('medico', 'Médico'),
|
||||
('lectura', 'Solo lectura'),
|
||||
], string='Rol', default='recepcion', required=True)
|
||||
role = fields.Selection(ROLE_SELECTION, string='Rol', default='recepcion', required=True)
|
||||
active = fields.Boolean(string='Activo', default=True)
|
||||
must_change_password = fields.Boolean(string='Debe cambiar contraseña', default=False)
|
||||
last_login = fields.Datetime(string='Último acceso', readonly=True)
|
||||
@@ -58,12 +55,7 @@ class SkeenFrontendUser(models.Model):
|
||||
def role_rank(self):
|
||||
"""Jerarquía simple para permisos: mayor número = más acceso."""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'lectura': 10,
|
||||
'medico': 50,
|
||||
'recepcion': 50,
|
||||
'admin': 100,
|
||||
}.get(self.role, 0)
|
||||
return ROLE_RANKS.get(self.role, 0)
|
||||
|
||||
def _allowed_menus_list(self):
|
||||
"""Devuelve la lista de hrefs permitidos o None (usar defaults por rol)"""
|
||||
|
||||
102
odoo-addons/skeen_whatsapp/roles.py
Normal file
102
odoo-addons/skeen_whatsapp/roles.py
Normal file
@@ -0,0 +1,102 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Roles del frontend React y matriz de permisos de menú por rol.
|
||||
|
||||
Módulo sin dependencias de Odoo para poder importarse tanto desde modelos
|
||||
como desde controladores sin problemas de orden de carga.
|
||||
|
||||
La matriz se persiste en ir.config_parameter 'skeen.menu_roles' como JSON
|
||||
{href: [roles...]}. Si no existe, se usan los defaults de DEFAULT_MENU_ROLES
|
||||
(mismos que MENU_ROLES hardcodeado del frontend) y los 6 roles nuevos heredan
|
||||
el mismo set de secciones que 'lectura'.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
ROLE_SELECTION = [
|
||||
('admin', 'Administrador'),
|
||||
('medico', 'Médico'),
|
||||
('recepcion', 'Recepción'),
|
||||
('enfermeria', 'Enfermería'),
|
||||
('cosmetologia', 'Cosmetología'),
|
||||
('psicologa', 'Psicóloga'),
|
||||
('farmacia', 'Farmacia'),
|
||||
('entrenador', 'Entrenador'),
|
||||
('marketing', 'Marketing'),
|
||||
('lectura', 'Lectura'),
|
||||
]
|
||||
|
||||
ROLE_KEYS = tuple(k for k, _ in ROLE_SELECTION)
|
||||
|
||||
ROLE_RANKS = {
|
||||
'admin': 100,
|
||||
'medico': 50,
|
||||
'recepcion': 50,
|
||||
'enfermeria': 50,
|
||||
'cosmetologia': 50,
|
||||
'psicologa': 50,
|
||||
'farmacia': 50,
|
||||
'entrenador': 10,
|
||||
'marketing': 10,
|
||||
'lectura': 10,
|
||||
}
|
||||
|
||||
MENU_ROLES_KEY = 'skeen.menu_roles'
|
||||
|
||||
# Defaults = MENU_ROLES de src/lib/auth.tsx (4 roles originales).
|
||||
DEFAULT_MENU_ROLES = {
|
||||
'/': ['admin', 'recepcion', 'medico', 'lectura'],
|
||||
'/agenda': ['admin', 'recepcion', 'medico'],
|
||||
'/pacientes': ['admin', 'recepcion', 'medico', 'lectura'],
|
||||
'/pos': ['admin', 'recepcion'],
|
||||
'/visitas': ['admin', 'recepcion', 'medico'],
|
||||
'/consultas': ['admin', 'recepcion', 'medico'],
|
||||
'/expedientes': ['admin', 'recepcion', 'medico'],
|
||||
'/medicos': ['admin', 'recepcion'],
|
||||
'/servicios': ['admin', 'recepcion', 'medico', 'lectura'],
|
||||
'/productos': ['admin', 'recepcion', 'lectura'],
|
||||
'/ventas': ['admin', 'recepcion'],
|
||||
'/pagos': ['admin', 'recepcion'],
|
||||
'/monedero': ['admin', 'recepcion', 'lectura'],
|
||||
'/inventario': ['admin', 'recepcion', 'lectura'],
|
||||
'/cortes': ['admin', 'recepcion'],
|
||||
'/reportes': ['admin', 'recepcion', 'lectura'],
|
||||
'/reportes?tab=adeudos': ['admin', 'recepcion', 'lectura'],
|
||||
'/reportes?tab=comisiones': ['admin', 'recepcion', 'lectura'],
|
||||
'/reportes?tab=horas-agenda': ['admin', 'recepcion', 'lectura'],
|
||||
'/reportes?tab=paquetes': ['admin', 'recepcion', 'lectura'],
|
||||
'/reportes?tab=vendedores': ['admin', 'recepcion', 'lectura'],
|
||||
'/reportes?tab=concentrado': ['admin', 'recepcion', 'lectura'],
|
||||
'/reportes?tab=recomendaciones': ['admin', 'recepcion', 'lectura'],
|
||||
'/reportes?tab=exportar': ['admin', 'recepcion', 'lectura'],
|
||||
'/configuracion': ['admin'],
|
||||
'/cumpleanos': ['admin', 'recepcion'],
|
||||
'/wacrm/messages': ['admin', 'recepcion'],
|
||||
'/wacrm/leads': ['admin', 'recepcion'],
|
||||
}
|
||||
|
||||
# Roles nuevos que por default ven lo mismo que 'lectura'.
|
||||
READ_ONLY_NEW_ROLES = ('enfermeria', 'cosmetologia', 'psicologa', 'farmacia', 'entrenador', 'marketing')
|
||||
|
||||
|
||||
def default_menu_roles():
|
||||
"""Matriz por default: MENU_ROLES + roles nuevos donde esté 'lectura'."""
|
||||
matrix = {}
|
||||
for href, roles in DEFAULT_MENU_ROLES.items():
|
||||
r = list(roles)
|
||||
if 'lectura' in roles:
|
||||
r.extend(nr for nr in READ_ONLY_NEW_ROLES if nr not in r)
|
||||
matrix[href] = r
|
||||
return matrix
|
||||
|
||||
|
||||
def get_menu_roles(env):
|
||||
"""Matriz vigente: la guardada en ir.config_parameter o los defaults."""
|
||||
raw = env['ir.config_parameter'].sudo().get_param(MENU_ROLES_KEY)
|
||||
if raw:
|
||||
try:
|
||||
val = json.loads(raw)
|
||||
if isinstance(val, dict):
|
||||
return val
|
||||
except Exception:
|
||||
pass
|
||||
return default_menu_roles()
|
||||
Reference in New Issue
Block a user