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 ## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons. ## 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 # User-specific files
*.rsuser *.rsuser
@@ -182,6 +9,9 @@ fabric.properties
*.user *.user
*.userosscache *.userosscache
*.sln.docstates *.sln.docstates
BlazorEcommerce.db
BlazorEcommerce.db-shm
BlazorEcommerce.db-wal
# User-specific files (MonoDevelop/Xamarin Studio) # User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs *.userprefs
@@ -196,7 +26,6 @@ mono_crash.*
[Rr]eleases/ [Rr]eleases/
x64/ x64/
x86/ x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/ [Aa][Rr][Mm]/
[Aa][Rr][Mm]64/ [Aa][Rr][Mm]64/
bld/ bld/
@@ -235,9 +64,6 @@ project.lock.json
project.fragment.lock.json project.fragment.lock.json
artifacts/ artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop # StyleCop
StyleCopReport.xml StyleCopReport.xml
@@ -263,7 +89,6 @@ StyleCopReport.xml
*.tmp_proj *.tmp_proj
*_wpftmp.csproj *_wpftmp.csproj
*.log *.log
*.tlog
*.vspscc *.vspscc
*.vssscc *.vssscc
.builds .builds
@@ -315,11 +140,6 @@ _TeamCity*
.axoCover/* .axoCover/*
!.axoCover/settings.json !.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results # Visual Studio code coverage results
*.coverage *.coverage
*.coveragexml *.coveragexml
@@ -467,17 +287,6 @@ node_modules/
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw *.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 # Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts **/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts
@@ -534,9 +343,6 @@ ASALocalRun/
# Local History for Visual Studio # Local History for Visual Studio
.localhistory/ .localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database # BeatPulse healthcheck temp database
healthchecksdb healthchecksdb
@@ -546,31 +352,6 @@ MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder # Ionide (cross platform F# VS Code tools) working folder
.ionide/ .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 # 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 # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
@@ -649,3 +430,32 @@ fabric.properties
# Android studio 3.1+ serialized cache file # Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser .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> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" /> <PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" /> <PackageReference Include="Blazored.SessionStorage" Version="2.4.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.1"> <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> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
@@ -19,6 +21,7 @@
<ItemGroup> <ItemGroup>
<Folder Include="Migrations\" /> <Folder Include="Migrations\" />
<Folder Include="Services\" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,79 +1,88 @@
@page "/login" @page "/login"
@using System.Security.Claims @using System.Web
@using BlazorPolicyAuth.Data @using System.Collections.Specialized
@using Blazored.LocalStorage
@using Blazored.SessionStorage
@using BlazorPolicyAuth.App.Services.AuthService
@using BlazorPolicyAuth.Models.ViewModels @using BlazorPolicyAuth.Models.ViewModels
@using Microsoft.AspNetCore.Authentication @inject IAuthService AuthService
@using Microsoft.AspNetCore.Authentication.Cookies @inject ILocalStorageService LocalStorageService
@using Microsoft.EntityFrameworkCore @inject ISessionStorageService SessionStorageService
@inject AppDbContext DbContext
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject AuthenticationStateProvider AuthenticationStateProvider
<div class="row"> <PageTitle>Login</PageTitle>
<div class="col-lg-4 offset-lg-4 pt-4 pb-4 border">
<EditForm Model="Model" OnValidSubmit="Authenticate" FormName="LoginForm"> <div class="d-flex justify-content-center align-items-center">
<DataAnnotationsValidator/> <div class="col-md-4 p-5 shadow-sm border rounded-3">
<div class="mb-3 text-center flex-column"> <h2 class="text-center mb-4 text-primary">Login Form</h2>
<img src="/images/login.png" style="max-height:5rem;"/> <EditForm Model="user" OnValidSubmit="HandleLogin" FormName="loginForm">
<h3>LOGIN</h3> <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>
<div class="mb-3"> <div class="mb-3">
<label>User Name</label> <label for="password">Password</label>
<InputText @bind-Value="Model.UserName" class="form-control" placeholder="Enter User Name"/> <InputText id="password" @bind-Value="user.Password" class="form-control border border-primary" type="password" />
<ValidationMessage For="() => Model.UserName"></ValidationMessage> <ValidationMessage For="@(() => user.Password)" />
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label>Password</label> <label for="rememberme">Remember me</label>
<InputText @bind-Value="Model.Password" class="form-control" placeholder="Enter Password"/> <InputCheckbox id="rememberme" @bind-Value="rememberMe" class="form-check-input" />
<ValidationMessage For="() => Model.Password"></ValidationMessage>
</div> </div>
<div class="mb-3 text-center"> <div class="d-grid">
<span class="text-danger">@_errorMessage</span> <button type="submit" class="btn btn-primary">Login</button>
</div> </div>
<div class="mb-3 d-grid gap-2"> <div class="mt-3">
<button class="btn btn-primary" type="submit">Login</button> <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> </div>
</EditForm> </EditForm>
</div> </div>
</div> </div>
<div class="text-danger">
<span>@errorMessage</span>
</div>
@code { @code {
[CascadingParameter] [SupplyParameterFromForm] private UserLogin user { get; set; }
public HttpContext? HttpContext { get; set; } private string errorMessage = string.Empty;
private string returnUrl = string.Empty;
private bool rememberMe;
[SupplyParameterFromForm] protected override void OnInitialized()
public LoginViewModel Model { get; set; } = new();
private string? _errorMessage;
private async Task Authenticate()
{ {
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"; returnUrl = queryStringCall["returnUrl"];
return; }
} }
var userAccount = DbContext.UserAccounts.FirstOrDefault(x => x.UserName == Model.UserName); private async Task HandleLogin()
if (userAccount is null || userAccount.Password != Model.Password)
{ {
_errorMessage = "Invalid User Name or Password"; var result = await AuthService.Login(user);
return; if (result.Success)
{
errorMessage = string.Empty;
if (rememberMe)
await LocalStorageService.SetItemAsync("authToken", result.Data);
else
await SessionStorageService.SetItemAsync("authToken", result.Data);
await AuthenticationStateProvider.GetAuthenticationStateAsync();
NavigationManager.NavigateTo(returnUrl);
} }
else
var claims = new List<Claim>
{ {
new Claim(ClaimTypes.Name, Model.UserName) errorMessage = result.Message;
}; }
/* 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("/");
} }
} }

View File

@@ -1,5 +1,6 @@
@page "/logout" @page "/logout"
@using Microsoft.AspNetCore.Authentication @using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Http
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
<div class="row"> <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" @page "/Error"
@using System.Diagnostics @using System.Diagnostics
@using Microsoft.AspNetCore.Http
<PageTitle>Error</PageTitle> <PageTitle>Error</PageTitle>

View File

@@ -10,49 +10,49 @@ public class AppDbContext(DbContextOptions dbContextOptions) : DbContext(dbConte
{ {
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
var demoUserAccounts = new UserAccount[] // var demoUserAccounts = new UserAccount[]
{ // {
new() {Id = 1, UserName = "user1", Password = "user1"}, // new() {Id = 1, Email = "user1", Password = "user1"},
new() {Id = 2, UserName = "user2", Password = "user2"}, // new() {Id = 2, Email = "user2", Password = "user2"},
new() {Id = 3, UserName = "user3", Password = "user3"}, // new() {Id = 3, Email = "user3", Password = "user3"},
new() {Id = 4, UserName = "user4", Password = "user4"}, // new() {Id = 4, Email = "user4", Password = "user4"},
new() {Id = 5, UserName = "user5", Password = "user5"}, // new() {Id = 5, Email = "user5", Password = "user5"},
}; // };
modelBuilder.Entity<UserAccount>().HasData(demoUserAccounts); // modelBuilder.Entity<UserAccount>().HasData(demoUserAccounts);
//
var demoUserAccountPolicies = new UserAccountPolicy[] // var demoUserAccountPolicies = new UserAccountPolicy[]
{ // {
/* User 1 Policies */ // /* User 1 Policies */
new() {Id = 1, UserAccountId = 1, UserPolicy = UserPolicy.VIEW_PRODUCT, IsEnabled = false}, // new() {Id = 1, UserAccountId = 1, UserPolicy = UserPolicy.VIEW_PRODUCT, IsEnabled = false},
new() {Id = 2, UserAccountId = 1, UserPolicy = UserPolicy.ADD_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 = 3, UserAccountId = 1, UserPolicy = UserPolicy.EDIT_PRODUCT, IsEnabled = false},
new() {Id = 4, UserAccountId = 1, UserPolicy = UserPolicy.DELETE_PRODUCT, IsEnabled = false}, // new() {Id = 4, UserAccountId = 1, UserPolicy = UserPolicy.DELETE_PRODUCT, IsEnabled = false},
//
/* User 2 Policies */ // /* User 2 Policies */
new() {Id = 5, UserAccountId = 2, UserPolicy = UserPolicy.VIEW_PRODUCT, IsEnabled = true}, // new() {Id = 5, UserAccountId = 2, UserPolicy = UserPolicy.VIEW_PRODUCT, IsEnabled = true},
new() {Id = 6, UserAccountId = 2, UserPolicy = UserPolicy.ADD_PRODUCT, IsEnabled = false}, // new() {Id = 6, UserAccountId = 2, UserPolicy = UserPolicy.ADD_PRODUCT, IsEnabled = false},
new() {Id = 7, UserAccountId = 2, UserPolicy = UserPolicy.EDIT_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}, // new() {Id = 8, UserAccountId = 2, UserPolicy = UserPolicy.DELETE_PRODUCT, IsEnabled = false},
//
/* User 3 Policies */ // /* User 3 Policies */
new() {Id = 9, UserAccountId = 3, UserPolicy = UserPolicy.VIEW_PRODUCT, IsEnabled = true}, // new() {Id = 9, UserAccountId = 3, UserPolicy = UserPolicy.VIEW_PRODUCT, IsEnabled = true},
new() {Id = 10, UserAccountId = 3, UserPolicy = UserPolicy.ADD_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 = 11, UserAccountId = 3, UserPolicy = UserPolicy.EDIT_PRODUCT, IsEnabled = false},
new() {Id = 12, UserAccountId = 3, UserPolicy = UserPolicy.DELETE_PRODUCT, IsEnabled = false}, // new() {Id = 12, UserAccountId = 3, UserPolicy = UserPolicy.DELETE_PRODUCT, IsEnabled = false},
//
/* User 4 Policies */ // /* User 4 Policies */
new() {Id = 13, UserAccountId = 4, UserPolicy = UserPolicy.VIEW_PRODUCT, IsEnabled = true}, // new() {Id = 13, UserAccountId = 4, UserPolicy = UserPolicy.VIEW_PRODUCT, IsEnabled = true},
new() {Id = 14, UserAccountId = 4, UserPolicy = UserPolicy.ADD_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 = 15, UserAccountId = 4, UserPolicy = UserPolicy.EDIT_PRODUCT, IsEnabled = true},
new() {Id = 16, UserAccountId = 4, UserPolicy = UserPolicy.DELETE_PRODUCT, IsEnabled = false}, // new() {Id = 16, UserAccountId = 4, UserPolicy = UserPolicy.DELETE_PRODUCT, IsEnabled = false},
//
/* User 5 Policies */ // /* User 5 Policies */
new() {Id = 17, UserAccountId = 5, UserPolicy = UserPolicy.VIEW_PRODUCT, IsEnabled = true}, // new() {Id = 17, UserAccountId = 5, UserPolicy = UserPolicy.VIEW_PRODUCT, IsEnabled = true},
new() {Id = 18, UserAccountId = 5, UserPolicy = UserPolicy.ADD_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 = 19, UserAccountId = 5, UserPolicy = UserPolicy.EDIT_PRODUCT, IsEnabled = true},
new() {Id = 20, UserAccountId = 5, UserPolicy = UserPolicy.DELETE_PRODUCT, IsEnabled = true}, // new() {Id = 20, UserAccountId = 5, UserPolicy = UserPolicy.DELETE_PRODUCT, IsEnabled = true},
}; // };
modelBuilder.Entity<UserAccountPolicy>().HasData(demoUserAccountPolicies); // modelBuilder.Entity<UserAccountPolicy>().HasData(demoUserAccountPolicies);
} }
public DbSet<UserAccount> UserAccounts { get; set; } 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 /> // <auto-generated />
using System;
using BlazorPolicyAuth.Data; using BlazorPolicyAuth.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -23,51 +24,34 @@ namespace BlazorPolicyAuth.Migrations
.HasColumnType("INTEGER") .HasColumnType("INTEGER")
.HasColumnName("id"); .HasColumnName("id");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("email");
b.Property<string>("Password") b.Property<string>("Password")
.HasMaxLength(100) .HasMaxLength(100)
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasColumnName("password"); .HasColumnName("password");
b.Property<string>("UserName") b.Property<byte[]>("PasswordHash")
.HasMaxLength(100) .IsRequired()
.HasColumnType("TEXT") .HasColumnType("BLOB");
.HasColumnName("user_name");
b.Property<byte[]>("PasswordSalt")
.IsRequired()
.HasColumnType("BLOB");
b.Property<string>("Role")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("user_account"); 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 => modelBuilder.Entity("BlazorPolicyAuth.Models.Entities.UserAccountPolicy", b =>
@@ -92,148 +76,6 @@ namespace BlazorPolicyAuth.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("user_account_policy"); 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 #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; using System.ComponentModel.DataAnnotations.Schema;
namespace BlazorPolicyAuth.Models.Entities; namespace BlazorPolicyAuth.Models.Entities;
@@ -11,11 +12,17 @@ public class UserAccount
[Column("id")] [Column("id")]
public int Id { get; set; } public int Id { get; set; }
[Column("user_name")] [Column("email")]
[MaxLength(100)] [MaxLength(200)]
public string? UserName { get; set; } public string? Email { get; set; }
[Column("password")] [Column("password")]
[MaxLength(100)] [MaxLength(100)]
public string? Password { get; set; } 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;
using BlazorPolicyAuth.Components; using BlazorPolicyAuth.Components;
using BlazorPolicyAuth.Data; using BlazorPolicyAuth.Data;
using BlazorPolicyAuth.Models.ViewModels;
using BlazorPolicyAuth.Services.AuthService;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore; 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); var builder = WebApplication.CreateBuilder(args);
@@ -30,6 +41,20 @@ builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationSc
}); });
builder.Services.AddCascadingAuthenticationState(); 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(); var app = builder.Build();
@@ -49,4 +74,11 @@ 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.MapPost("/api/auth/login", async (UserLogin request, IAuthService authService) =>
await authService.Login(request.Email, request.Password));
app.Run(); 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 public class UserPolicy
{ {

View File

@@ -9,5 +9,8 @@
"Microsoft.AspNetCore": "Warning" "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.