feat(frontend): Telefonia > Ramais — CRUD, reset de senha, isolamento por tenant
Tela /app/telefonia/ramais completa: listagem com busca/ordenacao, criacao com reveal de senha SIP (gerada pela API, mostrada uma unica vez), detalhe com redefinir senha (novo POST /extensions/:id/reset-password — unica forma de "editar" a senha, nunca reexpoe a existente) e remover (soft delete), confirmacao inline de 2 cliques em vez de modal. Cada tela deixa explicito de qual tenant os ramais sao, alem do isolamento por RLS que ja existia. Corrige de quebra um bug real que afeta qualquer acao futura sem payload: apiFetch sempre mandava Content-Type: application/json mesmo em requests sem body, e o parser do Fastify rejeita body vazio com esse header. Testado ponta a ponta com um tenant real (Acme Call Center): criar ramal, revelar senha, redefinir (senha nova confirmada diferente da original), remover, lista voltando vazia. Dark mode e mobile conferidos. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EWHKmcVJtstQFErbZ1AanY
This commit is contained in:
@@ -12,14 +12,21 @@ export class ApiError extends Error {
|
||||
|
||||
/** Chama a API real (NestJS/Fastify) do servidor (Server Component/Route
|
||||
* Handler) — nunca do client, o access token nunca sai como JS legível
|
||||
* (fica só no cookie httpOnly, ver session.ts). */
|
||||
* (fica só no cookie httpOnly, ver session.ts).
|
||||
*
|
||||
* **Bug real, achado nesta fase**: `Content-Type: application/json`
|
||||
* sempre presente, mesmo em requests sem body (ex.: `POST .../reset-
|
||||
* password`, `DELETE`) — o parser de body do Fastify rejeita um body
|
||||
* vazio quando o Content-Type diz JSON ("Body cannot be empty..."),
|
||||
* então toda ação sem payload quebrava com 400. Corrigido: só manda o
|
||||
* header quando `init.body` existe de verdade. */
|
||||
export async function apiFetch<T>(path: string, accessToken: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${API_BASE_URL}${path}`, {
|
||||
...init,
|
||||
headers: {
|
||||
...init?.headers,
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
...(init?.body ? { "Content-Type": "application/json" } : {}),
|
||||
},
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
16
apps/frontend/src/lib/extension-types.ts
Normal file
16
apps/frontend/src/lib/extension-types.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface Extension {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
number: string;
|
||||
name: string;
|
||||
domain: string;
|
||||
callerIdName: string | null;
|
||||
callerIdNumber: string | null;
|
||||
context: string;
|
||||
sofiaProfile: string;
|
||||
codecs: string;
|
||||
maxRegistrations: number;
|
||||
enabled: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
Reference in New Issue
Block a user