diff --git a/Extensions/StreamExtensions.cs b/Extensions/StreamExtensions.cs new file mode 100644 index 0000000..a4c55fd --- /dev/null +++ b/Extensions/StreamExtensions.cs @@ -0,0 +1,100 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using xCommons.Extensions; + +namespace xIdentityService.Extensions +{ + public static partial class StreamExtensions + { + /// + /// Converts an Stream to Async Enumerable ... + /// + /// + /// + /// + /// + public static async IAsyncEnumerable ToAsyncEnumerable( + this Stream stream, + int bufferSize = 8192, + [System.Runtime.CompilerServices.EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + var buffer = new byte[bufferSize]; + while (true) + { + // + int read = await stream + .ReadAsync( + buffer, + 0, + buffer.Length, + cancellationToken + ); + + // + if (read == 0) + { + yield break; + } + + // + yield return buffer.Take(read).ToArray(); + } + } + + /// + /// Converts an Stream to Async Enumerable ... + /// + /// + /// + /// + /// + public static async IAsyncEnumerable ToAsyncEnumerable( + this Stream stream, + int bufferSize = 8192, + [System.Runtime.CompilerServices.EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + var buffer = new byte[bufferSize]; + while (true) + { + // + int read = await stream + .ReadAsync( + buffer, + 0, + buffer.Length, + cancellationToken + ); + if (read == 0) + { + yield break; + } + + // + T chunk = default(T); + var readedBuffer = buffer.Take(read).ToArray(); + var str = readedBuffer.FromBytes(); + if (str.IsNullOrEmpty()) + { + yield return chunk; + } + try + { + chunk = str.FromJSON(); + } + catch + { } + + // + yield return chunk; + } + } + } +} \ No newline at end of file diff --git a/Interfaces/IXBaseIdentityHttpProvider.cs b/Interfaces/IXBaseIdentityHttpProvider.cs new file mode 100644 index 0000000..9971946 --- /dev/null +++ b/Interfaces/IXBaseIdentityHttpProvider.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using RestSharp; +using xCommons.Providers; +using xHttpService.Constants; +using xIdentityModels.Constants; +using xIdentityModels.Models; +using xIdentityService.Models; + +namespace xIdentityService.Interfaces +{ + /// + /// an interface for Identity Provided Http Based Services ... + /// + public interface IXBaseIdentityHttpProvider + { + /// + /// Client Base Url ... + /// + /// + string BaseUrl { get; } + + /// + /// Logger ... + /// + /// + ILogger Logger { get; } + + /// + /// Powered Value ... + /// + /// + string XPoweredValue { get; } + + /// + /// Default Client Timeout ... + /// + /// + int DefaultClientTimeout { get; } + + /// + /// Default Buffering Chunk Size ... + /// + /// + int DefaultBufferingChunckSize { get; } + + /// + /// Validation Provider ... + /// + /// + XValidationProvider ValidationProvider { get; } + + /// + /// Get an Instance of Http Client + /// + /// + HttpClient GetHttpClient(XTokenResponse tokens = null); + HttpClient GetHttpClient(XUserClaimsInfoDto userInfo = null); + + /// + /// Get an Instance of RestClient + /// + /// + RestClient GetRestClient(XTokenResponse tokens = null); + RestClient GetRestClient(XUserClaimsInfoDto userInfo = null); + + /// + /// Get an Instance of RestRequest + /// + /// + /// + /// + /// + /// + /// + RestRequest GetRestRequet( + Enum endpoint, + bool addXPoweredValue = true, + IDictionary @params = null, + IDictionary headers = null, + IDictionary queryStrings = null + ); + + /// + /// Handle Call Specific Request and retrieve Response + /// + /// + /// + /// + /// + /// + /// + /// + Task RunRestRequest( + RestClient client, + RestRequest request, + XHttpMethod method, + bool supportRefreshingTokens = true, + CancellationToken cancellationToken = default + ); + + /// + /// Access a Get Request Stream as AsyncEnumerable ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + IAsyncEnumerable StreamData( + Enum endpoint, + bool addXPoweredValue = true, + XTokenResponse tokens = null, + IDictionary @params = null, + IDictionary headers = null, + IDictionary queryStrings = null, + CancellationToken cancellationToken = default + ); + + /// + /// Handle Call Specified Request and Retrieve XBaseRestResponse ... + /// + /// + /// + /// + /// + /// + /// + /// an instance of XBaseRestResponse + Task> RunRestRequestByResponse( + RestClient client, + RestRequest request, + XHttpMethod method, + bool supportRefreshingTokens = true, + CancellationToken cancellationToken = default + ); + + /// + /// Get User SelectBy Param + /// + /// + /// + /// + /// + string GetUserSelectByParam( + XActionRequest model, + bool forceNotNull = true, + ICollection excludes = null + ); + + /// + /// Converts UserInfo to TokenResponse model ... + /// + /// + /// + XTokenResponse ToTokenResponse(XUserClaimsInfoDto userInfo = null); + + /// + /// Prepare Url Address of Specified Enum by Filling Params + /// and Query Strings Attach ... + /// + /// + /// + /// + /// + string PrepareUrl( + Enum endpoint, + IDictionary @params = null, + IDictionary queryStrings = null + ); + + /// + /// Prepare Url Address by Filling Params + /// and Query Strings Attach ... + /// + /// + /// + /// + /// + string PrepareUrl( + string url, + IDictionary @params = null, + IDictionary queryStrings = null + ); + } +} \ No newline at end of file diff --git a/Providers/XBaseIdentityHttpProvider.cs b/Providers/XBaseIdentityHttpProvider.cs new file mode 100644 index 0000000..c53696c --- /dev/null +++ b/Providers/XBaseIdentityHttpProvider.cs @@ -0,0 +1,871 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using RestSharp; +using RestSharp.Authenticators; +using xCommons.Constants; +using xCommons.Extensions; +using xCommons.Providers; +using xExceptions.Constants; +using xHttpService.Constants; +using xIdentityModels.Constants; +using xIdentityModels.Extensions; +using xIdentityModels.Models; +using xIdentityService.Constants; +using xIdentityService.Interfaces; +using xIdentityService.Models; +using xIdentityService.Extensions; + +namespace xIdentityService.Providers +{ + /// + /// an abstraction Implementation for Identity Provided Http Based Services ... + /// + public abstract class XBaseIdentityHttpProvider : IXBaseIdentityHttpProvider + { + /// + /// Client Base Url ... + /// + /// + public string BaseUrl { get; } + + /// + /// Logger ... + /// + /// + public ILogger Logger { get; } + + /// + /// Powered Value ... + /// + /// + public string XPoweredValue { get; } + + /// + /// Default Client Timeout ... + /// + /// + public int DefaultClientTimeout { get; } + + /// + /// Default Buffering Chunk Size ... + /// + /// + public int DefaultBufferingChunckSize { get; } + + /// + /// Validation Provider ... + /// + /// + public XValidationProvider ValidationProvider { get; } + + /// + /// Token Provider ... + /// + private readonly IXTokenProvider tokenProvider; + + /// + /// Identity Provider ... + /// + private readonly IXIdentityProvider identityProvider; + + public XBaseIdentityHttpProvider( + string baseUrl, + ILogger logger, + string poweredValue, + int defaultClientTimeout, + IXTokenProvider tokenProvider, + IXIdentityProvider identityProvider, + XValidationProvider validationProvider, + int defaultBufferingChunckSize = 8192 + ) + { + // + Logger = logger; + BaseUrl = baseUrl; + XPoweredValue = poweredValue; + this.tokenProvider = tokenProvider; + ValidationProvider = validationProvider; + this.identityProvider = identityProvider; + DefaultClientTimeout = defaultClientTimeout; + DefaultBufferingChunckSize = defaultBufferingChunckSize; + } + + // + #region Tools ... + /// + /// Get an Instance of Http Client + /// + /// + public HttpClient GetHttpClient(XTokenResponse tokens = null) + { + // + HttpClient httpClient = null; + + // + // httpClient = new HttpClient (); + var httpClientHandler = new HttpClientHandler() + { + ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => + { + // + Logger.LogInformation( + $"XSSL Handler: {Environment.NewLine}, sender: {sender}, {Environment.NewLine} cert: {cert}, {Environment.NewLine} chain: {chain}, {Environment.NewLine} sslPolicyErrors: {sslPolicyErrors}" + ); + + // + return true; + }, + ClientCertificateOptions = ClientCertificateOption.Manual, + }; + httpClient = new HttpClient(httpClientHandler); + + // + // Set Authenticator ... + if (!tokens.IsNull() && + !tokens.AccessToken.IsNullOrEmpty()) + { + // + httpClient.DefaultRequestHeaders + .Add( + XAuthorization.AccessToken, + $"{XAuthentication.BEARER} {tokens.AccessToken}"); + } + + // + // Refresh Token ... + if (!tokens.IsNull() && + !tokens.RefreshToken.IsNullOrEmpty()) + { + // + httpClient.DefaultRequestHeaders + .Add( + XAuthorization.RefreshToken, + tokens.RefreshToken); + } + + // + // ExpiresAt ... + if (!tokens.IsNull() && + tokens.ExpiresAt > 0) + { + // + httpClient.DefaultRequestHeaders + .Add( + XAuthorization.ExpiresAt, + tokens.ExpiresAt.AsHttpParamString()); + } + + // + // Set Default Timeout ... + httpClient.Timeout = TimeSpan.FromSeconds(DefaultClientTimeout); + + // + return httpClient; + } + public HttpClient GetHttpClient(XUserClaimsInfoDto userInfo = null) + { + // + XTokenResponse tokens = ToTokenResponse(userInfo); + + // + return GetHttpClient(tokens); + } + + /// + /// Get an Instance of RestClient + /// + /// + public RestClient GetRestClient(XTokenResponse tokens = null) + { + // + var client = new RestClient(BaseUrl); + + // + // Set Authenticator ... + if (!tokens.IsNull() && + !tokens.AccessToken.IsNullOrEmpty()) + { + client.Authenticator = new JwtAuthenticator(tokens.AccessToken); + } + + // + // Refresh Token ... + if (!tokens.IsNull() && + !tokens.RefreshToken.IsNullOrEmpty()) + { + client.AddDefaultHeader(XAuthorization.RefreshToken, tokens.RefreshToken); + } + + // + // ExpiresAt ... + if (!tokens.IsNull() && + tokens.ExpiresAt > 0) + { + client.AddDefaultHeader(XAuthorization.ExpiresAt, tokens.ExpiresAt.AsHttpParamString()); + } + + // + // Set Default Timeout ... + client.Timeout = DefaultClientTimeout; + + // + // SSL Validation Handler ... + client.RemoteCertificateValidationCallback = (sender, cert, chain, sslPolicyErrors) => + { + // + Logger.LogInformation( + $"XSSL Handler: {Environment.NewLine}, sender: {sender}, {Environment.NewLine} cert: {cert}, {Environment.NewLine} chain: {chain}, {Environment.NewLine} sslPolicyErrors: {sslPolicyErrors}" + ); + + // + return true; + }; + + // + return client; + } + public RestClient GetRestClient(XUserClaimsInfoDto userInfo = null) + { + // + XTokenResponse tokens = ToTokenResponse(userInfo); + + // + return GetRestClient(tokens); + } + + /// + /// Get an Instance of RestRequest + /// + /// + /// + /// + /// + /// + /// + public RestRequest GetRestRequet( + Enum endpoint, + bool addXPoweredValue = true, + IDictionary @params = null, + IDictionary headers = null, + IDictionary queryStrings = null + ) + { + // + var url = PrepareUrl( + endpoint, + @params, + queryStrings + ); + + // + // Create request ... + var request = new RestRequest(url, DataFormat.Json); + + // + if (addXPoweredValue) + { + request.AddHeader(XAuthorization.XPoweredBy, XPoweredValue); + } + + // + // Add Headers to request ... + if (!headers.IsNull() && headers.Count > 0) + { + // + var enumerator = headers.GetEnumerator(); + while (enumerator.MoveNext()) + { + // + var item = enumerator.Current; + request.AddHeader(item.Key, item.Value); + } + } + + // + return request; + } + + /// + /// Handle Call Specific Request and retrieve Response + /// + /// + /// + /// + /// + /// + /// + /// + public async Task RunRestRequest( + RestClient client, + RestRequest request, + XHttpMethod method, + bool supportRefreshingTokens = true, + CancellationToken cancellationToken = default + ) + { + // + var response = await Task.Run(async () => + { + // + IRestResponse resp = null; + + // + // Call Endpoint Service using Specified Method ... + switch (method) + { + // + // Get ... + case XHttpMethod.GET: + resp = client.Get(request); + break; + + // + // Put ... + case XHttpMethod.PUT: + resp = client.Put(request); + break; + + // + // Post ... + case XHttpMethod.POST: + resp = client.Post(request); + break; + + // + // Delete ... + case XHttpMethod.DELETE: + resp = client.Delete(request); + break; + } + + // + if (!resp.IsSuccessful) + { + // + // Log Exception ... + Logger.LogError($"Error: {resp.ErrorMessage}"); + Logger.LogError($"Exception: {resp.ErrorException.ToJSON()}"); + if (!resp.ErrorMessage.IsNullOrEmpty() && + resp.ErrorMessage.Contains("timed out")) + { + XException.Timeout.Throw(); + } + + // + // TODO: Check UnAuthorized Status here for + // Refresh Tokens or Issue Correct Exception ... + if (supportRefreshingTokens && + resp.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + // + var isRefreshed = request.Parameters.Any(rp => + rp.Name == XParam.XIsRefreshed.GetStringValue() && + rp.Value.ToString().ToNormalString() == true.ToJSON()); + + // + Logger.LogInformation($"isRefreshed: {isRefreshed}"); + + // + // Get Access Token ... + XTokenResponse tokens = null; + var accessToken = request.Parameters.FirstOrDefault(rp => + rp.Name.ToNormalString() == XAuthorization.Header.ToNormalString()).Value.ToString(); + if (!accessToken.IsNullOrEmpty()) + { + // + accessToken = accessToken.Replace(XAuthorization.TokenIdentifier, ""); + + // + if (!accessToken.IsNullOrEmpty()) + { + tokens = await tokenProvider.RetrieveToken(accessToken); + } + } + + // + // Refresh Token and then re do request ... + if (!isRefreshed && + !accessToken.IsNullOrEmpty() && + !tokens.IsNull() && + tokens.IsRefreshable()) + { + // + request.AddHeader(XParam.XIsRefreshed.GetStringValue(), true.ToJSON()); + + // + // Refreshe Tokens ... + var refreshedTokens = await identityProvider + .RefreshTokens(tokens); + + // + // Renew Rest Client with Refreshed Tokens ... + var timeout = client.Timeout; + client = GetRestClient(refreshedTokens); + client.Timeout = Timeout.Infinite; + + // + // Do Request Again using renew Client ... + return await RunRestRequest( + client: client, + request: request, + method: method); + } + } + + // + var ex = resp.Content.ToXError(); + if (!ex.IsNull()) + { + throw ex.ToException(); + } + + // + // UnAuthorized ... + if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + XException.NotAuthorized.Throw(); + } + + // + // NotFound ... + if (resp.StatusCode == System.Net.HttpStatusCode.NotFound) + { + XException.NotFound.Throw(); + } + + // + XException.ActionFailed.Throw(); + } + + // + var result = resp.Content.FromJSON(); + return result; + }, + cancellationToken: cancellationToken); + + // + return response; + } + + /// + /// Access a Get Request Stream as AsyncEnumerable ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async IAsyncEnumerable StreamData( + Enum endpoint, + bool addXPoweredValue = true, + XTokenResponse tokens = null, + IDictionary @params = null, + IDictionary headers = null, + IDictionary queryStrings = null, + [System.Runtime.CompilerServices.EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + var url = PrepareUrl( + endpoint, + @params, + queryStrings + ); + var client = GetHttpClient(tokens); + if (addXPoweredValue) + { + // + client.DefaultRequestHeaders + .Add( + XAuthorization.XPoweredBy, + XPoweredValue); + } + + // + // For SSE, you might want to set the Accept header, though not strictly necessary for just streaming + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept + .Add( + new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("text/event-stream")); + + // + using (var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken)) + { + // + response.EnsureSuccessStatusCode(); + var stream = await response.Content.ReadAsStreamAsync(); + await foreach (var item in stream.ToAsyncEnumerable(cancellationToken: cancellationToken)) + { + yield return item; + } + } + } + + /// + /// Handle Call Specified Request and Retrieve XBaseRestResponse ... + /// + /// + /// + /// + /// + /// + /// + /// an instance of XBaseRestResponse + public async Task> RunRestRequestByResponse( + RestClient client, + RestRequest request, + XHttpMethod method, + bool supportRefreshingTokens = true, + CancellationToken cancellationToken = default + ) + { + // + var response = await Task.Run(async () => + { + // + IRestResponse resp = null; + + // + // Call Endpoint Service using Specified Method ... + switch (method) + { + // + // Get ... + case XHttpMethod.GET: + resp = client.Get(request); + break; + + // + // Put ... + case XHttpMethod.PUT: + resp = client.Put(request); + break; + + // + // Post ... + case XHttpMethod.POST: + resp = client.Post(request); + break; + + // + // Delete ... + case XHttpMethod.DELETE: + resp = client.Delete(request); + break; + } + + // + if (!resp.IsSuccessful) + { + // + // Log Exception ... + Logger.LogError($"Error: {resp.ErrorMessage}"); + Logger.LogError($"Exception: {resp.ErrorException.ToJSON()}"); + if (!resp.ErrorMessage.IsNullOrEmpty() && + resp.ErrorMessage.Contains("timed out")) + { + XException.Timeout.Throw(); + } + + // + // TODO: Check UnAuthorized Status here for + // Refresh Tokens or Issue Correct Exception ... + if (supportRefreshingTokens && + resp.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + // + var isRefreshed = request.Parameters.Any(rp => + rp.Name == XParam.XIsRefreshed.GetStringValue() && + rp.Value.ToString().ToNormalString() == true.ToJSON()); + + // + Logger.LogInformation($"isRefreshed: {isRefreshed}"); + + // + // Get Access Token ... + XTokenResponse tokens = null; + var accessToken = request.Parameters.FirstOrDefault(rp => + rp.Name.ToNormalString() == XAuthorization.Header.ToNormalString()).Value.ToString(); + if (!accessToken.IsNullOrEmpty()) + { + // + accessToken = accessToken.Replace(XAuthorization.TokenIdentifier, ""); + + // + if (!accessToken.IsNullOrEmpty()) + { + tokens = await tokenProvider.RetrieveToken(accessToken); + } + } + + // + // Refresh Token and then re do request ... + if (!isRefreshed && + !accessToken.IsNullOrEmpty() && + !tokens.IsNull() && + tokens.IsRefreshable()) + { + // + request.AddHeader(XParam.XIsRefreshed.GetStringValue(), true.ToJSON()); + + // + // Refreshe Tokens ... + var refreshedTokens = await identityProvider + .RefreshTokens(tokens); + + // + // Renew Rest Client with Refreshed Tokens ... + var timeout = client.Timeout; + client = GetRestClient(refreshedTokens); + client.Timeout = Timeout.Infinite; + + // + // Do Request Again using renew Client ... + return await RunRestRequestByResponse( + client: client, + request: request, + method: method + ); + } + } + + // + var ex = resp.Content.ToXError(); + if (!ex.IsNull()) + { + throw ex.ToException(); + } + + // + // UnAuthorized ... + if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + XException.NotAuthorized.Throw(); + } + + // + // NotFound ... + if (resp.StatusCode == System.Net.HttpStatusCode.NotFound) + { + XException.NotFound.Throw(); + } + + // + XException.ActionFailed.Throw(); + } + + // + var result = new XBaseRestResponse + { + Response = resp, + Model = resp.Content.FromJSON() + }; + + // + return result; + }, + cancellationToken: cancellationToken); + + // + return response; + } + + /// + /// Get User SelectBy Param + /// + /// + /// + /// + /// + public string GetUserSelectByParam( + XActionRequest model, + bool forceNotNull = true, + ICollection excludes = null + ) + { + // + // Validate Args ... + ValidationProvider.NotNull(model); + + // + var id = model.UserId; + var userName = model.UserName; + var email = model.Email; + var phoneNumber = model.MobileNumber; + + // + if (excludes != null) + { + // + if (excludes.Contains(XUserSelectBy.ID)) + { + id = null; + } + if (excludes.Contains(XUserSelectBy.Email)) + { + email = null; + } + if (excludes.Contains(XUserSelectBy.MobileNumber)) + { + phoneNumber = null; + } + if (excludes.Contains(XUserSelectBy.Username)) + { + userName = null; + } + } + + var selectByParam = + id.IsNullOrEmpty() ? + userName.IsNullOrEmpty() ? + email.IsNullOrEmpty() ? + phoneNumber.IsNullOrEmpty() ? null : phoneNumber : email : userName : id; + + // + // Check result ... + if (forceNotNull && + selectByParam.IsNullOrEmpty()) + { + XException.InvalidData.Throw(); + } + + // + return selectByParam; + } + + /// + /// Prepare Url Address of Specified Enum by Filling Params + /// and Query Strings Attach ... + /// + /// + /// + /// + /// + public string PrepareUrl( + Enum endpoint, + IDictionary @params = null, + IDictionary queryStrings = null + ) + { + // + // Retrieving Url Template ... + var url = endpoint.GetStringValue(); + + // + // Filling Parmas and Query Strings of Url ... + url = PrepareUrl( + url, + @params, + queryStrings + ); + + // + return url; + } + + /// + /// Prepare Url Address by Filling Params + /// and Query Strings Attach ... + /// + /// + /// + /// + /// + public string PrepareUrl( + string url, + IDictionary @params = null, + IDictionary queryStrings = null + ) + { + // + // Add Route Payloads ... + if (!@params.IsNull() && @params.Count > 0) + { + // + var enumerator = @params.GetEnumerator(); + while (enumerator.MoveNext()) + { + // + var item = enumerator.Current; + + // + url = url.Replace(item.Key, item.Value.ToUrlEncoded()); + } + } + + // + // Add Query Strings ... + if (!queryStrings.IsNull() && queryStrings.Count > 0) + { + // + url = $"{url}?"; + var enumerator = queryStrings.GetEnumerator(); + while (enumerator.MoveNext()) + { + // + var item = enumerator.Current; + + // + url += $"{item.Key}={item.Value.ToUrlEncoded()}&"; + } + + // + // remove lates & ... + if (url.EndsWith("&")) + { + url = url.Substring(0, url.Length - 1); + } + } + + // + return url; + } + + /// + /// Converts UserInfo to TokenResponse model ... + /// + /// + /// + public XTokenResponse ToTokenResponse(XUserClaimsInfoDto userInfo = null) + { + // + XTokenResponse result = null; + + // + if (!userInfo.IsNull()) + { + // + result = new XTokenResponse + { + AccessToken = userInfo.AccessToken.IsNullOrEmpty() + ? "" + : userInfo.AccessToken, + RefreshToken = userInfo.RefreshToken.IsNullOrEmpty() + ? "" + : userInfo.RefreshToken, + ExpiresAt = userInfo.ExpiresAt, + }; + + // + if (userInfo.AccessToken.IsNullOrEmpty()) + { + result = null; + } + } + + // + return result; + } + #endregion + } +} \ No newline at end of file diff --git a/xIdentityService.csproj b/xIdentityService.csproj index f63617b..8430a0c 100644 --- a/xIdentityService.csproj +++ b/xIdentityService.csproj @@ -1,12 +1,12 @@ - - netstandard2.0 - xDashboard.xIdentityService 1.0.0 + 8.0 Hadi Khazaee Asl SaherElm IT Center + netstandard2.0 + xDashboard.xIdentityService provide an interface to connect xDashboard IdentityServer and do OAuth Actions to xDashboard project. @@ -23,11 +23,12 @@ - - + - - + + + + @@ -35,5 +36,4 @@ - \ No newline at end of file