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
103 lines
3.3 KiB
TypeScript
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);
|
|
}
|
|
}
|