first commit -push
This commit is contained in:
parent
674ae395be
commit
a9577c5756
19
.env
Normal file
19
.env
Normal 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
409
.gitignore
vendored
Normal 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/
|
||||||
0
RobotNet.AppHost/Dockerfile
Normal file
0
RobotNet.AppHost/Dockerfile
Normal file
48
RobotNet.AppHost/Program.cs
Normal file
48
RobotNet.AppHost/Program.cs
Normal 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();
|
||||||
31
RobotNet.AppHost/Properties/launchSettings.json
Normal file
31
RobotNet.AppHost/Properties/launchSettings.json
Normal 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"
|
||||||
|
}
|
||||||
27
RobotNet.AppHost/RobotNet.AppHost.csproj
Normal file
27
RobotNet.AppHost/RobotNet.AppHost.csproj
Normal 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>
|
||||||
8
RobotNet.AppHost/appsettings.Development.json
Normal file
8
RobotNet.AppHost/appsettings.Development.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
RobotNet.AppHost/appsettings.json
Normal file
9
RobotNet.AppHost/appsettings.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning",
|
||||||
|
"Aspire.Hosting.Dcp": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
RobotNet.Clients/HttpClientExtensions.cs
Normal file
36
RobotNet.Clients/HttpClientExtensions.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
60
RobotNet.Clients/HubClient.cs
Normal file
60
RobotNet.Clients/HubClient.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
RobotNet.Clients/RobotNet.Clients.csproj
Normal file
13
RobotNet.Clients/RobotNet.Clients.csproj
Normal 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>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; } = "";
|
||||||
|
}
|
||||||
355
RobotNet.IdentityServer/Components/Account/Pages/Infor.razor
Normal file
355
RobotNet.IdentityServer/Components/Account/Pages/Infor.razor
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
.mdi {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-size: cover;
|
||||||
|
margin-top:7px;
|
||||||
|
}
|
||||||
118
RobotNet.IdentityServer/Components/Account/Pages/Login.razor
Normal file
118
RobotNet.IdentityServer/Components/Account/Pages/Login.razor
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
285
RobotNet.IdentityServer/Components/Account/Pages/Password.razor
Normal file
285
RobotNet.IdentityServer/Components/Account/Pages/Password.razor
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
120
RobotNet.IdentityServer/Components/Account/Pages/Register.razor
Normal file
120
RobotNet.IdentityServer/Components/Account/Pages/Register.razor
Normal 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; } = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
923
RobotNet.IdentityServer/Components/Account/Pages/Role.razor
Normal file
923
RobotNet.IdentityServer/Components/Account/Pages/Role.razor
Normal 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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
@using RobotNet.IdentityServer.Components.Account.Shared
|
||||||
|
@attribute [ExcludeFromInteractiveRouting]
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
|
|
||||||
|
@code {
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
NavigationManager.NavigateTo($"Account/Login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}", forceLoad: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
RobotNet.IdentityServer/Components/App.razor
Normal file
30
RobotNet.IdentityServer/Components/App.razor
Normal 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>
|
||||||
29
RobotNet.IdentityServer/Components/Layout/MainLayout.razor
Normal file
29
RobotNet.IdentityServer/Components/Layout/MainLayout.razor
Normal 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>
|
||||||
123
RobotNet.IdentityServer/Components/Layout/MainLayout.razor.css
Normal file
123
RobotNet.IdentityServer/Components/Layout/MainLayout.razor.css
Normal 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;
|
||||||
|
}
|
||||||
214
RobotNet.IdentityServer/Components/Layout/NavMenu.razor
Normal file
214
RobotNet.IdentityServer/Components/Layout/NavMenu.razor
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
202
RobotNet.IdentityServer/Components/Layout/NavMenu.razor.css
Normal file
202
RobotNet.IdentityServer/Components/Layout/NavMenu.razor.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
RobotNet.IdentityServer/Components/Pages/Error.razor
Normal file
36
RobotNet.IdentityServer/Components/Pages/Error.razor
Normal 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;
|
||||||
|
}
|
||||||
27
RobotNet.IdentityServer/Components/Pages/Home.razor
Normal file
27
RobotNet.IdentityServer/Components/Pages/Home.razor
Normal 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 {
|
||||||
|
|
||||||
|
}
|
||||||
12
RobotNet.IdentityServer/Components/Routes.razor
Normal file
12
RobotNet.IdentityServer/Components/Routes.razor
Normal 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>
|
||||||
14
RobotNet.IdentityServer/Components/_Imports.razor
Normal file
14
RobotNet.IdentityServer/Components/_Imports.razor
Normal 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
|
||||||
402
RobotNet.IdentityServer/Controllers/AuthorizationController.cs
Normal file
402
RobotNet.IdentityServer/Controllers/AuthorizationController.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
63
RobotNet.IdentityServer/Controllers/UserinfoController.cs
Normal file
63
RobotNet.IdentityServer/Controllers/UserinfoController.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
RobotNet.IdentityServer/Data/ApplicationDbContext.cs
Normal file
13
RobotNet.IdentityServer/Data/ApplicationDbContext.cs
Normal 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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
190
RobotNet.IdentityServer/Data/ApplicationDbExtensions.cs
Normal file
190
RobotNet.IdentityServer/Data/ApplicationDbExtensions.cs
Normal 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"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
9
RobotNet.IdentityServer/Data/ApplicationRole.cs
Normal file
9
RobotNet.IdentityServer/Data/ApplicationRole.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace RobotNet.IdentityServer.Data
|
||||||
|
{
|
||||||
|
public class ApplicationRole : IdentityRole
|
||||||
|
{
|
||||||
|
public DateTime CreatedDate { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
RobotNet.IdentityServer/Data/ApplicationUser.cs
Normal file
13
RobotNet.IdentityServer/Data/ApplicationUser.cs
Normal 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; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
540
RobotNet.IdentityServer/Data/Migrations/20250716085859_InitializeApplicationDb.Designer.cs
generated
Normal file
540
RobotNet.IdentityServer/Data/Migrations/20250716085859_InitializeApplicationDb.Designer.cs
generated
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
RobotNet.IdentityServer/Dockerfile
Normal file
56
RobotNet.IdentityServer/Dockerfile
Normal 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"]
|
||||||
21
RobotNet.IdentityServer/Helpers/AsyncEnumerableExtensions.cs
Normal file
21
RobotNet.IdentityServer/Helpers/AsyncEnumerableExtensions.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
196
RobotNet.IdentityServer/Program.cs
Normal file
196
RobotNet.IdentityServer/Program.cs
Normal 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();
|
||||||
15
RobotNet.IdentityServer/Properties/launchSettings.json
Normal file
15
RobotNet.IdentityServer/Properties/launchSettings.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"mssql1": {
|
||||||
|
"type": "mssql",
|
||||||
|
"connectionId": "ConnectionStrings:DefaultConnection"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"mssql1": {
|
||||||
|
"type": "mssql.local",
|
||||||
|
"connectionId": "ConnectionStrings:DefaultConnection"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
RobotNet.IdentityServer/RobotNet.IdentityServer.csproj
Normal file
41
RobotNet.IdentityServer/RobotNet.IdentityServer.csproj
Normal 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>
|
||||||
69
RobotNet.IdentityServer/Services/IdentityService.cs
Normal file
69
RobotNet.IdentityServer/Services/IdentityService.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
61
RobotNet.IdentityServer/Services/PasswordStrengthService.cs
Normal file
61
RobotNet.IdentityServer/Services/PasswordStrengthService.cs
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
26
RobotNet.IdentityServer/Services/UserImageService.cs
Normal file
26
RobotNet.IdentityServer/Services/UserImageService.cs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
41
RobotNet.IdentityServer/Services/UserInfoService.cs
Normal file
41
RobotNet.IdentityServer/Services/UserInfoService.cs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
RobotNet.IdentityServer/appsettings.json
Normal file
18
RobotNet.IdentityServer/appsettings.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
15
RobotNet.IdentityServer/libman.json
Normal file
15
RobotNet.IdentityServer/libman.json
Normal 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/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
25
RobotNet.IdentityServer/nlog.config
Normal file
25
RobotNet.IdentityServer/nlog.config
Normal 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>
|
||||||
60
RobotNet.IdentityServer/wwwroot/app.css
Normal file
60
RobotNet.IdentityServer/wwwroot/app.css
Normal 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;
|
||||||
|
}
|
||||||
5
RobotNet.IdentityServer/wwwroot/favicon.svg
Normal file
5
RobotNet.IdentityServer/wwwroot/favicon.svg
Normal 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 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
81
RobotNet.IdentityServer/wwwroot/mud/fonts.googleapis.com.css
Normal file
81
RobotNet.IdentityServer/wwwroot/mud/fonts.googleapis.com.css
Normal 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;
|
||||||
|
}
|
||||||
BIN
RobotNet.IdentityServer/wwwroot/uploads/avatars/anh.jpg
Normal file
BIN
RobotNet.IdentityServer/wwwroot/uploads/avatars/anh.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 295 KiB |
130
RobotNet.MapManager/Controllers/ActionsController.cs
Normal file
130
RobotNet.MapManager/Controllers/ActionsController.cs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
269
RobotNet.MapManager/Controllers/EdgesController.cs
Normal file
269
RobotNet.MapManager/Controllers/EdgesController.cs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
258
RobotNet.MapManager/Controllers/ElementModelsController.cs
Normal file
258
RobotNet.MapManager/Controllers/ElementModelsController.cs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
211
RobotNet.MapManager/Controllers/ElementsController.cs
Normal file
211
RobotNet.MapManager/Controllers/ElementsController.cs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
66
RobotNet.MapManager/Controllers/ImagesController.cs
Normal file
66
RobotNet.MapManager/Controllers/ImagesController.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
249
RobotNet.MapManager/Controllers/MapExportController.cs
Normal file
249
RobotNet.MapManager/Controllers/MapExportController.cs
Normal 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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
381
RobotNet.MapManager/Controllers/MapsDataController.cs
Normal file
381
RobotNet.MapManager/Controllers/MapsDataController.cs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
571
RobotNet.MapManager/Controllers/MapsManagerController.cs
Normal file
571
RobotNet.MapManager/Controllers/MapsManagerController.cs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
79
RobotNet.MapManager/Controllers/MapsSettingController.cs
Normal file
79
RobotNet.MapManager/Controllers/MapsSettingController.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
58
RobotNet.MapManager/Controllers/NodesController.cs
Normal file
58
RobotNet.MapManager/Controllers/NodesController.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
232
RobotNet.MapManager/Controllers/ScriptElementsController.cs
Normal file
232
RobotNet.MapManager/Controllers/ScriptElementsController.cs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
114
RobotNet.MapManager/Controllers/ZonesController.cs
Normal file
114
RobotNet.MapManager/Controllers/ZonesController.cs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
RobotNet.MapManager/Data/Action.cs
Normal file
30
RobotNet.MapManager/Data/Action.cs
Normal 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; }
|
||||||
|
}
|
||||||
77
RobotNet.MapManager/Data/Edge.cs
Normal file
77
RobotNet.MapManager/Data/Edge.cs
Normal 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; }
|
||||||
|
}
|
||||||
49
RobotNet.MapManager/Data/Element.cs
Normal file
49
RobotNet.MapManager/Data/Element.cs
Normal 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; }
|
||||||
|
}
|
||||||
55
RobotNet.MapManager/Data/ElementModel.cs
Normal file
55
RobotNet.MapManager/Data/ElementModel.cs
Normal 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; }
|
||||||
|
}
|
||||||
125
RobotNet.MapManager/Data/Map.cs
Normal file
125
RobotNet.MapManager/Data/Map.cs
Normal 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
Loading…
Reference in New Issue
Block a user