diff --git a/BlazorPolicyAuth/App.Services/AuthService/AuthService.cs b/BlazorPolicyAuth/App.Services/AuthService/AuthService.cs new file mode 100644 index 0000000..f0c687a --- /dev/null +++ b/BlazorPolicyAuth/App.Services/AuthService/AuthService.cs @@ -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> Register(UserRegister request) + { + var result = await httpClient.PostAsJsonAsync("api/auth/register", request); + return await ReadResponse(result); + } + + public async Task> Login(UserLogin request) + { + var result = await httpClient.PostAsJsonAsync("api/auth/login", request); + return await ReadResponse(result); + } + + public async Task> ChangePassword(UserChangePassword request) + { + var result = await httpClient.PostAsJsonAsync("api/auth/change-password", request.Password); + return await ReadResponse(result); + } + + public async Task IsUserAuthenticated() + { + return (await authenticationStateProvider.GetAuthenticationStateAsync()).User.Identity.IsAuthenticated; + } + + private async Task> ReadResponse(HttpResponseMessage response) + { + if (response.IsSuccessStatusCode) + return await response.Content.ReadFromJsonAsync>(); + + return new ServiceResponse { Success = false, Message = await response.Content.ReadAsStringAsync() }; + } +} \ No newline at end of file diff --git a/BlazorPolicyAuth/App.Services/AuthService/IAuthService.cs b/BlazorPolicyAuth/App.Services/AuthService/IAuthService.cs new file mode 100644 index 0000000..39e3646 --- /dev/null +++ b/BlazorPolicyAuth/App.Services/AuthService/IAuthService.cs @@ -0,0 +1,11 @@ +using BlazorPolicyAuth.Models.ViewModels; + +namespace BlazorPolicyAuth.App.Services.AuthService; + +public interface IAuthService +{ + Task> Register(UserRegister request); + Task> Login(UserLogin request); + Task> ChangePassword(UserChangePassword request); + Task IsUserAuthenticated(); +} \ No newline at end of file diff --git a/BlazorPolicyAuth/Components/Pages/Account/Login.razor b/BlazorPolicyAuth/Components/Pages/Account/Login.razor index 720492c..e36f83f 100644 --- a/BlazorPolicyAuth/Components/Pages/Account/Login.razor +++ b/BlazorPolicyAuth/Components/Pages/Account/Login.razor @@ -1,10 +1,6 @@ @page "/login" -@using System.Security.Claims -@using BlazorPolicyAuth.Data +@using BlazorPolicyAuth.App.Services.AuthService @using BlazorPolicyAuth.Models.ViewModels -@using Microsoft.AspNetCore.Authentication -@using Microsoft.AspNetCore.Authentication.Cookies -@using Microsoft.EntityFrameworkCore @inject AuthenticationStateProvider AuthenticationStateProvider @inject NavigationManager NavigationManager @inject IAuthService AuthService @@ -52,7 +48,7 @@ { Console.WriteLine("***"); Console.WriteLine(userLogin.Email); - var result = await AuthService.Login(userLogin.Email, userLogin.Password); + var result = await AuthService.Login(userLogin); if (result.Success) { _errorMessage = string.Empty; diff --git a/BlazorPolicyAuth/Components/Pages/Account/Register.razor b/BlazorPolicyAuth/Components/Pages/Account/Register.razor index 65e80c3..87729bf 100644 --- a/BlazorPolicyAuth/Components/Pages/Account/Register.razor +++ b/BlazorPolicyAuth/Components/Pages/Account/Register.razor @@ -1,13 +1,12 @@ @page "/register" +@using BlazorPolicyAuth.App.Services.AuthService @using BlazorPolicyAuth.Models.ViewModels -@inject AuthenticationStateProvider AuthenticationStateProvider -@inject NavigationManager NavigationManager @inject IAuthService AuthService Register - +
@@ -46,16 +45,24 @@
@code { - UserRegister user = new(); + [SupplyParameterFromForm] private UserRegister user { get; set; } private string message = string.Empty; private string messageCssClass = 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; messageCssClass = result.Success ? "text-success" : "text-danger"; } + + async Task HandleInvalidSubmit() + { + message = "Data annotations validation failed."; + messageCssClass = "text-danger"; + } } \ No newline at end of file diff --git a/BlazorPolicyAuth/Components/_Imports.razor b/BlazorPolicyAuth/Components/_Imports.razor index c789c3c..bc22c9b 100644 --- a/BlazorPolicyAuth/Components/_Imports.razor +++ b/BlazorPolicyAuth/Components/_Imports.razor @@ -9,5 +9,4 @@ @using BlazorPolicyAuth @using BlazorPolicyAuth.Components @using BlazorPolicyAuth.Components.Layout -@using Microsoft.AspNetCore.Components.Authorization -@using BlazorPolicyAuth.Services.AuthService \ No newline at end of file +@using Microsoft.AspNetCore.Components.Authorization \ No newline at end of file diff --git a/BlazorPolicyAuth/HttpClientSetupService.cs b/BlazorPolicyAuth/HttpClientSetupService.cs new file mode 100644 index 0000000..caaf065 --- /dev/null +++ b/BlazorPolicyAuth/HttpClientSetupService.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; + +namespace BlazorPolicyAuth; +/// +/// Source: https://www.duracellko.net/posts/2020/06/hosting-both-blazor-server-and-webassembly +/// +/// +/// +/// +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(); + 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; + } +} \ No newline at end of file diff --git a/BlazorPolicyAuth/Program.cs b/BlazorPolicyAuth/Program.cs index 1d9c387..8f83081 100644 --- a/BlazorPolicyAuth/Program.cs +++ b/BlazorPolicyAuth/Program.cs @@ -1,9 +1,11 @@ using BlazorPolicyAuth; using BlazorPolicyAuth.Components; using BlazorPolicyAuth.Data; +using BlazorPolicyAuth.Models.ViewModels; using BlazorPolicyAuth.Services.AuthService; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.EntityFrameworkCore; +using ClientServices = BlazorPolicyAuth.App.Services; var builder = WebApplication.CreateBuilder(args); @@ -31,10 +33,18 @@ builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationSc }); builder.Services.AddCascadingAuthenticationState(); - -builder.Services.AddScoped(); builder.Services.AddHttpContextAccessor(); +// Blazor client services +builder.Services.AddScoped(); + +// Blazor server services +builder.Services.AddScoped(); + +// Get server base address when application starts to properly configure HttpClient for client service to call server service +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -53,4 +63,8 @@ app.MapStaticAssets(); app.MapRazorComponents() .AddInteractiveServerRenderMode(); +// Blazor server routing +app.MapPost("/api/auth/register", async (UserRegister request, IAuthService authService) => + await authService.Register(request)); + app.Run(); diff --git a/BlazorPolicyAuth/Services/AuthService/AuthService.cs b/BlazorPolicyAuth/Services/AuthService/AuthService.cs index e5d1536..03ccbb5 100644 --- a/BlazorPolicyAuth/Services/AuthService/AuthService.cs +++ b/BlazorPolicyAuth/Services/AuthService/AuthService.cs @@ -12,43 +12,33 @@ using System.Text; namespace BlazorPolicyAuth.Services.AuthService; -public class AuthService : IAuthService +public class AuthService(AppDbContext context, IConfiguration configuration, IHttpContextAccessor httpContextAccessor) + : IAuthService { - private readonly AppDbContext _context; - private readonly IConfiguration _configuration; - private readonly IHttpContextAccessor _httpContextAccessor; - - public AuthService(AppDbContext context, IConfiguration configuration, IHttpContextAccessor httpContextAccessor) + public async Task> Register (UserRegister request) { - _context = context; - _configuration = configuration; - _httpContextAccessor = httpContextAccessor; - } - - public async Task> Register ( string email, string password) - { - if (await UserExists(email)) + if (await UserExists(request.Email)) { return new ServiceResponse { 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 { - UserName = email, + UserName = request.Email, PasswordHash = passwordHash, PasswordSalt = passwordSalt }; - _context.UserAccounts.Add(user); - await _context.SaveChangesAsync(); + context.UserAccounts.Add(user); + await context.SaveChangesAsync(); return new ServiceResponse { Data = user.Id, Message = "Registration successful" }; } public async Task 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; } @@ -58,7 +48,7 @@ public class AuthService : IAuthService public async Task> Login(string email, string password) { var response = new ServiceResponse(); - 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) { response.Success = false; @@ -79,7 +69,7 @@ public class AuthService : IAuthService public async Task> ChangePassword(int userId, string newPassword) { - var user = await _context.UserAccounts.FindAsync(userId); + var user = await context.UserAccounts.FindAsync(userId); if (user == null) { return new ServiceResponse { Success = false, Message = "User not found." }; @@ -90,18 +80,18 @@ public class AuthService : IAuthService user.PasswordHash = passwordHash; user.PasswordSalt = passwordSalt; - await _context.SaveChangesAsync(); + await context.SaveChangesAsync(); return new ServiceResponse { 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 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) @@ -127,7 +117,7 @@ public class AuthService : IAuthService 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); diff --git a/BlazorPolicyAuth/Services/AuthService/IAuthService.cs b/BlazorPolicyAuth/Services/AuthService/IAuthService.cs index b99e817..a1c5189 100644 --- a/BlazorPolicyAuth/Services/AuthService/IAuthService.cs +++ b/BlazorPolicyAuth/Services/AuthService/IAuthService.cs @@ -5,7 +5,7 @@ namespace BlazorPolicyAuth.Services.AuthService; public interface IAuthService { - Task> Register(string email, string password); + Task> Register(UserRegister request); Task UserExists(string email); Task> Login(string email, string password); Task> ChangePassword(int userId, string newPassword);