initial commit
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
using Indotalent.ConfigFrontEnd.Extensions;
|
||||
using Indotalent.ConfigFrontEnd.Models;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
|
||||
namespace Indotalent.ConfigFrontEnd.Common;
|
||||
|
||||
|
||||
public abstract class BaseAppPage : ComponentBase
|
||||
{
|
||||
[Inject]
|
||||
protected AuthenticationStateProvider AuthStateProvider { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected CurrentUserState State { get; set; } = default!;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
await SyncUserContext();
|
||||
await base.OnParametersSetAsync();
|
||||
}
|
||||
|
||||
private async Task SyncUserContext()
|
||||
{
|
||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
var user = authState.User;
|
||||
|
||||
if (user.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
State.UserId = user.GetUserId();
|
||||
State.UserName = user.GetUserName();
|
||||
State.Email = user.GetEmail();
|
||||
State.FullName = user.GetFullName();
|
||||
State.TenantId = user.GetTenantId();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
using Indotalent.ConfigBackEnd.Interfaces;
|
||||
using Indotalent.Features.Account.Login.Cqrs;
|
||||
using Indotalent.Infrastructure.Authentication.Identity;
|
||||
using Indotalent.Shared.Models;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
using RestSharp;
|
||||
using System.Net;
|
||||
|
||||
namespace Indotalent.ConfigFrontEnd.Common;
|
||||
|
||||
public abstract class BaseService
|
||||
{
|
||||
protected readonly IHttpClientFactory ClientFactory;
|
||||
protected readonly NavigationManager Nav;
|
||||
protected readonly ISnackbar Snackbar;
|
||||
protected readonly ICurrentUserService CurrentUserService;
|
||||
protected readonly TokenProvider TokenProvider;
|
||||
|
||||
public BaseService(
|
||||
IHttpClientFactory clientFactory,
|
||||
NavigationManager nav,
|
||||
ISnackbar snackbar,
|
||||
ICurrentUserService currentUserService,
|
||||
TokenProvider tokenProvider)
|
||||
{
|
||||
ClientFactory = clientFactory;
|
||||
Nav = nav;
|
||||
Snackbar = snackbar;
|
||||
CurrentUserService = currentUserService;
|
||||
TokenProvider = tokenProvider;
|
||||
}
|
||||
|
||||
protected HttpClient CreateClient()
|
||||
{
|
||||
var client = ClientFactory.CreateClient();
|
||||
if (client.BaseAddress == null)
|
||||
{
|
||||
client.BaseAddress = new Uri(Nav.BaseUri);
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
protected async Task<ApiResponse<T>?> ExecuteWithResponseAsync<T>(RestClient client, RestRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(TokenProvider.Token))
|
||||
request.AddOrUpdateHeader("Authorization", $"Bearer {TokenProvider.Token}");
|
||||
|
||||
if (!string.IsNullOrEmpty(TokenProvider.RefreshToken))
|
||||
request.AddOrUpdateHeader("X-Refresh-Token", TokenProvider.RefreshToken);
|
||||
|
||||
if (!string.IsNullOrEmpty(CurrentUserService.UserId))
|
||||
{
|
||||
request.AddOrUpdateHeader("X-UserId", CurrentUserService.UserId);
|
||||
request.AddOrUpdateHeader("X-UserName", CurrentUserService.UserName ?? string.Empty);
|
||||
request.AddOrUpdateHeader("X-Email", CurrentUserService.Email ?? string.Empty);
|
||||
request.AddOrUpdateHeader("X-FullName", CurrentUserService.FullName ?? string.Empty);
|
||||
request.AddOrUpdateHeader("X-TenantId", CurrentUserService.TenantId ?? string.Empty);
|
||||
}
|
||||
|
||||
var response = await client.ExecuteAsync<ApiResponse<T>>(request);
|
||||
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
var httpClient = CreateClient();
|
||||
|
||||
var refreshRequestMessage = new HttpRequestMessage(HttpMethod.Post, "/api/account/refresh-token");
|
||||
|
||||
if (!string.IsNullOrEmpty(TokenProvider.RefreshToken))
|
||||
{
|
||||
refreshRequestMessage.Headers.Add("X-Refresh-Token", TokenProvider.RefreshToken);
|
||||
}
|
||||
|
||||
var refreshResponse = await httpClient.SendAsync(refreshRequestMessage);
|
||||
|
||||
if (refreshResponse.IsSuccessStatusCode)
|
||||
{
|
||||
var result = await refreshResponse.Content.ReadFromJsonAsync<LoginResponse>();
|
||||
if (result != null && !string.IsNullOrEmpty(result.Token))
|
||||
{
|
||||
TokenProvider.Token = result.Token;
|
||||
|
||||
request.AddOrUpdateHeader("Authorization", $"Bearer {TokenProvider.Token}");
|
||||
response = await client.ExecuteAsync<ApiResponse<T>>(request);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Nav.NavigateTo("/account/login");
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
if (response.Data != null)
|
||||
{
|
||||
if (!response.Data.IsSuccess)
|
||||
{
|
||||
HandleApiError(response.Data);
|
||||
}
|
||||
return response.Data;
|
||||
}
|
||||
|
||||
var fallbackMessage = GetFriendlyInfrastructureErrorMessage(response);
|
||||
Snackbar.Add(fallbackMessage, Severity.Error);
|
||||
|
||||
return new ApiResponse<T>
|
||||
{
|
||||
IsSuccess = false,
|
||||
StatusCode = (int)response.StatusCode,
|
||||
Message = fallbackMessage
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var systemError = $"Client System Error: {ex.Message}";
|
||||
Snackbar.Add(systemError, Severity.Error);
|
||||
return new ApiResponse<T> { IsSuccess = false, Message = systemError };
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleApiError<T>(ApiResponse<T> response)
|
||||
{
|
||||
var mainMessage = response.Message ?? "Operation failed";
|
||||
if (response.Errors != null && response.Errors.Any())
|
||||
{
|
||||
var combinedErrors = string.Join(Environment.NewLine, response.Errors.Select(e => $"• {e}"));
|
||||
Snackbar.Add(combinedErrors, Severity.Error, config =>
|
||||
{
|
||||
config.ShowCloseIcon = true;
|
||||
config.VisibleStateDuration = 3000;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add(mainMessage, Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetFriendlyInfrastructureErrorMessage(RestResponse response)
|
||||
{
|
||||
return response.StatusCode switch
|
||||
{
|
||||
HttpStatusCode.ServiceUnavailable => "Server is currently under maintenance.",
|
||||
HttpStatusCode.GatewayTimeout => "Server took too long to respond.",
|
||||
HttpStatusCode.Unauthorized => "Session expired. Please login again.",
|
||||
HttpStatusCode.Forbidden => "You do not have permission to access this resource.",
|
||||
HttpStatusCode.InternalServerError => "A critical error occurred on the server.",
|
||||
_ => "Unable to reach the server. Please check your connection."
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Indotalent.ConfigBackEnd.Interfaces;
|
||||
using Indotalent.ConfigFrontEnd.Models;
|
||||
using Indotalent.ConfigFrontEnd.Service;
|
||||
using MudBlazor;
|
||||
using MudBlazor.Services;
|
||||
|
||||
namespace Indotalent.ConfigFrontEnd;
|
||||
|
||||
public static class DI
|
||||
{
|
||||
public static IServiceCollection AddConfigFrontEndDI(this IServiceCollection services)
|
||||
{
|
||||
services.AddMudServices(config =>
|
||||
{
|
||||
config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.TopRight;
|
||||
config.SnackbarConfiguration.PreventDuplicates = true;
|
||||
config.SnackbarConfiguration.NewestOnTop = true;
|
||||
config.SnackbarConfiguration.ShowCloseIcon = true;
|
||||
config.SnackbarConfiguration.VisibleStateDuration = 5000;
|
||||
config.SnackbarConfiguration.HideTransitionDuration = 300;
|
||||
config.SnackbarConfiguration.ShowTransitionDuration = 300;
|
||||
config.SnackbarConfiguration.SnackbarVariant = Variant.Filled;
|
||||
});
|
||||
|
||||
|
||||
services.AddHttpClient("AppClient", client =>
|
||||
{
|
||||
client.Timeout = TimeSpan.FromMinutes(30);
|
||||
client.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
});
|
||||
|
||||
services.AddScoped<CurrentUserState>();
|
||||
services.AddScoped<ICurrentUserService, CurrentUserService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Indotalent.ConfigFrontEnd.Extensions;
|
||||
|
||||
public static class IdentityClaimPrincipalExtension
|
||||
{
|
||||
public static string? GetUserId(this ClaimsPrincipal? user)
|
||||
{
|
||||
return user?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
}
|
||||
|
||||
public static string? GetEmail(this ClaimsPrincipal? user)
|
||||
{
|
||||
return user?.FindFirst(ClaimTypes.Email)?.Value;
|
||||
}
|
||||
|
||||
public static string? GetUserName(this ClaimsPrincipal? user)
|
||||
{
|
||||
return user?.FindFirst(ClaimTypes.Name)?.Value;
|
||||
}
|
||||
|
||||
public static string? GetFullName(this ClaimsPrincipal? user)
|
||||
{
|
||||
return user?.FindFirst(ClaimTypes.GivenName)?.Value;
|
||||
}
|
||||
|
||||
public static string? GetTenantId(this ClaimsPrincipal? user)
|
||||
{
|
||||
return user?.FindFirst(ClaimTypes.GroupSid)?.Value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using Indotalent.ConfigBackEnd.Interfaces;
|
||||
using Indotalent.ConfigFrontEnd.Extensions;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Indotalent.ConfigFrontEnd.Filter;
|
||||
|
||||
public class CurrentUserFilter : IEndpointFilter
|
||||
{
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
|
||||
public CurrentUserFilter(ICurrentUserService currentUserService)
|
||||
{
|
||||
_currentUserService = currentUserService;
|
||||
}
|
||||
|
||||
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
|
||||
{
|
||||
|
||||
var httpContext = context.HttpContext;
|
||||
var user = httpContext.User;
|
||||
|
||||
string? userId = user.GetUserId();
|
||||
string? email = user.GetEmail();
|
||||
string? userName = user.GetUserName();
|
||||
string? fullName = user.GetFullName();
|
||||
string? tenantId = user.GetTenantId();
|
||||
|
||||
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
{
|
||||
userId = httpContext.Request.Headers["X-UserId"].ToString();
|
||||
userName = httpContext.Request.Headers["X-UserName"].ToString();
|
||||
email = httpContext.Request.Headers["X-Email"].ToString();
|
||||
fullName = httpContext.Request.Headers["X-FullName"].ToString();
|
||||
tenantId = httpContext.Request.Headers["X-TenantId"].ToString();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
{
|
||||
_currentUserService.UserId = userId;
|
||||
_currentUserService.UserName = userName;
|
||||
_currentUserService.Email = email;
|
||||
_currentUserService.FullName = fullName;
|
||||
_currentUserService.TenantId = tenantId;
|
||||
}
|
||||
|
||||
return await next(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Indotalent.ConfigFrontEnd.Models;
|
||||
|
||||
public class CurrentUserState
|
||||
{
|
||||
public string? TenantId { get; set; }
|
||||
public string? UserId { get; set; }
|
||||
public string? UserName { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? FullName { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using Indotalent.ConfigBackEnd.Interfaces;
|
||||
using Indotalent.ConfigFrontEnd.Models;
|
||||
|
||||
namespace Indotalent.ConfigFrontEnd.Service;
|
||||
|
||||
public class CurrentUserService : ICurrentUserService
|
||||
{
|
||||
private readonly CurrentUserState _state;
|
||||
|
||||
public CurrentUserService(CurrentUserState state)
|
||||
{
|
||||
_state = state;
|
||||
}
|
||||
|
||||
public string? TenantId
|
||||
{
|
||||
get => _state.TenantId;
|
||||
set => _state.TenantId = value;
|
||||
}
|
||||
|
||||
public string? UserId
|
||||
{
|
||||
get => _state.UserId;
|
||||
set => _state.UserId = value;
|
||||
}
|
||||
|
||||
public string? UserName
|
||||
{
|
||||
get => _state.UserName;
|
||||
set => _state.UserName = value;
|
||||
}
|
||||
|
||||
public string? Email
|
||||
{
|
||||
get => _state.Email;
|
||||
set => _state.Email = value;
|
||||
}
|
||||
|
||||
public string? FullName
|
||||
{
|
||||
get => _state.FullName;
|
||||
set => _state.FullName = value;
|
||||
}
|
||||
|
||||
public Task<bool> IsInRoleAsync(string role)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
public Task<bool> HasPermissionAsync(string permission)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using Indotalent.Data.Entities;
|
||||
using Indotalent.Infrastructure.Authentication.Identity;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
|
||||
namespace Indotalent.ConfigFrontEnd.Service;
|
||||
|
||||
public static class JwtService
|
||||
{
|
||||
public static string GenerateNewJwt(ApplicationUser user, JwtSettingsModel _jwtSettings)
|
||||
{
|
||||
var authClaims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.Name, user.UserName ?? ""),
|
||||
new(ClaimTypes.Email, user.Email ?? ""),
|
||||
new(ClaimTypes.NameIdentifier, user.Id),
|
||||
new(ClaimTypes.GivenName, user.FullName ?? ""),
|
||||
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||
};
|
||||
|
||||
var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.Key));
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: _jwtSettings.Issuer,
|
||||
audience: _jwtSettings.Audience,
|
||||
expires: DateTime.Now.AddMinutes(_jwtSettings.DurationInMinutes),
|
||||
claims: authClaims,
|
||||
signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
|
||||
);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
|
||||
public static string GenerateNewJwtWithClaims(ApplicationUser user, List<Claim> extraClaims, JwtSettingsModel settings)
|
||||
{
|
||||
var authClaims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.Name, user.UserName ?? ""),
|
||||
new(ClaimTypes.Email, user.Email ?? ""),
|
||||
new(ClaimTypes.NameIdentifier, user.Id),
|
||||
new(ClaimTypes.GivenName, user.FullName ?? ""),
|
||||
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||
};
|
||||
|
||||
authClaims.AddRange(extraClaims);
|
||||
|
||||
var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(settings.Key));
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: settings.Issuer,
|
||||
audience: settings.Audience,
|
||||
expires: DateTime.Now.AddMinutes(settings.DurationInMinutes),
|
||||
claims: authClaims,
|
||||
signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
|
||||
);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user