feat: implement authentication and RBAC
- packages/auth: Argon2id password hashing, JWT access tokens (jose), opaque refresh tokens with rotation, generic error messages (no user-enumeration via timing or message differences) - roles/permissions/role_permissions/user_roles/sessions/audit_logs schema (agente.md secoes 142-150); RBAC scope PLATFORM vs TENANT - withUserContext(): narrow RLS exception so a user can discover their own tenant_memberships before a tenant is chosen (login flow) - userHasPermission()/isPlatformUser(): explicit service-layer RBAC checks (roles/permissions tables are not RLS-protected — documented why in docs/AUTHENTICATION.md) - seed: permission catalog, 4 system roles, initial Platform Super Admin (password written once to FIRST_LOGIN.txt, 600, outside Git) - automated end-to-end test: login, RBAC check, refresh rotation, logout
This commit is contained in:
172
packages/auth/src/seed.ts
Normal file
172
packages/auth/src/seed.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Seed de RBAC (agente.md secoes 142, 145, 199, 200): catálogo de
|
||||
* permissions, roles de sistema, e o Platform Super Admin inicial.
|
||||
*
|
||||
* Idempotente — seguro rodar de novo (upsert por chave única). Roda com:
|
||||
* pnpm --filter @b2bcall/auth run seed
|
||||
*/
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { writeFileSync, chmodSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { getPrismaClient } from "@b2bcall/database";
|
||||
import { hashPassword } from "./password";
|
||||
|
||||
// Catálogo completo de permissions (agente.md secao 145).
|
||||
const PERMISSIONS: Array<{ key: string; description: string }> = [
|
||||
{ key: "tenants.view", description: "Ver tenants (plataforma)" },
|
||||
{ key: "tenants.manage", description: "Criar/editar/suspender tenants" },
|
||||
{ key: "billing.view", description: "Ver consumo e faturas" },
|
||||
{ key: "billing.manage", description: "Gerenciar fechamentos de billing" },
|
||||
{ key: "pricing.manage", description: "Gerenciar planos e price books" },
|
||||
{ key: "dashboard.view", description: "Ver dashboards" },
|
||||
{ key: "extensions.view", description: "Ver ramais" },
|
||||
{ key: "extensions.manage", description: "Criar/editar ramais" },
|
||||
{ key: "trunks.view", description: "Ver troncos" },
|
||||
{ key: "trunks.manage", description: "Criar/editar troncos" },
|
||||
{ key: "agents.view", description: "Ver agentes" },
|
||||
{ key: "agents.manage", description: "Criar/editar agentes" },
|
||||
{ key: "queues.view", description: "Ver filas" },
|
||||
{ key: "queues.manage", description: "Criar/editar filas" },
|
||||
{ key: "campaigns.view", description: "Ver campanhas" },
|
||||
{ key: "campaigns.create", description: "Criar campanhas" },
|
||||
{ key: "campaigns.update", description: "Editar campanhas" },
|
||||
{ key: "campaigns.start", description: "Iniciar campanhas" },
|
||||
{ key: "campaigns.pause", description: "Pausar campanhas" },
|
||||
{ key: "campaigns.stop", description: "Parar campanhas" },
|
||||
{ key: "monitoring.view", description: "Ver monitoramento em tempo real" },
|
||||
{ key: "reports.view", description: "Ver relatórios" },
|
||||
{ key: "reports.export", description: "Exportar relatórios" },
|
||||
{ key: "recordings.view", description: "Ver gravações" },
|
||||
{ key: "recordings.download", description: "Baixar gravações" },
|
||||
{ key: "ai.view", description: "Ver análises de IA" },
|
||||
{ key: "ai.manage", description: "Configurar providers/prompts de IA" },
|
||||
{ key: "ai.analyze", description: "Disparar análise de IA manualmente" },
|
||||
{ key: "freeswitch.view", description: "Ver estado do FreeSWITCH" },
|
||||
{ key: "freeswitch.configure", description: "Configurar FreeSWITCH" },
|
||||
{ key: "users.manage", description: "Gerenciar usuários" },
|
||||
{ key: "roles.manage", description: "Gerenciar roles/permissões" },
|
||||
{ key: "audit.view", description: "Ver audit log" },
|
||||
];
|
||||
|
||||
// Mapeamento inicial role -> permissions. Ponto de partida razoável;
|
||||
// revisar quando existir uma UI de administração de RBAC.
|
||||
const ROLE_PERMISSIONS: Record<string, string[]> = {
|
||||
platform_super_admin: PERMISSIONS.map((p) => p.key), // tudo
|
||||
tenant_admin: PERMISSIONS.map((p) => p.key).filter(
|
||||
(key) => !["tenants.view", "tenants.manage", "pricing.manage"].includes(key),
|
||||
),
|
||||
supervisor: [
|
||||
"dashboard.view",
|
||||
"extensions.view",
|
||||
"trunks.view",
|
||||
"agents.view",
|
||||
"agents.manage",
|
||||
"queues.view",
|
||||
"campaigns.view",
|
||||
"campaigns.update",
|
||||
"campaigns.start",
|
||||
"campaigns.pause",
|
||||
"campaigns.stop",
|
||||
"monitoring.view",
|
||||
"reports.view",
|
||||
"reports.export",
|
||||
"recordings.view",
|
||||
"recordings.download",
|
||||
"ai.view",
|
||||
],
|
||||
agent: ["dashboard.view", "campaigns.view"],
|
||||
};
|
||||
|
||||
const SYSTEM_ROLES: Array<{ key: string; name: string; scope: "PLATFORM" | "TENANT" }> = [
|
||||
{ key: "platform_super_admin", name: "Platform Super Admin", scope: "PLATFORM" },
|
||||
{ key: "tenant_admin", name: "Tenant Admin", scope: "TENANT" },
|
||||
{ key: "supervisor", name: "Supervisor", scope: "TENANT" },
|
||||
{ key: "agent", name: "Agente", scope: "TENANT" },
|
||||
];
|
||||
|
||||
const PLATFORM_ADMIN_EMAIL = "admin@b2bcall.local";
|
||||
|
||||
async function main() {
|
||||
const prisma = getPrismaClient();
|
||||
|
||||
for (const permission of PERMISSIONS) {
|
||||
await prisma.permission.upsert({
|
||||
where: { key: permission.key },
|
||||
update: { description: permission.description },
|
||||
create: permission,
|
||||
});
|
||||
}
|
||||
console.log(`Permissions: ${PERMISSIONS.length} sincronizadas.`);
|
||||
|
||||
for (const role of SYSTEM_ROLES) {
|
||||
const created = await prisma.role.upsert({
|
||||
where: { key: role.key },
|
||||
update: { name: role.name, scope: role.scope, isSystem: true },
|
||||
create: { ...role, isSystem: true },
|
||||
});
|
||||
|
||||
const permissionKeys = ROLE_PERMISSIONS[role.key] ?? [];
|
||||
const permissions = await prisma.permission.findMany({
|
||||
where: { key: { in: permissionKeys } },
|
||||
});
|
||||
|
||||
await prisma.rolePermission.deleteMany({ where: { roleId: created.id } });
|
||||
if (permissions.length > 0) {
|
||||
await prisma.rolePermission.createMany({
|
||||
data: permissions.map((p) => ({ roleId: created.id, permissionId: p.id })),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
console.log(`Role '${role.key}': ${permissions.length} permissions.`);
|
||||
}
|
||||
|
||||
const existingPlatformAdmin = await prisma.userRole.findFirst({
|
||||
where: { tenantId: null, role: { key: "platform_super_admin" } },
|
||||
});
|
||||
|
||||
if (existingPlatformAdmin) {
|
||||
console.log("Platform Super Admin já existe, pulando criação.");
|
||||
} else {
|
||||
const platformSuperAdminRole = await prisma.role.findUniqueOrThrow({
|
||||
where: { key: "platform_super_admin" },
|
||||
});
|
||||
|
||||
const initialPassword = randomBytes(18).toString("base64url");
|
||||
const passwordHash = await hashPassword(initialPassword);
|
||||
|
||||
const admin = await prisma.user.upsert({
|
||||
where: { email: PLATFORM_ADMIN_EMAIL },
|
||||
update: {},
|
||||
create: {
|
||||
email: PLATFORM_ADMIN_EMAIL,
|
||||
passwordHash,
|
||||
name: "Platform Super Admin",
|
||||
mustChangePassword: true,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.userRole.create({
|
||||
data: { userId: admin.id, roleId: platformSuperAdminRole.id, tenantId: null },
|
||||
});
|
||||
|
||||
const firstLoginPath = resolve(__dirname, "../../../FIRST_LOGIN.txt");
|
||||
writeFileSync(
|
||||
firstLoginPath,
|
||||
`B2BCall — Platform Super Admin (gerado em ${new Date().toISOString()})\n` +
|
||||
`Email: ${PLATFORM_ADMIN_EMAIL}\n` +
|
||||
`Senha: ${initialPassword}\n\n` +
|
||||
`Troca de senha OBRIGATÓRIA no primeiro login. Apague este arquivo depois de guardar a senha em um local seguro.\n`,
|
||||
);
|
||||
chmodSync(firstLoginPath, 0o600);
|
||||
|
||||
console.log(`Platform Super Admin criado: ${PLATFORM_ADMIN_EMAIL}`);
|
||||
console.log(`Senha salva em ${firstLoginPath} (permissao 600) — nao sera exibida no terminal.`);
|
||||
}
|
||||
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
main().catch(async (err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user