From 0fba047fea85150914169201eb65bbe3be4e04e1 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Mon, 26 Jan 2026 23:34:28 +0100 Subject: [PATCH 01/14] move anti-forgery to shared for jQuery demos --- .../BatchUpdateRequest/jQuery/index.html | 1 + .../BatchUpdateRequest/jQuery/index.js | 56 ++--------------- apps/demos/shared/jQuery/anti-forgery.js | 62 +++++++++++++++++++ 3 files changed, 69 insertions(+), 50 deletions(-) create mode 100644 apps/demos/shared/jQuery/anti-forgery.js diff --git a/apps/demos/Demos/DataGrid/BatchUpdateRequest/jQuery/index.html b/apps/demos/Demos/DataGrid/BatchUpdateRequest/jQuery/index.html index 5bfc9fb29a7c..9538dfc54813 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..fa59a3932013 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/shared/jQuery/anti-forgery.js b/apps/demos/shared/jQuery/anti-forgery.js new file mode 100644 index 000000000000..39a2648d3a82 --- /dev/null +++ b/apps/demos/shared/jQuery/anti-forgery.js @@ -0,0 +1,62 @@ +const orig$ = $; + +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(); + + $ = orig$; + + $.ajax = (url, options) => { + if (typeof url !== 'string') { + 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); + }; +} + + +$ = (...args) => orig$(async () => { + await setAntiForgery(); + + return $(...args); +}) From 2f39a44e08719f27a84092f0cca5a35d17cd0d4a Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Tue, 27 Jan 2026 22:51:33 +0100 Subject: [PATCH 02/14] move anti-forgery to shared for Vue demos --- .../DataGrid/BatchUpdateRequest/Vue/App.vue | 81 +++---------------- .../DataGrid/BatchUpdateRequest/Vue/index.ts | 1 + .../BatchUpdateRequest/jQuery/index.html | 2 +- apps/demos/shared/anti-forgery/frameworks.ts | 63 +++++++++++++++ .../jquery.js} | 0 5 files changed, 78 insertions(+), 69 deletions(-) create mode 100644 apps/demos/shared/anti-forgery/frameworks.ts rename apps/demos/shared/{jQuery/anti-forgery.js => anti-forgery/jquery.js} (100%) 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..ecd1a0ef1b0c 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 App from './App.vue'; +import '/shared/anti-forgery/frameworks.ts' 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 9538dfc54813..500cee0e6334 100644 --- a/apps/demos/Demos/DataGrid/BatchUpdateRequest/jQuery/index.html +++ b/apps/demos/Demos/DataGrid/BatchUpdateRequest/jQuery/index.html @@ -10,7 +10,7 @@ - + diff --git a/apps/demos/shared/anti-forgery/frameworks.ts b/apps/demos/shared/anti-forgery/frameworks.ts new file mode 100644 index 000000000000..b21f6ec1d9e0 --- /dev/null +++ b/apps/demos/shared/anti-forgery/frameworks.ts @@ -0,0 +1,63 @@ +import ajax from 'devextreme/core/utils/ajax'; + +const sendRequestOrig = ajax.sendRequest; +const fetchOrig = fetch; +const BASE_PATH = 'https://js.devexpress.com/Demos/NetCore'; + +ajax.sendRequest = (options) => { + options.xhrFields = { + withCredentials: true, + }; + + return sendRequestOrig(options); +} + +async function fetchAntiForgeryToken(): Promise<{ headerName: string; token: string }> { + 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(): 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; +} + +window.fetch = async (url, options) => { + const { headerName, token } = await getAntiForgeryTokenValue(); + + options.headers = { + [headerName]: token, + ...(options.headers || {}) + }; + + options.credentials = 'include'; + + return await fetchOrig(url, options); +} \ No newline at end of file diff --git a/apps/demos/shared/jQuery/anti-forgery.js b/apps/demos/shared/anti-forgery/jquery.js similarity index 100% rename from apps/demos/shared/jQuery/anti-forgery.js rename to apps/demos/shared/anti-forgery/jquery.js From cfaf4a3db191d16f81e48ffbb40d43cdc2b48c82 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Tue, 27 Jan 2026 23:07:16 +0100 Subject: [PATCH 03/14] move anti-forgery to shared for Angular demo --- .../Angular/app/app.component.ts | 9 +- .../Angular/app/app.service.ts | 104 ------------------ apps/demos/shared/anti-forgery/frameworks.ts | 2 +- 3 files changed, 4 insertions(+), 111 deletions(-) delete mode 100644 apps/demos/Demos/DataGrid/BatchUpdateRequest/Angular/app/app.service.ts 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..39430a6bcd3f 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 '/shared/anti-forgery/frameworks.ts'; 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/shared/anti-forgery/frameworks.ts b/apps/demos/shared/anti-forgery/frameworks.ts index b21f6ec1d9e0..a540be017fb4 100644 --- a/apps/demos/shared/anti-forgery/frameworks.ts +++ b/apps/demos/shared/anti-forgery/frameworks.ts @@ -49,7 +49,7 @@ async function getAntiForgeryTokenValue(): Promise<{ headerName: string; token: return tokenData; } -window.fetch = async (url, options) => { +window.fetch = async (url, options = {}) => { const { headerName, token } = await getAntiForgeryTokenValue(); options.headers = { From 6927ead17d47d9e43a950e12a9a67168dd4cab77 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Wed, 28 Jan 2026 00:18:47 +0100 Subject: [PATCH 04/14] move anti-forgery to shared for React demo --- .../DataGrid/BatchUpdateRequest/React/App.tsx | 73 ++++--------------- .../BatchUpdateRequest/React/index.tsx | 2 +- .../BatchUpdateRequest/ReactJs/App.js | 70 ++++-------------- .../BatchUpdateRequest/ReactJs/index.js | 1 + .../DataGrid/BatchUpdateRequest/Vue/index.ts | 2 +- 5 files changed, 31 insertions(+), 117 deletions(-) diff --git a/apps/demos/Demos/DataGrid/BatchUpdateRequest/React/App.tsx b/apps/demos/Demos/DataGrid/BatchUpdateRequest/React/App.tsx index 0cd9c8bf553e..7b3caac0a140 100644 --- a/apps/demos/Demos/DataGrid/BatchUpdateRequest/React/App.tsx +++ b/apps/demos/Demos/DataGrid/BatchUpdateRequest/React/App.tsx @@ -7,51 +7,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 +42,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..59ea2fa7453c 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 '/shared/anti-forgery/frameworks.ts'; 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..c0212bda7a07 100644 --- a/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/App.js +++ b/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/App.js @@ -5,48 +5,13 @@ 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 +39,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..1c18e8a4709c 100644 --- a/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/index.js +++ b/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/index.js @@ -1,5 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import '/shared/anti-forgery/frameworks.ts'; import App from './App.js'; ReactDOM.render(, document.getElementById('app')); diff --git a/apps/demos/Demos/DataGrid/BatchUpdateRequest/Vue/index.ts b/apps/demos/Demos/DataGrid/BatchUpdateRequest/Vue/index.ts index ecd1a0ef1b0c..200d86999268 100644 --- a/apps/demos/Demos/DataGrid/BatchUpdateRequest/Vue/index.ts +++ b/apps/demos/Demos/DataGrid/BatchUpdateRequest/Vue/index.ts @@ -1,5 +1,5 @@ import { createApp } from 'vue'; +import '/shared/anti-forgery/frameworks.ts'; import App from './App.vue'; -import '/shared/anti-forgery/frameworks.ts' createApp(App).mount('#app'); From e23bf9093c9dcedacdedc26e58d6e7417fb3de2a Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Wed, 28 Jan 2026 10:55:12 +0100 Subject: [PATCH 05/14] use anti-forgery by map systemjs --- .../BatchUpdateRequest/Angular/app/app.component.ts | 2 +- .../demos/Demos/DataGrid/BatchUpdateRequest/React/index.tsx | 2 +- .../Demos/DataGrid/BatchUpdateRequest/ReactJs/index.js | 2 +- apps/demos/Demos/DataGrid/BatchUpdateRequest/Vue/index.ts | 2 +- .../shared/anti-forgery/{frameworks.ts => frameworks.js} | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) rename apps/demos/shared/anti-forgery/{frameworks.ts => frameworks.js} (86%) 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 39430a6bcd3f..fa81f09c8f4e 100644 --- a/apps/demos/Demos/DataGrid/BatchUpdateRequest/Angular/app/app.component.ts +++ b/apps/demos/Demos/DataGrid/BatchUpdateRequest/Angular/app/app.component.ts @@ -4,7 +4,7 @@ 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 '/shared/anti-forgery/frameworks.ts'; +import '/shared/anti-forgery/frameworks.js'; if (!/localhost/.test(document.location.host)) { enableProdMode(); diff --git a/apps/demos/Demos/DataGrid/BatchUpdateRequest/React/index.tsx b/apps/demos/Demos/DataGrid/BatchUpdateRequest/React/index.tsx index 59ea2fa7453c..d73f247780c5 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 '/shared/anti-forgery/frameworks.ts'; +import '/shared/anti-forgery/frameworks.js'; import App from './App.tsx'; ReactDOM.render( diff --git a/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/index.js b/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/index.js index 1c18e8a4709c..e417faa2feb3 100644 --- a/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/index.js +++ b/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/index.js @@ -1,6 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import '/shared/anti-forgery/frameworks.ts'; +import '/shared/anti-forgery/frameworks.js'; import App from './App.js'; ReactDOM.render(, document.getElementById('app')); diff --git a/apps/demos/Demos/DataGrid/BatchUpdateRequest/Vue/index.ts b/apps/demos/Demos/DataGrid/BatchUpdateRequest/Vue/index.ts index 200d86999268..16830f531753 100644 --- a/apps/demos/Demos/DataGrid/BatchUpdateRequest/Vue/index.ts +++ b/apps/demos/Demos/DataGrid/BatchUpdateRequest/Vue/index.ts @@ -1,5 +1,5 @@ import { createApp } from 'vue'; -import '/shared/anti-forgery/frameworks.ts'; +import 'anti-forgery'; import App from './App.vue'; createApp(App).mount('#app'); diff --git a/apps/demos/shared/anti-forgery/frameworks.ts b/apps/demos/shared/anti-forgery/frameworks.js similarity index 86% rename from apps/demos/shared/anti-forgery/frameworks.ts rename to apps/demos/shared/anti-forgery/frameworks.js index a540be017fb4..d97f87de95a2 100644 --- a/apps/demos/shared/anti-forgery/frameworks.ts +++ b/apps/demos/shared/anti-forgery/frameworks.js @@ -12,7 +12,7 @@ ajax.sendRequest = (options) => { return sendRequestOrig(options); } -async function fetchAntiForgeryToken(): Promise<{ headerName: string; token: string }> { +async function fetchAntiForgeryToken() { try { const response = await fetchOrig(`${BASE_PATH}/api/Common/GetAntiForgeryToken`, { method: 'GET', @@ -32,8 +32,8 @@ async function fetchAntiForgeryToken(): Promise<{ headerName: string; token: str } } -async function getAntiForgeryTokenValue(): Promise<{ headerName: string; token: string }> { - const tokenMeta = document.querySelector('meta[name="csrf-token"]'); +async function getAntiForgeryTokenValue() { + const tokenMeta = document.querySelector('meta[name="csrf-token"]'); if (tokenMeta) { const headerName = tokenMeta.dataset.headerName || 'RequestVerificationToken'; const token = tokenMeta.getAttribute('content') || ''; From ea6a6e94a60f0f09c0e77286e3316ebcd29bf744 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Wed, 28 Jan 2026 11:57:54 +0100 Subject: [PATCH 06/14] use anti-forgery by map systemjs --- apps/demos/configs/Vue/config.js | 2 ++ 1 file changed, 2 insertions(+) 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', From 6cfe21504c8c992c6672c3c4cca0b06d0e8b8ea1 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Wed, 28 Jan 2026 13:55:01 +0100 Subject: [PATCH 07/14] use anti-forgery by map systemjs --- .../DataGrid/BatchUpdateRequest/Angular/app/app.component.ts | 2 +- apps/demos/Demos/DataGrid/BatchUpdateRequest/React/index.tsx | 2 +- apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/index.js | 2 +- apps/demos/configs/Angular/config.js | 2 ++ apps/demos/configs/React/config.js | 2 ++ apps/demos/configs/ReactJs/config.js | 2 ++ 6 files changed, 9 insertions(+), 3 deletions(-) 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 fa81f09c8f4e..ac0d7df56fc3 100644 --- a/apps/demos/Demos/DataGrid/BatchUpdateRequest/Angular/app/app.component.ts +++ b/apps/demos/Demos/DataGrid/BatchUpdateRequest/Angular/app/app.component.ts @@ -4,7 +4,7 @@ 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 '/shared/anti-forgery/frameworks.js'; +import 'anti-forgery'; if (!/localhost/.test(document.location.host)) { enableProdMode(); diff --git a/apps/demos/Demos/DataGrid/BatchUpdateRequest/React/index.tsx b/apps/demos/Demos/DataGrid/BatchUpdateRequest/React/index.tsx index d73f247780c5..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 '/shared/anti-forgery/frameworks.js'; +import 'anti-forgery'; import App from './App.tsx'; ReactDOM.render( diff --git a/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/index.js b/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/index.js index e417faa2feb3..b5193c1ef3a3 100644 --- a/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/index.js +++ b/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/index.js @@ -1,6 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import '/shared/anti-forgery/frameworks.js'; +import 'anti-forgery'; import App from './App.js'; ReactDOM.render(, document.getElementById('app')); 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', From af8f66c67cb1d54b4f2273b90f3c900a87f31e81 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Wed, 28 Jan 2026 23:13:35 +0100 Subject: [PATCH 08/14] fix lint --- .../DataGrid/BatchUpdateRequest/React/App.tsx | 1 - .../BatchUpdateRequest/ReactJs/App.js | 1 - apps/demos/shared/anti-forgery/frameworks.js | 94 ++++++++-------- apps/demos/shared/anti-forgery/jquery.js | 101 +++++++++--------- 4 files changed, 99 insertions(+), 98 deletions(-) diff --git a/apps/demos/Demos/DataGrid/BatchUpdateRequest/React/App.tsx b/apps/demos/Demos/DataGrid/BatchUpdateRequest/React/App.tsx index 7b3caac0a140..cb26fdc63b9c 100644 --- a/apps/demos/Demos/DataGrid/BatchUpdateRequest/React/App.tsx +++ b/apps/demos/Demos/DataGrid/BatchUpdateRequest/React/App.tsx @@ -6,7 +6,6 @@ import 'whatwg-fetch'; const BASE_PATH = 'https://js.devexpress.com/Demos/NetCore'; const URL = `${BASE_PATH}/api/DataGridBatchUpdateWebApi`; - const ordersStore = createStore({ key: 'OrderID', loadUrl: `${URL}/Orders`, diff --git a/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/App.js b/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/App.js index c0212bda7a07..cae1fd61bea9 100644 --- a/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/App.js +++ b/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/App.js @@ -5,7 +5,6 @@ import 'whatwg-fetch'; const BASE_PATH = 'https://js.devexpress.com/Demos/NetCore'; const URL = `${BASE_PATH}/api/DataGridBatchUpdateWebApi`; - const ordersStore = createStore({ key: 'OrderID', loadUrl: `${URL}/Orders`, diff --git a/apps/demos/shared/anti-forgery/frameworks.js b/apps/demos/shared/anti-forgery/frameworks.js index d97f87de95a2..417e3cb7e1d0 100644 --- a/apps/demos/shared/anti-forgery/frameworks.js +++ b/apps/demos/shared/anti-forgery/frameworks.js @@ -5,59 +5,59 @@ const fetchOrig = fetch; const BASE_PATH = 'https://js.devexpress.com/Demos/NetCore'; ajax.sendRequest = (options) => { - options.xhrFields = { - withCredentials: true, - }; - - return sendRequestOrig(options); -} + options.xhrFields = { + withCredentials: true, + }; + + return sendRequestOrig(options); +}; 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); + 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; + 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; } window.fetch = async (url, options = {}) => { - const { headerName, token } = await getAntiForgeryTokenValue(); - - options.headers = { - [headerName]: token, - ...(options.headers || {}) - }; - - options.credentials = 'include'; - - return await fetchOrig(url, options); -} \ No newline at end of file + 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 index 39a2648d3a82..ba7083084a37 100644 --- a/apps/demos/shared/anti-forgery/jquery.js +++ b/apps/demos/shared/anti-forgery/jquery.js @@ -1,62 +1,65 @@ +/* global $ */ const orig$ = $; 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(); + 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 }); - } + 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; - }); + 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(); - - $ = orig$; - - $.ajax = (url, options) => { - if (typeof url !== 'string') { - 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); - }; -} + 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); + }; +} +// eslint-disable-next-line no-global-assign $ = (...args) => orig$(async () => { - await setAntiForgery(); - - return $(...args); -}) + await setAntiForgery(); + + return $(...args); +}); From 127542bcd73fee7d46896bcbc36dd8b162f78a5a Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Thu, 29 Jan 2026 00:01:02 +0100 Subject: [PATCH 09/14] fix lint --- apps/demos/Demos/DataGrid/BatchUpdateRequest/jQuery/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/demos/Demos/DataGrid/BatchUpdateRequest/jQuery/index.js b/apps/demos/Demos/DataGrid/BatchUpdateRequest/jQuery/index.js index fa59a3932013..43765cdbae33 100644 --- a/apps/demos/Demos/DataGrid/BatchUpdateRequest/jQuery/index.js +++ b/apps/demos/Demos/DataGrid/BatchUpdateRequest/jQuery/index.js @@ -1,5 +1,5 @@ $(async () => { - const URL = `https://js.devexpress.com/Demos/NetCore/api/DataGridBatchUpdateWebApi`; + const URL = 'https://js.devexpress.com/Demos/NetCore/api/DataGridBatchUpdateWebApi'; $('#gridContainer').dxDataGrid({ dataSource: DevExpress.data.AspNet.createStore({ From ea8601627499bdc206fecfc647ef2dd3fb2e82b1 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Thu, 29 Jan 2026 00:20:12 +0100 Subject: [PATCH 10/14] fix ReactJs demo --- apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/index.js b/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/index.js index b5193c1ef3a3..104d91ea2671 100644 --- a/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/index.js +++ b/apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/index.js @@ -1,5 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; +// eslint-disable-next-line import/no-unresolved import 'anti-forgery'; import App from './App.js'; From 2d12f4a24c133eb650447cc6f38554bec7be4fa1 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Thu, 29 Jan 2026 22:59:36 +0100 Subject: [PATCH 11/14] insert anti-forgery checking in devextreme ajax --- .../Angular/app/app.component.ts | 1 + .../FileUploading/React/index.tsx | 1 + .../FileUploader/FileUploading/Vue/index.ts | 1 + .../FileUploading/jQuery/index.html | 1 + apps/demos/shared/anti-forgery/frameworks.js | 38 +++++++++++++++---- apps/demos/shared/anti-forgery/jquery.js | 18 ++++++++- 6 files changed, 51 insertions(+), 9 deletions(-) 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/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/shared/anti-forgery/frameworks.js b/apps/demos/shared/anti-forgery/frameworks.js index 417e3cb7e1d0..b426e86fe59e 100644 --- a/apps/demos/shared/anti-forgery/frameworks.js +++ b/apps/demos/shared/anti-forgery/frameworks.js @@ -1,17 +1,10 @@ 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'; -ajax.sendRequest = (options) => { - options.xhrFields = { - withCredentials: true, - }; - - return sendRequestOrig(options); -}; - async function fetchAntiForgeryToken() { try { const response = await fetchOrig(`${BASE_PATH}/api/Common/GetAntiForgeryToken`, { @@ -49,6 +42,35 @@ async function getAntiForgeryTokenValue() { 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(); diff --git a/apps/demos/shared/anti-forgery/jquery.js b/apps/demos/shared/anti-forgery/jquery.js index ba7083084a37..0fc65ed0c3e4 100644 --- a/apps/demos/shared/anti-forgery/jquery.js +++ b/apps/demos/shared/anti-forgery/jquery.js @@ -1,5 +1,6 @@ -/* global $ */ +/* global $, DevExpress */ const orig$ = $; +const ajaxSendRequestOrig = DevExpress.utils.ajax.sendRequest; function fetchAntiForgeryToken() { const d = orig$.Deferred(); @@ -50,11 +51,26 @@ async function setAntiForgery() { } 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(...args); + } } // eslint-disable-next-line no-global-assign From a7af0a97174cc81415b50b91c711f01158613cfb Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Fri, 30 Jan 2026 00:17:42 +0100 Subject: [PATCH 12/14] regenerate and fix lint --- .../Demos/FileUploader/FileUploading/ReactJs/index.js | 1 + apps/demos/shared/anti-forgery/frameworks.js | 4 ++-- apps/demos/shared/anti-forgery/jquery.js | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) 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/shared/anti-forgery/frameworks.js b/apps/demos/shared/anti-forgery/frameworks.js index b426e86fe59e..8b6d0cafdd7d 100644 --- a/apps/demos/shared/anti-forgery/frameworks.js +++ b/apps/demos/shared/anti-forgery/frameworks.js @@ -48,7 +48,7 @@ ajax.sendRequest = (options) => { getAntiForgeryTokenValue().then(({ headerName, token }) => { options.headers = { [headerName]: token, - ...(options.headers || {}) + ...(options.headers || {}), }; options.xhrFields = { @@ -66,7 +66,7 @@ ajax.sendRequest = (options) => { }, (e) => deferred.reject(e), ); - }) + }); return deferred.promise(); }; diff --git a/apps/demos/shared/anti-forgery/jquery.js b/apps/demos/shared/anti-forgery/jquery.js index 0fc65ed0c3e4..62538e84522a 100644 --- a/apps/demos/shared/anti-forgery/jquery.js +++ b/apps/demos/shared/anti-forgery/jquery.js @@ -51,7 +51,7 @@ async function setAntiForgery() { } else { options.url = url; } - + options.headers = { [tokenData.headerName]: tokenData.token, ...(options.headers || {}) }; options.xhrFields = { withCredentials: true, ...(options.xhrFields || {}) }; @@ -61,16 +61,16 @@ async function setAntiForgery() { DevExpress.utils.ajax.sendRequest = (options) => { options.headers = { [tokenData.headerName]: tokenData.token, - ...(options.headers || {}) + ...(options.headers || {}), }; options.xhrFields = { withCredentials: true, - ...(options.xhrFields || {}) + ...(options.xhrFields || {}), }; return ajaxSendRequestOrig(...args); - } + }; } // eslint-disable-next-line no-global-assign From ecdcd218172a1ce71385482847982178c8fe7be4 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Fri, 30 Jan 2026 00:38:07 +0100 Subject: [PATCH 13/14] fix anti-forgery for jquery --- apps/demos/shared/anti-forgery/jquery.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/demos/shared/anti-forgery/jquery.js b/apps/demos/shared/anti-forgery/jquery.js index 62538e84522a..f5a9ef8d99ac 100644 --- a/apps/demos/shared/anti-forgery/jquery.js +++ b/apps/demos/shared/anti-forgery/jquery.js @@ -69,7 +69,7 @@ async function setAntiForgery() { ...(options.xhrFields || {}), }; - return ajaxSendRequestOrig(...args); + return ajaxSendRequestOrig(options); }; } From ae49d0323c14fd7970f804bdabafc65c518c70c9 Mon Sep 17 00:00:00 2001 From: Aliullov Vlad Date: Fri, 30 Jan 2026 10:00:31 +0100 Subject: [PATCH 14/14] fix lint in ReactJs demo --- apps/demos/Demos/FileUploader/FileUploading/ReactJs/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/demos/Demos/FileUploader/FileUploading/ReactJs/index.js b/apps/demos/Demos/FileUploader/FileUploading/ReactJs/index.js index b5193c1ef3a3..104d91ea2671 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'; +// eslint-disable-next-line import/no-unresolved import 'anti-forgery'; import App from './App.js';