API do Cliente
O PowerPortalsPro publica uma superfície JSON-sobre-HTTP sob /api/* a qual qualquer aplicativo de página única pode chamar — o cliente Blazor WebAssembly na caixa de entrada usa para fazer backup IPowerPortalsProService e IAuthService, mas os mesmos endpoints são igualmente utilizáveis a partir de uma interface React, Vue ou vanilla JS hospedada ao lado (ou até mesmo cross-origin de) o servidor. Esta página é a referência completa: cada endpoint que pode ser chamado por cliente, com um exemplo de solicitação e resposta.
Para quem é isso
Se você está construindo um app Blazor e consumindo o framework via
IPowerPortalsProService, não precisa chamar esses endpoints diretamente — a implementação do cliente faz isso por você. Esta página documenta a superfície para equipes que escrevem um SPA que não seja Blazor, ou que conectam um cliente HTTP personalizado, contra o mesmo servidor.
Ativando os endpoints
UsePowerPortalsProWebServer faz a ligação dos endpoints de dados (CRUD da tabela, FetchXML, metadados, arquivos, localização, administração). MapAuthEndpoints<TUser> Liga a superfície de autenticação voltada para SPA — chame explicitamente se seu host precisar de autenticação por cookies de um SPA.
// Program.cs — pipeline de servidores
app.UsePowerPortalsProWebServer();
app.MapAuthEndpoints<PortalUser>();
Ambas as chamadas são no-ops quando o recurso não está em uso, então conecte-as uma Program.cs vez, independentemente do modo de interatividade que o host use.
O tipo Rotas — fonte única de verdade
PowerPortalsPro.Web.Common.Routes expõe todo caminho de endpoint como uma propriedade fortemente tipada. Tanto o cliente de entrada quanto qualquer SPA externo baseado em C# devem referenciar essas constantes em vez de escrever manualmente as cadeias, de modo que uma renomeação do lado do servidor aparece como um erro de compilação em vez de um 404 em tempo de execução. Clientes JavaScript/TypeScript obviamente precisarão fazer os caminhos em linha, mas o lado C# de Routes permanece a referência canônica para o que são esses caminhos.
// Em qualquer lugar — tanto PowerPortalsPro.Web.Client quanto SPAs externos em C#.
var loginUrl = Routes.Api.Auth.Login; // "/api/auth/login"
var meUrl = Routes.Api.Auth.Me; // "/api/auth/me"
var createUrl = Routes.Api.Tables.GetCreateRoute("contact");
var fetchUrl = Routes.Api.GetRetrieveMultipleRoute(fetchXml);
Routes.Api cobre os endpoints de dados e administradores; Routes.Api.Auth cobre o login / cadastro; Routes.Api.Auth.Manage cobre as operações de gerenciamento de contas do usuário logado.
Modelo de autenticação — cookies, não tokens
A autenticação é baseada em cookies do navegador. Não existe uma etapa de emissão do JWT. Um SPA chama /api/auth/login, o servidor define o .AspNetCore.Identity.Application cookie na resposta, e o navegador o anexa em cada requisição subsequente — incluindo chamadas de dados sob /api/table/*. Do JavaScript, isso significa fetch(..., { credentials: 'include' }) que a cada chamada (ou axios.defaults.withCredentials = true); sem ele, o cookie é descartado e o servidor retorna 401. Todos os exemplos abaixo incluem isso.
// React / Vue / busca simples — credenciais: 'incluir' é necessário para que o
// O navegador envia e armazena o cookie de autenticação emitido por /api/auth/login.
const me = await fetch('/api/auth/me', { credentials: 'include' })
.then(r => r.json());
if (me.isAuthenticated) {
console.log(me.userName, me.roles);
}
SPAs de origem cruzada
Se o SPA e o servidor estiverem em origens diferentes, o servidor deve enviar
Access-Control-Allow-Origin: <spa-origin>(não*) eAccess-Control-Allow-Credentials: true, e o SPA deve usarcredentials: 'include'. Hospedagem de mesma origem (o SPA servido do mesmo local que a API) evita o problema completamente.
A forma do recorde
Todo endpoint de dados que lê ou escreve uma linha troca o mesmo TableRecord envelope. properties é um mapa do nome lógico da coluna → um objeto valor tipado cujo $type discriminador identifica o tipo da coluna. permissions é uma máscara bit-flag (Read 1, Create 2, Write 4, Delete 8, Append 16, AppendTo 32) que descreve o que o usuário atual pode fazer com a linha. Nas escritas você só precisa enviar as colunas que está mudando.
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"tableName": "account",
"permissions": 15,
"properties": {
"name": { "$type": 14, "value": "Acme Corporation" },
"revenue": { "$type": 8, "value": 5000000, "formattedValue": "$5,000,000.00" },
"createdon": { "$type": 2, "value": "2026-05-15T10:30:00Z", "formattedValue": "May 15, 2026 10:30 AM" },
"primarycontactid": { "$type": 6, "value": "9f8e7d6c-...", "name": "Jane Doe", "tableName": "contact" }
},
"formattedValues": { "revenue": "$5,000,000.00" },
"currency": { "isoCode": "USD", "symbol": "$", "precision": 2 }
}
Os $type valores espelham o tipo de atributo do Dataverse: 0 Booleano, 2 DateTime, 3 Decimal, 4 Double, 5 Integer, 6 Lookup, 8 Money, 11 Choice, 14 String, 15 Uniqueidentifier, 40 MultiSelectChoice, 41 File, 42 Image. As consultas adicionam name + tableName; número, dinheiro e valores de data carregam um Dataverse formattedValueformatado por .
Respostas de erro
Chamadas falhadas retornam o RFC 9457 application/problem+json. A implementação do cliente na caixa de entrada reidrata o tipo original de exceção CLR dos detalhes do problema, de modo que o lado do servidor projeta a superfície como a mesma exceção no cliente; para non-.NET SPAs, os type campos / title / detail status / são o handle padrão.
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "Bad Request",
"status": 400,
"detail": "The record could not be saved because a required column was missing."
}
Referência de ponto final
Cada endpoint abaixo mostra o método e o caminho, o que ele faz, uma solicitação de exemplo (como chamada de navegador fetch ) e uma resposta de exemplo. Segmentos de caminho em {braces} são marcadores de posição. A menos que indicado o contrário, uma resposta 2xx é o caso de sucesso e as falhas retornam como problem+json.
Discos & CRUD
Crear, ler, atualizar e excluir registros únicos, além de consultas FetchXML arbitrárias e lotes transacionais. Respaldado por UsePowerPortalsProWebServer; toda leitura e escrita aplica ao consumidor ITablePermissionHandler / ITableRecordPermissionHandler interceptadores e a qualquer registrado IFetchXmlBuilderInterceptor.
POST /api/table/{tableLogicalName}
Cria uma nova linha na tabela nomeada. Envie um TableRecord que carrega as colunas para set; a resposta retorna o id do novo registro.
Pedido
const res = await fetch('/api/table/account', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tableName: 'account',
properties: {
name: { $type: 14, value: 'Acme Corporation' },
revenue: { $type: 8, value: 5000000 }
}
})
});
const { id } = await res.json();
Resposta
{
"responseName": "CreateResponse",
"outputParameters": {},
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
GET /api/table/{tableLogicalName}/{recordId}
Lê uma única linha por id. O parâmetro opcional ?columns= de consulta (nomes lógicos separados por vírgulas) restringe a projeção — omita-o para recuperar o conjunto padrão de colunas da tabela.
Pedido
const record = await fetch(
'/api/table/account/a1b2c3d4-e5f6-7890-abcd-ef1234567890?columns=name,revenue',
{ credentials: 'include' }
).then(r => r.json());
Resposta
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"tableName": "account",
"permissions": 15,
"properties": {
"name": { "$type": 14, "value": "Acme Corporation" },
"revenue": { "$type": 8, "value": 5000000, "formattedValue": "$5,000,000.00" }
},
"formattedValues": { "revenue": "$5,000,000.00" },
"currency": { "isoCode": "USD", "symbol": "$", "precision": 2 }
}
PATCH /api/table/{tableLogicalName}/{recordId}
Atualiza uma linha existente. Apenas as colunas presentes em properties estão escritas, então envie apenas os valores alterados.
Pedido
await fetch('/api/table/account/a1b2c3d4-e5f6-7890-abcd-ef1234567890', {
method: 'PATCH',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tableName: 'account',
properties: { revenue: { $type: 8, value: 6500000 } }
})
});
Resposta
{ "responseName": "UpdateResponse", "outputParameters": {} }
DELETE /api/table/{tableLogicalName}/{recordId}
Exclui a linha por id.
Pedido
await fetch('/api/table/account/a1b2c3d4-e5f6-7890-abcd-ef1234567890', {
method: 'DELETE',
credentials: 'include'
});
Resposta
{ "responseName": "DeleteResponse", "outputParameters": {} }
GET /api/retrieveMultiple?fetchXml=…
Executa uma consulta FetchXML arbitrária e retorna as linhas correspondentes mais as informações de paginação. Codifice o FetchXML na fetchXml string de consulta — de C#, Routes.Api.GetRetrieveMultipleRoute(fetchXml) faz isso para você.
Pedido
const fetchXml = `<fetch><entity name="account">
<attribute name="name" /><attribute name="revenue" />
<order attribute="name" />
</entity></fetch>`;
const result = await fetch(
'/api/retrieveMultiple?fetchXml=' + encodeURIComponent(fetchXml),
{ credentials: 'include' }
).then(r => r.json());
Resposta
{
"pagingInfo": { "totalRecordCount": 42, "pagingCookie": "<cookie page=…>", "pageNumber": 1 },
"tableRecords": [
{
"id": "a1b2c3d4-…",
"tableName": "account",
"permissions": 1,
"properties": { "name": { "$type": 14, "value": "Acme Corporation" } },
"formattedValues": {}
}
]
}
POST /api/executeMultiple?returnResponses=true|false
Executa um lote de requisições heterogêneas (Criar / Atualizar / Deletar / Associar / Dissociar) em uma única transação de banco de dados — se alguma falhar, todo o lote reverte. O corpo é um array JSON de OrganizationRequest objetos discriminados por $type. Passe ?returnResponses=false para pular a criação da lista de respostas por pedido.
Pedido
await fetch('/api/executeMultiple?returnResponses=true', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify([
{ $type: 'CreateRequest', record: { tableName: 'contact',
properties: { lastname: { $type: 14, value: 'Doe' } } } },
{ $type: 'DeleteRequest', record: { tableName: 'account', id: 'old-guid' } }
])
});
Resposta
[
{ "$type": "CreateResponse", "responseName": "CreateResponse", "outputParameters": {}, "id": "new-guid" },
{ "$type": "DeleteResponse", "responseName": "DeleteResponse", "outputParameters": {} }
]
Grades e mapas
Endpoints de consulta compostos por servidor. Em vez de fazer o cliente construir FetchXML, você passa um view id (ou seu próprio FetchXML) além de busca / ordenação / paginação, e o servidor resolve as colunas, aplica permissões e executa a consulta — o mesmo caminho de fonte única de verdade que o MainGrid e os componentes do gráfico usam.
POST /api/grids/data
Carrega uma página de dados de grade. Forneça um viewId ou seu próprio fetchXml, além de filtros opcionais searchTextde paginação sortse coluna. A resposta contém as linhas, as definições de colunas resolvidas e a máscara de permissões da tabela do chamador.
Pedido
const page = await fetch('/api/grids/data', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
viewId: '00000000-0000-0000-0000-000000000001',
searchText: 'acme',
sorts: [{ columnName: 'name', descending: false }],
pageNumber: 1,
pageSize: 50
})
}).then(r => r.json());
Resposta
{
"pagingInfo": { "totalRecordCount": 2, "pageNumber": 1 },
"tableRecords": [ { "id": "a1b2c3d4-…", "tableName": "account",
"properties": { "name": { "$type": 14, "value": "Acme Corporation" } } } ],
"columns": [
{ "columnName": "name", "displayName": "Name", "type": 14, "isSortable": true, "isPrimaryName": true }
],
"tablePermissions": 15
}
POST /api/charts/data
As cargas agregaram dados de cartas moldadas para os componentes de cartografia. Aceita uma configuração agregada, um viewId, ou raw fetchXml, mais o mapeamento de colunas de rótulos / valores / séries; retorna rótulos e conjuntos de dados no estilo Chart.js.
Pedido
const chart = await fetch('/api/charts/data', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
viewId: '00000000-0000-0000-0000-000000000002',
labelColumn: 'statuscode',
valueColumn: 'count',
singleSeriesLabel: 'Accounts by Status'
})
}).then(r => r.json());
Resposta
{
"data": {
"labels": ["Active", "Inactive"],
"datasets": [ { "label": "Accounts by Status", "data": [ { "value": 25 }, { "value": 8 } ] } ]
}
}
Metadados e permissões
Buscas somente leitura para metadados de tabela e visualização, a máscara de permissões do usuário atual e configurações em toda a organização. Todos são armazenados em cache do lado do servidor, então chamadas repetidas são baratas.
GET /api/tableMetadata/{tableLogicalName}
Retorna os metadados de uma tabela — colunas (com tipos, rótulos e restrições), colunas de id / nome / imagem primárias e relacionamentos.
Pedido
const meta = await fetch('/api/tableMetadata/account', { credentials: 'include' })
.then(r => r.json());
Resposta
{
"tableName": "account",
"objectTypeCode": 1,
"primaryIdColumn": "accountid",
"primaryNameColumn": "name",
"isIntersect": false,
"columns": [ { "$type": 14, "columnName": "name", "displayName": "Name", "maxLength": 160 } ],
"oneToMany": [ { "relationshipName": "account_contacts", "referencingEntity": "contact" } ],
"manyToOne": [],
"manyToMany": []
}
GET /api/permissions/table/{tableLogicalName}
Retorna a máscara combinada TableSecurityPermission do usuário atual para a tabela como um único inteiro (sinalizadores de bit: Leitura 1, Criação 2, Escrita 4, Exclusão 8, Anexo 16, Anexo 32).
Pedido
const mask = await fetch('/api/permissions/table/account', { credentials: 'include' })
.then(r => r.json());
// 15 === Leia(1) | Criar(2) | Escrever(4) | Delete(8)
Resposta
15
GET /api/viewMetadata/{viewId}
Retorna os metadados de uma visualização salva pelo seu GUID — seu FetchXML, colunas de layout e flags de visualização. A rota requer um GUID, que é como ela é desambiguada da rota de todas as visões abaixo.
Pedido
const view = await fetch('/api/viewMetadata/00000000-0000-0000-0000-000000000001',
{ credentials: 'include' }).then(r => r.json());
Resposta
{
"id": "00000000-0000-0000-0000-000000000001",
"name": "All Accounts",
"tableName": "account",
"isDefault": true,
"fetchXml": "<fetch>…</fetch>",
"columns": [ { "columnName": "name", "displayName": "Name", "width": 300 } ]
}
GET /api/viewMetadata/{tableLogicalName}
Retorna todas as visualizações salvas para uma tabela. Compartilha o /api/viewMetadata/ prefixo com a rota by-id — um segmento não-GUID (nome lógico de tabela) cai aqui.
Pedido
const views = await fetch('/api/viewMetadata/account', { credentials: 'include' })
.then(r => r.json());
Resposta
[
{ "id": "0000…0001", "name": "All Accounts", "isDefault": true, "tableName": "account" },
{ "id": "0000…0002", "name": "Active Accounts", "isDefault": false, "tableName": "account" }
]
GET /api/organizationSettings
Retorna configurações organizacionais provenientes do registro da organização Dataverse: a moeda padrão, a lista de extensões de arquivos bloqueadas e o tamanho máximo de upload em bytes.
Pedido
const settings = await fetch('/api/organizationSettings', { credentials: 'include' })
.then(r => r.json());
Resposta
{
"defaultCurrency": { "isoCode": "USD", "symbol": "$", "precision": 2 },
"blockedFileExtensions": ["exe", "bat", "js"],
"maxUploadFileSizeInBytes": 10485760
}
Arquivos
Leia o conteúdo das colunas de arquivos e imagens. Cargas binárias são retornadas codificadas base64 dentro do JSON; A includeData flag permite que você busque apenas metadados (por exemplo, para renderizar uma lista de download) sem transferir bytes.
GET /api/files/{tableLogicalName}/{recordId}/{columnName}?includeData=…
Retorna os metadados do arquivo de uma coluna de arquivo/imagem em um registro. Com ?includeData=true o conteúdo base64 é incorporado; com false apenas o nome e o tamanho retornam.
Pedido
const file = await fetch(
'/api/files/account/a1b2c3d4-…/new_attachment?includeData=true',
{ credentials: 'include' }
).then(r => r.json());
Resposta
{
"fileName": "contract.pdf",
"fileSizeInBytes": 245678,
"fileData": "JVBERi0xLjQKJeLjz9M…"
}
POST /api/files/{tableLogicalName}/{columnName}/batch?includeData=…
Obtém metadados de arquivos (e, opcionalmente, conteúdo) para muitos registros da mesma tabela / coluna em uma única viagem de ida e volta — o corpo é um array JSON de GUIDs de registros. Usado pelo Download Selected do FileGrid para que o cliente não dispare N chamadas separadas.
Pedido
const files = await fetch('/api/files/account/new_attachment/batch?includeData=false', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(['a1b2c3d4-…', 'b2c3d4e5-…'])
}).then(r => r.json());
Resposta
[
{ "fileName": "contract.pdf", "fileSizeInBytes": 245678, "fileData": null },
{ "fileName": "invoice.docx", "fileSizeInBytes": 89456, "fileData": null }
]
POST /api/files/createFileArchive
Zipa os valores escolhidos da coluna do arquivo para um conjunto de registros do lado do servidor. Retorna o fluxo bruto application/zip por padrão, ou — com responseFormat: 1 — um envelope JSON contendo o arquivo base64. POST (não GET) para que listas de IDs grandes não atinjam limites de comprimento de URL.
Pedido
// Padrão: aplicação bruta/zip stream. Pass responseFormat: 1 para envelope JSON.
const blob = await fetch('/api/files/createFileArchive', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tableName: 'account',
columnName: 'new_attachment',
recordIds: ['a1b2c3d4-…', 'b2c3d4e5-…']
})
}).then(r => r.blob());
Resposta
// responseFormat: 1 (Json) em vez do fluxo binário padrão
{
"fileName": "account-files.zip",
"contentType": "application/zip",
"data": "UEsDBBQAAAAIAP2t…"
}
Fibrados de localização
Cordas localizadas para front-ends que não são Blazor. A /api/localizedStrings rota retorna toda a árvore para uma cultura; as /localizations/* rotas servem pacotes com impressão digital, imutáveis em cache (padrão, por tabela, por visualização) para carregamento incremental eficiente — esses são públicos e não exigem o cookie de autenticação.
GET /api/localizedStrings/{culture}
Retorna a árvore completa de strings localizadas para uma cultura como um objeto aninhado — strings de framework, overrides de app, etiquetas de tabela e escolha.
Pedido
const strings = await fetch('/api/localizedStrings/fr-fr', { credentials: 'include' })
.then(r => r.json());
Resposta
{
"app": { "navigation": { "home": "Accueil" } },
"tables": { "account": { "columns": { "name": { "label": "Nom du compte" } } } }
}
GET /localizations/version
Retorna o manifesto de localização — apenas a lista de locais suportados. Servia sem cache para que uma nova versão fosse detectada no próximo carregamento da página. Público.
Pedido
const manifest = await fetch('/localizations/version').then(r => r.json());
Resposta
{ "supportedLocales": ["en-us", "fr-fr", "de-de"] }
GET /localizations/{locale}/thumbprints
Retorna as impressões digitais de conteúdo para um local: o pacote padrão mais cada tabela e visualização carregadas. Os clientes buscam esses pacotes e depois solicitam apenas os pacotes cuja impressão digital foi alterada. Público.
Pedido
const thumbs = await fetch('/localizations/fr-fr/thumbprints').then(r => r.json());
Resposta
{
"bundle": "a3f5e8c2d",
"tables": { "account": "b7f2d9e1a", "contact": "c4f8a1b3e" },
"views": { "550e8400e29b41d4a716446655440000": "e2f4b8d1c" }
}
GET /localizations/default/{filename} · /tables/{tableName}/{filename} · /views/{viewId}/{filename}
As três famílias de bundles — padrão (strings de corte transversal), por tabela (strings de uma tabela mais as escolhas globais referenciadas por suas colunas) e por visualização. O nome do arquivo é {locale}.{thumbprint}.json e cada um é servido public, immutable, max-age=31536000, então uma impressão digital estável é um acerto garantido no cache. Público.
Pedido
// O nome do arquivo é '{locale}. {thumbprint}.json', servido imutável + cache-forever.
const bundle = await fetch('/localizations/tables/account/fr-fr.b7f2d9e1a.json')
.then(r => r.json());
Resposta
{
"tables": { "account": { "label": "Compte", "columns": { "name": { "label": "Nom du compte" } } } },
"choices": { "account_industrycode": { "1": "Fabrication", "2": "Services" } }
}
Cultura
Muda a cultura ativa do navegador ao escrever o cookie de cultura.
GET /Culture/{culture}?redirectUri=…
Define o cookie de cultura e 302-redireciona para redirectUri. Navegue até ele (carregamento total da página) em vez de buscar, assim o Set-Cookie redirecionamento e entra em efeito. Público.
Pedido
// Navegação de página inteira para que o Set-Cookie + redirecionamento sejam respeitados.
window.location.href = '/Culture/fr-fr?redirectUri=' + encodeURIComponent('/dashboard');
Resposta
// 302 Encontrado → Localização: /dashboard
// Set-Cookie: . AspNetCore.Culture=c=fr-fr|uic=fr-fr
Admin — gerenciamento de cache
Inspeção e invalidação do cache do servidor. Todos os três endpoints são bloqueados por [Authorize(Roles = "SystemAdmin")] — apenas um administrador logado pode chamá-los.
GET /api/caches
Lista os nomes de todos os caches registrados do lado do servidor. Requer o papel de Administrador de Sistema.
Pedido
const names = await fetch('/api/caches', { credentials: 'include' }).then(r => r.json());
Resposta
["TableMetadataCache", "ViewMetadataCache", "CurrencyCache"]
POST /api/caches/clear
Limpa todos os caches do lado do servidor e retorna um resultado por cache (se foi bem-sucedido e quanto tempo levou). Requer o papel de Administrador de Sistema.
Pedido
const results = await fetch('/api/caches/clear', { method: 'POST', credentials: 'include' })
.then(r => r.json());
Resposta
[
{ "name": "TableMetadataCache", "succeeded": true, "error": null, "elapsedMs": 45 },
{ "name": "ViewMetadataCache", "succeeded": false, "error": "Timed out", "elapsedMs": 5000 }
]
POST /api/caches/{cacheName}/clear
Limpa um único cache nomeado (o nome vem do endpoint da lista). Retorna 404 se nenhum cache com esse nome estiver registrado. Requer o papel de Administrador de Sistema.
Pedido
const result = await fetch('/api/caches/TableMetadataCache/clear',
{ method: 'POST', credentials: 'include' }).then(r => r.json());
Resposta
{ "name": "TableMetadataCache", "succeeded": true, "error": null, "elapsedMs": 132 }
Autenticação
Login, cadastro e ciclo de vida da conta, respaldados por MapAuthEndpoints<TUser>. Baseado em cookies: um login bem-sucedido configura o cookie da aplicação ASP.NET Core Identity, e toda chamada posterior autentica ao reintroduzi-lo. A maioria retorna um result enum em HTTP 200 em vez de sinalizar o resultado através do código de status.
POST /api/auth/login
Faz login com e-mail + senha. O result enum distingue sucesso, um segundo fator obrigatório, credenciais ruins, um e-mail não confirmado e bloqueio. Em caso de sucesso, o cookie de autenticação é definido na resposta.
Pedido
const { result } = await fetch('/api/auth/login', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'user@example.com', password: 'P@ssw0rd!', rememberMe: true })
}).then(r => r.json());
// resultado: 0 Sucesso · 1 RequerTwoFactor · 2 CredenciaisInválidas · 3 EmailNotConfirmed · 4 Bloqueado
Resposta
{ "result": 0 }
POST /api/auth/login/2fa
Completa um login retornado RequiresTwoFactor enviando o código do autenticador (ou de recuperação). rememberMachine Define o cookie do navegador confiável para que logins futuros neste navegador pulem o segundo fator.
Pedido
const { success } = await fetch('/api/auth/login/2fa', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: '123456', rememberMachine: false })
}).then(r => r.json());
Resposta
{ "success": true }
POST /api/auth/logout
Limpa o cookie de autenticação, encerrando a sessão.
Pedido
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
Resposta
// 200 OK — sem corpo. O cookie de autenticação é liberado na resposta.
POST /api/auth/register
Cria uma nova conta local. Dependendo da configuração, o resultado é um e-mail de confirmação enviado, um login imediato ou um conflito com um e-mail existente. Falhas de senha fraca e outras validações retornam como 400 problem+json.
Pedido
const { result } = await fetch('/api/auth/register', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'newuser@example.com', password: 'P@ssw0rd!' })
}).then(r => r.json());
// resultado: 0 ConfirmaçãoEmailEnviado · 1 Assinado · 2 E-mailJáInUse
Resposta
{ "result": 0 }
POST /api/auth/forgot-password
Inicia uma redefinição de senha enviando um link de redefinição por e-mail. Sempre devolve 200 sem saber se o endereço existe ou não, então não pode ser usado para sondar e-mails registrados.
Pedido
await fetch('/api/auth/forgot-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'user@example.com' })
});
Resposta
// 200 OK — sem corpo, independentemente de o endereço existir ou não (seguro para enumeração).
POST /api/auth/reset-password
Completa um reset usando o token do e-mail mais a nova senha. result distingue sucesso, um token inválido/expirado e uma senha rejeitada (com as mensagens de validação em errors).
Pedido
const res = await fetch('/api/auth/reset-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'user@example.com', code: '<token-from-email>', newPassword: 'N3wP@ss!' })
}).then(r => r.json());
// resultado: 0 Sucesso · 1 InvalidOrExpiredToken · 2 Senha Inválida
Resposta
{ "result": 0, "errors": [] }
POST /api/auth/confirm-email
Confirma um e-mail recém-registrado usando o ID de usuário e o token do link de confirmação.
Pedido
const { success } = await fetch('/api/auth/confirm-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: '550e8400-…', code: '<token-from-email>' })
}).then(r => r.json());
Resposta
{ "success": true }
POST /api/auth/resend-email-confirmation
Reenvia o link de confirmação por e-mail. Assim como o esquecido de senha, ele sempre retorna 200 sem o corpo para evitar vazar quais endereços estão registrados.
Pedido
await fetch('/api/auth/resend-email-confirmation', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'user@example.com' })
});
Resposta
// 200 OK — sem corpo (seguro para enumeração).
GET /api/auth/options
components.PowerPortalsPro.Demo.Client.Customizations.Pages.ClientApi.ClientApiPage.ep-auth-options-desc
Pedido
const options = await fetch('/api/auth/options').then(r => r.json());
Resposta
{
"localAccountsEnabled": true,
"externalProviders": [
{ "name": "Microsoft", "displayName": "Microsoft" },
{ "name": "Google", "displayName": "Sign in with Google" },
{ "name": "Facebook", "displayName": "Facebook" }
]
}
GET /api/auth/external-login?provider=…&returnUrl=…
Inicia o fluxo OAuth para um provedor. Ele retorna um desafio 302 ao provedor, então navegue o navegador até ele (não busque) — um SPA deve definir window.location.href.
Pedido
// Navegue por página inteira — NÃO busque — para que o navegador siga a cadeia OAuth 302.
window.location.href =
'/api/auth/external-login?provider=Microsoft&returnUrl=' + encodeURIComponent('/dashboard');
Resposta
// 302 encontrado → página de login do provedor externo (além de cookies de correlação).
GET /api/auth/external-login/pending
Após o retorno do OAuth, faz snapshots do login externo em voo: o provedor, as reivindicações da identidade e — quando o e-mail corresponde a mais de uma identidade de portal — a lista de candidatos para escolher. Retorna o 204 quando não há login pendente.
Pedido
const res = await fetch('/api/auth/external-login/pending', { credentials: 'include' });
const pending = res.status === 204 ? null : await res.json();
Resposta
{
"loginProvider": "Microsoft",
"providerDisplayName": "Microsoft",
"identityEmail": "user@company.com",
"requiresChoice": false,
"candidates": []
}
POST /api/auth/external-login/confirm
Conclui um login externo pela primeira vez para uma nova conta confirmando o e-mail para o associado. Resolve para um login, um e-mail de confirmação, sem pendência ou falha.
Pedido
const { result } = await fetch('/api/auth/external-login/confirm', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'user@company.com' })
}).then(r => r.json());
// resultado: 0 Assinado · 1 ConfirmaçãoE-mail Enviado · 2 NoPendingExternalLogin · 3 Fracasso
Resposta
{ "result": 0, "errors": [] }
POST /api/auth/external-login/select
Conclui um login externo quando a identidade corresponde a múltiplas identidades do portal, escolhendo qual delas (Contato ou UsuárioSistema) usar para entrar.
Pedido
const { result } = await fetch('/api/auth/external-login/select', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ kind: 1 }) // 0 Contato · 1 SystemUser
}).then(r => r.json());
Resposta
{ "result": 0, "errors": [] }
GET /api/auth/me
Snapshot do diretor atual — id, nome, e-mail, funções e a tabela de apoio (contact vs. systemuser), além de qualquer identidade alternativa de irmão. Retorna uma forma anônima (isAuthenticated: false) em vez de 401 quando não há cookie presente, então um SPA pode chamá-la na primeira pintura sem desviar no código de status.
Pedido
const me = await fetch('/api/auth/me', { credentials: 'include' }).then(r => r.json());
Resposta
{
"isAuthenticated": true,
"userId": "550e8400-…",
"userName": "user@example.com",
"email": "user@example.com",
"roles": ["Member"],
"tableName": "contact",
"altIdentityTableName": "systemuser",
"altIdentityUserId": "660e8400-…"
}
POST /api/auth/switch-identity
Troca o cookie atual pela identidade alternativa do usuário (o pareamento Contact↔SystemUser apareceu em /api/auth/me). O corpo JSON é um objeto vazio.
Pedido
const { result } = await fetch('/api/auth/switch-identity', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: '{}'
}).then(r => r.json());
// resultado: 0 Trocado · 1 NoAltIdentity · 2 AltIdentidadeNãoEncontrado · 3 Não Autenticado
Resposta
{ "result": 0 }
Gerenciamento de contas
As operações de autoatendimento do usuário logado sob /api/auth/manage/* — perfil, senha, e-mail, dois fatores, logins externos vinculados e dados pessoais. Todos exigem uma sessão autenticada e espelham as páginas clássicas /Account/Manage do Razor do framework.
GET /api/auth/manage/profile
Retorna o perfil do usuário (nome, celular, e-mail) lido do contato vinculado do Dataverse, além dos flags de status de Identidade (e-mail confirmado, tem senha, 2FA ativado, somente leitura).
Pedido
const profile = await fetch('/api/auth/manage/profile', { credentials: 'include' })
.then(r => r.json());
Resposta
{
"firstName": "John", "lastName": "Doe", "mobilePhone": "+1-555-0123",
"email": "john.doe@example.com",
"isEmailConfirmed": true, "hasPassword": true, "isTwoFactorEnabled": false, "isReadOnly": false
}
POST /api/auth/manage/profile
Atualiza o nome/sobrenome e o celular no contato vinculado. Retorna 200 em caso de sucesso; As identidades suportadas pelo usuário do SystemUser, são somente leitura e recebem 403.
Pedido
await fetch('/api/auth/manage/profile', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ firstName: 'Jane', lastName: 'Smith', mobilePhone: '+1-555-9876' })
});
Resposta
// 200 OK — sem corpo. (403 para identidades suportadas pelo SystemUser em leitura somante.)
POST /api/auth/manage/password/set
Adiciona uma senha local para uma conta que não tem uma (por exemplo, uma conta que só permite login externo). Mensagens de validação, se houver, retornam em errors.
Pedido
const res = await fetch('/api/auth/manage/password/set', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ newPassword: 'N3wP@ss!' })
}).then(r => r.json());
Resposta
{ "success": true, "errors": [] }
POST /api/auth/manage/password/change
Altera a senha local; É necessário a senha atual. result Distingue sucesso, senha atual errada e senha nova rejeitada.
Pedido
const { result } = await fetch('/api/auth/manage/password/change', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ oldPassword: 'Old!', newPassword: 'N3wP@ss!' })
}).then(r => r.json());
// resultado: 0 Sucesso · 1 Senha Antiga Incorreta · 2 Senha Inválida
Resposta
{ "result": 0, "errors": [] }
POST /api/auth/manage/email/change
Inicia uma alteração de e-mail enviando um link de confirmação para o novo endereço. A mudança só entra em vigor quando esse link for seguido.
Pedido
const { result } = await fetch('/api/auth/manage/email/change', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ newEmail: 'new@example.com' })
}).then(r => r.json());
// resultado: 0 ConfirmaçãoEmailEnviado · 1 IgualComoE-E-Mail Atual
Resposta
{ "result": 0 }
POST /api/auth/manage/email/send-confirmation
Reenvia o link de confirmação do e-mail atual do usuário. sent é falso quando o e-mail já está confirmado.
Pedido
const { sent } = await fetch('/api/auth/manage/email/send-confirmation',
{ method: 'POST', credentials: 'include' }).then(r => r.json());
Resposta
{ "sent": true }
GET /api/auth/manage/2fa
Retorna o estado da 2FA — se um autenticador está registrado, se a 2FA está ativada, se o navegador é lembrado e quantos códigos de recuperação restam.
Pedido
const status = await fetch('/api/auth/manage/2fa', { credentials: 'include' })
.then(r => r.json());
Resposta
{ "hasAuthenticator": true, "is2faEnabled": true, "isMachineRemembered": false, "recoveryCodesLeft": 8 }
GET /api/auth/manage/authenticator/setup
Retorna a chave compartilhada e otpauth:// o URI para a tela de registro do código QR. Pare-o com o endpoint de verificação para finalizar a ativação da 2FA.
Pedido
const setup = await fetch('/api/auth/manage/authenticator/setup', { credentials: 'include' })
.then(r => r.json());
Resposta
{
"sharedKey": "abcd efgh ijkl mnop",
"authenticatorUri": "otpauth://totp/PowerPortalsPro:user@example.com?secret=ABCD…&issuer=PowerPortalsPro"
}
POST /api/auth/manage/authenticator/verify
Verifica um código do app autenticador e ativa a 2FA. Na primeira inscrição, a resposta também retorna o conjunto inicial de códigos de recuperação.
Pedido
const res = await fetch('/api/auth/manage/authenticator/verify', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: '123456' })
}).then(r => r.json());
Resposta
{ "success": true, "recoveryCodes": ["ABC123DEF456", "GHI789JKL012", "…"] }
POST /api/auth/manage/authenticator/reset
Gira a chave autenticadora. Isso também desativa a 2FA, então o usuário deve se reinscrever.
Pedido
const { success } = await fetch('/api/auth/manage/authenticator/reset',
{ method: 'POST', credentials: 'include' }).then(r => r.json());
Resposta
{ "success": true }
POST /api/auth/manage/2fa/disable
Desativa o 2FA da conta.
Pedido
const { success } = await fetch('/api/auth/manage/2fa/disable',
{ method: 'POST', credentials: 'include' }).then(r => r.json());
Resposta
{ "success": true }
POST /api/auth/manage/2fa/recovery-codes/generate
Regenera os códigos de recuperação, substituindo qualquer conjunto existente, e retorna os novos códigos.
Pedido
const { recoveryCodes } = await fetch('/api/auth/manage/2fa/recovery-codes/generate',
{ method: 'POST', credentials: 'include' }).then(r => r.json());
Resposta
{ "recoveryCodes": ["ABC123DEF456", "GHI789JKL012", "…"] }
POST /api/auth/manage/2fa/forget-browser
Remove o cookie do dispositivo confiável deste navegador, então 2FA será necessário novamente no próximo login aqui.
Pedido
const { success } = await fetch('/api/auth/manage/2fa/forget-browser',
{ method: 'POST', credentials: 'include' }).then(r => r.json());
Resposta
{ "success": true }
GET /api/auth/manage/external-logins
Lista os logins externos atualmente vinculados à conta.
Pedido
const logins = await fetch('/api/auth/manage/external-logins', { credentials: 'include' })
.then(r => r.json());
Resposta
{
"currentLogins": [
{ "loginProvider": "Microsoft", "providerKey": "oid-…", "providerDisplayName": "Microsoft" }
]
}
GET /api/auth/manage/login-info
Uma visão combinada dos caminhos de login do usuário — logins externos vinculados mais se uma senha local está definida — usada para decidir se remover um login bloquearia o acesso do usuário.
Pedido
const info = await fetch('/api/auth/manage/login-info', { credentials: 'include' })
.then(r => r.json());
Resposta
{
"externalLogins": [ { "loginProvider": "Microsoft", "providerKey": "oid-…", "providerDisplayName": "Microsoft" } ],
"hasLocalPassword": true
}
GET /api/auth/manage/external-logins/link?provider=…
Inicia o fluxo OAuth que vincula um provedor adicional à conta logada. Retorna um desafio 302, então navegue até ele em vez de buscar.
Pedido
// Navegação de página inteira; O callback abaixo adiciona o login e redireciona de volta.
window.location.href =
'/api/auth/manage/external-logins/link?provider=Google&returnUrl=' + encodeURIComponent('/account');
Resposta
// 302 encontrado → a página de consentimento do provedor externo.
GET /api/auth/manage/external-logins/link/callback
O retorno OAuth para o fluxo de link. O provedor redireciona o navegador para aqui; O servidor anexa o login e redireciona 302 para o returnUrl original fornecido. Você não liga diretamente.
Pedido
// Atingido pelo redirecionamento do provedor OAuth — não chamado diretamente pelo seu código.
Resposta
// 302 Encontrado → returnUrl fornecido ao endpoint do link, com o login agora anexado.
POST /api/auth/manage/external-logins/remove
Desvincula um login externo por provedor + chave de provedor.
Pedido
const { success } = await fetch('/api/auth/manage/external-logins/remove', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ loginProvider: 'Microsoft', providerKey: 'oid-…' })
}).then(r => r.json());
Resposta
{ "success": true }
GET /api/auth/manage/personal-data
Exporta os dados pessoais do usuário — todas [PersonalData] as propriedades mais seus logins externos vinculados — para download no estilo GDPR.
Pedido
const data = await fetch('/api/auth/manage/personal-data', { credentials: 'include' })
.then(r => r.json());
Resposta
{
"personalData": { "Id": "550e8400-…", "Email": "john.doe@example.com" },
"externalLogins": { "Microsoft": "oid-…" }
}
POST /api/auth/manage/personal-data/delete
Exclui permanentemente a conta do usuário e o desconecta. Requer a senha atual quando a conta tem uma; Passe null por contas apenas externas. result Distingue sucesso, senha errada e o caso em que uma senha é necessária, mas não foi fornecida.
Pedido
const { result } = await fetch('/api/auth/manage/personal-data/delete', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: 'CurrentP@ss!' }) // nulo para contas apenas externas
}).then(r => r.json());
// resultado: 0 Sucesso · 1 Senha Incorreta · 2 RequererPalavra-Senha Local
Resposta
{ "result": 0 }
Veja também
Documentação relacionada:
IPowerPortalsProService — o wrapper C# ao redor desses endpoints — o que seus componentes Blazor injetam quando não precisam emitir HTTP bruto por conta própria.Login SystemUser — Contexto sobre o porquê/api/auth/mede relatóriostableName: "systemuser"para alguns diretores etableName: "contact"para outros.
