diff --git a/TODO.md b/TODO.md index 602985f..a551552 100644 --- a/TODO.md +++ b/TODO.md @@ -1700,6 +1700,41 @@ secao 96-103, 124, 169) + achado real de autorização em `/ai/models` aplicado (0 chamadas — não existe tráfego real neste tenant ainda, resultado honesto) +## PHASE 48 — "Itens sem permissão não aparecem" (agente.md secao 169) +- [x] `getUserPermissionKeys(userId, tenantId?)` (novo, `packages/auth`) — + todas as permission keys do usuário no contexto atual, mesma query + de `userHasPermission` só que devolvendo o conjunto inteiro em vez + de checar uma. `GET /auth/me` agora devolve `permissionKeys` (só + leitura adicional, não é decisão de autorização — isso continua + sendo o `PermissionGuard` em cada endpoint, secao 31/146: esconder + um item de menu nunca substitui o check no backend) +- [x] `NavLeaf`/`NavSection` ganham campo opcional `permission`; cada um + dos ~35 itens de menu (tenant e platform) anotado com a permission + key mínima que o endpoint GET correspondente já exige (ex.: Discador + usa `campaigns.view`, Administração usa `users.manage`, + Infraestrutura usa `freeswitch.view`). `filterNavByPermissions()` + (novo, `nav-types.ts`) esconde o item, ou a seção inteira se + nenhum filho sobrar +- [x] achado de arquitetura ao implementar (não um bug, uma restrição já + documentada): `TenantSidebar`/`PlatformSidebar`/`*Topbar` são + client components que importam `TENANT_NAV`/`PLATFORM_NAV` + DIRETO (em vez de receber como prop do server layout) porque os + ícones (`LucideIcon`, funções) não são serializáveis através da + fronteira RSC — bug real já documentado desde a PHASE 23. Por isso + o filtro roda dentro desses wrappers client-side, sobre o array já + no bundle, recebendo só `permissionKeys: string[]` (serializável) + como prop +- [x] Testado ponta a ponta com um supervisor de teste de verdade + (convidado, senha trocada, logado via UI): menu mostra só Discador/ + Call Center/Telefonia (sem Dialplan, que exige `freeswitch.view` — + supervisor não tem)/Monitoramento/Gravações/IA/Relatórios — a seção + Administração inteira some (nenhum dos 3 filhos exige menos que + `users.manage`). Confirmado que a autorização de verdade continua + valendo mesmo sem o item no menu: acesso direto a + `/app/administracao/usuarios` continua batendo 403 no backend. + Regressão: tenant_admin e platform_super_admin continuam vendo + TODO item do próprio menu (sem perder nada) + --- ## Riscos conhecidos diff --git a/apps/api/src/auth/auth.controller.ts b/apps/api/src/auth/auth.controller.ts index 44dae3d..c9aeaef 100644 --- a/apps/api/src/auth/auth.controller.ts +++ b/apps/api/src/auth/auth.controller.ts @@ -3,6 +3,7 @@ import type { FastifyRequest } from "fastify"; import { getPrismaClient } from "@b2bcall/database"; import { changePassword, + getUserPermissionKeys, isPlatformUser, listUserTenants, login, @@ -70,6 +71,10 @@ export class AuthController { ...dbUser, isPlatformUser: await isPlatformUser(user.sub), tenant: tenant ? { id: tenant.id, code: tenant.code, name: tenant.tradeName ?? tenant.legalName } : null, + // "Itens sem permissão não aparecem" (secao 169) — só UX de menu, o + // PermissionGuard em cada endpoint continua sendo a única fonte + // confiável de autorização. + permissionKeys: await getUserPermissionKeys(user.sub, user.tenantId), }; } diff --git a/apps/frontend/.impeccable/review/nav-filter-platform-admin-desktop.png b/apps/frontend/.impeccable/review/nav-filter-platform-admin-desktop.png new file mode 100644 index 0000000..0baae1b Binary files /dev/null and b/apps/frontend/.impeccable/review/nav-filter-platform-admin-desktop.png differ diff --git a/apps/frontend/.impeccable/review/nav-filter-supervisor-desktop.png b/apps/frontend/.impeccable/review/nav-filter-supervisor-desktop.png new file mode 100644 index 0000000..6c3191a Binary files /dev/null and b/apps/frontend/.impeccable/review/nav-filter-supervisor-desktop.png differ diff --git a/apps/frontend/.impeccable/review/nav-filter-supervisor-direct-access-desktop.png b/apps/frontend/.impeccable/review/nav-filter-supervisor-direct-access-desktop.png new file mode 100644 index 0000000..b5403e2 Binary files /dev/null and b/apps/frontend/.impeccable/review/nav-filter-supervisor-direct-access-desktop.png differ diff --git a/apps/frontend/src/app/app/layout.tsx b/apps/frontend/src/app/app/layout.tsx index 0e5aaac..4c9a6de 100644 --- a/apps/frontend/src/app/app/layout.tsx +++ b/apps/frontend/src/app/app/layout.tsx @@ -10,6 +10,7 @@ interface Me { name: string; isPlatformUser: boolean; tenant: { id: string; code: string; name: string } | null; + permissionKeys: string[]; } export default async function TenantAppLayout({ children }: { children: React.ReactNode }) { @@ -23,9 +24,9 @@ export default async function TenantAppLayout({ children }: { children: React.Re return (
- +
- +
{children}
diff --git a/apps/frontend/src/app/platform/layout.tsx b/apps/frontend/src/app/platform/layout.tsx index 5aebd1d..d91f169 100644 --- a/apps/frontend/src/app/platform/layout.tsx +++ b/apps/frontend/src/app/platform/layout.tsx @@ -9,6 +9,7 @@ interface Me { email: string; name: string; isPlatformUser: boolean; + permissionKeys: string[]; } export default async function PlatformLayout({ children }: { children: React.ReactNode }) { @@ -18,9 +19,9 @@ export default async function PlatformLayout({ children }: { children: React.Rea return (
- +
- +
{children}
diff --git a/apps/frontend/src/components/platform-shell/nav-data.ts b/apps/frontend/src/components/platform-shell/nav-data.ts index 5989a25..b57b737 100644 --- a/apps/frontend/src/components/platform-shell/nav-data.ts +++ b/apps/frontend/src/components/platform-shell/nav-data.ts @@ -1,20 +1,20 @@ import { LayoutDashboard, Building2, CreditCard, ServerCog, Sparkles, Settings2 } from "lucide-react"; import type { NavSection } from "../shell/nav-types"; -/** IA fixa do menu Platform (agente.md secao 168) — "Visão Geral" e - * "Billing > Tarifas" têm página construída até agora, o resto existe pra - * provar a arquitetura completa (Product Principle #5 em PRODUCT.md: esta - * é a implementação de referência do design system pras próximas telas), - * renderizado como indisponível em vez de virar link morto. Os outros 3 - * itens de Billing (Consumo/Fechamentos/Relatórios) esperam um seletor de - * tenant real — não existe endpoint de listagem de tenants ainda (ver - * docs/BILLING.md), então ficam "em breve" até essa peça existir. */ +/** IA fixa do menu Platform (agente.md secao 168). Todo item aqui tem uma + * tela real por trás (fechado na PHASE 46) — "permission" em cada um bate + * com o `@RequirePermission` do endpoint GET que a tela chama, usado por + * `filterNavByPermissions` (secao 169: "itens sem permissão não + * aparecem"). Só platform_super_admin chega neste menu (o layout redireciona + * quem não é platform user antes de renderizar), mas a filtragem continua + * valendo caso um dia exista uma role de plataforma mais restrita. */ export const PLATFORM_NAV: NavSection[] = [ { label: "Visão Geral", icon: LayoutDashboard, href: "/platform", description: "Estado agregado de todos os tenants, agora", + permission: "tenants.view", }, { label: "Clientes", @@ -24,21 +24,25 @@ export const PLATFORM_NAV: NavSection[] = [ label: "Tenants", href: "/platform/clientes/tenants", description: "Clientes da plataforma — criar cria também o primeiro usuário", + permission: "tenants.view", }, { label: "Planos", href: "/platform/clientes/planos", description: "Catálogo de limites e recursos por plano", + permission: "pricing.manage", }, { label: "Assinaturas", href: "/platform/clientes/assinaturas", description: "Versão de preço assinada e ciclo de billing por tenant", + permission: "billing.manage", }, { label: "Quotas", href: "/platform/clientes/quotas", description: "Uso vs. limite do plano — todos os tenants, quem está perto do limite", + permission: "tenants.view", }, ], }, @@ -50,21 +54,25 @@ export const PLATFORM_NAV: NavSection[] = [ label: "Consumo", href: "/platform/billing/consumo", description: "Uso bruto de todos os tenants no mês corrente", + permission: "billing.view", }, { label: "Tarifas", href: "/platform/billing/tarifas", description: "Price books e rate decks — catálogos globais de preço", + permission: "pricing.manage", }, { label: "Fechamentos", href: "/platform/billing/fechamentos", description: "Fechar/reabrir período de billing por tenant", + permission: "billing.manage", }, { label: "Relatórios", href: "/platform/billing/relatorios", description: "Statements gerados por tenant", + permission: "billing.view", }, ], }, @@ -76,21 +84,25 @@ export const PLATFORM_NAV: NavSection[] = [ label: "FreeSWITCH", href: "/platform/infraestrutura/freeswitch", description: "Canais e chamadas ativas agora, direto do ESL", + permission: "freeswitch.view", }, { label: "SIP Profiles", href: "/platform/infraestrutura/sip-profiles", description: "sofia status e registros SIP ativos", + permission: "freeswitch.view", }, { label: "Nodes", href: "/platform/infraestrutura/nodes", description: "Nodes FreeSWITCH desta implantação (hoje: 1)", + permission: "freeswitch.view", }, { label: "Saúde", href: "/platform/infraestrutura/saude", description: "Postgres, Redis e FreeSWITCH — verificação ao vivo", + permission: "freeswitch.view", }, ], }, @@ -102,21 +114,25 @@ export const PLATFORM_NAV: NavSection[] = [ label: "Providers", href: "/platform/ia/providers", description: "Providers de IA globais, visíveis por qualquer tenant", + permission: "ai.view", }, { label: "Modelos", href: "/platform/ia/modelos", description: "Modelos dos providers globais, com custo unitário", + permission: "ai.view", }, { label: "Uso", href: "/platform/ia/uso", description: "Uso bruto de IA de todos os tenants no mês corrente", + permission: "tenants.view", }, { label: "Custos", href: "/platform/ia/custos", description: "Uso × custo unitário — estimado, nunca um número fixo", + permission: "tenants.view", }, ], }, @@ -128,21 +144,25 @@ export const PLATFORM_NAV: NavSection[] = [ label: "Usuários", href: "/platform/sistema/usuarios", description: "Todos os usuários da plataforma, cross-tenant", + permission: "users.manage", }, { label: "Permissões", href: "/platform/sistema/permissoes", description: "Catálogo de roles e permissions do sistema", + permission: "roles.manage", }, { label: "Auditoria", href: "/platform/sistema/auditoria", description: "Log de eventos de todos os tenants", + permission: "audit.view", }, { label: "Configurações", href: "/platform/sistema/configuracoes", description: "Flags de segurança/infra do .env — somente leitura", + permission: "tenants.view", }, ], }, diff --git a/apps/frontend/src/components/platform-shell/platform-sidebar.tsx b/apps/frontend/src/components/platform-shell/platform-sidebar.tsx index 2d0c227..14d7c01 100644 --- a/apps/frontend/src/components/platform-shell/platform-sidebar.tsx +++ b/apps/frontend/src/components/platform-shell/platform-sidebar.tsx @@ -1,6 +1,7 @@ "use client"; import { Sidebar } from "@/components/shell/sidebar"; +import { filterNavByPermissions } from "@/components/shell/nav-types"; import { PLATFORM_NAV } from "./nav-data"; /** @@ -10,8 +11,10 @@ import { PLATFORM_NAV } from "./nav-data"; * (client) um valor que já nasceu no bundle client; importar `nav-data` * aqui dentro (em vez de receber `items` como prop vindo do server) * resolve isso. Bug real, achado testando o app do tenant: "Functions - * cannot be passed directly to Client Components". + * cannot be passed directly to Client Components". Mesma razão pra + * filtrar por permission (secao 169) aqui dentro, sobre o array já no + * bundle client, em vez de receber os itens já filtrados como prop. */ -export function PlatformSidebar() { - return ; +export function PlatformSidebar({ permissionKeys }: { permissionKeys: string[] }) { + return ; } diff --git a/apps/frontend/src/components/platform-shell/platform-topbar.tsx b/apps/frontend/src/components/platform-shell/platform-topbar.tsx index 0acba3c..05a7270 100644 --- a/apps/frontend/src/components/platform-shell/platform-topbar.tsx +++ b/apps/frontend/src/components/platform-shell/platform-topbar.tsx @@ -1,9 +1,10 @@ "use client"; import { Topbar } from "@/components/shell/topbar"; +import { filterNavByPermissions } from "@/components/shell/nav-types"; import { PLATFORM_NAV } from "./nav-data"; /** Mesma razão do `PlatformSidebar` — ver comentário lá. */ -export function PlatformTopbar({ userLabel }: { userLabel: string }) { - return ; +export function PlatformTopbar({ userLabel, permissionKeys }: { userLabel: string; permissionKeys: string[] }) { + return ; } diff --git a/apps/frontend/src/components/shell/nav-types.ts b/apps/frontend/src/components/shell/nav-types.ts index 4a8b5e0..7626f9f 100644 --- a/apps/frontend/src/components/shell/nav-types.ts +++ b/apps/frontend/src/components/shell/nav-types.ts @@ -5,6 +5,11 @@ export interface NavLeaf { href?: string; /** Subtítulo mostrado na topbar quando esta rota está ativa. */ description?: string; + /** Permission key mínima pra ver este item (secao 169: "itens sem + * permissão não aparecem") — bate com o `@RequirePermission` do + * endpoint GET que a tela chama. Omitido = sempre visível (ex.: + * Dashboard, que todo usuário autenticado do tenant enxerga). */ + permission?: string; } export interface NavSection { @@ -12,9 +17,29 @@ export interface NavSection { icon: LucideIcon; href?: string; description?: string; + permission?: string; children?: NavLeaf[]; } +/** Filtra o menu pelas permissions do usuário (secao 169) — só UX, nunca + * autorização de verdade (isso continua sendo o `PermissionGuard` em + * cada endpoint). Uma seção com `children` só aparece se sobrar pelo + * menos 1 filho visível; uma seção com `href` direto (sem children) é + * filtrada pela própria `permission`. */ +export function filterNavByPermissions(items: NavSection[], permissionKeys: string[]): NavSection[] { + const has = (permission?: string) => !permission || permissionKeys.includes(permission); + + return items + .map((section) => { + if (section.children) { + const children = section.children.filter((child) => has(child.permission)); + return children.length > 0 ? { ...section, children } : null; + } + return has(section.permission) ? section : null; + }) + .filter((section): section is NavSection => section != null); +} + /** Título+descrição da topbar pra rota atual — casa pelo `href` mais longo * (uma subrota como `/price-books/123` ainda resolve pro item de menu * "Tarifas"), nunca deixa a topbar com um título de outra página (bug diff --git a/apps/frontend/src/components/tenant-shell/nav-data.ts b/apps/frontend/src/components/tenant-shell/nav-data.ts index bb1f10a..86d025b 100644 --- a/apps/frontend/src/components/tenant-shell/nav-data.ts +++ b/apps/frontend/src/components/tenant-shell/nav-data.ts @@ -11,16 +11,11 @@ import { } from "lucide-react"; import type { NavSection } from "../shell/nav-types"; -/** IA fixa do menu Tenant (agente.md secao 169). Páginas construídas até - * agora: Dashboard (PHASE 23), Telefonia > Ramais (PHASE 25), Discador > - * Campanhas (PHASE 26), Call Center > Filas/Pausas/Disposições, Telefonia - * > Troncos e Discador > Lista de Bloqueio (PHASE 27) — o resto existe pra - * provar a arquitetura completa (Product Principle #5 em PRODUCT.md), - * renderizado como indisponível em vez de virar link morto. "Itens sem - * permissão não aparecem" (secao 169) ainda não é aplicado aqui — este - * primeiro corte mostra a IA inteira pra qualquer usuário tenant - * autenticado; filtrar por permission real fica pra quando mais telas - * existirem pra testar contra. */ +/** IA fixa do menu Tenant (agente.md secao 169). Todo item aqui tem uma + * tela real por trás (fechado na PHASE 46) — "permission" em cada um bate + * com o `@RequirePermission` do endpoint GET que a tela chama, usado por + * `filterNavByPermissions` (secao 169: "itens sem permissão não + * aparecem"). Item sem `permission` = sempre visível (Dashboard). */ export const TENANT_NAV: NavSection[] = [ { label: "Dashboard", @@ -36,21 +31,25 @@ export const TENANT_NAV: NavSection[] = [ label: "Campanhas", href: "/app/discador/campanhas", description: "Discador preditivo — campanhas deste tenant", + permission: "campaigns.view", }, { label: "Leads", href: "/app/discador/leads", description: "Leads de uma campanha — buscar, adicionar, remover", + permission: "campaigns.view", }, { label: "Callbacks", href: "/app/discador/callbacks", description: "Leads que pediram retorno em outro horário — remarcar, tentar agora ou cancelar", + permission: "campaigns.view", }, { label: "Lista de Bloqueio", href: "/app/discador/bloqueio", description: "Números que o discador nunca chama", + permission: "campaigns.view", }, ], }, @@ -62,21 +61,25 @@ export const TENANT_NAV: NavSection[] = [ label: "Agentes", href: "/app/callcenter/agentes", description: "Identidade de call center — liga um usuário a um agente e a um ramal", + permission: "agents.view", }, { label: "Filas", href: "/app/callcenter/filas", description: "Filas de atendimento deste tenant", + permission: "queues.view", }, { label: "Pausas", href: "/app/callcenter/pausas", description: "Motivos de pausa que o agente pode escolher", + permission: "agents.view", }, { label: "Disposições", href: "/app/callcenter/disposicoes", description: "Motivos de encerramento de chamada", + permission: "agents.view", }, ], }, @@ -88,16 +91,19 @@ export const TENANT_NAV: NavSection[] = [ label: "Ramais", href: "/app/telefonia/ramais", description: "Ramais SIP deste tenant — números, senha, caller ID", + permission: "extensions.view", }, { label: "Troncos", href: "/app/telefonia/troncos", description: "Troncos SIP deste tenant", + permission: "trunks.view", }, { label: "Dialplan", href: "/app/telefonia/dialplan", description: "Editor de regras do contexto default — condição, ações, versões", + permission: "freeswitch.view", }, ], }, @@ -106,12 +112,14 @@ export const TENANT_NAV: NavSection[] = [ icon: Radar, href: "/app/monitoramento", description: "Eventos ao vivo — filas, agentes e chamadas em tempo real", + permission: "monitoring.view", }, { label: "Gravações", icon: Mic, href: "/app/gravacoes", description: "Chamadas gravadas deste tenant — player autenticado, sem URL direta pro storage", + permission: "recordings.view", }, { label: "IA", @@ -121,21 +129,25 @@ export const TENANT_NAV: NavSection[] = [ label: "Análises", href: "/app/relatorios/ia", description: "Dashboard de IA — score, sentimento, assuntos, compliance", + permission: "reports.view", }, { label: "Scorecards", href: "/app/ia/scorecards", description: "Critérios ponderados que a IA avalia em toda chamada analisada", + permission: "ai.view", }, { label: "Prompts", href: "/app/ia/prompts", description: "Templates de prompt usados na análise e no scorecard", + permission: "ai.view", }, { label: "Configurações", href: "/app/ia/configuracoes", description: "Providers e modelos de IA — BYOK deste tenant", + permission: "ai.view", }, ], }, @@ -147,26 +159,31 @@ export const TENANT_NAV: NavSection[] = [ label: "Chamadas", href: "/app/relatorios/chamadas", description: "Lista de chamadas — direção, fila, agente, duração, encerramento", + permission: "reports.view", }, { label: "Agentes", href: "/app/relatorios/agentes", description: "Tempo logado/pausado/em chamada e TMA por agente", + permission: "reports.view", }, { label: "Filas", href: "/app/relatorios/filas", description: "Recebidas/atendidas/abandonadas, TME/TMA, Service Level", + permission: "reports.view", }, { label: "Campanhas", href: "/app/relatorios/campanhas", description: "Leads, tentativas, taxas de atendimento e contato", + permission: "reports.view", }, { label: "Consumo", href: "/app/relatorios/consumo", description: "Uso bruto do mês — minutos, dias ativos, armazenamento, tokens de IA", + permission: "reports.view", }, ], }, @@ -178,16 +195,19 @@ export const TENANT_NAV: NavSection[] = [ label: "Usuários", href: "/app/administracao/usuarios", description: "Quem tem acesso a este tenant — convidar e trocar papel", + permission: "users.manage", }, { label: "Perfis", href: "/app/administracao/perfis", description: "O que cada papel pode fazer neste tenant", + permission: "users.manage", }, { label: "Configurações", href: "/app/administracao/configuracoes", description: "Dados do tenant — nome fantasia, fuso, idioma, privacidade de IA", + permission: "users.manage", }, ], }, diff --git a/apps/frontend/src/components/tenant-shell/tenant-sidebar.tsx b/apps/frontend/src/components/tenant-shell/tenant-sidebar.tsx index 94ce534..094e3fb 100644 --- a/apps/frontend/src/components/tenant-shell/tenant-sidebar.tsx +++ b/apps/frontend/src/components/tenant-shell/tenant-sidebar.tsx @@ -1,10 +1,14 @@ "use client"; import { Sidebar } from "@/components/shell/sidebar"; +import { filterNavByPermissions } from "@/components/shell/nav-types"; import { TENANT_NAV } from "./nav-data"; /** Ver comentário em `platform-shell/platform-sidebar.tsx` — mesma razão - * (ícones não são serializáveis através da fronteira RSC). */ -export function TenantSidebar({ railLabel }: { railLabel: string }) { - return ; + * (ícones não são serializáveis através da fronteira RSC) — por isso o + * filtro por permission (secao 169) acontece aqui, client-side, sobre o + * `TENANT_NAV` importado no bundle, nunca recebendo os itens já + * filtrados como prop do server. */ +export function TenantSidebar({ railLabel, permissionKeys }: { railLabel: string; permissionKeys: string[] }) { + return ; } diff --git a/apps/frontend/src/components/tenant-shell/tenant-topbar.tsx b/apps/frontend/src/components/tenant-shell/tenant-topbar.tsx index ee00c3d..9d05afe 100644 --- a/apps/frontend/src/components/tenant-shell/tenant-topbar.tsx +++ b/apps/frontend/src/components/tenant-shell/tenant-topbar.tsx @@ -1,9 +1,18 @@ "use client"; import { Topbar } from "@/components/shell/topbar"; +import { filterNavByPermissions } from "@/components/shell/nav-types"; import { TENANT_NAV } from "./nav-data"; /** Ver comentário em `platform-shell/platform-sidebar.tsx` — mesma razão. */ -export function TenantTopbar({ fallbackTitle, userLabel }: { fallbackTitle: string; userLabel: string }) { - return ; +export function TenantTopbar({ + fallbackTitle, + userLabel, + permissionKeys, +}: { + fallbackTitle: string; + userLabel: string; + permissionKeys: string[]; +}) { + return ; } diff --git a/packages/auth/src/permissions.ts b/packages/auth/src/permissions.ts index f5fa203..f277824 100644 --- a/packages/auth/src/permissions.ts +++ b/packages/auth/src/permissions.ts @@ -30,6 +30,33 @@ export async function userHasPermission( ); } +/** + * Todas as permission keys que `userId` tem no contexto de `tenantId` + * (ou só as de escopo PLATFORM, se `tenantId` for omitido) — usado pelo + * frontend pra filtrar o menu (agente.md secao 169: "itens sem permissão + * não aparecem"), nunca pra decidir autorização de escrita (isso continua + * sendo `userHasPermission`/`PermissionGuard` no backend, a única fonte + * confiável — o menu é só UX, esconder um item não substitui o check). + */ +export async function getUserPermissionKeys(userId: string, tenantId?: string): Promise { + const prisma = getPrismaClient(); + + const where = tenantId + ? { userId, OR: [{ tenantId }, { tenantId: null }] } + : { userId, tenantId: null }; + + const userRoles = await prisma.userRole.findMany({ + where, + include: { role: { include: { rolePermissions: { include: { permission: true } } } } }, + }); + + const keys = new Set(); + for (const userRole of userRoles) { + for (const rp of userRole.role.rolePermissions) keys.add(rp.permission.key); + } + return Array.from(keys); +} + /** Atalho: usuário tem QUALQUER role com scope PLATFORM (ex.: platform_super_admin). */ export async function isPlatformUser(userId: string): Promise { const prisma = getPrismaClient();