Skip to content

Commit b4a98d3

Browse files
committed
Refresh antiforgery token if not valid. Trigger UnhandledHydroError when issues with navigation.
1 parent da50022 commit b4a98d3

File tree

5 files changed

+65
-28
lines changed

5 files changed

+65
-28
lines changed

docs/content/features/events.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ This is fine in most scenarios when dispatching is not the only one operation we
7878
But sometimes your only intent is to dispatch an event:
7979

8080
```csharp
81-
// ProductList.cshtml.cs~~~~
81+
// ProductList.cshtml.cs
8282
8383
public class ProductList : HydroComponent
8484
{
@@ -104,7 +104,7 @@ Now, after clicking the button, the event `OpenAddModal` will be triggered witho
104104
Another way to avoid the extra render of the component is to use `[SkipOutput]` attribute on the Hydro action:
105105

106106
```csharp
107-
// ProductList.cshtml.cs~~~~
107+
// ProductList.cshtml.cs
108108
109109
public class ProductList : HydroComponent
110110
{

docs/content/features/form-validation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public class Counter(IValidator<Counter> validator) : HydroComponent
9595
// HydroValidationExtensions.cs
9696
9797
public static class HydroValidationExtensions
98-
{
98+
{~~~~
9999
public static bool Validate<TComponent>(this TComponent component, IValidator<TComponent> validator) where TComponent : HydroComponent
100100
{
101101
component.IsModelTouched = true;

src/HydroComponentsExtensions.cs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Microsoft.AspNetCore.Routing;
1414
using Microsoft.Extensions.DependencyInjection;
1515
using Hydro.Configuration;
16+
using Microsoft.Extensions.Logging;
1617
using Newtonsoft.Json;
1718

1819
namespace Hydro;
@@ -23,13 +24,31 @@ public static void MapHydroComponent(this IEndpointRouteBuilder app, Type compon
2324
{
2425
var componentName = componentType.Name;
2526

26-
app.MapPost($"/hydro/{componentName}/{{method?}}", async ([FromServices] IServiceProvider serviceProvider, [FromServices] IViewComponentHelper viewComponentHelper, [FromServices] HydroOptions hydroOptions, [FromServices] IAntiforgery antiforgery, HttpContext httpContext, string method) =>
27+
app.MapPost($"/hydro/{componentName}/{{method?}}", async (
28+
[FromServices] IServiceProvider serviceProvider,
29+
[FromServices] IViewComponentHelper viewComponentHelper,
30+
[FromServices] HydroOptions hydroOptions,
31+
[FromServices] IAntiforgery antiforgery,
32+
[FromServices] ILogger<HydroComponent> logger,
33+
HttpContext httpContext,
34+
string method
35+
) =>
2736
{
2837
if (hydroOptions.AntiforgeryTokenEnabled)
2938
{
30-
await antiforgery.ValidateRequestAsync(httpContext);
39+
try
40+
{
41+
await antiforgery.ValidateRequestAsync(httpContext);
42+
}
43+
catch (AntiforgeryValidationException exception)
44+
{
45+
logger.LogWarning(exception, "Antiforgery token not valid");
46+
var requestToken = antiforgery.GetTokens(httpContext).RequestToken;
47+
httpContext.Response.Headers.Add(HydroConsts.ResponseHeaders.RefreshToken, requestToken);
48+
return Results.BadRequest(new { token = requestToken });
49+
}
3150
}
32-
51+
3352
if (httpContext.IsHydro())
3453
{
3554
await ExecuteRequestOperations(httpContext, method);
@@ -40,7 +59,7 @@ public static void MapHydroComponent(this IEndpointRouteBuilder app, Type compon
4059
var htmlContent = await viewComponentHelper.InvokeAsync(componentType);
4160

4261
var content = await GetHtml(htmlContent);
43-
62+
4463
return Results.Content(content, MediaTypeNames.Text.Html);
4564
});
4665
}

src/HydroConsts.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public static class ResponseHeaders
2020
public const string Trigger = "Hydro-Trigger";
2121
public const string OperationId = "Hydro-Operation-Id";
2222
public const string SkipOutput = "Hydro-Skip-Output";
23+
public const string RefreshToken = "Refresh-Antiforgery-Token";
2324
}
2425

2526
public static class ContextItems

src/Scripts/hydro.js

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@
2828
}
2929

3030
if (!response.ok) {
31+
const eventDetail = {
32+
message: "Problem with loading the content",
33+
data: null
34+
}
35+
document.dispatchEvent(new CustomEvent(`global:UnhandledHydroError`, { detail: { data: eventDetail } }));
3136
throw new Error(`HTTP error! status: ${response.status}`);
3237
} else {
3338
let data = await response.text();
@@ -102,7 +107,7 @@
102107
const body = JSON.stringify(eventData.data);
103108
const hydroEvent = el.getAttribute("x-on-hydro-event");
104109
const wireEventData = JSON.parse(hydroEvent);
105-
await hydroRequest(el, url, 'application/json', body, null, wireEventData, operationId);
110+
await hydroRequest(el, url, 'application/json', body, 'event', wireEventData, operationId);
106111
}
107112

108113
async function hydroBind(el) {
@@ -130,11 +135,11 @@
130135
const bindAlreadyInitialized = [...binding[component.id].formData].length !== 0;
131136

132137
binding[component.id].formData.set(propertyName, value);
133-
134-
if (bindAlreadyInitialized){
138+
139+
if (bindAlreadyInitialized) {
135140
return Promise.resolve();
136141
}
137-
142+
138143
if (binding[component.id].timeout) {
139144
clearTimeout(binding[component.id].timeout);
140145
}
@@ -258,23 +263,35 @@
258263
});
259264

260265
if (!response.ok) {
261-
const contentType = response.headers.get("content-type");
262-
let eventDetail = {};
263-
264-
try {
265-
if (contentType && contentType.indexOf("application/json") !== -1) {
266-
const json = await response.json();
267-
eventDetail.message = json?.message || "Unhandled exception";
268-
eventDetail.data = json?.data;
269-
} else {
270-
eventDetail.message = await response.text();
266+
if (response.status === 400 && config.Antiforgery && response.headers.get("Refresh-Antiforgery-Token")) {
267+
const json = await response.json();
268+
config.Antiforgery.Token = json.token;
269+
} else if (response.status === 403) {
270+
if (type !== 'event') {
271+
document.dispatchEvent(new CustomEvent(`global:UnhandledHydroError`, { detail: { data: { message: "Unauthorized access" } } }));
272+
}
273+
throw new Error(`HTTP error! status: ${response.status}`);
274+
} else {
275+
const contentType = response.headers.get("content-type");
276+
let eventDetail = {};
277+
278+
try {
279+
if (contentType && contentType.indexOf("application/json") !== -1) {
280+
const json = await response.json();
281+
eventDetail.message = json?.message || "Unhandled exception";
282+
eventDetail.data = json?.data;
283+
} else {
284+
eventDetail.message = await response.text();
285+
}
286+
} catch {
287+
// ignore
271288
}
272-
} catch {
273-
// ignore
274-
}
275289

276-
document.dispatchEvent(new CustomEvent(`global:UnhandledHydroError`, { detail: { data: eventDetail } }));
277-
throw new Error(`HTTP error! status: ${response.status}`);
290+
if (type !== 'event') {
291+
document.dispatchEvent(new CustomEvent(`global:UnhandledHydroError`, { detail: { data: eventDetail } }));
292+
}
293+
throw new Error(`HTTP error! status: ${response.status}`);
294+
}
278295
} else {
279296
const skipOutputHeader = response.headers.get('Hydro-Skip-Output');
280297

@@ -519,10 +536,10 @@ document.addEventListener('alpine:init', () => {
519536
let timeout = 0;
520537

521538
const eventHandler = async (event) => {
522-
if (event === 'submit' || event === 'click'){
539+
if (event === 'submit' || event === 'click') {
523540
event.preventDefault();
524541
}
525-
542+
526543
clearTimeout(timeout);
527544

528545
timeout = setTimeout(async () => {

0 commit comments

Comments
 (0)