import { Router, Request, Response } from "express"; import { db } from "../../db/index"; import { eq, desc, and, or, isNull, inArray, sql } from "drizzle-orm"; import { users, profiles, crmPartners, crmClients, tenants, tenantPlans, partnerClients, partnerCommissions, insertTenantSchema, insertTenantPlanSchema, insertPartnerClientSchema } from "@shared/schema"; import fs from "fs"; import path from "path"; const router = Router(); // Helper: Retorna IDs de tenants que o usuário pode acessar async function getAllowedTenantIds(user: any): Promise { // null = acesso total (master) if (!user.tenantId || user.tenantType === "master") return null; if (user.tenantType === "partner") { // Partner vê seu próprio tenant + clientes vinculados const clientRelations = await db.select({ clientId: partnerClients.clientId }) .from(partnerClients) .where(eq(partnerClients.partnerId, user.tenantId)); return [user.tenantId, ...clientRelations.map(r => r.clientId)]; } // Client vê apenas seu próprio tenant return [user.tenantId]; } // Helper: Verifica se usuário pode acessar um tenant específico async function canAccessTenant(user: any, tenantId: number): Promise { const allowed = await getAllowedTenantIds(user); if (allowed === null) return true; // Master pode tudo return allowed.includes(tenantId); } router.use((req: Request, res: Response, next) => { if (!req.isAuthenticated()) { return res.status(401).json({ error: "Não autorizado" }); } const user = req.user as any; if (user.role !== "admin") { return res.status(403).json({ error: "Acesso negado. Apenas administradores." }); } next(); }); router.get("/profiles", async (req: Request, res: Response) => { try { const allProfiles = await db.select().from(profiles).orderBy(profiles.name); res.json(allProfiles); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/profiles", async (req: Request, res: Response) => { try { const { name, description, type, allowedModules, status } = req.body; const [profile] = await db.insert(profiles).values({ name, description, type: type || "custom", allowedModules: allowedModules || [], status: status || "active", }).returning(); res.status(201).json(profile); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/profiles/:id", async (req: Request, res: Response) => { try { const id = parseInt(req.params.id); const { name, description, allowedModules, status } = req.body; const [profile] = await db.update(profiles) .set({ name, description, allowedModules, status, updatedAt: new Date() }) .where(eq(profiles.id, id)) .returning(); if (!profile) return res.status(404).json({ error: "Perfil não encontrado" }); res.json(profile); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.delete("/profiles/:id", async (req: Request, res: Response) => { try { const id = parseInt(req.params.id); const [profile] = await db.select().from(profiles).where(eq(profiles.id, id)); if (!profile) return res.status(404).json({ error: "Perfil não encontrado" }); if (profile.isSystem === 1) { return res.status(400).json({ error: "Não é possível excluir perfis do sistema" }); } await db.delete(profiles).where(eq(profiles.id, id)); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.get("/users", async (req: Request, res: Response) => { try { const allUsers = await db.select({ id: users.id, username: users.username, name: users.name, email: users.email, role: users.role, profileId: users.profileId, partnerId: users.partnerId, status: users.status, lastLoginAt: users.lastLoginAt, createdAt: users.createdAt, }).from(users).orderBy(users.name); const usersWithProfile = await Promise.all(allUsers.map(async (user) => { let profile = null; let partner = null; if (user.profileId) { const [p] = await db.select().from(profiles).where(eq(profiles.id, user.profileId)); profile = p; } if (user.partnerId) { const [pt] = await db.select().from(crmPartners).where(eq(crmPartners.id, user.partnerId)); partner = pt; } return { ...user, profile, partner }; })); res.json(usersWithProfile); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.patch("/users/:id", async (req: Request, res: Response) => { try { const id = req.params.id; const { name, email, role, profileId, partnerId, status } = req.body; const [user] = await db.update(users) .set({ name, email, role, profileId, partnerId, status }) .where(eq(users.id, id)) .returning(); if (!user) return res.status(404).json({ error: "Usuário não encontrado" }); res.json(user); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/users/:id/status", async (req: Request, res: Response) => { try { const id = req.params.id; const { status } = req.body; const [user] = await db.update(users) .set({ status }) .where(eq(users.id, id)) .returning(); if (!user) return res.status(404).json({ error: "Usuário não encontrado" }); res.json(user); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/partners/:id/status", async (req: Request, res: Response) => { try { const id = parseInt(req.params.id); const { status } = req.body; const [partner] = await db.update(crmPartners) .set({ status, updatedAt: new Date() }) .where(eq(crmPartners.id, id)) .returning(); if (!partner) return res.status(404).json({ error: "Parceiro não encontrado" }); res.json(partner); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.patch("/clients/:id/status", async (req: Request, res: Response) => { try { const id = parseInt(req.params.id); const { status } = req.body; const [client] = await db.update(crmClients) .set({ status, updatedAt: new Date() }) .where(eq(crmClients.id, id)) .returning(); if (!client) return res.status(404).json({ error: "Cliente não encontrado" }); res.json(client); } catch (error: any) { res.status(400).json({ error: error.message }); } }); router.get("/stats", async (req: Request, res: Response) => { try { const [allUsers] = await db.select({ count: db.$count(users) }).from(users); const [activeUsers] = await db.select({ count: db.$count(users) }).from(users).where(eq(users.status, "active")); const [allProfiles] = await db.select({ count: db.$count(profiles) }).from(profiles); const [allPartners] = await db.select({ count: db.$count(crmPartners) }).from(crmPartners); const [activePartners] = await db.select({ count: db.$count(crmPartners) }).from(crmPartners).where(eq(crmPartners.status, "active")); const [allClients] = await db.select({ count: db.$count(crmClients) }).from(crmClients); const [activeClients] = await db.select({ count: db.$count(crmClients) }).from(crmClients).where(eq(crmClients.status, "active")); res.json({ users: { total: allUsers.count, active: activeUsers.count }, profiles: { total: allProfiles.count }, partners: { total: allPartners.count, active: activePartners.count }, clients: { total: allClients.count, active: activeClients.count }, }); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.get("/libraries", async (req: Request, res: Response) => { try { const packageJsonPath = path.join(process.cwd(), "package.json"); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); const dependencies = Object.entries(packageJson.dependencies || {}).map(([name, version]) => ({ name, version: String(version).replace(/^\^|~/, ""), type: "production", category: categorizePackage(name), })); const devDependencies = Object.entries(packageJson.devDependencies || {}).map(([name, version]) => ({ name, version: String(version).replace(/^\^|~/, ""), type: "development", category: categorizePackage(name), })); res.json({ nodejs: { dependencies, devDependencies, total: dependencies.length + devDependencies.length, }, python: { dependencies: [], total: 0, note: "Python microservices planejados para próximas versões", }, }); } catch (error: any) { res.status(500).json({ error: error.message }); } }); function categorizePackage(name: string): string { if (name.includes("react") || name.includes("radix") || name.includes("tailwind")) return "UI/Frontend"; if (name.includes("express") || name.includes("passport") || name.includes("session")) return "Backend/Auth"; if (name.includes("drizzle") || name.includes("pg") || name.includes("connect-pg")) return "Database"; if (name.includes("socket") || name.includes("ws") || name.includes("whatsapp")) return "Real-time/Comunicação"; if (name.includes("openai") || name.includes("ai")) return "AI/ML"; if (name.includes("zod") || name.includes("hook-form")) return "Validação/Forms"; if (name.includes("vite") || name.includes("typescript") || name.includes("tsx") || name.includes("esbuild")) return "Build/Dev Tools"; if (name.includes("lucide") || name.includes("framer")) return "Icons/Animações"; if (name.includes("pdf") || name.includes("docx") || name.includes("csv") || name.includes("zip")) return "Documentos/Arquivos"; if (name.includes("recharts") || name.includes("chart")) return "Visualização"; if (name.includes("date") || name.includes("day-picker")) return "Data/Tempo"; return "Utilitários"; } // ========================================== // MULTI-TENANT MANAGEMENT ROUTES // ========================================== // GET /api/admin/tenants - List all tenants with hierarchy router.get("/tenants", async (req: Request, res: Response) => { try { const { type } = req.query; const user = req.user as any; const allTenants = await db.select().from(tenants).orderBy(desc(tenants.createdAt)); // Filtrar por tenant do usuário let filteredByAccess = allTenants; if (user.tenantType === "partner") { // Partner vê apenas seu próprio tenant e seus clientes const clientRelations = await db.select().from(partnerClients) .where(eq(partnerClients.partnerId, user.tenantId)); const clientIds = clientRelations.map(r => r.clientId); filteredByAccess = allTenants.filter(t => t.id === user.tenantId || clientIds.includes(t.id) ); } else if (user.tenantType === "client") { // Client vê apenas seu próprio tenant filteredByAccess = allTenants.filter(t => t.id === user.tenantId); } // Master (tenantType === "master") vê todos // Filter by type if specified const filteredTenants = type ? filteredByAccess.filter(t => t.tenantType === type) : filteredByAccess; // Add parent tenant info const tenantsWithParent = await Promise.all(filteredTenants.map(async (tenant) => { let parentTenant = null; if (tenant.parentTenantId) { const [parent] = await db.select().from(tenants).where(eq(tenants.id, tenant.parentTenantId)); parentTenant = parent ? { id: parent.id, name: parent.name, tenantType: parent.tenantType } : null; } // Count child tenants const childTenants = allTenants.filter(t => t.parentTenantId === tenant.id); return { ...tenant, parentTenant, childCount: childTenants.length }; })); res.json(tenantsWithParent); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // POST /api/admin/tenants - Create new tenant router.post("/tenants", async (req: Request, res: Response) => { try { const validated = insertTenantSchema.parse(req.body); const [tenant] = await db.insert(tenants).values(validated).returning(); res.status(201).json(tenant); } catch (error: any) { res.status(400).json({ error: error.message }); } }); // PATCH /api/admin/tenants/:id - Update tenant router.patch("/tenants/:id", async (req: Request, res: Response) => { try { const id = parseInt(req.params.id); const user = req.user as any; // Verificar permissão de acesso if (!await canAccessTenant(user, id)) { return res.status(403).json({ error: "Sem permissão para modificar este tenant" }); } const { name, email, phone, plan, status, tenantType, parentTenantId, partnerCode, commissionRate, maxUsers, maxStorageMb, features, billingEmail, trialEndsAt } = req.body; const [tenant] = await db.update(tenants) .set({ name, email, phone, plan, status, tenantType, parentTenantId, partnerCode, commissionRate, maxUsers, maxStorageMb, features, billingEmail, trialEndsAt, updatedAt: new Date() }) .where(eq(tenants.id, id)) .returning(); if (!tenant) return res.status(404).json({ error: "Tenant não encontrado" }); res.json(tenant); } catch (error: any) { res.status(400).json({ error: error.message }); } }); // DELETE /api/admin/tenants/:id - Delete tenant router.delete("/tenants/:id", async (req: Request, res: Response) => { try { const id = parseInt(req.params.id); const user = req.user as any; // Apenas master pode excluir tenants if (user.tenantType !== "master") { return res.status(403).json({ error: "Apenas master pode excluir tenants" }); } const [tenant] = await db.select().from(tenants).where(eq(tenants.id, id)); if (!tenant) return res.status(404).json({ error: "Tenant não encontrado" }); if (tenant.tenantType === "master") { return res.status(400).json({ error: "Não é possível excluir o tenant master" }); } await db.delete(tenants).where(eq(tenants.id, id)); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // GET /api/admin/tenants/hierarchy - Get hierarchy tree router.get("/tenants/hierarchy", async (req: Request, res: Response) => { try { const user = req.user as any; const allowedIds = await getAllowedTenantIds(user); let allTenants = await db.select().from(tenants).orderBy(tenants.name); // Filtrar por acesso do usuário if (allowedIds !== null) { allTenants = allTenants.filter(t => allowedIds.includes(t.id)); } // Build hierarchy tree const buildTree = (parentId: number | null): any[] => { return allTenants .filter(t => t.parentTenantId === parentId) .map(t => ({ ...t, children: buildTree(t.id) })); }; // Start from master tenants (no parent) or user's tenant const hierarchy = allowedIds === null ? buildTree(null) : allTenants.map(t => ({ ...t, children: [] })); res.json(hierarchy); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========================================== // TENANT PLANS ROUTES // ========================================== // GET /api/admin/plans - List all plans router.get("/plans", async (req: Request, res: Response) => { try { const allPlans = await db.select().from(tenantPlans).orderBy(tenantPlans.sortOrder); res.json(allPlans); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // POST /api/admin/plans - Create new plan router.post("/plans", async (req: Request, res: Response) => { try { const validated = insertTenantPlanSchema.parse(req.body); const [plan] = await db.insert(tenantPlans).values(validated).returning(); res.status(201).json(plan); } catch (error: any) { res.status(400).json({ error: error.message }); } }); // PATCH /api/admin/plans/:id - Update plan router.patch("/plans/:id", async (req: Request, res: Response) => { try { const id = parseInt(req.params.id); const { name, description, maxUsers, maxStorageMb, features, monthlyPrice, yearlyPrice, trialDays, isActive, sortOrder } = req.body; const [plan] = await db.update(tenantPlans) .set({ name, description, maxUsers, maxStorageMb, features, monthlyPrice, yearlyPrice, trialDays, isActive, sortOrder }) .where(eq(tenantPlans.id, id)) .returning(); if (!plan) return res.status(404).json({ error: "Plano não encontrado" }); res.json(plan); } catch (error: any) { res.status(400).json({ error: error.message }); } }); // DELETE /api/admin/plans/:id - Delete plan router.delete("/plans/:id", async (req: Request, res: Response) => { try { const id = parseInt(req.params.id); await db.delete(tenantPlans).where(eq(tenantPlans.id, id)); res.status(204).send(); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========================================== // PARTNER-CLIENT RELATIONSHIPS // ========================================== // GET /api/admin/partner-clients - List partner-client relationships router.get("/partner-clients", async (req: Request, res: Response) => { try { const user = req.user as any; const allowedIds = await getAllowedTenantIds(user); let relationships = await db.select().from(partnerClients).orderBy(desc(partnerClients.startedAt)); // Filtrar por acesso do usuário if (allowedIds !== null) { relationships = relationships.filter(r => allowedIds.includes(r.partnerId) || allowedIds.includes(r.clientId) ); } // Add partner and client info const withDetails = await Promise.all(relationships.map(async (rel) => { const [partner] = await db.select().from(tenants).where(eq(tenants.id, rel.partnerId)); const [client] = await db.select().from(tenants).where(eq(tenants.id, rel.clientId)); return { ...rel, partner: partner ? { id: partner.id, name: partner.name } : null, client: client ? { id: client.id, name: client.name } : null, }; })); res.json(withDetails); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // POST /api/admin/partner-clients - Create partner-client relationship router.post("/partner-clients", async (req: Request, res: Response) => { try { const validated = insertPartnerClientSchema.parse(req.body); const [relationship] = await db.insert(partnerClients).values(validated).returning(); res.status(201).json(relationship); } catch (error: any) { res.status(400).json({ error: error.message }); } }); // ========================================== // PARTNER COMMISSIONS // ========================================== // GET /api/admin/commissions - List all commissions router.get("/commissions", async (req: Request, res: Response) => { try { const user = req.user as any; const allowedIds = await getAllowedTenantIds(user); const { partnerId, status } = req.query; let allCommissions = await db.select().from(partnerCommissions).orderBy(desc(partnerCommissions.createdAt)); // Filtrar por acesso do usuário if (allowedIds !== null) { allCommissions = allCommissions.filter(c => allowedIds.includes(c.partnerId) || allowedIds.includes(c.clientId) ); } if (partnerId) { allCommissions = allCommissions.filter(c => c.partnerId === parseInt(partnerId as string)); } if (status) { allCommissions = allCommissions.filter(c => c.status === status); } // Add partner and client info const withDetails = await Promise.all(allCommissions.map(async (comm) => { const [partner] = await db.select().from(tenants).where(eq(tenants.id, comm.partnerId)); const [client] = await db.select().from(tenants).where(eq(tenants.id, comm.clientId)); return { ...comm, partner: partner ? { id: partner.id, name: partner.name } : null, client: client ? { id: client.id, name: client.name } : null, }; })); res.json(withDetails); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // PATCH /api/admin/commissions/:id/approve - Approve commission router.patch("/commissions/:id/approve", async (req: Request, res: Response) => { try { const id = parseInt(req.params.id); const [commission] = await db.update(partnerCommissions) .set({ status: "approved", approvedAt: new Date() }) .where(eq(partnerCommissions.id, id)) .returning(); if (!commission) return res.status(404).json({ error: "Comissão não encontrada" }); res.json(commission); } catch (error: any) { res.status(400).json({ error: error.message }); } }); // PATCH /api/admin/commissions/:id/pay - Mark commission as paid router.patch("/commissions/:id/pay", async (req: Request, res: Response) => { try { const id = parseInt(req.params.id); const { paymentReference } = req.body; const [commission] = await db.update(partnerCommissions) .set({ status: "paid", paidAt: new Date(), paymentReference }) .where(eq(partnerCommissions.id, id)) .returning(); if (!commission) return res.status(404).json({ error: "Comissão não encontrada" }); res.json(commission); } catch (error: any) { res.status(400).json({ error: error.message }); } }); // GET /api/admin/tenants/stats - Tenant statistics router.get("/tenants/stats", async (req: Request, res: Response) => { try { const allTenants = await db.select().from(tenants); const stats = { total: allTenants.length, byType: { master: allTenants.filter(t => t.tenantType === "master").length, partner: allTenants.filter(t => t.tenantType === "partner").length, client: allTenants.filter(t => t.tenantType === "client").length, }, byStatus: { active: allTenants.filter(t => t.status === "active").length, trial: allTenants.filter(t => t.status === "trial").length, suspended: allTenants.filter(t => t.status === "suspended").length, cancelled: allTenants.filter(t => t.status === "cancelled").length, } }; res.json(stats); } catch (error: any) { res.status(500).json({ error: error.message }); } }); export default router;