arcadia-suite-sv/server/admin/routes.ts

622 lines
22 KiB
TypeScript

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<number[] | null> {
// 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<boolean> {
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;