Add user manager page (#22)
Reviewed-on: #22
This commit was merged in pull request #22.
This commit is contained in:
@@ -13,12 +13,8 @@ public static class GameHelper
|
|||||||
throw new ArgumentNullException(nameof(authState), "Authentication state missing");
|
throw new ArgumentNullException(nameof(authState), "Authentication state missing");
|
||||||
}
|
}
|
||||||
|
|
||||||
var userId = authState.User.FindFirstValue(ClaimTypes.Sid);
|
var userId = authState.User.FindFirstValue(ClaimTypes.Sid)
|
||||||
|
?? throw new ArgumentNullException(nameof(authState), "user state missing");
|
||||||
if (userId == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(authState), "user state missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
game.CreationUserId = userId;
|
game.CreationUserId = userId;
|
||||||
game.CreationDate = DateTime.Now;
|
game.CreationDate = DateTime.Now;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Components;
|
|||||||
|
|
||||||
namespace GameIdeas.BlazorApp.Pages.Games;
|
namespace GameIdeas.BlazorApp.Pages.Games;
|
||||||
|
|
||||||
public partial class Game
|
public partial class Games
|
||||||
{
|
{
|
||||||
[Inject] private IGameGateway GameGateway { get; set; } = default!;
|
[Inject] private IGameGateway GameGateway { get; set; } = default!;
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ public class GameGateway(IHttpClientService httpClientService) : IGameGateway
|
|||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
throw new CategoryNotFoundException(ResourcesKey.ErrorFetchGames);
|
throw new GameNotFoundException(ResourcesKey.ErrorFetchGames);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@using GameIdeas.BlazorApp.Pages.Games
|
@using GameIdeas.BlazorApp.Pages.Games
|
||||||
@using GameIdeas.BlazorApp.Pages.User
|
@using GameIdeas.BlazorApp.Pages.UserMenu
|
||||||
@using GameIdeas.BlazorApp.Shared.Components.Select
|
@using GameIdeas.BlazorApp.Shared.Components.Select
|
||||||
@using GameIdeas.BlazorApp.Shared.Components.Select.Models
|
@using GameIdeas.BlazorApp.Shared.Components.Select.Models
|
||||||
@using GameIdeas.BlazorApp.Shared.Models
|
@using GameIdeas.BlazorApp.Shared.Models
|
||||||
@@ -10,32 +10,35 @@
|
|||||||
@inherits ComponentBase
|
@inherits ComponentBase
|
||||||
|
|
||||||
<div class="header-tab">
|
<div class="header-tab">
|
||||||
<div class="icon-container" @onclick="HandleIconClicked">
|
<a href="/Games" class="icon-container">
|
||||||
<img src="icon.png" alt="Game Ideas">
|
<img src="icon.png" alt="Game Ideas">
|
||||||
</div>
|
</a>
|
||||||
|
|
||||||
@ChildContent
|
@ChildContent
|
||||||
|
|
||||||
<div class="account-add-container">
|
<div class="account-add-container">
|
||||||
<AuthorizeView Roles="@GlobalConstants.ADMIN_MEMBER">
|
@if (DisplayAdd)
|
||||||
<Authorized>
|
{
|
||||||
<div class="add-buttons">
|
<AuthorizeView Roles="@GlobalConstants.ADMIN_MEMBER">
|
||||||
<div class="first-button button">
|
<Authorized>
|
||||||
<svg class="button-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
<div class="add-buttons">
|
||||||
<path d="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
|
<div class="first-button button">
|
||||||
</svg>
|
|
||||||
</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">
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</Select>
|
<Select @ref="SelectListAdd" TItem="KeyValuePair<AddType, string>" THeader="object"
|
||||||
</div>
|
ValuesChanged=HandleAddTypeClicked Params=SelectParams Theme="SelectTheme.Navigation">
|
||||||
</Authorized>
|
<div class="second-button button">
|
||||||
</AuthorizeView>
|
<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 />
|
<UserMenu />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ namespace GameIdeas.BlazorApp.Pages.Games.Header;
|
|||||||
|
|
||||||
public partial class GameHeader : ComponentBase
|
public partial class GameHeader : ComponentBase
|
||||||
{
|
{
|
||||||
|
[Parameter] public bool DisplayAdd { get; set; } = true;
|
||||||
[Parameter] public EventCallback<AddType> AddTypeChanged { get; set; }
|
[Parameter] public EventCallback<AddType> AddTypeChanged { get; set; }
|
||||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||||
|
|
||||||
@@ -31,11 +32,6 @@ public partial class GameHeader : ComponentBase
|
|||||||
base.OnInitialized();
|
base.OnInitialized();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleIconClicked()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleAddTypeClicked(IEnumerable<KeyValuePair<AddType, string>> values)
|
private async Task HandleAddTypeClicked(IEnumerable<KeyValuePair<AddType, string>> values)
|
||||||
{
|
{
|
||||||
SelectListAdd?.Close();
|
SelectListAdd?.Close();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@using Blazored.FluentValidation
|
@using Blazored.FluentValidation
|
||||||
|
|
||||||
<EditForm EditContext="EditContext" OnSubmit="HandleLoginSubmit">
|
<EditForm EditContext="EditContext" OnSubmit="HandleLoginSubmit">
|
||||||
<FluentValidationValidator />
|
<FluentValidationValidator Validator="Validator" />
|
||||||
<div class="login-form">
|
<div class="login-form">
|
||||||
<div class="login-field">
|
<div class="login-field">
|
||||||
<div class="input-title">@ResourcesKey.EnterUsername</div>
|
<div class="input-title">@ResourcesKey.EnterUsername</div>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="login-field">
|
<div class="login-field">
|
||||||
<div class="input-title">@ResourcesKey.EnterPassword</div>
|
<div class="input-title">@ResourcesKey.EnterPassword</div>
|
||||||
<InputText class="input-text"
|
<InputText class="input-text" type="password"
|
||||||
@bind-Value="UserDto.Password" />
|
@bind-Value="UserDto.Password" />
|
||||||
</div>
|
</div>
|
||||||
<div class="login-field">
|
<div class="login-field">
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
using GameIdeas.BlazorApp.Pages.User.Gateways;
|
using GameIdeas.BlazorApp.Pages.UserMenu.Gateways;
|
||||||
using GameIdeas.Shared.Dto;
|
using GameIdeas.Shared.Dto;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.AspNetCore.Components.Forms;
|
using Microsoft.AspNetCore.Components.Forms;
|
||||||
|
|
||||||
namespace GameIdeas.BlazorApp.Pages.User.Components;
|
namespace GameIdeas.BlazorApp.Pages.UserMenu.Components;
|
||||||
|
|
||||||
public partial class Login
|
public partial class Login
|
||||||
{
|
{
|
||||||
@@ -12,7 +12,7 @@ public partial class Login
|
|||||||
private EditContext? EditContext;
|
private EditContext? EditContext;
|
||||||
private UserDto UserDto = new();
|
private UserDto UserDto = new();
|
||||||
private bool IsLoading = false;
|
private bool IsLoading = false;
|
||||||
|
private LoginValidator Validator = new();
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
EditContext = new EditContext(UserDto);
|
EditContext = new EditContext(UserDto);
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using GameIdeas.Shared.Dto;
|
using GameIdeas.Shared.Dto;
|
||||||
|
|
||||||
namespace GameIdeas.BlazorApp.Pages.User.Components;
|
namespace GameIdeas.BlazorApp.Pages.UserMenu.Components;
|
||||||
|
|
||||||
public class LoginValidator : AbstractValidator<UserDto>
|
public class LoginValidator : AbstractValidator<UserDto>
|
||||||
{
|
{
|
||||||
@@ -5,7 +5,7 @@ using GameIdeas.Resources;
|
|||||||
using GameIdeas.Shared.Dto;
|
using GameIdeas.Shared.Dto;
|
||||||
using Microsoft.AspNetCore.Components.Authorization;
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
|
|
||||||
namespace GameIdeas.BlazorApp.Pages.User.Gateways;
|
namespace GameIdeas.BlazorApp.Pages.UserMenu.Gateways;
|
||||||
|
|
||||||
public class AuthGateway(IHttpClientService httpClient,
|
public class AuthGateway(IHttpClientService httpClient,
|
||||||
AuthenticationStateProvider stateProvider) : IAuthGateway
|
AuthenticationStateProvider stateProvider) : IAuthGateway
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using GameIdeas.Shared.Dto;
|
using GameIdeas.Shared.Dto;
|
||||||
|
|
||||||
namespace GameIdeas.BlazorApp.Pages.User.Gateways;
|
namespace GameIdeas.BlazorApp.Pages.UserMenu.Gateways;
|
||||||
|
|
||||||
public interface IAuthGateway
|
public interface IAuthGateway
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@using GameIdeas.BlazorApp.Pages.User.Components
|
@using GameIdeas.BlazorApp.Pages.UserMenu.Components
|
||||||
@using GameIdeas.BlazorApp.Shared.Components.BackdropFilter
|
@using GameIdeas.BlazorApp.Shared.Components.BackdropFilter
|
||||||
@using GameIdeas.BlazorApp.Shared.Constants
|
@using GameIdeas.BlazorApp.Shared.Constants
|
||||||
@using GameIdeas.Shared.Constants
|
@using GameIdeas.Shared.Constants
|
||||||
@@ -23,9 +23,9 @@
|
|||||||
|
|
||||||
<AuthorizeView Roles="@GlobalConstants.ADMINISTRATOR">
|
<AuthorizeView Roles="@GlobalConstants.ADMINISTRATOR">
|
||||||
<Authorized>
|
<Authorized>
|
||||||
<div class="menu-element">
|
<a class="menu-element" href="/Users">
|
||||||
@ResourcesKey.UserManager
|
@ResourcesKey.UserManager
|
||||||
</div>
|
</a>
|
||||||
<span class="line"></span>
|
<span class="line"></span>
|
||||||
</Authorized>
|
</Authorized>
|
||||||
</AuthorizeView>
|
</AuthorizeView>
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
using GameIdeas.BlazorApp.Pages.User.Gateways;
|
using GameIdeas.BlazorApp.Pages.UserMenu.Gateways;
|
||||||
using GameIdeas.BlazorApp.Services;
|
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.AspNetCore.Components.Authorization;
|
|
||||||
|
|
||||||
namespace GameIdeas.BlazorApp.Pages.User;
|
namespace GameIdeas.BlazorApp.Pages.UserMenu;
|
||||||
|
|
||||||
public partial class UserMenu
|
public partial class UserMenu
|
||||||
{
|
{
|
||||||
[Inject] private IAuthGateway AuthGateway { get; set; } = default!;
|
[Inject] private IAuthGateway AuthGateway { get; set; } = default!;
|
||||||
|
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
|
||||||
|
|
||||||
private bool ContentVisile = false;
|
private bool ContentVisile = false;
|
||||||
|
|
||||||
private async Task HandleLogoutClicked()
|
private async Task HandleLogoutClicked()
|
||||||
{
|
{
|
||||||
await AuthGateway.Logout();
|
|
||||||
ContentVisile = false;
|
ContentVisile = false;
|
||||||
|
await AuthGateway.Logout();
|
||||||
|
NavigationManager.NavigateTo("/Games");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleAccountClicked()
|
private void HandleAccountClicked()
|
||||||
@@ -36,6 +36,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.menu-element {
|
.menu-element {
|
||||||
|
color: var(--white);
|
||||||
|
text-decoration: none;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
@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">
|
||||||
|
<InputText class="input-name" @bind-Value="@User.Username" disabled="@(!IsEditing)" />
|
||||||
|
</div>
|
||||||
|
<div class="password">
|
||||||
|
<InputText type="password" class="input-password" placeholder="********" @bind-Value="@User.Password" disabled="@(!IsEditing)" />
|
||||||
|
</div>
|
||||||
|
<div class="role">
|
||||||
|
<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>
|
||||||
|
@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>
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using GameIdeas.BlazorApp.Shared.Components.Select;
|
||||||
|
using GameIdeas.BlazorApp.Shared.Components.Select.Models;
|
||||||
|
using GameIdeas.Shared.Dto;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.AspNetCore.Components.Forms;
|
||||||
|
|
||||||
|
namespace GameIdeas.BlazorApp.Pages.Users.Components;
|
||||||
|
|
||||||
|
public partial class UserRow
|
||||||
|
{
|
||||||
|
[Parameter] public UserDto User { get; set; } = new();
|
||||||
|
[Parameter] public List<RoleDto> Roles { get; set; } = [];
|
||||||
|
[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;
|
||||||
|
private UserDto? OriginalUser;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
EditContext = new(User);
|
||||||
|
OriginalUser = new()
|
||||||
|
{
|
||||||
|
Username = User.Username,
|
||||||
|
Role = User.Role
|
||||||
|
};
|
||||||
|
|
||||||
|
base.OnInitialized();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnParametersSet()
|
||||||
|
{
|
||||||
|
SelectRoleParams = new()
|
||||||
|
{
|
||||||
|
GetItemLabel = role => role.Name,
|
||||||
|
Items = Roles
|
||||||
|
};
|
||||||
|
|
||||||
|
base.OnParametersSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleValuesChanged(IEnumerable<RoleDto> roles)
|
||||||
|
{
|
||||||
|
User.Role = roles.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleSubmitClicked()
|
||||||
|
{
|
||||||
|
if (EditContext?.Validate() == false)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsEditing = false;
|
||||||
|
await OnSubmit.InvokeAsync(User);
|
||||||
|
User.Password = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void HandleEditClicked()
|
||||||
|
{
|
||||||
|
if (IsEditing)
|
||||||
|
{
|
||||||
|
User.Username = OriginalUser?.Username;
|
||||||
|
User.Role = OriginalUser?.Role;
|
||||||
|
User.Password = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsEditing = !IsEditing;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleRemoveClicked()
|
||||||
|
{
|
||||||
|
await OnRemove.InvokeAsync(User);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
.row {
|
||||||
|
height: 64px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 48px 1fr 1fr 1fr auto;
|
||||||
|
grid-gap: 8px;
|
||||||
|
padding: 0 8px;
|
||||||
|
background: var(--input-secondary);
|
||||||
|
box-shadow: var(--drop-shadow);
|
||||||
|
border-radius: var(--big-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row > * {
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon ::deep svg {
|
||||||
|
fill: var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.role {
|
||||||
|
min-width: 160px;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
::deep .input-name,
|
||||||
|
::deep .input-name[disabled],
|
||||||
|
::deep .input-password,
|
||||||
|
::deep .input-password[disabled],
|
||||||
|
::deep .input-password::placeholder {
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
::deep .input-name,
|
||||||
|
::deep .input-password,
|
||||||
|
.role-label {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: none;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: var(--small-radius);
|
||||||
|
border: solid 1px var(--line);
|
||||||
|
background: rgb(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
::deep .input-name[disabled],
|
||||||
|
::deep .input-password[disabled],
|
||||||
|
.disabled {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-label {
|
||||||
|
display: block;
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
height:auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons > * {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
margin: auto;
|
||||||
|
height: 28px;
|
||||||
|
width: 28px;
|
||||||
|
background: none;
|
||||||
|
border-radius: var(--small-radius);
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons > *:hover, .selected {
|
||||||
|
background: var(--input-selected)
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove ::deep svg {
|
||||||
|
fill: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit ::deep svg {
|
||||||
|
fill: var(--yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit ::deep svg {
|
||||||
|
fill: var(--green);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
@using GameIdeas.BlazorApp.Shared.Constants
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="icon">@Icons.Account</div>
|
||||||
|
<div class="name pill"></div>
|
||||||
|
<div class="password pill"></div>
|
||||||
|
<div class="role pill"></div>
|
||||||
|
<div class="buttons">
|
||||||
|
<span class="remove">@Icons.Bin</span>
|
||||||
|
<span class="edit">@Icons.Pen</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
.row {
|
||||||
|
height: 64px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 48px 1fr 1fr 1fr auto;
|
||||||
|
grid-gap: 8px;
|
||||||
|
padding: 0 8px;
|
||||||
|
background: var(--input-secondary);
|
||||||
|
box-shadow: var(--drop-shadow);
|
||||||
|
border-radius: var(--big-radius);
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
height: 48px;
|
||||||
|
width: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon ::deep svg {
|
||||||
|
fill: var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill {
|
||||||
|
animation: loading 3s ease infinite;
|
||||||
|
align-self: center;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: var(--small-radius);
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
height: auto;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons > * {
|
||||||
|
margin: auto;
|
||||||
|
height: 28px;
|
||||||
|
width: 28px;
|
||||||
|
background: none;
|
||||||
|
border-radius: var(--small-radius);
|
||||||
|
padding: 2px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove ::deep svg {
|
||||||
|
fill: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit ::deep svg {
|
||||||
|
fill: var(--yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading {
|
||||||
|
0% {
|
||||||
|
background: rgb(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
background: rgb(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background: rgb(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using GameIdeas.Resources;
|
||||||
|
using GameIdeas.Shared.Dto;
|
||||||
|
|
||||||
|
namespace GameIdeas.BlazorApp.Pages.Users.Components;
|
||||||
|
|
||||||
|
public class UserCreateValidator : AbstractValidator<UserDto>
|
||||||
|
{
|
||||||
|
public UserCreateValidator()
|
||||||
|
{
|
||||||
|
RuleFor(user => user.Username)
|
||||||
|
.NotEmpty()
|
||||||
|
.WithMessage(ResourcesKey.MissingField);
|
||||||
|
|
||||||
|
RuleFor(user => user.Password)
|
||||||
|
.NotEmpty()
|
||||||
|
.WithMessage(ResourcesKey.MissingField);
|
||||||
|
|
||||||
|
RuleFor(user => user.Role)
|
||||||
|
.NotEmpty()
|
||||||
|
.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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using GameIdeas.Shared.Dto;
|
||||||
|
|
||||||
|
namespace GameIdeas.BlazorApp.Pages.Users.Filters;
|
||||||
|
|
||||||
|
public class UserFilterParams
|
||||||
|
{
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public List<RoleDto>? Roles { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using GameIdeas.BlazorApp.Pages.Users.Filters;
|
||||||
|
using GameIdeas.Shared.Dto;
|
||||||
|
|
||||||
|
namespace GameIdeas.BlazorApp.Pages.Users.Gateways;
|
||||||
|
|
||||||
|
public interface IUserGateway
|
||||||
|
{
|
||||||
|
Task<UserListDto> GetUsers(UserFilterParams filterParams, int currentPage);
|
||||||
|
Task<IEnumerable<RoleDto>> GetRoles();
|
||||||
|
Task<IdDto> CreateUser(UserDto user);
|
||||||
|
Task<IdDto> UpdateUser(UserDto user);
|
||||||
|
Task<IdDto> DeleteUser(string userId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using GameIdeas.BlazorApp.Pages.Users.Filters;
|
||||||
|
using GameIdeas.BlazorApp.Services;
|
||||||
|
using GameIdeas.BlazorApp.Shared.Constants;
|
||||||
|
using GameIdeas.BlazorApp.Shared.Exceptions;
|
||||||
|
using GameIdeas.Resources;
|
||||||
|
using GameIdeas.Shared.Dto;
|
||||||
|
|
||||||
|
namespace GameIdeas.BlazorApp.Pages.Users.Gateways;
|
||||||
|
|
||||||
|
public class UserGateway(IHttpClientService httpClient) : IUserGateway
|
||||||
|
{
|
||||||
|
public async Task<IdDto> CreateUser(UserDto user)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await httpClient.PostAsync<IdDto>(Endpoints.User.Create, user)
|
||||||
|
?? throw new InvalidOperationException(ResourcesKey.ErrorCreateUser);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
throw new UserCreationException(ResourcesKey.ErrorCreateUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IdDto> DeleteUser(string userId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await httpClient.DeleteAsync<IdDto>(Endpoints.User.Delete(userId))
|
||||||
|
?? throw new InvalidOperationException(ResourcesKey.ErrorDeleteUser);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
throw new UserCreationException(ResourcesKey.ErrorDeleteUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<RoleDto>> GetRoles()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await httpClient.FetchDataAsync<IEnumerable<RoleDto>>(Endpoints.User.Roles)
|
||||||
|
?? throw new InvalidOperationException(ResourcesKey.ErrorFetchRoles);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
throw new RoleNotFoundException(ResourcesKey.ErrorFetchRoles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserListDto> GetUsers(UserFilterParams filterParams, int currentPage)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UserFilterDto filter = new()
|
||||||
|
{
|
||||||
|
CurrentPage = currentPage,
|
||||||
|
Name = filterParams.Name,
|
||||||
|
RoleIds = filterParams.Roles?.Select(r => r.Id)
|
||||||
|
};
|
||||||
|
|
||||||
|
var url = Endpoints.User.Fetch(filter);
|
||||||
|
return await httpClient.FetchDataAsync<UserListDto>(url)
|
||||||
|
?? throw new InvalidOperationException(ResourcesKey.ErrorFetchUsers);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
throw new UserNotFoundException(ResourcesKey.ErrorFetchUsers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IdDto> UpdateUser(UserDto user)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await httpClient.PutAsync<IdDto>(Endpoints.User.Update(user.Id ?? string.Empty), user)
|
||||||
|
?? throw new InvalidOperationException(ResourcesKey.ErrorUpdateUser);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
throw new UserCreationException(ResourcesKey.ErrorUpdateUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
@page "/Users"
|
||||||
|
@using GameIdeas.BlazorApp.Pages.Games.Header
|
||||||
|
@using GameIdeas.BlazorApp.Layouts
|
||||||
|
@using GameIdeas.BlazorApp.Pages.Users.Components
|
||||||
|
@using GameIdeas.BlazorApp.Shared.Components.Popup
|
||||||
|
@using GameIdeas.BlazorApp.Shared.Components.Popup.Components
|
||||||
|
@using GameIdeas.BlazorApp.Shared.Components.Search
|
||||||
|
@using GameIdeas.BlazorApp.Shared.Components.Select.Models
|
||||||
|
@using GameIdeas.BlazorApp.Shared.Components.SelectSearch
|
||||||
|
@using GameIdeas.Shared.Dto
|
||||||
|
|
||||||
|
@layout MainLayout
|
||||||
|
|
||||||
|
<PageTitle>@ResourcesKey.GamesIdeas</PageTitle>
|
||||||
|
|
||||||
|
<GameHeader DisplayAdd="false">
|
||||||
|
<div class="header-content">
|
||||||
|
<SearchInput Placeholder="@ResourcesKey.EnterUsername" @bind-Text="FilterParams.Name" @bind-Text:after=HandleFilterChanged />
|
||||||
|
<SelectSearch TItem="RoleDto" Placeholder="@ResourcesKey.Roles" @bind-Values="FilterParams.Roles" @bind-Values:after=HandleFilterChanged
|
||||||
|
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="HandleOpenConfirmationPopup" OnSubmit="HandleUpdateUser" Validator="@(new UserUpdateValidator())" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@for (int i = 0; i < 20; i++)
|
||||||
|
{
|
||||||
|
<UserRowSkeleton />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Popup @ref=Popup Closable=false>
|
||||||
|
<ConfirmDelete OnCancel="HandleCancelPopupClicked" OnConfirm="HandleRemoveUser" />
|
||||||
|
</Popup>
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
using GameIdeas.BlazorApp.Pages.Users.Filters;
|
||||||
|
using GameIdeas.BlazorApp.Pages.Users.Gateways;
|
||||||
|
using GameIdeas.BlazorApp.Shared.Components.Popup;
|
||||||
|
using GameIdeas.Shared.Dto;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace GameIdeas.BlazorApp.Pages.Users;
|
||||||
|
|
||||||
|
public partial class Users
|
||||||
|
{
|
||||||
|
[Inject] private IUserGateway UserGateway { get; set; } = default!;
|
||||||
|
|
||||||
|
private Popup? Popup;
|
||||||
|
private bool IsLoading = false;
|
||||||
|
private UserFilterParams FilterParams = new();
|
||||||
|
private UserListDto UserList = new();
|
||||||
|
private IEnumerable<RoleDto> Roles = [];
|
||||||
|
private int CurrentPage = 1;
|
||||||
|
private UserDto UserAdd = new();
|
||||||
|
private UserDto? UserDelete;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await FetchData();
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task FetchData(bool fetchRoles = true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IsLoading = true;
|
||||||
|
|
||||||
|
if (fetchRoles)
|
||||||
|
Roles = await UserGateway.GetRoles();
|
||||||
|
|
||||||
|
UserList = await UserGateway.GetUsers(FilterParams, CurrentPage);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleSubmitUser(UserDto user)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IsLoading = true;
|
||||||
|
|
||||||
|
await UserGateway.CreateUser(user);
|
||||||
|
await FetchData(false);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserAdd = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleUpdateUser(UserDto user)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IsLoading = true;
|
||||||
|
|
||||||
|
await UserGateway.UpdateUser(user);
|
||||||
|
await FetchData(false);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleRemoveUser()
|
||||||
|
{
|
||||||
|
Popup?.Close();
|
||||||
|
|
||||||
|
if (UserDelete?.Id == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IsLoading = true;
|
||||||
|
|
||||||
|
await UserGateway.DeleteUser(UserDelete.Id);
|
||||||
|
await FetchData(false);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserDelete = null;
|
||||||
|
}
|
||||||
|
private void HandleResetUser(UserDto args)
|
||||||
|
{
|
||||||
|
UserAdd = new();
|
||||||
|
}
|
||||||
|
private async Task HandleFilterChanged()
|
||||||
|
{
|
||||||
|
await FetchData(false);
|
||||||
|
}
|
||||||
|
private void HandleCancelPopupClicked()
|
||||||
|
{
|
||||||
|
Popup?.Close();
|
||||||
|
}
|
||||||
|
private void HandleOpenConfirmationPopup(UserDto user)
|
||||||
|
{
|
||||||
|
UserDelete = user;
|
||||||
|
Popup?.Open();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::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 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
.container {
|
||||||
|
padding: 20px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,8 @@ using System.Net.Http.Json;
|
|||||||
using Blazored.LocalStorage;
|
using Blazored.LocalStorage;
|
||||||
using GameIdeas.BlazorApp;
|
using GameIdeas.BlazorApp;
|
||||||
using GameIdeas.BlazorApp.Pages.Games.Gateways;
|
using GameIdeas.BlazorApp.Pages.Games.Gateways;
|
||||||
using GameIdeas.BlazorApp.Pages.User.Gateways;
|
using GameIdeas.BlazorApp.Pages.UserMenu.Gateways;
|
||||||
|
using GameIdeas.BlazorApp.Pages.Users.Gateways;
|
||||||
using GameIdeas.BlazorApp.Services;
|
using GameIdeas.BlazorApp.Services;
|
||||||
using GameIdeas.Resources;
|
using GameIdeas.Resources;
|
||||||
using Microsoft.AspNetCore.Components.Authorization;
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
@@ -37,6 +38,7 @@ services.AddScoped<IHttpClientService, HttpClientService>();
|
|||||||
|
|
||||||
services.AddScoped<IAuthGateway, AuthGateway>();
|
services.AddScoped<IAuthGateway, AuthGateway>();
|
||||||
services.AddScoped<IGameGateway, GameGateway>();
|
services.AddScoped<IGameGateway, GameGateway>();
|
||||||
|
services.AddScoped<IUserGateway, UserGateway>();
|
||||||
|
|
||||||
services.AddSingleton<TranslationService>();
|
services.AddSingleton<TranslationService>();
|
||||||
services.AddSingleton<Translations>();
|
services.AddSingleton<Translations>();
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<div class="confirm-section">
|
||||||
|
<span class="descrption">@ResourcesKey.ConfirmDeleteDescription</span>
|
||||||
|
<div class="buttons">
|
||||||
|
<div class="cancel" @onclick=HandleCancelClicked>@ResourcesKey.Cancel</div>
|
||||||
|
<div class="confirm" @onclick=HandleConfirmClicked>@ResourcesKey.Confirm</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace GameIdeas.BlazorApp.Shared.Components.Popup.Components;
|
||||||
|
|
||||||
|
public partial class ConfirmDelete
|
||||||
|
{
|
||||||
|
[Parameter] public EventCallback OnCancel { get; set; }
|
||||||
|
[Parameter] public EventCallback OnConfirm { get; set; }
|
||||||
|
|
||||||
|
private async Task HandleConfirmClicked()
|
||||||
|
{
|
||||||
|
await OnConfirm.InvokeAsync();
|
||||||
|
}
|
||||||
|
private async Task HandleCancelClicked()
|
||||||
|
{
|
||||||
|
await OnCancel.InvokeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
.confirm-section {
|
||||||
|
padding: 10px 20px;
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.descrption {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
justify-content: end;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.cancel, .confirm {
|
||||||
|
height: 28px;
|
||||||
|
align-content: center;
|
||||||
|
padding: 0 10px;
|
||||||
|
background: var(--violet);
|
||||||
|
border-radius: var(--small-radius);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel:hover, .confirm:hover {
|
||||||
|
background: var(--violet-selected);
|
||||||
|
}
|
||||||
@@ -2,8 +2,7 @@
|
|||||||
@using GameIdeas.Shared.Constants
|
@using GameIdeas.Shared.Constants
|
||||||
|
|
||||||
<div class="search-container">
|
<div class="search-container">
|
||||||
<input id="searchInput"
|
<input type="text"
|
||||||
type="text"
|
|
||||||
class="search-field"
|
class="search-field"
|
||||||
placeholder="@Placeholder"
|
placeholder="@Placeholder"
|
||||||
disabled="@IsDisable"
|
disabled="@IsDisable"
|
||||||
|
|||||||
@@ -43,3 +43,12 @@
|
|||||||
.navigation .selected {
|
.navigation .selected {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/***** Single Theme *****/
|
||||||
|
.single {
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.single .selected {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ public static class SelectHelper
|
|||||||
SelectTheme.Filter => "filter",
|
SelectTheme.Filter => "filter",
|
||||||
SelectTheme.AdvancedFilter => "advanced-filter",
|
SelectTheme.AdvancedFilter => "advanced-filter",
|
||||||
SelectTheme.Creation => "creation",
|
SelectTheme.Creation => "creation",
|
||||||
|
SelectTheme.Single => "single",
|
||||||
_ => string.Empty
|
_ => string.Empty
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ public enum SelectTheme
|
|||||||
Sort,
|
Sort,
|
||||||
Filter,
|
Filter,
|
||||||
AdvancedFilter,
|
AdvancedFilter,
|
||||||
Creation
|
Creation,
|
||||||
|
Single
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
z-index: var(--index-dropdown);
|
z-index: var(--index-dropdown);
|
||||||
border-radius: var(--small-radius);
|
border-radius: var(--small-radius);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
box-shadow: var(--drop-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
@@ -23,7 +24,6 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
animation-name: fade-in;
|
animation-name: fade-in;
|
||||||
animation-duration: 0.2s;
|
animation-duration: 0.2s;
|
||||||
box-shadow: var(--drop-shadow);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.line {
|
.line {
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
border-bottom: 2px solid var(--input-selected);
|
border-bottom: 2px solid var(--input-selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
/***** Sort Theme *****/
|
/***** Creation Theme *****/
|
||||||
.creation .content {
|
.creation .content {
|
||||||
border-radius: var(--small-radius);
|
border-radius: var(--small-radius);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -100,3 +100,7 @@
|
|||||||
border-bottom: 2px solid var(--input-selected);
|
border-bottom: 2px solid var(--input-selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/***** Single Theme *****/
|
||||||
|
.single {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
@typeparam TItem
|
@typeparam TItem
|
||||||
|
|
||||||
<Select @ref=Select TItem="TItem" THeader="string" Theme="Theme" Type="SelectType.Multiple"
|
<Select @ref=Select TItem="TItem" THeader="string" Theme="Theme" Type="SelectType"
|
||||||
Params="SelectParams" Values=Values ValuesChanged="HandleValuesChanged" QuickAdd=QuickAdd>
|
Params="SelectParams" Values=Values ValuesChanged="HandleValuesChanged" QuickAdd=QuickAdd>
|
||||||
|
|
||||||
<div class="@SelectHelper.GetClassFromTheme(Theme)">
|
<div class="@SelectHelper.GetClassFromTheme(Theme)">
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public partial class SelectSearch<TItem>
|
|||||||
[Parameter] public string Placeholder { get; set; } = string.Empty;
|
[Parameter] public string Placeholder { get; set; } = string.Empty;
|
||||||
[Parameter] public bool QuickAdd { get; set; } = false;
|
[Parameter] public bool QuickAdd { get; set; } = false;
|
||||||
[Parameter] public Func<string, TItem>? AddItem { get; set; }
|
[Parameter] public Func<string, TItem>? AddItem { get; set; }
|
||||||
|
[Parameter] public SelectType SelectType { get; set; } = SelectType.Multiple;
|
||||||
|
|
||||||
private SelectParams<TItem, string> SelectParams = new();
|
private SelectParams<TItem, string> SelectParams = new();
|
||||||
private SearchInput? SearchInput;
|
private SearchInput? SearchInput;
|
||||||
|
|||||||
@@ -7,17 +7,27 @@ public static class Endpoints
|
|||||||
{
|
{
|
||||||
public static class Game
|
public static class Game
|
||||||
{
|
{
|
||||||
public static readonly string Create = "api/Game/Create";
|
public const string Create = "api/Game/Create";
|
||||||
public static string Fetch(GameFilterDto filter) => $"api/Game?{UrlHelper.BuildUrlParams(filter)}";
|
public static string Fetch(GameFilterDto filter) => $"api/Game?{UrlHelper.BuildUrlParams(filter)}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Category
|
public static class Category
|
||||||
{
|
{
|
||||||
public static readonly string AllCategories = "api/Category/All";
|
public const string AllCategories = "api/Category/All";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Auth
|
public static class Auth
|
||||||
{
|
{
|
||||||
public static readonly string Login = "api/User/Login";
|
public const string Login = "api/User/Login";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class User
|
||||||
|
{
|
||||||
|
public static string Fetch(UserFilterDto filter) => $"api/User?{UrlHelper.BuildUrlParams(filter)}";
|
||||||
|
public const string Roles = "api/User/Roles";
|
||||||
|
public const string Create = "api/User/Create";
|
||||||
|
public static string Delete(string userId) => $"api/User/Delete/{userId}";
|
||||||
|
public static string Update(string userId) => $"api/User/Update/{userId}";
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,4 +27,16 @@ public static class Icons
|
|||||||
public readonly static MarkupString Account = new(OpenBraket +
|
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\" />" +
|
"<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);
|
CloseBraket);
|
||||||
|
|
||||||
|
public readonly static MarkupString Bin = new(OpenBraket +
|
||||||
|
"<path d=\"M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z\" />" +
|
||||||
|
CloseBraket);
|
||||||
|
|
||||||
|
public readonly static MarkupString Pen = new(OpenBraket +
|
||||||
|
"<path d=\"M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z\" />" +
|
||||||
|
CloseBraket);
|
||||||
|
|
||||||
|
public readonly static MarkupString Check = new(OpenBraket +
|
||||||
|
"<path d=\"M9,20.42L2.79,14.21L5.62,11.38L9,14.77L18.88,4.88L21.71,7.71L9,20.42Z\" />" +
|
||||||
|
CloseBraket);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace GameIdeas.BlazorApp.Shared.Exceptions;
|
||||||
|
|
||||||
|
public class RoleNotFoundException(string message) : Exception(message);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace GameIdeas.BlazorApp.Shared.Exceptions;
|
||||||
|
|
||||||
|
public class UserCreationException(string message) : Exception(message);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace GameIdeas.BlazorApp.Shared.Exceptions;
|
||||||
|
|
||||||
|
public class UserNotFoundException(string message) : Exception(message);
|
||||||
@@ -54,6 +54,16 @@ public class Translations (TranslationService translationService)
|
|||||||
public string UserUnauthorized => translationService.Translate(nameof(UserUnauthorized));
|
public string UserUnauthorized => translationService.Translate(nameof(UserUnauthorized));
|
||||||
public string UserLoginFailed => translationService.Translate(nameof(UserLoginFailed));
|
public string UserLoginFailed => translationService.Translate(nameof(UserLoginFailed));
|
||||||
public string UserLogoutFailed => translationService.Translate(nameof(UserLogoutFailed));
|
public string UserLogoutFailed => translationService.Translate(nameof(UserLogoutFailed));
|
||||||
|
public string Roles => translationService.Translate(nameof(Roles));
|
||||||
|
public string ErrorFetchUsers => translationService.Translate(nameof(ErrorFetchUsers));
|
||||||
|
public string ErrorFetchRoles => translationService.Translate(nameof(ErrorFetchRoles));
|
||||||
|
public string MissingField => translationService.Translate(nameof(MissingField));
|
||||||
|
public string ErrorCreateUser => translationService.Translate(nameof(ErrorCreateUser));
|
||||||
|
public string ErrorUpdateUser => translationService.Translate(nameof(ErrorUpdateUser));
|
||||||
|
public string ErrorDeleteUser => translationService.Translate(nameof(ErrorDeleteUser));
|
||||||
|
public string Cancel => translationService.Translate(nameof(Cancel));
|
||||||
|
public string Confirm => translationService.Translate(nameof(Confirm));
|
||||||
|
public string ConfirmDeleteDescription => translationService.Translate(nameof(ConfirmDeleteDescription));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ResourcesKey
|
public static class ResourcesKey
|
||||||
@@ -116,4 +126,14 @@ public static class ResourcesKey
|
|||||||
public static string UserUnauthorized => _instance?.UserUnauthorized ?? throw new InvalidOperationException("ResourcesKey.UserUnauthorized is not initialized.");
|
public static string UserUnauthorized => _instance?.UserUnauthorized ?? throw new InvalidOperationException("ResourcesKey.UserUnauthorized is not initialized.");
|
||||||
public static string UserLoginFailed => _instance?.UserLoginFailed ?? throw new InvalidOperationException("ResourcesKey.UserLoginFailed is not initialized.");
|
public static string UserLoginFailed => _instance?.UserLoginFailed ?? throw new InvalidOperationException("ResourcesKey.UserLoginFailed is not initialized.");
|
||||||
public static string UserLogoutFailed => _instance?.UserLogoutFailed ?? throw new InvalidOperationException("ResourcesKey.UserLogoutFailed is not initialized.");
|
public static string UserLogoutFailed => _instance?.UserLogoutFailed ?? throw new InvalidOperationException("ResourcesKey.UserLogoutFailed is not initialized.");
|
||||||
|
public static string Roles => _instance?.Roles ?? throw new InvalidOperationException("ResourcesKey.Roles is not initialized.");
|
||||||
|
public static string ErrorFetchUsers => _instance?.ErrorFetchUsers ?? throw new InvalidOperationException("ResourcesKey.ErrorFetchUsers is not initialized.");
|
||||||
|
public static string ErrorFetchRoles => _instance?.ErrorFetchRoles ?? throw new InvalidOperationException("ResourcesKey.ErrorFetchRoles is not initialized.");
|
||||||
|
public static string MissingField => _instance?.MissingField ?? throw new InvalidOperationException("ResourcesKey.MissingField is not initialized.");
|
||||||
|
public static string ErrorCreateUser => _instance?.ErrorCreateUser ?? throw new InvalidOperationException("ResourcesKey.ErrorCreateUser is not initialized.");
|
||||||
|
public static string ErrorUpdateUser => _instance?.ErrorUpdateUser ?? throw new InvalidOperationException("ResourcesKey.ErrorUpdateUser is not initialized.");
|
||||||
|
public static string ErrorDeleteUser => _instance?.ErrorDeleteUser ?? throw new InvalidOperationException("ResourcesKey.ErrorDeleteUser is not initialized.");
|
||||||
|
public static string Cancel => _instance?.Cancel ?? throw new InvalidOperationException("ResourcesKey.Cancel is not initialized.");
|
||||||
|
public static string Confirm => _instance?.Confirm ?? throw new InvalidOperationException("ResourcesKey.Confirm is not initialized.");
|
||||||
|
public static string ConfirmDeleteDescription => _instance?.ConfirmDeleteDescription ?? throw new InvalidOperationException("ResourcesKey.ConfirmDeleteDescription is not initialized.");
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,9 @@ public class GlobalConstants
|
|||||||
public readonly static Guid MEMBER_ID = Guid.Parse("{BCE14DEA-1748-4A76-8485-ADEE83DF5EFD}");
|
public readonly static Guid MEMBER_ID = Guid.Parse("{BCE14DEA-1748-4A76-8485-ADEE83DF5EFD}");
|
||||||
|
|
||||||
public const string ADMINISTRATOR = "Administrateur";
|
public const string ADMINISTRATOR = "Administrateur";
|
||||||
|
public const string ADMINISTRATOR_NORMALIZED = "ADMINISTRATEUR";
|
||||||
public const string MEMBER = "Membre";
|
public const string MEMBER = "Membre";
|
||||||
|
public const string MEMBER_NORMALIZED = "MEMBRE";
|
||||||
public const string ADMIN_MEMBER = $"{ADMINISTRATOR}, {MEMBER}";
|
public const string ADMIN_MEMBER = $"{ADMINISTRATOR}, {MEMBER}";
|
||||||
|
|
||||||
public const int JWT_DURATION_HOUR = 12;
|
public const int JWT_DURATION_HOUR = 12;
|
||||||
|
|||||||
6
src/GameIdeas/GameIdeas.Shared/Dto/IdDto.cs
Normal file
6
src/GameIdeas/GameIdeas.Shared/Dto/IdDto.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace GameIdeas.Shared.Dto;
|
||||||
|
|
||||||
|
public class IdDto
|
||||||
|
{
|
||||||
|
public string? Id { get; set; }
|
||||||
|
}
|
||||||
7
src/GameIdeas/GameIdeas.Shared/Dto/RoleDto.cs
Normal file
7
src/GameIdeas/GameIdeas.Shared/Dto/RoleDto.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace GameIdeas.Shared.Dto;
|
||||||
|
|
||||||
|
public class RoleDto
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -4,8 +4,8 @@ namespace GameIdeas.Shared.Dto;
|
|||||||
|
|
||||||
public class UserDto
|
public class UserDto
|
||||||
{
|
{
|
||||||
public int? Id { get; set; }
|
public string? Id { get; set; }
|
||||||
public string? Username { get; set; }
|
public string? Username { get; set; }
|
||||||
public string? Password { get; set; }
|
public string? Password { get; set; }
|
||||||
public string? RoleId { get; set; }
|
public RoleDto? Role { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
8
src/GameIdeas/GameIdeas.Shared/Dto/UserFilterDto.cs
Normal file
8
src/GameIdeas/GameIdeas.Shared/Dto/UserFilterDto.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace GameIdeas.Shared.Dto;
|
||||||
|
|
||||||
|
public class UserFilterDto
|
||||||
|
{
|
||||||
|
public int CurrentPage { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public IEnumerable<string>? RoleIds { get; set; }
|
||||||
|
}
|
||||||
7
src/GameIdeas/GameIdeas.Shared/Dto/UserListDto.cs
Normal file
7
src/GameIdeas/GameIdeas.Shared/Dto/UserListDto.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace GameIdeas.Shared.Dto;
|
||||||
|
|
||||||
|
public class UserListDto
|
||||||
|
{
|
||||||
|
public IEnumerable<UserDto>? Users { get; set; }
|
||||||
|
public int UsersCount { get; set; }
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
using GameIdeas.Shared.Dto;
|
using GameIdeas.Shared.Constants;
|
||||||
|
using GameIdeas.Shared.Dto;
|
||||||
using GameIdeas.WebAPI.Exceptions;
|
using GameIdeas.WebAPI.Exceptions;
|
||||||
using GameIdeas.WebAPI.Services.Users;
|
using GameIdeas.WebAPI.Services.Users;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace GameIdeas.WebAPI.Controllers;
|
namespace GameIdeas.WebAPI.Controllers;
|
||||||
@@ -8,7 +10,8 @@ namespace GameIdeas.WebAPI.Controllers;
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class UserController(
|
public class UserController(
|
||||||
IUserService userService,
|
IUserReadService userReadService,
|
||||||
|
IUserWriteService userWriteService,
|
||||||
ILoggerFactory loggerFactory) : Controller
|
ILoggerFactory loggerFactory) : Controller
|
||||||
{
|
{
|
||||||
private readonly ILogger<UserController> logger = loggerFactory.CreateLogger<UserController>();
|
private readonly ILogger<UserController> logger = loggerFactory.CreateLogger<UserController>();
|
||||||
@@ -18,7 +21,7 @@ public class UserController(
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return Ok(await userService.Login(model));
|
return Ok(await userReadService.Login(model));
|
||||||
}
|
}
|
||||||
catch (UserInvalidException e)
|
catch (UserInvalidException e)
|
||||||
{
|
{
|
||||||
@@ -36,4 +39,82 @@ public class UserController(
|
|||||||
return StatusCode(500, e.Message);
|
return StatusCode(500, e.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize(Roles = GlobalConstants.ADMINISTRATOR)]
|
||||||
|
[HttpGet("Roles")]
|
||||||
|
public async Task<ActionResult<IEnumerable<RoleDto>>> GetRoles()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Ok(await userReadService.GetRoles());
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogError(e, "Internal error while get roles");
|
||||||
|
return StatusCode(500, e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize(Roles = GlobalConstants.ADMINISTRATOR)]
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ActionResult<UserListDto>> GetUsers([FromQuery] UserFilterDto filter)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Ok(await userReadService.GetUsers(filter));
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogError(e, "Internal error while get users");
|
||||||
|
return StatusCode(500, e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize(Roles = GlobalConstants.ADMINISTRATOR)]
|
||||||
|
[HttpPost("Create")]
|
||||||
|
public async Task<ActionResult<IdDto>> CreateUser([FromBody] UserDto user)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var id = new IdDto() { Id = await userWriteService.CreateUser(user) };
|
||||||
|
return Created("/Create", id);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogError(e, "Internal error while create user");
|
||||||
|
return StatusCode(500, e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize(Roles = GlobalConstants.ADMINISTRATOR)]
|
||||||
|
[HttpPut("Update/{userId}")]
|
||||||
|
public async Task<ActionResult<IdDto>> UpdateUser(string userId, [FromBody] UserDto user)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var id = new IdDto() { Id = await userWriteService.UpdateUser(userId, user) };
|
||||||
|
return Created("/Update", id);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogError(e, "Internal error while update user");
|
||||||
|
return StatusCode(500, e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize(Roles = GlobalConstants.ADMINISTRATOR)]
|
||||||
|
[HttpDelete("Delete/{userId}")]
|
||||||
|
public async Task<ActionResult<IdDto>> DeleteUser(string userId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var id = new IdDto() { Id = await userWriteService.DeleteUser(userId) };
|
||||||
|
return Created("/Delete", id);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogError(e, "Internal error while delete user");
|
||||||
|
return StatusCode(500, e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
"RequestFailedStatusFormat": "Erreur lors de la réponse, code {0}",
|
"RequestFailedStatusFormat": "Erreur lors de la réponse, code {0}",
|
||||||
"ErrorFetchCategories": "Erreur lors de la récupération des catégories",
|
"ErrorFetchCategories": "Erreur lors de la récupération des catégories",
|
||||||
"PlaceholderAdd": "Ajouter un nouveau",
|
"PlaceholderAdd": "Ajouter un nouveau",
|
||||||
"ErrorCreateGame": "Erreur lors de la Création d'un jeu",
|
"ErrorCreateGame": "Erreur lors de la création d'un jeu",
|
||||||
"InvalidTitle": "Le titre est incorrect",
|
"InvalidTitle": "Le titre est incorrect",
|
||||||
"InvalidInterest": "L'interêt est incorrect",
|
"InvalidInterest": "L'interêt est incorrect",
|
||||||
"Unknown": "Inconnu",
|
"Unknown": "Inconnu",
|
||||||
@@ -49,5 +49,15 @@
|
|||||||
"InvalidToken": "Le token JWT est invalide",
|
"InvalidToken": "Le token JWT est invalide",
|
||||||
"UserUnauthorized": "Utilisateur non authorisé",
|
"UserUnauthorized": "Utilisateur non authorisé",
|
||||||
"UserLoginFailed": "Authentification de l'utilisateur échoué",
|
"UserLoginFailed": "Authentification de l'utilisateur échoué",
|
||||||
"UserLogoutFailed": "Déconnection de l'utilisateur échoué"
|
"UserLogoutFailed": "Déconnection de l'utilisateur échoué",
|
||||||
|
"Roles": "Rôles",
|
||||||
|
"ErrorFetchUsers": "Erreur lors de la récupération des utilisateurs",
|
||||||
|
"ErrorFetchRoles": "Erreur lors de la récupération des rôles",
|
||||||
|
"MissingField": "Un champs est manquant",
|
||||||
|
"ErrorCreateUser": "Erreur lors de la création d'un utilisateur",
|
||||||
|
"ErrorUpdateUser": "Erreur lors de la mise à jour d'un utilisateur",
|
||||||
|
"ErrorDeleteUser": "Erreur lors de la suppression d'un utilisateur",
|
||||||
|
"Cancel": "Annuler",
|
||||||
|
"Confirm": "Confirmer",
|
||||||
|
"ConfirmDeleteDescription": "Êtes-vous sur de vouloir supprimer cet élément ?"
|
||||||
}
|
}
|
||||||
@@ -19,13 +19,13 @@ namespace GameIdeas.WebAPI.Migrations
|
|||||||
{
|
{
|
||||||
GlobalConstants.ADMINISTRATOR_ID.ToString(),
|
GlobalConstants.ADMINISTRATOR_ID.ToString(),
|
||||||
GlobalConstants.ADMINISTRATOR,
|
GlobalConstants.ADMINISTRATOR,
|
||||||
GlobalConstants.ADMINISTRATOR.Normalize(),
|
GlobalConstants.ADMINISTRATOR_NORMALIZED,
|
||||||
Guid.NewGuid().ToString()
|
Guid.NewGuid().ToString()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
GlobalConstants.MEMBER_ID.ToString(),
|
GlobalConstants.MEMBER_ID.ToString(),
|
||||||
GlobalConstants.MEMBER,
|
GlobalConstants.MEMBER,
|
||||||
GlobalConstants.MEMBER.Normalize(),
|
GlobalConstants.MEMBER_NORMALIZED,
|
||||||
Guid.NewGuid().ToString()
|
Guid.NewGuid().ToString()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using GameIdeas.Shared.Dto;
|
||||||
|
using GameIdeas.Shared.Model;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace GameIdeas.WebAPI.Profiles;
|
||||||
|
|
||||||
|
public class UserProfile : Profile
|
||||||
|
{
|
||||||
|
public UserProfile()
|
||||||
|
{
|
||||||
|
CreateMap<IdentityRole, RoleDto>()
|
||||||
|
.ForMember(d => d.Id, o => o.MapFrom(s => s.Id))
|
||||||
|
.ForMember(d => d.Name, o => o.MapFrom(s => s.Name));
|
||||||
|
|
||||||
|
CreateMap<User, UserDto>()
|
||||||
|
.ForMember(d => d.Id, o => o.MapFrom(s => s.Id))
|
||||||
|
.ForMember(d => d.Username, o => o.MapFrom(s => s.UserName));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,12 +59,24 @@ 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.AddAuthorization();
|
||||||
|
|
||||||
services.AddSingleton<TranslationService>();
|
services.AddSingleton<TranslationService>();
|
||||||
services.AddSingleton<Translations>();
|
services.AddSingleton<Translations>();
|
||||||
|
|
||||||
services.AddScoped<IUserService, UserService>();
|
services.AddScoped<IUserReadService, UserReadService>();
|
||||||
|
services.AddScoped<IUserWriteService, UserWriteService>();
|
||||||
services.AddScoped<IGameReadService, GameReadService>();
|
services.AddScoped<IGameReadService, GameReadService>();
|
||||||
services.AddScoped<IGameWriteService, GameWriteService>();
|
services.AddScoped<IGameWriteService, GameWriteService>();
|
||||||
services.AddScoped<ICategoryService, CategoryService>();
|
services.AddScoped<ICategoryService, CategoryService>();
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ public class GameReadService(GameIdeasContext context, IMapper mapper, ICategory
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyFilter(ref IQueryable<Game> query, GameFilterDto filter)
|
private static void ApplyFilter(ref IQueryable<Game> query, GameFilterDto filter)
|
||||||
{
|
{
|
||||||
if (filter.PlatformIds != null)
|
if (filter.PlatformIds != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using GameIdeas.Shared.Dto;
|
||||||
|
|
||||||
|
namespace GameIdeas.WebAPI.Services.Users;
|
||||||
|
|
||||||
|
public interface IUserReadService
|
||||||
|
{
|
||||||
|
Task<TokenDto> Login(UserDto user);
|
||||||
|
Task<IEnumerable<RoleDto>> GetRoles();
|
||||||
|
Task<UserListDto> GetUsers(UserFilterDto filter);
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
using GameIdeas.Shared.Dto;
|
|
||||||
|
|
||||||
namespace GameIdeas.WebAPI.Services.Users;
|
|
||||||
|
|
||||||
public interface IUserService
|
|
||||||
{
|
|
||||||
Task<TokenDto> Login(UserDto user);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using GameIdeas.Shared.Dto;
|
||||||
|
|
||||||
|
namespace GameIdeas.WebAPI.Services.Users;
|
||||||
|
|
||||||
|
public interface IUserWriteService
|
||||||
|
{
|
||||||
|
Task<string> CreateUser(UserDto user);
|
||||||
|
Task<string> UpdateUser(string userId, UserDto user);
|
||||||
|
Task<string> DeleteUser(string userId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using GameIdeas.Resources;
|
||||||
|
using GameIdeas.Shared.Constants;
|
||||||
|
using GameIdeas.Shared.Dto;
|
||||||
|
using GameIdeas.Shared.Model;
|
||||||
|
using GameIdeas.WebAPI.Context;
|
||||||
|
using GameIdeas.WebAPI.Exceptions;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace GameIdeas.WebAPI.Services.Users;
|
||||||
|
|
||||||
|
public class UserReadService(
|
||||||
|
UserManager<User> userManager,
|
||||||
|
GameIdeasContext context,
|
||||||
|
IMapper mapper) : IUserReadService
|
||||||
|
{
|
||||||
|
public async Task<IEnumerable<RoleDto>> GetRoles()
|
||||||
|
{
|
||||||
|
var roles = await context.Roles
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return mapper.Map<IEnumerable<RoleDto>>(roles);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserListDto> GetUsers(UserFilterDto filter)
|
||||||
|
{
|
||||||
|
var users = await context.Users
|
||||||
|
.OrderBy(u => u.UserName)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var count = users.Count;
|
||||||
|
ApplyStaticFilter(ref users, filter);
|
||||||
|
|
||||||
|
var usersDto = mapper.Map<IEnumerable<UserDto>>(users);
|
||||||
|
usersDto = await ApplyRoles(usersDto, filter);
|
||||||
|
|
||||||
|
return new UserListDto
|
||||||
|
{
|
||||||
|
Users = usersDto,
|
||||||
|
UsersCount = count
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IEnumerable<UserDto>> ApplyRoles(IEnumerable<UserDto> users, UserFilterDto filter)
|
||||||
|
{
|
||||||
|
var userRolesQuery = context.UserRoles
|
||||||
|
.Where(ur => users.Select(u => u.Id).Contains(ur.UserId))
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
var rolesQuery = context.Roles.AsQueryable();
|
||||||
|
|
||||||
|
if (filter.RoleIds != null)
|
||||||
|
{
|
||||||
|
userRolesQuery = userRolesQuery
|
||||||
|
.Where(ur => filter.RoleIds.Contains(ur.RoleId));
|
||||||
|
|
||||||
|
rolesQuery = rolesQuery.Where(role => filter.RoleIds.Contains(role.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
var roles = await rolesQuery.ToListAsync();
|
||||||
|
var userRoles = await userRolesQuery.ToListAsync();
|
||||||
|
users = users.Where(user => userRoles.Select(ur => ur.UserId).Contains(user.Id));
|
||||||
|
|
||||||
|
foreach (var user in users)
|
||||||
|
{
|
||||||
|
var currentRoleId = userRoles.FirstOrDefault(ur => ur.UserId == user.Id)?.RoleId;
|
||||||
|
|
||||||
|
if (currentRoleId == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentRole = roles.FirstOrDefault(r => r.Id == currentRoleId);
|
||||||
|
user.Role = mapper.Map<RoleDto>(currentRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyStaticFilter(ref List<User> users, UserFilterDto filter)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter.Name))
|
||||||
|
{
|
||||||
|
var keywords = filter.Name?
|
||||||
|
.Split([' '], StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(k => k.Trim())
|
||||||
|
.ToArray() ?? [];
|
||||||
|
|
||||||
|
users = users
|
||||||
|
.Where(user => keywords.All(
|
||||||
|
kw => user.UserName?.Contains(kw, StringComparison.OrdinalIgnoreCase) ?? true
|
||||||
|
))
|
||||||
|
.OrderBy(user => keywords.Min(kw =>
|
||||||
|
user.UserName?.IndexOf(kw, StringComparison.OrdinalIgnoreCase)
|
||||||
|
))
|
||||||
|
.ThenBy(user => user.UserName?.Length)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TokenDto> Login(UserDto userDto)
|
||||||
|
{
|
||||||
|
if (userDto.Username == null || userDto.Password == null)
|
||||||
|
throw new UserInvalidException(ResourcesKey.UserArgumentsNull);
|
||||||
|
|
||||||
|
var user = await userManager.FindByNameAsync(userDto.Username);
|
||||||
|
|
||||||
|
if (user == null || !await userManager.CheckPasswordAsync(user, userDto.Password))
|
||||||
|
{
|
||||||
|
throw new UserUnauthorizedException(ResourcesKey.UserUnauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Claim> authClaims =
|
||||||
|
[
|
||||||
|
new Claim(ClaimTypes.Name, user.UserName ?? string.Empty),
|
||||||
|
new Claim(ClaimTypes.Sid, user.Id),
|
||||||
|
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||||
|
];
|
||||||
|
|
||||||
|
authClaims.AddRange((await userManager.GetRolesAsync(user))
|
||||||
|
.Select(r => new Claim(ClaimTypes.Role, r)));
|
||||||
|
|
||||||
|
var jwtKey = Environment.GetEnvironmentVariable("JWT_KEY")
|
||||||
|
?? throw new ArgumentNullException(message: ResourcesKey.InvalidToken, null);
|
||||||
|
|
||||||
|
var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey));
|
||||||
|
|
||||||
|
var token = new JwtSecurityToken(
|
||||||
|
issuer: Environment.GetEnvironmentVariable("JWT_ISSUER"),
|
||||||
|
audience: Environment.GetEnvironmentVariable("JWT_AUDIENCE"),
|
||||||
|
expires: DateTime.Now.AddHours(GlobalConstants.JWT_DURATION_HOUR),
|
||||||
|
claims: authClaims,
|
||||||
|
signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
|
||||||
|
);
|
||||||
|
|
||||||
|
return new TokenDto
|
||||||
|
{
|
||||||
|
Token = new JwtSecurityTokenHandler().WriteToken(token),
|
||||||
|
Expiration = token.ValidTo
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
using GameIdeas.Resources;
|
|
||||||
using GameIdeas.Shared.Constants;
|
|
||||||
using GameIdeas.Shared.Dto;
|
|
||||||
using GameIdeas.Shared.Model;
|
|
||||||
using GameIdeas.WebAPI.Exceptions;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
|
||||||
using System.IdentityModel.Tokens.Jwt;
|
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace GameIdeas.WebAPI.Services.Users;
|
|
||||||
|
|
||||||
public class UserService(UserManager<User> userManager) : IUserService
|
|
||||||
{
|
|
||||||
public async Task<TokenDto> Login(UserDto userDto)
|
|
||||||
{
|
|
||||||
if (userDto.Username == null || userDto.Password == null)
|
|
||||||
throw new UserInvalidException(ResourcesKey.UserArgumentsNull);
|
|
||||||
|
|
||||||
var user = await userManager.FindByNameAsync(userDto.Username);
|
|
||||||
|
|
||||||
if (user == null || !await userManager.CheckPasswordAsync(user, userDto.Password))
|
|
||||||
{
|
|
||||||
throw new UserUnauthorizedException(ResourcesKey.UserUnauthorized);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Claim> authClaims =
|
|
||||||
[
|
|
||||||
new Claim(ClaimTypes.Name, user.UserName ?? string.Empty),
|
|
||||||
new Claim(ClaimTypes.Sid, user.Id),
|
|
||||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
|
||||||
];
|
|
||||||
|
|
||||||
authClaims.AddRange((await userManager.GetRolesAsync(user))
|
|
||||||
.Select(r => new Claim(ClaimTypes.Role, r)));
|
|
||||||
|
|
||||||
var jwtKey = Environment.GetEnvironmentVariable("JWT_KEY")
|
|
||||||
?? throw new ArgumentNullException(message: ResourcesKey.InvalidToken, null);
|
|
||||||
|
|
||||||
var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey));
|
|
||||||
|
|
||||||
var token = new JwtSecurityToken(
|
|
||||||
issuer: Environment.GetEnvironmentVariable("JWT_ISSUER"),
|
|
||||||
audience: Environment.GetEnvironmentVariable("JWT_AUDIENCE"),
|
|
||||||
expires: DateTime.Now.AddHours(GlobalConstants.JWT_DURATION_HOUR),
|
|
||||||
claims: authClaims,
|
|
||||||
signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
|
|
||||||
);
|
|
||||||
|
|
||||||
return new TokenDto
|
|
||||||
{
|
|
||||||
Token = new JwtSecurityTokenHandler().WriteToken(token),
|
|
||||||
Expiration = token.ValidTo
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using GameIdeas.Resources;
|
||||||
|
using GameIdeas.Shared.Dto;
|
||||||
|
using GameIdeas.Shared.Model;
|
||||||
|
using GameIdeas.WebAPI.Exceptions;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace GameIdeas.WebAPI.Services.Users;
|
||||||
|
|
||||||
|
public class UserWriteService(
|
||||||
|
UserManager<User> userManager) : IUserWriteService
|
||||||
|
{
|
||||||
|
public async Task<string> CreateUser(UserDto user)
|
||||||
|
{
|
||||||
|
if (user.Username == null ||
|
||||||
|
user.Password == null ||
|
||||||
|
user.Role == null)
|
||||||
|
{
|
||||||
|
throw new UserInvalidException(ResourcesKey.MissingField);
|
||||||
|
}
|
||||||
|
|
||||||
|
User userToCreate = new() { UserName = user.Username };
|
||||||
|
|
||||||
|
var result = await userManager.CreateAsync(userToCreate, user.Password);
|
||||||
|
|
||||||
|
if (result.Succeeded)
|
||||||
|
{
|
||||||
|
await userManager.AddToRoleAsync(userToCreate, user.Role.Name);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new UserInvalidException(string.Join("; ", result.Errors.Select(e => $"{e.Code} {e.Description}")));
|
||||||
|
}
|
||||||
|
|
||||||
|
return userToCreate.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> DeleteUser(string userId)
|
||||||
|
{
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(ResourcesKey.MissingField);
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = await userManager.FindByIdAsync(userId)
|
||||||
|
?? throw new UserInvalidException("User not found");
|
||||||
|
|
||||||
|
await userManager.DeleteAsync(user);
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> UpdateUser(string userId, UserDto user)
|
||||||
|
{
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(ResourcesKey.MissingField);
|
||||||
|
}
|
||||||
|
|
||||||
|
var userToUpdate = await userManager.FindByIdAsync(userId)
|
||||||
|
?? throw new UserInvalidException("User not found");
|
||||||
|
|
||||||
|
if (user.Username != null)
|
||||||
|
{
|
||||||
|
userToUpdate.UserName = user.Username;
|
||||||
|
await userManager.UpdateAsync(userToUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.Password != null)
|
||||||
|
{
|
||||||
|
await userManager.RemovePasswordAsync(userToUpdate);
|
||||||
|
await userManager.AddPasswordAsync(userToUpdate, user.Password);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.Role != null)
|
||||||
|
{
|
||||||
|
var roles = await userManager.GetRolesAsync(userToUpdate);
|
||||||
|
await userManager.RemoveFromRolesAsync(userToUpdate, roles);
|
||||||
|
await userManager.AddToRoleAsync(userToUpdate, user.Role.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return userToUpdate.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user