Skip to content

Commit 43ea983

Browse files
Web App signin, signout and cateries extracted from token to generate menu
1 parent afeefb7 commit 43ea983

File tree

8 files changed

+247
-44
lines changed

8 files changed

+247
-44
lines changed

ApiIntegrationMvc/ApiIntegrationMvc.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212

1313
<ItemGroup>
1414
<Folder Include="Areas\Home\Models\" />
15+
<Folder Include="Controllers\" />
16+
</ItemGroup>
17+
18+
<ItemGroup>
19+
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" />
1520
</ItemGroup>
1621

1722
</Project>

ApiIntegrationMvc/Areas/Account/Controllers/LoginController.cs

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
using ApiIntegrationMvc.Areas.Account.Models;
2+
using Microsoft.AspNetCore.Authentication;
3+
using Microsoft.AspNetCore.Authentication.Cookies;
24
using Microsoft.AspNetCore.Mvc;
5+
using System.Security.Claims;
36
using System.Text.Json;
47
using UserManagement.Contracts.Auth;
58
using UserManagement.Sdk.Abstractions;
@@ -11,7 +14,8 @@ public class LoginController : Controller
1114
{
1215
private readonly IUserManagementClient _users;
1316
private readonly IAccessTokenProvider _cache;
14-
public LoginController(IUserManagementClient users, IAccessTokenProvider cache) => (_users, _cache) = (users, cache);
17+
private readonly IHttpContextAccessor _http;
18+
public LoginController(IUserManagementClient users, IAccessTokenProvider cache, IHttpContextAccessor http) => (_users, _cache, _http) = (users, cache, http);
1519

1620
[HttpGet]
1721
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
@@ -41,7 +45,30 @@ public async Task<IActionResult> Index(LoginViewModel model, CancellationToken c
4145
return RedirectToAction(nameof(Index)); // ← PRG on failure
4246
}
4347

44-
48+
49+
// Build claims (at least a stable user id + name)
50+
var claims = new List<Claim>
51+
{
52+
new(ClaimTypes.NameIdentifier, result.UserId.ToString()),
53+
new(ClaimTypes.Name, result.UserName),
54+
55+
};
56+
57+
var identity = new ClaimsIdentity(
58+
claims, CookieAuthenticationDefaults.AuthenticationScheme);
59+
60+
// Sign-in (creates auth cookie)
61+
await HttpContext.SignInAsync(
62+
CookieAuthenticationDefaults.AuthenticationScheme,
63+
new ClaimsPrincipal(identity),
64+
new AuthenticationProperties
65+
{
66+
IsPersistent = false,
67+
// keep cookie a bit shorter than token
68+
ExpiresUtc = result.ExpiresAtUtc.AddMinutes(-5)
69+
});
70+
71+
4572
_cache.SetAccessToken(result.Token, result.UserId, result.ExpiresAtUtc);
4673

4774
return RedirectToAction("Index", "Home", new { area = "Home" });
@@ -63,6 +90,29 @@ public async Task<IActionResult> Index(LoginViewModel model, CancellationToken c
6390
}
6491
}
6592

66-
93+
private static string? Claim(ClaimsPrincipal? u, string t) => u?.FindFirst(t)?.Value;
94+
95+
private string? CurrentUserId() =>
96+
Claim(_http.HttpContext?.User, "sub")
97+
?? Claim(_http.HttpContext?.User, ClaimTypes.NameIdentifier);
98+
99+
100+
[HttpPost]
101+
[ValidateAntiForgeryToken]
102+
public async Task<IActionResult> Logout(CancellationToken ct)
103+
{
104+
var uid = CurrentUserId();
105+
if (string.IsNullOrEmpty(uid))
106+
{
107+
return RedirectToAction("Index", "Login", new { area = "Account" });
108+
}
109+
// IMPORTANT: remove tokens while the user is still authenticated
110+
await _cache.RemoveAsync(uid, ct);
111+
112+
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
113+
return RedirectToAction("Index", "Login", new { area = "Account" });
114+
}
115+
116+
67117
}
68118
}
Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,41 @@
11
using ApiIntegrationMvc.Areas.Account.Models;
22
using Microsoft.AspNetCore.Mvc;
3+
using System.IdentityModel.Tokens.Jwt;
4+
using System.Security.Claims;
35
using System.Text.Json;
46
using UserManagement.Contracts.Auth;
57
using UserManagement.Sdk.Abstractions;
8+
using UserManagementApi.Contracts.Models;
9+
using static System.Net.WebRequestMethods;
610

711
namespace ApiIntegrationMvc.Areas.Home.Controllers
812
{
913
[Area("Home")]
1014
public class HomeController : Controller
1115
{
12-
private readonly ILogger<HomeController> _logger;
16+
private readonly IAccessTokenProvider _tokens;
1317

14-
public HomeController(ILogger<HomeController> logger)
15-
{
16-
_logger = logger;
17-
}
1818

19-
public IActionResult Index()
20-
{
19+
public HomeController(IAccessTokenProvider tokens) => _tokens = tokens;
20+
21+
22+
public async Task<IActionResult> Index(CancellationToken ct)
23+
{
24+
var token = await _tokens.GetAccessTokenAsync(ct);
25+
26+
var handler = new JwtSecurityTokenHandler();
27+
var jwt = handler.ReadJwtToken(token);
28+
IEnumerable<Claim> claims = jwt.Claims;
29+
var list = claims.Where(c => c.Type == "categories").Select(c => c.Value).ToList();
30+
if (list.Count == 1)
31+
{
32+
var categories = JsonSerializer.Deserialize<List<Category>>(list[0]);
33+
ViewBag.Categories = categories;
34+
}
35+
else
36+
{
37+
ViewBag.Categories = new List<Category>();
38+
}
2139
return View();
2240
}
2341

@@ -26,5 +44,7 @@ public IActionResult Privacy()
2644
return View();
2745
}
2846

47+
48+
2949
}
3050
}

ApiIntegrationMvc/Controllers/HomeController.cs

Lines changed: 0 additions & 32 deletions
This file was deleted.

ApiIntegrationMvc/Program.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using Microsoft.AspNetCore.Authentication.Cookies;
12
using UserManagement.Sdk;
23
using UserManagement.Sdk.Abstractions;
34
using UserManagement.Sdk.Extensions;
@@ -16,6 +17,20 @@
1617
// Add services to the container.
1718
builder.Services.AddControllersWithViews();
1819

20+
// Cookie auth for the web app (UI)
21+
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
22+
.AddCookie(o =>
23+
{
24+
o.LoginPath = "/Account/Login";
25+
o.LogoutPath = "/Account/Logout";
26+
o.AccessDeniedPath = "/Account/Denied";
27+
// optional:
28+
o.SlidingExpiration = true;
29+
o.ExpireTimeSpan = TimeSpan.FromHours(8);
30+
});
31+
32+
builder.Services.AddAuthorization();
33+
1934
var app = builder.Build();
2035

2136
// Configure the HTTP request pipeline.

ApiIntegrationMvc/Views/Shared/_Layout.cshtml

Lines changed: 140 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,126 @@
1010
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" />
1111
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
1212
<link rel="stylesheet" href="~/ApiIntegrationMvc.styles.css" asp-append-version="true" />
13+
<style>
14+
body {
15+
margin: 0;
16+
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial;
17+
}
18+
19+
.shell {
20+
display: grid;
21+
grid-template-columns: 280px 1fr;
22+
height: 100vh;
23+
}
24+
25+
.left {
26+
border-right: 1px solid #eee;
27+
overflow: auto;
28+
padding: 12px;
29+
}
30+
31+
.right {
32+
overflow: auto;
33+
padding: 16px;
34+
}
35+
36+
/* Tree */
37+
.tree, .tree ul {
38+
list-style: none;
39+
margin: 0;
40+
padding-left: 0;
41+
}
42+
43+
.tree li {
44+
margin: 2px 0;
45+
}
46+
47+
.row {
48+
display: flex;
49+
align-items: center;
50+
gap: 6px;
51+
padding: 6px 8px;
52+
border-radius: 8px;
53+
cursor: pointer;
54+
}
55+
56+
.row:hover {
57+
background: #f5f7fb;
58+
}
59+
60+
.row.active {
61+
background: #e9f1ff;
62+
outline: 1px solid #cfe1ff;
63+
}
64+
65+
.toggle {
66+
width: 18px;
67+
text-align: center;
68+
user-select: none;
69+
font-weight: 600;
70+
color: #666;
71+
}
72+
73+
/* Tiles */
74+
.header {
75+
display: flex;
76+
align-items: center;
77+
justify-content: space-between;
78+
margin-bottom: 8px;
79+
}
80+
81+
.path {
82+
color: #888;
83+
font-size: 12px;
84+
}
85+
86+
.tiles {
87+
display: grid;
88+
grid-template-columns: repeat(auto-fill, minmax(220px,1fr));
89+
gap: 12px;
90+
}
91+
92+
.tile {
93+
border: 1px solid #eee;
94+
border-radius: 14px;
95+
padding: 16px;
96+
box-shadow: 0 1px 2px rgba(0,0,0,.04);
97+
transition: transform .06s, box-shadow .06s;
98+
}
99+
100+
.tile:hover {
101+
transform: translateY(-1px);
102+
box-shadow: 0 4px 10px rgba(0,0,0,.06);
103+
cursor: pointer;
104+
}
105+
106+
.tile h4 {
107+
margin: 0 0 6px 0;
108+
font-size: 16px;
109+
}
110+
111+
.tile p {
112+
margin: 0;
113+
color: #666;
114+
font-size: 13px;
115+
}
116+
117+
.empty {
118+
color: #999;
119+
padding: 12px 0;
120+
}
121+
122+
.section {
123+
margin: 10px 0 0 18px;
124+
}
125+
126+
.section-title {
127+
font-size: 12px;
128+
color: #666;
129+
text-transform: uppercase;
130+
margin: 4px 0;
131+
}
132+
</style>
13133
</head>
14134
<body>
15135
<header>
@@ -23,13 +143,31 @@
23143
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
24144
<ul class="navbar-nav flex-grow-1">
25145
<li class="nav-item">
26-
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
146+
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index"><i class="bi bi-house-door me-1"></i> Home</a>
27147
</li>
28148
<li class="nav-item">
29-
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
149+
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy"><i class="bi bi-shield-lock me-1"></i> Privacy</a>
30150
</li>
31151
</ul>
32152
</div>
153+
<div class="collapse navbar-collapse">
154+
<ul class="navbar-nav ms-auto">
155+
@if (User?.Identity?.IsAuthenticated == true)
156+
{
157+
<li class="nav-item">
158+
<form asp-area="Account" asp-controller="Login" asp-action="Logout" method="post" class="d-inline">
159+
@Html.AntiForgeryToken()
160+
<button type="submit" class="btn btn-link nav-link"><i class="bi bi-box-arrow-right me-1"></i> Logout</button>
161+
</form>
162+
</li>
163+
}
164+
else
165+
{
166+
<li class="nav-item">
167+
<a class="nav-link" asp-area="Account" asp-controller="Login" asp-action="Index">Login</a>
168+
</li>
169+
}
170+
</ul>
33171
</div>
34172
</nav>
35173
</header>

UserManagement.Sdk/Abstractions/IAccessTokenProvider.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ public interface IAccessTokenProvider
1010
{
1111
Task<string?> GetAccessTokenAsync(CancellationToken ct = default);
1212
void SetAccessToken(string token, int userId, DateTime expiresAtUtc);
13+
public Task RemoveAsync(string userId, CancellationToken ct = default);
1314
}
1415
}

UserManagement.Sdk/MemoryCacheAccessTokenProvider.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ public void SetAccessToken(string token, int userId, DateTime expiresAtUtc)
3737
_cache.Set($"token:{userId}", token, ttl);
3838
}
3939

40+
public Task RemoveAsync(string userId, CancellationToken ct = default)
41+
{
42+
_cache.Remove($"token:{userId}");
43+
return Task.CompletedTask;
44+
}
45+
4046
public static TimeSpan ToTtl(DateTime expiresAtUtc, TimeSpan? safety = null)
4147
{
4248
// ensure it's treated as UTC

0 commit comments

Comments
 (0)