621 lines
20 KiB
PL/PgSQL
621 lines
20 KiB
PL/PgSQL
-- =====================================================
|
|
-- SCHEMA: auth
|
|
-- PROPÓSITO: Autenticación, usuarios, roles, permisos
|
|
-- MÓDULOS: MGN-001 (Fundamentos), MGN-002 (Empresas)
|
|
-- FECHA: 2025-11-24
|
|
-- =====================================================
|
|
|
|
-- Crear schema
|
|
CREATE SCHEMA IF NOT EXISTS auth;
|
|
|
|
-- =====================================================
|
|
-- TYPES (ENUMs)
|
|
-- =====================================================
|
|
|
|
CREATE TYPE auth.user_status AS ENUM (
|
|
'active',
|
|
'inactive',
|
|
'suspended',
|
|
'pending_verification'
|
|
);
|
|
|
|
CREATE TYPE auth.tenant_status AS ENUM (
|
|
'active',
|
|
'suspended',
|
|
'trial',
|
|
'cancelled'
|
|
);
|
|
|
|
CREATE TYPE auth.session_status AS ENUM (
|
|
'active',
|
|
'expired',
|
|
'revoked'
|
|
);
|
|
|
|
CREATE TYPE auth.permission_action AS ENUM (
|
|
'create',
|
|
'read',
|
|
'update',
|
|
'delete',
|
|
'approve',
|
|
'cancel',
|
|
'export'
|
|
);
|
|
|
|
-- =====================================================
|
|
-- TABLES
|
|
-- =====================================================
|
|
|
|
-- Tabla: tenants (Multi-Tenancy)
|
|
CREATE TABLE auth.tenants (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name VARCHAR(255) NOT NULL,
|
|
subdomain VARCHAR(100) UNIQUE NOT NULL,
|
|
schema_name VARCHAR(100) UNIQUE NOT NULL,
|
|
status auth.tenant_status NOT NULL DEFAULT 'active',
|
|
settings JSONB DEFAULT '{}',
|
|
plan VARCHAR(50) DEFAULT 'basic', -- basic, pro, enterprise
|
|
max_users INTEGER DEFAULT 10,
|
|
|
|
-- Auditoría (tenant no tiene tenant_id)
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_by UUID, -- Puede ser NULL para primer tenant
|
|
updated_at TIMESTAMP,
|
|
updated_by UUID,
|
|
deleted_at TIMESTAMP,
|
|
deleted_by UUID,
|
|
|
|
CONSTRAINT chk_tenants_subdomain_format CHECK (subdomain ~ '^[a-z0-9-]+$'),
|
|
CONSTRAINT chk_tenants_max_users CHECK (max_users > 0)
|
|
);
|
|
|
|
-- Tabla: companies (Multi-Company dentro de tenant)
|
|
CREATE TABLE auth.companies (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
name VARCHAR(255) NOT NULL,
|
|
legal_name VARCHAR(255),
|
|
tax_id VARCHAR(50),
|
|
currency_id UUID, -- FK a core.currencies (se crea después)
|
|
parent_company_id UUID REFERENCES auth.companies(id),
|
|
partner_id UUID, -- FK a core.partners (se crea después)
|
|
settings JSONB DEFAULT '{}',
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_by UUID,
|
|
updated_at TIMESTAMP,
|
|
updated_by UUID,
|
|
deleted_at TIMESTAMP,
|
|
deleted_by UUID,
|
|
|
|
CONSTRAINT uq_companies_tax_id_tenant UNIQUE (tenant_id, tax_id),
|
|
CONSTRAINT chk_companies_no_self_parent CHECK (id != parent_company_id)
|
|
);
|
|
|
|
-- Tabla: users
|
|
CREATE TABLE auth.users (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
email VARCHAR(255) NOT NULL,
|
|
password_hash VARCHAR(255) NOT NULL,
|
|
full_name VARCHAR(255) NOT NULL,
|
|
avatar_url VARCHAR(500),
|
|
status auth.user_status NOT NULL DEFAULT 'active',
|
|
is_superuser BOOLEAN NOT NULL DEFAULT FALSE,
|
|
email_verified_at TIMESTAMP,
|
|
last_login_at TIMESTAMP,
|
|
last_login_ip INET,
|
|
login_count INTEGER DEFAULT 0,
|
|
language VARCHAR(10) DEFAULT 'es', -- es, en
|
|
timezone VARCHAR(50) DEFAULT 'America/Mexico_City',
|
|
settings JSONB DEFAULT '{}',
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_by UUID,
|
|
updated_at TIMESTAMP,
|
|
updated_by UUID,
|
|
deleted_at TIMESTAMP,
|
|
deleted_by UUID,
|
|
|
|
CONSTRAINT uq_users_email_tenant UNIQUE (tenant_id, email),
|
|
CONSTRAINT chk_users_email_format CHECK (email ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}$')
|
|
);
|
|
|
|
-- Tabla: roles
|
|
CREATE TABLE auth.roles (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
|
name VARCHAR(100) NOT NULL,
|
|
code VARCHAR(50) NOT NULL,
|
|
description TEXT,
|
|
is_system BOOLEAN NOT NULL DEFAULT FALSE, -- Roles del sistema no editables
|
|
color VARCHAR(20),
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_by UUID REFERENCES auth.users(id),
|
|
updated_at TIMESTAMP,
|
|
updated_by UUID REFERENCES auth.users(id),
|
|
deleted_at TIMESTAMP,
|
|
deleted_by UUID REFERENCES auth.users(id),
|
|
|
|
CONSTRAINT uq_roles_code_tenant UNIQUE (tenant_id, code)
|
|
);
|
|
|
|
-- Tabla: permissions
|
|
CREATE TABLE auth.permissions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
resource VARCHAR(100) NOT NULL, -- Tabla/endpoint
|
|
action auth.permission_action NOT NULL,
|
|
description TEXT,
|
|
module VARCHAR(50), -- MGN-001, MGN-004, etc.
|
|
|
|
-- Sin tenant_id: permisos son globales
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT uq_permissions_resource_action UNIQUE (resource, action)
|
|
);
|
|
|
|
-- Tabla: user_roles (many-to-many)
|
|
CREATE TABLE auth.user_roles (
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
role_id UUID NOT NULL REFERENCES auth.roles(id) ON DELETE CASCADE,
|
|
assigned_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
assigned_by UUID REFERENCES auth.users(id),
|
|
|
|
PRIMARY KEY (user_id, role_id)
|
|
);
|
|
|
|
-- Tabla: role_permissions (many-to-many)
|
|
CREATE TABLE auth.role_permissions (
|
|
role_id UUID NOT NULL REFERENCES auth.roles(id) ON DELETE CASCADE,
|
|
permission_id UUID NOT NULL REFERENCES auth.permissions(id) ON DELETE CASCADE,
|
|
granted_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
granted_by UUID REFERENCES auth.users(id),
|
|
|
|
PRIMARY KEY (role_id, permission_id)
|
|
);
|
|
|
|
-- Tabla: sessions
|
|
CREATE TABLE auth.sessions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
token VARCHAR(500) NOT NULL UNIQUE,
|
|
refresh_token VARCHAR(500) UNIQUE,
|
|
status auth.session_status NOT NULL DEFAULT 'active',
|
|
expires_at TIMESTAMP NOT NULL,
|
|
refresh_expires_at TIMESTAMP,
|
|
ip_address INET,
|
|
user_agent TEXT,
|
|
device_info JSONB,
|
|
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
revoked_at TIMESTAMP,
|
|
revoked_reason VARCHAR(100),
|
|
|
|
CONSTRAINT chk_sessions_expiration CHECK (expires_at > created_at),
|
|
CONSTRAINT chk_sessions_refresh_expiration CHECK (
|
|
refresh_expires_at IS NULL OR refresh_expires_at > expires_at
|
|
)
|
|
);
|
|
|
|
-- Tabla: user_companies (many-to-many)
|
|
-- Usuario puede acceder a múltiples empresas
|
|
CREATE TABLE auth.user_companies (
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
company_id UUID NOT NULL REFERENCES auth.companies(id) ON DELETE CASCADE,
|
|
is_default BOOLEAN DEFAULT FALSE,
|
|
assigned_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
PRIMARY KEY (user_id, company_id)
|
|
);
|
|
|
|
-- Tabla: password_resets
|
|
CREATE TABLE auth.password_resets (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
token VARCHAR(500) NOT NULL UNIQUE,
|
|
expires_at TIMESTAMP NOT NULL,
|
|
used_at TIMESTAMP,
|
|
ip_address INET,
|
|
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT chk_password_resets_expiration CHECK (expires_at > created_at)
|
|
);
|
|
|
|
-- =====================================================
|
|
-- INDICES
|
|
-- =====================================================
|
|
|
|
-- Tenants
|
|
CREATE INDEX idx_tenants_subdomain ON auth.tenants(subdomain);
|
|
CREATE INDEX idx_tenants_status ON auth.tenants(status) WHERE deleted_at IS NULL;
|
|
CREATE INDEX idx_tenants_created_at ON auth.tenants(created_at);
|
|
|
|
-- Companies
|
|
CREATE INDEX idx_companies_tenant_id ON auth.companies(tenant_id);
|
|
CREATE INDEX idx_companies_parent_company_id ON auth.companies(parent_company_id);
|
|
CREATE INDEX idx_companies_active ON auth.companies(tenant_id) WHERE deleted_at IS NULL;
|
|
CREATE INDEX idx_companies_tax_id ON auth.companies(tax_id);
|
|
|
|
-- Users
|
|
CREATE INDEX idx_users_tenant_id ON auth.users(tenant_id);
|
|
CREATE INDEX idx_users_email ON auth.users(email);
|
|
CREATE INDEX idx_users_status ON auth.users(status) WHERE deleted_at IS NULL;
|
|
CREATE INDEX idx_users_email_tenant ON auth.users(tenant_id, email);
|
|
CREATE INDEX idx_users_created_at ON auth.users(created_at);
|
|
|
|
-- Roles
|
|
CREATE INDEX idx_roles_tenant_id ON auth.roles(tenant_id);
|
|
CREATE INDEX idx_roles_code ON auth.roles(code);
|
|
CREATE INDEX idx_roles_is_system ON auth.roles(is_system);
|
|
|
|
-- Permissions
|
|
CREATE INDEX idx_permissions_resource ON auth.permissions(resource);
|
|
CREATE INDEX idx_permissions_action ON auth.permissions(action);
|
|
CREATE INDEX idx_permissions_module ON auth.permissions(module);
|
|
|
|
-- Sessions
|
|
CREATE INDEX idx_sessions_user_id ON auth.sessions(user_id);
|
|
CREATE INDEX idx_sessions_token ON auth.sessions(token);
|
|
CREATE INDEX idx_sessions_status ON auth.sessions(status);
|
|
CREATE INDEX idx_sessions_expires_at ON auth.sessions(expires_at);
|
|
|
|
-- User Roles
|
|
CREATE INDEX idx_user_roles_user_id ON auth.user_roles(user_id);
|
|
CREATE INDEX idx_user_roles_role_id ON auth.user_roles(role_id);
|
|
|
|
-- Role Permissions
|
|
CREATE INDEX idx_role_permissions_role_id ON auth.role_permissions(role_id);
|
|
CREATE INDEX idx_role_permissions_permission_id ON auth.role_permissions(permission_id);
|
|
|
|
-- User Companies
|
|
CREATE INDEX idx_user_companies_user_id ON auth.user_companies(user_id);
|
|
CREATE INDEX idx_user_companies_company_id ON auth.user_companies(company_id);
|
|
|
|
-- Password Resets
|
|
CREATE INDEX idx_password_resets_user_id ON auth.password_resets(user_id);
|
|
CREATE INDEX idx_password_resets_token ON auth.password_resets(token);
|
|
CREATE INDEX idx_password_resets_expires_at ON auth.password_resets(expires_at);
|
|
|
|
-- =====================================================
|
|
-- FUNCTIONS
|
|
-- =====================================================
|
|
|
|
-- Función: get_current_tenant_id
|
|
CREATE OR REPLACE FUNCTION get_current_tenant_id()
|
|
RETURNS UUID AS $$
|
|
BEGIN
|
|
RETURN current_setting('app.current_tenant_id', true)::UUID;
|
|
EXCEPTION
|
|
WHEN OTHERS THEN
|
|
RETURN NULL;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
|
|
|
|
COMMENT ON FUNCTION get_current_tenant_id() IS 'Obtiene el tenant_id del contexto actual';
|
|
|
|
-- Función: get_current_user_id
|
|
CREATE OR REPLACE FUNCTION get_current_user_id()
|
|
RETURNS UUID AS $$
|
|
BEGIN
|
|
RETURN current_setting('app.current_user_id', true)::UUID;
|
|
EXCEPTION
|
|
WHEN OTHERS THEN
|
|
RETURN NULL;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
|
|
|
|
COMMENT ON FUNCTION get_current_user_id() IS 'Obtiene el user_id del contexto actual';
|
|
|
|
-- Función: get_current_company_id
|
|
CREATE OR REPLACE FUNCTION get_current_company_id()
|
|
RETURNS UUID AS $$
|
|
BEGIN
|
|
RETURN current_setting('app.current_company_id', true)::UUID;
|
|
EXCEPTION
|
|
WHEN OTHERS THEN
|
|
RETURN NULL;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
|
|
|
|
COMMENT ON FUNCTION get_current_company_id() IS 'Obtiene el company_id del contexto actual';
|
|
|
|
-- Función: user_has_permission
|
|
CREATE OR REPLACE FUNCTION auth.user_has_permission(
|
|
p_user_id UUID,
|
|
p_resource VARCHAR,
|
|
p_action auth.permission_action
|
|
)
|
|
RETURNS BOOLEAN AS $$
|
|
DECLARE
|
|
v_has_permission BOOLEAN;
|
|
BEGIN
|
|
-- Superusers tienen todos los permisos
|
|
IF EXISTS (
|
|
SELECT 1 FROM auth.users
|
|
WHERE id = p_user_id AND is_superuser = TRUE AND deleted_at IS NULL
|
|
) THEN
|
|
RETURN TRUE;
|
|
END IF;
|
|
|
|
-- Verificar si el usuario tiene el permiso a través de sus roles
|
|
SELECT EXISTS (
|
|
SELECT 1
|
|
FROM auth.user_roles ur
|
|
JOIN auth.role_permissions rp ON ur.role_id = rp.role_id
|
|
JOIN auth.permissions p ON rp.permission_id = p.id
|
|
WHERE ur.user_id = p_user_id
|
|
AND p.resource = p_resource
|
|
AND p.action = p_action
|
|
) INTO v_has_permission;
|
|
|
|
RETURN v_has_permission;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
|
|
|
|
COMMENT ON FUNCTION auth.user_has_permission IS 'Verifica si un usuario tiene un permiso específico';
|
|
|
|
-- Función: clean_expired_sessions
|
|
CREATE OR REPLACE FUNCTION auth.clean_expired_sessions()
|
|
RETURNS INTEGER AS $$
|
|
DECLARE
|
|
v_deleted_count INTEGER;
|
|
BEGIN
|
|
WITH deleted AS (
|
|
DELETE FROM auth.sessions
|
|
WHERE status = 'active'
|
|
AND expires_at < CURRENT_TIMESTAMP
|
|
RETURNING id
|
|
)
|
|
SELECT COUNT(*) INTO v_deleted_count FROM deleted;
|
|
|
|
RETURN v_deleted_count;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION auth.clean_expired_sessions IS 'Limpia sesiones expiradas (ejecutar periódicamente)';
|
|
|
|
-- =====================================================
|
|
-- TRIGGERS
|
|
-- =====================================================
|
|
|
|
-- Trigger: Actualizar updated_at automáticamente
|
|
CREATE OR REPLACE FUNCTION auth.update_updated_at_column()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = CURRENT_TIMESTAMP;
|
|
NEW.updated_by = get_current_user_id();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER trg_tenants_updated_at
|
|
BEFORE UPDATE ON auth.tenants
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.update_updated_at_column();
|
|
|
|
CREATE TRIGGER trg_companies_updated_at
|
|
BEFORE UPDATE ON auth.companies
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.update_updated_at_column();
|
|
|
|
CREATE TRIGGER trg_users_updated_at
|
|
BEFORE UPDATE ON auth.users
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.update_updated_at_column();
|
|
|
|
CREATE TRIGGER trg_roles_updated_at
|
|
BEFORE UPDATE ON auth.roles
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.update_updated_at_column();
|
|
|
|
-- Trigger: Validar que tenant tenga al menos 1 admin
|
|
CREATE OR REPLACE FUNCTION auth.validate_tenant_has_admin()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
-- Al eliminar user_role, verificar que no sea el último admin
|
|
IF TG_OP = 'DELETE' THEN
|
|
IF EXISTS (
|
|
SELECT 1
|
|
FROM auth.users u
|
|
JOIN auth.roles r ON r.tenant_id = u.tenant_id
|
|
WHERE u.id = OLD.user_id
|
|
AND r.code = 'admin'
|
|
AND r.id = OLD.role_id
|
|
) THEN
|
|
-- Contar admins restantes
|
|
IF NOT EXISTS (
|
|
SELECT 1
|
|
FROM auth.user_roles ur
|
|
JOIN auth.roles r ON r.id = ur.role_id
|
|
JOIN auth.users u ON u.id = ur.user_id
|
|
WHERE r.code = 'admin'
|
|
AND u.tenant_id = (SELECT tenant_id FROM auth.users WHERE id = OLD.user_id)
|
|
AND ur.user_id != OLD.user_id
|
|
) THEN
|
|
RAISE EXCEPTION 'Cannot remove last admin from tenant';
|
|
END IF;
|
|
END IF;
|
|
END IF;
|
|
|
|
RETURN OLD;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER trg_validate_tenant_has_admin
|
|
BEFORE DELETE ON auth.user_roles
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.validate_tenant_has_admin();
|
|
|
|
-- Trigger: Auto-marcar sesión como expirada
|
|
CREATE OR REPLACE FUNCTION auth.auto_expire_session()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
IF NEW.expires_at < CURRENT_TIMESTAMP AND NEW.status = 'active' THEN
|
|
NEW.status = 'expired';
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER trg_auto_expire_session
|
|
BEFORE UPDATE ON auth.sessions
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION auth.auto_expire_session();
|
|
|
|
-- =====================================================
|
|
-- ROW LEVEL SECURITY (RLS)
|
|
-- =====================================================
|
|
|
|
-- Habilitar RLS en tablas con tenant_id
|
|
ALTER TABLE auth.companies ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE auth.users ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE auth.roles ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- Policy: Tenant Isolation - Companies
|
|
CREATE POLICY tenant_isolation_companies
|
|
ON auth.companies
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
-- Policy: Tenant Isolation - Users
|
|
CREATE POLICY tenant_isolation_users
|
|
ON auth.users
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
-- Policy: Tenant Isolation - Roles
|
|
CREATE POLICY tenant_isolation_roles
|
|
ON auth.roles
|
|
USING (tenant_id = get_current_tenant_id());
|
|
|
|
-- =====================================================
|
|
-- DATOS INICIALES (Seed Data)
|
|
-- =====================================================
|
|
|
|
-- Permisos estándar para recursos comunes
|
|
INSERT INTO auth.permissions (resource, action, description, module) VALUES
|
|
-- Auth
|
|
('users', 'create', 'Crear usuarios', 'MGN-001'),
|
|
('users', 'read', 'Ver usuarios', 'MGN-001'),
|
|
('users', 'update', 'Actualizar usuarios', 'MGN-001'),
|
|
('users', 'delete', 'Eliminar usuarios', 'MGN-001'),
|
|
('roles', 'create', 'Crear roles', 'MGN-001'),
|
|
('roles', 'read', 'Ver roles', 'MGN-001'),
|
|
('roles', 'update', 'Actualizar roles', 'MGN-001'),
|
|
('roles', 'delete', 'Eliminar roles', 'MGN-001'),
|
|
|
|
-- Financial
|
|
('invoices', 'create', 'Crear facturas', 'MGN-004'),
|
|
('invoices', 'read', 'Ver facturas', 'MGN-004'),
|
|
('invoices', 'update', 'Actualizar facturas', 'MGN-004'),
|
|
('invoices', 'delete', 'Eliminar facturas', 'MGN-004'),
|
|
('invoices', 'approve', 'Aprobar facturas', 'MGN-004'),
|
|
('invoices', 'cancel', 'Cancelar facturas', 'MGN-004'),
|
|
('journal_entries', 'create', 'Crear asientos contables', 'MGN-004'),
|
|
('journal_entries', 'read', 'Ver asientos contables', 'MGN-004'),
|
|
('journal_entries', 'approve', 'Aprobar asientos contables', 'MGN-004'),
|
|
|
|
-- Purchase
|
|
('purchase_orders', 'create', 'Crear órdenes de compra', 'MGN-006'),
|
|
('purchase_orders', 'read', 'Ver órdenes de compra', 'MGN-006'),
|
|
('purchase_orders', 'update', 'Actualizar órdenes de compra', 'MGN-006'),
|
|
('purchase_orders', 'delete', 'Eliminar órdenes de compra', 'MGN-006'),
|
|
('purchase_orders', 'approve', 'Aprobar órdenes de compra', 'MGN-006'),
|
|
|
|
-- Sales
|
|
('sale_orders', 'create', 'Crear órdenes de venta', 'MGN-007'),
|
|
('sale_orders', 'read', 'Ver órdenes de venta', 'MGN-007'),
|
|
('sale_orders', 'update', 'Actualizar órdenes de venta', 'MGN-007'),
|
|
('sale_orders', 'delete', 'Eliminar órdenes de venta', 'MGN-007'),
|
|
('sale_orders', 'approve', 'Aprobar órdenes de venta', 'MGN-007'),
|
|
|
|
-- Inventory
|
|
('products', 'create', 'Crear productos', 'MGN-005'),
|
|
('products', 'read', 'Ver productos', 'MGN-005'),
|
|
('products', 'update', 'Actualizar productos', 'MGN-005'),
|
|
('products', 'delete', 'Eliminar productos', 'MGN-005'),
|
|
('stock_moves', 'create', 'Crear movimientos de inventario', 'MGN-005'),
|
|
('stock_moves', 'read', 'Ver movimientos de inventario', 'MGN-005'),
|
|
('stock_moves', 'approve', 'Aprobar movimientos de inventario', 'MGN-005'),
|
|
|
|
-- Projects
|
|
('projects', 'create', 'Crear proyectos', 'MGN-011'),
|
|
('projects', 'read', 'Ver proyectos', 'MGN-011'),
|
|
('projects', 'update', 'Actualizar proyectos', 'MGN-011'),
|
|
('projects', 'delete', 'Eliminar proyectos', 'MGN-011'),
|
|
('tasks', 'create', 'Crear tareas', 'MGN-011'),
|
|
('tasks', 'read', 'Ver tareas', 'MGN-011'),
|
|
('tasks', 'update', 'Actualizar tareas', 'MGN-011'),
|
|
('tasks', 'delete', 'Eliminar tareas', 'MGN-011'),
|
|
|
|
-- Reports
|
|
('reports', 'read', 'Ver reportes', 'MGN-012'),
|
|
('reports', 'export', 'Exportar reportes', 'MGN-012');
|
|
|
|
-- =====================================================
|
|
-- COMENTARIOS EN TABLAS
|
|
-- =====================================================
|
|
|
|
COMMENT ON SCHEMA auth IS 'Schema de autenticación, usuarios, roles y permisos';
|
|
COMMENT ON TABLE auth.tenants IS 'Tenants (organizaciones raíz) con schema-level isolation';
|
|
COMMENT ON TABLE auth.companies IS 'Empresas dentro de un tenant (multi-company)';
|
|
COMMENT ON TABLE auth.users IS 'Usuarios del sistema con RBAC';
|
|
COMMENT ON TABLE auth.roles IS 'Roles con permisos asignados';
|
|
COMMENT ON TABLE auth.permissions IS 'Permisos granulares por recurso y acción';
|
|
COMMENT ON TABLE auth.user_roles IS 'Asignación de roles a usuarios (many-to-many)';
|
|
COMMENT ON TABLE auth.role_permissions IS 'Asignación de permisos a roles (many-to-many)';
|
|
COMMENT ON TABLE auth.sessions IS 'Sesiones JWT activas de usuarios';
|
|
COMMENT ON TABLE auth.user_companies IS 'Asignación de usuarios a empresas (multi-company)';
|
|
COMMENT ON TABLE auth.password_resets IS 'Tokens de reset de contraseña';
|
|
|
|
-- =====================================================
|
|
-- VISTAS ÚTILES
|
|
-- =====================================================
|
|
|
|
-- Vista: user_permissions (permisos efectivos de usuario)
|
|
CREATE OR REPLACE VIEW auth.user_permissions_view AS
|
|
SELECT DISTINCT
|
|
ur.user_id,
|
|
u.email,
|
|
u.full_name,
|
|
p.resource,
|
|
p.action,
|
|
p.description,
|
|
r.name as role_name,
|
|
r.code as role_code
|
|
FROM auth.user_roles ur
|
|
JOIN auth.users u ON ur.user_id = u.id
|
|
JOIN auth.roles r ON ur.role_id = r.id
|
|
JOIN auth.role_permissions rp ON r.id = rp.role_id
|
|
JOIN auth.permissions p ON rp.permission_id = p.id
|
|
WHERE u.deleted_at IS NULL
|
|
AND u.status = 'active';
|
|
|
|
COMMENT ON VIEW auth.user_permissions_view IS 'Vista de permisos efectivos por usuario';
|
|
|
|
-- Vista: active_sessions (sesiones activas)
|
|
CREATE OR REPLACE VIEW auth.active_sessions_view AS
|
|
SELECT
|
|
s.id,
|
|
s.user_id,
|
|
u.email,
|
|
u.full_name,
|
|
s.ip_address,
|
|
s.user_agent,
|
|
s.created_at as login_at,
|
|
s.expires_at,
|
|
EXTRACT(EPOCH FROM (s.expires_at - CURRENT_TIMESTAMP))/60 as minutes_until_expiry
|
|
FROM auth.sessions s
|
|
JOIN auth.users u ON s.user_id = u.id
|
|
WHERE s.status = 'active'
|
|
AND s.expires_at > CURRENT_TIMESTAMP;
|
|
|
|
COMMENT ON VIEW auth.active_sessions_view IS 'Vista de sesiones activas con tiempo restante';
|
|
|
|
-- =====================================================
|
|
-- FIN DEL SCHEMA AUTH
|
|
-- =====================================================
|