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:
110
packages/auth/src/__tests__/auth.test.ts
Normal file
110
packages/auth/src/__tests__/auth.test.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Verificação end-to-end do fluxo de autenticação (agente.md secao 148):
|
||||
* login, refresh rotation, logout/revogação, e checagem de RBAC.
|
||||
*
|
||||
* Roda com: pnpm --filter @b2bcall/auth run test:auth
|
||||
*/
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { getPrismaClient } from "@b2bcall/database";
|
||||
import { hashPassword } from "../password";
|
||||
import {
|
||||
InvalidCredentialsError,
|
||||
InvalidRefreshTokenError,
|
||||
login,
|
||||
logout,
|
||||
refreshSession,
|
||||
} from "../session";
|
||||
import { userHasPermission } from "../permissions";
|
||||
|
||||
function assert(condition: boolean, message: string): void {
|
||||
if (!condition) {
|
||||
throw new Error(`FALHOU: ${message}`);
|
||||
}
|
||||
console.log(`OK: ${message}`);
|
||||
}
|
||||
|
||||
async function assertThrows(fn: () => Promise<unknown>, ErrorClass: new (...a: any[]) => Error, message: string) {
|
||||
try {
|
||||
await fn();
|
||||
throw new Error(`FALHOU: ${message} (nao lancou erro)`);
|
||||
} catch (err) {
|
||||
assert(err instanceof ErrorClass, message);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const prisma = getPrismaClient();
|
||||
const suffix = randomUUID().slice(0, 8);
|
||||
const email = `auth-test-${suffix}@test.local`;
|
||||
const password = "S3nhaForteDeTeste!123";
|
||||
|
||||
const tenant = await prisma.tenant.create({
|
||||
data: { code: `auth-test-${suffix}`, slug: `auth-test-${suffix}`, legalName: "Auth Test LTDA" },
|
||||
});
|
||||
const user = await prisma.user.create({
|
||||
data: { email, passwordHash: await hashPassword(password), name: "Auth Test User" },
|
||||
});
|
||||
const agentRole = await prisma.role.findUniqueOrThrow({ where: { key: "agent" } });
|
||||
await prisma.userRole.create({
|
||||
data: { userId: user.id, roleId: agentRole.id, tenantId: tenant.id },
|
||||
});
|
||||
|
||||
// Login com senha errada / e-mail inexistente: mesmo erro genérico.
|
||||
await assertThrows(
|
||||
() => login({ email, password: "senha-errada" }),
|
||||
InvalidCredentialsError,
|
||||
"Senha errada rejeitada com erro genérico",
|
||||
);
|
||||
await assertThrows(
|
||||
() => login({ email: `nao-existe-${suffix}@test.local`, password }),
|
||||
InvalidCredentialsError,
|
||||
"E-mail inexistente rejeitado com o MESMO erro genérico (sem user-enumeration)",
|
||||
);
|
||||
|
||||
// Login correto.
|
||||
const result = await login({ email, password, ipAddress: "127.0.0.1", userAgent: "test-agent" });
|
||||
assert(!result.mustChangePassword, "Usuário de teste não precisa trocar senha");
|
||||
assert(Boolean(result.accessToken) && Boolean(result.refreshToken), "Login retorna access e refresh token");
|
||||
|
||||
// RBAC: agent tem dashboard.view, não tem tenants.manage.
|
||||
assert(
|
||||
await userHasPermission(user.id, "dashboard.view", tenant.id),
|
||||
"Agent tem permissão dashboard.view no próprio tenant",
|
||||
);
|
||||
assert(
|
||||
!(await userHasPermission(user.id, "tenants.manage", tenant.id)),
|
||||
"Agent NÃO tem permissão tenants.manage",
|
||||
);
|
||||
|
||||
// Refresh rotation: token antigo morre assim que um novo é emitido.
|
||||
const refreshed = await refreshSession(result.refreshToken);
|
||||
assert(Boolean(refreshed.accessToken), "Refresh emite novo access token");
|
||||
assert(refreshed.refreshToken !== result.refreshToken, "Refresh token rotacionado é diferente do anterior");
|
||||
await assertThrows(
|
||||
() => refreshSession(result.refreshToken),
|
||||
InvalidRefreshTokenError,
|
||||
"Refresh token antigo (já rotacionado) não funciona mais",
|
||||
);
|
||||
|
||||
// Logout revoga a sessão — refresh subsequente falha.
|
||||
await logout(result.sessionId);
|
||||
await assertThrows(
|
||||
() => refreshSession(refreshed.refreshToken),
|
||||
InvalidRefreshTokenError,
|
||||
"Refresh token de sessão revogada (logout) não funciona",
|
||||
);
|
||||
|
||||
// Limpeza.
|
||||
await prisma.userRole.deleteMany({ where: { userId: user.id } });
|
||||
await prisma.session.deleteMany({ where: { userId: user.id } });
|
||||
await prisma.user.delete({ where: { id: user.id } });
|
||||
await prisma.tenant.delete({ where: { id: tenant.id } });
|
||||
|
||||
console.log("\nFluxo de autenticação OK: login, RBAC, refresh rotation e logout funcionam.");
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
main().catch(async (err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user