Add Users And account administration panel #1

Merged
maxime.batista merged 5 commits from users into master 1 year ago

@ -20,6 +20,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.13" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.13" PrivateAssets="all" />
<PackageReference Include="MudBlazor" Version="6.12.0" />
</ItemGroup>
<ItemGroup>

@ -1,4 +1,8 @@
<Router AppAssembly="@typeof(App).Assembly">
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />

@ -2,11 +2,11 @@
@if (@User.IsAdmin)
{
<h3>Administrator @User.UserName</h3>
<h3>Administrator @User.Name</h3>
}
else
{
<h3>@User.UserName</h3>
<h3>@User.Name</h3>
}
<p>email: @User.Email</p>
<p>id: @User.Id</p>

@ -1,5 +1,14 @@
namespace AdminPanel.Models;
public record User(string UserName, string Email, int Id, bool IsAdmin) {
public class User
{
public required string Name { get; set; }
public required string Email { get; set; }
public uint Id { get; set; }
public bool IsAdmin { get; set; }
public override string ToString()
{
return $"{nameof(Name)}: {Name}, {nameof(Email)}: {Email}, {nameof(Id)}: {Id}, {nameof(IsAdmin)}: {IsAdmin}";
}
}

@ -1,32 +1,95 @@
@page "/users"
@using AdminPanel.Components;
@using AdminPanel.Models;
@using AdminPanel.Models
@using System.ComponentModel.DataAnnotations
<PageTitle>User Panel</PageTitle>
<h1>User Panel</h1>
<div class="users-div">
@if (Users == null)
{
<p>Fetching Data...</p>
}
else
foreach (User user in Users)
{
<div class="user-cell">
<UserComponent User=@user/>
<Leaflet>
<Header>
see more
</Header>
<Body>
<p>Ratio</p>
</Body>
</Leaflet>
<div id="users-div">
<MudOverlay Visible="IsAddingUser" DarkBackground>
<MudForm id="add-account-form">
<MudTextField T="string"
Label="Name"
@bind-Value="FormAccountName"
Required
Validation="@(VerifyLength(6, 256))"/>
<MudTextField T="string"
Label="Email"
@bind-Value="FormAccountEmail"
Required
Validation="@(new EmailAddressAttribute() { ErrorMessage = "The email address is invalid" })"/>
<MudTextField T="string"
InputType="InputType.Password"
Label="Password"
@bind-Value="FormAccountPassword"
Validation="@(VerifyLength(6, 256))"
Required/>
<MudCheckBox T="bool" Label="Is Administrator" @bind-Value="FormAccountIsAdmin"/>
<MudButton Variant="Variant.Filled" Color="Color.Secondary" OnClick="@(() => IsAddingUser = false)">Cancel</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ConfirmAddAccount">Add Account</MudButton>
</MudForm>
</MudOverlay>
<MudDataGrid
T="User"
@ref="Grid"
ServerData="LoadUsersFromServer"
MultiSelection="true"
ColumnResizeMode="ResizeMode.Column"
RowsPerPage="10"
ReadOnly="false"
EditMode="DataGridEditMode.Form"
@bind-SelectedItems="SelectedItems"
CommittedItemChanges="OnAccountUpdated">
<ToolBarContent>
<div style="display: flex; align-items: center">
<MudIconButton Icon="@Icons.Material.Filled.Add" OnClick="@(() => IsAddingUser = true)" Color="@Color.Success" Class="add-item-btn"/>
<MudText Align="Align.Center">Add Account</MudText>
</div>
}
<div style="display: flex; align-items: center">
<MudIconButton Icon="@Icons.Material.Filled.Remove" OnClick="RemoveSelection" Color="@Color.Error" Class="remove-item-btn"/>
<MudText Align="Align.Center">Remove Selection</MudText>
</div>
<MudSpacer/>
<MudText>Accounts</MudText>
<MudSpacer/>
<MudTextField
T="string"
@bind-Value="SearchString"
OnKeyDown="@ValidateSearch"
Placeholder="Search an email or a name"
AdornmentIcon="@Icons.Material.Filled.Search"
Immediate
IconSize="Size.Medium"/>
</ToolBarContent>
<Columns>
<SelectColumn T="User"/>
<PropertyColumn Property="x => x.Name" Required/>
<PropertyColumn Property="x => x.Email" Title="Email Address" Required/>
<TemplateColumn Title="Is Administrator">
<CellTemplate>
<MudCheckBox Value="@context.Item!.IsAdmin" ReadOnly/>
</CellTemplate>
<EditTemplate>
<MudText>Is Administrator :</MudText>
<MudCheckBox @bind-Value="context.Item.IsAdmin"/>
</EditTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.Id" IsEditable="false"/>
<TemplateColumn>
<CellTemplate>
<MudIconButton Size="Size.Small" Icon="@Icons.Material.Outlined.Edit" OnClick="@context.Actions.StartEditingItemAsync"/>
</CellTemplate>
</TemplateColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="User" PageSizeOptions="new[] { 1, 2, 4, 10, 25, 50, 100 }"/>
</PagerContent>
</MudDataGrid>
</div>

@ -1,34 +1,121 @@
using System.Net.Http.Json;
using System.Text;
using AdminPanel.Models;
using AdminPanel.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.Extensions.Primitives;
using MudBlazor;
namespace AdminPanel.Pages
{
public partial class UserListPanel
{
public List<User>? Users { get; private set; }
[Inject] private ISnackbar Snackbar { get; init; }
[Inject] private IUsersService Service { get; init; }
[Inject] private ILogger<UserListPanel> Logger { get; init; }
public UserListPanel()
private MudDataGrid<User> Grid { get; set; }
private string? SearchString { get; set; }
private HashSet<User> SelectedItems { get; set; } = new();
private string? FormAccountName { get; set; }
private string? FormAccountEmail { get; set; }
private string? FormAccountPassword { get; set; }
private bool FormAccountIsAdmin { get; set; }
public bool IsAddingUser { get; set; }
private async Task<GridData<User>> LoadUsersFromServer(GridState<User> state)
{
HttpClient client = new()
Logger.LogDebug($"Loading users from server, state = {state} searchString = {SearchString}");
var (count, users) =
await Service.ListUsers((uint)(state.Page * state.PageSize), (uint)state.PageSize, SearchString);
return new GridData<User>
{
BaseAddress = new Uri("http://localhost:8080")
TotalItems = (int)count,
Items = users
};
RetrieveUsers(client);
}
public void OnAccessToUserSpaceClicked(User user)
private async void OnAccountUpdated(User user)
{
Logger.LogDebug($"Account updated : {user}");
try
{
await Service.UpdateUser(user);
}
catch (ServiceException err)
{
ShowErrors(err);
}
}
private async void RetrieveUsers(HttpClient client)
private async void ConfirmAddAccount(MouseEventArgs e)
{
using HttpResponseMessage response = await client.GetAsync("/api/admin/list-users?start=0&n=100");
// We no longer add an account if it is confirmed
IsAddingUser = false;
response.EnsureSuccessStatusCode();
try
{
var user = await Service.AddUser(FormAccountName!, FormAccountEmail!, FormAccountPassword!,
FormAccountIsAdmin);
Logger.LogDebug($"Added user : {user}");
await Grid.ReloadServerData();
}
catch (ServiceException err)
{
ShowErrors(err);
}
}
Users = (await response.Content.ReadFromJsonAsync<List<User>>())!;
StateHasChanged();
Console.WriteLine(Users);
private void ShowErrors(ServiceException e)
{
foreach (var erronedArgument in e.ArgumentMessages)
{
var sb = new StringBuilder(erronedArgument.Key);
sb.Append(" :");
foreach (var message in erronedArgument.Value)
{
sb.Append("\n\t-");
sb.Append(message);
}
Snackbar.Add(sb.ToString());
}
}
private async void RemoveSelection(MouseEventArgs e)
{
var users = SelectedItems.ToList().ConvertAll(u => u.Id);
Logger.LogDebug($"Removing users : {users}");
try
{
await Service.RemoveUsers(users);
await Grid.ReloadServerData();
}
catch (ServiceException err)
{
ShowErrors(err);
}
}
private Func<string, string?> VerifyLength(uint min, uint max)
{
return s => s.Length >= min && s.Length <= max
? null
: $"length is incorrect (must be between {min} and {max})";
}
private void ValidateSearch(KeyboardEventArgs e)
{
Grid.ReloadServerData();
Logger.LogDebug($"Searching for {SearchString}");
}
}
}

@ -0,0 +1,8 @@
#add-account-form {
background-color: white;
visibility: hidden;
color: #ffba00;
}

@ -1,11 +1,28 @@
using AdminPanel;
using AdminPanel.Pages;
using AdminPanel.Services;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using MudBlazor.Services;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services
.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
// builder.Logging.SetMinimumLevel(LogLevel.Debug);
var client = new HttpClient
{
BaseAddress = new Uri("http://localhost:8080")
};
builder.Services.AddScoped<IUsersService>(sp => new HttpUsersService(client));
//builder.Services.AddScoped<IUsersService, UsersServiceStub>();
builder.Services.AddMudServices();
await builder.Build().RunAsync();

@ -0,0 +1,93 @@
using System.Diagnostics;
using System.Net;
using System.Net.Http.Json;
using AdminPanel.Models;
namespace AdminPanel.Services;
public class HttpUsersService : IUsersService
{
private HttpClient Client { get; }
public HttpUsersService(HttpClient client)
{
this.Client = client;
}
private record ServerErrorMessageDto(string? Field, string? Error, string Message);
private async Task EnsureResponseIsOk(HttpResponseMessage response)
{
if (response.StatusCode == HttpStatusCode.OK)
{
return;
}
var content = await response.Content.ReadFromJsonAsync<ServerErrorMessageDto[]>();
var messages = content!
.GroupBy(e => e.Field ?? e.Error!)
.ToDictionary(
g => g.Key,
g => g.ToList().ConvertAll(e => e.Message)
);
throw new ServiceException("Server refused request", messages);
}
private record ListUsersResponseDto(List<User> Users, uint TotalCount);
public async Task<(uint, List<User>)> ListUsers(uint from, uint len, string? searchString = null)
{
var httpResponse =
await Client.GetAsync($"/api/admin/list-users?start={from}&n={len}&search={searchString ?? ""}");
await EnsureResponseIsOk(httpResponse);
var response = await httpResponse.Content.ReadFromJsonAsync<ListUsersResponseDto>()
?? throw new Exception("Received non-parseable json from server");
return (response.TotalCount, response.Users);
}
private record AddUserRequestDto(string Username, string Email, string Password, bool IsAdmin);
private record AddUserResponseDto(uint Id);
public async Task<User> AddUser(string username, string email, string password, bool isAdmin)
{
var httpResponse = await Client.PostAsJsonAsync("/api/admin/user/add",
new AddUserRequestDto(username, email, password, isAdmin));
await EnsureResponseIsOk(httpResponse);
var response = await httpResponse.Content.ReadFromJsonAsync<AddUserResponseDto>()
?? throw new Exception("Received non-parseable json from server");
return new User
{
Name = username,
Email = email,
IsAdmin = isAdmin,
Id = response.Id
};
}
private record RemoveUsersRequestDto(uint[] Identifiers);
public async Task RemoveUsers(IEnumerable<uint> userIds)
{
var httpResponse =
await Client.PostAsJsonAsync("/api/admin/user/remove-all", new RemoveUsersRequestDto(userIds.ToArray()));
await EnsureResponseIsOk(httpResponse);
}
private record UpdateUserRequestDto(string Email, string Username, bool IsAdmin);
public async Task UpdateUser(User user)
{
var httpResponse = await Client.PostAsJsonAsync($"/api/admin/user/{user.Id}/update",
new UpdateUserRequestDto(user.Email, user.Name, user.IsAdmin));
await EnsureResponseIsOk(httpResponse);
}
}

@ -0,0 +1,15 @@
using AdminPanel.Models;
namespace AdminPanel.Services
{
public interface IUsersService
{
public Task<(uint, List<User>)> ListUsers(uint from, uint len, string? searchString = null);
public Task<User> AddUser(string username, string email, string password, bool isAdmin);
public Task RemoveUsers(IEnumerable<uint> userId);
public Task UpdateUser(User user);
}
}

@ -0,0 +1,18 @@
namespace AdminPanel.Services;
class ServiceException : Exception
{
public Dictionary<string, List<string>> ArgumentMessages { get; init; }
public ServiceException(string? message, Dictionary<string, List<string>> arguments) : base(message)
{
ArgumentMessages = arguments;
}
public ServiceException(string? message, Dictionary<string, List<string>> arguments, Exception? innerException) : base(message, innerException)
{
ArgumentMessages = arguments;
}
}

@ -0,0 +1,65 @@
using AdminPanel.Models;
namespace AdminPanel.Services
{
public class UsersServiceStub : IUsersService
{
private Dictionary<uint, User> Users { get; } = new[]
{
new User
{
Name = "Mathis",
Email = "mathis@gmail.com",
Id = 0,
IsAdmin = true
},
new User
{
Name = "Maeva",
Email = "maeva@gmail.com",
Id = 1,
IsAdmin = false
},
}.ToDictionary(u => u.Id);
public Task<User> AddUser(string username, string email, string password, bool isAdmin)
{
User user = new User
{
Email = email,
Name = username,
IsAdmin = isAdmin,
Id = (uint) Users.Count
};
Users[user.Id] = user;
return Task.FromResult(user);
}
public async Task<(uint, List<User>)> ListUsers(uint from, uint len, string? searchString = null)
{
//simulate a 1sec long request
await Task.Delay(1000);
var slice = Users.Values
.ToList()
.FindAll(a => searchString == null || a.Name.Contains(searchString) || a.Email.Contains(searchString))
.GetRange((int)from, (int)(from + len > Users.Count ? Users.Count - from : len));
return ((uint)Users.Count, slice);
}
public Task RemoveUsers(IEnumerable<uint> userIds)
{
foreach (var id in userIds)
{
Users.Remove(id);
}
return Task.CompletedTask;
}
public Task UpdateUser(User user)
{
Users[user.Id] = user;
return Task.CompletedTask;
}
}
}

@ -1,4 +1,8 @@
@inherits LayoutComponentBase
@using MudBlazor
<MudThemeProvider/>
<MudDialogProvider/>
<MudSnackbarProvider/>
<div class="page">
<div class="sidebar">

@ -8,3 +8,4 @@
@using Microsoft.JSInterop
@using AdminPanel
@using AdminPanel.Shared
@using MudBlazor

@ -13,9 +13,14 @@
<link href="manifest.json" rel="manifest" />
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
<link rel="apple-touch-icon" sizes="192x192" href="icon-192.png" />
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
</head>
<body>
<div id="app">
<svg class="loading-progress">
<circle r="40%" cx="50%" cy="50%" />
@ -29,6 +34,7 @@
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
<script src="_framework/blazor.webassembly.js"></script>
<script>navigator.serviceWorker.register('service-worker.js');</script>
</body>

Loading…
Cancel
Save