first commit -push

This commit is contained in:
dungtt 2025-10-15 15:15:53 +07:00
parent 674ae395be
commit a9577c5756
885 changed files with 74595 additions and 0 deletions

19
.env Normal file
View File

@ -0,0 +1,19 @@
DOCKER_HUB=robotics.doc/robotnet
TAG=0.12.0
HOST_IP=172.20.235.172
CERT_PASSWORD=RobotNet@2024
SQL_IP=172.20.235.170
SQL_PASSWORD=robotics@2022
SQL_IDENTITY_DB=IdentityDb
SQL_MAP_MANAGER_DB=MapDb
SQL_ROBOT_MANAGER_DB=RobotDb
SQL_SCRIPT_MANAGER_DB=ScriptDb
MINIO_IP=172.20.235.170
MINIO_ROOT_USER=minio
MINIO_ROOT_PASSWORD=robotics
IDENTITY_SERVER_PORT=8061
MAP_MANAGER_PORT=8177
ROBOT_MANAGER_PORT=8179
MQTT_PORT=1883
SCRIPT_MANAGER_PORT=8102
WEB_APP_PORT=8035

409
.gitignore vendored Normal file
View File

@ -0,0 +1,409 @@
# ---> VisualStudio
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
*/wwwroot/lib
certificate/*.pfx
certificate/*.crt
certificate/*.key
certificate/*.pem
certificate/*.srl
certificate/*.csr
.scripts/

View File

View File

@ -0,0 +1,48 @@
var builder = DistributedApplication.CreateBuilder(args);
//var cache = builder.AddRedis("cache");
var identity = builder.AddProject<Projects.RobotNet_IdentityServer>("identity-server")
.WithExternalHttpEndpoints();
//.WithReference(cache)
//.WaitFor(cache);
var mapManager = builder.AddProject<Projects.RobotNet_MapManager>("map-manager")
.WithExternalHttpEndpoints()
//.WithReference(cache)
//.WaitFor(cache)
.WithReference(identity)
.WaitFor(identity);
var robotManager = builder.AddProject<Projects.RobotNet_RobotManager>("robot-manager")
.WithExternalHttpEndpoints()
//.WithReference(cache)
//.WaitFor(cache)
.WithReference(identity)
.WaitFor(identity)
.WithReference(mapManager)
.WaitFor(mapManager);
var scriptManager = builder.AddProject<Projects.RobotNet_ScriptManager>("script-manager")
.WithExternalHttpEndpoints()
//.WithReference(cache)
//.WaitFor(cache)
.WithReference(identity)
.WaitFor(identity)
.WithReference(robotManager)
.WaitFor(robotManager);
builder.AddProject<Projects.RobotNet_WebApp>("robotnet-webapp")
.WithExternalHttpEndpoints()
//.WithReference(cache)
//.WaitFor(cache)
.WithReference(identity)
.WaitFor(identity)
.WithReference(mapManager)
.WaitFor(mapManager)
.WithReference(robotManager)
.WaitFor(robotManager)
.WithReference(scriptManager)
.WaitFor(scriptManager);
builder.Build().Run();

View File

@ -0,0 +1,31 @@
{
"profiles": {
"https": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21061",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22197"
},
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:17070;http://localhost:15043",
"remoteDebugEnabled": false,
"authenticationMode": "None"
},
"http": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19047",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20292"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:15043"
}
},
"$schema": "https://json.schemastore.org/launchsettings.json"
}

View File

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="9.0.0" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
<UserSecretsId>ee4f8e12-ccfe-4b55-94bb-c86fe3a6b387</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.4.1" />
<PackageReference Include="Aspire.Hosting.Redis" Version="9.4.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RobotNet.IdentityServer\RobotNet.IdentityServer.csproj" />
<ProjectReference Include="..\RobotNet.MapManager\RobotNet.MapManager.csproj" />
<ProjectReference Include="..\RobotNet.RobotManager\RobotNet.RobotManager.csproj" />
<ProjectReference Include="..\RobotNet.ScriptManager\RobotNet.ScriptManager.csproj" />
<ProjectReference Include="..\RobotNet.WebApp\RobotNet.WebApp.csproj" />
</ItemGroup>
</Project>

View File

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

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
}
}

View File

@ -0,0 +1,36 @@
using System.Net.Http.Json;
namespace RobotNet.Clients;
public static class HttpClientExtensions
{
public static async Task<TValue?> PostFromJsonAsync<TValue>(this HttpClient client, string requestUri, object value)
{
var response = await client.PostAsJsonAsync(requestUri, value);
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadFromJsonAsync<TValue>();
}
return default;
}
public static async Task<TValue?> PutFromJsonAsync<TValue>(this HttpClient client, string requestUri, object value)
{
var response = await client.PutAsJsonAsync(requestUri, value);
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadFromJsonAsync<TValue>();
}
return default;
}
public static async Task<TValue?> PatchFromJsonAsync<TValue>(this HttpClient client, string requestUri, object value)
{
var response = await client.PatchAsJsonAsync(requestUri, value);
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadFromJsonAsync<TValue>();
}
return default;
}
}

View File

@ -0,0 +1,60 @@
using Microsoft.AspNetCore.SignalR.Client;
namespace RobotNet.Clients;
public abstract class HubClient
{
public event Action<HubConnectionState>? ConnectionStateChanged;
public bool IsConnected => Connection.State == HubConnectionState.Connected;
protected HubConnection Connection { get; }
protected HubClient(Uri url, Func<Task<string?>> accessTokenProvider)
{
Connection = new HubConnectionBuilder()
.WithUrl(url, options =>
{
options.AccessTokenProvider = accessTokenProvider;
})
.WithAutomaticReconnect(new HubClientRepeatRetryPolicy(TimeSpan.FromSeconds(3)))
.Build();
Connection.Closed += Connection_Closed;
Connection.Reconnected += Connection_Reconnected;
}
private Task Connection_Closed(Exception? arg)
{
ConnectionStateChanged?.Invoke(Connection.State);
return Task.CompletedTask;
}
private Task Connection_Reconnected(string? arg)
{
ConnectionStateChanged?.Invoke(Connection.State);
return Task.CompletedTask;
}
public virtual async Task StartAsync()
{
if (Connection.State == HubConnectionState.Disconnected)
{
await Connection.StartAsync();
ConnectionStateChanged?.Invoke(Connection.State);
}
}
public virtual async Task StopAsync()
{
if (Connection.State != HubConnectionState.Disconnected)
{
await Connection.StopAsync();
ConnectionStateChanged?.Invoke(Connection.State);
}
}
public class HubClientRepeatRetryPolicy(TimeSpan repeatSpan) : IRetryPolicy
{
private readonly TimeSpan RepeatTimeSpan = repeatSpan;
public TimeSpan? NextRetryDelay(RetryContext retryContext) => RepeatTimeSpan;
}
}

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.8" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using RobotNet.IdentityServer.Data;
using System.Security.Claims;
namespace Microsoft.AspNetCore.Routing
{
internal static class IdentityComponentsEndpointRouteBuilderExtensions
{
// These endpoints are required by the Identity Razor components defined in the /Components/Account/Pages directory of this project.
public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEndpointRouteBuilder endpoints)
{
ArgumentNullException.ThrowIfNull(endpoints);
var accountGroup = endpoints.MapGroup("/Account");
accountGroup.MapPost("/Logout", async (
ClaimsPrincipal user,
SignInManager<ApplicationUser> signInManager,
[FromForm] string returnUrl) =>
{
await signInManager.SignOutAsync();
return TypedResults.LocalRedirect($"~/{returnUrl}");
});
return accountGroup;
}
}
}

View File

@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using RobotNet.IdentityServer.Data;
namespace RobotNet.IdentityServer.Components.Account
{
// Remove the "else if (EmailSender is IdentityNoOpEmailSender)" block from RegisterConfirmation.razor after updating with a real implementation.
internal sealed class IdentityNoOpEmailSender : IEmailSender<ApplicationUser>
{
private readonly IEmailSender emailSender = new NoOpEmailSender();
public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink) =>
emailSender.SendEmailAsync(email, "Confirm your email", $"Please confirm your account by <a href='{confirmationLink}'>clicking here</a>.");
public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) =>
emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password by <a href='{resetLink}'>clicking here</a>.");
public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) =>
emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password using the following code: {resetCode}");
}
}

View File

@ -0,0 +1,59 @@
using Microsoft.AspNetCore.Components;
using System.Diagnostics.CodeAnalysis;
namespace RobotNet.IdentityServer.Components.Account
{
internal sealed class IdentityRedirectManager(NavigationManager navigationManager)
{
public const string StatusCookieName = "Identity.StatusMessage";
private static readonly CookieBuilder StatusCookieBuilder = new()
{
SameSite = SameSiteMode.Strict,
HttpOnly = true,
IsEssential = true,
MaxAge = TimeSpan.FromSeconds(5),
};
[DoesNotReturn]
public void RedirectTo(string? uri)
{
uri ??= "";
// Prevent open redirects.
if (!Uri.IsWellFormedUriString(uri, UriKind.Relative))
{
uri = navigationManager.ToBaseRelativePath(uri);
}
// During static rendering, NavigateTo throws a NavigationException which is handled by the framework as a redirect.
// So as long as this is called from a statically rendered Identity component, the InvalidOperationException is never thrown.
navigationManager.NavigateTo(uri);
throw new InvalidOperationException($"{nameof(IdentityRedirectManager)} can only be used during static rendering.");
}
[DoesNotReturn]
public void RedirectTo(string uri, Dictionary<string, object?> queryParameters)
{
var uriWithoutQuery = navigationManager.ToAbsoluteUri(uri).GetLeftPart(UriPartial.Path);
var newUri = navigationManager.GetUriWithQueryParameters(uriWithoutQuery, queryParameters);
RedirectTo(newUri);
}
[DoesNotReturn]
public void RedirectToWithStatus(string uri, string message, HttpContext context)
{
context.Response.Cookies.Append(StatusCookieName, message, StatusCookieBuilder.Build(context));
RedirectTo(uri);
}
private string CurrentPath => navigationManager.ToAbsoluteUri(navigationManager.Uri).GetLeftPart(UriPartial.Path);
[DoesNotReturn]
public void RedirectToCurrentPage() => RedirectTo(CurrentPath);
[DoesNotReturn]
public void RedirectToCurrentPageWithStatus(string message, HttpContext context)
=> RedirectToWithStatus(CurrentPath, message, context);
}
}

View File

@ -0,0 +1,48 @@
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using RobotNet.IdentityServer.Data;
using System.Security.Claims;
namespace RobotNet.IdentityServer.Components.Account
{
// This is a server-side AuthenticationStateProvider that revalidates the security stamp for the connected user
// every 30 minutes an interactive circuit is connected.
internal sealed class IdentityRevalidatingAuthenticationStateProvider(
ILoggerFactory loggerFactory,
IServiceScopeFactory scopeFactory,
IOptions<IdentityOptions> options)
: RevalidatingServerAuthenticationStateProvider(loggerFactory)
{
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
protected override async Task<bool> ValidateAuthenticationStateAsync(
AuthenticationState authenticationState, CancellationToken cancellationToken)
{
// Get the user manager from a new scope to ensure it fetches fresh data
await using var scope = scopeFactory.CreateAsyncScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
return await ValidateSecurityStampAsync(userManager, authenticationState.User);
}
private async Task<bool> ValidateSecurityStampAsync(UserManager<ApplicationUser> userManager, ClaimsPrincipal principal)
{
var user = await userManager.GetUserAsync(principal);
if (user is null)
{
return false;
}
else if (!userManager.SupportsUserSecurityStamp)
{
return true;
}
else
{
var principalStamp = principal.FindFirstValue(options.Value.ClaimsIdentity.SecurityStampClaimType);
var userStamp = await userManager.GetSecurityStampAsync(user);
return principalStamp == userStamp;
}
}
}
}

View File

@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Identity;
using RobotNet.IdentityServer.Data;
namespace RobotNet.IdentityServer.Components.Account
{
internal sealed class IdentityUserAccessor(UserManager<ApplicationUser> userManager, IdentityRedirectManager redirectManager)
{
public async Task<ApplicationUser> GetRequiredUserAsync(HttpContext context)
{
var user = await userManager.GetUserAsync(context.User);
if (user is null)
{
redirectManager.RedirectToWithStatus("Account/InvalidUser", $"Error: Unable to load user with ID '{userManager.GetUserId(context.User)}'.", context);
}
return user;
}
}
}

View File

@ -0,0 +1,35 @@
@page "/Account/Login/Access"
@using Microsoft.Extensions.Primitives
@using Microsoft.AspNetCore.Antiforgery;
@attribute [RequireAntiforgeryToken]
<div class="w-100 h-100 d-flex flex-column justify-content-center align-items-center">
<div class="jumbotron">
<h1>Authorization</h1>
<p class="lead text-left">Do you want to grant <strong>@ApplicationName</strong> access to your data? (scopes requested: @Scope)</p>
<form action="api/Authorization/connect/authorize" method="post" >
<AntiforgeryToken />
@foreach (var parameter in HttpContext.Request.HasFormContentType ? (IEnumerable<KeyValuePair<string, StringValues>>)HttpContext.Request.Form : HttpContext.Request.Query)
{
<input type="hidden" name="@parameter.Key" value="@parameter.Value" />
}
<input class="btn btn-lg btn-success" name="submit.Accept" type="submit" value="Yes" />
<input class="btn btn-lg btn-danger" name="submit.Deny" type="submit" value="No" />
</form>
</div>
</div>
@code {
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromQuery(Name = "request_app")]
private string ApplicationName { get; set; } = "";
[SupplyParameterFromQuery(Name = "request_scope")]
private string Scope { get; set; } = "";
}

View File

@ -0,0 +1,355 @@
@rendermode InteractiveServer
@using Microsoft.AspNetCore.Identity
@using RobotNet.IdentityServer.Data
@using MudBlazor
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components
@using Microsoft.EntityFrameworkCore
@using System.Threading
@using RobotNet.IdentityServer.Services
@using System.Text.RegularExpressions
@using System.ComponentModel.DataAnnotations
@inherits LayoutComponentBase
@inject RobotNet.IdentityServer.Services.UserImageService UserImageService
@inject RobotNet.IdentityServer.Services.UserInfoService UserInfoService
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject UserManager<ApplicationUser> UserManager
@inject RoleManager<ApplicationRole> RoleManager
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject NavigationManager NavigationManager
<MudDialogProvider />
<MudSnackbarProvider />
<div class="d-flex justify-content-center align-items-center" style="height: 90vh; overflow-y: auto;">
<MudContainer MaxWidth="MaxWidth.Medium" Class="py-8">
@if (userInfo != null)
{
<MudCard Elevation="3" Class="rounded-lg">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h5" Class="mb-0">Thông tin cá nhân</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">Quản lý thông tin hồ sơ của bạn</MudText>
</CardHeaderContent>
<CardHeaderActions>
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Label="true">@string.Join(", ", userRoles)</MudChip>
</CardHeaderActions>
</MudCardHeader>
<MudCardContent>
<MudGrid>
<MudItem xs="12" md="4" Class="d-flex flex-column align-items-center">
<div class="position-relative d-flex justify-content-center my-3">
<MudImage Class="rounded-circle"
Style="width:150px; height:150px; object-fit:cover"
Src="@avatarUrl" />
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Color="Color.Default"
Size="Size.Small"
OnClick="ChangeAvatar"
Style="position:absolute; bottom:0; right:calc(50% - 60px); background-color:white" />
</div>
<MudText Typo="Typo.h6" Class="mt-3 text-center">@userInfo.FullName</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="text-center">
ID: @(userInfo.Id.Length > 10 ? userInfo.Id.Substring(0, 10) + "..." : userInfo.Id)
</MudText>
</MudItem>
<MudItem xs="12" md="8">
<MudPaper Elevation="0" Class="pa-4">
<MudForm @ref="form" Model="userInfo">
<MudTextField Label="Tên người dùng"
@bind-Value="userInfo.UserName"
Variant="Variant.Outlined"
Disabled="true"
HelperText="Tên người dùng không thể thay đổi"
Class="mb-3"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Person"
FullWidth="true" />
<MudTextField Label="Họ và tên"
@bind-Value="userInfo.FullName"
Variant="Variant.Outlined"
Required="true"
RequiredError="Họ và tên là bắt buộc"
@onfocus="EnableButtons"
Class="mb-3"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Badge"
FullWidth="true" />
<MudTextField Label="Email"
@bind-Value="userInfo.Email"
Variant="Variant.Outlined"
Required="true"
RequiredError="Email là bắt buộc"
@onfocus="EnableButtons"
Class="mb-3"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Email"
FullWidth="true" />
<MudTextField Label="Số điện thoại"
@bind-Value="userInfo.PhoneNumber"
Variant="Variant.Outlined"
@onfocus="EnableButtons"
Class="mb-3"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Phone"
FullWidth="true" />
</MudForm>
@if (!isButtonDisabled)
{
<MudPaper Class="d-flex gap-3 justify-end py-2 px-0" Elevation="0">
<MudButton Variant="Variant.Filled"
StartIcon="@Icons.Material.Filled.Cancel"
Color="Color.Error"
OnClick="ResetFields"
Size="Size.Medium">
Hủy
</MudButton>
<MudButton Variant="Variant.Filled"
StartIcon="@Icons.Material.Filled.Save"
Color="Color.Primary"
OnClick="SaveUserInfo"
Size="Size.Medium">
Lưu thay đổi
</MudButton>
</MudPaper>
}
</MudPaper>
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
}
else
{
<MudCard Elevation="3" Class="rounded-lg pa-8">
<MudCardContent Class="d-flex flex-column align-items-center justify-center">
<MudIcon Icon="@Icons.Material.Filled.ErrorOutline" Color="Color.Error" Size="Size.Large" Class="mb-4" />
<MudText Typo="Typo.h5" Class="mb-2">Vui lòng đăng nhập</MudText>
<MudText Typo="Typo.body1" Class="text-center mb-4">
Bạn cần đăng nhập để xem và chỉnh sửa thông tin cá nhân.
</MudText>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(() => NavigationManager.NavigateTo("/Account/Login"))">
Đăng nhập ngay
</MudButton>
</MudCardContent>
</MudCard>
}
</MudContainer>
</div>
<MudDialog @bind-Visible="ChangeAvatarVisible">
<DialogContent>
<div class="d-flex flex-column align-items-center text-center px-2">
<h5 class="mb-2">Thay đổi ảnh hồ sơ</h5>
<MudText Typo="Typo.caption" Class="mb-3">
Ảnh hồ sơ giúp người khác nhận ra bạn và xác nhận rằng bạn đã đăng nhập.
</MudText>
<div class="rounded-circle overflow-hidden mb-3"
style="width: 130px; height: 130px; border: 2px solid #ccc;">
<MudImage Src="@avatarPreview"
Alt="avatar preview"
Style="width: 100%; height: 100%; object-fit: cover;" />
</div>
<InputFile OnChange="HandleSelected" accept="image/*">
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Edit" Color="Color.Primary">
Thay đổi
</MudButton>
</InputFile>
</div>
</DialogContent>
<DialogActions>
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="ConfirmChangeAvatar">
Xác nhận
</MudButton>
<MudButton Variant="Variant.Text" OnClick="@(() => ChangeAvatarVisible = false)">
Hủy
</MudButton>
</DialogActions>
</MudDialog>
@code {
MudForm? form;
private string? avatarPreview;
private string? avatarUrl;
private IBrowserFile? selectedFile;
private bool ChangeAvatarVisible = false;
private bool isButtonDisabled = true;
private string originalFullName = "";
private string originalEmail = "";
private string originalPhoneNumber = "";
private string originalUserName = "";
private ApplicationUser? userInfo;
private List<string> userRoles = new List<string>();
private void EnableButtons()
{
isButtonDisabled = false;
}
private void ChangeAvatar()
{
ChangeAvatarVisible = true;
}
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
var authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authenticationState.User;
if (user?.Identity?.IsAuthenticated == true)
{
userInfo = await UserManager.GetUserAsync(user);
if (userInfo != null)
{
userRoles = (await UserManager.GetRolesAsync(userInfo)).ToList();
originalUserName = userInfo.UserName?? string.Empty;
originalFullName = userInfo.FullName?? string.Empty;
originalEmail = userInfo.Email?? string.Empty;
originalPhoneNumber = userInfo.PhoneNumber ?? string.Empty;
if (userInfo.AvatarImage != null)
{
avatarUrl = $"data:{userInfo.AvatarContentType};base64,{Convert.ToBase64String(userInfo.AvatarImage)}";
avatarPreview = avatarUrl;
}
else
{
avatarUrl = "/uploads/avatars/anh.jpg";
avatarPreview = avatarUrl;
}
}
}
else
{
Snackbar.Add("Vui lòng đăng nhập để tiếp tục", Severity.Error);
}
}
private async Task HandleSelected(InputFileChangeEventArgs e)
{
selectedFile = e.File;
const long maxSize = 5 * 1024 * 1024;
if (selectedFile.Size > maxSize)
{
Snackbar.Add("⚠️ Ảnh bạn chọn vượt quá 5MB. Vui lòng chọn ảnh nhỏ hơn.", Severity.Warning);
avatarPreview = avatarUrl;
selectedFile = null;
return;
}
try
{
(byte[] buffer, string contentType) = await UserImageService.ResizeAndConvertAsync(selectedFile.OpenReadStream());
avatarPreview = $"data:{contentType};base64,{Convert.ToBase64String(buffer)}";
if (userInfo != null)
{
userInfo.AvatarImage = buffer;
userInfo.AvatarContentType = selectedFile.ContentType;
}
}
catch (Exception ex)
{
Snackbar.Add($"❌ Lỗi khi đọc ảnh: {ex.Message}", Severity.Error);
avatarPreview = avatarUrl;
}
}
private async Task ConfirmChangeAvatar()
{
if (userInfo != null && userInfo.AvatarImage != null)
{
var result = await UserManager.UpdateAsync(userInfo);
if (result.Succeeded)
{
avatarUrl = avatarPreview;
ChangeAvatarVisible = false;
await Task.Delay(200);
await UserInfoService.NotifyUserInfoChanged();
StateHasChanged();
NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true);
Snackbar.Add("Cập nhật ảnh đại diện thành công!", Severity.Success);
}
else
{
Snackbar.Add("Lỗi khi cập nhật avatar.", Severity.Error);
}
}
}
private void ResetFields()
{
if (userInfo == null) return;
userInfo.FullName = originalFullName;
userInfo.Email = originalEmail;
userInfo.PhoneNumber = originalPhoneNumber;
userInfo.UserName = originalUserName;
isButtonDisabled = true;
}
private async Task SaveUserInfo()
{
if (userInfo != null)
{
try
{
var result = await UserManager.UpdateAsync(userInfo);
if (result.Succeeded)
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var userInfo = await UserManager.GetUserAsync(authState.User);
if(userInfo != null)
{
originalFullName = userInfo.FullName ?? string.Empty;
originalEmail = userInfo.Email ?? string.Empty;
originalPhoneNumber = userInfo.PhoneNumber ?? string.Empty;
originalUserName = userInfo.UserName ?? string.Empty;
}
isButtonDisabled = true;
await Task.Delay(200);
await UserInfoService.NotifyUserInfoChanged();
StateHasChanged();
Snackbar.Add("Thông tin đã được cập nhật!", Severity.Success);
}
else
{
Snackbar.Add("Lỗi khi cập nhật thông tin.", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"Error while saving user information: {ex.Message}", Severity.Error);
}
}
}
}

View File

@ -0,0 +1,7 @@
.mdi {
display: inline-flex;
justify-content: center;
align-items: center;
background-size: cover;
margin-top:7px;
}

View File

@ -0,0 +1,118 @@
@page "/Account/Login"
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Identity
@using RobotNet.IdentityServer.Data
@inject SignInManager<ApplicationUser> SignInManager
@inject ILogger<Login> Logger
@inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager
<PageTitle>Log in</PageTitle>
<div class="w-100 h-100 d-flex flex-column justify-content-center align-items-center">
<h1>Log in</h1>
@if (!string.IsNullOrEmpty(errorMessage))
{
var statusMessageClass = errorMessage.StartsWith("Error") ? "danger" : "success";
<div class="alert alert-@statusMessageClass" role="alert">
@errorMessage
</div>
}
<EditForm Model="Input" method="post" OnValidSubmit="LoginUser" FormName="login" style="width: 300px;">
<DataAnnotationsValidator />
<hr />
<ValidationSummary class="text-danger" role="alert" />
<div class="form-floating mb-3">
<InputText @bind-Value="Input.Username" class="form-control" autocomplete="username" aria-required="true" />
<label for="username" class="form-label">Username</label>
<ValidationMessage For="() => Input.Username" class="text-danger" />
</div>
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" />
<label for="password" class="form-label">Password</label>
<ValidationMessage For="() => Input.Password" class="text-danger" />
</div>
<div class="checkbox mb-3">
<label class="form-label">
<InputCheckbox @bind-Value="Input.RememberMe" class="darker-border-checkbox form-check-input" />
Remember me
</label>
</div>
<div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Log in</button>
</div>
</EditForm>
</div>
@code {
private string? errorMessage;
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
protected override async Task OnInitializedAsync()
{
if (HttpMethods.IsGet(HttpContext.Request.Method))
{
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
}
errorMessage = HttpContext.Request.Cookies[IdentityRedirectManager.StatusCookieName];
if (errorMessage is not null)
{
HttpContext.Response.Cookies.Delete(IdentityRedirectManager.StatusCookieName);
}
}
public async Task LoginUser()
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await SignInManager.PasswordSignInAsync(Input.Username, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
Logger.LogInformation("User logged in.");
RedirectManager.RedirectTo(ReturnUrl);
}
else if (result.RequiresTwoFactor)
{
RedirectManager.RedirectTo(
"Account/LoginWith2fa",
new() { ["returnUrl"] = ReturnUrl, ["rememberMe"] = Input.RememberMe });
}
else if (result.IsLockedOut)
{
Logger.LogWarning("User account locked out.");
RedirectManager.RedirectTo("Account/Lockout");
}
else
{
errorMessage = "Error: Invalid login attempt.";
}
}
private sealed class InputModel
{
[Required]
public string Username { get; set; } = "";
[Required]
[DataType(DataType.Password)]
public string Password { get; set; } = "";
[Display(Name = "Remember me?")]
public bool RememberMe { get; set; }
}
}

View File

@ -0,0 +1,37 @@
@page "/Account/Logout/Confirm"
@using Microsoft.EntityFrameworkCore.Metadata.Internal
@using Microsoft.Extensions.Primitives
@using Microsoft.AspNetCore.Antiforgery;
@attribute [RequireAntiforgeryToken]
@inject NavigationManager Navigation
<div class="w-100 h-100 d-flex flex-column justify-content-center align-items-center">
<div class="jumbotron">
<h1>Log out</h1>
<p class="lead text-left">Are you sure you want to sign out?</p>
<form action="api/Authorization/connect/logout" method="post">
<AntiforgeryToken />
@foreach (var parameter in HttpContext.Request.HasFormContentType ? (IEnumerable<KeyValuePair<string, StringValues>>)HttpContext.Request.Form : HttpContext.Request.Query)
{
<input type="hidden" name="@parameter.Key" value="@parameter.Value" />
}
<input class="btn btn-lg btn-success" name="submit.Confirm" type="submit" value="Yes" />
</form>
</div>
</div>
@code {
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
private Task OnSubmitLogout()
{
Navigation.NavigateTo("/Account/Login", forceLoad: true);
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,982 @@
@rendermode InteractiveServer
@attribute [Authorize]
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Authorization
@using MudBlazor
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.Components
@using OpenIddict.Abstractions
@using RobotNet.IdentityServer.Data
@using Microsoft.EntityFrameworkCore
@using System.Threading
@using static OpenIddict.Abstractions.OpenIddictConstants
@inherits LayoutComponentBase
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject NavigationManager NavigationManager
@inject IOpenIddictApplicationManager ApplicationManager
@inject IOpenIddictScopeManager ScopeManager
<MudDialogProvider />
<MudSnackbarProvider />
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Style="padding: 1rem;">
<div class="app-header">
<MudStack Row Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<div>
<MudText Typo="Typo.h4" Style="font-weight: 700; margin-bottom: 8px;">
OpenIddict Manager
</MudText>
<MudText Typo="Typo.body1" Style="opacity: 0.9;">
Quản lý ứng dụng OAuth2 & OpenID Connect một cách dễ dàng
</MudText>
</div>
<MudChip T="string" Icon="@Icons.Material.Filled.Security" Color="Color.Surface" Size="Size.Large" Style="color:white">
@filteredApplications.Count Apps
</MudChip>
</MudStack>
</div>
<MudPaper Class="glass-card pa-4">
<div style="padding: 1.5rem; border-bottom: 1px solid #e2e8f0; background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);">
<div style="display: flex; justify-content: space-between; align-items: center;">
<MudText Typo="Typo.h6" Style="margin: 0; color: #334155; font-weight: 600;">
Danh sách Application
</MudText>
<MudButton Variant="Variant.Filled"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@(() => OpenApplicationDialog())"
Class="add-scope-btn"
Style="text-transform: none; font-weight: 500;">
Thêm Application
</MudButton>
</div>
</div>
<MudTextField @bind-Value="applicationSearchTerm"
Label="Tìm kiếm application..."
Placeholder="Nhập Client ID, Display Name, Type hoặc ID để tìm kiếm"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search"
Clearable="true"
Immediate="true"
DebounceInterval="300"
Style="margin-top: 0.5rem;" />
<MudTable Items="FilteredApplications"
Hover="true"
Dense="true"
FixedHeader="true"
Loading="@loadingApplications"
Class="compact-table"
Virtualize="true">
<HeaderContent>
<MudTh Style="width: 120px;">Client</MudTh>
<MudTh Style="width: 100px;">Type</MudTh>
<MudTh Style="width: 150px;">Display Name</MudTh>
<MudTh Style="width: 80px;">Secret</MudTh>
<MudTh Style="width: 120px;">Endpoints</MudTh>
<MudTh Style="width: 140px;">Actions</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<MudStack Spacing="1">
<MudText Typo="Typo.body1" Style="font-weight: 600;">@context.ClientId</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">@context.Id[..8]...</MudText>
</MudStack>
</MudTd>
<MudTd>
<MudChip T="string" Size="Size.Small"
Color="@(context.ClientType == ClientTypes.Confidential ? Color.Primary : Color.Secondary)"
Variant="Variant.Filled">
@(context.ClientType == ClientTypes.Confidential ? "Confidential" : "Public")
</MudChip>
</MudTd>
<MudTd>
<MudText Typo="Typo.body2">@context.DisplayName</MudText>
</MudTd>
<MudTd>
<div class="status-badge">
@if (!string.IsNullOrEmpty(context.ClientSecret))
{
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" />
<span style="color: var(--mud-palette-success);">Yes</span>
}
else
{
<MudIcon Icon="@Icons.Material.Filled.Cancel" Color="Color.Error" Size="Size.Small" />
<span style="color: var(--mud-palette-error);">No</span>
}
</div>
</MudTd>
<MudTd>
@if (context.RedirectUris.Any())
{
<MudTooltip Text="@string.Join(", ", context.RedirectUris)">
<MudChip T="string" Size="Size.Small" Color="Color.Info" Variant="Variant.Text">
@context.RedirectUris.Count URIs
</MudChip>
</MudTooltip>
}
else
{
<MudText Typo="Typo.caption" Color="Color.Secondary">No URIs</MudText>
}
</MudTd>
<MudTd>
<div class="action-buttons">
<MudIconButton Icon="@Icons.Material.Filled.Visibility"
Size="Size.Small"
Color="Color.Info"
OnClick="@(() => ViewApplicationDetails(context))"
aria-label="Chi tiết" />
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Size="Size.Small"
Color="Color.Warning"
OnClick="@(() => EditApplication(context))"
aria-label="Sửa" />
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Size="Size.Small"
Color="Color.Error"
OnClick="@(() => DeleteApplication(context.ClientId))"
aria-label="Xóa" />
</div>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager PageSizeOptions="new int[] { 5, 10, 25, 50, 100, int.MaxValue }" />
</PagerContent>
</MudTable>
</MudPaper>
</MudContainer>
<MudDialog @bind-Visible="ShowApplicationDialog"
Options="@(new DialogOptions { MaxWidth = MaxWidth.Large, FullWidth = true, CloseOnEscapeKey = true })">
<TitleContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@(editingApplication != null ? Icons.Material.Filled.Edit : Icons.Material.Filled.Add)" Class="mr-2" />
@(editingApplication != null ? "Chỉnh sửa Application" : "Tạo Application Mới")
</MudText>
</TitleContent>
<DialogContent>
<MudContainer Style="max-height: 70vh; overflow-y: auto;">
<MudGrid Spacing="3">
<MudItem xs="12">
<MudText Typo="Typo.h6" Color="Color.Primary" Class="mb-3">
<MudIcon Icon="@Icons.Material.Filled.Info" Class="mr-2" />
Thông tin cơ bản
</MudText>
</MudItem>
<MudItem xs="12" md="6">
<MudTextField @bind-Value="applicationForm.ClientId"
Label="Client ID"
Required="true"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Key" />
</MudItem>
<MudItem xs="12" md="6">
<MudTextField @bind-Value="applicationForm.DisplayName"
Label="Display Name"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Label" />
</MudItem>
<MudItem xs="12" md="6">
<MudSelect Value="applicationForm.ClientType"
Label="Client Type"
Variant="Variant.Outlined"
ValueChanged="@((string value) => OnClientTypeChanged(value))"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Category">
<MudSelectItem Value="@ClientTypes.Public"> Public</MudSelectItem>
<MudSelectItem Value="@ClientTypes.Confidential"> Confidential</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" md="6">
<MudSelect @bind-Value="applicationForm.ConsentType"
Label="Consent Type"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.VerifiedUser">
<MudSelectItem Value="@ConsentTypes.Explicit"> Explicit</MudSelectItem>
<MudSelectItem Value="@ConsentTypes.Implicit"> Implicit</MudSelectItem>
</MudSelect>
</MudItem>
@if (applicationForm.ClientType == ClientTypes.Confidential)
{
<MudItem xs="12">
<MudTextField @bind-Value="applicationForm.ClientSecret"
Label="Client Secret"
InputType="InputType.Password"
Variant="Variant.Outlined"
Placeholder="@(editingApplication != null ? "Để trống nếu không muốn thay đổi" : "Nhập Client Secret")"
HelperText="@(editingApplication != null ? "Chỉ nhập nếu muốn thay đổi" : "Mật khẩu bí mật cho ứng dụng")"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Password" />
</MudItem>
}
<MudItem xs="12">
<MudText Typo="Typo.h6" Color="Color.Primary" Class="mb-3">
<MudIcon Icon="@Icons.Material.Filled.Link" Class="mr-2" />
Cấu hình Endpoints
</MudText>
</MudItem>
<MudItem xs="12" md="6">
<div class="uri-input-section">
<MudText Typo="Typo.subtitle2" Class="mb-2">Redirect URIs</MudText>
<MudTextField @bind-Value="redirectUriInput"
Label="Redirect URI"
Placeholder="https://app.com/callback"
Variant="Variant.Outlined"
Adornment="Adornment.End"
AdornmentIcon="@Icons.Material.Filled.Add"
OnAdornmentClick="AddRedirectUri"
@onkeypress="@(async (KeyboardEventArgs e) => { if (e.Key == "Enter") { AddRedirectUri(); } })" />
<MudStack Row Wrap="Wrap.Wrap" Class="mt-2">
@foreach (var uri in applicationForm.RedirectUris)
{
<MudChip T="string" Text="@uri" OnClose="@(() => RemoveRedirectUri(uri))" Color="Color.Primary" Size="Size.Small" />
}
</MudStack>
</div>
</MudItem>
<MudItem xs="12" md="6">
<div class="uri-input-section">
<MudText Typo="Typo.subtitle2" Class="mb-2">Post Logout URIs</MudText>
<MudTextField @bind-Value="postLogoutUriInput"
Label="Post Logout URI"
Placeholder="https://app.com/logout"
Variant="Variant.Outlined"
Adornment="Adornment.End"
AdornmentIcon="@Icons.Material.Filled.Add"
OnAdornmentClick="AddPostLogoutUri"
@onkeypress="@(async (KeyboardEventArgs e) => { if (e.Key == "Enter") { AddPostLogoutUri(); } })" />
<MudStack Row Wrap="Wrap.Wrap" Class="mt-2">
@foreach (var uri in applicationForm.PostLogoutRedirectUris)
{
<MudChip T="string" Text="@uri" OnClose="@(() => RemovePostLogoutUri(uri))" Color="Color.Secondary" Size="Size.Small" />
}
</MudStack>
</div>
</MudItem>
<MudItem xs="12">
<MudText Typo="Typo.h6" Color="Color.Primary" Class="mb-3">
<MudIcon Icon="@Icons.Material.Filled.Security" Class="mr-2" />
Permissions & Requirements
</MudText>
</MudItem>
<MudItem xs="12" md="8">
<MudSelect T="string"
Label="Permissions"
Variant="Variant.Outlined"
MultiSelection="true"
SelectAll="true"
SelectAllText="Chọn tất cả"
Dense="true"
MaxHeight="200"
SelectedValuesChanged="@OnPermissionSelectionChanged"
SelectedValues="@GetSelectedPermissions()">
@foreach (var permission in permissionChecks)
{
<MudSelectItem Value="@permission.Key">
@permission.Key.Split('.').Last()
</MudSelectItem>
}
</MudSelect>
<MudStack Row Wrap="Wrap.Wrap" Class="mt-2">
@foreach (var selectedPermission in GetSelectedPermissions())
{
<MudChip T="string" Text="@selectedPermission.Split('.').Last()"
OnClose="@(() => RemovePermission(selectedPermission))"
Color="Color.Primary"
Size="Size.Small" />
}
</MudStack>
</MudItem>
@if (applicationForm.ClientType == ClientTypes.Public)
{
<MudItem xs="12" md="4">
<MudText Typo="Typo.subtitle2" Class="mb-2">Requirements</MudText>
@foreach (var requirement in requirementChecks)
{
<MudCheckBox T="string" @bind-Checked="@requirementChecks[requirement.Key]"
Label="@requirement.Key.Split('.').Last()"
Dense="true" />
}
</MudItem>
}
</MudGrid>
</MudContainer>
</DialogContent>
<DialogActions>
<MudButton OnClick="CancelApplicationDialog"
Color="Color.Default"
StartIcon="@Icons.Material.Filled.Cancel">
Hủy
</MudButton>
<MudButton OnClick="SaveApplication"
Color="Color.Primary"
Variant="Variant.Filled"
StartIcon="@(editingApplication != null ? Icons.Material.Filled.Update : Icons.Material.Filled.Save)">
@(editingApplication != null ? "Cập nhật" : "Tạo mới")
</MudButton>
</DialogActions>
</MudDialog>
<MudDialog @bind-Visible="ShowDetailsDialog"
Options="@(new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true })">
<TitleContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.Info" Class="mr-2" />
Chi tiết Application
</MudText>
</TitleContent>
<DialogContent>
@if (selectedApplication != null)
{
<MudContainer Style="max-height: 60vh; overflow-y: auto;">
<MudGrid Spacing="2">
<MudItem xs="12">
<MudPaper Class="pa-3" Style="background: linear-gradient(45deg, #f8f9ff, #e8f2ff);">
<MudText Typo="Typo.subtitle2" Color="Color.Primary" Class="mb-2"> Thông tin cơ bản</MudText>
<MudStack Spacing="1">
<div><strong>ID:</strong> @selectedApplication.Id</div>
<div><strong>Client ID:</strong> @selectedApplication.ClientId</div>
<div><strong>Display Name:</strong> @selectedApplication.DisplayName</div>
<div><strong>Type:</strong> @selectedApplication.ClientType</div>
<div><strong>Consent:</strong> @selectedApplication.ConsentType</div>
</MudStack>
</MudPaper>
</MudItem>
<MudItem xs="12">
<MudPaper Class="pa-3" Style="background: linear-gradient(45deg, #fff8f0, #fff0e6);">
<MudText Typo="Typo.subtitle2" Color="Color.Warning" Class="mb-2">🔒 Bảo mật</MudText>
<div>
<strong>Client Secret:</strong>
@if (!string.IsNullOrEmpty(selectedApplication.ClientSecret))
{
<MudChip T="string" Color="Color.Success" Size="Size.Small">Có</MudChip>
}
else
{
<MudChip T="string" Color="Color.Error" Size="Size.Small">Không</MudChip>
}
</div>
</MudPaper>
</MudItem>
@if (selectedApplication.RedirectUris.Any())
{
<MudItem xs="12">
<MudPaper Class="pa-3" Style="background: linear-gradient(45deg, #f0fff8, #e6fff0);">
<MudText Typo="Typo.subtitle2" Color="Color.Success" Class="mb-2">🔗 Redirect URIs</MudText>
<MudStack Row Wrap="Wrap.Wrap">
@foreach (var uri in selectedApplication.RedirectUris)
{
<MudChip T="string" Text="@uri" Size="Size.Small" Color="Color.Success" />
}
</MudStack>
</MudPaper>
</MudItem>
}
@if (selectedApplication.Permissions.Any())
{
<MudItem xs="12">
<MudPaper Class="pa-3" Style="background: linear-gradient(45deg, #fff0f8, #ffe6f0);">
<MudText Typo="Typo.subtitle2" Color="Color.Secondary" Class="mb-2">🛡️ Permissions</MudText>
<MudStack Row Wrap="Wrap.Wrap">
@foreach (var permission in selectedApplication.Permissions)
{
<MudChip T="string" Text="@permission.Split('.').Last()" Size="Size.Small" Color="Color.Secondary" />
}
</MudStack>
</MudPaper>
</MudItem>
}
@if (selectedApplication.Requirements.Any())
{
<MudItem xs="12">
<MudPaper Class="pa-3" Style="background: linear-gradient(45deg, #f7f0f8, #f6fef0);">
<MudText Typo="Typo.subtitle2" Color="Color.Tertiary" Class="mb-2">⚙️ Requirements</MudText>
@if (selectedApplication.Requirements.Any())
{
<MudStack Row Wrap="Wrap.Wrap">
@foreach (var requirement in selectedApplication.Requirements)
{
<MudChip T="string" Text="@requirement" Size="Size.Small" Color="Color.Secondary" />
}
</MudStack>
}
else
{
<MudText Typo="Typo.caption" Color="Color.Secondary" Style="font-style: italic;">
Không có requirements nào được thiết lập
</MudText>
}
</MudPaper>
</MudItem>
}
</MudGrid>
</MudContainer>
}
</DialogContent>
<DialogActions>
<MudButton OnClick="CloseDetailsDialog"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Close">
Đóng
</MudButton>
</DialogActions>
</MudDialog>
@code {
private List<ApplicationInfo> filteredApplications = new();
private bool loadingApplications = false;
private bool ShowApplicationDialog = false;
private bool ShowDetailsDialog = false;
private bool showClientSecret = false;
private ApplicationInfo? editingApplication = null;
private ApplicationInfo? selectedApplication = null;
private ApplicationForm applicationForm = new();
private string redirectUriInput = string.Empty;
private string postLogoutUriInput = string.Empty;
private string customScopeInput = string.Empty;
private HashSet<string> customScopes = new();
private Dictionary<string, bool> permissionChecks = new();
private Dictionary<string, bool> requirementChecks = new();
private List<string> availableScopes = new();
public class ApplicationInfo
{
public string Id { get; set; } = string.Empty;
public string ApplicationType { get; set; } = string.Empty;
public string ClientId { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public string ClientType { get; set; } = string.Empty;
public string ConsentType { get; set; } = string.Empty;
public List<string> RedirectUris { get; set; } = new();
public List<string> PostLogoutRedirectUris { get; set; } = new();
public List<string> Permissions { get; set; } = new();
public List<string> Requirements { get; set; } = new();
public string? ClientSecret { get; set; }
}
public class ApplicationForm
{
public string ClientId { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public string ClientType { get; set; } = ClientTypes.Public;
public string ConsentType { get; set; } = ConsentTypes.Explicit;
public string? ClientSecret { get; set; }
public List<string> RedirectUris { get; set; } = new();
public List<string> PostLogoutRedirectUris { get; set; } = new();
}
private string applicationSearchTerm = "";
protected override async Task OnInitializedAsync()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
if (authState.User.Identity?.IsAuthenticated == true)
{
InitializePermissionChecks();
InitializeRequirementChecks();
await LoadApplicationsAsync();
await LoadAvailableScopesAsync();
}
}
private void InitializePermissionChecks()
{
permissionChecks.Clear();
permissionChecks.Add(Permissions.Endpoints.Authorization, false);
permissionChecks.Add(Permissions.Endpoints.EndSession, false);
permissionChecks.Add(Permissions.Endpoints.Token, false);
permissionChecks.Add(Permissions.Endpoints.Introspection, false);
permissionChecks.Add(Permissions.GrantTypes.AuthorizationCode, false);
permissionChecks.Add(Permissions.GrantTypes.RefreshToken, false);
permissionChecks.Add(Permissions.GrantTypes.ClientCredentials, false);
permissionChecks.Add(Permissions.ResponseTypes.Code, false);
permissionChecks.Add(Permissions.ResponseTypes.Token, false);
permissionChecks.Add(Permissions.Scopes.Email, false);
permissionChecks.Add(Permissions.Scopes.Profile, false);
permissionChecks.Add(Permissions.Scopes.Roles, false);
foreach (var scope in availableScopes)
{
var scopePermission = Permissions.Prefixes.Scope + scope;
if (!permissionChecks.ContainsKey(scopePermission))
{
permissionChecks.Add(scopePermission, false);
}
}
}
private void InitializeRequirementChecks()
{
requirementChecks = new() {
{ Requirements.Features.ProofKeyForCodeExchange, false }
};
}
private void OnClientTypeChanged(string newClientType)
{
applicationForm.ClientType = newClientType;
if (newClientType == ClientTypes.Public)
{
applicationForm.ClientSecret = null;
}
StateHasChanged();
}
private void OnPermissionSelectionChanged(IEnumerable<string> selectedValues)
{
foreach (var key in permissionChecks.Keys.ToList())
{
permissionChecks[key] = false;
}
foreach (var value in selectedValues)
{
if (permissionChecks.ContainsKey(value))
{
permissionChecks[value] = true;
}
}
StateHasChanged();
}
private IEnumerable<string> GetSelectedPermissions()
{
return permissionChecks.Where(x => x.Value).Select(x => x.Key);
}
private void RemovePermission(string permission)
{
if (permissionChecks.ContainsKey(permission))
{
permissionChecks[permission] = false;
StateHasChanged();
}
}
private IEnumerable<ApplicationInfo> FilteredApplications =>
string.IsNullOrWhiteSpace(applicationSearchTerm)
? filteredApplications
: filteredApplications.Where(r =>
(r.ClientId != null && r.ClientId.Contains(applicationSearchTerm, StringComparison.OrdinalIgnoreCase)) ||
(r.DisplayName != null && r.DisplayName.Contains(applicationSearchTerm, StringComparison.OrdinalIgnoreCase)) ||
(r.ClientType != null && r.ClientType.Contains(applicationSearchTerm, StringComparison.OrdinalIgnoreCase)) ||
(r.Id != null && r.Id.Contains(applicationSearchTerm, StringComparison.OrdinalIgnoreCase)));
private async Task LoadApplicationsAsync()
{
try
{
loadingApplications = true;
filteredApplications.Clear();
await foreach (var app in ApplicationManager.ListAsync())
{
string? clientSecret = null;
try
{
var properties = await ApplicationManager.GetPropertiesAsync(app);
clientSecret = properties.ContainsKey("client_secret") ? properties["client_secret"].ToString() : null;
if (string.IsNullOrEmpty(clientSecret))
{
var clientType = await ApplicationManager.GetClientTypeAsync(app);
if (clientType == ClientTypes.Confidential)
{
clientSecret = "***";
}
}
}
catch
{
var clientType = await ApplicationManager.GetClientTypeAsync(app);
if (clientType == ClientTypes.Confidential)
{
clientSecret = "***";
}
}
filteredApplications.Add(new ApplicationInfo
{
Id = await ApplicationManager.GetIdAsync(app) ?? string.Empty,
ApplicationType = await ApplicationManager.GetApplicationTypeAsync(app) ?? string.Empty,
ClientId = await ApplicationManager.GetClientIdAsync(app) ?? string.Empty,
DisplayName = await ApplicationManager.GetDisplayNameAsync(app) ?? string.Empty,
ClientType = await ApplicationManager.GetClientTypeAsync(app) ?? string.Empty,
ConsentType = await ApplicationManager.GetConsentTypeAsync(app) ?? string.Empty,
RedirectUris = (await ApplicationManager.GetRedirectUrisAsync(app)).Select(u => u.ToString()).ToList(),
PostLogoutRedirectUris = (await ApplicationManager.GetPostLogoutRedirectUrisAsync(app)).Select(u => u.ToString()).ToList(),
Permissions = (await ApplicationManager.GetPermissionsAsync(app)).ToList(),
Requirements = (await ApplicationManager.GetRequirementsAsync(app)).ToList(),
ClientSecret = clientSecret
});
}
}
catch (Exception ex)
{
Snackbar.Add($"Lỗi khi tải applications: {ex.Message}", Severity.Error);
}
finally
{
loadingApplications = false;
StateHasChanged();
}
}
private async Task LoadAvailableScopesAsync()
{
try
{
availableScopes.Clear();
await foreach (var scope in ScopeManager.ListAsync())
{
var scopeName = await ScopeManager.GetNameAsync(scope);
if (!string.IsNullOrEmpty(scopeName))
{
availableScopes.Add(scopeName);
}
}
}
catch (Exception ex)
{
Snackbar.Add($"Lỗi khi tải scopes: {ex.Message}", Severity.Error);
}
}
private void ViewApplicationDetails(ApplicationInfo application)
{
selectedApplication = application;
showClientSecret = false;
ShowDetailsDialog = true;
}
private void CloseDetailsDialog()
{
ShowDetailsDialog = false;
selectedApplication = null;
showClientSecret = false;
}
private void ToggleClientSecretVisibility()
{
showClientSecret = !showClientSecret;
}
private async void OpenApplicationDialog(ApplicationInfo? application = null)
{
await LoadAvailableScopesAsync();
InitializePermissionChecks();
ShowApplicationDialog = true;
editingApplication = application;
ResetApplicationForm();
if (application != null)
{
applicationForm = new ApplicationForm
{
ClientId = application.ClientId,
DisplayName = application.DisplayName,
ClientType = application.ClientType,
ConsentType = application.ConsentType,
RedirectUris = new(application.RedirectUris),
PostLogoutRedirectUris = new(application.PostLogoutRedirectUris),
ClientSecret = application.ClientType == ClientTypes.Confidential ? string.Empty : null
};
foreach (var permission in application.Permissions)
{
if (permissionChecks.ContainsKey(permission)) permissionChecks[permission] = true;
else if (permission.StartsWith(Permissions.Prefixes.Scope)) customScopes.Add(permission[Permissions.Prefixes.Scope.Length..]);
}
foreach (var requirement in application.Requirements)
{
if (requirementChecks.ContainsKey(requirement)) requirementChecks[requirement] = true;
}
}
StateHasChanged();
}
private void EditApplication(ApplicationInfo application) => OpenApplicationDialog(application);
private void ResetApplicationForm()
{
applicationForm = new();
redirectUriInput = string.Empty;
postLogoutUriInput = string.Empty;
customScopeInput = string.Empty;
customScopes.Clear();
foreach (var key in permissionChecks.Keys.ToList()) permissionChecks[key] = false;
foreach (var key in requirementChecks.Keys.ToList()) requirementChecks[key] = false;
}
private void AddRedirectUri()
{
if (!string.IsNullOrWhiteSpace(redirectUriInput))
{
if (Uri.TryCreate(redirectUriInput.Trim(), UriKind.Absolute, out _))
{
if (!applicationForm.RedirectUris.Contains(redirectUriInput.Trim()))
{
applicationForm.RedirectUris.Add(redirectUriInput.Trim());
redirectUriInput = string.Empty;
StateHasChanged();
}
else
{
Snackbar.Add("URI này đã tồn tại", Severity.Warning);
}
}
else
{
Snackbar.Add("URI không hợp lệ. Vui lòng nhập URI đầy đủ (ví dụ: https://example.com/login-callback)", Severity.Error);
}
}
}
private void RemoveRedirectUri(string uri) => applicationForm.RedirectUris.Remove(uri);
private void AddPostLogoutUri()
{
if (!string.IsNullOrWhiteSpace(postLogoutUriInput))
{
if (Uri.TryCreate(postLogoutUriInput.Trim(), UriKind.Absolute, out _))
{
if (!applicationForm.PostLogoutRedirectUris.Contains(postLogoutUriInput.Trim()))
{
applicationForm.PostLogoutRedirectUris.Add(postLogoutUriInput.Trim());
postLogoutUriInput = string.Empty;
StateHasChanged();
}
else
{
Snackbar.Add("URI này đã tồn tại", Severity.Warning);
}
}
else
{
Snackbar.Add("URI không hợp lệ. Vui lòng nhập URI đầy đủ (ví dụ: https://example.com/logout-callback)", Severity.Error);
}
}
}
private void RemovePostLogoutUri(string uri) => applicationForm.PostLogoutRedirectUris.Remove(uri);
private void AddCustomScope()
{
if (!string.IsNullOrWhiteSpace(customScopeInput) && !customScopes.Contains(customScopeInput))
{
customScopes.Add(customScopeInput);
customScopeInput = string.Empty;
}
}
private void RemoveCustomScope(string scope) => customScopes.Remove(scope);
private async Task SaveApplication()
{
try
{
if (string.IsNullOrWhiteSpace(applicationForm.ClientId))
{
Snackbar.Add("Client ID là bắt buộc", Severity.Error);
return;
}
if (applicationForm.ClientType == ClientTypes.Confidential)
{
if (editingApplication == null && string.IsNullOrWhiteSpace(applicationForm.ClientSecret))
{
Snackbar.Add("Client Secret là bắt buộc cho Confidential client", Severity.Error);
return;
}
}
if (editingApplication != null)
{
var existingApp = await ApplicationManager.FindByClientIdAsync(editingApplication.ClientId);
if (existingApp != null)
{
var descriptor = new OpenIddictApplicationDescriptor
{
ClientId = applicationForm.ClientId,
DisplayName = applicationForm.DisplayName,
ClientType = applicationForm.ClientType,
ConsentType = applicationForm.ConsentType
};
if (applicationForm.ClientType == ClientTypes.Confidential)
{
if (!string.IsNullOrWhiteSpace(applicationForm.ClientSecret))
{
descriptor.ClientSecret = applicationForm.ClientSecret;
}
}
else if (applicationForm.ClientType == ClientTypes.Public)
{
descriptor.ClientSecret = null;
}
var currentDescriptor = new OpenIddictApplicationDescriptor();
await ApplicationManager.PopulateAsync(currentDescriptor, existingApp);
if (applicationForm.ClientType == ClientTypes.Confidential &&
string.IsNullOrWhiteSpace(applicationForm.ClientSecret))
{
descriptor.ClientSecret = currentDescriptor.ClientSecret;
}
foreach (var uriString in applicationForm.RedirectUris)
{
if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri))
{
descriptor.RedirectUris.Add(uri);
}
else
{
Snackbar.Add($"Redirect URI không hợp lệ: {uriString}", Severity.Error);
return;
}
}
foreach (var uriString in applicationForm.PostLogoutRedirectUris)
{
if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri))
{
descriptor.PostLogoutRedirectUris.Add(uri);
}
else
{
Snackbar.Add($"Post Logout URI không hợp lệ: {uriString}", Severity.Error);
return;
}
}
permissionChecks.Where(x => x.Value).ToList().ForEach(kvp => descriptor.Permissions.Add(kvp.Key));
customScopes.ToList().ForEach(scope => descriptor.Permissions.Add(Permissions.Prefixes.Scope + scope));
requirementChecks.Where(x => x.Value).ToList().ForEach(kvp => descriptor.Requirements.Add(kvp.Key));
await ApplicationManager.UpdateAsync(existingApp, descriptor);
}
}
else
{
var descriptor = new OpenIddictApplicationDescriptor
{
ClientId = applicationForm.ClientId,
DisplayName = applicationForm.DisplayName,
ClientType = applicationForm.ClientType,
ConsentType = applicationForm.ConsentType,
ClientSecret = applicationForm.ClientType == ClientTypes.Confidential ? applicationForm.ClientSecret : null
};
foreach (var uriString in applicationForm.RedirectUris)
{
if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri))
{
descriptor.RedirectUris.Add(uri);
}
}
foreach (var uriString in applicationForm.PostLogoutRedirectUris)
{
if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri))
{
descriptor.PostLogoutRedirectUris.Add(uri);
}
}
permissionChecks.Where(x => x.Value).ToList().ForEach(kvp => descriptor.Permissions.Add(kvp.Key));
customScopes.ToList().ForEach(scope => descriptor.Permissions.Add(Permissions.Prefixes.Scope + scope));
requirementChecks.Where(x => x.Value).ToList().ForEach(kvp => descriptor.Requirements.Add(kvp.Key));
await ApplicationManager.CreateAsync(descriptor);
}
Snackbar.Add(editingApplication != null ? "Cập nhật application thành công" : "Tạo application thành công", Severity.Success);
ShowApplicationDialog = false;
await LoadApplicationsAsync();
}
catch (Exception ex)
{
Snackbar.Add($"Lỗi khi lưu application: {ex.Message}", Severity.Error);
}
}
private void CancelApplicationDialog()
{
ShowApplicationDialog = false;
ResetApplicationForm();
}
private async Task DeleteApplication(string clientId)
{
var confirm = await DialogService.ShowMessageBox("Xác nhận xóa", $"Bạn có chắc chắn muốn xóa application '{clientId}'?", yesText: "Xóa", cancelText: "Hủy");
if (confirm == true)
{
try
{
var app = await ApplicationManager.FindByClientIdAsync(clientId);
if (app != null)
{
await ApplicationManager.DeleteAsync(app);
Snackbar.Add("Xóa application thành công", Severity.Success);
await LoadApplicationsAsync();
}
}
catch (Exception ex)
{
Snackbar.Add($"Lỗi khi xóa application: {ex.Message}", Severity.Error);
}
}
}
}

View File

@ -0,0 +1,66 @@
.mdi {
display: inline-block;
position: relative;
background-size: cover;
align-items: center;
}
.app-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 2rem;
color: white;
margin-bottom: 2rem;
box-shadow: 0 20px 40px rgba(102, 126, 234, 0.3);
}
.glass-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.compact-table {
font-size: 0.875rem;
}
.compact-table .mud-table-cell {
padding: 8px 12px;
}
.action-buttons {
display: flex;
gap: 4px;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.75rem;
font-weight: 600;
}
.floating-add-btn {
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 1000;
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
}
.permission-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
max-height: 300px;
overflow-y: auto;
}
.uri-input-section {
background: rgba(102, 126, 234, 0.05);
border-radius: 12px;
padding: 16px;
margin: 12px 0;
}

View File

@ -0,0 +1,21 @@
@page "/Account/OpenIdDictManager"
@rendermode InteractiveServer
@using MudBlazor
<MudTabs Elevation="2" Rounded="true" Style="height:auto; max-height:100%;">
<MudTabPanel Text=" Application" Icon="@Icons.Material.Filled.Face4">
<OpenIdDictApplication />
</MudTabPanel>
<MudTabPanel Text=" Scope" Icon="@Icons.Material.Filled.Face5">
<OpenIdDictScope />
</MudTabPanel>
</MudTabs>
@code {
}

View File

@ -0,0 +1,734 @@
@rendermode InteractiveServer
@attribute [Authorize]
@using Microsoft.AspNetCore.Authorization
@using MudBlazor
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.Components
@using OpenIddict.Abstractions
@using RobotNet.IdentityServer.Data
@using Microsoft.EntityFrameworkCore
@using System.Threading
@using static OpenIddict.Abstractions.OpenIddictConstants
@inherits LayoutComponentBase
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject NavigationManager NavigationManager
@inject IOpenIddictApplicationManager ApplicationManager
@inject IOpenIddictScopeManager ScopeManager
<MudDialogProvider />
<MudSnackbarProvider />
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Style="padding: 1rem;">
<div class="header-gradient">
<div class="header-content">
<div style="display: flex; align-items: center;">
<MudStack Spacing="1">
<div style="display: flex; align-items: center;">
<MudIcon Icon="@Icons.Material.Filled.Security" Class="scope-icon" />
<MudText Typo="Typo.h4" Style="padding-right:2px; font-weight: 700;">
OpenIddict Scopes
</MudText>
</div>
<div>
<MudText Typo="Typo.subtitle2" Style="opacity: 0.7; ">
Quản lý phạm vi truy cập OAuth2 & OpenID Connect
</MudText>
</div>
</MudStack>
</div>
<div class="stats-badge">
<MudIcon Icon="@Icons.Material.Filled.Dataset" Style="margin-right: 0.5rem;" />
<MudText Typo="Typo.body1" Style="font-weight: 600;">
@filteredScopes.Count Scopes
</MudText>
</div>
</div>
</div>
<div class="scope-card">
<div style="padding: 1.5rem; border-bottom: 1px solid #e2e8f0; background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);">
<div style="display: flex; justify-content: space-between; align-items: center;">
<MudText Typo="Typo.h6" Style="margin: 0; color: #334155; font-weight: 600;">
Danh sách Scopes
</MudText>
<div style="display: flex; gap: 1rem; align-items: center;">
<MudButton Variant="Variant.Outlined"
StartIcon="@Icons.Material.Filled.Refresh"
OnClick="@(() => RefreshScopesAsync())"
Style="text-transform: none; font-weight: 500;">
Làm mới
</MudButton>
<MudButton Variant="Variant.Filled"
StartIcon="@Icons.Material.Filled.Add"
OnClick="@(() => OpenScopeDialog())"
Class="add-scope-btn"
Style="text-transform: none; font-weight: 500;">
Thêm Scope
</MudButton>
</div>
</div>
</div>
<MudTable Items="filteredScopes"
Class="scope-table"
Hover="true"
Dense="true"
FixedHeader="true"
Loading="@loadingScopes"
Style="background: transparent;">
<HeaderContent>
<MudTh Style="width: 30%;">Tên hiển thị</MudTh>
<MudTh Style="width: 30%;">Tên Scope</MudTh>
<MudTh Style="width: 25%;">Resources</MudTh>
<MudTh Style="width: 25%; text-align: center;">Thao tác</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Display Name">
<MudStack Spacing="1">
<MudText Typo="Typo.body1" Style="font-weight: 500; color: #1f2937;">
@(string.IsNullOrEmpty(context.DisplayName) ? context.Name : context.DisplayName)
</MudText>
<MudText Typo="Typo.caption" Class="textid">@context.Id[..12]...</MudText>
</MudStack>
</MudTd>
<MudTd DataLabel="Name">
<MudChip T="string"
Text="@context.Name"
Size="Size.Small"
Style="background: #f3f4f6; color: #374151; font-weight: 500;" />
</MudTd>
<MudTd DataLabel="Resources">
@{
var validResources = GetValidResources(context.Resources);
}
@if (validResources.Any())
{
<MudTooltip Text="@string.Join(", ", validResources.Select(r => GetResourceDisplayName(r)))">
<MudChip T="string" Color="Color.Tertiary"
Text="@($"{validResources.Count} resource{(validResources.Count > 1 ? "s" : "")}")"
Size="Size.Small"
Class="resource-chip" />
</MudTooltip>
@if (context.Resources.Count > validResources.Count)
{
<MudTooltip Text="@($"{context.Resources.Count - validResources.Count} resource(s) không tồn tại")">
<MudChip T="string" Color="Color.Warning"
Text="@($"{context.Resources.Count - validResources.Count} invalid")"
Size="Size.Small"
Style="margin-left: 0.25rem;" />
</MudTooltip>
}
}
else if (context.Resources.Any())
{
<MudTooltip Text="Tất cả resources không tồn tại">
<MudChip T="string" Color="Color.Error"
Text="All invalid"
Size="Size.Small" />
</MudTooltip>
}
else
{
<MudText Typo="Typo.caption" Style="color: #9ca3af; font-style: italic;">
Không có resources
</MudText>
}
</MudTd>
<MudTd DataLabel="Actions">
<div class="action-buttons" style="justify-content: center;">
<MudIconButton Icon="@Icons.Material.Filled.Visibility"
Size="Size.Small"
Style="color: #3b82f6;"
OnClick="@(() => ViewScopeDetails(context))"
aria-label="Xem chi tiết" />
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Size="Size.Small"
Style="color: #059669;"
OnClick="@(() => EditScope(context))"
aria-label="Chỉnh sửa" />
@if (HasInvalidResources(context.Resources))
{
<MudIconButton Icon="@Icons.Material.Filled.CleaningServices"
Size="Size.Small"
Style="color: #f59e0b;"
OnClick="@(() => CleanupScopeResources(context))"
aria-label="Dọn dẹp resources không hợp lệ" />
}
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Size="Size.Small"
Style="color: #dc2626;"
OnClick="@(() => DeleteScope(context.Name))"
aria-label="Xóa" />
</div>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager PageSizeOptions="new int[] { 5, 10, 25, 50, 100, int.MaxValue }" />
</PagerContent>
<LoadingContent>
<div style="text-align: center; padding: 2rem;">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
<MudText Typo="Typo.body1" Style="margin-top: 1rem; color: #6b7280;">
Đang tải dữ liệu...
</MudText>
</div>
</LoadingContent>
</MudTable>
</div>
<MudDialog @bind-Visible="showScopeDialog" Options="@(new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true, CloseButton = true })">
<TitleContent>
<div style="display: flex; align-items: center;">
<MudIcon Icon="@Icons.Material.Filled.Security" Style="margin-right: 0.5rem; color: #4f46e5;" />
<MudText Typo="Typo.h6" Style="margin: 0;">
@(editingScope?.Name != null ? "Chỉnh sửa Scope" : "Thêm Scope mới")
</MudText>
</div>
</TitleContent>
<DialogContent>
<div class="dialog-content">
<MudGrid Spacing="3">
<MudItem xs="12" md="6">
<MudTextField @bind-Value="scopeForm.Name"
Label="Tên Scope"
Required="true"
Variant="Variant.Outlined"
Style="background: white;" />
</MudItem>
<MudItem xs="12" md="6">
<MudTextField @bind-Value="scopeForm.DisplayName"
Label="Tên hiển thị"
Variant="Variant.Outlined"
Style="background: white;" />
</MudItem>
<MudItem xs="12">
<div class="resource-selection">
<MudText Typo="Typo.subtitle1" Style="margin-bottom: 1rem; color: #374151; font-weight: 600;">
<MudIcon Icon="@Icons.Material.Filled.Storage" Style="margin-right: 0.5rem;" />
Chọn Resources
</MudText>
<MudSelect T="string"
Label="Chọn Resources"
Variant="Variant.Outlined"
MultiSelection="true"
SelectAll="true"
SelectAllText="Chọn tất cả"
Dense="true"
MaxHeight="250"
Style="background: white; margin-bottom: 1rem;"
SelectedValuesChanged="@OnResourceSelectionChanged"
SelectedValues="@GetSelectedResources()">
@foreach (var resource in availableResources)
{
<MudSelectItem Value="@resource.ClientId">
<div style="display: flex; align-items: center;">
<MudIcon Icon="@Icons.Material.Filled.Apps" Style="margin-right: 0.5rem; color: #6b7280;" />
<div>
<MudText Typo="Typo.body1">@resource.DisplayName</MudText>
<MudText Typo="Typo.caption" Style="color: #9ca3af;">@resource.ClientId</MudText>
</div>
</div>
</MudSelectItem>
}
</MudSelect>
@if (GetSelectedResources().Any())
{
<MudText Typo="Typo.subtitle2" Style="margin-bottom: 0.5rem; color: #374151;">
Resources đã chọn:
</MudText>
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
@foreach (var selectedResource in GetSelectedResources())
{
var resourceInfo = availableResources.FirstOrDefault(r => r.ClientId == selectedResource);
<MudChip T="string"
Text="@(resourceInfo?.DisplayName ?? selectedResource)"
OnClose="@(() => RemoveResource(selectedResource))"
Class="selected-resource-chip"
Size="Size.Small" />
}
</div>
}
</div>
</MudItem>
</MudGrid>
</div>
</DialogContent>
<DialogActions>
<MudButton OnClick="CancelScopeDialog" Style="text-transform: none;">
Hủy
</MudButton>
<MudButton Color="Color.Primary"
Variant="Variant.Filled"
OnClick="SaveScope"
Style="background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); text-transform: none;">
<MudIcon Icon="@Icons.Material.Filled.Save" Style="margin-right: 0.5rem;" />
Lưu
</MudButton>
</DialogActions>
</MudDialog>
<MudDialog @bind-Visible="ShowDetailsDialog" Options="@(new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true, CloseButton = true })">
<TitleContent>
<div style="display: flex; align-items: center;">
<MudIcon Icon="@Icons.Material.Filled.Info" Style="margin-right: 0.5rem; color: #3b82f6;" />
<MudText Typo="Typo.h6" Style="margin: 0;">Chi tiết Scope</MudText>
</div>
</TitleContent>
<DialogContent>
@if (selectedScope != null)
{
<div style="background: #f8fafc; border-radius: 12px; padding: 1.5rem;">
<MudGrid Spacing="2">
<MudItem xs="12" md="6">
<div style="background: white; padding: 1rem; border-radius: 8px; border-left: 4px solid #3b82f6;">
<MudText Typo="Typo.caption" Style="color: #6b7280; margin-bottom: 0.25rem;">ID</MudText>
<MudText Typo="Typo.body2" Style="font-family: monospace; word-break: break-all;">@selectedScope.Id</MudText>
</div>
</MudItem>
<MudItem xs="12" md="6">
<div style="background: white; padding: 1rem; border-radius: 8px; border-left: 4px solid #059669;">
<MudText Typo="Typo.caption" Style="color: #6b7280; margin-bottom: 0.25rem;">Tên</MudText>
<MudText Typo="Typo.body1" Style="font-weight: 500;">@selectedScope.Name</MudText>
</div>
</MudItem>
<MudItem xs="12">
<div style="background: white; padding: 1rem; border-radius: 8px; border-left: 4px solid #7c3aed;">
<MudText Typo="Typo.caption" Style="color: #6b7280; margin-bottom: 0.25rem;">Tên hiển thị</MudText>
<MudText Typo="Typo.body1">@(string.IsNullOrEmpty(selectedScope.DisplayName) ? "Không có" : selectedScope.DisplayName)</MudText>
</div>
</MudItem>
<MudItem xs="12">
<div style="background: white; padding: 1rem; border-radius: 8px; border-left: 4px solid #f59e0b;">
<MudText Typo="Typo.caption" Style="color: #6b7280; margin-bottom: 0.5rem;">Resources</MudText>
<div>
@if (selectedScope.Resources.Any())
{
var validResources = GetValidResources(selectedScope.Resources);
var invalidResources = selectedScope.Resources.Except(validResources).ToList();
@if (validResources.Any())
{
<MudText Typo="Typo.caption" Style="color: #059669; margin-bottom: 0.5rem; font-weight: 600;">Resources hợp lệ:</MudText>
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1rem;">
@foreach (var resource in validResources)
{
<MudChip T="string" Text="@GetResourceDisplayName(resource)" Size="Size.Small" Style="background: #dcfce7; color: #166534;" />
}
</div>
}
@if (invalidResources.Any())
{
<MudText Typo="Typo.caption" Style="color: #dc2626; margin-bottom: 0.5rem; font-weight: 600;">Resources không hợp lệ:</MudText>
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
@foreach (var resource in invalidResources)
{
<MudChip T="string" Text="@resource" Size="Size.Small" Style="background: #fecaca; color: #991b1b;" />
}
</div>
}
}
else
{
<MudText Style="color: #9ca3af; font-style: italic;">Không có resources</MudText>
}
</div>
</div>
</MudItem>
</MudGrid>
</div>
}
</DialogContent>
<DialogActions>
<MudButton OnClick="CloseDetailsDialog" Color="Color.Primary" Style="text-transform: none;">
<MudIcon Icon="@Icons.Material.Filled.Close" Style="margin-right: 0.5rem;" />
Đóng
</MudButton>
</DialogActions>
</MudDialog>
</MudContainer>
@code {
private string resourceInput = string.Empty;
private List<ScopeInfo> filteredScopes = new();
private List<ResourceInfo> availableResources = new();
private Dictionary<string, bool> resourceChecks = new();
private bool loadingScopes = false;
private bool showScopeDialog = false;
private bool ShowDetailsDialog = false;
private ScopeInfo? editingScope = null;
private ScopeInfo? selectedScope = null;
private ScopeForm scopeForm = new();
public class ScopeInfo
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public List<string> Resources { get; set; } = new();
public Dictionary<string, string> Properties { get; set; } = new();
}
public class ScopeForm
{
public string Name { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public List<string> Resources { get; set; } = new();
}
public class ResourceInfo
{
public string ClientId { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
}
protected override async Task OnInitializedAsync()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
if (authState.User.Identity?.IsAuthenticated == true)
{
await LoadAvailableResourcesAsync();
await LoadScopesAsync();
}
}
private async Task LoadAvailableResourcesAsync()
{
try
{
availableResources.Clear();
await foreach (var app in ApplicationManager.ListAsync())
{
var clientType = await ApplicationManager.GetClientTypeAsync(app);
if (clientType == ClientTypes.Confidential)
{
var clientId = await ApplicationManager.GetClientIdAsync(app);
var displayName = await ApplicationManager.GetDisplayNameAsync(app);
if (!string.IsNullOrEmpty(clientId))
{
availableResources.Add(new ResourceInfo
{
ClientId = clientId,
DisplayName = string.IsNullOrEmpty(displayName) ? clientId : displayName
});
}
}
}
resourceChecks.Clear();
foreach (var resource in availableResources)
{
resourceChecks[resource.ClientId] = false;
}
}
catch (Exception ex)
{
Snackbar.Add($"Lỗi khi tải resources: {ex.Message}", Severity.Error);
}
}
private List<string> GetValidResources(List<string> resources)
{
var validResourceIds = availableResources.Select(r => r.ClientId).ToHashSet();
return resources.Where(r => validResourceIds.Contains(r)).ToList();
}
private bool HasInvalidResources(List<string> resources)
{
var validResourceIds = availableResources.Select(r => r.ClientId).ToHashSet();
return resources.Any(r => !validResourceIds.Contains(r));
}
private string GetResourceDisplayName(string clientId)
{
var resource = availableResources.FirstOrDefault(r => r.ClientId == clientId);
return resource?.DisplayName ?? clientId;
}
private async Task CleanupScopeResources(ScopeInfo scope)
{
var confirm = await DialogService.ShowMessageBox(
"Xác nhận dọn dẹp",
$"Bạn có muốn xóa các resources không hợp lệ khỏi scope '{scope.Name}'?",
yesText: "Dọn dẹp",
cancelText: "Hủy"
);
if (confirm == true)
{
try
{
var existingScope = await ScopeManager.FindByNameAsync(scope.Name);
if (existingScope != null)
{
var validResources = GetValidResources(scope.Resources);
var descriptor = new OpenIddictScopeDescriptor
{
Name = scope.Name,
DisplayName = scope.DisplayName,
Description = scope.Description
};
foreach (var resource in validResources)
{
descriptor.Resources.Add(resource);
}
await ScopeManager.PopulateAsync(existingScope, descriptor);
await ScopeManager.UpdateAsync(existingScope);
Snackbar.Add($"Đã dọn dẹp {scope.Resources.Count - validResources.Count} resources không hợp lệ", Severity.Success);
await LoadScopesAsync();
}
}
catch (Exception ex)
{
Snackbar.Add($"Lỗi khi dọn dẹp resources: {ex.Message}", Severity.Error);
}
}
}
private async Task RefreshScopesAsync()
{
await LoadAvailableResourcesAsync();
await LoadScopesAsync();
Snackbar.Add("Đã làm mới danh sách scopes", Severity.Success);
}
private void AddResource()
{
if (!string.IsNullOrWhiteSpace(resourceInput) && !scopeForm.Resources.Contains(resourceInput))
{
scopeForm.Resources.Add(resourceInput);
resourceInput = string.Empty;
StateHasChanged();
}
}
private void OnResourceSelectionChanged(IEnumerable<string> selectedValues)
{
foreach (var key in resourceChecks.Keys.ToList())
{
resourceChecks[key] = false;
}
foreach (var value in selectedValues)
{
if (resourceChecks.ContainsKey(value))
{
resourceChecks[value] = true;
}
}
scopeForm.Resources = selectedValues.ToList();
StateHasChanged();
}
private IEnumerable<string> GetSelectedResources()
{
return resourceChecks.Where(x => x.Value).Select(x => x.Key);
}
private void RemoveResource(string resource)
{
if (resourceChecks.ContainsKey(resource))
{
resourceChecks[resource] = false;
scopeForm.Resources.Remove(resource);
StateHasChanged();
}
}
private async Task LoadScopesAsync()
{
try
{
loadingScopes = true;
filteredScopes.Clear();
await foreach (var scope in ScopeManager.ListAsync())
{
var id = await ScopeManager.GetIdAsync(scope);
var name = await ScopeManager.GetNameAsync(scope);
var displayName = await ScopeManager.GetDisplayNameAsync(scope);
var description = await ScopeManager.GetDescriptionAsync(scope);
var resources = await ScopeManager.GetResourcesAsync(scope);
var properties = await ScopeManager.GetPropertiesAsync(scope);
filteredScopes.Add(new ScopeInfo
{
Id = id ?? string.Empty,
Name = name ?? string.Empty,
DisplayName = displayName ?? string.Empty,
Description = description ?? string.Empty,
Resources = resources.ToList(),
Properties = properties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToString() ?? string.Empty)
});
}
}
catch (Exception ex)
{
Snackbar.Add($"Lỗi khi tải scopes: {ex.Message}", Severity.Error);
}
finally
{
loadingScopes = false;
StateHasChanged();
}
}
private void ViewScopeDetails(ScopeInfo scope)
{
selectedScope = scope;
ShowDetailsDialog = true;
}
private void CloseDetailsDialog()
{
ShowDetailsDialog = false;
selectedScope = null;
}
private async Task OpenScopeDialog(ScopeInfo? scope = null)
{
editingScope = scope;
ResetScopeForm();
if (scope != null)
{
scopeForm.Name = scope.Name;
scopeForm.DisplayName = scope.DisplayName;
scopeForm.Description = scope.Description;
var validResources = GetValidResources(scope.Resources);
scopeForm.Resources = new List<string>(validResources);
foreach (var key in resourceChecks.Keys.ToList())
{
resourceChecks[key] = validResources.Contains(key);
}
if (scope.Resources.Count > validResources.Count)
{
var invalidCount = scope.Resources.Count - validResources.Count;
Snackbar.Add($"Đã loại bỏ {invalidCount} resource không hợp lệ khỏi form chỉnh sửa", Severity.Warning);
}
}
showScopeDialog = true;
await Task.CompletedTask;
}
private async void EditScope(ScopeInfo scope)
{
await OpenScopeDialog(scope);
}
private void ResetScopeForm()
{
scopeForm = new ScopeForm();
foreach (var key in resourceChecks.Keys.ToList())
{
resourceChecks[key] = false;
}
}
private async Task SaveScope()
{
try
{
if (string.IsNullOrWhiteSpace(scopeForm.Name))
{
Snackbar.Add("Name là bắt buộc", Severity.Error);
return;
}
var descriptor = new OpenIddictScopeDescriptor
{
Name = scopeForm.Name,
DisplayName = scopeForm.DisplayName,
Description = scopeForm.Description
};
var validResources = GetValidResources(scopeForm.Resources);
foreach (var resource in validResources)
{
descriptor.Resources.Add(resource);
}
if (editingScope != null)
{
var existingScope = await ScopeManager.FindByNameAsync(editingScope.Name);
if (existingScope != null)
{
await ScopeManager.PopulateAsync(existingScope, descriptor);
await ScopeManager.UpdateAsync(existingScope);
}
}
else
{
await ScopeManager.CreateAsync(descriptor);
}
Snackbar.Add(editingScope != null ? "Cập nhật scope thành công" : "Tạo scope thành công", Severity.Success);
showScopeDialog = false;
await LoadScopesAsync();
}
catch (Exception ex)
{
Snackbar.Add($"Lỗi khi lưu scope: {ex.Message}", Severity.Error);
}
}
private void CancelScopeDialog()
{
showScopeDialog = false;
ResetScopeForm();
}
private async Task DeleteScope(string name)
{
var confirm = await DialogService.ShowMessageBox("Xác nhận xóa", $"Bạn có chắc chắn muốn xóa scope '{name}'?", yesText: "Xóa", cancelText: "Hủy");
if (confirm == true)
{
try
{
var scope = await ScopeManager.FindByNameAsync(name);
if (scope != null)
{
await ScopeManager.DeleteAsync(scope);
Snackbar.Add("Xóa scope thành công", Severity.Success);
await LoadScopesAsync();
}
}
catch (Exception ex)
{
Snackbar.Add($"Lỗi khi xóa scope: {ex.Message}", Severity.Error);
}
}
}
}

View File

@ -0,0 +1,142 @@
.mdi {
display: inline-block;
position: relative;
background-size: cover;
align-items: center;
}
.textid {
font-family: 'Gill Sans', 'Gill Sans MT', 'Calibri', 'Trebuchet MS', 'sans-serif';
color: #6b7280;
}
.header-gradient {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
color: white;
padding: 2rem;
margin-bottom: 1.5rem;
position: relative;
overflow: hidden;
}
.header-gradient::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="50" cy="50" r="1" fill="white" opacity="0.1"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
opacity: 0.3;
}
.header-content {
position: relative;
z-index: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.scope-icon {
font-size: 4rem;
margin-right: 1rem;
padding-left: 1rem;
}
.stats-badge {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 0.5rem 1rem;
border: 1px solid rgba(255, 255, 255, 0.3);
display: flex;
align-items: center;
}
.scope-card {
background: white;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
overflow: hidden;
}
.scope-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
}
.scope-table {
background: transparent;
}
.scope-table .mud-table-head {
background: #f8fafc;
border-bottom: 2px solid #e2e8f0;
}
.scope-table .mud-table-head th {
font-weight: 600;
color: #334155;
padding: 1rem 0.75rem;
}
.scope-table .mud-table-row {
border-bottom: 1px solid #f1f5f9;
transition: background-color 0.2s ease;
}
.scope-table .mud-table-row:hover {
background-color: #f8fafc;
}
.scope-table .mud-table-cell {
padding: 1rem 0.75rem;
vertical-align: middle;
}
.action-buttons {
display: flex;
gap: 0.5rem;
align-items: center;
}
.add-scope-btn {
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
border: none;
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
transition: all 0.3s ease;
}
.add-scope-btn:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(79, 70, 229, 0.4);
}
.resource-chip {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
color: white;
border: none;
font-weight: 500;
}
.dialog-content {
background: #fafbfc;
border-radius: 12px;
padding: 1.5rem;
margin: 1rem 0;
}
.resource-selection {
background: white;
border-radius: 8px;
padding: 1rem;
border: 1px solid #e2e8f0;
}
.selected-resource-chip {
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
color: white;
margin: 0.25rem;
}

View File

@ -0,0 +1,285 @@
@rendermode InteractiveServer
@using Microsoft.AspNetCore.Identity
@using RobotNet.IdentityServer.Data
@using MudBlazor
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components
@using System.Threading
@using System.ComponentModel.DataAnnotations
@using RobotNet.IdentityServer.Services
@inherits LayoutComponentBase
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject PasswordStrengthService PasswordStrengthService
@inject UserManager<ApplicationUser> UserManager
@inject ISnackbar Snackbar
@inject NavigationManager NavigationManager
<MudSnackbarProvider />
<div class="password-container">
<div class="password-wrapper">
<MudCard Class="password-card" Elevation="0">
<div class="password-header">
<div class="header-content">
<MudIcon Icon="@Icons.Material.Filled.Lock" Class="header-icon" />
<div class="header-text">
<MudText Typo="Typo.h4">Đổi mật khẩu</MudText>
<MudText Typo="Typo.body1">Cập nhật mật khẩu để tăng cường bảo mật</MudText>
</div>
</div>
</div>
<MudCardContent Class="card-content">
<EditForm Model="@model" OnValidSubmit="ChangePassword" @ref="editForm">
<DataAnnotationsValidator />
<div class="form-fields">
<div class="form-group">
<MudTextField Label="Mật khẩu hiện tại"
@bind-Value="model.CurrentPassword"
InputType="@(showCurrentPassword? InputType.Text: InputType.Password)"
Variant="Variant.Outlined"
Class="password-field"
Adornment="Adornment.End"
AdornmentIcon="@(showCurrentPassword? Icons.Material.Filled.Visibility : Icons.Material.Filled.VisibilityOff)"
OnAdornmentClick="() => showCurrentPassword = !showCurrentPassword"
AdornmentAriaLabel="Toggle password visibility"
@onfocus="EnableButtons"
FullWidth="true" />
<ValidationMessage For="@(() => model.CurrentPassword)" class="validation-message" />
</div>
<div class="form-group">
<MudTextField Label="Mật khẩu mới"
@bind-Value="model.NewPassword"
InputType="@(showNewPassword? InputType.Text: InputType.Password)"
Variant="Variant.Outlined"
Class="password-field"
Adornment="Adornment.End"
AdornmentIcon="@(showNewPassword? Icons.Material.Filled.Visibility : Icons.Material.Filled.VisibilityOff)"
OnAdornmentClick="() => showNewPassword = !showNewPassword"
AdornmentAriaLabel="Toggle password visibility"
@onfocus="EnableButtons"
@oninput="OnNewPasswordChanged"
FullWidth="true" />
<ValidationMessage For="@(() => model.NewPassword)" class="validation-message" />
@if (!string.IsNullOrEmpty(model.NewPassword))
{
<div class="password-strength-container">
<MudText Typo="Typo.body2">Độ mạnh: @GetPasswordStrengthText()</MudText>
<MudProgressLinear Value="@GetPasswordStrength()" Color="@GetPasswordStrengthColor()" />
<div class="password-requirements">
<MudText Typo="Typo.caption">Yêu cầu:</MudText>
<div class="requirements-list">
<div class="requirement @(model.NewPassword.Length >= 6 ? "valid" : "invalid")">
<MudIcon Icon="@(model.NewPassword.Length >= 6 ? Icons.Material.Filled.Check : Icons.Material.Filled.Close)" />
<span>Tối thiểu 6 ký tự</span>
</div>
@* <div class="requirement @(model.NewPassword.Any(char.IsUpper) ? "valid" : "invalid")">
<MudIcon Icon="@(model.NewPassword.Any(char.IsUpper) ? Icons.Material.Filled.Check : Icons.Material.Filled.Close)" />
<span>Chữ hoa</span>
</div> *@
<div class="requirement @(model.NewPassword.Any(char.IsLower) ? "valid" : "invalid")">
<MudIcon Icon="@(model.NewPassword.Any(char.IsLower) ? Icons.Material.Filled.Check : Icons.Material.Filled.Close)" />
<span>Chữ thường</span>
</div>
@* <div class="requirement @(model.NewPassword.Any(char.IsDigit) ? "valid" : "invalid")">
<MudIcon Icon="@(model.NewPassword.Any(char.IsDigit) ? Icons.Material.Filled.Check : Icons.Material.Filled.Close)" />
<span>Số</span>
</div>
<div class="requirement @(model.NewPassword.Any(c => !char.IsLetterOrDigit(c)) ? "valid" : "invalid")">
<MudIcon Icon="@(model.NewPassword.Any(c => !char.IsLetterOrDigit(c)) ? Icons.Material.Filled.Check : Icons.Material.Filled.Close)" />
<span>Ký tự đặc biệt</span>
</div> *@
</div>
</div>
</div>
}
</div>
<div class="form-group">
<MudTextField Label="Xác nhận mật khẩu"
@bind-Value="model.ConfirmPassword"
InputType="@(showConfirmPassword? InputType.Text: InputType.Password)"
Variant="Variant.Outlined"
Class="password-field"
Adornment="Adornment.End"
AdornmentIcon="@(showConfirmPassword? Icons.Material.Filled.Visibility : Icons.Material.Filled.VisibilityOff)"
OnAdornmentClick="() => showConfirmPassword = !showConfirmPassword"
AdornmentAriaLabel="Toggle password visibility"
@onfocus="EnableButtons"
FullWidth="true" />
<ValidationMessage For="@(() => model.ConfirmPassword)" class="validation-message" />
@if (!string.IsNullOrEmpty(model.NewPassword) && !string.IsNullOrEmpty(model.ConfirmPassword))
{
<MudText Color="@(model.NewPassword == model.ConfirmPassword ? Color.Success : Color.Error)">
@(model.NewPassword == model.ConfirmPassword ? "Mật khẩu khớp" : "Mật khẩu không khớp")
</MudText>
}
</div>
</div>
@if (!string.IsNullOrEmpty(errorMessage))
{
<MudAlert Severity="Severity.Error" Class="mt-4" ShowCloseIcon="true" CloseIconClicked="() => errorMessage = string.Empty">
@errorMessage
</MudAlert>
}
</EditForm>
</MudCardContent>
<MudCardActions Class="d-flex justify-end gap-2 pb-4 px-4">
<MudButton Variant="Variant.Outlined"
Color="Color.Default"
OnClick="Cancel"
Disabled="@isButtonDisabled"
Class="action-button"
StartIcon="@Icons.Material.Filled.Cancel">
Hủy
</MudButton>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="SubmitForm"
Disabled="@(isButtonDisabled || isProcessing)"
Class="action-button"
StartIcon="@(isProcessing ? null : Icons.Material.Filled.Save)">
@if (isProcessing)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" />
<span class="ms-2">Đang xử lý...</span>
}
else
{
<span>Lưu</span>
}
</MudButton>
</MudCardActions>
</MudCard>
</div>
</div>
@code {
private bool isButtonDisabled = true;
private bool showCurrentPassword = false;
private bool showNewPassword = false;
private bool showConfirmPassword = false;
private ChangePasswordModel model = new();
private bool isProcessing = false;
private string errorMessage = string.Empty;
private EditForm? editForm;
private class ChangePasswordModel
{
[Required(ErrorMessage = "Vui lòng nhập mật khẩu hiện tại")]
public string CurrentPassword { get; set; } = string.Empty;
[Required(ErrorMessage = "Vui lòng nhập mật khẩu mới")]
[StringLength(100, ErrorMessage = "Mật khẩu phải từ {2} đến {1} ký tự", MinimumLength = 8)]
public string NewPassword { get; set; } = string.Empty;
[Required(ErrorMessage = "Vui lòng xác nhận mật khẩu mới")]
[Compare("NewPassword", ErrorMessage = "Mật khẩu xác nhận không khớp")]
public string ConfirmPassword { get; set; } = string.Empty;
}
private void EnableButtons()
{
isButtonDisabled = false;
}
private void OnNewPasswordChanged(ChangeEventArgs e)
{
model.NewPassword = e.Value?.ToString() ?? string.Empty;
StateHasChanged();
}
private async Task SubmitForm()
{
if (editForm?.EditContext?.Validate() == true)
{
await ChangePassword();
}
else
{
Snackbar.Add("Vui lòng kiểm tra lại thông tin nhập", Severity.Error);
}
}
private int GetPasswordStrength()
{
return PasswordStrengthService.EvaluatePasswordStrength(model.NewPassword);
}
private Color GetPasswordStrengthColor()
{
return PasswordStrengthService.GetStrengthColor(GetPasswordStrength());
}
private string GetPasswordStrengthText()
{
return PasswordStrengthService.GetStrengthDescription(GetPasswordStrength());
}
protected override async Task OnInitializedAsync()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
if (authState?.User?.Identity?.IsAuthenticated != true)
{
Snackbar.Add("Vui lòng đăng nhập", Severity.Error);
NavigationManager.NavigateTo("/Account/Login");
}
}
private async Task ChangePassword()
{
isProcessing = true;
errorMessage = string.Empty;
try
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = await UserManager.GetUserAsync(authState.User);
if (user == null)
{
Snackbar.Add("Không tìm thấy thông tin người dùng", Severity.Error);
return;
}
var result = await UserManager.ChangePasswordAsync(user, model.CurrentPassword, model.NewPassword);
if (result.Succeeded)
{
Snackbar.Add("Đổi mật khẩu thành công", Severity.Success);
model = new ChangePasswordModel();
isButtonDisabled = true;
}
else
{
errorMessage = string.Join(", ", result.Errors.Select(e => e.Description));
Snackbar.Add(errorMessage, Severity.Error);
}
}
catch (Exception ex)
{
errorMessage = $"Lỗi: {ex.Message}";
Snackbar.Add(errorMessage, Severity.Error);
}
finally
{
isProcessing = false;
StateHasChanged();
}
}
private void Cancel()
{
model = new ChangePasswordModel();
isButtonDisabled = true;
errorMessage = string.Empty;
StateHasChanged();
}
}

View File

@ -0,0 +1,566 @@
.password-container {
padding: 1rem;
min-height: 90vh;
display: flex;
align-items: center;
justify-content: center;
/*background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);*/
position: relative;
overflow: hidden;
}
.password-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="25" cy="25" r="1" fill="white" opacity="0.1"/><circle cx="75" cy="75" r="1" fill="white" opacity="0.1"/><circle cx="50" cy="10" r="0.5" fill="white" opacity="0.1"/><circle cx="20" cy="60" r="0.5" fill="white" opacity="0.1"/><circle cx="80" cy="40" r="0.5" fill="white" opacity="0.1"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
pointer-events: none;
}
.password-wrapper {
width: 100%;
max-width: 650px;
position: relative;
z-index: 1;
margin: 0 auto;
}
.password-card {
border-radius: 24px;
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 32px 64px rgba(0, 0, 0, 0.12), 0 16px 32px rgba(0, 0, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.6);
overflow: hidden;
width: 100%;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.password-card:hover {
transform: translateY(-8px);
box-shadow: 0 48px 96px rgba(0, 0, 0, 0.18), 0 24px 48px rgba(0, 0, 0, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.6);
}
.password-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 3rem 2.5rem;
position: relative;
overflow: hidden;
}
.password-header::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.15) 0%, transparent 70%);
animation: shimmer 4s ease-in-out infinite;
}
.password-header::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
}
@keyframes shimmer {
0%, 100% {
transform: translateX(-100%) translateY(-100%) rotate(0deg);
}
50% {
transform: translateX(20%) translateY(20%) rotate(180deg);
}
}
.header-content {
display: flex;
align-items: center;
gap: 2.5rem;
width: 100%;
position: relative;
z-index: 2;
}
.header-icon {
font-size: 3.5rem !important;
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
.header-text h4 {
font-weight: 700 !important;
margin-bottom: 0.75rem !important;
color: white !important;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
font-size: 2.25rem !important;
line-height: 1.2;
}
.header-text .mud-typography-body1 {
color: rgba(255, 255, 255, 0.95) !important;
line-height: 1.6;
font-weight: 400;
font-size: 1.125rem;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
}
.card-content {
padding: 3rem 2.5rem !important;
background: linear-gradient(180deg, #ffffff 0%, #fafbff 100%);
position: relative;
}
.form-fields {
display: flex;
flex-direction: column;
gap: 2.5rem;
}
.form-group {
position: relative;
width: 100%;
animation: slideInUp 0.6s ease-out;
}
.form-group:nth-child(1) {
animation-delay: 0.1s;
}
.form-group:nth-child(2) {
animation-delay: 0.2s;
}
.form-group:nth-child(3) {
animation-delay: 0.3s;
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Enhanced TextField Styling */
.password-field :deep(.mud-input-outlined .mud-input-root) {
border-radius: 16px !important;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 2px solid rgba(102, 126, 234, 0.2) !important;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
min-height: 64px;
}
.password-field :deep(.mud-input-outlined:hover .mud-input-root:not(.mud-input-error)) {
border-color: #667eea !important;
background: rgba(255, 255, 255, 1);
transform: translateY(-2px);
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.15);
}
.password-field :deep(.mud-input-outlined.mud-input-focused .mud-input-root:not(.mud-input-error)) {
border-color: #667eea !important;
background: white;
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1), 0 8px 32px rgba(102, 126, 234, 0.2);
transform: translateY(-3px);
}
.password-field :deep(.mud-input-label) {
font-weight: 600 !important;
color: #4a5568 !important;
font-size: 1rem;
}
.password-field :deep(.mud-input-outlined.mud-input-focused .mud-input-label) {
color: #667eea !important;
}
.password-field :deep(.mud-input-control) {
padding: 0 1rem;
font-size: 1rem;
font-weight: 500;
}
.password-field :deep(.mud-input-adornment-end) {
margin-right: 0.5rem;
}
.validation-message {
color: #e53e3e;
font-size: 0.875rem;
font-weight: 500;
margin-top: 0.5rem;
margin-left: 0.75rem;
animation: slideInUp 0.3s ease-out;
}
/* Password Strength Section */
.password-strength-container {
background: linear-gradient(135deg, #f8faff 0%, #ffffff 100%);
border-radius: 20px;
padding: 2rem;
margin-top: 1.5rem;
border: 1px solid rgba(102, 126, 234, 0.15);
animation: slideInUp 0.4s ease-out;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06);
position: relative;
overflow: hidden;
}
.password-strength-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 20px 20px 0 0;
}
.password-strength-container .mud-typography-body2 {
font-weight: 600 !important;
color: #2d3748 !important;
margin-bottom: 1rem !important;
font-size: 1rem;
}
.password-strength-container :deep(.mud-progress-linear) {
height: 12px !important;
border-radius: 8px !important;
margin-bottom: 1.5rem !important;
background-color: #e2e8f0 !important;
overflow: hidden;
position: relative;
}
.password-strength-container :deep(.mud-progress-linear-bar) {
border-radius: 8px !important;
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1) !important;
position: relative;
}
.password-strength-container :deep(.mud-progress-linear-bar::after) {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.4) 50%, transparent 100%);
animation: shine 2s ease-in-out infinite;
}
@keyframes shine {
0% {
transform: translateX(-100%);
}
50% {
transform: translateX(100%);
}
100% {
transform: translateX(100%);
}
}
.password-requirements {
margin-top: 1.5rem;
}
.password-requirements .mud-typography-caption {
font-weight: 600 !important;
color: #2d3748 !important;
margin-bottom: 1rem !important;
display: block;
font-size: 0.9375rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.requirements-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 1rem;
}
.requirement {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.875rem;
padding: 0.5rem 0.75rem;
border-radius: 16px;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
font-weight: 500;
border: 2px solid transparent;
position: relative;
overflow: hidden;
}
.requirement::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transition: left 0.4s ease;
}
.requirement.valid {
color: #22543d;
background: linear-gradient(135deg, #f0fff4 0%, #c6f6d5 100%);
border-color: #68d391;
animation: checkmark 0.5s ease-in-out;
transform: scale(1.02);
box-shadow: 0 4px 16px rgba(72, 187, 120, 0.2);
}
.requirement.valid::before {
left: 100%;
}
.requirement.invalid {
color: #718096;
background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
border-color: #e2e8f0;
}
.requirement :deep(.mud-icon-root) {
font-size: 1.25rem !important;
transition: all 0.3s ease;
}
.requirement.valid :deep(.mud-icon-root) {
animation: bounce 0.6s ease;
}
@keyframes checkmark {
0% {
transform: scale(0.8) rotate(-5deg);
opacity: 0.7;
}
50% {
transform: scale(1.1) rotate(2deg);
}
100% {
transform: scale(1.02) rotate(0deg);
opacity: 1;
}
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-4px);
}
60% {
transform: translateY(-2px);
}
}
/* Password Match Indicator */
.password-match-indicator {
margin-top: 1rem;
animation: slideInUp 0.3s ease-out;
}
.password-match-indicator .mud-typography {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.9375rem;
padding: 1rem 1.25rem;
border-radius: 16px;
font-weight: 600;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 2px solid transparent;
}
.password-match-indicator .mud-success {
background: linear-gradient(135deg, #f0fff4 0%, #c6f6d5 100%);
border-color: #68d391;
color: #22543d !important;
box-shadow: 0 4px 16px rgba(72, 187, 120, 0.15);
}
.password-match-indicator .mud-error {
background: linear-gradient(135deg, #fed7d7 0%, #feb2b2 100%);
border-color: #fc8181;
color: #742a2a !important;
box-shadow: 0 4px 16px rgba(245, 101, 101, 0.15);
}
.card-actions {
display:flex;
padding: 2rem 2.5rem 2.5rem !important;
background: linear-gradient(180deg, #fafbff 0%, #f4f6ff 100%);
border-top: 1px solid rgba(102, 126, 234, 0.1);
gap: 1.5rem;
justify-content: end;
}
.action-button {
min-width: 140px !important;
height: 56px !important;
border-radius: 16px !important;
font-weight: 600 !important;
font-size: 1rem !important;
text-transform: none !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1) !important;
position: relative;
overflow: hidden;
}
.action-button::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s ease;
}
.action-button:hover::before {
left: 100%;
}
.action-button:hover {
transform: translateY(-3px) !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15) !important;
}
.action-button:active {
transform: translateY(-1px) !important;
}
.action-button:disabled {
opacity: 0.6 !important;
transform: none !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05) !important;
}
/* Error Alert */
.mud-alert {
border-radius: 16px !important;
border: 2px solid rgba(245, 101, 101, 0.2) !important;
background: linear-gradient(135deg, #fed7d7 0%, #feb2b2 100%) !important;
color: #742a2a !important;
font-weight: 500 !important;
box-shadow: 0 4px 16px rgba(245, 101, 101, 0.15) !important;
margin-top: 1.5rem !important;
animation: slideInUp 0.3s ease-out;
}
/* Responsive Design */
@media (max-width: 768px) {
.password-container {
padding: 0.5rem;
}
.password-wrapper {
max-width: 100%;
}
.password-header {
padding: 2rem 1.5rem;
}
.header-content {
flex-direction: column;
gap: 1.5rem;
text-align: center;
}
.header-icon {
font-size: 2.5rem !important;
}
.header-text h4 {
font-size: 1.75rem !important;
}
.card-content {
padding: 2rem 1.5rem !important;
}
.form-fields {
gap: 2rem;
}
.password-strength-container {
padding: 1.5rem;
}
.requirements-list {
grid-template-columns: 1fr;
gap: 0.75rem;
}
.card-actions {
padding: 1.5rem !important;
flex-direction: column;
}
.action-button {
width: 100% !important;
min-width: unset !important;
}
}
@media (max-width: 480px) {
.password-header {
padding: 1.5rem 1rem;
}
.card-content {
padding: 1.5rem 1rem !important;
}
.password-strength-container {
padding: 1rem;
}
.card-actions {
padding: 1rem !important;
}
}

View File

@ -0,0 +1,120 @@
@page "/Account/Register"
@using System.ComponentModel.DataAnnotations
@using System.Text
@using System.Text.Encodings.Web
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities
@using RobotNet.IdentityServer.Data
@inject UserManager<ApplicationUser> UserManager
@inject IUserStore<ApplicationUser> UserStore
@inject SignInManager<ApplicationUser> SignInManager
@inject IEmailSender<ApplicationUser> EmailSender
@inject ILogger<Register> Logger
@inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager
<PageTitle>Register</PageTitle>
<div class="w-100 h-100 d-flex flex-column justify-content-center align-items-center">
<h1>Create a new account.</h1>
@if (!string.IsNullOrEmpty(errorMessage))
{
var statusMessageClass = errorMessage.StartsWith("Error") ? "danger" : "success";
<div class="alert alert-@statusMessageClass" role="alert">
@errorMessage
</div>
}
<EditForm style="width:500px" Model="Input" asp-route-returnUrl="@ReturnUrl" method="post" OnValidSubmit="RegisterUser" FormName="register">
<DataAnnotationsValidator />
<hr />
<ValidationSummary class="text-danger" role="alert" />
<div class="form-floating mb-3 ">
<InputText @bind-Value="Input.UserName" class="form-control" autocomplete="username" aria-required="true" placeholder="name" />
<label for="user">UserName</label>
<ValidationMessage For="() => Input.UserName" class="text-danger" />
</div>
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" placeholder="password" />
<label for="password">Password</label>
<ValidationMessage For="() => Input.Password" class="text-danger" />
</div>
<div class="form-floating mb-3">
<InputText type="password" @bind-Value="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" placeholder="password" />
<label for="confirm-password">Confirm Password</label>
<ValidationMessage For="() => Input.ConfirmPassword" class="text-danger" />
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Register</button>
</EditForm>
</div>
@code {
private string errorMessage = string.Empty;
private IEnumerable<IdentityError>? identityErrors;
[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
private string? Message => identityErrors is null ? null : $"Error: {string.Join(", ", identityErrors.Select(error => error.Description))}";
public async Task RegisterUser(EditContext editContext)
{
var user = CreateUser();
await UserStore.SetUserNameAsync(user, Input.UserName, CancellationToken.None);
user.NormalizedUserName = Input.UserName.ToUpperInvariant();
user.EmailConfirmed = true;
var result = await UserManager.CreateAsync(user, Input.Password);
if (!result.Succeeded)
{
identityErrors = result.Errors;
return;
}
Logger.LogInformation("User created a new account with password.");
await SignInManager.SignInAsync(user, isPersistent: false);
RedirectManager.RedirectTo(ReturnUrl);
}
private ApplicationUser CreateUser()
{
try
{
return Activator.CreateInstance<ApplicationUser>();
}
catch
{
throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " +
$"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor.");
}
}
private sealed class InputModel
{
[Required]
[Display(Name = "UserName")]
public string UserName { get; set; } = "";
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; } = "";
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; } = "";
}
}

View File

@ -0,0 +1,923 @@
@page "/Account/Rolemanager"
@rendermode InteractiveServer
@attribute [Authorize]
@using Microsoft.AspNetCore.Authorization
@using MudBlazor
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.Components
@using RobotNet.IdentityServer.Data
@using Microsoft.EntityFrameworkCore
@using System.Threading
@inherits LayoutComponentBase
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject UserManager<ApplicationUser> UserManager
@inject RoleManager<ApplicationRole> RoleManager
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject NavigationManager NavigationManager
<MudDialogProvider />
<MudSnackbarProvider />
<div class="pa-4">
<div class="role-header">
<MudGrid>
<MudItem xs="12" md="8">
<div class="d-flex align-center">
<MudIcon Icon="@Icons.Material.Filled.AdminPanelSettings" Size="Size.Large" Class="mr-3" />
<div>
<MudText Typo="Typo.h4" Class="mb-1">Role Management</MudText>
<MudText Typo="Typo.body1" Style="opacity: 0.9;">Quản lý vai trò và phân quyền người dùng</MudText>
</div>
</div>
</MudItem>
<MudItem xs="12" md="4" Class="text-right">
<div class="stats-card">
<MudText Typo="Typo.h5">@Roles.Count</MudText>
<MudText Typo="Typo.body2">Tổng số vai trò</MudText>
</div>
</MudItem>
</MudGrid>
</div>
<MudGrid>
<MudItem xs="12" lg="4">
<MudPaper Class="modern-card pa-4" Elevation="0">
<div class="section-title">
<MudIcon Icon="@Icons.Material.Filled.Security" />
<MudText Typo="Typo.h6">Danh Sách Vai Trò</MudText>
</div>
<div class="search-container mb-3">
<MudTextField @bind-Value="roleSearchTerm"
Label="Tìm kiếm vai trò"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search"
Margin="Margin.Dense"
FullWidth="true" />
</div>
<div class="mb-3">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="AddRole"
Class="action-button"
FullWidth="true">
Tạo Vai Trò Mới
</MudButton>
</div>
<div class="table-container">
<MudTable Items="@FilteredRoles" Hover="true" Dense="true" Striped="true">
<HeaderContent>
<MudTh><MudText Typo="Typo.subtitle2">Tên Vai Trò</MudText></MudTh>
<MudTh Style="width: 100px;"><MudText Typo="Typo.subtitle2">Thao Tác</MudText></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<div class="d-flex align-center">
<MudIcon Icon="@GetRoleIcon(context.Name ?? string.Empty)" Size="Size.Small" Class="mr-2" Color="@GetRoleColor(context.Name ?? string.Empty)" />
<MudText Typo="Typo.body2">@context.Name</MudText>
</div>
</MudTd>
<MudTd>
@if (LoggedInUserRoles.Contains(context.Name ?? string.Empty))
{
<MudTooltip Text="Vai trò không thể chỉnh sửa ">
<MudIconButton Icon="@Icons.Material.Filled.Lock"
Size="Size.Small"
Color="Color.Default"
Disabled="true" />
</MudTooltip>
}
else
{
<MudTooltip Text="Chỉnh sửa">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Size="Size.Small"
Color="Color.Primary"
OnClick="() => EditRole(context)"
Class="action-button mr-1" />
</MudTooltip>
<MudTooltip Text="Xóa">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Size="Size.Small"
Color="Color.Error"
OnClick="() => DelRole(context.Id, context.Name ?? string.Empty)"
Class="action-button" />
</MudTooltip>
}
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager PageSizeOptions="new int[] { 5, 10, 25, 50, 100, int.MaxValue }"
RowsPerPageString="@rowsPerPageString" />
</PagerContent>
</MudTable>
</div>
</MudPaper>
</MudItem>
<MudItem xs="12" lg="8">
<MudPaper Class="modern-card pa-4" Elevation="0">
<div class="section-title">
<MudIcon Icon="@Icons.Material.Filled.People" />
<MudText Typo="Typo.h6">Quản Lý Người Dùng</MudText>
</div>
<div class="search-container mb-3">
<MudTextField @bind-Value="userSearchTerm"
Label="Tìm kiếm người dùng"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search"
Margin="Margin.Dense"
FullWidth="true" />
</div>
<div class="table-container">
<MudTable Items="@FilteredUsers" Hover="true" Dense="true" Striped="true">
<HeaderContent>
<MudTh><MudText Typo="Typo.subtitle2">Người Dùng</MudText></MudTh>
<MudTh><MudText Typo="Typo.subtitle2">Vai Trò</MudText></MudTh>
<MudTh Style="width: 100px;"><MudText Typo="Typo.subtitle2">Thao Tác</MudText></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<div class="d-flex align-center">
<div class="user-avatar">
@(context.UserName?.Substring(0, 1).ToUpper())
</div>
<div>
<MudText Typo="Typo.body2" Class="font-weight-medium">@context.UserName</MudText>
@if (context.UserId == LoggedInUserId)
{
<MudChip T="string"
Size="Size.Small"
Color="Color.Info"
Class="mt-1">
Bạn
</MudChip>
}
</div>
</div>
</MudTd>
<MudTd>
<div class="d-flex flex-wrap">
@if (context.Roles.Any())
{
foreach (var role in context.Roles)
{
<MudChip T="string"
Size="Size.Small"
Color="@GetRoleChipColor(role)"
Icon="@GetRoleIcon(role)"
Class="role-chip">
@role
</MudChip>
}
}
else
{
<MudChip T="string"
Size="Size.Small"
Color="Color.Default"
Class="role-chip">
Chưa có vai trò
</MudChip>
}
</div>
</MudTd>
<MudTd>
@if (context.UserId == LoggedInUserId)
{
<MudTooltip Text="Không thể chỉnh sửa vai trò của chính mình">
<MudIconButton Icon="@Icons.Material.Filled.Lock"
Size="Size.Small"
Color="Color.Default"
Disabled="true" />
</MudTooltip>
}
else
{
<MudTooltip Text="Quản lý vai trò">
<MudIconButton Icon="@Icons.Material.Filled.ManageAccounts"
Size="Size.Small"
Color="Color.Primary"
OnClick="() => ManageUserRoles(context.UserId ?? string.Empty, context.UserName ?? string.Empty)"
Class="action-button" />
</MudTooltip>
}
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager PageSizeOptions="new int[] { 5, 10, 25, 50, 100, int.MaxValue }" />
</PagerContent>
</MudTable>
</div>
</MudPaper>
</MudItem>
</MudGrid>
</div>
<MudDialog @bind-Visible="CreateRoleVisible">
<TitleContent>
<div class="d-flex align-center">
<MudIcon Icon="@Icons.Material.Filled.Add" Class="mr-3" />
<MudText Typo="Typo.h6">Tạo Vai Trò Mới</MudText>
</div>
</TitleContent>
<DialogContent>
<MudTextField Label="Tên vai trò"
@bind-Value="NewRoleName"
Required="true"
Variant="Variant.Outlined"
FullWidth="true"
Margin="Margin.Dense"
Style="overflow:hidden;" />
</DialogContent>
<DialogActions>
<MudButton Color="Color.Primary"
Variant="Variant.Filled"
OnClick="CreateRole"
StartIcon="@Icons.Material.Filled.Save">
Tạo
</MudButton>
<MudButton Variant="Variant.Text"
OnClick="@(() => CreateRoleVisible = false)">
Hủy
</MudButton>
</DialogActions>
</MudDialog>
<MudDialog @bind-Visible="EditRoleVisible">
<TitleContent>
<div class="d-flex align-center">
<MudIcon Icon="@Icons.Material.Filled.Edit" Class="mr-3" />
<MudText Typo="Typo.h6">Chỉnh Sửa Vai Trò</MudText>
</div>
</TitleContent>
<DialogContent>
<MudTextField Label="Tên vai trò mới"
@bind-Value="EditRoleName"
Required="true"
Variant="Variant.Outlined"
FullWidth="true"
Margin="Margin.Dense" />
</DialogContent>
<DialogActions>
<MudButton Color="Color.Primary"
Variant="Variant.Filled"
OnClick="SaveEditRole"
StartIcon="@Icons.Material.Filled.Save">
Lưu
</MudButton>
<MudButton Variant="Variant.Text"
OnClick="@(() => EditRoleVisible = false)">
Hủy
</MudButton>
</DialogActions>
</MudDialog>
<MudDialog @bind-Visible="DelRoleVisible">
<TitleContent>
<div class="d-flex align-center">
<MudIcon Icon="@Icons.Material.Filled.Warning" Color="Color.Error" Class="mr-3" />
<MudText Typo="Typo.h6">Xác Nhận Xóa</MudText>
</div>
</TitleContent>
<DialogContent>
<MudAlert Severity="Severity.Warning" Class="mb-3">
Bạn có chắc chắn muốn xóa vai trò <strong>@RoleNameToDelete</strong>?
</MudAlert>
<MudText Typo="Typo.body2">Hành động này không thể hoàn tác.</MudText>
</DialogContent>
<DialogActions>
<MudButton Color="Color.Error"
Variant="Variant.Filled"
OnClick="ConfirmDelRole"
StartIcon="@Icons.Material.Filled.Delete">
Xóa
</MudButton>
<MudButton Variant="Variant.Text"
OnClick="@(() => DelRoleVisible = false)">
Hủy
</MudButton>
</DialogActions>
</MudDialog>
<MudDialog @bind-Visible="ManageUserRolesVisible">
<TitleContent>
<div class="d-flex align-center">
<MudIcon Icon="@Icons.Material.Filled.ManageAccounts" Class="mr-3" />
<MudText Typo="Typo.h6">Quản Lý Vai Trò: @SelectedUserName</MudText>
</div>
</TitleContent>
<DialogContent>
<MudGrid>
<MudItem xs="12">
<MudPaper Class="pa-4" Style="background: #f8fafc; border-radius: 12px;">
<MudText Typo="Typo.subtitle1" Class="mb-3">
<MudIcon Icon="@Icons.Material.Filled.Badge" Size="Size.Small" Class="mr-2" />
Vai Trò Hiện Tại
</MudText>
<div class="d-flex flex-wrap">
@if (AssignedRoles.Any())
{
foreach (var role in AssignedRoles)
{
if (role.Equals("Administrator", StringComparison.OrdinalIgnoreCase))
{
<MudChip T="string"
Color="Color.Warning"
Variant="Variant.Filled"
Icon="@Icons.Material.Filled.Lock"
Class="ma-1">
@role (Được bảo vệ)
</MudChip>
}
else
{
<MudChip T="string"
Color="@GetRoleChipColor(role)"
Variant="Variant.Filled"
Icon="@GetRoleIcon(role)"
Class="ma-1">
@role
</MudChip>
}
}
}
else
{
<MudText Typo="Typo.body2" Color="Color.Default">
Chưa có vai trò nào
</MudText>
}
</div>
</MudPaper>
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Class="pa-4" Style="border: 2px dashed #e2e8f0; border-radius: 12px;">
<MudText Typo="Typo.subtitle1" Class="mb-3" Color="Color.Success">
<MudIcon Icon="@Icons.Material.Filled.Add" Size="Size.Small" Class="mr-2" />
Thêm Vai Trò
</MudText>
<MudSelect T="string"
Label="Chọn vai trò để thêm"
MultiSelection="true"
@bind-SelectedValues="selectedRolesToAdd"
Variant="Variant.Outlined"
Clearable="true"
Dense="true"
MaxHeight="200"
FullWidth="true">
@foreach (var role in AvailableRoles)
{
<MudSelectItem T="string" Value="@role">
<div class="d-flex align-center">
<MudIcon Icon="@GetRoleIcon(role)" Size="Size.Small" Class="mr-2" />
@role
</div>
</MudSelectItem>
}
</MudSelect>
<MudButton Color="Color.Success"
Variant="Variant.Filled"
StartIcon="@Icons.Material.Filled.Add"
OnClick="AddSelectedRolesToUser"
Disabled="@(!selectedRolesToAdd.Any())"
Class="mt-3"
FullWidth="true">
Thêm (@selectedRolesToAdd.Count())
</MudButton>
</MudPaper>
</MudItem>
@{
var removableRoles = AssignedRoles
.Where(role => !role.Equals("Administrator", StringComparison.OrdinalIgnoreCase))
.ToList();
}
@if (removableRoles.Any())
{
<MudItem xs="12" md="6">
<MudPaper Class="pa-4" Style="border: 2px dashed #fed7d4; border-radius: 12px;">
<MudText Typo="Typo.subtitle1" Class="mb-3" Color="Color.Error">
<MudIcon Icon="@Icons.Material.Filled.Remove" Size="Size.Small" Class="mr-2" />
Xóa Vai Trò
</MudText>
<MudSelect T="string"
Label="Chọn vai trò để xóa"
MultiSelection="true"
@bind-SelectedValues="selectedRolesToRemove"
Variant="Variant.Outlined"
Clearable="true"
Dense="true"
MaxHeight="200"
FullWidth="true">
@foreach (var role in removableRoles)
{
<MudSelectItem T="string" Value="@role">
<div class="d-flex align-center">
<MudIcon Icon="@GetRoleIcon(role)" Size="Size.Small" Class="mr-2" />
@role
</div>
</MudSelectItem>
}
</MudSelect>
<MudButton Color="Color.Error"
Variant="Variant.Filled"
StartIcon="@Icons.Material.Filled.Remove"
OnClick="RemoveSelectedRolesFromUser"
Disabled="@(!selectedRolesToRemove.Any())"
Class="mt-3"
FullWidth="true">
Xóa (@selectedRolesToRemove.Count())
</MudButton>
</MudPaper>
</MudItem>
}
</MudGrid>
</DialogContent>
<DialogActions>
<MudButton Variant="Variant.Text"
OnClick="@(() => ManageUserRolesVisible = false)">
Đóng
</MudButton>
</DialogActions>
</MudDialog>
@code {
private bool CreateRoleVisible { get; set; }
private bool DelRoleVisible { get; set; }
private bool EditRoleVisible { get; set; }
private bool ManageUserRolesVisible { get; set; } = false;
private string CurrentRoleId { get; set; } = "";
private string EditRoleName { get; set; } = "";
private string NewRoleName { get; set; } = "";
private string RoleNameToDelete { get; set; } = "";
private string RoleIdToDelete { get; set; } = "";
private string SelectedUserName { get; set; } = string.Empty;
private string UserIdToManageRoles { get; set; } = string.Empty;
private string LoggedInUserId { get; set; } = string.Empty;
private List<string> LoggedInUserRoles { get; set; } = new();
private List<string> AllRoles { get; set; } = new List<string>();
private List<string> AvailableRoles { get; set; } = new List<string>();
private List<string> AssignedRoles { get; set; } = new List<string>();
private List<ApplicationRole> Roles { get; set; } = new List<ApplicationRole>();
private List<UserRoleModel> UsersWithRoles { get; set; } = new List<UserRoleModel>();
private IEnumerable<string> selectedRolesToAdd = new HashSet<string>();
private IEnumerable<string> selectedRolesToRemove = new HashSet<string>();
private string rowsPerPageString = "Rows:";
private string roleSearchTerm = "";
private string userSearchTerm = "";
private IEnumerable<ApplicationRole> FilteredRoles =>
string.IsNullOrWhiteSpace(roleSearchTerm)
? Roles
: Roles?.Where(r => r.Name != null && r.Name.Contains(roleSearchTerm, StringComparison.OrdinalIgnoreCase)) ?? Enumerable.Empty<ApplicationRole>();
private IEnumerable<UserRoleModel> FilteredUsers =>
string.IsNullOrWhiteSpace(userSearchTerm)
? UsersWithRoles
: UsersWithRoles?.Where(u => u.UserName != null && u.UserName.Contains(userSearchTerm, StringComparison.OrdinalIgnoreCase)) ?? Enumerable.Empty<UserRoleModel>();
private string GetRoleIcon(string roleName)
{
return roleName?.ToLower() switch
{
"administrator" => Icons.Material.Filled.SupervisorAccount,
"user" => Icons.Material.Filled.Person,
"guest" => Icons.Material.Filled.PersonOutline,
_ => Icons.Material.Filled.Security
};
}
private Color GetRoleColor(string roleName)
{
return roleName?.ToLower() switch
{
"administrator" => Color.Error,
"user" => Color.Primary,
"guest" => Color.Default,
_ => Color.Info
};
}
private Color GetRoleChipColor(string roleName)
{
return roleName?.ToLower() switch
{
"administrator" => Color.Error,
"user" => Color.Primary,
"guest" => Color.Default,
_ => Color.Info
};
}
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
var authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var currentUser = authenticationState.User;
if (currentUser.Identity?.IsAuthenticated == true)
{
LoggedInUserId = currentUser.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? string.Empty;
var currentUserObj = await UserManager.GetUserAsync(currentUser);
if (currentUserObj != null)
{
LoggedInUserRoles = (await UserManager.GetRolesAsync(currentUserObj)).ToList();
}
Roles = await RoleManager.Roles.OrderBy(r => r.CreatedDate).ToListAsync();
await LoadAllRoles();
await LoadUsersWithRoles();
StateHasChanged();
}
else
{
Snackbar.Add("User is not authenticated.", Severity.Error);
}
}
private async Task LoadUsersWithRoles()
{
try
{
var users = await UserManager.Users.ToListAsync();
if (users == null || !users.Any())
{
Snackbar.Add("No users found.", Severity.Error);
return;
}
var userRoleList = new List<UserRoleModel>();
foreach (var user in users)
{
var userRoles = await UserManager.GetRolesAsync(user);
userRoleList.Add(new UserRoleModel
{
UserName = user.UserName,
Roles = userRoles.ToList(),
UserId = user.Id
});
}
UsersWithRoles = userRoleList.OrderBy(u => u.UserId == LoggedInUserId ? 0 : 1).ToList();
StateHasChanged();
}
catch (Exception ex)
{
Snackbar.Add($"Lỗi khi tải người dùng và role: {ex.Message}", Severity.Error);
UsersWithRoles = new List<UserRoleModel>();
}
}
private async Task LoadAllRoles()
{
try
{
var roles = await RoleManager.Roles
.Select(role => role.Name)
.ToListAsync();
AllRoles = roles.Where(name => !string.IsNullOrEmpty(name)).Cast<string>().ToList() ?? new List<string>();
if (!AllRoles.Any())
{
Snackbar.Add("Không tìm thấy vai trò nào.", Severity.Warning);
}
}
catch (Exception ex)
{
Snackbar.Add($"Lỗi khi tải danh sách vai trò: {ex.Message}", Severity.Error);
AllRoles = new List<string>();
}
}
private void AddRole()
{
CreateRoleVisible = true;
StateHasChanged();
}
private void DelRole(string roleId, string roleName)
{
RoleIdToDelete = roleId;
RoleNameToDelete = roleName;
DelRoleVisible = true;
StateHasChanged();
}
private void EditRole(ApplicationRole role)
{
if (role?.Name is not null)
{
CurrentRoleId = role.Id;
EditRoleName = role.Name;
EditRoleVisible = true;
StateHasChanged();
}
else
{
Snackbar.Add("Role information is incomplete or invalid.", Severity.Error);
}
}
private async Task ManageUserRoles(string userId, string userName)
{
if (!LoggedInUserRoles.Contains("Administrator"))
{
Snackbar.Add("Bạn không có quyền quản lý role của user khác.", Severity.Error);
return;
}
if (!AllRoles.Any())
{
Snackbar.Add("Không có vai trò nào có thể chỉ định", Severity.Warning);
return;
}
SelectedUserName = userName;
UserIdToManageRoles = userId;
var user = await UserManager.FindByIdAsync(userId);
if (user != null)
{
var userRoles = await UserManager.GetRolesAsync(user);
AssignedRoles = userRoles.ToList();
AvailableRoles = AllRoles
.Except(userRoles)
.Where(role => !role.Equals("Administrator", StringComparison.OrdinalIgnoreCase))
.ToList();
}
selectedRolesToAdd = new HashSet<string>();
selectedRolesToRemove = new HashSet<string>();
ManageUserRolesVisible = true;
StateHasChanged();
}
private async Task AddSelectedRolesToUser()
{
foreach (var role in selectedRolesToAdd.ToList())
{
await AddRoleToSelectedUser(role);
}
selectedRolesToAdd = new HashSet<string>();
StateHasChanged();
}
private async Task RemoveSelectedRolesFromUser()
{
foreach (var role in selectedRolesToRemove.ToList())
{
await RemoveRoleFromSelectedUser(role);
}
selectedRolesToRemove = new HashSet<string>();
StateHasChanged();
}
private async Task AddRoleToSelectedUser(string roleName)
{
if (roleName.Equals("Administrator", StringComparison.OrdinalIgnoreCase))
{
Snackbar.Add("Không thể gán role Admin cho user khác.", Severity.Error);
return;
}
if (!LoggedInUserRoles.Contains("Administrator"))
{
Snackbar.Add("Bạn không có quyền thực hiện thao tác này.", Severity.Error);
return;
}
var user = await UserManager.FindByIdAsync(UserIdToManageRoles);
if (user != null)
{
var result = await UserManager.AddToRoleAsync(user, roleName);
if (result.Succeeded)
{
AssignedRoles.Add(roleName);
AvailableRoles.Remove(roleName);
selectedRolesToAdd = selectedRolesToAdd.Where(r => r != roleName);
Snackbar.Add($"Thêm role '{roleName}' cho {SelectedUserName} Thành Công.", Severity.Success);
await LoadUsersWithRoles();
StateHasChanged();
}
else
{
Snackbar.Add($"Failed to add role '{roleName}' to user.", Severity.Error);
}
}
}
private async Task RemoveRoleFromSelectedUser(string roleName)
{
if (roleName.Equals("Administrator", StringComparison.OrdinalIgnoreCase))
{
Snackbar.Add("Không thể xóa role Admin của user.", Severity.Error);
return;
}
if (!LoggedInUserRoles.Contains("Administrator"))
{
Snackbar.Add("Bạn không có quyền thực hiện thao tác này.", Severity.Error);
return;
}
var user = await UserManager.FindByIdAsync(UserIdToManageRoles);
if (user != null)
{
var result = await UserManager.RemoveFromRoleAsync(user, roleName);
if (result.Succeeded)
{
AssignedRoles.Remove(roleName);
AvailableRoles.Add(roleName);
selectedRolesToRemove = selectedRolesToRemove.Where(r => r != roleName);
Snackbar.Add($"Thành công xoá role '{roleName}' của {SelectedUserName}.", Severity.Success);
await LoadUsersWithRoles();
StateHasChanged();
}
else
{
Snackbar.Add($"Failed to remove role '{roleName}' from user.", Severity.Error);
}
}
}
private async Task CreateRole()
{
if (string.IsNullOrWhiteSpace(NewRoleName))
{
Snackbar.Add(" Tên Role không được để trống.", Severity.Error);
StateHasChanged();
return;
}
var roleExist = await RoleManager.RoleExistsAsync(NewRoleName.ToUpper());
if (roleExist)
{
Snackbar.Add(" Role đã tồn tại.", Severity.Warning);
CreateRoleVisible = false;
NewRoleName = "";
StateHasChanged();
return;
}
var newRole = new ApplicationRole
{
Name = NewRoleName,
NormalizedName = NewRoleName.ToUpper(),
CreatedDate = DateTime.UtcNow
};
var result = await RoleManager.CreateAsync(newRole);
if (result.Succeeded)
{
Roles.Add(newRole);
Snackbar.Add(" Tạo Role thành công!", Severity.Success);
await LoadAllRoles();
CreateRoleVisible = false;
NewRoleName = "";
StateHasChanged();
}
else
{
Snackbar.Add(" Tạo Role thất bại.", Severity.Error);
StateHasChanged();
}
}
private async Task SaveEditRole()
{
if (string.IsNullOrWhiteSpace(EditRoleName))
{
Snackbar.Add("Tên Role không được để trống.", Severity.Error);
StateHasChanged();
return;
}
var role = await RoleManager.FindByIdAsync(CurrentRoleId);
if (role != null)
{
role.Name = EditRoleName;
role.NormalizedName = EditRoleName.ToUpper();
var result = await RoleManager.UpdateAsync(role);
if (result.Succeeded)
{
var existingRole = Roles.FirstOrDefault(r => r.Id == role.Id);
if (existingRole != null)
{
existingRole.Name = role.Name;
}
Snackbar.Add("Role đã được sửa thành công!", Severity.Success);
await LoadAllRoles();
await LoadUsersWithRoles();
EditRoleVisible = false;
EditRoleName = "";
StateHasChanged();
}
else
{
Snackbar.Add("Sửa Role thất bại.", Severity.Error);
EditRoleVisible = false;
StateHasChanged();
}
}
else
{
Snackbar.Add("Không tìm thấy role với ID đã cho.", Severity.Error);
EditRoleVisible = false;
StateHasChanged();
}
}
private async Task ConfirmDelRole()
{
if (string.IsNullOrEmpty(RoleIdToDelete))
{
Snackbar.Add(" Không tìm thấy Role để xóa.", Severity.Error);
return;
}
var role = await RoleManager.FindByIdAsync(RoleIdToDelete);
if (role != null)
{
var result = await RoleManager.DeleteAsync(role);
if (result.Succeeded)
{
Snackbar.Add(" Đã xóa Role thành công.", Severity.Success);
Roles = await RoleManager.Roles
.OrderBy(r => r.CreatedDate)
.ToListAsync();
await LoadAllRoles();
await LoadUsersWithRoles();
}
else
{
Snackbar.Add(" Xóa Role thất bại.", Severity.Error);
}
}
else
{
Snackbar.Add(" Không tìm thấy Role để xóa.", Severity.Error);
}
DelRoleVisible = false;
RoleIdToDelete = "";
StateHasChanged();
}
public class UserRoleModel
{
public string? UserName { get; set; }
public List<string> Roles { get; set; } = new List<string>();
public string? UserId { get; set; }
}
}

View File

@ -0,0 +1,87 @@
.mdi {
display: inline-block;
position: relative;
background-size: cover;
align-items:center;
}
.pa-4{
overflow:hidden;
}
.role-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
}
.modern-card {
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
border: 1px solid rgba(0,0,0,0.06);
transition: all 0.3s ease;
}
.modern-card:hover {
box-shadow: 0 8px 40px rgba(0,0,0,0.12);
transform: translateY(-2px);
}
.section-title {
font-weight: 600;
color: #2d3748;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.stats-card {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
border-radius: 12px;
padding: 20px;
text-align: center;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
margin-right: 12px;
}
.role-chip {
margin: 2px;
font-size: 12px;
}
.action-button {
border-radius: 8px;
transition: all 0.2s ease;
}
.action-button:hover {
transform: scale(1.05);
}
.table-container {
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
}
.search-container {
background: #f8fafc;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}

View File

@ -0,0 +1,17 @@
@page "/Account/Usermanager"
@rendermode InteractiveServer
<MudTabs Elevation="2" Rounded="true" Style="height:auto; max-height:100%; overflow:hidden">
<MudTabPanel Icon="@Icons.Material.Filled.RamenDining">
<Infor/>
</MudTabPanel>
<MudTabPanel Icon="@Icons.Material.Filled.Build" >
<Password/>
</MudTabPanel>
</MudTabs>
@code {
}

View File

@ -0,0 +1,73 @@
.mdi {
display: inline-flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.user-manager-container {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background-attachment: fixed;
}
.header-section {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.modern-tabs {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
padding: 24px;
border: 1px solid rgba(255, 255, 255, 0.3);
}
.modern-tabs .mud-tabs-toolbar {
background: transparent;
border-radius: 12px;
padding: 8px;
margin-bottom: 24px;
background: rgba(21, 101, 192, 0.05);
}
.modern-tabs .mud-tab {
border-radius: 8px;
margin: 0 4px;
transition: all 0.3s ease;
font-weight: 500;
}
.modern-tabs .mud-tab:hover {
background: rgba(21, 101, 192, 0.1);
transform: translateY(-2px);
}
.modern-tabs .mud-tab.mud-tab-active {
background: rgba(21, 101, 192, 0.15);
color: #1565C0;
font-weight: 600;
}
.tab-content {
animation: fadeInUp 0.5s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.tab-panel {
padding: 0 !important;
}

View File

@ -0,0 +1,2 @@
@using RobotNet.IdentityServer.Components.Account.Shared
@attribute [ExcludeFromInteractiveRouting]

View File

@ -0,0 +1,8 @@
@inject NavigationManager NavigationManager
@code {
protected override void OnInitialized()
{
NavigationManager.NavigateTo($"Account/Login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}", forceLoad: true);
}
}

View File

@ -0,0 +1,30 @@
<!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="icon" type="image/svg+xml" href="favicon.svg" />
<link rel="stylesheet" href="@Assets["mud/fonts.googleapis.com.css"]" />
<link rel="stylesheet" href="_content/MudBlazor/MudBlazor.min.css" />
<link rel="stylesheet" href="@Assets["lib/bootstrap/css/bootstrap.min.css"]" />
<link rel="stylesheet" href="@Assets["lib/mdi/font/css/materialdesignicons.min.css"]" />
<link rel="stylesheet" href="@Assets["app.css"]" />
<link rel="stylesheet" href="@Assets["RobotNet.IdentityServer.styles.css"]" />
<ImportMap @rendermode="InteractiveServer" />
<HeadOutlet @rendermode="InteractiveServer" />
</head>
<body >
<Routes />
<script src="_framework/blazor.web.js"></script>
<script src="@Assets["lib/bootstrap/js/bootstrap.bundle.min.js"]"></script>
<script src="@Assets["lib/bootstrap/js/bootstrap.min.js"]"></script>
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,29 @@
@using MudBlazor
@inherits LayoutComponentBase
@inject NavigationManager NavigationManager
<MudThemeProvider />
<MudPopoverProvider @rendermode="InteractiveServer" />
<div class="page">
<div class="sidebar-container">
<div class="sidebar">
<NavMenu />
</div>
</div>
<main>
<article class="content px-2">
@Body
</article>
</main>
</div>
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>

View File

@ -0,0 +1,123 @@
.page {
position: relative;
display: flex;
flex-direction: column;
background-color: #f7f7f7;
min-height: 100vh;
}
.content px-2{
overflow:hidden;
}
main {
flex: 1;
padding-left: 1rem;
}
.sidebar-container {
position: relative;
z-index: 10;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
border-radius: 15px;
box-shadow: 4px 4px 12px rgba(0, 0, 0, 0.3);
transform: translateX(5px);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
.sidebar {
margin-left: 0;
width: 80%;
max-width: 250px;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar-container {
width: 280px;
height: 100vh;
}
.sidebar {
width: 270px;
height: calc(100vh - 20px);
position: fixed;
top:10px;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 0.75rem !important;
padding-right: 0.75rem !important;
}
}
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}

View File

@ -0,0 +1,214 @@
@using Microsoft.AspNetCore.Identity
@using RobotNet.IdentityServer.Data
@using MudBlazor
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components
@using Microsoft.EntityFrameworkCore
@using System.Threading
@using RobotNet.IdentityServer.Services
@using System.Security.Claims
@implements IDisposable
@inherits LayoutComponentBase
@inject RobotNet.IdentityServer.Services.IdentityService IdentityService
@inject RobotNet.IdentityServer.Services.UserInfoService UserInfoService
@inject NavigationManager NavigationManager
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject RoleManager<ApplicationRole> RoleManager
<div class="top-row px-4 py-3 bg-gradient-to-r from-blue-400 to-purple-500 flex items-center">
<a class="navbar-brand text-white font-bold text-lg" href="">User Management</a>
<button class="navbar-toggler md:hidden">
<span class="mdi mdi-menu text-white text-2xl"></span>
</button>
</div>
<div class="nav-scrollable">
<nav class="nav flex-column p-2 space-y-2">
<AuthorizeView>
<Authorized>
<div class="nav-item">
<NavLink class="nav-link flex items-center space-x-3 text-gray-200 hover:bg-blue-500/20 rounded-lg p-2 transition-all duration-300" href="Account/Usermanager">
<span class="mdi mdi-account-cog text-xl"></span>
<span class="text-nav">User Info Management</span>
</NavLink>
</div>
</Authorized>
</AuthorizeView>
<AuthorizeView Roles="Administrator">
<Authorized>
<div class="nav-item">
<NavLink class="nav-link flex items-center space-x-3 text-gray-200 hover:bg-blue-500/20 rounded-lg p-2 transition-all duration-300" href="Account/Rolemanager">
<span class="mdi mdi-account-cog text-xl"></span>
<span class="text-nav">Role Manager</span>
</NavLink>
</div>
<div class="nav-item">
<NavLink class="nav-link flex items-center space-x-3 text-gray-200 hover:bg-blue-500/20 rounded-lg p-2 transition-all duration-300" href="/Account/OpenIdDictManager">
<span class="mdi mdi-database-import-outline text-xl"></span>
<span class="text-nav">OpenIdDict</span>
</NavLink>
</div>
</Authorized>
</AuthorizeView>
<AuthorizeView>
<NotAuthorized>
<div class="nav-item">
<NavLink class="nav-link flex items-center space-x-3 text-gray-200 hover:bg-blue-500/20 rounded-lg p-2 transition-all duration-300" href="Account/Login">
<span class="mdi mdi-account-arrow-right text-xl"></span>
<span class="text-nav">Login</span>
</NavLink>
</div>
<div class="nav-item">
<NavLink class="nav-link flex items-center space-x-3 text-gray-200 hover:bg-blue-500/20 rounded-lg p-2 transition-all duration-300" href="Account/Register">
<span class="mdi mdi-account-plus text-xl"></span>
<span class="text-nav">Register</span>
</NavLink>
</div>
</NotAuthorized>
</AuthorizeView>
</nav>
<AuthorizeView>
<Authorized>
<div class="user-profile mt-auto">
<div class="user-profile-inner">
<div class="avatar">
@if (string.IsNullOrEmpty(userImageUrl))
{
<div class="avatar-placeholder">
<span class="mdi mdi-account text-3xl"></span>
</div>
}
else
{
<img src="@userImageUrl" alt="User Avatar" class="avatar-image" />
}
</div>
<div class="user-info">
<div class="username">@userName</div>
<div class="user-email">@userEmail</div>
</div>
<form action="Account/Logout" method="post" class="logout-form">
<AntiforgeryToken />
<input type="hidden" name="ReturnUrl" value="@currentUrl" />
<button type="submit" class="logout-button" title="Logout">
<span class=" mdi mdi-account-arrow-left text-xl"></span>
</button>
</form>
</div>
</div>
</Authorized>
</AuthorizeView>
</div>
@code {
private Func<Task>? _userInfoChangedHandler;
private string cacheBuster = "";
private string? currentUrl;
private ApplicationUser? currentUser;
@inject UserManager<ApplicationUser> UserManager
@inject AuthenticationStateProvider AuthenticationStateProvider
private string userName = string.Empty;
private string userEmail = string.Empty;
private string userImageUrl = string.Empty;
protected override async Task OnInitializedAsync()
{
currentUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
NavigationManager.LocationChanged += OnLocationChanged;
_userInfoChangedHandler = async () =>
{
await InvokeAsync(async () =>
{
await LoadUserInfoAsync();
StateHasChanged();
});
};
UserInfoService.RegisterHandler(_userInfoChangedHandler);
await LoadUserInfoAsync();
}
private async Task UserInfoChangedHandler()
{
await LoadUserInfoAsync();
await InvokeAsync(() =>
{
cacheBuster = $"?v={DateTime.Now.Ticks}";
StateHasChanged();
});
}
private async Task LoadUserInfoAsync()
{
try
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
if (user?.Identity?.IsAuthenticated == true)
{
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
if (!string.IsNullOrEmpty(userId))
{
currentUser = await IdentityService.GetUserByIdAsync(userId);
if (currentUser != null)
{
userName = currentUser.UserName ?? string.Empty; ;
userEmail = currentUser.Email ?? string.Empty; ;
if (currentUser.AvatarImage != null)
{
userImageUrl = $"data:{currentUser.AvatarContentType ?? "image/jpeg"};base64,{Convert.ToBase64String(currentUser.AvatarImage)}";
}
else
{
userImageUrl = "/uploads/avatars/anh.jpg";
}
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error loading user info: {ex.Message}");
}
}
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
{
InvokeAsync(() =>
{
currentUrl = NavigationManager.ToBaseRelativePath(e.Location);
StateHasChanged();
return Task.CompletedTask;
});
}
public void Dispose()
{
NavigationManager.LocationChanged -= OnLocationChanged;
if (UserInfoService != null && _userInfoChangedHandler != null)
{
UserInfoService.UnregisterHandler(_userInfoChangedHandler);
}
}
}

View File

@ -0,0 +1,202 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: turquoise;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
min-height: 3.5rem;
background-color: rgba(0,0,0,0.1);
border-radius: 15px 15px 0 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.navbar-brand {
font-size: 1.1rem;
}
.mdi {
display: inline-block;
position: relative;
font-size: 26px;
background-size: cover;
}
.nav-item {
font-size: 1.05rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 0.5rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #4a5568;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
transition: all 0.2s ease;
}
.nav-item ::deep a.active {
background-color: rgba(79, 70, 229, 0.2);
color: #4338ca;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(79, 70, 229, 0.1);
color: #4338ca;
}
.text-nav {
margin-left: 1rem;
}
.nav-scrollable {
display: none;
display: flex;
flex-direction: column;
height: calc(100% - 3.5rem);
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
.user-profile {
margin-top: auto;
padding: 1rem;
border-top: 1px solid rgba(0, 0, 0, 0.1);
background-color: #e8f0fe;
border-radius: 0 0 15px 15px;
}
.user-profile-inner {
display: flex;
align-items: center;
padding: 0.5rem;
position: relative;
}
.avatar {
width: 40px;
height: 40px;
min-width: 40px;
min-height: 40px;
max-width: 40px;
max-height: 40px;
border-radius: 50%;
overflow: hidden;
margin-right: 0.75rem;
background-color: rgba(79, 70, 229, 0.1);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
flex-shrink: 0;
}
.avatar-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #4a5568;
}
.avatar-image {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.user-info {
display: flex;
flex-direction: column;
justify-content: center;
flex: 1;
min-width: 0;
}
.username {
color: #4a5568;
font-weight: 500;
font-size: 0.9rem;
line-height: 1.2;
}
.user-email {
color: #718096;
font-size: 0.75rem;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 150px;
}
.logout-form {
margin-left: auto;
}
.logout-button {
background: none;
border: none;
color: #718096;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
}
.logout-button:hover {
background-color: rgba(79, 70, 229, 0.1);
color: #4338ca;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: flex;
flex-direction: column;
/* Allow sidebar to scroll for tall menus */
height: calc(100% - 3.5rem);
overflow-y: auto;
border-radius: 0 0 15px 15px;
background: #e8f0fe;
}
}

View File

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

View File

@ -0,0 +1,27 @@
@page "/"
@using Microsoft.AspNetCore.Authorization
@rendermode InteractiveServer
@attribute [Authorize]
<PageTitle>Home</PageTitle>
<MudDialogProvider />
<MudSnackbarProvider />
<h1>Hello</h1>
<AuthorizeView>
<NotAuthorized>
Vui lòng đăng nhập
</NotAuthorized>
<Authorized>
Hello @context.User.Identity?.Name!
</Authorized>
</AuthorizeView>
@code {
}

View File

@ -0,0 +1,12 @@
@using RobotNet.IdentityServer.Components.Account.Shared
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
<NotAuthorized>
<RedirectToLogin />
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>

View File

@ -0,0 +1,14 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Authorization
@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.JSInterop
@using MudBlazor
@using RobotNet.IdentityServer
@using RobotNet.IdentityServer.Components
@using System.ComponentModel.DataAnnotations

View File

@ -0,0 +1,402 @@
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using RobotNet.IdentityServer.Data;
using RobotNet.IdentityServer.Helpers;
using System.Security.Claims;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace RobotNet.IdentityServer.Controllers;
[EnableCors("RequestAuthorize")]
[Route("api/[controller]")]
[ApiController]
public class AuthorizationController(
IOpenIddictApplicationManager applicationManager,
IOpenIddictAuthorizationManager authorizationManager,
IOpenIddictScopeManager scopeManager,
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager) : ControllerBase
{
[HttpGet("connect/authorize")]
[HttpPost("connect/authorize")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Try to retrieve the user principal stored in the authentication cookie and redirect
// the user agent to the login page (or to an external provider) in the following cases:
//
// - If the user principal can't be extracted or the cookie is too old.
// - If prompt=login was specified by the client application.
// - If a max_age parameter was provided and the authentication cookie is not considered "fresh" enough.
//
// For scenarios where the default authentication handler configured in the ASP.NET Core
// authentication options shouldn't be used, a specific scheme can be specified here.
var result = await HttpContext.AuthenticateAsync();
if (result == null || !result.Succeeded || request.HasPromptValue(PromptValues.Login) ||
(request.MaxAge != null && result.Properties?.IssuedUtc != null &&
DateTimeOffset.UtcNow - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value)))
{
// If the client application requested promptless authentication,
// return an error indicating that the user is not logged in.
if (request.HasPromptValue(PromptValues.None))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
}));
}
// To avoid endless login -> authorization redirects, the prompt=login flag
// is removed from the authorization request payload before redirecting the user.
var prompt = string.Join(" ", request.GetPromptValues().Remove(PromptValues.Login));
var parameters = Request.HasFormContentType ?
Request.Form.Where(parameter => parameter.Key != Parameters.Prompt).ToList() :
Request.Query.Where(parameter => parameter.Key != Parameters.Prompt).ToList();
parameters.Add(KeyValuePair.Create(Parameters.Prompt, new StringValues(prompt)));
// For scenarios where the default challenge handler configured in the ASP.NET Core
// authentication options shouldn't be used, a specific scheme can be specified here.
return Challenge(new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters)
});
}
// Retrieve the profile of the logged in user.
var user = await userManager.GetUserAsync(result.Principal) ??
throw new InvalidOperationException("The user details cannot be retrieved.");
// Retrieve the application details from the database.
var application = await applicationManager.FindByClientIdAsync(request.ClientId ?? "") ??
throw new InvalidOperationException("Details concerning the calling client application cannot be found.");
// Retrieve the permanent authorizations associated with the user and the calling client application.
var authorizations = await authorizationManager.FindAsync(
subject: await userManager.GetUserIdAsync(user),
client: await applicationManager.GetIdAsync(application),
status: Statuses.Valid,
type: AuthorizationTypes.Permanent,
scopes: request.GetScopes()).ToListAsync();
switch (await applicationManager.GetConsentTypeAsync(application))
{
// If the consent is external (e.g when authorizations are granted by a sysadmin),
// immediately return an error if no authorization can be found in the database.
case ConsentTypes.External when authorizations.Count is 0:
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"The logged in user is not allowed to access this client application."
}));
// If the consent is implicit or if an authorization was found,
// return an authorization response without displaying the consent form.
case ConsentTypes.Implicit:
case ConsentTypes.External when authorizations.Count is not 0:
case ConsentTypes.Explicit when authorizations.Count is not 0 && !request.HasPromptValue(PromptValues.Consent):
// Create the claims-based identity that will be used by OpenIddict to generate tokens.
var identity = new ClaimsIdentity(
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
nameType: Claims.Name,
roleType: Claims.Role);
// Add the claims that will be persisted in the tokens.
identity.SetClaim(Claims.Subject, await userManager.GetUserIdAsync(user))
.SetClaim(Claims.Email, await userManager.GetEmailAsync(user))
.SetClaim(Claims.Name, await userManager.GetUserNameAsync(user))
.SetClaim(Claims.PreferredUsername, await userManager.GetUserNameAsync(user))
.SetClaims(Claims.Role, [.. (await userManager.GetRolesAsync(user))]);
// Note: in this sample, the granted scopes match the requested scope
// but you may want to allow the user to uncheck specific scopes.
// For that, simply restrict the list of scopes before calling SetScopes.
identity.SetScopes(request.GetScopes());
identity.SetResources(await scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
// Automatically create a permanent authorization to avoid requiring explicit consent
// for future authorization or token requests containing the same scopes.
var authorization = authorizations.LastOrDefault();
authorization ??= await authorizationManager.CreateAsync(
identity: identity,
subject: await userManager.GetUserIdAsync(user),
client: await applicationManager.GetIdAsync(application) ?? "",
type: AuthorizationTypes.Permanent,
scopes: identity.GetScopes());
identity.SetAuthorizationId(await authorizationManager.GetIdAsync(authorization));
identity.SetDestinations(GetDestinations);
return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
// At this point, no authorization was found in the database and an error must be returned
// if the client application specified prompt=none in the authorization request.
case ConsentTypes.Explicit when request.HasPromptValue(PromptValues.None):
case ConsentTypes.Systematic when request.HasPromptValue(PromptValues.None):
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"Interactive user consent is required."
}));
// In every other case, render the consent form.
default:
return Redirect($"/Account/Login/Access{Request.QueryString}&request_app={await applicationManager.GetLocalizedDisplayNameAsync(application)}&request_scope={request.Scope}");
}
}
[Authorize, FormValueRequired("submit.Accept")]
[HttpPost("connect/authorize"), ValidateAntiForgeryToken]
public async Task<IActionResult> Accept()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Retrieve the profile of the logged in user.
var user = await userManager.GetUserAsync(User) ??
throw new InvalidOperationException("The user details cannot be retrieved.");
// Retrieve the application details from the database.
var application = await applicationManager.FindByClientIdAsync(request.ClientId ?? "") ??
throw new InvalidOperationException("Details concerning the calling client application cannot be found.");
// Retrieve the permanent authorizations associated with the user and the calling client application.
var authorizations = await authorizationManager.FindAsync(
subject: await userManager.GetUserIdAsync(user),
client: await applicationManager.GetIdAsync(application),
status: Statuses.Valid,
type: AuthorizationTypes.Permanent,
scopes: request.GetScopes()).ToListAsync();
// Note: the same check is already made in the other action but is repeated
// here to ensure a malicious user can't abuse this POST-only endpoint and
// force it to return a valid response without the external authorization.
if (authorizations.Count is 0 && await applicationManager.HasConsentTypeAsync(application, ConsentTypes.External))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"The logged in user is not allowed to access this client application."
}));
}
// Create the claims-based identity that will be used by OpenIddict to generate tokens.
var identity = new ClaimsIdentity(
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
nameType: Claims.Name,
roleType: Claims.Role);
// Add the claims that will be persisted in the tokens.
identity.SetClaim(Claims.Subject, await userManager.GetUserIdAsync(user))
.SetClaim(Claims.Email, await userManager.GetEmailAsync(user))
.SetClaim(Claims.Name, await userManager.GetUserNameAsync(user))
.SetClaim(Claims.PreferredUsername, await userManager.GetUserNameAsync(user))
.SetClaims(Claims.Role, [.. (await userManager.GetRolesAsync(user))]);
// Note: in this sample, the granted scopes match the requested scope
// but you may want to allow the user to uncheck specific scopes.
// For that, simply restrict the list of scopes before calling SetScopes.
identity.SetScopes(request.GetScopes());
identity.SetResources(await scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
// Automatically create a permanent authorization to avoid requiring explicit consent
// for future authorization or token requests containing the same scopes.
var authorization = authorizations.LastOrDefault();
authorization ??= await authorizationManager.CreateAsync(
identity: identity,
subject: await userManager.GetUserIdAsync(user),
client: await applicationManager.GetIdAsync(application) ?? "",
type: AuthorizationTypes.Permanent,
scopes: identity.GetScopes());
identity.SetAuthorizationId(await authorizationManager.GetIdAsync(authorization));
identity.SetDestinations(GetDestinations);
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
[Authorize, FormValueRequired("submit.Deny")]
[HttpPost("connect/authorize"), ValidateAntiForgeryToken]
// Notify OpenIddict that the authorization grant has been denied by the resource owner
// to redirect the user agent to the client application using the appropriate response_mode.
public IActionResult Deny() => Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
[HttpGet("connect/logout")]
public IActionResult Logout() => Redirect($"/Account/Logout/Confirm{Request.QueryString}");
//[ActionName(nameof(Logout)), HttpPost("connect/logout"), ValidateAntiForgeryToken]
[Authorize, FormValueRequired("submit.Confirm")]
[HttpPost("connect/logout"), ValidateAntiForgeryToken]
public async Task<IActionResult> LogoutPost()
{
// Ask ASP.NET Core Identity to delete the local and external cookies created
// when the user agent is redirected from the external identity provider
// after a successful authentication flow (e.g Google or Facebook).
await signInManager.SignOutAsync();
// Returning a SignOutResult will ask OpenIddict to redirect the user agent
// to the post_logout_redirect_uri specified by the client application or to
// the RedirectUri specified in the authentication properties if none was set.
return SignOut(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties
{
RedirectUri = "/"
});
}
[HttpPost("connect/token"), IgnoreAntiforgeryToken, Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
{
// Retrieve the claims principal stored in the authorization code/refresh token.
var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
// Retrieve the user profile corresponding to the authorization code/refresh token.
var user = await userManager.FindByIdAsync(result.Principal?.GetClaim(Claims.Subject) ?? "");
if (user is null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid."
}));
}
// Ensure the user is still allowed to sign in.
if (!await signInManager.CanSignInAsync(user))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
}));
}
var identity = new ClaimsIdentity(result.Principal?.Claims,
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
nameType: Claims.Name,
roleType: Claims.Role);
// Override the user claims present in the principal in case they
// changed since the authorization code/refresh token was issued.
identity.SetClaim(Claims.Subject, await userManager.GetUserIdAsync(user))
.SetClaim(Claims.Email, await userManager.GetEmailAsync(user))
.SetClaim(Claims.Name, await userManager.GetUserNameAsync(user))
.SetClaim(Claims.PreferredUsername, await userManager.GetUserNameAsync(user))
.SetClaims(Claims.Role, [.. (await userManager.GetRolesAsync(user))]);
identity.SetDestinations(GetDestinations);
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
else if (request.IsClientCredentialsGrantType())
{
// Xử lý Client Credentials Flow
var application = await applicationManager.FindByClientIdAsync(request.ClientId ?? "");
if (application == null) throw new InvalidOperationException("The application details cannot be found in the database.");
// Create the claims-based identity that will be used by OpenIddict to generate tokens.
var identity = new ClaimsIdentity(
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
nameType: Claims.Name,
roleType: Claims.Role);
// Add the claims that will be persisted in the tokens (use the client_id as the subject identifier).
identity.SetClaim(Claims.Subject, await applicationManager.GetClientIdAsync(application));
identity.SetClaim(Claims.Name, await applicationManager.GetDisplayNameAsync(application));
// Note: In the original OAuth 2.0 specification, the client credentials grant
// doesn't return an identity token, which is an OpenID Connect concept.
//
// As a non-standardized extension, OpenIddict allows returning an id_token
// to convey information about the client application when the "openid" scope
// is granted (i.e specified when calling principal.SetScopes()). When the "openid"
// scope is not explicitly set, no identity token is returned to the client application.
// Set the list of scopes granted to the client application in access_token.
identity.SetScopes(request.GetScopes());
identity.SetResources(await scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
identity.SetDestinations(GetDestinations);
return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
throw new InvalidOperationException("The specified grant type is not supported.");
}
private static IEnumerable<string> GetDestinations(Claim claim)
{
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
switch (claim.Type)
{
case Claims.Name or Claims.PreferredUsername:
yield return Destinations.AccessToken;
if (claim.Subject?.HasScope(Scopes.Profile) ?? false)
yield return Destinations.IdentityToken;
yield break;
case Claims.Email:
yield return Destinations.AccessToken;
if (claim.Subject?.HasScope(Scopes.Email) ?? false)
yield return Destinations.IdentityToken;
yield break;
case Claims.Role:
yield return Destinations.AccessToken;
if (claim.Subject?.HasScope(Scopes.Roles) ?? false)
yield return Destinations.IdentityToken;
yield break;
// Never include the security stamp in the access and identity tokens, as it's a secret value.
case "AspNet.Identity.SecurityStamp": yield break;
default:
yield return Destinations.AccessToken;
yield break;
}
}
}

View File

@ -0,0 +1,48 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace RobotNet.IdentityServer.Controllers;
[Route("api/[controller]")]
[ApiController]
[AllowAnonymous]
public class IdentityServerLoggerController(ILogger<IdentityServerLoggerController> Logger) : ControllerBase
{
private readonly string LoggerDirectory = "identityServerlogs";
[HttpGet]
public async Task<IEnumerable<string>> GetLogs([FromQuery(Name = "date")] DateTime date)
{
string temp = "";
try
{
string fileName = $"{date:yyyy-MM-dd}.log";
string path = Path.Combine(LoggerDirectory, fileName);
if (!Path.GetFullPath(path).StartsWith(Path.GetFullPath(LoggerDirectory)))
{
Logger.LogWarning($"GetLogs: phát hiện đường dẫn không hợp lệ.");
return [];
}
if (!System.IO.File.Exists(path))
{
Logger.LogWarning($"GetLogs: không tìm thấy file log của ngày {date.ToShortDateString()} - {path}.");
return [];
}
temp = Path.Combine(LoggerDirectory, $"{Guid.NewGuid()}.log");
System.IO.File.Copy(path, temp);
return await System.IO.File.ReadAllLinesAsync(temp);
}
catch (Exception ex)
{
Logger.LogWarning($"GetLogs: Hệ thống có lỗi xảy ra - {ex.Message}");
return [];
}
finally
{
if (System.IO.File.Exists(temp)) System.IO.File.Delete(temp);
}
}
}

View File

@ -0,0 +1,63 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using RobotNet.IdentityServer.Data;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace RobotNet.IdentityServer.Controllers;
[EnableCors("RequestAuthorize")]
[Route("api/[controller]")]
[ApiController]
public class UserinfoController(UserManager<ApplicationUser> userManager) : ControllerBase
{// GET: /api/userinfo
[Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
[HttpGet(""), HttpPost(""), Produces("application/json")]
public async Task<IActionResult> Userinfo()
{
var user = await userManager.FindByIdAsync(User.GetClaim(Claims.Subject) ?? "");
if (user == null)
{
return Challenge(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidToken,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
"The specified access token is bound to an account that no longer exists."
}));
}
var claims = new Dictionary<string, object>(StringComparer.Ordinal)
{
// Note: the "sub" claim is a mandatory claim and must be included in the JSON response.
[Claims.Subject] = await userManager.GetUserIdAsync(user)
};
if (User.HasScope(Scopes.Email))
{
claims[Claims.Email] = await userManager.GetEmailAsync(user) ?? "";
claims[Claims.EmailVerified] = await userManager.IsEmailConfirmedAsync(user);
}
if (User.HasScope(Scopes.Phone))
{
claims[Claims.PhoneNumber] = await userManager.GetPhoneNumberAsync(user) ?? "";
claims[Claims.PhoneNumberVerified] = await userManager.IsPhoneNumberConfirmedAsync(user);
}
if (User.HasScope(Scopes.Roles))
{
claims[Claims.Role] = await userManager.GetRolesAsync(user);
}
// Note: the complete list of standard claims supported by the OpenID Connect specification
// can be found here: http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
return Ok(claims);
}
}

View File

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace RobotNet.IdentityServer.Data
{
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
}
}

View File

@ -0,0 +1,190 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace RobotNet.IdentityServer.Data;
public static class ApplicationDbExtensions
{
public static async Task SeedApplicationDbAsync(this IServiceProvider serviceProvider)
{
using var scope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope();
using var appDb = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await appDb.Database.MigrateAsync();
//await appDb.Database.EnsureCreatedAsync();
await appDb.SaveChangesAsync();
await scope.ServiceProvider.SeedRolesAsync();
await scope.ServiceProvider.SeedUsersAsync();
await scope.ServiceProvider.SeedOpenIddictApplicationAsync();
await scope.ServiceProvider.SeedOpenIddictScopesAsync();
}
private static async Task SeedRolesAsync(this IServiceProvider serviceProvider)
{
var roleManager = serviceProvider.GetRequiredService<RoleManager<ApplicationRole>>();
if (!await roleManager.RoleExistsAsync("Administrator"))
{
await roleManager.CreateAsync(new ApplicationRole()
{
Name = "Administrator",
NormalizedName = "ADMINISTRATOR",
CreatedDate = DateTime.UtcNow
});
}
}
private static async Task SeedUsersAsync(this IServiceProvider serviceProvider)
{
using var userManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
if (await userManager.FindByNameAsync("admin") is null)
{
var admin = new ApplicationUser()
{
UserName = "admin",
Email = "administrator@phenikaa-x.com",
NormalizedUserName = "ADMINISTRATOR",
NormalizedEmail = "ADMINISTRATOR@PHENIKAA-X.COM",
EmailConfirmed = true,
};
await userManager.CreateAsync(admin, "robotics");
await userManager.AddToRoleAsync(admin, "Administrator");
}
}
private static async Task CreateIfNotExistAsync(this IOpenIddictApplicationManager manager, OpenIddictApplicationDescriptor desciptor)
{
if (desciptor.ClientId == null) return;
if (await manager.FindByClientIdAsync(desciptor.ClientId) == null)
{
await manager.CreateAsync(desciptor);
}
}
private static async Task SeedOpenIddictApplicationAsync(this IServiceProvider serviceProvider)
{
var manager = serviceProvider.GetRequiredService<IOpenIddictApplicationManager>();
await manager.CreateIfNotExistAsync(new OpenIddictApplicationDescriptor
{
ClientId = "robotnet-webapp",
ConsentType = ConsentTypes.Explicit,
DisplayName = "RobotNet WebApp",
ClientType = ClientTypes.Public,
PostLogoutRedirectUris =
{
new Uri("https://localhost:7035/authentication/logout-callback")
},
RedirectUris =
{
new Uri("https://localhost:7035/authentication/login-callback")
},
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.EndSession,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.RefreshToken,
Permissions.ResponseTypes.Code,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles,
Permissions.Prefixes.Scope + "robotnet-script-api",
Permissions.Prefixes.Scope + "robotnet-robot-api",
Permissions.Prefixes.Scope + "robotnet-map-api",
},
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange,
},
});
await manager.CreateIfNotExistAsync(new OpenIddictApplicationDescriptor
{
ClientId = "robotnet-script-manager",
ClientSecret = "05594ECB-BBAE-4246-8EED-4F0841C3B475",
Permissions =
{
Permissions.Endpoints.Introspection,
Permissions.GrantTypes.ClientCredentials,
Permissions.Endpoints.Token,
Permissions.Prefixes.Scope + "robotnet-robot-api",
Permissions.Prefixes.Scope + "robotnet-map-api",
}
});
await manager.CreateIfNotExistAsync(new OpenIddictApplicationDescriptor
{
ClientId = "robotnet-map-manager",
ClientSecret = "72B36E68-2F2B-455B-858A-77B1DCC79979",
Permissions =
{
Permissions.Endpoints.Introspection,
}
});
await manager.CreateIfNotExistAsync(new OpenIddictApplicationDescriptor
{
ClientId = "robotnet-robot-manager",
ClientSecret = "469B2DEB-660E-4C91-97C7-D69550D9969D",
Permissions =
{
Permissions.Endpoints.Introspection,
Permissions.GrantTypes.ClientCredentials,
Permissions.Endpoints.Token,
Permissions.Prefixes.Scope + "robotnet-map-api",
}
});
}
private static async Task CreateIfNotExistAsync(this IOpenIddictScopeManager manager, OpenIddictScopeDescriptor desciptor)
{
if (desciptor.Name == null) return;
if (await manager.FindByNameAsync(desciptor.Name) is null)
{
await manager.CreateAsync(desciptor);
}
}
private static async Task SeedOpenIddictScopesAsync(this IServiceProvider serviceProvider)
{
var manager = serviceProvider.GetRequiredService<IOpenIddictScopeManager>();
await manager.CreateIfNotExistAsync(new OpenIddictScopeDescriptor
{
DisplayName = "RobotNet Script Manager API Access",
Name = "robotnet-script-api",
Resources =
{
"robotnet-script-manager"
}
});
await manager.CreateIfNotExistAsync(new OpenIddictScopeDescriptor
{
DisplayName = "RobotNet Map Manager API Access",
Name = "robotnet-map-api",
Resources =
{
"robotnet-map-manager"
}
});
await manager.CreateIfNotExistAsync(new OpenIddictScopeDescriptor
{
DisplayName = "RobotNet Robot Manager API Access",
Name = "robotnet-robot-api",
Resources =
{
"robotnet-robot-manager"
}
});
}
}

View File

@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Identity;
namespace RobotNet.IdentityServer.Data
{
public class ApplicationRole : IdentityRole
{
public DateTime CreatedDate { get; set; } = DateTime.UtcNow;
}
}

View File

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Identity;
namespace RobotNet.IdentityServer.Data
{
// Add profile data for application users by adding properties to the ApplicationUser class
public class ApplicationUser : IdentityUser
{
public string FullName { get; set; } = "";
public byte[]? AvatarImage { get; set; }
public string AvatarContentType { get; set; } = "";
}
}

View File

@ -0,0 +1,540 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using RobotNet.IdentityServer.Data;
#nullable disable
namespace RobotNet.IdentityServer.Data.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20250716085859_InitializeApplicationDb")]
partial class InitializeApplicationDb
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderKey")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("RoleId")
.HasColumnType("nvarchar(450)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
b.Property<string>("Value")
.HasColumnType("nvarchar(max)");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("nvarchar(450)");
b.Property<string>("ApplicationType")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ClientId")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("ClientSecret")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClientType")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ConsentType")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("DisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("DisplayNames")
.HasColumnType("nvarchar(max)");
b.Property<string>("JsonWebKeySet")
.HasColumnType("nvarchar(max)");
b.Property<string>("Permissions")
.HasColumnType("nvarchar(max)");
b.Property<string>("PostLogoutRedirectUris")
.HasColumnType("nvarchar(max)");
b.Property<string>("Properties")
.HasColumnType("nvarchar(max)");
b.Property<string>("RedirectUris")
.HasColumnType("nvarchar(max)");
b.Property<string>("Requirements")
.HasColumnType("nvarchar(max)");
b.Property<string>("Settings")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("ClientId")
.IsUnique()
.HasFilter("[ClientId] IS NOT NULL");
b.ToTable("OpenIddictApplications", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("nvarchar(450)");
b.Property<string>("ApplicationId")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime?>("CreationDate")
.HasColumnType("datetime2");
b.Property<string>("Properties")
.HasColumnType("nvarchar(max)");
b.Property<string>("Scopes")
.HasColumnType("nvarchar(max)");
b.Property<string>("Status")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Subject")
.HasMaxLength(400)
.HasColumnType("nvarchar(400)");
b.Property<string>("Type")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
b.ToTable("OpenIddictAuthorizations", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<string>("Descriptions")
.HasColumnType("nvarchar(max)");
b.Property<string>("DisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("DisplayNames")
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Properties")
.HasColumnType("nvarchar(max)");
b.Property<string>("Resources")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique()
.HasFilter("[Name] IS NOT NULL");
b.ToTable("OpenIddictScopes", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("nvarchar(450)");
b.Property<string>("ApplicationId")
.HasColumnType("nvarchar(450)");
b.Property<string>("AuthorizationId")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime?>("CreationDate")
.HasColumnType("datetime2");
b.Property<DateTime?>("ExpirationDate")
.HasColumnType("datetime2");
b.Property<string>("Payload")
.HasColumnType("nvarchar(max)");
b.Property<string>("Properties")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("RedemptionDate")
.HasColumnType("datetime2");
b.Property<string>("ReferenceId")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Status")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Subject")
.HasMaxLength(400)
.HasColumnType("nvarchar(400)");
b.Property<string>("Type")
.HasMaxLength(150)
.HasColumnType("nvarchar(150)");
b.HasKey("Id");
b.HasIndex("AuthorizationId");
b.HasIndex("ReferenceId")
.IsUnique()
.HasFilter("[ReferenceId] IS NOT NULL");
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
b.ToTable("OpenIddictTokens", (string)null);
});
modelBuilder.Entity("RobotNet.IdentityServer.Data.ApplicationRole", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreatedDate")
.HasColumnType("datetime2");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex")
.HasFilter("[NormalizedName] IS NOT NULL");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("RobotNet.IdentityServer.Data.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("AvatarContentType")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<byte[]>("AvatarImage")
.HasColumnType("varbinary(max)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<string>("FullName")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex")
.HasFilter("[NormalizedUserName] IS NOT NULL");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("RobotNet.IdentityServer.Data.ApplicationRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("RobotNet.IdentityServer.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("RobotNet.IdentityServer.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("RobotNet.IdentityServer.Data.ApplicationRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("RobotNet.IdentityServer.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("RobotNet.IdentityServer.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application")
.WithMany("Authorizations")
.HasForeignKey("ApplicationId");
b.Navigation("Application");
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b =>
{
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application")
.WithMany("Tokens")
.HasForeignKey("ApplicationId");
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization")
.WithMany("Tokens")
.HasForeignKey("AuthorizationId");
b.Navigation("Application");
b.Navigation("Authorization");
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b =>
{
b.Navigation("Authorizations");
b.Navigation("Tokens");
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.Navigation("Tokens");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,378 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace RobotNet.IdentityServer.Data.Migrations
{
/// <inheritdoc />
public partial class InitializeApplicationDb : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AspNetRoles",
columns: table => new
{
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
CreatedDate = table.Column<DateTime>(type: "datetime2", nullable: false),
Name = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
ConcurrencyStamp = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetUsers",
columns: table => new
{
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
FullName = table.Column<string>(type: "nvarchar(max)", nullable: false),
AvatarImage = table.Column<byte[]>(type: "varbinary(max)", nullable: true),
AvatarContentType = table.Column<string>(type: "nvarchar(max)", nullable: false),
UserName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
NormalizedUserName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
Email = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
NormalizedEmail = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
EmailConfirmed = table.Column<bool>(type: "bit", nullable: false),
PasswordHash = table.Column<string>(type: "nvarchar(max)", nullable: true),
SecurityStamp = table.Column<string>(type: "nvarchar(max)", nullable: true),
ConcurrencyStamp = table.Column<string>(type: "nvarchar(max)", nullable: true),
PhoneNumber = table.Column<string>(type: "nvarchar(max)", nullable: true),
PhoneNumberConfirmed = table.Column<bool>(type: "bit", nullable: false),
TwoFactorEnabled = table.Column<bool>(type: "bit", nullable: false),
LockoutEnd = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
LockoutEnabled = table.Column<bool>(type: "bit", nullable: false),
AccessFailedCount = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
});
migrationBuilder.CreateTable(
name: "OpenIddictApplications",
columns: table => new
{
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
ApplicationType = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
ClientId = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
ClientSecret = table.Column<string>(type: "nvarchar(max)", nullable: true),
ClientType = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
ConcurrencyToken = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
ConsentType = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
DisplayName = table.Column<string>(type: "nvarchar(max)", nullable: true),
DisplayNames = table.Column<string>(type: "nvarchar(max)", nullable: true),
JsonWebKeySet = table.Column<string>(type: "nvarchar(max)", nullable: true),
Permissions = table.Column<string>(type: "nvarchar(max)", nullable: true),
PostLogoutRedirectUris = table.Column<string>(type: "nvarchar(max)", nullable: true),
Properties = table.Column<string>(type: "nvarchar(max)", nullable: true),
RedirectUris = table.Column<string>(type: "nvarchar(max)", nullable: true),
Requirements = table.Column<string>(type: "nvarchar(max)", nullable: true),
Settings = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_OpenIddictApplications", x => x.Id);
});
migrationBuilder.CreateTable(
name: "OpenIddictScopes",
columns: table => new
{
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
ConcurrencyToken = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
Descriptions = table.Column<string>(type: "nvarchar(max)", nullable: true),
DisplayName = table.Column<string>(type: "nvarchar(max)", nullable: true),
DisplayNames = table.Column<string>(type: "nvarchar(max)", nullable: true),
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
Properties = table.Column<string>(type: "nvarchar(max)", nullable: true),
Resources = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_OpenIddictScopes", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetRoleClaims",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
RoleId = table.Column<string>(type: "nvarchar(450)", nullable: false),
ClaimType = table.Column<string>(type: "nvarchar(max)", nullable: true),
ClaimValue = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserClaims",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false),
ClaimType = table.Column<string>(type: "nvarchar(max)", nullable: true),
ClaimValue = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserLogins",
columns: table => new
{
LoginProvider = table.Column<string>(type: "nvarchar(450)", nullable: false),
ProviderKey = table.Column<string>(type: "nvarchar(450)", nullable: false),
ProviderDisplayName = table.Column<string>(type: "nvarchar(max)", nullable: true),
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
table.ForeignKey(
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserRoles",
columns: table => new
{
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false),
RoleId = table.Column<string>(type: "nvarchar(450)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserTokens",
columns: table => new
{
UserId = table.Column<string>(type: "nvarchar(450)", nullable: false),
LoginProvider = table.Column<string>(type: "nvarchar(450)", nullable: false),
Name = table.Column<string>(type: "nvarchar(450)", nullable: false),
Value = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
table.ForeignKey(
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "OpenIddictAuthorizations",
columns: table => new
{
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
ApplicationId = table.Column<string>(type: "nvarchar(450)", nullable: true),
ConcurrencyToken = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
CreationDate = table.Column<DateTime>(type: "datetime2", nullable: true),
Properties = table.Column<string>(type: "nvarchar(max)", nullable: true),
Scopes = table.Column<string>(type: "nvarchar(max)", nullable: true),
Status = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
Subject = table.Column<string>(type: "nvarchar(400)", maxLength: 400, nullable: true),
Type = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_OpenIddictAuthorizations", x => x.Id);
table.ForeignKey(
name: "FK_OpenIddictAuthorizations_OpenIddictApplications_ApplicationId",
column: x => x.ApplicationId,
principalTable: "OpenIddictApplications",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "OpenIddictTokens",
columns: table => new
{
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
ApplicationId = table.Column<string>(type: "nvarchar(450)", nullable: true),
AuthorizationId = table.Column<string>(type: "nvarchar(450)", nullable: true),
ConcurrencyToken = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
CreationDate = table.Column<DateTime>(type: "datetime2", nullable: true),
ExpirationDate = table.Column<DateTime>(type: "datetime2", nullable: true),
Payload = table.Column<string>(type: "nvarchar(max)", nullable: true),
Properties = table.Column<string>(type: "nvarchar(max)", nullable: true),
RedemptionDate = table.Column<DateTime>(type: "datetime2", nullable: true),
ReferenceId = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
Status = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
Subject = table.Column<string>(type: "nvarchar(400)", maxLength: 400, nullable: true),
Type = table.Column<string>(type: "nvarchar(150)", maxLength: 150, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_OpenIddictTokens", x => x.Id);
table.ForeignKey(
name: "FK_OpenIddictTokens_OpenIddictApplications_ApplicationId",
column: x => x.ApplicationId,
principalTable: "OpenIddictApplications",
principalColumn: "Id");
table.ForeignKey(
name: "FK_OpenIddictTokens_OpenIddictAuthorizations_AuthorizationId",
column: x => x.AuthorizationId,
principalTable: "OpenIddictAuthorizations",
principalColumn: "Id");
});
migrationBuilder.CreateIndex(
name: "IX_AspNetRoleClaims_RoleId",
table: "AspNetRoleClaims",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "RoleNameIndex",
table: "AspNetRoles",
column: "NormalizedName",
unique: true,
filter: "[NormalizedName] IS NOT NULL");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserClaims_UserId",
table: "AspNetUserClaims",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserLogins_UserId",
table: "AspNetUserLogins",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserRoles_RoleId",
table: "AspNetUserRoles",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "EmailIndex",
table: "AspNetUsers",
column: "NormalizedEmail");
migrationBuilder.CreateIndex(
name: "UserNameIndex",
table: "AspNetUsers",
column: "NormalizedUserName",
unique: true,
filter: "[NormalizedUserName] IS NOT NULL");
migrationBuilder.CreateIndex(
name: "IX_OpenIddictApplications_ClientId",
table: "OpenIddictApplications",
column: "ClientId",
unique: true,
filter: "[ClientId] IS NOT NULL");
migrationBuilder.CreateIndex(
name: "IX_OpenIddictAuthorizations_ApplicationId_Status_Subject_Type",
table: "OpenIddictAuthorizations",
columns: new[] { "ApplicationId", "Status", "Subject", "Type" });
migrationBuilder.CreateIndex(
name: "IX_OpenIddictScopes_Name",
table: "OpenIddictScopes",
column: "Name",
unique: true,
filter: "[Name] IS NOT NULL");
migrationBuilder.CreateIndex(
name: "IX_OpenIddictTokens_ApplicationId_Status_Subject_Type",
table: "OpenIddictTokens",
columns: new[] { "ApplicationId", "Status", "Subject", "Type" });
migrationBuilder.CreateIndex(
name: "IX_OpenIddictTokens_AuthorizationId",
table: "OpenIddictTokens",
column: "AuthorizationId");
migrationBuilder.CreateIndex(
name: "IX_OpenIddictTokens_ReferenceId",
table: "OpenIddictTokens",
column: "ReferenceId",
unique: true,
filter: "[ReferenceId] IS NOT NULL");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AspNetRoleClaims");
migrationBuilder.DropTable(
name: "AspNetUserClaims");
migrationBuilder.DropTable(
name: "AspNetUserLogins");
migrationBuilder.DropTable(
name: "AspNetUserRoles");
migrationBuilder.DropTable(
name: "AspNetUserTokens");
migrationBuilder.DropTable(
name: "OpenIddictScopes");
migrationBuilder.DropTable(
name: "OpenIddictTokens");
migrationBuilder.DropTable(
name: "AspNetRoles");
migrationBuilder.DropTable(
name: "AspNetUsers");
migrationBuilder.DropTable(
name: "OpenIddictAuthorizations");
migrationBuilder.DropTable(
name: "OpenIddictApplications");
}
}
}

View File

@ -0,0 +1,537 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using RobotNet.IdentityServer.Data;
#nullable disable
namespace RobotNet.IdentityServer.Data.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderKey")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("nvarchar(450)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("RoleId")
.HasColumnType("nvarchar(450)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("nvarchar(450)");
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
b.Property<string>("Value")
.HasColumnType("nvarchar(max)");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("nvarchar(450)");
b.Property<string>("ApplicationType")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ClientId")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("ClientSecret")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClientType")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ConsentType")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("DisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("DisplayNames")
.HasColumnType("nvarchar(max)");
b.Property<string>("JsonWebKeySet")
.HasColumnType("nvarchar(max)");
b.Property<string>("Permissions")
.HasColumnType("nvarchar(max)");
b.Property<string>("PostLogoutRedirectUris")
.HasColumnType("nvarchar(max)");
b.Property<string>("Properties")
.HasColumnType("nvarchar(max)");
b.Property<string>("RedirectUris")
.HasColumnType("nvarchar(max)");
b.Property<string>("Requirements")
.HasColumnType("nvarchar(max)");
b.Property<string>("Settings")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("ClientId")
.IsUnique()
.HasFilter("[ClientId] IS NOT NULL");
b.ToTable("OpenIddictApplications", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("nvarchar(450)");
b.Property<string>("ApplicationId")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime?>("CreationDate")
.HasColumnType("datetime2");
b.Property<string>("Properties")
.HasColumnType("nvarchar(max)");
b.Property<string>("Scopes")
.HasColumnType("nvarchar(max)");
b.Property<string>("Status")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Subject")
.HasMaxLength(400)
.HasColumnType("nvarchar(400)");
b.Property<string>("Type")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
b.ToTable("OpenIddictAuthorizations", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<string>("Descriptions")
.HasColumnType("nvarchar(max)");
b.Property<string>("DisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("DisplayNames")
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Properties")
.HasColumnType("nvarchar(max)");
b.Property<string>("Resources")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique()
.HasFilter("[Name] IS NOT NULL");
b.ToTable("OpenIddictScopes", (string)null);
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("nvarchar(450)");
b.Property<string>("ApplicationId")
.HasColumnType("nvarchar(450)");
b.Property<string>("AuthorizationId")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime?>("CreationDate")
.HasColumnType("datetime2");
b.Property<DateTime?>("ExpirationDate")
.HasColumnType("datetime2");
b.Property<string>("Payload")
.HasColumnType("nvarchar(max)");
b.Property<string>("Properties")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("RedemptionDate")
.HasColumnType("datetime2");
b.Property<string>("ReferenceId")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Status")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Subject")
.HasMaxLength(400)
.HasColumnType("nvarchar(400)");
b.Property<string>("Type")
.HasMaxLength(150)
.HasColumnType("nvarchar(150)");
b.HasKey("Id");
b.HasIndex("AuthorizationId");
b.HasIndex("ReferenceId")
.IsUnique()
.HasFilter("[ReferenceId] IS NOT NULL");
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
b.ToTable("OpenIddictTokens", (string)null);
});
modelBuilder.Entity("RobotNet.IdentityServer.Data.ApplicationRole", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreatedDate")
.HasColumnType("datetime2");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex")
.HasFilter("[NormalizedName] IS NOT NULL");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("RobotNet.IdentityServer.Data.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("AvatarContentType")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<byte[]>("AvatarImage")
.HasColumnType("varbinary(max)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<string>("FullName")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex")
.HasFilter("[NormalizedUserName] IS NOT NULL");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("RobotNet.IdentityServer.Data.ApplicationRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("RobotNet.IdentityServer.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("RobotNet.IdentityServer.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("RobotNet.IdentityServer.Data.ApplicationRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("RobotNet.IdentityServer.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("RobotNet.IdentityServer.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application")
.WithMany("Authorizations")
.HasForeignKey("ApplicationId");
b.Navigation("Application");
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b =>
{
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application")
.WithMany("Tokens")
.HasForeignKey("ApplicationId");
b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization")
.WithMany("Tokens")
.HasForeignKey("AuthorizationId");
b.Navigation("Application");
b.Navigation("Authorization");
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b =>
{
b.Navigation("Authorizations");
b.Navigation("Tokens");
});
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b =>
{
b.Navigation("Tokens");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,56 @@
FROM alpine:3.22 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY ["RobotNet.IdentityServer/RobotNet.IdentityServer.csproj", "RobotNet.IdentityServer/"]
COPY ["RobotNet.IdentityServer/libman.json", "RobotNet.IdentityServer/"]
COPY ["RobotNet.ServiceDefaults/RobotNet.ServiceDefaults.csproj", "RobotNet.ServiceDefaults/"]
# RUN dotnet package remove "Microsoft.EntityFrameworkCore.Tools" --project "RobotNet.IdentityServer/RobotNet.IdentityServer.csproj"
RUN dotnet restore "RobotNet.IdentityServer/RobotNet.IdentityServer.csproj"
WORKDIR /src/RobotNet.IdentityServer
RUN dotnet tool install -g Microsoft.Web.LibraryManager.Cli
ENV PATH="${PATH}:/root/.dotnet/tools"
# RUN libman restore
WORKDIR /src
COPY RobotNet.IdentityServer/ RobotNet.IdentityServer/
COPY RobotNet.ServiceDefaults/ RobotNet.ServiceDefaults/
RUN rm -rf ./RobotNet.IdentityServer/bin
RUN rm -rf ./RobotNet.IdentityServer/obj
RUN rm -rf ./RobotNet.ServiceDefaults/bin
RUN rm -rf ./RobotNet.ServiceDefaults/obj
WORKDIR "/src/RobotNet.IdentityServer"
RUN dotnet build -c Release -o /app/build
FROM build AS publish
WORKDIR /src/RobotNet.IdentityServer
RUN dotnet publish "RobotNet.IdentityServer.csproj" \
-c Release \
-o /app/publish \
--runtime linux-musl-x64 \
--self-contained true \
/p:PublishTrimmed=false \
/p:PublishReadyToRun=true
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish ./
RUN apk add --no-cache icu-libs tzdata ca-certificates
RUN echo '#!/bin/sh' >> ./start.sh
RUN echo 'update-ca-certificates' >> ./start.sh
RUN echo 'exec ./RobotNet.IdentityServer' >> ./start.sh
RUN chmod +x ./RobotNet.IdentityServer
RUN chmod +x ./start.sh
# Use the start script to ensure certificates are updated before starting the application
EXPOSE 443
ENTRYPOINT ["./start.sh"]

View File

@ -0,0 +1,21 @@
namespace RobotNet.IdentityServer.Helpers;
public static class AsyncEnumerableExtensions
{
public static Task<List<T>> ToListAsync<T>(this IAsyncEnumerable<T> source)
{
return source == null ? throw new ArgumentNullException(nameof(source)) : ExecuteAsync();
async Task<List<T>> ExecuteAsync()
{
var list = new List<T>();
await foreach (var element in source)
{
list.Add(element);
}
return list;
}
}
}

View File

@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
namespace RobotNet.IdentityServer.Helpers;
public sealed class FormValueRequiredAttribute(string name) : ActionMethodSelectorAttribute
{
private readonly string _name = name;
public override bool IsValidForRequest(RouteContext context, ActionDescriptor action)
{
if (string.Equals(context.HttpContext.Request.Method, "GET", StringComparison.OrdinalIgnoreCase) ||
string.Equals(context.HttpContext.Request.Method, "HEAD", StringComparison.OrdinalIgnoreCase) ||
string.Equals(context.HttpContext.Request.Method, "DELETE", StringComparison.OrdinalIgnoreCase) ||
string.Equals(context.HttpContext.Request.Method, "TRACE", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (string.IsNullOrEmpty(context.HttpContext.Request.ContentType))
{
return false;
}
if (!context.HttpContext.Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase))
{
return false;
}
return !string.IsNullOrEmpty(context.HttpContext.Request.Form[_name]);
}
}

View File

@ -0,0 +1,196 @@
using BlazorComponentBus;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using MudBlazor.Services;
using NLog.Web;
using Quartz;
using RobotNet.IdentityServer.Components;
using RobotNet.IdentityServer.Components.Account;
using RobotNet.IdentityServer.Components.Layout;
using RobotNet.IdentityServer.Data;
using RobotNet.IdentityServer.Services;
using System.Security.Cryptography.X509Certificates;
using static OpenIddict.Abstractions.OpenIddictConstants;
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseNLog();
// builder.AddServiceDefaults();
builder.Services.AddControllers();
builder.Services.AddControllersWithViews();
builder.Services.AddMudServices();
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddScoped<ComponentBus>();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<NavMenu>();
builder.Services.AddSingleton<PasswordStrengthService>();
builder.Services.AddSingleton<UserInfoService>();
builder.Services.AddScoped<UserImageService>();
builder.Services.AddScoped<IdentityService>();
builder.Services.AddScoped<IdentityUserAccessor>();
builder.Services.AddScoped<IdentityRedirectManager>();
builder.Services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(connectionString);
options.UseOpenIddict();
});
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddIdentity<ApplicationUser, ApplicationRole>(options =>
{
options.SignIn.RequireConfirmedAccount = true;
options.Lockout.AllowedForNewUsers = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
options.Password.RequireDigit = false;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddSingleton<IEmailSender<ApplicationUser>, IdentityNoOpEmailSender>();
builder.Services.AddQuartz(options =>
{
options.UseSimpleTypeLoader();
options.UseInMemoryStore();
});
builder.Services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
builder.Services.AddOpenIddict()
.AddCore(options =>
{
// Configure OpenIddict to use the Entity Framework Core stores and models.
// Note: call ReplaceDefaultEntities() to replace the default OpenIddict entities.
options.UseEntityFrameworkCore()
.UseDbContext<ApplicationDbContext>();
// Enable Quartz.NET integration.
options.UseQuartz();
})
.AddServer(options =>
{
options.SetIssuer(builder.Configuration["OpenIddictCertificate:Issuer"] ?? throw new InvalidOperationException("OpenIddictCertificate Issuer is not configured."));
// Enable the authorization, logout, token and userinfo endpoints.
options.SetAuthorizationEndpointUris("api/Authorization/connect/authorize")
.SetEndSessionEndpointUris("api/Authorization/connect/logout")
.SetIntrospectionEndpointUris("connect/introspect")
.SetTokenEndpointUris("api/Authorization/connect/token")
.AllowClientCredentialsFlow()
.SetUserInfoEndpointUris("api/Userinfo")
.SetEndUserVerificationEndpointUris("connect/verify");
// Mark the "email", "profile" and "roles" scopes as supported scopes.
options.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles);
// Note: this sample only uses the authorization code and refresh token
// flows but you can enable the other flows if you need to support
// implicit, password or client credentials.
options.AllowAuthorizationCodeFlow()
.AllowRefreshTokenFlow()
.AllowClientCredentialsFlow();
if (builder.Environment.IsDevelopment())
{
// Register the signing and encryption credentials.
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// Thêm ephemeral encryption key
//options.AddEphemeralEncryptionKey()
// .AddEphemeralSigningKey(); // Thêm signing key tạm thời
}
else if (builder.Environment.IsProduction())
{
// Thêm ephemeral encryption key
// Sử dụng chứng chỉ thực tế
var path = builder.Configuration["OpenIddictCertificate:Path"] ?? throw new InvalidOperationException("Certificate path is not configured.");
var password = builder.Configuration["OpenIddictCertificate:Password"] ?? throw new InvalidOperationException("Certificate password is not configured.");
if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(password))
{
throw new InvalidOperationException("Certificate path or password is not configured.");
}
var certificate = X509CertificateLoader.LoadPkcs12FromFile(path, password);
options.AddEncryptionCertificate(certificate)
.AddSigningCertificate(certificate);
}
options.UseDataProtection()
.PreferDefaultAccessTokenFormat()
.PreferDefaultAuthorizationCodeFormat()
.PreferDefaultRefreshTokenFormat();
// Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableEndSessionEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableUserInfoEndpointPassthrough()
.EnableStatusCodePagesIntegration();
// Can thiệp vào sự kiện logging
})
.AddValidation(options =>
{
// Import the configuration from the local OpenIddict server instance.
options.UseLocalServer();
// Register the ASP.NET Core host.
options.UseAspNetCore();
});
builder.Services.AddCors(options =>
{
options.AddPolicy("RequestAuthorize", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
builder.Services.AddMudServices();
var app = builder.Build();
await app.Services.SeedApplicationDbAsync();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// 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.UseCors("RequestAuthorize");
app.MapControllers();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.MapAdditionalIdentityEndpoints();
app.Run();

View File

@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"workingDirectory": "$(TargetDir)",
"applicationUrl": "https://localhost:7061",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,8 @@
{
"dependencies": {
"mssql1": {
"type": "mssql",
"connectionId": "ConnectionStrings:DefaultConnection"
}
}
}

View File

@ -0,0 +1,8 @@
{
"dependencies": {
"mssql1": {
"type": "mssql.local",
"connectionId": "ConnectionStrings:DefaultConnection"
}
}
}

View File

@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>aspnet-RobotNet.IdentityServer-e398adbb-379f-421d-8396-f36f060aca5f</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BlazorComponentBus" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="9.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MudBlazor" Version="8.11.0" />
<PackageReference Include="OpenIddict.AspNetCore" Version="7.0.0" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="7.0.0" />
<PackageReference Include="OpenIddict.Quartz" Version="7.0.0" />
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
<PackageReference Include="NLog" Version="6.0.3" />
<PackageReference Include="NLog.Web.AspNetCore" Version="6.0.3" />
</ItemGroup>
<ItemGroup>
<Folder Include="Data\Migrations\" />
</ItemGroup>
<ItemGroup>
<Content Update="nlog.config">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
</Project>

View File

@ -0,0 +1,69 @@
// IdentityService.cs - Tạo dịch vụ này để tránh lỗi DbContext
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RobotNet.IdentityServer.Data;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace RobotNet.IdentityServer.Services;
public class IdentityService
{
private readonly IServiceProvider _serviceProvider;
public IdentityService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task<ApplicationUser?> GetUserByIdAsync(string userId)
{
using var scope = _serviceProvider.CreateScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var user = await userManager.Users.AsNoTracking().FirstOrDefaultAsync(u => u.Id == userId);
return user;
}
public async Task<ApplicationUser?> GetUserByNameAsync(string userName)
{
using var scope = _serviceProvider.CreateScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
return await userManager.FindByNameAsync(userName);
}
public async Task<List<string>> GetUserRolesAsync(ApplicationUser user)
{
using var scope = _serviceProvider.CreateScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var roles = await userManager.GetRolesAsync(user);
return roles.ToList();
}
public async Task<IdentityResult> UpdateUserAsync(ApplicationUser user)
{
using var scope = _serviceProvider.CreateScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var existingUser = await userManager.FindByIdAsync(user.Id);
if (existingUser != null)
{
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
context.Entry(existingUser).State = EntityState.Detached;
}
return await userManager.UpdateAsync(user);
}
public async Task<IdentityResult> ChangePasswordAsync(ApplicationUser user, string currentPassword, string newPassword)
{
using var scope = _serviceProvider.CreateScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
return await userManager.ChangePasswordAsync(user, currentPassword, newPassword);
}
}

View File

@ -0,0 +1,61 @@
using MudBlazor;
namespace RobotNet.IdentityServer.Services;
public class PasswordStrengthService
{
/// <summary>
/// Đánh giá độ mạnh của mật khẩu (thang điểm 0-100)
/// </summary>
/// <param name="password">Mật khẩu cần đánh giá</param>
/// <returns>Điểm đánh giá từ 0-100</returns>
public int EvaluatePasswordStrength(string password)
{
if (string.IsNullOrEmpty(password))
return 0;
int strength = 0;
// Đánh giá dựa trên độ dài
if (password.Length >= 1) strength += 5;
if (password.Length >= 3) strength += 5;
if (password.Length >= 6) strength += 10;
if (password.Length >= 8) strength += 10;
if (password.Length >= 10) strength += 10;
// Đánh giá dựa trên độ phức tạp
if (password.Any(char.IsUpper)) strength += 15;
if (password.Any(char.IsLower)) strength += 15;
if (password.Any(char.IsDigit)) strength += 15;
if (password.Any(c => !char.IsLetterOrDigit(c))) strength += 15;
return System.Math.Min(strength, 100);
}
/// <summary>
/// Lấy màu tương ứng với độ mạnh của mật khẩu
/// </summary>
/// <param name="strength">Điểm đánh giá độ mạnh (0-100)</param>
/// <returns>Color tương ứng</returns>
public Color GetStrengthColor(int strength)
{
if (strength < 30) return Color.Error;
if (strength < 60) return Color.Warning;
if (strength < 80) return Color.Info;
return Color.Success;
}
/// <summary>
/// Lấy mô tả tương ứng với độ mạnh của mật khẩu
/// </summary>
/// <param name="strength">Điểm đánh giá độ mạnh (0-100)</param>
/// <returns>Mô tả dạng văn bản</returns>
public string GetStrengthDescription(int strength)
{
if (strength == 0) return "Chưa nhập mật khẩu";
if (strength < 30) return "Mật khẩu yếu";
if (strength < 60) return "Mật khẩu trung bình";
if (strength < 80) return "Mật khẩu tốt";
return "Mật khẩu mạnh";
}
}

View File

@ -0,0 +1,26 @@
using System.IO;
using System.Threading.Tasks;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
namespace RobotNet.IdentityServer.Services;
public class UserImageService
{
public async Task<(byte[] ImageBytes, string ContentType)> ResizeAndConvertAsync(Stream input)
{
using var image = await Image.LoadAsync(input);
image.Mutate(x => x.Resize(new ResizeOptions
{
Size = new Size(300, 300),
Mode = ResizeMode.Crop
}));
using var ms = new MemoryStream();
await image.SaveAsJpegAsync(ms, new JpegEncoder { Quality = 90 });
return (ms.ToArray(), "image/jpeg");
}
}

View File

@ -0,0 +1,41 @@
namespace RobotNet.IdentityServer.Services;
public class UserInfoService
{
private readonly List<Func<Task>> _handlers = [];
public void RegisterHandler(Func<Task> handler)
{
if (handler != null && !_handlers.Contains(handler))
{
_handlers.Add(handler);
}
}
public void UnregisterHandler(Func<Task> handler)
{
if (handler != null && _handlers.Contains(handler))
{
_handlers.Remove(handler);
}
}
public async Task NotifyUserInfoChanged()
{
var handlers = new List<Func<Task>>(_handlers);
foreach (var handler in handlers)
{
try
{
await handler();
}
catch (Exception ex)
{
Console.WriteLine($"Error in user info change handler: {ex.Message}");
}
}
}
}

View File

@ -0,0 +1,18 @@
{
"ConnectionStrings": {
"DefaultConnection": "Server=172.20.235.170;Database=RobotNet.Identity;User Id=sa;Password=robotics@2022;TrustServerCertificate=True;MultipleActiveResultSets=true"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore.Database": "Warning"
}
},
"AllowedHosts": "*",
"OpenIddictCertificate": {
"Issuer": "https://localhost:7061",
"Path": "/app/certs/robotnet.pfx",
"Password": "RobotNet@2024"
}
}

View File

@ -0,0 +1,15 @@
{
"version": "3.0",
"defaultProvider": "cdnjs",
"libraries": [
{
"library": "bootstrap@5.3.3",
"destination": "wwwroot/lib/bootstrap/"
},
{
"provider": "jsdelivr",
"library": "@mdi/font@7.4.47",
"destination": "wwwroot/lib/mdi/font/"
}
]
}

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true">
<extensions>
<add assembly="NLog.Web.AspNetCore"/>
</extensions>
<targets>
<target xsi:type="File" name="identityLogFile" fileName="${basedir}/identityServerlogs/${shortdate}.log" maxArchiveFiles="90" archiveEvery="Day" >
<layout type='JsonLayout'>
<attribute name='time' layout='${date:format=HH\:mm\:ss.ffff}' />
<attribute name='level' layout='${level:upperCase=true}'/>
<attribute name='logger' layout='${logger}' />
<attribute name='message' layout='${message}' />
<attribute name='exception' layout='${exception:format=tostring}' />
</layout>
</target>
</targets>
<rules>
<logger name="OpenIddict.*" minlevel="Debug" writeto="identityLogFile" />
<logger name="RobotNet.IdentityServer.*" minlevel="Debug" writeto="identityLogFile" />
</rules>
</nlog>

View File

@ -0,0 +1,60 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
a, .btn-link {
color: #006bb7;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
h1:focus {
outline: none;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid #e50000;
}
.validation-message {
color: #e50000;
}
.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::after {
content: "An error has occurred."
}
.darker-border-checkbox.form-check-input {
border-color: #929292;
}
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
color: var(--bs-secondary-color);
text-align: end;
}
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
text-align: start;
}

View File

@ -0,0 +1,5 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<path d="M31.81,1.79,21.29,14.32l-2.48-3a2.6,2.6,0,0,1,0-3.34l4.45-5.31a2.64,2.64,0,0,1,2-.93Z" fill="#e06e2e"/>
<path d="M.19,1.8,10.71,14.34l2.48-2.95a2.6,2.6,0,0,0,0-3.34L8.75,2.74a2.66,2.66,0,0,0-2-.94Z" fill="#e06e2e"/>
<path d="M32,30.21H25.36a2.61,2.61,0,0,1-2-.92L16.5,21.1a.65.65,0,0,0-1,0L8.63,29.29a2.58,2.58,0,0,1-2,.92H0L12.07,15.83,14,13.57a2.67,2.67,0,0,1,4.08,0l1.89,2.26Z" fill="#233871"/>
</svg>

After

Width:  |  Height:  |  Size: 510 B

View File

@ -0,0 +1,81 @@
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
src: url(KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmZiArmlw.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
src: url(KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmQiArmlw.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
src: url(KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmYiArmlw.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
src: url(KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmXiArmlw.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* math */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
src: url(KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVnoiArmlw.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
src: url(KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVn6iArmlw.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
src: url(KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmbiArmlw.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
src: url(KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmaiArmlw.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-stretch: 100%;
src: url(KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAo.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

View File

@ -0,0 +1,130 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RobotNet.MapManager.Data;
using RobotNet.MapManager.Services;
using RobotNet.MapShares.Dtos;
using RobotNet.Shares;
using System.Text.Json;
namespace RobotNet.MapManager.Controllers;
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class ActionsController(MapEditorDbContext MapDb, LoggerController<ActionsController> Logger) : ControllerBase
{
[HttpPost]
public async Task<MessageResult<ActionDto>> CreateAction([FromBody] ActionCreateModel model)
{
try
{
var map = await MapDb.Maps.FindAsync(model.MapId);
if (map == null) return new(false, $"Không tồn tại map id = {model.MapId}");
if (MapDb.Actions.Any(action => action.Name == model.Name && action.MapId == model.MapId)) return new(false, $"Tên Action {model.Name} đã tồn tại");
var entity = await MapDb.Actions.AddAsync(new()
{
MapId = model.MapId,
Name = model.Name,
Content = model.Content,
});
await MapDb.SaveChangesAsync();
return new(true)
{
Data = new()
{
Id = entity.Entity.Id,
MapId = entity.Entity.MapId,
Name = entity.Entity.Name,
Content = entity.Entity.Content,
}
};
}
catch(Exception ex)
{
Logger.Warning($"CreateAction: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"CreateAction: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
[HttpGet]
[Route("{id}")]
public async Task<IEnumerable<ActionDto>> GetActions(Guid id)
{
return await MapDb.Actions.Where(action => action.MapId == id).Select(action => new ActionDto()
{
Id = action.Id,
MapId = action.MapId,
Name = action.Name,
Content = action.Content,
}).ToListAsync();
}
[HttpPut]
public async Task<MessageResult> UpdateAction([FromBody] ActionDto model)
{
try
{
var action = await MapDb.Actions.FindAsync(model.Id);
if (action is not null)
{
action.Name = model.Name;
action.Content = model.Content;
MapDb.Actions.Update(action);
await MapDb.SaveChangesAsync();
return new(true);
}
return new(false, $"Hệ thống không tìm thấy Action {model.Name} này");
}
catch (Exception ex)
{
Logger.Warning($"UpdateAction: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"UpdateAction: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
[HttpDelete]
[Route("{id}")]
public async Task<MessageResult> DeleteAction(Guid id)
{
try
{
var action = await MapDb.Actions.FindAsync(id);
if (action is not null)
{
foreach (var node in MapDb.Nodes)
{
var actionIds = JsonSerializer.Deserialize<Guid[]>(node.Actions);
if (actionIds is not null && actionIds.Any(a => a == action.Id))
{
var acitonIdsAfter = actionIds.ToList();
acitonIdsAfter.Remove(action.Id);
node.Actions = JsonSerializer.Serialize(acitonIdsAfter.Count > 0 ? acitonIdsAfter : []);
}
}
foreach (var edge in MapDb.Edges)
{
var actionIds = JsonSerializer.Deserialize<Guid[]>(edge.Actions);
if (actionIds is not null && actionIds.Any(a => a == action.Id))
{
var acitonIdsAfter = actionIds.ToList();
acitonIdsAfter.Remove(action.Id);
edge.Actions = JsonSerializer.Serialize(acitonIdsAfter.Count > 0 ? acitonIdsAfter : []);
}
}
MapDb.Actions.Remove(action);
await MapDb.SaveChangesAsync();
return new(true) ;
}
return new(false, $"Hệ thống không tìm thấy Action {id} này");
}
catch (Exception ex)
{
Logger.Warning($"DeleteAction {id}: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"DeleteAction {id}: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
}

View File

@ -0,0 +1,269 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RobotNet.MapManager.Data;
using RobotNet.MapManager.Services;
using RobotNet.MapShares;
using RobotNet.MapShares.Dtos;
using RobotNet.MapShares.Enums;
using RobotNet.Shares;
using System.Text.Json;
namespace RobotNet.MapManager.Controllers;
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class EdgesController(MapEditorDbContext MapDb, LoggerController<EdgesController> Logger) : ControllerBase
{
[HttpPost]
[Route("")]
public async Task<MessageResult<EdgeCreateDto>> CreateEdge([FromBody] EdgeCreateModel model)
{
try
{
var map = await MapDb.Maps.FindAsync(model.MapId);
if (map == null) return new(false, $"Không tồn tại map id = {model.MapId}");
if (Math.Sqrt(Math.Pow(model.X1 - model.X2, 2) + Math.Pow(model.Y1 - model.Y2, 2)) < map.EdgeMinLengthDefault) return new(false, "Độ dài edge quá nhỏ");
var nodes = await MapDb.Nodes.Where(n => n.MapId == model.MapId).ToListAsync();
var edges = await MapDb.Edges.Where(e => e.MapId == model.MapId && e.TrajectoryDegree == TrajectoryDegree.One).ToListAsync();
var closesStartNode = MapEditorHelper.GetClosesNode(model.X1, model.Y1, [.. nodes.Select(node => new NodeDto()
{
MapId = node.MapId,
X = node.X,
Y = node.Y,
Theta = node.Theta,
Actions = node.Actions,
Id = node.Id,
Name = node.Name,
AllowedDeviationTheta = node.AllowedDeviationTheta,
AllowedDeviationXy = node.AllowedDeviationXy
})]);
var closesEndNode = MapEditorHelper.GetClosesNode(model.X2, model.Y2, [.. nodes.Select(node => new NodeDto()
{
MapId = node.MapId,
X = node.X,
Y = node.Y,
Theta = node.Theta,
Actions = node.Actions,
Id = node.Id,
Name = node.Name,
AllowedDeviationTheta = node.AllowedDeviationTheta,
AllowedDeviationXy = node.AllowedDeviationXy
})]);
Node? startNode = await MapDb.Nodes.FindAsync(closesStartNode?.Id);
Node? endNode = await MapDb.Nodes.FindAsync(closesEndNode?.Id);
List<Guid> RemoveEdge = [];
List<EdgeDto> AddEdgeDto = [];
if (startNode is null)
{
startNode = ServerHelper.CreateNode(map, model.X1, model.Y1);
await MapDb.Nodes.AddAsync(startNode);
var closesEdge = ServerHelper.GetClosesEdge(model.X1, model.Y1, nodes, edges, 0.1);
if (closesEdge is not null)
{
var closesEdgeStartNode = await MapDb.Nodes.FirstOrDefaultAsync(n => n.Id == closesEdge.StartNodeId);
var closesEdgeEndNode = await MapDb.Nodes.FirstOrDefaultAsync(n => n.Id == closesEdge.EndNodeId);
if (closesEdgeStartNode is not null && closesEdgeEndNode is not null)
{
var startEdge = ServerHelper.CreateEdge(map, closesEdgeStartNode.Id, startNode.Id, TrajectoryDegree.One);
var endEdge = ServerHelper.CreateEdge(map, startNode.Id, closesEdgeEndNode.Id, TrajectoryDegree.One);
await MapDb.Edges.AddAsync(startEdge);
await MapDb.Edges.AddAsync(endEdge);
MapDb.Edges.Remove(closesEdge);
edges.Remove(closesEdge);
RemoveEdge.Add(closesEdge.Id);
edges.Add(startEdge);
edges.Add(endEdge);
nodes.Add(startNode);
AddEdgeDto.Add(ServerHelper.CreateEdgeDto(startEdge, closesEdgeStartNode, startNode));
AddEdgeDto.Add(ServerHelper.CreateEdgeDto(endEdge, startNode, closesEdgeEndNode));
}
}
}
await MapDb.SaveChangesAsync();
if (endNode is null)
{
endNode = ServerHelper.CreateNode(map, model.X2, model.Y2);
await MapDb.Nodes.AddAsync(endNode);
var closesEdge = ServerHelper.GetClosesEdge(model.X2, model.Y2, nodes, edges, 0.1);
if (closesEdge is not null)
{
var closesEdgeStartNode = await MapDb.Nodes.FirstOrDefaultAsync(n => n.Id == closesEdge.StartNodeId);
var closesEdgeEndNode = await MapDb.Nodes.FirstOrDefaultAsync(n => n.Id == closesEdge.EndNodeId);
if (closesEdgeStartNode is not null && closesEdgeEndNode is not null)
{
var startEdge = ServerHelper.CreateEdge(map, closesEdgeStartNode.Id, endNode.Id, TrajectoryDegree.One);
var endEdge = ServerHelper.CreateEdge(map, endNode.Id, closesEdgeEndNode.Id, TrajectoryDegree.One);
await MapDb.Edges.AddAsync(startEdge);
await MapDb.Edges.AddAsync(endEdge);
MapDb.Edges.Remove(closesEdge);
RemoveEdge.Add(closesEdge.Id);
AddEdgeDto.Add(ServerHelper.CreateEdgeDto(startEdge, closesEdgeStartNode, endNode));
AddEdgeDto.Add(ServerHelper.CreateEdgeDto(endEdge, endNode, closesEdgeEndNode));
}
}
}
var edge = ServerHelper.CreateEdge(map, startNode.Id, endNode.Id, model.TrajectoryDegree, model.ControlPoint1X, model.ControlPoint1Y, model.ControlPoint2X, model.ControlPoint2Y);
await MapDb.Edges.AddAsync(edge);
await MapDb.SaveChangesAsync();
AddEdgeDto.Add(ServerHelper.CreateEdgeDto(edge, startNode, endNode));
return new(true)
{
Data = new EdgeCreateDto()
{
EdgesDto = AddEdgeDto,
RemoveEdge = RemoveEdge,
}
};
}
catch (Exception ex)
{
Logger.Warning($"CreateEdge: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"CreateEdge: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
[HttpDelete]
[Route("{id}")]
public async Task<MessageResult> DeleteEdge(Guid id)
{
using var transaction = await MapDb.Database.BeginTransactionAsync();
try
{
var edge = await MapDb.Edges.FindAsync(id);
if (edge == null) return new(false, $"Không tồn tại edge id = {id}");
MapDb.Edges.Remove(edge);
if (!MapDb.Edges.Any(e => (e.StartNodeId == edge.StartNodeId || e.EndNodeId == edge.StartNodeId) && e.Id != edge.Id))
{
var node = await MapDb.Nodes.FindAsync(edge.StartNodeId);
if (node != null)
{
var element = await MapDb.Elements.FirstOrDefaultAsync(e => e.NodeId == node.Id);
if (element is not null) MapDb.Elements.Remove(element);
MapDb.Nodes.Remove(node);
}
}
if (!MapDb.Edges.Any(e => (e.StartNodeId == edge.EndNodeId || e.EndNodeId == edge.EndNodeId) && e.Id != edge.Id))
{
var node = await MapDb.Nodes.FindAsync(edge.EndNodeId);
if (node != null)
{
var element = await MapDb.Elements.FirstOrDefaultAsync(e => e.NodeId == node.Id);
if (element is not null) MapDb.Elements.Remove(element);
MapDb.Nodes.Remove(node);
}
}
await MapDb.SaveChangesAsync();
await transaction.CommitAsync();
return new(true);
}
catch (Exception ex)
{
await transaction.RollbackAsync();
Logger.Warning($"DeleteEdge {id}: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"DeleteEdge {id}: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
[HttpDelete]
[Route("")]
public async Task<MessageResult> DeleteEdges([FromBody] IEnumerable<Guid> DeleteEdgesId)
{
try
{
List<Edge> deleteEdges = [];
List<Node> deleteNodes = [];
foreach (var edgeId in DeleteEdgesId)
{
var edge = await MapDb.Edges.FindAsync(edgeId);
if (edge == null) continue;
MapDb.Edges.Remove(edge);
if (!MapDb.Edges.Any(e => (e.StartNodeId == edge.StartNodeId || e.EndNodeId == edge.StartNodeId) && e.Id != edge.Id))
{
var node = await MapDb.Nodes.FindAsync(edge.StartNodeId);
if (node != null)
{
var element = await MapDb.Elements.FirstOrDefaultAsync(e => e.NodeId == node.Id);
if (element is not null) MapDb.Elements.Remove(element);
MapDb.Nodes.Remove(node);
}
}
if (!MapDb.Edges.Any(e => (e.StartNodeId == edge.EndNodeId || e.EndNodeId == edge.EndNodeId) && e.Id != edge.Id))
{
var node = await MapDb.Nodes.FindAsync(edge.EndNodeId);
if (node != null)
{
var element = await MapDb.Elements.FirstOrDefaultAsync(e => e.NodeId == node.Id);
if (element is not null) MapDb.Elements.Remove(element);
MapDb.Nodes.Remove(node);
}
}
await MapDb.SaveChangesAsync();
}
return new(true);
}
catch (Exception ex)
{
Logger.Warning($"DeleteEdges: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"DeleteEdges: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
[HttpPut]
[Route("")]
public async Task<MessageResult> UpdateEdge([FromBody] EdgeUpdateModel model)
{
try
{
var edge = await MapDb.Edges.FindAsync(model.Id);
if (edge == null) return new(false, $"Không tồn tại edge id = {model.Id}");
edge.MaxSpeed = model.MaxSpeed;
edge.MaxHeight = model.MaxHeight;
edge.MinHeight = model.MinHeight;
edge.ControlPoint1X = model.ControlPoint1X;
edge.ControlPoint1Y = model.ControlPoint1Y;
edge.ControlPoint2X = model.ControlPoint2X;
edge.ControlPoint2Y = model.ControlPoint2Y;
edge.DirectionAllowed = model.DirectionAllowed;
edge.RotationAllowed = model.RotationAllowed;
edge.MaxRotationSpeed = model.MaxRotationSpeed;
edge.Actions = JsonSerializer.Serialize(model.Actions ?? []);
edge.AllowedDeviationXy = model.AllowedDeviationXy;
edge.AllowedDeviationTheta = model.AllowedDeviationTheta;
await MapDb.SaveChangesAsync();
return new(true);
}
catch (Exception ex)
{
Logger.Warning($"UpdateEdge: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"UpdateEdge: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
}

View File

@ -0,0 +1,258 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RobotNet.MapManager.Data;
using RobotNet.MapManager.Services;
using RobotNet.MapShares.Dtos;
using RobotNet.Shares;
namespace RobotNet.MapManager.Controllers;
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class ElementModelsController(MapEditorDbContext MapDb, MapEditorStorageRepository MapStorage, LoggerController<ElementModelsController> Logger) : ControllerBase
{
[HttpGet]
[Route("map/{mapId}")]
public async Task<MessageResult<IEnumerable<ElementModelDto>>> Gets(Guid mapId)
{
try
{
return new(true)
{
Data = await (from elm in MapDb.ElementModels
where !string.IsNullOrEmpty(elm.Name) && elm.MapId == mapId
select new ElementModelDto()
{
Id = elm.Id,
MapId = elm.MapId,
Name = elm.Name,
Height = elm.Height,
Width = elm.Width,
Image1Height = elm.Image1Height,
Image2Height = elm.Image2Height,
Image1Width = elm.Image1Width,
Image2Width = elm.Image2Width,
Content = elm.Content,
}).ToListAsync()
};
}
catch (Exception ex)
{
Logger.Warning($"Gets: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"Gets: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
[HttpGet]
[Route("{id}")]
public async Task<MessageResult<ElementModelDto>> Get(Guid id)
{
try
{
var elmodel = await MapDb.ElementModels.FindAsync(id);
if (elmodel is null) return new(false, $"Element Model {id} không tồn tại");
return new(true)
{
Data = new ElementModelDto()
{
Id = elmodel.Id,
Name = elmodel.Name,
MapId = elmodel.MapId,
Height = elmodel.Height,
Width = elmodel.Width,
Content = elmodel.Content,
}
};
}
catch (Exception ex)
{
Logger.Warning($"Get {id}: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"Get {id}: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
[HttpPost]
public async Task<MessageResult<ElementModelDto>> CreateElementModel([FromForm] ElementModelCreateModel model, [FromForm(Name = "imageOpen")] IFormFile imageOpen, [FromForm(Name = "imageClose")] IFormFile imageClose)
{
try
{
if (model == null || imageOpen == null || imageClose == null) return new(false, "Dữ liệu đầu vào không hợp lệ");
var map = await MapDb.Maps.FindAsync(model.MapId);
if (map == null) return new(false, $"Không tồn tại map id = {model.MapId}");
if (MapDb.ElementModels.Any(elm => elm.Name == model.Name && elm.MapId == model.MapId)) return new(false, $"Tên Model {model.Name} đã tồn tại");
var image1 = SixLabors.ImageSharp.Image.Load(imageOpen.OpenReadStream());
var image2 = SixLabors.ImageSharp.Image.Load(imageClose.OpenReadStream());
var entity = MapDb.ElementModels.Add(new()
{
Name = model.Name,
MapId = model.MapId,
Width = model.Width,
Height = model.Height,
Image1Height = (ushort)image1.Height,
Image1Width = (ushort)image1.Width,
Image2Height = (ushort)image2.Height,
Image2Width = (ushort)image2.Width,
Content = "",
});
await MapDb.SaveChangesAsync();
var (isSuccess, message) = await MapStorage.UploadAsync("ElementOpenModels", $"{entity.Entity.Id}", imageOpen.OpenReadStream(), imageOpen.Length, imageOpen.ContentType, CancellationToken.None);
if (!isSuccess)
{
MapDb.ElementModels.Remove(entity.Entity);
await MapDb.SaveChangesAsync();
return new(false, message);
}
(isSuccess, message) = await MapStorage.UploadAsync("ElementCloseModels", $"{entity.Entity.Id}", imageClose.OpenReadStream(), imageClose.Length, imageClose.ContentType, CancellationToken.None);
if (!isSuccess)
{
MapDb.ElementModels.Remove(entity.Entity);
await MapDb.SaveChangesAsync();
await MapStorage.DeleteAsync("ElementOpenModels", $"{entity.Entity.Id}", CancellationToken.None);
return new(false, message);
}
return new(true)
{
Data = new()
{
Id = entity.Entity.Id,
Name = entity.Entity.Name,
MapId = entity.Entity.MapId,
Width = entity.Entity.Width,
Height = entity.Entity.Height,
Image1Height = entity.Entity.Image1Height,
Image1Width = entity.Entity.Image1Width,
Image2Height = entity.Entity.Image2Height,
Image2Width = entity.Entity.Image2Width,
Content = entity.Entity.Content
}
};
}
catch (Exception ex)
{
Logger.Warning($"CreateElementModel: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"CreateElementModel: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
[HttpPut]
public async Task<MessageResult<ElementModelDto>> Update([FromBody] ElementModelUpdateModel model)
{
try
{
var elModel = await MapDb.ElementModels.FindAsync(model.Id);
if (elModel is null) return new(false, $"Model {model.Id} không tồn tại");
if (MapDb.ElementModels.Any(elm => elm.Name == model.Name && elm.MapId == elModel.MapId && model.Id != elModel.Id)) return new(false, $"Tên Model {model.Name} đã tồn tại");
elModel.Name = model.Name;
elModel.Width = model.Width;
elModel.Height = model.Height;
elModel.Content = model.Content;
await MapDb.SaveChangesAsync();
return new(true)
{
Data = new()
{
Id = elModel.Id,
Name = elModel.Name,
MapId = elModel.MapId,
Width = elModel.Width,
Height = elModel.Height,
Image1Height = elModel.Image1Height,
Image1Width = elModel.Image1Width,
Image2Height = elModel.Image2Height,
Image2Width = elModel.Image2Width,
Content = elModel.Content
}
};
}
catch (Exception ex)
{
Logger.Warning($"Update: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"Update: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
[HttpDelete]
[Route("{id}")]
public async Task<MessageResult> Delete(Guid id)
{
try
{
var elModel = await MapDb.ElementModels.FindAsync(id);
if (elModel is null) return new(false, $"Model {id} không tồn tại");
await MapStorage.DeleteAsync("ElementOpenModels", id.ToString(), CancellationToken.None);
await MapStorage.DeleteAsync("ElementCloseModels", id.ToString(), CancellationToken.None);
MapDb.Elements.RemoveRange(MapDb.Elements.Where(e => e.ModelId == id));
MapDb.ElementModels.Remove(elModel);
await MapDb.SaveChangesAsync();
return new(true);
}
catch (Exception ex)
{
Logger.Warning($"Delete {id}: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"Delete {id}: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
[HttpPut]
[Route("openimage/{id}")]
public async Task<MessageResult> UpdateOpenImage(Guid id, [FromForm(Name = "image")] IFormFile image)
{
try
{
var elModel = await MapDb.ElementModels.FindAsync(id);
if (elModel is null) return new(false, $"Model {id} không tồn tại");
var imageStream = image.OpenReadStream();
var (isSuccess, message) = await MapStorage.UploadAsync("ElementOpenModels", $"{elModel.Id}", imageStream, image.Length, image.ContentType, CancellationToken.None);
if (!isSuccess) return new(false, message);
var imageUpdate = SixLabors.ImageSharp.Image.Load(image.OpenReadStream());
elModel.Image1Width = (ushort)imageUpdate.Width;
elModel.Image1Height = (ushort)imageUpdate.Height;
await MapDb.SaveChangesAsync();
return new(true);
}
catch (Exception ex)
{
Logger.Warning($"UpdateOpenImage {id}: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"UpdateOpenImage {id}: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
[HttpPut]
[Route("closeimage/{id}")]
public async Task<MessageResult> UpdateCloseImage(Guid id, [FromForm(Name = "image")] IFormFile image)
{
try
{
var elModel = await MapDb.ElementModels.FindAsync(id);
if (elModel is null) return new(false, $"Model {id} không tồn tại");
var imageStream = image.OpenReadStream();
var (isSuccess, message) = await MapStorage.UploadAsync("ElementCloseModels", $"{elModel.Id}", imageStream, image.Length, image.ContentType, CancellationToken.None);
if (!isSuccess) return new(false, message);
var imageUpdate = SixLabors.ImageSharp.Image.Load(image.OpenReadStream());
elModel.Image2Width = (ushort)imageUpdate.Width;
elModel.Image2Height = (ushort)imageUpdate.Height;
await MapDb.SaveChangesAsync();
return new(true);
}
catch (Exception ex)
{
Logger.Warning($"UpdateCloseImage {id}: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"UpdateCloseImage {id}: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
}

View File

@ -0,0 +1,211 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RobotNet.MapManager.Data;
using RobotNet.MapManager.Services;
using RobotNet.MapShares.Dtos;
using RobotNet.Shares;
namespace RobotNet.MapManager.Controllers;
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class ElementsController(MapEditorDbContext MapDb, LoggerController<ElementsController> Logger) : ControllerBase
{
[HttpGet]
public async Task<MessageResult<ElementDto>> GetElement([FromQuery] string mapName, [FromQuery] string elementName)
{
try
{
var map = await MapDb.Maps.FirstOrDefaultAsync(m => m.Name == mapName);
if (map is null) return new(false, $"Không tồn tại map tên = {mapName}");
var el = await MapDb.Elements.FirstOrDefaultAsync(e => e.MapId == map.Id && e.Name == elementName);
if (el is null) return new(false, $"Không tồn tại element name = {elementName}");
var elNode = await MapDb.Nodes.FindAsync(el.NodeId);
if (elNode is null) return new(false, $"Không tồn tại node id = {el.NodeId}");
return new(true)
{
Data = new()
{
Id = el.Id,
Name = el.Name,
MapId = el.MapId,
IsOpen = el.IsOpen,
NodeId = el.NodeId,
OffsetX = el.OffsetX,
OffsetY = el.OffsetY,
ModelId = el.ModelId,
Content = el.Content,
NodeName = elNode.Name,
X = elNode.X,
Y = elNode.Y,
Theta = elNode.Theta
}
};
}
catch (Exception ex)
{
Logger.Warning($"GetElement: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"GetElement: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
[HttpGet]
[Route("{modelId}")]
public async Task<MessageResult<IEnumerable<ElementDto>>> GetElementsByModelId([FromRoute] Guid modelId)
{
try
{
var elModel = await MapDb.ElementModels.FindAsync(modelId);
if (elModel is null) return new(false, $"Không tồn tại model id = {modelId}");
return new(true)
{
Data = await (from el in MapDb.Elements
where el.ModelId == modelId
select new ElementDto()
{
Id = el.Id,
MapId = el.MapId,
Name = el.Name,
ModelId = el.ModelId,
ModelName = elModel.Name,
IsOpen = el.IsOpen,
NodeId = el.NodeId,
OffsetX = el.OffsetX,
OffsetY = el.OffsetY,
Content = el.Content,
}).ToListAsync()
};
}
catch (Exception ex)
{
Logger.Warning($"GetElementsByModelId: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"GetElementsByModelId: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
[HttpPost]
public async Task<MessageResult<ElementDto>> Create([FromBody] ElementCreateModel model)
{
try
{
if (model == null || string.IsNullOrEmpty(model.Name)) return new(false, "Dữ liệu không hợp lệ");
var map = await MapDb.Maps.FindAsync(model.MapId);
if (map == null) return new(false, $"Không tồn tại map id = {model.MapId}");
var node = await MapDb.Nodes.FindAsync(model.NodeId);
if (node == null) return new(false, $"Không tồn tại node id = {model.NodeId}");
var elModel = await MapDb.ElementModels.FindAsync(model.ModelId);
if (elModel is null) return new(false, $"Không tồn tại element model id = {model.ModelId}");
if (MapDb.Elements.Any(el => el.Name == model.Name && el.MapId == model.MapId)) return new(false, $"Tên Element đã tồn tại");
if (MapDb.Elements.Any(el => el.NodeId == model.NodeId)) return new(false, $"Node này đã có Element");
var entity = await MapDb.Elements.AddAsync(new()
{
Name = model.Name,
MapId = model.MapId,
NodeId = model.NodeId,
ModelId = model.ModelId,
OffsetX = model.OffsetX,
OffsetY = model.OffsetY,
Content = elModel.Content,
});
await MapDb.SaveChangesAsync();
return new(true)
{
Data = new()
{
Id = entity.Entity.Id,
MapId = entity.Entity.MapId,
NodeId = entity.Entity.NodeId,
ModelName = elModel.Name,
Name = entity.Entity.Name,
ModelId = entity.Entity.ModelId,
OffsetX = entity.Entity.OffsetX,
OffsetY = entity.Entity.OffsetY,
Content = entity.Entity.Content,
IsOpen = entity.Entity.IsOpen,
}
};
}
catch (Exception ex)
{
Logger.Warning($"Create: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"Create: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
[HttpPut]
public async Task<MessageResult<ElementDto>> Update([FromBody] ElementUpdateModel model)
{
try
{
var element = await MapDb.Elements.FindAsync(model.Id);
if (element == null) return new(false, $"Không tồn tại element id = {model.Id}");
var map = await MapDb.Maps.FindAsync(element.MapId);
if (map == null) return new(false, $"Không tồn tại map id = {element.MapId}");
if (MapDb.Elements.Any(el => el.Name == model.Name && el.MapId == element.MapId && el.Id != model.Id)) return new(false, $"Tên Element đã tồn tại");
element.Name = model.Name;
element.OffsetX = model.OffsetX;
element.OffsetY = model.OffsetY;
element.Content = model.Content;
element.IsOpen = model.IsOpen;
await MapDb.SaveChangesAsync();
return new(true)
{
Data = new()
{
Id = element.Id,
MapId = element.MapId,
NodeId = element.NodeId,
Name = element.Name,
ModelId = element.ModelId,
OffsetX = element.OffsetX,
OffsetY = element.OffsetY,
Content = element.Content,
IsOpen = element.IsOpen,
}
};
}
catch (Exception ex)
{
Logger.Warning($"Update: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"Update: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
[HttpDelete]
[Route("{id}")]
public async Task<MessageResult> Delete(Guid id)
{
try
{
var element = await MapDb.Elements.FindAsync(id);
if (element == null) return new(false, $"Không tồn tại element id = {id}");
MapDb.Elements.Remove(element);
await MapDb.SaveChangesAsync();
return new(true);
}
catch (Exception ex)
{
Logger.Warning($"Delete {id}: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"Delete {id}: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
}

View File

@ -0,0 +1,66 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using RobotNet.MapManager.Services;
namespace RobotNet.MapManager.Controllers;
[Route("api/[controller]")]
[ApiController]
[AllowAnonymous]
public class ImagesController(MapEditorStorageRepository StorageRepo, IHttpClientFactory HttpClientFactory, LoggerController<ImagesController> Logger) : ControllerBase
{
[HttpGet]
[Route("map/{id}")]
public async Task<IActionResult> GetMapImage(Guid id)
{
try
{
var (usingLocal, url) = StorageRepo.GetUrl("MapImages", $"{id}");
if (!usingLocal)
{
var http = HttpClientFactory.CreateClient();
var imageBytes = await http.GetByteArrayAsync(url);
if (imageBytes != null && imageBytes.Length > 0) return File(imageBytes, "image/png");
else return NotFound("Không thể lấy được ảnh map.");
}
if (System.IO.File.Exists(url)) return File(System.IO.File.ReadAllBytes(url), "image/png");
else return NotFound();
}
catch(Exception ex)
{
Logger.Warning($"GetMapImage {id}: Hệ thống có lỗi xảy ra - {ex.Message}");
return NotFound();
}
}
[HttpGet]
[Route("elementModel/{id}")]
public async Task<IActionResult> GetElementModelImage(Guid id, [FromQuery] bool IsOpen)
{
try
{
var (usingLocal, url) = StorageRepo.GetUrl(IsOpen ? "ElementOpenModels" : "ElementCloseModels", id.ToString());
if (!usingLocal)
{
var http = HttpClientFactory.CreateClient();
var imageBytes = await http.GetByteArrayAsync(url);
if (imageBytes != null && imageBytes.Length > 0)
{
return File(imageBytes, "image/png");
}
else
{
return NotFound("Không thể lấy được ảnh element model.");
}
}
if (System.IO.File.Exists(url)) return File(System.IO.File.ReadAllBytes(url), "image/png");
else return NotFound();
}
catch (Exception ex)
{
Logger.Warning($"GetElementModelImage {id}: Hệ thống có lỗi xảy ra - {ex.Message}");
return NotFound();
}
}
}

View File

@ -0,0 +1,48 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using RobotNet.MapManager.Services;
namespace RobotNet.MapManager.Controllers;
[Route("api/[controller]")]
[ApiController]
[AllowAnonymous]
public class MapDesignerLoggerController(LoggerController<MapDesignerLoggerController> Logger) : ControllerBase
{
private readonly string LoggerDirectory = "mapManagerlogs";
[HttpGet]
public async Task<IEnumerable<string>> GetLogs([FromQuery(Name = "date")] DateTime date)
{
string temp = "";
try
{
string fileName = $"{date:yyyy-MM-dd}.log";
string path = Path.Combine(LoggerDirectory, fileName);
if (!Path.GetFullPath(path).StartsWith(Path.GetFullPath(LoggerDirectory)))
{
Logger.Warning($"GetLogs: phát hiện đường dẫn không hợp lệ.");
return [];
}
if (!System.IO.File.Exists(path))
{
Logger.Warning($"GetLogs: không tìm thấy file log của ngày {date.ToShortDateString()} - {path}.");
return [];
}
temp = Path.Combine(LoggerDirectory, $"{Guid.NewGuid()}.log");
System.IO.File.Copy(path, temp);
return await System.IO.File.ReadAllLinesAsync(temp);
}
catch(Exception ex)
{
Logger.Warning($"GetLogs: Hệ thống có lỗi xảy ra - {ex.Message}");
return [];
}
finally
{
if (System.IO.File.Exists(temp)) System.IO.File.Delete(temp);
}
}
}

View File

@ -0,0 +1,249 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RobotNet.MapManager.Data;
using RobotNet.MapManager.Services;
using RobotNet.MapShares;
using RobotNet.MapShares.Dtos;
using RobotNet.Shares;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace RobotNet.MapManager.Controllers;
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class MapExportController(MapEditorDbContext MapDb, MapEditorStorageRepository StorageRepo, IHttpClientFactory HttpClientFactory, LoggerController<MapExportController> Logger) : ControllerBase
{
private readonly byte[] Key = Encoding.UTF8.GetBytes("2512199802031998");
private readonly byte[] IV = Encoding.UTF8.GetBytes("2512199802031998");
[HttpGet]
[Route("encrypt/{Id}")]
public async Task<IActionResult> EncryptMap(Guid Id)
{
try
{
var map = await MapDb.Maps.FirstOrDefaultAsync(m => m.Id == Id);
if (map is null) return NotFound($"Map {Id} không tồn tại");
var (usingLocal, url) = StorageRepo.GetUrl("MapImages", $"{Id}");
byte[] imageData = await GetImageDataAsync(usingLocal, url);
var elementModels = MapDb.ElementModels.Where(n => n.MapId == Id);
List<ElementModelExportDto> ElementModelExport = [];
foreach (var elementModel in elementModels)
{
var getImageOpen = StorageRepo.GetUrl("ElementOpenModels", elementModel.Id.ToString());
byte[] imageElementModelOpenData = await GetImageDataAsync(getImageOpen.usingLocal, getImageOpen.url);
var getImageClose = StorageRepo.GetUrl("ElementCloseModels", elementModel.Id.ToString());
byte[] imageElementModelCloseData = await GetImageDataAsync(getImageClose.usingLocal, getImageClose.url);
ElementModelExport.Add(new ElementModelExportDto()
{
Id = elementModel.Id,
Height = elementModel.Height,
Width = elementModel.Width,
Image1Height = elementModel.Image1Height,
Image2Height = elementModel.Image2Height,
Image1Width = elementModel.Image1Width,
Image2Width = elementModel.Image2Width,
Name = elementModel.Name,
Content = elementModel.Content,
ImageOpenData = imageElementModelOpenData,
ImageCloseData = imageElementModelCloseData,
});
}
var nodes = MapDb.Nodes.Where(n => n.MapId == Id);
var edges = MapDb.Edges.Where(n => n.MapId == Id);
var zones = MapDb.Zones.Where(n => n.MapId == Id);
var actions = MapDb.Actions.Where(n => n.MapId == Id);
var elements = MapDb.Elements.Where(n => n.MapId == Id);
var mapDto = new MapExportDto()
{
Id = Id,
Name = map.Name,
Description = map.Description,
Info = new()
{
OriginX = map.OriginX,
OriginY = map.OriginY,
Resolution = map.Resolution,
ViewX = map.ViewX,
ViewY = map.ViewY,
ViewWidth = map.ViewWidth,
ViewHeight = map.ViewHeight,
VDA5050 = map.VDA5050,
},
Setting = new()
{
NodeNameAutoGenerate = map.NodeNameAutoGenerate,
NodeNameTemplate = map.NodeNameTemplateDefault,
NodeAllowedDeviationXy = map.EdgeAllowedDeviationXyDefault,
NodeAllowedDeviationTheta = map.EdgeAllowedDeviationThetaDefault,
EdgeStraightMaxSpeed = map.EdgeStraightMaxSpeedDefault,
EdgeCurveMaxSpeed = map.EdgeCurveMaxSpeedDefault,
EdgeMaxRotationSpeed = map.EdgeMaxRotationSpeedDefault,
EdgeMinLength = map.EdgeMinLengthDefault,
EdgeMaxHeight = map.EdgeMaxHeightDefault,
EdgeMinHeight = map.EdgeMinHeightDefault,
EdgeRotationAllowed = map.EdgeRotationAllowedDefault,
EdgeDirectionAllowed = map.EdgeDirectionAllowedDefault,
EdgeAllowedDeviationTheta = map.EdgeAllowedDeviationThetaDefault,
EdgeAllowedDeviationXy = map.EdgeAllowedDeviationXyDefault,
ZoneMinSquare = map.ZoneMinSquareDefault,
},
Data = new()
{
NodeCount = map.NodeCount,
Nodes = [.. nodes.Select(n => new NodeDto()
{
Id = n.Id,
Name = n.Name,
X = n.X,
Y = n.Y,
Theta = n.Theta,
AllowedDeviationXy = n.AllowedDeviationXy,
AllowedDeviationTheta = n.AllowedDeviationTheta,
Actions = n.Actions,
})],
Edges = [.. edges.Select(e => new EdgeDto()
{
ControlPoint1X = e.ControlPoint1X,
ControlPoint1Y = e.ControlPoint1Y,
ControlPoint2X = e.ControlPoint2X,
ControlPoint2Y = e.ControlPoint2Y,
TrajectoryDegree = e.TrajectoryDegree,
EndNodeId = e.EndNodeId,
StartNodeId = e.StartNodeId,
DirectionAllowed = e.DirectionAllowed,
RotationAllowed = e.RotationAllowed,
AllowedDeviationTheta = e.AllowedDeviationTheta,
AllowedDeviationXy = e.AllowedDeviationXy,
MaxHeight = e.MaxHeight,
MinHeight = e.MinHeight,
MaxSpeed = e.MaxSpeed,
MaxRotationSpeed = e.MaxRotationSpeed,
Actions = e.Actions,
})],
Zones = [.. zones.Select(z => new ZoneDto()
{
Type = z.Type,
X1 = z.X1,
Y1 = z.Y1,
X2 = z.X2,
Y2 = z.Y2,
X3 = z.X3,
Y3 = z.Y3,
X4 = z.X4,
Y4 = z.Y4,
})],
Actions = [.. actions.Select(a => new ActionDto()
{
Id = a.Id,
Name = a.Name,
Content = a.Content,
})],
ElementModels = [.. ElementModelExport],
Elements = [.. elements.Select(e => new ElementDto()
{
Name = e.Name,
IsOpen = e.IsOpen,
ModelId = e.ModelId,
NodeId = e.NodeId,
OffsetX = e.OffsetX,
OffsetY = e.OffsetY,
Content = e.Content,
})],
ImageData = imageData,
}
};
var jsonData = JsonSerializer.Serialize(mapDto, JsonOptionExtends.Write);
var data = EncryptDataAES(jsonData, Key, IV);
return File(data, "application/octet-stream", $"{map.Name}.map");
}
catch (Exception ex)
{
Logger.Warning($"EncryptMap: Hệ thống có lỗi xảy ra - {ex.Message}");
return NotFound("Hệ thống có lỗi xảy ra");
}
}
[HttpPost]
[Route("decrypt")]
public async Task<MessageResult<MapExportDto>> DecryptMap([FromForm(Name = "importmap")] IFormFile file)
{
try
{
if (file == null || file.Length == 0) return new(false, "File không hợp lệ");
if (!file.FileName.EndsWith(".map", StringComparison.OrdinalIgnoreCase)) return new(false, "Định dạng file không hợp lệ, yêu cầu file .map");
using var memoryStream = new MemoryStream();
await file.CopyToAsync(memoryStream);
byte[] fileBytes = memoryStream.ToArray();
var jsonData = DecryptDataAES(fileBytes, Key, IV);
var mapData = JsonSerializer.Deserialize<MapExportDto>(jsonData, JsonOptionExtends.Read);
if (mapData is null) return new(false, "Dữ liệu không hợp lệ");
else return new(true) { Data = mapData };
}
catch (Exception ex)
{
Logger.Warning($"EncryptMap: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"EncryptMap: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
private static byte[] EncryptDataAES(string data, byte[] key, byte[] iv)
{
using Aes aesAlg = Aes.Create();
aesAlg.Key = key;
aesAlg.IV = iv;
ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
using MemoryStream msEncrypt = new();
using CryptoStream csEncrypt = new(msEncrypt, encryptor, CryptoStreamMode.Write);
using (StreamWriter swEncrypt = new(csEncrypt))
{
swEncrypt.Write(data);
}
return msEncrypt.ToArray();
}
private static string DecryptDataAES(byte[] data, byte[] key, byte[] iv)
{
using Aes aesAlg = Aes.Create();
aesAlg.Key = key;
aesAlg.IV = iv;
ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
using MemoryStream msDecrypt = new(data);
using CryptoStream csDecrypt = new(msDecrypt, decryptor, CryptoStreamMode.Read);
using StreamReader srDecrypt = new(csDecrypt);
return srDecrypt.ReadToEnd();
}
public async Task<byte[]> GetImageDataAsync(bool usingLocal, string url)
{
if (!usingLocal)
{
var http = HttpClientFactory.CreateClient();
var response = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
if (!response.IsSuccessStatusCode) return [];
return await response.Content.ReadAsByteArrayAsync();
}
else
{
if (System.IO.File.Exists(url)) return System.IO.File.ReadAllBytes(url);
return [];
}
}
}

View File

@ -0,0 +1,381 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using RobotNet.MapManager.Data;
using RobotNet.MapManager.Services;
using RobotNet.MapShares;
using RobotNet.MapShares.Dtos;
using RobotNet.MapShares.Models;
using RobotNet.Shares;
using System.Text.Json;
namespace RobotNet.MapManager.Controllers;
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class MapsDataController(MapEditorDbContext MapDb, LoggerController<MapsDataController> Logger) : ControllerBase
{
[HttpGet]
[Route("{id}")]
public async Task<MessageResult<MapDataDto>> GetMapData(Guid id)
{
var map = await MapDb.Maps.FindAsync(id);
if (map is null) return new(false, $"Không tìm thấy bản đồ: {id}");
var result = new MessageResult<MapDataDto>(true)
{
Data = new()
{
Id = map.Id,
Name = map.Name,
OriginX = map.OriginX,
OriginY = map.OriginY,
Resolution = map.Resolution,
ImageHeight = map.ImageHeight,
ImageWidth = map.ImageWidth,
Active = map.Active,
Nodes = [.. MapDb.Nodes.Where(node => node.MapId == id).Select(node => new NodeDto()
{
Id = node.Id,
MapId = node.MapId,
Name = node.Name,
Theta = node.Theta,
X = node.X,
Y = node.Y,
AllowedDeviationXy = node.AllowedDeviationXy,
AllowedDeviationTheta = node.AllowedDeviationTheta,
Actions = node.Actions,
})],
Edges = [.. MapDb.Edges.Where(edge => edge.MapId == id).Select(edge => new EdgeDto()
{
Id = edge.Id,
MapId = edge.MapId,
StartNodeId = edge.StartNodeId,
EndNodeId = edge.EndNodeId,
MaxSpeed = edge.MaxSpeed,
MaxHeight = edge.MaxHeight,
MinHeight = edge.MinHeight,
DirectionAllowed = edge.DirectionAllowed,
RotationAllowed = edge.RotationAllowed,
TrajectoryDegree = edge.TrajectoryDegree,
ControlPoint1X = edge.ControlPoint1X,
ControlPoint1Y = edge.ControlPoint1Y,
ControlPoint2X = edge.ControlPoint2X,
ControlPoint2Y = edge.ControlPoint2Y,
MaxRotationSpeed = edge.MaxRotationSpeed,
Actions = edge.Actions,
AllowedDeviationXy = edge.AllowedDeviationXy,
AllowedDeviationTheta = edge.AllowedDeviationTheta,
})],
Zones = [.. MapDb.Zones.Where(zone => zone.MapId == id).Select(zone => new ZoneDto()
{
Id = zone.Id,
MapId = zone.MapId,
Type = zone.Type,
Name = zone.Name,
X1 = zone.X1,
X2 = zone.X2,
Y1 = zone.Y1,
Y2 = zone.Y2,
X3 = zone.X3,
Y3 = zone.Y3,
X4 = zone.X4,
Y4 = zone.Y4,
}).OrderBy(z => z.Type)],
Elements = [.. MapDb.Elements.Where(el => el.MapId == id).Select(element => new ElementDto()
{
Id = element.Id,
MapId = element.MapId,
ModelId = element.ModelId,
Name = element.Name,
NodeId = element.NodeId,
OffsetX = element.OffsetX,
OffsetY = element.OffsetY,
IsOpen = element.IsOpen,
Content = element.Content,
})],
Actions = [.. MapDb.Actions.Where(a => a.MapId == id).Select(action => new ActionDto()
{
Id = action.Id,
MapId = action.MapId,
Name = action.Name,
Content = action.Content,
})]
}
};
return result;
}
[HttpPut]
[Route("{id}/updates")]
public async Task<MessageResult<IEnumerable<EdgeDto>>> Updates(Guid id, MapEditorBackupModel model)
{
if (model == null || model.Steps == null) return new(false, "Dữ liệu đầu vào không hợp lệ");
try
{
var map = await MapDb.Maps.FindAsync(id);
if (map == null) return new(false, $"Không tồn tại map id = {id}");
List<EdgeDto> EdgeDtos = [];
foreach (var step in model.Steps)
{
switch (step.Type)
{
case MapEditorBackupType.Node:
PositionBackup? nodeUpdate = JsonSerializer.Deserialize<PositionBackup>(step.Obj.ToString() ?? "", JsonOptionExtends.Read);
if (nodeUpdate is not null)
{
var nodeDb = await MapDb.Nodes.FindAsync(step.Id);
if (nodeDb is not null)
{
nodeDb.X = nodeUpdate.X;
nodeDb.Y = nodeUpdate.Y;
}
}
break;
case MapEditorBackupType.ControlPoint1Edge:
PositionBackup? controlPoint1 = JsonSerializer.Deserialize<PositionBackup>(step.Obj.ToString() ?? "", JsonOptionExtends.Read);
if (controlPoint1 is not null)
{
var edgeDb = await MapDb.Edges.FindAsync(step.Id);
if (edgeDb is not null)
{
edgeDb.ControlPoint1X = controlPoint1.X;
edgeDb.ControlPoint1Y = controlPoint1.Y;
}
}
break;
case MapEditorBackupType.ControlPoint2Edge:
PositionBackup? controlPoint2 = JsonSerializer.Deserialize<PositionBackup>(step.Obj.ToString() ?? "", JsonOptionExtends.Read);
if (controlPoint2 is not null)
{
var edgeDb = await MapDb.Edges.FindAsync(step.Id);
if (edgeDb is not null)
{
edgeDb.ControlPoint2X = controlPoint2.X;
edgeDb.ControlPoint2Y = controlPoint2.Y;
}
}
break;
case MapEditorBackupType.Zone:
ZoneShapeBackup? zoneUpdate = JsonSerializer.Deserialize<ZoneShapeBackup>(step.Obj.ToString() ?? "", JsonOptionExtends.Read);
if (zoneUpdate is not null)
{
var zoneDb = await MapDb.Zones.FindAsync(step.Id);
if (zoneDb is not null)
{
zoneDb.X1 = zoneUpdate.X1;
zoneDb.Y1 = zoneUpdate.Y1;
zoneDb.X2 = zoneUpdate.X2;
zoneDb.Y2 = zoneUpdate.Y2;
zoneDb.X3 = zoneUpdate.X3;
zoneDb.Y3 = zoneUpdate.Y3;
zoneDb.X4 = zoneUpdate.X4;
zoneDb.Y4 = zoneUpdate.Y4;
}
}
break;
case MapEditorBackupType.MoveEdge:
List<EdgeBackup>? edgesUpdate = JsonSerializer.Deserialize<List<EdgeBackup>>(step.Obj.ToString() ?? "", JsonOptionExtends.Read);
if (edgesUpdate is not null && edgesUpdate.Count > 0)
{
foreach (var edgeUpate in edgesUpdate)
{
var edgeDb = await MapDb.Edges.FindAsync(edgeUpate.Id);
if (edgeDb is not null)
{
var startNode = await MapDb.Nodes.FindAsync(edgeDb.StartNodeId);
var endNode = await MapDb.Nodes.FindAsync(edgeDb.EndNodeId);
if (startNode is not null && endNode is not null)
{
startNode.X = edgeUpate.StartX;
startNode.Y = edgeUpate.StartY;
endNode.X = edgeUpate.EndX;
endNode.Y = edgeUpate.EndY;
}
edgeDb.ControlPoint1X = edgeUpate.ControlPoint1X;
edgeDb.ControlPoint1Y = edgeUpate.ControlPoint1Y;
edgeDb.ControlPoint2X = edgeUpate.ControlPoint2X;
edgeDb.ControlPoint2Y = edgeUpate.ControlPoint2Y;
}
}
}
break;
case MapEditorBackupType.Copy:
List<EdgeMapCopyModel>? edgesCopy = JsonSerializer.Deserialize<List<EdgeMapCopyModel>>(step.Obj.ToString() ?? "", JsonOptionExtends.Read);
if (edgesCopy is not null && edgesCopy.Count > 0)
{
Dictionary<Guid, Node> CreateNewNode = [];
foreach (var edgeCopy in edgesCopy)
{
if (!CreateNewNode.TryGetValue(edgeCopy.StartNodeId, out _))
{
var startNode = await MapDb.Nodes.FindAsync(edgeCopy.StartNodeId);
var newStartNode = await MapDb.Nodes.AddAsync(new Node()
{
MapId = edgeCopy.MapId,
Name = map.NodeNameAutoGenerate ? $"{map.NodeNameTemplateDefault}{++map.NodeCount}" : string.Empty,
X = edgeCopy.X1,
Y = edgeCopy.Y1,
Theta = startNode is not null ? startNode.Theta : 0,
Actions = startNode is not null ? startNode.Actions : "",
AllowedDeviationXy = startNode is not null ? startNode.AllowedDeviationXy : map.NodeAllowedDeviationXyDefault,
AllowedDeviationTheta = startNode is not null ? startNode.AllowedDeviationTheta : map.NodeAllowedDeviationThetaDefault,
});
CreateNewNode.Add(edgeCopy.StartNodeId, newStartNode.Entity);
}
if (!CreateNewNode.TryGetValue(edgeCopy.EndNodeId, out _))
{
var endNode = await MapDb.Nodes.FindAsync(edgeCopy.EndNodeId);
var newEndNode = await MapDb.Nodes.AddAsync(new Node()
{
MapId = edgeCopy.MapId,
Name = map.NodeNameAutoGenerate ? $"{map.NodeNameTemplateDefault}{++map.NodeCount}" : string.Empty,
X = edgeCopy.X2,
Y = edgeCopy.Y2,
Theta = endNode is not null ? endNode.Theta : 0,
Actions = endNode is not null ? endNode.Actions : "",
AllowedDeviationXy = endNode is not null ? endNode.AllowedDeviationXy : map.NodeAllowedDeviationXyDefault,
AllowedDeviationTheta = endNode is not null ? endNode.AllowedDeviationTheta : map.NodeAllowedDeviationThetaDefault,
});
CreateNewNode.Add(edgeCopy.EndNodeId, newEndNode.Entity);
}
var newEdge = await MapDb.Edges.AddAsync(new Edge()
{
MapId = edgeCopy.MapId,
StartNodeId = CreateNewNode[edgeCopy.StartNodeId] is null ? Guid.Empty : CreateNewNode[edgeCopy.StartNodeId].Id,
EndNodeId = CreateNewNode[edgeCopy.EndNodeId] is null ? Guid.Empty : CreateNewNode[edgeCopy.EndNodeId].Id,
TrajectoryDegree = edgeCopy.TrajectoryDegree,
ControlPoint1X = edgeCopy.ControlPoint1X,
ControlPoint1Y = edgeCopy.ControlPoint1Y,
ControlPoint2X = edgeCopy.ControlPoint2X,
ControlPoint2Y = edgeCopy.ControlPoint2Y,
DirectionAllowed = edgeCopy.DirectionAllowed,
RotationAllowed = edgeCopy.RotationAllowed,
MaxSpeed = edgeCopy.MaxSpeed,
MaxRotationSpeed = edgeCopy.MaxRotationSpeed,
MaxHeight = edgeCopy.MaxHeight,
MinHeight = edgeCopy.MinHeight,
Actions = edgeCopy.Actions,
AllowedDeviationTheta = edgeCopy.AllowedDeviationTheta,
AllowedDeviationXy = edgeCopy.AllowedDeviationXy,
});
EdgeDtos.Add(new()
{
Id = newEdge.Entity.Id,
MapId = newEdge.Entity.MapId,
StartNodeId = newEdge.Entity.StartNodeId,
EndNodeId = newEdge.Entity.EndNodeId,
TrajectoryDegree = newEdge.Entity.TrajectoryDegree,
ControlPoint1X = newEdge.Entity.ControlPoint1X,
ControlPoint1Y = newEdge.Entity.ControlPoint1Y,
ControlPoint2X = newEdge.Entity.ControlPoint2X,
ControlPoint2Y = newEdge.Entity.ControlPoint2Y,
DirectionAllowed = newEdge.Entity.DirectionAllowed,
RotationAllowed = newEdge.Entity.RotationAllowed,
MaxSpeed = newEdge.Entity.MaxSpeed,
MaxRotationSpeed = newEdge.Entity.MaxRotationSpeed,
MaxHeight = newEdge.Entity.MaxHeight,
MinHeight = newEdge.Entity.MinHeight,
Actions = newEdge.Entity.Actions,
AllowedDeviationXy = newEdge.Entity.AllowedDeviationXy,
AllowedDeviationTheta = newEdge.Entity.AllowedDeviationTheta,
StartNode = new NodeDto()
{
Id = CreateNewNode[edgeCopy.StartNodeId].Id,
Name = CreateNewNode[edgeCopy.StartNodeId].Name,
MapId = CreateNewNode[edgeCopy.StartNodeId].MapId,
Theta = CreateNewNode[edgeCopy.StartNodeId].Theta,
X = CreateNewNode[edgeCopy.StartNodeId].X,
Y = CreateNewNode[edgeCopy.StartNodeId].Y,
AllowedDeviationXy = CreateNewNode[edgeCopy.StartNodeId].AllowedDeviationXy,
AllowedDeviationTheta = CreateNewNode[edgeCopy.StartNodeId].AllowedDeviationTheta,
Actions = CreateNewNode[edgeCopy.StartNodeId].Actions,
},
EndNode = new NodeDto()
{
Id = CreateNewNode[edgeCopy.EndNodeId].Id,
Name = CreateNewNode[edgeCopy.EndNodeId].Name,
MapId = CreateNewNode[edgeCopy.EndNodeId].MapId,
Theta = CreateNewNode[edgeCopy.EndNodeId].Theta,
X = CreateNewNode[edgeCopy.EndNodeId].X,
Y = CreateNewNode[edgeCopy.EndNodeId].Y,
AllowedDeviationXy = CreateNewNode[edgeCopy.EndNodeId].AllowedDeviationXy,
AllowedDeviationTheta = CreateNewNode[edgeCopy.EndNodeId].AllowedDeviationTheta,
Actions = CreateNewNode[edgeCopy.EndNodeId].Actions,
},
});
}
}
break;
case MapEditorBackupType.SplitNode:
var nodeSplit = await MapDb.Nodes.FindAsync(step.Id);
if (nodeSplit is not null)
{
SplitNodeBackup? SplitNodeUpdate = JsonSerializer.Deserialize<SplitNodeBackup>(step.Obj.ToString() ?? "", JsonOptionExtends.Read);
if (SplitNodeUpdate is not null)
{
foreach (var data in SplitNodeUpdate.EdgeSplit)
{
var edge = await MapDb.Edges.FindAsync(data.Key);
if (edge is not null)
{
var newNode = new Node()
{
Id = data.Value.Id,
Name = data.Value.Name,
X = data.Value.X,
Y = data.Value.Y,
Theta = data.Value.Theta,
MapId = data.Value.MapId,
AllowedDeviationXy = data.Value.AllowedDeviationXy,
AllowedDeviationTheta = data.Value.AllowedDeviationTheta,
Actions = data.Value.Actions,
};
if (edge.StartNodeId == nodeSplit.Id) edge.StartNodeId = newNode.Id;
else if (edge.EndNodeId == nodeSplit.Id) edge.EndNodeId = newNode.Id;
else continue;
await MapDb.Nodes.AddAsync(newNode);
}
}
}
}
break;
case MapEditorBackupType.MergeNode:
var nodemerge = await MapDb.Nodes.FindAsync(step.Id);
if (nodemerge is not null)
{
MergeNodeUpdate? MergeNodeUpdate = JsonSerializer.Deserialize<MergeNodeUpdate>(step.Obj.ToString() ?? "", JsonOptionExtends.Read);
if (MergeNodeUpdate is not null)
{
foreach (var data in MergeNodeUpdate.EdgesMerge)
{
var edge = await MapDb.Edges.FindAsync(data.Key);
if (edge is not null)
{
var rmNode = await MapDb.Nodes.FindAsync(data.Value);
if (edge.StartNodeId == data.Value) edge.StartNodeId = nodemerge.Id;
else if (edge.EndNodeId == data.Value) edge.EndNodeId = nodemerge.Id;
if (rmNode is not null) MapDb.Nodes.Remove(rmNode);
}
}
}
}
break;
}
}
await MapDb.SaveChangesAsync();
Logger.Info($"User {HttpContext.User.Identity?.Name} đã cập nhật dữ liệu cho bản đồ: {map.Name} - {map.Id}");
return new(true) { Data = EdgeDtos };
}
catch (Exception ex)
{
Logger.Warning($"Updates: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, "Hệ thống có lỗi xảy ra");
}
}
}

View File

@ -0,0 +1,571 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using RobotNet.MapManager.Data;
using RobotNet.MapManager.Hubs;
using RobotNet.MapManager.Services;
using RobotNet.MapShares.Dtos;
using RobotNet.MapShares.Enums;
using RobotNet.Shares;
namespace RobotNet.MapManager.Controllers;
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class MapsManagerController(MapEditorDbContext MapDb, MapEditorStorageRepository StorageRepo, IHubContext<MapHub> MapHub, LoggerController<MapsManagerController> Logger) : ControllerBase
{
[HttpGet]
public async Task<IEnumerable<MapInfoDto>> GetMapInfos([FromQuery(Name = "txtSearch")] string? txtSearch)
{
try
{
if (string.IsNullOrWhiteSpace(txtSearch))
{
return await (from map in MapDb.Maps
select new MapInfoDto()
{
Id = map.Id,
VersionId = map.VersionId,
Name = map.Name,
Description = map.Description,
Active = map.Active,
OriginX = map.OriginX,
OriginY = map.OriginY,
Width = Math.Round(map.ImageWidth * map.Resolution, 2),
Height = Math.Round(map.ImageHeight * map.Resolution, 2),
Resolution = map.Resolution,
VDA5050 = map.VDA5050,
}).ToListAsync();
}
else
{
return await (from map in MapDb.Maps
where !string.IsNullOrEmpty(map.Name) && map.Name.Contains(txtSearch)
select new MapInfoDto()
{
Id = map.Id,
VersionId = map.VersionId,
Name = map.Name,
Description = map.Description,
Active = map.Active,
OriginX = map.OriginX,
OriginY = map.OriginY,
Width = Math.Round(map.ImageWidth * map.Resolution, 2),
Height = Math.Round(map.ImageHeight * map.Resolution, 2),
Resolution = map.Resolution,
VDA5050 = map.VDA5050,
}).ToListAsync();
}
}
catch (Exception ex)
{
Logger.Warning($"GetMapInfos: Hệ thống có lỗi xảy ra - {ex.Message}");
return [];
}
}
[HttpGet]
[Route("{id}")]
public async Task<MessageResult<MapInfoDto>> GetMapInfoId(Guid id)
{
var map = await MapDb.Maps.FirstOrDefaultAsync(m => m.Id == id);
if (map == null) return new MessageResult<MapInfoDto>(false, $"Không tìm thấy map {id}");
return new(true)
{
Data = new MapInfoDto()
{
Id = map.Id,
Name = map.Name,
Active = map.Active,
OriginX = map.OriginX,
OriginY = map.OriginY,
Width = Math.Round(map.ImageWidth * map.Resolution, 2),
Height = Math.Round(map.ImageHeight * map.Resolution, 2),
Resolution = map.Resolution,
},
};
}
[HttpGet]
[Route("info")]
public async Task<MessageResult<MapInfoDto>> GetMapInfoName([FromQuery(Name = "name")] string name)
{
var map = await MapDb.Maps.FirstOrDefaultAsync(m => m.Name == name);
if (map == null) return new MessageResult<MapInfoDto>(false, $"Không tìm thấy map {name}");
return new(true)
{
Data = new MapInfoDto()
{
Id = map.Id,
Name = map.Name,
Active = map.Active,
OriginX = map.OriginX,
OriginY = map.OriginY,
Width = Math.Round(map.ImageWidth * map.Resolution, 2),
Height = Math.Round(map.ImageHeight * map.Resolution, 2),
Resolution = map.Resolution,
},
};
}
[HttpPost]
[Route("")]
public async Task<MessageResult<MapInfoDto>> CreateMap([FromForm] MapCreateModel model, [FromForm(Name = "Image")] IFormFile imageUpload)
{
try
{
if (imageUpload is null) return new(false, "Dữ liệu không hợp lệ");
if (imageUpload.ContentType != "image/png") return new(false, "Ảnh map chỉ hỗ trợ định dạng image/png");
if (await MapDb.Maps.AnyAsync(map => map.Name == model.Name)) return new(false, "Tên của map đã tồn tại. Hãy đặt tên khác!");
var image = SixLabors.ImageSharp.Image.Load(imageUpload.OpenReadStream());
var entityMap = await MapDb.Maps.AddAsync(new Map()
{
Name = model.Name,
OriginX = model.OriginX,
OriginY = model.OriginY,
Resolution = model.Resolution,
ImageHeight = (ushort)image.Height,
ImageWidth = (ushort)image.Width,
Active = false,
NodeCount = 0,
NodeNameAutoGenerate = true,
NodeNameTemplateDefault = "Node",
NodeAllowedDeviationXyDefault = 0.1,
NodeAllowedDeviationThetaDefault = 0,
EdgeMinLengthDefault = 1,
EdgeMaxHeightDefault = 1,
EdgeMinHeightDefault = 0.1,
EdgeStraightMaxSpeedDefault = 1,
EdgeCurveMaxSpeedDefault = 0.3,
EdgeMaxRotationSpeedDefault = 0.5,
EdgeAllowedDeviationXyDefault = 0.1,
EdgeAllowedDeviationThetaDefault = 0,
EdgeRotationAllowedDefault = true,
EdgeDirectionAllowedDefault = DirectionAllowed.Both,
ZoneMinSquareDefault = 0.25,
});
await MapDb.SaveChangesAsync();
var (isSuccess, message) = await StorageRepo.UploadAsync("MapImages", $"{entityMap.Entity.Id}", imageUpload.OpenReadStream(), imageUpload.Length, imageUpload.ContentType, CancellationToken.None);
if (!isSuccess)
{
MapDb.Maps.Remove(entityMap.Entity);
await MapDb.SaveChangesAsync();
return new(false, message);
}
await MapDb.SaveChangesAsync();
Logger.Info($"User {HttpContext.User.Identity?.Name} đã tạo bản đồ mới với tên: {model.Name}");
return new(true)
{
Data = new MapInfoDto()
{
Id = entityMap.Entity.Id,
Name = entityMap.Entity.Name,
Active = entityMap.Entity.Active,
OriginX = entityMap.Entity.OriginX,
OriginY = entityMap.Entity.OriginY,
Width = entityMap.Entity.ImageWidth * entityMap.Entity.Resolution,
Height = entityMap.Entity.ImageHeight * entityMap.Entity.Resolution,
Resolution = entityMap.Entity.Resolution,
},
};
}
catch (Exception ex)
{
Logger.Warning($"CreateMap: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"CreateMap: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
[HttpPut]
[Route("{id}")]
public async Task<MessageResult<MapInfoDto>> UpdateMap(Guid id, [FromBody] MapUpdateModel model)
{
try
{
if (id != model.Id) return new(false, "Dữ liệu gửi không chính xác");
var map = await MapDb.Maps.FindAsync(id);
if (map == null) return new(false, $"Không tồn tại map id = {id}");
if (map.Name != model.Name && await MapDb.Maps.AnyAsync(m => m.Name == model.Name))
{
return new(false, "Tên của map đã tồn tại, Hãy đặt tên khác!");
}
if (model.Resolution <= 0)
{
return new(false, "Độ phân giải của bản đồ phải lớn hơn 0");
}
map.Name = model.Name;
bool originChange = map.OriginX != model.OriginX || map.OriginY != model.OriginY;
if (originChange)
{
map.OriginX = model.OriginX;
map.OriginY = model.OriginY;
}
if (map.Resolution != model.Resolution)
{
var scale = model.Resolution / map.Resolution;
map.Resolution = model.Resolution;
if (originChange)
{
map.OriginX *= scale;
map.OriginY *= scale;
}
var nodes = await MapDb.Nodes.Where(n => n.MapId == map.Id).ToListAsync();
foreach (var node in nodes)
{
node.X *= scale;
node.Y *= scale;
}
var edges = await MapDb.Edges.Where(e => e.MapId == map.Id).ToListAsync();
foreach (var edge in edges)
{
edge.ControlPoint1X *= scale;
edge.ControlPoint1Y *= scale;
edge.ControlPoint2X *= scale;
edge.ControlPoint2Y *= scale;
}
}
await MapDb.SaveChangesAsync();
Logger.Info($"User {HttpContext.User.Identity?.Name} đã cập nhật thông tin bản đồ : {model.Id} - {map.Name}");
return new(true)
{
Data = new()
{
Id = id,
Name = map.Name,
OriginX = map.OriginX,
OriginY = map.OriginY,
Resolution = model.Resolution,
Width = Math.Round(map.ImageWidth * map.Resolution, 2),
Height = Math.Round(map.ImageHeight * map.Resolution, 2),
},
};
}
catch (Exception ex)
{
Logger.Warning($"UpdateMap {id}: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"UpdateMap {id}: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
[HttpDelete]
[Route("{id}")]
public async Task<MessageResult> DeleteMap(Guid id)
{
try
{
var map = await MapDb.Maps.FindAsync(id);
if (map == null) return new(false, $"Không tồn tại map id = {id}");
MapDb.Elements.RemoveRange(MapDb.Elements.Where(e => e.MapId == map.Id));
MapDb.ElementModels.RemoveRange(MapDb.ElementModels.Where(em => em.MapId == map.Id));
MapDb.Edges.RemoveRange(MapDb.Edges.Where(edge => edge.MapId == map.Id));
MapDb.Nodes.RemoveRange(MapDb.Nodes.Where(node => node.MapId == map.Id));
MapDb.Zones.RemoveRange(MapDb.Zones.Where(zone => zone.MapId == map.Id));
MapDb.Actions.RemoveRange(MapDb.Actions.Where(action => action.MapId == map.Id));
MapDb.Maps.Remove(map);
await MapDb.SaveChangesAsync();
await StorageRepo.DeleteAsync("MapImages", $"{id}", CancellationToken.None);
Logger.Info($"User {HttpContext.User.Identity?.Name} đã xóa bản đồ {map.Name}");
return new(true);
}
catch (Exception ex)
{
Logger.Warning($"DeleteMap {id}: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"DeleteMap {id}: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
[HttpPut]
[Route("active")]
public async Task<MessageResult> ActiveToggle([FromBody] MapActiveModel model)
{
var map = await MapDb.Maps.FindAsync(model.Id);
if (map == null) return new(false, $"Không tồn tại map id = {model.Id}");
map.Active = model.Active;
await MapDb.SaveChangesAsync();
await MapHub.Clients.All.SendAsync("MapUpdated", model.Id);
Logger.Info($"User {HttpContext.User.Identity?.Name} đã thay đổi trạng thái active bản đồ {map.Name}: {model.Active}");
return new(true);
}
[HttpPut]
[Route("image/{id}")]
public async Task<MessageResult> UpdateImage(Guid id, [FromForm(Name = "image")] IFormFile image)
{
try
{
var map = await MapDb.Maps.FindAsync(id);
if (map == null) return new(false, $"Không tồn tại map id = {id}");
var imageStream = image.OpenReadStream();
var imageUpdate = SixLabors.ImageSharp.Image.Load(imageStream);
map.ImageWidth = (ushort)imageUpdate.Width;
map.ImageHeight = (ushort)imageUpdate.Height;
await MapDb.SaveChangesAsync();
var (isSuccess, message) = await StorageRepo.UploadAsync("MapImages", $"{id}", image.OpenReadStream(), image.Length, image.ContentType, CancellationToken.None);
Logger.Info($"User {HttpContext.User.Identity?.Name} đã thay đổi ảnh của bản đồ {map.Name}");
return new(true);
}
catch (Exception ex)
{
Logger.Warning($"UpdateImage {id}: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"UpdateImage {id}: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
[HttpPost]
[Route("import")]
public async Task<MessageResult<MapInfoDto>> ImportNewMap([FromBody] MapExportDto model)
{
if (model == null || model.Data == null || model.Data.ImageData == null || model.Data.ImageData.Length == 0)
{
return new(false, "Dữ liệu đầu vào không hợp lệ");
}
using var transaction = await MapDb.Database.BeginTransactionAsync();
try
{
if (await MapDb.Maps.AnyAsync(map => map.Name == model.Name)) return new(false, "Tên của map đã tồn tại, Hãy đặt tên khác!");
using var stream = new MemoryStream(model.Data.ImageData);
var imageFile = new FormFile(stream, 0, model.Data.ImageData.Length, "", $"{model.Name}.png")
{
Headers = new HeaderDictionary(),
ContentType = "image/png",
};
var image = SixLabors.ImageSharp.Image.Load(imageFile.OpenReadStream());
var entityMap = await MapDb.Maps.AddAsync(new Map()
{
Name = model.Name,
Description = model.Description,
OriginX = model.Info.OriginX,
OriginY = model.Info.OriginY,
Resolution = model.Info.Resolution,
ImageHeight = (ushort)image.Height,
ImageWidth = (ushort)image.Width,
Active = false,
ViewX = model.Info.ViewX,
ViewY = model.Info.ViewY,
ViewWidth = model.Info.ViewWidth,
ViewHeight = model.Info.ViewHeight,
VDA5050 = model.Info.VDA5050,
NodeCount = model.Data.NodeCount,
NodeNameAutoGenerate = model.Setting.NodeNameAutoGenerate,
NodeNameTemplateDefault = model.Setting.NodeNameTemplate,
NodeAllowedDeviationXyDefault = model.Setting.NodeAllowedDeviationXy,
NodeAllowedDeviationThetaDefault = model.Setting.NodeAllowedDeviationTheta,
EdgeMinLengthDefault = model.Setting.EdgeMinLength,
EdgeMaxHeightDefault = model.Setting.EdgeMaxHeight,
EdgeMinHeightDefault = model.Setting.EdgeMinHeight,
EdgeStraightMaxSpeedDefault = model.Setting.EdgeStraightMaxSpeed,
EdgeCurveMaxSpeedDefault = model.Setting.EdgeCurveMaxSpeed,
EdgeMaxRotationSpeedDefault = model.Setting.EdgeMaxRotationSpeed,
EdgeAllowedDeviationXyDefault = model.Setting.EdgeAllowedDeviationXy,
EdgeAllowedDeviationThetaDefault = model.Setting.EdgeAllowedDeviationTheta,
EdgeRotationAllowedDefault = model.Setting.EdgeRotationAllowed,
EdgeDirectionAllowedDefault = model.Setting.EdgeDirectionAllowed,
ZoneMinSquareDefault = model.Setting.ZoneMinSquare,
});
var (isSuccess, message) = await StorageRepo.UploadAsync("MapImages", $"{entityMap.Entity.Id}", imageFile.OpenReadStream(), imageFile.Length, imageFile.ContentType, CancellationToken.None);
if (!isSuccess)
{
await transaction.RollbackAsync();
return new(false, message);
}
Dictionary<Guid, Guid> actionSwap = [];
foreach (var action in model.Data.Actions)
{
var actionDb = await MapDb.Actions.AddAsync(new Data.Action()
{
MapId = entityMap.Entity.Id,
Name = action.Name,
Content = action.Content,
});
actionSwap.Add(action.Id, actionDb.Entity.Id);
}
Dictionary<Guid, Guid> nodeSwap = [];
foreach (var node in model.Data.Nodes)
{
var actions = System.Text.Json.JsonSerializer.Deserialize<Guid[]>(node.Actions ?? "");
List<Guid> newActions = [];
if (actions is not null && actions.Length > 0)
{
foreach (var actionId in actions)
{
if (actionSwap.TryGetValue(actionId, out Guid newActionId) && newActionId != Guid.Empty) newActions.Add(newActionId);
}
}
var nodeDb = await MapDb.Nodes.AddAsync(new Node()
{
Name = node.Name,
MapId = entityMap.Entity.Id,
X = node.X,
Y = node.Y,
Theta = node.Theta,
AllowedDeviationXy = node.AllowedDeviationXy,
AllowedDeviationTheta = node.AllowedDeviationTheta,
Actions = System.Text.Json.JsonSerializer.Serialize(newActions),
});
nodeSwap.Add(node.Id, nodeDb.Entity.Id);
}
var Edges = model.Data.Edges.Select(e => new Edge()
{
MapId = entityMap.Entity.Id,
StartNodeId = nodeSwap[e.StartNodeId],
EndNodeId = nodeSwap[e.EndNodeId],
ControlPoint1X = e.ControlPoint1X,
ControlPoint1Y = e.ControlPoint1Y,
ControlPoint2X = e.ControlPoint2X,
ControlPoint2Y = e.ControlPoint2Y,
TrajectoryDegree = e.TrajectoryDegree,
MaxHeight = e.MaxHeight,
MinHeight = e.MinHeight,
MaxSpeed = e.MaxSpeed,
MaxRotationSpeed = e.MaxRotationSpeed,
AllowedDeviationXy = e.AllowedDeviationXy,
AllowedDeviationTheta = e.AllowedDeviationTheta,
DirectionAllowed = e.DirectionAllowed,
RotationAllowed = e.RotationAllowed,
Actions = e.Actions,
}).ToList();
var Zones = model.Data.Zones.Select(z => new Zone()
{
MapId = entityMap.Entity.Id,
Type = z.Type,
X1 = z.X1,
X2 = z.X2,
Y1 = z.Y1,
Y2 = z.Y2,
X3 = z.X3,
X4 = z.X4,
Y3 = z.Y3,
Y4 = z.Y4,
}).ToList();
Dictionary<Guid, Guid> elementModelSwap = [];
foreach (var elementModel in model.Data.ElementModels)
{
var elementModelDb = await MapDb.ElementModels.AddAsync(new ElementModel()
{
MapId = entityMap.Entity.Id,
Name = elementModel.Name,
Height = elementModel.Height,
Width = elementModel.Width,
Image1Height = elementModel.Image1Height,
Image1Width = elementModel.Image1Width,
Image2Height = elementModel.Image2Height,
Image2Width = elementModel.Image2Width,
Content = elementModel.Content,
});
elementModelSwap.Add(elementModel.Id, elementModelDb.Entity.Id);
using var openStream = new MemoryStream(elementModel.ImageOpenData);
var imageOpenFile = new FormFile(openStream, 0, elementModel.ImageOpenData.Length, "", $"{elementModel.Name}O.png")
{
Headers = new HeaderDictionary(),
ContentType = "image/png",
};
await StorageRepo.UploadAsync("ElementOpenModels", $"{elementModelDb.Entity.Id}", imageOpenFile.OpenReadStream(), imageOpenFile.Length, imageOpenFile.ContentType, CancellationToken.None);
using var closeStream = new MemoryStream(elementModel.ImageCloseData);
var imageCloseFile = new FormFile(closeStream, 0, elementModel.ImageCloseData.Length, "", $"{elementModel.Name}C.png")
{
Headers = new HeaderDictionary(),
ContentType = "image/png",
};
await StorageRepo.UploadAsync("ElementCloseModels", $"{elementModelDb.Entity.Id}", imageCloseFile.OpenReadStream(), imageCloseFile.Length, imageCloseFile.ContentType, CancellationToken.None);
}
var Elements = model.Data.Elements.Select(e => new Data.Element()
{
MapId = entityMap.Entity.Id,
Name = e.Name,
IsOpen = e.IsOpen,
ModelId = elementModelSwap[e.ModelId],
Content = e.Content,
NodeId = nodeSwap[e.NodeId],
OffsetX = e.OffsetX,
OffsetY = e.OffsetY,
}).ToList();
if (Edges.Count > 0) await MapDb.Edges.AddRangeAsync(Edges);
if (Zones.Count > 0) await MapDb.Zones.AddRangeAsync(Zones);
if (Elements.Count > 0) await MapDb.Elements.AddRangeAsync(Elements);
await MapDb.SaveChangesAsync();
await transaction.CommitAsync();
return new(true)
{
Data = new MapInfoDto()
{
Id = entityMap.Entity.Id,
Name = entityMap.Entity.Name,
Active = entityMap.Entity.Active,
OriginX = entityMap.Entity.OriginX,
OriginY = entityMap.Entity.OriginY,
Width = entityMap.Entity.ImageWidth * entityMap.Entity.Resolution,
Height = entityMap.Entity.ImageHeight * entityMap.Entity.Resolution,
Resolution = entityMap.Entity.Resolution,
}
};
}
catch (IOException ex)
{
await transaction.RollbackAsync();
Logger.Warning($"ImportNewMap: Lỗi khi xử lý hình ảnh: {ex.Message}");
return new(false, $"ImportNewMap: Lỗi khi xử lý hình ảnh: {ex.Message}");
}
catch (DbUpdateException ex)
{
await transaction.RollbackAsync();
Logger.Warning($"ImportNewMap: Lỗi khi lưu vào database: {ex.Message}");
return new(false, $"ImportNewMap: Lỗi khi lưu vào database: {ex.Message}");
}
catch (Exception ex)
{
await transaction.RollbackAsync();
Logger.Warning($"ImportNewMap: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"ImportNewMap: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
}

View File

@ -0,0 +1,79 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RobotNet.MapManager.Data;
using RobotNet.MapShares.Dtos;
using RobotNet.Shares;
namespace RobotNet.MapManager.Controllers;
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class MapsSettingController(MapEditorDbContext MapDb) : ControllerBase
{
[HttpGet]
[Route("{id}")]
public async Task<MessageResult<MapSettingDefaultDto>> GetMapSetting(Guid id)
{
var map = await MapDb.Maps.FirstOrDefaultAsync(m => m.Id == id);
if (map == null) return new(false, $"Không tìm thấy map {id}");
return new(true)
{
Data = new MapSettingDefaultDto()
{
Id = map.Id,
EdgeStraightMaxSpeed = map.EdgeStraightMaxSpeedDefault,
EdgeCurveMaxSpeed = map.EdgeCurveMaxSpeedDefault,
EdgeMaxHeight = map.EdgeMaxHeightDefault,
EdgeMinHeight = map.EdgeMinHeightDefault,
EdgeMinLength = map.EdgeMinLengthDefault,
EdgeDirectionAllowed = map.EdgeDirectionAllowedDefault,
EdgeMaxRotationSpeed = map.EdgeMaxRotationSpeedDefault,
EdgeRotationAllowed = map.EdgeRotationAllowedDefault,
EdgeAllowedDeviationXy = map.EdgeAllowedDeviationXyDefault,
EdgeAllowedDeviationTheta = map.EdgeAllowedDeviationThetaDefault,
NodeAllowedDeviationTheta = map.NodeAllowedDeviationThetaDefault,
NodeAllowedDeviationXy = map.NodeAllowedDeviationXyDefault,
NodeNameAutoGenerate = map.NodeNameAutoGenerate,
NodeNameTemplate = map.NodeNameTemplateDefault,
ZoneMinSquare = map.ZoneMinSquareDefault,
},
};
}
[HttpPut]
[Route("")]
public async Task<MessageResult> Update([FromBody] MapSettingDefaultDto mapSetting)
{
var map = await MapDb.Maps.FirstOrDefaultAsync(m => m.Id == mapSetting.Id);
if (map == null) return new(false, $"Không tìm thấy map {mapSetting.Id}");
map.EdgeStraightMaxSpeedDefault = mapSetting.EdgeStraightMaxSpeed;
map.EdgeCurveMaxSpeedDefault = mapSetting.EdgeCurveMaxSpeed;
map.EdgeMaxHeightDefault = mapSetting.EdgeMaxHeight;
map.EdgeMinHeightDefault = mapSetting.EdgeMinHeight;
map.EdgeMinLengthDefault = mapSetting.EdgeMinLength;
map.EdgeDirectionAllowedDefault = mapSetting.EdgeDirectionAllowed;
map.EdgeMaxRotationSpeedDefault = mapSetting.EdgeMaxRotationSpeed;
map.EdgeRotationAllowedDefault = mapSetting.EdgeRotationAllowed;
map.EdgeAllowedDeviationXyDefault = mapSetting.EdgeAllowedDeviationXy;
map.EdgeAllowedDeviationThetaDefault = mapSetting.EdgeAllowedDeviationTheta;
map.NodeAllowedDeviationThetaDefault = mapSetting.NodeAllowedDeviationTheta;
map.NodeAllowedDeviationXyDefault = mapSetting.NodeAllowedDeviationXy;
map.NodeNameAutoGenerate = mapSetting.NodeNameAutoGenerate;
map.NodeNameTemplateDefault = mapSetting.NodeNameTemplate;
map.ZoneMinSquareDefault = mapSetting.ZoneMinSquare;
MapDb.Maps.Update(map);
await MapDb.SaveChangesAsync();
return new(true);
}
}

View File

@ -0,0 +1,58 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RobotNet.MapManager.Data;
using RobotNet.MapShares.Dtos;
using RobotNet.Shares;
namespace RobotNet.MapManager.Controllers;
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class NodesController(MapEditorDbContext MapDb) : ControllerBase
{
[HttpGet]
[Route("{mapId}")]
public async Task<NodeDto[]> GetNodes(Guid mapId)
{
return await (from node in MapDb.Nodes
where node.MapId == mapId
select new NodeDto()
{
Id = node.Id,
Name = node.Name,
MapId = mapId,
X = node.X,
Y = node.Y,
Theta = node.Theta,
AllowedDeviationXy = node.AllowedDeviationXy,
AllowedDeviationTheta = node.AllowedDeviationTheta,
Actions = node.Actions,
}).ToArrayAsync();
}
[HttpPut]
[Route("")]
public async Task<MessageResult> Update([FromBody] NodeUpdateModel model)
{
var node = await MapDb.Nodes.FindAsync(model.Id);
if (node == null) return new(false, $"Không tồn tại node id = {model.Id}");
if (node.Name != model.Name && !string.IsNullOrWhiteSpace(model.Name) && await MapDb.Nodes.AnyAsync(n => n.Name == model.Name && n.MapId == node.MapId))
{
return new(false, $"Tên node {model.Name} đã tồn tại trong map");
}
node.Name = model.Name;
node.X = model.X;
node.Y = model.Y;
node.Theta = model.Theta;
node.AllowedDeviationXy = model.AllowedDeviationXy;
node.AllowedDeviationTheta = model.AllowedDeviationTheta;
node.Actions = System.Text.Json.JsonSerializer.Serialize(model.Actions ?? []);
await MapDb.SaveChangesAsync();
return new(true);
}
}

View File

@ -0,0 +1,232 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RobotNet.MapManager.Data;
using RobotNet.MapManager.Services;
using RobotNet.MapShares;
using RobotNet.MapShares.Dtos;
using RobotNet.MapShares.Models;
using RobotNet.MapShares.Property;
using RobotNet.Shares;
using Serialize.Linq.Serializers;
using System.Linq.Expressions;
namespace RobotNet.MapManager.Controllers;
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class ScriptElementsController(MapEditorDbContext MapDb, LoggerController<ScriptElementsController> Logger) : ControllerBase
{
private static readonly ExpressionSerializer expressionSerializer;
static ScriptElementsController()
{
var jss = new Serialize.Linq.Serializers.JsonSerializer();
expressionSerializer = new ExpressionSerializer(jss);
expressionSerializer.AddKnownType(typeof(Script.Expressions.ElementProperties));
}
[HttpPost]
public async Task<MessageResult<IEnumerable<ElementDto>>> GetElementsWithCondition([FromBody] ElementExpressionModel model)
{
try
{
var map = await MapDb.Maps.FirstOrDefaultAsync(m => m.Name == model.MapName);
if (map is null) return new(false, $"Không tồn tại map tên = {model.MapName}");
var elModel = await MapDb.ElementModels.FirstOrDefaultAsync(m => m.MapId == map.Id && m.Name == model.ModelName);
if (elModel is null) return new(false, $"Không tồn tại element model tên = {model.ModelName}");
var modelProperties = System.Text.Json.JsonSerializer.Deserialize<List<ElementProperty>>(elModel.Content, JsonOptionExtends.Read);
if (modelProperties is null || modelProperties.Count == 0)
return new(false, $"Không tồn tại property nào trong element model tên = {model.ModelName}");
var expr = expressionSerializer.DeserializeText(model.Expression);
var lambda = (Expression<Func<Script.Expressions.ElementProperties, bool>>)expr;
// Compile và chạy:
var func = lambda.Compile();
var elements = await MapDb.Elements.Where(e => e.MapId == map.Id && e.ModelId == elModel.Id).ToListAsync();
List<ElementDto> elementSatisfies = [];
foreach (var element in elements)
{
var properties = MapManagerExtensions.GetElementProperties(element.IsOpen, element.Content);
if (func.Invoke(properties))
{
var elNode = await MapDb.Nodes.FindAsync(element.NodeId);
if (elNode is null) continue; // Bỏ qua nếu không tìm thấy node
elementSatisfies.Add(new ElementDto()
{
Id = element.Id,
Name = element.Name,
MapId = element.MapId,
IsOpen = element.IsOpen,
NodeId = element.NodeId,
OffsetX = element.OffsetX,
OffsetY = element.OffsetY,
ModelId = element.ModelId,
Content = element.Content,
NodeName = elNode.Name,
ModelName = elModel.Name,
X = elNode.X,
Y = elNode.Y,
Theta = elNode.Theta
});
}
}
return new(true)
{
Data = elementSatisfies
};
}
catch (Exception ex)
{
Logger.Warning($"GetElement: Hệ thống có lỗi xảy ra - {ex}");
return new(false, $"Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
[HttpGet]
[Route("{mapName}/node/{nodeName}")]
public async Task<MessageResult<NodeDto>> GetNode([FromRoute] string mapName, [FromRoute] string nodeName)
{
try
{
var map = await MapDb.Maps.FirstOrDefaultAsync(m => m.Name == mapName);
if (map is null) return new(false, $"Không tồn tại map tên = {mapName}");
var node = await MapDb.Nodes.FirstOrDefaultAsync(n => n.MapId == map.Id && n.Name == nodeName);
if (node is null) return new(false, $"Không tồn tại node {nodeName} trong map {mapName}");
return new(true)
{
Data = new NodeDto()
{
Id = node.Id,
Name = node.Name,
MapId = node.MapId,
X = node.X,
Y = node.Y,
Theta = node.Theta,
AllowedDeviationXy = node.AllowedDeviationXy,
AllowedDeviationTheta = node.AllowedDeviationTheta,
Actions = node.Actions,
}
};
}
catch (Exception ex)
{
Logger.Warning($"GetElement: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"GetElement: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
[HttpGet]
[Route("{mapName}/element/{elementName}")]
public async Task<MessageResult<ElementDto>> GetElement([FromRoute] string mapName, [FromRoute] string elementName)
{
try
{
var map = await MapDb.Maps.FirstOrDefaultAsync(m => m.Name == mapName);
if (map is null) return new(false, $"Không tồn tại map tên = {mapName}");
var el = await MapDb.Elements.FirstOrDefaultAsync(e => e.MapId == map.Id && e.Name == elementName);
if (el is null) return new(false, $"Không tồn tại element name = {elementName}");
var elModel = await MapDb.ElementModels.FindAsync(el.ModelId);
if (elModel == null) return new(false, $"Không tồn tại element model id = {el.ModelId}");
var elNode = await MapDb.Nodes.FindAsync(el.NodeId);
if (elNode is null) return new(false, $"Không tồn tại node id = {el.NodeId}");
return new(true)
{
Data = new()
{
Id = el.Id,
Name = el.Name,
MapId = el.MapId,
IsOpen = el.IsOpen,
NodeId = el.NodeId,
OffsetX = el.OffsetX,
OffsetY = el.OffsetY,
ModelId = el.ModelId,
Content = el.Content,
NodeName = elNode.Name,
ModelName = elModel.Name,
X = elNode.X,
Y = elNode.Y,
Theta = elNode.Theta
}
};
}
catch (Exception ex)
{
Logger.Warning($"GetElement: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"GetElement: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
[HttpPatch]
[Route("{mapName}/element/{elementName}")]
public async Task<MessageResult> UpdateElementProperty([FromRoute] string mapName, [FromRoute] string elementName, [FromBody] ElementPropertyUpdateModel model)
{
try
{
var map = await MapDb.Maps.FirstOrDefaultAsync(m => m.Name == mapName);
if (map is null) return new(false, $"Không tồn tại map tên = {mapName}");
var element = await MapDb.Elements.FirstOrDefaultAsync(m => m.Name == elementName && m.MapId == map.Id);
if (element == null) return new(false, $"Không tồn tại element tên = {elementName} trong map tên = {mapName}");
var properties = System.Text.Json.JsonSerializer.Deserialize<List<ElementProperty>>(element.Content, JsonOptionExtends.Read);
foreach (var property in model.Properties)
{
var existingProperty = properties?.FirstOrDefault(p => p.Name == property.Name);
if (existingProperty != null)
{
existingProperty.DefaultValue = property.DefaultValue;
}
else return new(false, $"Không tồn tại property name = {property.Name} trong element");
}
var content = System.Text.Json.JsonSerializer.Serialize(properties, JsonOptionExtends.Write);
element.Content = content;
await MapDb.SaveChangesAsync();
return new(true);
}
catch (Exception ex)
{
Logger.Warning($"Update: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"Update: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
[HttpPatch]
[Route("{mapName}/element/{elementName}/IsOpen")]
public async Task<MessageResult> UpdateOpenOfElement([FromRoute] string mapName, [FromRoute] string elementName, [FromQuery] bool isOpen)
{
try
{
var map = await MapDb.Maps.FirstOrDefaultAsync(m => m.Name == mapName);
if (map is null) return new(false, $"Không tồn tại map tên = {mapName}");
var element = await MapDb.Elements.FirstOrDefaultAsync(m => m.Name == elementName && m.MapId == map.Id);
if (element == null) return new(false, $"Không tồn tại element tên = {elementName} trong map tên = {mapName}");
element.IsOpen = isOpen;
await MapDb.SaveChangesAsync();
return new(true);
}
catch (Exception ex)
{
Logger.Warning($"Update: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"Update: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
}

View File

@ -0,0 +1,114 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RobotNet.MapManager.Data;
using RobotNet.MapManager.Services;
using RobotNet.MapShares;
using RobotNet.MapShares.Dtos;
using RobotNet.Shares;
namespace RobotNet.MapManager.Controllers;
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class ZonesController(MapEditorDbContext MapDb, LoggerController<ZonesController> Logger) : ControllerBase
{
[HttpPost]
[Route("")]
public async Task<MessageResult<ZoneDto>> Create([FromBody] ZoneCreateModel zone)
{
try
{
var map = await MapDb.Maps.FindAsync(zone.MapId);
if (map == null) return new(false, $"Không tồn tại map id = {zone.MapId}");
if (MapEditorHelper.CalculateQuadrilateralArea(zone.X1, zone.Y1, zone.X2, zone.Y2, zone.X3, zone.Y3, zone.X4, zone.Y4) < map.ZoneMinSquareDefault)
return new(false, "Kích thước Zone quá nhỏ");
var entity = await MapDb.Zones.AddAsync(new()
{
MapId = zone.MapId,
Type = zone.Type,
Name = "",
X1 = zone.X1,
X2 = zone.X2,
X3 = zone.X3,
X4 = zone.X4,
Y1 = zone.Y1,
Y2 = zone.Y2,
Y3 = zone.Y3,
Y4 = zone.Y4,
});
await MapDb.SaveChangesAsync();
return new(true)
{
Data = new ZoneDto() { Id = entity.Entity.Id, MapId = entity.Entity.MapId, Type = entity.Entity.Type, X1 = entity.Entity.X1, X2 = entity.Entity.X2, X3 = entity.Entity.X3, X4 = entity.Entity.X4, Y1 = entity.Entity.Y1, Y2 = entity.Entity.Y2, Y3 = entity.Entity.Y3, Y4 = entity.Entity.Y4 }
};
}
catch (Exception ex)
{
Logger.Warning($"Create: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, "Hệ thống có lỗi xảy ra");
}
}
[HttpDelete]
[Route("{id}")]
public async Task<MessageResult> Delete(Guid id)
{
try
{
var zoneExisted = await MapDb.Zones.FindAsync(id);
if (zoneExisted is not null)
{
MapDb.Zones.Remove(zoneExisted);
await MapDb.SaveChangesAsync();
return new(true);
}
return new(false, "Hệ thống không tìm thấy khu vực Zone này");
}
catch (Exception ex)
{
Logger.Warning($"Delete {id}: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, "Hệ thống có lỗi xảy ra");
}
}
[HttpPut]
[Route("")]
public async Task<MessageResult> Update([FromBody] ZoneUpdateModel zone)
{
try
{
var zoneExisted = await MapDb.Zones.FindAsync(zone.Id);
if (zoneExisted is not null)
{
if (zoneExisted.Name != zone.Name && !string.IsNullOrWhiteSpace(zone.Name) && await MapDb.Zones.AnyAsync(z => z.Name == zone.Name && z.MapId == zoneExisted.MapId))
{
return new(false, $"Tên zone {zone.Name} đã tồn tại trong map");
}
zoneExisted.Type = zone.Type;
zoneExisted.Name = zone.Name;
zoneExisted.X1 = zone.X1;
zoneExisted.X2 = zone.X2;
zoneExisted.X3 = zone.X3;
zoneExisted.X4 = zone.X4;
zoneExisted.Y1 = zone.Y1;
zoneExisted.Y2 = zone.Y2;
zoneExisted.Y3 = zone.Y3;
zoneExisted.Y4 = zone.Y4;
MapDb.Zones.Update(zoneExisted);
await MapDb.SaveChangesAsync();
return new(true);
}
return new(false, "Hệ thống không tìm thấy khu vực Zone này");
}
catch (Exception ex)
{
Logger.Warning($"Update: Hệ thống có lỗi xảy ra - {ex.Message}");
return new(false, $"Update: Hệ thống có lỗi xảy ra - {ex.Message}");
}
}
}

View File

@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
namespace RobotNet.MapManager.Data;
#nullable disable
[Table("Actions")]
[Index(nameof(MapId), nameof(Name), Name = "IX_Action_MapId_Name")]
public class Action
{
[Column("Id", TypeName = "uniqueidentifier")]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
[Required]
public Guid Id { get; set; }
[Column("MapId", TypeName = "uniqueidentifier")]
[Required]
public Guid MapId { get; set; }
[Column("Name", TypeName = "nvarchar(64)")]
public string Name { get; set; }
[Column("Content", TypeName = "nvarchar(max)")]
public string Content { get; set; }
public Map Map { get; set; }
}

View File

@ -0,0 +1,77 @@
using Microsoft.EntityFrameworkCore;
using RobotNet.MapShares.Enums;
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
namespace RobotNet.MapManager.Data;
#nullable disable
[Table("Edges")]
[Index(nameof(MapId), Name = "IX_Edge_MapId")]
public class Edge
{
[Column("Id", TypeName = "uniqueidentifier")]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
[Required]
public Guid Id { get; set; }
[Column("MapId", TypeName = "uniqueidentifier")]
[Required]
public Guid MapId { get; set; }
[Column("StartNodeId", TypeName = "uniqueidentifier")]
[Required]
public Guid StartNodeId { get; set; }
[Column("EndNodeId", TypeName = "uniqueidentifier")]
[Required]
public Guid EndNodeId { get; set; }
[Column("ControlPoint1X", TypeName = "float")]
public double ControlPoint1X { get; set; }
[Column("ControlPoint1Y", TypeName = "float")]
public double ControlPoint1Y { get; set; }
[Column("ControlPoint2X", TypeName = "float")]
public double ControlPoint2X { get; set; }
[Column("ControlPoint2Y", TypeName = "float")]
public double ControlPoint2Y { get; set; }
[Column("TrajectoryDegree", TypeName = "tinyint")]
public TrajectoryDegree TrajectoryDegree { get; set; }
[Column("MaxHeight", TypeName = "float")]
public double MaxHeight { get; set; }
[Column("MinHeight", TypeName = "float")]
public double MinHeight { get; set; }
[Column("DirectionAllowed", TypeName = "tinyint")]
public DirectionAllowed DirectionAllowed { get; set; }
[Column("RotationAllowed", TypeName = "bit")]
public bool RotationAllowed { get; set; }
[Column("MaxRotationSpeed", TypeName = "float")]
public double MaxRotationSpeed { get; set; }
[Column("MaxSpeed", TypeName = "float")]
public double MaxSpeed { get; set; }
[Column("AllowedDeviationXy", TypeName = "float")]
public double AllowedDeviationXy { get; set; }
[Column("AllowedDeviationTheta", TypeName = "float")]
public double AllowedDeviationTheta { get; set; }
[Column("Actions", TypeName = "nvarchar(max)")]
public string Actions { get; set; }
public Map Map { get; set; }
public virtual Node StartNode { get; set; }
public virtual Node EndNode { get; set; }
}

View File

@ -0,0 +1,49 @@
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
namespace RobotNet.MapManager.Data;
#nullable disable
[Table("Elements")]
[Index(nameof(MapId), nameof(ModelId), Name = "IX_Element_MapId_ModelId")]
public class Element
{
[Column("Id", TypeName = "uniqueidentifier")]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
[Required]
public Guid Id { get; set; }
[Column("MapId", TypeName = "uniqueidentifier")]
[Required]
public Guid MapId { get; set; }
[Column("ModelId", TypeName = "uniqueidentifier")]
[Required]
public Guid ModelId { get; set; }
[Column("NodeId", TypeName = "uniqueidentifier")]
[Required]
public Guid NodeId { get; set; }
[Column("Name", TypeName = "nvarchar(64)")]
public string Name { get; set; }
[Column("IsOpen", TypeName = "bit")]
public bool IsOpen { get; set; }
[Column("OffsetX", TypeName = "float")]
public double OffsetX { get; set; }
[Column("OffsetY", TypeName = "float")]
public double OffsetY { get; set; }
[Column("Content", TypeName = "nvarchar(max)")]
public string Content { get; set; }
public Map Map { get; set; }
public ElementModel Model { get; set; }
public Node Node { get; set; }
}

View File

@ -0,0 +1,55 @@
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
namespace RobotNet.MapManager.Data;
#nullable disable
[Table("ElementModels")]
[Index(nameof(MapId), nameof(Name), Name = "IX_ElementModel_MapId_Name")]
public class ElementModel
{
[Column("Id", TypeName = "uniqueidentifier")]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
[Required]
public Guid Id { get; set; }
[Column("MapId", TypeName = "uniqueidentifier")]
[Required]
public Guid MapId { get; set; }
[Column("Name", TypeName = "nvarchar(64)")]
public string Name { get; set; }
[Column("Width", TypeName = "float")]
[Required]
public double Width { get; set; }
[Column("Height", TypeName = "float")]
[Required]
public double Height { get; set; }
[Column("Image1Width", TypeName = "int")]
[Required]
public int Image1Width { get; set; }
[Column("Image1Height", TypeName = "int")]
[Required]
public int Image1Height { get; set; }
[Column("Image2Width", TypeName = "int")]
[Required]
public int Image2Width { get; set; }
[Column("Image2Height", TypeName = "int")]
[Required]
public int Image2Height { get; set; }
[Column("Content", TypeName = "nvarchar(max)")]
public string Content { get; set; }
public virtual ICollection<Element> Elements { get; } = [];
public Map Map { get; set; }
}

View File

@ -0,0 +1,125 @@
using RobotNet.MapShares.Enums;
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
namespace RobotNet.MapManager.Data;
#nullable disable
[Table("Maps")]
public class Map
{
[Column("Id", TypeName = "uniqueidentifier")]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
[Required]
public Guid Id { get; set; }
[Column("Name", TypeName = "nvarchar(64)")]
[Required]
public string Name { get; set; }
[Column("Description", TypeName = "ntext")]
public string Description { get; set; } = "";
[Column("VersionId", TypeName = "uniqueidentifier")]
[Required]
public Guid VersionId { get; set; }
[Column("OriginX", TypeName = "float")]
[Required]
public double OriginX { get; set; }
[Column("OriginY", TypeName = "float")]
[Required]
public double OriginY { get; set; }
[Column("Resolution", TypeName = "float")]
[Required]
public double Resolution { get; set; }
[Column("ViewX", TypeName = "float")]
[Required]
public double ViewX { get; set; }
[Column("ViewY", TypeName = "float")]
[Required]
public double ViewY { get; set; }
[Column("ViewWidth", TypeName = "float")]
[Required]
public double ViewWidth { get; set; }
[Column("ViewHeight", TypeName = "float")]
[Required]
public double ViewHeight { get; set; }
[Column("ImageWidth", TypeName = "float")]
[Required]
public double ImageWidth { get; set; }
[Column("ImageHeight", TypeName = "float")]
[Required]
public double ImageHeight { get; set; }
[Column("NodeCount", TypeName = "BigInt")]
public Int64 NodeCount { get; set; }
[Column("Active", TypeName = "bit")]
public bool Active { get; set; }
[Column("VDA5050", TypeName = "nvarchar(max)")]
public string VDA5050 { get; set; } = ""; //AdditionalAttributes
[Column("NodeNameAutoGenerate", TypeName = "bit")]
public bool NodeNameAutoGenerate { get; set; }
[Column("NodeNameTemplateDefault", TypeName = "nvarchar(64)")]
public string NodeNameTemplateDefault { get; set; }
[Column("NodeAllowedDeviationXyDefault", TypeName = "float")]
public double NodeAllowedDeviationXyDefault { get; set; }
[Column("NodeAllowedDeviationThetaDefault", TypeName = "float")]
public double NodeAllowedDeviationThetaDefault { get; set; }
[Column("EdgeMinLengthDefault", TypeName = "float")]
public double EdgeMinLengthDefault { get; set; }
[Column("EdgeStraightMaxSpeedDefault", TypeName = "float")]
public double EdgeStraightMaxSpeedDefault { get; set; }
[Column("EdgeCurveMaxSpeedDefault", TypeName = "float")]
public double EdgeCurveMaxSpeedDefault { get; set; }
[Column("EdgeMaxHeightDefault", TypeName = "float")]
public double EdgeMaxHeightDefault { get; set; }
[Column("EdgeMinHeightDefault", TypeName = "float")]
public double EdgeMinHeightDefault { get; set; }
[Column("EdgeMaxRoataionSpeedDefault", TypeName = "float")]
public double EdgeMaxRotationSpeedDefault { get; set; }
[Column("EdgeDirectionAllowedDefault", TypeName = "tinyint")]
public DirectionAllowed EdgeDirectionAllowedDefault { get; set; }
[Column("EdgeRotationAllowedDefault", TypeName = "bit")]
public bool EdgeRotationAllowedDefault { get; set; }
[Column("EdgeAllowedDeviationXyDefault", TypeName = "float")]
public double EdgeAllowedDeviationXyDefault { get; set; }
[Column("EdgeAllowedDeviationThetaDefault", TypeName = "float")]
public double EdgeAllowedDeviationThetaDefault { get; set; }
[Column("ZoneMinSquareDefault", TypeName = "float")]
public double ZoneMinSquareDefault { get; set; }
public virtual ICollection<Node> Nodes { get; } = [];
public virtual ICollection<Edge> Edges { get; } = [];
public virtual ICollection<Action> Actions { get; } = [];
public virtual ICollection<Zone> Zones { get; } = [];
public virtual ICollection<ElementModel> ElementModels { get; } = [];
public virtual ICollection<Element> Elements { get; } = [];
}

Some files were not shown because too many files have changed in this diff Show More