10 Commits

28 changed files with 746 additions and 854 deletions

256
.gitignore vendored
View File

@@ -1,180 +1,7 @@
### VisualStudioCode template
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### JetBrains+all template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### VisualStudio template
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
@@ -182,6 +9,9 @@ fabric.properties
*.user
*.userosscache
*.sln.docstates
BlazorEcommerce.db
BlazorEcommerce.db-shm
BlazorEcommerce.db-wal
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
@@ -196,7 +26,6 @@ mono_crash.*
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
@@ -235,9 +64,6 @@ project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
@@ -263,7 +89,6 @@ StyleCopReport.xml
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
@@ -315,11 +140,6 @@ _TeamCity*
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
@@ -467,17 +287,6 @@ node_modules/
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
@@ -534,9 +343,6 @@ ASALocalRun/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
@@ -546,31 +352,6 @@ MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
### JetBrains+iml template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
@@ -649,3 +430,32 @@ fabric.properties
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
#User Specific
*.userprefs
*.usertasks
#Mono Project Files
*.pidb
*.resources
test-results/
.idea/**/.idea/*
.vscode/**
stripe*.json
stripe.exe
# Sqlite database
BlazorPolicyAuth/blazorpolicyauth.db

View File

@@ -0,0 +1,42 @@
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
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,12 @@
using System.Threading.Tasks;
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

@@ -8,10 +8,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.1">
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="Blazored.SessionStorage" Version="2.4.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -19,6 +21,7 @@
<ItemGroup>
<Folder Include="Migrations\" />
<Folder Include="Services\" />
</ItemGroup>
</Project>

View File

@@ -1,79 +1,88 @@
@page "/login"
@using System.Security.Claims
@using BlazorPolicyAuth.Data
@using System.Web
@using System.Collections.Specialized
@using Blazored.LocalStorage
@using Blazored.SessionStorage
@using BlazorPolicyAuth.App.Services.AuthService
@using BlazorPolicyAuth.Models.ViewModels
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Authentication.Cookies
@using Microsoft.EntityFrameworkCore
@inject AppDbContext DbContext
@inject IAuthService AuthService
@inject ILocalStorageService LocalStorageService
@inject ISessionStorageService SessionStorageService
@inject NavigationManager NavigationManager
@inject AuthenticationStateProvider AuthenticationStateProvider
<div class="row">
<div class="col-lg-4 offset-lg-4 pt-4 pb-4 border">
<EditForm Model="Model" OnValidSubmit="Authenticate" FormName="LoginForm">
<DataAnnotationsValidator/>
<div class="mb-3 text-center flex-column">
<img src="/images/login.png" style="max-height:5rem;"/>
<h3>LOGIN</h3>
<PageTitle>Login</PageTitle>
<div class="d-flex justify-content-center align-items-center">
<div class="col-md-4 p-5 shadow-sm border rounded-3">
<h2 class="text-center mb-4 text-primary">Login Form</h2>
<EditForm Model="user" OnValidSubmit="HandleLogin" FormName="loginForm">
<DataAnnotationsValidator />
<div class="mb-3">
<label for="email">Email</label>
<InputText id="email" @bind-Value="user.Email" class="form-control border border-primary" />
<ValidationMessage For="@(() => user.Email)" />
</div>
<div class="mb-3">
<label>User Name</label>
<InputText @bind-Value="Model.UserName" class="form-control" placeholder="Enter User Name"/>
<ValidationMessage For="() => Model.UserName"></ValidationMessage>
<label for="password">Password</label>
<InputText id="password" @bind-Value="user.Password" class="form-control border border-primary" type="password" />
<ValidationMessage For="@(() => user.Password)" />
</div>
<div class="mb-3">
<label>Password</label>
<InputText @bind-Value="Model.Password" class="form-control" placeholder="Enter Password"/>
<ValidationMessage For="() => Model.Password"></ValidationMessage>
<label for="rememberme">Remember me</label>
<InputCheckbox id="rememberme" @bind-Value="rememberMe" class="form-check-input" />
</div>
<div class="mb-3 text-center">
<span class="text-danger">@_errorMessage</span>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Login</button>
</div>
<div class="mb-3 d-grid gap-2">
<button class="btn btn-primary" type="submit">Login</button>
<div class="mt-3">
<a href="#">Forgot password</a>
</div>
<div class="mt-3">
<p class="mb-0 text-center">You don't have an account? <a href="register" class="text-primary fw-bold">Register</a></p>
</div>
</EditForm>
</div>
</div>
<div class="text-danger">
<span>@errorMessage</span>
</div>
@code {
[CascadingParameter]
public HttpContext? HttpContext { get; set; }
[SupplyParameterFromForm] private UserLogin user { get; set; }
private string errorMessage = string.Empty;
private string returnUrl = string.Empty;
private bool rememberMe;
[SupplyParameterFromForm]
public LoginViewModel Model { get; set; } = new();
private string? _errorMessage;
private async Task Authenticate()
protected override void OnInitialized()
{
if(string.IsNullOrWhiteSpace(Model.UserName) || string.IsNullOrWhiteSpace(Model.Password))
user ??= new();
var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
NameValueCollection queryStringCall = HttpUtility.ParseQueryString(uri.Query);
if (queryStringCall.AllKeys.Contains("returnUrl"))
{
_errorMessage = "Invalid User Name or Password";
return;
returnUrl = queryStringCall["returnUrl"];
}
}
var userAccount = DbContext.UserAccounts.FirstOrDefault(x => x.UserName == Model.UserName);
if (userAccount is null || userAccount.Password != Model.Password)
private async Task HandleLogin()
{
var result = await AuthService.Login(user);
if (result.Success)
{
_errorMessage = "Invalid User Name or Password";
return;
errorMessage = string.Empty;
if (rememberMe)
await LocalStorageService.SetItemAsync("authToken", result.Data);
else
await SessionStorageService.SetItemAsync("authToken", result.Data);
await AuthenticationStateProvider.GetAuthenticationStateAsync();
NavigationManager.NavigateTo(returnUrl);
}
var claims = new List<Claim>
else
{
new Claim(ClaimTypes.Name, Model.UserName)
};
/* Add Policies */
var userAccountPolicies = await DbContext.UserAccountPolicies.Where(x => x.UserAccountId == userAccount.Id && x.IsEnabled).ToListAsync();
claims.AddRange(userAccountPolicies.Select(userAccountPolicy => new Claim(userAccountPolicy.UserPolicy, "true")));
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
await HttpContext?.SignInAsync(principal)!;
NavigationManager.NavigateTo("/");
errorMessage = result.Message;
}
}
}

View File

@@ -1,5 +1,6 @@
@page "/logout"
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Http
@inject NavigationManager NavigationManager
<div class="row">

View File

@@ -0,0 +1,68 @@
@page "/register"
@using BlazorPolicyAuth.App.Services.AuthService
@using BlazorPolicyAuth.Models.ViewModels
@inject IAuthService AuthService
<title>Register</title>
<EditForm Model="user" OnValidSubmit="HandleRegistration" OnInvalidSubmit="HandleInvalidSubmit" FormName="registerForm">
<DataAnnotationsValidator/>
<div class="d-flex justify-content-center align-items-center">
<div class="col-md-4 p-5 shadow-sm border rounded-3">
<h2 class="text-center mb-4 text-primary">Register Form</h2>
<div class="mb-3">
<label for="email" class="form-label">Email address</label>
<InputText type="email" @bind-Value="user.Email" class="form-control border border-primary" id="email" aria-describedby="email"/>
<ValidationMessage For="@(() => user.Email)"/>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<InputText type="password" @bind-Value="user.Password" class="form-control border border-primary" id="password" aria-describedby="password"/>
<ValidationMessage For="@(() => user.Password)"/>
</div>
<div class="mb-3">
<label for="confirmPassword" class="form-label">Confirm password</label>
<InputText type="password" @bind-Value="user.ConfirmPassword" class="form-control border border-primary" id="confirmPassword" aria-describedby="confirmPassword"/>
<ValidationMessage For="@(() => user.ConfirmPassword)"/>
</div>
<div class="d-grid">
<button class="btn btn-primary" type="submit">Register</button>
</div>
<div class="mt-3">
<p class="mb-0 text-center">You have an account? <a href="login" class="text-primary fw-bold">Sign In</a></p>
</div>
</div>
</div>
</EditForm>
<div class="text-danger">
<span>@errorMessage</span>
</div>
<div class="@messageCssClass">
<span>@message</span>
</div>
@code {
[SupplyParameterFromForm] private UserRegister user { get; set; }
private string message = string.Empty;
private string messageCssClass = string.Empty;
private string errorMessage = string.Empty;
protected override void OnInitialized() => user ??= new();
async Task HandleRegistration()
{
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";
}
}

View File

@@ -1,5 +1,6 @@
@page "/Error"
@using System.Diagnostics
@using Microsoft.AspNetCore.Http
<PageTitle>Error</PageTitle>

View File

@@ -10,49 +10,49 @@ public class AppDbContext(DbContextOptions dbContextOptions) : DbContext(dbConte
{
base.OnModelCreating(modelBuilder);
var demoUserAccounts = new UserAccount[]
{
new() {Id = 1, UserName = "user1", Password = "user1"},
new() {Id = 2, UserName = "user2", Password = "user2"},
new() {Id = 3, UserName = "user3", Password = "user3"},
new() {Id = 4, UserName = "user4", Password = "user4"},
new() {Id = 5, UserName = "user5", Password = "user5"},
};
modelBuilder.Entity<UserAccount>().HasData(demoUserAccounts);
var demoUserAccountPolicies = new UserAccountPolicy[]
{
/* User 1 Policies */
new() {Id = 1, UserAccountId = 1, UserPolicy = UserPolicy.VIEW_PRODUCT, IsEnabled = false},
new() {Id = 2, UserAccountId = 1, UserPolicy = UserPolicy.ADD_PRODUCT, IsEnabled = false},
new() {Id = 3, UserAccountId = 1, UserPolicy = UserPolicy.EDIT_PRODUCT, IsEnabled = false},
new() {Id = 4, UserAccountId = 1, UserPolicy = UserPolicy.DELETE_PRODUCT, IsEnabled = false},
/* User 2 Policies */
new() {Id = 5, UserAccountId = 2, UserPolicy = UserPolicy.VIEW_PRODUCT, IsEnabled = true},
new() {Id = 6, UserAccountId = 2, UserPolicy = UserPolicy.ADD_PRODUCT, IsEnabled = false},
new() {Id = 7, UserAccountId = 2, UserPolicy = UserPolicy.EDIT_PRODUCT, IsEnabled = false},
new() {Id = 8, UserAccountId = 2, UserPolicy = UserPolicy.DELETE_PRODUCT, IsEnabled = false},
/* User 3 Policies */
new() {Id = 9, UserAccountId = 3, UserPolicy = UserPolicy.VIEW_PRODUCT, IsEnabled = true},
new() {Id = 10, UserAccountId = 3, UserPolicy = UserPolicy.ADD_PRODUCT, IsEnabled = true},
new() {Id = 11, UserAccountId = 3, UserPolicy = UserPolicy.EDIT_PRODUCT, IsEnabled = false},
new() {Id = 12, UserAccountId = 3, UserPolicy = UserPolicy.DELETE_PRODUCT, IsEnabled = false},
/* User 4 Policies */
new() {Id = 13, UserAccountId = 4, UserPolicy = UserPolicy.VIEW_PRODUCT, IsEnabled = true},
new() {Id = 14, UserAccountId = 4, UserPolicy = UserPolicy.ADD_PRODUCT, IsEnabled = true},
new() {Id = 15, UserAccountId = 4, UserPolicy = UserPolicy.EDIT_PRODUCT, IsEnabled = true},
new() {Id = 16, UserAccountId = 4, UserPolicy = UserPolicy.DELETE_PRODUCT, IsEnabled = false},
/* User 5 Policies */
new() {Id = 17, UserAccountId = 5, UserPolicy = UserPolicy.VIEW_PRODUCT, IsEnabled = true},
new() {Id = 18, UserAccountId = 5, UserPolicy = UserPolicy.ADD_PRODUCT, IsEnabled = true},
new() {Id = 19, UserAccountId = 5, UserPolicy = UserPolicy.EDIT_PRODUCT, IsEnabled = true},
new() {Id = 20, UserAccountId = 5, UserPolicy = UserPolicy.DELETE_PRODUCT, IsEnabled = true},
};
modelBuilder.Entity<UserAccountPolicy>().HasData(demoUserAccountPolicies);
// var demoUserAccounts = new UserAccount[]
// {
// new() {Id = 1, Email = "user1", Password = "user1"},
// new() {Id = 2, Email = "user2", Password = "user2"},
// new() {Id = 3, Email = "user3", Password = "user3"},
// new() {Id = 4, Email = "user4", Password = "user4"},
// new() {Id = 5, Email = "user5", Password = "user5"},
// };
// modelBuilder.Entity<UserAccount>().HasData(demoUserAccounts);
//
// var demoUserAccountPolicies = new UserAccountPolicy[]
// {
// /* User 1 Policies */
// new() {Id = 1, UserAccountId = 1, UserPolicy = UserPolicy.VIEW_PRODUCT, IsEnabled = false},
// new() {Id = 2, UserAccountId = 1, UserPolicy = UserPolicy.ADD_PRODUCT, IsEnabled = false},
// new() {Id = 3, UserAccountId = 1, UserPolicy = UserPolicy.EDIT_PRODUCT, IsEnabled = false},
// new() {Id = 4, UserAccountId = 1, UserPolicy = UserPolicy.DELETE_PRODUCT, IsEnabled = false},
//
// /* User 2 Policies */
// new() {Id = 5, UserAccountId = 2, UserPolicy = UserPolicy.VIEW_PRODUCT, IsEnabled = true},
// new() {Id = 6, UserAccountId = 2, UserPolicy = UserPolicy.ADD_PRODUCT, IsEnabled = false},
// new() {Id = 7, UserAccountId = 2, UserPolicy = UserPolicy.EDIT_PRODUCT, IsEnabled = false},
// new() {Id = 8, UserAccountId = 2, UserPolicy = UserPolicy.DELETE_PRODUCT, IsEnabled = false},
//
// /* User 3 Policies */
// new() {Id = 9, UserAccountId = 3, UserPolicy = UserPolicy.VIEW_PRODUCT, IsEnabled = true},
// new() {Id = 10, UserAccountId = 3, UserPolicy = UserPolicy.ADD_PRODUCT, IsEnabled = true},
// new() {Id = 11, UserAccountId = 3, UserPolicy = UserPolicy.EDIT_PRODUCT, IsEnabled = false},
// new() {Id = 12, UserAccountId = 3, UserPolicy = UserPolicy.DELETE_PRODUCT, IsEnabled = false},
//
// /* User 4 Policies */
// new() {Id = 13, UserAccountId = 4, UserPolicy = UserPolicy.VIEW_PRODUCT, IsEnabled = true},
// new() {Id = 14, UserAccountId = 4, UserPolicy = UserPolicy.ADD_PRODUCT, IsEnabled = true},
// new() {Id = 15, UserAccountId = 4, UserPolicy = UserPolicy.EDIT_PRODUCT, IsEnabled = true},
// new() {Id = 16, UserAccountId = 4, UserPolicy = UserPolicy.DELETE_PRODUCT, IsEnabled = false},
//
// /* User 5 Policies */
// new() {Id = 17, UserAccountId = 5, UserPolicy = UserPolicy.VIEW_PRODUCT, IsEnabled = true},
// new() {Id = 18, UserAccountId = 5, UserPolicy = UserPolicy.ADD_PRODUCT, IsEnabled = true},
// new() {Id = 19, UserAccountId = 5, UserPolicy = UserPolicy.EDIT_PRODUCT, IsEnabled = true},
// new() {Id = 20, UserAccountId = 5, UserPolicy = UserPolicy.DELETE_PRODUCT, IsEnabled = true},
// };
// modelBuilder.Entity<UserAccountPolicy>().HasData(demoUserAccountPolicies);
}
public DbSet<UserAccount> UserAccounts { get; set; }

View File

@@ -0,0 +1,61 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.Extensions.Hosting;
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,244 +0,0 @@
// <auto-generated />
using BlazorPolicyAuth.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace BlazorPolicyAuth.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20251215131042_user account with policies")]
partial class useraccountwithpolicies
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.1");
modelBuilder.Entity("BlazorPolicyAuth.Models.Entities.UserAccount", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<string>("Password")
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("password");
b.Property<string>("UserName")
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("user_name");
b.HasKey("Id");
b.ToTable("user_account");
b.HasData(
new
{
Id = 1,
Password = "user1",
UserName = "user1"
},
new
{
Id = 2,
Password = "user2",
UserName = "user2"
},
new
{
Id = 3,
Password = "user3",
UserName = "user3"
},
new
{
Id = 4,
Password = "user4",
UserName = "user4"
},
new
{
Id = 5,
Password = "user5",
UserName = "user5"
});
});
modelBuilder.Entity("BlazorPolicyAuth.Models.Entities.UserAccountPolicy", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<bool>("IsEnabled")
.HasColumnType("INTEGER")
.HasColumnName("is_enabled");
b.Property<int>("UserAccountId")
.HasColumnType("INTEGER")
.HasColumnName("user_account_policy");
b.Property<string>("UserPolicy")
.HasColumnType("TEXT")
.HasColumnName("user_policy");
b.HasKey("Id");
b.ToTable("user_account_policy");
b.HasData(
new
{
Id = 1,
IsEnabled = false,
UserAccountId = 1,
UserPolicy = "VIEW_PRODUCT"
},
new
{
Id = 2,
IsEnabled = false,
UserAccountId = 1,
UserPolicy = "ADD_PRODUCT"
},
new
{
Id = 3,
IsEnabled = false,
UserAccountId = 1,
UserPolicy = "EDIT_PRODUCT"
},
new
{
Id = 4,
IsEnabled = false,
UserAccountId = 1,
UserPolicy = "DELETE_PRODUCT"
},
new
{
Id = 5,
IsEnabled = true,
UserAccountId = 2,
UserPolicy = "VIEW_PRODUCT"
},
new
{
Id = 6,
IsEnabled = false,
UserAccountId = 2,
UserPolicy = "ADD_PRODUCT"
},
new
{
Id = 7,
IsEnabled = false,
UserAccountId = 2,
UserPolicy = "EDIT_PRODUCT"
},
new
{
Id = 8,
IsEnabled = false,
UserAccountId = 2,
UserPolicy = "DELETE_PRODUCT"
},
new
{
Id = 9,
IsEnabled = true,
UserAccountId = 3,
UserPolicy = "VIEW_PRODUCT"
},
new
{
Id = 10,
IsEnabled = true,
UserAccountId = 3,
UserPolicy = "ADD_PRODUCT"
},
new
{
Id = 11,
IsEnabled = false,
UserAccountId = 3,
UserPolicy = "EDIT_PRODUCT"
},
new
{
Id = 12,
IsEnabled = false,
UserAccountId = 3,
UserPolicy = "DELETE_PRODUCT"
},
new
{
Id = 13,
IsEnabled = true,
UserAccountId = 4,
UserPolicy = "VIEW_PRODUCT"
},
new
{
Id = 14,
IsEnabled = true,
UserAccountId = 4,
UserPolicy = "ADD_PRODUCT"
},
new
{
Id = 15,
IsEnabled = true,
UserAccountId = 4,
UserPolicy = "EDIT_PRODUCT"
},
new
{
Id = 16,
IsEnabled = false,
UserAccountId = 4,
UserPolicy = "DELETE_PRODUCT"
},
new
{
Id = 17,
IsEnabled = true,
UserAccountId = 5,
UserPolicy = "VIEW_PRODUCT"
},
new
{
Id = 18,
IsEnabled = true,
UserAccountId = 5,
UserPolicy = "ADD_PRODUCT"
},
new
{
Id = 19,
IsEnabled = true,
UserAccountId = 5,
UserPolicy = "EDIT_PRODUCT"
},
new
{
Id = 20,
IsEnabled = true,
UserAccountId = 5,
UserPolicy = "DELETE_PRODUCT"
});
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,94 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
namespace BlazorPolicyAuth.Migrations
{
/// <inheritdoc />
public partial class useraccountwithpolicies : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "user_account",
columns: table => new
{
id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
user_name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
password = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_user_account", x => x.id);
});
migrationBuilder.CreateTable(
name: "user_account_policy",
columns: table => new
{
id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
user_account_policy = table.Column<int>(type: "INTEGER", nullable: false),
user_policy = table.Column<string>(type: "TEXT", nullable: true),
is_enabled = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_user_account_policy", x => x.id);
});
migrationBuilder.InsertData(
table: "user_account",
columns: new[] { "id", "password", "user_name" },
values: new object[,]
{
{ 1, "user1", "user1" },
{ 2, "user2", "user2" },
{ 3, "user3", "user3" },
{ 4, "user4", "user4" },
{ 5, "user5", "user5" }
});
migrationBuilder.InsertData(
table: "user_account_policy",
columns: new[] { "id", "is_enabled", "user_account_policy", "user_policy" },
values: new object[,]
{
{ 1, false, 1, "VIEW_PRODUCT" },
{ 2, false, 1, "ADD_PRODUCT" },
{ 3, false, 1, "EDIT_PRODUCT" },
{ 4, false, 1, "DELETE_PRODUCT" },
{ 5, true, 2, "VIEW_PRODUCT" },
{ 6, false, 2, "ADD_PRODUCT" },
{ 7, false, 2, "EDIT_PRODUCT" },
{ 8, false, 2, "DELETE_PRODUCT" },
{ 9, true, 3, "VIEW_PRODUCT" },
{ 10, true, 3, "ADD_PRODUCT" },
{ 11, false, 3, "EDIT_PRODUCT" },
{ 12, false, 3, "DELETE_PRODUCT" },
{ 13, true, 4, "VIEW_PRODUCT" },
{ 14, true, 4, "ADD_PRODUCT" },
{ 15, true, 4, "EDIT_PRODUCT" },
{ 16, false, 4, "DELETE_PRODUCT" },
{ 17, true, 5, "VIEW_PRODUCT" },
{ 18, true, 5, "ADD_PRODUCT" },
{ 19, true, 5, "EDIT_PRODUCT" },
{ 20, true, 5, "DELETE_PRODUCT" }
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "user_account");
migrationBuilder.DropTable(
name: "user_account_policy");
}
}
}

View File

@@ -0,0 +1,86 @@
// <auto-generated />
using System;
using BlazorPolicyAuth.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace BlazorPolicyAuth.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260130211227_createUserAccount")]
partial class createUserAccount
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.1");
modelBuilder.Entity("BlazorPolicyAuth.Models.Entities.UserAccount", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("email");
b.Property<string>("Password")
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("password");
b.Property<byte[]>("PasswordHash")
.IsRequired()
.HasColumnType("BLOB");
b.Property<byte[]>("PasswordSalt")
.IsRequired()
.HasColumnType("BLOB");
b.Property<string>("Role")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("user_account");
});
modelBuilder.Entity("BlazorPolicyAuth.Models.Entities.UserAccountPolicy", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<bool>("IsEnabled")
.HasColumnType("INTEGER")
.HasColumnName("is_enabled");
b.Property<int>("UserAccountId")
.HasColumnType("INTEGER")
.HasColumnName("user_account_policy");
b.Property<string>("UserPolicy")
.HasColumnType("TEXT")
.HasColumnName("user_policy");
b.HasKey("Id");
b.ToTable("user_account_policy");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,58 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BlazorPolicyAuth.Migrations
{
/// <inheritdoc />
public partial class createUserAccount : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "user_account",
columns: table => new
{
id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
email = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
password = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
PasswordHash = table.Column<byte[]>(type: "BLOB", nullable: false),
PasswordSalt = table.Column<byte[]>(type: "BLOB", nullable: false),
DateCreated = table.Column<DateTime>(type: "TEXT", nullable: false),
Role = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_user_account", x => x.id);
});
migrationBuilder.CreateTable(
name: "user_account_policy",
columns: table => new
{
id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
user_account_policy = table.Column<int>(type: "INTEGER", nullable: false),
user_policy = table.Column<string>(type: "TEXT", nullable: true),
is_enabled = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_user_account_policy", x => x.id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "user_account");
migrationBuilder.DropTable(
name: "user_account_policy");
}
}
}

View File

@@ -1,4 +1,5 @@
// <auto-generated />
using System;
using BlazorPolicyAuth.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -23,51 +24,34 @@ namespace BlazorPolicyAuth.Migrations
.HasColumnType("INTEGER")
.HasColumnName("id");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("email");
b.Property<string>("Password")
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("password");
b.Property<string>("UserName")
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("user_name");
b.Property<byte[]>("PasswordHash")
.IsRequired()
.HasColumnType("BLOB");
b.Property<byte[]>("PasswordSalt")
.IsRequired()
.HasColumnType("BLOB");
b.Property<string>("Role")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("user_account");
b.HasData(
new
{
Id = 1,
Password = "user1",
UserName = "user1"
},
new
{
Id = 2,
Password = "user2",
UserName = "user2"
},
new
{
Id = 3,
Password = "user3",
UserName = "user3"
},
new
{
Id = 4,
Password = "user4",
UserName = "user4"
},
new
{
Id = 5,
Password = "user5",
UserName = "user5"
});
});
modelBuilder.Entity("BlazorPolicyAuth.Models.Entities.UserAccountPolicy", b =>
@@ -92,148 +76,6 @@ namespace BlazorPolicyAuth.Migrations
b.HasKey("Id");
b.ToTable("user_account_policy");
b.HasData(
new
{
Id = 1,
IsEnabled = false,
UserAccountId = 1,
UserPolicy = "VIEW_PRODUCT"
},
new
{
Id = 2,
IsEnabled = false,
UserAccountId = 1,
UserPolicy = "ADD_PRODUCT"
},
new
{
Id = 3,
IsEnabled = false,
UserAccountId = 1,
UserPolicy = "EDIT_PRODUCT"
},
new
{
Id = 4,
IsEnabled = false,
UserAccountId = 1,
UserPolicy = "DELETE_PRODUCT"
},
new
{
Id = 5,
IsEnabled = true,
UserAccountId = 2,
UserPolicy = "VIEW_PRODUCT"
},
new
{
Id = 6,
IsEnabled = false,
UserAccountId = 2,
UserPolicy = "ADD_PRODUCT"
},
new
{
Id = 7,
IsEnabled = false,
UserAccountId = 2,
UserPolicy = "EDIT_PRODUCT"
},
new
{
Id = 8,
IsEnabled = false,
UserAccountId = 2,
UserPolicy = "DELETE_PRODUCT"
},
new
{
Id = 9,
IsEnabled = true,
UserAccountId = 3,
UserPolicy = "VIEW_PRODUCT"
},
new
{
Id = 10,
IsEnabled = true,
UserAccountId = 3,
UserPolicy = "ADD_PRODUCT"
},
new
{
Id = 11,
IsEnabled = false,
UserAccountId = 3,
UserPolicy = "EDIT_PRODUCT"
},
new
{
Id = 12,
IsEnabled = false,
UserAccountId = 3,
UserPolicy = "DELETE_PRODUCT"
},
new
{
Id = 13,
IsEnabled = true,
UserAccountId = 4,
UserPolicy = "VIEW_PRODUCT"
},
new
{
Id = 14,
IsEnabled = true,
UserAccountId = 4,
UserPolicy = "ADD_PRODUCT"
},
new
{
Id = 15,
IsEnabled = true,
UserAccountId = 4,
UserPolicy = "EDIT_PRODUCT"
},
new
{
Id = 16,
IsEnabled = false,
UserAccountId = 4,
UserPolicy = "DELETE_PRODUCT"
},
new
{
Id = 17,
IsEnabled = true,
UserAccountId = 5,
UserPolicy = "VIEW_PRODUCT"
},
new
{
Id = 18,
IsEnabled = true,
UserAccountId = 5,
UserPolicy = "ADD_PRODUCT"
},
new
{
Id = 19,
IsEnabled = true,
UserAccountId = 5,
UserPolicy = "EDIT_PRODUCT"
},
new
{
Id = 20,
IsEnabled = true,
UserAccountId = 5,
UserPolicy = "DELETE_PRODUCT"
});
});
#pragma warning restore 612, 618
}

View File

@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace BlazorPolicyAuth.Models.Entities;
@@ -11,11 +12,17 @@ public class UserAccount
[Column("id")]
public int Id { get; set; }
[Column("user_name")]
[MaxLength(100)]
public string? UserName { get; set; }
[Column("email")]
[MaxLength(200)]
public string? Email { get; set; }
[Column("password")]
[MaxLength(100)]
public string? Password { get; set; }
public byte[] PasswordHash { get; set; } = [];
public byte[] PasswordSalt { get; set; } = [];
public DateTime DateCreated { get; set; } = DateTime.Now;
public string Role { get; set; } = "User";
}

View File

@@ -1,7 +0,0 @@
namespace BlazorPolicyAuth.Models.ViewModels;
public class LoginViewModel
{
public string? UserName { get; set; }
public string? Password { get; set; }
}

View File

@@ -0,0 +1,9 @@
namespace BlazorPolicyAuth.Models.ViewModels
{
public class ServiceResponse<T>
{
public T? Data { get; set; }
public bool Success { get; set; } = true;
public string Message { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
namespace BlazorPolicyAuth.Models.ViewModels
{
public class UserChangePassword
{
[Required, StringLength(100, MinimumLength = 5)]
public string Password { get; set; } = string.Empty;
[Compare("Password", ErrorMessage = "The password do not match")]
public string ConfirmPassword { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace BlazorPolicyAuth.Models.ViewModels;
public class UserLogin
{
[Required, EmailAddress]
public string Email { get; set; } = string.Empty;
[Required]
public string Password { get; set; }
}

View File

@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace BlazorPolicyAuth.Models.ViewModels
{
public class UserRegister
{
[Required, EmailAddress]
public string Email { get; set; } = string.Empty;
[Required, StringLength(100, MinimumLength = 5)]
public string Password { get; set; } = string.Empty;
[Compare("Password", ErrorMessage = "The password do not match.")]
public string ConfirmPassword { get; set; } = string.Empty;
}
}

View File

@@ -1,8 +1,19 @@
using System;
using System.Net.Http;
using Blazored.LocalStorage;
using Blazored.SessionStorage;
using BlazorPolicyAuth;
using BlazorPolicyAuth.Components;
using BlazorPolicyAuth.Data;
using BlazorPolicyAuth.Models.ViewModels;
using BlazorPolicyAuth.Services.AuthService;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ClientServices = BlazorPolicyAuth.App.Services;
var builder = WebApplication.CreateBuilder(args);
@@ -30,6 +41,20 @@ builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationSc
});
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddHttpContextAccessor();
builder.Services.AddBlazoredLocalStorage();
builder.Services.AddBlazoredSessionStorage();
// 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();
@@ -49,4 +74,11 @@ app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
// Blazor server routing
app.MapPost("/api/auth/register", async (UserRegister request, IAuthService authService) =>
await authService.Register(request));
app.MapPost("/api/auth/login", async (UserLogin request, IAuthService authService) =>
await authService.Login(request.Email, request.Password));
app.Run();

View File

@@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using BlazorPolicyAuth.Data;
using BlazorPolicyAuth.Models.Entities;
using BlazorPolicyAuth.Models.ViewModels;
using BlazorPolicyAuth.Services.AuthService;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
namespace BlazorPolicyAuth.Services.AuthService;
public class AuthService(AppDbContext context, IConfiguration configuration, IHttpContextAccessor httpContextAccessor)
: IAuthService
{
public async Task<ServiceResponse<int>> Register (UserRegister request)
{
if (await UserExists(request.Email))
{
return new ServiceResponse<int> { Success = false, Message = "User already exist." };
}
CreatePasswordHash(request.Password, out byte[] passwordHash, out byte[] passwordSalt);
var user = new UserAccount
{
Email = request.Email,
PasswordHash = passwordHash,
PasswordSalt = passwordSalt
};
context.UserAccounts.Add(user);
await context.SaveChangesAsync();
return new ServiceResponse<int> { Data = user.Id, Message = "Registration successful" };
}
public async Task<bool> UserExists(string email)
{
if (await context.UserAccounts.AnyAsync(user => user.Email.ToLower().Equals(email.ToLower())))
{
return true;
}
return false;
}
public async Task<ServiceResponse<string>> Login(string email, string password)
{
var response = new ServiceResponse<string>();
var user = await context.UserAccounts.FirstOrDefaultAsync(u => u.Email.ToLower().Equals(email.ToLower()));
if (user == null)
{
response.Success = false;
response.Message = "User not found.";
}
else if (!VerifyPasswordHash(password, user.PasswordHash, user.PasswordSalt))
{
response.Success = false;
response.Message = "Wrong password.";
}
else
{
response.Data = CreateToken(user);
}
return response;
}
public async Task<ServiceResponse<bool>> ChangePassword(int userId, string newPassword)
{
var user = await context.UserAccounts.FindAsync(userId);
if (user == null)
{
return new ServiceResponse<bool> { Success = false, Message = "User not found." };
}
CreatePasswordHash(newPassword, out byte[] passwordHash, out byte[] passwordSalt);
user.PasswordHash = passwordHash;
user.PasswordSalt = passwordSalt;
await context.SaveChangesAsync();
return new ServiceResponse<bool> { Data = true, Message = "Password has been changed." };
}
public int GetUserId() => int.Parse(httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier));
public string GetUserEmail() => httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.Name);
public async Task<UserAccount> GetUserByEmail(string email)
{
return await context.UserAccounts.FirstOrDefaultAsync(u => u.Email.Equals(email));
}
private static void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt)
{
using var hmac = new HMACSHA512();
passwordSalt = hmac.Key;
passwordHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(password));
}
private static bool VerifyPasswordHash(string password, byte[] passwordHash, byte[] passwordSalt)
{
using var hmac = new HMACSHA512(passwordSalt);
var computedHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(password));
return computedHash.SequenceEqual(passwordHash);
}
private string CreateToken(UserAccount user)
{
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
new(ClaimTypes.Name, user.Email),
new(ClaimTypes.Role, user.Role)
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration.GetSection("AppSettings:Token").Value));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);
var token = new JwtSecurityToken(
claims: claims,
expires: DateTime.Now.AddDays(1),
signingCredentials: creds);
var jwt = new JwtSecurityTokenHandler().WriteToken(token);
return jwt;
}
}

View File

@@ -0,0 +1,16 @@
using System.Threading.Tasks;
using BlazorPolicyAuth.Models.Entities;
using BlazorPolicyAuth.Models.ViewModels;
namespace BlazorPolicyAuth.Services.AuthService;
public interface IAuthService
{
Task<ServiceResponse<int>> Register(UserRegister request);
Task<bool> UserExists(string email);
Task<ServiceResponse<string>> Login(string email, string password);
Task<ServiceResponse<bool>> ChangePassword(int userId, string newPassword);
int GetUserId();
string GetUserEmail();
Task<UserAccount> GetUserByEmail(string email);
}

View File

@@ -1,4 +1,6 @@
namespace BlazorPolicyAuth;
using System.Collections.Generic;
namespace BlazorPolicyAuth;
public class UserPolicy
{

View File

@@ -9,5 +9,8 @@
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"AppSettings" : {
"Token": "my top secret key my top secret key my top secret key my top secret key my top secret key my top secret key my top secret key my top secret key my top secret key my top secret key my top secret key my top secret key"
}
}

Binary file not shown.