diff --git a/apps/demos/Demos/DataGrid/BatchUpdateRequest/Angular/app/app.component.ts b/apps/demos/Demos/DataGrid/BatchUpdateRequest/Angular/app/app.component.ts index b193fda9ee3d..ac0d7df56fc3 100644 --- a/apps/demos/Demos/DataGrid/BatchUpdateRequest/Angular/app/app.component.ts +++ b/apps/demos/Demos/DataGrid/BatchUpdateRequest/Angular/app/app.component.ts @@ -1,10 +1,10 @@ import { bootstrapApplication } from '@angular/platform-browser'; import { Component, enableProdMode, provideZoneChangeDetection } from '@angular/core'; -import { HttpClient, provideHttpClient, withFetch, withInterceptors } from '@angular/common/http'; +import { HttpClient, provideHttpClient, withFetch } from '@angular/common/http'; import { lastValueFrom } from 'rxjs'; import * as AspNetData from 'devextreme-aspnet-data-nojquery'; import { DxDataGridComponent, DxDataGridModule, DxDataGridTypes } from 'devextreme-angular/ui/data-grid'; -import { antiForgeryInterceptor, AntiForgeryTokenService } from './app.service'; +import 'anti-forgery'; if (!/localhost/.test(document.location.host)) { enableProdMode(); @@ -30,15 +30,13 @@ if (window && window.config?.packageConfigPaths) { export class AppComponent { ordersStore: AspNetData.CustomStore; - constructor(private http: HttpClient, private tokenService: AntiForgeryTokenService) { + constructor(private http: HttpClient) { this.ordersStore = AspNetData.createStore({ key: 'OrderID', loadUrl: `${URL}/Orders`, async onBeforeSend(_method, ajaxOptions) { - const tokenData = await lastValueFrom(tokenService.getToken()); ajaxOptions.xhrFields = { withCredentials: true, - headers: { [tokenData.headerName]: tokenData.token }, }; }, }); @@ -108,7 +106,6 @@ bootstrapApplication(AppComponent, { provideZoneChangeDetection({ eventCoalescing: true, runCoalescing: true }), provideHttpClient( withFetch(), - withInterceptors([antiForgeryInterceptor]), ), ], }); diff --git a/apps/demos/Demos/DataGrid/BatchUpdateRequest/Angular/app/app.service.ts b/apps/demos/Demos/DataGrid/BatchUpdateRequest/Angular/app/app.service.ts deleted file mode 100644 index 185bed7e6c3d..000000000000 --- a/apps/demos/Demos/DataGrid/BatchUpdateRequest/Angular/app/app.service.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { Injectable, inject } from '@angular/core'; -import { HttpClient, HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http'; -import { Observable, of, throwError } from 'rxjs'; -import { catchError, switchMap, map, shareReplay } from 'rxjs/operators'; - -interface TokenData { - headerName: string; - token: string; -} - -@Injectable({ - providedIn: 'root', -}) -export class AntiForgeryTokenService { - private BASE_PATH = 'https://js.devexpress.com/Demos/NetCore'; - - private tokenCache$: Observable | null = null; - - constructor(private http: HttpClient) {} - - getToken(): Observable { - const tokenMeta = document.querySelector('meta[name="csrf-token"]'); - if (tokenMeta) { - const headerName = tokenMeta.dataset.headerName || 'RequestVerificationToken'; - const token = tokenMeta.getAttribute('content') || ''; - return of({ headerName, token }); - } - - if (!this.tokenCache$) { - this.tokenCache$ = this.fetchToken().pipe( - map((tokenData) => { - this.storeTokenInMeta(tokenData); - return tokenData; - }), - shareReplay({ bufferSize: 1, refCount: false }), - catchError((error) => { - this.tokenCache$ = null; - return throwError(() => error); - }), - ); - } - - return this.tokenCache$; - } - - private fetchToken(): Observable { - return this.http.get( - `${this.BASE_PATH}/api/Common/GetAntiForgeryToken`, - { - withCredentials: true, - }, - ).pipe( - catchError((error) => { - const errorMessage = typeof error.error === 'string' ? error.error : (error.statusText || 'Unknown error'); - return throwError(() => new Error(`Failed to retrieve anti-forgery token: ${errorMessage}`)); - }), - ); - } - - private storeTokenInMeta(tokenData: TokenData): void { - const meta = document.createElement('meta'); - meta.name = 'csrf-token'; - meta.content = tokenData.token; - meta.dataset.headerName = tokenData.headerName; - document.head.appendChild(meta); - } - - clearToken(): void { - this.tokenCache$ = null; - const tokenMeta = document.querySelector('meta[name="csrf-token"]'); - if (tokenMeta) { - tokenMeta.remove(); - } - } -} - -export const antiForgeryInterceptor: HttpInterceptorFn = (req, next) => { - const tokenService = inject(AntiForgeryTokenService); - - if (req.method === 'GET' && req.url.includes('/GetAntiForgeryToken')) { - return next(req); - } - - if (req.method !== 'GET') { - return tokenService.getToken().pipe( - switchMap((tokenData) => { - const clonedRequest = req.clone({ - setHeaders: { - [tokenData.headerName]: tokenData.token, - }, - }); - return next(clonedRequest); - }), - catchError((error: HttpErrorResponse) => { - if (error.status === 401 || error.status === 403) { - tokenService.clearToken(); - } - return throwError(() => error); - }), - ); - } - - return next(req); -}; diff --git a/apps/demos/Demos/DataGrid/BatchUpdateRequest/React/App.tsx b/apps/demos/Demos/DataGrid/BatchUpdateRequest/React/App.tsx index 0cd9c8bf553e..cb26fdc63b9c 100644 --- a/apps/demos/Demos/DataGrid/BatchUpdateRequest/React/App.tsx +++ b/apps/demos/Demos/DataGrid/BatchUpdateRequest/React/App.tsx @@ -6,52 +6,12 @@ import 'whatwg-fetch'; const BASE_PATH = 'https://js.devexpress.com/Demos/NetCore'; const URL = `${BASE_PATH}/api/DataGridBatchUpdateWebApi`; - -async function fetchAntiForgeryToken(): Promise<{ headerName: string; token: string }> { - try { - const response = await fetch(`${BASE_PATH}/api/Common/GetAntiForgeryToken`, { - method: 'GET', - credentials: 'include', - cache: 'no-cache', - }); - - if (!response.ok) { - const errorMessage = await response.text(); - throw new Error(`Failed to retrieve anti-forgery token: ${errorMessage || response.statusText}`); - } - - return await response.json(); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - throw new Error(errorMessage); - } -} - -async function getAntiForgeryTokenValue(): Promise<{ headerName: string; token: string }> { - const tokenMeta = document.querySelector('meta[name="csrf-token"]'); - if (tokenMeta) { - const headerName = tokenMeta.dataset.headerName || 'RequestVerificationToken'; - const token = tokenMeta.getAttribute('content') || ''; - return Promise.resolve({ headerName, token }); - } - - const tokenData = await fetchAntiForgeryToken(); - const meta = document.createElement('meta'); - meta.name = 'csrf-token'; - meta.content = tokenData.token; - meta.dataset.headerName = tokenData.headerName; - document.head.appendChild(meta); - return tokenData; -} - const ordersStore = createStore({ key: 'OrderID', loadUrl: `${URL}/Orders`, async onBeforeSend(_method, ajaxOptions) { - const tokenData = await getAntiForgeryTokenValue(); ajaxOptions.xhrFields = { withCredentials: true, - headers: { [tokenData.headerName]: tokenData.token }, }; }, }); @@ -81,31 +41,25 @@ function normalizeChanges(changes: DataGridTypes.DataChange[]): DataGridTypes.Da }) as DataGridTypes.DataChange[]; } -async function sendBatchRequest(url: string, changes: DataGridTypes.DataChange[], headers: Record) { - try { - const response = await fetch(url, { - method: 'POST', - body: JSON.stringify(changes), - headers: { - 'Content-Type': 'application/json;charset=UTF-8', - ...headers, - }, - credentials: 'include', - }); +async function sendBatchRequest(url: string, changes: DataGridTypes.DataChange[]) { + const result = await fetch(url, { + method: 'POST', + body: JSON.stringify(changes), + headers: { + 'Content-Type': 'application/json;charset=UTF-8', + }, + credentials: 'include', + }); - if (!response.ok) { - const errorMessage = await response.text(); - throw new Error(`Batch save failed: ${errorMessage || response.statusText}`); - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - throw new Error(errorMessage); + if (!result.ok) { + const json = await result.json(); + + throw json.Message; } } async function processBatchRequest(url: string, changes: DataGridTypes.DataChange[], component: ReturnType) { - const tokenData = await getAntiForgeryTokenValue(); - await sendBatchRequest(url, changes, { [tokenData.headerName]: tokenData.token }); + await sendBatchRequest(url, changes); await component.refresh(true); component.cancelEditData(); } diff --git a/apps/demos/Demos/DataGrid/BatchUpdateRequest/React/index.tsx b/apps/demos/Demos/DataGrid/BatchUpdateRequest/React/index.tsx index 8acbec4b6179..6db05c9d4619 100644 --- a/apps/demos/Demos/DataGrid/BatchUpdateRequest/React/index.tsx +++ b/apps/demos/Demos/DataGrid/BatchUpdateRequest/React/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; - +import 'anti-forgery'; import App from './App.tsx'; ReactDOM.render( diff --git a/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/App.js b/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/App.js index fcde2ae8ae4b..cae1fd61bea9 100644 --- a/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/App.js +++ b/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/App.js @@ -5,48 +5,12 @@ import 'whatwg-fetch'; const BASE_PATH = 'https://js.devexpress.com/Demos/NetCore'; const URL = `${BASE_PATH}/api/DataGridBatchUpdateWebApi`; -async function fetchAntiForgeryToken() { - try { - const response = await fetch(`${BASE_PATH}/api/Common/GetAntiForgeryToken`, { - method: 'GET', - credentials: 'include', - cache: 'no-cache', - }); - if (!response.ok) { - const errorMessage = await response.text(); - throw new Error( - `Failed to retrieve anti-forgery token: ${errorMessage || response.statusText}`, - ); - } - return await response.json(); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - throw new Error(errorMessage); - } -} -async function getAntiForgeryTokenValue() { - const tokenMeta = document.querySelector('meta[name="csrf-token"]'); - if (tokenMeta) { - const headerName = tokenMeta.dataset.headerName || 'RequestVerificationToken'; - const token = tokenMeta.getAttribute('content') || ''; - return Promise.resolve({ headerName, token }); - } - const tokenData = await fetchAntiForgeryToken(); - const meta = document.createElement('meta'); - meta.name = 'csrf-token'; - meta.content = tokenData.token; - meta.dataset.headerName = tokenData.headerName; - document.head.appendChild(meta); - return tokenData; -} const ordersStore = createStore({ key: 'OrderID', loadUrl: `${URL}/Orders`, async onBeforeSend(_method, ajaxOptions) { - const tokenData = await getAntiForgeryTokenValue(); ajaxOptions.xhrFields = { withCredentials: true, - headers: { [tokenData.headerName]: tokenData.token }, }; }, }); @@ -74,29 +38,22 @@ function normalizeChanges(changes) { } }); } -async function sendBatchRequest(url, changes, headers) { - try { - const response = await fetch(url, { - method: 'POST', - body: JSON.stringify(changes), - headers: { - 'Content-Type': 'application/json;charset=UTF-8', - ...headers, - }, - credentials: 'include', - }); - if (!response.ok) { - const errorMessage = await response.text(); - throw new Error(`Batch save failed: ${errorMessage || response.statusText}`); - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - throw new Error(errorMessage); +async function sendBatchRequest(url, changes) { + const result = await fetch(url, { + method: 'POST', + body: JSON.stringify(changes), + headers: { + 'Content-Type': 'application/json;charset=UTF-8', + }, + credentials: 'include', + }); + if (!result.ok) { + const json = await result.json(); + throw json.Message; } } async function processBatchRequest(url, changes, component) { - const tokenData = await getAntiForgeryTokenValue(); - await sendBatchRequest(url, changes, { [tokenData.headerName]: tokenData.token }); + await sendBatchRequest(url, changes); await component.refresh(true); component.cancelEditData(); } diff --git a/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/index.js b/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/index.js index b853e0be8242..104d91ea2671 100644 --- a/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/index.js +++ b/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/index.js @@ -1,5 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; +// eslint-disable-next-line import/no-unresolved +import 'anti-forgery'; import App from './App.js'; ReactDOM.render(, document.getElementById('app')); diff --git a/apps/demos/Demos/DataGrid/BatchUpdateRequest/Vue/App.vue b/apps/demos/Demos/DataGrid/BatchUpdateRequest/Vue/App.vue index a3aa421c640d..00531b25652c 100644 --- a/apps/demos/Demos/DataGrid/BatchUpdateRequest/Vue/App.vue +++ b/apps/demos/Demos/DataGrid/BatchUpdateRequest/Vue/App.vue @@ -39,53 +39,9 @@ import 'whatwg-fetch'; const BASE_PATH = 'https://js.devexpress.com/Demos/NetCore'; const URL = `${BASE_PATH}/api/DataGridBatchUpdateWebApi`; -async function fetchAntiForgeryToken(): Promise<{ headerName: string; token: string }> { - try { - const response = await fetch(`${BASE_PATH}/api/Common/GetAntiForgeryToken`, { - method: 'GET', - credentials: 'include', - cache: 'no-cache', - }); - - if (!response.ok) { - const errorMessage = await response.text(); - throw new Error(`Failed to retrieve anti-forgery token: ${errorMessage || response.statusText}`); - } - - return await response.json(); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - throw new Error(errorMessage); - } -} - -async function getAntiForgeryTokenValue(): Promise<{ headerName: string; token: string }> { - const tokenMeta = document.querySelector('meta[name="csrf-token"]'); - if (tokenMeta) { - const headerName = tokenMeta.dataset.headerName || 'RequestVerificationToken'; - const token = tokenMeta.getAttribute('content') || ''; - return Promise.resolve({ headerName, token }); - } - - const tokenData = await fetchAntiForgeryToken(); - const meta = document.createElement('meta'); - meta.name = 'csrf-token'; - meta.content = tokenData.token; - meta.dataset.headerName = tokenData.headerName; - document.head.appendChild(meta); - return tokenData; -} - const ordersStore = createStore({ key: 'OrderID', loadUrl: `${URL}/Orders`, - async onBeforeSend(_method, ajaxOptions) { - const tokenData = await getAntiForgeryTokenValue(); - ajaxOptions.xhrFields = { - withCredentials: true, - headers: { [tokenData.headerName]: tokenData.token }, - }; - }, }); const onSaving = (e: DxDataGridTypes.SavingEvent) => { @@ -125,36 +81,25 @@ function normalizeChanges(changes: DxDataGridTypes.DataChange[]): DxDataGridType async function processBatchRequest( url: string, changes: DxDataGridTypes.DataChange[], component: DxDataGrid['instance'], ) { - const tokenData = await getAntiForgeryTokenValue(); - await sendBatchRequest(url, changes, { [tokenData.headerName]: tokenData.token }); + await sendBatchRequest(url, changes); await component?.refresh(true); component?.cancelEditData(); } -async function sendBatchRequest( - url: string, - changes: DxDataGridTypes.DataChange[], - headers: Record, -) { - try { - const response = await fetch(url, { - method: 'POST', - body: JSON.stringify(changes), - headers: { - 'Content-Type': 'application/json;charset=UTF-8', - ...headers, - }, - credentials: 'include', - }); +async function sendBatchRequest(url: string, changes: DxDataGridTypes.DataChange[]) { + const result = await fetch(url, { + method: 'POST', + body: JSON.stringify(changes), + headers: { + 'Content-Type': 'application/json;charset=UTF-8', + }, + }); - if (!response.ok) { - const errorMessage = await response.text(); - throw new Error(`Batch save failed: ${errorMessage || response.statusText}`); - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - throw new Error(errorMessage); + if (!result.ok) { + const json = await result.json(); + + throw json.Message; } } diff --git a/apps/demos/Demos/DataGrid/BatchUpdateRequest/Vue/index.ts b/apps/demos/Demos/DataGrid/BatchUpdateRequest/Vue/index.ts index 684d04215d72..16830f531753 100644 --- a/apps/demos/Demos/DataGrid/BatchUpdateRequest/Vue/index.ts +++ b/apps/demos/Demos/DataGrid/BatchUpdateRequest/Vue/index.ts @@ -1,4 +1,5 @@ import { createApp } from 'vue'; +import 'anti-forgery'; import App from './App.vue'; createApp(App).mount('#app'); diff --git a/apps/demos/Demos/DataGrid/BatchUpdateRequest/jQuery/index.html b/apps/demos/Demos/DataGrid/BatchUpdateRequest/jQuery/index.html index 5bfc9fb29a7c..500cee0e6334 100644 --- a/apps/demos/Demos/DataGrid/BatchUpdateRequest/jQuery/index.html +++ b/apps/demos/Demos/DataGrid/BatchUpdateRequest/jQuery/index.html @@ -10,6 +10,7 @@ + diff --git a/apps/demos/Demos/DataGrid/BatchUpdateRequest/jQuery/index.js b/apps/demos/Demos/DataGrid/BatchUpdateRequest/jQuery/index.js index cfb60d3753c4..43765cdbae33 100644 --- a/apps/demos/Demos/DataGrid/BatchUpdateRequest/jQuery/index.js +++ b/apps/demos/Demos/DataGrid/BatchUpdateRequest/jQuery/index.js @@ -1,52 +1,10 @@ -$(() => { - const BASE_PATH = 'https://js.devexpress.com/Demos/NetCore'; - const URL = `${BASE_PATH}/api/DataGridBatchUpdateWebApi`; - - function fetchAntiForgeryToken() { - const d = $.Deferred(); - $.ajax({ - url: `${BASE_PATH}/api/Common/GetAntiForgeryToken`, - method: 'GET', - xhrFields: { withCredentials: true }, - cache: false, - }).done((data) => { - d.resolve(data); - }).fail((xhr) => { - const error = xhr.responseJSON?.message || xhr.statusText || 'Unknown error'; - d.reject(new Error(`Failed to retrieve anti-forgery token: ${error}`)); - }); - return d.promise(); - } - - function getAntiForgeryTokenValue() { - const tokenMeta = document.querySelector('meta[name="csrf-token"]'); - if (tokenMeta) { - const headerName = tokenMeta.dataset.headerName || 'RequestVerificationToken'; - const token = tokenMeta.getAttribute('content'); - return $.Deferred().resolve({ headerName, token }); - } - - return fetchAntiForgeryToken().then((tokenData) => { - const meta = document.createElement('meta'); - meta.name = 'csrf-token'; - meta.content = tokenData.token; - meta.dataset.headerName = tokenData.headerName; - document.head.appendChild(meta); - return tokenData; - }); - } +$(async () => { + const URL = 'https://js.devexpress.com/Demos/NetCore/api/DataGridBatchUpdateWebApi'; $('#gridContainer').dxDataGrid({ dataSource: DevExpress.data.AspNet.createStore({ key: 'OrderID', loadUrl: `${URL}/Orders`, - async onBeforeSend(_, ajaxOptions) { - const tokenData = await getAntiForgeryTokenValue(); - ajaxOptions.xhrFields = { - withCredentials: true, - headers: { [tokenData.headerName]: tokenData.token }, - }; - }, }), pager: { visible: true, @@ -65,11 +23,11 @@ $(() => { if (e.changes.length) { const changes = normalizeChanges(e.changes); - e.promise = getAntiForgeryTokenValue().then((tokenData) => sendBatchRequest(`${URL}/Batch`, changes, { [tokenData.headerName]: tokenData.token })) - .then(() => e.component.refresh(true)) - .then(() => { + e.promise = sendBatchRequest(`${URL}/Batch`, changes).done(() => { + e.component.refresh(true).done(() => { e.component.cancelEditData(); }); + }); } }, columns: [{ @@ -116,14 +74,12 @@ $(() => { }); } - function sendBatchRequest(url, changes, headers) { + function sendBatchRequest(url, changes) { const d = $.Deferred(); $.ajax(url, { method: 'POST', data: JSON.stringify(changes), - headers, - xhrFields: { withCredentials: true }, cache: false, contentType: 'application/json', }).done(d.resolve).fail((xhr) => { diff --git a/apps/demos/Demos/FileUploader/FileUploading/Angular/app/app.component.ts b/apps/demos/Demos/FileUploader/FileUploading/Angular/app/app.component.ts index 8a64af0fe763..8c6ecb283b89 100644 --- a/apps/demos/Demos/FileUploader/FileUploading/Angular/app/app.component.ts +++ b/apps/demos/Demos/FileUploader/FileUploading/Angular/app/app.component.ts @@ -1,4 +1,5 @@ import { bootstrapApplication } from '@angular/platform-browser'; +import 'anti-forgery'; import { Component, enableProdMode, Pipe, PipeTransform, provideZoneChangeDetection } from '@angular/core'; import { DxCheckBoxModule, DxFileUploaderModule, DxSelectBoxModule } from 'devextreme-angular'; diff --git a/apps/demos/Demos/FileUploader/FileUploading/React/index.tsx b/apps/demos/Demos/FileUploader/FileUploading/React/index.tsx index 8acbec4b6179..ddf1cff8231a 100644 --- a/apps/demos/Demos/FileUploader/FileUploading/React/index.tsx +++ b/apps/demos/Demos/FileUploader/FileUploading/React/index.tsx @@ -1,5 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import 'anti-forgery'; import App from './App.tsx'; diff --git a/apps/demos/Demos/FileUploader/FileUploading/ReactJs/index.js b/apps/demos/Demos/FileUploader/FileUploading/ReactJs/index.js index b853e0be8242..b5193c1ef3a3 100644 --- a/apps/demos/Demos/FileUploader/FileUploading/ReactJs/index.js +++ b/apps/demos/Demos/FileUploader/FileUploading/ReactJs/index.js @@ -1,5 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import 'anti-forgery'; import App from './App.js'; ReactDOM.render(, document.getElementById('app')); diff --git a/apps/demos/Demos/FileUploader/FileUploading/Vue/index.ts b/apps/demos/Demos/FileUploader/FileUploading/Vue/index.ts index 684d04215d72..16830f531753 100644 --- a/apps/demos/Demos/FileUploader/FileUploading/Vue/index.ts +++ b/apps/demos/Demos/FileUploader/FileUploading/Vue/index.ts @@ -1,4 +1,5 @@ import { createApp } from 'vue'; +import 'anti-forgery'; import App from './App.vue'; createApp(App).mount('#app'); diff --git a/apps/demos/Demos/FileUploader/FileUploading/jQuery/index.html b/apps/demos/Demos/FileUploader/FileUploading/jQuery/index.html index 6afecda61def..ee24d67c6376 100644 --- a/apps/demos/Demos/FileUploader/FileUploading/jQuery/index.html +++ b/apps/demos/Demos/FileUploader/FileUploading/jQuery/index.html @@ -8,6 +8,7 @@ + diff --git a/apps/demos/configs/Angular/config.js b/apps/demos/configs/Angular/config.js index daf8948d74f5..7838c70572d0 100644 --- a/apps/demos/configs/Angular/config.js +++ b/apps/demos/configs/Angular/config.js @@ -155,8 +155,10 @@ window.config = { 'npm:': '../../../../node_modules/', 'bundles:': '../../../../bundles/', 'externals:': '../../../../bundles/externals/', + 'anti-forgery:': '../../../../shared/anti-forgery/', }, map: { + 'anti-forgery': 'anti-forgery:frameworks.js', 'ts': 'npm:plugin-typescript/lib/plugin.js', 'typescript': 'npm:typescript/lib/typescript.js', 'jszip': 'npm:jszip/dist/jszip.min.js', diff --git a/apps/demos/configs/React/config.js b/apps/demos/configs/React/config.js index 82e22347d2f7..85e68f786823 100644 --- a/apps/demos/configs/React/config.js +++ b/apps/demos/configs/React/config.js @@ -46,9 +46,11 @@ window.config = { 'npm:': '../../../../node_modules/', 'bundles:': '../../../../bundles/', 'externals:': '../../../../bundles/externals/', + 'anti-forgery:': '../../../../shared/anti-forgery/', }, defaultExtension: 'js', map: { + 'anti-forgery': 'anti-forgery:frameworks.js', 'ts': 'npm:plugin-typescript/lib/plugin.js', 'typescript': 'npm:typescript/lib/typescript.js', 'jszip': 'npm:jszip/dist/jszip.min.js', diff --git a/apps/demos/configs/ReactJs/config.js b/apps/demos/configs/ReactJs/config.js index 82e22347d2f7..85e68f786823 100644 --- a/apps/demos/configs/ReactJs/config.js +++ b/apps/demos/configs/ReactJs/config.js @@ -46,9 +46,11 @@ window.config = { 'npm:': '../../../../node_modules/', 'bundles:': '../../../../bundles/', 'externals:': '../../../../bundles/externals/', + 'anti-forgery:': '../../../../shared/anti-forgery/', }, defaultExtension: 'js', map: { + 'anti-forgery': 'anti-forgery:frameworks.js', 'ts': 'npm:plugin-typescript/lib/plugin.js', 'typescript': 'npm:typescript/lib/typescript.js', 'jszip': 'npm:jszip/dist/jszip.min.js', diff --git a/apps/demos/configs/Vue/config.js b/apps/demos/configs/Vue/config.js index b8211d96ac1f..5be49feb6f84 100644 --- a/apps/demos/configs/Vue/config.js +++ b/apps/demos/configs/Vue/config.js @@ -44,8 +44,10 @@ window.config = { 'npm:': '../../../../node_modules/', 'bundles:': '../../../../bundles/', 'externals:': '../../../../bundles/externals/', + 'anti-forgery:': '../../../../shared/anti-forgery/', }, map: { + 'anti-forgery': 'anti-forgery:frameworks.js', 'vue': 'npm:vue/dist/vue.esm-browser.js', '@vue/shared': 'npm:@vue/shared/dist/shared.cjs.prod.js', 'vue-loader': 'npm:dx-systemjs-vue-browser/index.js', diff --git a/apps/demos/shared/anti-forgery/frameworks.js b/apps/demos/shared/anti-forgery/frameworks.js new file mode 100644 index 000000000000..8b6d0cafdd7d --- /dev/null +++ b/apps/demos/shared/anti-forgery/frameworks.js @@ -0,0 +1,85 @@ +import ajax from 'devextreme/core/utils/ajax'; +import { Deferred } from 'devextreme/core/utils/deferred'; + +const sendRequestOrig = ajax.sendRequest; +const fetchOrig = fetch; +const BASE_PATH = 'https://js.devexpress.com/Demos/NetCore'; + +async function fetchAntiForgeryToken() { + try { + const response = await fetchOrig(`${BASE_PATH}/api/Common/GetAntiForgeryToken`, { + method: 'GET', + credentials: 'include', + cache: 'no-cache', + }); + + if (!response.ok) { + const errorMessage = await response.text(); + throw new Error(`Failed to retrieve anti-forgery token: ${errorMessage || response.statusText}`); + } + + return await response.json(); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + throw new Error(errorMessage); + } +} + +async function getAntiForgeryTokenValue() { + const tokenMeta = document.querySelector('meta[name="csrf-token"]'); + if (tokenMeta) { + const headerName = tokenMeta.dataset.headerName || 'RequestVerificationToken'; + const token = tokenMeta.getAttribute('content') || ''; + return Promise.resolve({ headerName, token }); + } + + const tokenData = await fetchAntiForgeryToken(); + const meta = document.createElement('meta'); + meta.name = 'csrf-token'; + meta.content = tokenData.token; + meta.dataset.headerName = tokenData.headerName; + document.head.appendChild(meta); + return tokenData; +} + +ajax.sendRequest = (options) => { + const deferred = new Deferred(); + + getAntiForgeryTokenValue().then(({ headerName, token }) => { + options.headers = { + [headerName]: token, + ...(options.headers || {}), + }; + + options.xhrFields = { + withCredentials: true, + }; + + sendRequestOrig(options).then( + (result) => { + deferred.resolve(result); + if (result.success) { + deferred.resolve(result); + } else { + deferred.reject(result); + } + }, + (e) => deferred.reject(e), + ); + }); + + return deferred.promise(); +}; + +window.fetch = async (url, options = {}) => { + const { headerName, token } = await getAntiForgeryTokenValue(); + + options.headers = { + [headerName]: token, + ...(options.headers || {}), + }; + + options.credentials = 'include'; + + return fetchOrig(url, options); +}; diff --git a/apps/demos/shared/anti-forgery/jquery.js b/apps/demos/shared/anti-forgery/jquery.js new file mode 100644 index 000000000000..f5a9ef8d99ac --- /dev/null +++ b/apps/demos/shared/anti-forgery/jquery.js @@ -0,0 +1,81 @@ +/* global $, DevExpress */ +const orig$ = $; +const ajaxSendRequestOrig = DevExpress.utils.ajax.sendRequest; + +function fetchAntiForgeryToken() { + const d = orig$.Deferred(); + + orig$.ajax({ + url: 'https://js.devexpress.com/Demos/NetCore/api/Common/GetAntiForgeryToken', + method: 'GET', + xhrFields: { withCredentials: true }, + cache: false, + }).done((data) => { + d.resolve(data); + }).fail((xhr) => { + const error = xhr.responseJSON?.message || xhr.statusText || 'Unknown error'; + d.reject(new Error(`Failed to retrieve anti-forgery token: ${error}`)); + }); + return d.promise(); +} + +function getAntiForgeryTokenValue() { + const tokenMeta = document.querySelector('meta[name="csrf-token"]'); + if (tokenMeta) { + const headerName = tokenMeta.dataset.headerName || 'RequestVerificationToken'; + const token = tokenMeta.getAttribute('content'); + return orig$.Deferred().resolve({ headerName, token }); + } + + return fetchAntiForgeryToken().then((tokenData) => { + const meta = document.createElement('meta'); + meta.name = 'csrf-token'; + meta.content = tokenData.token; + meta.dataset.headerName = tokenData.headerName; + document.head.appendChild(meta); + return tokenData; + }); +} + +async function setAntiForgery() { + const originalAjax = orig$.ajax; + const tokenData = await getAntiForgeryTokenValue(); + + // eslint-disable-next-line no-global-assign + $ = orig$; + + $.ajax = (url, options) => { + if (typeof url !== 'string') { + // eslint-disable-next-line no-param-reassign + options = url; + } else { + options.url = url; + } + + options.headers = { [tokenData.headerName]: tokenData.token, ...(options.headers || {}) }; + options.xhrFields = { withCredentials: true, ...(options.xhrFields || {}) }; + + return originalAjax.call(this, options); + }; + + DevExpress.utils.ajax.sendRequest = (options) => { + options.headers = { + [tokenData.headerName]: tokenData.token, + ...(options.headers || {}), + }; + + options.xhrFields = { + withCredentials: true, + ...(options.xhrFields || {}), + }; + + return ajaxSendRequestOrig(options); + }; +} + +// eslint-disable-next-line no-global-assign +$ = (...args) => orig$(async () => { + await setAntiForgery(); + + return $(...args); +});