API del cliente
PowerPortalsPro publica una superficie JSON-over-HTTP bajo /api/* la cual puede llamar cualquier aplicación de una sola página — el cliente de Blazor WebAssembly en bandeja la utiliza para respaldar IPowerPortalsProService y IAuthService, pero los mismos endpoints son igualmente utilizables desde una interfaz de React, Vue o JS vanilla alojada junto a (o incluso cross-origin desde) el servidor. Esta página es la referencia completa: cada endpoint que se puede llamar por cliente, con una solicitud y respuesta de ejemplo.
¿Para quién es esto?
Si estás construyendo una app Blazor y consumiendo el framework a través de
IPowerPortalsProService, no necesitas llamar directamente a estos endpoints — la implementación del cliente lo hace por ti. Esta página documenta la superficie para equipos que escriben un SPA que no sea Blazor, o que conectan un cliente HTTP personalizado, contra el mismo servidor.
Habilitación de los puntos finales
UsePowerPortalsProWebServer cablea los puntos finales de datos (CRUD de la tabla, FetchXML, metadatos, archivos, localización, administración). MapAuthEndpoints<TUser> conecta la superficie de autenticación orientada a SPA — llámala explícitamente si tu host necesita autenticación por cookies de una SPA.
// Program.cs — pipeline de servidores
app.UsePowerPortalsProWebServer();
app.MapAuthEndpoints<PortalUser>();
Ambas llamadas son no-ops cuando su función no está en uso, así que conecta el cable una Program.cs vez conectado, independientemente del modo de interactividad que use el anfitrión.
El tipo de rutas — fuente única de verdad
PowerPortalsPro.Web.Common.Routes expone cada ruta de extremo como una propiedad fuertemente tipada. Tanto el cliente de entrada como cualquier SPA externo basado en C# deberían referenciar estas constantes en lugar de escribir cadenas manuales, de modo que un renombramiento en el lado del servidor aparece como un error de compilación en lugar de un 404 en tiempo de ejecución. Los clientes JavaScript/TypeScript, por supuesto, tendrán que hacer los caminos en línea, pero el lado C# de Routes sigue siendo la referencia canónica de cuáles son esos caminos.
// En cualquier lugar — tanto PowerPortalsPro.Web.Client como en SPAs externos de 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 cubre los endpoints de datos y administración; Routes.Api.Auth cubre el inicio de sesión / registro; Routes.Api.Auth.Manage Cubre las operaciones de gestión de cuentas del usuario iniciado sesión.
Modelo de autenticación — cookies, no tokens
La autenticación es basada en cookies de navegador. No hay un paso de emisión del JWT. Un SPA llama /api/auth/logina , el servidor establece la .AspNetCore.Identity.Application cookie en la respuesta, y el navegador la adjunta en cada solicitud posterior — incluyendo llamadas a datos bajo /api/table/*. Desde JavaScript, esto significa fetch(..., { credentials: 'include' }) que en cada llamada (o axios.defaults.withCredentials = true); sin ella, la cookie se pierde y el servidor devuelve 401. Todos los ejemplos que aparecen a continuación lo incluyen.
// React / Vue / busca pura — credenciales: se requiere 'incluir' para que el
// El navegador envía y almacena la cookie de autenticación emitida 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);
}
SPA de origen cruzado
Si el SPA y el servidor están en orígenes diferentes, el servidor debe enviar
Access-Control-Allow-Origin: <spa-origin>(no*) yAccess-Control-Allow-Credentials: true, y el SPA debe usarcredentials: 'include'. El alojamiento de origen similar (el SPA servido desde el mismo sitio que la API) evita el problema por completo.
La forma del récord
Cada extremo de datos que lee o escribe una fila intercambia el mismo TableRecord envolvente. properties es un mapa del nombre lógico de la columna → un objeto valor tipado cuyo $type discriminador identifica el tipo de columna. permissions es una máscara de bandera de bits (Read 1, Create 2, Write 4, Delete 8, Append 16, AppendTo 32) que describe lo que el usuario actual puede hacer con la fila. En los writes solo necesitas enviar las columnas que vas a cambiar.
{
"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 }
}
Los $type valores reflejan el tipo de atributo Dataverse: 0 Booleano, 2 FechaHora, 3 Decimal, 4 Doble, 5 Entero, 6 Búsqueda, 8 Dinero, 11 Elección, 14 Cadena, 15 Identificador único, 40 MultiSelectChoice, 41 Archivo, 42 Imagen. Las búsquedas suman name + tableName; número, dinero y valores de fecha llevan un formato formattedValueDataverse .
Respuestas de error
Llamadas fallidas devuelven RFC 9457 application/problem+json. La implementación del cliente en la bandeja de entrada rehidrata el tipo de excepción CLR original de los detalles del problema, de modo que el lado del servidor lanza la superficie como la misma excepción en el cliente; para non-.NET SPAs, los type campos / title / detail status / son el handle estándar.
{
"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."
}
Referencia de punto final
Cada endpoint a continuación muestra el método y la ruta, qué hace, una solicitud de ejemplo (como llamada de navegador fetch ) y una respuesta de ejemplo. Los segmentos de camino en {braces} son marcadores de posición. Salvo que se indique lo contrario, una respuesta 2xx es el caso de éxito y los fallos vuelven como problem+json.
Discos y CRUD
Crear, leer, actualizar y eliminar un solo registro, además de consultas arbitrarias de FetchXML y lotes transaccionales. Respaldado por UsePowerPortalsProWebServer; cada lectura y escritura aplica al consumidor ITablePermissionHandler / ITableRecordPermissionHandler interceptor y a cualquier archivo registrado IFetchXmlBuilderInterceptor.
POST /api/table/{tableLogicalName}
Crea una nueva fila en la tabla nombrada. Envía un TableRecord que lleva las columnas a set; la respuesta devuelve el id del nuevo registro.
Petición
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();
Respuesta
{
"responseName": "CreateResponse",
"outputParameters": {},
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
GET /api/table/{tableLogicalName}/{recordId}
Lee una sola fila por id. El parámetro de consulta opcional ?columns= (nombres lógicos separados por comas) reduce la proyección — omitirlo para recuperar el conjunto de columnas por defecto de la tabla.
Petición
const record = await fetch(
'/api/table/account/a1b2c3d4-e5f6-7890-abcd-ef1234567890?columns=name,revenue',
{ credentials: 'include' }
).then(r => r.json());
Respuesta
{
"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}
Actualiza una fila existente. Solo se escriben las columnas presentes properties , así que envía solo los valores cambiados.
Petición
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 } }
})
});
Respuesta
{ "responseName": "UpdateResponse", "outputParameters": {} }
DELETE /api/table/{tableLogicalName}/{recordId}
Elimina la fila por id.
Petición
await fetch('/api/table/account/a1b2c3d4-e5f6-7890-abcd-ef1234567890', {
method: 'DELETE',
credentials: 'include'
});
Respuesta
{ "responseName": "DeleteResponse", "outputParameters": {} }
GET /api/retrieveMultiple?fetchXml=…
Ejecuta una consulta arbitraria de FetchXML y devuelve las filas correspondientes más la información de paginación. Codifica el FetchXML en la fetchXml cadena de consulta — de C#, Routes.Api.GetRetrieveMultipleRoute(fetchXml) hace esto por ti.
Petición
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());
Respuesta
{
"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
Ejecuta un lote de peticiones heterogéneas (Crear / Actualizar / Eliminar / Asociar / Disociar) en una sola transacción de base de datos — si alguna falla, todo el lote se revierte. El cuerpo es un array JSON de OrganizationRequest objetos discriminados por $type. Paso ?returnResponses=false para saltarse la creación de la lista de respuestas por solicitud.
Petición
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' } }
])
});
Respuesta
[
{ "$type": "CreateResponse", "responseName": "CreateResponse", "outputParameters": {}, "id": "new-guid" },
{ "$type": "DeleteResponse", "responseName": "DeleteResponse", "outputParameters": {} }
]
Cuadrículas y gráficos
Endpoints de consulta compuestos por servidores. En lugar de que el cliente construya FetchXML, pasas un id de vista (o tu propio FetchXML) más búsqueda / ordenación / paginación, y el servidor resuelve columnas, aplica permisos y ejecuta la consulta — el mismo camino de fuente única de verdad que usan MainGrid y los componentes del gráfico.
POST /api/grids/data
Carga una página de datos de cuadrícula. Proporciona un viewId filtro de paginación o de columnas propio fetchXml, además de opcionales searchTextde , sortsde paginación y de columnas. La respuesta lleva las filas, las definiciones de columnas resueltas y la máscara de permisos de tabla del llamante.
Petición
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());
Respuesta
{
"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
Cargas agregadas datos de cartas adaptadas a los componentes de cartografía. Acepta una configuración agregada, un viewId, o raw fetchXml, más el mapeo de columnas de etiquetas / valores / serie; devuelve etiquetas y conjuntos de datos al estilo Chart.js.
Petición
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());
Respuesta
{
"data": {
"labels": ["Active", "Inactive"],
"datasets": [ { "label": "Accounts by Status", "data": [ { "value": 25 }, { "value": 8 } ] } ]
}
}
Metadatos y permisos
Consultas de solo lectura para metadatos de tablas y vistas, la máscara de permisos del usuario actual y la configuración de toda la organización. Todas están en caché en el lado del servidor, así que las llamadas repetidas son baratas.
GET /api/tableMetadata/{tableLogicalName}
Devuelve los metadatos de una tabla — columnas (con tipos, etiquetas y restricciones), columnas de id / nombre / imagen primarias y relaciones.
Petición
const meta = await fetch('/api/tableMetadata/account', { credentials: 'include' })
.then(r => r.json());
Respuesta
{
"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}
Devuelve la máscara combinada TableSecurityPermission del usuario actual para la tabla como un solo entero (indicadores de bit: Leer 1, Crear 2, Escribir 4, Eliminar 8, Añadir 16, Añadir 32).
Petición
const mask = await fetch('/api/permissions/table/account', { credentials: 'include' })
.then(r => r.json());
// 15 === Leer(1) | Crear(2) | Escribe(4) | Eliminar(8)
Respuesta
15
GET /api/viewMetadata/{viewId}
Devuelve los metadatos de una vista guardada por su GUID — su FetchXML, columnas de diseño y banderas de vista. La ruta requiere un GUID, que es como se desambigua de la ruta de todas las vistas que aparece más abajo.
Petición
const view = await fetch('/api/viewMetadata/00000000-0000-0000-0000-000000000001',
{ credentials: 'include' }).then(r => r.json());
Respuesta
{
"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}
Devuelve todas las vistas guardadas de una tabla. Comparte el /api/viewMetadata/ prefijo con la ruta by-id — un segmento que no es GUID (un nombre lógico de tabla) aterriza aquí.
Petición
const views = await fetch('/api/viewMetadata/account', { credentials: 'include' })
.then(r => r.json());
Respuesta
[
{ "id": "0000…0001", "name": "All Accounts", "isDefault": true, "tableName": "account" },
{ "id": "0000…0002", "name": "Active Accounts", "isDefault": false, "tableName": "account" }
]
GET /api/organizationSettings
Devuelve configuraciones a nivel de organización procedentes del registro de la organización Dataverse: la moneda predeterminada, la lista de extensiones de archivo bloqueadas y el tamaño máximo de subida en bytes.
Petición
const settings = await fetch('/api/organizationSettings', { credentials: 'include' })
.then(r => r.json());
Respuesta
{
"defaultCurrency": { "isoCode": "USD", "symbol": "$", "precision": 2 },
"blockedFileExtensions": ["exe", "bat", "js"],
"maxUploadFileSizeInBytes": 10485760
}
Archivos
Lee el contenido de los archivos y las columnas de imágenes. Las cargas útiles binarias se devuelven codificadas en base64 dentro de JSON; La includeData bandera solo te permite obtener metadatos (por ejemplo, para renderizar una lista de descargas) sin transferir bytes.
GET /api/files/{tableLogicalName}/{recordId}/{columnName}?includeData=…
Devuelve los metadatos del archivo de una columna de archivo o imagen en un registro. Con ?includeData=true el contenido base64 está incrustado; solo false el nombre y el tamaño vuelven.
Petición
const file = await fetch(
'/api/files/account/a1b2c3d4-…/new_attachment?includeData=true',
{ credentials: 'include' }
).then(r => r.json());
Respuesta
{
"fileName": "contract.pdf",
"fileSizeInBytes": 245678,
"fileData": "JVBERi0xLjQKJeLjz9M…"
}
POST /api/files/{tableLogicalName}/{columnName}/batch?includeData=…
Obtiene metadatos de archivos (y opcionalmente contenido) para muchos registros de la misma tabla o columna en un solo viaje de ida y vuelta — el cuerpo es un array JSON de GUIDs de registros. Usado por la Selección de Descarga de FileGrid para que el cliente no lance N llamadas separadas.
Petición
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());
Respuesta
[
{ "fileName": "contract.pdf", "fileSizeInBytes": 245678, "fileData": null },
{ "fileName": "invoice.docx", "fileSizeInBytes": 89456, "fileData": null }
]
POST /api/files/createFileArchive
Comprime los valores elegidos de la columna de archivo para un conjunto de registros en el lado del servidor. Devuelve el flujo en bruto application/zip por defecto, o — con responseFormat: 1 — un sobre JSON que contiene el archivo base64. POST (no GET) para que las listas de ID grandes no alcancen límites de longitud de URL.
Petición
// Por defecto: aplicación bruta/zip stream. Pass responseFormat: 1 para un sobre 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());
Respuesta
// responseFormat: 1 (Json) en lugar del flujo binario predeterminado
{
"fileName": "account-files.zip",
"contentType": "application/zip",
"data": "UEsDBBQAAAAIAP2t…"
}
Fibrados de localización
Cuerdas localizadas para frontends que no son Blazor. La /api/localizedStrings ruta devuelve todo el árbol de una cultura; las /localizations/* rutas sirven paquetes con huellas digitales e caché inmutables (por defecto, por tabla, por vista) para una carga incremental eficiente — estos son públicos y no requieren la cookie de autenticación.
GET /api/localizedStrings/{culture}
Devuelve el árbol completo de cadenas localizadas para una cultura como objeto anidado — cadenas de framework, anulaciones de aplicaciones, tablas y etiquetas de elección.
Petición
const strings = await fetch('/api/localizedStrings/fr-fr', { credentials: 'include' })
.then(r => r.json());
Respuesta
{
"app": { "navigation": { "home": "Accueil" } },
"tables": { "account": { "columns": { "name": { "label": "Nom du compte" } } } }
}
GET /localizations/version
Devuelve el manifiesto de localización — solo la lista de ubicaciones soportadas. Sirve sin caché para que se detecte una nueva versión en la siguiente carga de página. Público.
Petición
const manifest = await fetch('/localizations/version').then(r => r.json());
Respuesta
{ "supportedLocales": ["en-us", "fr-fr", "de-de"] }
GET /localizations/{locale}/thumbprints
Devuelve las huellas digitales de contenido de una localidad: el paquete por defecto más cada tabla y vista cargada. Los clientes obtienen estos paquetes y luego solo solicitan los paquetes cuya huella digital cambió. Público.
Petición
const thumbs = await fetch('/localizations/fr-fr/thumbprints').then(r => r.json());
Respuesta
{
"bundle": "a3f5e8c2d",
"tables": { "account": "b7f2d9e1a", "contact": "c4f8a1b3e" },
"views": { "550e8400e29b41d4a716446655440000": "e2f4b8d1c" }
}
GET /localizations/default/{filename} · /tables/{tableName}/{filename} · /views/{viewId}/{filename}
Las tres familias de bundles — por defecto (cadenas de corte transversal), por tabla (cadenas de una tabla más las elecciones globales que referencian sus columnas) y por vista. El nombre del archivo es {locale}.{thumbprint}.json y cada uno se sirve public, immutable, max-age=31536000, por lo que una huella digital estable es un impacto garantizado en la caché. Público.
Petición
// El nombre del archivo es '{locale}. {thumbprint}.json', servido inmutable + caché-forever.
const bundle = await fetch('/localizations/tables/account/fr-fr.b7f2d9e1a.json')
.then(r => r.json());
Respuesta
{
"tables": { "account": { "label": "Compte", "columns": { "name": { "label": "Nom du compte" } } } },
"choices": { "account_industrycode": { "1": "Fabrication", "2": "Services" } }
}
Cultura
Cambia la cultura activa del navegador escribiendo la cookie de cultura.
GET /Culture/{culture}?redirectUri=…
Establece la cookie de cultura y 302-redirige a redirectUri. Navega hasta él (carga completa de página) en lugar de buscarlo, para que el Set-Cookie redireccionamiento y haga efecto. Público.
Petición
// Navegación a página completa para que se respeten las Set-Cookie + la redirección.
window.location.href = '/Culture/fr-fr?redirectUri=' + encodeURIComponent('/dashboard');
Respuesta
// 302 Encontrado → Ubicación: /dashboard
// Set-Cookie: . AspNetCore.Culture=c=fr-fr|uic=fr-fr
Admin — gestión de caché
Inspección e invalidación de la caché del servidor. Los tres endpoints están bloqueados por [Authorize(Roles = "SystemAdmin")] — solo un administrador con inicio de sesión puede llamarlos.
GET /api/caches
Lista los nombres de todas las cachés registradas del lado del servidor. Requiere el rol de Administrador de Sistemas.
Petición
const names = await fetch('/api/caches', { credentials: 'include' }).then(r => r.json());
Respuesta
["TableMetadataCache", "ViewMetadataCache", "CurrencyCache"]
POST /api/caches/clear
Borra todas las cachés del lado del servidor y devuelve un resultado por caché (si tuvo éxito y cuánto tiempo tardó). Requiere el rol de Administrador de Sistemas.
Petición
const results = await fetch('/api/caches/clear', { method: 'POST', credentials: 'include' })
.then(r => r.json());
Respuesta
[
{ "name": "TableMetadataCache", "succeeded": true, "error": null, "elapsedMs": 45 },
{ "name": "ViewMetadataCache", "succeeded": false, "error": "Timed out", "elapsedMs": 5000 }
]
POST /api/caches/{cacheName}/clear
Borra una única caché con nombre (el nombre proviene del endpoint de la lista). Devuelve 404 si no hay caché con ese nombre registrado. Requiere el rol de Administrador de Sistemas.
Petición
const result = await fetch('/api/caches/TableMetadataCache/clear',
{ method: 'POST', credentials: 'include' }).then(r => r.json());
Respuesta
{ "name": "TableMetadataCache", "succeeded": true, "error": null, "elapsedMs": 132 }
Autenticación
Inicio de sesión, registro y ciclo de vida de la cuenta, respaldado por MapAuthEndpoints<TUser>. Basado en cookies: un inicio de sesión exitoso activa la cookie de la aplicación ASP.NET Core Identity, y cada llamada posterior se autentica presentándola de nuevo. La mayoría devuelve un result enum en HTTP 200 en lugar de señalar el resultado mediante el código de estado.
POST /api/auth/login
Inicia sesión con correo electrónico + contraseña. El result enum distingue éxito, un segundo factor necesario, malas credenciales, un correo electrónico no confirmado y bloqueo. En caso de éxito, la cookie de autenticación se fija en la respuesta.
Petición
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 Éxito · 1 RequiereDosFactores · 2 CredencialesInvalid · 3 EmailNotConfirmed · 4 Bloqueado
Respuesta
{ "result": 0 }
POST /api/auth/login/2fa
Completa un inicio de sesión que se devuelve RequiresTwoFactor enviando el código del autenticador (o de recuperación). rememberMachine Establece la cookie de navegador confiable para que los inicios de sesión futuros en este navegador se salten el segundo factor.
Petición
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());
Respuesta
{ "success": true }
POST /api/auth/logout
Limpia la galleta de autenticación, terminando la sesión.
Petición
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
Respuesta
// 200 OK — sin cuerpo. La cookie de autenticación queda limpia con la respuesta.
POST /api/auth/register
Crea una nueva cuenta local. Dependiendo de la configuración, el resultado es un correo electrónico de confirmación enviado, un inicio de sesión inmediato o un choque con un correo existente. Los fallos de contraseña débil y otros fallos de validación aparecen como 400 problem+json.
Petición
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 ConfirmaciónCorreo Enviado · 1 FirmadoEn · 2 Correo Ya InUse
Respuesta
{ "result": 0 }
POST /api/auth/forgot-password
Inicia un restablecimiento de contraseña enviando un enlace de restablecimiento por correo electrónico. Siempre devuelve 200 sin que el cuerpo indique si la dirección existe o no, así que no se puede usar para buscar correos registrados.
Petición
await fetch('/api/auth/forgot-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'user@example.com' })
});
Respuesta
// 200 OK — sin cuerpo, independientemente de si la dirección existe o no (seguro para enumeración).
POST /api/auth/reset-password
Completa un restablecimiento usando el token del correo más la nueva contraseña. result distingue éxito, un token inválido / expirado y una contraseña rechazada (con los mensajes de validación en errors).
Petición
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 Éxito · 1 InvalidOrExpiredToken · 2 Contraseña inválida
Respuesta
{ "result": 0, "errors": [] }
POST /api/auth/confirm-email
Confirma un correo electrónico recién registrado usando el ID de usuario y el token del enlace de confirmación.
Petición
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());
Respuesta
{ "success": true }
POST /api/auth/resend-email-confirmation
Reenvía el enlace de confirmación por correo electrónico. Al igual que olvidado de contraseña, siempre devuelve 200 sin necesidad de filtrar qué direcciones están registradas.
Petición
await fetch('/api/auth/resend-email-confirmation', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'user@example.com' })
});
Respuesta
// 200 OK — sin cuerpo (seguro para enumeración).
GET /api/auth/options
components.PowerPortalsPro.Demo.Client.Customizations.Pages.ClientApi.ClientApiPage.ep-auth-options-desc
Petición
const options = await fetch('/api/auth/options').then(r => r.json());
Respuesta
{
"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 el flujo OAuth para un profesional. Devuelve un desafío 302 al proveedor, así que navega por el navegador hasta él (no lo busques) — un SPA debería configurar window.location.href.
Petición
// Navegación a página completa — NO buscar — para que el navegador siga la cadena OAuth 302.
window.location.href =
'/api/auth/external-login?provider=Microsoft&returnUrl=' + encodeURIComponent('/dashboard');
Respuesta
// 302 encontrado → la página de inicio de sesión del proveedor externo (más cookies de correlación).
GET /api/auth/external-login/pending
Tras la devolución de llamada OAuth, se captura instantáneas del inicio de sesión externo en el vuelo: el proveedor, las reclamaciones de la identidad y — cuando el correo coincide con más de una identidad de portal — la lista de candidatos para elegir. Devuelve el 204 cuando no hay inicio de sesión pendiente.
Petición
const res = await fetch('/api/auth/external-login/pending', { credentials: 'include' });
const pending = res.status === 204 ? null : await res.json();
Respuesta
{
"loginProvider": "Microsoft",
"providerDisplayName": "Microsoft",
"identityEmail": "user@company.com",
"requiresChoice": false,
"candidates": []
}
POST /api/auth/external-login/confirm
Completa un inicio de sesión externo por primera vez para una cuenta nueva confirmando el correo al asociado. Se resuelve con un inicio de sesión, un correo de confirmación, sin pendiente o fallo.
Petición
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 FirmedIn · 1 ConfirmaciónCorreo Enviado · 2 NoPendingExternalLogin · 3 Fracaso
Respuesta
{ "result": 0, "errors": [] }
POST /api/auth/external-login/select
Completa un inicio de sesión externo cuando la identidad coincide con múltiples identidades de portal, eligiendo con cuál (Contacto o UsuarioSystem) iniciar sesión.
Petición
const { result } = await fetch('/api/auth/external-login/select', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ kind: 1 }) // 0 Contacto · 1 SystemUser
}).then(r => r.json());
Respuesta
{ "result": 0, "errors": [] }
GET /api/auth/me
Instantánea del director actual — id, nombre, correo electrónico, roles y la tabla de apoyo (contact vs. systemuser), más cualquier identidad alternativa de hermano. Devuelve una forma anónima (isAuthenticated: false) en lugar de 401 cuando no hay cookie presente, por lo que un SPA puede llamarlo en la primera pintura sin desviarse en el código de estado.
Petición
const me = await fetch('/api/auth/me', { credentials: 'include' }).then(r => r.json());
Respuesta
{
"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
Intercambia la cookie actual por la identidad hermana alternativa del usuario (el emparejamiento Contact↔SystemUser apareció en /api/auth/me). El cuerpo JSON es un objeto vacío.
Petición
const { result } = await fetch('/api/auth/switch-identity', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: '{}'
}).then(r => r.json());
// resultado: 0 Cambiado · 1 NoAltIdentity · 2 AltIdidentidadNoEncontrado · 3 NoAutenticado
Respuesta
{ "result": 0 }
Gestión de cuentas
Las operaciones de autoservicio del usuario iniciado sesión en /api/auth/manage/* — perfil, contraseña, correo electrónico, acceso a dos pasos, accesos externos vinculados y datos personales. Todos requieren una sesión autenticada y reflejan las páginas clásicas /Account/Manage de Razor del framework.
GET /api/auth/manage/profile
Devuelve el perfil del usuario (nombre, móvil, correo electrónico) leído desde el contacto vinculado de Dataverse, además de las banderas de estado de identidad (correo confirmado, con contraseña, 2FA activada, solo lectura).
Petición
const profile = await fetch('/api/auth/manage/profile', { credentials: 'include' })
.then(r => r.json());
Respuesta
{
"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
Actualiza el nombre / apellido y el teléfono móvil en el contacto vinculado. Retorna 200 en caso de éxito; Las identidades respaldadas por SystemUser son de solo lectura y reciben 403.
Petición
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' })
});
Respuesta
// 200 OK — sin cuerpo. (403 para identidades respaldadas por SystemUser de solo lectura.)
POST /api/auth/manage/password/set
Añade una contraseña local a una cuenta que no la tiene (por ejemplo, una cuenta solo de inicio de sesión externo). Los mensajes de validación, si los hay, vuelven a aparecer en errors.
Petición
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());
Respuesta
{ "success": true, "errors": [] }
POST /api/auth/manage/password/change
Cambia la contraseña local; Requiere la contraseña actual. result distingue éxito, contraseña actual incorrecta y contraseña nueva rechazada.
Petición
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 Éxito · 1 IncorrectOldPassword · 2 Contraseña inválida
Respuesta
{ "result": 0, "errors": [] }
POST /api/auth/manage/email/change
Inicia un cambio de correo enviando un enlace de confirmación a la nueva dirección. El cambio solo entra en vigor una vez que se sigue ese enlace.
Petición
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 ConfirmaciónCorreo Enviado · 1 MismoComoCorreo Electrónico Actual
Respuesta
{ "result": 0 }
POST /api/auth/manage/email/send-confirmation
Reenvía el enlace de confirmación del correo actual del usuario. sent es falso cuando el correo ya está confirmado.
Petición
const { sent } = await fetch('/api/auth/manage/email/send-confirmation',
{ method: 'POST', credentials: 'include' }).then(r => r.json());
Respuesta
{ "sent": true }
GET /api/auth/manage/2fa
Devuelve el estado de 2FA: si un autenticador está inscrito, si la 2FA está activada, si este navegador se recuerda y cuántos códigos de recuperación quedan.
Petición
const status = await fetch('/api/auth/manage/2fa', { credentials: 'include' })
.then(r => r.json());
Respuesta
{ "hasAuthenticator": true, "is2faEnabled": true, "isMachineRemembered": false, "recoveryCodesLeft": 8 }
GET /api/auth/manage/authenticator/setup
Devuelve la clave compartida y otpauth:// el URI para la pantalla de inscripción del código QR. Emparejalo con el endpoint de verificación para terminar de habilitar la 2FA.
Petición
const setup = await fetch('/api/auth/manage/authenticator/setup', { credentials: 'include' })
.then(r => r.json());
Respuesta
{
"sharedKey": "abcd efgh ijkl mnop",
"authenticatorUri": "otpauth://totp/PowerPortalsPro:user@example.com?secret=ABCD…&issuer=PowerPortalsPro"
}
POST /api/auth/manage/authenticator/verify
Verifica un código desde la app de autenticación y activa la 2FA. En la primera inscripción, la respuesta también devuelve el conjunto inicial de códigos de recuperación.
Petición
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());
Respuesta
{ "success": true, "recoveryCodes": ["ABC123DEF456", "GHI789JKL012", "…"] }
POST /api/auth/manage/authenticator/reset
Gira la clave de autenticación. Esto también deshabilita la 2FA, por lo que el usuario debe volver a inscribirse.
Petición
const { success } = await fetch('/api/auth/manage/authenticator/reset',
{ method: 'POST', credentials: 'include' }).then(r => r.json());
Respuesta
{ "success": true }
POST /api/auth/manage/2fa/disable
Desactiva el 2FA de la cuenta.
Petición
const { success } = await fetch('/api/auth/manage/2fa/disable',
{ method: 'POST', credentials: 'include' }).then(r => r.json());
Respuesta
{ "success": true }
POST /api/auth/manage/2fa/recovery-codes/generate
Regenera los códigos de recuperación, reemplazando cualquier conjunto existente, y devuelve los nuevos códigos.
Petición
const { recoveryCodes } = await fetch('/api/auth/manage/2fa/recovery-codes/generate',
{ method: 'POST', credentials: 'include' }).then(r => r.json());
Respuesta
{ "recoveryCodes": ["ABC123DEF456", "GHI789JKL012", "…"] }
POST /api/auth/manage/2fa/forget-browser
Borra la cookie de dispositivo confiable de este navegador, así que será necesaria 2FA de nuevo en el próximo inicio de sesión aquí.
Petición
const { success } = await fetch('/api/auth/manage/2fa/forget-browser',
{ method: 'POST', credentials: 'include' }).then(r => r.json());
Respuesta
{ "success": true }
GET /api/auth/manage/external-logins
Lista los inicios de sesión externos actualmente vinculados a la cuenta.
Petición
const logins = await fetch('/api/auth/manage/external-logins', { credentials: 'include' })
.then(r => r.json());
Respuesta
{
"currentLogins": [
{ "loginProvider": "Microsoft", "providerKey": "oid-…", "providerDisplayName": "Microsoft" }
]
}
GET /api/auth/manage/login-info
Una vista combinada de las rutas de inicio de sesión del usuario — inicios de sesión externos vinculados más si se establece una contraseña local — se utiliza para decidir si eliminar un inicio de sesión bloquearía al usuario.
Petición
const info = await fetch('/api/auth/manage/login-info', { credentials: 'include' })
.then(r => r.json());
Respuesta
{
"externalLogins": [ { "loginProvider": "Microsoft", "providerKey": "oid-…", "providerDisplayName": "Microsoft" } ],
"hasLocalPassword": true
}
GET /api/auth/manage/external-logins/link?provider=…
Inicia el flujo OAuth que vincula a un proveedor adicional con la cuenta iniciada sesión. Devuelve un desafío 302, así que navega hasta él en vez de buscarlo.
Petición
// Navegación a página completa; La llamada de vuelta de abajo añade el inicio de sesión y redirige de vuelta.
window.location.href =
'/api/auth/manage/external-logins/link?provider=Google&returnUrl=' + encodeURIComponent('/account');
Respuesta
// 302 Encontrado → la página de consentimiento del proveedor externo.
GET /api/auth/manage/external-logins/link/callback
El callback OAuth para el flujo de enlace. El proveedor redirige el navegador aquí; El servidor adjunta el inicio de sesión y 302 redirige al returnUrl sistema originalmente suministrado. No se llama directamente a esto.
Petición
// Afectado por la redirección del proveedor OAuth — no es llamado directamente por tu código.
Respuesta
// 302 Encontrado → el returnUrl suministrado al endpoint del enlace, con el inicio de sesión ahora adjunto.
POST /api/auth/manage/external-logins/remove
Desvincula un inicio de sesión externo por proveedor + clave de proveedor.
Petición
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());
Respuesta
{ "success": true }
GET /api/auth/manage/personal-data
Exporta los datos personales del usuario — cada [PersonalData] propiedad más sus accesos externos vinculados — para su descarga al estilo GDPR.
Petición
const data = await fetch('/api/auth/manage/personal-data', { credentials: 'include' })
.then(r => r.json());
Respuesta
{
"personalData": { "Id": "550e8400-…", "Email": "john.doe@example.com" },
"externalLogins": { "Microsoft": "oid-…" }
}
POST /api/auth/manage/personal-data/delete
Elimina permanentemente la cuenta del usuario y lo desconecta. Requiere la contraseña actual cuando la cuenta la tiene; Pasar null por cuentas solo externas. result Distingue el éxito, una contraseña incorrecta y el caso en que se requiere una contraseña pero no se ha proporcionado.
Petición
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!' }) // null para cuentas solo externas
}).then(r => r.json());
// resultado: 0 Éxito · 1 Contraseña incorrecta · 2 RequererContraseñaLocalLocal
Respuesta
{ "result": 0 }
Véase también
Documentación relacionada:
IPowerPortalsProService — el envoltorio C# alrededor de estos extremos — lo que tus componentes Blazor inyectan cuando no necesitan emitir HTTP en bruto.Inicio de sesión SystemUser — Contexto sobre los informestableName: "systemuser"por qué/api/auth/mepara algunos directores ytableName: "contact"para otros.
