diff --git a/ShopAdmin/Components/Layout/MainLayout.razor b/ShopAdmin/Components/Layout/MainLayout.razor index 80c2494..765a2ad 100644 --- a/ShopAdmin/Components/Layout/MainLayout.razor +++ b/ShopAdmin/Components/Layout/MainLayout.razor @@ -17,7 +17,7 @@ - + diff --git a/ShopAdmin/Components/Pages/Login.razor b/ShopAdmin/Components/Pages/Login.razor index 3673380..deec9c9 100644 --- a/ShopAdmin/Components/Pages/Login.razor +++ b/ShopAdmin/Components/Pages/Login.razor @@ -1,5 +1,7 @@ @page "/login" @using Microsoft.AspNetCore.Components.Authorization +@inject NavigationManager Navigation +@rendermode InteractiveServer
@@ -67,8 +69,16 @@ [CascadingParameter] private Task? AuthState { get; set; } - private void HandleLogin() + protected override async Task OnInitializedAsync() { - // Wire up your OAuth / OpenID Connect Redirect or Auth trigger state here + if (AuthState is not null) + { + var state = await AuthState; + + if (state.User.Identity?.IsAuthenticated ?? false) + Navigation.NavigateTo("/", replace: true); + } } + + private void HandleLogin() => Navigation.NavigateTo("/auth/login", forceLoad: true); } \ No newline at end of file diff --git a/ShopAdmin/Components/Pages/NavShelf.razor b/ShopAdmin/Components/Pages/NavShelf.razor index 490589d..b97e686 100644 --- a/ShopAdmin/Components/Pages/NavShelf.razor +++ b/ShopAdmin/Components/Pages/NavShelf.razor @@ -60,22 +60,24 @@ Notifications -
+
- - - Settings - - - - - Policies - - - - + + + + + Profile + + + + + + + + Logout + @@ -83,6 +85,24 @@ [Parameter] public bool IsOpen { get; set; } = false; [Parameter] public EventCallback IsOpenChanged { get; set; } + [Inject] private IConfiguration? Configuration { get; set; } + + private string? ProfileUrl { get; set; } + + protected override void OnInitialized() + { + if (Configuration is null) return; + + var authority = Configuration["IdKongisa:Authority"]; + + if (!string.IsNullOrWhiteSpace(authority)) + { + var uri = new Uri(authority); + + ProfileUrl = $"{uri.Scheme}://{uri.Host}/if/user/#/settings"; + } + } + private async Task ToggleShelf() { IsOpen = !IsOpen; diff --git a/ShopAdmin/Components/Pages/NavShelf.razor.css b/ShopAdmin/Components/Pages/NavShelf.razor.css index d1f6e8b..fc4727d 100644 --- a/ShopAdmin/Components/Pages/NavShelf.razor.css +++ b/ShopAdmin/Components/Pages/NavShelf.razor.css @@ -1,8 +1,8 @@ /* --- 1. The Flyout Side Panel --- */ .nav-shelf-panel { position: fixed; - top: var(--header-height); /* Drops clean below your top-bar */ - right: -300px; /* Hidden by default */ + top: var(--header-height); + right: -300px; width: 300px; height: calc(100vh - var(--header-height)); background-color: var(--shelf-bg); @@ -10,7 +10,8 @@ z-index: 2000; display: flex; flex-direction: column; - padding: 2rem 1.5rem; + /* MODIFIED: Reduce padding-bottom so items don't clip raw against the bottom line window frame */ + padding: 2rem 1.5rem 1rem 1.5rem; box-sizing: border-box; transition: right 0.35s cubic-bezier(0.16, 1, 0.3, 1); box-shadow: -15px 0 40px rgba(0, 0, 0, 0.6); @@ -68,6 +69,7 @@ margin-bottom: 2rem; padding-bottom: 1rem; border-bottom: 1px solid rgba(144, 224, 239, 0.1); + flex-shrink: 0; /* CRITICAL: Prevents the header text from squishing up on small screens */ } .shelf-header h3 { @@ -92,6 +94,14 @@ display: flex; flex-direction: column; gap: 8px; + /* NEW RULES: Absorb all available viewport height and isolate scrolling bounds */ + flex: 1; + overflow-y: auto; + overflow-x: hidden; + /* Optional: Adds smooth mobile elastic touch responses on iOS devices */ + -webkit-overflow-scrolling: touch; + /* Clean up spacing so bottom-most links have breathing room when fully scrolled down */ + padding-bottom: 2rem; } /* Deep selection matching Blazor's active class matching system */ diff --git a/ShopAdmin/Components/TopBarAuthstateView.razor b/ShopAdmin/Components/TopBarAuthstateView.razor index e50b75c..47aaadc 100644 --- a/ShopAdmin/Components/TopBarAuthstateView.razor +++ b/ShopAdmin/Components/TopBarAuthstateView.razor @@ -1,37 +1,67 @@ -
- @if (!IsAuthenticated) - { -
- - - - - - - - - - - - - - - - - -
- } - else - { -
-
- ADMIN_OPERATOR - ID: 409-CLUSTER +@using Microsoft.AspNetCore.Components.Authorization +
+ + +
+
+ @Name + @Email + @LoginTime +
-
- } + + +
+ + + + + + + + + + + + + + + + + +
+
+
@code { - [Parameter] public bool IsAuthenticated { get; set; } = false; + [CascadingParameter] + private Task? AuthStateTask { get; set; } + + private System.Security.Claims.ClaimsPrincipal? UserPrincipal; + + private string? Name { get; set; } + private string? Email { get; set; } + private DateTime? LoginTime { get; set; } + + protected override async Task OnInitializedAsync() + { + if (AuthStateTask != null) + { + var authState = await AuthStateTask; + + UserPrincipal = authState.User; + + Name = UserPrincipal?.Identity?.Name; + Email = UserPrincipal?.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value; + + var authTimeClaim = UserPrincipal?.FindFirst("auth_time")?.Value; + + if (!string.IsNullOrEmpty(authTimeClaim) && long.TryParse(authTimeClaim, out long unixSeconds)) + { + var dateTimeOffset = DateTimeOffset.FromUnixTimeSeconds(unixSeconds); + LoginTime = dateTimeOffset.LocalDateTime; + } + } + } } \ No newline at end of file diff --git a/ShopAdmin/Program.cs b/ShopAdmin/Program.cs index 821838b..34ce093 100644 --- a/ShopAdmin/Program.cs +++ b/ShopAdmin/Program.cs @@ -1,5 +1,6 @@ using LiteCharms.Features.Extensions; using LiteCharms.Features.Mediator; +using Microsoft.AspNetCore.Authentication; using ShopAdmin.Components; using static LiteCharms.Features.Email.Extensions.Constants; @@ -8,6 +9,8 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); +builder.Services.AddCascadingAuthenticationState(); + builder.AddMonitoring(); builder.Services.AddControllers(); @@ -33,6 +36,52 @@ builder.Services.AddPostgresHealtchCheck(); builder.Services.AddQuartzHealtchCheck(); builder.Services.AddHealthChecksSupport(builder.Configuration); +builder.Services.AddAuthentication(options => +{ + options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; +}) +.AddCookie() +.AddOpenIdConnect(options => +{ + options.Authority = builder.Configuration.GetSection("IdKongisa:Authority").Value; + options.ClientId = builder.Configuration.GetSection("IdKongisa:ClientId").Value; + options.ClientSecret = builder.Configuration.GetSection("IdKongisa:ClientSecret").Value; + + options.ResponseType = "code"; + options.SaveTokens = true; + + options.GetClaimsFromUserInfoEndpoint = true; + + options.MetadataAddress = $"{options.Authority}/.well-known/openid-configuration"; + + options.Scope.Clear(); + options.Scope.Add("openid"); + options.Scope.Add("profile"); + options.Scope.Add("email"); + + options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters + { + NameClaimType = "name", + RoleClaimType = "groups" + }; + + options.Events = new OpenIdConnectEvents + { + OnRedirectToIdentityProviderForSignOut = async callbackContext => + { + var request = callbackContext.Request; + string currentBaseUrl = $"{request.Scheme}://{request.Host}{request.PathBase}/"; + + callbackContext.ProtocolMessage.PostLogoutRedirectUri = currentBaseUrl; + + var idToken = await callbackContext.HttpContext.GetTokenAsync("id_token"); + + if (!string.IsNullOrEmpty(idToken)) callbackContext.ProtocolMessage.IdTokenHint = idToken; + } + }; +}); + var app = builder.Build(); var schedulerFactory = app.Services.GetRequiredService(); @@ -53,11 +102,29 @@ app.UseHealthChecks("/health", new HealthCheckOptions }); app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true); -app.UseHttpsRedirection(); +app.UseHttpsRedirection(); app.UseAntiforgery(); +app.UseAuthentication(); +app.UseAuthorization(); + app.MapStaticAssets(); + +app.MapGet("/auth/login", (string redirectUri = "/") => + Results.Challenge(new AuthenticationProperties { RedirectUri = redirectUri }, [OpenIdConnectDefaults.AuthenticationScheme])); +app.MapGet("/auth/logout", async (HttpContext context) => +{ + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + + string currentBaseUrl = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.PathBase}/"; + + await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties + { + RedirectUri = currentBaseUrl + }); +}); + app.MapRazorComponents() .AddInteractiveServerRenderMode(); diff --git a/ShopAdmin/ShopAdmin.csproj b/ShopAdmin/ShopAdmin.csproj index 0eb2409..309dd4e 100644 --- a/ShopAdmin/ShopAdmin.csproj +++ b/ShopAdmin/ShopAdmin.csproj @@ -20,6 +20,15 @@ + + + + + + + + + diff --git a/ShopAdmin/appsettings.json b/ShopAdmin/appsettings.json index 417425d..7eb3aaa 100644 --- a/ShopAdmin/appsettings.json +++ b/ShopAdmin/appsettings.json @@ -1,4 +1,7 @@ { + "IdKongisa": { + "Authority": "https://id.khongisa.co.za/application/o/litecharms-shopadmin" + }, "Email": { "Credentials": { "Username": "shop@litecharms.co.za" diff --git a/ShopAdmin/wwwroot/app.css b/ShopAdmin/wwwroot/app.css index 5944cd6..c7838b2 100644 --- a/ShopAdmin/wwwroot/app.css +++ b/ShopAdmin/wwwroot/app.css @@ -42,7 +42,7 @@ html, body { display: flex; align-items: center; justify-content: space-between; - padding: 0 4rem 0 1rem; + padding: 0 1.5rem; /* Balanced horizontal spacing for both sides */ z-index: 1000; border-bottom: 1px solid rgba(144, 224, 239, 0.15); } @@ -119,7 +119,7 @@ main { /* --- Responsive Logic --- */ @media (max-width: 1200px) { .top-bar { - padding: 0 2rem; + padding: 0 1rem; } } diff --git a/litecharms-shopadmin-uat.yml b/litecharms-shopadmin-uat.yml index e1392cc..7586be7 100644 --- a/litecharms-shopadmin-uat.yml +++ b/litecharms-shopadmin-uat.yml @@ -14,6 +14,7 @@ data: ASPNETCORE_URLS: "http://0.0.0.0:8080" Monitoring__Address: "http://aspire-dashboard-service.aspire.svc.cluster.local:18889" Monitoring__ServiceName: "LiteCharms.ShopAdmin.Uat" + IdKongisa__Authority: "https://id.khongisa.co.za/application/o/litecharms-shopadmin" --- apiVersion: v1 kind: Secret @@ -25,6 +26,8 @@ data: connection-string: SG9zdD0xOTIuMTY4LjEuMTcwO0RhdGFiYXNlPXNob3AtZGV2O1VzZXJuYW1lPXNob3AtZGV2LXVzZXI7UGFzc3dvcmQ9a1ZWbW9XS0ozeHpnUVg7UGVyc2lzdCBTZWN1cml0eSBJbmZvPVRydWU= connection-string-quartz: SG9zdD0xOTIuMTY4LjEuMTcwO0RhdGFiYXNlPXNjaGVkdWxlci1kZXY7VXNlcm5hbWU9c2NoZWR1bGVyLWRldi11c2VyO1Bhc3N3b3JkPWtWVm1vV0tKM3h6Z1FYO1BlcnNpc3QgU2VjdXJpdHkgSW5mbz1UcnVl aspire-apikey: bWMzRzYzSzJqNVpPRXNpMEFqTW9qTFRYbTFLRVpGY3R6SUlqU3dEaVRHdXQ4cUdTa1B1V3d4R1AxUmJzY0pVbw== + auth-clientid: NUxldE5hSERsUlhOWXo1N3FkMVV1RWN4R01uUDNmT3FXc0RHcWdjUg== + auth-clientsecret: a3ZxN3k1anc0M0g4WDRjQW91eGRqRDhNNXdxVUhUQ2I4UjNSVjNVWjI4TUZjTk51NWxhM3g3V1ZZRzQ2QnJFMjVPMnhXRmhoeEk0VXNSaFlMTHRqSGRhWWVrUTBmdHpIQ3ZNczV5TXdRdERpcDBkM3QzTkNDa0RtN1JXeW1XSTg= --- apiVersion: v1 kind: PersistentVolumeClaim @@ -102,6 +105,16 @@ spec: secretKeyRef: name: shopadmin-secrets key: aspire-apikey + - name: IdKongisa__ClientId + valueFrom: + secretKeyRef: + name: shopadmin-secrets + key: auth-clientid + - name: IdKongisa__ClientSecret + valueFrom: + secretKeyRef: + name: shopadmin-secrets + key: auth-clientsecret volumeMounts: - name: data mountPath: /app/wwwroot/content