From 6202b8b82997c5685da6c1fe4aec7801a86ba82a Mon Sep 17 00:00:00 2001 From: Christophe Date: Mon, 13 Apr 2026 22:33:56 +0200 Subject: [PATCH] Mise en place de l'authentification Keycloak --- .env.example | 5 ++ .gitignore | 1 + ChessCubing.App/App.razor | 25 ++++-- ChessCubing.App/ChessCubing.App.csproj | 1 + .../Components/RedirectToLogin.razor | 13 +++ .../Components/UserAccessBar.razor | 85 +++++++++++++++++++ ChessCubing.App/Pages/ApplicationPage.razor | 2 + ChessCubing.App/Pages/Authentication.razor | 13 +++ ChessCubing.App/Pages/ChronoPage.razor | 1 + ChessCubing.App/Pages/CubePage.razor | 1 + ChessCubing.App/Pages/Home.razor | 1 + ChessCubing.App/Pages/RulesPage.razor | 1 + ChessCubing.App/Program.cs | 44 ++++++++++ ChessCubing.App/Services/BrowserBridge.cs | 12 +-- .../Services/KeycloakAccountFactory.cs | 53 ++++++++++++ ChessCubing.App/Services/MatchStore.cs | 35 ++++++-- ChessCubing.App/Services/UserSession.cs | 39 +++++++++ ChessCubing.App/_Imports.razor | 3 + ChessCubing.App/wwwroot/appsettings.json | 13 +++ .../wwwroot/js/chesscubing-interop.js | 6 +- README.md | 39 ++++++++- docker-compose.yml | 46 ++++++++++ keycloak/realm/chesscubing-realm.json | 62 ++++++++++++++ nginx.conf | 10 +++ styles.css | 41 +++++++++ 25 files changed, 531 insertions(+), 21 deletions(-) create mode 100644 .env.example create mode 100644 ChessCubing.App/Components/RedirectToLogin.razor create mode 100644 ChessCubing.App/Components/UserAccessBar.razor create mode 100644 ChessCubing.App/Pages/Authentication.razor create mode 100644 ChessCubing.App/Services/KeycloakAccountFactory.cs create mode 100644 ChessCubing.App/Services/UserSession.cs create mode 100644 ChessCubing.App/wwwroot/appsettings.json create mode 100644 keycloak/realm/chesscubing-realm.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0193641 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +KEYCLOAK_DB_NAME=keycloak +KEYCLOAK_DB_USER=keycloak +KEYCLOAK_DB_PASSWORD=change-me +KEYCLOAK_ADMIN_USER=admin +KEYCLOAK_ADMIN_PASSWORD=change-me diff --git a/.gitignore b/.gitignore index aa605ff..d78b62a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .codex +.env WhatsApp Video 2026-04-11 at 20.38.50.mp4 ChessCubing.App/bin/ ChessCubing.App/obj/ diff --git a/ChessCubing.App/App.razor b/ChessCubing.App/App.razor index 983d151..5bdf683 100644 --- a/ChessCubing.App/App.razor +++ b/ChessCubing.App/App.razor @@ -1,5 +1,20 @@ - - - - - + + + + + +
+
+

Authentification

+

Verification de la session en cours...

+
+
+
+ + + +
+ +
+
+
diff --git a/ChessCubing.App/ChessCubing.App.csproj b/ChessCubing.App/ChessCubing.App.csproj index dbbd2f6..883d2b4 100644 --- a/ChessCubing.App/ChessCubing.App.csproj +++ b/ChessCubing.App/ChessCubing.App.csproj @@ -11,6 +11,7 @@ + diff --git a/ChessCubing.App/Components/RedirectToLogin.razor b/ChessCubing.App/Components/RedirectToLogin.razor new file mode 100644 index 0000000..4cb94b6 --- /dev/null +++ b/ChessCubing.App/Components/RedirectToLogin.razor @@ -0,0 +1,13 @@ +@inject NavigationManager Navigation + +@code { + protected override void OnInitialized() + { + var relativePath = Navigation.ToBaseRelativePath(Navigation.Uri); + var returnUrl = string.IsNullOrWhiteSpace(relativePath) + ? "/" + : relativePath.StartsWith("/", StringComparison.Ordinal) ? relativePath : $"/{relativePath}"; + + Navigation.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(returnUrl)}"); + } +} diff --git a/ChessCubing.App/Components/UserAccessBar.razor b/ChessCubing.App/Components/UserAccessBar.razor new file mode 100644 index 0000000..f75a4ec --- /dev/null +++ b/ChessCubing.App/Components/UserAccessBar.razor @@ -0,0 +1,85 @@ +@using System.Security.Claims +@inject NavigationManager Navigation + + + +
+
+ Compte Keycloak + @BuildDisplayName(authState.User) + @BuildMeta(authState.User) +
+ +
+
+ +
+
+ Compte Keycloak + Connexion requise pour lancer et reprendre les matchs + Chaque compte conserve son propre etat de match dans ce navigateur. +
+ +
+
+
+ +@code { + private string LoginHref => BuildAuthHref("login", CurrentReturnUrl); + private string LogoutHref => BuildAuthHref("logout", "/"); + + private string CurrentReturnUrl + { + get + { + var relativePath = Navigation.ToBaseRelativePath(Navigation.Uri); + if (string.IsNullOrWhiteSpace(relativePath)) + { + return "/"; + } + + return relativePath.StartsWith("/", StringComparison.Ordinal) + ? relativePath + : $"/{relativePath}"; + } + } + + private static string BuildAuthHref(string action, string returnUrl) + => $"authentication/{action}?returnUrl={Uri.EscapeDataString(returnUrl)}"; + + private static string BuildDisplayName(ClaimsPrincipal user) + => user.Identity?.Name + ?? user.FindFirst("name")?.Value + ?? user.FindFirst("preferred_username")?.Value + ?? "Utilisateur connecte"; + + private static string BuildMeta(ClaimsPrincipal user) + { + var details = new List(); + var email = user.FindFirst("email")?.Value; + if (!string.IsNullOrWhiteSpace(email)) + { + details.Add(email); + } + + var roles = user.FindAll("role") + .Select(claim => claim.Value) + .Where(value => !string.IsNullOrWhiteSpace(value)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(value => value, StringComparer.OrdinalIgnoreCase) + .ToArray(); + + if (roles.Length > 0) + { + details.Add($"Roles : {string.Join(", ", roles)}"); + } + + return details.Count > 0 + ? string.Join(" | ", details) + : "Session authentifiee via Keycloak."; + } +} diff --git a/ChessCubing.App/Pages/ApplicationPage.razor b/ChessCubing.App/Pages/ApplicationPage.razor index b8e469f..4b20a23 100644 --- a/ChessCubing.App/Pages/ApplicationPage.razor +++ b/ChessCubing.App/Pages/ApplicationPage.razor @@ -1,5 +1,6 @@ @page "/application" @page "/application.html" +@attribute [Authorize] @inject BrowserBridge Browser @inject MatchStore Store @inject NavigationManager Navigation @@ -23,6 +24,7 @@ Accueil du site Consulter le reglement +