@@ -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
- {
-
-
@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