Add user manager page (#22)

Reviewed-on: #22
This commit was merged in pull request #22.
This commit is contained in:
2025-04-27 20:49:57 +02:00
parent 033747899b
commit a2e93c9438
63 changed files with 1249 additions and 135 deletions

View File

@@ -0,0 +1,29 @@
@using Blazored.FluentValidation
<EditForm EditContext="EditContext" OnSubmit="HandleLoginSubmit">
<FluentValidationValidator Validator="Validator" />
<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" type="password"
@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.UserMenu.Gateways;
using GameIdeas.Shared.Dto;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
namespace GameIdeas.BlazorApp.Pages.UserMenu.Components;
public partial class Login
{
[Parameter] public IAuthGateway AuthGateway { get; set; } = default!;
private EditContext? EditContext;
private UserDto UserDto = new();
private bool IsLoading = false;
private LoginValidator Validator = new();
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

@@ -0,0 +1,55 @@
.login-form {
display: flex;
flex-direction: column;
padding: 20px 8px;
gap: 20px;
max-width: 400px;
}
.login-field {
display: flex;
flex-direction: column;
width: 100%;
height: fit-content;
}
::deep .input-text {
background: var(--input-selected);
border: 2px solid var(--input-selected);
border-radius: var(--small-radius);
padding: 6px;
color: var(--white);
}
::deep .input-text:focus-visible {
border: 2px solid var(--violet) !important;
}
.login-button {
background: var(--violet);
border: none;
border-radius: 100px;
height: 32px;
color: var(--white);
font-weight: bold;
}
.login-button:hover {
background: var(--violet-selected);
cursor: pointer;
}
.login-button:disabled {
background: var(--violet-selected);
cursor: wait;
}
.loading {
width: 18px;
height: 18px;
border-radius: 50%;
border: 3px solid rgba(0, 0, 0, 0.2);
border-top-color: var(--white);
animation: loading 1s linear infinite;
justify-self: center;
}

View File

@@ -0,0 +1,18 @@
using FluentValidation;
using GameIdeas.Shared.Dto;
namespace GameIdeas.BlazorApp.Pages.UserMenu.Components;
public class LoginValidator : AbstractValidator<UserDto>
{
public LoginValidator()
{
RuleFor(dto => dto.Username)
.NotNull()
.NotEmpty();
RuleFor(dto => dto.Password)
.NotNull()
.NotEmpty();
}
}

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.UserMenu.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.UserMenu.Gateways;
public interface IAuthGateway
{
Task<bool> Login(UserDto userDto);
Task Logout();
}

View File

@@ -0,0 +1,51 @@
@using GameIdeas.BlazorApp.Pages.UserMenu.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>
<a class="menu-element" href="/Users">
@ResourcesKey.UserManager
</a>
<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.UserMenu.Gateways;
using Microsoft.AspNetCore.Components;
namespace GameIdeas.BlazorApp.Pages.UserMenu;
public partial class UserMenu
{
[Inject] private IAuthGateway AuthGateway { get; set; } = default!;
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
private bool ContentVisile = false;
private async Task HandleLogoutClicked()
{
ContentVisile = false;
await AuthGateway.Logout();
NavigationManager.NavigateTo("/Games");
}
private void HandleAccountClicked()
{
ContentVisile = true;
}
private void HandleBackdropFilterClicked()
{
ContentVisile = false;
}
}

View File

@@ -0,0 +1,50 @@
.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 {
color: var(--white);
text-decoration: none;
height: 32px;
padding: 0 20px;
align-content: center;
text-wrap: nowrap;
cursor: pointer;
}
.menu-element:hover {
background: var(--input-selected)
}