Ajoute une zone d'administration des utilisateurs
This commit is contained in:
@@ -27,6 +27,10 @@
|
||||
<a class="@BuildNavLinkClass(ApplicationPaths)" href="application.html" aria-current="@BuildAriaCurrent(ApplicationPaths)">Application</a>
|
||||
<a class="@BuildNavLinkClass(RulesPaths)" href="reglement.html" aria-current="@BuildAriaCurrent(RulesPaths)">Reglement</a>
|
||||
<a class="@BuildNavLinkClass(UserPaths)" href="utilisateur.html" aria-current="@BuildAriaCurrent(UserPaths)">Utilisateur</a>
|
||||
@if (IsAdmin)
|
||||
{
|
||||
<a class="@BuildNavLinkClass(AdminPaths)" href="administration.html" aria-current="@BuildAriaCurrent(AdminPaths)">Administration</a>
|
||||
}
|
||||
</nav>
|
||||
|
||||
<div class="site-menu-account">
|
||||
@@ -70,6 +74,10 @@
|
||||
<a class="@BuildNavLinkClass(ApplicationPaths)" href="application.html" aria-current="@BuildAriaCurrent(ApplicationPaths)">Application</a>
|
||||
<a class="@BuildNavLinkClass(RulesPaths)" href="reglement.html" aria-current="@BuildAriaCurrent(RulesPaths)">Reglement</a>
|
||||
<a class="@BuildNavLinkClass(UserPaths)" href="utilisateur.html" aria-current="@BuildAriaCurrent(UserPaths)">Utilisateur</a>
|
||||
@if (IsAdmin)
|
||||
{
|
||||
<a class="@BuildNavLinkClass(AdminPaths)" href="administration.html" aria-current="@BuildAriaCurrent(AdminPaths)">Administration</a>
|
||||
}
|
||||
</nav>
|
||||
|
||||
<div class="site-menu-mobile-account">
|
||||
@@ -202,11 +210,13 @@
|
||||
private static readonly string[] ApplicationPaths = ["application", "application.html"];
|
||||
private static readonly string[] RulesPaths = ["reglement", "reglement.html"];
|
||||
private static readonly string[] UserPaths = ["utilisateur", "utilisateur.html"];
|
||||
private static readonly string[] AdminPaths = ["administration", "administration.html"];
|
||||
|
||||
private readonly LoginFormModel LoginModel = new();
|
||||
private readonly RegisterFormModel RegisterModel = new();
|
||||
|
||||
private bool IsAuthenticated;
|
||||
private bool IsAdmin;
|
||||
private bool ShowAuthModal;
|
||||
private bool IsSubmitting;
|
||||
private bool IsMobileMenuOpen;
|
||||
@@ -444,6 +454,7 @@
|
||||
if (user.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
IsAuthenticated = true;
|
||||
IsAdmin = user.IsInRole("admin");
|
||||
DisplayName = BuildDisplayName(user);
|
||||
DisplayMeta = BuildMeta(user);
|
||||
}
|
||||
@@ -463,6 +474,7 @@
|
||||
private void ResetAuthenticationDisplay()
|
||||
{
|
||||
IsAuthenticated = false;
|
||||
IsAdmin = false;
|
||||
DisplayName = "Utilisateur connecte";
|
||||
DisplayMeta = "Session active";
|
||||
}
|
||||
|
||||
28
ChessCubing.App/Models/Users/AdminUpdateUserRequest.cs
Normal file
28
ChessCubing.App/Models/Users/AdminUpdateUserRequest.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace ChessCubing.App.Models.Users;
|
||||
|
||||
public sealed class AdminUpdateUserRequest
|
||||
{
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
public string? Email { get; set; }
|
||||
|
||||
public string? FirstName { get; set; }
|
||||
|
||||
public string? LastName { get; set; }
|
||||
|
||||
public bool IsEnabled { get; set; }
|
||||
|
||||
public bool IsEmailVerified { get; set; }
|
||||
|
||||
public string? DisplayName { get; set; }
|
||||
|
||||
public string? Club { get; set; }
|
||||
|
||||
public string? City { get; set; }
|
||||
|
||||
public string? PreferredFormat { get; set; }
|
||||
|
||||
public string? FavoriteCube { get; set; }
|
||||
|
||||
public string? Bio { get; set; }
|
||||
}
|
||||
40
ChessCubing.App/Models/Users/AdminUserDetailResponse.cs
Normal file
40
ChessCubing.App/Models/Users/AdminUserDetailResponse.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
namespace ChessCubing.App.Models.Users;
|
||||
|
||||
public sealed class AdminUserDetailResponse
|
||||
{
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
public string? Email { get; set; }
|
||||
|
||||
public string? FirstName { get; set; }
|
||||
|
||||
public string? LastName { get; set; }
|
||||
|
||||
public string IdentityDisplayName { get; set; } = string.Empty;
|
||||
|
||||
public bool IsEnabled { get; set; }
|
||||
|
||||
public bool IsEmailVerified { get; set; }
|
||||
|
||||
public DateTime? AccountCreatedUtc { get; set; }
|
||||
|
||||
public bool HasSiteProfile { get; set; }
|
||||
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
public string? Club { get; set; }
|
||||
|
||||
public string? City { get; set; }
|
||||
|
||||
public string? PreferredFormat { get; set; }
|
||||
|
||||
public string? FavoriteCube { get; set; }
|
||||
|
||||
public string? Bio { get; set; }
|
||||
|
||||
public DateTime? SiteProfileCreatedUtc { get; set; }
|
||||
|
||||
public DateTime? SiteProfileUpdatedUtc { get; set; }
|
||||
}
|
||||
30
ChessCubing.App/Models/Users/AdminUserSummaryResponse.cs
Normal file
30
ChessCubing.App/Models/Users/AdminUserSummaryResponse.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
namespace ChessCubing.App.Models.Users;
|
||||
|
||||
public sealed class AdminUserSummaryResponse
|
||||
{
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
public string? Email { get; set; }
|
||||
|
||||
public string IdentityDisplayName { get; set; } = string.Empty;
|
||||
|
||||
public string? SiteDisplayName { get; set; }
|
||||
|
||||
public bool IsEnabled { get; set; }
|
||||
|
||||
public bool IsEmailVerified { get; set; }
|
||||
|
||||
public bool HasSiteProfile { get; set; }
|
||||
|
||||
public string? Club { get; set; }
|
||||
|
||||
public string? City { get; set; }
|
||||
|
||||
public string? PreferredFormat { get; set; }
|
||||
|
||||
public DateTime? AccountCreatedUtc { get; set; }
|
||||
|
||||
public DateTime? SiteProfileUpdatedUtc { get; set; }
|
||||
}
|
||||
747
ChessCubing.App/Pages/AdminPage.razor
Normal file
747
ChessCubing.App/Pages/AdminPage.razor
Normal file
@@ -0,0 +1,747 @@
|
||||
@page "/administration"
|
||||
@page "/administration.html"
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using System.Net
|
||||
@using System.Net.Http.Json
|
||||
@using ChessCubing.App.Models.Users
|
||||
@implements IDisposable
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
@inject HttpClient Http
|
||||
|
||||
<PageTitle>ChessCubing Arena | Administration</PageTitle>
|
||||
<PageBody BodyClass="home-body" />
|
||||
|
||||
<div class="ambient ambient-left"></div>
|
||||
<div class="ambient ambient-right"></div>
|
||||
|
||||
<div class="rules-shell">
|
||||
<header class="hero hero-home user-hero">
|
||||
<div class="hero-copy">
|
||||
<p class="eyebrow">Administration</p>
|
||||
<h1>Piloter les utilisateurs du site depuis l'interface</h1>
|
||||
<p class="lead">
|
||||
Cette premiere zone d'administration centralise les comptes du site,
|
||||
l'etat du compte Keycloak et les donnees metier stockees en MySQL.
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a class="button secondary" href="utilisateur.html">Voir l'espace utilisateur</a>
|
||||
<a class="button ghost" href="index.html">Retour au site</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="hero-preview">
|
||||
<div class="preview-card">
|
||||
<p class="micro-label">Vue d'ensemble</p>
|
||||
<div class="home-mini-grid two-columns admin-hero-stats">
|
||||
<article class="mini-panel">
|
||||
<span class="micro-label">Comptes</span>
|
||||
<strong>@Users.Count</strong>
|
||||
</article>
|
||||
<article class="mini-panel">
|
||||
<span class="micro-label">Profils site</span>
|
||||
<strong>@Users.Count(user => user.HasSiteProfile)</strong>
|
||||
</article>
|
||||
<article class="mini-panel">
|
||||
<span class="micro-label">Actifs</span>
|
||||
<strong>@Users.Count(user => user.IsEnabled)</strong>
|
||||
</article>
|
||||
<article class="mini-panel">
|
||||
<span class="micro-label">Emails verifies</span>
|
||||
<strong>@Users.Count(user => user.IsEmailVerified)</strong>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-banner">
|
||||
<span class="mini-chip">Etat d'acces</span>
|
||||
<strong>@HeroStatusTitle</strong>
|
||||
<p>@HeroStatusDescription</p>
|
||||
</div>
|
||||
</aside>
|
||||
</header>
|
||||
|
||||
<main class="rules-grid">
|
||||
@if (!IsAuthenticated)
|
||||
{
|
||||
<section class="panel panel-wide cta-panel">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Connexion requise</p>
|
||||
<h2>Connecte-toi avec un compte administrateur</h2>
|
||||
</div>
|
||||
<p class="section-copy">
|
||||
Utilise le menu du site pour te connecter. La zone d'administration
|
||||
devient disponible des qu'un compte avec le role `admin` est actif.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
else if (!IsAdmin)
|
||||
{
|
||||
<section class="panel panel-wide">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Acces refuse</p>
|
||||
<h2>Ce compte n'a pas les droits d'administration</h2>
|
||||
</div>
|
||||
<p class="section-copy">
|
||||
La gestion des utilisateurs est reservee aux comptes portant le role `admin`.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
else if (IsLoadingUsers)
|
||||
{
|
||||
<section class="panel panel-wide">
|
||||
<p class="eyebrow">Chargement</p>
|
||||
<h2>Recuperation de la liste des utilisateurs</h2>
|
||||
<p class="section-copy">Le site rassemble les comptes Keycloak et les profils du site.</p>
|
||||
</section>
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(LoadError))
|
||||
{
|
||||
<section class="panel panel-wide">
|
||||
<p class="eyebrow">Serveur</p>
|
||||
<h2>Impossible de charger l'administration</h2>
|
||||
<p class="profile-feedback error">@LoadError</p>
|
||||
<div class="hero-actions">
|
||||
<button class="button secondary" type="button" @onclick="LoadUsersAsync">Reessayer</button>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
else
|
||||
{
|
||||
<section class="panel panel-third admin-list-panel">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Utilisateurs</p>
|
||||
<h2>Parcourir les comptes</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-toolbar">
|
||||
<label class="field admin-search-field">
|
||||
<span>Recherche</span>
|
||||
<input @bind="SearchTerm" @bind:event="oninput" placeholder="Nom, email, club, ville..." />
|
||||
</label>
|
||||
|
||||
<button class="button ghost small" type="button" @onclick="LoadUsersAsync" disabled="@IsLoadingUsers">
|
||||
Actualiser
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (FilteredUsers.Count == 0)
|
||||
{
|
||||
<div class="callout">
|
||||
<span class="micro-label">Recherche</span>
|
||||
<p>Aucun utilisateur ne correspond au filtre en cours.</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="admin-user-list">
|
||||
@foreach (var user in FilteredUsers)
|
||||
{
|
||||
<button class="admin-user-card @(SelectedSubject == user.Subject ? "is-selected" : string.Empty)"
|
||||
type="button"
|
||||
@onclick="@(() => SelectUserAsync(user.Subject))">
|
||||
<div class="admin-user-card-head">
|
||||
<div>
|
||||
<strong>@BuildListDisplayName(user)</strong>
|
||||
<span>@user.Username</span>
|
||||
</div>
|
||||
<span class="mini-chip @(user.IsEnabled ? "admin-chip-success" : "admin-chip-danger")">
|
||||
@(user.IsEnabled ? "Actif" : "Suspendu")
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="admin-user-card-meta">
|
||||
<span>@(user.Email ?? "Email non renseigne")</span>
|
||||
<span>@(user.HasSiteProfile ? "Profil site actif" : "Profil site absent")</span>
|
||||
<span>@BuildUserCardFootnote(user)</span>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="panel admin-detail-panel">
|
||||
@if (IsLoadingDetail)
|
||||
{
|
||||
<p class="eyebrow">Chargement</p>
|
||||
<h2>Recuperation de la fiche utilisateur</h2>
|
||||
<p class="section-copy">Les details du compte sont en cours de chargement.</p>
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(DetailError))
|
||||
{
|
||||
<p class="eyebrow">Serveur</p>
|
||||
<h2>Impossible de charger cette fiche</h2>
|
||||
<p class="profile-feedback error">@DetailError</p>
|
||||
}
|
||||
else if (SelectedUser is null)
|
||||
{
|
||||
<p class="eyebrow">Selection</p>
|
||||
<h2>Choisis un utilisateur</h2>
|
||||
<p class="section-copy">La fiche detaillee apparait ici des qu'un compte est selectionne.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Edition</p>
|
||||
<h2>@SelectedUser.DisplayName</h2>
|
||||
</div>
|
||||
<p class="section-copy">
|
||||
Les roles restent geres dans Keycloak. Cette page couvre l'etat du compte et le profil du site.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="profile-meta-grid">
|
||||
<article class="profile-meta-card">
|
||||
<span class="micro-label">Identite Keycloak</span>
|
||||
<strong>@SelectedUser.IdentityDisplayName</strong>
|
||||
</article>
|
||||
<article class="profile-meta-card">
|
||||
<span class="micro-label">Compte cree le</span>
|
||||
<strong>@FormatDate(SelectedUser.AccountCreatedUtc)</strong>
|
||||
</article>
|
||||
<article class="profile-meta-card">
|
||||
<span class="micro-label">Profil site cree le</span>
|
||||
<strong>@FormatDate(SelectedUser.SiteProfileCreatedUtc)</strong>
|
||||
</article>
|
||||
<article class="profile-meta-card">
|
||||
<span class="micro-label">Profil site mis a jour</span>
|
||||
<strong>@FormatDate(SelectedUser.SiteProfileUpdatedUtc)</strong>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(SaveError))
|
||||
{
|
||||
<p class="profile-feedback error">@SaveError</p>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(SaveMessage))
|
||||
{
|
||||
<p class="profile-feedback success">@SaveMessage</p>
|
||||
}
|
||||
|
||||
<EditForm Model="@Form" OnValidSubmit="SaveUserAsync">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<div class="admin-toggle-grid">
|
||||
<label class="admin-toggle-card">
|
||||
<InputCheckbox @bind-Value="Form.IsEnabled" />
|
||||
<div>
|
||||
<strong>Compte actif</strong>
|
||||
<span>Autoriser ou bloquer la connexion a l'application.</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="admin-toggle-card">
|
||||
<InputCheckbox @bind-Value="Form.IsEmailVerified" />
|
||||
<div>
|
||||
<strong>Email verifie</strong>
|
||||
<span>Indique si l'adresse email a ete validee dans Keycloak.</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="admin-form-grid">
|
||||
<label class="field">
|
||||
<span>Nom d'utilisateur</span>
|
||||
<InputText @bind-Value="Form.Username" />
|
||||
<ValidationMessage For="@(() => Form.Username)" />
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>Email</span>
|
||||
<InputText @bind-Value="Form.Email" />
|
||||
<ValidationMessage For="@(() => Form.Email)" />
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>Prenom</span>
|
||||
<InputText @bind-Value="Form.FirstName" />
|
||||
<ValidationMessage For="@(() => Form.FirstName)" />
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>Nom</span>
|
||||
<InputText @bind-Value="Form.LastName" />
|
||||
<ValidationMessage For="@(() => Form.LastName)" />
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>Nom affiche sur le site</span>
|
||||
<InputText @bind-Value="Form.DisplayName" />
|
||||
<ValidationMessage For="@(() => Form.DisplayName)" />
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>Club</span>
|
||||
<InputText @bind-Value="Form.Club" />
|
||||
<ValidationMessage For="@(() => Form.Club)" />
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>Ville</span>
|
||||
<InputText @bind-Value="Form.City" />
|
||||
<ValidationMessage For="@(() => Form.City)" />
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>Format prefere</span>
|
||||
<InputSelect @bind-Value="Form.PreferredFormat">
|
||||
<option value="">Aucune preference</option>
|
||||
<option value="Twice">Twice</option>
|
||||
<option value="Time">Time</option>
|
||||
<option value="Les deux">Les deux</option>
|
||||
</InputSelect>
|
||||
<ValidationMessage For="@(() => Form.PreferredFormat)" />
|
||||
</label>
|
||||
|
||||
<label class="field span-2">
|
||||
<span>Cube favori</span>
|
||||
<InputText @bind-Value="Form.FavoriteCube" />
|
||||
<ValidationMessage For="@(() => Form.FavoriteCube)" />
|
||||
</label>
|
||||
|
||||
<label class="field span-2">
|
||||
<span>Bio</span>
|
||||
<InputTextArea @bind-Value="Form.Bio" />
|
||||
<ValidationMessage For="@(() => Form.Bio)" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="profile-actions">
|
||||
<button class="button secondary" type="submit" disabled="@IsSaving">
|
||||
@(IsSaving ? "Enregistrement..." : "Enregistrer les modifications")
|
||||
</button>
|
||||
<p class="section-copy">Le profil site est cree automatiquement lors du premier enregistrement.</p>
|
||||
</div>
|
||||
</EditForm>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private readonly AdminUserFormModel Form = new();
|
||||
private readonly List<AdminUserSummaryResponse> Users = [];
|
||||
|
||||
private AdminUserDetailResponse? SelectedUser;
|
||||
private string? SelectedSubject;
|
||||
private string SearchTerm = string.Empty;
|
||||
private bool IsAuthenticated;
|
||||
private bool IsAdmin;
|
||||
private bool IsLoadingUsers = true;
|
||||
private bool IsLoadingDetail;
|
||||
private bool IsSaving;
|
||||
private string? LoadError;
|
||||
private string? DetailError;
|
||||
private string? SaveError;
|
||||
private string? SaveMessage;
|
||||
|
||||
private List<AdminUserSummaryResponse> FilteredUsers
|
||||
=> Users
|
||||
.Where(MatchesSearch)
|
||||
.ToList();
|
||||
|
||||
private string HeroStatusTitle
|
||||
=> !IsAuthenticated
|
||||
? "Connexion requise"
|
||||
: !IsAdmin
|
||||
? "Compte non admin"
|
||||
: IsLoadingUsers
|
||||
? "Chargement des utilisateurs"
|
||||
: $"{Users.Count} comptes disponibles";
|
||||
|
||||
private string HeroStatusDescription
|
||||
=> !IsAuthenticated
|
||||
? "Connecte-toi avec un compte admin pour ouvrir la zone d'administration."
|
||||
: !IsAdmin
|
||||
? "Le compte actuel est authentifie mais ne dispose pas du role admin."
|
||||
: IsLoadingUsers
|
||||
? "Le site recupere les comptes et les profils deja relies a MySQL."
|
||||
: "La fiche detaillee permet de modifier les informations de compte et le profil du site.";
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
AuthenticationStateProvider.AuthenticationStateChanged += HandleAuthenticationStateChanged;
|
||||
await LoadUsersAsync();
|
||||
}
|
||||
|
||||
private void HandleAuthenticationStateChanged(Task<AuthenticationState> authenticationStateTask)
|
||||
=> _ = InvokeAsync(LoadUsersAsync);
|
||||
|
||||
private async Task LoadUsersAsync()
|
||||
{
|
||||
LoadError = null;
|
||||
DetailError = null;
|
||||
SaveError = null;
|
||||
SaveMessage = null;
|
||||
IsLoadingUsers = true;
|
||||
|
||||
try
|
||||
{
|
||||
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||||
if (authState.User.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
ResetAdminState();
|
||||
return;
|
||||
}
|
||||
|
||||
IsAuthenticated = true;
|
||||
IsAdmin = authState.User.IsInRole("admin");
|
||||
|
||||
if (!IsAdmin)
|
||||
{
|
||||
Users.Clear();
|
||||
SelectedSubject = null;
|
||||
SelectedUser = null;
|
||||
Form.Reset();
|
||||
return;
|
||||
}
|
||||
|
||||
var response = await Http.GetAsync("api/admin/users");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
LoadError = await ReadErrorAsync(response, "La liste des utilisateurs n'a pas pu etre chargee.");
|
||||
Users.Clear();
|
||||
SelectedSubject = null;
|
||||
SelectedUser = null;
|
||||
Form.Reset();
|
||||
return;
|
||||
}
|
||||
|
||||
var users = await response.Content.ReadFromJsonAsync<List<AdminUserSummaryResponse>>();
|
||||
Users.Clear();
|
||||
Users.AddRange(users ?? []);
|
||||
|
||||
if (Users.Count == 0)
|
||||
{
|
||||
SelectedSubject = null;
|
||||
SelectedUser = null;
|
||||
Form.Reset();
|
||||
return;
|
||||
}
|
||||
|
||||
var nextSubject = Users.Any(user => user.Subject == SelectedSubject)
|
||||
? SelectedSubject
|
||||
: Users[0].Subject;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(nextSubject))
|
||||
{
|
||||
await LoadUserDetailAsync(nextSubject, keepFeedback: true);
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
LoadError = "Le service d'administration est temporairement indisponible.";
|
||||
Users.Clear();
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
LoadError = "Le chargement de l'administration a pris trop de temps.";
|
||||
Users.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoadingUsers = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SelectUserAsync(string subject)
|
||||
{
|
||||
if (string.Equals(subject, SelectedSubject, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SaveError = null;
|
||||
SaveMessage = null;
|
||||
await LoadUserDetailAsync(subject, keepFeedback: false);
|
||||
}
|
||||
|
||||
private async Task LoadUserDetailAsync(string subject, bool keepFeedback)
|
||||
{
|
||||
if (!keepFeedback)
|
||||
{
|
||||
SaveError = null;
|
||||
SaveMessage = null;
|
||||
}
|
||||
|
||||
IsLoadingDetail = true;
|
||||
DetailError = null;
|
||||
SelectedSubject = subject;
|
||||
|
||||
try
|
||||
{
|
||||
var response = await Http.GetAsync($"api/admin/users/{Uri.EscapeDataString(subject)}");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
DetailError = await ReadErrorAsync(response, "La fiche utilisateur n'a pas pu etre chargee.");
|
||||
SelectedUser = null;
|
||||
return;
|
||||
}
|
||||
|
||||
SelectedUser = await response.Content.ReadFromJsonAsync<AdminUserDetailResponse>();
|
||||
if (SelectedUser is null)
|
||||
{
|
||||
DetailError = "Le serveur a retourne une fiche vide.";
|
||||
return;
|
||||
}
|
||||
|
||||
FillForm(SelectedUser);
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
DetailError = "Le detail utilisateur est temporairement indisponible.";
|
||||
SelectedUser = null;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
DetailError = "Le detail utilisateur a pris trop de temps a se charger.";
|
||||
SelectedUser = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoadingDetail = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveUserAsync()
|
||||
{
|
||||
if (IsSaving || SelectedSubject is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsSaving = true;
|
||||
SaveError = null;
|
||||
SaveMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
var payload = new AdminUpdateUserRequest
|
||||
{
|
||||
Username = Form.Username ?? string.Empty,
|
||||
Email = Form.Email,
|
||||
FirstName = Form.FirstName,
|
||||
LastName = Form.LastName,
|
||||
IsEnabled = Form.IsEnabled,
|
||||
IsEmailVerified = Form.IsEmailVerified,
|
||||
DisplayName = Form.DisplayName,
|
||||
Club = Form.Club,
|
||||
City = Form.City,
|
||||
PreferredFormat = Form.PreferredFormat,
|
||||
FavoriteCube = Form.FavoriteCube,
|
||||
Bio = Form.Bio,
|
||||
};
|
||||
|
||||
var response = await Http.PutAsJsonAsync($"api/admin/users/{Uri.EscapeDataString(SelectedSubject)}", payload);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
SaveError = await ReadErrorAsync(response, "La mise a jour de l'utilisateur a echoue.");
|
||||
return;
|
||||
}
|
||||
|
||||
SelectedUser = await response.Content.ReadFromJsonAsync<AdminUserDetailResponse>();
|
||||
if (SelectedUser is null)
|
||||
{
|
||||
SaveError = "Le serveur a retourne une fiche vide apres la sauvegarde.";
|
||||
return;
|
||||
}
|
||||
|
||||
FillForm(SelectedUser);
|
||||
UpdateSummary(SelectedUser);
|
||||
SaveMessage = "La fiche utilisateur a bien ete mise a jour.";
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
SaveError = "Le service d'administration est temporairement indisponible.";
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
SaveError = "La sauvegarde a pris trop de temps.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSummary(AdminUserDetailResponse detail)
|
||||
{
|
||||
var summary = Users.FirstOrDefault(user => user.Subject == detail.Subject);
|
||||
if (summary is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
summary.Username = detail.Username;
|
||||
summary.Email = detail.Email;
|
||||
summary.IdentityDisplayName = detail.IdentityDisplayName;
|
||||
summary.SiteDisplayName = detail.DisplayName;
|
||||
summary.IsEnabled = detail.IsEnabled;
|
||||
summary.IsEmailVerified = detail.IsEmailVerified;
|
||||
summary.HasSiteProfile = detail.HasSiteProfile;
|
||||
summary.Club = detail.Club;
|
||||
summary.City = detail.City;
|
||||
summary.PreferredFormat = detail.PreferredFormat;
|
||||
summary.AccountCreatedUtc = detail.AccountCreatedUtc;
|
||||
summary.SiteProfileUpdatedUtc = detail.SiteProfileUpdatedUtc;
|
||||
}
|
||||
|
||||
private void FillForm(AdminUserDetailResponse user)
|
||||
{
|
||||
Form.Username = user.Username;
|
||||
Form.Email = user.Email;
|
||||
Form.FirstName = user.FirstName;
|
||||
Form.LastName = user.LastName;
|
||||
Form.IsEnabled = user.IsEnabled;
|
||||
Form.IsEmailVerified = user.IsEmailVerified;
|
||||
Form.DisplayName = user.DisplayName;
|
||||
Form.Club = user.Club;
|
||||
Form.City = user.City;
|
||||
Form.PreferredFormat = user.PreferredFormat;
|
||||
Form.FavoriteCube = user.FavoriteCube;
|
||||
Form.Bio = user.Bio;
|
||||
}
|
||||
|
||||
private void ResetAdminState()
|
||||
{
|
||||
IsAuthenticated = false;
|
||||
IsAdmin = false;
|
||||
Users.Clear();
|
||||
SelectedSubject = null;
|
||||
SelectedUser = null;
|
||||
Form.Reset();
|
||||
}
|
||||
|
||||
private bool MatchesSearch(AdminUserSummaryResponse user)
|
||||
{
|
||||
var search = SearchTerm.Trim();
|
||||
if (string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return Contains(user.Username, search)
|
||||
|| Contains(user.Email, search)
|
||||
|| Contains(user.IdentityDisplayName, search)
|
||||
|| Contains(user.SiteDisplayName, search)
|
||||
|| Contains(user.Club, search)
|
||||
|| Contains(user.City, search);
|
||||
}
|
||||
|
||||
private static bool Contains(string? value, string search)
|
||||
=> value?.Contains(search, StringComparison.OrdinalIgnoreCase) == true;
|
||||
|
||||
private static string BuildListDisplayName(AdminUserSummaryResponse user)
|
||||
=> user.SiteDisplayName
|
||||
?? user.IdentityDisplayName
|
||||
?? user.Username;
|
||||
|
||||
private static string BuildUserCardFootnote(AdminUserSummaryResponse user)
|
||||
=> user.SiteProfileUpdatedUtc is not null
|
||||
? $"Maj site {FormatDate(user.SiteProfileUpdatedUtc)}"
|
||||
: user.AccountCreatedUtc is not null
|
||||
? $"Compte cree {FormatDate(user.AccountCreatedUtc)}"
|
||||
: "Aucune date disponible";
|
||||
|
||||
private static async Task<string> ReadErrorAsync(HttpResponseMessage response, string fallbackMessage)
|
||||
{
|
||||
try
|
||||
{
|
||||
var error = await response.Content.ReadFromJsonAsync<ApiErrorMessage>();
|
||||
if (!string.IsNullOrWhiteSpace(error?.Message))
|
||||
{
|
||||
return error.Message;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return response.StatusCode switch
|
||||
{
|
||||
HttpStatusCode.Unauthorized => "La session a expire. Reconnecte-toi puis recharge la page.",
|
||||
HttpStatusCode.Forbidden => "Ce compte n'a pas le role admin requis pour cette action.",
|
||||
HttpStatusCode.NotFound => "Cet utilisateur n'existe plus ou n'est plus disponible.",
|
||||
_ => fallbackMessage,
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatDate(DateTime? value)
|
||||
=> value is null
|
||||
? "Non disponible"
|
||||
: value.Value.ToLocalTime().ToString("dd MMM yyyy 'a' HH:mm", CultureInfo.GetCultureInfo("fr-FR"));
|
||||
|
||||
public void Dispose()
|
||||
=> AuthenticationStateProvider.AuthenticationStateChanged -= HandleAuthenticationStateChanged;
|
||||
|
||||
private sealed class ApiErrorMessage
|
||||
{
|
||||
public string? Message { get; set; }
|
||||
}
|
||||
|
||||
private sealed class AdminUserFormModel
|
||||
{
|
||||
[Required(ErrorMessage = "Le nom d'utilisateur est obligatoire.")]
|
||||
[MaxLength(120, ErrorMessage = "Le nom d'utilisateur doit rester sous 120 caracteres.")]
|
||||
public string? Username { get; set; }
|
||||
|
||||
[MaxLength(255, ErrorMessage = "L'email doit rester sous 255 caracteres.")]
|
||||
[EmailAddress(ErrorMessage = "L'email n'est pas valide.")]
|
||||
public string? Email { get; set; }
|
||||
|
||||
[MaxLength(120, ErrorMessage = "Le prenom doit rester sous 120 caracteres.")]
|
||||
public string? FirstName { get; set; }
|
||||
|
||||
[MaxLength(120, ErrorMessage = "Le nom doit rester sous 120 caracteres.")]
|
||||
public string? LastName { get; set; }
|
||||
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
|
||||
public bool IsEmailVerified { get; set; }
|
||||
|
||||
[MaxLength(120, ErrorMessage = "Le nom affiche doit rester sous 120 caracteres.")]
|
||||
public string? DisplayName { get; set; }
|
||||
|
||||
[MaxLength(120, ErrorMessage = "Le club doit rester sous 120 caracteres.")]
|
||||
public string? Club { get; set; }
|
||||
|
||||
[MaxLength(120, ErrorMessage = "La ville doit rester sous 120 caracteres.")]
|
||||
public string? City { get; set; }
|
||||
|
||||
[MaxLength(40, ErrorMessage = "Le format prefere doit rester sous 40 caracteres.")]
|
||||
public string? PreferredFormat { get; set; }
|
||||
|
||||
[MaxLength(120, ErrorMessage = "Le cube favori doit rester sous 120 caracteres.")]
|
||||
public string? FavoriteCube { get; set; }
|
||||
|
||||
[MaxLength(1200, ErrorMessage = "La bio doit rester sous 1200 caracteres.")]
|
||||
public string? Bio { get; set; }
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Username = string.Empty;
|
||||
Email = string.Empty;
|
||||
FirstName = string.Empty;
|
||||
LastName = string.Empty;
|
||||
IsEnabled = true;
|
||||
IsEmailVerified = false;
|
||||
DisplayName = string.Empty;
|
||||
Club = string.Empty;
|
||||
City = string.Empty;
|
||||
PreferredFormat = string.Empty;
|
||||
FavoriteCube = string.Empty;
|
||||
Bio = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user