@using System.ComponentModel.DataAnnotations @using System.Net @using System.Net.Http.Json @using System.Security.Claims @using ChessCubing.App.Models.Auth @using Microsoft.AspNetCore.Components.Authorization @implements IDisposable @inject AppAuthenticationStateProvider AuthenticationStateProvider @inject BrowserBridge Browser @inject HttpClient Http @inject NavigationManager Navigation @code { private static readonly string[] HomePaths = ["", "index.html"]; 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 readonly LoginFormModel LoginModel = new(); private readonly RegisterFormModel RegisterModel = new(); private bool IsAuthenticated; private bool ShowAuthModal; private bool IsSubmitting; private string? FormError; private string AuthModalTitle = "Se connecter"; private AuthMode Mode = AuthMode.Login; private string DisplayName = "Utilisateur connecte"; private string DisplayMeta = "Session active"; private bool _syncMenuAfterRender; private string CurrentPath { get { var absolutePath = new Uri(Navigation.Uri).AbsolutePath; return absolutePath.Trim('/'); } } protected override async Task OnInitializedAsync() { AuthenticationStateProvider.AuthenticationStateChanged += HandleAuthenticationStateChanged; await RefreshAuthenticationStateAsync(); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (!_syncMenuAfterRender) { return; } _syncMenuAfterRender = false; await Browser.SyncMenuAsync(); } private string BuildNavLinkClass(string[] paths) => IsCurrentPage(paths) ? "site-menu-link is-active" : "site-menu-link"; private string? BuildAriaCurrent(string[] paths) => IsCurrentPage(paths) ? "page" : null; private bool IsCurrentPage(string[] paths) => paths.Any(path => string.Equals(CurrentPath, path, StringComparison.OrdinalIgnoreCase)); private void OpenLoginModal() { ShowAuthModal = true; SwitchToLogin(); RequestMenuSync(); } private void OpenRegisterModal() { ShowAuthModal = true; SwitchToRegister(); RequestMenuSync(); } private void CloseAuthModal() { if (IsSubmitting) { return; } ShowAuthModal = false; FormError = null; RequestMenuSync(); } private void SwitchToLogin() { Mode = AuthMode.Login; AuthModalTitle = "Se connecter"; FormError = null; } private void SwitchToRegister() { Mode = AuthMode.Register; AuthModalTitle = "Creer un compte"; FormError = null; } private string BuildModeButtonClass(AuthMode mode) => mode == Mode ? "button secondary small" : "button ghost small"; private async Task SubmitLoginAsync() { await SubmitAsync( "api/auth/login", new LoginRequest { Username = LoginModel.Username.Trim(), Password = LoginModel.Password, }, "Connexion impossible."); } private async Task SubmitRegisterAsync() { await SubmitAsync( "api/auth/register", new RegisterRequest { Username = RegisterModel.Username.Trim(), Email = RegisterModel.Email.Trim(), Password = RegisterModel.Password, ConfirmPassword = RegisterModel.ConfirmPassword, FirstName = RegisterModel.FirstName?.Trim(), LastName = RegisterModel.LastName?.Trim(), }, "Creation du compte impossible."); } private async Task SubmitAsync(string endpoint, TRequest payload, string fallbackMessage) { if (IsSubmitting) { return; } IsSubmitting = true; FormError = null; try { if (!await EnsureAuthServiceReadyAsync()) { FormError = "Le service d'authentification demarre encore. Reessaie dans quelques secondes."; return; } var response = await Http.PostAsJsonAsync(endpoint, payload); if (!response.IsSuccessStatusCode) { FormError = await ReadErrorAsync(response, fallbackMessage); return; } var session = await response.Content.ReadFromJsonAsync(); if (session is not null && session.IsAuthenticated) { AuthenticationStateProvider.SetAuthenticated(session); } else { await AuthenticationStateProvider.RefreshAsync(); } ShowAuthModal = false; FormError = null; ResetForms(); await RefreshAuthenticationStateAsync(); RequestMenuSync(); } catch (HttpRequestException) { FormError = "Le service d'authentification est temporairement indisponible. Reessaie dans quelques secondes."; } catch (TaskCanceledException) { FormError = "La reponse du service d'authentification a pris trop de temps. Reessaie dans quelques secondes."; } catch { FormError = fallbackMessage; } finally { IsSubmitting = false; } } private async Task LogoutAsync() { if (IsSubmitting) { return; } IsSubmitting = true; FormError = null; try { await Http.PostAsync("api/auth/logout", null); AuthenticationStateProvider.SetAnonymous(); await RefreshAuthenticationStateAsync(); } finally { IsSubmitting = false; } } private void HandleAuthenticationStateChanged(Task authenticationStateTask) => _ = InvokeAsync(RefreshAuthenticationStateAsync); private void RequestMenuSync() => _syncMenuAfterRender = true; private async Task RefreshAuthenticationStateAsync() { try { var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); var user = authState.User; if (user.Identity?.IsAuthenticated == true) { IsAuthenticated = true; DisplayName = BuildDisplayName(user); DisplayMeta = BuildMeta(user); } else { ResetAuthenticationDisplay(); } } catch { ResetAuthenticationDisplay(); } StateHasChanged(); } private void ResetAuthenticationDisplay() { IsAuthenticated = false; DisplayName = "Utilisateur connecte"; DisplayMeta = "Session active"; } private void ResetForms() { LoginModel.Username = string.Empty; LoginModel.Password = string.Empty; RegisterModel.FirstName = string.Empty; RegisterModel.LastName = string.Empty; RegisterModel.Username = string.Empty; RegisterModel.Email = string.Empty; RegisterModel.Password = string.Empty; RegisterModel.ConfirmPassword = string.Empty; } private static string BuildDisplayName(ClaimsPrincipal user) => user.FindFirst("name")?.Value ?? user.FindFirst("preferred_username")?.Value ?? user.Identity?.Name ?? "Utilisateur connecte"; private static string BuildMeta(ClaimsPrincipal user) => user.FindFirst("email")?.Value ?? "Session active"; 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.BadGateway or HttpStatusCode.ServiceUnavailable or HttpStatusCode.GatewayTimeout => "Le service d'authentification demarre encore. Reessaie dans quelques secondes.", HttpStatusCode.Conflict => "Ce nom d'utilisateur ou cet email existe deja.", _ => fallbackMessage, }; } private async Task EnsureAuthServiceReadyAsync() { try { using var response = await Http.GetAsync("api/health"); return response.IsSuccessStatusCode; } catch { return false; } } private static string BoolString(bool value) => value ? "true" : "false"; public void Dispose() => AuthenticationStateProvider.AuthenticationStateChanged -= HandleAuthenticationStateChanged; private enum AuthMode { Login, Register, } private sealed class ApiErrorMessage { public string? Message { get; set; } } private sealed class LoginFormModel { [Required(ErrorMessage = "L'identifiant est obligatoire.")] public string Username { get; set; } = string.Empty; [Required(ErrorMessage = "Le mot de passe est obligatoire.")] public string Password { get; set; } = string.Empty; } private sealed class RegisterFormModel { public string? FirstName { get; set; } public string? LastName { get; set; } [Required(ErrorMessage = "Le nom d'utilisateur est obligatoire.")] public string Username { get; set; } = string.Empty; [Required(ErrorMessage = "L'email est obligatoire.")] [EmailAddress(ErrorMessage = "L'email n'est pas valide.")] public string Email { get; set; } = string.Empty; [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; } = string.Empty; [Required(ErrorMessage = "La confirmation est obligatoire.")] [Compare(nameof(Password), ErrorMessage = "Les mots de passe ne correspondent pas.")] public string ConfirmPassword { get; set; } = string.Empty; } }