fluent login base

main
Peace 9 months ago
parent 99f3eeb2a4
commit 978e50acfb
  1. 25
      FluentPolicyAuth/FluentPolicyAuth.sln
  2. 20
      FluentPolicyAuth/FluentPolicyAuth/Components/App.razor
  3. 35
      FluentPolicyAuth/FluentPolicyAuth/Components/Layout/MainLayout.razor
  4. 21
      FluentPolicyAuth/FluentPolicyAuth/Components/Layout/NavMenu.razor
  5. 10
      FluentPolicyAuth/FluentPolicyAuth/Components/Pages/Account/AccessDenied.razor
  6. 75
      FluentPolicyAuth/FluentPolicyAuth/Components/Pages/Account/Login.razor
  7. 27
      FluentPolicyAuth/FluentPolicyAuth/Components/Pages/Account/Logout.razor
  8. 22
      FluentPolicyAuth/FluentPolicyAuth/Components/Pages/Counter.razor
  9. 36
      FluentPolicyAuth/FluentPolicyAuth/Components/Pages/Error.razor
  10. 11
      FluentPolicyAuth/FluentPolicyAuth/Components/Pages/Home.razor
  11. 51
      FluentPolicyAuth/FluentPolicyAuth/Components/Pages/Weather.razor
  12. 6
      FluentPolicyAuth/FluentPolicyAuth/Components/Routes.razor
  13. 15
      FluentPolicyAuth/FluentPolicyAuth/Components/_Imports.razor
  14. 17
      FluentPolicyAuth/FluentPolicyAuth/Data/AppDbContext.cs
  15. 24
      FluentPolicyAuth/FluentPolicyAuth/FluentPolicyAuth.csproj
  16. 102
      FluentPolicyAuth/FluentPolicyAuth/Migrations/20240813062503_createIdentityMariaDB.Designer.cs
  17. 83
      FluentPolicyAuth/FluentPolicyAuth/Migrations/20240813062503_createIdentityMariaDB.cs
  18. 99
      FluentPolicyAuth/FluentPolicyAuth/Migrations/AppDbContextModelSnapshot.cs
  19. 17
      FluentPolicyAuth/FluentPolicyAuth/Models/Entities/Product.cs
  20. 15
      FluentPolicyAuth/FluentPolicyAuth/Models/Entities/User.cs
  21. 13
      FluentPolicyAuth/FluentPolicyAuth/Models/Entities/UserPolicy.cs
  22. 13
      FluentPolicyAuth/FluentPolicyAuth/Models/ViewModels/LoginViewModel.cs
  23. 10
      FluentPolicyAuth/FluentPolicyAuth/Models/ViewModels/RegisterViewModel.cs
  24. 21
      FluentPolicyAuth/FluentPolicyAuth/Policy.cs
  25. 67
      FluentPolicyAuth/FluentPolicyAuth/Program.cs
  26. 38
      FluentPolicyAuth/FluentPolicyAuth/Properties/launchSettings.json
  27. 10
      FluentPolicyAuth/FluentPolicyAuth/Responses/CustomResponses.cs
  28. 12
      FluentPolicyAuth/FluentPolicyAuth/Services/IUserService.cs
  29. 37
      FluentPolicyAuth/FluentPolicyAuth/Services/UserService.cs
  30. 8
      FluentPolicyAuth/FluentPolicyAuth/appsettings.Development.json
  31. 12
      FluentPolicyAuth/FluentPolicyAuth/appsettings.json
  32. 191
      FluentPolicyAuth/FluentPolicyAuth/wwwroot/app.css
  33. 7
      FluentPolicyAuth/FluentPolicyAuth/wwwroot/bootstrap/bootstrap.min.css
  34. 1
      FluentPolicyAuth/FluentPolicyAuth/wwwroot/bootstrap/bootstrap.min.css.map
  35. BIN
      FluentPolicyAuth/FluentPolicyAuth/wwwroot/favicon.ico

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.10.35013.160
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentPolicyAuth", "FluentPolicyAuth\FluentPolicyAuth.csproj", "{2A527D8B-3ADC-488E-BF22-2A9820A61B31}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2A527D8B-3ADC-488E-BF22-2A9820A61B31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2A527D8B-3ADC-488E-BF22-2A9820A61B31}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2A527D8B-3ADC-488E-BF22-2A9820A61B31}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2A527D8B-3ADC-488E-BF22-2A9820A61B31}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2F6A8294-77DC-4F29-9B4B-768CE8BBBE5C}
EndGlobalSection
EndGlobal

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="FluentPolicyAuth.styles.css" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<HeadOutlet />
</head>
<body>
<Routes />
<script src="_framework/blazor.web.js"></script>
</body>
</html>

@ -0,0 +1,35 @@
@inherits LayoutComponentBase
<FluentLayout>
<FluentHeader>
FluentPolicyAuth
<FluentSpacer />
<AuthorizeView>
<Authorized>
<FluentAnchor Href="/logout" Appearance="Appearance.Neutral">Logout</FluentAnchor>
</Authorized>
<NotAuthorized>
<FluentAnchor Href="/login" Appearance="Appearance.Neutral">Login</FluentAnchor>
</NotAuthorized>
</AuthorizeView>
</FluentHeader>
<FluentStack Class="main" Orientation="Orientation.Horizontal" Width="100%">
<NavMenu />
<FluentBodyContent Class="body-content">
<div class="content">
@Body
</div>
</FluentBodyContent>
</FluentStack>
<FluentFooter>
<a href="https://www.fluentui-blazor.net" target="_blank">Documentation and demos</a>
<FluentSpacer />
<a href="https://learn.microsoft.com/en-us/aspnet/core/blazor" target="_blank">About Blazor</a>
</FluentFooter>
</FluentLayout>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>

@ -0,0 +1,21 @@
@rendermode InteractiveServer
<div class="navmenu">
<input type="checkbox" title="Menu expand/collapse toggle" id="navmenu-toggle" class="navmenu-icon" />
<label for="navmenu-toggle" class="navmenu-icon"><FluentIcon Value="@(new Icons.Regular.Size20.Navigation())" Color="Color.Fill" /></label>
<nav class="sitenav" aria-labelledby="main-menu">
<FluentNavMenu Id="main-menu" Collapsible="true" Width="250" Title="Navigation menu" @bind-Expanded="expanded" CustomToggle="true">
<FluentNavLink Href="/" Match="NavLinkMatch.All" Icon="@(new Icons.Regular.Size20.Home())" IconColor="Color.Accent">Home</FluentNavLink>
<AuthorizeView>
<Authorized>
<FluentNavLink Href="counter" Icon="@(new Icons.Regular.Size20.NumberSymbolSquare())" IconColor="Color.Accent">Counter</FluentNavLink>
<FluentNavLink Href="weather" Icon="@(new Icons.Regular.Size20.WeatherPartlyCloudyDay())" IconColor="Color.Accent">Weather</FluentNavLink>
</Authorized>
</AuthorizeView>
</FluentNavMenu>
</nav>
</div>
@code {
private bool expanded = true;
}

@ -0,0 +1,10 @@
@page "/access-denied"
<FluentGrid Class="mt-3" Spacing="1" Justify="JustifyContent.Center">
<FluentGridItem>
<FluentStack Orientation="Orientation.Vertical" HorizontalAlignment="HorizontalAlignment.Center">
<FluentEmoji Value="@(new Emojis.SmileysEmotion.Color.Default.ThinkingFace())" Width="5rem" />
<FluentLabel Color="Color.Warning">You don't have permission to access this page.</FluentLabel>
</FluentStack>
</FluentGridItem>
</FluentGrid>

@ -0,0 +1,75 @@
@page "/login"
@using System.Security.Claims
@using FluentPolicyAuth.Models.ViewModels
@using FluentPolicyAuth.Services
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Authentication.Cookies
@using Microsoft.EntityFrameworkCore
@inject IUserService UserService
@inject NavigationManager NavManager
<style>
.fullwidth {
width:100%
}
</style>
<FluentGrid Class="mt-3" Spacing="1" Justify="JustifyContent.Center">
<FluentGridItem Class="py-4 border rounded" xs="12" sm="8" lg="4">
<FluentEditForm Model="@Model" OnSubmit="Authenticate" FormName="loginform">
<DataAnnotationsValidator />
<FluentValidationSummary />
<FluentStack Orientation="Orientation.Vertical" VerticalGap="3" HorizontalAlignment="HorizontalAlignment.Center">
<div>
<FluentEmoji Value="@(new Emojis.SmileysEmotion.Color.Default.GrinningFace())" Width="5rem" />
<h3>LOGIN</h3>
</div>
<div class="fullwidth">
<FluentTextField class="fullwidth" Style="width:100%" Name="Model.UserName" @bind-Value="Model.UserName" Label="User name" Placeholder="User name" Required />
<ValidationMessage class="fullwidth" For="@(() => Model.UserName)" />
</div>
<div class="fullwidth">
<FluentTextField class="fullwidth" Name="Model.Password" @bind-Value="Model.Password" TextFieldType="TextFieldType.Password" Label="password" Placeholder="Password" Required />
<ValidationMessage class="fullwidth" For="@(() => Model.Password)" />
</div>
<FluentLabel class="fullwidth" Color="Color.Warning">@errorMessage?.ToString()</FluentLabel>
<FluentButton class="fullwidth" Type="ButtonType.Submit" Appearance="Appearance.Accent">Login</FluentButton>
</FluentStack>
</FluentEditForm>
</FluentGridItem>
</FluentGrid>
@code {
[CascadingParameter]
public HttpContext? HttpContext { get; set; }
[SupplyParameterFromForm]
public LoginViewModel Model { get; set; } = new();
private string? errorMessage;
private async Task Authenticate()
{
LoginResponse loginResponse = await UserService.LoginAsync(Model);
if (!loginResponse.Success)
{
errorMessage = loginResponse.Message;
return;
}
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, loginResponse.user.Name),
};
foreach (var policy in loginResponse.policies)
claims.Add(new Claim(policy.Policy, "true"));
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
await HttpContext?.SignInAsync(principal);
NavManager.NavigateTo("/");
}
}

@ -0,0 +1,27 @@
@page "/logout"
@using Microsoft.AspNetCore.Authentication
@inject NavigationManager NavManager
<FluentGrid Class="mt-3" Spacing="1" Justify="JustifyContent.Center">
<FluentGridItem>
<FluentStack Orientation="Orientation.Vertical" HorizontalAlignment="HorizontalAlignment.Center">
<FluentEmoji Value="@(new Emojis.SmileysEmotion.Color.Default.HuggingFace())" Width="5rem" />
<FluentLabel>You've successfully logged out of the application.</FluentLabel>
</FluentStack>
</FluentGridItem>
</FluentGrid>
@code {
[CascadingParameter]
public HttpContext? HttpContext { get; set; }
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
if (HttpContext.User.Identity.IsAuthenticated)
{
await HttpContext.SignOutAsync();
NavManager.NavigateTo("/logout", true);
}
}
}

@ -0,0 +1,22 @@
@page "/counter"
@rendermode InteractiveServer
@attribute [Authorize]
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<div role="status" style="padding-bottom: 1em;">
Current count: <FluentBadge Appearance="Appearance.Neutral">@currentCount</FluentBadge>
</div>
<FluentButton Appearance="Appearance.Accent" @onclick="IncrementCount">Click me</FluentButton>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}

@ -0,0 +1,36 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}

@ -0,0 +1,11 @@
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, welcome to <b class="text-danger">BLAZOR 🔥 FluentUI</b></h1>
<AuthorizeView>
<Authorized>
<h4>Logged in as <b>@context.User.Identity.Name</b></h4>
</Authorized>
</AuthorizeView>

@ -0,0 +1,51 @@
@page "/weather"
@attribute [StreamRendering]
@attribute [Authorize(Policy = Policy.VIEW_PRODUCT)]
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<!-- This page is rendered in SSR mode, so the FluentDataGrid component does not offer any interactivity (like sorting). -->
<FluentDataGrid Id="weathergrid" Items="@forecasts" GridTemplateColumns="1fr 1fr 1fr 2fr" TGridItem="WeatherForecast">
<PropertyColumn Title="Date" Property="@(c => c!.Date)" Align="Align.Start"/>
<PropertyColumn Title="Temp. (C)" Property="@(c => c!.TemperatureC)" Align="Align.Center"/>
<PropertyColumn Title="Temp. (F)" Property="@(c => c!.TemperatureF)" Align="Align.Center"/>
<PropertyColumn Title="Summary" Property="@(c => c!.Summary)" Align="Align.End"/>
</FluentDataGrid>
}
@code {
private IQueryable<WeatherForecast>? forecasts;
protected override async Task OnInitializedAsync()
{
// Simulate asynchronous loading to demonstrate streaming rendering
await Task.Delay(500);
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).AsQueryable();
}
private class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}

@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>

@ -0,0 +1,15 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.FluentUI.AspNetCore.Components
@using Microsoft.JSInterop
@using FluentPolicyAuth
@using FluentPolicyAuth.Components
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Authorization
@using FluentPolicyAuth.Data
@using static FluentPolicyAuth.Responses.CustomResponses

@ -0,0 +1,17 @@
using FluentPolicyAuth.Models.Entities;
using Microsoft.EntityFrameworkCore;
namespace FluentPolicyAuth.Data
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions options) : base(options)
{
}
public DbSet<User> Users { get; set; }
public DbSet<UserPolicy> UserPolicies { get; set; }
public DbSet<Product> Products { get; set; }
}
}

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components" Version="4.*-* " />
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Emoji" Version="4.6.0" />
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" Version="4.*-* " />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
</ItemGroup>
<ItemGroup>
<Folder Include="Components\Pages\Product\" />
</ItemGroup>
</Project>

@ -0,0 +1,102 @@
// <auto-generated />
using System;
using FluentPolicyAuth.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FluentPolicyAuth.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20240813062503_createIdentityMariaDB")]
partial class createIdentityMariaDB
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
modelBuilder.Entity("FluentPolicyAuth.Models.Entities.Product", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<int?>("Price")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("Products");
});
modelBuilder.Entity("FluentPolicyAuth.Models.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("Password")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("FluentPolicyAuth.Models.Entities.UserPolicy", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsEnable")
.HasColumnType("tinyint(1)");
b.Property<string>("Policy")
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("UserPolicies");
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,83 @@
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FluentPolicyAuth.Migrations
{
/// <inheritdoc />
public partial class createIdentityMariaDB : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Products",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Code = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Name = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Price = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Products", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "UserPolicies",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
UserId = table.Column<int>(type: "int", nullable: false),
Policy = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
IsEnable = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserPolicies", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Name = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Password = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: false)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Products");
migrationBuilder.DropTable(
name: "UserPolicies");
migrationBuilder.DropTable(
name: "Users");
}
}
}

@ -0,0 +1,99 @@
// <auto-generated />
using System;
using FluentPolicyAuth.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FluentPolicyAuth.Migrations
{
[DbContext(typeof(AppDbContext))]
partial class AppDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
modelBuilder.Entity("FluentPolicyAuth.Models.Entities.Product", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<int?>("Price")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("Products");
});
modelBuilder.Entity("FluentPolicyAuth.Models.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.Property<string>("Password")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("varchar(100)");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("FluentPolicyAuth.Models.Entities.UserPolicy", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsEnable")
.HasColumnType("tinyint(1)");
b.Property<string>("Policy")
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("UserPolicies");
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
namespace FluentPolicyAuth.Models.Entities
{
public class Product
{
public int Id { get; set; }
[MaxLength(100)]
public string Code { get; set; }
[MaxLength(100)]
public string Name { get; set; }
public int? Price { get; set; }
}
}

@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
namespace FluentPolicyAuth.Models.Entities
{
public class User
{
public int Id { get; set; }
[MaxLength(100)]
public string Name { get; set; }
[MaxLength(100)]
public string Password { get; set; }
}
}

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace FluentPolicyAuth.Models.Entities
{
public class UserPolicy
{
public int Id { get; set; }
public int UserId { get; set; }
[MaxLength(50)]
public string? Policy { get; set; }
public bool IsEnable { get; set; }
}
}

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace FluentPolicyAuth.Models.ViewModels
{
public class LoginViewModel
{
[Required(AllowEmptyStrings = false, ErrorMessage = "Please enter user name"), DataType(DataType.Text), StringLength(50, MinimumLength = 2)]
public string? UserName { get; set; }
[Required(AllowEmptyStrings = false, ErrorMessage = "Please enter password"), DataType(DataType.Password), StringLength(50, MinimumLength = 4)]
public string? Password { get; set; }
}
}

@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;
namespace FluentPolicyAuth.Models.ViewModels
{
public class RegisterViewModel : LoginViewModel
{
[Required, DataType(DataType.Password), Compare(nameof(Password))]
public string ConfirmPassword { get; set; } = string.Empty;
}
}

@ -0,0 +1,21 @@
namespace FluentPolicyAuth
{
public class Policy
{
public const string VIEW_PRODUCT = "VIEW_PRODUCT";
public const string ADD_PRODUCT = "ADD_PRODUCT";
public const string EDIT_PRODUCT = "EDIT_PRODUCT";
public const string DELETE_PRODUCT = "DELETE_PRODUCT";
public static List<string> GetPoilicies()
{
return new List<string>
{
VIEW_PRODUCT,
ADD_PRODUCT,
EDIT_PRODUCT,
DELETE_PRODUCT
};
}
}
}

@ -0,0 +1,67 @@
using FluentPolicyAuth.Components;
using FluentPolicyAuth.Data;
using FluentPolicyAuth.Services;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.EntityFrameworkCore;
using Microsoft.FluentUI.AspNetCore.Components;
namespace FluentPolicyAuth
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddFluentUIComponents();
// For authentication
builder.Services.AddAuthorization(config =>
{
foreach (var userPolicy in Policy.GetPoilicies())
config.AddPolicy(userPolicy, c => c.RequireClaim(userPolicy, "true"));
});
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = "auth_token";
options.LoginPath = "/login";
options.Cookie.MaxAge = TimeSpan.FromMinutes(30);
options.AccessDeniedPath = "/access-denied";
});
builder.Services.AddCascadingAuthenticationState();
// For database
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));
});
builder.Services.AddScoped<IUserService, UserService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();
}
}
}

@ -0,0 +1,38 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:14060",
"sslPort": 44324
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5233",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7261;http://localhost:5233",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

@ -0,0 +1,10 @@
using FluentPolicyAuth.Models.Entities;
namespace FluentPolicyAuth.Responses
{
public class CustomResponses
{
public record RegistrationResponse(bool Success = false, string Message = null!);
public record LoginResponse(bool Success = false, string Message = null!, User user = null!, List<UserPolicy> policies = null!);
}
}

@ -0,0 +1,12 @@
using FluentPolicyAuth.Models.Entities;
using FluentPolicyAuth.Models.ViewModels;
using static FluentPolicyAuth.Responses.CustomResponses;
namespace FluentPolicyAuth.Services
{
public interface IUserService
{
Task<RegistrationResponse> RegisterAsync(RegisterViewModel model);
Task<LoginResponse> LoginAsync(LoginViewModel model);
}
}

@ -0,0 +1,37 @@
using FluentPolicyAuth.Data;
using FluentPolicyAuth.Models.Entities;
using FluentPolicyAuth.Models.ViewModels;
using Microsoft.EntityFrameworkCore;
using static FluentPolicyAuth.Responses.CustomResponses;
namespace FluentPolicyAuth.Services
{
public class UserService : IUserService
{
private AppDbContext _appDbContext;
public UserService(AppDbContext appDbContext)
{
_appDbContext = appDbContext;
}
public async Task<LoginResponse> LoginAsync(LoginViewModel model)
{
if (string.IsNullOrWhiteSpace(model.UserName) || string.IsNullOrWhiteSpace(model.Password))
return new LoginResponse(false, "Invalid account information");
var user = _appDbContext.Users.Where(u => u.Name == model.UserName).FirstOrDefault();
if (user is null || user.Password != model.Password)
return new LoginResponse(false, "Invalid account information");
var policies = await _appDbContext.UserPolicies.Where(p => p.UserId == user.Id && p.IsEnable).ToListAsync();
return new LoginResponse(true, "Success", user, policies);
}
public Task<RegistrationResponse> RegisterAsync(RegisterViewModel model)
{
throw new NotImplementedException();
}
}
}

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

@ -0,0 +1,12 @@
{
"ConnectionStrings": {
"DefaultConnection": "Server=peacecloud.synology.me; Port=23306; Database=BLAZORAUTH; Uid=pds; Pwd=Pds92070983!@"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

@ -0,0 +1,191 @@
@import '/_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css';
body {
--body-font: "Segoe UI Variable", "Segoe UI", sans-serif;
font-family: var(--body-font);
font-size: var(--type-ramp-base-font-size);
line-height: var(--type-ramp-base-line-height);
margin: 0;
}
.navmenu-icon {
display: none;
}
.main {
min-height: calc(100dvh - 86px);
color: var(--neutral-foreground-rest);
align-items: stretch !important;
}
.body-content {
align-self: stretch;
height: calc(100dvh - 86px) !important;
display: flex;
}
.content {
padding: 0.5rem 1.5rem;
align-self: stretch !important;
width: 100%;
}
.manage {
width: 100dvw;
}
footer {
background: var(--neutral-layer-4);
color: var(--neutral-foreground-rest);
align-items: center;
padding: 10px 10px;
}
footer a {
color: var(--neutral-foreground-rest);
text-decoration: none;
}
footer a:focus {
outline: 1px dashed;
outline-offset: 3px;
}
footer a:hover {
text-decoration: underline;
}
.alert {
border: 1px dashed var(--accent-fill-rest);
padding: 5px;
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
margin: 20px 0;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::before {
content: "An error has occurred. "
}
.loading-progress {
position: relative;
display: block;
width: 8rem;
height: 8rem;
margin: 20vh auto 1rem auto;
}
.loading-progress circle {
fill: none;
stroke: #e0e0e0;
stroke-width: 0.6rem;
transform-origin: 50% 50%;
transform: rotate(-90deg);
}
.loading-progress circle:last-child {
stroke: #1b6ec2;
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
transition: stroke-dasharray 0.05s ease-in-out;
}
.loading-progress-text {
position: absolute;
text-align: center;
font-weight: bold;
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
}
.loading-progress-text:after {
content: var(--blazor-load-percentage-text, "Loading");
}
code {
color: #c02d76;
}
@media (max-width: 600px) {
.header-gutters {
margin: 0.5rem 3rem 0.5rem 1.5rem !important;
}
[dir="rtl"] .header-gutters {
margin: 0.5rem 1.5rem 0.5rem 3rem !important;
}
.main {
flex-direction: column !important;
row-gap: 0 !important;
}
nav.sitenav {
width: 100%;
height: 100%;
}
#main-menu {
width: 100% !important;
}
#main-menu > div:first-child:is(.expander) {
display: none;
}
.navmenu {
width: 100%;
}
#navmenu-toggle {
appearance: none;
}
#navmenu-toggle ~ nav {
display: none;
}
#navmenu-toggle:checked ~ nav {
display: block;
}
.navmenu-icon {
cursor: pointer;
z-index: 10;
display: block;
position: absolute;
top: 15px;
left: unset;
right: 20px;
width: 20px;
height: 20px;
border: none;
}
[dir="rtl"] .navmenu-icon {
left: 20px;
right: unset;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Loading…
Cancel
Save