From dfbc1cfdb0f6676f356cc0679035b04e0581ecd7 Mon Sep 17 00:00:00 2001 From: niyyzf Date: Sat, 22 Jun 2024 10:54:02 +0800 Subject: [PATCH] init --- .gitignore | 8 + LoongPanel-Asp.sln | 22 + LoongPanel-Asp.sln.DotSettings.user | 4 + LoongPanel-Asp/ApplicationDbContext.cs | 210 + LoongPanel-Asp/Configs/alert.ini | 2 + LoongPanel-Asp/Configs/jobs.ini | 71 + LoongPanel-Asp/Configs/servers.ini | 15 + .../Controllers/AccountController.cs | 203 + LoongPanel-Asp/Controllers/JobController.cs | 24 + LoongPanel-Asp/Controllers/RoteController.cs | 31 + .../Controllers/ServerController.cs | 278 + LoongPanel-Asp/Controllers/UserController.cs | 97 + LoongPanel-Asp/Helpers/ApiHelper.cs | 48 + LoongPanel-Asp/Helpers/DataHelper.cs | 37 + LoongPanel-Asp/Helpers/EmailHelper.cs | 44 + LoongPanel-Asp/Helpers/JobConfigHelper.cs | 146 + .../Helpers/PrometheusQueryHelper.cs | 50 + LoongPanel-Asp/Helpers/TokenHelper.cs | 65 + LoongPanel-Asp/Hubs/SessionHub.cs | 90 + LoongPanel-Asp/Jobs/CpuJob.cs | 175 + LoongPanel-Asp/Jobs/DiskJob.cs | 131 + LoongPanel-Asp/Jobs/MemoryJob.cs | 94 + LoongPanel-Asp/Jobs/NetworkJob.cs | 79 + LoongPanel-Asp/Jobs/ProcessJob.cs | 99 + LoongPanel-Asp/LoongPanel-Asp.csproj | 44 + LoongPanel-Asp/LoongPanel-Asp.csproj.user | 6 + LoongPanel-Asp/LoongPanel-Asp.http | 6 + .../Middlewares/ApiPermissionMiddleware.cs | 122 + .../Middlewares/PermissionMiddleware.cs | 71 + LoongPanel-Asp/Models/AccountModel.cs | 27 + LoongPanel-Asp/Models/OrderType.cs | 20 + .../Models/PrometheusQueryResultModel.cs | 23 + LoongPanel-Asp/Program.cs | 134 + LoongPanel-Asp/Properties/launchSettings.json | 41 + LoongPanel-Asp/Servers/SSHService.cs | 96 + LoongPanel-Asp/Types/EmailCode.cs | 12 + .../UserConfigs/Layouts/default.json | 1012 ++ LoongPanel-Asp/appsettings.Development.json | 8 + LoongPanel-Asp/appsettings.json | 23 + LoongPanel-Asp/utils/ControllerScanner.cs | 42 + web/.env | 1 + web/.gitignore | 24 + web/README.md | 75 + web/app.vue | 18 + web/assets/diagonal1.cur | Bin 0 -> 4286 bytes web/assets/link.cur | Bin 0 -> 4286 bytes web/assets/min.scss | 23 + web/assets/normal.cur | Bin 0 -> 4286 bytes web/assets/normalshowel.cur | Bin 0 -> 4286 bytes web/base.scss | 35 + web/components/AddCard.vue | 61 + web/components/Cards/ICard.vue | 133 + web/components/Cards/IChart.vue | 178 + web/components/Cards/MiniCard.vue | 171 + web/components/Cards/OnlineUsersCard.vue | 82 + web/components/Cards/SystemInfoCard.vue | 89 + web/components/Charts/AreaChart.vue | 214 + web/components/Charts/BarChart.vue | 66 + web/components/Charts/LineChart.vue | 212 + web/components/Charts/PieChart.vue | 50 + web/components/Charts/ScatterChart.vue | 11 + web/components/ColorSwitch.vue | 81 + web/components/Grid/MainGrid.vue | 219 + web/components/Grid/TopGrid.vue | 42 + web/components/HeroBox.vue | 78 + web/components/Icon.vue | 34 + web/components/Logo.vue | 44 + web/components/Notification.vue | 134 + web/components/SettingCard.vue | 483 + web/components/SidebarRight.vue | 9 + web/components/Term.vue | 46 + web/components/UserMini.vue | 54 + web/components/UserPage/UserItem.vue | 149 + web/components/UserPage/UserPageBar.vue | 126 + web/components/UserPage/UserPageIndicator.vue | 60 + web/components/XScroll.vue | 50 + web/components/shell/FloaterBar.vue | 83 + web/components/shell/SideBar.vue | 314 + web/components/shell/TitleBar.vue | 67 + web/config/cards.ts | 21 + web/config/charts.ts | 65 + web/config/useridComparisonTable.ts | 6 + web/layouts/Login.vue | 352 + web/layouts/Main.vue | 308 + web/middleware/auth.ts | 57 + web/nuxt.config.ts | 61 + web/package.json | 53 + web/pages/SignIn.vue | 255 + web/pages/SignUp.vue | 459 + web/pages/home.vue | 35 + web/pages/host.vue | 195 + web/pages/host/cpu.vue | 445 + web/pages/host/disk/[[id]].vue | 360 + web/pages/host/gpu/[[id]].vue | 11 + web/pages/host/memory.vue | 491 + web/pages/host/network/[[id]].vue | 11 + web/pages/index.vue | 13 + web/pages/user.vue | 59 + web/patches/grid-layout-plus+1.0.5.patch | 12 + web/plugins/apexcharts.ts | 5 + web/plugins/echarts.ts | 22 + web/plugins/vue-toast.ts | 6 + web/public/1.jpg | Bin 0 -> 38651 bytes web/public/Bg1.png | Bin 0 -> 2979698 bytes web/public/Dragon_Head.gif | Bin 0 -> 165554 bytes web/public/Error.svg | 10 + web/public/Facebook.svg | 19 + web/public/Fox/css/style.css | 80 + web/public/Fox/index.html | 21 + web/public/Fox/js/script.js | 730 ++ web/public/Gmail.svg | 29 + web/public/Informative.svg | 11 + web/public/Instagram.svg | 36 + web/public/Linkedin.svg | 23 + web/public/Logo.svg | 5 + web/public/Success.svg | 10 + web/public/Warning.svg | 12 + web/public/apple-touch-icon-180x180.png | Bin 0 -> 1070 bytes web/public/audios/idle1.mp3 | Bin 0 -> 6078 bytes web/public/audios/leida.mp3 | Bin 0 -> 424371 bytes web/public/bg2.png | Bin 0 -> 3055531 bytes web/public/chartIcon/Area.svg | 100 + web/public/chartIcon/Bubble.svg | 93 + web/public/chartIcon/Doughnut.svg | 93 + web/public/chartIcon/Filled_Radar.svg | 65 + web/public/chartIcon/Histogram.svg | 117 + .../chartIcon/Increasing_Circle_Process.svg | 62 + web/public/chartIcon/Line.svg | 89 + web/public/chartIcon/Pareto.svg | 165 + web/public/chartIcon/Pie.svg | 85 + web/public/chartIcon/Scatter.svg | 148 + web/public/favicon.ico | Bin 0 -> 629 bytes web/public/maskable-icon-512x512.png | Bin 0 -> 3008 bytes web/public/pwa-192x192.png | Bin 0 -> 1273 bytes web/public/pwa-512x512.png | Bin 0 -> 3108 bytes web/public/pwa-64x64.png | Bin 0 -> 495 bytes web/server/tsconfig.json | 3 + web/strores/DataStore.ts | 62 + web/strores/HubStore.ts | 84 + web/strores/LoadStore.ts | 10 + web/strores/UseMainLayoutStore.ts | 88 + web/tsconfig.json | 4 + web/types/UserType.ts | 36 + web/types/baseType.ts | 6 + web/utils.ts | 42 + web/yarn.lock | 8642 +++++++++++++++++ 146 files changed, 21508 insertions(+) create mode 100644 .gitignore create mode 100755 LoongPanel-Asp.sln create mode 100755 LoongPanel-Asp.sln.DotSettings.user create mode 100755 LoongPanel-Asp/ApplicationDbContext.cs create mode 100644 LoongPanel-Asp/Configs/alert.ini create mode 100755 LoongPanel-Asp/Configs/jobs.ini create mode 100644 LoongPanel-Asp/Configs/servers.ini create mode 100755 LoongPanel-Asp/Controllers/AccountController.cs create mode 100755 LoongPanel-Asp/Controllers/JobController.cs create mode 100755 LoongPanel-Asp/Controllers/RoteController.cs create mode 100755 LoongPanel-Asp/Controllers/ServerController.cs create mode 100755 LoongPanel-Asp/Controllers/UserController.cs create mode 100755 LoongPanel-Asp/Helpers/ApiHelper.cs create mode 100755 LoongPanel-Asp/Helpers/DataHelper.cs create mode 100755 LoongPanel-Asp/Helpers/EmailHelper.cs create mode 100755 LoongPanel-Asp/Helpers/JobConfigHelper.cs create mode 100755 LoongPanel-Asp/Helpers/PrometheusQueryHelper.cs create mode 100755 LoongPanel-Asp/Helpers/TokenHelper.cs create mode 100755 LoongPanel-Asp/Hubs/SessionHub.cs create mode 100755 LoongPanel-Asp/Jobs/CpuJob.cs create mode 100755 LoongPanel-Asp/Jobs/DiskJob.cs create mode 100755 LoongPanel-Asp/Jobs/MemoryJob.cs create mode 100755 LoongPanel-Asp/Jobs/NetworkJob.cs create mode 100644 LoongPanel-Asp/Jobs/ProcessJob.cs create mode 100755 LoongPanel-Asp/LoongPanel-Asp.csproj create mode 100755 LoongPanel-Asp/LoongPanel-Asp.csproj.user create mode 100755 LoongPanel-Asp/LoongPanel-Asp.http create mode 100755 LoongPanel-Asp/Middlewares/ApiPermissionMiddleware.cs create mode 100755 LoongPanel-Asp/Middlewares/PermissionMiddleware.cs create mode 100755 LoongPanel-Asp/Models/AccountModel.cs create mode 100755 LoongPanel-Asp/Models/OrderType.cs create mode 100755 LoongPanel-Asp/Models/PrometheusQueryResultModel.cs create mode 100755 LoongPanel-Asp/Program.cs create mode 100755 LoongPanel-Asp/Properties/launchSettings.json create mode 100755 LoongPanel-Asp/Servers/SSHService.cs create mode 100755 LoongPanel-Asp/Types/EmailCode.cs create mode 100644 LoongPanel-Asp/UserConfigs/Layouts/default.json create mode 100755 LoongPanel-Asp/appsettings.Development.json create mode 100755 LoongPanel-Asp/appsettings.json create mode 100755 LoongPanel-Asp/utils/ControllerScanner.cs create mode 100755 web/.env create mode 100755 web/.gitignore create mode 100755 web/README.md create mode 100755 web/app.vue create mode 100755 web/assets/diagonal1.cur create mode 100755 web/assets/link.cur create mode 100755 web/assets/min.scss create mode 100755 web/assets/normal.cur create mode 100755 web/assets/normalshowel.cur create mode 100755 web/base.scss create mode 100644 web/components/AddCard.vue create mode 100644 web/components/Cards/ICard.vue create mode 100755 web/components/Cards/IChart.vue create mode 100755 web/components/Cards/MiniCard.vue create mode 100644 web/components/Cards/OnlineUsersCard.vue create mode 100755 web/components/Cards/SystemInfoCard.vue create mode 100644 web/components/Charts/AreaChart.vue create mode 100644 web/components/Charts/BarChart.vue create mode 100755 web/components/Charts/LineChart.vue create mode 100755 web/components/Charts/PieChart.vue create mode 100644 web/components/Charts/ScatterChart.vue create mode 100755 web/components/ColorSwitch.vue create mode 100644 web/components/Grid/MainGrid.vue create mode 100755 web/components/Grid/TopGrid.vue create mode 100755 web/components/HeroBox.vue create mode 100755 web/components/Icon.vue create mode 100755 web/components/Logo.vue create mode 100755 web/components/Notification.vue create mode 100755 web/components/SettingCard.vue create mode 100644 web/components/SidebarRight.vue create mode 100644 web/components/Term.vue create mode 100755 web/components/UserMini.vue create mode 100644 web/components/UserPage/UserItem.vue create mode 100644 web/components/UserPage/UserPageBar.vue create mode 100644 web/components/UserPage/UserPageIndicator.vue create mode 100644 web/components/XScroll.vue create mode 100755 web/components/shell/FloaterBar.vue create mode 100755 web/components/shell/SideBar.vue create mode 100755 web/components/shell/TitleBar.vue create mode 100644 web/config/cards.ts create mode 100755 web/config/charts.ts create mode 100755 web/config/useridComparisonTable.ts create mode 100755 web/layouts/Login.vue create mode 100755 web/layouts/Main.vue create mode 100755 web/middleware/auth.ts create mode 100755 web/nuxt.config.ts create mode 100755 web/package.json create mode 100755 web/pages/SignIn.vue create mode 100755 web/pages/SignUp.vue create mode 100755 web/pages/home.vue create mode 100644 web/pages/host.vue create mode 100644 web/pages/host/cpu.vue create mode 100644 web/pages/host/disk/[[id]].vue create mode 100644 web/pages/host/gpu/[[id]].vue create mode 100644 web/pages/host/memory.vue create mode 100644 web/pages/host/network/[[id]].vue create mode 100755 web/pages/index.vue create mode 100644 web/pages/user.vue create mode 100644 web/patches/grid-layout-plus+1.0.5.patch create mode 100755 web/plugins/apexcharts.ts create mode 100644 web/plugins/echarts.ts create mode 100755 web/plugins/vue-toast.ts create mode 100644 web/public/1.jpg create mode 100755 web/public/Bg1.png create mode 100755 web/public/Dragon_Head.gif create mode 100755 web/public/Error.svg create mode 100755 web/public/Facebook.svg create mode 100644 web/public/Fox/css/style.css create mode 100644 web/public/Fox/index.html create mode 100644 web/public/Fox/js/script.js create mode 100755 web/public/Gmail.svg create mode 100755 web/public/Informative.svg create mode 100755 web/public/Instagram.svg create mode 100755 web/public/Linkedin.svg create mode 100755 web/public/Logo.svg create mode 100755 web/public/Success.svg create mode 100755 web/public/Warning.svg create mode 100755 web/public/apple-touch-icon-180x180.png create mode 100644 web/public/audios/idle1.mp3 create mode 100644 web/public/audios/leida.mp3 create mode 100755 web/public/bg2.png create mode 100755 web/public/chartIcon/Area.svg create mode 100755 web/public/chartIcon/Bubble.svg create mode 100755 web/public/chartIcon/Doughnut.svg create mode 100755 web/public/chartIcon/Filled_Radar.svg create mode 100755 web/public/chartIcon/Histogram.svg create mode 100755 web/public/chartIcon/Increasing_Circle_Process.svg create mode 100755 web/public/chartIcon/Line.svg create mode 100755 web/public/chartIcon/Pareto.svg create mode 100755 web/public/chartIcon/Pie.svg create mode 100755 web/public/chartIcon/Scatter.svg create mode 100755 web/public/favicon.ico create mode 100755 web/public/maskable-icon-512x512.png create mode 100755 web/public/pwa-192x192.png create mode 100755 web/public/pwa-512x512.png create mode 100755 web/public/pwa-64x64.png create mode 100755 web/server/tsconfig.json create mode 100755 web/strores/DataStore.ts create mode 100755 web/strores/HubStore.ts create mode 100644 web/strores/LoadStore.ts create mode 100755 web/strores/UseMainLayoutStore.ts create mode 100755 web/tsconfig.json create mode 100755 web/types/UserType.ts create mode 100755 web/types/baseType.ts create mode 100644 web/utils.ts create mode 100644 web/yarn.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e6af5ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/LoongPanel-Asp/bin/ +/LoongPanel-Asp/app.db +/LoongPanel-Asp/app.db-shm +/LoongPanel-Asp/app.db-wal +/LoongPanel-Asp/temp.db +/LoongPanel-Asp/obj/ +/LoongPanel-Asp/Migrations/ +/LoongPanel-Asp/Entities/ diff --git a/LoongPanel-Asp.sln b/LoongPanel-Asp.sln new file mode 100755 index 0000000..6aa2584 --- /dev/null +++ b/LoongPanel-Asp.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoongPanel-Asp", "LoongPanel-Asp\LoongPanel-Asp.csproj", "{3AED83DF-9EF2-4AC6-BDBC-6E1C9D823405}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pty.Net", "PtyTerminal\Pty.Net\Pty.Net.csproj", "{FEE728E2-BD7E-49C5-BC52-A9AE66D84DD5}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3AED83DF-9EF2-4AC6-BDBC-6E1C9D823405}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3AED83DF-9EF2-4AC6-BDBC-6E1C9D823405}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3AED83DF-9EF2-4AC6-BDBC-6E1C9D823405}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3AED83DF-9EF2-4AC6-BDBC-6E1C9D823405}.Release|Any CPU.Build.0 = Release|Any CPU + {FEE728E2-BD7E-49C5-BC52-A9AE66D84DD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FEE728E2-BD7E-49C5-BC52-A9AE66D84DD5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FEE728E2-BD7E-49C5-BC52-A9AE66D84DD5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FEE728E2-BD7E-49C5-BC52-A9AE66D84DD5}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/LoongPanel-Asp.sln.DotSettings.user b/LoongPanel-Asp.sln.DotSettings.user new file mode 100755 index 0000000..6fb4815 --- /dev/null +++ b/LoongPanel-Asp.sln.DotSettings.user @@ -0,0 +1,4 @@ + + <AssemblyExplorer> + <Assembly Path="C:\Users\niyyz\.nuget\packages\czgl.systeminfo\2.2.0\lib\net7.0\CZGL.SystemInfo.dll" /> +</AssemblyExplorer> \ No newline at end of file diff --git a/LoongPanel-Asp/ApplicationDbContext.cs b/LoongPanel-Asp/ApplicationDbContext.cs new file mode 100755 index 0000000..f23ab78 --- /dev/null +++ b/LoongPanel-Asp/ApplicationDbContext.cs @@ -0,0 +1,210 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using LoongPanel_Asp.Models; +using LoongPanel_Asp.utils; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; + +namespace LoongPanel_Asp; + +public class ApplicationDbContext(DbContextOptions options) + : IdentityDbContext(options) +{ + public DbSet ApiPermissions { get; set; } + public DbSet RotePermissions { get; set; } + public DbSet ApplicationRoles { get; set; } + public DbSet ServerMonitoringData { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // 配置 Identity 框架的默认实体 + modelBuilder.Entity().ToTable("Users"); + modelBuilder.Entity().ToTable("Roles"); + modelBuilder.Entity().ToTable("Roles"); + modelBuilder.Entity>().ToTable("UserClaims"); + modelBuilder.Entity>().ToTable("UserRoles"); + modelBuilder.Entity>().ToTable("UserLogins"); + modelBuilder.Entity>().ToTable("RoleClaims"); + modelBuilder.Entity>().ToTable("UserTokens"); + + + // 配置 ApiPermission 实体 + modelBuilder.Entity(entity => + { + entity.ToTable("ApiPermissions"); + entity.HasKey(ap => ap.Id); + entity.Property(ap => ap.Name).HasMaxLength(255).IsRequired() + .HasColumnType("varchar(255)"); + entity.Property(ap => ap.Controller).HasMaxLength(100).IsRequired() + .HasColumnType("varchar(100)"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("RotePermissions"); + entity.HasKey(rp => rp.Id); + entity.Property(rp => rp.Name).HasMaxLength(255).IsRequired() + .HasColumnType("varchar(255)"); + entity.Property(rp => rp.Router).HasMaxLength(100).IsRequired() + .HasColumnType("varchar(100)"); + }); + + // 配置 ApplicationRole 实体 + modelBuilder.Entity(entity => + { + entity.Property(r => r.ApiPermissions) + .HasColumnType("longtext") + .HasConversion( + v => string.Join(',', v), + v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList() + ).Metadata.SetValueComparer(new ListOfStringValueComparer()); + + entity.Property(r => r.RouterPermissions) + .HasColumnType("longtext") + .HasConversion( + v => string.Join(',', v), + v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList() + ).Metadata.SetValueComparer(new ListOfStringValueComparer()); + }); + + + // 配置 ServerMonitoringData 实体 + modelBuilder.Entity(entity => + { + entity.ToTable("ServerMonitoringData"); + //id 自增主键 + entity.Property(smd => smd.Id).HasMaxLength(100).ValueGeneratedOnAdd(); + entity.Property(smd => smd.Time).HasMaxLength(100).IsRequired().HasConversion( + v => v!.Value.ToUniversalTime(), v => v.ToLocalTime()); + entity.Property(smd => smd.ServerId).HasMaxLength(100).IsRequired(); + entity.Property(smd => smd.Data).HasMaxLength(100).IsRequired(); + entity.Property(smd => smd.DataName).HasMaxLength(100).IsRequired(); + entity.Property(smd => smd.DataType).HasMaxLength(100).IsRequired(); + }); + InitializeRoles(modelBuilder); + } + + private static void InitializeRoles(ModelBuilder modelBuilder) + { + string[] roleNames = ["Admin", "User"]; + + foreach (var roleName in roleNames) + { + //如果是管理员角色,则赋予所有权限 + if (roleName == "Admin") + { + modelBuilder.Entity().HasData(new ApplicationRole + { + Id = roleName.ToLowerInvariant(), + Name = roleName, + NormalizedName = roleName.ToUpperInvariant(), + ApiPermissions = ["*"], + RouterPermissions = ["*"] + }); + continue; + } + + modelBuilder.Entity().HasData(new ApplicationRole + { + Id = roleName.ToLowerInvariant(), + Name = roleName, + NormalizedName = roleName.ToUpperInvariant(), + ApiPermissions = + ["1","2,","3","4","5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20"], + RouterPermissions = ["1", "2", "3", "4"] + }); + } + + + var apiRouterPermissions = ControllerScanner.GetApiPermissions(); + + foreach (var permission in apiRouterPermissions) + modelBuilder.Entity().HasData(new ApiPermission + { + Id = permission.Id, + Name = permission.Name, + Controller = permission.Controller + }); + + //创建列表 {name:"主页",rote:"/Home"} + List rotePermissions = + [ + new RotePermission { Id = 1, Name = "主页", Router = "/Home" }, + new RotePermission { Id = 2, Name = "用户", Router = "/User" }, + new RotePermission { Id = 3, Name = "cpu", Router = "/Host/Cpu" }, + new RotePermission { Id = 4, Name = "内存", Router = "/Host/Memory" } + ]; + + foreach (var permission in rotePermissions) modelBuilder.Entity().HasData(permission); + } +} + +public class ApplicationUser : IdentityUser +{ + [MaxLength(50)] public string? NickName { get; set; } + + [MaxLength(255)] public required string Avatar { get; set; } + + [MaxLength(255)] public string? Desc { get; set; } + + //职位 + [MaxLength(255)] public required string Posts { get; set; } + + [MaxLength(255)] public string? Address { get; set; } + + [MaxLength(255)] public DateTime? LastLoginTime { get; set; } + + [MaxLength(255)] public required DateTime CreateDate { get; init; } + + [MaxLength(255)] public required DateTime ModifiedDate { get; set; } + + [MaxLength(255)] public string? PhysicalAddress { get; set; } + +} + +public class ApplicationRole : IdentityRole +{ + public required List ApiPermissions { get; set; } + + public required List RouterPermissions { get; set; } +} + +public class ApiPermission +{ + public required int Id { get; set; } + + [MaxLength(255)] public required string Name { get; set; } // 权限名称 + + [MaxLength(100)] public required string Controller { get; set; } // 接口所在控制器 +} + +public class RotePermission +{ + public int Id { get; init; } + + [MaxLength(255)] public required string Name { get; set; } // 权限名称 + [MaxLength(100)] public required string Router { get; set; } // 路由名称 +} + +public class ServerMonitoringData +{ + public int? Id { get; set; } + + public DateTime? Time { get; set; } + + public required string ServerId { get; set; } + + public required string? Data { get; set; } + + public required string DataName { get; set; } + + public required string? DataType { get; set; } +} + +public class ListOfStringValueComparer() : ValueComparer>((c1, c2) => c1!.SequenceEqual(c2!), + c => c.Aggregate(0, (h, v) => HashCode.Combine(h, v.GetHashCode())), + c => c.ToList()); \ No newline at end of file diff --git a/LoongPanel-Asp/Configs/alert.ini b/LoongPanel-Asp/Configs/alert.ini new file mode 100644 index 0000000..013ac4e --- /dev/null +++ b/LoongPanel-Asp/Configs/alert.ini @@ -0,0 +1,2 @@ +[CpuTotalUsage] +Value=80 \ No newline at end of file diff --git a/LoongPanel-Asp/Configs/jobs.ini b/LoongPanel-Asp/Configs/jobs.ini new file mode 100755 index 0000000..8979c78 --- /dev/null +++ b/LoongPanel-Asp/Configs/jobs.ini @@ -0,0 +1,71 @@ +[CpuTotalJob] +Group = CPU +ValueName = CPU使用率 +Description = A simple job that uses the CPU +JobType = LoongPanel_Asp.Jobs.CpuTotalJob, LoongPanel-Asp +Executor = d3YT,xseg +CronExpression = 0/3 * * * * ? * + +[CpuSingleUsageJob] +Group = CPU +ValueName = CPU单核使用率 +Description = A simple job that uses the CPU +JobType = LoongPanel_Asp.Jobs.CpuSingleUsageJob, LoongPanel-Asp +Executor = d3YT,xseg +CronExpression = 5/10 * * * * ? * + +[CpuSpeedJob] +Group = CPU +ValueName = CPU速度 +Description = A simple job that uses the CPU +JobType = LoongPanel_Asp.Jobs.CpuSpeedJob, LoongPanel-Asp +Executor = d3YT,xseg +CronExpression = 2/10 * * * * ? * + +[ProcessTotalJob] +Group = Process +ValueName = 进程总使用数 +Description = A simple job that uses the Process +JobType = LoongPanel_Asp.Jobs.ProcessTotalJob, LoongPanel-Asp +Executor = d3YT,xseg +CronExpression = 3/10 * * * * ? * + +[PhrasePatternJob] +Group = Process +ValueName = 句柄总使用数 +Description = A simple job that uses the Process +JobType = LoongPanel_Asp.Jobs.PhrasePatternJob, LoongPanel-Asp +Executor = d3YT,xseg +CronExpression = 6/30 * * * * ? * + +[MemoryTotalJob] +Group = Memory +ValueName = 内存总使用率 +Description = A simple job that uses the Memory +JobType = LoongPanel_Asp.Jobs.MemoryTotalJob, LoongPanel-Asp +Executor = d3YT,xseg +CronExpression = 3/5 * * * * ? * + +[DiskTotalJob] +Group = Disk +ValueName = 磁盘总使用率 +Description = A simple job that uses the Disk +JobType = LoongPanel_Asp.Jobs.DiskTotalJob, LoongPanel-Asp +Executor = d3YT,xseg +CronExpression = 2 0/10 * * * ? * + +[DiskUseJob] +Group = Disk +ValueName = 磁盘总数据 +Description = A simple job that uses the Disk +JobType = LoongPanel_Asp.Jobs.DiskUseJob, LoongPanel-Asp +Executor = d3YT,xseg +CronExpression = 4/10 * * * * ? * + +[NetworkTotalJob] +Group = Network +ValueName = 网络总使用率 +Description = A simple job that uses the Network +JobType = LoongPanel_Asp.Jobs.NetworkTotalJob, LoongPanel-Asp +Executor = d3YT,xseg +CronExpression = 3/15 * * * * ? * \ No newline at end of file diff --git a/LoongPanel-Asp/Configs/servers.ini b/LoongPanel-Asp/Configs/servers.ini new file mode 100644 index 0000000..bf3fd32 --- /dev/null +++ b/LoongPanel-Asp/Configs/servers.ini @@ -0,0 +1,15 @@ +[d3YT] +address = 192.168.0.26 +port = 22 +serverName = 龙芯 +password = loongnix +username = loongnix +https = false + +[xseg] +address = 127.0.0.1 +port = 22 +serverName = 本机 +password = 123123 +username = zwb +https = false \ No newline at end of file diff --git a/LoongPanel-Asp/Controllers/AccountController.cs b/LoongPanel-Asp/Controllers/AccountController.cs new file mode 100755 index 0000000..a38f2fc --- /dev/null +++ b/LoongPanel-Asp/Controllers/AccountController.cs @@ -0,0 +1,203 @@ +using System.Security.Claims; +using LiteDB; +using LoongPanel_Asp.Helpers; +using LoongPanel_Asp.Models; +using LoongPanel_Asp.Types; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; + +namespace LoongPanel_Asp.Controllers; + +[ApiController] +[Route("Api/[controller]")] +public class AccountController( + UserManager userManager, + SignInManager signInManager, + EmailHelper emailHelper, + TokenHelper tokenHelper, + ILiteDatabase db) + : ControllerBase +{ + [HttpPost("SendVerificationCode")] + public async Task SendVerificationCode([FromBody] EmailModel model) + { + if (!ModelState.IsValid) return BadRequest(ModelState); + + try + { + var code = emailHelper.GenerateVerificationCode(); + Console.WriteLine(code); + await emailHelper.SendVerificationEmailAsync(model.Email, code); + + // 生成过期时间 5分钟 + var expireTime = DateTime.Now.AddMinutes(5); + var col = db.GetCollection("EmailCode"); + var userCode = new EmailCode + { + Email = model.Email, + Code = code, + ExpireTime = expireTime + }; + + col.EnsureIndex(x => x.Email, true); + + // 使用 Upsert 来插入或更新验证码记录 + col.DeleteMany(x => x.Email == model.Email); + col.Insert(userCode); + + // 注册成功,返回一个合适的响应 + return Ok("Registration successful. Please check your email for the verification code."); + } + catch (Exception ex) + { + // Log the exception + Console.WriteLine(ex.Message); + // 返回一个错误响应 + return StatusCode(StatusCodes.Status500InternalServerError, + $"An error occurred while processing your request.{ex.Message}"); + } + } + + [HttpPost("Register")] + public async Task Register([FromBody] RegisterModel model) + { + if (!ModelState.IsValid) return BadRequest(new ApiResponse(ApiResponseState.Error, "Invalid request")); + + try + { + // 获取code,email + var col = db.GetCollection("EmailCode"); + // 使用email查询 + var userCode = col.FindOne(x => x.Email == model.Email); + // 判断结果 + if (userCode == null) return BadRequest(new ApiResponse(ApiResponseState.Error, "Code not found")); + + if (userCode.Code != model.Code) + return BadRequest(new ApiResponse(ApiResponseState.Error, "Code does not match")); + + // 判断是否过期 + if (userCode.ExpireTime < DateTime.Now) + return BadRequest(new ApiResponse(ApiResponseState.Error, "Code expired")); + + // 注册用户 + if (HttpContext.RequestServices.GetService(typeof(UserManager)) is not + UserManager manager) + return StatusCode(StatusCodes.Status500InternalServerError, + new ApiResponse(ApiResponseState.Error, "UserManager not found")); + + var user = new ApplicationUser + { + UserName = model.UserName, + Email = model.Email, + PhoneNumber = model.Phone, + NickName = model.NickName, + EmailConfirmed = true, + + Avatar = $"https://api.multiavatar.com/{model.UserName}.svg", + Posts = "员工", + CreateDate = DateTime.UtcNow, + ModifiedDate = DateTime.UtcNow + }; + + var result = await manager.CreateAsync(user, model.Password); + // 验证成功,删除验证码 + col.DeleteMany(x => x.Email == model.Email); + + if (!result.Succeeded) + return BadRequest(new ApiResponse(ApiResponseState.Error, "User creation failed", null, result.Errors)); + + // 添加用户角色 + if (HttpContext.RequestServices.GetService(typeof(RoleManager)) is not + RoleManager roleManager) + { + // 如果角色管理器不存在,删除刚刚创建的用户 + await manager.DeleteAsync(user); + return StatusCode(StatusCodes.Status500InternalServerError, + new ApiResponse(ApiResponseState.Error, "RoleManager not found")); + } + + var roleResult = await manager.AddToRoleAsync(user, "user"); + + if (!roleResult.Succeeded) + { + // 如果角色分配失败,删除刚刚创建的用户 + await manager.DeleteAsync(user); + return BadRequest(new ApiResponse(ApiResponseState.Error, "Role assignment failed", null, + roleResult.Errors)); + } + + // 用户注册成功 + return Ok(new ApiResponse(ApiResponseState.Success, "User registered successfully")); + } + catch (Exception ex) + { + // Log the exception + Console.WriteLine(ex.Message); + return StatusCode(StatusCodes.Status500InternalServerError, + "An error occurred while processing your request."); + } + } + + [HttpPost("VerifyEmailName")] + public async Task VerifyEmailName([FromBody] VerifyEmailNameModel model) + { + if (!ModelState.IsValid) return BadRequest(new ApiResponse(ApiResponseState.Error, "Invalid request", null)); + + var user = await userManager.FindByEmailAsync(model.Email); + + if (user != null) return Ok(new ApiResponse(ApiResponseState.Error, "Email already exists", null)); + + var userName = await userManager.FindByNameAsync(model.UserName); + + if (userName != null) return Ok(new ApiResponse(ApiResponseState.Error, "UserName already exists", null)); + + return Ok(new ApiResponse(ApiResponseState.Success, "Email and UserName are available", null)); + } + + [HttpPost("Login")] + public async Task Login([FromBody] LoginModel model) + { + if (!ModelState.IsValid) return BadRequest(new ApiResponse(ApiResponseState.Error, "Invalid request")); + + try + { + ApplicationUser? user = null; + // 判断字符串是否包含@ + if (model.EmailOrUserName.Contains('@')) + user = await userManager.FindByEmailAsync(model.EmailOrUserName); + else + user = await userManager.FindByNameAsync(model.EmailOrUserName); + if (user == null) return Ok(new ApiResponse(ApiResponseState.Error, "Invalid email or username")); + Console.WriteLine(user.UserName); + var result = await signInManager.CheckPasswordSignInAsync(user, model.Password, model.RememberMe); + if (!result.Succeeded) return Ok(new ApiResponse(ApiResponseState.Error, "Invalid password")); + + var roles = await userManager.GetRolesAsync(user); + var roleId = roles.ToList()[0]; // 直接获取角色ID列表 + var claimsIdentity = new ClaimsIdentity(new[] + { + // userId + new Claim(ClaimTypes.NameIdentifier, user.Id), + // email + new Claim(ClaimTypes.Email, user.Email!), + // role + new Claim(ClaimTypes.Role, roleId.ToLower()) // 将角色ID列表转换为逗号分隔的字符串 + }); + var token = tokenHelper.GenerateToken(claimsIdentity); + + return Ok(new ApiResponse(ApiResponseState.Success, "Login successful", new + { + user.UserName, + Token = token + })); + ; + } + catch (Exception ex) + { + // Log the exception + // logger.LogError(ex, "An error occurred while processing the login request."); + return StatusCode(StatusCodes.Status500InternalServerError, + new ApiResponse(ApiResponseState.Error, ex.Message)); + } + } +} \ No newline at end of file diff --git a/LoongPanel-Asp/Controllers/JobController.cs b/LoongPanel-Asp/Controllers/JobController.cs new file mode 100755 index 0000000..d21d192 --- /dev/null +++ b/LoongPanel-Asp/Controllers/JobController.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Mvc; + +namespace LoongPanel_Asp.Controllers; + +[ApiController] +[Route("Api/[controller]")] +public class JobController(ApplicationDbContext dbContext) : ControllerBase +{ + [HttpGet("GetJobList")] + public IActionResult GetJobList(string serverId) + { + var serverMonitoringData = dbContext.ServerMonitoringData; + //获得所有DataName类型 + var dataNameTypesWithDataType = serverMonitoringData + .GroupBy(x => x.DataName) // 假设DataName是ServerMonitoringData实体中的一个属性 + .Select(group => new + { + DataName = group.Key, group.First().DataType // 假设DataType是ServerMonitoringData实体中的一个属性 + }) + .ToList(); + + return Ok(dataNameTypesWithDataType); // 返回所有唯一的DataName类型及其对应的DataType + } +} \ No newline at end of file diff --git a/LoongPanel-Asp/Controllers/RoteController.cs b/LoongPanel-Asp/Controllers/RoteController.cs new file mode 100755 index 0000000..553f10c --- /dev/null +++ b/LoongPanel-Asp/Controllers/RoteController.cs @@ -0,0 +1,31 @@ +using System.Security.Claims; +using System.Text.RegularExpressions; +using LoongPanel_Asp.Helpers; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; + +namespace LoongPanel_Asp.Controllers; + +[ApiController] +[Route("Api/[controller]")] +public class RoteController( + RoleManager roleManager, + ApplicationDbContext dbContext) : ControllerBase +{ + [HttpGet("RoteVerify")] + public async Task RoteVerify(string path) + { + var roleId = HttpContext.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Role)!.Value; + //获得role + Console.WriteLine(roleId); + var role = await roleManager.FindByNameAsync(roleId); + var rotes = role!.RouterPermissions.ToList(); + //获取路由列表 + var apiPermissions = dbContext.RotePermissions.ToList().Select(x => x.Router); + //将path全部小写 + path = path.ToLower(); + //使用正则匹配 + var firstOrDefault = apiPermissions.FirstOrDefault(x => Regex.IsMatch(path,x)); + return Ok(firstOrDefault != null ? new ApiResponse(ApiResponseState.Success) : new ApiResponse(ApiResponseState.Forbidden)); + } +} \ No newline at end of file diff --git a/LoongPanel-Asp/Controllers/ServerController.cs b/LoongPanel-Asp/Controllers/ServerController.cs new file mode 100755 index 0000000..2e33789 --- /dev/null +++ b/LoongPanel-Asp/Controllers/ServerController.cs @@ -0,0 +1,278 @@ +using System.Collections.Concurrent; +using System.Collections.Specialized; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Text.RegularExpressions; +using LoongPanel_Asp.Helpers; +using LoongPanel_Asp.Models; +using LoongPanel_Asp.Servers; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LoongPanel_Asp.Controllers; + +[ApiController] +[Route("Api/[controller]")] +public class ServerController(IServiceProvider serviceProvider, ApplicationDbContext dbContext) : ControllerBase +{ + //获取远程服务器地址列表 + [HttpGet("GetServerList")] + public IActionResult GetServerList() + { + var serverConfigs=JobConfigHelper.GetServers(); + //读取每一个配置项转换为列表 + var serverList = serverConfigs.Select(section => new ServerInfo + { + Name = section.ServerName, + Id = section.Id + }).ToList(); + return Ok(serverList); + } + + [HttpGet("GetServerInfo")] + public async Task GetServerInfo(string serverId) + { + var sshClient = serviceProvider.GetService(); + var output = await sshClient?.ExecuteCommandAsync(serverId, "neofetch", "--off", "|", "sed", + @"'s/\x1B\[[0-9;]*[a-zA-Z]//g'")!; + if (string.IsNullOrEmpty(output)) return BadRequest(); + Dictionary paresData = new(); + var pattern = @"^(.*?)\s*:\s*(.*?)\s*$"; + var matches = Regex.Matches(output ?? "", pattern, RegexOptions.Multiline); + foreach (Match match in matches) paresData[match.Groups[1].Value] = match.Groups[2].Value; + return Ok(paresData); + } + + [HttpGet("GetServerHistoryDate")] + public async Task GetServerHistoryDate([FromQuery] string serverId, + [FromQuery] List? dataTypes = null, [FromQuery] int? startIndex = 0) + { + if (string.IsNullOrWhiteSpace(serverId)) return BadRequest("ServerId is required."); + + var query = dbContext.ServerMonitoringData + .Where(s => s.ServerId == serverId && s.DataType != null); + + if (dataTypes != null && dataTypes.Any()) query = query.Where(s => dataTypes.Contains(s.DataType)); + + var allData = await query + .OrderByDescending(s => s.Time) + .Skip(startIndex ?? 0) + .Take(1000) + .ToListAsync(); + + if (allData.Count == 0) return Ok(new { done = true }); + + // 获取时间集合并排序 + var timeList = allData.Select(s => s.Time).Distinct().OrderBy(s => s).ToList(); + + // 获取DataType + var groupedData = new ConcurrentDictionary>(); + var distinctDataTypes = dataTypes ?? allData.Select(s => s.DataType).Distinct().ToList(); + + foreach (var dataType in distinctDataTypes) + { + var dataList = allData.Where(s => s.DataType == dataType).ToList(); + var backDataList = new List(); + var temp = double.Parse(dataList[0].Data ?? string.Empty); + foreach (var time in timeList) + { + var currentData = dataList.Where(d => d.Time == time).ToList(); + if (currentData.Count > 0) + { + //计算data平均值 + var dataSum = currentData.Sum(d => double.Parse(d.Data ?? string.Empty)); + temp = dataSum; + } + + backDataList.Add(temp); + } + + if (dataType != null) groupedData[dataType] = backDataList; + } + + //timelist转换为当前时间 月 日 分 秒 + var timeList2 = timeList.Select(time => time?.ToLocalTime().ToString("MM-dd HH:mm:ss")).ToList(); + + // 返回结果 + + var returnValue = new + { + times = timeList2, + data = groupedData, + endIndex = dataTypes is { Count: > 0 } ? startIndex + timeList2.Count * dataTypes.Count : + startIndex + allData.Count < 1000 ? allData.Count : 1000, + done = false + }; + + return Ok(returnValue); + } + + + [HttpGet("GetServerCpuInfo")] + public async Task GetServerCpuInfo(string serverId) + { + var sshClient = serviceProvider.GetService(); + var output = await sshClient?.ExecuteCommandAsync(false, serverId, "lscpu", "-J")!; + if (string.IsNullOrEmpty(output)) return BadRequest(); + return Ok(output); + } + + [HttpGet("GetServerHistoryStep")] + public async Task GetServerHistoryStep([FromQuery] string serverId, [FromQuery] string dataType, + [FromQuery] string timeRange) + { + var query = dbContext.ServerMonitoringData + .Where(s => s.ServerId == serverId && s.DataType != null && s.DataType == dataType); + int timeSpan; + int timeNum; + switch (timeRange.ToLower()) + { + case "1m": + timeSpan = 1; + timeNum = 60; + break; + case "1h": + timeSpan = 60; + timeNum = 60; + break; + case "1d": + timeSpan = 60 * 12; + timeNum = 12; + break; + default: + return BadRequest("Invalid time range."); + } + + var time = DateTime.UtcNow; + var timeList = new List(); + var dataList = new List(); + for (var i = 0; i < timeNum; i++) + { + var endTime = time.AddSeconds(-(timeSpan * i) + 3 * timeSpan); + var startTime = time.AddSeconds(-(timeSpan * i) - 3 * timeSpan); + var data = query.FirstOrDefault(d => d.Time <= endTime && d.Time >= startTime); + var value = "0"; + if (data is { Data: not null }) value = data.Data; + timeList.Add(startTime); + dataList.Add(value); + } + + dataList.Reverse(); + var backData = new + { + data = dataList, + time = timeList.Select(dateTime => dateTime.ToLocalTime().ToString("MM-dd HH:mm:ss")).ToList() + }; + return Ok(backData); + } + + [HttpGet("GetServerMemoryInfo")] + public async Task GetServerMemoryInfo([FromQuery] string serverId) + { + var sshClient = serviceProvider.GetService(); + var serverConfigs = JobConfigHelper.GetServers().ToList(); + var server = serverConfigs.Find(x =>x.Id == serverId); + if (server == null) return BadRequest(); + var output = await sshClient?.ExecuteCommandAsync(false, serverId,"echo",$"'{server.Password}'","|","sudo","-S","lshw","-class","memory","-json")!; + if (string.IsNullOrEmpty(output)) return BadRequest(); + return Ok(output); + } + + + [HttpGet("GetServerDiskList")] + public async Task GetServerDiskList([FromQuery] string serverId) + { + var sshClient = serviceProvider.GetService(); + var serverConfigs = JobConfigHelper.GetServers().ToList(); + var server = serverConfigs.Find(x =>x.Id == serverId); + if (server == null) return BadRequest(); + var output = await sshClient?.ExecuteCommandAsync(serverId,$"echo {server.Password}","|","sudo -S fdisk -l","|","grep 'Disk /'","|","awk '{print $2,$3}'")!; + if (string.IsNullOrEmpty(output)) return BadRequest(); + var diskList = output.Split("\n", StringSplitOptions.RemoveEmptyEntries); + var outList = diskList.Select(disk => disk.Split(":", StringSplitOptions.RemoveEmptyEntries)).Select(info => new { name = info[0], size = info[1] }).ToList(); + return Ok(outList); + } + + [HttpGet("GetServerNetworkEquipmentList")] + public async Task GetServerNetworkEquipmentList([FromQuery] string serverId) + { + var sshClient = serviceProvider.GetService(); + var serverConfigs = JobConfigHelper.GetServers().ToList(); + var server = serverConfigs.Find(x =>x.Id == serverId); + if (server == null) return BadRequest(); + var output = await sshClient?.ExecuteCommandAsync(serverId,"ip link show" ,"|","grep '^[0-9]'","|","awk -F': ' '{print $2}'")!; + if (string.IsNullOrEmpty(output)) return BadRequest(); + var data = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList(); + return Ok(data); + } + [HttpGet("GetServerGpuList")] + public async Task GetServerGpuList([FromQuery] string serverId) + { + var sshClient = serviceProvider.GetService(); + var serverConfigs = JobConfigHelper.GetServers().ToList(); + var server = serverConfigs.Find(x =>x.Id == serverId); + if (server == null) return BadRequest(); + var output = await sshClient?.ExecuteCommandAsync(serverId,"lspci" ,"|","awk '/VGA/ {print $1}' ")!; + if (string.IsNullOrEmpty(output)) return BadRequest(); + var data = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList(); + return Ok(data); + } + + [HttpGet("GetServerDiskInfo")] + public async Task GetServerDiskInfo([FromQuery] string serverId,[FromQuery] string diskId) + { + var sshClient = serviceProvider.GetService(); + var serverConfigs = JobConfigHelper.GetServers().ToList(); + var server = serverConfigs.Find(x =>x.Id == serverId); + if (server == null) return BadRequest(); + diskId = $"/dev/{diskId}"; + var output = await sshClient?.ExecuteCommandAsync(serverId,$"echo '{server.Password}'","|","sudo -S","smartctl -i",diskId,"-T permissive","|","awk 'NR>4'")!; + if (string.IsNullOrEmpty(output)) return BadRequest(); + var diskInfo = output + .Split('\n', StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Trim().Split(':')) + .ToList().Select(x => new + { + name=x[0].Replace(" ","_"), + value=x[1] + }).ToList(); + return Ok(diskInfo); + } + + [HttpGet("GetServerTerminalPath")] + public async Task GetServerTerminalPath([FromQuery] string serverId) + { + var serverConfig = JobConfigHelper.GetServers().Find(x => x.Id == serverId); + if (serverConfig == null) return BadRequest(); + //获得本机ip + string? localIp; + using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0)) + { + await socket.ConnectAsync("8.8.8.8", 65530); + var endPoint = socket.LocalEndPoint as IPEndPoint; + localIp = endPoint?.Address.ToString(); + } + //拼接网络路径 + var uriBuilder = new UriBuilder + { + Scheme = "http", + Host = localIp, + Port = 8888, + }; + var query = System.Web.HttpUtility.ParseQueryString(uriBuilder.Query); + query["hostname"]=serverConfig.Address; + query["username"] = serverConfig.Username; + query["password"] = Convert.ToBase64String(Encoding.UTF8.GetBytes(serverConfig.Password)); + query["port"] = serverConfig.Port.ToString(); + uriBuilder.Query = query.ToString(); + var url = uriBuilder.Uri.ToString(); + return Ok(url); + } +} + +public class ServerInfo +{ + public required string Name { get; init; } + public required string Id { get; init; } +} diff --git a/LoongPanel-Asp/Controllers/UserController.cs b/LoongPanel-Asp/Controllers/UserController.cs new file mode 100755 index 0000000..ec3ae0d --- /dev/null +++ b/LoongPanel-Asp/Controllers/UserController.cs @@ -0,0 +1,97 @@ +using System.Security.Claims; +using LoongPanel_Asp.Helpers; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LoongPanel_Asp.Controllers; + +[ApiController] +[Route("Api/[controller]")] +public class UserController(UserManager userManager) : ControllerBase +{ + [HttpGet("Info")] + public async Task Info() + { + var userId = HttpContext.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)!.Value; + // 获取用户的信息 + var user = await userManager.FindByIdAsync(userId); + + // 创建一个匿名对象,只包含您想要公开的信息 + var userInfo = new + { + user!.NickName, + user.Id, + user.UserName, + user.NormalizedUserName, + user.Email, + user.NormalizedEmail, + user.EmailConfirmed, + user.PhoneNumber, + user.PhoneNumberConfirmed, + user.TwoFactorEnabled, + user.Avatar, + user.Desc, + user.Posts, + }; + + // 返回用户信息 + return Ok(new ApiResponse(ApiResponseState.Success, "获取成功", userInfo)); + } + + [HttpPost("GetLayoutConfig")] + public async Task GetLayoutConfig([FromBody] LayoutModel model) + { + //从session中获取用户信息 + var userId = HttpContext.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)!.Value; + //从UserConfigs/Layouts/{userId中获取用户配置信息 + var fullPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UserConfigs", "Layouts", userId + ".json"); + //判断是否存在 + if (!System.IO.File.Exists(fullPath)) + fullPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UserConfigs", "Layouts", "default.json"); + //读取文件内容 + var content = await System.IO.File.ReadAllTextAsync(fullPath); + return Ok(model.Layout != content ? content : "null"); + } + + [HttpPut("PutLayoutConfig")] + public async Task PutLayoutConfig([FromBody] LayoutModel model) + { + //从session中获取用户信息 + var userId = HttpContext.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)!.Value; + var fullPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UserConfigs", "Layouts", userId + ".json"); + //覆盖文件 + await System.IO.File.WriteAllTextAsync(fullPath, model.Layout); + return Ok(); + } + + [HttpGet("GetUserInfoList")] + public async Task GetUserInfoList() + { + var users = await userManager.Users.ToListAsync(); + // 创建一个匿名对象,只包含您想要公开的信息 + var userInfoList = users.Select(user => new + { + user.Address, + user.Avatar, + CreateDate=user.CreateDate.ToLocalTime().ToString("U"), + user.Desc, + user.Email, + user.Id, + user.NickName, + user.PhoneNumber, + LastLoginTime = user.LastLoginTime?.ToLocalTime().ToString("U"), + user.Posts, + ModifiedDate=user.ModifiedDate.ToLocalTime().ToString("U"), + user.UserName, + user.PhysicalAddress, + IsLock=user.LockoutEnd > DateTimeOffset.UtcNow + }).ToList(); + return Ok(userInfoList); + } +} + +public class LayoutModel +{ + public string? Layout { get; set; } +} \ No newline at end of file diff --git a/LoongPanel-Asp/Helpers/ApiHelper.cs b/LoongPanel-Asp/Helpers/ApiHelper.cs new file mode 100755 index 0000000..b0a2367 --- /dev/null +++ b/LoongPanel-Asp/Helpers/ApiHelper.cs @@ -0,0 +1,48 @@ +namespace LoongPanel_Asp.Helpers; + +public enum ApiResponseState +{ + Success = 200, + NotFound = 404, + Error = 500, + + Forbidden = 403 + // 您可以添加更多的响应状态 +} + +public class ApiResponse +{ + public ApiResponse(ApiResponseState state, string message = "", object? data = null, object? error = null) + { + Code = (int)state; + Data = data; + Message = string.IsNullOrEmpty(message) ? state.ToString() : message; + Error = error; + + // 根据状态设置默认的消息和错误 + switch (state) + { + case ApiResponseState.Success: + Message = message ?? "操作成功"; + break; + case ApiResponseState.NotFound: + Message = message ?? "未找到资源"; + Error = error ?? new { Message = "请求的资源不存在" }; + break; + case ApiResponseState.Forbidden: + Message = message ?? "禁止访问"; + Error = error ?? new { Message = "您没有权限访问该资源" }; + break; + case ApiResponseState.Error: + default: + Message = message ?? "服务器错误"; + Error = error; + break; + } + } + + public int Code { get; set; } + public object? Data { get; set; } + public string Message { get; set; } + public object? Error { get; set; } +} \ No newline at end of file diff --git a/LoongPanel-Asp/Helpers/DataHelper.cs b/LoongPanel-Asp/Helpers/DataHelper.cs new file mode 100755 index 0000000..2297d2a --- /dev/null +++ b/LoongPanel-Asp/Helpers/DataHelper.cs @@ -0,0 +1,37 @@ +namespace LoongPanel_Asp.Helpers; + +public class DataHelper(ApplicationDbContext dbContext) +{ + public async Task SaveData(ServerMonitoringData data) + { + // 保存数据到数据库 + var dataDb = dbContext.ServerMonitoringData; + //获取当前时间 + var time = DateTime.UtcNow; + data.Time = time; + dataDb.Add(data); + //提交 + await dbContext.SaveChangesAsync(); + } + + //批量添加 + public async Task SaveData(List data) + { + var dataDb = dbContext.ServerMonitoringData; + var time = DateTime.UtcNow; + foreach (var i in data) + { + i.Time = time; + dataDb.Add(i); + } + + await dbContext.SaveChangesAsync(); + } + public static async Task CheckData(string serverId,string dataType,double value) + { + var alertConfigs = JobConfigHelper.GetAlerts(); + var alert=alertConfigs[serverId][dataType]; + + + } +} \ No newline at end of file diff --git a/LoongPanel-Asp/Helpers/EmailHelper.cs b/LoongPanel-Asp/Helpers/EmailHelper.cs new file mode 100755 index 0000000..bdcf598 --- /dev/null +++ b/LoongPanel-Asp/Helpers/EmailHelper.cs @@ -0,0 +1,44 @@ +using LiteDB; +using MailKit.Net.Smtp; +using MailKit.Security; +using MimeKit; +using MimeKit.Text; + +namespace LoongPanel_Asp.Helpers; + +public class EmailHelper( + ILiteDatabase? database, + string? smtpHost, + int smtpPort, + string smtpUsername, + string smtpPassword) +{ + private readonly string? _smtpHost = smtpHost; + private readonly string _smtpPassword = smtpPassword; + private readonly int _smtpPort = smtpPort; + private readonly string _smtpUsername = smtpUsername; + private ILiteDatabase? _database = database; + + public async Task SendVerificationEmailAsync(string toEmail, string code) + { + var emailMessage = new MimeMessage(); + emailMessage.From.Add(new MailboxAddress("龙腾云御", _smtpUsername)); + emailMessage.To.Add(new MailboxAddress("", toEmail)); + var body = $"

请验证您的邮箱

这是你的验证码

{code}

"; + emailMessage.Body = new TextPart(TextFormat.Html) + { + Text = body + }; + + using var client = new SmtpClient(); + await client.ConnectAsync(_smtpHost, _smtpPort, SecureSocketOptions.SslOnConnect); + await client.AuthenticateAsync(_smtpUsername, _smtpPassword); + await client.SendAsync(emailMessage); + await client.DisconnectAsync(true); + } + + public string GenerateVerificationCode() + { + return Guid.NewGuid().ToString()[..6]; + } +} \ No newline at end of file diff --git a/LoongPanel-Asp/Helpers/JobConfigHelper.cs b/LoongPanel-Asp/Helpers/JobConfigHelper.cs new file mode 100755 index 0000000..c195ed5 --- /dev/null +++ b/LoongPanel-Asp/Helpers/JobConfigHelper.cs @@ -0,0 +1,146 @@ +using System.Text; +using IniParser; +using LoongPanel_Asp.Models; + +namespace LoongPanel_Asp.Helpers; + +public static class JobConfigHelper +{ + private static List? _serverConfigs; + private static Dictionary>>? _alertsConfigs; + + public static IEnumerable ReadJobConfigurations() + { + // 创建ini文件读取器 + var parser = new FileIniDataParser(); + + var fullPath = Path.Combine(Environment.CurrentDirectory, "Configs", "jobs.ini"); + + // 读取ini文件 + var data = parser.ReadFile(fullPath, Encoding.UTF8); + // 获取所有section + var sections = data.Sections; + + var serverConfigs = GetServers(); + // 创建一个jobConfig列表 + var jobConfigs = new List(); + foreach (var section in sections) + jobConfigs.Add(new JobConfiguration + { + Name = section.SectionName, + Group = section.Keys["Group"], + JobType = section.Keys["JobType"], + Description = section.Keys["Description"], + CronExpression = section.Keys["CronExpression"], + ValueName = section.Keys["ValueName"], + Executor = section.Keys["Executor"].Split(',').Select(executor => + { + // 查找serverConfigs 中id匹配 未找到则跳过 + var server = serverConfigs.FirstOrDefault(server => server.Id == executor); + // 如果找到则返回server对象,否则不添加 + return server; + }).Where(server => server != null).ToList() + }); + + // 返回 + return jobConfigs; + } + + public static List GetServers() + { + // 检查是否已经读取过配置,如果有,直接返回 + if (_serverConfigs != null) return _serverConfigs; + + // 创建ini文件读取器 + var parser = new FileIniDataParser(); + var serverFullPath = Path.Combine(Environment.CurrentDirectory, "Configs", "servers.ini"); + var serverData = parser.ReadFile(serverFullPath, Encoding.UTF8); + var serverSections = serverData.Sections; + + // 读取配置信息 + _serverConfigs = serverSections.Select(section => new ServerModel + { + Id = section.SectionName, + Address = section.Keys["address"], + Port = int.Parse(section.Keys["port"]), + ServerName = section.Keys["serverName"], + Password = section.Keys["password"], + Username = section.Keys["username"], + Http = bool.Parse(section.Keys["https"]) + }).ToList(); + return _serverConfigs; + } + + public static Dictionary>> GetAlerts() + { + if (_alertsConfigs != null) return _alertsConfigs; + + // 创建_alertsConfigs + _alertsConfigs = new Dictionary>>(); + var parser = new FileIniDataParser(); + var alertsFolderPath = Path.Combine(Environment.CurrentDirectory, "Configs", "Alerts"); + var defaultAlert = Path.Combine(Environment.CurrentDirectory, "Configs", "alert.ini"); + // 检查目录是否存在,如果不存在则创建 + if (!Directory.Exists(alertsFolderPath)) Directory.CreateDirectory(alertsFolderPath); + + // 获取目录下所有.ini文件的路径 + var alertFiles = Directory.GetFiles(alertsFolderPath, "*.ini"); + foreach (var filePath in alertFiles) + { + var name = Path.GetFileNameWithoutExtension(filePath); + var parts = name.Split('_'); + var serverId = parts[0] ; + var userId = parts[1]; + // 读取ini文件 + + var data = parser.ReadFile(filePath, Encoding.UTF8); + Console.WriteLine(data.ToString()); + // 获取所有section + foreach (var section in data.Sections) + { + // 解析每个section为AlertsModel + var type = section.SectionName; + var value = double.Parse(section.Keys["Value"]); + + // 添加到字典 + if (serverId != null && !(_alertsConfigs.ContainsKey(serverId))) + { + _alertsConfigs[serverId] = new Dictionary>(); + } + if (serverId != null && !_alertsConfigs[serverId].ContainsKey(type)) + { + _alertsConfigs[serverId][type] = new Dictionary(); + } + + if (userId == null) continue; + if (serverId != null) _alertsConfigs[serverId][type][userId] = value; + } + } + + return _alertsConfigs; + } +} + +public class JobConfiguration +{ + //名称 + public required string Name { get; init; } + + //分组 + public required string Group { get; init; } + + //描述 + public string? Description { get; init; } + + //执行者 + public required List Executor { get; init; } + + //任务类型 + public required string JobType { get; init; } + + //cron表达式 + public required string CronExpression { get; init; } + + //ValueName + public string? ValueName { get; init; } +} diff --git a/LoongPanel-Asp/Helpers/PrometheusQueryHelper.cs b/LoongPanel-Asp/Helpers/PrometheusQueryHelper.cs new file mode 100755 index 0000000..210c1a7 --- /dev/null +++ b/LoongPanel-Asp/Helpers/PrometheusQueryHelper.cs @@ -0,0 +1,50 @@ +using System.Text.Json; +using LoongPanel_Asp.Models; + +namespace LoongPanel_Asp.Helpers; + +public class PrometheusQueryHelper +{ + private readonly HttpClient _httpClient = new(); + + public async Task QueryPrometheus(string query, string prometheusAddress, int prometheusPort, bool https) + { + try + { + var uriBuilder = new UriBuilder + { + Scheme = https ? "https" : "http", // 这里可以根据需要设置为 http 或 https + Host = prometheusAddress, + Port = prometheusPort, + Path = "/api/v1/query", + Query = $"query={Uri.EscapeDataString(query)}" + }; + Console.WriteLine(uriBuilder.ToString()); + var response = await _httpClient.GetAsync(uriBuilder.ToString()); + + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(content); + return result.Data.Result.Last().Value.Last().ToString(); + } + catch (JsonException ex) + { + // 这里处理反序列化错误 + Console.WriteLine($"Failed to deserialize Prometheus response: {ex.Message}"); + return null; + } + catch (HttpRequestException ex) + { + // 这里处理请求错误 + Console.WriteLine($"Request to Prometheus failed: {ex.Message}"); + return null; + } + catch (Exception ex) + { + // 这里处理其他错误 + Console.WriteLine($"An error occurred: {ex.Message}"); + return null; + } + } +} \ No newline at end of file diff --git a/LoongPanel-Asp/Helpers/TokenHelper.cs b/LoongPanel-Asp/Helpers/TokenHelper.cs new file mode 100755 index 0000000..69e30cb --- /dev/null +++ b/LoongPanel-Asp/Helpers/TokenHelper.cs @@ -0,0 +1,65 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.IdentityModel.Tokens; + +namespace LoongPanel_Asp.Helpers; + +public class TokenHelper(IConfiguration configuration) +{ + public string GenerateToken(ClaimsIdentity claimsIdentity) + { + var tokenHandler = new JwtSecurityTokenHandler(); + var key = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(configuration["Secret"] ?? throw new InvalidOperationException())); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = claimsIdentity, + Issuer = configuration["Issuer"], + Audience = configuration["Audience"], + NotBefore = DateTime.UtcNow, + Expires = DateTime.UtcNow.AddDays(7), + SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature) + }; + var token = tokenHandler.CreateToken(tokenDescriptor); + return tokenHandler.WriteToken(token); + } + + public ClaimsPrincipal? ValidateToken(string token) + { + var tokenHandler = new JwtSecurityTokenHandler(); + var key = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(configuration["Secret"] ?? throw new InvalidOperationException())); + try + { + tokenHandler.ValidateToken(token, new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = key, + ValidateIssuer = true, + ValidIssuer = configuration["Issuer"], + ValidateAudience = true, + ValidAudience = configuration["Audience"], + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero + }, out var validatedToken); + + return tokenHandler.ValidateToken(token, new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = key, + ValidateIssuer = true, + ValidIssuer = configuration["Issuer"], + ValidateAudience = true, + ValidAudience = configuration["Audience"], + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero + }, out _); + } + catch + { + // 如果token无效,返回null或者抛出异常 + return null; + } + } +} \ No newline at end of file diff --git a/LoongPanel-Asp/Hubs/SessionHub.cs b/LoongPanel-Asp/Hubs/SessionHub.cs new file mode 100755 index 0000000..68b4760 --- /dev/null +++ b/LoongPanel-Asp/Hubs/SessionHub.cs @@ -0,0 +1,90 @@ +using System.Security.Claims; +using LiteDB; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.SignalR; + +namespace LoongPanel_Asp.Hubs; + +public class SessionHub(UserManager userManager, ILiteDatabase database) + : Hub +{ + public override async Task OnConnectedAsync() + { + var userId = Context.User!.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value; + Console.WriteLine(userId); + //获得登陆ip + var ip = Context.GetHttpContext()!.Connection.RemoteIpAddress; + Console.WriteLine(ip); + Console.WriteLine(userId); + await Groups.AddToGroupAsync(Context.ConnectionId, userId); + // 查询获取用户对象 + var user = await userManager.FindByIdAsync(userId); + user!.Address = ip?.ToString(); + + user!.LastLoginTime = DateTime.UtcNow; + // 更新用户信息 + await userManager.UpdateAsync(user); + //获取role + var role = await userManager.GetRolesAsync(user!); + // 存储用户对象 + var userInfo = new UserInfo + { + Id = userId, + UserName = user!.UserName!, + NickName = user.NickName!, + Avatar = user.Avatar, + Role = role.ToList(), + Posts = user.Posts, + }; + + + var collection = database.GetCollection("UsersContent"); + var existingUserInfo = collection.FindOne(x => x.Id == userId); + + if (existingUserInfo != null) + // 更新现有记录 + collection.Update(userInfo); + else + // 插入新记录 + collection.Insert(userInfo); + var users = collection.FindAll().ToList(); + await Clients.All.SendAsync("userJoined", users); + Console.WriteLine($"用户加入:{userId}"); + await base.OnConnectedAsync(); + } + + public override async Task OnDisconnectedAsync(Exception? exception) + { + var userId = Context.User!.Claims.First(x => x.Type == ClaimTypes.NameIdentifier).Value; + Console.WriteLine($"用户退出:{userId}"); + // 移除用户 + await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId); + Console.WriteLine($"删除用户:{userId}"); + //从数据库中移除 + database.GetCollection("UsersContent").DeleteMany(x => x.Id == userId); + // 通知所有用户有链接离开 + await Clients.All.SendAsync("userLeft", userId); + await base.OnDisconnectedAsync(exception); + } + + public async Task SendMessage(string userId, string receiver ,string message) + { + Console.WriteLine("12312312312{0},{1}", userId, message); + await Clients.Group(receiver).SendAsync("sendMessage", userId, message); + } + +} + +//定义类型UserInfo +public class UserInfo +{ + public required string Id { get; init; } + public required string UserName { get; init; } + + public required string NickName { get; init; } + public required string Avatar { get; init; } + + public required List Role { get; init; } + + public required string Posts { get; init;} +} \ No newline at end of file diff --git a/LoongPanel-Asp/Jobs/CpuJob.cs b/LoongPanel-Asp/Jobs/CpuJob.cs new file mode 100755 index 0000000..5ede169 --- /dev/null +++ b/LoongPanel-Asp/Jobs/CpuJob.cs @@ -0,0 +1,175 @@ +using System.Globalization; +using LoongPanel_Asp.Helpers; +using LoongPanel_Asp.Hubs; +using LoongPanel_Asp.Models; +using LoongPanel_Asp.Servers; +using Microsoft.AspNetCore.SignalR; +using Quartz; +using Property = LoongPanel_Asp.Models.Property; + +namespace LoongPanel_Asp.Jobs; + +public class CpuTotalJob( + IHubContext hubContext, + IServiceProvider serviceProvider, + ApplicationDbContext dbContext) : IJob +{ + private static int _count; + + public async Task Execute(IJobExecutionContext context) + { + var dataMap = context.JobDetail.JobDataMap; + var dataHelper = new DataHelper(dbContext); + var serverList = (List)dataMap["executor"]; + var cpuDataListAll = new List(); + foreach (var server in serverList) + { + var sshClient = serviceProvider.GetService(); + var output = await sshClient?.ExecuteCommandAsync(server.Id, "sar", "-u", "3 1", "|", "grep", "Average")!; + if (string.IsNullOrEmpty(output)) continue; + + output = output.Replace("Average:", "").Replace("all", "").TrimStart(); + var cpuList = output.Split(' ', StringSplitOptions.RemoveEmptyEntries); + + var cpuProperties = new List + { + new() { Name = "CpuUserUsage", DisplayName = "CPU用户使用率", Order = 0 }, + new() { Name = "CpuSystemUsage", DisplayName = "CPU系统使用率", Order = 1 }, + new() { Name = "CpuIOWaitUsage", DisplayName = "CPUIO等待使用率", Order = 2 } + }; + + var cpuDataList = cpuProperties.Select(property => new ServerMonitoringData { ServerId = server.Id, Data = cpuList[(int)property.Order!], DataName = property.DisplayName, DataType = property.Name }).ToList(); + + // Calculate CpuTotalUsage separately + var idleUsage = double.Parse(cpuList[5]); + var totalUsage = new ServerMonitoringData + { + ServerId = server.Id, + Data = (100 - idleUsage).ToString(CultureInfo.InvariantCulture), + DataName = "CPU总使用率", + DataType = "CpuTotalUsage" + }; + cpuDataList.Add(totalUsage); + cpuDataList.ForEach(data => + { + hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data); + }); + cpuDataListAll.AddRange(cpuDataList); + } + + _count++; + if (_count <= 10) return; + _count = 0; + // Add to database + await dataHelper.SaveData(cpuDataListAll); + } +} + +public class CpuSpeedJob( + IHubContext hubContext, + IServiceProvider serviceProvider, + ApplicationDbContext dbContext) : IJob +{ + private static int _count; + + public async Task Execute(IJobExecutionContext context) + { + var dataMap = context.JobDetail.JobDataMap; + var dataHelper = new DataHelper(dbContext); + var serverList = (List)dataMap["executor"]; + var cpuDataListAll = new List(); + foreach (var server in serverList) + { + var sshClient = serviceProvider.GetService(); + var output = + await sshClient?.ExecuteCommandAsync(server.Id, "cat", "/proc/cpuinfo", "|", "grep", "'cpu MHz'")!; + if (string.IsNullOrEmpty(output)) continue; + //切分每行 + var cpuSpeedList = output.Split('\n', StringSplitOptions.RemoveEmptyEntries); + //获取第一行 + var cpuTotalSpeed = cpuSpeedList[0].Split(':', StringSplitOptions.RemoveEmptyEntries)[1].Trim(); + var cpuDataList = new List(); + var totalSpeed = new ServerMonitoringData + { + ServerId = server.Id, + Data = cpuTotalSpeed, + DataName = "CPU总速度", + DataType = "CpuTotalSpeed" + }; + cpuDataList.Add(totalSpeed); + //遍历剩下的行 + foreach (var (cpuSpeed, index) in cpuSpeedList.Skip(1).Select((x, index) => (x, index))) + { + var speed = cpuSpeed.Split(':', StringSplitOptions.RemoveEmptyEntries)[1].Trim(); + var singleSpeed = new ServerMonitoringData + { + ServerId = server.Id, + Data = speed, + DataName = $"CPU单核速度-{index}", + DataType = $"CpuSingleSpeed-{index}" + }; + cpuDataList.Add(singleSpeed); + } + + cpuDataListAll.AddRange(cpuDataList); + cpuDataList.ForEach(data => + { + hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data); + if (data is { Data: not null, DataType: not null }) _ = DataHelper.CheckData(server.Id, data.DataType, double.Parse(data.Data)); + }); + + } + + _count++; + if (_count <= 10) return; + _count = 0; + await dataHelper.SaveData(cpuDataListAll); + } +} + +public class CpuSingleUsageJob( + IHubContext hubContext, + IServiceProvider serviceProvider, + ApplicationDbContext dbContext) : IJob +{ + private static int _count; + + public async Task Execute(IJobExecutionContext context) + { + var dataMap = context.JobDetail.JobDataMap; + var dataHelper = new DataHelper(dbContext); + var serverList = (List)dataMap["executor"]; + var cpuDataListAll = new List(); + foreach (var server in serverList) + { + var sshClient = serviceProvider.GetService(); + var output = + await sshClient?.ExecuteCommandAsync(server.Id, "sar -P ALL 3 1", "|", "grep Average", "|", + "awk 'NR>2 {print 100-$NF}'")!; + if (string.IsNullOrEmpty(output)) continue; + var cpuDataList = new List(); + foreach (var (cpuUsage, index) in output.Split("\n", StringSplitOptions.RemoveEmptyEntries) + .Select((x, index) => (x, index))) + { + var singleUsage = new ServerMonitoringData + { + ServerId = server.Id, + Data = cpuUsage, + DataName = $"CPU单核使用率-{index}", + DataType = $"CpuSingleUsage-{index}" + }; + cpuDataList.Add(singleUsage); + } + + cpuDataList.ForEach(data => + { + hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data); + }); + } + + _count++; + if (_count <= 10) return; + _count = 0; + await dataHelper.SaveData(cpuDataListAll); + } +} \ No newline at end of file diff --git a/LoongPanel-Asp/Jobs/DiskJob.cs b/LoongPanel-Asp/Jobs/DiskJob.cs new file mode 100755 index 0000000..38bba03 --- /dev/null +++ b/LoongPanel-Asp/Jobs/DiskJob.cs @@ -0,0 +1,131 @@ +using LoongPanel_Asp.Helpers; +using LoongPanel_Asp.Hubs; +using LoongPanel_Asp.Models; +using LoongPanel_Asp.Servers; +using Microsoft.AspNetCore.SignalR; +using Quartz; + +namespace LoongPanel_Asp.Jobs; + +public class DiskTotalJob( + IHubContext hubContext, + IServiceProvider serviceProvider, + ApplicationDbContext dbContext) : IJob +{ + private static int _count; + + public async Task Execute(IJobExecutionContext context) + { + var dataMap = context.JobDetail.JobDataMap; + var dataHelper = new DataHelper(dbContext); + var serverList = (List)dataMap["executor"]; + //获得cpu信息 + var diskDataListAll = new List(); + foreach (var server in serverList) + { + var sshClient = serviceProvider.GetService(); + var diskDataList = new List(); + var output = await sshClient?.ExecuteCommandAsync(server.Id, "df", "--total", "|", "grep", "total")!; + if (string.IsNullOrEmpty(output)) return; + var disk = output.Split(" ", StringSplitOptions.RemoveEmptyEntries); + var diskTotalUsage = new ServerMonitoringData + { + ServerId = server.Id, + Data = disk[4].Replace("%", ""), + DataName = "磁盘总使用率", + DataType = "DiskTotalUsage" + }; + await hubContext.Clients.All.SendAsync("ReceiveData", diskTotalUsage.ServerId, diskTotalUsage.DataType, + diskTotalUsage.Data); + diskDataList.Add(diskTotalUsage); + diskDataListAll.AddRange(diskDataList); + } + + _count++; + if (_count <= 10) return; + _count = 0; + await dataHelper.SaveData(diskDataListAll); + } +} + +public class DiskUseJob( + IHubContext hubContext, + IServiceProvider serviceProvider, + ApplicationDbContext dbContext) : IJob +{ + private static int _count; + + public async Task Execute(IJobExecutionContext context) + { + var dataMap = context.JobDetail.JobDataMap; + var dataHelper = new DataHelper(dbContext); + var serverList = (List)dataMap["executor"]; + //获得cpu信息 + var diskDataListAll = new List(); + foreach (var server in serverList) + { + var sshClient = serviceProvider.GetService(); + var diskDataList = new List(); + var output = + await sshClient?.ExecuteCommandAsync(server.Id, "sar -d 3 1", "|", "grep Average:", "|", "awk 'NR>1'","|","awk '{$1=\"\";print $0}'")!; + if (string.IsNullOrEmpty(output)) return; + var lines = output.Split("\n", StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList(); + + foreach (var line in lines) + { + var disk = line.Split(" ", StringSplitOptions.RemoveEmptyEntries); + var dev = disk[0]; + // 每秒传输数 + var diskTps = new ServerMonitoringData + { + ServerId = server.Id, + Data = disk[1], + DataName = $"磁盘每秒传输数-{dev}" , + DataType = $"diskTps-{dev}" + }; + diskDataList.Add(diskTps); + var diskReadKb = new ServerMonitoringData + { + ServerId = server.Id, + Data = disk[2], + DataName = $"磁盘每秒读取数据量-{dev}" , + DataType = $"diskReadKB-{dev}" + }; + diskDataList.Add(diskReadKb); + var diskWriteKb = new ServerMonitoringData + { + ServerId = server.Id, + Data = disk[3], + DataName = $"磁盘每秒写入数据量-{dev}" , + DataType = $"diskWriteKB-{dev}" + }; + diskDataList.Add(diskWriteKb); + var diskAwait = new ServerMonitoringData + { + ServerId = server.Id, + Data = disk[7], + DataName = $"磁盘平均等待时间-{dev}" , + DataType = $"diskAwait-{dev}" + }; + diskDataList.Add(diskAwait); + var diskUtil = new ServerMonitoringData + { + ServerId = server.Id, + Data = disk[8], + DataName = $"磁盘利用率-{dev}" , + DataType = $"diskUtil-{dev}" + }; + diskDataList.Add(diskUtil); + diskDataList.ForEach(data => + { + hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data); + }); + } + diskDataListAll.AddRange(diskDataList); + } + _count++; + if (_count <= 10) return; + _count = 0; + await dataHelper.SaveData(diskDataListAll); + } +} \ No newline at end of file diff --git a/LoongPanel-Asp/Jobs/MemoryJob.cs b/LoongPanel-Asp/Jobs/MemoryJob.cs new file mode 100755 index 0000000..214bf8f --- /dev/null +++ b/LoongPanel-Asp/Jobs/MemoryJob.cs @@ -0,0 +1,94 @@ +using System.Globalization; +using LoongPanel_Asp.Helpers; +using LoongPanel_Asp.Hubs; +using LoongPanel_Asp.Models; +using LoongPanel_Asp.Servers; +using Microsoft.AspNetCore.SignalR; +using Quartz; + +namespace LoongPanel_Asp.Jobs; + +public class MemoryTotalJob( + IHubContext hubContext, + IServiceProvider serviceProvider, + ApplicationDbContext dbContext) : IJob +{ + private static int _count; + + public async Task Execute(IJobExecutionContext context) + { + var dataMap = context.JobDetail.JobDataMap; + var dataHelper = new DataHelper(dbContext); + // 从JobDataMap中获取参数 + var serverList = (List)dataMap["executor"]; + + var serverDataListAll = new List(); + foreach (var server in serverList) + { + var sshClient = serviceProvider.GetService(); + var output = await sshClient?.ExecuteCommandAsync(server.Id, "free -w", "|", "awk 'NR>1'", "|", + "awk '{$1=\"\";print $0}'", "|", "xargs")!; + if (string.IsNullOrEmpty(output)) continue; + var serverDataList = new List(); + var dataList = output.Split(" ", StringSplitOptions.RemoveEmptyEntries); + var memoryProperties = new List + { + new() { Name = "MemoryTotal", DisplayName = "内存总量", Order = 0 }, + new() { Name = "MemoryUsed", DisplayName = "内存使用量", Order = 1 }, + new() { Name = "MemoryFree", DisplayName = "内存空闲量", Order = 2 }, + new() { Name = "MemoryCache", DisplayName = "内存缓存量", Order = 5 }, + new() { Name = "SwapTotal", DisplayName = "Swap总量", Order = 7 }, + new() { Name = "SwapUsed", DisplayName = "Swap使用量", Order = 8 }, + new() { Name = "SwapFree", DisplayName = "Swap空闲量", Order = 9 } + }; + memoryProperties.ForEach(data => + { + var d = new ServerMonitoringData + { + ServerId = server.Id, + Data = double.Parse(dataList[(int)data.Order!]).ToString(CultureInfo.CurrentCulture), + DataName = data.DisplayName, + DataType = data.Name + }; + serverDataList.Add(d); + }); + //计算内存使用率 + var memoryUsed = double.Parse(dataList[1]); + var memoryTotal = double.Parse(dataList[0]); + if (memoryTotal <= 0) memoryTotal = 1; + var memoryUsedRate = memoryUsed / memoryTotal * 100; + + var memoryData = new ServerMonitoringData + { + ServerId = server.Id, + Data = memoryUsedRate.ToString(CultureInfo.InvariantCulture), + DataName = "内存总使用率", + DataType = "MemoryTotalUsage" + }; + serverDataList.Add(memoryData); + //计算交换使用率 + var swapUsed = double.Parse(dataList[8]); + var swapTotal = double.Parse(dataList[7]); + if (swapTotal <= 0) swapTotal = 1; + var swapUsedRate = swapUsed / swapTotal * 100; + var swapData = new ServerMonitoringData + { + ServerId = server.Id, + Data = swapUsedRate.ToString(CultureInfo.InvariantCulture), + DataName = "Swap使用率", + DataType = "SwapTotalUsage" + }; + serverDataList.Add(swapData); + serverDataList.ForEach(data => + { + hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data); + }); + serverDataListAll.AddRange(serverDataList); + } + + _count++; + if (_count <= 10) return; + _count = 0; + await dataHelper.SaveData(serverDataListAll); + } +} \ No newline at end of file diff --git a/LoongPanel-Asp/Jobs/NetworkJob.cs b/LoongPanel-Asp/Jobs/NetworkJob.cs new file mode 100755 index 0000000..4e2c04a --- /dev/null +++ b/LoongPanel-Asp/Jobs/NetworkJob.cs @@ -0,0 +1,79 @@ +using System.Globalization; +using LoongPanel_Asp.Helpers; +using LoongPanel_Asp.Hubs; +using LoongPanel_Asp.Models; +using LoongPanel_Asp.Servers; +using Microsoft.AspNetCore.SignalR; +using Quartz; + +namespace LoongPanel_Asp.Jobs; + +public class NetworkTotalJob( + IHubContext hubContext, + IServiceProvider serviceProvider, + ApplicationDbContext dbContext) : IJob +{ + private static int _count; + + public async Task Execute(IJobExecutionContext context) + { + var dataMap = context.JobDetail.JobDataMap; + var dataHelper = new DataHelper(dbContext); + var serverList = (List)dataMap["executor"]; + var netWorkDataListAll = new List(); + foreach (var server in serverList) + { + var sshClient = serviceProvider.GetService(); + var netWorkDataList = new List(); + var output = await sshClient?.ExecuteCommandAsync(server.Id, "sar", "-n", "DEV", "3 1", "|", "grep", + "Average:", "|", "awk 'NR>1'", "|", "awk '{$1=\"\";print $0}'")!; + if (string.IsNullOrEmpty(output)) continue; + var lines = output.Split("\n"); + var totalUsage = 0.0; + foreach (var line in lines) + { + if (string.IsNullOrEmpty(line)) continue; + var data = line.Split(" ", StringSplitOptions.RemoveEmptyEntries); + var iFace = data[0]; + var dataNum = data.Skip(1).Select(double.Parse).ToList(); + var netWorkProperties = new List + { + new() { Name = "ReceivedPacketsPerSecond", DisplayName = "每秒钟接收到的数据包数量", Order = 0 }, + new() { Name = "TransmittedPacketsPerSecond", DisplayName = "每秒钟发送的数据包数量", Order = 1 }, + new() { Name = "InterfaceUtilizationPercentage", DisplayName = "网络接口的使用率", Order = 7 } + }; + netWorkProperties.ForEach(property => + { + var d = new ServerMonitoringData + { + ServerId = server.Id, + Data = dataNum[(int)property.Order!].ToString(CultureInfo.InvariantCulture), + DataName = $"{property.DisplayName}-{iFace}", + DataType = $"{property.Name}-{iFace}" + }; + netWorkDataList.Add(d); + }); + totalUsage += dataNum[7]; + } + + var d = new ServerMonitoringData + { + ServerId = server.Id, + Data = totalUsage.ToString(CultureInfo.InvariantCulture), + DataName = "网络接口总体使用率", + DataType = "InterfaceTotalUtilizationPercentage" + }; + netWorkDataList.Add(d); + netWorkDataListAll.AddRange(netWorkDataList); + netWorkDataList.ForEach(data => + { + hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data); + }); + } + + _count++; + if (_count <= 10) return; + _count = 0; + await dataHelper.SaveData(netWorkDataListAll); + } +} \ No newline at end of file diff --git a/LoongPanel-Asp/Jobs/ProcessJob.cs b/LoongPanel-Asp/Jobs/ProcessJob.cs new file mode 100644 index 0000000..b70aefc --- /dev/null +++ b/LoongPanel-Asp/Jobs/ProcessJob.cs @@ -0,0 +1,99 @@ +using LoongPanel_Asp.Helpers; +using LoongPanel_Asp.Hubs; +using LoongPanel_Asp.Models; +using LoongPanel_Asp.Servers; +using Microsoft.AspNetCore.SignalR; +using Quartz; + +namespace LoongPanel_Asp.Jobs; + +public class ProcessTotalJob( + IHubContext hubContext, + IServiceProvider serviceProvider, + ApplicationDbContext dbContext) : IJob +{ + private static int _count; + + public async Task Execute(IJobExecutionContext context) + { + var dataMap = context.JobDetail.JobDataMap; + var dataHelper = new DataHelper(dbContext); + var serverList = (List)dataMap["executor"]; + var processDataListAll = new List(); + foreach (var server in serverList) + { + var sshClient = serviceProvider.GetService(); + var output = await sshClient?.ExecuteCommandAsync(server.Id, "ps", "-e", "|", "wc", "-l")!; + if (string.IsNullOrEmpty(output)) continue; + var processDataList = new List(); + var count = int.Parse(output); + var processTotalCount = new ServerMonitoringData + { + ServerId = server.Id, + Data = count.ToString(), + DataName = "进程总数", + DataType = "ProcessTotalCount" + }; + processDataList.Add(processTotalCount); + output = await sshClient?.ExecuteCommandAsync(server.Id, "ps", "-eLf", "|", "wc", "-l")!; + if (string.IsNullOrEmpty(output)) continue; + count = int.Parse(output); + var threadsTotalCount = new ServerMonitoringData + { + ServerId = server.Id, + Data = count.ToString(), + DataName = "线程总数", + DataType = "ThreadsTotalCount" + }; + processDataList.Add(threadsTotalCount); + processDataList.ForEach(data => + { + hubContext.Clients.All.SendAsync("ReceiveData", data.ServerId, data.DataType, data.Data); + }); + processDataListAll.AddRange(processDataList); + } + + _count++; + if (_count <= 10) return; + _count = 0; + await dataHelper.SaveData(processDataListAll); + } +} + +public class PhrasePatternJob( + IHubContext hubContext, + IServiceProvider serviceProvider, + ApplicationDbContext dbContext) : IJob +{ + private static int _count; + + public async Task Execute(IJobExecutionContext context) + { + var dataMap = context.JobDetail.JobDataMap; + var dataHelper = new DataHelper(dbContext); + var serverList = (List)dataMap["executor"]; + var processDataListAll = new List(); + foreach (var server in serverList) + { + var sshClient = serviceProvider.GetService(); + var output = await sshClient?.ExecuteCommandAsync(server.Id, "lsof", "|", "wc", "-l")!; + if (string.IsNullOrEmpty(output)) continue; + var count = int.Parse(output); + var phrasePatternCount = new ServerMonitoringData + { + ServerId = server.Id, + Data = count.ToString(), + DataName = "句柄", + DataType = "PhrasePatternCount" + }; + processDataListAll.Add(phrasePatternCount); + await hubContext.Clients.All.SendAsync("ReceiveData", server.Id, phrasePatternCount.DataType, + phrasePatternCount.Data); + } + + _count++; + if (_count <= 10) return; + _count = 0; + await dataHelper.SaveData(processDataListAll); + } +} \ No newline at end of file diff --git a/LoongPanel-Asp/LoongPanel-Asp.csproj b/LoongPanel-Asp/LoongPanel-Asp.csproj new file mode 100755 index 0000000..bc2e4c0 --- /dev/null +++ b/LoongPanel-Asp/LoongPanel-Asp.csproj @@ -0,0 +1,44 @@ + + + + net8.0 + enable + enable + LoongPanel_Asp + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + + + diff --git a/LoongPanel-Asp/LoongPanel-Asp.csproj.user b/LoongPanel-Asp/LoongPanel-Asp.csproj.user new file mode 100755 index 0000000..9ff5820 --- /dev/null +++ b/LoongPanel-Asp/LoongPanel-Asp.csproj.user @@ -0,0 +1,6 @@ + + + + https + + \ No newline at end of file diff --git a/LoongPanel-Asp/LoongPanel-Asp.http b/LoongPanel-Asp/LoongPanel-Asp.http new file mode 100755 index 0000000..dcc8a5c --- /dev/null +++ b/LoongPanel-Asp/LoongPanel-Asp.http @@ -0,0 +1,6 @@ +@LoongPanel_Asp_HostAddress = http://localhost:5253 + +GET {{LoongPanel_Asp_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/LoongPanel-Asp/Middlewares/ApiPermissionMiddleware.cs b/LoongPanel-Asp/Middlewares/ApiPermissionMiddleware.cs new file mode 100755 index 0000000..b76c093 --- /dev/null +++ b/LoongPanel-Asp/Middlewares/ApiPermissionMiddleware.cs @@ -0,0 +1,122 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Identity; + +namespace LoongPanel_Asp.Middlewares; + +public class ApiPermissionMiddleware( + RequestDelegate next, + ILogger logger, + IServiceProvider serviceProvider, + IConfiguration configuration) +{ + public async Task InvokeAsync(HttpContext context) + { + try + { + // 获取配置中定义的公开API列表 + var publicApis = configuration["PublicApi"]?.Split(";", StringSplitOptions.RemoveEmptyEntries) ?? + new string[0]; + + // 如果请求路径在公开API列表中,则直接调用下一个中间件 + if (publicApis.Any(api => api == context.Request.Path.Value)) + { + await next(context); + return; + } + + + // 验证Token + var payload = context.User; + + string[] hubKeywords = { "ServerHub", "MessageHub", "SessionHub", "TermHub" }; + //如果请求的地址是 (*Hub/*) + if (hubKeywords.Any(keyword => context.Request.Path.Value!.Contains(keyword))) + { + await next(context); + return; + } + + // 获取用户信息 + using var scope = serviceProvider.CreateScope(); + var userManager = scope.ServiceProvider.GetRequiredService>(); + var roleManager = scope.ServiceProvider.GetRequiredService>(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + var userId = payload.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; + if (userId == null) + { + await UnauthorizedResponse(context); + return; + } + + var user = await userManager.FindByIdAsync(userId); + if (user == null) + { + await UnauthorizedResponse(context); + return; + } + + // 获取用户角色 + var userRoles = await userManager.GetRolesAsync(user); + var roleId = userRoles.FirstOrDefault(); + if (roleId == null) + { + await UnauthorizedResponse(context); + return; + } + + // 获取角色信息 + var role = await roleManager.FindByIdAsync(roleId.ToLower()); + if (role == null) + { + context.Response.StatusCode = 401; + await context.Response.WriteAsync("Role not found"); + return; + } + + // 获取角色的API权限 + var apiPermissions = dbContext.ApiPermissions.ToList() + .Select(x => $"/Api/{x.Controller.Replace("Controller", "")}/{x.Name}"); + + // 判断请求是否拥有权限 + if (!apiPermissions.Any(x => + context.Request.Path.Value != null && context.Request.Path.Value.StartsWith(x))) + { + await ForbiddenResponse(context); + } + else + { + // 请求拥有权限,调用下一个中间件 + await next(context); + } + } + catch (Exception ex) + { + // 记录异常信息 + logger.LogError(ex, "An error occurred in ApiPermissionMiddleware"); + await InternalServerErrorResponse(context); + } + } + + private static async Task UnauthorizedResponse(HttpContext context) + { + context.Response.StatusCode = 401; + await context.Response.WriteAsync("Unauthorized"); + } + + private static async Task ForbiddenResponse(HttpContext context) + { + context.Response.StatusCode = 403; + await context.Response.WriteAsync("Forbidden"); + } + + private static async Task InternalServerErrorResponse(HttpContext context) + { + context.Response.StatusCode = 500; + await context.Response.WriteAsync("An internal server error occurred."); + } + + private static string ExtractToken(IEnumerable authHeader) + { + return authHeader.FirstOrDefault()?.Split(" ").Last() ?? string.Empty; + } +} \ No newline at end of file diff --git a/LoongPanel-Asp/Middlewares/PermissionMiddleware.cs b/LoongPanel-Asp/Middlewares/PermissionMiddleware.cs new file mode 100755 index 0000000..2016908 --- /dev/null +++ b/LoongPanel-Asp/Middlewares/PermissionMiddleware.cs @@ -0,0 +1,71 @@ +using LoongPanel_Asp.Helpers; + +namespace LoongPanel_Asp.Middlewares; + +public class PermissionMiddleware( + RequestDelegate next, + TokenHelper tokenHelper, + IConfiguration configuration, + ILogger logger) +{ + public async Task InvokeAsync(HttpContext context) + { + var publicApis = configuration["PublicApi"]?.Split(";") ?? []; + + // 如果请求路径在公开API列表中,则直接调用下一个中间件 + if (publicApis.Any(api => api == context.Request.Path.Value)) + { + await next(context); + return; + } + Console.WriteLine(context.Request.Path.Value!); + + // 获取请求头中的Authorization信息 + var authorizationHeader = context.Request.Headers["Authorization"]; + + // 提取Token + var token = authorizationHeader.ToString().Replace("Bearer ", ""); + //如果Token不存在则尝试从Cookie中获取 + if (string.IsNullOrEmpty(token)) + { + //Cookie token + var cookieToken = context.Request.Cookies["token"]; + + if (string.IsNullOrEmpty(cookieToken)) + { + //如果Cookie中也没有Token,则返回401 Unauthorized + logger.LogWarning("Token not found in Authorization header or cookie."); + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + return; + } + + token = cookieToken; + } + + try + { + // 验证Token并获取声明 + var claims = tokenHelper.ValidateToken(token); + if (claims != null) + { + context.User = claims; + + // Token验证成功,继续处理请求 + logger.LogInformation("Token validated successfully."); + await next(context); + } + else + { + // Token验证失败,返回401 Unauthorized + logger.LogError("Token validation failed."); + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + } + } + catch (Exception ex) + { + // Token解析过程中发生异常,返回500 Internal Server Error + logger.LogError(ex, "An error occurred while validating the token."); + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + } + } +} \ No newline at end of file diff --git a/LoongPanel-Asp/Models/AccountModel.cs b/LoongPanel-Asp/Models/AccountModel.cs new file mode 100755 index 0000000..894f774 --- /dev/null +++ b/LoongPanel-Asp/Models/AccountModel.cs @@ -0,0 +1,27 @@ +namespace LoongPanel_Asp.Models; + +public class EmailModel +{ + public required string Email { get; set; } +} + +public class VerifyEmailNameModel : EmailModel +{ + public required string UserName { get; set; } +} + +public class RegisterModel : EmailModel +{ + public required string UserName { get; set; } + public required string NickName { get; set; } + public required string Code { get; set; } + public required string Phone { get; set; } + public required string Password { get; set; } +} + +public class LoginModel +{ + public required string EmailOrUserName { get; set; } + public required string Password { get; set; } + public bool RememberMe { get; set; } = false; +} \ No newline at end of file diff --git a/LoongPanel-Asp/Models/OrderType.cs b/LoongPanel-Asp/Models/OrderType.cs new file mode 100755 index 0000000..745da03 --- /dev/null +++ b/LoongPanel-Asp/Models/OrderType.cs @@ -0,0 +1,20 @@ +namespace LoongPanel_Asp.Models; + +public class Property +{ + public required string Name { get; set; } + public required string DisplayName { get; set; } + public int? Order { get; set; } +} + + +public class ServerModel +{ + public required string Id { get;init; } + public required string Address { get; set; } + public required int Port { get; set; } + public required string ServerName { get; set; } + public required string Password { get; set; } + public required string Username { get; set; } + public required bool Http { get; set; } +} diff --git a/LoongPanel-Asp/Models/PrometheusQueryResultModel.cs b/LoongPanel-Asp/Models/PrometheusQueryResultModel.cs new file mode 100755 index 0000000..a3d0d20 --- /dev/null +++ b/LoongPanel-Asp/Models/PrometheusQueryResultModel.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; + +namespace LoongPanel_Asp.Models; + +public struct PrometheusQueryResult +{ + [JsonPropertyName("status")] public required string Status { get; init; } + [JsonPropertyName("data")] public required PrometheusQueryData Data { get; init; } +} + +public struct PrometheusQueryData +{ + [JsonPropertyName("resultType")] public required string ResultType { get; init; } + + [JsonPropertyName("result")] public required List Result { get; init; } +} + +public struct PrometheusQueryResultValue +{ + [JsonPropertyName("metric")] public required Dictionary Metric { get; init; } + + [JsonPropertyName("value")] public required List Value { get; init; } +} \ No newline at end of file diff --git a/LoongPanel-Asp/Program.cs b/LoongPanel-Asp/Program.cs new file mode 100755 index 0000000..d96dc36 --- /dev/null +++ b/LoongPanel-Asp/Program.cs @@ -0,0 +1,134 @@ +using LiteDB; +using LoongPanel_Asp; +using LoongPanel_Asp.Helpers; +using LoongPanel_Asp.Hubs; +using LoongPanel_Asp.Middlewares; +using LoongPanel_Asp.Servers; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Quartz; +using Quartz.AspNetCore; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +const string myAllowSpecificOrigins = "_myAllowSpecificOrigins"; + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + + +builder.Services.Configure(options => +{ + // Password settings. + options.Password.RequireDigit = true; + options.Password.RequireLowercase = true; + options.Password.RequireNonAlphanumeric = true; + options.Password.RequireUppercase = true; + options.Password.RequiredLength = 8; + options.Password.RequiredUniqueChars = 1; + + // Lockout settings. + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); + options.Lockout.MaxFailedAccessAttempts = 5; + options.Lockout.AllowedForNewUsers = true; + + // User settings. + options.User.AllowedUserNameCharacters = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; + options.User.RequireUniqueEmail = false; +}); + +builder.Services.AddSignalR(); + +var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); +var sqlConnectionString = builder.Configuration.GetConnectionString("SqliteConnection"); + +builder.Services.AddDbContext(options => + options.UseNpgsql(connectionString)); + +// builder.Services.AddDbContext(options => +// options.UseSqlite(sqlConnectionString)); + +builder.Services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + +builder.Services.AddSingleton(builder.Configuration); +builder.Services.AddSingleton(); + +var emailSettings = builder.Configuration.GetSection("EmailSettings"); +builder.Services.AddSingleton(sp => + new EmailHelper( + sp.GetService(), + emailSettings["Host"] ?? throw new InvalidOperationException(), + int.Parse(emailSettings["Port"] ?? throw new InvalidOperationException()), + emailSettings["Username"] ?? throw new InvalidOperationException(), + emailSettings["Password"] ?? throw new InvalidOperationException() + )); + +builder.Services.AddSingleton( + sp => new LiteDatabase("Filename=temp.db;Connection=shared;")); + +// 跨域 +builder.Services.AddCors(options => +{ + options.AddPolicy(myAllowSpecificOrigins, + policy => + { + //允许全部 + policy.WithOrigins("http://localhost:3001", "http://192.168.0.13:3001", "http://192.168.0.13:3002", + "http://192.168.0.13").AllowAnyHeader().AllowAnyMethod().AllowCredentials(); + }); +}); + +builder.Services.AddQuartz(q => +{ + var jobList = JobConfigHelper.ReadJobConfigurations().ToList(); + foreach (var jobConfig in jobList) + { + var jobType = Type.GetType(jobConfig.JobType); + if (jobType == null) continue; + var jobDataMap = new JobDataMap { { "executor", jobConfig.Executor } }; + var jobKey = new JobKey(jobConfig.Name, jobConfig.Group); + var triggerKey = new TriggerKey($"Trigger_for_{jobConfig.Name}", jobConfig.Group); + q.AddJob(jobType, jobKey, opts => opts.SetJobData(jobDataMap)); + q.AddTrigger( + opts => opts.ForJob(jobKey).WithIdentity(triggerKey).WithCronSchedule(jobConfig.CronExpression)); + } +}); +builder.Services.AddQuartzServer(options => +{ + // when shutting down we want jobs to complete gracefully + options.WaitForJobsToComplete = true; +}); + +builder.Services.AddScoped(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + + +app.UseCors(myAllowSpecificOrigins); + +app.UseMiddleware(); +app.UseMiddleware(); + + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.MapHub("/SessionHub"); +app.Run(); \ No newline at end of file diff --git a/LoongPanel-Asp/Properties/launchSettings.json b/LoongPanel-Asp/Properties/launchSettings.json new file mode 100755 index 0000000..d43adb8 --- /dev/null +++ b/LoongPanel-Asp/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://192.168.0.13:58826", + "sslPort": 44304 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "launchUrl": "swagger", + "applicationUrl": "http://192.168.0.13:5253", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://192.168.0.13:7233;http://192.168.0.13:5253", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/LoongPanel-Asp/Servers/SSHService.cs b/LoongPanel-Asp/Servers/SSHService.cs new file mode 100755 index 0000000..2f0c4ca --- /dev/null +++ b/LoongPanel-Asp/Servers/SSHService.cs @@ -0,0 +1,96 @@ +using LoongPanel_Asp.Helpers; +using LoongPanel_Asp.Models; +using Renci.SshNet; +using ConnectionInfo = Renci.SshNet.ConnectionInfo; + +namespace LoongPanel_Asp.Servers; + +public class SshService : IDisposable +{ + private readonly CancellationTokenSource _cts = new(); + private readonly ApplicationDbContext _db; + private readonly ILogger _logger; + private readonly Dictionary _serverConnectionInfos = new(); + private readonly Dictionary _serverSshClients = new(); + + public SshService(ApplicationDbContext db, ILogger logger) + { + _db = db; + _logger = logger; + LoadServerConfigurations(); + } + + public void Dispose() + { + _cts.Cancel(); + foreach (var sshClient in _serverSshClients.Values) sshClient.Dispose(); + _serverSshClients.Clear(); + } + + private void LoadServerConfigurations() + { + // 从数据库中加载服务器配置信息 + var serverConfigurations=JobConfigHelper.GetServers(); + + foreach (var serverConfiguration in serverConfigurations) + { + var connectionInfo = new ConnectionInfo(serverConfiguration.Address, serverConfiguration.Port, + serverConfiguration.Username, new PasswordAuthenticationMethod(serverConfiguration.Username, serverConfiguration.Password)); + _serverConnectionInfos[serverConfiguration.Id] = connectionInfo; + var sshClient = new SshClient(connectionInfo); + // 设置超时时间 + sshClient.ConnectionInfo.Timeout = TimeSpan.FromSeconds(10); + _serverSshClients[serverConfiguration.Id] = sshClient; + } + } + + public async Task ExecuteCommandAsync(string serverId, string command, params string[] arguments) + { + var sshClient = _serverSshClients[serverId]; + if (sshClient == null) throw new InvalidOperationException($"SSH client for server ID '{serverId}' not found."); + + var output = ""; + try + { + // 确保在执行命令前连接到服务器 + if (!sshClient.IsConnected) await sshClient.ConnectAsync(_cts.Token); + + var commandString = string.Join(" ", "LANG=C", command, string.Join(" ", arguments)); + using var commandResult = sshClient.RunCommand(commandString); + output = commandResult.Result; + if (commandResult.ExitStatus != 0) output = commandResult.Error; + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + + return output; + } + + public async Task ExecuteCommandAsync(bool langC, string serverId, string command, + params string[] arguments) + { + var sshClient = _serverSshClients[serverId]; + if (sshClient == null) throw new InvalidOperationException($"SSH client for server ID '{serverId}' not found."); + + var output = ""; + try + { + // 确保在执行命令前连接到服务器 + if (!sshClient.IsConnected) await sshClient.ConnectAsync(_cts.Token); + var result = langC ? "LANG=C" : ""; + + var commandString = string.Join(" ", result, command, string.Join(" ", arguments)); + using var commandResult = sshClient.RunCommand(commandString); + output = commandResult.Result; + if (commandResult.ExitStatus != 0) output = commandResult.Error; + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + + return output; + } +} \ No newline at end of file diff --git a/LoongPanel-Asp/Types/EmailCode.cs b/LoongPanel-Asp/Types/EmailCode.cs new file mode 100755 index 0000000..0887d25 --- /dev/null +++ b/LoongPanel-Asp/Types/EmailCode.cs @@ -0,0 +1,12 @@ +using LiteDB; + +namespace LoongPanel_Asp.Types; + +public class EmailCode +{ + public ObjectId? Id { get; set; } + public required string Email { get; set; } + public required string Code { get; set; } + + public required DateTime ExpireTime { get; set; } +} \ No newline at end of file diff --git a/LoongPanel-Asp/UserConfigs/Layouts/default.json b/LoongPanel-Asp/UserConfigs/Layouts/default.json new file mode 100644 index 0000000..b02e958 --- /dev/null +++ b/LoongPanel-Asp/UserConfigs/Layouts/default.json @@ -0,0 +1,1012 @@ +{ + "md": [ + { + "type": "chart", + "chartRage": 6, + "serverValues": [ + { + "dataName": "CPU总使用率", + "dataType": "CpuTotalUsage" + }, + { + "dataName": "内存使用率", + "dataType": "MemoryUsage" + }, + { + "dataName": "磁盘总使用率", + "dataType": "DiskTotalUsage" + } + ], + "i": "069299c5-ceec-41cd-a1a8-b18ce0598317", + "x": 0, + "y": 0, + "h": 12, + "w": 10, + "selectChart": "line", + "cardConfig": { + "name": { + "label": "卡片名称", + "value": "系统信息卡片", + "type": "text" + }, + "description": { + "label": "卡片描述", + "value": "这是一张默认的卡片", + "type": "text" + }, + "foreground": { + "label": "卡片前景色", + "value": "ffffff", + "type": "color" + }, + "color": { + "label": "图表主色", + "value": "002EA6", + "type": "color" + } + }, + "moved": false + }, + { + "type": "card", + "h": 16, + "w": 6, + "x": 4, + "y": 12, + "i": "68af2024-4a21-4351-b1ce-707aeeb3ea9c", + "selectCard": "systemInfo", + "moved": false + }, + { + "type": "card", + "h": 16, + "w": 4, + "x": 0, + "y": 12, + "i": "8104448e-6c47-455f-b635-43b3ac75d4df", + "selectCard": "onlineUsers", + "moved": false + }, + { + "type": "chart", + "chartRage": 6, + "serverValues": [ + { + "dataName": "空闲内存", + "dataType": "MemoryFree" + }, + { + "dataName": "已使用内存", + "dataType": "MemoryUsed" + } + ], + "i": "6aa12623-fee2-48e3-86f1-88fcd538c4a8", + "x": 6, + "y": 44, + "h": 16, + "w": 4, + "selectChart": "pie", + "cardConfig": { + "name": { + "label": "卡片名称", + "value": "默认卡片名称", + "type": "text" + }, + "description": { + "label": "卡片描述", + "value": "这是一张默认的卡片", + "type": "text" + }, + "foreground": { + "label": "卡片前景色", + "value": "ffffff", + "type": "color" + }, + "color": { + "label": "图表主色", + "value": "002EA6", + "type": "color" + } + }, + "moved": false + }, + { + "type": "chart", + "chartRage": 6, + "serverValues": [ + { + "dataName": "磁盘总使用率", + "dataType": "DiskTotalUsage" + }, + { + "dataName": "内存使用率", + "dataType": "MemoryUsage" + }, + { + "dataName": "CPU总使用率", + "dataType": "CpuTotalUsage" + } + ], + "i": "17334b32-cede-441f-9bef-772d170f1c2a", + "x": 0, + "y": 44, + "h": 16, + "w": 6, + "selectChart": "histogram", + "cardConfig": { + "name": { + "label": "卡片名称", + "value": "默认卡片名称", + "type": "text" + }, + "description": { + "label": "卡片描述", + "value": "这是一张默认的卡片", + "type": "text" + }, + "foreground": { + "label": "卡片前景色", + "value": "ffffff", + "type": "color" + }, + "color": { + "label": "图表主色", + "value": "002EA6", + "type": "color" + } + }, + "moved": false + }, + { + "type": "chart", + "chartRage": 6, + "serverValues": [ + { + "dataName": "空闲内存", + "dataType": "MemoryFree" + }, + { + "dataName": "已使用内存", + "dataType": "MemoryUsed" + } + ], + "i": "06e64544-dfb8-4a28-b777-96cb4fd70aec", + "x": 0, + "y": 28, + "h": 16, + "w": 10, + "selectChart": "area", + "cardConfig": { + "name": { + "label": "卡片名称", + "value": "网络使用率卡片", + "type": "text" + }, + "description": { + "label": "卡片描述", + "value": "这是一张默认的卡片", + "type": "text" + }, + "foreground": { + "label": "卡片前景色", + "value": "ffffff", + "type": "color" + }, + "color": { + "label": "图表主色", + "value": "002EA6", + "type": "color" + } + }, + "moved": false + } + ], + "lg": [ + { + "type": "chart", + "chartRage": 6, + "serverValues": [ + { + "dataName": "CPU总使用率", + "dataType": "CpuTotalUsage" + }, + { + "dataName": "内存使用率", + "dataType": "MemoryUsage" + }, + { + "dataName": "磁盘总使用率", + "dataType": "DiskTotalUsage" + } + ], + "i": "069299c5-ceec-41cd-a1a8-b18ce0598317", + "x": 0, + "y": 0, + "h": 13, + "w": 10, + "selectChart": "line", + "cardConfig": { + "name": { + "label": "卡片名称", + "value": "系统信息卡片", + "type": "text" + }, + "description": { + "label": "卡片描述", + "value": "这是一张默认的卡片", + "type": "text" + }, + "foreground": { + "label": "卡片前景色", + "value": "ffffff", + "type": "color" + }, + "color": { + "label": "图表主色", + "value": "002EA6", + "type": "color" + } + }, + "moved": false + }, + { + "type": "card", + "h": 13, + "w": 2, + "x": 10, + "y": 0, + "i": "68af2024-4a21-4351-b1ce-707aeeb3ea9c", + "selectCard": "systemInfo", + "moved": false + }, + { + "type": "card", + "h": 16, + "w": 3, + "x": 9, + "y": 13, + "i": "8104448e-6c47-455f-b635-43b3ac75d4df", + "selectCard": "onlineUsers", + "moved": false + }, + { + "type": "chart", + "chartRage": 6, + "serverValues": [ + { + "dataName": "空闲内存", + "dataType": "MemoryFree" + }, + { + "dataName": "已使用内存", + "dataType": "MemoryUsed" + } + ], + "i": "6aa12623-fee2-48e3-86f1-88fcd538c4a8", + "x": 9, + "y": 29, + "h": 16, + "w": 3, + "selectChart": "pie", + "cardConfig": { + "name": { + "label": "卡片名称", + "value": "默认卡片名称", + "type": "text" + }, + "description": { + "label": "卡片描述", + "value": "这是一张默认的卡片", + "type": "text" + }, + "foreground": { + "label": "卡片前景色", + "value": "ffffff", + "type": "color" + }, + "color": { + "label": "图表主色", + "value": "002EA6", + "type": "color" + } + }, + "moved": false + }, + { + "type": "chart", + "chartRage": 6, + "serverValues": [ + { + "dataName": "磁盘总使用率", + "dataType": "DiskTotalUsage" + }, + { + "dataName": "内存使用率", + "dataType": "MemoryUsage" + }, + { + "dataName": "CPU总使用率", + "dataType": "CpuTotalUsage" + } + ], + "i": "17334b32-cede-441f-9bef-772d170f1c2a", + "x": 0, + "y": 29, + "h": 16, + "w": 9, + "selectChart": "histogram", + "cardConfig": { + "name": { + "label": "卡片名称", + "value": "默认卡片名称", + "type": "text" + }, + "description": { + "label": "卡片描述", + "value": "这是一张默认的卡片", + "type": "text" + }, + "foreground": { + "label": "卡片前景色", + "value": "ffffff", + "type": "color" + }, + "color": { + "label": "图表主色", + "value": "002EA6", + "type": "color" + } + }, + "moved": false + }, + { + "type": "chart", + "chartRage": 6, + "serverValues": [ + { + "dataName": "空闲内存", + "dataType": "MemoryFree" + }, + { + "dataName": "已使用内存", + "dataType": "MemoryUsed" + } + ], + "i": "06e64544-dfb8-4a28-b777-96cb4fd70aec", + "x": 0, + "y": 13, + "h": 16, + "w": 9, + "selectChart": "area", + "cardConfig": { + "name": { + "label": "卡片名称", + "value": "网络使用率卡片", + "type": "text" + }, + "description": { + "label": "卡片描述", + "value": "这是一张默认的卡片", + "type": "text" + }, + "foreground": { + "label": "卡片前景色", + "value": "ffffff", + "type": "color" + }, + "color": { + "label": "图表主色", + "value": "002EA6", + "type": "color" + } + }, + "moved": false + } + ], + "xl": [ + { + "type": "chart", + "chartRage": 6, + "serverValues": [ + { + "dataName": "CPU总使用率", + "dataType": "CpuTotalUsage" + }, + { + "dataName": "内存使用率", + "dataType": "MemoryUsage" + }, + { + "dataName": "磁盘总使用率", + "dataType": "DiskTotalUsage" + } + ], + "i": "069299c5-ceec-41cd-a1a8-b18ce0598317", + "x": 0, + "y": 0, + "h": 12, + "w": 9, + "selectChart": "line", + "cardConfig": { + "name": { + "label": "卡片名称", + "value": "系统信息卡片", + "type": "text" + }, + "description": { + "label": "卡片描述", + "value": "这是一张默认的卡片", + "type": "text" + }, + "foreground": { + "label": "卡片前景色", + "value": "ffffff", + "type": "color" + }, + "color": { + "label": "图表主色", + "value": "002EA6", + "type": "color" + } + }, + "moved": false + }, + { + "type": "card", + "h": 15, + "w": 3, + "x": 9, + "y": 0, + "i": "68af2024-4a21-4351-b1ce-707aeeb3ea9c", + "selectCard": "systemInfo", + "moved": false + }, + { + "type": "card", + "h": 29, + "w": 3, + "x": 9, + "y": 15, + "i": "8104448e-6c47-455f-b635-43b3ac75d4df", + "selectCard": "onlineUsers", + "moved": false + }, + { + "type": "chart", + "chartRage": 6, + "serverValues": [ + { + "dataName": "空闲内存", + "dataType": "MemoryFree" + }, + { + "dataName": "已使用内存", + "dataType": "MemoryUsed" + } + ], + "i": "6aa12623-fee2-48e3-86f1-88fcd538c4a8", + "x": 4, + "y": 28, + "h": 16, + "w": 5, + "selectChart": "pie", + "cardConfig": { + "name": { + "label": "卡片名称", + "value": "默认卡片名称", + "type": "text" + }, + "description": { + "label": "卡片描述", + "value": "这是一张默认的卡片", + "type": "text" + }, + "foreground": { + "label": "卡片前景色", + "value": "ffffff", + "type": "color" + }, + "color": { + "label": "图表主色", + "value": "002EA6", + "type": "color" + } + }, + "moved": false + }, + { + "type": "chart", + "chartRage": 6, + "serverValues": [ + { + "dataName": "磁盘总使用率", + "dataType": "DiskTotalUsage" + }, + { + "dataName": "内存使用率", + "dataType": "MemoryUsage" + }, + { + "dataName": "CPU总使用率", + "dataType": "CpuTotalUsage" + } + ], + "i": "17334b32-cede-441f-9bef-772d170f1c2a", + "x": 0, + "y": 28, + "h": 16, + "w": 4, + "selectChart": "histogram", + "cardConfig": { + "name": { + "label": "卡片名称", + "value": "默认卡片名称", + "type": "text" + }, + "description": { + "label": "卡片描述", + "value": "这是一张默认的卡片", + "type": "text" + }, + "foreground": { + "label": "卡片前景色", + "value": "ffffff", + "type": "color" + }, + "color": { + "label": "图表主色", + "value": "002EA6", + "type": "color" + } + }, + "moved": false + }, + { + "type": "chart", + "chartRage": 6, + "serverValues": [ + { + "dataName": "空闲内存", + "dataType": "MemoryFree" + }, + { + "dataName": "已使用内存", + "dataType": "MemoryUsed" + } + ], + "i": "06e64544-dfb8-4a28-b777-96cb4fd70aec", + "x": 0, + "y": 12, + "h": 16, + "w": 9, + "selectChart": "area", + "cardConfig": { + "name": { + "label": "卡片名称", + "value": "网络使用率卡片", + "type": "text" + }, + "description": { + "label": "卡片描述", + "value": "这是一张默认的卡片", + "type": "text" + }, + "foreground": { + "label": "卡片前景色", + "value": "ffffff", + "type": "color" + }, + "color": { + "label": "图表主色", + "value": "002EA6", + "type": "color" + } + }, + "moved": false + } + ], + "sm": [ + { + "type": "chart", + "chartRage": 6, + "serverValues": [ + { + "dataName": "CPU总使用率", + "dataType": "CpuTotalUsage" + }, + { + "dataName": "内存使用率", + "dataType": "MemoryUsage" + }, + { + "dataName": "磁盘总使用率", + "dataType": "DiskTotalUsage" + } + ], + "i": "069299c5-ceec-41cd-a1a8-b18ce0598317", + "x": 0, + "y": 0, + "h": 12, + "w": 9, + "selectChart": "line", + "cardConfig": { + "name": { + "label": "卡片名称", + "value": "系统信息卡片", + "type": "text" + }, + "description": { + "label": "卡片描述", + "value": "这是一张默认的卡片", + "type": "text" + }, + "foreground": { + "label": "卡片前景色", + "value": "ffffff", + "type": "color" + }, + "color": { + "label": "图表主色", + "value": "002EA6", + "type": "color" + } + }, + "moved": false + }, + { + "type": "card", + "h": 16, + "w": 3, + "x": 0, + "y": 12, + "i": "68af2024-4a21-4351-b1ce-707aeeb3ea9c", + "selectCard": "systemInfo", + "moved": false + }, + { + "type": "card", + "h": 16, + "w": 3, + "x": 3, + "y": 12, + "i": "8104448e-6c47-455f-b635-43b3ac75d4df", + "selectCard": "onlineUsers", + "moved": false + }, + { + "type": "chart", + "chartRage": 6, + "serverValues": [ + { + "dataName": "空闲内存", + "dataType": "MemoryFree" + }, + { + "dataName": "已使用内存", + "dataType": "MemoryUsed" + } + ], + "i": "6aa12623-fee2-48e3-86f1-88fcd538c4a8", + "x": 3, + "y": 44, + "h": 16, + "w": 3, + "selectChart": "pie", + "cardConfig": { + "name": { + "label": "卡片名称", + "value": "默认卡片名称", + "type": "text" + }, + "description": { + "label": "卡片描述", + "value": "这是一张默认的卡片", + "type": "text" + }, + "foreground": { + "label": "卡片前景色", + "value": "ffffff", + "type": "color" + }, + "color": { + "label": "图表主色", + "value": "002EA6", + "type": "color" + } + }, + "moved": false + }, + { + "type": "chart", + "chartRage": 6, + "serverValues": [ + { + "dataName": "磁盘总使用率", + "dataType": "DiskTotalUsage" + }, + { + "dataName": "内存使用率", + "dataType": "MemoryUsage" + }, + { + "dataName": "CPU总使用率", + "dataType": "CpuTotalUsage" + } + ], + "i": "17334b32-cede-441f-9bef-772d170f1c2a", + "x": 0, + "y": 44, + "h": 16, + "w": 3, + "selectChart": "histogram", + "cardConfig": { + "name": { + "label": "卡片名称", + "value": "默认卡片名称", + "type": "text" + }, + "description": { + "label": "卡片描述", + "value": "这是一张默认的卡片", + "type": "text" + }, + "foreground": { + "label": "卡片前景色", + "value": "ffffff", + "type": "color" + }, + "color": { + "label": "图表主色", + "value": "002EA6", + "type": "color" + } + }, + "moved": false + }, + { + "type": "chart", + "chartRage": 6, + "serverValues": [ + { + "dataName": "空闲内存", + "dataType": "MemoryFree" + }, + { + "dataName": "已使用内存", + "dataType": "MemoryUsed" + } + ], + "i": "06e64544-dfb8-4a28-b777-96cb4fd70aec", + "x": 0, + "y": 28, + "h": 16, + "w": 9, + "selectChart": "area", + "cardConfig": { + "name": { + "label": "卡片名称", + "value": "网络使用率卡片", + "type": "text" + }, + "description": { + "label": "卡片描述", + "value": "这是一张默认的卡片", + "type": "text" + }, + "foreground": { + "label": "卡片前景色", + "value": "ffffff", + "type": "color" + }, + "color": { + "label": "图表主色", + "value": "002EA6", + "type": "color" + } + }, + "moved": false + } + ], + "xs": [ + { + "type": "chart", + "chartRage": 6, + "serverValues": [ + { + "dataName": "CPU总使用率", + "dataType": "CpuTotalUsage" + }, + { + "dataName": "内存使用率", + "dataType": "MemoryUsage" + }, + { + "dataName": "磁盘总使用率", + "dataType": "DiskTotalUsage" + } + ], + "i": "069299c5-ceec-41cd-a1a8-b18ce0598317", + "x": 0, + "y": 0, + "h": 12, + "w": 9, + "selectChart": "line", + "cardConfig": { + "name": { + "label": "卡片名称", + "value": "系统信息卡片", + "type": "text" + }, + "description": { + "label": "卡片描述", + "value": "这是一张默认的卡片", + "type": "text" + }, + "foreground": { + "label": "卡片前景色", + "value": "ffffff", + "type": "color" + }, + "color": { + "label": "图表主色", + "value": "002EA6", + "type": "color" + } + }, + "moved": false + }, + { + "type": "card", + "h": 16, + "w": 2, + "x": 2, + "y": 12, + "i": "68af2024-4a21-4351-b1ce-707aeeb3ea9c", + "selectCard": "systemInfo", + "moved": false + }, + { + "type": "card", + "h": 16, + "w": 2, + "x": 0, + "y": 12, + "i": "8104448e-6c47-455f-b635-43b3ac75d4df", + "selectCard": "onlineUsers", + "moved": false + }, + { + "type": "chart", + "chartRage": 6, + "serverValues": [ + { + "dataName": "空闲内存", + "dataType": "MemoryFree" + }, + { + "dataName": "已使用内存", + "dataType": "MemoryUsed" + } + ], + "i": "6aa12623-fee2-48e3-86f1-88fcd538c4a8", + "x": 3, + "y": 59, + "h": 16, + "w": 4, + "selectChart": "pie", + "cardConfig": { + "name": { + "label": "卡片名称", + "value": "默认卡片名称", + "type": "text" + }, + "description": { + "label": "卡片描述", + "value": "这是一张默认的卡片", + "type": "text" + }, + "foreground": { + "label": "卡片前景色", + "value": "ffffff", + "type": "color" + }, + "color": { + "label": "图表主色", + "value": "002EA6", + "type": "color" + } + }, + "moved": false + }, + { + "type": "chart", + "chartRage": 6, + "serverValues": [ + { + "dataName": "磁盘总使用率", + "dataType": "DiskTotalUsage" + }, + { + "dataName": "内存使用率", + "dataType": "MemoryUsage" + }, + { + "dataName": "CPU总使用率", + "dataType": "CpuTotalUsage" + } + ], + "i": "17334b32-cede-441f-9bef-772d170f1c2a", + "x": 0, + "y": 44, + "h": 15, + "w": 4, + "selectChart": "histogram", + "cardConfig": { + "name": { + "label": "卡片名称", + "value": "默认卡片名称", + "type": "text" + }, + "description": { + "label": "卡片描述", + "value": "这是一张默认的卡片", + "type": "text" + }, + "foreground": { + "label": "卡片前景色", + "value": "ffffff", + "type": "color" + }, + "color": { + "label": "图表主色", + "value": "002EA6", + "type": "color" + } + }, + "moved": false + }, + { + "type": "chart", + "chartRage": 6, + "serverValues": [ + { + "dataName": "空闲内存", + "dataType": "MemoryFree" + }, + { + "dataName": "已使用内存", + "dataType": "MemoryUsed" + } + ], + "i": "06e64544-dfb8-4a28-b777-96cb4fd70aec", + "x": 0, + "y": 28, + "h": 16, + "w": 9, + "selectChart": "area", + "cardConfig": { + "name": { + "label": "卡片名称", + "value": "网络使用率卡片", + "type": "text" + }, + "description": { + "label": "卡片描述", + "value": "这是一张默认的卡片", + "type": "text" + }, + "foreground": { + "label": "卡片前景色", + "value": "ffffff", + "type": "color" + }, + "color": { + "label": "图表主色", + "value": "002EA6", + "type": "color" + } + }, + "moved": false + } + ] +} \ No newline at end of file diff --git a/LoongPanel-Asp/appsettings.Development.json b/LoongPanel-Asp/appsettings.Development.json new file mode 100755 index 0000000..0c208ae --- /dev/null +++ b/LoongPanel-Asp/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/LoongPanel-Asp/appsettings.json b/LoongPanel-Asp/appsettings.json new file mode 100755 index 0000000..21edd5a --- /dev/null +++ b/LoongPanel-Asp/appsettings.json @@ -0,0 +1,23 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Host=192.168.0.26;Port=54321;Username=SYSTEM;Password=loongnix;Database=app", + "SqliteConnection": "Data Source=app.db;" + }, + "EmailSettings": { + "Host": "smtp.qq.com", + "Port": "465", + "Username": "70975268@qq.com", + "Password": "udyligofifdibhdc" + }, + "Secret": "p4Qzf/+GPP/XNLalZGCzwlelOl6skiFZscj6iZ6rZZE=", + "Issuer": "LoongPanel", + "Audience": "LoongPanel", + "PubLicApi": "/Api/Account/SendVerificationCode;/Api/Account/Register;/Api/Account/Login;/Api/Account/VerifyEmailName;" +} diff --git a/LoongPanel-Asp/utils/ControllerScanner.cs b/LoongPanel-Asp/utils/ControllerScanner.cs new file mode 100755 index 0000000..99cbdcf --- /dev/null +++ b/LoongPanel-Asp/utils/ControllerScanner.cs @@ -0,0 +1,42 @@ +using System.Reflection; +using Microsoft.AspNetCore.Mvc; + +namespace LoongPanel_Asp.utils; + +public static class ControllerScanner +{ + public static List GetApiPermissions() + { + var apiPermissions = new List(); + + var allTypes = Assembly.GetExecutingAssembly().GetTypes(); + + var controllerTypes = allTypes + .Where(t => t.Namespace != null && t.Namespace.StartsWith("LoongPanel_Asp.Controllers") && + t.BaseType == typeof(ControllerBase)) + .ToList(); + var index = 0; + foreach (var controller in controllerTypes) + { + //获取控制器所有路由 + var routes = controller.GetMethods(BindingFlags.Public | BindingFlags.Instance); + ; + foreach (var route in routes) + { + //判断返回类型是否是System.Threading.Tasks.Task`1[Microsoft.AspNetCore.Mvc.IActionResult] + if (route.ReturnType != typeof(Task) && + route.ReturnType != typeof(IActionResult)) continue; + index++; + var newPermission = new ApiPermission + { + Id = index, + Name = route.Name, + Controller = controller.Name + }; + apiPermissions.Add(newPermission); + } + } + + return apiPermissions; + } +} \ No newline at end of file diff --git a/web/.env b/web/.env new file mode 100755 index 0000000..e468ea0 --- /dev/null +++ b/web/.env @@ -0,0 +1 @@ +API_SERVER="http://192.168.0.13:5253" \ No newline at end of file diff --git a/web/.gitignore b/web/.gitignore new file mode 100755 index 0000000..b477fd8 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,24 @@ +# Nuxt dev/build outputs +.output +.data +.nuxt +.nitro +.cache +dist + +# Node dependencies +node_modules + +# Logs +logs +*.log + +# Misc +.DS_Store +.fleet +.idea + +# Local env files +#.env +#.env.* +#!.env.example diff --git a/web/README.md b/web/README.md new file mode 100755 index 0000000..f5db2a2 --- /dev/null +++ b/web/README.md @@ -0,0 +1,75 @@ +# Nuxt 3 Minimal Starter + +Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. + +## Setup + +Make sure to install the dependencies: + +```bash +# npm +npm install + +# pnpm +pnpm install + +# yarn +yarn install + +# bun +bun install +``` + +## Development Server + +Start the development server on `http://localhost:3000`: + +```bash +# npm +npm run dev + +# pnpm +pnpm run dev + +# yarn +yarn dev + +# bun +bun run dev +``` + +## Production + +Build the application for production: + +```bash +# npm +npm run build + +# pnpm +pnpm run build + +# yarn +yarn build + +# bun +bun run build +``` + +Locally preview production build: + +```bash +# npm +npm run preview + +# pnpm +pnpm run preview + +# yarn +yarn preview + +# bun +bun run preview +``` + +Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. diff --git a/web/app.vue b/web/app.vue new file mode 100755 index 0000000..b1b48bf --- /dev/null +++ b/web/app.vue @@ -0,0 +1,18 @@ + + + + diff --git a/web/assets/diagonal1.cur b/web/assets/diagonal1.cur new file mode 100755 index 0000000000000000000000000000000000000000..20857fb8e4bc262f65714d3fe9325c1ff0522970 GIT binary patch literal 4286 zcmeH}J#NB45QPUW099Hjk$eU#DJ*ddpTrWBp>ppNqzos3E(b|PBuC;;qCiaE+h{uK zN5QT&UVCTO-_DG@J7z;0*mP<(!u2sWdoi;a1Vi=)ffjeOC3Lyv2s~Q^s;cr3rSHK! z9*%xKpVAsse6oLbJOAx_-)^?vb)A$?Ke>kW`qSICm9xvT^rmSNceQ$#@`KGj;-JcV zu#2KdY!#OrG8ywnvFp0dY+=Hg=MW(_p~!vKd_VFZBRlprMb3GKkQ4iwnjL3{+Hd+y z-+6ZO42?K&T+zs5&A{4$H3Vx3Ih&BWS?&q_Aq8%#AYZVd}Dn$ zwfM}-95-*Tx+&t^?_B$ai-ohv4(G(D=KDF|G7mm=Q&~9JQha##U!NLu_wgP@eCj#i zl;)?n%t3yt{nH;F*BzgHZa#hCq_{li)cfxg`LULMrTJsIeqXG|`uvd3x?T6}`>`JD z^C_SD-*QuZca7KL*Wz+rYVm7v|M%VhFRJ~X4SmnKPq{76U2Q&dU~}&?)eoMxsruY; xRc(K$Q_-lSnRceNETb%}LHM1;gIcQzl4aC7mZ)|Iz1o=pOjg;e@1mBD`UZ>tBlrLS literal 0 HcmV?d00001 diff --git a/web/assets/min.scss b/web/assets/min.scss new file mode 100755 index 0000000..2f89066 --- /dev/null +++ b/web/assets/min.scss @@ -0,0 +1,23 @@ +@import "base"; + +* { + margin: 0; + box-sizing: border-box; + transition: color .2s, background-color .2s, border-color .2s, box-shadow .2s, stork .2s, fill .2s, opacity .2s; + cursor: url("assets/normal.cur"), auto; +} + +body { + background: $light-bg-underline-color; + color: $light-text-color; + + .dark-mode & { + background: $dark-bg-underline-color; + color: $dark-text-color; + } +} + +// 去除a标签的默认下划线 +a { + text-decoration: none; +} diff --git a/web/assets/normal.cur b/web/assets/normal.cur new file mode 100755 index 0000000000000000000000000000000000000000..2ef5163df17d4d1ade468abcf8212b50cd8792eb GIT binary patch literal 4286 zcmdUuF;2rk5Jd-QkU)ecl$IMrBub>Trlq0d1Qc)sT3T`r3JMP4j)J4m_y9q}>{?HP zQ7kKwy=yG}OlHU4H-E<#*_S;jONrOpsmPiATbN4IwaKdXEAni$TvvI1sGF3kpH1wr zmYRQeKi8mTbrSX8`2HOG`adcb8dTgqM&X;-_q?x9g-`!o{owoJ;YEYy%bO_gXWJZJ zEZ}j# zpZ(#%A*aszP<-}<^Zs$ELFAr8&U-<=>-=-^x8-_&_$K;#U!IHK#cjS{HE35qeSWFu z;^*Q{#W(Zi`zB8{Z+NNr%sVyL&wDC-`WSoOUH((^@#!P|jU7Fgd~7cJt7`p6jfzGc q&$KbErHZm;4I*!BJ*c*Of~1OC$6HjJgI4v-04BS$RlAE?IqC=VUEiGm literal 0 HcmV?d00001 diff --git a/web/assets/normalshowel.cur b/web/assets/normalshowel.cur new file mode 100755 index 0000000000000000000000000000000000000000..e568830dcdc4fd63c95789f89364a2b363a1e343 GIT binary patch literal 4286 zcmcgvJx|+E6gBOZ7DzfI(4ldM7H8`cXTubb2nQJ0scRj1($WnevwnVKKT|9UbrP* zZC6UQt$r>x$25;qz|MoW2V5`Q*3ddUkPZdEs@i+Coqi7V0=UJL^Q-jkTWrp#u~Ms)!JRH z1D_EpPv$UcebU#g*WLFuU^9qcQq7abviq^O>9?uMih7Mcr?xtG;5oZ4t)qI~U(y_O z#-PWuy_uIyQFnv843C*wQ*(p+Fj`yu7d@{Te7tK?vgXv@uDj2Xoc-Xb zw4=IoqK2GbucNnm@6dH%@;w4v{-#J{uK7Vm%Ci~i+%%-kKptkUVwTtV{v>bq7&*UA zg!6E{%FvzKpIxs>`??_)(K_CrD^kGk7BBp&Ih4X(!#|;#ld}(>k#{=+ZuD7>elE(% z<*Xcc7iGWY%HFBl<4oSAM0p#{fe+pt)jgD>ZlKtQ-Fa!Yrsd#t+Vq#|o{ZPZvlHN* zk@M&yVhvH3@0Sa5(p^x#%~oCxe!P^!?h^8uhaW!4#Z6t#el_qMpB?nZu!eEZsWYP! zeNdgbH*t=;b6WEz>dxBpJEILRTp(9^qTK7xxLBjs5nF2xpA(yVQ}=I|Z&7c9P2H&l zuhWy-7pN1t^Z8Nx#=S-k+{gIb@Y-)ZN6n`-2XdwsohVdKYQFF&=R;hKyVjM1obo4k z<%Ly_wOueAm8l?~xC^Pdwr_QR_BxpuVaz@}eh+%ifH;Z`^e{ z>l7c@tyh`@>tL~&2m6Avff0A#xa+8WCCJMDX+g1B2Wmk-Fc0PJ!yK&_G)(?JXz&@~ literal 0 HcmV?d00001 diff --git a/web/base.scss b/web/base.scss new file mode 100755 index 0000000..48074fd --- /dev/null +++ b/web/base.scss @@ -0,0 +1,35 @@ +$light-bg-color: #FFFFFF; +$light-bg-underline-color: #e6e9ed; +$dark-bg-underline-color: #141723; +$dark-bg-color: #222738; + +//$primary-color: #64a77c; +$primary-color: #002EA6; +$secondary-color: #ce5230; +$tertiary-color: #c4c744; + +$bg-color-1: #3B8AFF; +$bg-color-2: #0051B5; + +$unfocused-color: #8A92A6; + +$light-text-color: #04040B; +$light-unfocused-color: $unfocused-color; + +$dark-text-color: #D3D3D3; +$dark-unfocused-color: $unfocused-color; + +$gap: 8px; +$padding: 16px; +$radius: 8px; + +.dark-mode html { + $dark-bg-color: red; +} + +@mixin SC_Font { + font-family: "Noto Sans SC", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; +} \ No newline at end of file diff --git a/web/components/AddCard.vue b/web/components/AddCard.vue new file mode 100644 index 0000000..bfd3d87 --- /dev/null +++ b/web/components/AddCard.vue @@ -0,0 +1,61 @@ + + + + + \ No newline at end of file diff --git a/web/components/Cards/ICard.vue b/web/components/Cards/ICard.vue new file mode 100644 index 0000000..6668748 --- /dev/null +++ b/web/components/Cards/ICard.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/web/components/Cards/IChart.vue b/web/components/Cards/IChart.vue new file mode 100755 index 0000000..42e30e5 --- /dev/null +++ b/web/components/Cards/IChart.vue @@ -0,0 +1,178 @@ + + + + + diff --git a/web/components/Cards/MiniCard.vue b/web/components/Cards/MiniCard.vue new file mode 100755 index 0000000..9570c6f --- /dev/null +++ b/web/components/Cards/MiniCard.vue @@ -0,0 +1,171 @@ + + + + + \ No newline at end of file diff --git a/web/components/Cards/OnlineUsersCard.vue b/web/components/Cards/OnlineUsersCard.vue new file mode 100644 index 0000000..b2f9379 --- /dev/null +++ b/web/components/Cards/OnlineUsersCard.vue @@ -0,0 +1,82 @@ + + +