487 lines
17 KiB
Plaintext
487 lines
17 KiB
Plaintext
@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 HttpClient Http
|
|
@inject NavigationManager Navigation
|
|
|
|
<div class="site-menu-shell">
|
|
<header class="site-menu-bar">
|
|
<div class="site-menu-main">
|
|
<a class="site-menu-brand" href="index.html" aria-label="Accueil ChessCubing">
|
|
<img class="site-menu-brand-icon" src="logo.png" alt="Icone ChessCubing" />
|
|
<span class="site-menu-brand-copy">
|
|
<span class="micro-label">ChessCubing Arena</span>
|
|
<strong>Menu general</strong>
|
|
</span>
|
|
</a>
|
|
|
|
<nav class="site-menu-links" aria-label="Navigation principale">
|
|
<a class="@BuildNavLinkClass(HomePaths)" href="index.html" aria-current="@BuildAriaCurrent(HomePaths)">Accueil</a>
|
|
<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>
|
|
</nav>
|
|
|
|
<div class="site-menu-account">
|
|
<span class="micro-label">Compte joueur</span>
|
|
@if (IsAuthenticated)
|
|
{
|
|
<div class="site-menu-account-panel">
|
|
<div class="site-menu-user">
|
|
<strong>@DisplayName</strong>
|
|
<span>@DisplayMeta</span>
|
|
</div>
|
|
<button class="button ghost small" type="button" @onclick="LogoutAsync" disabled="@IsSubmitting">Se deconnecter</button>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="site-menu-account-actions">
|
|
<button class="button secondary small" type="button" @onclick="OpenLoginModal">Se connecter</button>
|
|
<button class="button ghost small" type="button" @onclick="OpenRegisterModal">Creer un compte</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
</div>
|
|
|
|
<section class="modal @(ShowAuthModal ? string.Empty : "hidden")" aria-hidden="@BoolString(!ShowAuthModal)">
|
|
<div class="modal-backdrop" @onclick="CloseAuthModal"></div>
|
|
<div class="modal-card auth-modal-card">
|
|
<div class="modal-head">
|
|
<div>
|
|
<p class="eyebrow">Compte joueur</p>
|
|
<h2>@AuthModalTitle</h2>
|
|
</div>
|
|
<button class="button ghost small" type="button" @onclick="CloseAuthModal" disabled="@IsSubmitting">Fermer</button>
|
|
</div>
|
|
|
|
<p class="auth-modal-copy">
|
|
L'authentification se fait maintenant directement dans l'application, sans redirection vers une page externe.
|
|
</p>
|
|
|
|
<div class="auth-modal-switch">
|
|
<button class="@BuildModeButtonClass(AuthMode.Login)" type="button" @onclick="SwitchToLogin" disabled="@IsSubmitting">Se connecter</button>
|
|
<button class="@BuildModeButtonClass(AuthMode.Register)" type="button" @onclick="SwitchToRegister" disabled="@IsSubmitting">Creer un compte</button>
|
|
</div>
|
|
|
|
@if (!string.IsNullOrWhiteSpace(FormError))
|
|
{
|
|
<p class="auth-form-error">@FormError</p>
|
|
}
|
|
|
|
@if (Mode == AuthMode.Login)
|
|
{
|
|
<EditForm Model="@LoginModel" OnValidSubmit="SubmitLoginAsync">
|
|
<DataAnnotationsValidator />
|
|
<div class="auth-form-grid">
|
|
<label class="field">
|
|
<span>Identifiant ou email</span>
|
|
<InputText @bind-Value="LoginModel.Username" autocomplete="username email" />
|
|
<ValidationMessage For="@(() => LoginModel.Username)" />
|
|
</label>
|
|
|
|
<label class="field">
|
|
<span>Mot de passe</span>
|
|
<InputText @bind-Value="LoginModel.Password" type="password" autocomplete="current-password" />
|
|
<ValidationMessage For="@(() => LoginModel.Password)" />
|
|
</label>
|
|
</div>
|
|
|
|
<div class="modal-actions">
|
|
<button class="button secondary" type="submit" disabled="@IsSubmitting">
|
|
@(IsSubmitting ? "Connexion..." : "Se connecter")
|
|
</button>
|
|
</div>
|
|
</EditForm>
|
|
}
|
|
else
|
|
{
|
|
<EditForm Model="@RegisterModel" OnValidSubmit="SubmitRegisterAsync">
|
|
<DataAnnotationsValidator />
|
|
<div class="auth-form-grid two-columns">
|
|
<label class="field">
|
|
<span>Prenom</span>
|
|
<InputText @bind-Value="RegisterModel.FirstName" autocomplete="given-name" />
|
|
</label>
|
|
|
|
<label class="field">
|
|
<span>Nom</span>
|
|
<InputText @bind-Value="RegisterModel.LastName" autocomplete="family-name" />
|
|
</label>
|
|
|
|
<label class="field">
|
|
<span>Nom d'utilisateur</span>
|
|
<InputText @bind-Value="RegisterModel.Username" autocomplete="username" />
|
|
<ValidationMessage For="@(() => RegisterModel.Username)" />
|
|
</label>
|
|
|
|
<label class="field">
|
|
<span>Email</span>
|
|
<InputText @bind-Value="RegisterModel.Email" autocomplete="email" />
|
|
<ValidationMessage For="@(() => RegisterModel.Email)" />
|
|
</label>
|
|
|
|
<label class="field">
|
|
<span>Mot de passe</span>
|
|
<InputText @bind-Value="RegisterModel.Password" type="password" autocomplete="new-password" />
|
|
<ValidationMessage For="@(() => RegisterModel.Password)" />
|
|
</label>
|
|
|
|
<label class="field">
|
|
<span>Confirmation du mot de passe</span>
|
|
<InputText @bind-Value="RegisterModel.ConfirmPassword" type="password" autocomplete="new-password" />
|
|
<ValidationMessage For="@(() => RegisterModel.ConfirmPassword)" />
|
|
</label>
|
|
</div>
|
|
|
|
<div class="modal-actions">
|
|
<button class="button secondary" type="submit" disabled="@IsSubmitting">
|
|
@(IsSubmitting ? "Creation..." : "Creer mon compte")
|
|
</button>
|
|
</div>
|
|
</EditForm>
|
|
}
|
|
</div>
|
|
</section>
|
|
|
|
@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 string CurrentPath
|
|
{
|
|
get
|
|
{
|
|
var absolutePath = new Uri(Navigation.Uri).AbsolutePath;
|
|
return absolutePath.Trim('/');
|
|
}
|
|
}
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
AuthenticationStateProvider.AuthenticationStateChanged += HandleAuthenticationStateChanged;
|
|
await RefreshAuthenticationStateAsync();
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
private void OpenRegisterModal()
|
|
{
|
|
ShowAuthModal = true;
|
|
SwitchToRegister();
|
|
}
|
|
|
|
private void CloseAuthModal()
|
|
{
|
|
if (IsSubmitting)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ShowAuthModal = false;
|
|
FormError = null;
|
|
}
|
|
|
|
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<TRequest>(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<AuthSessionResponse>();
|
|
if (session is not null && session.IsAuthenticated)
|
|
{
|
|
AuthenticationStateProvider.SetAuthenticated(session);
|
|
}
|
|
else
|
|
{
|
|
await AuthenticationStateProvider.RefreshAsync();
|
|
}
|
|
|
|
ShowAuthModal = false;
|
|
FormError = null;
|
|
ResetForms();
|
|
await RefreshAuthenticationStateAsync();
|
|
}
|
|
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<AuthenticationState> authenticationStateTask)
|
|
=> _ = InvokeAsync(RefreshAuthenticationStateAsync);
|
|
|
|
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<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.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<bool> 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;
|
|
}
|
|
}
|