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:
2026-08-28 05:58:49 -03:00
parent d66170c795
commit 70c5586595
16 changed files with 1172 additions and 10 deletions

172
packages/auth/src/seed.ts Normal file
View 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);
});