diff --git a/src/blazor/admin/BootstrapAdmin.Web/Controllers/AccountController.cs b/src/blazor/admin/BootstrapAdmin.Web/Controllers/AccountController.cs index 13d43263..7f046f88 100644 --- a/src/blazor/admin/BootstrapAdmin.Web/Controllers/AccountController.cs +++ b/src/blazor/admin/BootstrapAdmin.Web/Controllers/AccountController.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.WebUtilities; using System.Security.Claims; -namespace Bootstrap.Admin.Controllers +namespace BootstrapAdmin.Web.Controllers { /// /// Account controller. diff --git a/src/blazor/admin/BootstrapAdmin.Web/Controllers/api/GiteeController.cs b/src/blazor/admin/BootstrapAdmin.Web/Controllers/api/GiteeController.cs new file mode 100644 index 00000000..afc31131 --- /dev/null +++ b/src/blazor/admin/BootstrapAdmin.Web/Controllers/api/GiteeController.cs @@ -0,0 +1,109 @@ +using BootstrapAdmin.Web.HealthChecks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Text.RegularExpressions; +using RouteAttribute = Microsoft.AspNetCore.Mvc.RouteAttribute; + +namespace BootstrapAdmin.Web.Controllers.Api +{ + /// + /// Gitee 网站信息接口类 + /// + [Route("api/[controller]/[action]")] + [ApiController] + [AllowAnonymous] + public class GiteeController : ControllerBase + { + /// + /// 获取 Gitee 网站 Issues 信息 + /// + /// + /// + /// + /// + /// + /// + [HttpGet] + public async Task Issues([FromServices] GiteeHttpClient client, [FromQuery] string? userName = "LongbowEnterprise", [FromQuery] string? repoName = "BootstrapAdmin", [FromQuery] string? label = "custom badge", [FromQuery] string? color = "orange") + { + var content = await client.HttpClient.GetStringAsync($"https://gitee.com/{userName}/{repoName}/issues"); + var regex = Regex.Matches(content, "
([\\d]+)
", RegexOptions.IgnoreCase); + var labels = new string[] { "open", "progressing", "closed", "rejected" }; + var result = string.IsNullOrEmpty(content) ? new string[] { "unknown" } : regex.Select((m, i) => $"{labels[i]} {m.Groups[1].Value}"); + var msg = string.Join(" ", result); + color = msg.StartsWith("open 0 progressing 0", StringComparison.OrdinalIgnoreCase) ? "success" : color; + return new JsonResult(new { schemaVersion = 1, label, message = msg, color }); + } + + /// + /// 获取 Gitee 网站 Pulls 信息 + /// + /// + /// + /// + /// + /// + /// + [HttpGet] + public async Task Pulls([FromServices] GiteeHttpClient client, [FromQuery] string? userName = "LongbowEnterprise", [FromQuery] string? repoName = "BootstrapAdmin", [FromQuery] string? label = "custom badge", [FromQuery] string? color = "orange") + { + var content = await client.HttpClient.GetStringAsync($"https://gitee.com/{userName}/{repoName}/pulls") ?? ""; + var regex = Regex.Matches(content, "
([\\d]+)
", RegexOptions.IgnoreCase); + var labels = new string[] { "open", "merged", "closed" }; + var result = string.IsNullOrEmpty(content) ? new string[] { "unknown" } : regex.Select((m, i) => $"{labels[i]} {m.Groups[1].Value}"); + var msg = string.Join(" ", result); + return new JsonResult(new { schemaVersion = 1, label, message = msg, color }); + } + + /// + /// 获取 Gitee 网站 Releases 信息 + /// + /// + /// + /// + /// + /// + /// + [HttpGet] + public async Task Releases([FromServices] GiteeHttpClient client, [FromQuery] string? userName = "LongbowEnterprise", [FromQuery] string? repoName = "BootstrapAdmin", [FromQuery] string? label = "custom badge", [FromQuery] string? color = "orange") + { + var content = await client.HttpClient.GetStringAsync($"https://gitee.com/{userName}/{repoName}/releases") ?? ""; + var regex = Regex.Match(content, $"", RegexOptions.IgnoreCase); + var msg = regex?.Groups[1].Value ?? "unknown"; + return new JsonResult(new { schemaVersion = 1, label, message = msg, color }); + } + + /// + /// 获取 Gitee 网站 Builds 信息 + /// + /// + /// + /// + /// + /// + /// + /// + [HttpGet] + public async Task Builds([FromServices] GiteeHttpClient client, [FromQuery] string? userName = "ArgoZhang", [FromQuery] string? projName = "bootstrapadmin", [FromQuery] string? branchName = "master", [FromQuery] string? label = "custom badge", [FromQuery] string? color = "orange") + { + var content = await client.HttpClient.GetFromJsonAsync($"https://ci.appveyor.com/api/projects/{userName}/{projName}/branch/{branchName}", new CancellationTokenSource(10000).Token); + return new JsonResult(new { schemaVersion = 1, label, message = content?.Build.Version ?? "unknown", color }); + } + + private class AppveyorBuildResult + { + /// + /// Appveyor 编译版本实例 + /// + public Build Build { get; set; } = new Build(); + } + + private class Build + { + /// + /// Build 版本信息 + /// + public string Version { get; set; } = ""; + } + } +} diff --git a/src/blazor/admin/BootstrapAdmin.Web/Extensions/ApplicationBuilderExtensions.cs b/src/blazor/admin/BootstrapAdmin.Web/Extensions/ApplicationBuilderExtensions.cs index eddd456e..c177c7fc 100644 --- a/src/blazor/admin/BootstrapAdmin.Web/Extensions/ApplicationBuilderExtensions.cs +++ b/src/blazor/admin/BootstrapAdmin.Web/Extensions/ApplicationBuilderExtensions.cs @@ -10,8 +10,11 @@ ///
/// /// - public static IApplicationBuilder UseBootstrapBlazorAdmin(this IApplicationBuilder builder) + public static WebApplication UseBootstrapBlazorAdmin(this WebApplication builder) { + // 开启健康检查 + builder.MapBootstrapHealthChecks(); + builder.UseAuthentication(); builder.UseAuthorization(); diff --git a/src/blazor/admin/BootstrapAdmin.Web/Extensions/ServicesExtensions.cs b/src/blazor/admin/BootstrapAdmin.Web/Extensions/ServicesExtensions.cs index 9460ed5c..d2a1e943 100644 --- a/src/blazor/admin/BootstrapAdmin.Web/Extensions/ServicesExtensions.cs +++ b/src/blazor/admin/BootstrapAdmin.Web/Extensions/ServicesExtensions.cs @@ -4,6 +4,8 @@ using BootstrapAdmin.Web.Services.SMS.Tencent; using BootstrapAdmin.Web.Utils; //using Microsoft.EntityFrameworkCore; using System.Text; +using System.Text.Encodings.Web; +using System.Text.Unicode; namespace Microsoft.Extensions.DependencyInjection { @@ -19,16 +21,21 @@ namespace Microsoft.Extensions.DependencyInjection public static IServiceCollection AddBootstrapBlazorAdmin(this IServiceCollection services) { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - services.AddLogging(logging => logging.AddFileLogger().AddCloudLogger().AddDBLogger(ExceptionsHelper.Log)); services.AddCors(); services.AddResponseCompression(); + // 增加编码服务 + services.AddSingleton(HtmlEncoder.Create(UnicodeRanges.All)); + + // 增加 健康检查服务 + services.AddAdminHealthChecks(); + // 增加 BootstrapBlazor 组件 services.AddBootstrapBlazor(); // 增加手机短信服务 - services.AddScoped(); + services.AddSingleton(); // 增加认证授权服务 services.AddBootstrapAdminSecurity(); diff --git a/src/blazor/admin/BootstrapAdmin.Web/HealthChecks/DBHealthCheck.cs b/src/blazor/admin/BootstrapAdmin.Web/HealthChecks/DBHealthCheck.cs new file mode 100644 index 00000000..d8ad5175 --- /dev/null +++ b/src/blazor/admin/BootstrapAdmin.Web/HealthChecks/DBHealthCheck.cs @@ -0,0 +1,125 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace BootstrapAdmin.Web.HealthChecks; + +/// +/// 数据库检查类 +/// +class DBHealthCheck : IHealthCheck +{ + private readonly IConfiguration _configuration; + private readonly IHttpContextAccessor _httpContextAccessor; + private static readonly Func ConnectionStringResolve = (c, name) => string.IsNullOrEmpty(name) + ? c.GetSection("ConnectionStrings").GetChildren().FirstOrDefault()?.Value + : c.GetConnectionString(name); + + /// + /// 构造函数 + /// + /// + /// + public DBHealthCheck(IConfiguration configuration, IHttpContextAccessor httpContextAccessor) + { + _configuration = configuration; + _httpContextAccessor = httpContextAccessor; + } + + /// + /// 异步检查方法 + /// + /// + /// + /// + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + //var db = _configuration.GetSection("DB").GetChildren() + // .Select(config => new DbOption() + // { + // Enabled = bool.TryParse(config["Enabled"], out var en) ? en : false, + // ProviderName = config["ProviderName"], + // Widget = config["Widget"], + // ConnectionString = ConnectionStringResolve(config.GetSection("ConnectionStrings").Exists() ? config : _configuration, string.Empty) + // }).FirstOrDefault(i => i.Enabled) ?? new DbOption() + // { + // Enabled = true, + // ProviderName = Longbow.Data.DatabaseProviderType.SqlServer.ToString(), + // Widget = typeof(User).Assembly.FullName, + // ConnectionString = Longbow.Data.DbManager.GetConnectionString() + // }; + + //// 检查 当前用户 账户权限 + //var loginUser = _httpContextAccessor.HttpContext?.User.Identity?.Name; + //var userName = loginUser ?? "Admin"; + //var dictsCount = 0; + //var menusCount = 0; + //var roles = string.Empty; + //var displayName = string.Empty; + //var healths = false; + //Exception? error = null; + //try + //{ + // var user = UserHelper.RetrieveUserByUserName(userName); + // displayName = user?.DisplayName ?? string.Empty; + // roles = string.Join(",", RoleHelper.RetrievesByUserName(userName) ?? new string[0]); + // menusCount = MenuHelper.RetrieveMenusByUserName(userName)?.Count() ?? 0; + // dictsCount = DictHelper.RetrieveDicts()?.Count() ?? 0; + // healths = user != null && !string.IsNullOrEmpty(roles) && menusCount > 0 && dictsCount > 0; + + // // 检查数据库是否可写 + // var dict = new BootstrapDict() + // { + // Category = "DB-Check", + // Name = "WriteTest", + // Code = "1" + // }; + // if (DictHelper.Save(dict) && !string.IsNullOrEmpty(dict.Id)) DictHelper.Delete(new string[] { dict.Id }); + //} + //catch (Exception ex) + //{ + // error = ex; + //} + //var data = new Dictionary() + //{ + // { "ConnectionString", db.ConnectionString ?? string.Empty }, + // { "Reference", DbContextManager.Create()?.GetType().Assembly.FullName ?? db.Widget ?? string.Empty }, + // { "DbType", db?.ProviderName ?? string.Empty }, + // { "Dicts", dictsCount }, + // { "LoginName", userName }, + // { "DisplayName", displayName }, + // { "Roles", roles }, + // { "Navigations", menusCount } + //}; + + //if (string.IsNullOrEmpty(db?.ConnectionString)) + //{ + // // 未启用连接字符串 + // data["ConnectionString"] = "未配置数据库连接字符串"; + // return Task.FromResult(HealthCheckResult.Unhealthy("Error", null, data)); + //} + + //if (DbContextManager.Exception != null) error = DbContextManager.Exception; + //if (error != null) + //{ + // data.Add("Exception", error.Message); + + // if (error.Message.Contains("SQLite Error 8: 'attempt to write a readonly database'.")) data.Add("解决办法", "更改数据库文件为可读,并授予进程可写权限"); + // if (error.Message.Contains("Could not load", StringComparison.OrdinalIgnoreCase)) data.Add("解决办法", "Nuget 引用相对应的数据库驱动 dll"); + + // // UNDONE: Json 序列化循环引用导致异常 NET 5.0 修复此问题 + // // 目前使用 new Exception() 临时解决 + // return Task.FromResult(HealthCheckResult.Unhealthy("Error", new Exception(error.Message), data)); + //} + + //return healths ? Task.FromResult(HealthCheckResult.Healthy("Ok", data)) : Task.FromResult(HealthCheckResult.Degraded("Failed", null, data)); + + return Task.FromResult(HealthCheckResult.Healthy("Ok")); + } + + private class DbOption + { + public bool Enabled { get; set; } + public string? ProviderName { get; set; } + public string? Widget { get; set; } + public string? ConnectionString { get; set; } + } +} diff --git a/src/blazor/admin/BootstrapAdmin.Web/HealthChecks/GiteeHttpClient.cs b/src/blazor/admin/BootstrapAdmin.Web/HealthChecks/GiteeHttpClient.cs new file mode 100644 index 00000000..d3b54366 --- /dev/null +++ b/src/blazor/admin/BootstrapAdmin.Web/HealthChecks/GiteeHttpClient.cs @@ -0,0 +1,23 @@ +namespace BootstrapAdmin.Web.HealthChecks; + +/// +/// Gitee HttpClient 操作类 +/// +public class GiteeHttpClient +{ + /// + /// HttpClient 实例 + /// + public HttpClient HttpClient { get; private set; } + + /// + /// 构造函数 + /// + /// + public GiteeHttpClient(HttpClient client) + { + client.Timeout = TimeSpan.FromSeconds(10); + client.DefaultRequestHeaders.Connection.Add("keep-alive"); + HttpClient = client; + } +} diff --git a/src/blazor/admin/BootstrapAdmin.Web/HealthChecks/GiteeHttpHealthCheck.cs b/src/blazor/admin/BootstrapAdmin.Web/HealthChecks/GiteeHttpHealthCheck.cs new file mode 100644 index 00000000..65196565 --- /dev/null +++ b/src/blazor/admin/BootstrapAdmin.Web/HealthChecks/GiteeHttpHealthCheck.cs @@ -0,0 +1,51 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; +using System.Diagnostics; + +namespace BootstrapAdmin.Web.HealthChecks; + +/// +/// Gitee 接口检查器 +/// +class GiteeHttpHealthCheck : IHealthCheck +{ + private readonly GiteeHttpClient _client; + /// + /// 构造函数 + /// + /// + /// + public GiteeHttpHealthCheck(GiteeHttpClient client, IHttpContextAccessor accessor) + { + _client = client; + _client.HttpClient.BaseAddress = new Uri($"{accessor.HttpContext!.Request.Scheme}://{accessor.HttpContext?.Request.Host}{accessor.HttpContext?.Request.PathBase}"); + } + + /// + /// 异步检查方法 + /// + /// + /// + /// + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + var urls = new string[] { "Issues", "Pulls", "Releases", "Builds" }; + var data = new Dictionary(); + + Task.WaitAll(urls.Select(url => Task.Run(async () => + { + var sw = Stopwatch.StartNew(); + Exception? error = null; + object? result = null; + try + { + result = await _client.HttpClient.GetFromJsonAsync($"/api/Gitee/{url}", cancellationToken); + } + catch (Exception ex) { error = ex; } + sw.Stop(); + data.Add(url, error == null + ? $"{result} Elapsed: {sw.Elapsed}" + : $"Elapsed: {sw.Elapsed} Exception: {error}"); + })).ToArray(), cancellationToken); + return Task.FromResult(HealthCheckResult.Healthy("Ok", data)); + } +} diff --git a/src/blazor/admin/BootstrapAdmin.Web/HealthChecks/HealthChecksBuilderExtensions.cs b/src/blazor/admin/BootstrapAdmin.Web/HealthChecks/HealthChecksBuilderExtensions.cs new file mode 100644 index 00000000..73b3a21c --- /dev/null +++ b/src/blazor/admin/BootstrapAdmin.Web/HealthChecks/HealthChecksBuilderExtensions.cs @@ -0,0 +1,26 @@ +using BootstrapAdmin.Web.HealthChecks; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// 健康检查扩展类 +/// +static class HealthChecksBuilderExtensions +{ + /// + /// 添加 BootstrapAdmin 健康检查 + /// + /// + /// + public static IServiceCollection AddAdminHealthChecks(this IServiceCollection services) + { + services.AddHttpContextAccessor(); + services.AddHttpClient(); + + var builder = services.AddHealthChecks(); + builder.AddCheck("db"); + builder.AddBootstrapAdminHealthChecks(); + builder.AddCheck("Gitee"); + return services; + } +} diff --git a/src/blazor/admin/BootstrapAdmin.Web/Pages/Admin/Healths.razor b/src/blazor/admin/BootstrapAdmin.Web/Pages/Admin/Healths.razor new file mode 100644 index 00000000..6c3d4293 --- /dev/null +++ b/src/blazor/admin/BootstrapAdmin.Web/Pages/Admin/Healths.razor @@ -0,0 +1 @@ +@page "/Admin/Healths" diff --git a/src/blazor/admin/BootstrapAdmin.Web/Pages/Admin/Healths.razor.cs b/src/blazor/admin/BootstrapAdmin.Web/Pages/Admin/Healths.razor.cs new file mode 100644 index 00000000..4d7246af --- /dev/null +++ b/src/blazor/admin/BootstrapAdmin.Web/Pages/Admin/Healths.razor.cs @@ -0,0 +1,5 @@ +namespace BootstrapAdmin.Web.Pages.Admin; + +public partial class Healths +{ +}