From ac0ebe601e0a16329dfb8291270ecc33cbcd5a7e Mon Sep 17 00:00:00 2001 From: Hadi Khazaee Asl Date: Mon, 5 Oct 2026 17:37:28 +0330 Subject: [PATCH] last ... --- Configurations/XApiKeyConfiguration.cs | 2 +- Constants/ConfigurationNodeNames.cs | 1 + DI/XDIHelperExtension.cs | 64 +- DbContext/XIdentityDbContext.cs | 2 +- Extensions/IdentityExtensions.cs | 4 +- Helpers/XApiKeyHelper.cs | 68 ++ Helpers/XIdentityHelper.cs | 16 +- Interfaces/IXApplicationProvider.cs | 231 +++++- Providers/XApplicationProvider.cs | 280 +++++++ .../XApplicationProvider+ApiKey.cs | 773 ++++++++++++++++++ .../XApplicationProvider+ApiKeyUsage.cs | 531 ++++++++++++ .../XApplicationProvider+Application.cs | 624 ++++++++++++++ .../XApplicationProvider+Custom.cs | 155 ++++ .../XApplicationProvider+Enricher.cs | 160 ++++ appsettings.Development.json | 7 + 15 files changed, 2881 insertions(+), 37 deletions(-) create mode 100644 Helpers/XApiKeyHelper.cs create mode 100644 Providers/XApplicationProvider.cs create mode 100644 Providers/XApplicationProvider/XApplicationProvider+ApiKey.cs create mode 100644 Providers/XApplicationProvider/XApplicationProvider+ApiKeyUsage.cs create mode 100644 Providers/XApplicationProvider/XApplicationProvider+Application.cs create mode 100644 Providers/XApplicationProvider/XApplicationProvider+Custom.cs create mode 100644 Providers/XApplicationProvider/XApplicationProvider+Enricher.cs diff --git a/Configurations/XApiKeyConfiguration.cs b/Configurations/XApiKeyConfiguration.cs index f7dae46..c67b3f7 100644 --- a/Configurations/XApiKeyConfiguration.cs +++ b/Configurations/XApiKeyConfiguration.cs @@ -23,7 +23,7 @@ namespace xIds.Configurations /// /// Enable audit logging /// - public bool EnableAuditLog { get; set; } = true; + public bool EnableAuditLog { get; set; } = false; /// /// Default rate limit (requests per minute) diff --git a/Constants/ConfigurationNodeNames.cs b/Constants/ConfigurationNodeNames.cs index 9a350de..f317b30 100644 --- a/Constants/ConfigurationNodeNames.cs +++ b/Constants/ConfigurationNodeNames.cs @@ -4,6 +4,7 @@ namespace xIds.Constants { public const string DB_SEED_NODE_NAME = "DbSeeder"; public const string PROVIDER_NODE_NAME = "DbProvider"; + public const string APIKEY_NODE_NAME = "ApiKeyConfiguration"; public const string IDENTITY_NODE_NAME = "IdentityConfiguration"; public const string DATA_SERVICE_NODE_NAME = "DataServiceConfiguration"; public const string IDENTITY_RESOURCE_NODE_NAME = "IdentityResourceConfiguration"; diff --git a/DI/XDIHelperExtension.cs b/DI/XDIHelperExtension.cs index b964d19..a39d80c 100644 --- a/DI/XDIHelperExtension.cs +++ b/DI/XDIHelperExtension.cs @@ -1,31 +1,31 @@ -using System.Collections.Generic; -using IdentityServer4; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.HttpOverrides; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using xCommons.Authorization; -using xCommons.Constants; -using xCommons.Extensions; -using xExceptions.Constants; -using xIdentityHelper; -using xIdentityModels; -using xIdentityModels.Configurations; -using xIds.Configurations; +using xIds.Helpers; using xIds.Constants; using xIds.DbContext; -using xIds.Extensions; -using xIds.Helpers; -using xIds.Interfaces; using xIds.Providers; +using IdentityServer4; +using xIdentityHelper; +using xIdentityModels; +using xIds.Extensions; +using xIds.Interfaces; using xIds.Validators; +using xCommons.Constants; using xMessageService.DI; -using xMessageService.Interfaces; using xStorageService.DI; +using xCommons.Extensions; +using xIds.Configurations; +using xExceptions.Constants; +using xCommons.Authorization; +using System.Collections.Generic; +using xMessageService.Interfaces; using xStorageService.Interfaces; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Identity; +using xIdentityModels.Configurations; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; namespace xIds.DI { @@ -80,7 +80,7 @@ namespace xIds.DI } /// - /// Retrive XDataService Configurations + /// Retrive XDataService Configurations ... /// /// /// @@ -107,6 +107,26 @@ namespace xIds.DI return result; } + /// + /// Retrieve XApiKeyConfiguration Configurations ... + /// + /// + /// + public static XApiKeyConfiguration GetXApiKeyConfiguration(this IConfiguration source) + { + // + var section = source + .GetSection(Constants.ConfigurationNodeNames.APIKEY_NODE_NAME); + var result = section.Get(); + if (result.IsNull()) + { + result = new XApiKeyConfiguration(); + } + + // + return result; + } + /// /// Register XDataService Configuration /// diff --git a/DbContext/XIdentityDbContext.cs b/DbContext/XIdentityDbContext.cs index 7d01345..5efe4c7 100644 --- a/DbContext/XIdentityDbContext.cs +++ b/DbContext/XIdentityDbContext.cs @@ -15,7 +15,7 @@ namespace xIds.DbContext public DbSet Devices { get; set; } public DbSet ApiKeys { get; set; } public DbSet Avatars { get; set; } - public DbSet ApiKeyUsage { get; set; } + public DbSet ApiKeyUsages { get; set; } public DbSet Applications { get; set; } public DbSet BannedDevices { get; set; } public DbSet Followers { get; set; } diff --git a/Extensions/IdentityExtensions.cs b/Extensions/IdentityExtensions.cs index ffc7c42..6f01dec 100644 --- a/Extensions/IdentityExtensions.cs +++ b/Extensions/IdentityExtensions.cs @@ -1,6 +1,6 @@ -using Microsoft.Extensions.DependencyInjection; -using xIds.Interfaces; using xIds.Providers; +using xIds.Interfaces; +using Microsoft.Extensions.DependencyInjection; namespace xIds.Extensions { diff --git a/Helpers/XApiKeyHelper.cs b/Helpers/XApiKeyHelper.cs new file mode 100644 index 0000000..09d5bb7 --- /dev/null +++ b/Helpers/XApiKeyHelper.cs @@ -0,0 +1,68 @@ +namespace xIds.Helpers +{ + /// + /// Api Key Generating Helper Class ... + /// + public static class XApiKeyHelper + { + /// + /// Generate an Api Key ... + /// + /// + public static (string plainKey, string hash, string prefix) Generate() + { + // + // Generate 32 bytes of cryptographically secure random data + var randomBytes = new byte[32]; + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(randomBytes); + } + + // + // Format: xapp_{base64url} + var plainKey = $"xapp_{Convert.ToBase64String(randomBytes) + .Replace("+", "-").Replace("/", "_").TrimEnd('=')}"; + + // + // Hash with SHA256 for storage (never store plain key) + using (var sha256 = SHA256.Create()) + { + // + var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(plainKey)); + var hash = BitConverter.ToString(hashBytes).Replace("-", "").ToLower(); + + // + // Prefix for quick identification (first 12 chars) + var prefix = plainKey.Substring(0, 16) + "..."; + + // + return (plainKey, hash, prefix); + } + } + + /// + /// Verify Generating Api Key ... + /// + /// + /// + /// + public static bool Verify( + string plainKey, + string storedHash + ) + { + // + using (var sha256 = SHA256.Create()) + { + // + var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(plainKey)); + var computedHash = BitConverter.ToString(hashBytes) + .Replace("-", "").ToLower(); + + // + return computedHash == storedHash; + } + } + } +} \ No newline at end of file diff --git a/Helpers/XIdentityHelper.cs b/Helpers/XIdentityHelper.cs index a8b8fd1..e5283b9 100644 --- a/Helpers/XIdentityHelper.cs +++ b/Helpers/XIdentityHelper.cs @@ -1,13 +1,13 @@ using System; -using System.IdentityModel.Tokens.Jwt; -using Microsoft.IdentityModel.Tokens; -using xCommons.Extensions; -using xCommons.Providers; -using xExceptions.Constants; -using xIdentityModels.Configurations; -using xIdentityModels.Extensions; -using xIdentityModels.Models; using xIds.Interfaces; +using xCommons.Providers; +using xCommons.Extensions; +using xExceptions.Constants; +using xIdentityModels.Models; +using xIdentityModels.Extensions; +using Microsoft.IdentityModel.Tokens; +using xIdentityModels.Configurations; +using System.IdentityModel.Tokens.Jwt; namespace xIds.Helpers { diff --git a/Interfaces/IXApplicationProvider.cs b/Interfaces/IXApplicationProvider.cs index dfc28c5..f176185 100644 --- a/Interfaces/IXApplicationProvider.cs +++ b/Interfaces/IXApplicationProvider.cs @@ -27,7 +27,7 @@ namespace xIds.Interfaces /// Task AddApplicationAsync( XApplicationDto item, - XUserClaimsInfoDto userInfo, + XUserClaimsInfoDto userInfo = null, CancellationToken cancellationToken = default ); @@ -117,12 +117,14 @@ namespace xIds.Interfaces /// /// /// + /// /// /// /// Task FindOneApplicationAsync( Expression> predicate, bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, Func, IIncludableQueryable> includeBuilder = null, CancellationToken cancellationToken = default ); @@ -132,6 +134,7 @@ namespace xIds.Interfaces /// /// /// + /// /// /// /// @@ -139,6 +142,7 @@ namespace xIds.Interfaces Task> FindManyApplicationAsync( Expression> predicate, bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, Func, IOrderedQueryable> orderBuilder = null, Func, IIncludableQueryable> includeBuilder = null, CancellationToken cancellationToken = default @@ -149,6 +153,7 @@ namespace xIds.Interfaces /// /// /// + /// /// /// /// @@ -157,6 +162,7 @@ namespace xIds.Interfaces Task> QueryApplicationAsync( XQuery query, bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, Expression> predicate = null, Func, IOrderedQueryable> orderBuilder = null, Func, IIncludableQueryable> includeBuilder = null, @@ -175,7 +181,7 @@ namespace xIds.Interfaces /// Task AddApiKeyAsync( XApiKey item, - XUserClaimsInfoDto userInfo, + XUserClaimsInfoDto userInfo = null, CancellationToken cancellationToken = default ); @@ -265,12 +271,14 @@ namespace xIds.Interfaces /// /// /// + /// /// /// /// Task FindOneApiKeyAsync( Expression> predicate, bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, Func, IIncludableQueryable> includeBuilder = null, CancellationToken cancellationToken = default ); @@ -280,6 +288,7 @@ namespace xIds.Interfaces /// /// /// + /// /// /// /// @@ -287,6 +296,7 @@ namespace xIds.Interfaces Task> FindManyApiKeyAsync( Expression> predicate, bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, Func, IOrderedQueryable> orderBuilder = null, Func, IIncludableQueryable> includeBuilder = null, CancellationToken cancellationToken = default @@ -297,6 +307,7 @@ namespace xIds.Interfaces /// /// /// + /// /// /// /// @@ -305,6 +316,7 @@ namespace xIds.Interfaces Task> QueryApiKeyAsync( XQuery query, bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, Expression> predicate = null, Func, IOrderedQueryable> orderBuilder = null, Func, IIncludableQueryable> includeBuilder = null, @@ -323,7 +335,7 @@ namespace xIds.Interfaces /// Task AddApiKeyUsageAsync( XApiKeyUsageDto item, - XUserClaimsInfoDto userInfo, + XUserClaimsInfoDto userInfo = null, CancellationToken cancellationToken = default ); @@ -413,12 +425,14 @@ namespace xIds.Interfaces /// /// /// + /// /// /// /// Task FindOneApiKeyUsageAsync( Expression> predicate, bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, Func, IIncludableQueryable> includeBuilder = null, CancellationToken cancellationToken = default ); @@ -428,6 +442,7 @@ namespace xIds.Interfaces /// /// /// + /// /// /// /// @@ -435,6 +450,7 @@ namespace xIds.Interfaces Task> FindManyApiKeyUsageAsync( Expression> predicate, bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, Func, IOrderedQueryable> orderBuilder = null, Func, IIncludableQueryable> includeBuilder = null, CancellationToken cancellationToken = default @@ -445,6 +461,7 @@ namespace xIds.Interfaces /// /// /// + /// /// /// /// @@ -453,6 +470,7 @@ namespace xIds.Interfaces Task> QueryApiKeyUsageAsync( XQuery query, bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, Expression> predicate = null, Func, IOrderedQueryable> orderBuilder = null, Func, IIncludableQueryable> includeBuilder = null, @@ -462,10 +480,217 @@ namespace xIds.Interfaces // #region Custom Actions ... + /// + /// Generate a New ApiKey and Add It to DB ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task CreateApiKey( + int applicationId, + TimeSpan? expiration = null, + IEnumerable scopes = null, + IEnumerable allowedIPs = null, + int? rateLimit = null, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ); + + /// + /// Revoke Specified Api Key ... + /// + /// + /// + /// + /// + Task RevokeApiKey( + Guid apiKeyId, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ); + + /// + /// Renew Specified Api Key ... + /// + /// + /// + /// + /// + /// + Task RotateApiKey( + Guid apiKeyId, + TimeSpan? newExpiration = null, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ); + + /// + /// Retrieve All Api Keys for Specified Application ... + /// + /// + /// + /// + /// + Task> GetApplicationApiKeys( + Guid applicationId, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ); + + /// + /// Validate Specified Api Key in based on ClientIP ... + /// + /// + /// + /// + /// + /// + Task ValidateApiKey( + string apiKey, + string clientIP, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ); #endregion // #region Enrichers ... + /// + /// Enrich Api Key ... + /// + /// + /// + /// + Task EnrichAsync( + XApiKey source, + CancellationToken cancellationToken = default + ); + + /// + /// Enrich Api Key ... + /// + /// + /// + /// + Task EnrichAsync( + XApiKeyDto source, + CancellationToken cancellationToken = default + ); + + /// + /// Enrich Api Key Usage ... + /// + /// + /// + /// + Task EnrichAsync( + XApiKeyUsage source, + CancellationToken cancellationToken = default + ); + + /// + /// Enrich Application ... + /// + /// + /// + /// + Task EnrichAsync( + XApplication source, + CancellationToken cancellationToken = default + ); + + /// + /// Enrich Api Key Usage ... + /// + /// + /// + /// + Task EnrichAsync( + XApiKeyUsageDto source, + CancellationToken cancellationToken = default + ); + + /// + /// Enrich Application ... + /// + /// + /// + /// + Task EnrichAsync( + XApplicationDto source, + CancellationToken cancellationToken = default + ); + + /// + /// Enrich a Collection of Api Key(s) ... + /// + /// + /// + /// + Task EnrichRangeAsync( + IEnumerable sources, + CancellationToken cancellationToken = default + ); + + /// + /// Enrich a Collection of Api Key(s) ... + /// + /// + /// + /// + Task EnrichRangeAsync( + IEnumerable sources, + CancellationToken cancellationToken = default + ); + + /// + /// Enrich a Collection of Api Key Usage(s) ... + /// + /// + /// + /// + Task EnrichRangeAsync( + IEnumerable sources, + CancellationToken cancellationToken = default + ); + + /// + /// Enrich a Collection of Application(s) ... + /// + /// + /// + /// + Task EnrichRangeAsync( + IEnumerable sources, + CancellationToken cancellationToken = default + ); + + /// + /// Enrich a Collection of Api Key Usage(s) ... + /// + /// + /// + /// + Task EnrichRangeAsync( + IEnumerable sources, + CancellationToken cancellationToken = default + ); + + /// + /// Enrich a Collection of Application(s) ... + /// + /// + /// + /// + Task EnrichRangeAsync( + IEnumerable sources, + CancellationToken cancellationToken = default + ); #endregion } } \ No newline at end of file diff --git a/Providers/XApplicationProvider.cs b/Providers/XApplicationProvider.cs new file mode 100644 index 0000000..4216a72 --- /dev/null +++ b/Providers/XApplicationProvider.cs @@ -0,0 +1,280 @@ +using System; +using System.Linq; +using xModels.Dtos; +using xIds.DbContext; +using xIds.Interfaces; +using xCommons.Extensions; +using xIds.Configurations; +using xIdentityModels.Dtos; +using xExceptions.Constants; +using System.Threading.Tasks; +using xIdentityModels.Models; +using System.Linq.Expressions; +using IdentityServer4.Extensions; +using xIdentityModels.Extensions; +using Microsoft.EntityFrameworkCore.Query; + +namespace xIds.Providers +{ + /// + /// a Service For Manage Security Applications and also Api Key(s) ... + /// + public partial class XApplicationProvider : IXApplicationProvider + { + private readonly string permittedRole; + private readonly XIdentityDbContext dbContext; + private readonly IXIdentityManager identityManager; + private readonly XDataServiceConfiguration configuration; + private readonly XApiKeyConfiguration apiKeyConfiguration; + + public XApplicationProvider( + XIdentityDbContext dbContext, + IXIdentityManager identityManager, + XDataServiceConfiguration configuration, + XApiKeyConfiguration apiKeyConfiguration, + string permittedRole = "admin" + ) + { + // + this.dbContext = dbContext; + this.permittedRole = permittedRole; + this.configuration = configuration; + this.identityManager = identityManager; + this.apiKeyConfiguration = apiKeyConfiguration; + } + + /// + /// Retrieve Owner ... + /// + /// + /// + public async Task GetOwner(string ownerId) + { + // + if (ownerId.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + var user = await identityManager.GetUserAsync(ownerId); + XPersonDto result = user.ToXPerson(); + if (result.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + return result; + } + + /// + /// Check Specifed User Info is Owned for Action or not ... + /// + /// + /// + /// + public bool IsOwned( + XApplication source, + XUserClaimsInfoDto userInfo = null + ) + { + // + var result = + !source.IsNullOrDefault() && + !userInfo.IsNullOrDefault() && + (userInfo.UserId == source.OwnerId || + userInfo.Roles + .Any(r => + r.ToNormalString() == permittedRole.ToNormalString())); + + // + return result; + } + + /// + /// Check Specifed User Info is Owned for Action or not ... + /// + /// + /// + /// + public bool IsOwned( + XApplicationDto source, + XUserClaimsInfoDto userInfo = null + ) + { + // + var result = + !source.IsNullOrDefault() && + !userInfo.IsNullOrDefault() && + (userInfo.UserId == source.OwnerId || + userInfo.Roles + .Any(r => + r.ToNormalString() == permittedRole.ToNormalString())); + + // + return result; + } + + /// + /// Normalize an XQuery based on Data Service Configurations ... + /// + /// + /// + /// + public XQuery NormalizeQuery( + XQuery source, + XDataServiceConfiguration configuration // + ) + { + // + if (source.IsNull()) + { + source = new XQuery(); + } + + // + // Page Size Normalization ... + if (source.PageSize < configuration.PagingConfiguration.MinAvailablePageSize) + { + source.PageSize = configuration.PagingConfiguration.MinAvailablePageSize; + } + if (source.PageSize > configuration.PagingConfiguration.MaxAvailablePageSize) + { + source.PageSize = configuration.PagingConfiguration.MaxAvailablePageSize; + } + + // + return source; + } + + /// + /// Prepare a Queryable of Items ... + /// + /// + /// + /// + /// + public IQueryable GetApplicationsQeryable( + Expression> predicate = null, + Func, IOrderedQueryable> orderBuilder = null, + Func, IIncludableQueryable> includeBuilder = null + ) + { + // + // Global Result ... + var result = dbContext.Applications.AsQueryable(); + + // + // Handle Filtering ... + if (!predicate.IsNull()) + { + result = result.Where(predicate); + } + + // + // Handle Including ... + if (!includeBuilder.IsNull()) + { + result = includeBuilder(result); + } + + // + // Handle Ordering ... + if (!orderBuilder.IsNull()) + { + result = orderBuilder(result); + } + + // + // Return result ... + return result; + } + + /// + /// Prepare a Queryable of Items ... + /// + /// + /// + /// + /// + public IQueryable GetApiKeysQeryable( + Expression> predicate = null, + Func, IOrderedQueryable> orderBuilder = null, + Func, IIncludableQueryable> includeBuilder = null + ) + { + // + // Global Result ... + var result = dbContext.ApiKeys.AsQueryable(); + + // + // Handle Filtering ... + if (!predicate.IsNull()) + { + result = result.Where(predicate); + } + + // + // Handle Including ... + if (!includeBuilder.IsNull()) + { + result = includeBuilder(result); + } + + // + // Handle Ordering ... + if (!orderBuilder.IsNull()) + { + result = orderBuilder(result); + } + + // + // Return result ... + return result; + } + + /// + /// Prepare a Queryable of Items ... + /// + /// + /// + /// + /// + public IQueryable GetApiKeyUsagesQeryable( + Expression> predicate = null, + Func, IOrderedQueryable> orderBuilder = null, + Func, IIncludableQueryable> includeBuilder = null + ) + { + // + // Global Result ... + var result = dbContext.ApiKeyUsage.AsQueryable(); + + // + // Handle Filtering ... + if (!predicate.IsNull()) + { + result = result.Where(predicate); + } + + // + // Handle Including ... + if (!includeBuilder.IsNull()) + { + result = includeBuilder(result); + } + + // + // Handle Ordering ... + if (!orderBuilder.IsNull()) + { + result = orderBuilder(result); + } + + // + // Return result ... + return result; + } + } +} \ No newline at end of file diff --git a/Providers/XApplicationProvider/XApplicationProvider+ApiKey.cs b/Providers/XApplicationProvider/XApplicationProvider+ApiKey.cs new file mode 100644 index 0000000..2df3d7b --- /dev/null +++ b/Providers/XApplicationProvider/XApplicationProvider+ApiKey.cs @@ -0,0 +1,773 @@ +using System; +using System.Linq; +using xModels.Dtos; +using System.Threading; +using xCommons.Extensions; +using xIdentityModels.Dtos; +using xExceptions.Constants; +using System.Threading.Tasks; +using xIdentityModels.Models; +using System.Linq.Expressions; +using xDataService.Extensions; +using System.Collections.Generic; +using xIdentityModels.Extensions; +using Microsoft.EntityFrameworkCore; +using System.Runtime.CompilerServices; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.ChangeTracking; + +namespace xIds.Providers +{ + public partial class XApplicationProvider + { + // + #region ApiKey ... + /// + /// Add Specified Item ... + /// + /// + /// + /// + /// + public async Task AddApiKeyAsync( + XApiKey item, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (!item.IsValidForAdd()) + { + XException.InvalidArgs.Throw(); + } + + // + // Permission ... + if (userInfo.IsNullOrDefault()) + { + XException.NotAllowed.Throw(); + } + + // + // Check Application Exists ... + var application = await GetApplicationAsync( + userInfo: userInfo, + includeBuilder: null, + id: item.ApplicationId, + ignoreSoftDeleteds: true, + cancellationToken: cancellationToken + ); + if (application.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Validate Owner ... + if (!IsOwned(application, userInfo)) + { + XException.NotAllowed.Throw(); + } + + // + // Check COunt of API Keys ... + var appApiKeysCount = await dbContext.ApiKeys.CountAsync( + cancellationToken: cancellationToken, + predicate: x => x.ApplicationId == item.ApplicationId + ); + if (appApiKeysCount >= apiKeyConfiguration.MaxKeysPerApplication) + { + XException.NotAllowed.Throw(); + } + + // + item.CreatedOn = DateTime.UtcNow; + + // + var entity = item.MapConvert(); + if (entity.Id.IsValidIntId()) + { + entity.Id = -1; + } + + // + var entry = await dbContext.ApiKeys.AddAsync( + entity: entity, + cancellationToken: cancellationToken + ); + var changes = await dbContext.SaveChangesAsync(cancellationToken); + + // + if ( + changes <= 0 || + entry.IsNull() || + entry.Entity.IsNullOrDefault() + ) + { + XException.ActionFailed.Throw(); + } + + // + var result = await EnrichAsync( + source: entry.Entity, + cancellationToken: cancellationToken + ); + return result; + } + + /// + /// Update Specified Item ... + /// + /// + /// + /// + /// + /// + public async Task UpdateApiKeyAsync( + Guid id, + XApiKeyDto item, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if ( + id != item.Id || + !id.IsDefaultGuid() || + !item.IsValidForAdd() || + item.IsNullOrDefault() || + userInfo.IsNullOrDefault() + ) + { + XException.InvalidArgs.Throw(); + } + + // + // Retrieve Exists Entity ... + var exists = await dbContext.ApiKeys + .FirstOrDefaultAsync(x => x.Id == id && !x.Deleted); + if (exists.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Permission Checking ... + var application = await GetApplicationAsync( + userInfo: userInfo, + includeBuilder: null, + id: item.ApplicationId, + ignoreSoftDeleteds: true, + cancellationToken: cancellationToken + ); + if (application.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + if (!IsOwned(application, userInfo)) + { + XException.NotAllowed.Throw(); + } + + // + // Update Data on Exists Entity ... + exists = exists.UpdateData( + updateWith: item, + propertyWhiteList: [ + nameof(XApiKey.KeyHash), + nameof(XApiKey.KeyPrefix), + nameof(XApiKey.ExpiresAt), + nameof(XApiKey.CreatedOn), + nameof(XApiKey.RevokedAt), + nameof(XApiKey.RevokedBy), + nameof(XApiKey.LastUsedAt), + nameof(XApiKey.AllowedIPs), + nameof(XApiKey.ApplicationId), + nameof(XApiKey.AllowedScopes), + nameof(XApiKey.RateLimitPerMinute), + ] + ); + + // + var entry = dbContext.ApiKeys.Attach(exists); + entry.State = EntityState.Modified; + + // + var changes = await dbContext.SaveChangesAsync(cancellationToken); + if ( + changes <= 0 || + entry.IsNull() || + entry.Entity.IsNullOrDefault() + ) + { + XException.ActionFailed.Throw(); + } + + // + var result = await EnrichAsync( + source: entry.Entity, + cancellationToken: cancellationToken + ); + return result; + } + + /// + /// Remove Specified Item ... + /// + /// + /// + /// + /// + /// + public async Task RemoveApiKeyAsync( + Guid id, + bool softDelete = true, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if ( + id.IsDefaultGuid() || + userInfo.IsNullOrDefault() + ) + { + XException.InvalidArgs.Throw(); + } + + // + // Retrieve Exists Entity ... + var exists = await dbContext.ApiKeys + .FirstOrDefaultAsync(x => x.Id == id && !x.Deleted); + if (exists.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Permission Checking ... + var application = await GetApplicationAsync( + userInfo: userInfo, + includeBuilder: null, + id: exists.ApplicationId, + ignoreSoftDeleteds: true, + cancellationToken: cancellationToken + ); + if (application.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + if (!IsOwned(application, userInfo)) + { + XException.NotAllowed.Throw(); + } + + // + EntityEntry entry = null; + if (softDelete) + { + // + exists.Deleted = true; + entry = dbContext.ApiKeys.Update(exists); + } + else + { + entry = dbContext.ApiKeys.Remove(exists); + } + + // + var changes = await dbContext.SaveChangesAsync(cancellationToken); + if ( + changes <= 0 || + entry.IsNull() || + entry.Entity.IsNull() + ) + { + XException.ActionFailed.Throw(); + } + + // + var result = await EnrichAsync( + source: entry.Entity, + cancellationToken: cancellationToken + ); + return result; + } + + /// + /// Retrieve an Item by Providing it's ID ... + /// + /// + /// + /// + /// + /// + /// + public async Task GetApiKeyAsync( + Guid id, + bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, + Func, IIncludableQueryable> includeBuilder = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (id.IsDefaultGuid() || + userInfo.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + // Prepare Condition ... + Expression> predicate = x => + !ignoreSoftDeleteds || !x.Deleted; + + // + // Retrieve Queryable ... + var queryable = GetApiKeysQeryable( + orderBuilder: null, + predicate: predicate, + includeBuilder: includeBuilder + ); + + // + var entity = await queryable.FirstOrDefaultAsync( + predicate: x => x.Id == id, + cancellationToken: cancellationToken + ); + if (entity.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Permission Checking ... + var application = await GetApplicationAsync( + userInfo: userInfo, + includeBuilder: null, + id: entity.ApplicationId, + ignoreSoftDeleteds: true, + cancellationToken: cancellationToken + ); + if (application.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + if (!IsOwned(application, userInfo)) + { + XException.NotAllowed.Throw(); + } + + // + var result = await EnrichAsync( + source: entity, + cancellationToken: cancellationToken + ); + return result; + } + + /// + /// Retrieve all Items ... + /// + /// + /// + /// + /// + /// + /// + public async Task> GetAllApiKeyAsync( + bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, + Func, IOrderedQueryable> orderBuilder = null, + Func, IIncludableQueryable> includeBuilder = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (userInfo.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + var applications = await GetAllApplicationAsync( + userInfo: userInfo, + orderBuilder: null, + includeBuilder: null, + cancellationToken: cancellationToken, + ignoreSoftDeleteds: ignoreSoftDeleteds + ); + applications = applications.Where(a => IsOwned(a, userInfo)); + var appIds = applications + .Select(a => a.Id) + .ToList(); + + // + Expression> predicate = x => + appIds.Contains(x.ApplicationId) && + !ignoreSoftDeleteds || !x.Deleted; + var queryable = GetApiKeysQeryable( + predicate: predicate, + orderBuilder: orderBuilder, + includeBuilder: includeBuilder + ); + + // + var items = await queryable.ToListAsync(cancellationToken); + var result = await EnrichRangeAsync( + sources: items, + cancellationToken: cancellationToken + ); + return result; + } + + /// + /// Retrieve all Items as Async Enumerable ... + /// + /// + /// + /// + /// + /// + /// + public async IAsyncEnumerable GetAllApiKeyAsAsyncEnumerable( + bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, + Func, IOrderedQueryable> orderBuilder = null, + Func, IIncludableQueryable> includeBuilder = null, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (userInfo.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + var applications = await GetAllApplicationAsync( + userInfo: userInfo, + orderBuilder: null, + includeBuilder: null, + cancellationToken: cancellationToken, + ignoreSoftDeleteds: ignoreSoftDeleteds + ); + applications = applications.Where(a => IsOwned(a, userInfo)); + var appIds = applications + .Select(a => a.Id) + .ToList(); + + // + Expression> predicate = x => + appIds.Contains(x.ApplicationId) && + !ignoreSoftDeleteds || !x.Deleted; + var queryable = GetApiKeysQeryable( + predicate: predicate, + orderBuilder: orderBuilder, + includeBuilder: includeBuilder + ); + + // + var enumerator = queryable.AsAsyncEnumerable(); + await foreach (var item in enumerator) + { + // + // Handle Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + var dto = await EnrichAsync( + source: item, + cancellationToken: cancellationToken + ); + + yield return dto; + } + } + + /// + /// Find an Item By Providing a Conditional Expression ... + /// + /// + /// + /// + /// + /// + /// + public async Task FindOneApiKeyAsync( + Expression> predicate, + bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, + Func, IIncludableQueryable> includeBuilder = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (userInfo.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + Expression> _predicate = x => !ignoreSoftDeleteds || !x.Deleted; + var queryable = GetApiKeysQeryable( + orderBuilder: null, + predicate: _predicate, + includeBuilder: includeBuilder + ); + + // + var entity = (await queryable + .ToListAsync(cancellationToken)) + .FirstOrDefault(x => predicate.Compile()(x)); + + // + // Permission Checking ... + var application = await GetApplicationAsync( + userInfo: userInfo, + includeBuilder: null, + id: entity.ApplicationId, + ignoreSoftDeleteds: true, + cancellationToken: cancellationToken + ); + if (application.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + if (!IsOwned(application, userInfo)) + { + XException.NotAllowed.Throw(); + } + + // + var result = await EnrichAsync( + source: entity, + cancellationToken: cancellationToken + ); + return result; + } + + /// + /// Find a Collection of Items By Providing a Conditional Expression ... + /// + /// + /// + /// + /// + /// + /// + /// + public async Task> FindManyApiKeyAsync( + Expression> predicate, + bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, + Func, IOrderedQueryable> orderBuilder = null, + Func, IIncludableQueryable> includeBuilder = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (userInfo.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + Expression> _predicate = x => !ignoreSoftDeleteds || !x.Deleted; + var queryable = GetApiKeysQeryable( + orderBuilder: null, + predicate: _predicate, + includeBuilder: includeBuilder + ); + + // + var entities = (await queryable + .ToListAsync(cancellationToken)) + .Where(x => predicate.Compile()(x)) + .ToList(); + + // + var appIds = entities + .Select(k => k.ApplicationId) + .ToList(); + + // + foreach (var appId in appIds) + { + // + // Permission Checking ... + var application = await GetApplicationAsync( + id: appId, + userInfo: userInfo, + includeBuilder: null, + ignoreSoftDeleteds: true, + cancellationToken: cancellationToken + ); + if ( + application.IsNullOrDefault() || + !IsOwned(application, userInfo)) + { + entities = [.. entities.Where(e => e.ApplicationId != appId)]; + } + } + + // + var result = await EnrichRangeAsync( + sources: entities, + cancellationToken: cancellationToken + ); + return result; + } + + /// + /// Retrieve Items based on XQuery Pagination Structure ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task> QueryApiKeyAsync( + XQuery query, + bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, + Expression> predicate = null, + Func, IOrderedQueryable> orderBuilder = null, + Func, IIncludableQueryable> includeBuilder = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (userInfo.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + // Normalize Query ... + query = NormalizeQuery(query, configuration); + + // + Expression> _predicate = x => !ignoreSoftDeleteds || !x.Deleted; + var queryable = GetApiKeysQeryable( + predicate: _predicate, + orderBuilder: orderBuilder, + includeBuilder: includeBuilder + ); + + // + var items = (await queryable + .ToListAsync(cancellationToken)) + .Where(x => predicate.Compile()(x)); + + // + var appIds = items + .Select(k => k.ApplicationId) + .ToList(); + + // + foreach (var appId in appIds) + { + // + // Permission Checking ... + var application = await GetApplicationAsync( + id: appId, + userInfo: userInfo, + includeBuilder: null, + ignoreSoftDeleteds: true, + cancellationToken: cancellationToken + ); + if ( + application.IsNullOrDefault() || + !IsOwned(application, userInfo)) + { + items = [.. items.Where(e => e.ApplicationId != appId)]; + } + } + + // + var totalItemsCount = items.Count(); + + // + // Apply Filter ... + if (!query.Filter.IsNullOrEmpty()) + { + // + items = items + .AsEnumerable() + .ApplyFilter(query.Filter); + } + int filteredItemsCount = items.Count(); + + // + // Count Pages ... + var totalPagesCount = query.CountPages(totalItemsCount); + var filteredPagesCount = query.CountPages(filteredItemsCount); + + // + // Apply Paging and Sorting ... + if (totalItemsCount > 0 && + filteredItemsCount > 0 + ) + { + // + // Apply Sorting ... + items = items + .ToList() + .ApplySorting( + query.SortBy, + query.IsAscending + ); + + // + // Apply Paging ... + items = items + .ToList() + .ApplyPaging( + query.Page, + query.PageSize + ); + } + + // + var resultItems = await EnrichRangeAsync( + sources: items, + cancellationToken: cancellationToken + ); + + // + // Prepare Result ... + var result = new XQueryResult + { + Page = query.Page, + Items = [.. resultItems], + PageSize = query.PageSize, + TotalPages = totalPagesCount, + TotalItems = totalItemsCount, + TotalFilteredPages = filteredPagesCount, + TotalFilteredItems = filteredItemsCount + }; + + // + return result; + } + #endregion + } +} \ No newline at end of file diff --git a/Providers/XApplicationProvider/XApplicationProvider+ApiKeyUsage.cs b/Providers/XApplicationProvider/XApplicationProvider+ApiKeyUsage.cs new file mode 100644 index 0000000..b51cc9a --- /dev/null +++ b/Providers/XApplicationProvider/XApplicationProvider+ApiKeyUsage.cs @@ -0,0 +1,531 @@ +using System; +using System.Linq; +using xModels.Dtos; +using System.Threading; +using xCommons.Extensions; +using xIdentityModels.Dtos; +using xExceptions.Constants; +using System.Threading.Tasks; +using xIdentityModels.Models; +using System.Linq.Expressions; +using xDataService.Extensions; +using System.Collections.Generic; +using xIdentityModels.Extensions; +using Microsoft.EntityFrameworkCore; +using System.Runtime.CompilerServices; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.ChangeTracking; + +namespace xIds.Providers +{ + public partial class XApplicationProvider + { + // + #region ApiKeyUsage ... + /// + /// Add Specified Item ... + /// + /// + /// + /// + /// + public async Task AddApiKeyUsageAsync( + XApiKeyUsageDto item, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (!item.IsValidForAdd()) + { + XException.InvalidArgs.Throw(); + } + + // + // Check ApiKey Exists ... + var apiKey = await dbContext.ApiKeys + .FirstOrDefaultAsync(x => x.Id == item.ApiKeyId); + if (apiKey.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Check apiKey Activation ... + if (!apiKey.IsActive()) + { + XException.NotAllowed.Throw(); + } + + // + var entity = item.MapConvert(); + var entry = await dbContext.ApiKeyUsages.AddAsync( + entity: entity, + cancellationToken: cancellationToken + ); + var changes = await dbContext.SaveChangesAsync(cancellationToken); + if ( + changes <= 0 || + entry.IsNull() || + entry.Entity.IsNullOrDefault() + ) + { + XException.ActionFailed.Throw(); + } + + // + var result = await EnrichAsync( + source: entry.Entity, + cancellationToken: cancellationToken + ); + return result; + } + + /// + /// Update Specified Item ... + /// + /// + /// + /// + /// + /// + public async Task UpdateApiKeyUsageAsync( + long id, + XApiKeyUsageDto item, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if ( + id <= 0 || + id != item.Id || + !item.IsValidForAdd() || + item.IsNullOrDefault() + ) + { + XException.InvalidArgs.Throw(); + } + + // + // Check Item Exists ... + var exists = dbContext.ApiKeyUsages.FirstOrDefault(x => x.Id == id); + if (exists.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Update Data on Exists Entity ... + exists = exists.UpdateData( + updateWith: item, + propertyWhiteList: [ + nameof(XApiKeyUsage.ApiKeyId), + nameof(XApiKeyUsage.Endpoint), + nameof(XApiKeyUsage.ClientIP), + nameof(XApiKeyUsage.HttpMethod), + nameof(XApiKeyUsage.StatusCode), + nameof(XApiKeyUsage.RequestedOn), + nameof(XApiKeyUsage.ResponseTimeMs), + ] + ); + + // + var entry = dbContext.ApiKeyUsages.Attach(exists); + entry.State = EntityState.Modified; + + // + var changes = await dbContext.SaveChangesAsync(cancellationToken); + if ( + changes <= 0 || + entry.IsNull() || + entry.Entity.IsNullOrDefault() + ) + { + XException.ActionFailed.Throw(); + } + + // + var result = await EnrichAsync( + source: entry.Entity, + cancellationToken: cancellationToken + ); + return result; + } + + /// + /// Remove Specified Item ... + /// + /// + /// + /// + /// + /// + public async Task RemoveApiKeyUsageAsync( + long id, + bool softDelete = true, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (id <= 0) + { + XException.InvalidArgs.Throw(); + } + + // + // Retrieve Exists Entity ... + var exists = await dbContext.ApiKeyUsages + .FirstOrDefaultAsync(x => x.Id == id && !x.Deleted); + if (exists.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + EntityEntry entry = null; + if (softDelete) + { + // + exists.Deleted = true; + entry = dbContext.ApiKeyUsages.Update(exists); + } + else + { + entry = dbContext.ApiKeyUsages.Remove(exists); + } + + // + var changes = await dbContext.SaveChangesAsync(cancellationToken); + if ( + changes <= 0 || + entry.IsNull() || + entry.Entity.IsNull() + ) + { + XException.ActionFailed.Throw(); + } + + // + var result = await EnrichAsync( + source: entry.Entity, + cancellationToken: cancellationToken + ); + return result; + } + + /// + /// Retrieve an Item by Providing it's ID ... + /// + /// + /// + /// + /// + /// + /// + public async Task GetApiKeyUsageAsync( + long id, + bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, + Func, IIncludableQueryable> includeBuilder = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (id <= 0) + { + XException.InvalidArgs.Throw(); + } + + // + // Prepare Condition ... + Expression> predicate = x => + !ignoreSoftDeleteds || !x.Deleted; + + // + // Retrieve Queryable ... + var queryable = GetApiKeyUsagesQeryable( + orderBuilder: null, + predicate: predicate, + includeBuilder: includeBuilder + ); + + // + var entity = await queryable.FirstOrDefaultAsync( + predicate: x => x.Id == id, + cancellationToken: cancellationToken + ); + if (entity.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + var result = await EnrichAsync( + source: entity, + cancellationToken: cancellationToken + ); + return result; + } + + /// + /// Retrieve all Items ... + /// + /// + /// + /// + /// + /// + /// + public async Task> GetAllApiKeyUsageAsync( + bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, + Func, IOrderedQueryable> orderBuilder = null, + Func, IIncludableQueryable> includeBuilder = null, + CancellationToken cancellationToken = default + ) + { + // + Expression> predicate = x => + !ignoreSoftDeleteds || !x.Deleted; + var queryable = GetApiKeyUsagesQeryable( + predicate: predicate, + orderBuilder: orderBuilder, + includeBuilder: includeBuilder + ); + + // + var items = await queryable.ToListAsync(cancellationToken); + var result = await EnrichRangeAsync( + sources: items, + cancellationToken: cancellationToken + ); + return result; + } + + /// + /// Retrieve all Items as Async Enumerable ... + /// + /// + /// + /// + /// + /// + /// + public async IAsyncEnumerable GetAllApiKeyUsageAsAsyncEnumerable( + bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, + Func, IOrderedQueryable> orderBuilder = null, + Func, IIncludableQueryable> includeBuilder = null, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + Expression> predicate = x => + !ignoreSoftDeleteds || !x.Deleted; + var queryable = GetApiKeyUsagesQeryable( + predicate: predicate, + orderBuilder: orderBuilder, + includeBuilder: includeBuilder + ); + + // + var enumerator = queryable.AsAsyncEnumerable(); + await foreach (var item in enumerator) + { + // + // Handle Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + var dto = await EnrichAsync( + source: item, + cancellationToken: cancellationToken + ); + + yield return dto; + } + } + + /// + /// Find an Item By Providing a Conditional Expression ... + /// + /// + /// + /// + /// + /// + /// + public async Task FindOneApiKeyUsageAsync( + Expression> predicate, + bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, + Func, IIncludableQueryable> includeBuilder = null, + CancellationToken cancellationToken = default + ) + { + // + Expression> _predicate = x => !ignoreSoftDeleteds || !x.Deleted; + var queryable = GetApiKeyUsagesQeryable( + orderBuilder: null, + predicate: _predicate, + includeBuilder: includeBuilder + ); + + // + var entity = (await queryable + .ToListAsync(cancellationToken)) + .FirstOrDefault(x => predicate.Compile()(x)); + + // + var result = await EnrichAsync( + source: entity, + cancellationToken: cancellationToken + ); + return result; + } + + /// + /// Find a Collection of Items By Providing a Conditional Expression ... + /// + /// + /// + /// + /// + /// + /// + /// + public async Task> FindManyApiKeyUsageAsync( + Expression> predicate, + bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, + Func, IOrderedQueryable> orderBuilder = null, + Func, IIncludableQueryable> includeBuilder = null, + CancellationToken cancellationToken = default + ) + { + // + Expression> _predicate = x => !ignoreSoftDeleteds || !x.Deleted; + var queryable = GetApiKeyUsagesQeryable( + orderBuilder: null, + predicate: _predicate, + includeBuilder: includeBuilder + ); + + // + var entities = (await queryable + .ToListAsync(cancellationToken)) + .Where(x => predicate.Compile()(x)) + .ToList(); + + // + var result = await EnrichRangeAsync( + sources: entities, + cancellationToken: cancellationToken + ); + return result; + } + + /// + /// Retrieve Items based on XQuery Pagination Structure ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task> QueryApiKeyUsageAsync( + XQuery query, + bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, + Expression> predicate = null, + Func, IOrderedQueryable> orderBuilder = null, + Func, IIncludableQueryable> includeBuilder = null, + CancellationToken cancellationToken = default + ) + { + // + // Normalize Query ... + query = NormalizeQuery(query, configuration); + + // + var items = await GetAllApiKeyUsageAsync( + userInfo: userInfo, + orderBuilder: orderBuilder, + includeBuilder: includeBuilder, + cancellationToken: cancellationToken, + ignoreSoftDeleteds: ignoreSoftDeleteds + ); + var totalItemsCount = items.Count(); + + // + // Apply Filter ... + if (!query.Filter.IsNullOrEmpty()) + { + // + items = items + .ApplyFilter(query.Filter); + } + int filteredItemsCount = items.Count(); + + // + // Count Pages ... + var totalPagesCount = query.CountPages(totalItemsCount); + var filteredPagesCount = query.CountPages(filteredItemsCount); + + // + // Apply Paging and Sorting ... + if (totalItemsCount > 0 && + filteredItemsCount > 0 + ) + { + // + // Apply Sorting ... + items = items + .ToList() + .ApplySorting( + query.SortBy, + query.IsAscending + ); + + // + // Apply Paging ... + items = items + .ToList() + .ApplyPaging( + query.Page, + query.PageSize + ); + } + + // + // Prepare Result ... + var result = new XQueryResult + { + Page = query.Page, + Items = [.. items], + PageSize = query.PageSize, + TotalPages = totalPagesCount, + TotalItems = totalItemsCount, + TotalFilteredPages = filteredPagesCount, + TotalFilteredItems = filteredItemsCount + }; + + // + return result; + } + #endregion + } +} \ No newline at end of file diff --git a/Providers/XApplicationProvider/XApplicationProvider+Application.cs b/Providers/XApplicationProvider/XApplicationProvider+Application.cs new file mode 100644 index 0000000..a402919 --- /dev/null +++ b/Providers/XApplicationProvider/XApplicationProvider+Application.cs @@ -0,0 +1,624 @@ +using System; +using System.Linq; +using xModels.Dtos; +using System.Threading; +using xCommons.Extensions; +using xIdentityModels.Dtos; +using xExceptions.Constants; +using System.Threading.Tasks; +using xIdentityModels.Models; +using System.Linq.Expressions; +using xDataService.Extensions; +using System.Collections.Generic; +using xIdentityModels.Extensions; +using xIdentityHelper.Extensions; +using Microsoft.EntityFrameworkCore; +using System.Runtime.CompilerServices; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.ChangeTracking; + +namespace xIds.Providers +{ + public partial class XApplicationProvider + { + // + #region Application ... + /// + /// Add Specified Item ... + /// + /// + /// + /// + /// + public async Task AddApplicationAsync( + XApplicationDto item, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (!item.IsValidForAdd()) + { + XException.InvalidArgs.Throw(); + } + + // + // Permission ... + if (userInfo.IsNullOrDefault()) + { + XException.NotAllowed.Throw(); + } + + // + item.OwnerId = userInfo.UserId; + item.CreatedOn = DateTime.UtcNow; + + // + var entity = item.MapConvert(); + if (entity.Id.IsValidIntId()) + { + entity.Id = -1; + } + + // + var entry = await dbContext.Applications.AddAsync( + entity: entity, + cancellationToken: cancellationToken + ); + var changes = await dbContext.SaveChangesAsync(cancellationToken); + + // + if ( + changes <= 0 || + entry.IsNull() || + entry.Entity.IsNullOrDefault() + ) + { + XException.ActionFailed.Throw(); + } + + // + var result = await EnrichAsync( + source: entry.Entity, + cancellationToken: cancellationToken + ); + return result; + } + + /// + /// Update Specified Item ... + /// + /// + /// + /// + /// + /// + public async Task UpdateApplicationAsync( + int id, + XApplicationDto item, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if ( + id != item.Id || + !id.IsValidIntId() || + item.IsNullOrDefault() || + userInfo.IsNullOrDefault() + ) + { + XException.InvalidArgs.Throw(); + } + + // + // Retrieve Exists Entity ... + var exists = await dbContext.Applications + .FirstOrDefaultAsync(x => x.Id == id && !x.Deleted); + if (exists.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Permission Checking ... + if (!IsOwned(exists, userInfo)) + { + XException.NotAllowed.Throw(); + } + + // + // Update Data on Exists Entity ... + exists = exists.UpdateData( + updateWith: item, + propertyWhiteList: [ + nameof(XApplication.Name), + nameof(XApplication.Type), + nameof(XApplication.OwnerId), + nameof(XApplication.IsActive), + nameof(XApplication.CreatedOn), + nameof(XApplication.UpdatedAt), + nameof(XApplication.Description), + ] + ); + + // + var entry = dbContext.Applications.Attach(exists); + entry.State = EntityState.Modified; + + // + var changes = await dbContext.SaveChangesAsync(cancellationToken); + if ( + changes <= 0 || + entry.IsNull() || + entry.Entity.IsNullOrDefault() + ) + { + XException.ActionFailed.Throw(); + } + + // + var result = await EnrichAsync( + source: entry.Entity, + cancellationToken: cancellationToken + ); + return result; + } + + /// + /// Remove Specified Item ... + /// + /// + /// + /// + /// + /// + public async Task RemoveApplicationAsync( + int id, + bool softDelete = true, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if ( + !id.IsValidIntId() || + userInfo.IsNullOrDefault() + ) + { + XException.InvalidArgs.Throw(); + } + + // + // Retrieve Exists Entity ... + var exists = await dbContext.Applications + .FirstOrDefaultAsync(x => x.Id == id && !x.Deleted); + if (exists.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Permission Checking ... + if (!IsOwned(exists, userInfo)) + { + XException.NotAllowed.Throw(); + } + + // + EntityEntry entry = null; + if (softDelete) + { + // + exists.Deleted = true; + entry = dbContext.Applications.Update(exists); + } + else + { + entry = dbContext.Applications.Remove(exists); + } + + // + var changes = await dbContext.SaveChangesAsync(cancellationToken); + if ( + changes <= 0 || + entry.IsNull() || + entry.Entity.IsNull() + ) + { + XException.ActionFailed.Throw(); + } + + // + var result = await EnrichAsync( + source: entry.Entity, + cancellationToken: cancellationToken + ); + return result; + } + + /// + /// Retrieve an Item by Providing it's ID ... + /// + /// + /// + /// + /// + /// + /// + public async Task GetApplicationAsync( + int id, + bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, + Func, IIncludableQueryable> includeBuilder = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (!id.IsValidIntId() || + userInfo.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + // Prepare Condition ... + Expression> predicate = x => + x.OwnerId != userInfo.UserId || + !ignoreSoftDeleteds || !x.Deleted; + if (userInfo.IsAdmin()) + { + // + predicate = x => + !ignoreSoftDeleteds || !x.Deleted; + } + + // + // Retrieve Queryable ... + var queryable = GetApplicationsQeryable( + orderBuilder: null, + predicate: predicate, + includeBuilder: includeBuilder + ); + + // + var entity = await queryable.FirstOrDefaultAsync( + predicate: x => x.Id == id, + cancellationToken: cancellationToken + ); + + // + var result = await EnrichAsync( + source: entity, + cancellationToken: cancellationToken + ); + return result; + } + + /// + /// Retrieve all Items ... + /// + /// + /// + /// + /// + /// + /// + public async Task> GetAllApplicationAsync( + bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, + Func, IOrderedQueryable> orderBuilder = null, + Func, IIncludableQueryable> includeBuilder = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (userInfo.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + // Prepare Condition ... + Expression> predicate = x => + x.OwnerId == userInfo.UserId && + (!ignoreSoftDeleteds || !x.Deleted); + if (userInfo.IsAdmin()) + { + // + predicate = x => + !ignoreSoftDeleteds || !x.Deleted; + } + + // + // Retrieve Queryable ... + var queryable = GetApplicationsQeryable( + predicate: predicate, + orderBuilder: orderBuilder, + includeBuilder: includeBuilder + ); + + // + var items = await queryable.ToListAsync(cancellationToken); + var result = await EnrichRangeAsync( + sources: items, + cancellationToken: cancellationToken + ); + + // + return result; + } + + /// + /// Retrieve all Items as Async Enumerable ... + /// + /// + /// + /// + /// + /// + /// + public async IAsyncEnumerable GetAllApplicationAsAsyncEnumerable( + bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, + Func, IOrderedQueryable> orderBuilder = null, + Func, IIncludableQueryable> includeBuilder = null, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (userInfo.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + // Prepare Condition ... + Expression> predicate = x => + x.OwnerId == userInfo.UserId && + (!ignoreSoftDeleteds || !x.Deleted); + if (userInfo.IsAdmin()) + { + // + predicate = x => + !ignoreSoftDeleteds || !x.Deleted; + } + + // + // Retrieve Queryable ... + var queryable = GetApplicationsQeryable( + predicate: predicate, + orderBuilder: orderBuilder, + includeBuilder: includeBuilder + ); + + // + var enumerator = queryable.AsAsyncEnumerable(); + await foreach (var item in enumerator) + { + // + // Handle Cancellation Token ... + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + // + var dto = await EnrichAsync( + source: item, + cancellationToken: cancellationToken + ); + + yield return dto; + } + } + + /// + /// Find an Item By Providing a Conditional Expression ... + /// + /// + /// + /// + /// + /// + /// + public async Task FindOneApplicationAsync( + Expression> predicate, + bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, + Func, IIncludableQueryable> includeBuilder = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (userInfo.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + Expression> _predicate = x => !ignoreSoftDeleteds || !x.Deleted; + if (!userInfo.IsAdmin()) + { + _predicate = x => + x.OwnerId == userInfo.UserId && + (!ignoreSoftDeleteds || !x.Deleted); + } + + // + var queryable = GetApplicationsQeryable( + orderBuilder: null, + predicate: _predicate, + includeBuilder: includeBuilder + ); + + // + var entity = (await queryable + .ToListAsync(cancellationToken)) + .FirstOrDefault(x => predicate.Compile()(x)); + + // + var result = await EnrichAsync( + source: entity, + cancellationToken: cancellationToken + ); + return result; + } + + /// + /// Find a Collection of Items By Providing a Conditional Expression ... + /// + /// + /// + /// + /// + /// + /// + /// + public async Task> FindManyApplicationAsync( + Expression> predicate, + bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, + Func, IOrderedQueryable> orderBuilder = null, + Func, IIncludableQueryable> includeBuilder = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if (userInfo.IsNullOrDefault()) + { + XException.InvalidArgs.Throw(); + } + + // + Expression> _predicate = x => !ignoreSoftDeleteds || !x.Deleted; + if (!userInfo.IsAdmin()) + { + _predicate = x => + x.OwnerId == userInfo.UserId && + (!ignoreSoftDeleteds || !x.Deleted); + } + + // + var queryable = GetApplicationsQeryable( + orderBuilder: null, + predicate: _predicate, + includeBuilder: includeBuilder + ); + + // + var entities = (await queryable + .ToListAsync(cancellationToken)) + .Where(x => predicate.Compile()(x)); + + // + var result = await EnrichRangeAsync( + sources: entities, + cancellationToken: cancellationToken + ); + return result; + } + + /// + /// Retrieve Items based on XQuery Pagination Structure ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task> QueryApplicationAsync( + XQuery query, + bool ignoreSoftDeleteds = true, + XUserClaimsInfoDto userInfo = null, + Expression> predicate = null, + Func, IOrderedQueryable> orderBuilder = null, + Func, IIncludableQueryable> includeBuilder = null, + CancellationToken cancellationToken = default + ) + { + // + // Normalize Query ... + query = NormalizeQuery(query, configuration); + + // + var items = await GetAllApplicationAsync( + userInfo: userInfo, + orderBuilder: orderBuilder, + includeBuilder: includeBuilder, + cancellationToken: cancellationToken, + ignoreSoftDeleteds: ignoreSoftDeleteds + ); + var totalItemsCount = items.Count(); + + // + // Apply Filter ... + if (!query.Filter.IsNullOrEmpty()) + { + // + items = items + .ApplyFilter(query.Filter); + } + int filteredItemsCount = items.Count(); + + // + // Count Pages ... + var totalPagesCount = query.CountPages(totalItemsCount); + var filteredPagesCount = query.CountPages(filteredItemsCount); + + // + // Apply Paging and Sorting ... + if (totalItemsCount > 0 && + filteredItemsCount > 0 + ) + { + // + // Apply Sorting ... + items = items + .ToList() + .ApplySorting( + query.SortBy, + query.IsAscending + ); + + // + // Apply Paging ... + items = items + .ToList() + .ApplyPaging( + query.Page, + query.PageSize + ); + } + + // + // Prepare Result ... + var result = new XQueryResult + { + Page = query.Page, + Items = [.. items], + PageSize = query.PageSize, + TotalPages = totalPagesCount, + TotalItems = totalItemsCount, + TotalFilteredPages = filteredPagesCount, + TotalFilteredItems = filteredItemsCount + }; + + // + return result; + } + #endregion + } +} \ No newline at end of file diff --git a/Providers/XApplicationProvider/XApplicationProvider+Custom.cs b/Providers/XApplicationProvider/XApplicationProvider+Custom.cs new file mode 100644 index 0000000..7725ea0 --- /dev/null +++ b/Providers/XApplicationProvider/XApplicationProvider+Custom.cs @@ -0,0 +1,155 @@ +using System; +using System.Linq; +using System.Threading; +using xCommons.Extensions; +using xIdentityModels.Dtos; +using xExceptions.Constants; +using System.Threading.Tasks; +using xIdentityModels.Models; +using System.Collections.Generic; + +namespace xIds.Providers +{ + public partial class XApplicationProvider + { + // + #region Custom Actions ... + /// + /// Generate a New ApiKey and Add It to DB ... + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task CreateApiKey( + int applicationId, + TimeSpan? expiration = null, + IEnumerable scopes = null, + IEnumerable allowedIPs = null, + int? rateLimit = null, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ) + { + // + // Validate ... + if ( + userInfo.IsNullOrDefault() || + !applicationId.IsValidIntId() + ) + { + XException.InvalidArgs.Throw(); + } + + // + // Check Application Exists ... + var application = await GetApplicationAsync( + id: applicationId, + userInfo: userInfo, + includeBuilder: null, + ignoreSoftDeleteds: true, + cancellationToken: cancellationToken + ); + if (application.IsNullOrDefault()) + { + XException.NotFound.Throw(); + } + + // + // Validate Owner ... + if (!IsOwned(application, userInfo)) + { + XException.NotAllowed.Throw(); + } + + // + // Normalizing ... + if (!rateLimit.HasValue) + { + rateLimit = apiKeyConfiguration.DefaultRateLimit; + } + if (!expiration.HasValue) + { + expiration = TimeSpan.FromMinutes(apiKeyConfiguration.DefaultExpirationMinutes); + } + else if (expiration.Value > TimeSpan.FromMinutes(apiKeyConfiguration.MaxExpirationMinutes)) + { + expiration = TimeSpan.FromMinutes(apiKeyConfiguration.MaxExpirationMinutes); + } + + // + var + + // + var item = new XApiKeyDto + { + + } + } + + /// + /// Revoke Specified Api Key ... + /// + /// + /// + /// + /// + public async Task RevokeApiKey( + Guid apiKeyId, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ) + { } + + /// + /// Renew Specified Api Key ... + /// + /// + /// + /// + /// + /// + public async Task RotateApiKey( + Guid apiKeyId, + TimeSpan? newExpiration = null, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ) + { } + + /// + /// Retrieve All Api Keys for Specified Application ... + /// + /// + /// + /// + /// + public async Task> GetApplicationApiKeys( + Guid applicationId, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ) + { } + + /// + /// Validate Specified Api Key in based on ClientIP ... + /// + /// + /// + /// + /// + /// + public async Task ValidateApiKey( + string apiKey, + string clientIP, + XUserClaimsInfoDto userInfo = null, + CancellationToken cancellationToken = default + ) + { } + #endregion + } +} \ No newline at end of file diff --git a/Providers/XApplicationProvider/XApplicationProvider+Enricher.cs b/Providers/XApplicationProvider/XApplicationProvider+Enricher.cs new file mode 100644 index 0000000..6447457 --- /dev/null +++ b/Providers/XApplicationProvider/XApplicationProvider+Enricher.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using xIdentityModels.Dtos; +using xIdentityModels.Models; + +namespace xIds.Providers +{ + public partial class XApplicationProvider + { + // + #region Enrichers ... + /// + /// Enrich Api Key ... + /// + /// + /// + /// + public async Task EnrichAsync( + XApiKey source, + CancellationToken cancellationToken = default + ) + { } + + /// + /// Enrich Api Key ... + /// + /// + /// + /// + public async Task EnrichAsync( + XApiKeyDto source, + CancellationToken cancellationToken = default + ) + { } + + /// + /// Enrich Api Key Usage ... + /// + /// + /// + /// + public async Task EnrichAsync( + XApiKeyUsage source, + CancellationToken cancellationToken = default + ) + { } + + /// + /// Enrich Application ... + /// + /// + /// + /// + public async Task EnrichAsync( + XApplication source, + CancellationToken cancellationToken = default + ) + { } + + /// + /// Enrich Api Key Usage ... + /// + /// + /// + /// + public async Task EnrichAsync( + XApiKeyUsageDto source, + CancellationToken cancellationToken = default + ) + { } + + /// + /// Enrich Application ... + /// + /// + /// + /// + public async Task EnrichAsync( + XApplicationDto source, + CancellationToken cancellationToken = default + ) + { } + + /// + /// Enrich a Collection of Api Key(s) ... + /// + /// + /// + /// + public async Task> EnrichRangeAsync( + IEnumerable sources, + CancellationToken cancellationToken = default + ) + { } + + /// + /// Enrich a Collection of Api Key(s) ... + /// + /// + /// + /// + public async Task> EnrichRangeAsync( + IEnumerable sources, + CancellationToken cancellationToken = default + ) + { } + + /// + /// Enrich a Collection of Api Key Usage(s) ... + /// + /// + /// + /// + public async Task> EnrichRangeAsync( + IEnumerable sources, + CancellationToken cancellationToken = default + ) + { } + + /// + /// Enrich a Collection of Application(s) ... + /// + /// + /// + /// + public async Task> EnrichRangeAsync( + IEnumerable sources, + CancellationToken cancellationToken = default + ) + { } + + /// + /// Enrich a Collection of Api Key Usage(s) ... + /// + /// + /// + /// + public async Task> EnrichRangeAsync( + IEnumerable sources, + CancellationToken cancellationToken = default + ) + { } + + /// + /// Enrich a Collection of Application(s) ... + /// + /// + /// + /// + public async Task> EnrichRangeAsync( + IEnumerable sources, + CancellationToken cancellationToken = default + ) + { } + #endregion + } +} \ No newline at end of file diff --git a/appsettings.Development.json b/appsettings.Development.json index d894880..267897f 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -36,6 +36,13 @@ "ThumbQuality": 72, "MaxFileSize": 41943040 }, + "ApiKeyConfiguration": { + "DefaultRateLimit": 60, + "EnableAuditLog": false, + "MaxKeysPerApplication": "10", + "MaxExpirationMinutes": "525600", + "DefaultExpirationMinutes": "43200" + }, "DbSeeder": { "UpdateExists": false, "Clients": [