Client-API
PowerPortalsPro veröffentlicht eine JSON-over-HTTP-Oberfläche, unter /api/* der jede Single-Page-Anwendung aufrufen kann – der Inbox-Blazor WebAssembly-Client nutzt sie, um zu unterstützen IPowerPortalsProService und IAuthService, aber dieselben Endpunkte sind gleichermaßen von einem React-, Vue- oder Vanilla-JS-Frontend aus nutzbar, das neben dem Server gehostet wird (oder sogar cross-origin von) dem Server gehostet wird. Diese Seite ist die vollständige Referenz: jeder klient-aufrufbare Endpunkt mit einer Beispielanfrage und -antwort.
Für wen ist das
Wenn Sie eine Blazor-App bauen und das Framework über
IPowerPortalsProServicenutzen, müssen Sie diese Endpunkte nicht direkt aufrufen – die Client-Implementierung erledigt das für Sie. Diese Seite dokumentiert das Surface für Teams, die ein Nicht-Blazor-SPA schreiben oder einen benutzerdefinierten HTTP-Client am selben Server verkabeln.
Aktivierung der Endpunkte
UsePowerPortalsProWebServer verdrahtet die Datenendpunkte (Table CRUD, FetchXML, Metadaten, Dateien, Lokalisierung, Admin). MapAuthEndpoints<TUser> verdrahtet die SPA-gegenüberliegende Authentifizierungsoberfläche – nennen Sie sie ausdrücklich, wenn Ihr Host eine Cookie-Auth von einem SPA benötigt.
// Program.cs — Server-Pipeline
app.UsePowerPortalsProWebServer();
app.MapAuthEndpoints<PortalUser>();
Beide Anrufe sind No-Ops, wenn ihre Funktion nicht genutzt wird, also verdrahte sie einmal Program.cs , unabhängig davon, welchen Interaktivitätsmodus der Host verwendet.
Der Routentyp – eine einzige Quelle der Wahrheit
PowerPortalsPro.Web.Common.Routes stellt jeden Endpunktpfad als stark typisierte Eigenschaft frei. Sowohl der Inbox-Client als auch jede externe C#-basierte SPA sollten diese Konstanten anstelle von handgeschriebenen Strings referenzieren, sodass eine serverseitige Umbenennung als Kompilierungsfehler und nicht als Laufzeit-404 auftaucht. JavaScript/TypeScript-Clients müssen natürlich die Pfade inlinen, aber die C#-Seite bleibt Routes die kanonische Referenz dafür, was diese Pfade sind.
// Überall – sowohl PowerPortalsPro.Web.Client als auch externe C# SPAs.
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 deckt die Daten- und Admin-Endpunkte ab; Routes.Api.Auth Covers Anmelde- und Anmeldekurs; Routes.Api.Auth.Manage behandelt die Kontoverwaltungsoperationen des angemeldeten Benutzers.
Authentifizierungsmodell – Cookies, keine Token
Die Auth basiert auf Browser-Cookies. Es gibt keinen JWT-Ausgabeschritt. Ein SPA-Aufruf /api/auth/login, der Server setzt das .AspNetCore.Identity.Application Cookie auf die Antwort, und der Browser hängt es bei jeder nachfolgenden Anfrage an – einschließlich Datenaufrufe unter /api/table/*. Aus JavaScript bedeutet fetch(..., { credentials: 'include' }) das, dass bei jedem Aufruf (oder axios.defaults.withCredentials = true); ohne JavaScript wird das Cookie gelöscht und der Server gibt 401 zurück. Jedes untenstehende Beispiel enthält das.
// React / Vue / Plain Fetch — Zugangsdaten: 'Include' ist erforderlich, damit die
// Der Browser sendet und speichert das von /api/auth/login ausgegebene Authentifizierungscookie.
const me = await fetch('/api/auth/me', { credentials: 'include' })
.then(r => r.json());
if (me.isAuthenticated) {
console.log(me.userName, me.roles);
}
Cross-origin SPAs
Wenn SPA und Server an unterschiedlichen Ursprüngen liegen, muss
Access-Control-Allow-Origin: <spa-origin>der Server (nicht*) undAccess-Control-Allow-Credentials: true, senden, und der SPA musscredentials: 'include'verwenden. Same-Origin-Hosting (die SPA wird vom selben Standort wie die API bereitgestellt) vermeidet das Problem vollständig.
Die Form der Platte
Jeder Datenendpunkt, der eine Zeile liest oder schreibt, tauscht denselben TableRecord Umschlag aus. properties ist eine Karte des logischen Spaltennamens → eines typisierten Wertobjekts, dessen $type Diskriminator den Spaltentyp identifiziert. permissions ist eine Bit-Flag-Maske (Read 1, Create 2, Write 4, Delete 8, Apend 16, AppendTo 32), die beschreibt, was der aktuelle Benutzer mit der Zeile machen kann. Beim Schreiben musst du nur die Kolumnen abschicken, die du änderst.
{
"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 }
}
Die Werte $type spiegeln die Art des Dataverse-Attributs wider: 0 Boolean, 2 DateTime, 3 Decimal, 4 Double, 5 Integer, 6 Lookup, 8 Money, 11 Choice, 14 String, 15 Uniqueidentifier, 40 MultiSelectChoice, 41 File, 42 Image. Nachschlagefunktionen addieren name + tableName; Zahl, Geld und Datumswerte tragen ein Dataverse-formatiertes formattedValue.
Fehlerantworten
Fehlgeschlagene Anrufe geben RFC 9457 application/problem+jsonzurück. Die In-Box-Client-Implementierung rehydratisiert den ursprünglichen CLR-Ausnahmetyp aus den Problemdetails, sodass serverseitige Oberfläche als dieselbe Ausnahme auf dem Client geworfen wird; für non-.NET SPAs sind die type / title / detail / Felder status der Standardgriff.
{
"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."
}
Endpunktreferenz
Jeder folgende Endpunkt zeigt die Methode und den Pfad, was sie bewirkt, eine Beispielanforderung (als Browseraufruf fetch ) und eine Beispielantwort. Pfadsegmente in {braces} sind Platzhalter. Sofern nicht anders angegeben, ist eine 2xx-Antwort der Erfolgsfall und Fehler werden als problem+json.
Aufzeichnungen & CRUD
Einzeldatensatz-Erstellen, Lesen, Aktualisieren und Löschen sowie beliebige FetchXML-Abfragen und transaktionale Chargen. Unterstützt von UsePowerPortalsProWebServer; jedes Lesen und Schreiben gilt für die Verbraucher / ITableRecordPermissionHandler Abfanggeräte ITablePermissionHandler und alle registrierten IFetchXmlBuilderInterceptor.
POST /api/table/{tableLogicalName}
Erstellt eine neue Zeile in der benannten Tabelle. Senden Sie eine TableRecord mit den Kolonnen zum Set; die Antwort gibt die ID des neuen Datensatzes zurück.
Anfrage
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();
Reaktion
{
"responseName": "CreateResponse",
"outputParameters": {},
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
GET /api/table/{tableLogicalName}/{recordId}
Liest eine einzelne Zeile nach id. Der optionale ?columns= Abfrageparameter (kommagetrennte logische Namen) schränkt die Projektion ein – lässt ihn weg, um den Standard-Spaltensatz der Tabelle abzurufen.
Anfrage
const record = await fetch(
'/api/table/account/a1b2c3d4-e5f6-7890-abcd-ef1234567890?columns=name,revenue',
{ credentials: 'include' }
).then(r => r.json());
Reaktion
{
"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}
Aktualisiert eine bestehende Zeile. Nur die vorhandenen Spalten in properties werden geschrieben, also senden Sie nur die geänderten Werte.
Anfrage
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 } }
})
});
Reaktion
{ "responseName": "UpdateResponse", "outputParameters": {} }
DELETE /api/table/{tableLogicalName}/{recordId}
Löscht die Zeile nach id.
Anfrage
await fetch('/api/table/account/a1b2c3d4-e5f6-7890-abcd-ef1234567890', {
method: 'DELETE',
credentials: 'include'
});
Reaktion
{ "responseName": "DeleteResponse", "outputParameters": {} }
GET /api/retrieveMultiple?fetchXml=…
Führt eine beliebige FetchXML-Abfrage aus und liefert die passenden Zeilen plus Paging-Informationen zurück. Kodiere das FetchXML in die fetchXml Abfrage-Zeichenkette – aus C# Routes.Api.GetRetrieveMultipleRoute(fetchXml) macht das für dich.
Anfrage
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());
Reaktion
{
"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
Führt einen Batch heterogener Anfragen aus (Erstellen / Aktualisieren / Löschen / Zuordnen / Disassozieren) in einer einzigen Datenbanktransaktion – wenn eine fehlschlägt, rollt der gesamte Batch zurück. Der Körper ist ein JSON-Array von OrganizationRequest Objekten, die durch diskriminiert werden $type. Überspringen ?returnResponses=false Sie das Erstellen der Antwortliste pro Anfrage.
Anfrage
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' } }
])
});
Reaktion
[
{ "$type": "CreateResponse", "responseName": "CreateResponse", "outputParameters": {}, "id": "new-guid" },
{ "$type": "DeleteResponse", "responseName": "DeleteResponse", "outputParameters": {} }
]
Raster & Diagramme
Server-komponierte Abfrage-Endpunkte. Anstatt dass der Client FetchXML erstellt, übergibt man eine View-ID (oder eigene FetchXML) plus Suche/Sortieren/Paging, und der Server löst Spalten auf, wendet Berechtigungen an und führt die Abfrage aus – denselben Single-Source-of-Truth-Pfad, den MainGrid und die Diagrammkomponenten verwenden.
POST /api/grids/data
Lädt eine Seite mit Rasterdaten. Stellen Sie entweder einen viewId oder Ihren eigenen fetchXmlsowie optional searchText, sorts, Paging- und Spaltenfilter bereit. Die Antwort enthält die Zeilen, die aufgelösten Spaltendefinitionen und die Tabellen-Berechtigungsmaske des Aufrufers.
Anfrage
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());
Reaktion
{
"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
Lädt aggregierte Diagrammdaten, die für die Diagrammkomponenten geformt wurden. Akzeptiert eine aggregierte Konfiguration, eine viewId, oder rohe fetchXml, plus die Label-/Wert-/Serienzuordnung; gibt Chart.js-ähnliche Labels und Datensätze zurück.
Anfrage
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());
Reaktion
{
"data": {
"labels": ["Active", "Inactive"],
"datasets": [ { "label": "Accounts by Status", "data": [ { "value": 25 }, { "value": 8 } ] } ]
}
}
Metadaten & Berechtigungen
Schreibgeschützte Nachschlagefunktionen für Tabellen- und Ansichtsmetadaten, die Berechtigungsmaske des aktuellen Benutzers und organisationsweite Einstellungen. Alle sind serverseitige Zwischenrufe, daher sind wiederholte Anrufe günstig.
GET /api/tableMetadata/{tableLogicalName}
Gibt die Metadaten einer Tabelle zurück – Spalten (mit Typen, Labels und Einschränkungen), primäre ID / Name / Bildspalten und Beziehungen.
Anfrage
const meta = await fetch('/api/tableMetadata/account', { credentials: 'include' })
.then(r => r.json());
Reaktion
{
"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}
Gibt die kombinierte TableSecurityPermission Maske des aktuellen Benutzers für die Tabelle als einzelne Zahl zurück (Bit-Flags: Read 1, Create 2, Write 4, Delete 8, Append 16, AppendTo 32).
Anfrage
const mask = await fetch('/api/permissions/table/account', { credentials: 'include' })
.then(r => r.json());
// 15 === Read(1) | Create(2) | Write(4) | Löschen(8)
Reaktion
15
GET /api/viewMetadata/{viewId}
Gibt die Metadaten für eine gespeicherte Ansicht über seinen GUID zurück – seine FetchXML, Layout-Spalten und Ansichtsflags. Die Route erfordert einen GUID, weshalb sie sich von der untenstehenden All-View-Route unterscheiden lässt.
Anfrage
const view = await fetch('/api/viewMetadata/00000000-0000-0000-0000-000000000001',
{ credentials: 'include' }).then(r => r.json());
Reaktion
{
"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}
Gibt jede gespeicherte Ansicht für eine Tabelle zurück. Teilt sich das /api/viewMetadata/ Präfix mit der By-id-Route – ein nicht-GUID-Segment (ein logischer Name der Tabelle) landet hier.
Anfrage
const views = await fetch('/api/viewMetadata/account', { credentials: 'include' })
.then(r => r.json());
Reaktion
[
{ "id": "0000…0001", "name": "All Accounts", "isDefault": true, "tableName": "account" },
{ "id": "0000…0002", "name": "Active Accounts", "isDefault": false, "tableName": "account" }
]
GET /api/organizationSettings
Gibt organisationsweite Einstellungen zurück, die aus dem Dataverse-Organisationsdatensatz stammen: die Standardwährung, die Liste der blockierten Dateierweiterungen und die maximale Upload-Größe in Bytes.
Anfrage
const settings = await fetch('/api/organizationSettings', { credentials: 'include' })
.then(r => r.json());
Reaktion
{
"defaultCurrency": { "isoCode": "USD", "symbol": "$", "precision": 2 },
"blockedFileExtensions": ["exe", "bat", "js"],
"maxUploadFileSizeInBytes": 10485760
}
Akten
Lesen Sie den Inhalt der Datei- und Bildspalten. Binäre Nutzlasten werden innerhalb von JSON base64-kodiert zurückgegeben; Das Flag includeData erlaubt es nur, Metadaten abzurufen (z. B. um eine Download-Liste zu rendern), ohne Bytes zu übertragen.
GET /api/files/{tableLogicalName}/{recordId}/{columnName}?includeData=…
Gibt die Dateimetadaten für eine Datei-/Bildspalte auf einem Datensatz zurück. Mit ?includeData=true der Base64 ist Inhalt eingebettet; nur false Name und Größe werden zurückgegeben.
Anfrage
const file = await fetch(
'/api/files/account/a1b2c3d4-…/new_attachment?includeData=true',
{ credentials: 'include' }
).then(r => r.json());
Reaktion
{
"fileName": "contract.pdf",
"fileSizeInBytes": 245678,
"fileData": "JVBERi0xLjQKJeLjz9M…"
}
POST /api/files/{tableLogicalName}/{columnName}/batch?includeData=…
Ruft Dateimetadaten (und optional Inhalt) für viele Datensätze derselben Tabelle / Spalte in einer Rundreise ab – der Hauptteil ist ein JSON-Array von Datensatz-GUIDs. Wird von FileGrids Download Selected verwendet, damit der Client keine N separaten Aufrufe auslöst.
Anfrage
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());
Reaktion
[
{ "fileName": "contract.pdf", "fileSizeInBytes": 245678, "fileData": null },
{ "fileName": "invoice.docx", "fileSizeInBytes": 89456, "fileData": null }
]
POST /api/files/createFileArchive
Zippt die ausgewählten Dateispaltenwerte für eine Menge Datensätze serverseitig. Gibt standardmäßig den rohen application/zip Strom zurück oder – mit responseFormat: 1 – einem JSON-Umschlag, der das base64-Archiv trägt. POST (nicht GET), damit große ID-Listen keine URL-Längenbeschränkungen erreichen.
Anfrage
// Standard: rohe Anwendung/Zip-Stream. Pass ResponseFormat: 1 für einen JSON-Umschlag.
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());
Reaktion
// responseFormat: 1 (Json) statt des Standard-Binärstroms
{
"fileName": "account-files.zip",
"contentType": "application/zip",
"data": "UEsDBBQAAAAIAP2t…"
}
Lokalisierungsbündel
Lokalisierte Saiten für Nicht-Blazor-Frontends. Die Route /api/localizedStrings liefert den gesamten Baum für eine Kultur zurück; die Routen /localizations/* bedienen fingerabgedruckte, unveränderlich gecachte Bundles (Standard, pro Tabelle, pro Ansicht) für effizientes inkrementelles Laden – diese sind öffentlich und benötigen kein Authentifizierungscookie.
GET /api/localizedStrings/{culture}
Gibt den vollständigen lokalisierten String-Baum einer Kultur als verschachteltes Objekt zurück – Framework-Strings, App-Overrides, Tabellen- und Auswahllabels.
Anfrage
const strings = await fetch('/api/localizedStrings/fr-fr', { credentials: 'include' })
.then(r => r.json());
Reaktion
{
"app": { "navigation": { "home": "Accueil" } },
"tables": { "account": { "columns": { "name": { "label": "Nom du compte" } } } }
}
GET /localizations/version
Gibt das Lokalisierungsmanifest zurück – nur die Liste der unterstützten Orte. No-Cache bereitgestellt, sodass beim nächsten Seitenladen eine neue Version erkannt wird. Öffentlich.
Anfrage
const manifest = await fetch('/localizations/version').then(r => r.json());
Reaktion
{ "supportedLocales": ["en-us", "fr-fr", "de-de"] }
GET /localizations/{locale}/thumbprints
Gibt die Inhalts-Daumenabdrücke für einen Standort zurück: das Standardpaket plus jede geladene Tabelle und Ansicht. Clients rufen diese ab und fordern dann nur die Bundles an, deren Daumenabdruck sich geändert hat. Öffentlich.
Anfrage
const thumbs = await fetch('/localizations/fr-fr/thumbprints').then(r => r.json());
Reaktion
{
"bundle": "a3f5e8c2d",
"tables": { "account": "b7f2d9e1a", "contact": "c4f8a1b3e" },
"views": { "550e8400e29b41d4a716446655440000": "e2f4b8d1c" }
}
GET /localizations/default/{filename} · /tables/{tableName}/{filename} · /views/{viewId}/{filename}
Die drei Bündelfamilien – Standard (Cross-Cutting Strings), per-table (die Strings einer Tabelle plus die globalen Auswahlen, auf die ihre Spalten verweisen) und per-view. Der Dateiname ist {locale}.{thumbprint}.json und jeder wird bereitgestellt public, immutable, max-age=31536000, also ist ein stabiler Daumenabdruck ein garantierter Cache-Hit. Öffentlich.
Anfrage
// Dateiname ist '{locale}. {Daumenabdruck}.json', diente unveränderlich + Cache-für immer.
const bundle = await fetch('/localizations/tables/account/fr-fr.b7f2d9e1a.json')
.then(r => r.json());
Reaktion
{
"tables": { "account": { "label": "Compte", "columns": { "name": { "label": "Nom du compte" } } } },
"choices": { "account_industrycode": { "1": "Fabrication", "2": "Services" } }
}
Kultur
Wechselt die aktive Kultur des Browsers, indem man den Kulturcookie schreibt.
GET /Culture/{culture}?redirectUri=…
Setzt das Kultur-Cookie und 302-Weiterleitungen auf redirectUri. Navigiere darauf (vollständiges Seitenlade) statt es abzurufen, damit die und die Set-Cookie Umleitung wirksam werden. Öffentlich.
Anfrage
// Ganzseitig navigieren, damit die Set-Cookie + Weiterleitung anerkannt wird.
window.location.href = '/Culture/fr-fr?redirectUri=' + encodeURIComponent('/dashboard');
Reaktion
// 302 Gefunden → Standort: /dashboard
// Set-Cookie: . AspNetCore.Culture=c=fr-fr|uic=fr-fr
Verwaltung — Cache-Verwaltung
Server-Cache-Inspektion und Invalidierung. Alle drei Endpunkte sind durch die Grenze gesperrt [Authorize(Roles = "SystemAdmin")] – nur ein angemeldeter Administrator kann sie aufrufen.
GET /api/caches
Listet die Namen jedes registrierten serverseitigen Caches auf. Erfordert die Rolle Systemadministrator.
Anfrage
const names = await fetch('/api/caches', { credentials: 'include' }).then(r => r.json());
Reaktion
["TableMetadataCache", "ViewMetadataCache", "CurrencyCache"]
POST /api/caches/clear
Löscht jeden serverseitigen Cache und liefert ein Ergebnis pro Cache zurück (ob es erfolgreich war und wie lange es gedauert hat). Erfordert die Rolle Systemadministrator.
Anfrage
const results = await fetch('/api/caches/clear', { method: 'POST', credentials: 'include' })
.then(r => r.json());
Reaktion
[
{ "name": "TableMetadataCache", "succeeded": true, "error": null, "elapsedMs": 45 },
{ "name": "ViewMetadataCache", "succeeded": false, "error": "Timed out", "elapsedMs": 5000 }
]
POST /api/caches/{cacheName}/clear
Löscht einen einzelnen benannten Cache (der Name stammt vom Listen-Endpunkt). Gibt 404 zurück, wenn kein Cache mit diesem Namen registriert wird. Erfordert die Rolle Systemadministrator.
Anfrage
const result = await fetch('/api/caches/TableMetadataCache/clear',
{ method: 'POST', credentials: 'include' }).then(r => r.json());
Reaktion
{ "name": "TableMetadataCache", "succeeded": true, "error": null, "elapsedMs": 132 }
Authentifizierung
Anmeldung, Anmeldung und der Kontolebenszyklus, unterstützt von MapAuthEndpoints<TUser>. Cookie-basiert: Ein erfolgreiches Anmelden setzt das ASP.NET Core Identity-Anwendungscookie, und jeder spätere Aufruf authentifiziert sich, indem es zurückgegeben wird. Die meisten geben ein result Enum bei HTTP 200 zurück, anstatt das Ergebnis über den Statuscode zu signalisieren.
POST /api/auth/login
Melde dich mit E-Mail + Passwort an. Das Enum result unterscheidet Erfolg, einen erforderlichen zweiten Faktor, schlechte Zugangsdaten, eine unbestätigte E-Mail und eine Sperre. Nach Erfolg wird das Authentifizierungscookie auf die Antwort gesetzt.
Anfrage
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());
// Ergebnis: 0 Erfolg · 1 BenötigtZweiFaktor · 2 Ungültige Zugangsdaten · 3 E-MailNichtBestätigt · 4 LockedOut
Reaktion
{ "result": 0 }
POST /api/auth/login/2fa
Es wird eine zurückgegebene RequiresTwoFactor Anmeldung durch das Einreichen des Authentifikator- (oder Wiederherstellungs-) Codes abgeschlossen. rememberMachine Setzt den Trusted-Browser-Cookie, sodass zukünftige Anmeldungen in diesem Browser den zweiten Faktor überspringen.
Anfrage
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());
Reaktion
{ "success": true }
POST /api/auth/logout
Löscht das Authentifizierungscookie und beendet die Sitzung.
Anfrage
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
Reaktion
// 200 OK — keine Leiche. Das Authentifizierungscookie ist in der Antwort freigegeben.
POST /api/auth/register
Erstellt ein neues lokales Konto. Je nach Konfiguration ist das Ergebnis entweder eine Bestätigungs-E-Mail, eine sofortige Anmeldung oder ein Konflikt mit einer bestehenden E-Mail. Schwach-Passwort- und andere Validierungsfehler ergeben 400 problem+json.
Anfrage
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());
// Ergebnis: 0 BestätigungE-MailGesendet · 1 SignedIn · 2 EmailAlreadyInUse
Reaktion
{ "result": 0 }
POST /api/auth/forgot-password
Startet ein Passwort-Zurücksetzen, indem man einen Link zum Zurücksetzen per E-Mail verschickt. Es gibt immer 200 zurück, ohne dass die Adresse existiert, egal ob die Adresse existiert oder nicht, daher kann es nicht verwendet werden, um nach eingeschriebenen E-Mails zu suchen.
Anfrage
await fetch('/api/auth/forgot-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'user@example.com' })
});
Reaktion
// 200 OK – kein Körper, unabhängig davon, ob die Adresse existiert oder nicht (aufzählungssicher).
POST /api/auth/reset-password
Führt einen Reset mit dem Token aus der E-Mail plus dem neuen Passwort durch. result Unterscheidet Erfolg, ein ungültiges / abgelaufenes Token und ein abgelehntes Passwort (mit den Validierungsnachrichten in errors).
Anfrage
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());
// Ergebnis: 0 Erfolg · 1 InvalidOrExpiredToken · 2 InvalidPassword
Reaktion
{ "result": 0, "errors": [] }
POST /api/auth/confirm-email
Bestätigt eine neu registrierte E-Mail mit der Benutzer-ID und dem Token über den Bestätigungslink.
Anfrage
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());
Reaktion
{ "success": true }
POST /api/auth/resend-email-confirmation
Schickt den Link zur E-Mail-Bestätigung erneut. Wie beim vergessenen Passwort gibt es immer 200 zurück, ohne Body, um zu verhindern, dass die registrierten Adressen geleakt werden.
Anfrage
await fetch('/api/auth/resend-email-confirmation', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'user@example.com' })
});
Reaktion
// 200 OK — kein Körper (aufzählungssicher).
GET /api/auth/options
components.PowerPortalsPro.Demo.Client.Customizations.Pages.ClientApi.ClientApiPage.ep-auth-options-desc
Anfrage
const options = await fetch('/api/auth/options').then(r => r.json());
Reaktion
{
"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=…
Das setzt den OAuth-Flow für einen Anbieter in Gang. Es gibt dem Anbieter eine 302-Herausforderung zurück, also navigiere im Browser dorthin (nicht abrufen) – ein SPA sollte gesetzt window.location.hrefwerden.
Anfrage
// Ganzseitige Navigation – NICHT abrufen – damit der Browser der OAuth 302-Kette folgt.
window.location.href =
'/api/auth/external-login?provider=Microsoft&returnUrl=' + encodeURIComponent('/dashboard');
Reaktion
// 302 Gefunden → der Anmeldeseite des externen Anbieters (plus Korrelationscookies).
GET /api/auth/external-login/pending
Nach dem OAuth-Rückruf nimmt er einen Schnappschuss auf die externe Anmeldung während des Fluges: den Anbieter, die Ansprüche der Identität und – wenn die E-Mail mit mehr als einer Portalidentität übereinstimmt – die Kandidatenliste zur Auswahl. Es gibt 204 zurück, wenn kein ausstehender Login vorliegt.
Anfrage
const res = await fetch('/api/auth/external-login/pending', { credentials: 'include' });
const pending = res.status === 204 ? null : await res.json();
Reaktion
{
"loginProvider": "Microsoft",
"providerDisplayName": "Microsoft",
"identityEmail": "user@company.com",
"requiresChoice": false,
"candidates": []
}
POST /api/auth/external-login/confirm
Führt eine erstmalige externe Anmeldung für ein neues Konto durch Bestätigung der E-Mail zur Assoziation ab. Es wird auf eine Anmeldung, eine Bestätigungs-E-Mail, kein ausstehendes Verfahren oder ein Fehlschlag aufgelöst.
Anfrage
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());
// Ergebnis: 0 SignedIn · 1 BestätigungE-MailGesendet · 2 NoPendingExternalLogin · 3 Scheitern
Reaktion
{ "result": 0, "errors": [] }
POST /api/auth/external-login/select
Führt eine externe Anmeldung durch, wenn die Identität mit mehreren Portalidentitäten übereinstimmt, indem ausgewählt wird, als welche (Contact oder SystemUser) sich anmelden soll.
Anfrage
const { result } = await fetch('/api/auth/external-login/select', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ kind: 1 }) // 0 Kontakt · 1 SystemUser
}).then(r => r.json());
Reaktion
{ "result": 0, "errors": [] }
GET /api/auth/me
Snapshot des aktuellen Schulleiters – ID, Name, E-Mail, Rollen und der Backing Table (contact vs. systemuser), plus eventuelle alternative Geschwister-Identität. Gibt eine anonyme Form (isAuthenticated: false) statt 401 zurück, wenn kein Cookie vorhanden ist, sodass ein SPA sie beim ersten Paint aufrufen kann, ohne im Statuscode zu verzweigen.
Anfrage
const me = await fetch('/api/auth/me', { credentials: 'include' }).then(r => r.json());
Reaktion
{
"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
Tauscht das aktuelle Cookie gegen die alternative Geschwisteridentität des Nutzers (das Contact↔SystemUser-Pairing, das auf aufgetaucht ist /api/auth/me). Der JSON-Körper ist ein leeres Objekt.
Anfrage
const { result } = await fetch('/api/auth/switch-identity', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: '{}'
}).then(r => r.json());
// Ergebnis: 0 Switched · 1 NoAltIdentity · 2 AltIdentitätNichtGefunden · 3 Nicht authentifiziert
Reaktion
{ "result": 0 }
Kontoverwaltung
Die Self-Service-Operationen des angemeldeten Nutzers unter /api/auth/manage/* — Profil, Passwort, E-Mail, Zwei-Faktor-, verknüpfte externe Logins und persönliche Daten. Alle erfordern eine authentifizierte Sitzung und spiegeln die klassischen /Account/Manage Razor-Seiten des Frameworks wider.
GET /api/auth/manage/profile
Gibt das Profil des Nutzers (Name, Handy, E-Mail) zurück, das vom verknüpften Dataverse-Kontakt gelesen wurde, sowie Identitätsstatus-Flags (E-Mail bestätigt, Passwort, 2FA aktiviert, nur lesbar).
Anfrage
const profile = await fetch('/api/auth/manage/profile', { credentials: 'include' })
.then(r => r.json());
Reaktion
{
"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
Aktualisiert den Vor- und Nachnamen sowie das Handy im verknüpften Kontakt. Bringt bei Erfolg 200 zurück; SystemUser-gesicherte Identitäten sind schreibgeschützt und erhalten 403.
Anfrage
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' })
});
Reaktion
// 200 OK — keine Leiche. (403 für schreibgeschützte SystemUser-gestützte Identitäten.)
POST /api/auth/manage/password/set
Fügt einem Konto, das keines hat, ein lokales Passwort hinzu (z. B. ein Konto mit externem Login). Bestätigungsnachrichten, falls vorhanden, kommen zurück.errors
Anfrage
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());
Reaktion
{ "success": true, "errors": [] }
POST /api/auth/manage/password/change
Ändert das lokale Passwort; Benötigt das aktuelle Passwort. result Unterscheidet Erfolg, ein falsches aktuelles Passwort und ein abgelehntes neues Passwort.
Anfrage
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());
// Ergebnis: 0 Erfolg · 1 FalschesAltpasswort · 2 InvalidPassword
Reaktion
{ "result": 0, "errors": [] }
POST /api/auth/manage/email/change
Startet eine E-Mail-Änderung, indem man einen Bestätigungslink an die neue Adresse schickt. Die Änderung tritt erst in Kraft, wenn dieser Link gefolgt ist.
Anfrage
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());
// Ergebnis: 0 BestätigungE-MailGesendet · 1 SameAsCurrentEmail
Reaktion
{ "result": 0 }
POST /api/auth/manage/email/send-confirmation
Sendet den Bestätigungslink für die aktuelle E-Mail des Nutzers erneut. sent ist falsch, wenn die E-Mail bereits bestätigt ist.
Anfrage
const { sent } = await fetch('/api/auth/manage/email/send-confirmation',
{ method: 'POST', credentials: 'include' }).then(r => r.json());
Reaktion
{ "sent": true }
GET /api/auth/manage/2fa
Gibt den 2FA-Status zurück – ob ein Authenticator registriert ist, ob 2FA aktiviert ist, ob dieser Browser gespeichert wird und wie viele Wiederherstellungscodes noch übrig sind.
Anfrage
const status = await fetch('/api/auth/manage/2fa', { credentials: 'include' })
.then(r => r.json());
Reaktion
{ "hasAuthenticator": true, "is2faEnabled": true, "isMachineRemembered": false, "recoveryCodesLeft": 8 }
GET /api/auth/manage/authenticator/setup
Gibt den gemeinsamen Schlüssel und otpauth:// die URI für den QR-Code-Anmeldebildschirm zurück. Koppele es mit dem Verify-Endpunkt, um die Aktivierung von 2FA abzuschließen.
Anfrage
const setup = await fetch('/api/auth/manage/authenticator/setup', { credentials: 'include' })
.then(r => r.json());
Reaktion
{
"sharedKey": "abcd efgh ijkl mnop",
"authenticatorUri": "otpauth://totp/PowerPortalsPro:user@example.com?secret=ABCD…&issuer=PowerPortalsPro"
}
POST /api/auth/manage/authenticator/verify
Verifiziert einen Code aus der Authenticator-App und aktiviert 2FA. Bei der ersten Anmeldung gibt die Antwort auch den ursprünglichen Satz der Wiederherstellungscodes zurück.
Anfrage
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());
Reaktion
{ "success": true, "recoveryCodes": ["ABC123DEF456", "GHI789JKL012", "…"] }
POST /api/auth/manage/authenticator/reset
Dreht den Authentifikator-Schlüssel. Dies deaktiviert auch 2FA, sodass der Nutzer sich neu registrieren muss.
Anfrage
const { success } = await fetch('/api/auth/manage/authenticator/reset',
{ method: 'POST', credentials: 'include' }).then(r => r.json());
Reaktion
{ "success": true }
POST /api/auth/manage/2fa/disable
Schaltet 2FA für das Konto aus.
Anfrage
const { success } = await fetch('/api/auth/manage/2fa/disable',
{ method: 'POST', credentials: 'include' }).then(r => r.json());
Reaktion
{ "success": true }
POST /api/auth/manage/2fa/recovery-codes/generate
Regeneriert die Wiederherstellungscodes, ersetzt alle bestehenden Sets und gibt die neuen Codes zurück.
Anfrage
const { recoveryCodes } = await fetch('/api/auth/manage/2fa/recovery-codes/generate',
{ method: 'POST', credentials: 'include' }).then(r => r.json());
Reaktion
{ "recoveryCodes": ["ABC123DEF456", "GHI789JKL012", "…"] }
POST /api/auth/manage/2fa/forget-browser
Löscht das Vertrauensgeräte-Cookie dieses Browsers, daher wird bei der nächsten Anmeldung erneut 2FA benötigt.
Anfrage
const { success } = await fetch('/api/auth/manage/2fa/forget-browser',
{ method: 'POST', credentials: 'include' }).then(r => r.json());
Reaktion
{ "success": true }
GET /api/auth/manage/external-logins
Listet die externen Logins auf, die derzeit mit dem Konto verknüpft sind.
Anfrage
const logins = await fetch('/api/auth/manage/external-logins', { credentials: 'include' })
.then(r => r.json());
Reaktion
{
"currentLogins": [
{ "loginProvider": "Microsoft", "providerKey": "oid-…", "providerDisplayName": "Microsoft" }
]
}
GET /api/auth/manage/login-info
Eine kombinierte Ansicht der Anmeldepfade des Benutzers – verknüpfte externe Logins sowie ob ein lokales Passwort gesetzt ist – wird verwendet, um zu entscheiden, ob das Entfernen eines Logins den Nutzer aussperrt.
Anfrage
const info = await fetch('/api/auth/manage/login-info', { credentials: 'include' })
.then(r => r.json());
Reaktion
{
"externalLogins": [ { "loginProvider": "Microsoft", "providerKey": "oid-…", "providerDisplayName": "Microsoft" } ],
"hasLocalPassword": true
}
GET /api/auth/manage/external-logins/link?provider=…
Startet den OAuth-Flow, der einen zusätzlichen Anbieter mit dem angemeldeten Konto verknüpft. Es gibt eine 302-Herausforderung zurück, also navigiere darauf, anstatt sie zu holen.
Anfrage
// Ganzseitige Navigation; Der untenstehende Rückruf fügt den Login hinzu und leitet zurück.
window.location.href =
'/api/auth/manage/external-logins/link?provider=Google&returnUrl=' + encodeURIComponent('/account');
Reaktion
// 302 Gefunden → der Einwilligungsseite des externen Anbieters.
GET /api/auth/manage/external-logins/link/callback
Der OAuth-Rückruf für den Link-Flow. Der Anbieter leitet den Browser hierher weiter; Der Server hängt den Login an und leitet 302-Umleitungen zum returnUrl ursprünglich bereitgestellten Formular an. Das nennt man nicht direkt.
Anfrage
// Von der Weiterleitung des OAuth-Anbieters getroffen – nicht direkt mit deinem Code angesprochen.
Reaktion
// 302 → der returnUrl gefunden, die an den Link-Endpunkt geliefert wurde, mit dem Login jetzt angehängt.
POST /api/auth/manage/external-logins/remove
Entkoppelt einen externen Login über Anbieter + Anbieterschlüssel.
Anfrage
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());
Reaktion
{ "success": true }
GET /api/auth/manage/personal-data
Exportiert die persönlichen Daten des Nutzers – jede [PersonalData] Immobilie plus die verknüpften externen Logins – für einen DSGVO-ähnlichen Download.
Anfrage
const data = await fetch('/api/auth/manage/personal-data', { credentials: 'include' })
.then(r => r.json());
Reaktion
{
"personalData": { "Id": "550e8400-…", "Email": "john.doe@example.com" },
"externalLogins": { "Microsoft": "oid-…" }
}
POST /api/auth/manage/personal-data/delete
Löscht das Konto des Nutzers dauerhaft und meldet ihn ab. Benötigt das aktuelle Passwort, wenn das Konto eines hat; null Pass für externe Konten result aus. Unterscheidet zwischen Erfolg, falschem Passwort und dem Fall, in dem ein Passwort erforderlich ist, aber nicht angegeben wurde.
Anfrage
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 für ausschließlich externe Konten
}).then(r => r.json());
// Ergebnis: 0 Erfolg · 1 FalschPasswort · 2 RecrereLocalPassword
Reaktion
{ "result": 0 }
Siehe auch
Verwandte Dokumentation:
IPowerPortalsProService — der C#-Wrapper um diese Endpunkte – was deine Blazor-Komponenten injizieren, wenn sie selbst kein rohes HTTP ausgeben müssen.SystemUser-Anmeldung — Hintergrund zu den Why-Berichten/api/auth/metableName: "systemuser"für einige Schulleiter undtableName: "contact"für andere.
