~ login page

main
Peace 9 months ago
parent 379025365c
commit 9153dabed7
  1. 25
      FluentBlazorApp/FluentBlazorApp.sln
  2. 25
      FluentBlazorApp/FluentBlazorApp/Components/App.razor
  3. 29
      FluentBlazorApp/FluentBlazorApp/Components/Layout/MainLayout.razor
  4. 19
      FluentBlazorApp/FluentBlazorApp/Components/Layout/NavMenu.razor
  5. 21
      FluentBlazorApp/FluentBlazorApp/Components/Pages/Counter.razor
  6. 36
      FluentBlazorApp/FluentBlazorApp/Components/Pages/Error.razor
  7. 7
      FluentBlazorApp/FluentBlazorApp/Components/Pages/Home.razor
  8. 43
      FluentBlazorApp/FluentBlazorApp/Components/Pages/Login.razor
  9. 50
      FluentBlazorApp/FluentBlazorApp/Components/Pages/Weather.razor
  10. 12
      FluentBlazorApp/FluentBlazorApp/Components/Routes.razor
  11. 23
      FluentBlazorApp/FluentBlazorApp/Components/_Imports.razor
  12. 34
      FluentBlazorApp/FluentBlazorApp/Controllers/AccountController.cs
  13. 4
      FluentBlazorApp/FluentBlazorApp/DTOs/CustomUserCliams.cs
  14. 13
      FluentBlazorApp/FluentBlazorApp/DTOs/LoginDTO.cs
  15. 13
      FluentBlazorApp/FluentBlazorApp/DTOs/RegisterDTO.cs
  16. 15
      FluentBlazorApp/FluentBlazorApp/Data/AppDbContext.cs
  17. 28
      FluentBlazorApp/FluentBlazorApp/FluentBlazorApp.csproj
  18. 54
      FluentBlazorApp/FluentBlazorApp/Migrations/20240802051133_First.Designer.cs
  19. 44
      FluentBlazorApp/FluentBlazorApp/Migrations/20240802051133_First.cs
  20. 51
      FluentBlazorApp/FluentBlazorApp/Migrations/AppDbContextModelSnapshot.cs
  21. 13
      FluentBlazorApp/FluentBlazorApp/Models/ApplicationUser.cs
  22. 98
      FluentBlazorApp/FluentBlazorApp/Program.cs
  23. 38
      FluentBlazorApp/FluentBlazorApp/Properties/launchSettings.json
  24. 85
      FluentBlazorApp/FluentBlazorApp/Repos/Account.cs
  25. 11
      FluentBlazorApp/FluentBlazorApp/Repos/IAccount.cs
  26. 8
      FluentBlazorApp/FluentBlazorApp/Responses/CustomResponses.cs
  27. 30
      FluentBlazorApp/FluentBlazorApp/Services/AccountService.cs
  28. 11
      FluentBlazorApp/FluentBlazorApp/Services/IAccountService.cs
  29. 7
      FluentBlazorApp/FluentBlazorApp/States/Constants.cs
  30. 73
      FluentBlazorApp/FluentBlazorApp/States/CustomAuthenticationStateProvider.cs
  31. 8
      FluentBlazorApp/FluentBlazorApp/appsettings.Development.json
  32. 18
      FluentBlazorApp/FluentBlazorApp/appsettings.json
  33. 40
      FluentBlazorApp/FluentBlazorApp/wwwroot/app-cutom.css
  34. 191
      FluentBlazorApp/FluentBlazorApp/wwwroot/app.css
  35. 7
      FluentBlazorApp/FluentBlazorApp/wwwroot/bootstrap/bootstrap.min.css
  36. 1
      FluentBlazorApp/FluentBlazorApp/wwwroot/bootstrap/bootstrap.min.css.map
  37. BIN
      FluentBlazorApp/FluentBlazorApp/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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentBlazorApp", "FluentBlazorApp\FluentBlazorApp.csproj", "{FD9E4832-8E83-4472-B116-D61F5DBD6B79}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{FD9E4832-8E83-4472-B116-D61F5DBD6B79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FD9E4832-8E83-4472-B116-D61F5DBD6B79}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FD9E4832-8E83-4472-B116-D61F5DBD6B79}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FD9E4832-8E83-4472-B116-D61F5DBD6B79}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0DEA2766-5A37-4848-AC55-8A063C6B2A7E}
EndGlobalSection
EndGlobal

@ -0,0 +1,25 @@
<!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="app.css" />
<link rel="stylesheet" href="FluentBlazorApp.styles.css" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="app-cutom.css" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link href="_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css" rel="stylesheet" />
<HeadOutlet />
</head>
<body>
<Routes @rendermode="RenderMode.InteractiveServer" />
<script src="_content/Microsoft.FluentUI.AspNetCore.Components/js/loading-theme.js" type="text/javascript"></script>
<loading-theme storage-name="theme"></loading-theme>
<script src="_framework/blazor.web.js"></script>
<script src="_content/Microsoft.FluentUI.AspNetCore.Components/Microsoft.FluentUI.AspNetCore.Components.lib.module.js" type="module" async></script>
</body>
</html>

@ -0,0 +1,29 @@
@inherits LayoutComponentBase
<FluentLayout>
<FluentHeader>
FluentBlazorApp
</FluentHeader>
<FluentStack Class="main" Orientation="Orientation.Horizontal" Width="100%">
<NavMenu />
<FluentBodyContent Class="body-content">
<div class="content">
@Body
</div>
<FluentToastProvider MaxToastCount="10" />
</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>
<FluentToastProvider />
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>

@ -0,0 +1,19 @@
@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>
<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>
<FluentNavLink Href="login" Icon="@(new Icons.Regular.Size20.PersonPasskey())" IconColor="Color.Accent">Login</FluentNavLink>
<FluentNavLink Href="register" Icon="@(new Icons.Regular.Size20.PersonAdd())" IconColor="Color.Accent">Register</FluentNavLink>
</FluentNavMenu>
</nav>
</div>
@code {
private bool expanded = true;
}

@ -0,0 +1,21 @@
@page "/counter"
@rendermode InteractiveServer
<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,7 @@
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new Fluent Blazor app.

@ -0,0 +1,43 @@
@page "/login"
@inject IToastService ToastService
@rendermode RenderMode.InteractiveServer
<FluentGrid Justify="JustifyContent.Center">
<FluentGridItem sm="4">
<h2>Login</h2>
<FluentEditForm Model="@login" OnValidSubmit="@OnLoginSubmit">
<DataAnnotationsValidator/>
<FluentValidationSummary/>
<FluentStack Orientation="Orientation.Vertical">
<div>
<FluentTextField @bind-Value="login.Name" Label="Id" Required/>
<FluentValidationMessage For="@(() => login.Name)"/>
</div>
<div>
<FluentTextField @bind-Value="login.Password" TextFieldType="TextFieldType.Password" Label="Password" Required />
<FluentValidationMessage For="@(() => login.Password)" />
</div>
<FluentButton Type="ButtonType.Submit" Appearance="Appearance.Accent">Login</FluentButton>
</FluentStack>
</FluentEditForm>
</FluentGridItem>
</FluentGrid>
@code {
LoginDTO login = new LoginDTO();
async Task OnLoginSubmit(EditContext context)
{
LoginResponse response = await AccountService.LoginAsync(login);
if (!response.Flag)
{
ToastService.ShowError(response.message, 5000);
return;
}
var customAuthProvider = (CustomAuthenticationStateProvider)AuthStateProvider;
customAuthProvider.UpdateAuthenticationState(response.JWTToken);
NavManager.NavigateTo("/", forceLoad: true);
}
}

@ -0,0 +1,50 @@
@page "/weather"
@attribute [StreamRendering]
<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,12 @@
<CascadingAuthenticationState>
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
<Authorizing>
Please wait...
</Authorizing>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>
</CascadingAuthenticationState>

@ -0,0 +1,23 @@
@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 FluentBlazorApp
@using FluentBlazorApp.Components
@using Microsoft.AspNetCore.Components.Authorization
@using static FluentBlazorApp.Responses.CustomResponses
@using FluentBlazorApp.Services
@using FluentBlazorApp.DTOs
@using FluentBlazorApp.Responses
@using FluentBlazorApp.States
@inject IAccountService AccountService
@inject NavigationManager NavManager
@inject AuthenticationStateProvider AuthStateProvider

@ -0,0 +1,34 @@
using FluentBlazorApp.DTOs;
using FluentBlazorApp.Repos;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using static FluentBlazorApp.Responses.CustomResponses;
namespace FluentBlazorApp.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AccountController : ControllerBase
{
private readonly IAccount _accountRepo;
public AccountController(IAccount accountRepo)
{
_accountRepo = accountRepo;
}
[HttpPost("register")]
public async Task<ActionResult<RegistrationResonse>> RegiserAsync(RegisterDTO model)
{
var result = await _accountRepo.RegisterAsync(model);
return Ok(result);
}
[HttpPost("login")]
public async Task<ActionResult<LoginResponse>> LoginAsync(LoginDTO model)
{
var result = await _accountRepo.LoginAsync(model);
return Ok(result);
}
}
}

@ -0,0 +1,4 @@
namespace FluentBlazorApp.DTOs
{
public record CustomUserClaims(string Name = null!);
}

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace FluentBlazorApp.DTOs
{
public class LoginDTO
{
[Required, DataType(DataType.Text), StringLength(50, MinimumLength = 2)]
public string Name { get; set; } = string.Empty;
[Required, DataType(DataType.Password)]
public string Password { get; set; } = string.Empty;
}
}

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace FluentBlazorApp.DTOs
{
public class RegisterDTO : LoginDTO
{
[Required, DataType(DataType.EmailAddress), EmailAddress]
public string Email { get; set; } = string.Empty;
[Required, DataType(DataType.Password), Compare(nameof(Password))]
public string ConfirmPassword { get; set; } = string.Empty;
}
}

@ -0,0 +1,15 @@
using FluentBlazorApp.Models;
using Microsoft.EntityFrameworkCore;
namespace FluentBlazorApp.Data
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
public DbSet<ApplicationUser> Users { get; set; }
}
}

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<!--Server-->
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.7" />
<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.IdentityModel.Tokens" Version="8.0.1" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.7.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" />
<!--Client-->
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components" Version="4.*-* " />
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" Version="4.*-* " />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.7" />
</ItemGroup>
</Project>

@ -0,0 +1,54 @@
// <auto-generated />
using FluentBlazorApp.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FluentBlazorApp.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20240802051133_First")]
partial class First
{
/// <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("FluentBlazorApp.Models.ApplicationUser", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,44 @@
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FluentBlazorApp.Migrations
{
/// <inheritdoc />
public partial class First : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.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: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Email = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Password = table.Column<string>(type: "longtext", 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: "Users");
}
}
}

@ -0,0 +1,51 @@
// <auto-generated />
using FluentBlazorApp.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FluentBlazorApp.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("FluentBlazorApp.Models.ApplicationUser", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,13 @@
namespace FluentBlazorApp.Models
{
public class ApplicationUser
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
}

@ -0,0 +1,98 @@
using FluentBlazorApp.Components;
using FluentBlazorApp.Data;
using FluentBlazorApp.Repos;
using FluentBlazorApp.Services;
using FluentBlazorApp.States;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.FluentUI.AspNetCore.Components;
using Microsoft.IdentityModel.Tokens;
using System.Text;
namespace FluentBlazorApp
{
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 controller
builder.Services.AddControllers();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo { Title = "My API", Version = "v1" });
});
// For database
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));
});
// For JWT Authentication
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
};
});
// For API base-address
builder.Services.AddScoped(c => new HttpClient { BaseAddress = new Uri("https://localhost:7180") });
// For Dependency Injection
builder.Services.AddScoped<IAccount, Account>();
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
builder.Services.AddScoped<IAccountService, AccountService>();
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.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "API V1");
});
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.MapControllers();
app.UseAuthentication();
app.UseAuthorization();
app.Run();
}
}
}

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

@ -0,0 +1,85 @@
using FluentBlazorApp.Data;
using FluentBlazorApp.DTOs;
using FluentBlazorApp.Models;
using FluentBlazorApp.Responses;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using static FluentBlazorApp.Responses.CustomResponses;
namespace FluentBlazorApp.Repos
{
public class Account : IAccount
{
private readonly AppDbContext _appDbContext;
private readonly IConfiguration _config;
private readonly TimeSpan _expireHour;
public Account(AppDbContext appDbContext, IConfiguration config)
{
_appDbContext = appDbContext;
_config = config;
double expireHour = 1d;
double.TryParse(config["Jwt:ExpireDurationHour"], out expireHour);
_expireHour = TimeSpan.FromHours(expireHour);
}
public async Task<LoginResponse> LoginAsync(LoginDTO model)
{
var findUser = await GetUser(model.Name);
if (findUser == null)
return new LoginResponse(false, "User does not exist.");
if (!BCrypt.Net.BCrypt.Verify(model.Password, findUser!.Password))
return new LoginResponse(false, "Invalid account information.");
string jwtToken = GenerateToken(findUser);
return new LoginResponse(true, "Success", jwtToken);
}
public async Task<RegistrationResonse> RegisterAsync(RegisterDTO model)
{
var findUser = await GetUser(model.Name);
if (findUser != null)
return new RegistrationResonse(false, "User already exist.");
_appDbContext.Users.Add(
new ApplicationUser()
{
Name = model.Name,
Email = model.Email,
Password = BCrypt.Net.BCrypt.HashPassword(model.Password)
});
await _appDbContext.SaveChangesAsync();
return new RegistrationResonse(true, "Success");
}
private async Task<ApplicationUser> GetUser(string name)
{
return _appDbContext.Users.FirstOrDefault(u => u.Name == name);
}
private string GenerateToken(ApplicationUser user)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var userClaims = new[]
{
new Claim(ClaimTypes.Name, user.Name!),
};
var token = new JwtSecurityToken(
issuer: _config["Jwt:Issuer"],
audience: _config["Jwt:Audience"],
claims: userClaims,
expires: DateTime.Now.Add(_expireHour),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
}

@ -0,0 +1,11 @@
using FluentBlazorApp.DTOs;
using static FluentBlazorApp.Responses.CustomResponses;
namespace FluentBlazorApp.Repos
{
public interface IAccount
{
Task<RegistrationResonse> RegisterAsync(RegisterDTO model);
Task<LoginResponse> LoginAsync(LoginDTO model);
}
}

@ -0,0 +1,8 @@
namespace FluentBlazorApp.Responses
{
public class CustomResponses
{
public record RegistrationResonse(bool Flag = false, string message = null!);
public record LoginResponse(bool Flag = false, string message = null!, string JWTToken = null!);
}
}

@ -0,0 +1,30 @@
using FluentBlazorApp.DTOs;
using FluentBlazorApp.Responses;
using static FluentBlazorApp.Responses.CustomResponses;
namespace FluentBlazorApp.Services
{
public class AccountService : IAccountService
{
private readonly HttpClient _httpClient;
public AccountService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<LoginResponse> LoginAsync(LoginDTO model)
{
var resonse = await _httpClient.PostAsJsonAsync("api/account/login", model);
var result = await resonse.Content.ReadFromJsonAsync<LoginResponse>();
return result;
}
public async Task<RegistrationResonse> RegisterAsync(RegisterDTO model)
{
var resonse = await _httpClient.PostAsJsonAsync("api/account/register", model);
var result = await resonse.Content.ReadFromJsonAsync<RegistrationResonse>();
return result;
}
}
}

@ -0,0 +1,11 @@
using FluentBlazorApp.DTOs;
using static FluentBlazorApp.Responses.CustomResponses;
namespace FluentBlazorApp.Services
{
public interface IAccountService
{
Task<RegistrationResonse> RegisterAsync(RegisterDTO model);
Task<LoginResponse> LoginAsync(LoginDTO model);
}
}

@ -0,0 +1,7 @@
namespace FluentBlazorApp.States
{
public class Constants
{
public static string JWTToken { get; set; } = string.Empty;
}
}

@ -0,0 +1,73 @@
using FluentBlazorApp.DTOs;
using Microsoft.AspNetCore.Components.Authorization;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
namespace FluentBlazorApp.States
{
public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly ClaimsPrincipal ANONYMOUS = new ClaimsPrincipal(new ClaimsIdentity());
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
try
{
if (string.IsNullOrEmpty(Constants.JWTToken))
return await Task.FromResult(new AuthenticationState(ANONYMOUS));
var userClaims = DecryptToken(Constants.JWTToken);
if (userClaims == null)
return await Task.FromResult(new AuthenticationState(ANONYMOUS));
var claimsPrincipal = SetClaimPrincipal(userClaims);
return await Task.FromResult(new AuthenticationState(claimsPrincipal));
}
catch
{
return await Task.FromResult(new AuthenticationState(ANONYMOUS));
}
}
public async void UpdateAuthenticationState(string jwtToken)
{
var claimsPrincipal = new ClaimsPrincipal();
if (!string.IsNullOrEmpty(jwtToken))
{
Constants.JWTToken = jwtToken;
var getUserClaims = DecryptToken(jwtToken);
claimsPrincipal = SetClaimPrincipal(getUserClaims);
}
else
{
Constants.JWTToken = null!;
}
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(claimsPrincipal)));
}
public static CustomUserClaims DecryptToken(string jwtToken)
{
if (string.IsNullOrEmpty(jwtToken))
return new CustomUserClaims();
var handler = new JwtSecurityTokenHandler();
var token = handler.ReadJwtToken(jwtToken);
var name = token.Claims.FirstOrDefault(t => t.Type == ClaimTypes.Name);
return new CustomUserClaims(name!.Value);
}
public static ClaimsPrincipal SetClaimPrincipal(CustomUserClaims claims)
{
if (claims.Name is null)
return new ClaimsPrincipal();
return new ClaimsPrincipal(new ClaimsIdentity(
new List<Claim>
{
new Claim(ClaimTypes.Name, claims.Name!),
}, "JwtAuth"));
}
}
}

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

@ -0,0 +1,18 @@
{
"ConnectionStrings": {
"DefaultConnection": "Server=peacecloud.synology.me; Port=23306; Database=BLAZORTEST; Uid=pds; Pwd=Pds92070983!@"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Jwt": {
"Key": "he0ErCNloe4J7Id0Ry2SEDg09lKkZkfsRiGsdXvgEg",
"Issuer": "https://localhost:7180",
"Audience": "https://localhost:7180",
"ExpireDurationHour": 2.0
}
}

@ -0,0 +1,40 @@
.fluent-slider-w300px {
max-width: 300px;
margin-top: 10px;
}
.fluent-border-s1 {
border-style: solid;
border-color: var(--accent-fill-rest);
border-width: 1px;
}
.fluent-border-s1-r10 {
border-radius: 10px;
border-style: solid;
border-color: var(--accent-fill-rest);
border-width: 1px;
}
.fluent-border-s1-r5 {
border-radius: 5px;
border-style: solid;
border-color: var(--accent-fill-rest);
border-width: 1px;
}
.fluent-border-d1 {
border-style: dashed;
border-color: var(--accent-fill-rest);
border-width: 1px;
}
.fluent-fixed-w100 {
max-width: 100px;
min-width: 100px;
}
.fluent-calendar {
overflow-y: initial;
overflow-x: initial;
}

@ -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