User
add: admin dashboard
691d646
Raw
History Blame Contribute Delete
13.6 kB
using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using TaskTrackingSystem.Shared;
using TaskTrackingSystem.Shared.Models.Auth;
using TaskTrackingSystem.WebApp.Components.Partial;
namespace TaskTrackingSystem.WebApp;
public static class AccountEndpoints
{
public static void MapAccountEndpoints(this WebApplication app)
{
app.MapPost("/account/login", LoginAsync);
app.MapPost("/account/register", RegisterAsync);
app.MapPost("/account/reset-password", ResetPasswordAsync);
app.MapGet("/account/logout", LogoutAsync);
app.MapPost("/account/logout", LogoutAsync);
}
private static async Task<IResult> LoginAsync(
HttpContext context,
IHttpClientFactory httpClientFactory,
MenuAuthorizationService menuAuthorization,
[FromForm] string usernameOrEmail,
[FromForm] string password,
[FromForm] bool? rememberMe,
[FromForm] string? returnUrl)
{
if (string.IsNullOrWhiteSpace(usernameOrEmail) || string.IsNullOrWhiteSpace(password))
{
return RedirectToLogin(returnUrl, "invalid");
}
HttpResponseMessage response;
try
{
var client = httpClientFactory.CreateClient("WebApi");
response = await client.PostAsJsonAsync("Auth/login", new LoginDto
{
UsernameOrEmail = usernameOrEmail,
Password = password
});
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or TimeoutException)
{
return RedirectToLogin(returnUrl, "unavailable");
}
if (!response.IsSuccessStatusCode)
{
return RedirectToLogin(returnUrl, "invalid");
}
var result = await response.Content.ReadFromJsonAsync<Result<AuthResponseDto>>(Serialization.CaseInsensitive);
if (result?.IsSuccess != true || result.Value == null || string.IsNullOrWhiteSpace(result.Value.Username))
{
return RedirectToLogin(returnUrl, "invalid");
}
await SignInUserAsync(context, result.Value, rememberMe ?? false);
var landingPage = await ResolveLandingPageAsync(menuAuthorization, result.Value);
return Results.Redirect(GetSafeReturnUrl(returnUrl, landingPage));
}
private static async Task<IResult> RegisterAsync(
HttpContext context,
IHttpClientFactory httpClientFactory,
MenuAuthorizationService menuAuthorization,
[FromForm] RegisterDto registerDto)
{
HttpResponseMessage response;
try
{
var client = httpClientFactory.CreateClient("WebApi");
response = await client.PostAsJsonAsync("Auth/register", registerDto);
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or TimeoutException)
{
return Results.Redirect($"/register?error={Uri.EscapeDataString("Unable to reach the API server. Please ensure TaskTrackingSystem.WebApi is running.")}");
}
string errorMessage = ResultMessages.RegistrationFailed;
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<Result<AuthResponseDto>>(Serialization.CaseInsensitive);
if (result?.IsSuccess == true && result.Value != null && !string.IsNullOrWhiteSpace(result.Value.Username))
{
await SignInUserAsync(context, result.Value, false);
var landingPage = await ResolveLandingPageAsync(menuAuthorization, result.Value);
return Results.Redirect(GetSafeReturnUrl(null, landingPage));
}
else if (result != null && !string.IsNullOrEmpty(result.ErrorMessage))
{
errorMessage = result.ErrorMessage;
}
}
else
{
try
{
var contentString = await response.Content.ReadAsStringAsync();
// Try parsing as Result<AuthResponseDto>
try
{
var result = JsonSerializer.Deserialize<Result<AuthResponseDto>>(contentString, Serialization.CaseInsensitive);
if (result != null && !string.IsNullOrEmpty(result.ErrorMessage))
{
errorMessage = result.ErrorMessage;
}
}
catch
{
// Ignore and fall back to checking validation errors
}
// If no errorMessage from Result, try parsing standard validation errors
if (errorMessage == ResultMessages.RegistrationFailed)
{
try
{
using var doc = JsonDocument.Parse(contentString);
var root = doc.RootElement;
if (root.TryGetProperty("errors", out var errorsProp) && errorsProp.ValueKind == JsonValueKind.Object)
{
var errorsList = new List<string>();
foreach (var prop in errorsProp.EnumerateObject())
{
foreach (var val in prop.Value.EnumerateArray())
{
errorsList.Add(val.GetString() ?? "");
}
}
if (errorsList.Count > 0)
{
errorMessage = string.Join(" ", errorsList.Where(s => !string.IsNullOrEmpty(s)));
}
}
else if (root.TryGetProperty("errorMessage", out var errMsgProp))
{
errorMessage = errMsgProp.GetString() ?? errorMessage;
}
}
catch
{
// Ignore and keep fallback
}
}
}
catch
{
// Fallback to default
}
}
return Results.Redirect($"/register?error={Uri.EscapeDataString(errorMessage)}");
}
private static async Task<IResult> ResetPasswordAsync(
HttpContext context,
IHttpClientFactory httpClientFactory,
[FromForm] ResetPasswordDto resetPasswordDto)
{
HttpResponseMessage response;
try
{
var client = httpClientFactory.CreateClient("WebApi");
response = await client.PostAsJsonAsync("Auth/reset-password", resetPasswordDto);
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or TimeoutException)
{
return Results.Redirect($"/reset-password?error={Uri.EscapeDataString("Unable to reach the API server. Please ensure TaskTrackingSystem.WebApi is running.")}");
}
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<Result>(Serialization.CaseInsensitive);
if (result?.IsSuccess == true)
{
return Results.Redirect("/login?reset=true");
}
}
var errorMessage = "Failed to reset password.";
if (response.Content != null)
{
try
{
var contentString = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<Result>(contentString, Serialization.CaseInsensitive);
if (result != null && !string.IsNullOrWhiteSpace(result.ErrorMessage))
{
errorMessage = result.ErrorMessage;
}
}
catch
{
// Keep default error message.
}
}
return Results.Redirect($"/reset-password?error={Uri.EscapeDataString(errorMessage)}");
}
private static async Task<IResult> LogoutAsync(HttpContext context, UserSessionState sessionState)
{
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
context.Response.Cookies.Delete(CookieAuthenticationDefaults.AuthenticationScheme);
sessionState.ClearSession();
return Results.Redirect("/login?loggedOut=true");
}
private static async Task SignInUserAsync(HttpContext context, AuthResponseDto authResult, bool rememberMe)
{
var claims = new List<Claim>
{
new(ClaimTypes.Name, authResult.Username),
new(ClaimTypes.Email, authResult.Email)
};
if (!string.IsNullOrWhiteSpace(authResult.RoleName))
{
claims.Add(new Claim(ClaimTypes.Role, authResult.RoleName));
}
if (authResult.RoleId > 0)
{
claims.Add(new Claim("role_id", authResult.RoleId.ToString()));
}
var userId = TryGetUserIdFromJwt(authResult.Token);
if (!string.IsNullOrWhiteSpace(userId))
{
claims.Add(new Claim(ClaimTypes.NameIdentifier, userId));
}
claims.Add(new Claim("jwt_token", authResult.Token));
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
await context.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
principal,
new AuthenticationProperties
{
IsPersistent = rememberMe,
ExpiresUtc = rememberMe ? DateTimeOffset.UtcNow.AddDays(7) : DateTimeOffset.UtcNow.AddHours(8)
});
}
private static string? TryGetUserIdFromJwt(string token)
{
if (string.IsNullOrWhiteSpace(token))
{
return null;
}
try
{
var parts = token.Split('.');
if (parts.Length < 2)
{
return null;
}
var payload = parts[1];
switch (payload.Length % 4)
{
case 2: payload += "=="; break;
case 3: payload += "="; break;
}
var json = Convert.FromBase64String(payload);
var data = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json);
if (data == null)
{
return null;
}
if (data.TryGetValue("nameid", out var nameId))
{
return nameId.GetString();
}
if (data.TryGetValue(ClaimTypes.NameIdentifier, out var fullNameId))
{
return fullNameId.GetString();
}
}
catch
{
return null;
}
return null;
}
private static IResult RedirectToLogin(string? returnUrl, string error)
{
var safeReturnUrl = string.IsNullOrWhiteSpace(returnUrl)
? string.Empty
: $"&ReturnUrl={Uri.EscapeDataString(returnUrl)}";
return Results.Redirect($"/login?error={error}{safeReturnUrl}");
}
private static string GetSafeReturnUrl(string? returnUrl, string landingPage)
{
if (string.IsNullOrWhiteSpace(returnUrl) || !returnUrl.StartsWith('/') || returnUrl.StartsWith("//"))
{
return landingPage;
}
return returnUrl;
}
private static async Task<string> ResolveLandingPageAsync(MenuAuthorizationService menuAuthorization, AuthResponseDto authResult)
{
var principal = BuildTemporaryPrincipal(authResult);
var menus = await menuAuthorization.GetUserMenusAsync(principal);
var dashboardMenu = FindFirstDashboardMenu(menus);
return dashboardMenu ?? "/dashboard";
}
private static ClaimsPrincipal BuildTemporaryPrincipal(AuthResponseDto authResult)
{
var claims = new List<Claim>
{
new(ClaimTypes.Name, authResult.Username),
new(ClaimTypes.Email, authResult.Email),
new(ClaimTypes.Role, authResult.RoleName),
new("role_id", authResult.RoleId.ToString()),
new("jwt_token", authResult.Token)
};
return new ClaimsPrincipal(new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme));
}
private static string? FindFirstDashboardMenu(IEnumerable<TaskTrackingSystem.Shared.Models.Menu.MenuDto> menus)
{
foreach (var menu in menus)
{
var found = FindFirstDashboardMenu(menu);
if (!string.IsNullOrWhiteSpace(found))
{
return found;
}
}
return null;
}
private static string? FindFirstDashboardMenu(TaskTrackingSystem.Shared.Models.Menu.MenuDto menu)
{
if (!string.IsNullOrWhiteSpace(menu.MenuUrl) &&
menu.MenuUrl.StartsWith("/dashboard", StringComparison.OrdinalIgnoreCase))
{
return menu.MenuUrl;
}
foreach (var subMenu in menu.SubMenus)
{
var found = FindFirstDashboardMenu(subMenu);
if (!string.IsNullOrWhiteSpace(found))
{
return found;
}
}
return null;
}
}