Add user manager page #22

Merged
Egamorf merged 11 commits from feature/user-manager into main 2025-04-27 20:49:57 +02:00
17 changed files with 182 additions and 44 deletions
Showing only changes of commit 757e9db08d - Show all commits

View File

@@ -10,7 +10,7 @@
</div>
<div class="login-field">
<div class="input-title">@ResourcesKey.EnterPassword</div>
<InputText class="input-text"
<InputText class="input-text" type="password"
@bind-Value="UserDto.Password" />
</div>
<div class="login-field">

View File

@@ -6,13 +6,15 @@ 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()
{
await AuthGateway.Logout();
ContentVisile = false;
await AuthGateway.Logout();
NavigationManager.NavigateTo("/Games");
}
private void HandleAccountClicked()

View File

@@ -1,29 +1,34 @@
@using GameIdeas.BlazorApp.Shared.Components.Select
@using Blazored.FluentValidation
@using GameIdeas.BlazorApp.Shared.Components.Select
@using GameIdeas.BlazorApp.Shared.Components.Select.Models
@using GameIdeas.BlazorApp.Shared.Constants
@using GameIdeas.Shared.Dto
<EditForm EditContext="EditContext" OnSubmit="HandleSubmitClicked">
<FluentValidationValidator Validator="Validator" />
<div class="row">
<div class="icon">
@Icons.Account
</div>
<div class="name">
<input type="text" class="input-name" @bind="@User.Username" disabled="@(!CanEdit)">
<InputText class="input-name" @bind-Value="@User.Username" disabled="@(!IsEditing)" />
</div>
<div class="password">
<input type="password" class="input-password" placeholder="********" @bind="@User.Password" disabled="@(!CanEdit)">
<InputText type="password" class="input-password" placeholder="********" @bind-Value="@User.Password" disabled="@(!IsEditing)" />
</div>
<div class="role">
<Select TItem="RoleDto" THeader="object" DisableClicked=!CanEdit Type="SelectType.Single"
Theme="SelectTheme.Single" Values="Roles" ValuesChanged="HandleValuesChanged" Params="SelectRoleParams">
<span class="role-label @(!CanEdit ? "disabled" : "")">@(User.Role?.Name ?? ResourcesKey.Unknown)</span>
<Select TItem="RoleDto" THeader="object" DisableClicked=!IsEditing Type="SelectType.Single"
Theme="SelectTheme.Single" Values="Roles" ValuesChanged="HandleValuesChanged" Params="SelectRoleParams">
<span class="role-label @(!IsEditing ? "disabled" : "")">@(User.Role?.Name ?? ResourcesKey.Unknown)</span>
</Select>
</div>
<div class="buttons">
<button type="button" class="remove" @onclick="HandleRemoveClicked">@Icons.Bin</button>
<button type="button" class="edit @(CanEdit ? "selected" : "")" @onclick="HandleEditClicked">@Icons.Pen</button>
<button type="submit" class="submit" style="display: @(CanEdit ? "block" : "none");">@Icons.Check</button>
@if (CanEdit)
{
<button type="button" class="edit @(IsEditing ? "selected" : "")" @onclick="HandleEditClicked">@Icons.Pen</button>
}
<button type="submit" class="submit" style="display: @(IsEditing ? "block" : "none");">@Icons.Check</button>
</div>
</div>
</EditForm>

View File

@@ -1,3 +1,4 @@
using FluentValidation;
using GameIdeas.BlazorApp.Shared.Components.Select;
using GameIdeas.BlazorApp.Shared.Components.Select.Models;
using GameIdeas.Shared.Dto;
@@ -10,9 +11,11 @@ public partial class UserRow
{
[Parameter] public UserDto User { get; set; } = new();
[Parameter] public List<RoleDto> Roles { get; set; } = [];
[Parameter] public bool CanEdit { get; set; } = false;
[Parameter] public bool CanEdit { get; set; } = true;
[Parameter] public bool IsEditing { get; set; } = false;
[Parameter] public EventCallback<UserDto> OnRemove { get; set; }
[Parameter] public EventCallback<UserDto> OnSubmit { get; set; }
[Parameter] public IValidator Validator { get; set; } = default!;
private SelectParams<RoleDto, object> SelectRoleParams = new();
private EditContext? EditContext;
@@ -53,7 +56,7 @@ public partial class UserRow
return;
}
CanEdit = false;
IsEditing = false;
await OnSubmit.InvokeAsync(User);
User.Password = null;
}
@@ -61,14 +64,14 @@ public partial class UserRow
private void HandleEditClicked()
{
if (CanEdit)
if (IsEditing)
{
User.Username = OriginalUser?.Username;
User.Role = OriginalUser?.Role;
User.Password = null;
}
CanEdit = !CanEdit;
IsEditing = !IsEditing;
}
private async Task HandleRemoveClicked()

View File

@@ -22,11 +22,17 @@
width: fit-content;
}
input[disabled], input, input::placeholder {
::deep .input-name,
::deep .input-name[disabled],
::deep .input-password,
::deep .input-password[disabled],
::deep .input-password::placeholder {
color: var(--white);
}
input, .role-label {
::deep .input-name,
::deep .input-password,
.role-label {
border: none;
outline: none;
background: none;
@@ -38,7 +44,9 @@ input, .role-label {
background: rgb(0, 0, 0, 0.2);
}
input[disabled], .disabled {
::deep .input-name[disabled],
::deep .input-password[disabled],
.disabled {
border: none;
background: none;
}

View File

@@ -4,9 +4,9 @@ using GameIdeas.Shared.Dto;
namespace GameIdeas.BlazorApp.Pages.Users.Components;
public class UserValidator : AbstractValidator<UserDto>
public class UserCreateValidator : AbstractValidator<UserDto>
{
public UserValidator()
public UserCreateValidator()
{
RuleFor(user => user.Username)
.NotEmpty()
@@ -21,3 +21,30 @@ public class UserValidator : AbstractValidator<UserDto>
.WithMessage(ResourcesKey.MissingField);
}
}
public class UserUpdateValidator : AbstractValidator<UserDto>
{
public UserUpdateValidator()
{
When(user => user.Password == null && user.Role == null, () =>
{
RuleFor(user => user.Username)
.NotEmpty()
.WithMessage(ResourcesKey.MissingField);
});
When(user => user.Username == null && user.Role == null, () =>
{
RuleFor(user => user.Password)
.NotEmpty()
.WithMessage(ResourcesKey.MissingField);
});
When(user => user.Username == null && user.Password == null, () =>
{
RuleFor(user => user.Role)
.NotEmpty()
.WithMessage(ResourcesKey.MissingField);
});
}
}

View File

@@ -7,7 +7,7 @@ public interface IUserGateway
{
Task<UserListDto> GetUsers(UserFilterParams filterParams, int currentPage);
Task<IEnumerable<RoleDto>> GetRoles();
Task<string> CreateUser(UserDto user);
Task<string> UpdateUser(UserDto user);
Task<string> DeleteUser(string userId);
Task<IdDto> CreateUser(UserDto user);
Task<IdDto> UpdateUser(UserDto user);
Task<IdDto> DeleteUser(string userId);
}

View File

@@ -9,11 +9,11 @@ namespace GameIdeas.BlazorApp.Pages.Users.Gateways;
public class UserGateway(IHttpClientService httpClient) : IUserGateway
{
public async Task<string> CreateUser(UserDto user)
public async Task<IdDto> CreateUser(UserDto user)
{
try
{
return await httpClient.PostAsync<string>(Endpoints.User.Create, user)
return await httpClient.PostAsync<IdDto>(Endpoints.User.Create, user)
?? throw new InvalidOperationException(ResourcesKey.ErrorCreateUser);
}
catch (Exception)
@@ -22,11 +22,11 @@ public class UserGateway(IHttpClientService httpClient) : IUserGateway
}
}
public async Task<string> DeleteUser(string userId)
public async Task<IdDto> DeleteUser(string userId)
{
try
{
return await httpClient.DeleteAsync<string>(Endpoints.User.Delete(userId))
return await httpClient.DeleteAsync<IdDto>(Endpoints.User.Delete(userId))
?? throw new InvalidOperationException(ResourcesKey.ErrorDeleteUser);
}
catch (Exception)
@@ -69,11 +69,11 @@ public class UserGateway(IHttpClientService httpClient) : IUserGateway
}
}
public async Task<string> UpdateUser(UserDto user)
public async Task<IdDto> UpdateUser(UserDto user)
{
try
{
return await httpClient.PutAsync<string>(Endpoints.User.Update(user.Id ?? string.Empty), user)
return await httpClient.PutAsync<IdDto>(Endpoints.User.Update(user.Id ?? string.Empty), user)
?? throw new InvalidOperationException(ResourcesKey.ErrorUpdateUser);
}
catch (Exception)

View File

@@ -16,17 +16,21 @@
<div class="header-content">
<SearchInput Placeholder="@ResourcesKey.EnterUsername" @bind-Text="FilterParams.Name" />
<SelectSearch TItem="RoleDto" Placeholder="@ResourcesKey.Roles" @bind-Values="FilterParams.Roles"
Items="Roles.ToList()" GetLabel="@(role => role.Name)" Theme="SelectTheme.Filter" />
Items="Roles.ToList()" GetLabel="@(role => role.Name)" Theme="SelectTheme.Filter" />
</div>
</GameHeader>
<div class="container">
<UserRow User="UserAdd" Roles="Roles.ToList()" CanEdit="false" IsEditing="true" OnRemove="HandleResetUser" OnSubmit="HandleSubmitUser" Validator="@(new UserCreateValidator())" />
<span class="line"></span>
<div class="content">
@if (!IsLoading)
{
@foreach (var user in UserList.Users ?? [])
{
<UserRow User="user" Roles="Roles.ToList()" OnRemove="HandleRemoveUser" OnSubmit="HandleSubmitUser" />
<UserRow User="user" Roles="Roles.ToList()" OnRemove="HandleRemoveUser" OnSubmit="HandleUpdateUser" Validator="@(new UserUpdateValidator())" />
}
}
else

View File

@@ -16,6 +16,7 @@ public partial class Users
private UserListDto UserList = new();
private IEnumerable<RoleDto> Roles = [];
private int CurrentPage = 1;
private UserDto UserAdd = new();
protected override async Task OnInitializedAsync()
{
@@ -43,12 +44,72 @@ public partial class Users
IsLoading = false;
}
}
private Task HandleSubmitUser(UserDto args)
private async Task HandleSubmitUser(UserDto user)
{
throw new NotImplementedException();
try
{
IsLoading = true;
await UserGateway.CreateUser(user);
await FetchData(false);
}
catch (Exception)
{
throw;
}
finally
{
IsLoading = false;
}
UserAdd = new();
}
private Task HandleRemoveUser(UserDto args)
private async Task HandleUpdateUser(UserDto user)
{
throw new NotImplementedException();
try
{
IsLoading = true;
await UserGateway.UpdateUser(user);
await FetchData(false);
}
catch (Exception)
{
throw;
}
finally
{
IsLoading = false;
}
}
private async Task HandleRemoveUser(UserDto user)
{
if (user.Id == null)
{
return;
}
try
{
IsLoading = true;
await UserGateway.DeleteUser(user.Id);
await FetchData(false);
}
catch (Exception)
{
throw;
}
finally
{
IsLoading = false;
}
}
private void HandleResetUser(UserDto args)
{
UserAdd = new();
}
}

View File

@@ -4,13 +4,19 @@
gap: 8px;
}
::deep .search-container .select-container {
::deep .search-container, ::deep .select-container {
box-sizing: border-box;
max-width: 200px;
}
.container {
padding: 20px 200px;
display: grid;
grid-gap: 20px;
}
.line {
border: 1px solid var(--line);
}
.content {

View File

@@ -7,7 +7,9 @@ public class GlobalConstants
public readonly static Guid MEMBER_ID = Guid.Parse("{BCE14DEA-1748-4A76-8485-ADEE83DF5EFD}");
public const string ADMINISTRATOR = "Administrateur";
public const string ADMINISTRATOR_NORMALIZED = "ADMINISTRATEUR";
public const string MEMBER = "Membre";
public const string MEMBER_NORMALIZED = "MEMBRE";
public const string ADMIN_MEMBER = $"{ADMINISTRATOR}, {MEMBER}";
public const int JWT_DURATION_HOUR = 12;

View File

@@ -0,0 +1,6 @@
namespace GameIdeas.Shared.Dto;
public class IdDto
{
public string? Id { get; set; }
}

View File

@@ -72,11 +72,12 @@ public class UserController(
[Authorize(Roles = GlobalConstants.ADMINISTRATOR)]
[HttpPost("Create")]
public async Task<ActionResult<string>> CreateUser([FromBody] UserDto user)
public async Task<ActionResult<IdDto>> CreateUser([FromBody] UserDto user)
{
try
{
return Created("/Create", await userWriteService.CreateUser(user));
var id = new IdDto() { Id = await userWriteService.CreateUser(user) };
return Created("/Create", id);
}
catch (Exception e)
{
@@ -87,11 +88,12 @@ public class UserController(
[Authorize(Roles = GlobalConstants.ADMINISTRATOR)]
[HttpPut("Update/{userId}")]
public async Task<ActionResult<string>> UpdateUser(string userId, [FromBody] UserDto user)
public async Task<ActionResult<IdDto>> UpdateUser(string userId, [FromBody] UserDto user)
{
try
{
return Created("/Update", await userWriteService.UpdateUser(userId, user));
var id = new IdDto() { Id = await userWriteService.UpdateUser(userId, user) };
return Created("/Update", id);
}
catch (Exception e)
{
@@ -102,11 +104,12 @@ public class UserController(
[Authorize(Roles = GlobalConstants.ADMINISTRATOR)]
[HttpDelete("Delete/{userId}")]
public async Task<ActionResult<string>> DeleteUser(string userId)
public async Task<ActionResult<IdDto>> DeleteUser(string userId)
{
try
{
return Created("/Delete", await userWriteService.DeleteUser(userId));
var id = new IdDto() { Id = await userWriteService.DeleteUser(userId) };
return Created("/Delete", id);
}
catch (Exception e)
{

View File

@@ -19,13 +19,13 @@ namespace GameIdeas.WebAPI.Migrations
{
GlobalConstants.ADMINISTRATOR_ID.ToString(),
GlobalConstants.ADMINISTRATOR,
GlobalConstants.ADMINISTRATOR.Normalize(),
GlobalConstants.ADMINISTRATOR_NORMALIZED,
Guid.NewGuid().ToString()
},
{
GlobalConstants.MEMBER_ID.ToString(),
GlobalConstants.MEMBER,
GlobalConstants.MEMBER.Normalize(),
GlobalConstants.MEMBER_NORMALIZED,
Guid.NewGuid().ToString()
}
});

View File

@@ -59,6 +59,17 @@ services.AddAuthentication(options =>
};
});
services.Configure<IdentityOptions>(options =>
{
// Default Password settings.
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequiredLength = 6;
options.Password.RequiredUniqueChars = 1;
});
services.AddAuthorization();
services.AddSingleton<TranslationService>();

View File

@@ -29,7 +29,7 @@ public class UserWriteService(
}
else
{
throw new UserInvalidException(string.Join("; ", result.Errors));
throw new UserInvalidException(string.Join("; ", result.Errors.Select(e => $"{e.Code} {e.Description}")));
}
return userToCreate.Id;