diff --git a/.gitignore b/.gitignore index 8d9d548..de41017 100644 --- a/.gitignore +++ b/.gitignore @@ -60,7 +60,7 @@ dlldata.c *.pgc *.pgd *.rsp -*.sbr +*.sbr *.tlb *.tli *.tlh diff --git a/AdminPanel.csproj b/AdminPanel.csproj index 86d5318..d5bc199 100644 --- a/AdminPanel.csproj +++ b/AdminPanel.csproj @@ -7,7 +7,7 @@ service-worker-assets.js - + @@ -19,7 +19,8 @@ - + + diff --git a/App.razor b/App.razor index 623580d..9ad29dc 100644 --- a/App.razor +++ b/App.razor @@ -1,4 +1,8 @@ - + + + + + diff --git a/Components/UserComponent.razor b/Components/UserComponent.razor index 7f59922..b33f1f9 100644 --- a/Components/UserComponent.razor +++ b/Components/UserComponent.razor @@ -2,11 +2,11 @@ @if (@User.IsAdmin) { -

Administrator @User.UserName

+

Administrator @User.Name

} else { -

@User.UserName

+

@User.Name

}

email: @User.Email

id: @User.Id

diff --git a/Models/Account.cs b/Models/Account.cs index 0226997..4c02b81 100644 --- a/Models/Account.cs +++ b/Models/Account.cs @@ -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}"; + } +} \ No newline at end of file diff --git a/Pages/UserListPanel.razor b/Pages/UserListPanel.razor index d72c80e..51834a3 100644 --- a/Pages/UserListPanel.razor +++ b/Pages/UserListPanel.razor @@ -1,32 +1,95 @@ @page "/users" - - -@using AdminPanel.Components; -@using AdminPanel.Models; +@using AdminPanel.Models +@using System.ComponentModel.DataAnnotations User Panel

User Panel

-
- @if (Users == null) - { -

Fetching Data...

- } - else - foreach (User user in Users) - { -
- - -
- see more -
- - -

Ratio

- -
+
+ + + + + + + + + Cancel + Add Account + + + + + + + +
+ + Add Account
- } +
+ + Remove Selection +
+ + Accounts + + +
+ + + + + + + + + + + Is Administrator : + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/Pages/UserListPanel.razor.cs b/Pages/UserListPanel.razor.cs index 243fb9f..f4eff47 100644 --- a/Pages/UserListPanel.razor.cs +++ b/Pages/UserListPanel.razor.cs @@ -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? Users { get; private set; } + [Inject] private ISnackbar Snackbar { get; init; } + [Inject] private IUsersService Service { get; init; } + [Inject] private ILogger Logger { get; init; } - public UserListPanel() + private MudDataGrid Grid { get; set; } + + + private string? SearchString { get; set; } + + + private HashSet 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> LoadUsersFromServer(GridState 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 { - 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 ConfirmAddAccount(MouseEventArgs e) { + // We no longer add an account if it is confirmed + IsAddingUser = false; + + try + { + var user = await Service.AddUser(FormAccountName!, FormAccountEmail!, FormAccountPassword!, + FormAccountIsAdmin); + Logger.LogDebug($"Added user : {user}"); + await Grid.ReloadServerData(); + } + catch (ServiceException err) + { + ShowErrors(err); + } + } + + 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 RetrieveUsers(HttpClient client) + private async void RemoveSelection(MouseEventArgs e) { - using HttpResponseMessage response = await client.GetAsync("/api/admin/list-users?start=0&n=100"); + 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); + } + } - response.EnsureSuccessStatusCode(); + private Func VerifyLength(uint min, uint max) + { + return s => s.Length >= min && s.Length <= max + ? null + : $"length is incorrect (must be between {min} and {max})"; + } - Users = (await response.Content.ReadFromJsonAsync>())!; - StateHasChanged(); - Console.WriteLine(Users); + private void ValidateSearch(KeyboardEventArgs e) + { + Grid.ReloadServerData(); + Logger.LogDebug($"Searching for {SearchString}"); } } } \ No newline at end of file diff --git a/Pages/UserListPanel.razor.css b/Pages/UserListPanel.razor.css new file mode 100644 index 0000000..d29687f --- /dev/null +++ b/Pages/UserListPanel.razor.css @@ -0,0 +1,8 @@ + + +#add-account-form { + background-color: white; + visibility: hidden; + color: #ffba00; +} + diff --git a/Program.cs b/Program.cs index d4e1597..91dd8e2 100644 --- a/Program.cs +++ b/Program.cs @@ -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"); builder.RootComponents.Add("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) }); -await builder.Build().RunAsync(); +// builder.Logging.SetMinimumLevel(LogLevel.Debug); + +var client = new HttpClient +{ + BaseAddress = new Uri("http://localhost:8080") +}; + +builder.Services.AddScoped(sp => new HttpUsersService(client)); +//builder.Services.AddScoped(); + +builder.Services.AddMudServices(); + + +await builder.Build().RunAsync(); \ No newline at end of file diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json index db1c6be..d5df975 100644 --- a/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -2,8 +2,8 @@ "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:18284", + "iisExpress": { + "applicationUrl": "http://localhost:18284", "sslPort": 0 } }, @@ -12,8 +12,8 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", - "applicationUrl": "http://localhost:5081", + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:5081", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/Services/HttpUsersService.cs b/Services/HttpUsersService.cs new file mode 100644 index 0000000..824fefd --- /dev/null +++ b/Services/HttpUsersService.cs @@ -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(); + 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 Users, uint TotalCount); + + public async Task<(uint, List)> 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() + ?? 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 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() + ?? 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 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); + } +} \ No newline at end of file diff --git a/Services/IUsersService.cs b/Services/IUsersService.cs new file mode 100644 index 0000000..c523caf --- /dev/null +++ b/Services/IUsersService.cs @@ -0,0 +1,15 @@ +using AdminPanel.Models; + +namespace AdminPanel.Services +{ + public interface IUsersService + { + public Task<(uint, List)> ListUsers(uint from, uint len, string? searchString = null); + + public Task AddUser(string username, string email, string password, bool isAdmin); + + public Task RemoveUsers(IEnumerable userId); + + public Task UpdateUser(User user); + } +} diff --git a/Services/ServiceException.cs b/Services/ServiceException.cs new file mode 100644 index 0000000..8dbcb00 --- /dev/null +++ b/Services/ServiceException.cs @@ -0,0 +1,18 @@ +namespace AdminPanel.Services; + +class ServiceException : Exception +{ + public Dictionary> ArgumentMessages { get; init; } + + + + public ServiceException(string? message, Dictionary> arguments) : base(message) + { + ArgumentMessages = arguments; + } + + public ServiceException(string? message, Dictionary> arguments, Exception? innerException) : base(message, innerException) + { + ArgumentMessages = arguments; + } +} \ No newline at end of file diff --git a/Services/UsersServiceStub.cs b/Services/UsersServiceStub.cs new file mode 100644 index 0000000..d30845e --- /dev/null +++ b/Services/UsersServiceStub.cs @@ -0,0 +1,65 @@ +using AdminPanel.Models; + +namespace AdminPanel.Services +{ + public class UsersServiceStub : IUsersService + { + private Dictionary 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 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)> 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 userIds) + { + foreach (var id in userIds) + { + Users.Remove(id); + } + return Task.CompletedTask; + } + + public Task UpdateUser(User user) + { + Users[user.Id] = user; + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Shared/MainLayout.razor b/Shared/MainLayout.razor index 0b3cad5..dc88df5 100644 --- a/Shared/MainLayout.razor +++ b/Shared/MainLayout.razor @@ -1,4 +1,8 @@ @inherits LayoutComponentBase +@using MudBlazor + + +