Add cleint-side and server-side services

This commit is contained in:
2026-01-30 21:44:55 +01:00
parent e35125c306
commit 6df1ca6d8f
9 changed files with 154 additions and 43 deletions

View File

@@ -0,0 +1,39 @@
using BlazorPolicyAuth.Models.ViewModels;
using Microsoft.AspNetCore.Components.Authorization;
namespace BlazorPolicyAuth.App.Services.AuthService;
public class AuthService(HttpClient httpClient, AuthenticationStateProvider authenticationStateProvider)
: IAuthService
{
public async Task<ServiceResponse<int>> Register(UserRegister request)
{
var result = await httpClient.PostAsJsonAsync("api/auth/register", request);
return await ReadResponse<int>(result);
}
public async Task<ServiceResponse<string>> Login(UserLogin request)
{
var result = await httpClient.PostAsJsonAsync("api/auth/login", request);
return await ReadResponse<string>(result);
}
public async Task<ServiceResponse<bool>> ChangePassword(UserChangePassword request)
{
var result = await httpClient.PostAsJsonAsync("api/auth/change-password", request.Password);
return await ReadResponse<bool>(result);
}
public async Task<bool> IsUserAuthenticated()
{
return (await authenticationStateProvider.GetAuthenticationStateAsync()).User.Identity.IsAuthenticated;
}
private async Task<ServiceResponse<T>> ReadResponse<T>(HttpResponseMessage response)
{
if (response.IsSuccessStatusCode)
return await response.Content.ReadFromJsonAsync<ServiceResponse<T>>();
return new ServiceResponse<T> { Success = false, Message = await response.Content.ReadAsStringAsync() };
}
}

View File

@@ -0,0 +1,11 @@
using BlazorPolicyAuth.Models.ViewModels;
namespace BlazorPolicyAuth.App.Services.AuthService;
public interface IAuthService
{
Task<ServiceResponse<int>> Register(UserRegister request);
Task<ServiceResponse<string>> Login(UserLogin request);
Task<ServiceResponse<bool>> ChangePassword(UserChangePassword request);
Task<bool> IsUserAuthenticated();
}

View File

@@ -1,10 +1,6 @@
@page "/login" @page "/login"
@using System.Security.Claims @using BlazorPolicyAuth.App.Services.AuthService
@using BlazorPolicyAuth.Data
@using BlazorPolicyAuth.Models.ViewModels @using BlazorPolicyAuth.Models.ViewModels
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Authentication.Cookies
@using Microsoft.EntityFrameworkCore
@inject AuthenticationStateProvider AuthenticationStateProvider @inject AuthenticationStateProvider AuthenticationStateProvider
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IAuthService AuthService @inject IAuthService AuthService
@@ -52,7 +48,7 @@
{ {
Console.WriteLine("***"); Console.WriteLine("***");
Console.WriteLine(userLogin.Email); Console.WriteLine(userLogin.Email);
var result = await AuthService.Login(userLogin.Email, userLogin.Password); var result = await AuthService.Login(userLogin);
if (result.Success) if (result.Success)
{ {
_errorMessage = string.Empty; _errorMessage = string.Empty;

View File

@@ -1,13 +1,12 @@
@page "/register" @page "/register"
@using BlazorPolicyAuth.App.Services.AuthService
@using BlazorPolicyAuth.Models.ViewModels @using BlazorPolicyAuth.Models.ViewModels
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject NavigationManager NavigationManager
@inject IAuthService AuthService @inject IAuthService AuthService
<title>Register</title> <title>Register</title>
<EditForm Model="user" OnValidSubmit="HandleRegistration" FormName="registerForm"> <EditForm Model="user" OnValidSubmit="HandleRegistration" OnInvalidSubmit="HandleInvalidSubmit" FormName="registerForm">
<DataAnnotationsValidator/> <DataAnnotationsValidator/>
<div class="d-flex justify-content-center align-items-center"> <div class="d-flex justify-content-center align-items-center">
<div class="col-md-4 p-5 shadow-sm border rounded-3"> <div class="col-md-4 p-5 shadow-sm border rounded-3">
@@ -46,16 +45,24 @@
</div> </div>
@code { @code {
UserRegister user = new(); [SupplyParameterFromForm] private UserRegister user { get; set; }
private string message = string.Empty; private string message = string.Empty;
private string messageCssClass = string.Empty; private string messageCssClass = string.Empty;
private string errorMessage = string.Empty; private string errorMessage = string.Empty;
async void HandleRegistration() protected override void OnInitialized() => user ??= new();
async Task HandleRegistration()
{ {
var result = await AuthService.Register(user.Email, user.Password); var result = await AuthService.Register(user);
message = result.Message; message = result.Message;
messageCssClass = result.Success ? "text-success" : "text-danger"; messageCssClass = result.Success ? "text-success" : "text-danger";
} }
async Task HandleInvalidSubmit()
{
message = "Data annotations validation failed.";
messageCssClass = "text-danger";
}
} }

View File

@@ -9,5 +9,4 @@
@using BlazorPolicyAuth @using BlazorPolicyAuth
@using BlazorPolicyAuth.Components @using BlazorPolicyAuth.Components
@using BlazorPolicyAuth.Components.Layout @using BlazorPolicyAuth.Components.Layout
@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Authorization
@using BlazorPolicyAuth.Services.AuthService

View File

@@ -0,0 +1,55 @@
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
namespace BlazorPolicyAuth;
/// <summary>
/// Source: https://www.duracellko.net/posts/2020/06/hosting-both-blazor-server-and-webassembly
/// </summary>
/// <param name="httpClient"></param>
/// <param name="server"></param>
/// <param name="applicationLifetime"></param>
public class HttpClientSetupService(
HttpClient httpClient,
IServer server,
IHostApplicationLifetime applicationLifetime)
: BackgroundService
{
private readonly HttpClient _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
private readonly IServer _server = server ?? throw new ArgumentNullException(nameof(server));
private readonly IHostApplicationLifetime _applicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime));
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
var applicationStartedToken = _applicationLifetime.ApplicationStarted;
if (applicationStartedToken.IsCancellationRequested)
{
ConfigureHttpClient();
}
else
{
applicationStartedToken.Register(ConfigureHttpClient);
}
return Task.CompletedTask;
}
private void ConfigureHttpClient()
{
var serverAddresses = _server.Features.Get<IServerAddressesFeature>();
var address = serverAddresses.Addresses.FirstOrDefault();
if (address == null)
{
// Default ASP.NET Core Kestrel endpoint
address = "http://localhost:5000";
}
else
{
address = address.Replace("*", "localhost", StringComparison.Ordinal);
address = address.Replace("+", "localhost", StringComparison.Ordinal);
address = address.Replace("[::]", "localhost", StringComparison.Ordinal);
}
var baseUri = new Uri(address);
_httpClient.BaseAddress = baseUri;
}
}

View File

@@ -1,9 +1,11 @@
using BlazorPolicyAuth; using BlazorPolicyAuth;
using BlazorPolicyAuth.Components; using BlazorPolicyAuth.Components;
using BlazorPolicyAuth.Data; using BlazorPolicyAuth.Data;
using BlazorPolicyAuth.Models.ViewModels;
using BlazorPolicyAuth.Services.AuthService; using BlazorPolicyAuth.Services.AuthService;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using ClientServices = BlazorPolicyAuth.App.Services;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -31,10 +33,18 @@ builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationSc
}); });
builder.Services.AddCascadingAuthenticationState(); builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddHttpContextAccessor(); builder.Services.AddHttpContextAccessor();
// Blazor client services
builder.Services.AddScoped<ClientServices.AuthService.IAuthService, ClientServices.AuthService.AuthService>();
// Blazor server services
builder.Services.AddScoped<IAuthService, AuthService>();
// Get server base address when application starts to properly configure HttpClient for client service to call server service
builder.Services.AddSingleton<HttpClient>();
builder.Services.AddSingleton<IHostedService, HttpClientSetupService>();
var app = builder.Build(); var app = builder.Build();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
@@ -53,4 +63,8 @@ app.MapStaticAssets();
app.MapRazorComponents<App>() app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode(); .AddInteractiveServerRenderMode();
// Blazor server routing
app.MapPost("/api/auth/register", async (UserRegister request, IAuthService authService) =>
await authService.Register(request));
app.Run(); app.Run();

View File

@@ -12,43 +12,33 @@ using System.Text;
namespace BlazorPolicyAuth.Services.AuthService; namespace BlazorPolicyAuth.Services.AuthService;
public class AuthService : IAuthService public class AuthService(AppDbContext context, IConfiguration configuration, IHttpContextAccessor httpContextAccessor)
: IAuthService
{ {
private readonly AppDbContext _context; public async Task<ServiceResponse<int>> Register (UserRegister request)
private readonly IConfiguration _configuration;
private readonly IHttpContextAccessor _httpContextAccessor;
public AuthService(AppDbContext context, IConfiguration configuration, IHttpContextAccessor httpContextAccessor)
{ {
_context = context; if (await UserExists(request.Email))
_configuration = configuration;
_httpContextAccessor = httpContextAccessor;
}
public async Task<ServiceResponse<int>> Register ( string email, string password)
{
if (await UserExists(email))
{ {
return new ServiceResponse<int> { Success = false, Message = "User already exist." }; return new ServiceResponse<int> { Success = false, Message = "User already exist." };
} }
CreatePasswordHash(password, out byte[] passwordHash, out byte[] passwordSalt); CreatePasswordHash(request.Password, out byte[] passwordHash, out byte[] passwordSalt);
var user = new UserAccount var user = new UserAccount
{ {
UserName = email, UserName = request.Email,
PasswordHash = passwordHash, PasswordHash = passwordHash,
PasswordSalt = passwordSalt PasswordSalt = passwordSalt
}; };
_context.UserAccounts.Add(user); context.UserAccounts.Add(user);
await _context.SaveChangesAsync(); await context.SaveChangesAsync();
return new ServiceResponse<int> { Data = user.Id, Message = "Registration successful" }; return new ServiceResponse<int> { Data = user.Id, Message = "Registration successful" };
} }
public async Task<bool> UserExists(string email) public async Task<bool> UserExists(string email)
{ {
if (await _context.UserAccounts.AnyAsync(user => user.UserName.ToLower().Equals(email.ToLower()))) if (await context.UserAccounts.AnyAsync(user => user.UserName.ToLower().Equals(email.ToLower())))
{ {
return true; return true;
} }
@@ -58,7 +48,7 @@ public class AuthService : IAuthService
public async Task<ServiceResponse<string>> Login(string email, string password) public async Task<ServiceResponse<string>> Login(string email, string password)
{ {
var response = new ServiceResponse<string>(); var response = new ServiceResponse<string>();
var user = await _context.UserAccounts.FirstOrDefaultAsync(u => u.UserName.ToLower().Equals(email.ToLower())); var user = await context.UserAccounts.FirstOrDefaultAsync(u => u.UserName.ToLower().Equals(email.ToLower()));
if (user == null) if (user == null)
{ {
response.Success = false; response.Success = false;
@@ -79,7 +69,7 @@ public class AuthService : IAuthService
public async Task<ServiceResponse<bool>> ChangePassword(int userId, string newPassword) public async Task<ServiceResponse<bool>> ChangePassword(int userId, string newPassword)
{ {
var user = await _context.UserAccounts.FindAsync(userId); var user = await context.UserAccounts.FindAsync(userId);
if (user == null) if (user == null)
{ {
return new ServiceResponse<bool> { Success = false, Message = "User not found." }; return new ServiceResponse<bool> { Success = false, Message = "User not found." };
@@ -90,18 +80,18 @@ public class AuthService : IAuthService
user.PasswordHash = passwordHash; user.PasswordHash = passwordHash;
user.PasswordSalt = passwordSalt; user.PasswordSalt = passwordSalt;
await _context.SaveChangesAsync(); await context.SaveChangesAsync();
return new ServiceResponse<bool> { Data = true, Message = "Password has been changed." }; return new ServiceResponse<bool> { Data = true, Message = "Password has been changed." };
} }
public int GetUserId() => int.Parse(_httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier)); public int GetUserId() => int.Parse(httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier));
public string GetUserEmail() => _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.Name); public string GetUserEmail() => httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.Name);
public async Task<UserAccount> GetUserByEmail(string email) public async Task<UserAccount> GetUserByEmail(string email)
{ {
return await _context.UserAccounts.FirstOrDefaultAsync(u => u.UserName.Equals(email)); return await context.UserAccounts.FirstOrDefaultAsync(u => u.UserName.Equals(email));
} }
private static void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt) private static void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt)
@@ -127,7 +117,7 @@ public class AuthService : IAuthService
new(ClaimTypes.Role, user.Role) new(ClaimTypes.Role, user.Role)
}; };
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.GetSection("AppSettings:Token").Value)); var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration.GetSection("AppSettings:Token").Value));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);

View File

@@ -5,7 +5,7 @@ namespace BlazorPolicyAuth.Services.AuthService;
public interface IAuthService public interface IAuthService
{ {
Task<ServiceResponse<int>> Register(string email, string password); Task<ServiceResponse<int>> Register(UserRegister request);
Task<bool> UserExists(string email); Task<bool> UserExists(string email);
Task<ServiceResponse<string>> Login(string email, string password); Task<ServiceResponse<string>> Login(string email, string password);
Task<ServiceResponse<bool>> ChangePassword(int userId, string newPassword); Task<ServiceResponse<bool>> ChangePassword(int userId, string newPassword);