@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 ChessCubing Arena | Administration

Administration

Piloter les utilisateurs du site depuis l'interface

Cette premiere zone d'administration centralise les comptes du site, l'etat du compte Keycloak et les donnees metier stockees en MySQL.

@if (!IsAuthenticated) {

Connexion requise

Connecte-toi avec un compte administrateur

Utilise le menu du site pour te connecter. La zone d'administration devient disponible des qu'un compte avec le role `admin` est actif.

} else if (!IsAdmin) {

Acces refuse

Ce compte n'a pas les droits d'administration

La gestion des utilisateurs est reservee aux comptes portant le role `admin`.

} else if (IsLoadingUsers) {

Chargement

Recuperation de la liste des utilisateurs

Le site rassemble les comptes Keycloak et les profils du site.

} else if (!string.IsNullOrWhiteSpace(LoadError)) {

Serveur

Impossible de charger l'administration

@LoadError

} else {

Utilisateurs

Parcourir les comptes

@if (FilteredUsers.Count == 0) {
Recherche

Aucun utilisateur ne correspond au filtre en cours.

} else {
@foreach (var user in FilteredUsers) { }
}
@if (IsLoadingDetail) {

Chargement

Recuperation de la fiche utilisateur

Les details du compte sont en cours de chargement.

} else if (!string.IsNullOrWhiteSpace(DetailError)) {

Serveur

Impossible de charger cette fiche

@DetailError

} else if (SelectedUser is null) {

Selection

Choisis un utilisateur

La fiche detaillee apparait ici des qu'un compte est selectionne.

} else {

Edition

@SelectedUser.DisplayName

Les roles restent geres dans Keycloak. Cette page couvre l'etat du compte et le profil du site.

Identite Keycloak @SelectedUser.IdentityDisplayName
Compte cree le @FormatDate(SelectedUser.AccountCreatedUtc)
Profil site cree le @FormatDate(SelectedUser.SiteProfileCreatedUtc)
Profil site mis a jour @FormatDate(SelectedUser.SiteProfileUpdatedUtc)
@if (!string.IsNullOrWhiteSpace(SaveError)) {

@SaveError

} @if (!string.IsNullOrWhiteSpace(SaveMessage)) {

@SaveMessage

}

Le profil site est cree automatiquement lors du premier enregistrement.

}
}
@code { private readonly AdminUserFormModel Form = new(); private readonly List 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 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 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>(); 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(); 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(); 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 ReadErrorAsync(HttpResponseMessage response, string fallbackMessage) { try { var error = await response.Content.ReadFromJsonAsync(); 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; } } }