From 794d9a740b206edf020675e4987ab01df67a01a4 Mon Sep 17 00:00:00 2001 From: Efkan Bakanay Date: Tue, 21 May 2024 23:33:26 +0300 Subject: [PATCH] CorePackages Added --- NArchitecture.sln | 45 +++++ .../Core.Application/Core.Application.csproj | 26 +++ .../Authorization/AuthorizationBehavior.cs | 33 ++++ .../Authorization/ISecuredRequest.cs | 6 + .../Caching/CacheRemovingBehavior.cs | 38 ++++ .../Pipelines/Caching/CacheSettings.cs | 6 + .../Pipelines/Caching/CachingBehavior.cs | 57 ++++++ .../Pipelines/Caching/ICachableRequest.cs | 8 + .../Pipelines/Caching/ICacheRemoverRequest.cs | 7 + .../Pipelines/Logging/ILoggableRequest.cs | 5 + .../Pipelines/Logging/LoggingBehavior.cs | 46 +++++ .../Validation/RequestValidationBehavior.cs | 29 +++ .../Pipelines/Validation/ValidationTool.cs | 14 ++ .../Core.Application/Requests/PageRequest.cs | 7 + .../Core.CrossCuttingConcerns.csproj | 17 ++ .../Core.CrossCuttingConcers.csproj | 18 ++ .../Exceptions/AuthorizationException.cs | 8 + .../Exceptions/AuthorizationProblemDetails.cs | 9 + .../Exceptions/BusinessException.cs | 8 + .../Exceptions/BusinessProblemDetails.cs | 9 + .../Exceptions/ExceptionMiddleware.cs | 97 +++++++++ .../ExceptionMiddlewareExtensions.cs | 11 ++ .../Exceptions/ValidationProblemDetails.cs | 11 ++ .../Logging/LogDetail.cs | 9 + .../Logging/LogDetailWithException.cs | 6 + .../Logging/LogParameter.cs | 8 + .../FileLogConfiguration.cs | 6 + .../Logging/Serilog/Logger/FileLogger.cs | 31 +++ .../Logging/Serilog/LoggerServiceBase.cs | 15 ++ .../Serilog/Messages/SerilogMessages.cs | 7 + src/corePackages/Core.ElasticSearch/Class1.cs | 6 + .../Core.ElasticSearch.csproj | 17 ++ .../ElasticSearchManager.cs | 187 ++++++++++++++++++ .../Core.ElasticSearch/IElasticSearch.cs | 24 +++ .../Models/ElasticSearchConfig.cs | 8 + .../Models/ElasticSearchGetModel.cs | 7 + .../Models/ElasticSearchInsertManyModel.cs | 6 + .../Models/ElasticSearchInsertUpdateModel.cs | 6 + .../Models/ElasticSearchModel.cs | 9 + .../Models/ElasticSearchResult.cs | 17 ++ .../Models/IElasticSearchResult.cs | 7 + .../Core.ElasticSearch/Models/IndexModel.cs | 9 + .../Models/SearchByFieldParameters.cs | 7 + .../Models/SearchByQueryParameters.cs | 8 + .../Models/SearchParameters.cs | 8 + src/corePackages/Core.Mailing/Class1.cs | 6 + .../Core.Mailing/Core.Mailing.csproj | 16 ++ src/corePackages/Core.Mailing/IMailService.cs | 6 + src/corePackages/Core.Mailing/Mail.cs | 28 +++ .../MailKitMailService.cs | 46 +++++ src/corePackages/Core.Mailing/MailSettings.cs | 26 +++ src/corePackages/Core.Persistence/Class1.cs | 6 + .../Core.Persistence/Core.Persistence.csproj | 14 ++ .../Core.Persistence/Dynamic/Dynamic.cs | 17 ++ .../Core.Persistence/Dynamic/Filter.cs | 23 +++ .../IQueryableDynamicFilterExtensions.cs | 99 ++++++++++ .../Core.Persistence/Dynamic/Sort.cs | 17 ++ .../Paging/BasePageableModel.cs | 11 ++ .../Core.Persistence/Paging/IPaginate.cs | 13 ++ .../Paging/IQueryablePaginateExtensions.cs | 47 +++++ .../Core.Persistence/Paging/Paginate.cs | 126 ++++++++++++ .../Repositories/EfRepositoryBase.cs | 133 +++++++++++++ .../Core.Persistence/Repositories/Entity.cs | 15 ++ .../Repositories/IAsyncRepository.cs | 25 +++ .../Core.Persistence/Repositories/IQuery.cs | 6 + .../Repositories/IRepository.cs | 24 +++ .../Core.Security/Core.Security.csproj | 26 +++ .../Core.Security/Dtos/UserForLoginDto.cs | 8 + .../Core.Security/Dtos/UserForRegisterDto.cs | 9 + .../EmailAuthenticatorHelper.cs | 18 ++ .../IEmailAuthenticatorHelper.cs | 7 + .../Encryption/SecurityKeyHelper.cs | 12 ++ .../Encryption/SigningCredentialsHelper.cs | 11 ++ .../Entities/EmailAuthenticator.cs | 24 +++ .../Core.Security/Entities/OperationClaim.cs | 17 ++ .../Entities/OtpAuthenticator.cs | 24 +++ .../Core.Security/Entities/RefreshToken.cs | 40 ++++ .../Core.Security/Entities/User.cs | 37 ++++ .../Entities/UserOperationClaim.cs | 22 +++ .../Core.Security/Enums/AuthenticatorType.cs | 8 + .../Extensions/ClaimExtensions.cs | 27 +++ .../Extensions/ClaimsPrincipalExtensions.cs | 22 +++ .../Core.Security/Hashing/HashingHelper.cs | 29 +++ .../Core.Security/JWT/AccessToken.cs | 7 + .../Core.Security/JWT/ITokenHelper.cs | 10 + .../Core.Security/JWT/JwtHelper.cs | 78 ++++++++ .../Core.Security/JWT/TokenOptions.cs | 10 + .../IOtpAuthenticatorHelper.cs | 8 + .../OtpNet/OtpNetOtpAuthenticatorHelper.cs | 33 ++++ .../SecurityServiceRegistration.cs | 18 ++ 90 files changed, 2132 insertions(+) create mode 100644 src/corePackages/Core.Application/Core.Application.csproj create mode 100644 src/corePackages/Core.Application/Pipelines/Authorization/AuthorizationBehavior.cs create mode 100644 src/corePackages/Core.Application/Pipelines/Authorization/ISecuredRequest.cs create mode 100644 src/corePackages/Core.Application/Pipelines/Caching/CacheRemovingBehavior.cs create mode 100644 src/corePackages/Core.Application/Pipelines/Caching/CacheSettings.cs create mode 100644 src/corePackages/Core.Application/Pipelines/Caching/CachingBehavior.cs create mode 100644 src/corePackages/Core.Application/Pipelines/Caching/ICachableRequest.cs create mode 100644 src/corePackages/Core.Application/Pipelines/Caching/ICacheRemoverRequest.cs create mode 100644 src/corePackages/Core.Application/Pipelines/Logging/ILoggableRequest.cs create mode 100644 src/corePackages/Core.Application/Pipelines/Logging/LoggingBehavior.cs create mode 100644 src/corePackages/Core.Application/Pipelines/Validation/RequestValidationBehavior.cs create mode 100644 src/corePackages/Core.Application/Pipelines/Validation/ValidationTool.cs create mode 100644 src/corePackages/Core.Application/Requests/PageRequest.cs create mode 100644 src/corePackages/Core.CrossCuttingConcers/Core.CrossCuttingConcerns.csproj create mode 100644 src/corePackages/Core.CrossCuttingConcers/Core.CrossCuttingConcers.csproj create mode 100644 src/corePackages/Core.CrossCuttingConcers/Exceptions/AuthorizationException.cs create mode 100644 src/corePackages/Core.CrossCuttingConcers/Exceptions/AuthorizationProblemDetails.cs create mode 100644 src/corePackages/Core.CrossCuttingConcers/Exceptions/BusinessException.cs create mode 100644 src/corePackages/Core.CrossCuttingConcers/Exceptions/BusinessProblemDetails.cs create mode 100644 src/corePackages/Core.CrossCuttingConcers/Exceptions/ExceptionMiddleware.cs create mode 100644 src/corePackages/Core.CrossCuttingConcers/Exceptions/ExceptionMiddlewareExtensions.cs create mode 100644 src/corePackages/Core.CrossCuttingConcers/Exceptions/ValidationProblemDetails.cs create mode 100644 src/corePackages/Core.CrossCuttingConcers/Logging/LogDetail.cs create mode 100644 src/corePackages/Core.CrossCuttingConcers/Logging/LogDetailWithException.cs create mode 100644 src/corePackages/Core.CrossCuttingConcers/Logging/LogParameter.cs create mode 100644 src/corePackages/Core.CrossCuttingConcers/Logging/Serilog/ConfigurationModels/FileLogConfiguration.cs create mode 100644 src/corePackages/Core.CrossCuttingConcers/Logging/Serilog/Logger/FileLogger.cs create mode 100644 src/corePackages/Core.CrossCuttingConcers/Logging/Serilog/LoggerServiceBase.cs create mode 100644 src/corePackages/Core.CrossCuttingConcers/Logging/Serilog/Messages/SerilogMessages.cs create mode 100644 src/corePackages/Core.ElasticSearch/Class1.cs create mode 100644 src/corePackages/Core.ElasticSearch/Core.ElasticSearch.csproj create mode 100644 src/corePackages/Core.ElasticSearch/ElasticSearchManager.cs create mode 100644 src/corePackages/Core.ElasticSearch/IElasticSearch.cs create mode 100644 src/corePackages/Core.ElasticSearch/Models/ElasticSearchConfig.cs create mode 100644 src/corePackages/Core.ElasticSearch/Models/ElasticSearchGetModel.cs create mode 100644 src/corePackages/Core.ElasticSearch/Models/ElasticSearchInsertManyModel.cs create mode 100644 src/corePackages/Core.ElasticSearch/Models/ElasticSearchInsertUpdateModel.cs create mode 100644 src/corePackages/Core.ElasticSearch/Models/ElasticSearchModel.cs create mode 100644 src/corePackages/Core.ElasticSearch/Models/ElasticSearchResult.cs create mode 100644 src/corePackages/Core.ElasticSearch/Models/IElasticSearchResult.cs create mode 100644 src/corePackages/Core.ElasticSearch/Models/IndexModel.cs create mode 100644 src/corePackages/Core.ElasticSearch/Models/SearchByFieldParameters.cs create mode 100644 src/corePackages/Core.ElasticSearch/Models/SearchByQueryParameters.cs create mode 100644 src/corePackages/Core.ElasticSearch/Models/SearchParameters.cs create mode 100644 src/corePackages/Core.Mailing/Class1.cs create mode 100644 src/corePackages/Core.Mailing/Core.Mailing.csproj create mode 100644 src/corePackages/Core.Mailing/IMailService.cs create mode 100644 src/corePackages/Core.Mailing/Mail.cs create mode 100644 src/corePackages/Core.Mailing/MailKitImplementations/MailKitMailService.cs create mode 100644 src/corePackages/Core.Mailing/MailSettings.cs create mode 100644 src/corePackages/Core.Persistence/Class1.cs create mode 100644 src/corePackages/Core.Persistence/Core.Persistence.csproj create mode 100644 src/corePackages/Core.Persistence/Dynamic/Dynamic.cs create mode 100644 src/corePackages/Core.Persistence/Dynamic/Filter.cs create mode 100644 src/corePackages/Core.Persistence/Dynamic/IQueryableDynamicFilterExtensions.cs create mode 100644 src/corePackages/Core.Persistence/Dynamic/Sort.cs create mode 100644 src/corePackages/Core.Persistence/Paging/BasePageableModel.cs create mode 100644 src/corePackages/Core.Persistence/Paging/IPaginate.cs create mode 100644 src/corePackages/Core.Persistence/Paging/IQueryablePaginateExtensions.cs create mode 100644 src/corePackages/Core.Persistence/Paging/Paginate.cs create mode 100644 src/corePackages/Core.Persistence/Repositories/EfRepositoryBase.cs create mode 100644 src/corePackages/Core.Persistence/Repositories/Entity.cs create mode 100644 src/corePackages/Core.Persistence/Repositories/IAsyncRepository.cs create mode 100644 src/corePackages/Core.Persistence/Repositories/IQuery.cs create mode 100644 src/corePackages/Core.Persistence/Repositories/IRepository.cs create mode 100644 src/corePackages/Core.Security/Core.Security.csproj create mode 100644 src/corePackages/Core.Security/Dtos/UserForLoginDto.cs create mode 100644 src/corePackages/Core.Security/Dtos/UserForRegisterDto.cs create mode 100644 src/corePackages/Core.Security/EmailAuthenticator/EmailAuthenticatorHelper.cs create mode 100644 src/corePackages/Core.Security/EmailAuthenticator/IEmailAuthenticatorHelper.cs create mode 100644 src/corePackages/Core.Security/Encryption/SecurityKeyHelper.cs create mode 100644 src/corePackages/Core.Security/Encryption/SigningCredentialsHelper.cs create mode 100644 src/corePackages/Core.Security/Entities/EmailAuthenticator.cs create mode 100644 src/corePackages/Core.Security/Entities/OperationClaim.cs create mode 100644 src/corePackages/Core.Security/Entities/OtpAuthenticator.cs create mode 100644 src/corePackages/Core.Security/Entities/RefreshToken.cs create mode 100644 src/corePackages/Core.Security/Entities/User.cs create mode 100644 src/corePackages/Core.Security/Entities/UserOperationClaim.cs create mode 100644 src/corePackages/Core.Security/Enums/AuthenticatorType.cs create mode 100644 src/corePackages/Core.Security/Extensions/ClaimExtensions.cs create mode 100644 src/corePackages/Core.Security/Extensions/ClaimsPrincipalExtensions.cs create mode 100644 src/corePackages/Core.Security/Hashing/HashingHelper.cs create mode 100644 src/corePackages/Core.Security/JWT/AccessToken.cs create mode 100644 src/corePackages/Core.Security/JWT/ITokenHelper.cs create mode 100644 src/corePackages/Core.Security/JWT/JwtHelper.cs create mode 100644 src/corePackages/Core.Security/JWT/TokenOptions.cs create mode 100644 src/corePackages/Core.Security/OtpAuthenticator/IOtpAuthenticatorHelper.cs create mode 100644 src/corePackages/Core.Security/OtpAuthenticator/OtpNet/OtpNetOtpAuthenticatorHelper.cs create mode 100644 src/corePackages/Core.Security/SecurityServiceRegistration.cs diff --git a/NArchitecture.sln b/NArchitecture.sln index 06e28076..b7a691cc 100644 --- a/NArchitecture.sln +++ b/NArchitecture.sln @@ -32,6 +32,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "corePackages", "corePackages", "{F7AAFAB2-1A86-4672-A160-A1F8277CDE3E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Application", "src\corePackages\Core.Application\Core.Application.csproj", "{41A6CDA6-B909-4BC4-8466-51EE8CC03018}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.CrossCuttingConcers", "src\corePackages\Core.CrossCuttingConcers\Core.CrossCuttingConcers.csproj", "{ED379FAF-0F5A-42E4-AA07-715F8280E2FC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.ElasticSearch", "src\corePackages\Core.ElasticSearch\Core.ElasticSearch.csproj", "{4A5607F1-D28E-4E2D-ADCE-F94A3B3BD96D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Mailing", "src\corePackages\Core.Mailing\Core.Mailing.csproj", "{7CE04CAC-8306-4E5C-9524-67C3F3C66E88}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Persistence", "src\corePackages\Core.Persistence\Core.Persistence.csproj", "{1FE0F616-CEBE-4589-AE2E-DD14D0751CC2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Security", "src\corePackages\Core.Security\Core.Security.csproj", "{D20AC6FA-3133-40B1-B4B2-630F6D08ECF9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -62,6 +76,30 @@ Global {A06975C0-631D-4E8B-8106-D1C8E3A05D19}.Debug|Any CPU.Build.0 = Debug|Any CPU {A06975C0-631D-4E8B-8106-D1C8E3A05D19}.Release|Any CPU.ActiveCfg = Release|Any CPU {A06975C0-631D-4E8B-8106-D1C8E3A05D19}.Release|Any CPU.Build.0 = Release|Any CPU + {41A6CDA6-B909-4BC4-8466-51EE8CC03018}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {41A6CDA6-B909-4BC4-8466-51EE8CC03018}.Debug|Any CPU.Build.0 = Debug|Any CPU + {41A6CDA6-B909-4BC4-8466-51EE8CC03018}.Release|Any CPU.ActiveCfg = Release|Any CPU + {41A6CDA6-B909-4BC4-8466-51EE8CC03018}.Release|Any CPU.Build.0 = Release|Any CPU + {ED379FAF-0F5A-42E4-AA07-715F8280E2FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED379FAF-0F5A-42E4-AA07-715F8280E2FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED379FAF-0F5A-42E4-AA07-715F8280E2FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED379FAF-0F5A-42E4-AA07-715F8280E2FC}.Release|Any CPU.Build.0 = Release|Any CPU + {4A5607F1-D28E-4E2D-ADCE-F94A3B3BD96D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A5607F1-D28E-4E2D-ADCE-F94A3B3BD96D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A5607F1-D28E-4E2D-ADCE-F94A3B3BD96D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A5607F1-D28E-4E2D-ADCE-F94A3B3BD96D}.Release|Any CPU.Build.0 = Release|Any CPU + {7CE04CAC-8306-4E5C-9524-67C3F3C66E88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7CE04CAC-8306-4E5C-9524-67C3F3C66E88}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7CE04CAC-8306-4E5C-9524-67C3F3C66E88}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7CE04CAC-8306-4E5C-9524-67C3F3C66E88}.Release|Any CPU.Build.0 = Release|Any CPU + {1FE0F616-CEBE-4589-AE2E-DD14D0751CC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1FE0F616-CEBE-4589-AE2E-DD14D0751CC2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1FE0F616-CEBE-4589-AE2E-DD14D0751CC2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1FE0F616-CEBE-4589-AE2E-DD14D0751CC2}.Release|Any CPU.Build.0 = Release|Any CPU + {D20AC6FA-3133-40B1-B4B2-630F6D08ECF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D20AC6FA-3133-40B1-B4B2-630F6D08ECF9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D20AC6FA-3133-40B1-B4B2-630F6D08ECF9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D20AC6FA-3133-40B1-B4B2-630F6D08ECF9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -74,6 +112,13 @@ Global {5A5C5789-75A7-4773-AB44-13721819502A} = {F4CFDEC8-18F0-4A9C-A16B-3045B325FA69} {8696C5CF-50A9-41A3-848F-414D39A8FB21} = {F4CFDEC8-18F0-4A9C-A16B-3045B325FA69} {A06975C0-631D-4E8B-8106-D1C8E3A05D19} = {07E15C51-014F-4C05-B709-C273D2D4E78C} + {F7AAFAB2-1A86-4672-A160-A1F8277CDE3E} = {BC7CA20F-741B-4757-8833-EA38E62FC786} + {41A6CDA6-B909-4BC4-8466-51EE8CC03018} = {F7AAFAB2-1A86-4672-A160-A1F8277CDE3E} + {ED379FAF-0F5A-42E4-AA07-715F8280E2FC} = {F7AAFAB2-1A86-4672-A160-A1F8277CDE3E} + {4A5607F1-D28E-4E2D-ADCE-F94A3B3BD96D} = {F7AAFAB2-1A86-4672-A160-A1F8277CDE3E} + {7CE04CAC-8306-4E5C-9524-67C3F3C66E88} = {F7AAFAB2-1A86-4672-A160-A1F8277CDE3E} + {1FE0F616-CEBE-4589-AE2E-DD14D0751CC2} = {F7AAFAB2-1A86-4672-A160-A1F8277CDE3E} + {D20AC6FA-3133-40B1-B4B2-630F6D08ECF9} = {F7AAFAB2-1A86-4672-A160-A1F8277CDE3E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9208CFCE-156A-49CD-9E43-32CE67AAB957} diff --git a/src/corePackages/Core.Application/Core.Application.csproj b/src/corePackages/Core.Application/Core.Application.csproj new file mode 100644 index 00000000..8d089573 --- /dev/null +++ b/src/corePackages/Core.Application/Core.Application.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + + diff --git a/src/corePackages/Core.Application/Pipelines/Authorization/AuthorizationBehavior.cs b/src/corePackages/Core.Application/Pipelines/Authorization/AuthorizationBehavior.cs new file mode 100644 index 00000000..913624d0 --- /dev/null +++ b/src/corePackages/Core.Application/Pipelines/Authorization/AuthorizationBehavior.cs @@ -0,0 +1,33 @@ +using Core.CrossCuttingConcerns.Exceptions; +using Core.Security.Extensions; +using MediatR; +using Microsoft.AspNetCore.Http; +using Microsoft.IdentityModel.Tokens; + +namespace Core.Application.Pipelines.Authorization; + +public class AuthorizationBehavior : IPipelineBehavior + where TRequest : IRequest, ISecuredRequest +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public AuthorizationBehavior(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public async Task Handle(TRequest request, CancellationToken cancellationToken, + RequestHandlerDelegate next) + { + List? roleClaims = _httpContextAccessor.HttpContext.User.ClaimRoles(); + + if (roleClaims == null) throw new AuthorizationException("Claims not found."); + + bool isNotMatchedARoleClaimWithRequestRoles = + roleClaims.FirstOrDefault(roleClaim => request.Roles.Any(role => role == roleClaim)).IsNullOrEmpty(); + if (isNotMatchedARoleClaimWithRequestRoles) throw new AuthorizationException("You are not authorized."); + + TResponse response = await next(); + return response; + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Application/Pipelines/Authorization/ISecuredRequest.cs b/src/corePackages/Core.Application/Pipelines/Authorization/ISecuredRequest.cs new file mode 100644 index 00000000..3914cba8 --- /dev/null +++ b/src/corePackages/Core.Application/Pipelines/Authorization/ISecuredRequest.cs @@ -0,0 +1,6 @@ +namespace Core.Application.Pipelines.Authorization; + +public interface ISecuredRequest +{ + public string[] Roles { get; } +} \ No newline at end of file diff --git a/src/corePackages/Core.Application/Pipelines/Caching/CacheRemovingBehavior.cs b/src/corePackages/Core.Application/Pipelines/Caching/CacheRemovingBehavior.cs new file mode 100644 index 00000000..7040d80c --- /dev/null +++ b/src/corePackages/Core.Application/Pipelines/Caching/CacheRemovingBehavior.cs @@ -0,0 +1,38 @@ +using MediatR; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Logging; + +namespace Core.Application.Pipelines.Caching; + +public class CacheRemovingBehavior : IPipelineBehavior + where TRequest : IRequest, ICacheRemoverRequest +{ + private readonly IDistributedCache _cache; + private readonly ILogger> _logger; + + public CacheRemovingBehavior(IDistributedCache cache, ILogger> logger + ) + { + _cache = cache; + _logger = logger; + } + + public async Task Handle(TRequest request, CancellationToken cancellationToken, + RequestHandlerDelegate next) + { + TResponse response; + if (request.BypassCache) return await next(); + + async Task GetResponseAndRemoveCache() + { + response = await next(); + await _cache.RemoveAsync(request.CacheKey, cancellationToken); + return response; + } + + response = await GetResponseAndRemoveCache(); + _logger.LogInformation($"Removed Cache -> {request.CacheKey}"); + + return response; + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Application/Pipelines/Caching/CacheSettings.cs b/src/corePackages/Core.Application/Pipelines/Caching/CacheSettings.cs new file mode 100644 index 00000000..6d7d8d4a --- /dev/null +++ b/src/corePackages/Core.Application/Pipelines/Caching/CacheSettings.cs @@ -0,0 +1,6 @@ +namespace Core.Application.Pipelines.Caching; + +public class CacheSettings +{ + public int SlidingExpiration { get; set; } +} \ No newline at end of file diff --git a/src/corePackages/Core.Application/Pipelines/Caching/CachingBehavior.cs b/src/corePackages/Core.Application/Pipelines/Caching/CachingBehavior.cs new file mode 100644 index 00000000..ac5b7df9 --- /dev/null +++ b/src/corePackages/Core.Application/Pipelines/Caching/CachingBehavior.cs @@ -0,0 +1,57 @@ +using MediatR; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System.Text; + +namespace Core.Application.Pipelines.Caching; + +public class CachingBehavior : IPipelineBehavior + where TRequest : IRequest, ICachableRequest +{ + private readonly IDistributedCache _cache; + private readonly ILogger> _logger; + + private readonly CacheSettings _cacheSettings; + + public CachingBehavior(IDistributedCache cache, ILogger> logger, + IConfiguration configuration) + { + _cache = cache; + _logger = logger; + _cacheSettings = configuration.GetSection("CacheSettings").Get(); + } + + public async Task Handle(TRequest request, CancellationToken cancellationToken, + RequestHandlerDelegate next) + { + TResponse response; + if (request.BypassCache) return await next(); + + async Task GetResponseAndAddToCache() + { + response = await next(); + TimeSpan? slidingExpiration = + request.SlidingExpiration ?? TimeSpan.FromDays(_cacheSettings.SlidingExpiration); + DistributedCacheEntryOptions cacheOptions = new() { SlidingExpiration = slidingExpiration }; + byte[] serializeData = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(response)); + await _cache.SetAsync(request.CacheKey, serializeData, cacheOptions, cancellationToken); + return response; + } + + byte[]? cachedResponse = await _cache.GetAsync(request.CacheKey, cancellationToken); + if (cachedResponse != null) + { + response = JsonConvert.DeserializeObject(Encoding.Default.GetString(cachedResponse)); + _logger.LogInformation($"Fetched from Cache -> {request.CacheKey}"); + } + else + { + response = await GetResponseAndAddToCache(); + _logger.LogInformation($"Added to Cache -> {request.CacheKey}"); + } + + return response; + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Application/Pipelines/Caching/ICachableRequest.cs b/src/corePackages/Core.Application/Pipelines/Caching/ICachableRequest.cs new file mode 100644 index 00000000..a2b79db6 --- /dev/null +++ b/src/corePackages/Core.Application/Pipelines/Caching/ICachableRequest.cs @@ -0,0 +1,8 @@ +namespace Core.Application.Pipelines.Caching; + +public interface ICachableRequest +{ + bool BypassCache { get; } + string CacheKey { get; } + TimeSpan? SlidingExpiration { get; } +} \ No newline at end of file diff --git a/src/corePackages/Core.Application/Pipelines/Caching/ICacheRemoverRequest.cs b/src/corePackages/Core.Application/Pipelines/Caching/ICacheRemoverRequest.cs new file mode 100644 index 00000000..0f1f58f6 --- /dev/null +++ b/src/corePackages/Core.Application/Pipelines/Caching/ICacheRemoverRequest.cs @@ -0,0 +1,7 @@ +namespace Core.Application.Pipelines.Caching; + +public interface ICacheRemoverRequest +{ + bool BypassCache { get; } + string CacheKey { get; } +} \ No newline at end of file diff --git a/src/corePackages/Core.Application/Pipelines/Logging/ILoggableRequest.cs b/src/corePackages/Core.Application/Pipelines/Logging/ILoggableRequest.cs new file mode 100644 index 00000000..3a295fb2 --- /dev/null +++ b/src/corePackages/Core.Application/Pipelines/Logging/ILoggableRequest.cs @@ -0,0 +1,5 @@ +namespace Core.Application.Pipelines.Logging; + +public interface ILoggableRequest +{ +} \ No newline at end of file diff --git a/src/corePackages/Core.Application/Pipelines/Logging/LoggingBehavior.cs b/src/corePackages/Core.Application/Pipelines/Logging/LoggingBehavior.cs new file mode 100644 index 00000000..27d0fd28 --- /dev/null +++ b/src/corePackages/Core.Application/Pipelines/Logging/LoggingBehavior.cs @@ -0,0 +1,46 @@ +using Core.CrossCuttingConcerns.Logging; +using Core.CrossCuttingConcerns.Logging.Serilog; +using MediatR; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; + +namespace Core.Application.Pipelines.Logging; + +public class LoggingBehavior : IPipelineBehavior + where TRequest : IRequest, ILoggableRequest +{ + private readonly LoggerServiceBase _loggerServiceBase; + private readonly IHttpContextAccessor _httpContextAccessor; + + + public LoggingBehavior(LoggerServiceBase loggerServiceBase, IHttpContextAccessor httpContextAccessor) + { + _loggerServiceBase = loggerServiceBase; + _httpContextAccessor = httpContextAccessor; + } + + public Task Handle(TRequest request, CancellationToken cancellationToken, + RequestHandlerDelegate next) + { + List logParameters = new(); + logParameters.Add(new LogParameter + { + Type = request.GetType().Name, + Value = request + }); + + LogDetail logDetail = new() + { + MethodName = next.Method.Name, + Parameters = logParameters, + User = _httpContextAccessor.HttpContext == null || + _httpContextAccessor.HttpContext.User.Identity.Name == null + ? "?" + : _httpContextAccessor.HttpContext.User.Identity.Name + }; + + _loggerServiceBase.Info(JsonConvert.SerializeObject(logDetail)); + + return next(); + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Application/Pipelines/Validation/RequestValidationBehavior.cs b/src/corePackages/Core.Application/Pipelines/Validation/RequestValidationBehavior.cs new file mode 100644 index 00000000..6f988390 --- /dev/null +++ b/src/corePackages/Core.Application/Pipelines/Validation/RequestValidationBehavior.cs @@ -0,0 +1,29 @@ +using FluentValidation; +using FluentValidation.Results; +using MediatR; + +namespace Core.Application.Pipelines.Validation; + +public class RequestValidationBehavior : IPipelineBehavior + where TRequest : IRequest +{ + private readonly IEnumerable> _validators; + + public RequestValidationBehavior(IEnumerable> validators) + { + _validators = validators; + } + + public Task Handle(TRequest request, CancellationToken cancellationToken, + RequestHandlerDelegate next) + { + ValidationContext context = new(request); + List failures = _validators + .Select(validator => validator.Validate(context)) + .SelectMany(result => result.Errors) + .Where(failure => failure != null) + .ToList(); + if (failures.Count != 0) throw new ValidationException(failures); + return next(); + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Application/Pipelines/Validation/ValidationTool.cs b/src/corePackages/Core.Application/Pipelines/Validation/ValidationTool.cs new file mode 100644 index 00000000..a5968b79 --- /dev/null +++ b/src/corePackages/Core.Application/Pipelines/Validation/ValidationTool.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using FluentValidation.Results; + +namespace Core.Application.Pipelines.Validation; + +public class ValidationTool +{ + public static void Validate(IValidator validator, object entity) + { + ValidationContext context = new(entity); + ValidationResult result = validator.Validate(context); + if (!result.IsValid) throw new ValidationException(result.Errors); + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Application/Requests/PageRequest.cs b/src/corePackages/Core.Application/Requests/PageRequest.cs new file mode 100644 index 00000000..a51ebad7 --- /dev/null +++ b/src/corePackages/Core.Application/Requests/PageRequest.cs @@ -0,0 +1,7 @@ +namespace Core.Application.Requests; + +public class PageRequest +{ + public int Page { get; set; } + public int PageSize { get; set; } +} \ No newline at end of file diff --git a/src/corePackages/Core.CrossCuttingConcers/Core.CrossCuttingConcerns.csproj b/src/corePackages/Core.CrossCuttingConcers/Core.CrossCuttingConcerns.csproj new file mode 100644 index 00000000..edcfd0ae --- /dev/null +++ b/src/corePackages/Core.CrossCuttingConcers/Core.CrossCuttingConcerns.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + diff --git a/src/corePackages/Core.CrossCuttingConcers/Core.CrossCuttingConcers.csproj b/src/corePackages/Core.CrossCuttingConcers/Core.CrossCuttingConcers.csproj new file mode 100644 index 00000000..40798bf6 --- /dev/null +++ b/src/corePackages/Core.CrossCuttingConcers/Core.CrossCuttingConcers.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/src/corePackages/Core.CrossCuttingConcers/Exceptions/AuthorizationException.cs b/src/corePackages/Core.CrossCuttingConcers/Exceptions/AuthorizationException.cs new file mode 100644 index 00000000..3e3f25f3 --- /dev/null +++ b/src/corePackages/Core.CrossCuttingConcers/Exceptions/AuthorizationException.cs @@ -0,0 +1,8 @@ +namespace Core.CrossCuttingConcerns.Exceptions; + +public class AuthorizationException : Exception +{ + public AuthorizationException(string message) : base(message) + { + } +} \ No newline at end of file diff --git a/src/corePackages/Core.CrossCuttingConcers/Exceptions/AuthorizationProblemDetails.cs b/src/corePackages/Core.CrossCuttingConcers/Exceptions/AuthorizationProblemDetails.cs new file mode 100644 index 00000000..d5c64bb5 --- /dev/null +++ b/src/corePackages/Core.CrossCuttingConcers/Exceptions/AuthorizationProblemDetails.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; + +namespace Core.CrossCuttingConcerns.Exceptions; + +public class AuthorizationProblemDetails : ProblemDetails +{ + public override string ToString() => JsonConvert.SerializeObject(this); +} \ No newline at end of file diff --git a/src/corePackages/Core.CrossCuttingConcers/Exceptions/BusinessException.cs b/src/corePackages/Core.CrossCuttingConcers/Exceptions/BusinessException.cs new file mode 100644 index 00000000..1af6f403 --- /dev/null +++ b/src/corePackages/Core.CrossCuttingConcers/Exceptions/BusinessException.cs @@ -0,0 +1,8 @@ +namespace Core.CrossCuttingConcerns.Exceptions; + +public class BusinessException : Exception +{ + public BusinessException(string message) : base(message) + { + } +} \ No newline at end of file diff --git a/src/corePackages/Core.CrossCuttingConcers/Exceptions/BusinessProblemDetails.cs b/src/corePackages/Core.CrossCuttingConcers/Exceptions/BusinessProblemDetails.cs new file mode 100644 index 00000000..69a95005 --- /dev/null +++ b/src/corePackages/Core.CrossCuttingConcers/Exceptions/BusinessProblemDetails.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; + +namespace Core.CrossCuttingConcerns.Exceptions; + +public class BusinessProblemDetails : ProblemDetails +{ + public override string ToString() => JsonConvert.SerializeObject(this); +} \ No newline at end of file diff --git a/src/corePackages/Core.CrossCuttingConcers/Exceptions/ExceptionMiddleware.cs b/src/corePackages/Core.CrossCuttingConcers/Exceptions/ExceptionMiddleware.cs new file mode 100644 index 00000000..027ef4fd --- /dev/null +++ b/src/corePackages/Core.CrossCuttingConcers/Exceptions/ExceptionMiddleware.cs @@ -0,0 +1,97 @@ +using FluentValidation; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System.Net; + +namespace Core.CrossCuttingConcerns.Exceptions; + +public class ExceptionMiddleware +{ + private readonly RequestDelegate _next; + + public ExceptionMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext context) + { + try + { + await _next(context); + } + catch (Exception exception) + { + await HandleExceptionAsync(context, exception); + } + } + + private Task HandleExceptionAsync(HttpContext context, Exception exception) + { + context.Response.ContentType = "application/json"; + + if (exception.GetType() == typeof(ValidationException)) return CreateValidationException(context, exception); + if (exception.GetType() == typeof(BusinessException)) return CreateBusinessException(context, exception); + if (exception.GetType() == typeof(AuthorizationException)) + return CreateAuthorizationException(context, exception); + return CreateInternalException(context, exception); + } + + private Task CreateAuthorizationException(HttpContext context, Exception exception) + { + context.Response.StatusCode = Convert.ToInt32(HttpStatusCode.Unauthorized); + + return context.Response.WriteAsync(new AuthorizationProblemDetails + { + Status = StatusCodes.Status401Unauthorized, + Type = "https://example.com/probs/authorization", + Title = "Authorization exception", + Detail = exception.Message, + Instance = "" + }.ToString()); + } + + private Task CreateBusinessException(HttpContext context, Exception exception) + { + context.Response.StatusCode = Convert.ToInt32(HttpStatusCode.BadRequest); + + return context.Response.WriteAsync(new BusinessProblemDetails + { + Status = StatusCodes.Status400BadRequest, + Type = "https://example.com/probs/business", + Title = "Business exception", + Detail = exception.Message, + Instance = "" + }.ToString()); + } + + private Task CreateValidationException(HttpContext context, Exception exception) + { + context.Response.StatusCode = Convert.ToInt32(HttpStatusCode.BadRequest); + object errors = ((ValidationException)exception).Errors; + + return context.Response.WriteAsync(new ValidationProblemDetails + { + Status = StatusCodes.Status400BadRequest, + Type = "https://example.com/probs/validation", + Title = "Validation error(s)", + Detail = "", + Instance = "", + Errors = errors + }.ToString()); + } + + private Task CreateInternalException(HttpContext context, Exception exception) + { + context.Response.StatusCode = Convert.ToInt32(HttpStatusCode.InternalServerError); + + return context.Response.WriteAsync(new ProblemDetails + { + Status = StatusCodes.Status500InternalServerError, + Type = "https://example.com/probs/internal", + Title = "Internal exception", + Detail = exception.Message, + Instance = "" + }.ToString()); + } +} \ No newline at end of file diff --git a/src/corePackages/Core.CrossCuttingConcers/Exceptions/ExceptionMiddlewareExtensions.cs b/src/corePackages/Core.CrossCuttingConcers/Exceptions/ExceptionMiddlewareExtensions.cs new file mode 100644 index 00000000..2d27bea6 --- /dev/null +++ b/src/corePackages/Core.CrossCuttingConcers/Exceptions/ExceptionMiddlewareExtensions.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Builder; + +namespace Core.CrossCuttingConcerns.Exceptions; + +public static class ExceptionMiddlewareExtensions +{ + public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app) + { + app.UseMiddleware(); + } +} \ No newline at end of file diff --git a/src/corePackages/Core.CrossCuttingConcers/Exceptions/ValidationProblemDetails.cs b/src/corePackages/Core.CrossCuttingConcers/Exceptions/ValidationProblemDetails.cs new file mode 100644 index 00000000..64a27c47 --- /dev/null +++ b/src/corePackages/Core.CrossCuttingConcers/Exceptions/ValidationProblemDetails.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; + +namespace Core.CrossCuttingConcerns.Exceptions; + +public class ValidationProblemDetails : ProblemDetails +{ + public object Errors { get; set; } + + public override string ToString() => JsonConvert.SerializeObject(this); +} \ No newline at end of file diff --git a/src/corePackages/Core.CrossCuttingConcers/Logging/LogDetail.cs b/src/corePackages/Core.CrossCuttingConcers/Logging/LogDetail.cs new file mode 100644 index 00000000..0c1fe2bc --- /dev/null +++ b/src/corePackages/Core.CrossCuttingConcers/Logging/LogDetail.cs @@ -0,0 +1,9 @@ +namespace Core.CrossCuttingConcerns.Logging; + +public class LogDetail +{ + public string FullName { get; set; } + public string MethodName { get; set; } + public string User { get; set; } + public List Parameters { get; set; } +} \ No newline at end of file diff --git a/src/corePackages/Core.CrossCuttingConcers/Logging/LogDetailWithException.cs b/src/corePackages/Core.CrossCuttingConcers/Logging/LogDetailWithException.cs new file mode 100644 index 00000000..781202fc --- /dev/null +++ b/src/corePackages/Core.CrossCuttingConcers/Logging/LogDetailWithException.cs @@ -0,0 +1,6 @@ +namespace Core.CrossCuttingConcerns.Logging; + +public class LogDetailWithException : LogDetail +{ + public string ExceptionMessage { get; set; } +} \ No newline at end of file diff --git a/src/corePackages/Core.CrossCuttingConcers/Logging/LogParameter.cs b/src/corePackages/Core.CrossCuttingConcers/Logging/LogParameter.cs new file mode 100644 index 00000000..dd4f13e4 --- /dev/null +++ b/src/corePackages/Core.CrossCuttingConcers/Logging/LogParameter.cs @@ -0,0 +1,8 @@ +namespace Core.CrossCuttingConcerns.Logging; + +public class LogParameter +{ + public string Name { get; set; } + public object Value { get; set; } + public string Type { get; set; } +} \ No newline at end of file diff --git a/src/corePackages/Core.CrossCuttingConcers/Logging/Serilog/ConfigurationModels/FileLogConfiguration.cs b/src/corePackages/Core.CrossCuttingConcers/Logging/Serilog/ConfigurationModels/FileLogConfiguration.cs new file mode 100644 index 00000000..e5c7521a --- /dev/null +++ b/src/corePackages/Core.CrossCuttingConcers/Logging/Serilog/ConfigurationModels/FileLogConfiguration.cs @@ -0,0 +1,6 @@ +namespace Core.CrossCuttingConcerns.Logging.Serilog.ConfigurationModels; + +public class FileLogConfiguration +{ + public string FolderPath { get; set; } +} \ No newline at end of file diff --git a/src/corePackages/Core.CrossCuttingConcers/Logging/Serilog/Logger/FileLogger.cs b/src/corePackages/Core.CrossCuttingConcers/Logging/Serilog/Logger/FileLogger.cs new file mode 100644 index 00000000..640af046 --- /dev/null +++ b/src/corePackages/Core.CrossCuttingConcers/Logging/Serilog/Logger/FileLogger.cs @@ -0,0 +1,31 @@ +using Core.CrossCuttingConcerns.Logging.Serilog.ConfigurationModels; +using Core.CrossCuttingConcerns.Logging.Serilog.Messages; +using Microsoft.Extensions.Configuration; +using Serilog; + +namespace Core.CrossCuttingConcerns.Logging.Serilog.Logger; + +public class FileLogger : LoggerServiceBase +{ + private IConfiguration _configuration; + + public FileLogger(IConfiguration configuration) + { + _configuration = configuration; + + FileLogConfiguration logConfig = configuration.GetSection("SeriLogConfigurations:FileLogConfiguration") + .Get() ?? + throw new Exception(SerilogMessages.NullOptionsMessage); + + string logFilePath = string.Format("{0}{1}", Directory.GetCurrentDirectory() + logConfig.FolderPath, ".txt"); + + Logger = new LoggerConfiguration() + .WriteTo.File( + logFilePath, + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: null, + fileSizeLimitBytes: 5000000, + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}") + .CreateLogger(); + } +} \ No newline at end of file diff --git a/src/corePackages/Core.CrossCuttingConcers/Logging/Serilog/LoggerServiceBase.cs b/src/corePackages/Core.CrossCuttingConcers/Logging/Serilog/LoggerServiceBase.cs new file mode 100644 index 00000000..617fbfe6 --- /dev/null +++ b/src/corePackages/Core.CrossCuttingConcers/Logging/Serilog/LoggerServiceBase.cs @@ -0,0 +1,15 @@ +using Serilog; + +namespace Core.CrossCuttingConcerns.Logging.Serilog; + +public abstract class LoggerServiceBase +{ + protected ILogger Logger { get; set; } + + public void Verbose(string message) => Logger.Verbose(message); + public void Fatal(string message) => Logger.Fatal(message); + public void Info(string message) => Logger.Information(message); + public void Warn(string message) => Logger.Warning(message); + public void Debug(string message) => Logger.Debug(message); + public void Error(string message) => Logger.Error(message); +} \ No newline at end of file diff --git a/src/corePackages/Core.CrossCuttingConcers/Logging/Serilog/Messages/SerilogMessages.cs b/src/corePackages/Core.CrossCuttingConcers/Logging/Serilog/Messages/SerilogMessages.cs new file mode 100644 index 00000000..719baef4 --- /dev/null +++ b/src/corePackages/Core.CrossCuttingConcers/Logging/Serilog/Messages/SerilogMessages.cs @@ -0,0 +1,7 @@ +namespace Core.CrossCuttingConcerns.Logging.Serilog.Messages; + +public static class SerilogMessages +{ + public static string NullOptionsMessage => + "You have sent a blank value! Something went wrong. Please try again."; +} \ No newline at end of file diff --git a/src/corePackages/Core.ElasticSearch/Class1.cs b/src/corePackages/Core.ElasticSearch/Class1.cs new file mode 100644 index 00000000..3bcb35cf --- /dev/null +++ b/src/corePackages/Core.ElasticSearch/Class1.cs @@ -0,0 +1,6 @@ +namespace Core.ElasticSearch; + +public class Class1 +{ + +} diff --git a/src/corePackages/Core.ElasticSearch/Core.ElasticSearch.csproj b/src/corePackages/Core.ElasticSearch/Core.ElasticSearch.csproj new file mode 100644 index 00000000..cbc950b2 --- /dev/null +++ b/src/corePackages/Core.ElasticSearch/Core.ElasticSearch.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/src/corePackages/Core.ElasticSearch/ElasticSearchManager.cs b/src/corePackages/Core.ElasticSearch/ElasticSearchManager.cs new file mode 100644 index 00000000..c7de9d19 --- /dev/null +++ b/src/corePackages/Core.ElasticSearch/ElasticSearchManager.cs @@ -0,0 +1,187 @@ +using Core.ElasticSearch.Models; +using Elasticsearch.Net; +using Microsoft.Extensions.Configuration; +using Nest; +using Nest.JsonNetSerializer; +using Newtonsoft.Json; + +namespace Core.ElasticSearch; + +public class ElasticSearchManager : IElasticSearch +{ + private readonly ConnectionSettings _connectionSettings; + + public ElasticSearchManager(IConfiguration configuration) + { + ElasticSearchConfig? settings = configuration.GetSection("ElasticSearchConfig").Get(); + SingleNodeConnectionPool pool = new(new Uri(settings.ConnectionString)); + _connectionSettings = new ConnectionSettings(pool, (builtInSerializer, connectionSettings) => + new JsonNetSerializer( + builtInSerializer, connectionSettings, () => + new JsonSerializerSettings + { + ReferenceLoopHandling = + ReferenceLoopHandling.Ignore + })); + } + + + public IReadOnlyDictionary GetIndexList() + { + ElasticClient elasticClient = new(_connectionSettings); + return elasticClient.Indices.Get(new GetIndexRequest(Indices.All)).Indices; + } + + + public async Task InsertManyAsync(string indexName, object[] items) + { + ElasticClient elasticClient = GetElasticClient(indexName); + BulkResponse? response = await elasticClient.BulkAsync(a => + a.Index(indexName) + .IndexMany(items)); + + return new ElasticSearchResult( + response.IsValid, + response.IsValid ? "Success" : response.ServerError.Error.Reason); + } + + public async Task CreateNewIndexAsync(IndexModel indexModel) + { + ElasticClient elasticClient = GetElasticClient(indexModel.IndexName); + if (elasticClient.Indices.Exists(indexModel.IndexName).Exists) + return new ElasticSearchResult(false, "Index already exists"); + + CreateIndexResponse? response = await elasticClient.Indices.CreateAsync(indexModel.IndexName, se => + se.Settings(a => a.NumberOfReplicas( + indexModel.NumberOfReplicas) + .NumberOfShards( + indexModel.NumberOfShards)) + .Aliases(x => x.Alias(indexModel.AliasName))); + + return new ElasticSearchResult( + response.IsValid, + response.IsValid ? "Success" : response.ServerError.Error.Reason); + } + + + public async Task DeleteByElasticIdAsync(ElasticSearchModel model) + { + ElasticClient elasticClient = GetElasticClient(model.IndexName); + DeleteResponse? response = + await elasticClient.DeleteAsync(model.ElasticId, x => x.Index(model.IndexName)); + return new ElasticSearchResult( + response.IsValid, + response.IsValid ? "Success" : response.ServerError.Error.Reason); + } + + + public async Task>> GetAllSearch(SearchParameters parameters) + where T : class + { + Type type = typeof(T); + + ElasticClient elasticClient = GetElasticClient(parameters.IndexName); + ISearchResponse? searchResponse = await elasticClient.SearchAsync(s => s + .Index(Indices.Index(parameters.IndexName)) + .From(parameters.From) + .Size(parameters.Size)); + + + List> list = searchResponse.Hits.Select(x => new ElasticSearchGetModel + { + ElasticId = x.Id, + Item = x.Source + }).ToList(); + + return list; + } + + public async Task>> GetSearchByField(SearchByFieldParameters fieldParameters) + where T : class + { + ElasticClient elasticClient = GetElasticClient(fieldParameters.IndexName); + ISearchResponse? searchResponse = await elasticClient.SearchAsync(s => s + .Index(fieldParameters.IndexName) + .From(fieldParameters.From) + .Size(fieldParameters.Size)); + + List> list = searchResponse.Hits.Select(x => new ElasticSearchGetModel + { + ElasticId = x.Id, + Item = x.Source + }).ToList(); + + return list; + } + + + public async Task>> GetSearchBySimpleQueryString( + SearchByQueryParameters queryParameters) + where T : class + { + ElasticClient elasticClient = GetElasticClient(queryParameters.IndexName); + ISearchResponse? searchResponse = await elasticClient.SearchAsync(s => s + .Index(queryParameters.IndexName) + .From(queryParameters.From) + .Size(queryParameters.Size) + .MatchAll() + .Query(a => a.SimpleQueryString(c => c + .Name(queryParameters.QueryName) + .Boost(1.1) + .Fields(queryParameters.Fields) + .Query(queryParameters.Query) + .Analyzer("standard") + .DefaultOperator(Operator.Or) + .Flags(SimpleQueryStringFlags.And | + SimpleQueryStringFlags.Near) + .Lenient() + .AnalyzeWildcard(false) + .MinimumShouldMatch("30%") + .FuzzyPrefixLength(0) + .FuzzyMaxExpansions(50) + .FuzzyTranspositions() + .AutoGenerateSynonymsPhraseQuery( + false)))); + + List> list = searchResponse.Hits.Select(x => new ElasticSearchGetModel + { + ElasticId = x.Id, + Item = x.Source + }).ToList(); + + return list; + } + + + public async Task InsertAsync(ElasticSearchInsertUpdateModel model) + { + ElasticClient elasticClient = GetElasticClient(model.IndexName); + + IndexResponse? response = await elasticClient.IndexAsync(model.Item, i => i.Index(model.IndexName) + .Id(model.ElasticId) + .Refresh(Refresh.True)); + + return new ElasticSearchResult( + response.IsValid, + response.IsValid ? "Success" : response.ServerError.Error.Reason); + } + + public async Task UpdateByElasticIdAsync(ElasticSearchInsertUpdateModel model) + { + ElasticClient elasticClient = GetElasticClient(model.IndexName); + UpdateResponse? response = + await elasticClient.UpdateAsync(model.ElasticId, u => u.Index(model.IndexName).Doc(model.Item)); + return new ElasticSearchResult( + response.IsValid, + response.IsValid ? "Success" : response.ServerError.Error.Reason); + } + + + private ElasticClient GetElasticClient(string indexName) + { + if (string.IsNullOrEmpty(indexName)) + throw new ArgumentNullException(indexName, "Index name cannot be null or empty "); + + return new ElasticClient(_connectionSettings); + } +} \ No newline at end of file diff --git a/src/corePackages/Core.ElasticSearch/IElasticSearch.cs b/src/corePackages/Core.ElasticSearch/IElasticSearch.cs new file mode 100644 index 00000000..3448ea36 --- /dev/null +++ b/src/corePackages/Core.ElasticSearch/IElasticSearch.cs @@ -0,0 +1,24 @@ +using Core.ElasticSearch.Models; +using Nest; + +namespace Core.ElasticSearch; + +public interface IElasticSearch +{ + Task CreateNewIndexAsync(IndexModel indexModel); + Task InsertAsync(ElasticSearchInsertUpdateModel model); + Task InsertManyAsync(string indexName, object[] items); + IReadOnlyDictionary GetIndexList(); + + Task>> GetAllSearch(SearchParameters parameters) + where T : class; + + Task>> GetSearchByField(SearchByFieldParameters fieldParameters) + where T : class; + + Task>> GetSearchBySimpleQueryString(SearchByQueryParameters queryParameters) + where T : class; + + Task UpdateByElasticIdAsync(ElasticSearchInsertUpdateModel model); + Task DeleteByElasticIdAsync(ElasticSearchModel model); +} \ No newline at end of file diff --git a/src/corePackages/Core.ElasticSearch/Models/ElasticSearchConfig.cs b/src/corePackages/Core.ElasticSearch/Models/ElasticSearchConfig.cs new file mode 100644 index 00000000..ad06d185 --- /dev/null +++ b/src/corePackages/Core.ElasticSearch/Models/ElasticSearchConfig.cs @@ -0,0 +1,8 @@ +namespace Core.ElasticSearch.Models; + +public class ElasticSearchConfig +{ + public string ConnectionString { get; set; } + public string UserName { get; set; } + public string Password { get; set; } +} \ No newline at end of file diff --git a/src/corePackages/Core.ElasticSearch/Models/ElasticSearchGetModel.cs b/src/corePackages/Core.ElasticSearch/Models/ElasticSearchGetModel.cs new file mode 100644 index 00000000..850757b5 --- /dev/null +++ b/src/corePackages/Core.ElasticSearch/Models/ElasticSearchGetModel.cs @@ -0,0 +1,7 @@ +namespace Core.ElasticSearch.Models; + +public class ElasticSearchGetModel +{ + public string ElasticId { get; set; } + public T Item { get; set; } +} \ No newline at end of file diff --git a/src/corePackages/Core.ElasticSearch/Models/ElasticSearchInsertManyModel.cs b/src/corePackages/Core.ElasticSearch/Models/ElasticSearchInsertManyModel.cs new file mode 100644 index 00000000..91ac968c --- /dev/null +++ b/src/corePackages/Core.ElasticSearch/Models/ElasticSearchInsertManyModel.cs @@ -0,0 +1,6 @@ +namespace Core.ElasticSearch.Models; + +public class ElasticSearchInsertManyModel : ElasticSearchModel +{ + public object[] Items { get; set; } +} \ No newline at end of file diff --git a/src/corePackages/Core.ElasticSearch/Models/ElasticSearchInsertUpdateModel.cs b/src/corePackages/Core.ElasticSearch/Models/ElasticSearchInsertUpdateModel.cs new file mode 100644 index 00000000..a975ee7f --- /dev/null +++ b/src/corePackages/Core.ElasticSearch/Models/ElasticSearchInsertUpdateModel.cs @@ -0,0 +1,6 @@ +namespace Core.ElasticSearch.Models; + +public class ElasticSearchInsertUpdateModel : ElasticSearchModel +{ + public object Item { get; set; } +} \ No newline at end of file diff --git a/src/corePackages/Core.ElasticSearch/Models/ElasticSearchModel.cs b/src/corePackages/Core.ElasticSearch/Models/ElasticSearchModel.cs new file mode 100644 index 00000000..d98f49c3 --- /dev/null +++ b/src/corePackages/Core.ElasticSearch/Models/ElasticSearchModel.cs @@ -0,0 +1,9 @@ +using Nest; + +namespace Core.ElasticSearch.Models; + +public class ElasticSearchModel +{ + public Id ElasticId { get; set; } + public string IndexName { get; set; } +} \ No newline at end of file diff --git a/src/corePackages/Core.ElasticSearch/Models/ElasticSearchResult.cs b/src/corePackages/Core.ElasticSearch/Models/ElasticSearchResult.cs new file mode 100644 index 00000000..09baf4d1 --- /dev/null +++ b/src/corePackages/Core.ElasticSearch/Models/ElasticSearchResult.cs @@ -0,0 +1,17 @@ +namespace Core.ElasticSearch.Models; + +public class ElasticSearchResult : IElasticSearchResult //todo: refactor +{ + public ElasticSearchResult(bool success, string message) : this(success) + { + Message = message; + } + + public ElasticSearchResult(bool success) + { + Success = success; + } + + public bool Success { get; set; } + public string Message { get; set; } +} \ No newline at end of file diff --git a/src/corePackages/Core.ElasticSearch/Models/IElasticSearchResult.cs b/src/corePackages/Core.ElasticSearch/Models/IElasticSearchResult.cs new file mode 100644 index 00000000..1fd7eeca --- /dev/null +++ b/src/corePackages/Core.ElasticSearch/Models/IElasticSearchResult.cs @@ -0,0 +1,7 @@ +namespace Core.ElasticSearch.Models; + +public interface IElasticSearchResult +{ + bool Success { get; } + string Message { get; } +} \ No newline at end of file diff --git a/src/corePackages/Core.ElasticSearch/Models/IndexModel.cs b/src/corePackages/Core.ElasticSearch/Models/IndexModel.cs new file mode 100644 index 00000000..a48ba87b --- /dev/null +++ b/src/corePackages/Core.ElasticSearch/Models/IndexModel.cs @@ -0,0 +1,9 @@ +namespace Core.ElasticSearch.Models; + +public class IndexModel +{ + public string IndexName { get; set; } + public string AliasName { get; set; } + public int NumberOfReplicas { get; set; } = 3; + public int NumberOfShards { get; set; } = 3; +} \ No newline at end of file diff --git a/src/corePackages/Core.ElasticSearch/Models/SearchByFieldParameters.cs b/src/corePackages/Core.ElasticSearch/Models/SearchByFieldParameters.cs new file mode 100644 index 00000000..991ca97e --- /dev/null +++ b/src/corePackages/Core.ElasticSearch/Models/SearchByFieldParameters.cs @@ -0,0 +1,7 @@ +namespace Core.ElasticSearch.Models; + +public class SearchByFieldParameters : SearchParameters +{ + public string FieldName { get; set; } + public string Value { get; set; } +} \ No newline at end of file diff --git a/src/corePackages/Core.ElasticSearch/Models/SearchByQueryParameters.cs b/src/corePackages/Core.ElasticSearch/Models/SearchByQueryParameters.cs new file mode 100644 index 00000000..57bdefc8 --- /dev/null +++ b/src/corePackages/Core.ElasticSearch/Models/SearchByQueryParameters.cs @@ -0,0 +1,8 @@ +namespace Core.ElasticSearch.Models; + +public class SearchByQueryParameters : SearchParameters +{ + public string QueryName { get; set; } + public string Query { get; set; } + public string[] Fields { get; set; } +} \ No newline at end of file diff --git a/src/corePackages/Core.ElasticSearch/Models/SearchParameters.cs b/src/corePackages/Core.ElasticSearch/Models/SearchParameters.cs new file mode 100644 index 00000000..5d8bf522 --- /dev/null +++ b/src/corePackages/Core.ElasticSearch/Models/SearchParameters.cs @@ -0,0 +1,8 @@ +namespace Core.ElasticSearch.Models; + +public class SearchParameters +{ + public string IndexName { get; set; } + public int From { get; set; } = 0; + public int Size { get; set; } = 10; +} \ No newline at end of file diff --git a/src/corePackages/Core.Mailing/Class1.cs b/src/corePackages/Core.Mailing/Class1.cs new file mode 100644 index 00000000..c43ccaf1 --- /dev/null +++ b/src/corePackages/Core.Mailing/Class1.cs @@ -0,0 +1,6 @@ +namespace Core.Mailing; + +public class Class1 +{ + +} diff --git a/src/corePackages/Core.Mailing/Core.Mailing.csproj b/src/corePackages/Core.Mailing/Core.Mailing.csproj new file mode 100644 index 00000000..0562e7a6 --- /dev/null +++ b/src/corePackages/Core.Mailing/Core.Mailing.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + + + + + + + + + + diff --git a/src/corePackages/Core.Mailing/IMailService.cs b/src/corePackages/Core.Mailing/IMailService.cs new file mode 100644 index 00000000..5edc5b58 --- /dev/null +++ b/src/corePackages/Core.Mailing/IMailService.cs @@ -0,0 +1,6 @@ +namespace Core.Mailing; + +public interface IMailService +{ + void SendMail(Mail mail); +} \ No newline at end of file diff --git a/src/corePackages/Core.Mailing/Mail.cs b/src/corePackages/Core.Mailing/Mail.cs new file mode 100644 index 00000000..0ddc2921 --- /dev/null +++ b/src/corePackages/Core.Mailing/Mail.cs @@ -0,0 +1,28 @@ +using MimeKit; + +namespace Core.Mailing; + +public class Mail +{ + public string Subject { get; set; } + public string TextBody { get; set; } + public string HtmlBody { get; set; } + public AttachmentCollection? Attachments { get; set; } + public string ToFullName { get; set; } + public string ToEmail { get; set; } + + public Mail() + { + } + + public Mail(string subject, string textBody, string htmlBody, AttachmentCollection? attachments, string toFullName, + string toEmail) + { + Subject = subject; + TextBody = textBody; + HtmlBody = htmlBody; + Attachments = attachments; + ToFullName = toFullName; + ToEmail = toEmail; + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Mailing/MailKitImplementations/MailKitMailService.cs b/src/corePackages/Core.Mailing/MailKitImplementations/MailKitMailService.cs new file mode 100644 index 00000000..128e68df --- /dev/null +++ b/src/corePackages/Core.Mailing/MailKitImplementations/MailKitMailService.cs @@ -0,0 +1,46 @@ +using MailKit.Net.Smtp; +using Microsoft.Extensions.Configuration; +using MimeKit; + +namespace Core.Mailing.MailKitImplementations; + +public class MailKitMailService : IMailService +{ + private IConfiguration _configuration; + private readonly MailSettings _mailSettings; + + public MailKitMailService(IConfiguration configuration) + { + _configuration = configuration; + _mailSettings = configuration.GetSection("MailSettings").Get(); + } + + public void SendMail(Mail mail) + { + MimeMessage email = new(); + + email.From.Add(new MailboxAddress(_mailSettings.SenderFullName, _mailSettings.SenderEmail)); + + email.To.Add(new MailboxAddress(mail.ToFullName, mail.ToEmail)); + + email.Subject = mail.Subject; + + BodyBuilder bodyBuilder = new() + { + TextBody = mail.TextBody, + HtmlBody = mail.HtmlBody + }; + + if (mail.Attachments != null) + foreach (MimeEntity? attachment in mail.Attachments) + bodyBuilder.Attachments.Add(attachment); + + email.Body = bodyBuilder.ToMessageBody(); + + using SmtpClient smtp = new(); + smtp.Connect(_mailSettings.Server, _mailSettings.Port); + //smtp.Authenticate(_mailSettings.UserName, _mailSettings.Password); + smtp.Send(email); + smtp.Disconnect(true); + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Mailing/MailSettings.cs b/src/corePackages/Core.Mailing/MailSettings.cs new file mode 100644 index 00000000..4f7cfbdc --- /dev/null +++ b/src/corePackages/Core.Mailing/MailSettings.cs @@ -0,0 +1,26 @@ +namespace Core.Mailing; + +public class MailSettings +{ + public string Server { get; set; } + public int Port { get; set; } + public string SenderFullName { get; set; } + public string SenderEmail { get; set; } + public string UserName { get; set; } + public string Password { get; set; } + + public MailSettings() + { + } + + public MailSettings(string server, int port, string senderFullName, string senderEmail, string userName, + string password) + { + Server = server; + Port = port; + SenderFullName = senderFullName; + SenderEmail = senderEmail; + UserName = userName; + Password = password; + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Persistence/Class1.cs b/src/corePackages/Core.Persistence/Class1.cs new file mode 100644 index 00000000..cc61827d --- /dev/null +++ b/src/corePackages/Core.Persistence/Class1.cs @@ -0,0 +1,6 @@ +namespace Core.Persistence; + +public class Class1 +{ + +} diff --git a/src/corePackages/Core.Persistence/Core.Persistence.csproj b/src/corePackages/Core.Persistence/Core.Persistence.csproj new file mode 100644 index 00000000..e6087c8f --- /dev/null +++ b/src/corePackages/Core.Persistence/Core.Persistence.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/src/corePackages/Core.Persistence/Dynamic/Dynamic.cs b/src/corePackages/Core.Persistence/Dynamic/Dynamic.cs new file mode 100644 index 00000000..e06e7a08 --- /dev/null +++ b/src/corePackages/Core.Persistence/Dynamic/Dynamic.cs @@ -0,0 +1,17 @@ +namespace Core.Persistence.Dynamic; + +public class Dynamic +{ + public IEnumerable? Sort { get; set; } + public Filter? Filter { get; set; } + + public Dynamic() + { + } + + public Dynamic(IEnumerable? sort, Filter? filter) + { + Sort = sort; + Filter = filter; + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Persistence/Dynamic/Filter.cs b/src/corePackages/Core.Persistence/Dynamic/Filter.cs new file mode 100644 index 00000000..c679416a --- /dev/null +++ b/src/corePackages/Core.Persistence/Dynamic/Filter.cs @@ -0,0 +1,23 @@ +namespace Core.Persistence.Dynamic; + +public class Filter +{ + public string Field { get; set; } + public string Operator { get; set; } + public string? Value { get; set; } + public string? Logic { get; set; } + public IEnumerable? Filters { get; set; } + + public Filter() + { + } + + public Filter(string field, string @operator, string? value, string? logic, IEnumerable? filters) : this() + { + Field = field; + Operator = @operator; + Value = value; + Logic = logic; + Filters = filters; + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Persistence/Dynamic/IQueryableDynamicFilterExtensions.cs b/src/corePackages/Core.Persistence/Dynamic/IQueryableDynamicFilterExtensions.cs new file mode 100644 index 00000000..7a863f31 --- /dev/null +++ b/src/corePackages/Core.Persistence/Dynamic/IQueryableDynamicFilterExtensions.cs @@ -0,0 +1,99 @@ +using System.Linq.Dynamic.Core; +using System.Text; + +namespace Core.Persistence.Dynamic; + +public static class IQueryableDynamicFilterExtensions +{ + private static readonly IDictionary + Operators = new Dictionary + { + { "eq", "=" }, + { "neq", "!=" }, + { "lt", "<" }, + { "lte", "<=" }, + { "gt", ">" }, + { "gte", ">=" }, + { "isnull", "== null" }, + { "isnotnull", "!= null" }, + { "startswith", "StartsWith" }, + { "endswith", "EndsWith" }, + { "contains", "Contains" }, + { "doesnotcontain", "Contains" } + }; + + public static IQueryable ToDynamic( + this IQueryable query, Dynamic dynamic) + { + if (dynamic.Filter is not null) query = Filter(query, dynamic.Filter); + if (dynamic.Sort is not null && dynamic.Sort.Any()) query = Sort(query, dynamic.Sort); + return query; + } + + private static IQueryable Filter( + IQueryable queryable, Filter filter) + { + IList filters = GetAllFilters(filter); + string?[] values = filters.Select(f => f.Value).ToArray(); + string where = Transform(filter, filters); + queryable = queryable.Where(where, values); + + return queryable; + } + + private static IQueryable Sort( + IQueryable queryable, IEnumerable sort) + { + if (sort.Any()) + { + string ordering = string.Join(",", sort.Select(s => $"{s.Field} {s.Dir}")); + return queryable.OrderBy(ordering); + } + + return queryable; + } + + public static IList GetAllFilters(Filter filter) + { + List filters = new(); + GetFilters(filter, filters); + return filters; + } + + private static void GetFilters(Filter filter, IList filters) + { + filters.Add(filter); + if (filter.Filters is not null && filter.Filters.Any()) + foreach (Filter item in filter.Filters) + GetFilters(item, filters); + } + + public static string Transform(Filter filter, IList filters) + { + int index = filters.IndexOf(filter); + string comparison = Operators[filter.Operator]; + StringBuilder where = new(); + + if (!string.IsNullOrEmpty(filter.Value)) + { + if (filter.Operator == "doesnotcontain") + where.Append($"(!np({filter.Field}).{comparison}(@{index}))"); + else if (comparison == "StartsWith" || + comparison == "EndsWith" || + comparison == "Contains") + where.Append($"(np({filter.Field}).{comparison}(@{index}))"); + else + where.Append($"np({filter.Field}) {comparison} @{index}"); + } + else if (filter.Operator == "isnull" || filter.Operator == "isnotnull") + { + where.Append($"np({filter.Field}) {comparison}"); + } + + if (filter.Logic is not null && filter.Filters is not null && filter.Filters.Any()) + return + $"{where} {filter.Logic} ({string.Join($" {filter.Logic} ", filter.Filters.Select(f => Transform(f, filters)).ToArray())})"; + + return where.ToString(); + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Persistence/Dynamic/Sort.cs b/src/corePackages/Core.Persistence/Dynamic/Sort.cs new file mode 100644 index 00000000..07d73cbc --- /dev/null +++ b/src/corePackages/Core.Persistence/Dynamic/Sort.cs @@ -0,0 +1,17 @@ +namespace Core.Persistence.Dynamic; + +public class Sort +{ + public string Field { get; set; } + public string Dir { get; set; } + + public Sort() + { + } + + public Sort(string field, string dir) + { + Field = field; + Dir = dir; + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Persistence/Paging/BasePageableModel.cs b/src/corePackages/Core.Persistence/Paging/BasePageableModel.cs new file mode 100644 index 00000000..57ef98d1 --- /dev/null +++ b/src/corePackages/Core.Persistence/Paging/BasePageableModel.cs @@ -0,0 +1,11 @@ +namespace Core.Persistence.Paging; + +public class BasePageableModel +{ + public int Index { get; set; } + public int Size { get; set; } + public int Count { get; set; } + public int Pages { get; set; } + public bool HasPrevious { get; set; } + public bool HasNext { get; set; } +} \ No newline at end of file diff --git a/src/corePackages/Core.Persistence/Paging/IPaginate.cs b/src/corePackages/Core.Persistence/Paging/IPaginate.cs new file mode 100644 index 00000000..14c0fe61 --- /dev/null +++ b/src/corePackages/Core.Persistence/Paging/IPaginate.cs @@ -0,0 +1,13 @@ +namespace Core.Persistence.Paging; + +public interface IPaginate +{ + int From { get; } + int Index { get; } + int Size { get; } + int Count { get; } + int Pages { get; } + IList Items { get; } + bool HasPrevious { get; } + bool HasNext { get; } +} \ No newline at end of file diff --git a/src/corePackages/Core.Persistence/Paging/IQueryablePaginateExtensions.cs b/src/corePackages/Core.Persistence/Paging/IQueryablePaginateExtensions.cs new file mode 100644 index 00000000..77a19fce --- /dev/null +++ b/src/corePackages/Core.Persistence/Paging/IQueryablePaginateExtensions.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore; + +namespace Core.Persistence.Paging; + +public static class IQueryablePaginateExtensions +{ + public static async Task> ToPaginateAsync(this IQueryable source, int index, int size, + int from = 0, + CancellationToken cancellationToken = default) + { + if (from > index) throw new ArgumentException($"From: {from} > Index: {index}, must from <= Index"); + + int count = await source.CountAsync(cancellationToken).ConfigureAwait(false); + List items = await source.Skip((index - from) * size).Take(size).ToListAsync(cancellationToken) + .ConfigureAwait(false); + Paginate list = new() + { + Index = index, + Size = size, + From = from, + Count = count, + Items = items, + Pages = (int)Math.Ceiling(count / (double)size) + }; + return list; + } + + + public static IPaginate ToPaginate(this IQueryable source, int index, int size, + int from = 0) + { + if (from > index) throw new ArgumentException($"From: {from} > Index: {index}, must from <= Index"); + + int count = source.Count(); + List items = source.Skip((index - from) * size).Take(size).ToList(); + Paginate list = new() + { + Index = index, + Size = size, + From = from, + Count = count, + Items = items, + Pages = (int)Math.Ceiling(count / (double)size) + }; + return list; + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Persistence/Paging/Paginate.cs b/src/corePackages/Core.Persistence/Paging/Paginate.cs new file mode 100644 index 00000000..333d1a5e --- /dev/null +++ b/src/corePackages/Core.Persistence/Paging/Paginate.cs @@ -0,0 +1,126 @@ +namespace Core.Persistence.Paging; + +public class Paginate : IPaginate +{ + internal Paginate(IEnumerable source, int index, int size, int from) + { + var enumerable = source as T[] ?? source.ToArray(); + + if (from > index) + throw new ArgumentException($"indexFrom: {from} > pageIndex: {index}, must indexFrom <= pageIndex"); + + if (source is IQueryable querable) + { + Index = index; + Size = size; + From = from; + Count = querable.Count(); + Pages = (int)Math.Ceiling(Count / (double)Size); + + Items = querable.Skip((Index - From) * Size).Take(Size).ToList(); + } + else + { + Index = index; + Size = size; + From = from; + + Count = enumerable.Count(); + Pages = (int)Math.Ceiling(Count / (double)Size); + + Items = enumerable.Skip((Index - From) * Size).Take(Size).ToList(); + } + } + + internal Paginate() + { + Items = new T[0]; + } + + public int From { get; set; } + public int Index { get; set; } + public int Size { get; set; } + public int Count { get; set; } + public int Pages { get; set; } + public IList Items { get; set; } + public bool HasPrevious => Index - From > 0; + public bool HasNext => Index - From + 1 < Pages; +} + +internal class Paginate : IPaginate +{ + public Paginate(IEnumerable source, Func, IEnumerable> converter, + int index, int size, int from) + { + var enumerable = source as TSource[] ?? source.ToArray(); + + if (from > index) throw new ArgumentException($"From: {from} > Index: {index}, must From <= Index"); + + if (source is IQueryable queryable) + { + Index = index; + Size = size; + From = from; + Count = queryable.Count(); + Pages = (int)Math.Ceiling(Count / (double)Size); + + var items = queryable.Skip((Index - From) * Size).Take(Size).ToArray(); + + Items = new List(converter(items)); + } + else + { + Index = index; + Size = size; + From = from; + Count = enumerable.Count(); + Pages = (int)Math.Ceiling(Count / (double)Size); + + var items = enumerable.Skip((Index - From) * Size).Take(Size).ToArray(); + + Items = new List(converter(items)); + } + } + + + public Paginate(IPaginate source, Func, IEnumerable> converter) + { + Index = source.Index; + Size = source.Size; + From = source.From; + Count = source.Count; + Pages = source.Pages; + + Items = new List(converter(source.Items)); + } + + public int Index { get; } + + public int Size { get; } + + public int Count { get; } + + public int Pages { get; } + + public int From { get; } + + public IList Items { get; } + + public bool HasPrevious => Index - From > 0; + + public bool HasNext => Index - From + 1 < Pages; +} + +public static class Paginate +{ + public static IPaginate Empty() + { + return new Paginate(); + } + + public static IPaginate From(IPaginate source, + Func, IEnumerable> converter) + { + return new Paginate(source, converter); + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Persistence/Repositories/EfRepositoryBase.cs b/src/corePackages/Core.Persistence/Repositories/EfRepositoryBase.cs new file mode 100644 index 00000000..8454cd6a --- /dev/null +++ b/src/corePackages/Core.Persistence/Repositories/EfRepositoryBase.cs @@ -0,0 +1,133 @@ +using System.Linq.Expressions; +using Core.Persistence.Dynamic; +using Core.Persistence.Paging; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; + +namespace Core.Persistence.Repositories; + +public class EfRepositoryBase : IAsyncRepository, IRepository + where TEntity : Entity + where TContext : DbContext +{ + protected TContext Context { get; } + + public EfRepositoryBase(TContext context) + { + Context = context; + } + + public async Task GetAsync(Expression> predicate) + { + return await Context.Set().FirstOrDefaultAsync(predicate); + } + + public async Task> GetListAsync(Expression>? predicate = null, + Func, IOrderedQueryable>? orderBy = + null, + Func, IIncludableQueryable>? + include = null, + int index = 0, int size = 10, bool enableTracking = true, + CancellationToken cancellationToken = default) + { + IQueryable queryable = Query(); + if (!enableTracking) queryable = queryable.AsNoTracking(); + if (include != null) queryable = include(queryable); + if (predicate != null) queryable = queryable.Where(predicate); + if (orderBy != null) + return await orderBy(queryable).ToPaginateAsync(index, size, 0, cancellationToken); + return await queryable.ToPaginateAsync(index, size, 0, cancellationToken); + } + + public async Task> GetListByDynamicAsync(Dynamic.Dynamic dynamic, + Func, + IIncludableQueryable>? + include = null, + int index = 0, int size = 10, + bool enableTracking = true, + CancellationToken cancellationToken = default) + { + IQueryable queryable = Query().AsQueryable().ToDynamic(dynamic); + if (!enableTracking) queryable = queryable.AsNoTracking(); + if (include != null) queryable = include(queryable); + return await queryable.ToPaginateAsync(index, size, 0, cancellationToken); + } + + public IQueryable Query() + { + return Context.Set(); + } + + public async Task AddAsync(TEntity entity) + { + Context.Entry(entity).State = EntityState.Added; + await Context.SaveChangesAsync(); + return entity; + } + + public async Task UpdateAsync(TEntity entity) + { + Context.Entry(entity).State = EntityState.Modified; + await Context.SaveChangesAsync(); + return entity; + } + + public async Task DeleteAsync(TEntity entity) + { + Context.Entry(entity).State = EntityState.Deleted; + await Context.SaveChangesAsync(); + return entity; + } + + public TEntity? Get(Expression> predicate) + { + return Context.Set().FirstOrDefault(predicate); + } + + public IPaginate GetList(Expression>? predicate = null, + Func, IOrderedQueryable>? orderBy = null, + Func, IIncludableQueryable>? include = null, + int index = 0, int size = 10, + bool enableTracking = true) + { + IQueryable queryable = Query(); + if (!enableTracking) queryable = queryable.AsNoTracking(); + if (include != null) queryable = include(queryable); + if (predicate != null) queryable = queryable.Where(predicate); + if (orderBy != null) + return orderBy(queryable).ToPaginate(index, size); + return queryable.ToPaginate(index, size); + } + + public IPaginate GetListByDynamic(Dynamic.Dynamic dynamic, + Func, IIncludableQueryable>? + include = null, int index = 0, int size = 10, + bool enableTracking = true) + { + IQueryable queryable = Query().AsQueryable().ToDynamic(dynamic); + if (!enableTracking) queryable = queryable.AsNoTracking(); + if (include != null) queryable = include(queryable); + return queryable.ToPaginate(index, size); + } + + public TEntity Add(TEntity entity) + { + Context.Entry(entity).State = EntityState.Added; + Context.SaveChanges(); + return entity; + } + + public TEntity Update(TEntity entity) + { + Context.Entry(entity).State = EntityState.Modified; + Context.SaveChanges(); + return entity; + } + + public TEntity Delete(TEntity entity) + { + Context.Entry(entity).State = EntityState.Deleted; + Context.SaveChanges(); + return entity; + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Persistence/Repositories/Entity.cs b/src/corePackages/Core.Persistence/Repositories/Entity.cs new file mode 100644 index 00000000..3ec635c8 --- /dev/null +++ b/src/corePackages/Core.Persistence/Repositories/Entity.cs @@ -0,0 +1,15 @@ +namespace Core.Persistence.Repositories; + +public class Entity +{ + public int Id { get; set; } + + public Entity() + { + } + + public Entity(int id) : this() + { + Id = id; + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Persistence/Repositories/IAsyncRepository.cs b/src/corePackages/Core.Persistence/Repositories/IAsyncRepository.cs new file mode 100644 index 00000000..38b99bd5 --- /dev/null +++ b/src/corePackages/Core.Persistence/Repositories/IAsyncRepository.cs @@ -0,0 +1,25 @@ +using System.Linq.Expressions; +using Core.Persistence.Paging; +using Microsoft.EntityFrameworkCore.Query; + +namespace Core.Persistence.Repositories; + +public interface IAsyncRepository : IQuery where T : Entity +{ + Task GetAsync(Expression> predicate); + + Task> GetListAsync(Expression>? predicate = null, + Func, IOrderedQueryable>? orderBy = null, + Func, IIncludableQueryable>? include = null, + int index = 0, int size = 10, bool enableTracking = true, + CancellationToken cancellationToken = default); + + Task> GetListByDynamicAsync(Dynamic.Dynamic dynamic, + Func, IIncludableQueryable>? include = null, + int index = 0, int size = 10, bool enableTracking = true, + CancellationToken cancellationToken = default); + + Task AddAsync(T entity); + Task UpdateAsync(T entity); + Task DeleteAsync(T entity); +} \ No newline at end of file diff --git a/src/corePackages/Core.Persistence/Repositories/IQuery.cs b/src/corePackages/Core.Persistence/Repositories/IQuery.cs new file mode 100644 index 00000000..e53d14f3 --- /dev/null +++ b/src/corePackages/Core.Persistence/Repositories/IQuery.cs @@ -0,0 +1,6 @@ +namespace Core.Persistence.Repositories; + +public interface IQuery +{ + IQueryable Query(); +} \ No newline at end of file diff --git a/src/corePackages/Core.Persistence/Repositories/IRepository.cs b/src/corePackages/Core.Persistence/Repositories/IRepository.cs new file mode 100644 index 00000000..472100cc --- /dev/null +++ b/src/corePackages/Core.Persistence/Repositories/IRepository.cs @@ -0,0 +1,24 @@ +using System.Linq.Expressions; +using Core.Persistence.Paging; +using Microsoft.EntityFrameworkCore.Query; + +namespace Core.Persistence.Repositories; + +public interface IRepository : IQuery where T : Entity +{ + T Get(Expression> predicate); + + IPaginate GetList(Expression>? predicate = null, + Func, IOrderedQueryable>? orderBy = null, + Func, IIncludableQueryable>? include = null, + int index = 0, int size = 10, + bool enableTracking = true); + + IPaginate GetListByDynamic(Dynamic.Dynamic dynamic, + Func, IIncludableQueryable>? include = null, + int index = 0, int size = 10, bool enableTracking = true); + + T Add(T entity); + T Update(T entity); + T Delete(T entity); +} \ No newline at end of file diff --git a/src/corePackages/Core.Security/Core.Security.csproj b/src/corePackages/Core.Security/Core.Security.csproj new file mode 100644 index 00000000..177378ed --- /dev/null +++ b/src/corePackages/Core.Security/Core.Security.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + + diff --git a/src/corePackages/Core.Security/Dtos/UserForLoginDto.cs b/src/corePackages/Core.Security/Dtos/UserForLoginDto.cs new file mode 100644 index 00000000..a316edd1 --- /dev/null +++ b/src/corePackages/Core.Security/Dtos/UserForLoginDto.cs @@ -0,0 +1,8 @@ +namespace Core.Security.Dtos; + +public class UserForLoginDto +{ + public string Email { get; set; } + public string Password { get; set; } + public string? AuthenticatorCode { get; set; } +} \ No newline at end of file diff --git a/src/corePackages/Core.Security/Dtos/UserForRegisterDto.cs b/src/corePackages/Core.Security/Dtos/UserForRegisterDto.cs new file mode 100644 index 00000000..9918cafa --- /dev/null +++ b/src/corePackages/Core.Security/Dtos/UserForRegisterDto.cs @@ -0,0 +1,9 @@ +namespace Core.Security.Dtos; + +public class UserForRegisterDto +{ + public string Email { get; set; } + public string Password { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } +} \ No newline at end of file diff --git a/src/corePackages/Core.Security/EmailAuthenticator/EmailAuthenticatorHelper.cs b/src/corePackages/Core.Security/EmailAuthenticator/EmailAuthenticatorHelper.cs new file mode 100644 index 00000000..781f7a41 --- /dev/null +++ b/src/corePackages/Core.Security/EmailAuthenticator/EmailAuthenticatorHelper.cs @@ -0,0 +1,18 @@ +using System.Security.Cryptography; + +namespace Core.Security.EmailAuthenticator; + +public class EmailAuthenticatorHelper : IEmailAuthenticatorHelper +{ + public Task CreateEmailActivationKey() + { + string key = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)); + return Task.FromResult(key); + } + + public Task CreateEmailActivationCode() + { + string code = RandomNumberGenerator.GetInt32(Convert.ToInt32(Math.Pow(10, 6))).ToString().PadLeft(6, '0'); + return Task.FromResult(code); + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Security/EmailAuthenticator/IEmailAuthenticatorHelper.cs b/src/corePackages/Core.Security/EmailAuthenticator/IEmailAuthenticatorHelper.cs new file mode 100644 index 00000000..f01eb907 --- /dev/null +++ b/src/corePackages/Core.Security/EmailAuthenticator/IEmailAuthenticatorHelper.cs @@ -0,0 +1,7 @@ +namespace Core.Security.EmailAuthenticator; + +public interface IEmailAuthenticatorHelper +{ + public Task CreateEmailActivationKey(); + public Task CreateEmailActivationCode(); +} \ No newline at end of file diff --git a/src/corePackages/Core.Security/Encryption/SecurityKeyHelper.cs b/src/corePackages/Core.Security/Encryption/SecurityKeyHelper.cs new file mode 100644 index 00000000..823cd6fb --- /dev/null +++ b/src/corePackages/Core.Security/Encryption/SecurityKeyHelper.cs @@ -0,0 +1,12 @@ +using Microsoft.IdentityModel.Tokens; +using System.Text; + +namespace Core.Security.Encryption; + +public class SecurityKeyHelper +{ + public static SecurityKey CreateSecurityKey(string securityKey) + { + return new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey)); + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Security/Encryption/SigningCredentialsHelper.cs b/src/corePackages/Core.Security/Encryption/SigningCredentialsHelper.cs new file mode 100644 index 00000000..cdde6bf9 --- /dev/null +++ b/src/corePackages/Core.Security/Encryption/SigningCredentialsHelper.cs @@ -0,0 +1,11 @@ +using Microsoft.IdentityModel.Tokens; + +namespace Core.Security.Encryption; + +public class SigningCredentialsHelper +{ + public static SigningCredentials CreateSigningCredentials(SecurityKey securityKey) + { + return new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha512Signature); + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Security/Entities/EmailAuthenticator.cs b/src/corePackages/Core.Security/Entities/EmailAuthenticator.cs new file mode 100644 index 00000000..2eae07b1 --- /dev/null +++ b/src/corePackages/Core.Security/Entities/EmailAuthenticator.cs @@ -0,0 +1,24 @@ +using Core.Persistence.Repositories; + +namespace Core.Security.Entities; + +public class EmailAuthenticator : Entity +{ + public int UserId { get; set; } + public string? ActivationKey { get; set; } + public bool IsVerified { get; set; } + + public virtual User User { get; set; } + + public EmailAuthenticator() + { + } + + public EmailAuthenticator(int id, int userId, string? activationKey, bool isVerified) : this() + { + Id = id; + UserId = userId; + ActivationKey = activationKey; + IsVerified = isVerified; + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Security/Entities/OperationClaim.cs b/src/corePackages/Core.Security/Entities/OperationClaim.cs new file mode 100644 index 00000000..5e2f4549 --- /dev/null +++ b/src/corePackages/Core.Security/Entities/OperationClaim.cs @@ -0,0 +1,17 @@ +using Core.Persistence.Repositories; + +namespace Core.Security.Entities; + +public class OperationClaim : Entity +{ + public string Name { get; set; } + + public OperationClaim() + { + } + + public OperationClaim(int id, string name) : base(id) + { + Name = name; + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Security/Entities/OtpAuthenticator.cs b/src/corePackages/Core.Security/Entities/OtpAuthenticator.cs new file mode 100644 index 00000000..4d76db2a --- /dev/null +++ b/src/corePackages/Core.Security/Entities/OtpAuthenticator.cs @@ -0,0 +1,24 @@ +using Core.Persistence.Repositories; + +namespace Core.Security.Entities; + +public class OtpAuthenticator : Entity +{ + public int UserId { get; set; } + public byte[] SecretKey { get; set; } + public bool IsVerified { get; set; } + + public virtual User User { get; set; } + + public OtpAuthenticator() + { + } + + public OtpAuthenticator(int id, int userId, byte[] secretKey, bool isVerified) : this() + { + Id = id; + UserId = userId; + SecretKey = secretKey; + IsVerified = isVerified; + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Security/Entities/RefreshToken.cs b/src/corePackages/Core.Security/Entities/RefreshToken.cs new file mode 100644 index 00000000..bcf306b4 --- /dev/null +++ b/src/corePackages/Core.Security/Entities/RefreshToken.cs @@ -0,0 +1,40 @@ +using Core.Persistence.Repositories; + +namespace Core.Security.Entities; + +public class RefreshToken : Entity +{ + public int UserId { get; set; } + public string Token { get; set; } + public DateTime Expires { get; set; } + public DateTime Created { get; set; } + public string CreatedByIp { get; set; } + public DateTime? Revoked { get; set; } + public string? RevokedByIp { get; set; } + public string? ReplacedByToken { get; set; } + + public string? ReasonRevoked { get; set; } + //public bool IsExpired => DateTime.UtcNow >= Expires; + //public bool IsRevoked => Revoked != null; + //public bool IsActive => !IsRevoked && !IsExpired; + + public virtual User User { get; set; } + + public RefreshToken() + { + } + + public RefreshToken(int id, string token, DateTime expires, DateTime created, string createdByIp, DateTime? revoked, + string revokedByIp, string replacedByToken, string reasonRevoked) + { + Id = id; + Token = token; + Expires = expires; + Created = created; + CreatedByIp = createdByIp; + Revoked = revoked; + RevokedByIp = revokedByIp; + ReplacedByToken = replacedByToken; + ReasonRevoked = reasonRevoked; + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Security/Entities/User.cs b/src/corePackages/Core.Security/Entities/User.cs new file mode 100644 index 00000000..d10f149b --- /dev/null +++ b/src/corePackages/Core.Security/Entities/User.cs @@ -0,0 +1,37 @@ +using Core.Persistence.Repositories; +using Core.Security.Enums; + +namespace Core.Security.Entities; + +public class User : Entity +{ + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + public byte[] PasswordSalt { get; set; } + public byte[] PasswordHash { get; set; } + public bool Status { get; set; } + public AuthenticatorType AuthenticatorType { get; set; } + + public virtual ICollection UserOperationClaims { get; set; } + public virtual ICollection RefreshTokens { get; set; } + + public User() + { + UserOperationClaims = new HashSet(); + RefreshTokens = new HashSet(); + } + + public User(int id, string firstName, string lastName, string email, byte[] passwordSalt, byte[] passwordHash, + bool status, AuthenticatorType authenticatorType) : this() + { + Id = id; + FirstName = firstName; + LastName = lastName; + Email = email; + PasswordSalt = passwordSalt; + PasswordHash = passwordHash; + Status = status; + AuthenticatorType = authenticatorType; + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Security/Entities/UserOperationClaim.cs b/src/corePackages/Core.Security/Entities/UserOperationClaim.cs new file mode 100644 index 00000000..9c7cbeae --- /dev/null +++ b/src/corePackages/Core.Security/Entities/UserOperationClaim.cs @@ -0,0 +1,22 @@ +using Core.Persistence.Repositories; + +namespace Core.Security.Entities; + +public class UserOperationClaim : Entity +{ + public int UserId { get; set; } + public int OperationClaimId { get; set; } + + public virtual User User { get; set; } + public virtual OperationClaim OperationClaim { get; set; } + + public UserOperationClaim() + { + } + + public UserOperationClaim(int id, int userId, int operationClaimId) : base(id) + { + UserId = userId; + OperationClaimId = operationClaimId; + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Security/Enums/AuthenticatorType.cs b/src/corePackages/Core.Security/Enums/AuthenticatorType.cs new file mode 100644 index 00000000..dcc49ffd --- /dev/null +++ b/src/corePackages/Core.Security/Enums/AuthenticatorType.cs @@ -0,0 +1,8 @@ +namespace Core.Security.Enums; + +public enum AuthenticatorType +{ + None = 0, + Email = 1, + Otp = 2 +} \ No newline at end of file diff --git a/src/corePackages/Core.Security/Extensions/ClaimExtensions.cs b/src/corePackages/Core.Security/Extensions/ClaimExtensions.cs new file mode 100644 index 00000000..1a29bdf7 --- /dev/null +++ b/src/corePackages/Core.Security/Extensions/ClaimExtensions.cs @@ -0,0 +1,27 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; + +namespace Core.Security.Extensions; + +public static class ClaimExtensions +{ + public static void AddEmail(this ICollection claims, string email) + { + claims.Add(new Claim(JwtRegisteredClaimNames.Email, email)); + } + + public static void AddName(this ICollection claims, string name) + { + claims.Add(new Claim(ClaimTypes.Name, name)); + } + + public static void AddNameIdentifier(this ICollection claims, string nameIdentifier) + { + claims.Add(new Claim(ClaimTypes.NameIdentifier, nameIdentifier)); + } + + public static void AddRoles(this ICollection claims, string[] roles) + { + roles.ToList().ForEach(role => claims.Add(new Claim(ClaimTypes.Role, role))); + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Security/Extensions/ClaimsPrincipalExtensions.cs b/src/corePackages/Core.Security/Extensions/ClaimsPrincipalExtensions.cs new file mode 100644 index 00000000..4e270454 --- /dev/null +++ b/src/corePackages/Core.Security/Extensions/ClaimsPrincipalExtensions.cs @@ -0,0 +1,22 @@ +using System.Security.Claims; + +namespace Core.Security.Extensions; + +public static class ClaimsPrincipalExtensions +{ + public static List? Claims(this ClaimsPrincipal claimsPrincipal, string claimType) + { + List? result = claimsPrincipal?.FindAll(claimType)?.Select(x => x.Value).ToList(); + return result; + } + + public static List? ClaimRoles(this ClaimsPrincipal claimsPrincipal) + { + return claimsPrincipal?.Claims(ClaimTypes.Role); + } + + public static int GetUserId(this ClaimsPrincipal claimsPrincipal) + { + return Convert.ToInt32(claimsPrincipal?.Claims(ClaimTypes.NameIdentifier)?.FirstOrDefault()); + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Security/Hashing/HashingHelper.cs b/src/corePackages/Core.Security/Hashing/HashingHelper.cs new file mode 100644 index 00000000..a11af9aa --- /dev/null +++ b/src/corePackages/Core.Security/Hashing/HashingHelper.cs @@ -0,0 +1,29 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Core.Security.Hashing; + +public class HashingHelper +{ + public static void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt) + { + using (HMACSHA512 hmac = new()) + { + passwordSalt = hmac.Key; + passwordHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(password)); + } + } + + public static bool VerifyPasswordHash(string password, byte[] passwordHash, byte[] passwordSalt) + { + using (HMACSHA512 hmac = new(passwordSalt)) + { + byte[] computedHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(password)); + for (int i = 0; i < computedHash.Length; i++) + if (computedHash[i] != passwordHash[i]) + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Security/JWT/AccessToken.cs b/src/corePackages/Core.Security/JWT/AccessToken.cs new file mode 100644 index 00000000..4ca2e94e --- /dev/null +++ b/src/corePackages/Core.Security/JWT/AccessToken.cs @@ -0,0 +1,7 @@ +namespace Core.Security.JWT; + +public class AccessToken +{ + public string Token { get; set; } + public DateTime Expiration { get; set; } +} \ No newline at end of file diff --git a/src/corePackages/Core.Security/JWT/ITokenHelper.cs b/src/corePackages/Core.Security/JWT/ITokenHelper.cs new file mode 100644 index 00000000..acceefed --- /dev/null +++ b/src/corePackages/Core.Security/JWT/ITokenHelper.cs @@ -0,0 +1,10 @@ +using Core.Security.Entities; + +namespace Core.Security.JWT; + +public interface ITokenHelper +{ + AccessToken CreateToken(User user, IList operationClaims); + + RefreshToken CreateRefreshToken(User user, string ipAddress); +} \ No newline at end of file diff --git a/src/corePackages/Core.Security/JWT/JwtHelper.cs b/src/corePackages/Core.Security/JWT/JwtHelper.cs new file mode 100644 index 00000000..5ae02bc5 --- /dev/null +++ b/src/corePackages/Core.Security/JWT/JwtHelper.cs @@ -0,0 +1,78 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using Core.Security.Encryption; +using Core.Security.Entities; +using Core.Security.Extensions; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; + +namespace Core.Security.JWT; + +public class JwtHelper : ITokenHelper +{ + public IConfiguration Configuration { get; } + private readonly TokenOptions _tokenOptions; + private DateTime _accessTokenExpiration; + + public JwtHelper(IConfiguration configuration) + { + Configuration = configuration; + _tokenOptions = Configuration.GetSection("TokenOptions").Get(); + } + + public AccessToken CreateToken(User user, IList operationClaims) + { + _accessTokenExpiration = DateTime.Now.AddMinutes(_tokenOptions.AccessTokenExpiration); + SecurityKey securityKey = SecurityKeyHelper.CreateSecurityKey(_tokenOptions.SecurityKey); + SigningCredentials signingCredentials = SigningCredentialsHelper.CreateSigningCredentials(securityKey); + JwtSecurityToken jwt = CreateJwtSecurityToken(_tokenOptions, user, signingCredentials, operationClaims); + JwtSecurityTokenHandler jwtSecurityTokenHandler = new(); + string? token = jwtSecurityTokenHandler.WriteToken(jwt); + + return new AccessToken + { + Token = token, + Expiration = _accessTokenExpiration + }; + } + + public RefreshToken CreateRefreshToken(User user, string ipAddress) + { + RefreshToken refreshToken = new() + { + UserId = user.Id, + Token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)), + Expires = DateTime.UtcNow.AddDays(7), + Created = DateTime.UtcNow, + CreatedByIp = ipAddress + }; + + return refreshToken; + } + + public JwtSecurityToken CreateJwtSecurityToken(TokenOptions tokenOptions, User user, + SigningCredentials signingCredentials, + IList operationClaims) + { + JwtSecurityToken jwt = new( + tokenOptions.Issuer, + tokenOptions.Audience, + expires: _accessTokenExpiration, + notBefore: DateTime.Now, + claims: SetClaims(user, operationClaims), + signingCredentials: signingCredentials + ); + return jwt; + } + + private IEnumerable SetClaims(User user, IList operationClaims) + { + List claims = new(); + claims.AddNameIdentifier(user.Id.ToString()); + claims.AddEmail(user.Email); + claims.AddName($"{user.FirstName} {user.LastName}"); + claims.AddRoles(operationClaims.Select(c => c.Name).ToArray()); + return claims; + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Security/JWT/TokenOptions.cs b/src/corePackages/Core.Security/JWT/TokenOptions.cs new file mode 100644 index 00000000..18574333 --- /dev/null +++ b/src/corePackages/Core.Security/JWT/TokenOptions.cs @@ -0,0 +1,10 @@ +namespace Core.Security.JWT; + +public class TokenOptions +{ + public string Audience { get; set; } + public string Issuer { get; set; } + public int AccessTokenExpiration { get; set; } + public string SecurityKey { get; set; } + public int RefreshTokenTTL { get; set; } +} \ No newline at end of file diff --git a/src/corePackages/Core.Security/OtpAuthenticator/IOtpAuthenticatorHelper.cs b/src/corePackages/Core.Security/OtpAuthenticator/IOtpAuthenticatorHelper.cs new file mode 100644 index 00000000..dffb500e --- /dev/null +++ b/src/corePackages/Core.Security/OtpAuthenticator/IOtpAuthenticatorHelper.cs @@ -0,0 +1,8 @@ +namespace Core.Security.OtpAuthenticator; + +public interface IOtpAuthenticatorHelper +{ + public Task GenerateSecretKey(); + public Task ConvertSecretKeyToString(byte[] secretKey); + public Task VerifyCode(byte[] secretKey, string code); +} \ No newline at end of file diff --git a/src/corePackages/Core.Security/OtpAuthenticator/OtpNet/OtpNetOtpAuthenticatorHelper.cs b/src/corePackages/Core.Security/OtpAuthenticator/OtpNet/OtpNetOtpAuthenticatorHelper.cs new file mode 100644 index 00000000..43616b08 --- /dev/null +++ b/src/corePackages/Core.Security/OtpAuthenticator/OtpNet/OtpNetOtpAuthenticatorHelper.cs @@ -0,0 +1,33 @@ +using OtpNet; + +namespace Core.Security.OtpAuthenticator.OtpNet; + +public class OtpNetOtpAuthenticatorHelper : IOtpAuthenticatorHelper +{ + public Task GenerateSecretKey() + { + byte[] key = KeyGeneration.GenerateRandomKey(20); + + string base32String = Base32Encoding.ToString(key); + byte[] base32Bytes = Base32Encoding.ToBytes(base32String); + + return Task.FromResult(base32Bytes); + } + + public Task ConvertSecretKeyToString(byte[] secretKey) + { + string base32String = Base32Encoding.ToString(secretKey); + return Task.FromResult(base32String); + } + + public Task VerifyCode(byte[] secretKey, string code) + { + Totp totp = new(secretKey); + + string totpCode = totp.ComputeTotp(DateTime.UtcNow); + + bool result = totpCode == code; + + return Task.FromResult(result); + } +} \ No newline at end of file diff --git a/src/corePackages/Core.Security/SecurityServiceRegistration.cs b/src/corePackages/Core.Security/SecurityServiceRegistration.cs new file mode 100644 index 00000000..bee84ff9 --- /dev/null +++ b/src/corePackages/Core.Security/SecurityServiceRegistration.cs @@ -0,0 +1,18 @@ +using Core.Security.EmailAuthenticator; +using Core.Security.JWT; +using Core.Security.OtpAuthenticator; +using Core.Security.OtpAuthenticator.OtpNet; +using Microsoft.Extensions.DependencyInjection; + +namespace Application; + +public static class SecurityServiceRegistration +{ + public static IServiceCollection AddSecurityServices(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + return services; + } +} \ No newline at end of file