workspace-v1/shared/libs/multi-tenancy/README.md
rckrdmrd 66161b1566 feat: Workspace-v1 complete migration with NEXUS v3.4
Sistema NEXUS v3.4 migrado con:

Estructura principal:
- core/orchestration: Sistema SIMCO + CAPVED (27 directivas, 28 perfiles)
- core/catalog: Catalogo de funcionalidades reutilizables
- shared/knowledge-base: Base de conocimiento compartida
- devtools/scripts: Herramientas de desarrollo
- control-plane/registries: Control de servicios y CI/CD
- orchestration/: Configuracion de orquestacion de agentes

Proyectos incluidos (11):
- gamilit (submodule -> GitHub)
- trading-platform (OrbiquanTIA)
- erp-suite con 5 verticales:
  - erp-core, construccion, vidrio-templado
  - mecanicas-diesel, retail, clinicas
- betting-analytics
- inmobiliaria-analytics
- platform_marketing_content
- pos-micro, erp-basico

Configuracion:
- .gitignore completo para Node.js/Python/Docker
- gamilit como submodule (git@github.com:rckrdmrd/gamilit-workspace.git)
- Sistema de puertos estandarizado (3005-3199)

Generated with NEXUS v3.4 Migration System
EPIC-010: Configuracion Git y Repositorios
2026-01-04 03:37:42 -06:00

9.8 KiB

Soporte Multi-Tenant

Versión: 1.0.0 Origen: projects/gamilit Estado: Producción Última actualización: 2025-12-08


Descripción

Sistema de multi-tenancy para aplicaciones SaaS:

  • Aislamiento de datos por organización
  • Usuarios pueden pertenecer a múltiples tenants
  • Roles específicos por tenant (owner, admin, member)
  • Configuración y personalización por tenant
  • Límites de recursos (usuarios, storage)
  • Niveles de suscripción

Características

Característica Descripción
Aislamiento Datos separados por tenant
Multi-membresía Usuario en múltiples tenants
Roles por tenant owner, admin, member, viewer
Suscripciones free, basic, pro, enterprise
Personalización Theme, logo, dominio custom
Límites Máx usuarios, storage
RLS (opcional) Row Level Security en PostgreSQL

Stack Tecnológico

backend:
  framework: NestJS
  orm: TypeORM
  database: PostgreSQL

patterns:
  - Tenant discriminator (tenant_id en tablas)
  - Middleware de tenant context
  - Optional: RLS para seguridad extra

Tablas Requeridas

Tabla Propósito
auth_management.tenants Organizaciones/empresas
auth_management.memberships Relación usuario-tenant
auth_management.profiles Perfil extendido con tenant_id

Estructura del Módulo

multi-tenancy/
├── entities/
│   ├── tenant.entity.ts
│   └── membership.entity.ts
├── services/
│   ├── tenant.service.ts
│   └── membership.service.ts
├── middleware/
│   └── tenant-context.middleware.ts
├── guards/
│   └── tenant-member.guard.ts
├── decorators/
│   └── current-tenant.decorator.ts
└── dto/
    ├── create-tenant.dto.ts
    └── membership.dto.ts

Modelos de Datos

Tenant (Organización)

interface Tenant {
  id: string;                    // UUID
  name: string;                  // "Empresa XYZ"
  slug: string;                  // "empresa-xyz" (único)
  domain?: string;               // "xyz.app.com"
  logo_url?: string;
  subscription_tier: 'free' | 'basic' | 'pro' | 'enterprise';
  max_users: number;             // Límite de usuarios
  max_storage_gb: number;        // Límite de storage
  is_active: boolean;
  trial_ends_at?: Date;
  settings: {                    // Configuración JSONB
    theme: string;
    features: Record<string, boolean>;
    language: string;
    timezone: string;
  };
  metadata: Record<string, any>;
}

Membership (Usuario-Tenant)

interface Membership {
  id: string;
  user_id: string;
  tenant_id: string;
  role: 'owner' | 'admin' | 'member' | 'viewer';
  status: 'pending' | 'active' | 'suspended';
  invited_by?: string;
  joined_at: Date;
}

Flujo de Multi-Tenancy

1. Usuario autenticado
       │
       ▼
2. Middleware extrae tenant de:
   - Header: X-Tenant-ID
   - Subdomain: xyz.app.com → "xyz"
   - Query param: ?tenant=xyz
       │
       ▼
3. Verificar membresía activa
       │
       ▼
4. Inyectar tenant_id en contexto
       │
       ▼
5. Queries filtran por tenant_id

Uso Rápido

1. Middleware de Tenant

// src/common/middleware/tenant-context.middleware.ts
@Injectable()
export class TenantContextMiddleware implements NestMiddleware {
  constructor(
    private readonly membershipService: MembershipService,
  ) {}

  async use(req: Request, res: Response, next: NextFunction) {
    const tenantId = this.extractTenantId(req);

    if (!tenantId) {
      return next(); // Rutas sin tenant
    }

    // Verificar membresía si hay usuario
    if (req.user?.id) {
      const membership = await this.membershipService.findByUserAndTenant(
        req.user.id,
        tenantId,
      );

      if (!membership || membership.status !== 'active') {
        throw new ForbiddenException('No tienes acceso a este tenant');
      }

      req.tenantContext = {
        tenantId,
        role: membership.role,
      };
    }

    next();
  }

  private extractTenantId(req: Request): string | null {
    // Opción 1: Header
    const headerTenant = req.headers['x-tenant-id'] as string;
    if (headerTenant) return headerTenant;

    // Opción 2: Subdomain
    const host = req.hostname;
    const subdomain = host.split('.')[0];
    if (subdomain && subdomain !== 'www' && subdomain !== 'app') {
      return subdomain;
    }

    // Opción 3: Query param
    return req.query.tenant as string;
  }
}

2. Guard de Membresía

// src/common/guards/tenant-member.guard.ts
@Injectable()
export class TenantMemberGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const req = context.switchToHttp().getRequest();

    if (!req.tenantContext) {
      throw new ForbiddenException('Tenant context required');
    }

    return true;
  }
}

// Guard con rol específico
@Injectable()
export class TenantAdminGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const req = context.switchToHttp().getRequest();

    if (!req.tenantContext) {
      throw new ForbiddenException('Tenant context required');
    }

    const allowedRoles = ['owner', 'admin'];
    if (!allowedRoles.includes(req.tenantContext.role)) {
      throw new ForbiddenException('Admin role required');
    }

    return true;
  }
}

3. Decorador para obtener tenant

// src/common/decorators/current-tenant.decorator.ts
export const CurrentTenant = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.tenantContext;
  },
);

// Uso en controller
@Get('data')
@UseGuards(JwtAuthGuard, TenantMemberGuard)
getData(@CurrentTenant() tenant: TenantContext) {
  return this.service.findByTenant(tenant.tenantId);
}

4. Service con filtro de tenant

@Injectable()
export class ProjectService {
  async findAll(tenantId: string): Promise<Project[]> {
    return this.projectRepository.find({
      where: { tenant_id: tenantId },
    });
  }

  async create(tenantId: string, dto: CreateProjectDto): Promise<Project> {
    const project = this.projectRepository.create({
      ...dto,
      tenant_id: tenantId, // Siempre asignar tenant
    });
    return this.projectRepository.save(project);
  }
}

Crear Nuevo Tenant

async createTenant(dto: CreateTenantDto, ownerId: string): Promise<Tenant> {
  // 1. Crear tenant
  const tenant = this.tenantRepository.create({
    name: dto.name,
    slug: this.generateSlug(dto.name),
    subscription_tier: 'free',
    max_users: 10,
    max_storage_gb: 1,
    settings: {
      theme: 'default',
      features: { analytics: true },
      language: 'es',
    },
  });
  await this.tenantRepository.save(tenant);

  // 2. Crear membresía de owner
  const membership = this.membershipRepository.create({
    user_id: ownerId,
    tenant_id: tenant.id,
    role: 'owner',
    status: 'active',
    joined_at: new Date(),
  });
  await this.membershipRepository.save(membership);

  return tenant;
}

Invitar Usuario a Tenant

async inviteUser(
  tenantId: string,
  inviterId: string,
  email: string,
  role: string,
): Promise<void> {
  // 1. Buscar usuario por email
  const user = await this.userRepository.findOne({ where: { email } });

  if (!user) {
    // Enviar invitación por email para registro
    await this.mailService.sendInvitation(email, tenantId, role);
    return;
  }

  // 2. Verificar si ya es miembro
  const existing = await this.membershipRepository.findOne({
    where: { user_id: user.id, tenant_id: tenantId },
  });

  if (existing) {
    throw new ConflictException('Usuario ya es miembro');
  }

  // 3. Crear membresía
  const membership = this.membershipRepository.create({
    user_id: user.id,
    tenant_id: tenantId,
    role,
    status: 'active',
    invited_by: inviterId,
    joined_at: new Date(),
  });
  await this.membershipRepository.save(membership);
}

Row Level Security (Opcional)

Para seguridad adicional a nivel de base de datos:

-- Habilitar RLS en tabla
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

-- Policy: usuarios solo ven proyectos de sus tenants
CREATE POLICY tenant_isolation ON projects
    USING (
        tenant_id IN (
            SELECT tenant_id FROM memberships
            WHERE user_id = current_setting('app.current_user_id')::uuid
            AND status = 'active'
        )
    );

-- Antes de cada query, setear el user_id
SET app.current_user_id = 'user-uuid';

Variables de Entorno

# Multi-tenancy
ENABLE_MULTITENANCY=true
DEFAULT_TENANT_SLUG=main

# Límites por defecto
DEFAULT_MAX_USERS=100
DEFAULT_MAX_STORAGE_GB=5

Endpoints Principales

Método Ruta Descripción
GET /tenants Listar tenants del usuario
POST /tenants Crear nuevo tenant
GET /tenants/:id Obtener detalle de tenant
PUT /tenants/:id Actualizar tenant (admin)
POST /tenants/:id/invite Invitar usuario
GET /tenants/:id/members Listar miembros
PUT /tenants/:id/members/:userId Cambiar rol
DELETE /tenants/:id/members/:userId Remover miembro

Adaptaciones Necesarias

  1. Método de detección: Header, subdomain, o query param
  2. Roles: Ajustar según necesidades (owner, admin, etc.)
  3. Suscripciones: Definir tiers y límites
  4. Settings: Estructura de configuración por tenant
  5. RLS: Implementar si se requiere seguridad extra

Referencias


Mantenido por: Sistema NEXUS Proyecto origen: Gamilit Platform