Skip to main content

terral-architecture

Arquitectura completa de TerrAn — ERP municipal con vista 3D, gestión documental, IA y datos en tiempo real. Referencia para hardware, escalabilidad y patrones.

الانتقال إلى التثبيت

معلومات المصدر

المستودع
Ntizar/NtizarBrainMasterMind
آخر نشاط في المصدر
٢٦ يونيو ٢٠٢٦ في ١٢:٠٥
لغة SKILL.md المكتشفة
الإسبانية
النجوم
٢
التفرعات
٠

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

مستكشف الملفات
9 ملفات

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
terral-architecture
description
Arquitectura completa de TerrAn — ERP municipal con vista 3D, gestión documental, IA y datos en tiempo real. Referencia para hardware, escalabilidad y patrones.
version
2.0.0
author
David Antizar
tags
["erp","municipal","saas","3d","gis","postgis","threejs","ai","rag","terran"]
# TerrAn — Arquitectura de Referencia ## Qué es TerrAn **Terr**itorio + **An**tizar (+ NaN builders). ERP municipal con vista 3D donde ayuntamientos y empresas gestionan activos físicos y humanos sobre un mapa interactivo de España. Datos en tiempo real (clima, trenes, cámaras), búsqueda semántica con IA, y gestión documental completa. ## Stack tecnológico | Capa | Tecnología | Versión | Justificación | |---|---|---|---| | **Backend** | Node.js + Express | 20 LTS | Rápido de desarrollar, ESM | | **BD principal** | PostgreSQL + PostGIS | 16+ | GIS nativo, particiones, FTS | | **Cache** | Redis | 7+ | KPIs, sesiones, posiciones | | **Documentos** | MinIO (S3 self-hosted) | Latest | PDFs/fotos, no en BD | | **Búsqueda semántica** | ChromaDB | Latest | Embeddings, RAG | | **IA** | qwen3.6 vía NaN API | — | Asistente conversacional | | **Frontend 3D** | Three.js | 0.163+ | Terreno, activos, LOD | | **Frontend UI** | Vanilla JS + Aurora CSS | — | Estilo David, sin framework | | **WebSocket** | ws (Node.js) | — | Real-time | | **PDF extraction** | pdf-parse | — | Extraer texto de PDFs | | **Deploy** | Docker + NaN Builders | — | Contenedor aislado | | **Object Storage** | MinIO | — | Alternativa self-hosted a S3 | ## Hardware mínimo recomendado ### Desarrollo local - 4GB RAM, 2 cores, 40GB disco - PostgreSQL + Redis + MinIO + Node.js = ~2GB RAM total - ChromaDB = ~500MB RAM adicional ### Producción (1 ayuntamiento pequeño-medium) - **Servidor:** 8GB RAM, 4 cores, 200GB SSD - **PostgreSQL:** 4GB RAM, 100GB SSD (con particiones) - **Redis:** 1GB RAM - **MinIO:** 50GB (documentos) - **Node.js:** 2GB RAM (2 instancias para HA) - **Total estimado:** ~8GB RAM, 4 cores, 200GB SSD ### Producción (multi-tenant, 10+ ayuntamientos) - **Servidor:** 32GB RAM, 8 cores, 1TB SSD - **PostgreSQL:** 16GB RAM (read replica) - **Redis:** 4GB RAM (cluster) - **MinIO:** 500GB (documentos) - **Node.js:** 8GB RAM (4 instancias, load balancer) - **Total estimado:** ~32GB RAM, 8 cores, 1TB SSD ## Patrones de arquitectura ### Multi-tenant - Cada ayuntamiento = una `organizacion` con `org_id` - TODAS las queries filtran por `org_id` - RLS (Row Level Security) en PostgreSQL para aislamiento - Un usuario solo ve datos de su organización ### Sistema de Permisos y RBAC (v2 — corregido tras auditoría) ⚠️ **La versión v1 del schema (CHECK constraint con 7 roles fijos + tabla `permisos` con `alcance` como VARCHAR libre) tiene estos problemas graves:** 1. **Roles fijos en CHECK no escalan a multi-tenant** — Cada ayuntamiento necesita roles distintos. No todos tienen alcalde, concejal, inspector. 2. **`permisos.alcance` como VARCHAR(100) plano** — Sin jerarquía, sin validación, se rompe silenciosamente con typos. 3. **Sin herencia de permisos** — Un `operario` de policía no hereda "ver activos" de su rol ni de su departamento. 4. **Sin distinción VER vs EDITAR** — Un alcalde debe poder ver todo pero NO editar salarios ni crear usuarios. 5. **Sin RLS real** — El filtrado está en middleware Node.js, no en PostgreSQL. Un bug en la API expone todos los datos. 6. **Sin lock release automático** — Si un usuario abre un activo, lo bloquea con optimistic locking, y se va → el activo queda bloqueado para siempre. #### Arquitectura RBAC corregida (3 capas) ``` Capa 1: PLATAFORMA ┌─────────────────────────────────┐ │ superadmin │ │ • Gestiona toda la plataforma │ │ • Crear organizaciones │ │ • Configurar tiers/precios │ │ • Ver logs de todas las orgs │ └─────────────────────────────────┘ Capa 2: ORGANIZACIÓN (por tenant) ┌─────────────────────────────────┐ │ admin_org (1 por organización) │ │ • Puede TODO en su org │ │ • Crear/editar/eliminar │ │ CUALQUIER recurso │ │ • Gestionar usuarios locales │ │ • Definir roles personalizados │ │ • Ver audit logs de su org │ │ • Override de optimistic locks │ ├─────────────────────────────────┤ │ roles_personalizados │ │ (cada organización define │ │ sus propios roles con │ │ permisos asignables) │ └─────────────────────────────────┘ Capa 3: DATOS ┌─────────────────────────────────┐ │ RLS en PostgreSQL (obligatorio)│ │ • org_id en TODAS las queries │ │ • RLS policies por tabla │ │ • current_setting('app.*') │ └─────────────────────────────────┘ ``` #### Schema de permisos corregido ```sql -- ============================================ -- ROLES: ya no son CHECK fijo, son por organización -- ============================================ CREATE TABLE roles_organizacion ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), org_id UUID NOT NULL REFERENCES organizaciones(id), nombre VARCHAR(100) NOT NULL, -- 'alcalde', 'concejal', 'inspector_medioambiental' nivel INTEGER NOT NULL DEFAULT 0, -- 0=admin, 10=directivo, 50=operario, 99=lectura hereda_de UUID REFERENCES roles_organizacion(id), -- Herencia: operario hereda de lectura es_admin BOOLEAN DEFAULT false, -- Si true, puede overridear TODO activo BOOLEAN DEFAULT true, created_at TIMESTAMPTZ DEFAULT now(), UNIQUE (org_id, nombre) ); -- ============================================ -- PERMISOS: estructurados, no VARCHAR mágico -- ============================================ CREATE TABLE permisos_rol ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), rol_id UUID NOT NULL REFERENCES roles_organizacion(id), -- Qué recurso recurso_categoria VARCHAR(100) NOT NULL, -- 'contenedor', 'ambulancia', 'doctor', 'usuario', 'documento' -- Niveles de permiso (jerárquicos: READ < WRITE < DELETE < ADMIN) permiso_nivel VARCHAR(20) NOT NULL CHECK (permiso_nivel IN ( 'none', -- Sin acceso (explícito) 'read', -- Solo ver 'write', -- Ver + crear + editar (implica read) 'delete', -- Ver + editar + borrar (implica write) 'admin' -- Todo + conceder a otros (implica delete) )), -- Alcance geográfico/departamental alcance_tipo VARCHAR(30) NOT NULL CHECK (alcance_tipo IN ( 'global', -- Todo el municipio 'departamento', -- 'policia', 'sanidad', 'urbanismo' 'zona', -- 'centro', 'norte', 'poligono_industrial' 'propio' -- Solo activos que creó o tiene asignados )), alcance_valor VARCHAR(100), -- Valor concreto: 'policia', 'centro', etc. NULL si alcance_tipo = 'global' created_at TIMESTAMPTZ DEFAULT now(), UNIQUE (rol_id, recurso_categoria, alcance_tipo, COALESCE(alcance_valor, '__global__')) ); -- ============================================ -- PERMISOS ESPECIALES (ad-hoc, para excepciones) -- ============================================ CREATE TABLE permisos_usuario ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), usuario_id UUID NOT NULL REFERENCES usuarios(id), recurso_categoria VARCHAR(100) NOT NULL, permiso_nivel VARCHAR(20) NOT NULL CHECK (permiso_nivel IN ('none', 'read', 'write', 'delete', 'admin')), alcance_tipo VARCHAR(30) NOT NULL, alcance_valor VARCHAR(100), granted_by UUID NOT NULL REFERENCES usuarios(id), expira_en TIMESTAMPTZ, -- NULL = permanente motivo TEXT, created_at TIMESTAMPTZ DEFAULT now(), UNIQUE (usuario_id, recurso_categoria, alcance_tipo, COALESCE(alcance_valor, '__global__')) ); -- ============================================ -- RLS: aislamiento REAL en PostgreSQL -- ============================================ -- Función helper CREATE OR REPLACE FUNCTION app_current_org_id() RETURNS UUID AS $$ SELECT NULLIF(current_setting('app.org_id', true), '')::UUID; $$ LANGUAGE SQL STABLE; CREATE OR REPLACE FUNCTION app_current_user_id() RETURNS UUID AS $$ SELECT NULLIF(current_setting('app.user_id', true), '')::UUID; $$ LANGUAGE SQL STABLE; CREATE OR REPLACE FUNCTION app_is_admin() RETURNS BOOLEAN AS $$ SELECT current_setting('app.is_admin', true) = 'true'; $$ LANGUAGE SQL STABLE; -- RLS en activos ALTER TABLE activos ENABLE ROW LEVEL SECURITY; -- Policy base: solo activos de mi organización CREATE POLICY activos_org ON activos FOR ALL USING (org_id = app_current_org_id()); -- Policy para admin: puede ver/editar todo en su org CREATE POLICY activos_admin ON activos FOR ALL USING (app_is_admin() AND org_id = app_current_org_id()) WITH CHECK (true); -- Policy para operario con alcance de zona CREATE POLICY activos_zona ON activos FOR ALL USING ( org_id = app_current_org_id() AND ( -- El usuario tiene alcance_zona = valor de activos.zona EXISTS ( SELECT 1 FROM permisos_usuario pu JOIN usuarios u ON u.id = pu.usuario_id WHERE u.id = app_current_user_id() AND pu.alcance_tipo = 'zona' AND (activos.metadata->>'zona') = pu.alcance_valor AND pu.permiso_nivel IN ('write', 'delete', 'admin') ) OR app_is_admin() ) ); -- ============================================ -- LOCK RELEASE AUTOMÁTICO -- ============================================ ALTER TABLE activos ADD COLUMN IF NOT EXISTS locked_by UUID REFERENCES usuarios(id); ALTER TABLE activos ADD COLUMN IF NOT EXISTS locked_at TIMESTAMPTZ; -- Función de liberación automática (ejecutar cada 5 min vía cron) CREATE OR REPLACE FUNCTION release_stale_locks() RETURNS INTEGER AS $$ DECLARE released INTEGER; BEGIN UPDATE activos SET locked_by = NULL, locked_at = NULL WHERE locked_at IS NOT NULL AND locked_at < now() - INTERVAL '10 minutes' AND deleted_at IS NULL; GET DIAGNOSTICS released = ROW_COUNT; RETURN released; END; $$ LANGUAGE plpgsql; ``` #### Middleware de autorización (Node.js) ```javascript // middleware/rls-context.js // Antes de cada request, inyecta el contexto en PostgreSQL para RLS async function setRLSContext(req, res, next) { if (req.user) { await pool.query(`SELECT set_config('app.org_id', $1, true)`, [req.user.org_id]); await pool.query(`SELECT set_config('app.user_id', $1, true)`, [req.user.id]); await pool.query(`SELECT set_config('app.is_admin', $1, true)`, [req.user.es_admin ? 'true' : 'false']); } next(); } // middleware/permission-check.js // Verificación adicional a nivel de aplicación (no solo RLS) function requirePermission(categoria, nivelMinimo, alcanceTipo = null) { return async (req, res, next) => { if (req.user.es_admin) return next(); // ADMIN puede TODO const tienePermiso = await checkPermission( req.user.id, categoria, nivelMinimo, alcanceTipo, req.params.zona ); if (!tienePermiso) { return res.status(403).json({ error: 'permiso_denegado', message: `No tienes permiso para ${accionNivel(nivelMinimo)} ${categoria}`, required: { categoria, nivelMinimo, alcance: alcanceTipo } }); } next(); }; } ``` #### Verificación de permisos en Node.js ```javascript // services/permission-query.js async function checkPermission(userId, categoria, nivelMinimo, alcanceTipo, alcanceValor) { const niveles = { 'none': 0, 'read': 1, 'write': 2, 'delete': 3, 'admin': 4 }; const minNivel = niveles[nivelMinimo];
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub