Compare commits
	
		
			7 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | b64ac0539e | ||
|   | 541c60b363 | ||
|   | 824e95f7cb | ||
|   | 38f7850196 | ||
|   | bef9de88e2 | ||
|   | 48cd5e7c7f | ||
|   | 3b44fda51c | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -366,3 +366,4 @@ FodyWeavers.xsd | ||||
| /src/*Pro* | ||||
| /src/*pro* | ||||
| /src/*pro*/ | ||||
| /src/ThingsGateway.Server/Configuration/GiteeOAuthSettings.json | ||||
|   | ||||
| @@ -0,0 +1,21 @@ | ||||
| //------------------------------------------------------------------------------ | ||||
| //  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充 | ||||
| //  此代码版权(除特别声明外的代码)归作者本人Diego所有 | ||||
| //  源代码使用协议遵循本仓库的开源协议及附加协议 | ||||
| //  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway | ||||
| //  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway | ||||
| //  使用文档:https://thingsgateway.cn/ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
|  | ||||
| namespace ThingsGateway.Admin.Application; | ||||
|  | ||||
|  | ||||
| [AttributeUsage(AttributeTargets.Method)] | ||||
| public sealed class LoginLogAttribute : Attribute | ||||
| { | ||||
| } | ||||
| [AttributeUsage(AttributeTargets.Method)] | ||||
| public sealed class LogoutLogAttribute : Attribute | ||||
| { | ||||
| } | ||||
| @@ -8,6 +8,7 @@ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
|  | ||||
| using Microsoft.AspNetCore.Authentication; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
|  | ||||
| @@ -27,14 +28,31 @@ public class AuthController : ControllerBase | ||||
|  | ||||
|     [HttpPost("login")] | ||||
|     [AllowAnonymous] | ||||
|     [LoginLog] | ||||
|     public Task<LoginOutput> LoginAsync([FromBody] LoginInput input) | ||||
|     { | ||||
|  | ||||
|         return _authService.LoginAsync(input); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     [HttpGet("oauth-login")] | ||||
|     [AllowAnonymous] | ||||
|     [SuppressRequestAudit] | ||||
|     public IActionResult OAuthLogin(string scheme = "Gitee", string returnUrl = "/") | ||||
|     { | ||||
|         var props = new AuthenticationProperties | ||||
|         { | ||||
|             RedirectUri = returnUrl | ||||
|         }; | ||||
|         return Challenge(props, scheme); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     [HttpPost("logout")] | ||||
|     [Authorize] | ||||
|     [IgnoreRolePermission] | ||||
|     [LogoutLog] | ||||
|     public Task LogoutAsync() | ||||
|     { | ||||
|         return _authService.LoginOutAsync(); | ||||
|   | ||||
| @@ -8,6 +8,8 @@ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
|  | ||||
| using System.Reflection; | ||||
|  | ||||
| namespace ThingsGateway.Admin.Application; | ||||
|  | ||||
| public class RequestAuditData | ||||
| @@ -94,5 +96,6 @@ public class RequestAuditData | ||||
|     /// 验证错误信息 | ||||
|     /// </summary> | ||||
|     public Validation Validation { get; set; } | ||||
|     public MethodInfo MethodInfo { get; set; } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -123,12 +123,12 @@ public class RequestAuditFilter : IAsyncActionFilter, IOrderedFilter | ||||
|         var desc = App.CreateLocalizerByType(controllerActionDescriptor.ControllerTypeInfo.AsType())[actionMethod.Name]; | ||||
|         //获取特性 | ||||
|  | ||||
|  | ||||
|         logData.CateGory = desc.Value;//传操作名称 | ||||
|         logData.Operation = desc.Value;//传操作名称 | ||||
|         logData.Client = client; | ||||
|         logData.Path = httpContext.Request.Path.Value;//请求地址 | ||||
|         logData.Method = httpContext.Request.Method;//请求方法 | ||||
|         logData.MethodInfo = actionMethod;//请求方法 | ||||
|  | ||||
|         logData.ControllerName = controllerActionDescriptor.ControllerName; | ||||
|         logData.ActionName = controllerActionDescriptor.ActionName; | ||||
|   | ||||
| @@ -0,0 +1,289 @@ | ||||
| using Microsoft.AspNetCore.Authentication; | ||||
| using Microsoft.AspNetCore.Authentication.OAuth; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.WebUtilities; | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
| using Microsoft.Extensions.Hosting; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Microsoft.Extensions.Options; | ||||
|  | ||||
| using System.Collections.Concurrent; | ||||
| using System.Net.Http.Headers; | ||||
| using System.Security.Claims; | ||||
| using System.Text; | ||||
| using System.Text.Encodings.Web; | ||||
| using System.Text.Json; | ||||
|  | ||||
| using ThingsGateway.Extension; | ||||
|  | ||||
| namespace ThingsGateway.Admin.Application; | ||||
|  | ||||
| /// <summary> | ||||
| /// 只适合 Demo 登录,会直接授权超管的权限 | ||||
| /// </summary> | ||||
| public class AdminOAuthHandler<TOptions>( | ||||
|    IVerificatInfoService verificatInfoService, | ||||
|    IAppService appService, | ||||
|    ISysUserService sysUserService, | ||||
|    ISysDictService configService, | ||||
|     IOptionsMonitor<TOptions> options, | ||||
|     ILoggerFactory logger, | ||||
|     IUserAgentService userAgentService, | ||||
|     UrlEncoder encoder | ||||
| ) : OAuthHandler<TOptions>(options, logger, encoder) | ||||
|     where TOptions : AdminOAuthOptions, new() | ||||
| { | ||||
|     private async Task<LoginEvent> GetLogin() | ||||
|     { | ||||
|         var sysUser = await sysUserService.GetUserByIdAsync(RoleConst.SuperAdminId).ConfigureAwait(false);//获取用户信息 | ||||
|  | ||||
|         var appConfig = await configService.GetAppConfigAsync().ConfigureAwait(false); | ||||
|  | ||||
|  | ||||
|         var expire = appConfig.LoginPolicy.VerificatExpireTime; | ||||
|  | ||||
|         var loginEvent = new LoginEvent | ||||
|         { | ||||
|             Ip = appService.RemoteIpAddress, | ||||
|             Device = appService.UserAgent?.Platform, | ||||
|             Expire = expire, | ||||
|             SysUser = sysUser, | ||||
|             VerificatId = CommonUtils.GetSingleId() | ||||
|         }; | ||||
|  | ||||
|         //获取verificat列表 | ||||
|         var tokenTimeout = loginEvent.DateTime.AddMinutes(loginEvent.Expire); | ||||
|         //生成verificat信息 | ||||
|         var verificatInfo = new VerificatInfo | ||||
|         { | ||||
|             Device = loginEvent.Device, | ||||
|             Expire = loginEvent.Expire, | ||||
|             VerificatTimeout = tokenTimeout, | ||||
|             Id = loginEvent.VerificatId, | ||||
|             UserId = loginEvent.SysUser.Id, | ||||
|             LoginIp = loginEvent.Ip, | ||||
|             LoginTime = loginEvent.DateTime | ||||
|         }; | ||||
|  | ||||
|  | ||||
|         //添加到verificat列表 | ||||
|         verificatInfoService.Add(verificatInfo); | ||||
|  | ||||
|         return loginEvent; | ||||
|     } | ||||
|     /// <summary> | ||||
|     /// 登录事件 | ||||
|     /// </summary> | ||||
|     /// <param name="loginEvent"></param> | ||||
|     /// <returns></returns> | ||||
|     private async Task UpdateUser(LoginEvent loginEvent) | ||||
|     { | ||||
|         var sysUser = loginEvent.SysUser; | ||||
|  | ||||
|         #region 登录/密码策略 | ||||
|  | ||||
|         var key = CacheConst.Cache_LoginErrorCount + sysUser.Account;//获取登录错误次数Key值 | ||||
|         App.CacheService.Remove(key);//移除登录错误次数 | ||||
|  | ||||
|         //获取用户verificat列表 | ||||
|         var userToken = verificatInfoService.GetOne(loginEvent.VerificatId); | ||||
|  | ||||
|         #endregion 登录/密码策略 | ||||
|  | ||||
|         #region 重新赋值属性,设置本次登录信息为最新的信息 | ||||
|  | ||||
|         sysUser.LastLoginIp = sysUser.LatestLoginIp; | ||||
|         sysUser.LastLoginTime = sysUser.LatestLoginTime; | ||||
|         sysUser.LatestLoginIp = loginEvent.Ip; | ||||
|         sysUser.LatestLoginTime = loginEvent.DateTime; | ||||
|  | ||||
|         #endregion 重新赋值属性,设置本次登录信息为最新的信息 | ||||
|  | ||||
|         using var db = DbContext.Db.GetConnectionScopeWithAttr<SysUser>().CopyNew(); | ||||
|         //更新用户登录信息 | ||||
|         if (await db.Updateable(sysUser).UpdateColumns(it => new | ||||
|         { | ||||
|             it.LastLoginIp, | ||||
|             it.LastLoginTime, | ||||
|             it.LatestLoginIp, | ||||
|             it.LatestLoginTime, | ||||
|         }).ExecuteCommandAsync().ConfigureAwait(false) > 0) | ||||
|             App.CacheService.HashAdd(CacheConst.Cache_SysUser, sysUser.Id.ToString(), sysUser);//更新Cache信息 | ||||
|     } | ||||
|  | ||||
|  | ||||
|     static AdminOAuthHandler() | ||||
|     { | ||||
|         Task.Factory.StartNew(Insertable, TaskCreationOptions.LongRunning); | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 日志消息队列(线程安全) | ||||
|     /// </summary> | ||||
|     protected static readonly ConcurrentQueue<SysOperateLog> _operateLogMessageQueue = new(); | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 创建访问日志 | ||||
|     /// </summary> | ||||
|     private static async Task Insertable() | ||||
|     { | ||||
|         var db = DbContext.Db.GetConnectionScopeWithAttr<SysOperateLog>().CopyNew(); | ||||
|         var appLifetime = App.RootServices!.GetService<IHostApplicationLifetime>()!; | ||||
|         while (!appLifetime.ApplicationStopping.IsCancellationRequested) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 var data = _operateLogMessageQueue.ToListWithDequeue(); // 从日志队列中获取数据 | ||||
|                 if (data.Count > 0) | ||||
|                 { | ||||
|                     await db.InsertableWithAttr(data).ExecuteCommandAsync(appLifetime.ApplicationStopping).ConfigureAwait(false);//入库 | ||||
|                 } | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 NewLife.Log.XTrace.WriteException(ex); | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 await Task.Delay(3000, appLifetime.ApplicationStopping).ConfigureAwait(false); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|  | ||||
|     } | ||||
|  | ||||
|  | ||||
|     protected override async Task<AuthenticationTicket> CreateTicketAsync( | ||||
|         ClaimsIdentity identity, | ||||
|         AuthenticationProperties properties, | ||||
|         OAuthTokenResponse tokens) | ||||
|     { | ||||
|         properties.RedirectUri = Options.HomePath; | ||||
|         properties.IsPersistent = true; | ||||
|  | ||||
|         if (!string.IsNullOrEmpty(tokens.ExpiresIn) && int.TryParse(tokens.ExpiresIn, out var result)) | ||||
|         { | ||||
|             properties.ExpiresUtc = TimeProvider.System.GetUtcNow().AddSeconds(result); | ||||
|         } | ||||
|         var user = await HandleUserInfoAsync(tokens).ConfigureAwait(false); | ||||
|  | ||||
|         var loginEvent = await GetLogin().ConfigureAwait(false); | ||||
|         await UpdateUser(loginEvent).ConfigureAwait(false); | ||||
|         identity.AddClaim(new Claim(ClaimConst.VerificatId, loginEvent.VerificatId.ToString())); | ||||
|         identity.AddClaim(new Claim(ClaimConst.UserId, RoleConst.SuperAdminId.ToString())); | ||||
|  | ||||
|         identity.AddClaim(new Claim(ClaimConst.SuperAdmin, "true")); | ||||
|         identity.AddClaim(new Claim(ClaimConst.OrgId, RoleConst.DefaultTenantId.ToString())); | ||||
|         identity.AddClaim(new Claim(ClaimConst.TenantId, RoleConst.DefaultTenantId.ToString())); | ||||
|  | ||||
|  | ||||
|         var context = new OAuthCreatingTicketContext( | ||||
|             new ClaimsPrincipal(identity), | ||||
|             properties, | ||||
|             Context, | ||||
|             Scheme, | ||||
|             Options, | ||||
|             Backchannel, | ||||
|             tokens, | ||||
|             user | ||||
|         ); | ||||
|  | ||||
|         context.RunClaimActions(); | ||||
|         await Events.CreatingTicket(context).ConfigureAwait(false); | ||||
|  | ||||
|         var httpContext = context.HttpContext; | ||||
|         UserAgent? userAgent = null; | ||||
|         var str = httpContext?.Request?.Headers?.UserAgent; | ||||
|         if (!string.IsNullOrEmpty(str)) | ||||
|         { | ||||
|             userAgent = userAgentService.Parse(str); | ||||
|         } | ||||
|  | ||||
|         var sysOperateLog = new SysOperateLog() | ||||
|         { | ||||
|             Name = this.Scheme.Name, | ||||
|             Category = LogCateGoryEnum.Login, | ||||
|             ExeStatus = true, | ||||
|             OpIp = httpContext.GetRemoteIpAddressToIPv4(), | ||||
|             OpBrowser = userAgent?.Browser, | ||||
|             OpOs = userAgent?.Platform, | ||||
|             OpTime = DateTime.Now, | ||||
|             VerificatId = loginEvent.VerificatId, | ||||
|             OpAccount = Options.GetName(user), | ||||
|  | ||||
|             ReqMethod = "OAuth", | ||||
|             ReqUrl = string.Empty, | ||||
|             ResultJson = string.Empty, | ||||
|             ClassName = nameof(AdminOAuthHandler<TOptions>), | ||||
|             MethodName = string.Empty, | ||||
|             ParamJson = string.Empty, | ||||
|         }; | ||||
|         _operateLogMessageQueue.Enqueue(sysOperateLog); | ||||
|         return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name); | ||||
|     } | ||||
|  | ||||
|     /// <summary>刷新 Token 方法</summary> | ||||
|     protected virtual async Task<OAuthTokenResponse> RefreshTokenAsync(OAuthTokenResponse oAuthToken) | ||||
|     { | ||||
|         var query = new Dictionary<string, string> | ||||
|         { | ||||
|             { "refresh_token", oAuthToken.RefreshToken }, | ||||
|             { "grant_type", "refresh_token" } | ||||
|         }; | ||||
|  | ||||
|         var request = new HttpRequestMessage(HttpMethod.Post, QueryHelpers.AddQueryString(Options.TokenEndpoint, query)); | ||||
|         request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); | ||||
|  | ||||
|         var response = await Backchannel.SendAsync(request, Context.RequestAborted).ConfigureAwait(false); | ||||
|  | ||||
|         var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); | ||||
|  | ||||
|         if (response.IsSuccessStatusCode) | ||||
|         { | ||||
|             return OAuthTokenResponse.Success(JsonDocument.Parse(content)); | ||||
|         } | ||||
|  | ||||
|         return OAuthTokenResponse.Failed(new OAuthTokenException($"OAuth token endpoint failure: {await Display(response).ConfigureAwait(false)}")); | ||||
|     } | ||||
|  | ||||
|     /// <summary>处理用户信息方法</summary> | ||||
|     protected virtual async Task<JsonElement> HandleUserInfoAsync(OAuthTokenResponse tokens) | ||||
|     { | ||||
|         var request = new HttpRequestMessage(HttpMethod.Get, BuildUserInfoUrl(tokens)); | ||||
|         request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); | ||||
|  | ||||
|         var response = await Backchannel.SendAsync(request, Context.RequestAborted).ConfigureAwait(false); | ||||
|  | ||||
|         var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); | ||||
|  | ||||
|         if (response.IsSuccessStatusCode) | ||||
|         { | ||||
|             return JsonDocument.Parse(content).RootElement; | ||||
|         } | ||||
|  | ||||
|         throw new OAuthTokenException($"OAuth user info endpoint failure: {await Display(response).ConfigureAwait(false)}"); | ||||
|     } | ||||
|  | ||||
|     /// <summary>生成用户信息请求地址方法</summary> | ||||
|     protected virtual string BuildUserInfoUrl(OAuthTokenResponse tokens) | ||||
|     { | ||||
|         return QueryHelpers.AddQueryString(Options.UserInformationEndpoint, new Dictionary<string, string> | ||||
|         { | ||||
|             { "access_token", tokens.AccessToken } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /// <summary>生成错误信息方法</summary> | ||||
|     protected static async Task<string> Display(HttpResponseMessage response) | ||||
|     { | ||||
|         var output = new StringBuilder(); | ||||
|         output.Append($"Status: {response.StatusCode}; "); | ||||
|         output.Append($"Headers: {response.Headers}; "); | ||||
|         output.Append($"Body: {await response.Content.ReadAsStringAsync().ConfigureAwait(false)};"); | ||||
|  | ||||
|         return output.ToString(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// <summary>自定义 Token 异常</summary> | ||||
| public class OAuthTokenException(string message) : Exception(message); | ||||
| @@ -0,0 +1,40 @@ | ||||
| using Microsoft.AspNetCore.Authentication.OAuth; | ||||
|  | ||||
| using System.Text.Json; | ||||
|  | ||||
| namespace ThingsGateway.Admin.Application; | ||||
|  | ||||
| /// <summary>OAuthOptions 配置类</summary> | ||||
| public abstract class AdminOAuthOptions : OAuthOptions | ||||
| { | ||||
|     /// <summary>默认构造函数</summary> | ||||
|     protected AdminOAuthOptions() | ||||
|     { | ||||
|         ConfigureClaims(); | ||||
|         this.Events.OnRemoteFailure = context => | ||||
|         { | ||||
|             var redirectUri = string.IsNullOrEmpty(HomePath) ? "/" : HomePath; | ||||
|             context.Response.Redirect(redirectUri); | ||||
|             context.HandleResponse(); | ||||
|             return Task.CompletedTask; | ||||
|         }; | ||||
|  | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /// <summary>配置 Claims 映射</summary> | ||||
|     protected virtual void ConfigureClaims() | ||||
|     { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public virtual string GetName(JsonElement element) | ||||
|     { | ||||
|         JsonElement.ObjectEnumerator target = element.EnumerateObject(); | ||||
|         return target.TryGetValue("name"); | ||||
|     } | ||||
|  | ||||
|     /// <summary>获得/设置 登陆后首页</summary> | ||||
|     public string HomePath { get; set; } = "/"; | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,77 @@ | ||||
| using Microsoft.AspNetCore.Authentication; | ||||
| using Microsoft.AspNetCore.Authentication.OAuth; | ||||
| using Microsoft.AspNetCore.WebUtilities; | ||||
|  | ||||
| using System.Net.Http.Headers; | ||||
| using System.Text.Json; | ||||
|  | ||||
| namespace ThingsGateway.Admin.Application; | ||||
|  | ||||
| public class GiteeOAuthOptions : AdminOAuthOptions | ||||
| { | ||||
|  | ||||
|     public GiteeOAuthOptions() : base() | ||||
|     { | ||||
|         this.SignInScheme = ClaimConst.Scheme; | ||||
|         this.AuthorizationEndpoint = "https://gitee.com/oauth/authorize"; | ||||
|         this.TokenEndpoint = "https://gitee.com/oauth/token"; | ||||
|         this.UserInformationEndpoint = "https://gitee.com/api/v5/user"; | ||||
|         this.HomePath = "/"; | ||||
|         this.CallbackPath = "/signin-gitee"; | ||||
|         Scope.Add("user_info"); | ||||
|         Scope.Add("projects"); | ||||
|  | ||||
|         Events.OnCreatingTicket = async context => | ||||
|         { | ||||
|             await HandlerGiteeStarredUrl(context).ConfigureAwait(false); | ||||
|         }; | ||||
|  | ||||
|         Events.OnRedirectToAuthorizationEndpoint = context => | ||||
|         { | ||||
|             //context.RedirectUri = context.RedirectUri.Replace("http%3A%2F%2F", "https%3A%2F%2F"); // 强制替换 | ||||
|             context.Response.Redirect(context.RedirectUri); | ||||
|             return Task.CompletedTask; | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     public override string GetName(JsonElement element) | ||||
|     { | ||||
|         JsonElement.ObjectEnumerator target = element.EnumerateObject(); | ||||
|         return target.TryGetValue("name"); | ||||
|     } | ||||
|  | ||||
|     private static async Task HandlerGiteeStarredUrl(OAuthCreatingTicketContext context, string repoFullName = "ThingsGateway/ThingsGateway") | ||||
|     { | ||||
|         if (string.IsNullOrWhiteSpace(context.AccessToken)) | ||||
|             throw new InvalidOperationException("Access token is missing."); | ||||
|  | ||||
|         var uri = $"https://gitee.com/api/v5/user/starred/{repoFullName}"; | ||||
|  | ||||
|         var queryString = new Dictionary<string, string> | ||||
|         { | ||||
|             { "access_token", context.AccessToken } | ||||
|         }; | ||||
|  | ||||
|         var request = new HttpRequestMessage(HttpMethod.Put, QueryHelpers.AddQueryString(uri, queryString)) | ||||
|         { | ||||
|             Headers = { Accept = { new MediaTypeWithQualityHeaderValue("application/json") } } | ||||
|         }; | ||||
|  | ||||
|         var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted).ConfigureAwait(false); | ||||
|  | ||||
|         if (!response.IsSuccessStatusCode) | ||||
|         { | ||||
|             var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); | ||||
|             throw new Exception($"Failed to star repository: {response.StatusCode}, {content}"); | ||||
|         } | ||||
|  | ||||
|  | ||||
|     } | ||||
|     protected override void ConfigureClaims() | ||||
|     { | ||||
|         ClaimActions.MapJsonKey(ClaimConst.AvatarUrl, "avatar_url"); | ||||
|         ClaimActions.MapJsonKey(ClaimConst.Account, "name"); | ||||
|  | ||||
|         base.ConfigureClaims(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,7 @@ | ||||
| namespace ThingsGateway.Admin.Application; | ||||
|  | ||||
| public class GiteeOAuthSettings | ||||
| { | ||||
|     public string ClientId { get; set; } | ||||
|     public string ClientSecret { get; set; } | ||||
| } | ||||
| @@ -0,0 +1,12 @@ | ||||
| namespace ThingsGateway.Admin.Application; | ||||
|  | ||||
| public class GiteeOAuthUser | ||||
| { | ||||
|     public string Id { get; set; } | ||||
|  | ||||
|     public string Login { get; set; } | ||||
|  | ||||
|     public string Name { get; set; } | ||||
|  | ||||
|     public string Avatar_Url { get; set; } | ||||
| } | ||||
| @@ -0,0 +1,22 @@ | ||||
| using System.Text.Json; | ||||
|  | ||||
| namespace ThingsGateway.Admin.Application; | ||||
|  | ||||
| public static class OAuthUserExtensions | ||||
| { | ||||
|     public static GiteeOAuthUser ToAuthUser(this JsonElement element) | ||||
|     { | ||||
|         GiteeOAuthUser authUser = new GiteeOAuthUser(); | ||||
|         JsonElement.ObjectEnumerator target = element.EnumerateObject(); | ||||
|         authUser.Id = target.TryGetValue("id"); | ||||
|         authUser.Login = target.TryGetValue("login"); | ||||
|         authUser.Name = target.TryGetValue("name"); | ||||
|         authUser.Avatar_Url = target.TryGetValue("avatar_url"); | ||||
|         return authUser; | ||||
|     } | ||||
|  | ||||
|     public static string TryGetValue(this JsonElement.ObjectEnumerator target, string propertyName) | ||||
|     { | ||||
|         return target.FirstOrDefault<JsonProperty>((Func<JsonProperty, bool>)(t => t.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase))).Value.ToString() ?? string.Empty; | ||||
|     } | ||||
| } | ||||
| @@ -11,11 +11,13 @@ | ||||
| using SqlSugar; | ||||
|  | ||||
| using System.Collections.Concurrent; | ||||
| using System.Reflection; | ||||
|  | ||||
| using ThingsGateway.Extension; | ||||
| using ThingsGateway.FriendlyException; | ||||
| using ThingsGateway.Logging; | ||||
| using ThingsGateway.NewLife.Json.Extension; | ||||
| using ThingsGateway.Razor; | ||||
|  | ||||
| namespace ThingsGateway.Admin.Application; | ||||
|  | ||||
| @@ -51,13 +53,18 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter | ||||
|             var client = requestAuditData.Client;//获取客户端信息 | ||||
|             var path = requestAuditData.Path;//获取操作名称 | ||||
|             var method = requestAuditData.Method;//获取方法 | ||||
|             var methodInfo = requestAuditData.MethodInfo; | ||||
|             var login = methodInfo.GetCustomAttribute(typeof(LoginLogAttribute)); | ||||
|             var logout = methodInfo.GetCustomAttribute(typeof(LogoutLogAttribute)); | ||||
|  | ||||
|             //表示访问日志 | ||||
|             if (path == "/api/auth/login" || path == "/api/auth/logout") | ||||
|             if (login != null || logout != null) | ||||
|             { | ||||
|                 //如果没有异常信息 | ||||
|                 if (requestAuditData.Exception == null) | ||||
|                 { | ||||
|                     save = await CreateVisitLog(operation, path, requestAuditData, client, flush).ConfigureAwait(false);//添加到访问日志 | ||||
|                     LogCateGoryEnum logCateGoryEnum = login != null ? LogCateGoryEnum.Login : LogCateGoryEnum.Logout; | ||||
|                     save = await CreateVisitLog(operation, path, requestAuditData, client, logCateGoryEnum, flush).ConfigureAwait(false);//添加到访问日志 | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
| @@ -151,18 +158,21 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter | ||||
|     /// <param name="path"></param> | ||||
|     /// <param name="requestAuditData">requestAuditData</param> | ||||
|     /// <param name="userAgent">客户端信息</param> | ||||
|     /// <param name="logCateGoryEnum">logCateGory</param> | ||||
|     /// <param name="flush"></param> | ||||
|     private async Task<bool> CreateVisitLog(string operation, string path, RequestAuditData requestAuditData, UserAgent userAgent, bool flush) | ||||
|     private async Task<bool> CreateVisitLog(string operation, string path, RequestAuditData requestAuditData, UserAgent userAgent, LogCateGoryEnum logCateGoryEnum, bool flush) | ||||
|     { | ||||
|         long verificatId = 0;//验证Id | ||||
|         var opAccount = "";//用户账号 | ||||
|         if (path == "/api/auth/login") | ||||
|         if (logCateGoryEnum == LogCateGoryEnum.Login) | ||||
|         { | ||||
|             //如果是登录,用户信息就从返回值里拿 | ||||
|             dynamic userInfo = requestAuditData.ReturnInformation; | ||||
|             if (requestAuditData.ReturnInformation is UnifyResult<LoginOutput> userInfo) | ||||
|             { | ||||
|                 opAccount = userInfo.Data.Account;//赋值账号 | ||||
|                 verificatId = userInfo.Data.VerificatId; | ||||
|             } | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             //如果是登录出,用户信息就从AuthorizationClaims里拿 | ||||
| @@ -173,7 +183,7 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter | ||||
|         var sysLogVisit = new SysOperateLog | ||||
|         { | ||||
|             Name = operation, | ||||
|             Category = path == "/api/auth/login" ? LogCateGoryEnum.Login : LogCateGoryEnum.Logout, | ||||
|             Category = logCateGoryEnum, | ||||
|             ExeStatus = true, | ||||
|             OpIp = requestAuditData.RemoteIPv4, | ||||
|             OpBrowser = userAgent?.Browser, | ||||
|   | ||||
| @@ -0,0 +1,24 @@ | ||||
| //------------------------------------------------------------------------------ | ||||
| //  此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充 | ||||
| //  此代码版权(除特别声明外的代码)归作者本人Diego所有 | ||||
| //  源代码使用协议遵循本仓库的开源协议及附加协议 | ||||
| //  Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway | ||||
| //  Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway | ||||
| //  使用文档:https://thingsgateway.cn/ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
|  | ||||
| using System.Security.Claims; | ||||
|  | ||||
| namespace ThingsGateway.Admin.Application; | ||||
|  | ||||
| public class HybridClaimsPrincipalService : IClaimsPrincipalService | ||||
| { | ||||
|     HybridAppService _hybridAppService; | ||||
|     public HybridClaimsPrincipalService(HybridAppService hybridAppService) | ||||
|     { | ||||
|         _hybridAppService = hybridAppService; | ||||
|     } | ||||
|     public ClaimsPrincipal? User => _hybridAppService.User; | ||||
|  | ||||
| } | ||||
| @@ -12,8 +12,6 @@ using Microsoft.AspNetCore.Authentication.Cookies; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.Extensions.Localization; | ||||
|  | ||||
| using SqlSugar; | ||||
|  | ||||
| using System.Security.Claims; | ||||
|  | ||||
| using ThingsGateway.DataEncryption; | ||||
| @@ -64,6 +62,10 @@ public class AuthService : IAuthService | ||||
|         { | ||||
|             throw Oops.Bah(appConfig.WebsitePolicy.CloseTip); | ||||
|         } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|         string? password = input.Password; | ||||
|         if (isCookie) //openApi登录不再需要解密 | ||||
|         { | ||||
| @@ -237,25 +239,20 @@ public class AuthService : IAuthService | ||||
|         var logingEvent = new LoginEvent | ||||
|         { | ||||
|             Ip = _appService.RemoteIpAddress, | ||||
|             Device = App.GetService<IAppService>().UserAgent?.Platform, | ||||
|             Device = _appService.UserAgent?.Platform, | ||||
|             Expire = expire, | ||||
|             SysUser = sysUser, | ||||
|             VerificatId = verificatId | ||||
|         }; | ||||
|         await WriteTokenToCache(loginPolicy, logingEvent).ConfigureAwait(false);//写入verificat到cache | ||||
|         await UpdateUser(logingEvent).ConfigureAwait(false); | ||||
|         if (sysUser.Account == RoleConst.SuperAdmin) | ||||
|         { | ||||
|             var modules = (await _sysResourceService.GetAllAsync().ConfigureAwait(false)).Where(a => a.Category == ResourceCategoryEnum.Module).OrderBy(a => a.SortCode);//获取模块列表 | ||||
|             sysUser.ModuleList = modules.ToList();//模块列表赋值给用户 | ||||
|         } | ||||
|  | ||||
|         //返回结果 | ||||
|         return new LoginOutput | ||||
|         { | ||||
|             VerificatId = verificatId, | ||||
|             Account = sysUser.Account, | ||||
|             Id = sysUser.Id, | ||||
|             ModuleList = sysUser.ModuleList, | ||||
|             AccessToken = accessToken, | ||||
|             RefreshToken = refreshToken | ||||
|         }; | ||||
|   | ||||
| @@ -466,7 +466,7 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService | ||||
|             var exist = await GetUserByIdAsync(input.Id).ConfigureAwait(false);//获取用户信息 | ||||
|             if (exist != null) | ||||
|             { | ||||
|                 var isSuperAdmin = exist.Account == RoleConst.SuperAdmin;//判断是否有超管 | ||||
|                 var isSuperAdmin = exist.Id == RoleConst.SuperAdminId;//判断是否有超管 | ||||
|                 if (isSuperAdmin && !UserManager.SuperAdmin) | ||||
|                     throw Oops.Bah(Localizer["CanotEditAdminUser"]); | ||||
|  | ||||
| @@ -540,7 +540,7 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService | ||||
|         await CheckApiDataScopeAsync(sysUser.OrgId, sysUser.CreateUserId).ConfigureAwait(false); | ||||
|         if (sysUser != null) | ||||
|         { | ||||
|             var isSuperAdmin = (sysUser.Account == RoleConst.SuperAdmin || input.GrantInfoList.Any(a => a == RoleConst.SuperAdminRoleId)) && !UserManager.SuperAdmin;//判断是否有超管 | ||||
|             var isSuperAdmin = (sysUser.Id == RoleConst.SuperAdminId || input.GrantInfoList.Any(a => a == RoleConst.SuperAdminRoleId)) && !UserManager.SuperAdmin;//判断是否有超管 | ||||
|             if (isSuperAdmin) | ||||
|                 throw Oops.Bah(Localizer["CanotGrantAdmin"]); | ||||
|  | ||||
| @@ -660,7 +660,7 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService | ||||
|     public async Task<bool> DeleteUserAsync(IEnumerable<long> ids) | ||||
|     { | ||||
|         using var db = GetDB(); | ||||
|         var containsSuperAdmin = await db.Queryable<SysUser>().Where(it => it.Account == RoleConst.SuperAdmin && ids.Contains(it.Id)).AnyAsync().ConfigureAwait(false);//判断是否有超管 | ||||
|         var containsSuperAdmin = await db.Queryable<SysUser>().Where(it => it.Id == RoleConst.SuperAdminId && ids.Contains(it.Id)).AnyAsync().ConfigureAwait(false);//判断是否有超管 | ||||
|         if (containsSuperAdmin) | ||||
|             throw Oops.Bah(Localizer["CanotDeleteAdminUser"]); | ||||
|         if (ids.Contains(UserManager.UserId)) | ||||
| @@ -899,7 +899,7 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService | ||||
|             var tenantId = await _sysOrgService.GetTenantIdByOrgIdAsync(sysUser.OrgId, sysOrgList).ConfigureAwait(false); | ||||
|             sysUser.TenantId = tenantId; | ||||
|  | ||||
|             if (sysUser.Account == RoleConst.SuperAdmin) | ||||
|             if (sysUser.Id == RoleConst.SuperAdminId) | ||||
|             { | ||||
|                 var modules = (await _sysResourceService.GetAllAsync().ConfigureAwait(false)).Where(a => a.Category == ResourceCategoryEnum.Module).OrderBy(a => a.SortCode); | ||||
|                 sysUser.ModuleList = modules.ToList();//模块列表赋值给用户 | ||||
|   | ||||
| @@ -40,6 +40,8 @@ public class BlazorAppContext | ||||
|     /// </summary> | ||||
|     public SysUser CurrentUser { get; private set; } | ||||
|  | ||||
|     public string? Avatar => UserManager.AvatarUrl.IsNullOrEmpty() ? CurrentUser.Avatar : UserManager.AvatarUrl; | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 用户个人菜单 | ||||
|     /// </summary> | ||||
|   | ||||
| @@ -38,7 +38,7 @@ public partial class UserCenterPage | ||||
|     protected override async Task OnParametersSetAsync() | ||||
|     { | ||||
|         SysUser = AppContext.CurrentUser.Adapt<SysUser>(); | ||||
|         SysUser.Avatar = AppContext.CurrentUser.Avatar; | ||||
|         SysUser.Avatar = AppContext.Avatar; | ||||
|         WorkbenchInfo = (await UserCenterService.GetLoginWorkbenchAsync(SysUser.Id)).Adapt<WorkbenchInfo>(); | ||||
|  | ||||
|         await base.OnParametersSetAsync(); | ||||
|   | ||||
							
								
								
									
										12
									
								
								src/Admin/ThingsGateway.AdminServer/GlobalUsings.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/Admin/ThingsGateway.AdminServer/GlobalUsings.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| // ------------------------------------------------------------------------ | ||||
| // 版权信息 | ||||
| // 版权归百小僧及百签科技(广东)有限公司所有。 | ||||
| // 所有权利保留。 | ||||
| // 官方网站:https://baiqian.com | ||||
| // | ||||
| // 许可证信息 | ||||
| // 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。 | ||||
| // 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 | ||||
| // ------------------------------------------------------------------------ | ||||
|  | ||||
| global using ThingsGateway.Admin.Application; | ||||
| @@ -18,7 +18,6 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Components; | ||||
| using Microsoft.Extensions.Localization; | ||||
|  | ||||
| using ThingsGateway.Admin.Application; | ||||
| using ThingsGateway.Admin.Razor; | ||||
| using ThingsGateway.Extension; | ||||
|  | ||||
|   | ||||
| @@ -13,8 +13,6 @@ using Microsoft.Extensions.Localization; | ||||
|  | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
|  | ||||
| using ThingsGateway.Admin.Application; | ||||
|  | ||||
| namespace ThingsGateway.AdminServer; | ||||
|  | ||||
| public partial class AccessDenied | ||||
|   | ||||
| @@ -20,11 +20,11 @@ using Microsoft.Extensions.Options; | ||||
|  | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
|  | ||||
| using ThingsGateway.Admin.Application; | ||||
| using ThingsGateway.DataEncryption; | ||||
| using ThingsGateway.NewLife.Extension; | ||||
| using ThingsGateway.Razor; | ||||
|  | ||||
|  | ||||
| namespace ThingsGateway.AdminServer; | ||||
|  | ||||
| public partial class Login | ||||
|   | ||||
| @@ -48,7 +48,7 @@ | ||||
|                         <CultureChooser /> | ||||
|                     </div> | ||||
|  | ||||
|                     <Logout ImageUrl="@(AppContext.CurrentUser.Avatar??$"{WebsiteConst.DefaultResourceUrl}images/defaultUser.svg")" ShowUserName=false DisplayName="@UserManager.UserAccount" UserName="@UserManager.VerificatId.ToString()" PrefixUserNameText=@AdminLocalizer["CurrentVerificat"]> | ||||
|                     <Logout ImageUrl="@(AppContext.Avatar??$"{WebsiteConst.DefaultResourceUrl}images/defaultUser.svg")" ShowUserName=false DisplayName="@UserManager.UserAccount" UserName="@UserManager.VerificatId.ToString()" PrefixUserNameText=@AdminLocalizer["CurrentVerificat"]> | ||||
|                         <LinkTemplate> | ||||
|                             <a href=@("/") class="h6"><i class="fa-solid fa-suitcase me-2"></i>@Localizer["系统首页"]</a> | ||||
|  | ||||
|   | ||||
| @@ -17,7 +17,6 @@ using Microsoft.Extensions.Options; | ||||
|  | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
|  | ||||
| using ThingsGateway.Admin.Application; | ||||
| using ThingsGateway.Admin.Razor; | ||||
| using ThingsGateway.Razor; | ||||
|  | ||||
| @@ -27,38 +26,6 @@ public partial class MainLayout : IDisposable | ||||
| { | ||||
|     [Inject] | ||||
|     IStringLocalizer<ThingsGateway.Razor._Imports> RazorLocalizer { get; set; } | ||||
|     private Task OnRefresh(ContextMenuItem item, object? context) | ||||
|     { | ||||
|         if (context is TabItem tabItem) | ||||
|         { | ||||
|             _tab.Refresh(tabItem); | ||||
|         } | ||||
|         return Task.CompletedTask; | ||||
|     } | ||||
|  | ||||
|     private async Task OnClose(ContextMenuItem item, object? context) | ||||
|     { | ||||
|         if (context is TabItem tabItem) | ||||
|         { | ||||
|             await _tab.RemoveTab(tabItem); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private Task OnCloseOther(ContextMenuItem item, object? context) | ||||
|     { | ||||
|         if (context is TabItem tabItem) | ||||
|         { | ||||
|             _tab.ActiveTab(tabItem); | ||||
|         } | ||||
|         _tab.CloseOtherTabs(); | ||||
|         return Task.CompletedTask; | ||||
|     } | ||||
|  | ||||
|     private Task OnCloseAll(ContextMenuItem item, object? context) | ||||
|     { | ||||
|         _tab.CloseAllTabs(); | ||||
|         return Task.CompletedTask; | ||||
|     } | ||||
|  | ||||
|     #region 全局通知 | ||||
|  | ||||
|   | ||||
| @@ -25,7 +25,6 @@ using System.Text; | ||||
| using System.Text.Encodings.Web; | ||||
| using System.Text.Unicode; | ||||
|  | ||||
| using ThingsGateway.Admin.Application; | ||||
| using ThingsGateway.Admin.Razor; | ||||
| using ThingsGateway.Extension; | ||||
| using ThingsGateway.NewLife.Caching; | ||||
| @@ -369,12 +368,6 @@ public class Startup : AppStartup | ||||
|         app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = provider }); | ||||
|         app.UseStaticFiles(); | ||||
|  | ||||
|         app.Use(async (context, next) => | ||||
|         { | ||||
|             context.Response.Headers.Append("ThingsGateway", "ThingsGateway"); | ||||
|             await next().ConfigureAwait(false); | ||||
|         }); | ||||
|  | ||||
|  | ||||
|         // 特定文件类型(文件后缀)处理 | ||||
|         var contentTypeProvider = GetFileExtensionContentTypeProvider(); | ||||
|   | ||||
| @@ -39,7 +39,7 @@ | ||||
| 		<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" /> | ||||
| 		<PackageReference Include="Mapster" Version="7.4.0" /> | ||||
| 		<PackageReference Include="MiniProfiler.AspNetCore.Mvc" Version="4.5.4" /> | ||||
| 		<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.2" /> | ||||
| 		<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.3" /> | ||||
| 	</ItemGroup> | ||||
|  | ||||
| 	<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' "> | ||||
|   | ||||
| @@ -31,6 +31,11 @@ public class ClaimConst | ||||
|     /// </summary> | ||||
|     public const string UserId = "UserId"; | ||||
|  | ||||
|     /// <summary> | ||||
|     /// AvatarUrl | ||||
|     /// </summary> | ||||
|     public const string AvatarUrl = "AvatarUrl"; | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 验证Id | ||||
|     /// </summary> | ||||
|   | ||||
| @@ -27,11 +27,17 @@ public static class UserManager | ||||
|     /// </summary> | ||||
|     public static bool SuperAdmin => (_claimsPrincipalService.User?.FindFirst(ClaimConst.SuperAdmin)?.Value).ToBoolean(false); | ||||
|  | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 当前用户账号 | ||||
|     /// </summary> | ||||
|     public static string UserAccount => _claimsPrincipalService.User?.FindFirst(ClaimConst.Account)?.Value; | ||||
|  | ||||
|     /// <summary> | ||||
|     /// AvatarUrl | ||||
|     /// </summary> | ||||
|     public static string AvatarUrl => (_claimsPrincipalService.User?.FindFirst(ClaimConst.AvatarUrl)?.Value); | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 当前用户Id | ||||
|     /// </summary> | ||||
|   | ||||
| @@ -11,7 +11,7 @@ | ||||
| 	</PropertyGroup> | ||||
| 	 | ||||
| 	<ItemGroup> | ||||
| 		<PackageReference Include="SqlSugarCore" Version="5.1.4.193" /> | ||||
| 		<PackageReference Include="SqlSugarCore" Version="5.1.4.195" /> | ||||
| 		<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.5" /> | ||||
| 	</ItemGroup> | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| <Project> | ||||
|  | ||||
| 	<PropertyGroup> | ||||
| 		<PluginVersion>10.7.0</PluginVersion> | ||||
| 		<ProPluginVersion>10.7.0</ProPluginVersion> | ||||
| 		<AuthenticationVersion>2.2.0</AuthenticationVersion> | ||||
| 		<PluginVersion>10.7.20</PluginVersion> | ||||
| 		<ProPluginVersion>10.7.20</ProPluginVersion> | ||||
| 		<AuthenticationVersion>2.4.0</AuthenticationVersion> | ||||
| 	</PropertyGroup> | ||||
|  | ||||
| 	<PropertyGroup> | ||||
|   | ||||
| @@ -10,8 +10,8 @@ | ||||
|  | ||||
| 	<ItemGroup> | ||||
| 		<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="9.0.5" /> | ||||
| 		<PackageReference Include="TouchSocket" Version="3.1.5" /> | ||||
| 		<PackageReference Include="TouchSocket.SerialPorts" Version="3.1.5" /> | ||||
| 		<PackageReference Include="TouchSocket" Version="3.1.6" /> | ||||
| 		<PackageReference Include="TouchSocket.SerialPorts" Version="3.1.6" /> | ||||
| 	</ItemGroup> | ||||
|  | ||||
| 	<ItemGroup> | ||||
|   | ||||
| @@ -12,6 +12,8 @@ using BootstrapBlazor.Components; | ||||
|  | ||||
| using Microsoft.Extensions.Localization; | ||||
|  | ||||
| using System.Text; | ||||
|  | ||||
| using ThingsGateway.NewLife; | ||||
| using ThingsGateway.NewLife.Threading; | ||||
| using ThingsGateway.Razor; | ||||
| @@ -345,8 +347,9 @@ public abstract class DriverBase : DisposableObject, IDriver | ||||
|  | ||||
|     #region 插件重写 | ||||
|  | ||||
|     public virtual bool Authentication() | ||||
|     public virtual bool GetAuthentication(out DateTime? expireTime) | ||||
|     { | ||||
|         expireTime = null; | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
| @@ -354,8 +357,24 @@ public abstract class DriverBase : DisposableObject, IDriver | ||||
|     { | ||||
|         if (PluginServiceUtil.IsEducation(GetType())) | ||||
|         { | ||||
|             ThingsGateway.Authentication.ProAuthentication.TryGetAuthorizeInfo(out var authorizeInfo); | ||||
|             return authorizeInfo.Auth ? Localizer["Authorized"] : Localizer["Unauthorized"]; | ||||
|             StringBuilder stringBuilder = new(); | ||||
|             var ret = GetAuthentication(out var expireTime); | ||||
|             if (ret) | ||||
|             { | ||||
|                 stringBuilder.Append(Localizer["Authorized"]); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 stringBuilder.Append(Localizer["Unauthorized"]); | ||||
|             } | ||||
|  | ||||
|             stringBuilder.Append("   "); | ||||
|             if (expireTime.HasValue && (DateTime.Now - expireTime.Value).TotalHours > -72) | ||||
|             { | ||||
|                 stringBuilder.Append(Localizer["ExpireTime", expireTime.Value.ToString("yyyy-MM-dd HH")]); | ||||
|             } | ||||
|  | ||||
|             return stringBuilder.ToString(); | ||||
|         } | ||||
|         return string.Empty; | ||||
|     } | ||||
|   | ||||
| @@ -43,6 +43,6 @@ namespace ThingsGateway.Gateway.Application | ||||
|         Task SetLogAsync(LogLevel? logLevel = null, bool upDataBase = true); | ||||
|         Task AfterVariablesChangedAsync(CancellationToken cancellationToken); | ||||
|         string GetAuthString(); | ||||
|         bool Authentication(); | ||||
|         bool GetAuthentication(out DateTime? expireTime); | ||||
|     } | ||||
| } | ||||
| @@ -234,6 +234,7 @@ | ||||
|   "ThingsGateway.Gateway.Application.DriverBase": { | ||||
|     "Authorized": "Authorized", | ||||
|     "Unauthorized": "Unauthorized", | ||||
|     "ExpireTime": "ExpireTime", | ||||
|     "DeviceTaskStart": "Device {0} thread started", | ||||
|     "DeviceTaskStartTimeout": "Device {0} thread start timeout {1} s", | ||||
|     "DeviceTaskStop": "Device {0} thread stopped", | ||||
|   | ||||
| @@ -240,6 +240,7 @@ | ||||
|   "ThingsGateway.Gateway.Application.DriverBase": { | ||||
|     "Authorized": "已授权", | ||||
|     "Unauthorized": "未授权", | ||||
|     "ExpireTime": "过期时间", | ||||
|     "DeviceTaskStart": "设备 {0} 线程开始", | ||||
|     "DeviceTaskStartTimeout": "设备 {0} 线程启动超时 {1} s", | ||||
|     "DeviceTaskStop": "设备 {0} 线程停止", | ||||
|   | ||||
| @@ -8,9 +8,10 @@ | ||||
| 	<ItemGroup> | ||||
| 		<PackageReference Include="Portable.BouncyCastle" Version="1.9.0" /> | ||||
| 		<PackageReference Include="Rougamo.Fody" Version="5.0.0" /> | ||||
| 		<PackageReference Include="TouchSocket.Dmtp" Version="3.1.5" /> | ||||
| 		<PackageReference Include="TouchSocket.WebApi.Swagger" Version="3.1.5" /> | ||||
| 		<PackageReference Include="TouchSocket.Dmtp" Version="3.1.6" /> | ||||
| 		<PackageReference Include="TouchSocket.WebApi.Swagger" Version="3.1.6" /> | ||||
| 		<PackageReference Include="ThingsGateway.Authentication" Version="$(AuthenticationVersion)" /> | ||||
| 		<!--<ProjectReference Include="..\..\PluginPro\ThingsGateway.Authentication\ThingsGateway.Authentication.csproj" />--> | ||||
|  | ||||
| 	</ItemGroup> | ||||
|  | ||||
|   | ||||
| @@ -19,7 +19,7 @@ | ||||
|                     <Tag Color="Color.Primary" >PRO</Tag> | ||||
|                 } | ||||
|                 <span class="ms-2 text-truncate" title=@Name>@(Name)</span> | ||||
|                 <span class=@(DeviceRuntime?.Driver?.Authentication()==true?"green--text ms-3":"red--text ms-3")>@(DeviceRuntime?.Driver?.GetAuthString())</span> | ||||
|                 <span class=@(DeviceRuntime?.Driver?.GetAuthentication(out _)==true?"green--text ms-3":"red--text ms-3")>@(DeviceRuntime?.Driver?.GetAuthString())</span> | ||||
|             </span> | ||||
|  | ||||
|             <div style="flex-grow: 1;"></div> | ||||
|   | ||||
| @@ -25,39 +25,6 @@ public partial class MainLayout | ||||
| { | ||||
|     [Inject] | ||||
|     IStringLocalizer<ThingsGateway.Razor._Imports> RazorLocalizer { get; set; } | ||||
|     private Task OnRefresh(ContextMenuItem item, object? context) | ||||
|     { | ||||
|         if (context is TabItem tabItem) | ||||
|         { | ||||
|             _tab.Refresh(tabItem); | ||||
|         } | ||||
|         return Task.CompletedTask; | ||||
|     } | ||||
|  | ||||
|     private async Task OnClose(ContextMenuItem item, object? context) | ||||
|     { | ||||
|         if (context is TabItem tabItem) | ||||
|         { | ||||
|             await _tab.RemoveTab(tabItem); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private Task OnCloseOther(ContextMenuItem item, object? context) | ||||
|     { | ||||
|         if (context is TabItem tabItem) | ||||
|         { | ||||
|             _tab.ActiveTab(tabItem); | ||||
|         } | ||||
|         _tab.CloseOtherTabs(); | ||||
|         return Task.CompletedTask; | ||||
|     } | ||||
|  | ||||
|     private Task OnCloseAll(ContextMenuItem item, object? context) | ||||
|     { | ||||
|         _tab.CloseAllTabs(); | ||||
|         return Task.CompletedTask; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private Tab _tab { get; set; } | ||||
|  | ||||
|   | ||||
| @@ -10,7 +10,7 @@ | ||||
|       <PrivateAssets>all</PrivateAssets> | ||||
|       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||
|     </PackageReference> | ||||
|     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" /> | ||||
|     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" /> | ||||
|     <PackageReference Include="xunit" Version="2.9.3" /> | ||||
|     <PackageReference Include="xunit.runner.visualstudio" Version="3.1.0"> | ||||
|       <PrivateAssets>all</PrivateAssets> | ||||
|   | ||||
| @@ -19,7 +19,7 @@ | ||||
| 		 | ||||
| 		 | ||||
| 		 | ||||
| 		<PackageReference Include="SqlSugar.TDengineCore" Version="4.18.33" GeneratePathProperty="true"> | ||||
| 		<PackageReference Include="SqlSugar.TDengineCore" Version="4.18.36" GeneratePathProperty="true"> | ||||
| 			<PrivateAssets>contentFiles;compile;build;buildMultitargeting;buildTransitive;analyzers;</PrivateAssets> | ||||
| 		</PackageReference> | ||||
|  | ||||
|   | ||||
| @@ -15,7 +15,6 @@ using Microsoft.AspNetCore.DataProtection; | ||||
| using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption; | ||||
| using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel; | ||||
| using Microsoft.AspNetCore.Hosting; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.HttpOverrides; | ||||
| using Microsoft.AspNetCore.StaticFiles; | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
| @@ -52,6 +51,7 @@ public class Startup : AppStartup | ||||
|         services.AddSingleton<IAuthRazorService, HybridAuthRazorService>(); | ||||
|         services.AddSingleton<HybridAppService>(); | ||||
|         services.AddSingleton<IAppService, HybridAppService>(a => a.GetService<HybridAppService>()); | ||||
|         services.AddSingleton<IClaimsPrincipalService, HybridClaimsPrincipalService>(); | ||||
|  | ||||
|         services.AddScoped<IPlatformService, HybridPlatformService>(); | ||||
|         services.AddScoped<IGatewayExportService, HybridGatewayExportService>(); | ||||
| @@ -351,12 +351,6 @@ public class Startup : AppStartup | ||||
|         app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = provider }); | ||||
|         app.UseStaticFiles(); | ||||
|  | ||||
|         app.Use(async (context, next) => | ||||
|         { | ||||
|             context.Response.Headers.Append("ThingsGateway", "ThingsGateway"); | ||||
|             await next().ConfigureAwait(false); | ||||
|         }); | ||||
|  | ||||
|  | ||||
|         // 特定文件类型(文件后缀)处理 | ||||
|         var contentTypeProvider = GetFileExtensionContentTypeProvider(); | ||||
|   | ||||
| @@ -64,8 +64,17 @@ public partial class Login | ||||
|         _versionString = $"v{VersionService.Version}"; | ||||
|         return base.OnInitializedAsync(); | ||||
|     } | ||||
|  | ||||
|     [Inject] | ||||
|     NavigationManager NavigationManager { get; set; } | ||||
|     private async Task LoginAsync(EditContext context) | ||||
|     { | ||||
|  | ||||
|         var websiteOptions = App.GetOptions<WebsiteOptions>()!; | ||||
|         if (websiteOptions.Demo) | ||||
|         { | ||||
|             NavigationManager.NavigateTo("/api/auth/oauth-login", forceLoad: true); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             var model = loginModel.Adapt<LoginInput>(); | ||||
|             model.Password = DESEncryption.Encrypt(model.Password); | ||||
| @@ -99,8 +108,7 @@ public partial class Login | ||||
|                 await ToastService.Error(Localizer["LoginErrorh2"], Localizer["LoginErrorc2"]); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|  | ||||
|     } | ||||
|  | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -47,7 +47,7 @@ | ||||
|                         <CultureChooser /> | ||||
|                     </div> | ||||
|  | ||||
|                     <Logout ImageUrl="@(AppContext.CurrentUser.Avatar??$"{WebsiteConst.DefaultResourceUrl}images/defaultUser.svg")" ShowUserName=false DisplayName="@UserManager.UserAccount" UserName="@UserManager.VerificatId.ToString()" PrefixUserNameText=@AdminLocalizer["CurrentVerificat"]> | ||||
|                     <Logout ImageUrl="@(AppContext.Avatar??$"{WebsiteConst.DefaultResourceUrl}images/defaultUser.svg")" ShowUserName=false DisplayName="@UserManager.UserAccount" UserName="@UserManager.VerificatId.ToString()" PrefixUserNameText=@AdminLocalizer["CurrentVerificat"]> | ||||
|                         <LinkTemplate> | ||||
|                             <a href=@("/") class="h6"><i class="fa-solid fa-suitcase me-2"></i>@Localizer["系统首页"]</a> | ||||
|  | ||||
|   | ||||
| @@ -28,39 +28,6 @@ public partial class MainLayout : IDisposable | ||||
| { | ||||
|     [Inject] | ||||
|     IStringLocalizer<ThingsGateway.Razor._Imports> RazorLocalizer { get; set; } | ||||
|     private Task OnRefresh(ContextMenuItem item, object? context) | ||||
|     { | ||||
|         if (context is TabItem tabItem) | ||||
|         { | ||||
|             _tab.Refresh(tabItem); | ||||
|         } | ||||
|         return Task.CompletedTask; | ||||
|     } | ||||
|  | ||||
|     private async Task OnClose(ContextMenuItem item, object? context) | ||||
|     { | ||||
|         if (context is TabItem tabItem) | ||||
|         { | ||||
|             await _tab.RemoveTab(tabItem); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private Task OnCloseOther(ContextMenuItem item, object? context) | ||||
|     { | ||||
|         if (context is TabItem tabItem) | ||||
|         { | ||||
|             _tab.ActiveTab(tabItem); | ||||
|         } | ||||
|         _tab.CloseOtherTabs(); | ||||
|         return Task.CompletedTask; | ||||
|     } | ||||
|  | ||||
|     private Task OnCloseAll(ContextMenuItem item, object? context) | ||||
|     { | ||||
|         _tab.CloseAllTabs(); | ||||
|         return Task.CompletedTask; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     #region 全局通知 | ||||
|  | ||||
|   | ||||
| @@ -29,6 +29,7 @@ using ThingsGateway.Admin.Application; | ||||
| using ThingsGateway.Admin.Razor; | ||||
| using ThingsGateway.Extension; | ||||
| using ThingsGateway.NewLife.Caching; | ||||
| using ThingsGateway.Razor; | ||||
|  | ||||
| namespace ThingsGateway.Server; | ||||
|  | ||||
| @@ -287,6 +288,18 @@ public class Startup : AppStartup | ||||
|             a.LoginPath = "/Account/Login/"; | ||||
|         }); | ||||
|  | ||||
|         var websiteOptions = App.GetOptions<WebsiteOptions>()!; | ||||
|         if (websiteOptions.Demo) | ||||
|         { | ||||
|             authenticationBuilder.AddOAuth<GiteeOAuthOptions, AdminOAuthHandler<GiteeOAuthOptions>>("Gitee", "Gitee", options => | ||||
|         { | ||||
|             var data = App.GetConfig<GiteeOAuthSettings>("GiteeOAuthSettings"); | ||||
|             options.ClientId = data.ClientId; | ||||
|             options.ClientSecret = data.ClientSecret; | ||||
|  | ||||
|         }); | ||||
|         } | ||||
|  | ||||
|         // 添加jwt授权 | ||||
|         authenticationBuilder.AddJwt(); | ||||
|  | ||||
| @@ -371,13 +384,6 @@ public class Startup : AppStartup | ||||
|         app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = provider }); | ||||
|         app.UseStaticFiles(); | ||||
|  | ||||
|         app.Use(async (context, next) => | ||||
|         { | ||||
|             context.Response.Headers.Append("ThingsGateway", "ThingsGateway"); | ||||
|             await next().ConfigureAwait(false); | ||||
|         }); | ||||
|  | ||||
|  | ||||
|         // 特定文件类型(文件后缀)处理 | ||||
|         var contentTypeProvider = GetFileExtensionContentTypeProvider(); | ||||
|         // contentTypeProvider.Mappings[".文件后缀"] = "MIME 类型"; | ||||
|   | ||||
| @@ -16,7 +16,7 @@ | ||||
|  | ||||
|  | ||||
| 	<ItemGroup> | ||||
| 	  <PackageReference Include="TouchSocket.Dmtp" Version="3.1.5" /> | ||||
| 	  <PackageReference Include="TouchSocket.Dmtp" Version="3.1.6" /> | ||||
| 	</ItemGroup> | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -11,8 +11,6 @@ | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
|  | ||||
| using ThingsGateway.Admin.Application; | ||||
|  | ||||
| namespace ThingsGateway.Upgrade; | ||||
|  | ||||
| /// <summary> | ||||
|   | ||||
							
								
								
									
										16
									
								
								src/Upgrade/ThingsGateway.UpgradeServer/GlobalUsings.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/Upgrade/ThingsGateway.UpgradeServer/GlobalUsings.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| // ------------------------------------------------------------------------ | ||||
| // 版权信息 | ||||
| // 版权归百小僧及百签科技(广东)有限公司所有。 | ||||
| // 所有权利保留。 | ||||
| // 官方网站:https://baiqian.com | ||||
| // | ||||
| // 许可证信息 | ||||
| // 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。 | ||||
| // 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。 | ||||
| // ------------------------------------------------------------------------ | ||||
|  | ||||
| global using Mapster; | ||||
|  | ||||
| global using ThingsGateway.Admin.Application; | ||||
| global using ThingsGateway.Admin.Razor; | ||||
| global using ThingsGateway.Razor; | ||||
| @@ -18,8 +18,6 @@ using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.Components; | ||||
| using Microsoft.Extensions.Localization; | ||||
|  | ||||
| using ThingsGateway.Admin.Application; | ||||
| using ThingsGateway.Admin.Razor; | ||||
| using ThingsGateway.Extension; | ||||
|  | ||||
| namespace ThingsGateway.UpgradeServer; | ||||
|   | ||||
| @@ -13,8 +13,6 @@ using Microsoft.Extensions.Localization; | ||||
|  | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
|  | ||||
| using ThingsGateway.Admin.Application; | ||||
|  | ||||
| namespace ThingsGateway.UpgradeServer; | ||||
|  | ||||
| public partial class AccessDenied | ||||
|   | ||||
| @@ -11,8 +11,6 @@ | ||||
| #pragma warning disable CA2007 // 考虑对等待的任务调用 ConfigureAwait | ||||
| using BootstrapBlazor.Components; | ||||
|  | ||||
| using Mapster; | ||||
|  | ||||
| using Microsoft.AspNetCore.Components; | ||||
| using Microsoft.AspNetCore.Components.Forms; | ||||
| using Microsoft.Extensions.Localization; | ||||
| @@ -20,10 +18,9 @@ using Microsoft.Extensions.Options; | ||||
|  | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
|  | ||||
| using ThingsGateway.Admin.Application; | ||||
| using ThingsGateway.DataEncryption; | ||||
| using ThingsGateway.NewLife.Extension; | ||||
| using ThingsGateway.Razor; | ||||
|  | ||||
| using TouchSocket.Core; | ||||
|  | ||||
| namespace ThingsGateway.UpgradeServer; | ||||
|  | ||||
|   | ||||
| @@ -48,7 +48,7 @@ | ||||
|                         <CultureChooser /> | ||||
|                     </div> | ||||
|  | ||||
|                     <Logout ImageUrl="@(AppContext.CurrentUser.Avatar??$"{WebsiteConst.DefaultResourceUrl}images/defaultUser.svg")" ShowUserName=false DisplayName="@UserManager.UserAccount" UserName="@UserManager.VerificatId.ToString()" PrefixUserNameText=@AdminLocalizer["CurrentVerificat"]> | ||||
|                     <Logout ImageUrl="@(AppContext.Avatar??$"{WebsiteConst.DefaultResourceUrl}images/defaultUser.svg")" ShowUserName=false DisplayName="@UserManager.UserAccount" UserName="@UserManager.VerificatId.ToString()" PrefixUserNameText=@AdminLocalizer["CurrentVerificat"]> | ||||
|                         <LinkTemplate> | ||||
|                             <a href=@("/") class="h6"><i class="fa-solid fa-suitcase me-2"></i>@Localizer["系统首页"]</a> | ||||
|  | ||||
|   | ||||
| @@ -17,49 +17,12 @@ using Microsoft.Extensions.Options; | ||||
|  | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
|  | ||||
| using ThingsGateway.Admin.Application; | ||||
| using ThingsGateway.Admin.Razor; | ||||
| using ThingsGateway.Razor; | ||||
|  | ||||
| namespace ThingsGateway.UpgradeServer; | ||||
|  | ||||
| public partial class MainLayout : IDisposable | ||||
| { | ||||
|     [Inject] | ||||
|     IStringLocalizer<ThingsGateway.Razor._Imports> RazorLocalizer { get; set; } | ||||
|     private Task OnRefresh(ContextMenuItem item, object? context) | ||||
|     { | ||||
|         if (context is TabItem tabItem) | ||||
|         { | ||||
|             _tab.Refresh(tabItem); | ||||
|         } | ||||
|         return Task.CompletedTask; | ||||
|     } | ||||
|  | ||||
|     private async Task OnClose(ContextMenuItem item, object? context) | ||||
|     { | ||||
|         if (context is TabItem tabItem) | ||||
|         { | ||||
|             await _tab.RemoveTab(tabItem); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private Task OnCloseOther(ContextMenuItem item, object? context) | ||||
|     { | ||||
|         if (context is TabItem tabItem) | ||||
|         { | ||||
|             _tab.ActiveTab(tabItem); | ||||
|         } | ||||
|         _tab.CloseOtherTabs(); | ||||
|         return Task.CompletedTask; | ||||
|     } | ||||
|  | ||||
|     private Task OnCloseAll(ContextMenuItem item, object? context) | ||||
|     { | ||||
|         _tab.CloseAllTabs(); | ||||
|         return Task.CompletedTask; | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|     #region 全局通知 | ||||
|   | ||||
| @@ -25,8 +25,6 @@ using System.Text; | ||||
| using System.Text.Encodings.Web; | ||||
| using System.Text.Unicode; | ||||
|  | ||||
| using ThingsGateway.Admin.Application; | ||||
| using ThingsGateway.Admin.Razor; | ||||
| using ThingsGateway.Extension; | ||||
|  | ||||
| namespace ThingsGateway.UpgradeServer; | ||||
| @@ -366,12 +364,6 @@ public class Startup : AppStartup | ||||
|         app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = provider }); | ||||
|         app.UseStaticFiles(); | ||||
|  | ||||
|         app.Use(async (context, next) => | ||||
|         { | ||||
|             context.Response.Headers.Append("ThingsGateway", "ThingsGateway"); | ||||
|             await next().ConfigureAwait(false); | ||||
|         }); | ||||
|  | ||||
|  | ||||
|         // 特定文件类型(文件后缀)处理 | ||||
|         var contentTypeProvider = GetFileExtensionContentTypeProvider(); | ||||
|   | ||||
| @@ -10,8 +10,6 @@ | ||||
|  | ||||
| using System.Reflection; | ||||
|  | ||||
| using ThingsGateway.Admin.Application; | ||||
|  | ||||
| namespace ThingsGateway.Upgrade; | ||||
|  | ||||
| [AppStartup(100000000)] | ||||
|   | ||||
| @@ -8,7 +8,6 @@ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
|  | ||||
| using ThingsGateway.Admin.Application; | ||||
| namespace ThingsGateway.Upgrade; | ||||
|  | ||||
| /// <summary> | ||||
|   | ||||
| @@ -8,8 +8,6 @@ | ||||
| //  QQ群:605534569 | ||||
| //------------------------------------------------------------------------------ | ||||
|  | ||||
| using ThingsGateway.Admin.Application; | ||||
|  | ||||
| namespace ThingsGateway.Upgrade; | ||||
|  | ||||
| /// <summary> | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <Project> | ||||
|   <PropertyGroup> | ||||
|     <Version>10.7.0</Version> | ||||
|     <Version>10.7.20</Version> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user