Auth System
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<Router AppAssembly="@typeof(App).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
|
||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
|
||||
</AuthorizeRouteView>
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
|
||||
</Found>
|
||||
<NotFound>
|
||||
|
||||
133
FoodsharingSiegen.Server/Auth/AuthHelper.cs
Normal file
133
FoodsharingSiegen.Server/Auth/AuthHelper.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace FoodsharingSiegen.Server.Auth
|
||||
{
|
||||
/// <summary>
|
||||
/// The auth helper class (a. beging, 04.04.2022)
|
||||
/// </summary>
|
||||
public static class AuthHelper
|
||||
{
|
||||
#region Public Method Decrypt
|
||||
|
||||
/// <summary>
|
||||
/// Decrypts the crypted text (a. beging, 04.04.2022)
|
||||
/// </summary>
|
||||
/// <param name="cryptedText">The crypted text</param>
|
||||
/// <returns>The string</returns>
|
||||
public static string Decrypt(string cryptedText)
|
||||
{
|
||||
CreateAlgorithm(out var tripleDes);
|
||||
|
||||
var toEncryptArray = Convert.FromBase64String(cryptedText);
|
||||
|
||||
|
||||
|
||||
var cTransform = tripleDes.CreateDecryptor();
|
||||
var resultArray = cTransform.TransformFinalBlock(toEncryptArray, 0, toEncryptArray.Length);
|
||||
|
||||
tripleDes.Clear();
|
||||
|
||||
return Encoding.UTF8.GetString(resultArray);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Method Encrypt
|
||||
|
||||
/// <summary>
|
||||
/// Encrypts the plain text (a. beging, 04.04.2022)
|
||||
/// </summary>
|
||||
/// <param name="plainText">The plain text</param>
|
||||
/// <returns>The string</returns>
|
||||
public static string Encrypt(string plainText)
|
||||
{
|
||||
CreateAlgorithm(out var tripleDes);
|
||||
|
||||
var toEncryptArray = Encoding.UTF8.GetBytes(plainText );
|
||||
|
||||
|
||||
|
||||
var cTransform = tripleDes.CreateEncryptor();
|
||||
|
||||
var resultArray = cTransform.TransformFinalBlock(toEncryptArray, 0, toEncryptArray.Length);
|
||||
|
||||
tripleDes.Clear();
|
||||
|
||||
return Convert.ToBase64String(resultArray, 0, resultArray.Length);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Method GetSigningKey
|
||||
|
||||
/// <summary>
|
||||
/// Gets the signing key (a. beging, 04.04.2022)
|
||||
/// </summary>
|
||||
/// <returns>The security key</returns>
|
||||
public static SecurityKey GetSigningKey() => new SymmetricSecurityKey(Encoding.ASCII.GetBytes(SigningKey));
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Method ValidateToken
|
||||
|
||||
/// <summary>
|
||||
/// Validates the token using the specified token (a. beging, 04.04.2022)
|
||||
/// </summary>
|
||||
/// <param name="token">The token</param>
|
||||
/// <returns>A task containing the bool</returns>
|
||||
public static async Task<bool> ValidateToken(string? token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var result = await tokenHandler.ValidateTokenAsync(token, new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = GetSigningKey(),
|
||||
|
||||
ValidateAudience = true,
|
||||
ValidAudience = "FS-Siegen",
|
||||
|
||||
ValidateIssuer = true,
|
||||
ValidIssuer = "FS-Siegen"
|
||||
});
|
||||
|
||||
return result.IsValid;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Method CreateAlgorithm
|
||||
|
||||
/// <summary>
|
||||
/// Creates the algorithm using the specified triple des (a. beging, 04.04.2022)
|
||||
/// </summary>
|
||||
/// <param name="tripleDes">The triple des</param>
|
||||
private static void CreateAlgorithm(out TripleDES tripleDes)
|
||||
{
|
||||
var md5 = MD5.Create();
|
||||
var keyArray = md5.ComputeHash(Encoding.UTF8.GetBytes(SigningKey));
|
||||
md5.Clear();
|
||||
|
||||
tripleDes = TripleDES.Create();
|
||||
tripleDes.Key = keyArray;
|
||||
tripleDes.Mode = CipherMode.ECB;
|
||||
tripleDes.Padding = PaddingMode.PKCS7;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// The signing key
|
||||
/// </summary>
|
||||
public const string SigningKey = "2uasw2§$%1nd47n9s43&%Zs3529s23&/%AW";
|
||||
}
|
||||
}
|
||||
147
FoodsharingSiegen.Server/Auth/AuthService.cs
Normal file
147
FoodsharingSiegen.Server/Auth/AuthService.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using FoodsharingSiegen.Contracts;
|
||||
using FoodsharingSiegen.Contracts.Entity;
|
||||
using FoodsharingSiegen.Server.Data;
|
||||
using FoodsharingSiegen.Server.Data.Service;
|
||||
using FoodsharingSiegen.Server.Service;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace FoodsharingSiegen.Server.Auth
|
||||
{
|
||||
/// <summary>
|
||||
/// The auth service class (a. beging, 04.04.2022)
|
||||
/// </summary>
|
||||
/// <seealso cref="ServiceBase"/>
|
||||
public class AuthService : ServiceBase
|
||||
{
|
||||
#region Public Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the user (ab)
|
||||
/// </summary>
|
||||
public User? User { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Fields
|
||||
|
||||
/// <summary>
|
||||
/// The authentication state provider
|
||||
/// </summary>
|
||||
private readonly AuthenticationStateProvider _authenticationStateProvider;
|
||||
|
||||
/// <summary>
|
||||
/// The local storage service
|
||||
/// </summary>
|
||||
private readonly LocalStorageService _localStorageService;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Setup/Teardown
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthService"/> class
|
||||
/// </summary>
|
||||
/// <param name="context">The context</param>
|
||||
/// <param name="localStorageService">The local storage service</param>
|
||||
/// <param name="authenticationStateProvider">The authentication state provider</param>
|
||||
public AuthService(FsContext context, LocalStorageService localStorageService, AuthenticationStateProvider authenticationStateProvider) : base(context)
|
||||
{
|
||||
_localStorageService = localStorageService;
|
||||
_authenticationStateProvider = authenticationStateProvider;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Method Login
|
||||
|
||||
/// <summary>
|
||||
/// Logins the mail address (a. beging, 04.04.2022)
|
||||
/// </summary>
|
||||
/// <param name="mailAddress">The mail address</param>
|
||||
/// <param name="password">The password</param>
|
||||
/// <returns>A task containing the operation result</returns>
|
||||
public async Task<OperationResult> Login(string mailAddress, string password)
|
||||
{
|
||||
#region Ensure Admin
|
||||
|
||||
var existingTroogS = await Context.Users.AnyAsync(x => x.Mail == "fs@beging.de");
|
||||
if (!existingTroogS)
|
||||
{
|
||||
var troogs = new User
|
||||
{
|
||||
Name = "Andre",
|
||||
Mail = "fs@beging.de",
|
||||
Type = UserType.Admin,
|
||||
Created = DateTime.UtcNow,
|
||||
EncryptedPassword = "qSIxTZo7J8M="
|
||||
};
|
||||
|
||||
await Context.Users.AddAsync(troogs);
|
||||
await Context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
#endregion Ensure Admin
|
||||
|
||||
var encryptedPassword = AuthHelper.Encrypt(password);
|
||||
|
||||
User = await Context.Users.FirstOrDefaultAsync(x => x.Mail.ToLower() == mailAddress.ToLower() && x.EncryptedPassword == encryptedPassword);
|
||||
|
||||
if (User != null)
|
||||
{
|
||||
|
||||
|
||||
// Daten korrekt
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var tokenDescriptor = new SecurityTokenDescriptor
|
||||
{
|
||||
Subject = new ClaimsIdentity(new[]
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, User.Id.ToString()),
|
||||
}),
|
||||
Expires = DateTime.UtcNow.AddDays(30),
|
||||
Issuer = "FS-Siegen",
|
||||
Audience = "FS-Siegen",
|
||||
SigningCredentials = new SigningCredentials(AuthHelper.GetSigningKey(), SecurityAlgorithms.HmacSha256Signature)
|
||||
};
|
||||
|
||||
var token = tokenHandler.CreateToken(tokenDescriptor);
|
||||
var serializedToken = tokenHandler.WriteToken(token);
|
||||
|
||||
await _localStorageService.SetItem(StorageKeys.TokenKey, serializedToken);
|
||||
|
||||
return new OperationResult();
|
||||
}
|
||||
|
||||
return new OperationResult(new Exception("Invalid"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Method Logout
|
||||
|
||||
/// <summary>
|
||||
/// Logouts this instance (a. beging, 04.04.2022)
|
||||
/// </summary>
|
||||
/// <returns>A task containing the operation result</returns>
|
||||
public async Task<OperationResult> Logout()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _localStorageService.RemoveItem(StorageKeys.TokenKey);
|
||||
User = null;
|
||||
((TokenAuthStateProvider) _authenticationStateProvider).MarkUserAsLoggedOut();
|
||||
return new OperationResult();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new OperationResult(e);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
78
FoodsharingSiegen.Server/Auth/LocalStorageService.cs
Normal file
78
FoodsharingSiegen.Server/Auth/LocalStorageService.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace FoodsharingSiegen.Server.Service
|
||||
{
|
||||
/// <summary>
|
||||
/// The local storage service class (a. beging, 02.04.2022)
|
||||
/// </summary>
|
||||
public class LocalStorageService
|
||||
{
|
||||
#region Private Fields
|
||||
|
||||
/// <summary>
|
||||
/// The js runtime
|
||||
/// </summary>
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Setup/Teardown
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="jsRuntime"></param>
|
||||
public LocalStorageService(IJSRuntime jsRuntime) => _jsRuntime = jsRuntime;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Method GetItem
|
||||
|
||||
/// <summary>
|
||||
/// Ein Item aus dem LocalStorage laden
|
||||
/// </summary>
|
||||
/// <param name="key">Der Key des Items</param>
|
||||
/// <typeparam name="T">Typ des Item</typeparam>
|
||||
/// <returns></returns>
|
||||
public async Task<T?> GetItem<T>(string key)
|
||||
{
|
||||
var json = await _jsRuntime.InvokeAsync<string>("localStorage.getItem", key);
|
||||
|
||||
if (json == null)
|
||||
return default;
|
||||
|
||||
return JsonSerializer.Deserialize<T>(json);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Method RemoveItem
|
||||
|
||||
/// <summary>
|
||||
/// Ein Item aus dem LocalStorage löschen
|
||||
/// </summary>
|
||||
/// <param name="key">Der Key des Items</param>
|
||||
public async Task RemoveItem(string key)
|
||||
{
|
||||
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", key);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Method SetItem
|
||||
|
||||
/// <summary>
|
||||
/// Ein Item in den LocalStorage schreiben
|
||||
/// </summary>
|
||||
/// <param name="key">Der Key des Items</param>
|
||||
/// <param name="value">Das Item</param>
|
||||
/// <typeparam name="T">Typ des Item</typeparam>
|
||||
public async Task SetItem<T>(string key, T value)
|
||||
{
|
||||
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", key, JsonSerializer.Serialize(value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
79
FoodsharingSiegen.Server/Auth/TokenAuthStateProvider.cs
Normal file
79
FoodsharingSiegen.Server/Auth/TokenAuthStateProvider.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System.Security.Claims;
|
||||
using FoodsharingSiegen.Contracts;
|
||||
using FoodsharingSiegen.Server.Auth;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
|
||||
namespace FoodsharingSiegen.Server.Service
|
||||
{
|
||||
/// <summary>
|
||||
/// The token auth state provider class (a. beging, 02.04.2022)
|
||||
/// </summary>
|
||||
/// <seealso cref="AuthenticationStateProvider"/>
|
||||
public class TokenAuthStateProvider : AuthenticationStateProvider
|
||||
{
|
||||
#region Private Fields
|
||||
|
||||
/// <summary> LocalStorageService </summary>
|
||||
private readonly LocalStorageService _localStorageService;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Setup/Teardown
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="localStorageService"></param>
|
||||
public TokenAuthStateProvider(LocalStorageService localStorageService) => _localStorageService = localStorageService;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Override GetAuthenticationStateAsync
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/// <summary> Get the current authenticationstate </summary>
|
||||
/// <remarks> A. Beging, 02.02.2022. </remarks>
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
{
|
||||
var token = await _localStorageService.GetItem<string>(StorageKeys.TokenKey);
|
||||
var tokenValid = await AuthHelper.ValidateToken(token);
|
||||
|
||||
var identity = new ClaimsIdentity();
|
||||
if (tokenValid)
|
||||
identity = new ClaimsIdentity(new[]
|
||||
{
|
||||
new Claim(ClaimTypes.Name, "user")
|
||||
}, "TODO");
|
||||
|
||||
var claimsPrincipal = new ClaimsPrincipal(identity);
|
||||
return new AuthenticationState(claimsPrincipal);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Method MarkUserAsAuthenticated
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/// <summary> Mark user as authenticated. </summary>
|
||||
/// <remarks> A. Beging, 02.02.2022. </remarks>
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
public void MarkUserAsAuthenticated() => NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Method MarkUserAsLoggedOut
|
||||
|
||||
/// <summary>
|
||||
/// Marks the user as logged out (a. beging, 02.04.2022)
|
||||
/// </summary>
|
||||
public void MarkUserAsLoggedOut()
|
||||
{
|
||||
var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity());
|
||||
var authState = Task.FromResult(new AuthenticationState(anonymousUser));
|
||||
NotifyAuthenticationStateChanged(authState);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
29
FoodsharingSiegen.Server/Pages/Login.razor
Normal file
29
FoodsharingSiegen.Server/Pages/Login.razor
Normal file
@@ -0,0 +1,29 @@
|
||||
@page "/login"
|
||||
@using FoodsharingSiegen.Server.Service
|
||||
@using FoodsharingSiegen.Server.Auth
|
||||
@layout LoginLayout
|
||||
|
||||
@inject AuthService AuthService
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
@code
|
||||
{
|
||||
private string? Mailaddress { get; set; }
|
||||
|
||||
private string? Password { get; set; }
|
||||
|
||||
private async Task PerformLogin()
|
||||
{
|
||||
//Todo Eingaben Validieren [04.04.22 - Andre Beging]
|
||||
|
||||
var loginR = await AuthService.Login(Mailaddress, Password);
|
||||
if (loginR.Success)
|
||||
{
|
||||
NavigationManager.NavigateTo("/", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<TextEdit @bind-Text="Mailaddress" Placeholder="Mail"></TextEdit>
|
||||
<TextEdit @bind-Text="Password" Placeholder="Password"></TextEdit>
|
||||
<Button Clicked="PerformLogin">Go</Button>
|
||||
17
FoodsharingSiegen.Server/Pages/Logout.razor
Normal file
17
FoodsharingSiegen.Server/Pages/Logout.razor
Normal file
@@ -0,0 +1,17 @@
|
||||
@layout LoginLayout
|
||||
@page "/logout"
|
||||
@using FoodsharingSiegen.Server.Service
|
||||
@using FoodsharingSiegen.Server.Auth
|
||||
|
||||
@inject AuthService AuthService
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
@code {
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var logoutR = await AuthService.Logout();
|
||||
if(logoutR.Success) NavigationManager.NavigateTo("/");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,4 +5,4 @@
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
<component type="typeof(App)" render-mode="ServerPrerendered"/>
|
||||
<component type="typeof(App)" render-mode="Server"/>
|
||||
@@ -1,8 +1,11 @@
|
||||
using Blazorise;
|
||||
using Blazorise.Icons.Material;
|
||||
using Blazorise.Material;
|
||||
using FoodsharingSiegen.Server.Auth;
|
||||
using FoodsharingSiegen.Server.Data;
|
||||
using FoodsharingSiegen.Server.Data.Service;
|
||||
using FoodsharingSiegen.Server.Service;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@@ -15,7 +18,11 @@ builder.Services.AddDbContextFactory<FsContext>(opt =>
|
||||
opt.UseSqlite($"Data Source=app.db"));
|
||||
|
||||
// DI
|
||||
builder.Services.AddScoped<LocalStorageService>();
|
||||
builder.Services.AddScoped<AuthenticationStateProvider, TokenAuthStateProvider>();
|
||||
|
||||
builder.Services.AddScoped<FsContext>();
|
||||
builder.Services.AddScoped<AuthService>();
|
||||
builder.Services.AddScoped<UserService>();
|
||||
builder.Services.AddScoped<ProspectService>();
|
||||
|
||||
|
||||
11
FoodsharingSiegen.Server/Shared/LoginLayout.razor
Normal file
11
FoodsharingSiegen.Server/Shared/LoginLayout.razor
Normal file
@@ -0,0 +1,11 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<PageTitle>Login</PageTitle>
|
||||
|
||||
<div class="page">
|
||||
<main>
|
||||
<article class="content px-4">
|
||||
@Body
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
@@ -2,18 +2,25 @@
|
||||
|
||||
<PageTitle>FoodsharingSiegen.Server</PageTitle>
|
||||
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
<NavMenu/>
|
||||
</div>
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
<NavMenu/>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<div class="top-row px-4">
|
||||
|
||||
<main>
|
||||
<div class="top-row px-4">
|
||||
|
||||
</div>
|
||||
|
||||
<article class="content px-4">
|
||||
@Body
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<article class="content px-4">
|
||||
@Body
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
<RedirectToLogin/>
|
||||
</NotAuthorized>
|
||||
</AuthorizeView>
|
||||
@@ -19,9 +19,13 @@
|
||||
<span class="fas fa-users mr-1" aria-hidden="true" style="font-size: 1.4em;"></span> Benutzer
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3 pt-5">
|
||||
<NavLink class="nav-link" href="logout" Match="NavLinkMatch.All">
|
||||
<span class="fa-solid fa-door-open mr-1" aria-hidden="true" style="font-size: 1.4em;"></span> Ausloggen
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool collapseNavMenu = true;
|
||||
|
||||
|
||||
10
FoodsharingSiegen.Server/Shared/RedirectToLogin.razor
Normal file
10
FoodsharingSiegen.Server/Shared/RedirectToLogin.razor
Normal file
@@ -0,0 +1,10 @@
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
@code
|
||||
{
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
NavigationManager.NavigateTo("/login");
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user