Client API
PowerPortalsPro publie une surface JSON sur HTTP sous /api/* laquelle toute application d’une seule page peut être appelée — le client Blazor WebAssembly intégré l’utilise pour sauvegarder IPowerPortalsProService et IAuthService, mais les mêmes points d’extrémité sont également utilisables depuis une interface React, Vue ou JS vanilla hébergée à côté (ou même en cross-origin de) le serveur. Cette page est la référence complète : chaque terminaison appelant par client, avec une requête et une réponse exemple.
Pour qui c’est ?
Si vous développez une application Blazor et consommez le framework via
IPowerPortalsProService, vous n’avez pas besoin d’appeler ces points de terminaison directement — c’est l’implémentation client qui s’en charge pour vous. Cette page documente la surface pour les équipes qui écrivent un SPA non-Blazor, ou qui câblent un client HTTP personnalisé, sur le même serveur.
Activation des points de terminaison
UsePowerPortalsProWebServer câble les points de terminaison des données (CRUD de table, FetchXML, métadonnées, fichiers, localisation, administration). MapAuthEndpoints<TUser> câble la surface d’authentification orientée SPA — appelez-la explicitement si votre hôte a besoin d’authentification par cookies d’un SPA.
// Program.cs — server pipeline
app.UsePowerPortalsProWebServer();
app.MapAuthEndpoints<PortalUser>();
Les deux appels sont no-ops lorsque leur fonction n’est pas utilisée, donc connectez-les une Program.cs fois, quel que soit le mode d’interactivité utilisé par l’hôte.
Le type des Routes — source unique de vérité
PowerPortalsPro.Web.Common.Routes expose chaque chemin de terminaison comme une propriété fortement typée. Le client dans la boîte et tout SPA externe basé sur C# devraient référencer ces constantes au lieu d’écrire des chaînes à la main, de sorte qu’un renommage côté serveur apparaît comme une erreur de compilation plutôt qu’un 404 à l’exécution. Les clients JavaScript/TypeScript devront bien sûr intégrer les chemins, mais le côté C# reste Routes la référence canonique pour ces chemins.
// N’importe où — à la fois PowerPortalsPro.Web.Client et des SPA C# externes.
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 couvre les points de terminaison de données et d’administration ; Routes.Api.Auth couvre la connexion / inscription ; Routes.Api.Auth.Manage couvre les opérations de gestion de compte de l’utilisateur connecté.
Modèle d’authentification — cookies, pas tokens
L’authentification est basée sur les cookies du navigateur. Il n’y a pas d’étape d’émission JWT. Un SPA appelle /api/auth/login, le serveur définit le .AspNetCore.Identity.Application cookie sur la réponse, et le navigateur l’attache à chaque requête suivante — y compris les appels de données sous /api/table/*. Avec JavaScript, cela signifie fetch(..., { credentials: 'include' }) qu’à chaque appel (ou axios.defaults.withCredentials = true) ; sans cela, le cookie est abandonné et le serveur retourne 401. Chaque exemple ci-dessous l’inclut.
// Réaction / Vue / simple récupération — identifiants : « inclure » est requis donc le
// Le navigateur envoie et stocke le cookie d’authentification émis par /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 à origine croisée
Si le SPA et le serveur sont sur des origines différentes, le serveur doit envoyer
Access-Control-Allow-Origin: <spa-origin>(non*) etAccess-Control-Allow-Credentials: true, et le SPA doit utilisercredentials: 'include'. L’hébergement de même origine (le SPA servi depuis le même site que l’API) évite complètement ce problème.
La forme du record
Chaque extrémité de données qui lit ou écrit une ligne échange la même TableRecord enveloppe. properties est une carte du nom logique de la colonne → un objet valeur typé dont $type le discriminateur identifie le type de colonne. permissions est un masque de drapeau de bits (Read 1, Create 2, Write 4, Delete 8, Append 16, AppendTo 32) décrivant ce que l’utilisateur actuel peut faire avec la ligne. Pour les écrits, il suffit d’envoyer les colonnes que vous changez.
{
"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 }
}
Les $type valeurs reflètent le type d’attribut Dataverse : 0 Booléen, 2 DateTime, 3 Décimal, 4 Double, 5 Entier, 6 Recherche, 8 Argent, 11 Choix, 14 Chaîne, 15 Identifiant unique, 40 MultiSelectChoice, 41 Fichier, 42 Image. Les recherches ajoutent name + tableName; nombre, monnaie et date portent un formattedValueDataverse formaté .
Réponses à l’erreur
Les appels ratés retournent le RFC 9457 application/problem+json. L’implémentation client dans la boîte d’entrée réhydrate le type d’exception CLR original à partir des détails du problème, de sorte que côté serveur projette la surface comme la même exception sur le client ; pour non-.NET SPA, les type champs / title / detail status / sont le handle standard.
{
"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."
}
Référence de point d’arrivée
Chaque point de terminaison ci-dessous montre la méthode et le chemin, ce qu’il fait, une requête d’exemple (en tant qu’appel de navigateur fetch ) et une réponse d’exemple. Les segments de chemin dans {braces} sont des substituts. Sauf indication contraire, une réponse 2xx est le cas de réussite et les échecs reviennent comme problem+json.
Records & CRUD
Création, lecture, mise à jour et suppression d’un seul enregistrement, ainsi que des requêtes FetchXML arbitraires et des lots transactionnels. Soutenu par UsePowerPortalsProWebServer; chaque lecture et écriture s’applique aux consommateurs ITablePermissionHandler / ITableRecordPermissionHandler intercepteurs et à tout enregistrement IFetchXmlBuilderInterceptor.
POST /api/table/{tableLogicalName}
Crée une nouvelle ligne dans la table nommée. Envoyez un TableRecord contenant les colonnes à set ; la réponse renvoie l’id du nouvel enregistrement.
Demande
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();
Réception
{
"responseName": "CreateResponse",
"outputParameters": {},
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
GET /api/table/{tableLogicalName}/{recordId}
Affiche une seule ligne par identifiant. Le paramètre de requête optionnel ?columns= (noms logiques séparés par virgules) réduit la projection — il est omés pour récupérer l’ensemble de colonnes par défaut de la table.
Demande
const record = await fetch(
'/api/table/account/a1b2c3d4-e5f6-7890-abcd-ef1234567890?columns=name,revenue',
{ credentials: 'include' }
).then(r => r.json());
Réception
{
"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}
Met à jour une ligne existante. Seules les colonnes présentes dans properties sont écrites, donc envoyez uniquement les valeurs modifiées.
Demande
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 } }
})
});
Réception
{ "responseName": "UpdateResponse", "outputParameters": {} }
DELETE /api/table/{tableLogicalName}/{recordId}
Supprime la ligne par id.
Demande
await fetch('/api/table/account/a1b2c3d4-e5f6-7890-abcd-ef1234567890', {
method: 'DELETE',
credentials: 'include'
});
Réception
{ "responseName": "DeleteResponse", "outputParameters": {} }
GET /api/retrieveMultiple?fetchXml=…
Exécute une requête FetchXML arbitraire et retourne les lignes correspondantes ainsi que les informations de pagination. Encodez le FetchXML dans la fetchXml chaîne de requête — depuis C#, cela Routes.Api.GetRetrieveMultipleRoute(fetchXml) fait pour vous.
Demande
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());
Réception
{
"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
Exécute un lot de requêtes hétérogènes (Créer / Mettre à jour / Supprimer / Associer / Dissocier) dans une seule transaction de base de données — si l’une d’elles échoue, tout le lot revient en arrière. Le corps est un tableau JSON d’objets OrganizationRequest discriminés par $type. Passez ?returnResponses=false pour éviter de constituer la liste de réponses par demande.
Demande
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' } }
])
});
Réception
[
{ "$type": "CreateResponse", "responseName": "CreateResponse", "outputParameters": {}, "id": "new-guid" },
{ "$type": "DeleteResponse", "responseName": "DeleteResponse", "outputParameters": {} }
]
Grilles & graphiques
Points de terminaison de requête composés par le serveur. Plutôt que de laisser le client construire FetchXML, vous passez un identifiant de vue (ou votre propre FetchXML) plus recherche / tri / paginage, et le serveur résout les colonnes, applique les permissions et exécute la requête — le même chemin à source unique de vérité utilisé par MainGrid et les composants du graphique.
POST /api/grids/data
Charge une page de données de grille. Fournissez soit un filtre a viewId , soit votre propre fetchXml, ainsi que des filtres optionnels searchText, sortsde pagination et de colonnes. La réponse contient les lignes, les définitions de colonnes résolues et le masque de permission de table de l’appelant.
Demande
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());
Réception
{
"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
Les charges agrégèrent les données graphiques pour les composants de la cartographie. Accepte une configuration agrégée, un viewId, ou brut fetchXml, plus le mappage de colonnes étiquette / valeur / série ; retourne des étiquettes et jeux de données de style Chart.js.
Demande
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());
Réception
{
"data": {
"labels": ["Active", "Inactive"],
"datasets": [ { "label": "Accounts by Status", "data": [ { "value": 25 }, { "value": 8 } ] } ]
}
}
Métadonnées et permissions
Recherches en lecture seule pour les métadonnées de table et de vue, le masque d’autorisation de l’utilisateur actuel et les paramètres à l’échelle de l’organisation. Tous sont mis en cache côté serveur, donc les appels répétés sont peu coûteux.
GET /api/tableMetadata/{tableLogicalName}
Retourne les métadonnées d’une table — colonnes (avec types, étiquettes et contraintes), colonnes d’identifiant / nom / image primaire, et relations.
Demande
const meta = await fetch('/api/tableMetadata/account', { credentials: 'include' })
.then(r => r.json());
Réception
{
"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}
Retourne le masque combiné TableSecurityPermission de l’utilisateur actuel pour la table sous forme d’entier unique (drapeaux de bit : Lecture 1, Création 2, Écriture 4, Suppression 8, Annexe 16, Annexe 32).
Demande
const mask = await fetch('/api/permissions/table/account', { credentials: 'include' })
.then(r => r.json());
// 15 === Read(1) | Créer(2) | Écrire(4) | Supprimer(8)
Réception
15
GET /api/viewMetadata/{viewId}
Retourne les métadonnées d’une vue sauvegardée via son GUID — son FetchXML, ses colonnes de mise en page et ses drapeaux de vue. L’itinéraire nécessite un GUID, ce qui explique comment il est dissocié de l’itinéraire tout-vue ci-dessous.
Demande
const view = await fetch('/api/viewMetadata/00000000-0000-0000-0000-000000000001',
{ credentials: 'include' }).then(r => r.json());
Réception
{
"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}
Ça renvoie toutes les vues sauvegardées pour une table. Partage le /api/viewMetadata/ préfixe avec la route by-id — un segment non-GUID (un nom logique de table) arrive ici.
Demande
const views = await fetch('/api/viewMetadata/account', { credentials: 'include' })
.then(r => r.json());
Réception
[
{ "id": "0000…0001", "name": "All Accounts", "isDefault": true, "tableName": "account" },
{ "id": "0000…0002", "name": "Active Accounts", "isDefault": false, "tableName": "account" }
]
GET /api/organizationSettings
Retourne les paramètres à l’échelle de l’organisation provenant de l’enregistrement de l’organisation Dataverse : la monnaie par défaut, la liste des extensions de fichiers bloquées, et la taille maximale de téléversement en octets.
Demande
const settings = await fetch('/api/organizationSettings', { credentials: 'include' })
.then(r => r.json());
Réception
{
"defaultCurrency": { "isoCode": "USD", "symbol": "$", "precision": 2 },
"blockedFileExtensions": ["exe", "bat", "js"],
"maxUploadFileSizeInBytes": 10485760
}
Dossiers
Lisez le contenu des fichiers et des colonnes d’images. Les charges utiles binaires sont renvoyées encodées en base64 à l’intérieur de JSON ; Le drapeau includeData vous permet de récupérer uniquement des métadonnées (par exemple pour afficher une liste de téléchargement) sans transférer d’octets.
GET /api/files/{tableLogicalName}/{recordId}/{columnName}?includeData=…
Retourne les métadonnées du fichier pour une colonne de fichier / image sur un enregistrement. Avec ?includeData=true le contenu base64 est intégré ; seuls false le nom et la taille reviennent.
Demande
const file = await fetch(
'/api/files/account/a1b2c3d4-…/new_attachment?includeData=true',
{ credentials: 'include' }
).then(r => r.json());
Réception
{
"fileName": "contract.pdf",
"fileSizeInBytes": 245678,
"fileData": "JVBERi0xLjQKJeLjz9M…"
}
POST /api/files/{tableLogicalName}/{columnName}/batch?includeData=…
Récupère les métadonnées des fichiers (et éventuellement le contenu) pour de nombreux enregistrements de la même table / colonne en un aller-retour — le corps est un tableau JSON de GUIDs d’enregistrements. Utilisé par le téléchargement sélectionné par le FileGrid pour que le client ne déclenche pas N appels séparés.
Demande
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());
Réception
[
{ "fileName": "contract.pdf", "fileSizeInBytes": 245678, "fileData": null },
{ "fileName": "invoice.docx", "fileSizeInBytes": 89456, "fileData": null }
]
POST /api/files/createFileArchive
Compacte les valeurs choisies dans les colonnes de fichiers pour un ensemble d’enregistrements côté serveur. Retourne le flux brut application/zip par défaut, ou — avec responseFormat: 1 — une enveloppe JSON contenant l’archive base64. POST (pas GET) pour que les grandes listes d’identifiants n’atteignent pas les limites de longueur des URL.
Demande
// Par défaut : application brute/flux zip. Pass responseFormat : 1 pour une enveloppe 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());
Réception
// responseFormat : 1 (Json) au lieu du flux binaire par défaut
{
"fileName": "account-files.zip",
"contentType": "application/zip",
"data": "UEsDBBQAAAAIAP2t…"
}
Fibrés de localisation
Cordes localisées pour front-ends non-Blazor. La /api/localizedStrings route renvoie l’arbre complet pour une culture ; les /localizations/* routes servent des bundles immuables en cache imprimés (par défaut, par table, par vue) pour un chargement incrémental efficace — ils sont publics et ne nécessitent pas le cookie d’authentification.
GET /api/localizedStrings/{culture}
Retourne l’arbre complet de chaînes localisées pour une culture en tant qu’objet imbriqué — chaînes de framework, overrides d’applications, étiquettes de table et de choix.
Demande
const strings = await fetch('/api/localizedStrings/fr-fr', { credentials: 'include' })
.then(r => r.json());
Réception
{
"app": { "navigation": { "home": "Accueil" } },
"tables": { "account": { "columns": { "name": { "label": "Nom du compte" } } } }
}
GET /localizations/version
Retourne le manifeste de localisation — juste la liste des lieux pris en charge. Servi sans cache pour qu’une nouvelle version soit détectée au chargement de la page suivante. Public.
Demande
const manifest = await fetch('/localizations/version').then(r => r.json());
Réception
{ "supportedLocales": ["en-us", "fr-fr", "de-de"] }
GET /localizations/{locale}/thumbprints
Retourne les empreintes digitales de contenu pour un lieu : le bundle par défaut plus chaque table et vue chargées. Les clients les récuplent, puis ne demandent que les bundles dont l’empreinte digitale a changé. Public.
Demande
const thumbs = await fetch('/localizations/fr-fr/thumbprints').then(r => r.json());
Réception
{
"bundle": "a3f5e8c2d",
"tables": { "account": "b7f2d9e1a", "contact": "c4f8a1b3e" },
"views": { "550e8400e29b41d4a716446655440000": "e2f4b8d1c" }
}
GET /localizations/default/{filename} · /tables/{tableName}/{filename} · /views/{viewId}/{filename}
Les trois familles de fibrés — par défaut (chaînes de découpe transversale), par table (chaînes d’une table plus les choix globaux référencés par ses colonnes) et par vue. Le nom du fichier est {locale}.{thumbprint}.json et each est servi public, immutable, max-age=31536000, donc une empreinte digitale stable est un résultat garanti dans le cache. Public.
Demande
// Le nom du fichier est '{locale}. {empreinte digitale}.json', servie immuable + cache-indéfiniment.
const bundle = await fetch('/localizations/tables/account/fr-fr.b7f2d9e1a.json')
.then(r => r.json());
Réception
{
"tables": { "account": { "label": "Compte", "columns": { "name": { "label": "Nom du compte" } } } },
"choices": { "account_industrycode": { "1": "Fabrication", "2": "Services" } }
}
Culture
Ça change la culture active du navigateur en écrivant le cookie de culture.
GET /Culture/{culture}?redirectUri=…
Définit le cookie de culture et redirige en 302 vers redirectUri. Naviguez vers elle (chargement complet de la page) au lieu de le récupérer, pour que la Set-Cookie redirection et prenne effet. Public.
Demande
// Navigation pleine page pour que le Set-Cookie + la redirection soient respectés.
window.location.href = '/Culture/fr-fr?redirectUri=' + encodeURIComponent('/dashboard');
Réception
// 302 Trouvé → Emplacement : /dashboard
// Set-Cookie : . AspNetCore.Culture=c=fr-fr|uic=fr-fr
Admin — gestion du cache
Inspection et invalidation du cache serveur. Les trois points de terminaison sont verrouillés — [Authorize(Roles = "SystemAdmin")] seul un administrateur connecté peut les appeler.
GET /api/caches
Liste les noms de chaque cache enregistré côté serveur. Nécessite le rôle d’administrateur système.
Demande
const names = await fetch('/api/caches', { credentials: 'include' }).then(r => r.json());
Réception
["TableMetadataCache", "ViewMetadataCache", "CurrencyCache"]
POST /api/caches/clear
Efface tous les caches côté serveur et renvoie un résultat par cache (s’il a réussi et combien de temps cela a pris). Nécessite le rôle d’administrateur système.
Demande
const results = await fetch('/api/caches/clear', { method: 'POST', credentials: 'include' })
.then(r => r.json());
Réception
[
{ "name": "TableMetadataCache", "succeeded": true, "error": null, "elapsedMs": 45 },
{ "name": "ViewMetadataCache", "succeeded": false, "error": "Timed out", "elapsedMs": 5000 }
]
POST /api/caches/{cacheName}/clear
Efface un seul cache nommé (le nom vient du point d’extrémité liste). Retourne 404 si aucun cache portant ce nom n’est enregistré. Nécessite le rôle d’administrateur système.
Demande
const result = await fetch('/api/caches/TableMetadataCache/clear',
{ method: 'POST', credentials: 'include' }).then(r => r.json());
Réception
{ "name": "TableMetadataCache", "succeeded": true, "error": null, "elapsedMs": 132 }
Authentification
Connexion, inscription et cycle de vie du compte, soutenu par MapAuthEndpoints<TUser>. Basé sur les cookies : une connexion réussie active le cookie de l’application ASP.NET Core Identity, et chaque appel ultérieur s’authentifie en le représentant. La plupart renvoient un result enum en HTTP 200 plutôt que de signaler le résultat via le code de statut.
POST /api/auth/login
Se connecte avec un email + mot de passe. L’enum result distingue le succès, un second facteur requis, de mauvaises références, un e-mail non confirmé et un verrouillage. En cas de succès, le cookie d’authentification est fixé sur la réponse.
Demande
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());
// résultat : 0 Succès · 1 RequiereDeuxFacteurs · 2 InvalidCredentials · 3 EmailNotConfirmed · 4 Verrouillé
Réception
{ "result": 0 }
POST /api/auth/login/2fa
Complète une connexion renvoyée RequiresTwoFactor en soumettant le code d’authentification (ou de récupération). rememberMachine Définit le cookie de navigateur de confiance afin que les connexions futures sur ce navigateur sautent le second facteur.
Demande
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());
Réception
{ "success": true }
POST /api/auth/logout
Règle le cookie d’authent, mettant fin à la séance.
Demande
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
Réception
// 200 OK — pas de corps. Le cookie d’authentification est annulé à la réponse.
POST /api/auth/register
Crée un nouveau compte local. Selon la configuration, le résultat est soit un email de confirmation envoyé, une connexion immédiate, ou un conflit avec un email existant. Les échecs de faibles mots de passe et autres validations reviennent à 400 problem+json.
Demande
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());
// résultat : 0 ConfirmationEmailEnvoyé · 1 Signé · 2 EmailAlInUse
Réception
{ "result": 0 }
POST /api/auth/forgot-password
Commence une réinitialisation du mot de passe en envoyant un lien de réinitialisation par e-mail. Il retourne toujours 200 sans que l’adresse existe ou non, donc il ne peut pas servir à sonder les emails recommandés.
Demande
await fetch('/api/auth/forgot-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'user@example.com' })
});
Réception
// 200 OK — pas de corps, que l’adresse existe ou non (énumération sûre).
POST /api/auth/reset-password
Effectue une réinitialisation en utilisant le jeton de l’email plus le nouveau mot de passe. result distingue le succès, un jeton invalide / expiré, et un mot de passe rejeté (avec les messages de validation dans errors).
Demande
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());
// résultat : 0 Succès · 1 InvalidOrExpiredToken · 2 Mot de passe invalide
Réception
{ "result": 0, "errors": [] }
POST /api/auth/confirm-email
Confirme un nouvel e-mail enregistré en utilisant l’identifiant utilisateur et le jeton du lien de confirmation.
Demande
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());
Réception
{ "success": true }
POST /api/auth/resend-email-confirmation
Envoie à nouveau le lien de confirmation par e-mail. Comme pour le mot de passe oublié, il retourne toujours 200 sans corps pour éviter de divulguer quelles adresses sont enregistrées.
Demande
await fetch('/api/auth/resend-email-confirmation', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'user@example.com' })
});
Réception
// 200 OK — pas de corps (sans énumération).
GET /api/auth/options
components.PowerPortalsPro.Demo.Client.Customizations.Pages.ClientApi.ClientApiPage.ep-auth-options-desc
Demande
const options = await fetch('/api/auth/options').then(r => r.json());
Réception
{
"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=…
Cela lance le flux OAuth pour un prestataire. Il renvoie un défi 302 au fournisseur, donc naviguez dans le navigateur vers celui-ci (ne pas récupérer) — un SPA devrait définir window.location.href.
Demande
// Navigation pleine page — NE PAS chercher — pour que le navigateur suive la chaîne OAuth 302.
window.location.href =
'/api/auth/external-login?provider=Microsoft&returnUrl=' + encodeURIComponent('/dashboard');
Réception
// 302 trouvé → la page de connexion du fournisseur externe (plus les cookies de corrélation).
GET /api/auth/external-login/pending
Après le rappel OAuth, il capture les instantanés de la connexion externe en vol : le fournisseur, les revendications de l’identité, et — lorsque l’email correspond à plusieurs identifiants de portail — la liste des candidats parmi lesquels choisir. Retour 204 quand il n’y a pas de connexion en attente.
Demande
const res = await fetch('/api/auth/external-login/pending', { credentials: 'include' });
const pending = res.status === 204 ? null : await res.json();
Réception
{
"loginProvider": "Microsoft",
"providerDisplayName": "Microsoft",
"identityEmail": "user@company.com",
"requiresChoice": false,
"candidates": []
}
POST /api/auth/external-login/confirm
Complète une première connexion externe pour un nouveau compte en confirmant l’e-mail à l’associé. Cela se résout en une connexion, un email de confirmation, sans attente ou échec.
Demande
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());
// résultat : 0 Signé · 1 ConfirmationEmailEnvoyé · 2 NoPendingExternalLogin · 3 Échec
Réception
{ "result": 0, "errors": [] }
POST /api/auth/external-login/select
Complète une connexion externe lorsque l’identité correspond à plusieurs identités de portail, en choisissant celle (Contact ou SystemUser) sous laquelle se connecter.
Demande
const { result } = await fetch('/api/auth/external-login/select', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ kind: 1 }) // 0 Contact · 1 SystemUser
}).then(r => r.json());
Réception
{ "result": 0, "errors": [] }
GET /api/auth/me
Instantané du principal actuel — identifiant, nom, email, rôles et la table de soutien (contact vs. systemuser), plus toute identité de frère ou sœur alternative. Retourne une forme anonyme (isAuthenticated: false) plutôt que 401 lorsqu’aucun cookie n’est présent, donc un SPA peut l’appeler dès la première peinture sans se brancher sur le code de statut.
Demande
const me = await fetch('/api/auth/me', { credentials: 'include' }).then(r => r.json());
Réception
{
"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
Échange le cookie actuel contre l’identité de frère alternatif de l’utilisateur (l’appariement Contact↔SystemUser est apparu sur /api/auth/me). Le corps JSON est un objet vide.
Demande
const { result } = await fetch('/api/auth/switch-identity', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: '{}'
}).then(r => r.json());
// résultat : 0 Commuté · 1 NoAltIdentity · 2 AltIdidentitéPasTrouvée · 3 NonAuthentifié
Réception
{ "result": 0 }
Gestion des comptes
Les opérations en libre-service de l’utilisateur connecté sous /api/auth/manage/* — profil, mot de passe, email, connexion à deux facteurs, connexions externes liées, et données personnelles. Tous nécessitent une session authentifiée et reflètent les pages classiques /Account/Manage de Razor du framework.
GET /api/auth/manage/profile
Retourne le profil de l’utilisateur (nom, mobile, email) lu depuis le contact Dataverse lié, ainsi que les indicateurs d’état d’identité (email confirmé, mot de passe, 2FA, en lecture seule).
Demande
const profile = await fetch('/api/auth/manage/profile', { credentials: 'include' })
.then(r => r.json());
Réception
{
"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
Met à jour le prénom/nom de famille et le téléphone portable sur le contact lié. Retour 200 en cas de succès ; Les identités soutenues par SystemUser sont en lecture seule et reçoivent 403.
Demande
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' })
});
Réception
// 200 OK — pas de corps. (403 pour les identités en lecture seule soutenues par SystemUser.)
POST /api/auth/manage/password/set
Ajoute un mot de passe local à un compte qui n’en a pas (par exemple un compte uniquement externe). Les messages de validation, s’ils existent, reviennent dans errors.
Demande
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());
Réception
{ "success": true, "errors": [] }
POST /api/auth/manage/password/change
Modifie le mot de passe local ; nécessite le mot de passe actuel. result Distingue le succès, un mot de passe actuel erroné et un nouveau mot de passe rejeté.
Demande
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());
// résultat : 0 Succès · 1 IncorrectOldPassword · 2 Mot de passe invalide
Réception
{ "result": 0, "errors": [] }
POST /api/auth/manage/email/change
Commence un changement d’e-mail en envoyant un lien de confirmation à la nouvelle adresse. Le changement ne prend effet qu’une fois ce lien respecté.
Demande
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());
// résultat : 0 ConfirmationEmailEnvoyé · 1 E-mail IdentiqueQueCurrent
Réception
{ "result": 0 }
POST /api/auth/manage/email/send-confirmation
Envoie à nouveau le lien de confirmation de l’adresse e-mail actuelle de l’utilisateur. sent est faux lorsque l’e-mail est déjà confirmé.
Demande
const { sent } = await fetch('/api/auth/manage/email/send-confirmation',
{ method: 'POST', credentials: 'include' }).then(r => r.json());
Réception
{ "sent": true }
GET /api/auth/manage/2fa
Retourne l’état de la 2FA — si un authentificateur est inscrit, si la 2FA est activée, si ce navigateur est mémorisé, et combien de codes de récupération restent.
Demande
const status = await fetch('/api/auth/manage/2fa', { credentials: 'include' })
.then(r => r.json());
Réception
{ "hasAuthenticator": true, "is2faEnabled": true, "isMachineRemembered": false, "recoveryCodesLeft": 8 }
GET /api/auth/manage/authenticator/setup
Retourne la clé partagée et otpauth:// l’URI pour l’écran d’inscription au code QR. Appairez-le avec le point de terminaison de vérification pour finir d’activer la 2FA.
Demande
const setup = await fetch('/api/auth/manage/authenticator/setup', { credentials: 'include' })
.then(r => r.json());
Réception
{
"sharedKey": "abcd efgh ijkl mnop",
"authenticatorUri": "otpauth://totp/PowerPortalsPro:user@example.com?secret=ABCD…&issuer=PowerPortalsPro"
}
POST /api/auth/manage/authenticator/verify
Vérifie un code depuis l’application d’authentification et active la 2FA. Lors de la première inscription, la réponse retourne également l’ensemble initial de codes de récupération.
Demande
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());
Réception
{ "success": true, "recoveryCodes": ["ABC123DEF456", "GHI789JKL012", "…"] }
POST /api/auth/manage/authenticator/reset
Fait pivoter la clé d’authentification. Cela désactive également la 2FA, donc l’utilisateur doit se réinscrire.
Demande
const { success } = await fetch('/api/auth/manage/authenticator/reset',
{ method: 'POST', credentials: 'include' }).then(r => r.json());
Réception
{ "success": true }
POST /api/auth/manage/2fa/disable
Ça désactive le 2FA pour le compte.
Demande
const { success } = await fetch('/api/auth/manage/2fa/disable',
{ method: 'POST', credentials: 'include' }).then(r => r.json());
Réception
{ "success": true }
POST /api/auth/manage/2fa/recovery-codes/generate
Régénère les codes de récupération, remplaçant tout ensemble existant, et renvoie les nouveaux codes.
Demande
const { recoveryCodes } = await fetch('/api/auth/manage/2fa/recovery-codes/generate',
{ method: 'POST', credentials: 'include' }).then(r => r.json());
Réception
{ "recoveryCodes": ["ABC123DEF456", "GHI789JKL012", "…"] }
POST /api/auth/manage/2fa/forget-browser
Efface le cookie de l’appareil de confiance de ce navigateur, donc la 2FA sera de nouveau nécessaire lors de la prochaine connexion ici.
Demande
const { success } = await fetch('/api/auth/manage/2fa/forget-browser',
{ method: 'POST', credentials: 'include' }).then(r => r.json());
Réception
{ "success": true }
GET /api/auth/manage/external-logins
Liste les identifiants externes actuellement liés au compte.
Demande
const logins = await fetch('/api/auth/manage/external-logins', { credentials: 'include' })
.then(r => r.json());
Réception
{
"currentLogins": [
{ "loginProvider": "Microsoft", "providerKey": "oid-…", "providerDisplayName": "Microsoft" }
]
}
GET /api/auth/manage/login-info
Une vue combinée des chemins de connexion de l’utilisateur — connexions externes liées plus la présence d’un mot de passe local — permet de décider si supprimer une connexion bloquerait l’accès à l’utilisateur.
Demande
const info = await fetch('/api/auth/manage/login-info', { credentials: 'include' })
.then(r => r.json());
Réception
{
"externalLogins": [ { "loginProvider": "Microsoft", "providerKey": "oid-…", "providerDisplayName": "Microsoft" } ],
"hasLocalPassword": true
}
GET /api/auth/manage/external-logins/link?provider=…
Démarre le flux OAuth qui relie un fournisseur supplémentaire au compte connecté. Un défi 302 revient, donc navigue vers là plutôt que de le chercher.
Demande
// Navigation pleine page ; Le rappel ci-dessous ajoute la connexion et redirige vers elle.
window.location.href =
'/api/auth/manage/external-logins/link?provider=Google&returnUrl=' + encodeURIComponent('/account');
Réception
// 302 trouvé → la page de consentement du prestataire externe.
GET /api/auth/manage/external-logins/link/callback
Le rappel OAuth pour le flux de liaison. Le fournisseur redirige le navigateur ici ; Le serveur attache la connexion et redirige 302 vers le returnUrl fournisseur initial. On n’appelle pas ça directement.
Demande
// Touché par la redirection du fournisseur OAuth — pas directement appelé par votre code.
Réception
// 302 trouvé → l’Url returnUrl fournie au point d’arrivée du lien, avec la connexion désormais attachée.
POST /api/auth/manage/external-logins/remove
Débranche un identifiant externe par fournisseur + clé de fournisseur.
Demande
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());
Réception
{ "success": true }
GET /api/auth/manage/personal-data
Exporte les données personnelles de l’utilisateur — chaque [PersonalData] propriété ainsi que ses identifiants externes liés — pour un téléchargement à la manière du RGPD.
Demande
const data = await fetch('/api/auth/manage/personal-data', { credentials: 'include' })
.then(r => r.json());
Réception
{
"personalData": { "Id": "550e8400-…", "Email": "john.doe@example.com" },
"externalLogins": { "Microsoft": "oid-…" }
}
POST /api/auth/manage/personal-data/delete
Supprime définitivement le compte de l’utilisateur et le déconnecte. Nécessite le mot de passe actuel lorsque le compte en a un ; Passez null pour des comptes externes uniquement. result Distingue le succès, un mauvais mot de passe et le cas où un mot de passe est requis mais n’a pas été fourni.
Demande
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 pour les comptes externes uniquement
}).then(r => r.json());
// résultat : 0 Succès · 1 Mot de passe incorrect · 2 NécessiteLocalPassword
Réception
{ "result": 0 }
Voir aussi
Documentation associée :
IPowerPortalsProService — le wrapper C# autour de ces terminaux — ce que vos composants Blazor injectent lorsqu’ils n’ont pas besoin d’émettre eux-mêmes de HTTP brut.Connexion SystemUser — Contexte sur les rapportstableName: "systemuser"WHY/api/auth/mepour certains principaux ettableName: "contact"pour d’autres.
