From 740074c49e8d01b5d47aa7e353d57d38c132a604 Mon Sep 17 00:00:00 2001 From: Christophe Date: Mon, 13 Apr 2026 23:09:17 +0200 Subject: [PATCH] Ouverture de l'authentification dans une modal --- ChessCubing.App/Components/SiteMenu.razor | 103 +++++++++++++++++- ChessCubing.App/Layout/MainLayout.razor | 3 +- ChessCubing.App/Pages/Authentication.razor | 24 ++++ .../wwwroot/js/chesscubing-interop.js | 47 ++++++++ README.md | 2 +- styles.css | 36 ++++++ 6 files changed, 209 insertions(+), 6 deletions(-) diff --git a/ChessCubing.App/Components/SiteMenu.razor b/ChessCubing.App/Components/SiteMenu.razor index 90dc54b..d7141be 100644 --- a/ChessCubing.App/Components/SiteMenu.razor +++ b/ChessCubing.App/Components/SiteMenu.razor @@ -1,5 +1,7 @@ @using System.Security.Claims +@implements IAsyncDisposable @inject NavigationManager Navigation +@inject IJSRuntime JS + + @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 DotNetObjectReference? _dotNetReference; + private bool _listenerRegistered; + private string? AuthModalSource; + private string AuthModalTitle = "Authentification"; + private bool ShowAuthModal; + private string LoginHref => BuildAuthHref("login", EffectiveReturnUrl); private string RegisterHref => BuildAuthHref("register", EffectiveReturnUrl); private string LogoutHref => BuildAuthHref("logout", "/"); @@ -109,4 +139,69 @@ private static string BuildMeta(ClaimsPrincipal user) => user.FindFirst("email")?.Value ?? "Session active"; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + { + return; + } + + _dotNetReference = DotNetObjectReference.Create(this); + await JS.InvokeVoidAsync("chesscubingAuthModal.registerListener", _dotNetReference); + _listenerRegistered = true; + } + + private void OpenLoginModal() + => OpenAuthModal("Se connecter", LoginHref); + + private void OpenRegisterModal() + => OpenAuthModal("Creer un compte", RegisterHref); + + private void OpenAuthModal(string title, string source) + { + AuthModalTitle = title; + AuthModalSource = source; + ShowAuthModal = true; + } + + private void CloseAuthModal() + { + ShowAuthModal = false; + AuthModalSource = null; + } + + [JSInvokable] + public Task HandleAuthModalMessage(string status) + { + if (!string.Equals(status, "login-succeeded", StringComparison.OrdinalIgnoreCase) + && !string.Equals(status, "logout-succeeded", StringComparison.OrdinalIgnoreCase)) + { + return Task.CompletedTask; + } + + CloseAuthModal(); + StateHasChanged(); + Navigation.NavigateTo(Navigation.Uri, forceLoad: true); + return Task.CompletedTask; + } + + public async ValueTask DisposeAsync() + { + if (_listenerRegistered) + { + try + { + await JS.InvokeVoidAsync("chesscubingAuthModal.unregisterListener"); + } + catch + { + } + } + + _dotNetReference?.Dispose(); + } + + private static string BoolString(bool value) + => value ? "true" : "false"; } diff --git a/ChessCubing.App/Layout/MainLayout.razor b/ChessCubing.App/Layout/MainLayout.razor index 200d7c3..03d437f 100644 --- a/ChessCubing.App/Layout/MainLayout.razor +++ b/ChessCubing.App/Layout/MainLayout.razor @@ -18,7 +18,8 @@ return string.Equals(currentPath, "chrono", StringComparison.OrdinalIgnoreCase) || string.Equals(currentPath, "chrono.html", StringComparison.OrdinalIgnoreCase) || string.Equals(currentPath, "cube", StringComparison.OrdinalIgnoreCase) - || string.Equals(currentPath, "cube.html", StringComparison.OrdinalIgnoreCase); + || string.Equals(currentPath, "cube.html", StringComparison.OrdinalIgnoreCase) + || currentPath.StartsWith("authentication/", StringComparison.OrdinalIgnoreCase); } } } diff --git a/ChessCubing.App/Pages/Authentication.razor b/ChessCubing.App/Pages/Authentication.razor index 978ce4a..1422a05 100644 --- a/ChessCubing.App/Pages/Authentication.razor +++ b/ChessCubing.App/Pages/Authentication.razor @@ -1,5 +1,6 @@ @page "/authentication/{action}" @inject NavigationManager Navigation +@inject IJSRuntime JS
@@ -45,6 +46,29 @@ Navigation.NavigateToLogin("authentication/login", request); } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + { + return; + } + + var status = Action switch + { + RemoteAuthenticationActions.LogInCallback => "login-succeeded", + RemoteAuthenticationActions.LogOutCallback => "logout-succeeded", + _ => null, + }; + + if (status is null) + { + return; + } + + await Task.Delay(700); + await JS.InvokeVoidAsync("chesscubingAuthModal.notifyParent", status); + } + private static string NormalizeReturnUrl(string? returnUrl) { if (string.IsNullOrWhiteSpace(returnUrl)) diff --git a/ChessCubing.App/wwwroot/js/chesscubing-interop.js b/ChessCubing.App/wwwroot/js/chesscubing-interop.js index bb0abb6..0fceb13 100644 --- a/ChessCubing.App/wwwroot/js/chesscubing-interop.js +++ b/ChessCubing.App/wwwroot/js/chesscubing-interop.js @@ -2,6 +2,7 @@ const assetTokenStorageKey = "chesscubing-arena-asset-token"; let viewportStarted = false; let audioContext = null; + let authModalMessageHandler = null; function syncViewportHeight() { const visibleHeight = window.visualViewport?.height ?? window.innerHeight; @@ -198,5 +199,51 @@ }, }; + window.chesscubingAuthModal = { + registerListener(dotNetReference) { + if (authModalMessageHandler) { + window.removeEventListener("message", authModalMessageHandler); + } + + authModalMessageHandler = (event) => { + if (event.origin !== window.location.origin) { + return; + } + + const data = event.data; + if (!data || data.source !== "chesscubing-auth-modal" || typeof data.status !== "string") { + return; + } + + dotNetReference.invokeMethodAsync("HandleAuthModalMessage", data.status).catch(() => undefined); + }; + + window.addEventListener("message", authModalMessageHandler); + }, + + unregisterListener() { + if (!authModalMessageHandler) { + return; + } + + window.removeEventListener("message", authModalMessageHandler); + authModalMessageHandler = null; + }, + + notifyParent(status) { + if (!window.parent || window.parent === window) { + return; + } + + window.parent.postMessage( + { + source: "chesscubing-auth-modal", + status, + }, + window.location.origin, + ); + }, + }; + startViewport(); })(); diff --git a/README.md b/README.md index 9ef7c99..d6d84a8 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Le projet continue a exposer les routes historiques `index.html`, `application.h L'application embarque maintenant une authentification OpenID Connect basee sur Keycloak. - toutes les pages du site restent accessibles sans connexion -- un menu general en haut des pages site et application regroupe la navigation et les actions `Se connecter` / `Creer un compte` +- un menu general en haut des pages site et application regroupe la navigation et les actions `Se connecter` / `Creer un compte` dans une modal integree - l'action `Creer un compte` ouvre le parcours d'inscription natif de Keycloak - les roles Keycloak du realm sont exposes dans l'application - l'etat du match est isole par utilisateur dans le navigateur grace a une cle de stockage derivee du compte connecte diff --git a/styles.css b/styles.css index 5ac3534..7bf5d39 100644 --- a/styles.css +++ b/styles.css @@ -1152,6 +1152,34 @@ body[data-page="cube"] .zone-button.cube-hold-ready::after { font-size: 0.9rem; } +.auth-modal-card { + width: min(980px, 100%); + padding: 1.1rem; + border-radius: 28px; + border: 1px solid var(--panel-border); + background: rgba(14, 16, 21, 0.96); +} + +.auth-modal-copy { + margin-bottom: 1rem; + color: var(--muted); +} + +.auth-modal-frame-shell { + overflow: hidden; + border-radius: 22px; + border: 1px solid var(--panel-border); + background: rgba(10, 12, 17, 0.85); +} + +.auth-modal-frame { + display: block; + width: 100%; + height: min(78dvh, 760px); + border: 0; + background: #141414; +} + .hero-rules h1 { margin: 0; font-size: clamp(2.25rem, 4.8vw, 3.8rem); @@ -1493,6 +1521,14 @@ body[data-page="cube"] .zone-button.cube-hold-ready::after { text-align: left; } + .auth-modal-card { + padding: 1rem; + } + + .auth-modal-frame { + height: min(70dvh, 680px); + } + .setup-form { grid-template-columns: 1fr; }