Add authentication and authorization (#21)

Reviewed-on: #21
This commit was merged in pull request #21.
This commit is contained in:
2025-04-21 01:53:58 +02:00
parent 51dab81121
commit 033747899b
55 changed files with 2186 additions and 317 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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();
}
}
}

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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();
}
}
}

View File

@@ -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)
}

View File

@@ -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()
{

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,9 @@
using GameIdeas.Shared.Dto;
namespace GameIdeas.BlazorApp.Pages.User.Gateways;
public interface IAuthGateway
{
Task<bool> Login(UserDto userDto);
Task Logout();
}

View File

@@ -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" />

View File

@@ -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;
}
}

View File

@@ -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)
}

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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)));
}
}

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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";
}
}

View File

@@ -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);
}

View File

@@ -0,0 +1,3 @@
namespace GameIdeas.BlazorApp.Shared.Exceptions;
public class AuthenticationUserException(string message) : Exception(message);