Skip to content

Commit bb94682

Browse files
Demos | DataGrid - BatchUpdateRequest: Support BackEnd / EndPoint Anti Forgery (all frameworks) (#32057)
Co-authored-by: Tom <[email protected]>
1 parent 35e9a2d commit bb94682

File tree

8 files changed

+426
-84
lines changed

8 files changed

+426
-84
lines changed

apps/demos/Demos/DataGrid/BatchUpdateRequest/Angular/app/app.component.ts

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import { bootstrapApplication } from '@angular/platform-browser';
22
import { Component, enableProdMode, provideZoneChangeDetection } from '@angular/core';
3-
import { HttpClient, provideHttpClient, withFetch } from '@angular/common/http';
3+
import { HttpClient, provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
44
import { lastValueFrom } from 'rxjs';
55
import * as AspNetData from 'devextreme-aspnet-data-nojquery';
66
import { DxDataGridComponent, DxDataGridModule, DxDataGridTypes } from 'devextreme-angular/ui/data-grid';
7+
import { antiForgeryInterceptor, AntiForgeryTokenService } from './app.service';
78

89
if (!/localhost/.test(document.location.host)) {
910
enableProdMode();
1011
}
1112

12-
const URL = 'https://js.devexpress.com/Demos/NetCore/api/DataGridBatchUpdateWebApi';
13+
const BASE_PATH = 'https://js.devexpress.com/Demos/NetCore';
14+
const URL = `${BASE_PATH}/api/DataGridBatchUpdateWebApi`;
1315

1416
let modulePrefix = '';
1517
// @ts-ignore
@@ -28,12 +30,16 @@ if (window && window.config?.packageConfigPaths) {
2830
export class AppComponent {
2931
ordersStore: AspNetData.CustomStore;
3032

31-
constructor(private http: HttpClient) {
33+
constructor(private http: HttpClient, private tokenService: AntiForgeryTokenService) {
3234
this.ordersStore = AspNetData.createStore({
3335
key: 'OrderID',
3436
loadUrl: `${URL}/Orders`,
35-
onBeforeSend(method, ajaxOptions) {
36-
ajaxOptions.xhrFields = { withCredentials: true };
37+
async onBeforeSend(_method, ajaxOptions) {
38+
const tokenData = await lastValueFrom(tokenService.getToken());
39+
ajaxOptions.xhrFields = {
40+
withCredentials: true,
41+
headers: { [tokenData.headerName]: tokenData.token },
42+
};
3743
},
3844
});
3945
}
@@ -52,16 +58,23 @@ export class AppComponent {
5258
changes: DxDataGridTypes.DataChange[],
5359
component: DxDataGridComponent['instance'],
5460
): Promise<void> {
55-
await lastValueFrom(
56-
this.http.post(url, JSON.stringify(changes), {
57-
withCredentials: true,
58-
headers: {
59-
'Content-Type': 'application/json',
60-
},
61-
}),
62-
);
63-
await component.refresh(true);
64-
component.cancelEditData();
61+
try {
62+
await lastValueFrom(
63+
this.http.post(url, JSON.stringify(changes), {
64+
withCredentials: true,
65+
headers: {
66+
'Content-Type': 'application/json',
67+
},
68+
}),
69+
);
70+
await component.refresh(true);
71+
component.cancelEditData();
72+
} catch (error: any) {
73+
const errorMessage = (typeof error?.error === 'string' && error.error)
74+
? error.error
75+
: (error?.statusText || 'Unknown error');
76+
throw new Error(`Batch save failed: ${errorMessage}`);
77+
}
6578
}
6679

6780
normalizeChanges(changes: DxDataGridTypes.DataChange[]): DxDataGridTypes.DataChange[] {
@@ -93,6 +106,9 @@ export class AppComponent {
93106
bootstrapApplication(AppComponent, {
94107
providers: [
95108
provideZoneChangeDetection({ eventCoalescing: true, runCoalescing: true }),
96-
provideHttpClient(withFetch()),
109+
provideHttpClient(
110+
withFetch(),
111+
withInterceptors([antiForgeryInterceptor]),
112+
),
97113
],
98114
});
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { Injectable, inject } from '@angular/core';
2+
import { HttpClient, HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
3+
import { Observable, of, throwError } from 'rxjs';
4+
import { catchError, switchMap, map, shareReplay } from 'rxjs/operators';
5+
6+
interface TokenData {
7+
headerName: string;
8+
token: string;
9+
}
10+
11+
@Injectable({
12+
providedIn: 'root',
13+
})
14+
export class AntiForgeryTokenService {
15+
private BASE_PATH = 'https://js.devexpress.com/Demos/NetCore';
16+
17+
private tokenCache$: Observable<TokenData> | null = null;
18+
19+
constructor(private http: HttpClient) {}
20+
21+
getToken(): Observable<TokenData> {
22+
const tokenMeta = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]');
23+
if (tokenMeta) {
24+
const headerName = tokenMeta.dataset.headerName || 'RequestVerificationToken';
25+
const token = tokenMeta.getAttribute('content') || '';
26+
return of({ headerName, token });
27+
}
28+
29+
if (!this.tokenCache$) {
30+
this.tokenCache$ = this.fetchToken().pipe(
31+
map((tokenData) => {
32+
this.storeTokenInMeta(tokenData);
33+
return tokenData;
34+
}),
35+
shareReplay({ bufferSize: 1, refCount: false }),
36+
catchError((error) => {
37+
this.tokenCache$ = null;
38+
return throwError(() => error);
39+
}),
40+
);
41+
}
42+
43+
return this.tokenCache$;
44+
}
45+
46+
private fetchToken(): Observable<TokenData> {
47+
return this.http.get<TokenData>(
48+
`${this.BASE_PATH}/api/Common/GetAntiForgeryToken`,
49+
{
50+
withCredentials: true,
51+
},
52+
).pipe(
53+
catchError((error) => {
54+
const errorMessage = typeof error.error === 'string' ? error.error : (error.statusText || 'Unknown error');
55+
return throwError(() => new Error(`Failed to retrieve anti-forgery token: ${errorMessage}`));
56+
}),
57+
);
58+
}
59+
60+
private storeTokenInMeta(tokenData: TokenData): void {
61+
const meta = document.createElement('meta');
62+
meta.name = 'csrf-token';
63+
meta.content = tokenData.token;
64+
meta.dataset.headerName = tokenData.headerName;
65+
document.head.appendChild(meta);
66+
}
67+
68+
clearToken(): void {
69+
this.tokenCache$ = null;
70+
const tokenMeta = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]');
71+
if (tokenMeta) {
72+
tokenMeta.remove();
73+
}
74+
}
75+
}
76+
77+
export const antiForgeryInterceptor: HttpInterceptorFn = (req, next) => {
78+
const tokenService = inject(AntiForgeryTokenService);
79+
80+
if (req.method === 'GET' && req.url.includes('/GetAntiForgeryToken')) {
81+
return next(req);
82+
}
83+
84+
if (req.method !== 'GET') {
85+
return tokenService.getToken().pipe(
86+
switchMap((tokenData) => {
87+
const clonedRequest = req.clone({
88+
setHeaders: {
89+
[tokenData.headerName]: tokenData.token,
90+
},
91+
});
92+
return next(clonedRequest);
93+
}),
94+
catchError((error: HttpErrorResponse) => {
95+
if (error.status === 401 || error.status === 403) {
96+
tokenService.clearToken();
97+
}
98+
return throwError(() => error);
99+
}),
100+
);
101+
}
102+
103+
return next(req);
104+
};

apps/demos/Demos/DataGrid/BatchUpdateRequest/React/App.tsx

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,55 @@ import type { DataGridRef, DataGridTypes } from 'devextreme-react/data-grid';
44
import { createStore } from 'devextreme-aspnet-data-nojquery';
55
import 'whatwg-fetch';
66

7-
const URL = 'https://js.devexpress.com/Demos/NetCore/api/DataGridBatchUpdateWebApi';
7+
const BASE_PATH = 'https://js.devexpress.com/Demos/NetCore';
8+
const URL = `${BASE_PATH}/api/DataGridBatchUpdateWebApi`;
9+
10+
async function fetchAntiForgeryToken(): Promise<{ headerName: string; token: string }> {
11+
try {
12+
const response = await fetch(`${BASE_PATH}/api/Common/GetAntiForgeryToken`, {
13+
method: 'GET',
14+
credentials: 'include',
15+
cache: 'no-cache',
16+
});
17+
18+
if (!response.ok) {
19+
const errorMessage = await response.text();
20+
throw new Error(`Failed to retrieve anti-forgery token: ${errorMessage || response.statusText}`);
21+
}
22+
23+
return await response.json();
24+
} catch (error) {
25+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
26+
throw new Error(errorMessage);
27+
}
28+
}
29+
30+
async function getAntiForgeryTokenValue(): Promise<{ headerName: string; token: string }> {
31+
const tokenMeta = document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]');
32+
if (tokenMeta) {
33+
const headerName = tokenMeta.dataset.headerName || 'RequestVerificationToken';
34+
const token = tokenMeta.getAttribute('content') || '';
35+
return Promise.resolve({ headerName, token });
36+
}
37+
38+
const tokenData = await fetchAntiForgeryToken();
39+
const meta = document.createElement('meta');
40+
meta.name = 'csrf-token';
41+
meta.content = tokenData.token;
42+
meta.dataset.headerName = tokenData.headerName;
43+
document.head.appendChild(meta);
44+
return tokenData;
45+
}
846

947
const ordersStore = createStore({
1048
key: 'OrderID',
1149
loadUrl: `${URL}/Orders`,
12-
onBeforeSend: (method, ajaxOptions) => {
13-
ajaxOptions.xhrFields = { withCredentials: true };
50+
async onBeforeSend(_method, ajaxOptions) {
51+
const tokenData = await getAntiForgeryTokenValue();
52+
ajaxOptions.xhrFields = {
53+
withCredentials: true,
54+
headers: { [tokenData.headerName]: tokenData.token },
55+
};
1456
},
1557
});
1658

@@ -39,25 +81,31 @@ function normalizeChanges(changes: DataGridTypes.DataChange[]): DataGridTypes.Da
3981
}) as DataGridTypes.DataChange[];
4082
}
4183

42-
async function sendBatchRequest(url: string, changes: DataGridTypes.DataChange[]) {
43-
const result = await fetch(url, {
44-
method: 'POST',
45-
body: JSON.stringify(changes),
46-
headers: {
47-
'Content-Type': 'application/json;charset=UTF-8',
48-
},
49-
credentials: 'include',
50-
});
51-
52-
if (!result.ok) {
53-
const json = await result.json();
84+
async function sendBatchRequest(url: string, changes: DataGridTypes.DataChange[], headers: Record<string, string>) {
85+
try {
86+
const response = await fetch(url, {
87+
method: 'POST',
88+
body: JSON.stringify(changes),
89+
headers: {
90+
'Content-Type': 'application/json;charset=UTF-8',
91+
...headers,
92+
},
93+
credentials: 'include',
94+
});
5495

55-
throw json.Message;
96+
if (!response.ok) {
97+
const errorMessage = await response.text();
98+
throw new Error(`Batch save failed: ${errorMessage || response.statusText}`);
99+
}
100+
} catch (error) {
101+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
102+
throw new Error(errorMessage);
56103
}
57104
}
58105

59106
async function processBatchRequest(url: string, changes: DataGridTypes.DataChange[], component: ReturnType<DataGridRef['instance']>) {
60-
await sendBatchRequest(url, changes);
107+
const tokenData = await getAntiForgeryTokenValue();
108+
await sendBatchRequest(url, changes, { [tokenData.headerName]: tokenData.token });
61109
await component.refresh(true);
62110
component.cancelEditData();
63111
}

apps/demos/Demos/DataGrid/BatchUpdateRequest/ReactJs/App.js

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,51 @@ import DataGrid, { Column, Editing, Pager } from 'devextreme-react/data-grid';
33
import { createStore } from 'devextreme-aspnet-data-nojquery';
44
import 'whatwg-fetch';
55

6-
const URL = 'https://js.devexpress.com/Demos/NetCore/api/DataGridBatchUpdateWebApi';
6+
const BASE_PATH = 'https://js.devexpress.com/Demos/NetCore';
7+
const URL = `${BASE_PATH}/api/DataGridBatchUpdateWebApi`;
8+
async function fetchAntiForgeryToken() {
9+
try {
10+
const response = await fetch(`${BASE_PATH}/api/Common/GetAntiForgeryToken`, {
11+
method: 'GET',
12+
credentials: 'include',
13+
cache: 'no-cache',
14+
});
15+
if (!response.ok) {
16+
const errorMessage = await response.text();
17+
throw new Error(
18+
`Failed to retrieve anti-forgery token: ${errorMessage || response.statusText}`,
19+
);
20+
}
21+
return await response.json();
22+
} catch (error) {
23+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
24+
throw new Error(errorMessage);
25+
}
26+
}
27+
async function getAntiForgeryTokenValue() {
28+
const tokenMeta = document.querySelector('meta[name="csrf-token"]');
29+
if (tokenMeta) {
30+
const headerName = tokenMeta.dataset.headerName || 'RequestVerificationToken';
31+
const token = tokenMeta.getAttribute('content') || '';
32+
return Promise.resolve({ headerName, token });
33+
}
34+
const tokenData = await fetchAntiForgeryToken();
35+
const meta = document.createElement('meta');
36+
meta.name = 'csrf-token';
37+
meta.content = tokenData.token;
38+
meta.dataset.headerName = tokenData.headerName;
39+
document.head.appendChild(meta);
40+
return tokenData;
41+
}
742
const ordersStore = createStore({
843
key: 'OrderID',
944
loadUrl: `${URL}/Orders`,
10-
onBeforeSend: (method, ajaxOptions) => {
11-
ajaxOptions.xhrFields = { withCredentials: true };
45+
async onBeforeSend(_method, ajaxOptions) {
46+
const tokenData = await getAntiForgeryTokenValue();
47+
ajaxOptions.xhrFields = {
48+
withCredentials: true,
49+
headers: { [tokenData.headerName]: tokenData.token },
50+
};
1251
},
1352
});
1453
function normalizeChanges(changes) {
@@ -35,22 +74,29 @@ function normalizeChanges(changes) {
3574
}
3675
});
3776
}
38-
async function sendBatchRequest(url, changes) {
39-
const result = await fetch(url, {
40-
method: 'POST',
41-
body: JSON.stringify(changes),
42-
headers: {
43-
'Content-Type': 'application/json;charset=UTF-8',
44-
},
45-
credentials: 'include',
46-
});
47-
if (!result.ok) {
48-
const json = await result.json();
49-
throw json.Message;
77+
async function sendBatchRequest(url, changes, headers) {
78+
try {
79+
const response = await fetch(url, {
80+
method: 'POST',
81+
body: JSON.stringify(changes),
82+
headers: {
83+
'Content-Type': 'application/json;charset=UTF-8',
84+
...headers,
85+
},
86+
credentials: 'include',
87+
});
88+
if (!response.ok) {
89+
const errorMessage = await response.text();
90+
throw new Error(`Batch save failed: ${errorMessage || response.statusText}`);
91+
}
92+
} catch (error) {
93+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
94+
throw new Error(errorMessage);
5095
}
5196
}
5297
async function processBatchRequest(url, changes, component) {
53-
await sendBatchRequest(url, changes);
98+
const tokenData = await getAntiForgeryTokenValue();
99+
await sendBatchRequest(url, changes, { [tokenData.headerName]: tokenData.token });
54100
await component.refresh(true);
55101
component.cancelEditData();
56102
}

0 commit comments

Comments
 (0)