diff --git a/src/Eurofurence.App.Backoffice/Authentication/BackendAccountClaimsFactory.cs b/src/Eurofurence.App.Backoffice/Authentication/BackendAccountClaimsFactory.cs index 8e87345d..1ea97643 100644 --- a/src/Eurofurence.App.Backoffice/Authentication/BackendAccountClaimsFactory.cs +++ b/src/Eurofurence.App.Backoffice/Authentication/BackendAccountClaimsFactory.cs @@ -5,37 +5,45 @@ namespace Eurofurence.App.Backoffice.Authentication { - public class BackendAccountClaimsFactory : AccountClaimsPrincipalFactory - { - public IServiceProvider _serviceProvider { get; set; } - - public BackendAccountClaimsFactory(IServiceProvider serviceProvider, IAccessTokenProviderAccessor accessor) : base(accessor) - { - _serviceProvider = serviceProvider; - } - - public override async ValueTask CreateUserAsync(RemoteUserAccount account, RemoteAuthenticationUserOptions options) - { - var userAccount = await base.CreateUserAsync(account, options); - - if (!(userAccount.Identity?.IsAuthenticated ?? false)) - { - return userAccount; - } - - if (userAccount.Identity is ClaimsIdentity identity) - { - var usersService = _serviceProvider.GetRequiredService(); - var userRecord = await usersService.GetUserSelf(); - foreach (var role in userRecord.Roles) - { - identity.AddClaim(new Claim(identity.RoleClaimType, role)); - } - } - - return userAccount; - - } - } - + public class BackendAccountClaimsFactory : AccountClaimsPrincipalFactory + { + public IServiceProvider _serviceProvider { get; set; } + + public BackendAccountClaimsFactory(IServiceProvider serviceProvider, IAccessTokenProviderAccessor accessor) : + base(accessor) + { + _serviceProvider = serviceProvider; + } + + public override async ValueTask CreateUserAsync(RemoteUserAccount account, + RemoteAuthenticationUserOptions options) + { + var userAccount = await base.CreateUserAsync(account, options); + + if (!(userAccount.Identity?.IsAuthenticated ?? false)) + { + return userAccount; + } + + if (userAccount.Identity is ClaimsIdentity identity) + { + var usersService = _serviceProvider.GetRequiredService(); + try + { + var userRecord = await usersService.GetUserSelf(); + foreach (var role in userRecord.Roles) + { + identity.AddClaim(new Claim(identity.RoleClaimType, role)); + } + } + catch (AccessTokenNotAvailableException) + { + // No token, no user record + // AccessTokenNotAvailableException is ignored here because it will be handled by the TokenAuthorizationMessageHandler + } + } + + return userAccount; + } + } } \ No newline at end of file diff --git a/src/Eurofurence.App.Backoffice/Authentication/TokenAuthorizationMessageHandler.cs b/src/Eurofurence.App.Backoffice/Authentication/TokenAuthorizationMessageHandler.cs index 2c42e83d..f19c5e7c 100644 --- a/src/Eurofurence.App.Backoffice/Authentication/TokenAuthorizationMessageHandler.cs +++ b/src/Eurofurence.App.Backoffice/Authentication/TokenAuthorizationMessageHandler.cs @@ -1,13 +1,36 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.WebAssembly.Authentication; -namespace Eurofurence.App.Backoffice.Authentication +namespace Eurofurence.App.Backoffice.Authentication; + +public class TokenAuthorizationMessageHandler : DelegatingHandler { - public class TokenAuthorizationMessageHandler : AuthorizationMessageHandler + private readonly IAccessTokenProvider _tokenProvider; + private readonly NavigationManager _navigation; + + public TokenAuthorizationMessageHandler(IAccessTokenProvider tokenProvider, NavigationManager navigation) + { + _tokenProvider = tokenProvider; + _navigation = navigation; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - public TokenAuthorizationMessageHandler(IAccessTokenProvider provider, NavigationManager navigation, IConfiguration configuration) : base(provider, navigation) + var result = await _tokenProvider.RequestAccessToken(); + + if (result.TryGetToken(out var token)) + { + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.Value); + return await base.SendAsync(request, cancellationToken); + } + else { - ConfigureHandler(new[] { configuration.GetValue("ApiUrl") ?? string.Empty }); + // Redirect to login page if the token is not available + if (!_navigation.Uri.Contains("authentication/login")) + { + _navigation.NavigateTo("authentication/login"); + } + throw new AccessTokenNotAvailableException(_navigation, result, null); } } -} +} \ No newline at end of file diff --git a/src/Eurofurence.App.Backoffice/Layout/RedirectToLogin.razor b/src/Eurofurence.App.Backoffice/Layout/RedirectToLogin.razor index a1cf4003..8abc488c 100644 --- a/src/Eurofurence.App.Backoffice/Layout/RedirectToLogin.razor +++ b/src/Eurofurence.App.Backoffice/Layout/RedirectToLogin.razor @@ -4,6 +4,6 @@ @code { protected override void OnInitialized() { - Navigation.NavigateToLogin("authentication/login"); + Navigation.NavigateTo("authentication/login"); } } diff --git a/src/Eurofurence.App.Backoffice/Program.cs b/src/Eurofurence.App.Backoffice/Program.cs index 058c27fb..bb6cdf44 100644 --- a/src/Eurofurence.App.Backoffice/Program.cs +++ b/src/Eurofurence.App.Backoffice/Program.cs @@ -18,7 +18,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddHttpClient("api", options => diff --git a/src/Eurofurence.App.Backoffice/Services/IUsersService.cs b/src/Eurofurence.App.Backoffice/Services/IUserService.cs similarity index 81% rename from src/Eurofurence.App.Backoffice/Services/IUsersService.cs rename to src/Eurofurence.App.Backoffice/Services/IUserService.cs index 6a0855f4..272ac99b 100644 --- a/src/Eurofurence.App.Backoffice/Services/IUsersService.cs +++ b/src/Eurofurence.App.Backoffice/Services/IUserService.cs @@ -2,7 +2,7 @@ namespace Eurofurence.App.Backoffice.Services { - public interface IUsersService + public interface IUserService { public Task GetUserSelf(); } diff --git a/src/Eurofurence.App.Backoffice/Services/UsersService.cs b/src/Eurofurence.App.Backoffice/Services/UserService.cs similarity index 88% rename from src/Eurofurence.App.Backoffice/Services/UsersService.cs rename to src/Eurofurence.App.Backoffice/Services/UserService.cs index 65dcdb26..7dd0f319 100644 --- a/src/Eurofurence.App.Backoffice/Services/UsersService.cs +++ b/src/Eurofurence.App.Backoffice/Services/UserService.cs @@ -5,7 +5,7 @@ namespace Eurofurence.App.Backoffice.Services { - public class UsersService(HttpClient http) : IUsersService + public class UserService(HttpClient http) : IUserService { public async Task GetUserSelf() { diff --git a/src/Eurofurence.App.Backoffice/wwwroot/index.html b/src/Eurofurence.App.Backoffice/wwwroot/index.html index 7f18e4f4..02c25b12 100644 --- a/src/Eurofurence.App.Backoffice/wwwroot/index.html +++ b/src/Eurofurence.App.Backoffice/wwwroot/index.html @@ -9,7 +9,6 @@ - diff --git a/src/Eurofurence.App.Common/DataDiffUtils/PatchDefinition.cs b/src/Eurofurence.App.Common/DataDiffUtils/PatchDefinition.cs index 7a3c2125..3a569168 100644 --- a/src/Eurofurence.App.Common/DataDiffUtils/PatchDefinition.cs +++ b/src/Eurofurence.App.Common/DataDiffUtils/PatchDefinition.cs @@ -4,17 +4,19 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; using Eurofurence.App.Common.Abstractions; namespace Eurofurence.App.Common.DataDiffUtils { public class PatchDefinition where TTarget : IEntityBase, new() { - private readonly List> _operators = new List>(); + private readonly List> _operators = []; private readonly Func, TTarget> _targetItemLocator; /// - /// A selector that, inside an enumerable of TTarget, finds the + /// A selector that, inside an IEnumerable of TTarget, finds the /// single TTarget that corresponds to the provided TSource /// public PatchDefinition(Func, TTarget> targetItemLocator) @@ -22,7 +24,7 @@ public PatchDefinition(Func, TTarget> targetItemLo _targetItemLocator = targetItemLocator; } - private bool ArraysEqual(Array a1, Array a2) + private static bool ArraysEqual(Array a1, Array a2) { if (a1.Length == a2.Length) { @@ -39,6 +41,35 @@ private bool ArraysEqual(Array a1, Array a2) return false; } + private static bool CollectionsEqual(ICollection collection1, ICollection collection2) + { + if (collection1.Count == collection2.Count) + { + var a1e = collection1.GetEnumerator(); + var a2e = collection2.GetEnumerator(); + + for (int i = 0; i < collection1.Count; i++) + { + if (!a1e.MoveNext() || !a2e.MoveNext()) + { + return false; + } + + if (!a1e.Current.Equals(a2e.Current)) + return false; + } + return true; + } + return false; + } + + private static bool DictionariesEqual(IDictionary dictionary1, IDictionary dictionary2) + { + var string1 = JsonSerializer.Serialize(dictionary1); + var string2 = JsonSerializer.Serialize(dictionary2); + return string1 == string2; + } + public PatchDefinition Map( Func sourceValueSelector, Expression> targetSelector) @@ -54,21 +85,29 @@ public PatchDefinition Map( if (sourceValue == null && targetValue == null) return true; - if (sourceValue == null && targetValue != null || sourceValue != null && targetValue == null) + if (sourceValue == null || targetValue == null) return false; if (sourceValue.GetType().IsArray) return ArraysEqual((sourceValue as Array), (targetValue as Array)); - return sourceValue?.Equals(targetValue) ?? false; + if (sourceValue is IDictionary && + sourceValue.GetType().IsGenericType) + return DictionariesEqual((IDictionary)sourceValue, (IDictionary)targetValue); + + if (sourceValue is ICollection && + sourceValue.GetType().IsGenericType) + return CollectionsEqual((ICollection)sourceValue, (ICollection)targetValue); + + return (bool)sourceValue?.Equals(targetValue); }, ApplySourceValueToTarget = (source, target) => { var value = sourceValueSelector(source); - var memberSelectorExpression = targetSelector.Body as MemberExpression; + if (targetSelector.Body is not MemberExpression memberSelectorExpression) return; var property = memberSelectorExpression.Member as PropertyInfo; - property.SetValue(target, value, null); + if (property != null) property.SetValue(target, value, null); } }; @@ -84,10 +123,11 @@ public List> Patch(IEnumerable sources, IEnumer var unprocessedTargets = targets.ToList(); var newTargets = new List(); + if (newTargets == null) throw new ArgumentNullException(nameof(newTargets)); foreach (var sourceItem in sources) { - var target = default(TTarget); + TTarget target; var result = new PatchOperation {Action = ActionEnum.NotModified}; @@ -107,10 +147,8 @@ public List> Patch(IEnumerable sources, IEnumer result.Entity = target; - foreach (var o in _operators) + foreach (var o in _operators.Where(o => !o.IsEqual(sourceItem, target))) { - if (o.IsEqual(sourceItem, target)) continue; - if (result.Action == ActionEnum.NotModified) result.Action = ActionEnum.Update; @@ -120,12 +158,7 @@ public List> Patch(IEnumerable sources, IEnumer patchResults.Add(result); } - foreach (var targetItem in unprocessedTargets) - patchResults.Add(new PatchOperation - { - Action = ActionEnum.Delete, - Entity = targetItem - }); + patchResults.AddRange(unprocessedTargets.Select(targetItem => new PatchOperation { Action = ActionEnum.Delete, Entity = targetItem })); return patchResults; }