Ajoute une table de gestion des utilisateurs

This commit is contained in:
2026-04-15 21:53:24 +02:00
parent 655637072c
commit 0e6115e423
8 changed files with 865 additions and 189 deletions

View File

@@ -0,0 +1,32 @@
namespace ChessCubing.App.Models.Users;
public sealed class AdminCreateUserRequest
{
public string Username { get; set; } = string.Empty;
public string? Email { get; set; }
public string Password { get; set; } = string.Empty;
public string ConfirmPassword { get; set; } = string.Empty;
public string? FirstName { get; set; }
public string? LastName { get; set; }
public bool IsEnabled { get; set; } = true;
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; }
}

View File

@@ -18,10 +18,10 @@
<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>
<h1>Gerer les utilisateurs avec une vue tableau</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.
Cette zone rassemble les comptes Keycloak et les profils du site
dans une interface plus classique, avec ajout, edition et suppression.
</p>
<div class="hero-actions">
<a class="button secondary" href="utilisateur.html">Voir l'espace utilisateur</a>
@@ -70,8 +70,8 @@
<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.
La gestion des utilisateurs devient disponible des qu'un compte
portant le role `admin` est connecte.
</p>
</div>
</section>
@@ -105,29 +105,44 @@
<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>
<button class="button secondary" type="button" @onclick="ReloadUsersAsync">Reessayer</button>
</div>
</section>
}
else
{
<section class="panel panel-third admin-list-panel">
<section class="panel panel-wide admin-table-panel">
<div class="section-heading">
<div>
<p class="eyebrow">Utilisateurs</p>
<h2>Parcourir les comptes</h2>
<h2>Liste des comptes</h2>
</div>
<p class="section-copy">
Le tableau centralise les comptes du site avec une colonne d'actions
pour les operations courantes.
</p>
</div>
<div class="admin-toolbar">
@if (!string.IsNullOrWhiteSpace(SaveError))
{
<p class="profile-feedback error">@SaveError</p>
}
@if (!string.IsNullOrWhiteSpace(SaveMessage))
{
<p class="profile-feedback success">@SaveMessage</p>
}
<div class="admin-toolbar admin-toolbar-grid">
<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 class="admin-toolbar-actions">
<button class="button secondary small" type="button" @onclick="OpenCreateModal">Ajouter un utilisateur</button>
<button class="button ghost small" type="button" @onclick="ReloadUsersAsync" disabled="@IsLoadingUsers">Actualiser</button>
</div>
</div>
@if (FilteredUsers.Count == 0)
@@ -139,34 +154,61 @@
}
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 class="admin-table-shell">
<table class="admin-table">
<thead>
<tr>
<th>Utilisateur</th>
<th>Email</th>
<th>Nom affiche</th>
<th>Etat</th>
<th>Profil site</th>
<th>Derniere mise a jour</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var user in FilteredUsers)
{
<tr class="@BuildTableRowClass(user)">
<td>
<div class="admin-cell-stack">
<strong>@user.Username</strong>
<span>@user.IdentityDisplayName</span>
</div>
</td>
<td>@(user.Email ?? "Non renseigne")</td>
<td>@(user.SiteDisplayName ?? "A definir")</td>
<td>
<span class="mini-chip @(user.IsEnabled ? "admin-chip-success" : "admin-chip-danger")">
@(user.IsEnabled ? "Actif" : "Suspendu")
</span>
</td>
<td>
<span class="mini-chip @(user.HasSiteProfile ? "admin-chip-neutral" : "admin-chip-outline")">
@(user.HasSiteProfile ? "Cree" : "Absent")
</span>
</td>
<td>@BuildUserCardFootnote(user)</td>
<td>
<div class="admin-actions-cell">
<button class="button ghost small" type="button" @onclick="@(() => SelectUserAsync(user.Subject))">
Modifier
</button>
<button class="button danger small" type="button" @onclick="@(() => PromptDeleteUser(user))">
Supprimer
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</section>
<section class="panel admin-detail-panel">
<section class="panel panel-wide admin-detail-panel">
@if (IsLoadingDetail)
{
<p class="eyebrow">Chargement</p>
@@ -181,9 +223,11 @@
}
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>
<p class="eyebrow">Edition</p>
<h2>Choisis un utilisateur dans le tableau</h2>
<p class="section-copy">
Le bouton `Modifier` charge la fiche detaillee ici pour editer le compte et le profil du site.
</p>
}
else
{
@@ -193,7 +237,7 @@
<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.
Les roles restent geres dans Keycloak. Cette fiche couvre l'etat du compte et le profil du site.
</p>
</div>
@@ -216,22 +260,12 @@
</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">
<EditForm Model="@EditFormModel" OnValidSubmit="SaveUserAsync">
<DataAnnotationsValidator />
<div class="admin-toggle-grid">
<label class="admin-toggle-card">
<InputCheckbox @bind-Value="Form.IsEnabled" />
<InputCheckbox @bind-Value="EditFormModel.IsEnabled" />
<div>
<strong>Compte actif</strong>
<span>Autoriser ou bloquer la connexion a l'application.</span>
@@ -239,7 +273,7 @@
</label>
<label class="admin-toggle-card">
<InputCheckbox @bind-Value="Form.IsEmailVerified" />
<InputCheckbox @bind-Value="EditFormModel.IsEmailVerified" />
<div>
<strong>Email verifie</strong>
<span>Indique si l'adresse email a ete validee dans Keycloak.</span>
@@ -250,67 +284,67 @@
<div class="admin-form-grid">
<label class="field">
<span>Nom d'utilisateur</span>
<InputText @bind-Value="Form.Username" />
<ValidationMessage For="@(() => Form.Username)" />
<InputText @bind-Value="EditFormModel.Username" />
<ValidationMessage For="@(() => EditFormModel.Username)" />
</label>
<label class="field">
<span>Email</span>
<InputText @bind-Value="Form.Email" />
<ValidationMessage For="@(() => Form.Email)" />
<InputText @bind-Value="EditFormModel.Email" />
<ValidationMessage For="@(() => EditFormModel.Email)" />
</label>
<label class="field">
<span>Prenom</span>
<InputText @bind-Value="Form.FirstName" />
<ValidationMessage For="@(() => Form.FirstName)" />
<InputText @bind-Value="EditFormModel.FirstName" />
<ValidationMessage For="@(() => EditFormModel.FirstName)" />
</label>
<label class="field">
<span>Nom</span>
<InputText @bind-Value="Form.LastName" />
<ValidationMessage For="@(() => Form.LastName)" />
<InputText @bind-Value="EditFormModel.LastName" />
<ValidationMessage For="@(() => EditFormModel.LastName)" />
</label>
<label class="field">
<span>Nom affiche sur le site</span>
<InputText @bind-Value="Form.DisplayName" />
<ValidationMessage For="@(() => Form.DisplayName)" />
<InputText @bind-Value="EditFormModel.DisplayName" />
<ValidationMessage For="@(() => EditFormModel.DisplayName)" />
</label>
<label class="field">
<span>Club</span>
<InputText @bind-Value="Form.Club" />
<ValidationMessage For="@(() => Form.Club)" />
<InputText @bind-Value="EditFormModel.Club" />
<ValidationMessage For="@(() => EditFormModel.Club)" />
</label>
<label class="field">
<span>Ville</span>
<InputText @bind-Value="Form.City" />
<ValidationMessage For="@(() => Form.City)" />
<InputText @bind-Value="EditFormModel.City" />
<ValidationMessage For="@(() => EditFormModel.City)" />
</label>
<label class="field">
<span>Format prefere</span>
<InputSelect @bind-Value="Form.PreferredFormat">
<InputSelect @bind-Value="EditFormModel.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)" />
<ValidationMessage For="@(() => EditFormModel.PreferredFormat)" />
</label>
<label class="field span-2">
<span>Cube favori</span>
<InputText @bind-Value="Form.FavoriteCube" />
<ValidationMessage For="@(() => Form.FavoriteCube)" />
<InputText @bind-Value="EditFormModel.FavoriteCube" />
<ValidationMessage For="@(() => EditFormModel.FavoriteCube)" />
</label>
<label class="field span-2">
<span>Bio</span>
<InputTextArea @bind-Value="Form.Bio" />
<ValidationMessage For="@(() => Form.Bio)" />
<InputTextArea @bind-Value="EditFormModel.Bio" />
<ValidationMessage For="@(() => EditFormModel.Bio)" />
</label>
</div>
@@ -327,11 +361,175 @@
</main>
</div>
<section class="modal @(ShowCreateModal ? string.Empty : "hidden")" aria-hidden="@BoolString(!ShowCreateModal)">
<div class="modal-backdrop" @onclick="CloseCreateModal"></div>
<div class="modal-card admin-modal-card">
<div class="modal-head">
<div>
<p class="eyebrow">Ajout</p>
<h2>Ajouter un utilisateur</h2>
</div>
<button class="button ghost small" type="button" @onclick="CloseCreateModal" disabled="@IsCreating">Fermer</button>
</div>
<p class="section-copy">
La creation ajoute un compte dans Keycloak puis initialise le profil du site.
</p>
@if (!string.IsNullOrWhiteSpace(CreateError))
{
<p class="profile-feedback error">@CreateError</p>
}
<EditForm Model="@CreateFormModel" OnValidSubmit="CreateUserAsync">
<DataAnnotationsValidator />
<div class="admin-form-grid">
<label class="field">
<span>Nom d'utilisateur</span>
<InputText @bind-Value="CreateFormModel.Username" />
<ValidationMessage For="@(() => CreateFormModel.Username)" />
</label>
<label class="field">
<span>Email</span>
<InputText @bind-Value="CreateFormModel.Email" />
<ValidationMessage For="@(() => CreateFormModel.Email)" />
</label>
<label class="field">
<span>Prenom</span>
<InputText @bind-Value="CreateFormModel.FirstName" />
<ValidationMessage For="@(() => CreateFormModel.FirstName)" />
</label>
<label class="field">
<span>Nom</span>
<InputText @bind-Value="CreateFormModel.LastName" />
<ValidationMessage For="@(() => CreateFormModel.LastName)" />
</label>
<label class="field">
<span>Mot de passe</span>
<InputText @bind-Value="CreateFormModel.Password" type="password" />
<ValidationMessage For="@(() => CreateFormModel.Password)" />
</label>
<label class="field">
<span>Confirmation du mot de passe</span>
<InputText @bind-Value="CreateFormModel.ConfirmPassword" type="password" />
<ValidationMessage For="@(() => CreateFormModel.ConfirmPassword)" />
</label>
<label class="field">
<span>Nom affiche sur le site</span>
<InputText @bind-Value="CreateFormModel.DisplayName" />
<ValidationMessage For="@(() => CreateFormModel.DisplayName)" />
</label>
<label class="field">
<span>Club</span>
<InputText @bind-Value="CreateFormModel.Club" />
<ValidationMessage For="@(() => CreateFormModel.Club)" />
</label>
<label class="field">
<span>Ville</span>
<InputText @bind-Value="CreateFormModel.City" />
<ValidationMessage For="@(() => CreateFormModel.City)" />
</label>
<label class="field">
<span>Format prefere</span>
<InputSelect @bind-Value="CreateFormModel.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="@(() => CreateFormModel.PreferredFormat)" />
</label>
<label class="field span-2">
<span>Cube favori</span>
<InputText @bind-Value="CreateFormModel.FavoriteCube" />
<ValidationMessage For="@(() => CreateFormModel.FavoriteCube)" />
</label>
<label class="field span-2">
<span>Bio</span>
<InputTextArea @bind-Value="CreateFormModel.Bio" />
<ValidationMessage For="@(() => CreateFormModel.Bio)" />
</label>
</div>
<div class="admin-toggle-grid">
<label class="admin-toggle-card">
<InputCheckbox @bind-Value="CreateFormModel.IsEnabled" />
<div>
<strong>Compte actif</strong>
<span>Permettre au nouvel utilisateur de se connecter immediatement.</span>
</div>
</label>
<label class="admin-toggle-card">
<InputCheckbox @bind-Value="CreateFormModel.IsEmailVerified" />
<div>
<strong>Email verifie</strong>
<span>Marquer l'adresse comme deja verifiee dans Keycloak.</span>
</div>
</label>
</div>
<div class="modal-actions">
<button class="button secondary" type="submit" disabled="@IsCreating">
@(IsCreating ? "Creation..." : "Creer l'utilisateur")
</button>
</div>
</EditForm>
</div>
</section>
<section class="modal @(ShowDeleteModal ? string.Empty : "hidden")" aria-hidden="@BoolString(!ShowDeleteModal)">
<div class="modal-backdrop" @onclick="CloseDeleteModal"></div>
<div class="modal-card admin-confirm-card">
<div class="modal-head">
<div>
<p class="eyebrow">Suppression</p>
<h2>Supprimer cet utilisateur</h2>
</div>
<button class="button ghost small" type="button" @onclick="CloseDeleteModal" disabled="@IsDeleting">Fermer</button>
</div>
@if (PendingDeleteUser is not null)
{
<p>
Le compte <strong>@PendingDeleteUser.Username</strong> sera supprime de Keycloak
ainsi que son profil du site. Cette action est irreversible.
</p>
}
@if (!string.IsNullOrWhiteSpace(DeleteError))
{
<p class="profile-feedback error">@DeleteError</p>
}
<div class="modal-actions">
<button class="button danger" type="button" @onclick="DeleteUserAsync" disabled="@IsDeleting">
@(IsDeleting ? "Suppression..." : "Supprimer definitivement")
</button>
<button class="button ghost" type="button" @onclick="CloseDeleteModal" disabled="@IsDeleting">Annuler</button>
</div>
</div>
</section>
@code {
private readonly AdminUserFormModel Form = new();
private readonly AdminUserFormModel EditFormModel = new();
private readonly AdminCreateUserFormModel CreateFormModel = new();
private readonly List<AdminUserSummaryResponse> Users = [];
private AdminUserDetailResponse? SelectedUser;
private AdminUserSummaryResponse? PendingDeleteUser;
private string? SelectedSubject;
private string SearchTerm = string.Empty;
private bool IsAuthenticated;
@@ -339,10 +537,16 @@
private bool IsLoadingUsers = true;
private bool IsLoadingDetail;
private bool IsSaving;
private bool IsCreating;
private bool IsDeleting;
private bool ShowCreateModal;
private bool ShowDeleteModal;
private string? LoadError;
private string? DetailError;
private string? SaveError;
private string? SaveMessage;
private string? CreateError;
private string? DeleteError;
private List<AdminUserSummaryResponse> FilteredUsers
=> Users
@@ -365,7 +569,7 @@
? "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.";
: "Le tableau permet maintenant les operations classiques d'ajout, modification et suppression.";
protected override async Task OnInitializedAsync()
{
@@ -374,14 +578,22 @@
}
private void HandleAuthenticationStateChanged(Task<AuthenticationState> authenticationStateTask)
=> _ = InvokeAsync(LoadUsersAsync);
=> _ = InvokeAsync(() => LoadUsersAsync(SelectedSubject));
private async Task LoadUsersAsync()
private async Task ReloadUsersAsync()
=> await LoadUsersAsync(SelectedSubject);
private async Task LoadUsersAsync(string? preferredSubject = null, bool preserveFeedback = false)
{
LoadError = null;
DetailError = null;
SaveError = null;
SaveMessage = null;
if (!preserveFeedback)
{
SaveError = null;
SaveMessage = null;
}
IsLoadingUsers = true;
try
@@ -401,7 +613,7 @@
Users.Clear();
SelectedSubject = null;
SelectedUser = null;
Form.Reset();
EditFormModel.Reset();
return;
}
@@ -412,7 +624,7 @@
Users.Clear();
SelectedSubject = null;
SelectedUser = null;
Form.Reset();
EditFormModel.Reset();
return;
}
@@ -424,13 +636,15 @@
{
SelectedSubject = null;
SelectedUser = null;
Form.Reset();
EditFormModel.Reset();
return;
}
var nextSubject = Users.Any(user => user.Subject == SelectedSubject)
? SelectedSubject
: Users[0].Subject;
var nextSubject = Users.Any(user => user.Subject == preferredSubject)
? preferredSubject
: Users.Any(user => user.Subject == SelectedSubject)
? SelectedSubject
: Users[0].Subject;
if (!string.IsNullOrWhiteSpace(nextSubject))
{
@@ -456,11 +670,6 @@
private async Task SelectUserAsync(string subject)
{
if (string.Equals(subject, SelectedSubject, StringComparison.Ordinal))
{
return;
}
SaveError = null;
SaveMessage = null;
await LoadUserDetailAsync(subject, keepFeedback: false);
@@ -495,7 +704,7 @@
return;
}
FillForm(SelectedUser);
FillEditForm(SelectedUser);
}
catch (HttpRequestException)
{
@@ -529,18 +738,18 @@
{
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,
Username = EditFormModel.Username ?? string.Empty,
Email = EditFormModel.Email,
FirstName = EditFormModel.FirstName,
LastName = EditFormModel.LastName,
IsEnabled = EditFormModel.IsEnabled,
IsEmailVerified = EditFormModel.IsEmailVerified,
DisplayName = EditFormModel.DisplayName,
Club = EditFormModel.Club,
City = EditFormModel.City,
PreferredFormat = EditFormModel.PreferredFormat,
FavoriteCube = EditFormModel.FavoriteCube,
Bio = EditFormModel.Bio,
};
var response = await Http.PutAsJsonAsync($"api/admin/users/{Uri.EscapeDataString(SelectedSubject)}", payload);
@@ -557,7 +766,7 @@
return;
}
FillForm(SelectedUser);
FillEditForm(SelectedUser);
UpdateSummary(SelectedUser);
SaveMessage = "La fiche utilisateur a bien ete mise a jour.";
}
@@ -575,6 +784,150 @@
}
}
private void OpenCreateModal()
{
CreateFormModel.Reset();
CreateError = null;
ShowCreateModal = true;
}
private void CloseCreateModal()
{
if (IsCreating)
{
return;
}
ShowCreateModal = false;
CreateError = null;
}
private async Task CreateUserAsync()
{
if (IsCreating)
{
return;
}
IsCreating = true;
CreateError = null;
try
{
var payload = new AdminCreateUserRequest
{
Username = CreateFormModel.Username ?? string.Empty,
Email = CreateFormModel.Email,
Password = CreateFormModel.Password ?? string.Empty,
ConfirmPassword = CreateFormModel.ConfirmPassword ?? string.Empty,
FirstName = CreateFormModel.FirstName,
LastName = CreateFormModel.LastName,
IsEnabled = CreateFormModel.IsEnabled,
IsEmailVerified = CreateFormModel.IsEmailVerified,
DisplayName = CreateFormModel.DisplayName,
Club = CreateFormModel.Club,
City = CreateFormModel.City,
PreferredFormat = CreateFormModel.PreferredFormat,
FavoriteCube = CreateFormModel.FavoriteCube,
Bio = CreateFormModel.Bio,
};
var response = await Http.PostAsJsonAsync("api/admin/users", payload);
if (!response.IsSuccessStatusCode)
{
CreateError = await ReadErrorAsync(response, "La creation de l'utilisateur a echoue.");
return;
}
var createdUser = await response.Content.ReadFromJsonAsync<AdminUserDetailResponse>();
if (createdUser is null)
{
CreateError = "Le serveur a retourne une fiche vide apres la creation.";
return;
}
ShowCreateModal = false;
SaveMessage = $"L'utilisateur {createdUser.Username} a bien ete cree.";
SaveError = null;
await LoadUsersAsync(createdUser.Subject, preserveFeedback: true);
}
catch (HttpRequestException)
{
CreateError = "Le service d'administration est temporairement indisponible.";
}
catch (TaskCanceledException)
{
CreateError = "La creation a pris trop de temps.";
}
finally
{
IsCreating = false;
}
}
private void PromptDeleteUser(AdminUserSummaryResponse user)
{
PendingDeleteUser = user;
DeleteError = null;
ShowDeleteModal = true;
}
private void CloseDeleteModal()
{
if (IsDeleting)
{
return;
}
ShowDeleteModal = false;
DeleteError = null;
PendingDeleteUser = null;
}
private async Task DeleteUserAsync()
{
if (IsDeleting || PendingDeleteUser is null)
{
return;
}
IsDeleting = true;
DeleteError = null;
try
{
var deletedUsername = PendingDeleteUser.Username;
var deletedSubject = PendingDeleteUser.Subject;
var response = await Http.DeleteAsync($"api/admin/users/{Uri.EscapeDataString(deletedSubject)}");
if (!response.IsSuccessStatusCode)
{
DeleteError = await ReadErrorAsync(response, "La suppression de l'utilisateur a echoue.");
return;
}
ShowDeleteModal = false;
PendingDeleteUser = null;
SaveError = null;
SaveMessage = $"L'utilisateur {deletedUsername} a bien ete supprime.";
var nextSubject = SelectedSubject == deletedSubject ? null : SelectedSubject;
await LoadUsersAsync(nextSubject, preserveFeedback: true);
}
catch (HttpRequestException)
{
DeleteError = "Le service d'administration est temporairement indisponible.";
}
catch (TaskCanceledException)
{
DeleteError = "La suppression a pris trop de temps.";
}
finally
{
IsDeleting = false;
}
}
private void UpdateSummary(AdminUserDetailResponse detail)
{
var summary = Users.FirstOrDefault(user => user.Subject == detail.Subject);
@@ -597,20 +950,20 @@
summary.SiteProfileUpdatedUtc = detail.SiteProfileUpdatedUtc;
}
private void FillForm(AdminUserDetailResponse user)
private void FillEditForm(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;
EditFormModel.Username = user.Username;
EditFormModel.Email = user.Email;
EditFormModel.FirstName = user.FirstName;
EditFormModel.LastName = user.LastName;
EditFormModel.IsEnabled = user.IsEnabled;
EditFormModel.IsEmailVerified = user.IsEmailVerified;
EditFormModel.DisplayName = user.DisplayName;
EditFormModel.Club = user.Club;
EditFormModel.City = user.City;
EditFormModel.PreferredFormat = user.PreferredFormat;
EditFormModel.FavoriteCube = user.FavoriteCube;
EditFormModel.Bio = user.Bio;
}
private void ResetAdminState()
@@ -620,7 +973,11 @@
Users.Clear();
SelectedSubject = null;
SelectedUser = null;
Form.Reset();
PendingDeleteUser = null;
ShowCreateModal = false;
ShowDeleteModal = false;
EditFormModel.Reset();
CreateFormModel.Reset();
}
private bool MatchesSearch(AdminUserSummaryResponse user)
@@ -642,17 +999,17 @@
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 string BuildTableRowClass(AdminUserSummaryResponse user)
=> string.Equals(SelectedSubject, user.Subject, StringComparison.Ordinal)
? "is-selected"
: string.Empty;
private static string BuildUserCardFootnote(AdminUserSummaryResponse user)
=> user.SiteProfileUpdatedUtc is not null
? $"Maj site {FormatDate(user.SiteProfileUpdatedUtc)}"
? FormatDate(user.SiteProfileUpdatedUtc)
: user.AccountCreatedUtc is not null
? $"Compte cree {FormatDate(user.AccountCreatedUtc)}"
: "Aucune date disponible";
? FormatDate(user.AccountCreatedUtc)
: "Non disponible";
private static async Task<string> ReadErrorAsync(HttpResponseMessage response, string fallbackMessage)
{
@@ -682,6 +1039,9 @@
? "Non disponible"
: value.Value.ToLocalTime().ToString("dd MMM yyyy 'a' HH:mm", CultureInfo.GetCultureInfo("fr-FR"));
private static string BoolString(bool value)
=> value ? "true" : "false";
public void Dispose()
=> AuthenticationStateProvider.AuthenticationStateChanged -= HandleAuthenticationStateChanged;
@@ -690,7 +1050,7 @@
public string? Message { get; set; }
}
private sealed class AdminUserFormModel
private class AdminUserFormModel
{
[Required(ErrorMessage = "Le nom d'utilisateur est obligatoire.")]
[MaxLength(120, ErrorMessage = "Le nom d'utilisateur doit rester sous 120 caracteres.")]
@@ -728,7 +1088,7 @@
[MaxLength(1200, ErrorMessage = "La bio doit rester sous 1200 caracteres.")]
public string? Bio { get; set; }
public void Reset()
public virtual void Reset()
{
Username = string.Empty;
Email = string.Empty;
@@ -744,4 +1104,22 @@
Bio = string.Empty;
}
}
private sealed class AdminCreateUserFormModel : AdminUserFormModel
{
[Required(ErrorMessage = "Le mot de passe est obligatoire.")]
[MinLength(8, ErrorMessage = "Le mot de passe doit contenir au moins 8 caracteres.")]
public string? Password { get; set; }
[Required(ErrorMessage = "La confirmation est obligatoire.")]
[Compare(nameof(Password), ErrorMessage = "Les mots de passe ne correspondent pas.")]
public string? ConfirmPassword { get; set; }
public override void Reset()
{
base.Reset();
Password = string.Empty;
ConfirmPassword = string.Empty;
}
}
}