From b3212c5e0db0d09f716ed85d8e009a0eb5bdb5e5 Mon Sep 17 00:00:00 2001 From: Egamorf Date: Mon, 21 Apr 2025 01:52:01 +0200 Subject: [PATCH] Authorize API --- .../Client/GameIdeas.BlazorApp/App.razor | 28 +++++----- .../GameIdeas.BlazorApp/Helpers/GameHelper.cs | 18 ++++++- .../Components/GameCreationForm.razor.cs | 9 +++- .../Pages/Games/Header/GameHeader.razor | 33 +++++++----- .../Pages/Games/Header/GameHeader.razor.css | 5 +- .../Pages/User/Component.razor | 0 .../Pages/User/UserMenu.razor | 54 +++++++++---------- .../Services/HttpClientService.cs | 25 ++++++++- .../JwtAuthenticationStateProvider.cs | 9 ++-- .../Constants/GlobalConstants.cs | 11 ++-- .../GameIdeas.Shared/Dto/GameDetailDto.cs | 4 +- .../Controllers/GameController.cs | 7 ++- .../Services/Users/UserService.cs | 1 + 13 files changed, 130 insertions(+), 74 deletions(-) delete mode 100644 src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/User/Component.razor diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/App.razor b/src/GameIdeas/Client/GameIdeas.BlazorApp/App.razor index cc3223e..8118d77 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/App.razor +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/App.razor @@ -1,14 +1,18 @@ @using GameIdeas.BlazorApp.Layouts +@using Microsoft.AspNetCore.Components.Authorization + + + + + + + + + Not found + +

Sorry, there's nothing at this address.

+
+
+
+
- - - - - - - Not found - -

Sorry, there's nothing at this address.

-
-
-
diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Helpers/GameHelper.cs b/src/GameIdeas/Client/GameIdeas.BlazorApp/Helpers/GameHelper.cs index 9673b84..181281d 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Helpers/GameHelper.cs +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Helpers/GameHelper.cs @@ -1,12 +1,26 @@ using GameIdeas.Shared.Dto; +using Microsoft.AspNetCore.Components.Authorization; +using System.Security.Claims; namespace GameIdeas.BlazorApp.Helpers; public static class GameHelper { - public static void WriteTrackingDto(GameDetailDto game) + public static void WriteTrackingDto(GameDetailDto game, AuthenticationState authState) { - game.CreationUserId = 100000; + if (authState == null) + { + throw new ArgumentNullException(nameof(authState), "Authentication state missing"); + } + + var userId = authState.User.FindFirstValue(ClaimTypes.Sid); + + if (userId == null) + { + throw new ArgumentNullException(nameof(authState), "user state missing"); + } + + game.CreationUserId = userId; game.CreationDate = DateTime.Now; } diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameCreationForm.razor.cs b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameCreationForm.razor.cs index 895fb72..9685dc5 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameCreationForm.razor.cs +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Components/GameCreationForm.razor.cs @@ -5,8 +5,10 @@ using GameIdeas.BlazorApp.Shared.Components.Select.Models; using GameIdeas.BlazorApp.Shared.Components.Slider; using GameIdeas.Shared.Dto; using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Forms; using Microsoft.JSInterop; +using System.Security.Claims; namespace GameIdeas.BlazorApp.Pages.Games.Components; @@ -14,6 +16,7 @@ public partial class GameCreationForm { [Inject] private IJSRuntime Js { get; set; } = default!; [Inject] private IGameGateway GameGateway { get; set; } = default!; + [Inject] private AuthenticationStateProvider AuthenticationState { get; set; } = default!; [CascadingParameter] private Popup? Popup { get; set; } [Parameter] public CategoriesDto? Categories { get; set; } [Parameter] public EventCallback OnSubmit { get; set; } @@ -33,7 +36,6 @@ public partial class GameCreationForm protected override async Task OnAfterRenderAsync(bool firstRender) { await Js.InvokeVoidAsync("resizeGameForm"); - } private void HandleOnCancel() @@ -52,7 +54,9 @@ public partial class GameCreationForm { IsLoading = true; - GameHelper.WriteTrackingDto(GameDto); + var authState = await AuthenticationState.GetAuthenticationStateAsync(); + GameHelper.WriteTrackingDto(GameDto, authState); + var gameId = await GameGateway.CreateGame(GameDto); if (gameId != 0) @@ -68,6 +72,7 @@ public partial class GameCreationForm finally { IsLoading = false; + StateHasChanged(); } } } \ No newline at end of file diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Header/GameHeader.razor b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Header/GameHeader.razor index d62b74a..0516759 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Header/GameHeader.razor +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Header/GameHeader.razor @@ -4,6 +4,8 @@ @using GameIdeas.BlazorApp.Shared.Components.Select.Models @using GameIdeas.BlazorApp.Shared.Models @using GameIdeas.Resources +@using GameIdeas.Shared.Constants +@using Microsoft.AspNetCore.Components.Authorization @inherits ComponentBase @@ -15,23 +17,26 @@ @ChildContent
-
-
-
- - - -
- -
-
+ +
+ + + diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Header/GameHeader.razor.css b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Header/GameHeader.razor.css index 0650995..4b6f742 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Header/GameHeader.razor.css +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Header/GameHeader.razor.css @@ -29,15 +29,12 @@ align-items: flex-end; } -.add-container { - margin-right: 40px; -} - .add-buttons { display: flex; flex-direction: row; background: var(--violet); border-radius: var(--small-radius); + margin-right: 40px; } .button { diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/User/Component.razor b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/User/Component.razor deleted file mode 100644 index e69de29..0000000 diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/User/UserMenu.razor b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/User/UserMenu.razor index b069564..49c6910 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/User/UserMenu.razor +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/User/UserMenu.razor @@ -12,36 +12,34 @@ @if (ContentVisile) {
- - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - - - - + + + + + + + +
} diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Services/HttpClientService.cs b/src/GameIdeas/Client/GameIdeas.BlazorApp/Services/HttpClientService.cs index f0c39ac..41c4cf0 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Services/HttpClientService.cs +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Services/HttpClientService.cs @@ -3,10 +3,15 @@ using System.Net.Http.Headers; using System.Text.Json.Serialization; using System.Text.Json; using System.Text; +using Blazored.LocalStorage; +using GameIdeas.Shared.Constants; namespace GameIdeas.BlazorApp.Services; -public class HttpClientService(IHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory) : IHttpClientService +public class HttpClientService( + IHttpClientFactory httpClientFactory, + ILoggerFactory loggerFactory, + ILocalStorageService localStorage) : IHttpClientService { private readonly HttpClient httpClient = httpClientFactory.CreateClient("GameIdeas.WebAPI"); private readonly ILogger logger = loggerFactory.CreateLogger(); @@ -25,6 +30,8 @@ public class HttpClientService(IHttpClientFactory httpClientFactory, ILoggerFact public async Task PostAsync(string url, object data) { + await SetAuthorizationHeader(); + var jsonContent = JsonSerializer.Serialize(data, _optionsCamelCase); var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); var response = await httpClient.PostAsync(url, content); @@ -32,8 +39,11 @@ public class HttpClientService(IHttpClientFactory httpClientFactory, ILoggerFact return await GetResultValue(response, ResourcesKey.ErrorWhenPostingData); } + public async Task PutAsync(string url, object data) { + await SetAuthorizationHeader(); + var jsonContent = JsonSerializer.Serialize(data, _optionsCamelCase); var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); var response = await httpClient.PutAsync(url, content); @@ -43,6 +53,7 @@ public class HttpClientService(IHttpClientFactory httpClientFactory, ILoggerFact public async Task DeleteAsync(string? url) { + await SetAuthorizationHeader(); var response = await httpClient.DeleteAsync(url); return await GetResultValue(response, ResourcesKey.ErrorWhenDeletingData); @@ -50,6 +61,7 @@ public class HttpClientService(IHttpClientFactory httpClientFactory, ILoggerFact public async Task FetchDataAsync(string? url) { + await SetAuthorizationHeader(); var response = await httpClient.GetAsync(url); return await GetResultValue(response, ResourcesKey.ErrorWhenFetchingData); @@ -57,6 +69,7 @@ public class HttpClientService(IHttpClientFactory httpClientFactory, ILoggerFact public async Task FetchBytesAsync(string? url) { + await SetAuthorizationHeader(); var response = await httpClient.GetAsync(url); if (response.IsSuccessStatusCode) @@ -71,6 +84,7 @@ public class HttpClientService(IHttpClientFactory httpClientFactory, ILoggerFact public async Task FetchStreamAsync(string? url) { + await SetAuthorizationHeader(); var response = await httpClient.GetAsync(url); if (response.IsSuccessStatusCode) @@ -84,6 +98,8 @@ public class HttpClientService(IHttpClientFactory httpClientFactory, ILoggerFact public async Task PostFileAsync(string? url, Stream fileStream, string fileName, string contentType) { + await SetAuthorizationHeader(); + using var content = new MultipartFormDataContent(); var streamContent = new StreamContent(fileStream); @@ -122,4 +138,11 @@ public class HttpClientService(IHttpClientFactory httpClientFactory, ILoggerFact throw new HttpRequestException( $"{errorMessage} + StatusCode: {response.StatusCode} + Reason: {response.ReasonPhrase}"); } + + private async Task SetAuthorizationHeader() + { + var token = await localStorage.GetItemAsStringAsync(GlobalConstants.LS_AUTH_STORAGE_KEY); + httpClient.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("bearer", token); + } } diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Services/JwtAuthenticationStateProvider.cs b/src/GameIdeas/Client/GameIdeas.BlazorApp/Services/JwtAuthenticationStateProvider.cs index 2417dac..6ea212f 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Services/JwtAuthenticationStateProvider.cs +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Services/JwtAuthenticationStateProvider.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Components.Authorization; using System.Security.Claims; using System.IdentityModel.Tokens.Jwt; +using GameIdeas.Shared.Constants; namespace GameIdeas.BlazorApp.Services; @@ -9,7 +10,7 @@ public class JwtAuthenticationStateProvider(ILocalStorageService localStorage) : { public override async Task GetAuthenticationStateAsync() { - var savedToken = await localStorage.GetItemAsStringAsync("authToken"); + var savedToken = await localStorage.GetItemAsStringAsync(GlobalConstants.LS_AUTH_STORAGE_KEY); if (!string.IsNullOrWhiteSpace(savedToken)) { @@ -23,7 +24,7 @@ public class JwtAuthenticationStateProvider(ILocalStorageService localStorage) : } catch { - await localStorage.RemoveItemAsync("authToken"); + await localStorage.RemoveItemAsync(GlobalConstants.LS_AUTH_STORAGE_KEY); } } @@ -32,14 +33,14 @@ public class JwtAuthenticationStateProvider(ILocalStorageService localStorage) : public async Task NotifyUserAuthenticationAsync(string token) { - await localStorage.SetItemAsStringAsync("authToken", token); + await localStorage.SetItemAsStringAsync(GlobalConstants.LS_AUTH_STORAGE_KEY, token); NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } public async Task NotifyUserLogoutAsync() { - await localStorage.RemoveItemAsync("authToken"); + await localStorage.RemoveItemAsync(GlobalConstants.LS_AUTH_STORAGE_KEY); var nobody = new ClaimsPrincipal(new ClaimsIdentity()); NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(nobody))); diff --git a/src/GameIdeas/GameIdeas.Shared/Constants/GlobalConstants.cs b/src/GameIdeas/GameIdeas.Shared/Constants/GlobalConstants.cs index 7989c69..3801e02 100644 --- a/src/GameIdeas/GameIdeas.Shared/Constants/GlobalConstants.cs +++ b/src/GameIdeas/GameIdeas.Shared/Constants/GlobalConstants.cs @@ -6,11 +6,14 @@ public class GlobalConstants public readonly static Guid ADMINISTRATOR_USER_ID = Guid.Parse("{2AB56FCB-0CDE-4DAE-AC9C-FC7635B0D18A}"); public readonly static Guid MEMBER_ID = Guid.Parse("{BCE14DEA-1748-4A76-8485-ADEE83DF5EFD}"); - public readonly static string ADMINISTRATOR = "Administrateur"; - public readonly static string MEMBER = "Membre"; + public const string ADMINISTRATOR = "Administrateur"; + public const string MEMBER = "Membre"; + public const string ADMIN_MEMBER = $"{ADMINISTRATOR}, {MEMBER}"; - public readonly static int JWT_DURATION_HOUR = 12; + public const int JWT_DURATION_HOUR = 12; - public readonly static int NUMBER_PER_PAGE = 50; + public const int NUMBER_PER_PAGE = 50; + + public const string LS_AUTH_STORAGE_KEY = "authToken"; } \ No newline at end of file diff --git a/src/GameIdeas/GameIdeas.Shared/Dto/GameDetailDto.cs b/src/GameIdeas/GameIdeas.Shared/Dto/GameDetailDto.cs index 6510480..78e5548 100644 --- a/src/GameIdeas/GameIdeas.Shared/Dto/GameDetailDto.cs +++ b/src/GameIdeas/GameIdeas.Shared/Dto/GameDetailDto.cs @@ -6,9 +6,9 @@ public class GameDetailDto public string? Title { get; set; } public DateTime? ReleaseDate { get; set; } public DateTime? CreationDate { get; set; } - public int CreationUserId { get; set; } + public string CreationUserId { get; set; } = string.Empty; public DateTime? ModificationDate { get; set; } - public int? ModificationUserId { get; set; } + public string? ModificationUserId { get; set; } public double? StorageSpace { get; set; } public string? Description { get; set; } public int Interest { get; set; } = 3; diff --git a/src/GameIdeas/Server/GameIdeas.WebAPI/Controllers/GameController.cs b/src/GameIdeas/Server/GameIdeas.WebAPI/Controllers/GameController.cs index 9b0e15f..f747635 100644 --- a/src/GameIdeas/Server/GameIdeas.WebAPI/Controllers/GameController.cs +++ b/src/GameIdeas/Server/GameIdeas.WebAPI/Controllers/GameController.cs @@ -1,5 +1,7 @@ -using GameIdeas.Shared.Dto; +using GameIdeas.Shared.Constants; +using GameIdeas.Shared.Dto; using GameIdeas.WebAPI.Services.Games; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace GameIdeas.WebAPI.Controllers; @@ -42,6 +44,7 @@ public class GameController( } } + [Authorize(Roles = GlobalConstants.ADMIN_MEMBER)] [HttpPost("Create")] public async Task> CreateGame([FromBody] GameDetailDto game) { @@ -57,6 +60,7 @@ public class GameController( } } + [Authorize(Roles = GlobalConstants.ADMIN_MEMBER)] [HttpPut("Update")] public async Task> UpdateGame([FromBody] GameDetailDto game) { @@ -72,6 +76,7 @@ public class GameController( } } + [Authorize(Roles = GlobalConstants.ADMIN_MEMBER)] [HttpDelete("Delete/{id:int}")] public async Task> DeleteGame(int id) { diff --git a/src/GameIdeas/Server/GameIdeas.WebAPI/Services/Users/UserService.cs b/src/GameIdeas/Server/GameIdeas.WebAPI/Services/Users/UserService.cs index 03dc42d..65058a3 100644 --- a/src/GameIdeas/Server/GameIdeas.WebAPI/Services/Users/UserService.cs +++ b/src/GameIdeas/Server/GameIdeas.WebAPI/Services/Users/UserService.cs @@ -28,6 +28,7 @@ public class UserService(UserManager userManager) : IUserService List authClaims = [ new Claim(ClaimTypes.Name, user.UserName ?? string.Empty), + new Claim(ClaimTypes.Sid, user.Id), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), ];