Files
B2BCall-dialer/apps/api/src/auth/auth.controller.ts
Matheus 565110eac3 feat(frontend): app do tenant, dashboard ao vivo, fluxo de login multi-tenant
Corrige um gap real: o login sempre mandava pra /platform mesmo pra
usuarios sem role de plataforma, sem nunca chamar /auth/tenants ou
select-tenant. Agora POST /api/post-login decide o destino server-side
(plataforma / tenant unico / seletor com >1 tenant) antes de redirecionar,
trocando o access token quando necessario sem nunca expor token ao client.

Adiciona GET /reports/dashboard (secao 162) e a tela /app correspondente
— chamadas/agentes/TME/TMA/rates ao vivo, "consumo do plano" e "valor
estimado" seguindo a mesma disciplina de honestidade (null > numero
inventado) do dashboard de plataforma.

Shell (sidebar/topbar/drawer mobile) extraido pra components/shell,
compartilhado entre os menus Platform e Tenant (secao 168-169) via
wrappers client-only por area — corrige de quebra um erro real de
serializacao RSC (passar NavSection[] com icones como prop de Server
Component pra Client Component quebra: "Functions cannot be passed
directly to Client Components").

Testado ponta a ponta com um tenant semeado (Acme Call Center): login →
/app direto, platform admin barrado de /app e vice-versa, dashboard com
numeros reais (zeros honestos), drawer mobile, dark mode.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWHKmcVJtstQFErbZ1AanY
2026-08-29 09:30:16 -03:00

103 lines
3.3 KiB
TypeScript

import { Body, Controller, Get, HttpCode, HttpStatus, NotFoundException, Post, Req, UseGuards } from "@nestjs/common";
import type { FastifyRequest } from "fastify";
import { getPrismaClient } from "@b2bcall/database";
import {
changePassword,
isPlatformUser,
listUserTenants,
login,
logout,
refreshSession,
setActiveTenant,
type AccessTokenClaims,
} from "@b2bcall/auth";
import { JwtAuthGuard } from "../common/guards/jwt-auth.guard";
import { CurrentUser } from "../common/decorators/current-user.decorator";
import { LoginDto } from "./dto/login.dto";
import { RefreshDto } from "./dto/refresh.dto";
import { SelectTenantDto } from "./dto/select-tenant.dto";
import { ChangePasswordDto } from "./dto/change-password.dto";
import { LoginRateLimitGuard } from "./login-rate-limit.guard";
@Controller("auth")
export class AuthController {
@UseGuards(LoginRateLimitGuard)
@Post("login")
@HttpCode(HttpStatus.OK)
async login(@Body() dto: LoginDto, @Req() req: FastifyRequest) {
return login({
email: dto.email,
password: dto.password,
ipAddress: req.ip,
userAgent: req.headers["user-agent"],
});
}
@Post("refresh")
@HttpCode(HttpStatus.OK)
async refresh(@Body() dto: RefreshDto) {
return refreshSession(dto.refreshToken);
}
@UseGuards(JwtAuthGuard)
@Post("logout")
@HttpCode(HttpStatus.NO_CONTENT)
async logout(@CurrentUser() user: AccessTokenClaims) {
await logout(user.sessionId);
}
@UseGuards(JwtAuthGuard)
@Get("me")
async me(@CurrentUser() user: AccessTokenClaims) {
const prisma = getPrismaClient();
const dbUser = await prisma.user.findUnique({
where: { id: user.sub },
select: { id: true, email: true, name: true },
});
if (!dbUser) throw new NotFoundException();
// Nome do tenant ativo pra topbar (frontend, PHASE 23) — `tenants` não
// tem RLS (tabela raiz, ver docs/TENANT_ISOLATION.md), leitura direta
// sem `withTenantContext` é segura aqui.
const tenant = user.tenantId
? await prisma.tenant.findUnique({
where: { id: user.tenantId },
select: { id: true, code: true, tradeName: true, legalName: true },
})
: null;
return {
...dbUser,
isPlatformUser: await isPlatformUser(user.sub),
tenant: tenant ? { id: tenant.id, code: tenant.code, name: tenant.tradeName ?? tenant.legalName } : null,
};
}
@UseGuards(JwtAuthGuard)
@Get("tenants")
async tenants(@CurrentUser() user: AccessTokenClaims) {
const memberships = await listUserTenants(user.sub);
return memberships.map((m) => ({
tenantId: m.tenant.id,
code: m.tenant.code,
name: m.tenant.tradeName ?? m.tenant.legalName,
status: m.tenant.status,
}));
}
@UseGuards(JwtAuthGuard)
@Post("select-tenant")
@HttpCode(HttpStatus.OK)
async selectTenant(@CurrentUser() user: AccessTokenClaims, @Body() dto: SelectTenantDto) {
const accessToken = await setActiveTenant(user.sessionId, user.sub, dto.tenantId);
return { accessToken };
}
@UseGuards(JwtAuthGuard)
@Post("change-password")
@HttpCode(HttpStatus.NO_CONTENT)
async changePassword(@CurrentUser() user: AccessTokenClaims, @Body() dto: ChangePasswordDto) {
await changePassword(user.sub, user.sessionId, dto.currentPassword, dto.newPassword);
}
}