Tema
Usuarios, roles y permisos
Esquema RBAC (control de acceso basado en roles): un usuario puede tener varios roles y cada rol agrupa varios permisos. Es el modelo que usan la mayoría de los paneles de administración y el que necesitas cuando el cliente dice "quiero que el vendedor vea pedidos pero no pueda borrarlos".
Diagrama
txt
usuarios ──< usuario_rol >── roles ──< rol_permiso >── permisos
│
├──< sesiones / password_resets
└──< usuario_permiso (excepciones individuales, opcional)Esquema completo
sql
-- ============================================
-- Usuarios, roles y permisos (MySQL 8)
-- ============================================
SET NAMES utf8mb4;
CREATE TABLE usuarios (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
apellidos VARCHAR(100) NULL,
email VARCHAR(150) NOT NULL,
telefono VARCHAR(20) NULL,
password_hash VARCHAR(255) NOT NULL,
avatar VARCHAR(255) NULL,
email_verificado_en DATETIME NULL,
activo TINYINT(1) NOT NULL DEFAULT 1,
ultimo_login DATETIME NULL,
creado_en TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
actualizado_en TIMESTAMP NULL ON UPDATE CURRENT_TIMESTAMP,
eliminado_en DATETIME NULL, -- borrado lógico
UNIQUE KEY uq_usuarios_email (email),
KEY idx_usuarios_activo (activo)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE roles (
id SMALLINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
clave VARCHAR(50) NOT NULL, -- 'admin', 'vendedor'
nombre VARCHAR(100) NOT NULL, -- 'Administrador'
descripcion VARCHAR(255) NULL,
UNIQUE KEY uq_roles_clave (clave)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE permisos (
id SMALLINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
clave VARCHAR(100) NOT NULL, -- 'pedidos.ver'
modulo VARCHAR(50) NOT NULL, -- 'pedidos' (para agrupar en pantalla)
descripcion VARCHAR(255) NULL,
UNIQUE KEY uq_permisos_clave (clave)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE usuario_rol (
usuario_id INT UNSIGNED NOT NULL,
rol_id SMALLINT UNSIGNED NOT NULL,
asignado_en TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (usuario_id, rol_id),
CONSTRAINT fk_ur_usuario FOREIGN KEY (usuario_id) REFERENCES usuarios(id) ON DELETE CASCADE,
CONSTRAINT fk_ur_rol FOREIGN KEY (rol_id) REFERENCES roles(id) ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE TABLE rol_permiso (
rol_id SMALLINT UNSIGNED NOT NULL,
permiso_id SMALLINT UNSIGNED NOT NULL,
PRIMARY KEY (rol_id, permiso_id),
CONSTRAINT fk_rp_rol FOREIGN KEY (rol_id) REFERENCES roles(id) ON DELETE CASCADE,
CONSTRAINT fk_rp_permiso FOREIGN KEY (permiso_id) REFERENCES permisos(id) ON DELETE CASCADE
) ENGINE=InnoDB;
-- Excepciones por usuario: conceder (1) o quitar (0) un permiso puntual
CREATE TABLE usuario_permiso (
usuario_id INT UNSIGNED NOT NULL,
permiso_id SMALLINT UNSIGNED NOT NULL,
permitido TINYINT(1) NOT NULL DEFAULT 1,
PRIMARY KEY (usuario_id, permiso_id),
FOREIGN KEY (usuario_id) REFERENCES usuarios(id) ON DELETE CASCADE,
FOREIGN KEY (permiso_id) REFERENCES permisos(id) ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE TABLE password_resets (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
usuario_id INT UNSIGNED NOT NULL,
token_hash CHAR(64) NOT NULL,
expira_en DATETIME NOT NULL,
creado_en TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_pr_token (token_hash),
FOREIGN KEY (usuario_id) REFERENCES usuarios(id) ON DELETE CASCADE
) ENGINE=InnoDB;
-- ============================================
-- Datos iniciales
-- ============================================
INSERT INTO roles (clave, nombre, descripcion) VALUES
('admin', 'Administrador', 'Acceso total al sistema'),
('editor', 'Editor', 'Gestiona contenido y productos'),
('vendedor', 'Vendedor', 'Consulta y atiende pedidos'),
('cliente', 'Cliente', 'Compra y ve sus propios pedidos');
INSERT INTO permisos (clave, modulo, descripcion) VALUES
('usuarios.ver', 'usuarios', 'Ver usuarios'),
('usuarios.crear', 'usuarios', 'Crear usuarios'),
('usuarios.editar', 'usuarios', 'Editar usuarios'),
('usuarios.eliminar', 'usuarios', 'Eliminar usuarios'),
('productos.ver', 'productos', 'Ver productos'),
('productos.crear', 'productos', 'Crear productos'),
('productos.editar', 'productos', 'Editar productos'),
('productos.eliminar', 'productos', 'Eliminar productos'),
('pedidos.ver', 'pedidos', 'Ver todos los pedidos'),
('pedidos.editar', 'pedidos', 'Cambiar estado de pedidos'),
('pedidos.eliminar', 'pedidos', 'Cancelar/eliminar pedidos'),
('reportes.ver', 'reportes', 'Ver reportes de ventas'),
('ajustes.editar', 'ajustes', 'Modificar la configuración');
-- Admin: todos los permisos
INSERT INTO rol_permiso (rol_id, permiso_id)
SELECT r.id, p.id FROM roles r CROSS JOIN permisos p WHERE r.clave = 'admin';
-- Editor: productos completos
INSERT INTO rol_permiso (rol_id, permiso_id)
SELECT r.id, p.id FROM roles r JOIN permisos p ON p.modulo = 'productos' WHERE r.clave = 'editor';
-- Vendedor: ver productos, ver y editar pedidos, ver reportes
INSERT INTO rol_permiso (rol_id, permiso_id)
SELECT r.id, p.id FROM roles r JOIN permisos p
ON p.clave IN ('productos.ver', 'pedidos.ver', 'pedidos.editar', 'reportes.ver')
WHERE r.clave = 'vendedor';
-- Usuario administrador inicial (contraseña: Cambiame123 — ¡cámbiala al entrar!)
-- Hash generado con: php -r "echo password_hash('Cambiame123', PASSWORD_DEFAULT);"
INSERT INTO usuarios (nombre, email, password_hash, email_verificado_en)
VALUES ('Administrador', 'admin@minegocio.mx', '$2y$10$8KzQ8IzAF9tXDV0tqXg6/.k3Zr3eHk0cJH7R1HjO1M7zv0iY2s7lK', NOW());
INSERT INTO usuario_rol (usuario_id, rol_id)
SELECT u.id, r.id FROM usuarios u, roles r WHERE u.email = 'admin@minegocio.mx' AND r.clave = 'admin';Sobre el hash de ejemplo
El hash del administrador es sólo ilustrativo. Genera el tuyo con password_hash() en PHP o bcrypt.hash() en Node y reemplázalo antes de importar.
Consultas clave
sql
-- ¿Qué permisos tiene un usuario? (roles + excepciones individuales)
SELECT p.clave
FROM permisos p
WHERE (
p.id IN (
SELECT rp.permiso_id
FROM usuario_rol ur JOIN rol_permiso rp ON rp.rol_id = ur.rol_id
WHERE ur.usuario_id = 1
)
OR p.id IN (SELECT permiso_id FROM usuario_permiso WHERE usuario_id = 1 AND permitido = 1)
)
AND p.id NOT IN (SELECT permiso_id FROM usuario_permiso WHERE usuario_id = 1 AND permitido = 0);
-- ¿Puede el usuario 5 eliminar pedidos? (devuelve 1 o 0)
SELECT EXISTS (
SELECT 1 FROM usuario_rol ur
JOIN rol_permiso rp ON rp.rol_id = ur.rol_id
JOIN permisos p ON p.id = rp.permiso_id
WHERE ur.usuario_id = 5 AND p.clave = 'pedidos.eliminar'
) AS puede;
-- Listado de usuarios con sus roles en una columna
SELECT u.id, u.nombre, u.email, GROUP_CONCAT(r.nombre ORDER BY r.nombre SEPARATOR ', ') AS roles
FROM usuarios u
LEFT JOIN usuario_rol ur ON ur.usuario_id = u.id
LEFT JOIN roles r ON r.id = ur.rol_id
WHERE u.eliminado_en IS NULL
GROUP BY u.id
ORDER BY u.nombre;
-- Matriz rol × permiso (para una pantalla de configuración con casillas)
SELECT r.clave AS rol, p.modulo, p.clave AS permiso,
(rp.rol_id IS NOT NULL) AS asignado
FROM roles r
CROSS JOIN permisos p
LEFT JOIN rol_permiso rp ON rp.rol_id = r.id AND rp.permiso_id = p.id
ORDER BY r.id, p.modulo, p.clave;Integración
- PHP: funciones
puede()yautorizar()en Protección por roles. - Node: carga los permisos al hacer login y guárdalos en caché (o en el JWT si son pocos).
¿Roles simples o RBAC?
Si el sistema tiene 2–3 tipos de usuario fijos, basta una columna rol ENUM(...) en usuarios. Pasa a este esquema cuando el cliente necesite crear roles nuevos o ajustar permisos sin tocar código.