Login and auth and api swagger improvements

This commit is contained in:
Adam 2023-11-23 22:50:07 +01:00
parent 515904e6bd
commit 43bdccc5fa
44 changed files with 904 additions and 335 deletions

View File

@ -3,6 +3,7 @@ using TIAMMobileApp.Services;
using TIAMWebApp.Shared.Application.Interfaces;
using DevExpress.Blazor;
using TIAMMobilApp.Services;
using TIAMWebApp.Shared.Application.Utility;
namespace TIAMMobileApp
{
@ -35,7 +36,7 @@ namespace TIAMMobileApp
/*Android*/
//client.BaseAddress = new Uri("https://10.0.2.2:7116");
builder.Services.AddScoped(sp => client);
builder.Services.AddSingleton(sp => client);
builder.Services.AddDevExpressBlazor(configure => configure.BootstrapVersion = BootstrapVersion.v5);
@ -43,7 +44,9 @@ namespace TIAMMobileApp
builder.Services.AddScoped<ITransferDataService, TransferDataService>();
builder.Services.AddScoped<IPopulationStructureDataProvider, PopulationStructureDataProvider>();
builder.Services.AddScoped<ISupplierService, SupplierService>();
builder.Services.AddSingleton<IUserDataService, UserDataService>();
builder.Services.AddScoped<IUserDataService, UserDataService>();
builder.Services.AddScoped<ISecureStorageHandler, SecureStorageHandler>();
builder.Services.AddScoped<LogToBrowserConsole>();
return builder.Build();
}

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TIAMWebApp.Shared.Application.Interfaces;
using TIAMWebApp.Shared.Application.Models.ClientSide;
namespace TIAMMobileApp.Services
{
public class SecureStorageHandler : ISecureStorageHandler
{
public async Task SaveToSecureStorageAsync(string key, string value)
{
await SecureStorage.SetAsync(key, value);
}
public async Task<string> GetFromSecureStorageAsync(string key)
{
return await SecureStorage.GetAsync(key);
}
}
}

View File

@ -4,6 +4,7 @@ using System.Net.Http.Json;
using System.Text;
using TIAMWebApp.Shared.Application.Interfaces;
using TIAMWebApp.Shared.Application.Models;
using TIAMWebApp.Shared.Application.Models.ClientSide;
using TIAMWebApp.Shared.Application.Models.PageModels;
@ -12,12 +13,14 @@ namespace TIAMMobilApp.Services
public class UserDataService : IUserDataService
{
private readonly HttpClient http;
public User? User { get; set; } = new User("","","");
private readonly ISecureStorageHandler secureStorageHandler;
public User? User { get; set; } = new User("", "", "");
public Dictionary<int, string> userRoleTypes { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public UserDataService(HttpClient http)
public UserDataService(HttpClient http, ISecureStorageHandler secureStorageHandler)
{
this.http = http;
this.secureStorageHandler = secureStorageHandler;
}
@ -40,7 +43,7 @@ namespace TIAMMobilApp.Services
{
if (User == null)
{
User = new User("","","");
User = new User("", "", "");
User.IsLoggedIn = false;
User.UserType = UserType.User;
return User;
@ -50,7 +53,7 @@ namespace TIAMMobilApp.Services
{
return User;
}
}
//Mock method for now
@ -66,7 +69,8 @@ namespace TIAMMobilApp.Services
return User;
}
public async Task<string> TestUserApi(int Param) {
public async Task<string> TestUserApi(int Param)
{
var url = APIUrls.UserTest;
var response = await http.PostAsJsonAsync(url, Param);
var result = await response.Content.ReadAsStringAsync();
@ -75,15 +79,15 @@ namespace TIAMMobilApp.Services
public async Task<string> AuthenticateUser(LoginModel loginModel)
{
string result = string.Empty;
var url = APIUrls.AuthenticateUser;
string result = string.Empty;
var url = APIUrls.AuthenticateUser;
var response = await http.PostAsJsonAsync(url, loginModel);
if(response.IsSuccessStatusCode)
if (response.IsSuccessStatusCode)
{
result = await response.Content.ReadAsStringAsync();
}
@ -95,7 +99,7 @@ namespace TIAMMobilApp.Services
//result = await response.Content.ReadAsStringAsync();
return result;
}
public async Task<(bool isSuccess, string ErrorMessage)> CreateUser(RegistrationModel regModel)
@ -105,7 +109,7 @@ namespace TIAMMobilApp.Services
string result = string.Empty;
var url = APIUrls.CreateUser;
var response = await http.PostAsJsonAsync(url, regModel);
var response = await http.PostAsJsonAsync(url, regModel);
result = await response.Content.ReadAsStringAsync();
/*if (response.IsSuccessStatusCode)
{
@ -122,25 +126,67 @@ namespace TIAMMobilApp.Services
return (isSuccess, result);
}
public async Task<bool> RefreshToken()
{
bool isTokenRefreshed = false;
using (var client = new HttpClient())
{
var url = APIUrls.RefreshToken;
var serializedStr = JsonConvert.SerializeObject(new AuthenticateRequestAndResponse
{
RefreshToken = Setting.UserBasicDetails.RefreshToken,
AccessToken = Setting.UserBasicDetails.AccessToken
});
try
{
var response = await client.PostAsync(url, new StringContent(serializedStr, Encoding.UTF8, "application/json"));
if (response.IsSuccessStatusCode)
{
string contentStr = await response.Content.ReadAsStringAsync();
var mainResponse = JsonConvert.DeserializeObject<MainResponse>(contentStr);
if (mainResponse.IsSuccess)
{
var tokenDetails = JsonConvert.DeserializeObject<AuthenticateRequestAndResponse>(mainResponse.Content.ToString());
Setting.UserBasicDetails.AccessToken = tokenDetails.AccessToken;
Setting.UserBasicDetails.RefreshToken = tokenDetails.RefreshToken;
string userDetailsStr = JsonConvert.SerializeObject(Setting.UserBasicDetails);
await secureStorageHandler.SaveToSecureStorageAsync(nameof(Setting.UserBasicDetails), userDetailsStr);
isTokenRefreshed = true;
}
}
}
catch (Exception ex)
{
string msg = ex.Message;
}
}
return isTokenRefreshed;
}
public Task<Dictionary<int, string>> GetUserRolesAsync(User user)
{
//get the user's roles
int role = User.UserRoles;
int role = User.UserRoles;
foreach (var roleType in roleTypes)
foreach (var roleType in roleTypes)
{
if ((role & roleType.Id) == roleType.Id)
{
if ((role & roleType.Id) == roleType.Id)
{
//add the role to the dictionary
userRoleTypes.Add(roleType.Id, roleType.RoleName);
}
//add the role to the dictionary
userRoleTypes.Add(roleType.Id, roleType.RoleName);
}
}
return Task.FromResult(userRoleTypes);
}
}
}

View File

@ -0,0 +1,56 @@
@page "/";
@using TIAMWebApp.Shared.Application.Interfaces
@using TIAMWebApp.Shared.Application.Models
@using TIAMWebApp.Shared.Application.Utility
@using Newtonsoft.Json
@using System.IdentityModel.Tokens.Jwt
@using TIAMWebApp.Shared.Application.Models.ClientSide
@inject NavigationManager NavManager
@inject LogToBrowserConsole logToBrowserConsole
@inject IUserDataService UserDataService
@inject ISecureStorageHandler SecureStorageHandler
<h3>AppLaunch</h3>
Loading....
@code {
protected async override Task OnInitializedAsync()
{
string userDetailsStr = await SecureStorageHandler.GetFromSecureStorageAsync(nameof(Setting.UserBasicDetails));
logToBrowserConsole.LogToBC(userDetailsStr);
if (!string.IsNullOrWhiteSpace(userDetailsStr))
{
var userBasicDetail = JsonConvert.DeserializeObject<UserBasicDetails>(userDetailsStr);
var handler = new JwtSecurityTokenHandler();
var jsontoken = handler.ReadToken(userBasicDetail.AccessToken) as JwtSecurityToken;
Setting.UserBasicDetails = userBasicDetail;
if (jsontoken.ValidTo < DateTime.UtcNow)
{
bool isTokenRefreshed = await UserDataService.RefreshToken();
if (isTokenRefreshed)
{
NavManager.NavigateTo("/home");
}
else
{
NavManager.NavigateTo("/login");
}
}
else
{
NavManager.NavigateTo("/home");
}
}
else
{
NavManager.NavigateTo("/login");
}
}
}

View File

@ -15,7 +15,7 @@
Mask="\+(9[976]\d|8[987530]\d|6[987]\d|5[90]\d|42\d|3[875]\d|2[98654321]\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|4[987654310]|3[9643210]|2[70]|7|1)\W*\d\W*\d\W*\d\W*\d\W*\d\W*\d\W*\d\W*\d\W*(\d{1,2})"
MaskMode="@MaskMode.RegEx">
<DxRegExMaskProperties Placeholder="Placeholder"
PlaceholdersVisible=false/>
PlaceholdersVisible=true/>
</DxMaskedInput>

View File

@ -1,4 +1,4 @@
@page "/"
@page "/index"
@using TIAMSharedUI.Shared
<PageTitle>Index</PageTitle>

View File

@ -1,4 +1,4 @@
@page "/login"
@page "/login_old"
@using TIAMWebApp.Shared.Application.Interfaces;
@using TIAMWebApp.Shared.Application.Models;
@using TIAMWebApp.Shared.Application.Models.PageModels;

View File

@ -1,10 +1,20 @@
@page "/login2"
@page "/login"
@using System.IdentityModel.Tokens.Jwt;
@using System.Security.Claims;
@using Newtonsoft.Json.Linq;
@using System.Text.Json;
@using System.Reflection;
@using TIAMWebApp.Shared.Application.Interfaces;
@using TIAMWebApp.Shared.Application.Models.PageModels;
@using TIAMSharedUI.Pages.Components;
@using TIAMWebApp.Shared.Application.Models.ClientSide;
@using TIAMWebApp.Shared.Application.Models;
@using TIAMWebApp.Shared.Application.Utility;
@inject NavigationManager navManager
@inject LogToBrowserConsole logToBrowserConsole
@inject IUserDataService UserDataservice
@inject IJSRuntime jsRuntime
@inject ISecureStorageHandler SecureStorageHandler
<PageTitle>Login</PageTitle>
@ -54,11 +64,11 @@
@code {
LoginModel loginModel = new();
private int currentStep = 1;
bool loggedIn = false;
private void GoToNextStep()
{
@ -73,58 +83,80 @@
private async void SubmitLogin()
{
// Implement your registration logic here
// You can use Email, PhoneNumber, and Password variables
// Reset currentStep after successful registration
loggedIn = true;
currentStep = 1;
LogToBrowserConsole("Login started: " + "Email: " + loginModel.Email + ", Password: " + loginModel.Password);
logToBrowserConsole.LogToBC("Login started: " + "Email: " + loginModel.Email + ", Password: " + loginModel.Password);
var response = await UserDataservice.AuthenticateUser(loginModel);
//var response = await UserDataservice.TestUserApi(30);
LogToBrowserConsole("Login started");
logToBrowserConsole.LogToBC("Login started");
logToBrowserConsole.LogToBC(response);
if (!string.IsNullOrEmpty(response))
{
LogToBrowserConsole(response);
if (response == "no")
//get token and save to local storage
//parse to Mainresponse from json string
//var Mainresponse = JsonSerializer.Deserialize<MainResponse>(response);
var Mainresponse = JsonSerializer.Deserialize<MainResponse>(response, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (Mainresponse != null)
{
//await App.Current.MainPage.DisplayAlert("Error", "Invalid credentials", "Ok");
//display error message via jsinterop
LogToBrowserConsole("Invalid credentials");
navManager.NavigateTo("login2");
//check for bad request
string AuthResponseJson = JsonSerializer.Serialize(Mainresponse.Content);
var AuthResponse = JsonSerializer.Deserialize<AuthenticationResponse>(AuthResponseJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
string accessToken = AuthResponse.AccessToken;
var handler = new JwtSecurityTokenHandler();
var token = handler.ReadJwtToken(accessToken) as JwtSecurityToken;
string _userId = token.Claims.First(claim => claim.Type == JwtRegisteredClaimNames.NameId).Value;
string _email = token.Claims.First(claim => claim.Type == JwtRegisteredClaimNames.Email).Value;
var userBasicDetails = new UserBasicDetails(_userId, _email, AuthResponse.AccessToken, AuthResponse.RefreshToken);
string userBasicDetailsJson = JsonSerializer.Serialize(userBasicDetails);
//save to local storage
await SecureStorageHandler.SaveToSecureStorageAsync(nameof(Setting.UserBasicDetails), userBasicDetailsJson);
if (!Mainresponse.IsSuccess)
{
//await App.Current.MainPage.DisplayAlert("Error", "Invalid credentials", "Ok");
//display error message via jsinterop
logToBrowserConsole.LogToBC("Invalid credentials");
navManager.NavigateTo("login2");
}
else
{
//await App.Current.MainPage.DisplayAlert("Success", "Successful login", "Ok");
//display success message via jsinterop
logToBrowserConsole.LogToBC("Successful login");
var user = await UserDataservice.IsLoggedInAsync();
user.IsLoggedIn = true;
user.UserType = UserType.Admin;
navManager.NavigateTo("home");
}
}
else if(response == "yes")
{
//await App.Current.MainPage.DisplayAlert("Success", "Successful login", "Ok");
//display success message via jsinterop
LogToBrowserConsole("Successful login");
navManager.NavigateTo("home");
}
else
{
//await App.Current.MainPage.DisplayAlert("Error", "An error occured while trying to login", "Ok");
//display error message via jsinterop
LogToBrowserConsole("An error occured while trying to login");
navManager.NavigateTo("login2");
}
}
else
{
//api error
//await App.Current.MainPage.DisplayAlert("Error", "An error occured while trying to login", "Ok");
//display error message via jsinterop
LogToBrowserConsole("An error occured while trying to login");
logToBrowserConsole.LogToBC("An error occured while trying to login");
navManager.NavigateTo("login2");
}
}
public void LogToBrowserConsole(string message)
{
jsRuntime.InvokeVoidAsync("console.log", message);
}
}

View File

@ -1,35 +0,0 @@
@page "/register2"
@inject NavigationManager navManager;
<PageTitle>Register step 2</PageTitle>
<div class="wrapper">
<div class="my-logo">
<img src="_content/TIAMSharedUI/images/png-logo-0.png" alt="">
</div>
<div class="text-center mt-4 name">
Let's create your account!
</div>
<form class="p-3 mt-3">
<div class="form-field d-flex align-items-center">
<span class="fas fa-key"></span>
<input type="password" name="password" id="pwd" placeholder="Password">
</div>
<a class="btn btn-secondary mt-3" @onclick="back">Previous</a>
<a class="btn btn-primary mt-3" @onclick="next">Next</a>
</form>
<div class="text-center fs-6">
Already have an account? <a href="login">Sign in here!</a>
</div>
</div>
@code {
private void next()
{
navManager.NavigateTo("register3");
}
private void back()
{
navManager.NavigateTo("register");
}
}

View File

@ -1,3 +0,0 @@
.wrapper {
max-width: 400px;
}

View File

@ -1,35 +0,0 @@
@page "/register3"
@inject NavigationManager navManager;
<PageTitle>Register step 3</PageTitle>
<div class="wrapper">
<div class="my-logo">
<img src="_content/TIAMSharedUI/images/png-logo-0.png" alt="">
</div>
<div class="text-center mt-4 name">
Let's create your account!
</div>
<form class="p-3 mt-3">
<div class="form-field d-flex align-items-center">
<span class="fas fa-key"></span>
<input type="password" name="password" id="pwd" placeholder="Confirm password">
</div>
<a class="btn btn-secondary mt-3" @onclick="back">Previous</a>
<a class="btn btn-primary mt-3" @onclick="next">Next</a>
</form>
<div class="text-center fs-6">
Already have an account? <a href="login">Sign in here!</a>
</div>
</div>
@code {
private void next()
{
navManager.NavigateTo("register4");
}
private void back()
{
navManager.NavigateTo("register2");
}
}

View File

@ -1,3 +0,0 @@
.wrapper {
max-width: 400px;
}

View File

@ -1,50 +0,0 @@
@page "/register4"
@inject NavigationManager navManager;
<PageTitle>Register step 4</PageTitle>
<div class="wrapper">
<div class="my-logo">
<img src="_content/TIAMSharedUI/images/png-logo-0.png" alt="">
</div>
<div class="text-center mt-4 name">
Let's create your account!
</div>
<form class="p-3 mt-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="flexRadioDefault" id="flexRadioDefault1" checked>
<label class="form-check-label" for="flexRadioDefault1">
Customer
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="flexRadioDefault" id="flexRadioDefault2" disabled>
<label class="form-check-label" for="flexRadioDefault2">
Service provider (not available yet)
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="flexCheckDefault">
<label class="form-check-label" for="flexCheckDefault">
I accept the <a href="terms">Terms and Conditions</a> of use.
</label>
</div>
<a class="btn btn-secondary mt-3" @onclick="back">Previous</a>
<a class="btn btn-primary mt-3" @onclick="next">Next</a>
</form>
<div class="text-center fs-6">
Already have an account? <a href="login">Sign in here!</a>
</div>
</div>
@code {
private void next()
{
navManager.NavigateTo("login");
}
private void back()
{
navManager.NavigateTo("register3");
}
}

View File

@ -1,65 +0,0 @@
.wrapper {
max-width: 400px;
}
label {
width: 100%;
font-size: 1rem;
}
.card-input-element + .card {
height: calc(36px + 2*1rem);
color: var(--primary);
-webkit-box-shadow: none;
box-shadow: none;
border: 2px solid transparent;
border-radius: 4px;
}
.card-input-element + .card:hover {
cursor: pointer;
}
.card-input-element:checked + .card {
border: 2px solid var(--primary);
-webkit-transition: border .3s;
-o-transition: border .3s;
transition: border .3s;
}
.card-input-element:checked + .card::after {
content: '\e5ca';
color: #AFB8EA;
font-family: 'Material Icons';
font-size: 24px;
-webkit-animation-name: fadeInCheckbox;
animation-name: fadeInCheckbox;
-webkit-animation-duration: .5s;
animation-duration: .5s;
-webkit-animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
@-webkit-keyframes fadeInCheckbox {
from {
opacity: 0;
-webkit-transform: rotateZ(-20deg);
}
to {
opacity: 1;
-webkit-transform: rotateZ(0deg);
}
}
@keyframes fadeInCheckbox {
from {
opacity: 0;
transform: rotateZ(-20deg);
}
to {
opacity: 1;
transform: rotateZ(0deg);
}
}

View File

@ -14,12 +14,9 @@
<article class="content">
@{
if (isUserLoggedIn)
{
<TopRow></TopRow>
}
}
<TopRow></TopRow>
@Body
</article>
</main>
@ -27,7 +24,7 @@
@code {
bool isUserLoggedIn;
/*bool isUserLoggedIn;
int userType = 0;
protected override async Task OnInitializedAsync()
@ -39,5 +36,5 @@
{
NavigationManager.NavigateTo("/login");
}
}
}*/
}

View File

@ -1,12 +1,17 @@
@inherits LayoutComponentBase
@using TIAMWebApp.Shared.Application.Interfaces
@using TIAMWebApp.Shared.Application.Models.ClientSide;
@inject IUserDataService UserDataService;
@inject IJSRuntime jsRuntime
<div class="page">
<div class="my-sidebar">
<NavMenu />
</div>
@if (Setting.UserBasicDetails != null)
{
<div class="my-sidebar">
<NavMenu />
</div>
}
<main>
@ -43,16 +48,13 @@
protected override void OnAfterRender(bool isFirst)
{
LogToBrowserConsole("0 ");
}
public void LogToBrowserConsole(string message)
{
jsRuntime.InvokeVoidAsync("console.log", message);
}
}

View File

@ -29,12 +29,9 @@
<NavLink class="nav-link" href="counter">
Counter
</NavLink>
</div>
<div-- class="nav-item px-3">
<NavLink class="nav-link" href="fetchdata">
Fetch data
</NavLink>
</div-->
<div class="nav-item px-3">
<NavLink class="nav-link" href="transfer">
Transfer
@ -45,6 +42,12 @@
Login
</NavLink>
</div>
<hr />
<div class="nav-item px-3">
<NavLink class="nav-link" href="swagger">
Api
</NavLink>
</div>
</nav>
</div>

View File

@ -1,9 +1,12 @@
using Blazored.LocalStorage;
using DevExpress.Blazor;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.JSInterop;
using TIAMWebApp.Client;
using TIAMWebApp.Client.Services;
using TIAMWebApp.Shared.Application.Interfaces;
using TIAMWebApp.Shared.Application.Utility;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
@ -13,7 +16,15 @@ builder.Services.AddScoped<IWeatherForecastService, WeatherForecastService>();
builder.Services.AddScoped<ITransferDataService, TransferDataService>();
builder.Services.AddScoped<IPopulationStructureDataProvider, PopulationStructureDataProvider>();
builder.Services.AddScoped<ISupplierService, SupplierService>();
builder.Services.AddSingleton<IUserDataService, UserDataService>();
builder.Services.AddScoped<IUserDataService, UserDataService>();
builder.Services.AddScoped<ISecureStorageHandler, SecureStorageHandler>();
builder.Services.AddScoped<LogToBrowserConsole>();
builder.Services.AddBlazoredLocalStorage();
//WebSpecific
builder.Services.AddScoped<SessionStorageAccessor>();
//WebSpecific end
builder.Services.AddSingleton(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddDevExpressBlazor(configure => configure.BootstrapVersion = BootstrapVersion.v5);
await builder.Build().RunAsync();

View File

@ -0,0 +1,31 @@
using Microsoft.JSInterop;
namespace TIAMWebApp.Client.Services
{
public class LocalStorageAccessor : IAsyncDisposable
{
private Lazy<IJSObjectReference> _accessorJsRef = new();
private readonly IJSRuntime _jsRuntime;
public LocalStorageAccessor(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
private async Task WaitForReference()
{
if (_accessorJsRef.IsValueCreated is false)
{
_accessorJsRef = new(await _jsRuntime.InvokeAsync<IJSObjectReference>("import", "/js/LocalStorageAccessor.js"));
}
}
public async ValueTask DisposeAsync()
{
if (_accessorJsRef.IsValueCreated)
{
await _accessorJsRef.Value.DisposeAsync();
}
}
}
}

View File

@ -0,0 +1,33 @@
using Blazored.LocalStorage;
using TIAMWebApp.Shared.Application.Interfaces;
namespace TIAMWebApp.Client.Services
{
public class SecureStorageHandler : ISecureStorageHandler
{
private readonly SessionStorageAccessor ssa;
private readonly ILocalStorageService localStoragService;
public SecureStorageHandler(SessionStorageAccessor ssa, ILocalStorageService localStorageService)
{
this.ssa = ssa;
this.localStoragService = localStorageService;
}
public async Task SaveToSecureStorageAsync(string key, string value)
{
await localStoragService.SetItemAsync(key, value);
await ssa.SetValueAsync(key, value);
}
public async Task<string> GetFromSecureStorageAsync(string key)
{
return await localStoragService.GetItemAsync<string>(key);
return await ssa.GetValueAsync<string>(key);
}
}
}

View File

@ -0,0 +1,58 @@
using Microsoft.JSInterop;
namespace TIAMWebApp.Client.Services
{
public class SessionStorageAccessor : IAsyncDisposable
{
private Lazy<IJSObjectReference> _accessorJsRef = new();
private readonly IJSRuntime _jsRuntime;
public SessionStorageAccessor(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
private async Task WaitForReference()
{
if (_accessorJsRef.IsValueCreated is false)
{
_accessorJsRef = new(await _jsRuntime.InvokeAsync<IJSObjectReference>("import", "/js/SessionStorageAccessor.js"));
}
}
public async ValueTask DisposeAsync()
{
if (_accessorJsRef.IsValueCreated)
{
await _accessorJsRef.Value.DisposeAsync();
}
}
public async Task<T> GetValueAsync<T>(string key)
{
await WaitForReference();
var result = await _accessorJsRef.Value.InvokeAsync<T>("get", key);
return result;
}
public async Task SetValueAsync<T>(string key, T value)
{
await WaitForReference();
await _accessorJsRef.Value.InvokeVoidAsync("set", key, value);
}
public async Task Clear()
{
await WaitForReference();
await _accessorJsRef.Value.InvokeVoidAsync("clear");
}
public async Task RemoveAsync(string key)
{
await WaitForReference();
await _accessorJsRef.Value.InvokeVoidAsync("remove", key);
}
}
}

View File

@ -4,6 +4,7 @@ using System.Net.Http.Json;
using System.Text;
using TIAMWebApp.Shared.Application.Interfaces;
using TIAMWebApp.Shared.Application.Models;
using TIAMWebApp.Shared.Application.Models.ClientSide;
using TIAMWebApp.Shared.Application.Models.PageModels;
@ -12,12 +13,14 @@ namespace TIAMWebApp.Client.Services
public class UserDataService : IUserDataService
{
private readonly HttpClient http;
public User? User { get; set; } = new User("","","");
private readonly ISecureStorageHandler secureStorageHandler;
public User? User { get; set; } = new User("", "", "");
public Dictionary<int, string> userRoleTypes { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public UserDataService(HttpClient http)
public UserDataService(HttpClient http, ISecureStorageHandler secureStorageHandler)
{
this.http = http;
this.secureStorageHandler = secureStorageHandler;
}
@ -40,7 +43,7 @@ namespace TIAMWebApp.Client.Services
{
if (User == null)
{
User = new User("","","");
User = new User("", "", "");
User.IsLoggedIn = false;
User.UserType = UserType.User;
return User;
@ -50,7 +53,7 @@ namespace TIAMWebApp.Client.Services
{
return User;
}
}
//Mock method for now
@ -66,7 +69,8 @@ namespace TIAMWebApp.Client.Services
return User;
}
public async Task<string> TestUserApi(int Param) {
public async Task<string> TestUserApi(int Param)
{
var url = APIUrls.UserTest;
var response = await http.PostAsJsonAsync(url, Param);
var result = await response.Content.ReadAsStringAsync();
@ -75,15 +79,15 @@ namespace TIAMWebApp.Client.Services
public async Task<string> AuthenticateUser(LoginModel loginModel)
{
string result = string.Empty;
var url = APIUrls.AuthenticateUser;
string result = string.Empty;
var url = APIUrls.AuthenticateUser;
var response = await http.PostAsJsonAsync(url, loginModel);
if(response.IsSuccessStatusCode)
if (response.IsSuccessStatusCode)
{
result = await response.Content.ReadAsStringAsync();
}
@ -95,7 +99,7 @@ namespace TIAMWebApp.Client.Services
//result = await response.Content.ReadAsStringAsync();
return result;
}
public async Task<(bool isSuccess, string ErrorMessage)> CreateUser(RegistrationModel regModel)
@ -105,7 +109,7 @@ namespace TIAMWebApp.Client.Services
string result = string.Empty;
var url = APIUrls.CreateUser;
var response = await http.PostAsJsonAsync(url, regModel);
var response = await http.PostAsJsonAsync(url, regModel);
result = await response.Content.ReadAsStringAsync();
/*if (response.IsSuccessStatusCode)
{
@ -122,25 +126,67 @@ namespace TIAMWebApp.Client.Services
return (isSuccess, result);
}
public async Task<bool> RefreshToken()
{
bool isTokenRefreshed = false;
using (var client = new HttpClient())
{
var url = APIUrls.RefreshToken;
var serializedStr = JsonConvert.SerializeObject(new AuthenticateRequestAndResponse
{
RefreshToken = Setting.UserBasicDetails.RefreshToken,
AccessToken = Setting.UserBasicDetails.AccessToken
});
try
{
var response = await client.PostAsync(url, new StringContent(serializedStr, Encoding.UTF8, "application/json"));
if (response.IsSuccessStatusCode)
{
string contentStr = await response.Content.ReadAsStringAsync();
var mainResponse = JsonConvert.DeserializeObject<MainResponse>(contentStr);
if (mainResponse.IsSuccess)
{
var tokenDetails = JsonConvert.DeserializeObject<AuthenticateRequestAndResponse>(mainResponse.Content.ToString());
Setting.UserBasicDetails.AccessToken = tokenDetails.AccessToken;
Setting.UserBasicDetails.RefreshToken = tokenDetails.RefreshToken;
string userDetailsStr = JsonConvert.SerializeObject(Setting.UserBasicDetails);
await secureStorageHandler.SaveToSecureStorageAsync(nameof(Setting.UserBasicDetails), userDetailsStr);
isTokenRefreshed = true;
}
}
}
catch (Exception ex)
{
string msg = ex.Message;
}
}
return isTokenRefreshed;
}
public Task<Dictionary<int, string>> GetUserRolesAsync(User user)
{
//get the user's roles
int role = User.UserRoles;
int role = User.UserRoles;
foreach (var roleType in roleTypes)
foreach (var roleType in roleTypes)
{
if ((role & roleType.Id) == roleType.Id)
{
if ((role & roleType.Id) == roleType.Id)
{
//add the role to the dictionary
userRoleTypes.Add(roleType.Id, roleType.RoleName);
}
//add the role to the dictionary
userRoleTypes.Add(roleType.Id, roleType.RoleName);
}
}
return Task.FromResult(userRoleTypes);
}
}
}

View File

@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazored.LocalStorage" Version="4.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.10" PrivateAssets="all" />
</ItemGroup>

View File

@ -0,0 +1,15 @@
export function get(key) {
return window.sessionStorage.getItem(key);
}
export function set(key, value) {
window.sessionStorage.setItem(key, value);
}
export function clear() {
window.sessionStorage.clear();
}
export function remove(key) {
window.sessionStorage.removeItem(key);
}

View File

@ -0,0 +1,15 @@
export function get(key) {
return window.sessionStorage.getItem(key);
}
export function set(key, value) {
window.sessionStorage.setItem(key, value);
}
export function clear() {
window.sessionStorage.clear();
}
export function remove(key) {
window.sessionStorage.removeItem(key);
}

View File

@ -5,35 +5,48 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using System.Reflection.Metadata;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text.Json;
using TIAMWebApp.Shared.Application.Models;
using TIAMWebApp.Shared.Application.Models.PageModels;
using TIAMWebApp.Server.Models;
using System.Text;
using Microsoft.AspNetCore.Hosting;
using TIAMWebApp.Server.ModelsTIAMWebApp.Shared.Application.Models;
namespace TIAMWebApp.Server.Controllers
{
[Authorize]
[ApiController]
[Route("[controller]")]
[Route("api/[controller]")]
public class UserAPIController : ControllerBase
{
private readonly IConfiguration _configuration;
private readonly IWebHostEnvironment _webHostEnvironment;
PasswordHasher hasher = new PasswordHasher();
private User[] users = new User[]
{
new User("test@tiam.hu", "+36701234567", "asd123")
new User(new Guid("540271f6-c604-4c16-8160-d5a7cafedf00"), "test@tiam.hu", "+36701234567", "Asdasd123456"),
new User(new Guid("4cbaed43-2465-4d99-84f1-c8bc6b7025f7"), "adam@tiam.hu", "+36701234567", "Asdasd987654")
};
private readonly ILogger<SupplierAPIController> _logger;
private readonly ILogger<UserAPIController> _logger;
public UserAPIController(ILogger<SupplierAPIController> logger)
public UserAPIController(ILogger<UserAPIController> logger, IConfiguration configuration, IWebHostEnvironment webHostEnvironment)
{
_logger = logger;
_configuration = configuration;
_webHostEnvironment = webHostEnvironment;
}
[HttpPost]
/*[HttpPost]
[Route("Auth")]
public async Task<IActionResult> AuthenticateUser([FromBody] JsonElement SerializedLoginModel)
{
@ -50,7 +63,7 @@ namespace TIAMWebApp.Server.Controllers
Console.WriteLine(user.Email);
Console.WriteLine(user.Password);
if (user.Email == "test@tiam.hu" && user.Password == "asd123")
if (user.Email == "test@tiam.hu" && user.Password == "Asdasd123456")
{
Console.WriteLine("User authenticated");
return Ok("yes");
@ -62,8 +75,178 @@ namespace TIAMWebApp.Server.Controllers
}
}
}*/
[AllowAnonymous]
[HttpPost("AuthenticateUser")]
public async Task<IActionResult> AuthenticateUser([FromBody] JsonElement SerializedLoginModel)
{
var authenticateUser = JObject.Parse(SerializedLoginModel.GetRawText()).ToObject<LoginModel>();
//check if user exists
//var user = await _userManager.FindByNameAsync(authenticateUser.UserName);
//if (user == null) return Unauthorized();
//mocking
var user = users.FirstOrDefault(x => x.Email == authenticateUser.Email);
//check if password is valid
//bool isValidUser = await _userManager.CheckPasswordAsync(user, authenticateUser.Password);
//mocking
bool isValidUser = false;
if (user.Password == authenticateUser.Password)
{
isValidUser = true;
}
if (isValidUser)
{
Console.WriteLine("User authenticated, let's start JWT");
string accessToken = GenerateAccessToken(user);
Console.WriteLine("Generate refresh token");
var refreshToken = GenerateRefreshToken();
user.RefreshToken = refreshToken;
//Update user with refreshToken!!
//await _userManager.UpdateAsync(user);
var response = new MainResponse
{
Content = new AuthenticationResponse
{
RefreshToken = refreshToken,
AccessToken = accessToken
},
IsSuccess = true,
ErrorMessage = ""
};
return Ok(response);
}
else
{
return Unauthorized();
}
}
private string GenerateAccessToken(User user)
{
var tokenHandler = new JwtSecurityTokenHandler();
var token = new JwtSecurityToken();
Console.WriteLine("----------------------------------------------------------");
var keyDetail = Encoding.UTF8.GetBytes(_configuration["JWT:Key"]);
Console.WriteLine(_configuration["JWT:Key"]);
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Email, user.Email)
};
var tokenDescriptor = new SecurityTokenDescriptor
{
Audience = _configuration["JWT:Audience"],
Issuer = _configuration["JWT:Issuer"],
Expires = DateTime.UtcNow.AddMinutes(30),
Subject = new ClaimsIdentity(claims),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(keyDetail), SecurityAlgorithms.HmacSha256Signature)
};
token = tokenHandler.CreateToken(tokenDescriptor) as JwtSecurityToken;
string writtenToken = tokenHandler.WriteToken(token);
Console.WriteLine(writtenToken);
return writtenToken;
}
[AllowAnonymous]
[HttpPost("RefreshToken")]
public async Task<IActionResult> RefreshToken(RefreshTokenRequest refreshTokenRequest)
{
var response = new MainResponse();
if (refreshTokenRequest is null)
{
response.ErrorMessage = "Invalid request";
return BadRequest(response);
}
var principal = GetPrincipalFromExpiredToken(refreshTokenRequest.AccessToken);
if (principal != null)
{
var email = principal.Claims.FirstOrDefault(f => f.Type == ClaimTypes.Email);
//var user = await _userManager.FindByEmailAsync(email?.Value);
var user = users.FirstOrDefault(x => x.Email == email?.Value);
if (user is null || user.RefreshToken != refreshTokenRequest.RefreshToken)
{
response.ErrorMessage = "Invalid Request";
return BadRequest(response);
}
string newAccessToken = GenerateAccessToken(user);
string refreshToken = GenerateRefreshToken();
//mocking - update user with new refreshToken
user.RefreshToken = refreshToken;
//await _userManager.UpdateAsync(user);
response.IsSuccess = true;
response.Content = new AuthenticationResponse
{
RefreshToken = refreshToken,
AccessToken = newAccessToken
};
return Ok(response);
}
else
{
return NotFound("Invalid Token Found");
}
}
private ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
var keyDetail = Encoding.UTF8.GetBytes(_configuration["JWT:Key"]);
var tokenValidationParameter = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = false,
ValidateIssuerSigningKey = true,
ValidIssuer = _configuration["JWT:Issuer"],
ValidAudience = _configuration["JWT:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(keyDetail),
};
SecurityToken securityToken;
var principal = tokenHandler.ValidateToken(token, tokenValidationParameter, out securityToken);
var jwtSecurityToken = securityToken as JwtSecurityToken;
if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
throw new SecurityTokenException("Invalid token");
return principal;
}
private string GenerateRefreshToken()
{
var randomNumber = new byte[32];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
}
[HttpPost]
[Route("CreateUser")]
public async Task<IActionResult> CreateUser([FromBody] JsonElement SerializedRegistrationModel)

View File

@ -0,0 +1,10 @@
namespace TIAMWebApp.Server.Models
{
public class AuthenticateUser
{
public string Email { get; set; }
public string Password { get; set; }
}
}

View File

@ -0,0 +1,8 @@
namespace TIAMWebApp.Server.Models
{
public class RefreshTokenRequest
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
}
}

View File

@ -6,6 +6,11 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using TIAM.Entities.TransferDestinations;
using TIAM.Database.DbContexts;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using TIAMWebApp.Shared.Application.Models;
var builder = WebApplication.CreateBuilder(args);
@ -15,6 +20,71 @@ builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddDbContext<TransferDestinationDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DeveloperDbConnection")));;
builder.Services.AddSwaggerGen(swagger =>
{
swagger.SwaggerDoc("v1",
new OpenApiInfo
{
Title = "API Title",
Version = "V1",
Description = "API Description"
});
var securitySchema = new OpenApiSecurityScheme
{
Description = "Authorization header using the Bearer scheme. Example \"Authorization: Bearer {token}\"",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.Http,
Scheme = "Bearer",
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
};
swagger.AddSecurityDefinition(securitySchema.Reference.Id, securitySchema);
swagger.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{securitySchema,Array.Empty<string>() }
});
});
builder.Services.AddAuthentication(f =>
{
f.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
f.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(k =>
{
var Key = Encoding.UTF8.GetBytes(builder.Configuration["JWT:Key"]);
k.SaveToken = true;
k.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["JWT:Issuer"],
ValidAudience = builder.Configuration["JWT:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Key),
ClockSkew = TimeSpan.Zero
};
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = false;
options.ExpireTimeSpan = TimeSpan.FromMinutes(5);
options.LoginPath = "/Login";
options.SlidingExpiration = true;
});
var app = builder.Build();
// Configure the HTTP request pipeline.
@ -29,12 +99,21 @@ else
app.UseHsts();
}
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "v1");
});
//app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseAuthentication();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();

View File

@ -8,8 +8,13 @@
<ItemGroup>
<PackageReference Include="GoogleApi" Version="5.2.4" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.14" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="7.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.13" />
<PackageReference Include="Microsoft.OpenApi" Version="1.6.11" />
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.5.0" />
</ItemGroup>
<ItemGroup>

View File

@ -4,5 +4,10 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
},
"JWT": {
"Key": "Cee4400-rDMFkVvHPufyLDSzbfu2grgRhpepos299IhTLOXsljkcpt3yUR4RRjPQ",
"Issuer": "https://localhost:7116",
"Audience": "http://localhost:7116"
}
}

View File

@ -8,5 +8,10 @@
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"JWT": {
"Key": "Cee4400-rDMFkVvHPufyLDSzbfu2grgRhpepos299IhTLOXsljkcpt3yUR4RRjPQ",
"Issuer": "http://localhost:5000",
"Audience": "http://localhost:5000"
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TIAMWebApp.Shared.Application.Interfaces
{
public interface ISecureStorageHandler
{
public Task SaveToSecureStorageAsync(string key, string value);
public Task<string> GetFromSecureStorageAsync(string key);
}
}

View File

@ -23,5 +23,7 @@ namespace TIAMWebApp.Shared.Application.Interfaces
public Task<string> TestUserApi(int Param);
public Task<Dictionary<int, string>> GetUserRolesAsync(User user);
Task<bool> RefreshToken();
}
}

View File

@ -8,9 +8,10 @@ namespace TIAMWebApp.Shared.Application.Models
{
public class APIUrls
{
public const string UserTest = "UserAPI/test1";
public const string AuthenticateUser = "UserAPI/Auth";
public const string CreateUser = "UserAPI/CreateUser";
public const string UserTest = "api/UserAPI/test1";
public const string AuthenticateUser = "api/UserAPI/AuthenticateUser";
public const string CreateUser = "api/UserAPI/CreateUser";
public const string RefreshToken = "api/UserAPI/RefreshToken";
public const string WeatherForecast = "WeatherForecastAPI";
public const string PopulationStructure = "PopulationStructureAPI";
}

View File

@ -0,0 +1,9 @@

namespace TIAMWebApp.Shared.Application.Models
{
public class AuthenticateRequestAndResponse
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace TIAMWebApp.Shared.Application.Models
{
public class AuthenticationResponse
{
public string? AccessToken { get; set; }
public string? RefreshToken { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TIAMWebApp.Shared.Application.Models.ClientSide
{
public class Setting
{
public static UserBasicDetails UserBasicDetails { get; set; }
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TIAMWebApp.Shared.Application.Models.ClientSide
{
public class UserBasicDetails
{
public string UserId { get; set; }
public string Email { get; set; }
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public UserBasicDetails(string userId, string email, string token, string refreshToken)
{
UserId = userId;
Email = email;
AccessToken = token;
RefreshToken = refreshToken;
}
}
}

View File

@ -0,0 +1,16 @@
using TIAMWebApp.Shared.Application.Models;
namespace TIAMWebApp.Server.ModelsTIAMWebApp.Shared.Application.Models
{
public class ErrorResponse
{
public static MainResponse ReturnErrorResponse(string errorMessage)
{
return new MainResponse
{
ErrorMessage = errorMessage,
IsSuccess = true
};
}
}
}

View File

@ -0,0 +1,9 @@
namespace TIAMWebApp.Shared.Application.Models
{
public class MainResponse
{
public bool IsSuccess { get; set; }
public string? ErrorMessage { get; set; }
public object? Content { get; set; }
}
}

View File

@ -1,12 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TIAMWebApp.Shared.Application.Models
namespace TIAMWebApp.Shared.Application.Models
{
public class User
public class User
{
public Guid Id { get; set; }
public string? Email { get; set; }
@ -15,6 +9,7 @@ namespace TIAMWebApp.Shared.Application.Models
public bool IsLoggedIn { get; set; }
public UserType UserType { get; set; }
public int UserRoles { get; set; }
public string? RefreshToken { get; set; }
public Dictionary<int, string> UserRolesDictionary { get; set; }
public User(string email, string phonenumber, string password)
@ -26,6 +21,15 @@ namespace TIAMWebApp.Shared.Application.Models
UserRolesDictionary = new Dictionary<int, string>();
}
public User(Guid id, string email, string phonenumber, string password)
{
Id = id;
Email = email;
Password = password;
PhoneNumber = phonenumber;
UserRolesDictionary = new Dictionary<int, string>();
}
}
public enum UserType

View File

@ -16,6 +16,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.1.39" />
<PackageReference Include="Microsoft.JSInterop" Version="7.0.10" />
</ItemGroup>

View File

@ -1,21 +1,10 @@
using Microsoft.JSInterop;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TIAMWebApp.Shared.Application.Models;
namespace TIAMWebApp.Shared.Application.Utility
{
internal class LogToBrowserConsole
public class LogToBrowserConsole
{
private readonly JSRuntime jsRuntime;
public LogToBrowserConsole(JSRuntime jSRuntime)
{
this.jsRuntime = jsRuntime;
}
private readonly JSRuntime jsRuntime;
public void LogToBC(string message)
{