Add authentication and authorization (#21)
Reviewed-on: #21
This commit was merged in pull request #21.
This commit is contained in:
@@ -1,14 +1,18 @@
|
||||
@using GameIdeas.BlazorApp.Layouts
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
|
||||
<CascadingAuthenticationState>
|
||||
<Router AppAssembly="@typeof(App).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<PageTitle>Not found</PageTitle>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p role="alert">Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
</CascadingAuthenticationState>
|
||||
|
||||
<Router AppAssembly="@typeof(App).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<PageTitle>Not found</PageTitle>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p role="alert">Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
|
||||
@@ -9,10 +9,13 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Blazored.FluentValidation" Version="2.2.0" />
|
||||
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
|
||||
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.2" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.2" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
@using GameIdeas.BlazorApp.Pages.Games
|
||||
@using GameIdeas.BlazorApp.Shared.Components.Account
|
||||
@using GameIdeas.BlazorApp.Pages.User
|
||||
@using GameIdeas.BlazorApp.Shared.Components.Select
|
||||
@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,30 +17,26 @@
|
||||
@ChildContent
|
||||
|
||||
<div class="account-add-container">
|
||||
<div class="add-container">
|
||||
<div class="add-buttons">
|
||||
<div class="first-button button">
|
||||
<svg class="button-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<Select @ref="SelectListAdd" TItem="KeyValuePair<AddType, string>" THeader="object"
|
||||
ValuesChanged=HandleAddTypeClicked Params=SelectParams Theme="SelectTheme.Navigation">
|
||||
<div class="second-button button">
|
||||
<AuthorizeView Roles="@GlobalConstants.ADMIN_MEMBER">
|
||||
<Authorized>
|
||||
<div class="add-buttons">
|
||||
<div class="first-button button">
|
||||
<svg class="button-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M1 3H23L12 22" />
|
||||
<path d="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
|
||||
</svg>
|
||||
</div>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="account-container">
|
||||
<div class="icon-container" @onclick=HandleAccountClicked>
|
||||
<svg class="account-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<AccountSettings @ref="AccountSettings" />
|
||||
</div>
|
||||
<Select @ref="SelectListAdd" TItem="KeyValuePair<AddType, string>" THeader="object"
|
||||
ValuesChanged=HandleAddTypeClicked Params=SelectParams Theme="SelectTheme.Navigation">
|
||||
<div class="second-button button">
|
||||
<svg class="button-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M1 3H23L12 22" />
|
||||
</svg>
|
||||
</div>
|
||||
</Select>
|
||||
</div>
|
||||
</Authorized>
|
||||
</AuthorizeView>
|
||||
|
||||
<UserMenu />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using GameIdeas.BlazorApp.Shared.Components.Account;
|
||||
using GameIdeas.BlazorApp.Shared.Components.Select;
|
||||
using GameIdeas.BlazorApp.Shared.Components.Select.Models;
|
||||
using GameIdeas.BlazorApp.Shared.Models;
|
||||
@@ -18,7 +17,6 @@ public partial class GameHeader : ComponentBase
|
||||
{ AddType.Auto, ResourcesKey.AutoAdd }
|
||||
};
|
||||
|
||||
private AccountSettings? AccountSettings;
|
||||
private Select<KeyValuePair<AddType, string>, object>? SelectListAdd;
|
||||
private SelectParams<KeyValuePair<AddType, string>, object> SelectParams = new();
|
||||
|
||||
@@ -26,7 +24,7 @@ public partial class GameHeader : ComponentBase
|
||||
{
|
||||
SelectParams = new()
|
||||
{
|
||||
Items = AddTypes.ToList(),
|
||||
Items = [.. AddTypes],
|
||||
GetItemLabel = item => item.Value
|
||||
};
|
||||
|
||||
@@ -43,9 +41,4 @@ public partial class GameHeader : ComponentBase
|
||||
SelectListAdd?.Close();
|
||||
await AddTypeChanged.InvokeAsync(values.FirstOrDefault().Key);
|
||||
}
|
||||
|
||||
private void HandleAccountClicked()
|
||||
{
|
||||
AccountSettings?.Toggle();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
align-items: center;
|
||||
width: 40px;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-container img {
|
||||
@@ -21,10 +22,6 @@
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.icon-container:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.account-add-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -32,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 {
|
||||
@@ -70,8 +64,4 @@
|
||||
.button-icon:hover {
|
||||
background: var(--violet-selected);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.account-icon {
|
||||
fill: var(--line);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
@using Blazored.FluentValidation
|
||||
|
||||
<EditForm EditContext="EditContext" OnSubmit="HandleLoginSubmit">
|
||||
<FluentValidationValidator />
|
||||
<div class="login-form">
|
||||
<div class="login-field">
|
||||
<div class="input-title">@ResourcesKey.EnterUsername</div>
|
||||
<InputText class="input-text"
|
||||
@bind-Value="UserDto.Username" />
|
||||
</div>
|
||||
<div class="login-field">
|
||||
<div class="input-title">@ResourcesKey.EnterPassword</div>
|
||||
<InputText class="input-text"
|
||||
@bind-Value="UserDto.Password" />
|
||||
</div>
|
||||
<div class="login-field">
|
||||
<button class="login-button" type="submit" disabled="@IsLoading">
|
||||
@if (IsLoading)
|
||||
{
|
||||
<div class="loading"></div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@ResourcesKey.Login
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</EditForm>
|
||||
@@ -0,0 +1,44 @@
|
||||
using GameIdeas.BlazorApp.Pages.User.Gateways;
|
||||
using GameIdeas.Shared.Dto;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
|
||||
namespace GameIdeas.BlazorApp.Pages.User.Components;
|
||||
|
||||
public partial class Login
|
||||
{
|
||||
[Parameter] public IAuthGateway AuthGateway { get; set; } = default!;
|
||||
|
||||
private EditContext? EditContext;
|
||||
private UserDto UserDto = new();
|
||||
private bool IsLoading = false;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
EditContext = new EditContext(UserDto);
|
||||
}
|
||||
|
||||
private async Task HandleLoginSubmit()
|
||||
{
|
||||
if (EditContext?.Validate() == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
await AuthGateway.Login(UserDto);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
UserDto.Password = string.Empty;
|
||||
EditContext?.Validate();
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,4 @@
|
||||
.account-setting-content {
|
||||
overflow: hidden;
|
||||
border-radius: var(--big-radius);
|
||||
position: fixed;
|
||||
animation-name: fade-in;
|
||||
animation-duration: 0.4s;
|
||||
border: 2px solid var(--input-selected);
|
||||
background: var(--dropdown-content);
|
||||
right: 10px;
|
||||
margin-top: 4px;
|
||||
z-index: var(--index-floating);
|
||||
}
|
||||
|
||||
.invisible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px 8px;
|
||||
@@ -53,7 +36,7 @@
|
||||
|
||||
.login-button:hover {
|
||||
background: var(--violet-selected);
|
||||
cursor:pointer;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-button:disabled {
|
||||
@@ -70,26 +53,3 @@
|
||||
animation: loading 1s linear infinite;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.settings-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.line {
|
||||
margin: 0 6px;
|
||||
border-bottom: 2px solid var(--line);
|
||||
}
|
||||
|
||||
.settings-element {
|
||||
max-width: 140px;
|
||||
height: 40px;
|
||||
padding: 0 26px;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.settings-element:hover {
|
||||
background: var(--line)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using FluentValidation;
|
||||
using GameIdeas.Shared.Dto;
|
||||
|
||||
namespace GameIdeas.BlazorApp.Shared.Components.Account;
|
||||
namespace GameIdeas.BlazorApp.Pages.User.Components;
|
||||
|
||||
public class LoginValidator : AbstractValidator<LoginDto>
|
||||
public class LoginValidator : AbstractValidator<UserDto>
|
||||
{
|
||||
public LoginValidator()
|
||||
{
|
||||
@@ -0,0 +1,38 @@
|
||||
using GameIdeas.BlazorApp.Services;
|
||||
using GameIdeas.BlazorApp.Shared.Constants;
|
||||
using GameIdeas.BlazorApp.Shared.Exceptions;
|
||||
using GameIdeas.Resources;
|
||||
using GameIdeas.Shared.Dto;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
|
||||
namespace GameIdeas.BlazorApp.Pages.User.Gateways;
|
||||
|
||||
public class AuthGateway(IHttpClientService httpClient,
|
||||
AuthenticationStateProvider stateProvider) : IAuthGateway
|
||||
{
|
||||
public async Task<bool> Login(UserDto userDto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var token = await httpClient.PostAsync<TokenDto>(Endpoints.Auth.Login, userDto);
|
||||
await ((JwtAuthenticationStateProvider)stateProvider).NotifyUserAuthenticationAsync(token!.Token!);
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw new AuthenticationUserException(ResourcesKey.UserLoginFailed);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Logout()
|
||||
{
|
||||
try
|
||||
{
|
||||
await ((JwtAuthenticationStateProvider)stateProvider).NotifyUserLogoutAsync();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw new AuthenticationUserException(ResourcesKey.UserLogoutFailed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using GameIdeas.Shared.Dto;
|
||||
|
||||
namespace GameIdeas.BlazorApp.Pages.User.Gateways;
|
||||
|
||||
public interface IAuthGateway
|
||||
{
|
||||
Task<bool> Login(UserDto userDto);
|
||||
Task Logout();
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
@using GameIdeas.BlazorApp.Pages.User.Components
|
||||
@using GameIdeas.BlazorApp.Shared.Components.BackdropFilter
|
||||
@using GameIdeas.BlazorApp.Shared.Constants
|
||||
@using GameIdeas.Shared.Constants
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
|
||||
<div class="menu">
|
||||
<div class="icon" @onclick=HandleAccountClicked>
|
||||
@Icons.Account
|
||||
</div>
|
||||
<div class="container">
|
||||
@if (ContentVisile)
|
||||
{
|
||||
<div class="content">
|
||||
<AuthorizeView Roles="@GlobalConstants.ADMIN_MEMBER">
|
||||
<Authorized>
|
||||
<div class="menu-element">
|
||||
@ResourcesKey.CategoriesManager
|
||||
</div>
|
||||
<span class="line"></span>
|
||||
</Authorized>
|
||||
</AuthorizeView>
|
||||
|
||||
<AuthorizeView Roles="@GlobalConstants.ADMINISTRATOR">
|
||||
<Authorized>
|
||||
<div class="menu-element">
|
||||
@ResourcesKey.UserManager
|
||||
</div>
|
||||
<span class="line"></span>
|
||||
</Authorized>
|
||||
</AuthorizeView>
|
||||
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<div class="menu-element" @onclick="HandleLogoutClicked">
|
||||
@ResourcesKey.Logout
|
||||
</div>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
<Login AuthGateway="AuthGateway" />
|
||||
</NotAuthorized>
|
||||
</AuthorizeView>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BackdropFilter AllowBodyScroll=true CloseOnClick=true Color="BackdropFilterColor.Transparent"
|
||||
IsVisible="ContentVisile" OnClick="HandleBackdropFilterClicked" />
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using GameIdeas.BlazorApp.Pages.User.Gateways;
|
||||
using GameIdeas.BlazorApp.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
|
||||
namespace GameIdeas.BlazorApp.Pages.User;
|
||||
|
||||
public partial class UserMenu
|
||||
{
|
||||
[Inject] private IAuthGateway AuthGateway { get; set; } = default!;
|
||||
|
||||
private bool ContentVisile = false;
|
||||
|
||||
private async Task HandleLogoutClicked()
|
||||
{
|
||||
await AuthGateway.Logout();
|
||||
ContentVisile = false;
|
||||
}
|
||||
|
||||
private void HandleAccountClicked()
|
||||
{
|
||||
ContentVisile = true;
|
||||
}
|
||||
|
||||
private void HandleBackdropFilterClicked()
|
||||
{
|
||||
ContentVisile = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
.menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 40px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon ::deep svg {
|
||||
fill: var(--line);
|
||||
}
|
||||
|
||||
.container {
|
||||
right: 0;
|
||||
position: absolute;
|
||||
margin-top: 4px;
|
||||
z-index: var(--index-dropdown);
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow: hidden;
|
||||
border-radius: var(--big-radius);
|
||||
border: 2px solid var(--input-selected);
|
||||
background: var(--dropdown-content);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.line {
|
||||
margin: 0 6px;
|
||||
border-bottom: 2px solid var(--input-selected);
|
||||
}
|
||||
|
||||
.menu-element {
|
||||
height: 32px;
|
||||
padding: 0 20px;
|
||||
align-content: center;
|
||||
text-wrap: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-element:hover {
|
||||
background: var(--input-selected)
|
||||
}
|
||||
@@ -1,12 +1,17 @@
|
||||
using System.Net.Http.Json;
|
||||
using Blazored.LocalStorage;
|
||||
using GameIdeas.BlazorApp;
|
||||
using GameIdeas.BlazorApp.Pages.Games.Gateways;
|
||||
using GameIdeas.BlazorApp.Pages.User.Gateways;
|
||||
using GameIdeas.BlazorApp.Services;
|
||||
using GameIdeas.Resources;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
var services = builder.Services;
|
||||
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
|
||||
@@ -15,7 +20,7 @@ UriBuilder uriBuilder = new(builder.HostEnvironment.BaseAddress)
|
||||
Port = 8000
|
||||
};
|
||||
|
||||
builder.Services.AddHttpClient(
|
||||
services.AddHttpClient(
|
||||
"GameIdeas.WebAPI",
|
||||
client =>
|
||||
{
|
||||
@@ -23,11 +28,18 @@ builder.Services.AddHttpClient(
|
||||
client.Timeout = TimeSpan.FromMinutes(3);
|
||||
});
|
||||
|
||||
builder.Services.AddScoped<IHttpClientService, HttpClientService>();
|
||||
builder.Services.AddScoped<IGameGateway, GameGateway>();
|
||||
services.AddBlazoredLocalStorage();
|
||||
services.AddAuthorizationCore();
|
||||
|
||||
builder.Services.AddSingleton<TranslationService>();
|
||||
builder.Services.AddSingleton<Translations>();
|
||||
services.AddScoped<AuthenticationStateProvider, JwtAuthenticationStateProvider>();
|
||||
|
||||
services.AddScoped<IHttpClientService, HttpClientService>();
|
||||
|
||||
services.AddScoped<IAuthGateway, AuthGateway>();
|
||||
services.AddScoped<IGameGateway, GameGateway>();
|
||||
|
||||
services.AddSingleton<TranslationService>();
|
||||
services.AddSingleton<Translations>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
||||
@@ -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<HttpClientService> logger = loggerFactory.CreateLogger<HttpClientService>();
|
||||
@@ -25,6 +30,8 @@ public class HttpClientService(IHttpClientFactory httpClientFactory, ILoggerFact
|
||||
|
||||
public async Task<T?> PostAsync<T>(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<T>(response, ResourcesKey.ErrorWhenPostingData);
|
||||
}
|
||||
|
||||
|
||||
public async Task<T?> PutAsync<T>(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<T?> DeleteAsync<T>(string? url)
|
||||
{
|
||||
await SetAuthorizationHeader();
|
||||
var response = await httpClient.DeleteAsync(url);
|
||||
|
||||
return await GetResultValue<T>(response, ResourcesKey.ErrorWhenDeletingData);
|
||||
@@ -50,6 +61,7 @@ public class HttpClientService(IHttpClientFactory httpClientFactory, ILoggerFact
|
||||
|
||||
public async Task<T?> FetchDataAsync<T>(string? url)
|
||||
{
|
||||
await SetAuthorizationHeader();
|
||||
var response = await httpClient.GetAsync(url);
|
||||
|
||||
return await GetResultValue<T>(response, ResourcesKey.ErrorWhenFetchingData);
|
||||
@@ -57,6 +69,7 @@ public class HttpClientService(IHttpClientFactory httpClientFactory, ILoggerFact
|
||||
|
||||
public async Task<byte[]?> 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<Stream?> 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<T?> PostFileAsync<T>(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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
using Blazored.LocalStorage;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using System.Security.Claims;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using GameIdeas.Shared.Constants;
|
||||
|
||||
namespace GameIdeas.BlazorApp.Services;
|
||||
|
||||
public class JwtAuthenticationStateProvider(ILocalStorageService localStorage) : AuthenticationStateProvider
|
||||
{
|
||||
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
{
|
||||
var savedToken = await localStorage.GetItemAsStringAsync(GlobalConstants.LS_AUTH_STORAGE_KEY);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(savedToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
var token = new JwtSecurityTokenHandler().ReadJwtToken(savedToken);
|
||||
|
||||
return new AuthenticationState(
|
||||
new ClaimsPrincipal(new ClaimsIdentity(token.Claims, "jwt"))
|
||||
);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await localStorage.RemoveItemAsync(GlobalConstants.LS_AUTH_STORAGE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
||||
}
|
||||
|
||||
public async Task NotifyUserAuthenticationAsync(string token)
|
||||
{
|
||||
await localStorage.SetItemAsStringAsync(GlobalConstants.LS_AUTH_STORAGE_KEY, token);
|
||||
|
||||
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
||||
}
|
||||
|
||||
public async Task NotifyUserLogoutAsync()
|
||||
{
|
||||
await localStorage.RemoveItemAsync(GlobalConstants.LS_AUTH_STORAGE_KEY);
|
||||
|
||||
var nobody = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(nobody)));
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
@using GameIdeas.Resources
|
||||
@using Blazored.FluentValidation;
|
||||
|
||||
<div class="account-setting-container" tabindex="1000">
|
||||
<div class="account-setting-content @(ContentVisile ? string.Empty : "invisible")">
|
||||
@if (!IsLogin)
|
||||
{
|
||||
<EditForm EditContext="EditContext" OnSubmit="HandleLoginSubmit">
|
||||
<FluentValidationValidator />
|
||||
<div class="login-form">
|
||||
<div class="login-field">
|
||||
<div class="input-title">@ResourcesKey.EnterUsername</div>
|
||||
<InputText class="input-text"
|
||||
@bind-Value="LoginDto.Username" />
|
||||
</div>
|
||||
<div class="login-field">
|
||||
<div class="input-title">@ResourcesKey.EnterPassword</div>
|
||||
<InputText class="input-text"
|
||||
@bind-Value="LoginDto.Password" />
|
||||
</div>
|
||||
<div class="login-field">
|
||||
<button class="login-button" type="submit" disabled="@IsLoading">
|
||||
@if (IsLoading)
|
||||
{
|
||||
<div class="loading"></div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@ResourcesKey.Login
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</EditForm>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="settings-list">
|
||||
<div class="settings-element">
|
||||
@ResourcesKey.UserManager
|
||||
</div>
|
||||
<span class="line"></span>
|
||||
<div class="settings-element" @onclick="HandleLogoutClicked">
|
||||
@ResourcesKey.Logout
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,47 +0,0 @@
|
||||
using GameIdeas.Shared.Dto;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
|
||||
namespace GameIdeas.BlazorApp.Shared.Components.Account;
|
||||
|
||||
public partial class AccountSettings
|
||||
{
|
||||
private bool ContentVisile = false;
|
||||
private EditContext? EditContext;
|
||||
private LoginDto LoginDto = new();
|
||||
private bool IsLoading = false;
|
||||
private bool IsLogin = true;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
EditContext = new EditContext(LoginDto);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
ContentVisile = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public void Toggle()
|
||||
{
|
||||
ContentVisile = !ContentVisile;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task HandleLoginSubmit()
|
||||
{
|
||||
if (EditContext?.Validate() == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsLoading = true;
|
||||
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||
Close();
|
||||
IsLoading = false;
|
||||
}
|
||||
private void HandleLogoutClicked()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
@@ -15,4 +15,9 @@ public static class Endpoints
|
||||
{
|
||||
public static readonly string AllCategories = "api/Category/All";
|
||||
}
|
||||
|
||||
public static class Auth
|
||||
{
|
||||
public static readonly string Login = "api/User/Login";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,4 +23,8 @@ public static class Icons
|
||||
public readonly static MarkupString Game = new(OpenBraket +
|
||||
"<path d=\"M6,7H18A5,5 0 0,1 23,12A5,5 0 0,1 18,17C16.36,17 14.91,16.21 14,15H10C9.09,16.21 7.64,17 6,17A5,5 0 0,1 1,12A5,5 0 0,1 6,7M19.75,9.5A1.25,1.25 0 0,0 18.5,10.75A1.25,1.25 0 0,0 19.75,12A1.25,1.25 0 0,0 21,10.75A1.25,1.25 0 0,0 19.75,9.5M17.25,12A1.25,1.25 0 0,0 16,13.25A1.25,1.25 0 0,0 17.25,14.5A1.25,1.25 0 0,0 18.5,13.25A1.25,1.25 0 0,0 17.25,12M5,9V11H3V13H5V15H7V13H9V11H7V9H5Z\">" +
|
||||
CloseBraket);
|
||||
|
||||
public readonly static MarkupString Account = new(OpenBraket +
|
||||
"<path d=\"M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z\" />" +
|
||||
CloseBraket);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace GameIdeas.BlazorApp.Shared.Exceptions;
|
||||
|
||||
public class AuthenticationUserException(string message) : Exception(message);
|
||||
Reference in New Issue
Block a user