From d3464ef39c6c93bc9b59b4ab7fc62b5bf7ff0f90 Mon Sep 17 00:00:00 2001 From: Hadi Khazaee Asl Date: Sun, 22 Mar 2026 14:08:25 +0330 Subject: [PATCH] Initial ... --- .gitignore | 14 + Configurations/CertificatesConfigurations.cs | 10 + Configurations/XDataServiceConfiguration.cs | 73 + .../XIdentityResourceConfiguration.cs | 18 + Constants/ConfigurationNodeNames.cs | 11 + Constants/ConnectionStringNames.cs | 9 + Constants/IdentityMessagesKeys.cs | 14 + Constants/XDataServiceConstants.cs | 12 + Constants/XDbProviderConfigurations.cs | 10 + Constants/XDbProviders.cs | 23 + .../Account/AccountController+Admin.cs | 151 ++ .../AccountController+Authentication.cs | 392 ++++ .../Account/AccountController+Friendship.cs | 889 ++++++++ Controllers/Account/AccountController+Open.cs | 68 + .../Account/AccountController+Profile.cs | 511 +++++ .../Account/AccountController+Query.cs | 63 + .../Account/AccountController+Registration.cs | 1314 +++++++++++ .../Account/AccountController+Search.cs | 62 + Controllers/Account/AccountController+Test.cs | 161 ++ Controllers/AccountController.cs | 30 + Controllers/Base/XIBaseController.cs | 269 +++ Controllers/StartupController.cs | 44 + DI/XDIHelperExtension.cs | 420 ++++ Db/.gitkeep | 0 DbContext/XIdentityDbContext.cs | 57 + Extensions/DbExtensions.cs | 168 ++ .../IIdentityServerBuilderExtensions.cs | 117 + Extensions/IQueryableExtensions.cs | 18 + Extensions/IdentityExtensions.cs | 29 + Extensions/XDbSeederExtensions.cs | 428 ++++ Extensions/XModelExtensions.cs | 91 + Helpers/XIdentityHelper.cs | 426 ++++ Interfaces/IXIdentityHelper.cs | 29 + Interfaces/IXIdentityManager.cs | 506 +++++ Interfaces/IXIdentityMessageProvider.cs | 16 + Interfaces/IXSecurityProvider.cs | 10 + Models/XDbInfo.cs | 31 + Models/XDbSeedDescriptor.cs | 19 + Program.cs | 20 + Properties/launchSettings.json | 30 + Providers/XIdentityManager.cs | 187 ++ .../XIdentityManager+Admin.cs | 224 ++ .../XIdentityManager+Device.cs | 481 ++++ .../XIdentityManager+Friendship.cs | 1680 ++++++++++++++ .../XIdentityManager+Identity.cs | 342 +++ .../XIdentityManager+MessageProvider.cs | 341 +++ .../XIdentityManager/XIdentityManager+Open.cs | 211 ++ .../XIdentityManager+Private.cs | 552 +++++ .../XIdentityManager+Profile.cs | 2016 +++++++++++++++++ .../XIdentityManager+Query.cs | 110 + .../XIdentityManager+Registration.cs | 700 ++++++ .../XIdentityManager+Request.cs | 722 ++++++ .../XIdentityManager/XIdentityManager+Role.cs | 273 +++ .../XIdentityManager+Search.cs | 119 + .../XIdentityManager/XIdentityManager+Seed.cs | 132 ++ .../XIdentityManager+Token.cs | 742 ++++++ .../XIdentityManager/XIdentityManager+User.cs | 503 ++++ .../XIdentityManager+Validators.cs | 1520 +++++++++++++ .../XIdentityManager+VerificationCode.cs | 220 ++ Providers/XIdentityMessageProvider.cs | 95 + Providers/XIdentityProfileService.cs | 51 + Providers/XSecurityProvider.cs | 168 ++ README.md | 12 + Startup.cs | 123 + Stores/XPersistedGrantStore.cs | 151 ++ Validators/XResourceOwnerPasswordValidator.cs | 193 ++ appsettings.Development.json | 177 ++ appsettings.json | 397 ++++ nuget.config | 8 + tempkey.jwk | 1 + xIds.csproj | 73 + 71 files changed, 19087 insertions(+) create mode 100644 .gitignore create mode 100644 Configurations/CertificatesConfigurations.cs create mode 100644 Configurations/XDataServiceConfiguration.cs create mode 100644 Configurations/XIdentityResourceConfiguration.cs create mode 100644 Constants/ConfigurationNodeNames.cs create mode 100644 Constants/ConnectionStringNames.cs create mode 100644 Constants/IdentityMessagesKeys.cs create mode 100644 Constants/XDataServiceConstants.cs create mode 100644 Constants/XDbProviderConfigurations.cs create mode 100644 Constants/XDbProviders.cs create mode 100644 Controllers/Account/AccountController+Admin.cs create mode 100644 Controllers/Account/AccountController+Authentication.cs create mode 100644 Controllers/Account/AccountController+Friendship.cs create mode 100644 Controllers/Account/AccountController+Open.cs create mode 100644 Controllers/Account/AccountController+Profile.cs create mode 100644 Controllers/Account/AccountController+Query.cs create mode 100644 Controllers/Account/AccountController+Registration.cs create mode 100644 Controllers/Account/AccountController+Search.cs create mode 100644 Controllers/Account/AccountController+Test.cs create mode 100644 Controllers/AccountController.cs create mode 100644 Controllers/Base/XIBaseController.cs create mode 100644 Controllers/StartupController.cs create mode 100644 DI/XDIHelperExtension.cs create mode 100644 Db/.gitkeep create mode 100644 DbContext/XIdentityDbContext.cs create mode 100644 Extensions/DbExtensions.cs create mode 100644 Extensions/IIdentityServerBuilderExtensions.cs create mode 100644 Extensions/IQueryableExtensions.cs create mode 100644 Extensions/IdentityExtensions.cs create mode 100644 Extensions/XDbSeederExtensions.cs create mode 100644 Extensions/XModelExtensions.cs create mode 100644 Helpers/XIdentityHelper.cs create mode 100644 Interfaces/IXIdentityHelper.cs create mode 100644 Interfaces/IXIdentityManager.cs create mode 100644 Interfaces/IXIdentityMessageProvider.cs create mode 100644 Interfaces/IXSecurityProvider.cs create mode 100644 Models/XDbInfo.cs create mode 100644 Models/XDbSeedDescriptor.cs create mode 100644 Program.cs create mode 100644 Properties/launchSettings.json create mode 100644 Providers/XIdentityManager.cs create mode 100644 Providers/XIdentityManager/XIdentityManager+Admin.cs create mode 100644 Providers/XIdentityManager/XIdentityManager+Device.cs create mode 100644 Providers/XIdentityManager/XIdentityManager+Friendship.cs create mode 100644 Providers/XIdentityManager/XIdentityManager+Identity.cs create mode 100644 Providers/XIdentityManager/XIdentityManager+MessageProvider.cs create mode 100644 Providers/XIdentityManager/XIdentityManager+Open.cs create mode 100644 Providers/XIdentityManager/XIdentityManager+Private.cs create mode 100644 Providers/XIdentityManager/XIdentityManager+Profile.cs create mode 100644 Providers/XIdentityManager/XIdentityManager+Query.cs create mode 100644 Providers/XIdentityManager/XIdentityManager+Registration.cs create mode 100644 Providers/XIdentityManager/XIdentityManager+Request.cs create mode 100644 Providers/XIdentityManager/XIdentityManager+Role.cs create mode 100644 Providers/XIdentityManager/XIdentityManager+Search.cs create mode 100644 Providers/XIdentityManager/XIdentityManager+Seed.cs create mode 100644 Providers/XIdentityManager/XIdentityManager+Token.cs create mode 100644 Providers/XIdentityManager/XIdentityManager+User.cs create mode 100644 Providers/XIdentityManager/XIdentityManager+Validators.cs create mode 100644 Providers/XIdentityManager/XIdentityManager+VerificationCode.cs create mode 100644 Providers/XIdentityMessageProvider.cs create mode 100644 Providers/XIdentityProfileService.cs create mode 100644 Providers/XSecurityProvider.cs create mode 100644 README.md create mode 100644 Startup.cs create mode 100644 Stores/XPersistedGrantStore.cs create mode 100644 Validators/XResourceOwnerPasswordValidator.cs create mode 100644 appsettings.Development.json create mode 100644 appsettings.json create mode 100644 nuget.config create mode 100644 tempkey.jwk create mode 100644 xIds.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..068077e --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# +bin +obj + +# +Db/* +!Db/.gitkeep + +# +Migrations/* + +# +wwwroot +!wwwroot/.gitkeep diff --git a/Configurations/CertificatesConfigurations.cs b/Configurations/CertificatesConfigurations.cs new file mode 100644 index 0000000..201f4e2 --- /dev/null +++ b/Configurations/CertificatesConfigurations.cs @@ -0,0 +1,10 @@ +namespace xIds.Configurations +{ + public class CertificatesConfigurations + { + public struct CertificateNames + { + public const string IdentityServerSigning = "IdentityServerSigning"; + } + } +} \ No newline at end of file diff --git a/Configurations/XDataServiceConfiguration.cs b/Configurations/XDataServiceConfiguration.cs new file mode 100644 index 0000000..25a00dd --- /dev/null +++ b/Configurations/XDataServiceConfiguration.cs @@ -0,0 +1,73 @@ +using xIds.Constants; + +namespace xIds.Configurations +{ + /// + /// Represent Configurations of DataService Module ... + /// + public partial class XDataServiceConfiguration + { + /// + /// Provider Type ... + /// + /// + public XDbProviders Provider { get; set; } + + /// + /// Connection String which provide Requires Data to Connect to Db Provider ... + /// + /// + public string ConnectionString { get; set; } + + /// + /// Enable Tracking of Entities ... + /// Only Used on EFCore ... + /// + /// + public bool EnableTracking { get; set; } = false; + + /// + /// Enable Logging Details of Errors ... + /// Only Used on EFCore ... + /// + /// + public bool EnableDetailedErrors { get; set; } = false; + + /// + /// Enable Logging Sensitive Data ... + /// Only Used on EFCore ... + /// + /// + public bool EnableSensitiveDataLogging { get; set; } = false; + + /// + /// this is a way to provide Default Pagination Data on XQuery based requests ... + /// + /// + public PagingConfiguration PagingConfiguration { get; set; } = new PagingConfiguration(); + } + + /// + /// this is a way to provide Default Pagination Data on XQuery based requests ... + /// + public partial class PagingConfiguration + { + /// + /// Default Page Size ... + /// + /// + public int DefaultPageSize { get; set; } = XDataServiceConstants.DEFAULT_PAGE_SIZE; + + /// + /// restrict Maximum Page Size ... + /// + /// + public int MaxAvailablePageSize { get; set; } = XDataServiceConstants.MAX_AVAILABLE_PAGE_SIZE; + + /// + /// restrice Minimum Page Size ... + /// + /// + public int MinAvailablePageSize { get; set; } = XDataServiceConstants.MIN_AVAILABLE_PAGE_SIZE; + } +} \ No newline at end of file diff --git a/Configurations/XIdentityResourceConfiguration.cs b/Configurations/XIdentityResourceConfiguration.cs new file mode 100644 index 0000000..1e281dc --- /dev/null +++ b/Configurations/XIdentityResourceConfiguration.cs @@ -0,0 +1,18 @@ +namespace xIds.Configurations +{ + /// + /// determines a resource and it's connection info + /// + public partial class XIdentityResourceConfiguration + { + public string Authority { get; set; } + public string ApiName { get; set; } + public string ApiSecret { get; set; } + + public string ClientId { get; set; } + public string ClientSecret { get; set; } + + public string XPoweredValue { get; set; } + public string XRevisionSecretKey { get; set; } + } +} \ No newline at end of file diff --git a/Constants/ConfigurationNodeNames.cs b/Constants/ConfigurationNodeNames.cs new file mode 100644 index 0000000..9a350de --- /dev/null +++ b/Constants/ConfigurationNodeNames.cs @@ -0,0 +1,11 @@ +namespace xIds.Constants +{ + public struct ConfigurationNodeNames + { + public const string DB_SEED_NODE_NAME = "DbSeeder"; + public const string PROVIDER_NODE_NAME = "DbProvider"; + public const string IDENTITY_NODE_NAME = "IdentityConfiguration"; + public const string DATA_SERVICE_NODE_NAME = "DataServiceConfiguration"; + public const string IDENTITY_RESOURCE_NODE_NAME = "IdentityResourceConfiguration"; + } +} \ No newline at end of file diff --git a/Constants/ConnectionStringNames.cs b/Constants/ConnectionStringNames.cs new file mode 100644 index 0000000..a8639b7 --- /dev/null +++ b/Constants/ConnectionStringNames.cs @@ -0,0 +1,9 @@ +namespace xIds.Constants +{ + public struct ConnectionStringNames + { + public const string IDENTITY_CONNECTION_NAME = "IdentityDb"; + public const string CONFIGURATION_CONNECTION_NAME = "ConfigurationDb"; + public const string PRESISTED_GRANTS_CONNECTION_NAME = "PersistedGrantDb"; + } +} \ No newline at end of file diff --git a/Constants/IdentityMessagesKeys.cs b/Constants/IdentityMessagesKeys.cs new file mode 100644 index 0000000..a5d4524 --- /dev/null +++ b/Constants/IdentityMessagesKeys.cs @@ -0,0 +1,14 @@ +namespace xIds.Constants +{ + public struct IdentityMessagesKeys + { + public const string Terms = "terms"; + public const string InviteMsg = "user_invite_msg"; + public const string RegistrationApproveMsg = "registration_approve_msg"; + public const string RegisteredMsg = "registered_msg"; + public const string VerificationCodeMsg = "verification_code_msg"; + public const string ChangePasswordMsg = "change_password_msg"; + public const string PasswordChangedMsg = "password_changed_msg"; + public const string NewDeviceLoggedInMsg = "new_device_logged_in_msg"; + } +} \ No newline at end of file diff --git a/Constants/XDataServiceConstants.cs b/Constants/XDataServiceConstants.cs new file mode 100644 index 0000000..546f322 --- /dev/null +++ b/Constants/XDataServiceConstants.cs @@ -0,0 +1,12 @@ +namespace xIds.Constants +{ + /// + /// /// Default Pagination Values ... + /// + public partial struct XDataServiceConstants + { + public static int DEFAULT_PAGE_SIZE = 50; + public static int MAX_AVAILABLE_PAGE_SIZE = 500; + public static int MIN_AVAILABLE_PAGE_SIZE = 20; + } +} \ No newline at end of file diff --git a/Constants/XDbProviderConfigurations.cs b/Constants/XDbProviderConfigurations.cs new file mode 100644 index 0000000..8ee83c2 --- /dev/null +++ b/Constants/XDbProviderConfigurations.cs @@ -0,0 +1,10 @@ +namespace xIds.Constants +{ + public partial class XDbProviderConfigurations + { + /// + /// Default ConnectionString Name ... + /// + public const string DEFAULT_CONNECTION_NAME = "DataConnection"; + } +} \ No newline at end of file diff --git a/Constants/XDbProviders.cs b/Constants/XDbProviders.cs new file mode 100644 index 0000000..4eab73c --- /dev/null +++ b/Constants/XDbProviders.cs @@ -0,0 +1,23 @@ +namespace xIds.Constants +{ + /// + /// /// Represent Supported DBMS for Managing Data ... + /// + public enum XDbProviders + { + None, + MySQL, + SQLite, + SQLServer, + } + + /// + /// Represent Supported DBMS for Managing Data ... + /// + public partial struct ProviderType + { + public const string MySQL = "MYSQL"; + public const string SQLite = "SQLITE"; + public const string SQLServer = "SQLSERVER"; + } +} \ No newline at end of file diff --git a/Controllers/Account/AccountController+Admin.cs b/Controllers/Account/AccountController+Admin.cs new file mode 100644 index 0000000..2a68b6e --- /dev/null +++ b/Controllers/Account/AccountController+Admin.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using xIdentityHelper; +using xCommons.Attributes; +using xCommons.Extensions; +using xIdentityModels.Dtos; +using static IdentityServer4.IdentityServerConstants; + +namespace xIds.Controllers +{ + public partial class AccountController + { + // + #region Admin Actions ... + /// + /// Ban Specific Users + /// + /// an instance of XUserNameIdRequest which represents user identifier list to Ban + /// a list of banned users identifiers + [RequireXPowered] + [HttpPost("Ban")] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledAdmin)] + public async Task>> Ban( + [FromBody] XUserNameIdRequest model + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + await ValidationProvider + .GroupValidationBuilder() + .AddNotNull(model) + .AddNotZeroChilds(model.Ids) + .ValidateGroupAsync(); + + // + var result = await IdentityManager + .Ban( + User.Identity.Name, + model + ); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// UnBann Specific Users + /// + /// an instance of XUserNameIdRequest which represents user identifier list to Ban + /// a list of unbanned users identifiers + [RequireXPowered] + [HttpPost("UnBan")] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledAdmin)] + public async Task>> UnBan( + [FromBody] XUserNameIdRequest model + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + await ValidationProvider + .GroupValidationBuilder() + .AddNotNull(model) + .AddNotZeroChilds(model.Ids) + .ValidateGroupAsync(); + + // + var result = await IdentityManager + .UnBan( + User.Identity.Name, + model + ); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Check Specific User is Banned or not + /// + /// specified user's identifier + /// a boolean value which represent user banned or not + [RequireXPowered] + [Authorize(Policy = LocalApi.PolicyName)] + [HttpGet("IsBanned/{userSelectByParam?}")] + [Authorize(Policy = XPolicies.EnabledAdmin)] + [Authorize(Policy = XPolicies.EnabledAgent)] + public async Task> IsBanned( + [FromRoute] string userSelectByParam = null + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + if (userSelectByParam.IsNullOrEmpty()) + { + userSelectByParam = User.Identity.Name; + } + ValidationProvider.NotEmpty(userSelectByParam); + + // + var result = await IdentityManager + .IsBanned( + User.Identity.Name, + userSelectByParam + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + #endregion + } +} \ No newline at end of file diff --git a/Controllers/Account/AccountController+Authentication.cs b/Controllers/Account/AccountController+Authentication.cs new file mode 100644 index 0000000..ddaf856 --- /dev/null +++ b/Controllers/Account/AccountController+Authentication.cs @@ -0,0 +1,392 @@ +using System; +using System.Threading.Tasks; +using IdentityModel.Client; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using xIdentityHelper; +using xCommons.Attributes; +using xCommons.Extensions; +using xExceptions.Constants; +using xIdentityModels.Extensions; +using xIdentityModels.Models; +using static IdentityServer4.IdentityServerConstants; +using static xIdentityHelper.XApiScopeHelper; +using Microsoft.Extensions.Logging; + +namespace xIds.Controllers +{ + public partial class AccountController + { + // + #region Authentication Actions ... + /// + /// Retrieve OAuth Discovery Document + /// + /// an instance of DiscoveryDocumentResponse + [AllowAnonymous] + [RequireXPowered] + [HttpGet("DiscoveryDocument")] + public async Task> GetDiscoveryDocument() + { + // + // Do Action ... + try + { + // + var result = await IdentityManager + .RequestDiscoveryDocument(); + + // + // Ceck Response Result ... + if (result.IsError) + { + XException.InvalidConfiguration.Throw(); + } + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Request AccessToken for Specific XApiScope + /// + /// a member of XApiScope + /// an instance of XTokenResponse + [AllowAnonymous] + [RequireXPowered] + [HttpPost("RequestScopeAccessToken")] + public async Task> RequestScopeAccessToken( + [FromHeader] XApiScope scope + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + var scopeName = string.Empty; + try + { + scopeName = scope.GetStringValue(); + } + catch { } + ValidationProvider.NotEmpty(scopeName); + + // + var result = await IdentityManager + .RequestScopeAccessToken(scopeName); + + // + // Ceck Response Result ... + if (result.IsError) + { + XException.InvalidConfiguration.Throw(); + } + + // + return Ok(result + .CreateXTokenResponse() + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Authenticate User + /// + /// + /// Sample request: + /// + /// { + /// "password": "", + /// "userSelectBy": "", + /// } + /// + /// + /// an instance of XLoginRequest class which represent Authentication requirements + /// an instance of XTokenResponse + [AllowAnonymous] + [RequireXPowered] + [HttpPost("Authenticate")] + public async Task> Authenticate( + [FromBody] XLoginRequest model + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + if (!ModelState.IsValid) + { + XException.InvalidArgs.Throw(); + } + await ValidationProvider + .GroupValidationBuilder() + .AddNotNull(model) + .AddNotEmpty( + model.UserSelectBy, + model.Password + ) + .ValidateGroupAsync(); + + // + var result = await IdentityManager + .Authenticate(model); + + // + // Check Result ... + if (result.IsError) + { + throw result.GetException(); + } + + // + // Return Result ... + return Ok(result + .CreateXTokenResponse() + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Authenticate User + /// + /// + /// Sample request: + /// + /// { + /// "language": "fa-IR" + /// "password": "", + /// "userSelectBy": "", + /// "device": { + /// "os":"Mac", + /// "browser":"Chrome", + /// "osVersion":"mac-os-x-15", + /// "userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36", + /// "deviceType":3, + /// "identifier":"[Mac]-[mac-os-x-15]-[3]-[Chrome]-[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36]", + /// "token":"96f400dd9aa5c57a8cec9d6f4775bad3" + /// } + /// } + /// + /// + /// an instance of XLoginRequest class which represent Authentication requirements + /// an instance of XLoginResponse + [AllowAnonymous] + [RequireXPowered] + [HttpPost("Login")] + public async Task> Login( + [FromBody] XLoginRequest model + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + if (!ModelState.IsValid) + { + XException.InvalidArgs.Throw(); + } + await ValidationProvider + .GroupValidationBuilder() + .AddNotNull( + model, + model.Device) + .AddNotEmpty( + model.Language, + model.UserSelectBy, + model.Password + ) + .ValidateGroupAsync(); + + // + var result = await IdentityManager + .Login(model); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Refresh Expired Tokens + /// + /// + /// in addition to AccessToken, you had to pass RefreshToken due to Headers + /// + /// an instance of XTokenResponse + [AllowAnonymous] + [RequireXPowered] + [HttpPost("RefreshTokens")] + public async Task> RefreshTokens() + { + // + // Do Action ... + try + { + // + // Retrieve Toke Response ... + var model = await RetrieveTokensAsXTokenResponse(); + + // + // Validate Args ... + ValidationProvider + .GroupValidationBuilder() + .AddNotNull(model) + .AddNotEmpty( + model.AccessToken, + model.RefreshToken) + .ValidateGroup(); + + // + var result = await IdentityManager + .RefreshTokens(model); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Password Actions ... + /// + /// Change a User's Password ... + /// + /// + /// for this action you have to provide: + /// - Lang: client device currently used locale, such as: en-US. + /// - Password: user's current active password. + /// - NewPassword: user's new password to change. + /// - Device: user's client device which is an instance of XDevice. + /// - ReturnUrl: client application Login URL. + /// + /// Sample request: + /// + /// { + /// "lang": "fa-IR", + /// "password": "" + /// "newPassword": "", + /// "returnUrl": "http://localhost/login", + /// "device": { + /// "os":"Mac", + /// "browser":"Chrome", + /// "osVersion":"mac-os-x-15", + /// "userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36", + /// "deviceType":3, + /// "identifier":"[Mac]-[mac-os-x-15]-[3]-[Chrome]-[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36]", + /// "token":"96f400dd9aa5c57a8cec9d6f4775bad3" + /// } + /// } + /// + /// + /// an instance of XActionRequest class which provider requirement for action + /// + [RequireXPowered] + [HttpPost("ChangePassword")] + [Authorize(Policy = XPolicies.User)] + [Authorize(Policy = XPolicies.EnabledUser)] + [Authorize(Policy = LocalApi.PolicyName)] + public async Task ChangePassword( + [FromBody] XActionRequest model + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + if (!ModelState.IsValid) + { + XException.InvalidArgs.Throw(); + } + await ValidationProvider + .GroupValidationBuilder() + .AddNotNull( + model, + model.Device + ) + .AddNotEmpty( + model.Lang, + model.Password, + model.NewPassword, + model.ReturnUrl + ) + .ValidateGroupAsync(); + + // + // Get User Select By ... + var userSelectByParam = GetUserSelectByParam( + model, + forceNotNull: false + ); + if (userSelectByParam.IsNullOrEmpty()) + { + userSelectByParam = User.Identity.Name; + } + ValidationProvider.NotEmpty(userSelectByParam); + + // + // Get Request ... + await IdentityManager + .ChangePassword( + model.Lang, + model.Device, + userSelectByParam, + model.Password, + model.NewPassword, + model.ReturnUrl + ); + + // + return Ok(); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + #endregion + } +} \ No newline at end of file diff --git a/Controllers/Account/AccountController+Friendship.cs b/Controllers/Account/AccountController+Friendship.cs new file mode 100644 index 0000000..e276459 --- /dev/null +++ b/Controllers/Account/AccountController+Friendship.cs @@ -0,0 +1,889 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using xCommons.Attributes; +using xCommons.Extensions; +using xExceptions.Constants; +using xIdentityHelper; +using xIdentityModels.Constants; +using xIdentityModels.Dtos; +using xIdentityModels.Navigations; +using xModels.Dtos; +using static IdentityServer4.IdentityServerConstants; + +namespace xIds.Controllers +{ + public partial class AccountController + { + // + #region Friendship Actions ... + // + #region Actions ... + /// + /// Follow a User + /// + /// a user identifier which represent destination user + /// an instance of XFriendshipFollowing + [RequireXPowered] + [HttpPost("Friendship/{destUser}/Follow")] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + public async Task> Follow( + [FromRoute] string destUser + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + ValidationProvider.NotEmpty(destUser); + + // + var result = await IdentityManager + .Follow( + User.Identity.Name, + destUser + ); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Cancel Following + /// + /// a user identifier which represent destination user + /// a boolean value + [HttpPost("Friendship/{destUser}/Cancel")] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + public async Task> Cancel( + [FromRoute] string destUser + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + ValidationProvider.NotEmpty(destUser); + + // + var result = await IdentityManager + .Cancel( + User.Identity.Name, + destUser + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Unfollow a Follower + /// + /// a user identifier which represent destination user + /// + [HttpPost("Friendship/{destUser}/UnFollowFollower")] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + public async Task UnFollowFollower( + [FromRoute] string destUser + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + ValidationProvider.NotEmpty(destUser); + + // + await IdentityManager + .UnFollowFollower( + User.Identity.Name, + destUser + ); + + // + return Ok(); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Unfollow Following + /// + /// a user identifier which represent destination user + /// + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + [HttpPost("Friendship/{destUser}/UnFollowFollowing")] + public async Task UnFollowFollowing( + [FromRoute] string destUser + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + ValidationProvider.NotEmpty(destUser); + + // + await IdentityManager + .UnFollowFollowing( + User.Identity.Name, + destUser + ); + + // + return Ok(); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Block a Follower + /// + /// a user identifier which represent destination user + /// an instance of XFriendshipFollowing + [HttpPost("Friendship/{destUser}/Block")] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + public async Task> Block( + [FromRoute] string destUser + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + ValidationProvider.NotEmpty(destUser); + + // + var result = await IdentityManager + .Block( + User.Identity.Name, + destUser + ); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + + // + return exResult; + } + } + + /// + /// Unblock a Blocked User + /// + /// an instance of XFriendshipFollowing + /// an instance of XFriendshipFollowing + [HttpPost("Friendship/{destUser}/UnBlock")] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + public async Task> UnBlock( + [FromRoute] string destUser + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + ValidationProvider.NotEmpty(destUser); + + // + var result = await IdentityManager + .UnBlock( + User.Identity.Name, + destUser + ); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + #endregion + + // + #region Request Handlers ... + /// + /// Accept a Following Request + /// + /// a user identifier which represent destination user + /// an instance of XFriendshipFollower + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + [HttpPost("Friendship/{destUser}/AcceptRequest")] + public async Task> AcceptRequest( + [FromRoute] string destUser + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + ValidationProvider.NotEmpty(destUser); + + // + var result = await IdentityManager + .AcceptRequest( + User.Identity.Name, + destUser + ); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Reject a Following Request + /// + /// a user identifier which represent destination user + /// an instance of XFriendshipFollower + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + [HttpPost("Friendship/{destUser}/RejectRequest")] + public async Task> RejectRequest( + [FromRoute] string destUser + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + ValidationProvider.NotEmpty(destUser); + + // + var result = await IdentityManager + .RejectRequest( + User.Identity.Name, + destUser + ); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + #endregion + + // + #region Getters ... + /// + /// Check a User IsFollower of Requested User + /// + /// a user identifier which represent destination user + /// a boolean value + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + [HttpGet("Friendship/{destUser}/IsFollower")] + public async Task> IsFollower( + [FromRoute] string destUser + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + ValidationProvider.NotEmpty(destUser); + + // + var result = await IdentityManager + .IsFollower( + User.Identity.Name, + destUser + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Get Follower State of a User + /// + /// a user identifier which represent destination user + /// an instance of XFriendshipState + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + [HttpGet("Friendship/{destUser}/FollowerState")] + public async Task> FollowerState( + [FromRoute] string destUser + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + ValidationProvider.NotEmpty(destUser); + + // + var result = await IdentityManager + .GetFollowerState( + User.Identity.Name, + destUser + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Get Specific Follower + /// + /// a user identifier which represent destination user + /// an instance of XFriendshipFollower + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + [HttpGet("Friendship/{destUser}/Follower")] + public async Task> Follower( + [FromRoute] string destUser + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + ValidationProvider.NotEmpty(destUser); + + // + var result = await IdentityManager + .GetFollower( + User.Identity.Name, + destUser + ); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Get Followers List Of Current User + /// + /// a collection of XFriendshipFollower + [HttpGet("Friendship/Followers")] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + public async Task>> Followers() + { + // + // Do Action ... + try + { + // + var result = await IdentityManager + .GetFollowers(User.Identity.Name); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + + // + return exResult; + } + } + + /// + /// Get All Followers List Includes Blocked, Requested and etc + /// of Current User + /// + /// a collection of XFriendshipFollower + [HttpGet("Friendship/AllFollowers")] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + public async Task>> AllFollowers() + { + // + // Do Action ... + try + { + // + var result = await IdentityManager + .GetAllFollowers(User.Identity.Name); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Get All Followers List Includes Blocked, Requested and etc + /// of Current User based On Query Model ... + /// + /// a Query Result of XFriendDto + [HttpGet("Friendship/QueryFollowers")] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + public async Task>> QueryFollowers( + [FromQuery] XQuery query, + [FromQuery] string destUser + ) + { + // + // Do Action ... + try + { + // + // Normalizing Dest User ... + var userInfo = await GetUserInfo(); + bool isAdmin = userInfo.Roles.Contains(XUserRoles.ADMIN); + if (destUser.IsNullOrEmpty()) + { + destUser = userInfo.UserName; + } + bool isDestUserValid = isAdmin || destUser == userInfo.UserId || destUser == userInfo.Email || destUser == userInfo.PhoneNumber || destUser == userInfo.UserName; + if (!isDestUserValid) + { + XException.NotAllowed.Throw(); + } + + // + var result = await IdentityManager + .QueryFollowers( + query: query, + userSelectByParam: destUser + ); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Check a User is in Followings of Current User + /// + /// a user identifier which represent destination user + /// a boolean value + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + [HttpGet("Friendship/{destUser}/IsFollowing")] + public async Task> IsFollowing( + [FromRoute] string destUser + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + ValidationProvider.NotEmpty(destUser); + + // + var result = await IdentityManager + .IsFollowing( + User.Identity.Name, + destUser + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Get Following State Relation between Specific User + /// and Current User + /// + /// a user identifier which represent destination user + /// an instance of XFriendshipState + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + [HttpGet("Friendship/{destUser}/FollowingState")] + public async Task> FollowingState( + [FromRoute] string destUser + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + ValidationProvider.NotEmpty(destUser); + + // + var result = await IdentityManager + .GetFollowingState( + User.Identity.Name, + destUser + ); + + // + return Ok(result); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Get Specific Following Model + /// + /// a user identifier which represent destination user + /// an instance of XFriendshipFollowing + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + [HttpGet("Friendship/{destUser}/Following")] + public async Task> Following( + [FromRoute] string destUser + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + ValidationProvider.NotEmpty(destUser); + + // + var result = await IdentityManager + .GetFollowing( + User.Identity.Name, + destUser + ); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Get Followings of Current User + /// + /// a collection of XFriendshipFollowing + [HttpGet("Friendship/Followings")] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + public async Task>> Followings() + { + // + // Do Action ... + try + { + // + var result = await IdentityManager + .GetFollowings(User.Identity.Name); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Get All Following List Includes Blocked, Requested and etc + /// of Current User + /// + /// a collection of XFriendshipFollowing + [HttpGet("Friendship/AllFollowings")] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + public async Task>> AllFollowings() + { + // + // Do Action ... + try + { + // + var result = await IdentityManager + .GetAllFollowings(User.Identity.Name); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Get All Following List Includes Blocked, Requested and etc + /// of Current User based On Query Model ... + /// + /// a Query Result of XFriendDto + [HttpGet("Friendship/QueryFollowings")] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + public async Task>> QueryFollowings( + [FromQuery] XQuery query, + [FromQuery] string destUser + ) + { + // + // Do Action ... + try + { + // + // Normalizing Dest User ... + var userInfo = await GetUserInfo(); + bool isAdmin = userInfo.Roles.Contains(XUserRoles.ADMIN); + if (destUser.IsNullOrEmpty()) + { + destUser = userInfo.UserName; + } + bool isDestUserValid = isAdmin || destUser == userInfo.UserId || destUser == userInfo.Email || destUser == userInfo.PhoneNumber || destUser == userInfo.UserName; + if (!isDestUserValid) + { + XException.NotAllowed.Throw(); + } + + // + var result = await IdentityManager + .QueryFollowings( + query: query, + userSelectByParam: destUser + ); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + #endregion + + // + #region Others ... + /// + /// Return Following UserName's List of Current User + /// + /// a collection of UserNames + [HttpGet("Friendship/FollowingsList")] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + public async Task>> FollowingsList() + { + // + // Do Action ... + try + { + // + var result = await IdentityManager + .GetFollowingList(User.Identity.Name); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Return Followers UserName's List of Current User + /// + /// a collection of UserNames + [HttpGet("Friendship/FollowersList")] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + public async Task>> FollowersList() + { + // + // Do Action ... + try + { + // + var result = await IdentityManager + .GetFollowersList(User.Identity.Name); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Get Friendship Info Model between Specific User and Current User + /// + /// a user identifier which represent destination user + /// an instance of XFriendshipInfoDto + [HttpGet("Friendship/{destUser}/FriendshipInfo")] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + public async Task> FriendshipInfo( + [FromRoute] string destUser + ) + { + // + // Do Action ... + try + { + // + var result = await IdentityManager + .GetFriendshipInfo(User.Identity.Name, destUser); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + #endregion + #endregion + } +} \ No newline at end of file diff --git a/Controllers/Account/AccountController+Open.cs b/Controllers/Account/AccountController+Open.cs new file mode 100644 index 0000000..735b88c --- /dev/null +++ b/Controllers/Account/AccountController+Open.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using xCommons.Attributes; +using xCommons.Constants; +using xIdentityModels.Dtos; + +namespace xIds.Controllers +{ + public partial class AccountController + { + // + // Implememnt all Open Actions here ... + + // + #region Open Actions ... + [AllowAnonymous] + [RequireXPowered] + [HttpGet("Open")] + public async Task> Get( + [FromHeader] string token, + [FromQuery] string request + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty(token) + .AddNotEmpty(request) + .ValidateGroupAsync(); + + // + // Prepare Inner Dto Model ... + var dto = new XOpenActionInnerDto + { + Token = token, + Payload = request + }; + + // + // Do Action ... + var resultDto = await IdentityManager.OpenGet(dto); + Response.Headers.Add(XAuthentication.TOKEN, resultDto.Token); + + // + // Return Result + return Ok(resultDto.Payload); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Private ... + #endregion + } +} \ No newline at end of file diff --git a/Controllers/Account/AccountController+Profile.cs b/Controllers/Account/AccountController+Profile.cs new file mode 100644 index 0000000..8dbd5fe --- /dev/null +++ b/Controllers/Account/AccountController+Profile.cs @@ -0,0 +1,511 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using xIdentityHelper; +using xCommons.Attributes; +using xCommons.Extensions; +using xIdentityModels.Dtos; +using xIdentityModels.Models; +using xModels.Dtos; +using static IdentityServer4.IdentityServerConstants; + +namespace xIds.Controllers +{ + public partial class AccountController + { + // + #region Retrieve Actions ... + /// + /// Retrieve User Names based on UserIds + /// + /// a comma seperated list of UserIds + /// a collection of UserNames + [HttpGet("Profile/{ids}/GetNames")] + [Authorize(Policy = XPolicies.User)] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + public async Task>> GetNames( + [FromRoute] string ids + ) + { + // + // Do Action ... + try + { + // + ValidationProvider.NotEmpty(ids); + + // + var xIdList = ids.ParseListString(); + var result = await IdentityManager.GetUserNamesAsync( + xIdList + ); + + // + // Return Result + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// get user ids and retrieve corresponding user names + /// + /// an instance of XUserNameIdRequest which represent required UserIds collection + /// a collection of XUserNameIdResponse instance + [HttpPost("Profile/GetNameIds")] + [Authorize(Policy = XPolicies.User)] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + public async Task>> GetNameIds( + [FromBody] XUserNameIdRequest model + ) + { + // + // Do Action ... + try + { + // + ValidationProvider.NotNull(model); + + // + var result = await IdentityManager.GetUserNameIdsAsync( + model + ); + + // + // Return Result + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Get Profile Object of Specific User + /// + /// specifies which user profile must be retrieved + /// an instance of XUserProfileDto + [RequireXPowered] + [Authorize(Policy = XPolicies.User)] + [Authorize(Policy = LocalApi.PolicyName)] + [HttpGet("Profile/{userSelectByParam?}")] + [Authorize(Policy = XPolicies.EnabledUser)] + public async Task> GetProfile( + [FromRoute] string userSelectByParam = "" + ) + { + // + // Do Action ... + try + { + // + if (userSelectByParam.IsNullOrEmpty()) + { + userSelectByParam = User.Identity.Name; + } + + // + var result = await IdentityManager + .GetUserProfileAsync( + userSelectByParam, + User.Identity.Name + ); + + // + // Return Result + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Query Users + /// for specifiy users role for query user 'role: string' and 'forceRole: boolean' in header + /// + /// how to filter results based on XQuery structure + /// an string which represent user role + /// if it's true the user must has exact role, otherwise top level users also listed + /// an instance of XQueryResult of XUserProfileDto + [RequireXPowered] + [HttpGet("Profile/Query")] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledAdmin)] + [Authorize(Policy = XPolicies.EnabledAgent)] + public async Task>> QueryProfiles( + [FromQuery] XQuery query, [FromHeader] string role, [FromHeader] bool forceRole = false + ) + { + // + // Do Action ... + try + { + // + var userId = User.Identity.Name; + + // + var result = new XQueryResult(); + if (role.IsNullOrEmpty()) + { + // + result = await IdentityManager.QueryUsers( + userId, + query + ); + } + else + { + // + result = await IdentityManager.QueryInRoleUsers( + userId, + role, + query, + forceRole + ); + } + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exception = GetExceptionActionResult(ex); + return exception; + } + } + + /// + /// Query Specified User's Profile Images + /// + /// determine's which user profile must be retrieved + /// how to filter results based on XQuery structure + /// an instance of XQueryResult of XProfileImage + [RequireXPowered] + [Authorize(Policy = XPolicies.User)] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + [Authorize(Policy = XPolicies.EnabledAdmin)] + [HttpGet("Profile/Avatars/Query/{userSelectByParam?}")] + public async Task>> QueryAvatars( + [FromQuery] XQuery query, [FromRoute] string userSelectByParam = null + ) + { + // + // Do Action ... + try + { + // + if (userSelectByParam.IsNullOrEmpty()) + { + userSelectByParam = User.Identity.Name; + } + + // + var result = await IdentityManager.QueryAvatars( + userSelectByParam, + query + ); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exception = GetExceptionActionResult(ex); + return exception; + } + } + #endregion + + // + #region Update Actions ... + /// + /// Update User Profile based on XProfileUpdateRequest (FirstName/LastName/DateOfBirth) + /// + /// user update info, an instance of XProfileUpdateRequest + /// determine's which user profile must be retrieved + /// an instance of XUserProfileDto + [RequireXPowered] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + [HttpPost("Profile/Update/{userSelectByParam?}")] + public async Task> UpdateProfile( + [FromBody] XProfileUpdateRequest model, [FromRoute] string userSelectByParam = null + ) + { + // + // Do Action ... + try + { + // + if (userSelectByParam.IsNullOrEmpty()) + { + userSelectByParam = User.Identity.Name; + } + + // + var result = await IdentityManager.ProfileUpdateAsync( + userSelectByParam, + User.Identity.Name, + model + ); + + // + // Return Result + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Update a User Profile based on XProfileUpdateRequest Full + /// + /// user update info, an instance of XProfileUpdateRequest + /// determine's which user profile must be retrieved + /// an instance of XUserProfileDto + [RequireXPowered] + [Authorize(Policy = LocalApi.PolicyName)] + [HttpPost("Profile/{userSelectByParam?}")] + [Authorize(Policy = XPolicies.EnabledAdminOrAgent)] + public async Task> FullUpdateProfile( + [FromBody] XProfileUpdateRequest model, [FromRoute] string userSelectByParam = null + ) + { + // + // Do Action ... + try + { + // + if (userSelectByParam.IsNullOrEmpty()) + { + userSelectByParam = User.Identity.Name; + } + + // + var result = await IdentityManager.FullProfileUpdateAsync( + userSelectByParam, + User.Identity.Name, + model + ); + + // + // Return Result + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + #endregion + + // + #region Avatar Actions ... + /// + /// Add a New Profile Image + /// + /// an specific File to upload, IFormFile + /// an instance of XUserProfileDto + [RequireXPowered] + [HttpPost("Profile/Avatar")] + [RequestSizeLimit(966_367_641)] + [Authorize(Policy = XPolicies.User)] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + public async Task> AddAvatar( + [FromForm] IFormFile file + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + ValidationProvider.NotNull(file); + + // + // Get Request ... + var result = await IdentityManager + .AddAvatar( + User.Identity.Name, + file + ); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Add a New Collection of Profile Images + /// + /// a collection of Files to upload, IFormFileCollection + /// an instance of XUserProfileDto + [RequireXPowered] + [HttpPost("Profile/Avatars")] + [RequestSizeLimit(966_367_641)] + [Authorize(Policy = XPolicies.User)] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + public async Task> AddAvatars( + [FromForm] IFormFileCollection files + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + ValidationProvider.NotZeroChilds(files); + + // + // Get Request ... + var result = await IdentityManager.AddAvatars( + User.Identity.Name, + files + ); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Remove Profile Images + /// + /// a comma seperated list of avatarIds to remove + /// an instance of XUserProfileDto + [RequireXPowered] + [Authorize(Policy = XPolicies.User)] + [HttpDelete("Profile/Avatars/{ids}")] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + public async Task> RemoveAvatars( + [FromRoute] string ids + ) + { + // + // Do Action ... + try + { + // + ValidationProvider.NotEmpty(ids); + var idList = ids.Split(","); + var idCollection = new Collection(); + + // + foreach (var s in idList) + { + idCollection.Add(int.Parse(s)); + } + + // + // Get Request ... + var result = await IdentityManager + .RemoveAvatar( + User.Identity.Name, + idCollection.ToArray() + ); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// set Specified Profile Image as Avatar + /// + /// an integer which reperesent AvatarId to set as current Avatar + /// an instance of XUserProfileDto + [RequireXPowered] + [HttpPost("Profile/Avatars/{id:int}/Set")] + [Authorize(Policy = XPolicies.User)] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + public async Task> SetAvatar( + [FromRoute] int id + ) + { + // + // Do Action ... + try + { + // + var result = await IdentityManager + .SetAvatar( + User.Identity.Name, + id + ); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exception = GetExceptionActionResult(ex); + return exception; + } + } + #endregion + } +} \ No newline at end of file diff --git a/Controllers/Account/AccountController+Query.cs b/Controllers/Account/AccountController+Query.cs new file mode 100644 index 0000000..2580b32 --- /dev/null +++ b/Controllers/Account/AccountController+Query.cs @@ -0,0 +1,63 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using xCommons.Attributes; +using xCommons.Extensions; +using xIdentityHelper; +using xModels.Dtos; +using static IdentityServer4.IdentityServerConstants; + +namespace xIds.Controllers +{ + public partial class AccountController + { + /// + /// Query Users + /// for specifiy users role for query user 'role: string' and 'forceRole: boolean' in header + /// + /// how to filter results based on XQuery structure + /// an string which represent user role + /// if it's true the user must has exact role, otherwise top level users also listed + /// an instance of XQueryResult of XUserProfileDto + [RequireXPowered] + [HttpGet("Users/Query")] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + public async Task>> QueryUsers( + [FromQuery] XQuery query, + [FromHeader] string role, + [FromHeader] bool forceRole = false + ) + { + // + // Do Actions ... + try + { + // + // Retrieve User Info ... + var userInfo = await GetUserInfo(); + + // + // Retrieve Result Based on Provided Query ... + var result = await IdentityManager.QueryUsers( + role: role, + query: query, + forceRole: forceRole, + requestedUserSelectByParam: userInfo.UserId + ); + + // + // Return result ... + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exception = GetExceptionActionResult(ex); + return exception; + } + } + } +} \ No newline at end of file diff --git a/Controllers/Account/AccountController+Registration.cs b/Controllers/Account/AccountController+Registration.cs new file mode 100644 index 0000000..c971646 --- /dev/null +++ b/Controllers/Account/AccountController+Registration.cs @@ -0,0 +1,1314 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using xIdentityHelper; +using xCommons.Attributes; +using xCommons.Extensions; +using xExceptions.Constants; +using xIdentityModels.Constants; +using xIdentityModels.Models; +using static IdentityServer4.IdentityServerConstants; + +namespace xIds.Controllers +{ + public partial class AccountController + { + // + #region CanRegister ... + /// + /// Check a UserName is Available For Registration or not + /// + /// an string value which represent desired username + /// a boolean value + [AllowAnonymous] + [RequireXPowered] + [HttpGet("CanRegisterUserName/{userName?}")] + public async Task> CanRegisterUserName( + [FromRoute] string userName + ) + { + // + // Validate Args ... + if (userName.IsNullOrEmpty()) + { + return BadRequest( + XException.InvalidArgs + .ToXError() + ); + } + + // + // Do Action ... + try + { + // + var result = await IdentityManager + .CanRegister(userName); + + // + // Return Result + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + + // + return result; + } + } + + /// + /// Check a Mobile Number is Available For Registration or not + /// + /// an string value which represent desired mobile number + /// a boolean value + [AllowAnonymous] + [RequireXPowered] + [HttpGet("CanRegisterMobileNumber/{mobileNumber?}")] + public async Task> CanRegisterMobileNumber( + [FromRoute] string mobileNumber + ) + { + // + // Validate Args ... + if (mobileNumber.IsNullOrEmpty()) + { + return BadRequest( + XException.InvalidArgs + .ToXError() + ); + } + if (!mobileNumber.IsValidMobileNumber()) + { + return BadRequest( + XException.InvalidMobileNumber + .ToXError() + ); + } + + // + // Do Action ... + try + { + // + var result = await IdentityManager + .CanRegister(mobileNumber); + + // + // Return Result + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + + // + return result; + } + } + + /// + /// Check a Email Address is Available For Registration or not + /// + /// an string value which represent desired email address + /// a boolean value + [AllowAnonymous] + [RequireXPowered] + [HttpGet("CanRegisterEmail/{email?}")] + public async Task> CanRegisterEmail( + [FromRoute] string email + ) + { + // + // Validate Args ... + if (email.IsNullOrEmpty()) + { + return BadRequest( + XException.InvalidArgs + .ToXError() + ); + } + if (!email.IsValidEmail()) + { + return BadRequest( + XException.InvalidEmailAddress + .ToXError() + ); + } + + // + // Do Action ... + try + { + // + var result = await IdentityManager + .CanRegister(email); + + // + // Return Result + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + + // + return result; + } + } + #endregion + + // + #region Confirmations ... + /// + /// Check User is Confirmed Email or not + /// + /// a boolean value + [RequireXPowered] + [HttpGet("IsConfirmedEmail")] + [Authorize(Policy = XPolicies.User)] + [Authorize(Policy = LocalApi.PolicyName)] + public async Task> IsConfirmedEmail() + { + // + // Do Action ... + try + { + // + var result = await IdentityManager + .IsConfirmedEmail(User.Identity.Name); + + // + // Return Result + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + + // + return result; + } + } + + /// + /// Check User is Confirmed Mobile or not + /// + /// a boolean value + [RequireXPowered] + [HttpGet("IsConfirmedMobile")] + [Authorize(Policy = XPolicies.User)] + [Authorize(Policy = LocalApi.PolicyName)] + public async Task> IsConfirmedMobile() + { + // + // Do Action ... + try + { + // + var result = await IdentityManager + .IsConfirmedMobile(User.Identity.Name); + + // + // Return Result + return Ok(result); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + + // + return result; + } + } + + /// + /// Confirm Registration + /// + /// + /// for this action you have to provide: + /// - Lang: client device currently used locale, such as: en-US. + /// - ActionToken: user's requested action token for this action. + /// - Device: user's client device which is an instance of XDevice. + /// - ReturnUrl: client application Login URL. + /// + /// Sample request: + /// + /// { + /// "lang": "fa-IR", + /// "actionToken": "" + /// "returnUrl": "http://localhost/login", + /// "device": { + /// "os":"Mac", + /// "browser":"Chrome", + /// "osVersion":"mac-os-x-15", + /// "userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36", + /// "deviceType":3, + /// "identifier":"[Mac]-[mac-os-x-15]-[3]-[Chrome]-[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36]", + /// "token":"96f400dd9aa5c57a8cec9d6f4775bad3" + /// } + /// } + /// + /// + /// an instance of XActionRequest class which provider requirement for action + /// + [AllowAnonymous] + [RequireXPowered] + [HttpPost("ConfirmRegistration")] + public async Task ConfirmRegistration( + [FromBody] XActionRequest model + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + if (!ModelState.IsValid) + { + XException.InvalidArgs.Throw(); + } + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty( + model.Lang, + model.ActionToken, + model.ReturnUrl + ) + .AddNotNull(model.Device) + .ValidateGroupAsync(); + + // + // Get Request ... + await IdentityManager + .ConfirmRegistration( + model.Lang, + model.Device, + model.ActionToken, + model.ReturnUrl + ); + + // + return Ok(); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Confirm Requested Mobile Number + /// + /// + /// for this action you have to provide: + /// - Lang: client device currently used locale, such as: en-US. + /// - ActionToken: user's requested action token for this action. + /// - Device: user's client device which is an instance of XDevice. + /// - MobileVerificationCode: a verification code which produced due Request Action. + /// + /// Sample request: + /// + /// { + /// "lang": "fa-IR", + /// "actionToken": "" + /// "mobileVerificationCode": "", + /// "device": { + /// "os":"Mac", + /// "browser":"Chrome", + /// "osVersion":"mac-os-x-15", + /// "userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36", + /// "deviceType":3, + /// "identifier":"[Mac]-[mac-os-x-15]-[3]-[Chrome]-[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36]", + /// "token":"96f400dd9aa5c57a8cec9d6f4775bad3" + /// } + /// } + /// + /// + /// an instance of XActionRequest class which provider requirement for action + /// an instance of XActionResponse + [AllowAnonymous] + [RequireXPowered] + [HttpPost("ConfirmMobileNumber")] + public async Task> ConfirmMobileNumber( + [FromBody] XActionRequest model + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + if (!ModelState.IsValid) + { + XException.InvalidArgs.Throw(); + } + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty( + model.Lang, + model.ActionToken, + model.MobileVerificationCode + ) + .AddNotNull(model.Device) + .ValidateGroupAsync(); + + // + // Get Request ... + var result = await IdentityManager + .ConfirmMobileNumber( + model.Lang, + model.Device, + model.ActionToken, + model.MobileVerificationCode + ); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Confirm Requested Email Address + /// + /// + /// for this action you have to provide: + /// - Lang: client device currently used locale, such as: en-US. + /// - ActionToken: user's requested action token for this action. + /// - Device: user's client device which is an instance of XDevice. + /// - EmailVerificationCode: a verification code which produced due Request Action. + /// + /// Sample request: + /// + /// { + /// "lang": "fa-IR", + /// "actionToken": "" + /// "emailVerificationCode": "", + /// "device": { + /// "os":"Mac", + /// "browser":"Chrome", + /// "osVersion":"mac-os-x-15", + /// "userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36", + /// "deviceType":3, + /// "identifier":"[Mac]-[mac-os-x-15]-[3]-[Chrome]-[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36]", + /// "token":"96f400dd9aa5c57a8cec9d6f4775bad3" + /// } + /// } + /// + /// + /// an instance of XActionRequest class which provider requirement for action + /// an instance of XActionResponse + [AllowAnonymous] + [RequireXPowered] + [HttpPost("ConfirmEmailAddress")] + public async Task> ConfirmEmailAddress( + [FromBody] XActionRequest model + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + if (!ModelState.IsValid) + { + XException.InvalidArgs.Throw(); + } + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty( + model.Lang, + model.ActionToken, + model.EmailVerificationCode + ) + .AddNotNull(model.Device) + .ValidateGroupAsync(); + + // + // Get Request ... + var result = await IdentityManager + .ConfirmEmailAddress( + model.Lang, + model.Device, + model.ActionToken, + model.EmailVerificationCode + ); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + #endregion + + // + #region Request for Actions ... + /// + /// Request For Mobile Number Change/Confirm + /// + /// + /// for this action you have to provide: + /// - Lang: client device currently used locale, such as: en-US. + /// - Password: user's current active password. + /// - MobileNumber: which phone number is going to confirm. + /// - Device: user's client device which is an instance of XDevice. + /// + /// Sample request: + /// + /// { + /// "lang": "fa-IR", + /// "password": "", + /// "mobileNumber": "", + /// "device": { + /// "os":"Mac", + /// "browser":"Chrome", + /// "osVersion":"mac-os-x-15", + /// "userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36", + /// "deviceType":3, + /// "identifier":"[Mac]-[mac-os-x-15]-[3]-[Chrome]-[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36]", + /// "token":"96f400dd9aa5c57a8cec9d6f4775bad3" + /// } + /// } + /// + /// + /// an instance of XActionRequest class which provider requirement for action + /// an instance of XActionResponse + [AllowAnonymous] + [RequireXPowered] + [HttpPost("RequestConfirmMobile")] + public async Task> RequestConfirmMobile( + [FromBody] XActionRequest model + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + if (!ModelState.IsValid) + { + XException.InvalidArgs.Throw(); + } + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty( + model.Lang, + model.Password, + model.MobileNumber + ) + .AddNotNull(model.Device) + .AddMobileNumber(model.MobileNumber) + .ValidateGroupAsync(); + + // + // Get UserSelect By Param ... + var userSelectByParam = GetUserSelectByParam( + model, + excludes: new List { + XUserSelectBy.Email, + XUserSelectBy.MobileNumber + }); + + // + // Get Request ... + var result = await IdentityManager + .RequestConfirmMobile( + model.Lang, + model.Device, + userSelectByParam, + model.Password, + model.MobileNumber + ); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Request For Email Address Change/Confirm + /// + /// + /// for this action you have to provide: + /// - Lang: client device currently used locale, such as: en-US. + /// - Password: user's current active password. + /// - Email: which email address is going to confirm. + /// - Device: user's client device which is an instance of XDevice. + /// + /// Sample request: + /// + /// { + /// "lang": "fa-IR", + /// "password": "", + /// "email": "", + /// "device": { + /// "os":"Mac", + /// "browser":"Chrome", + /// "osVersion":"mac-os-x-15", + /// "userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36", + /// "deviceType":3, + /// "identifier":"[Mac]-[mac-os-x-15]-[3]-[Chrome]-[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36]", + /// "token":"96f400dd9aa5c57a8cec9d6f4775bad3" + /// } + /// } + /// + /// + /// an instance of XActionRequest class which provider requirement for action + /// an instance of XActionResponse + [AllowAnonymous] + [RequireXPowered] + [HttpPost("RequestConfirmEmail")] + public async Task> RequestConfirmEmail( + [FromBody] XActionRequest model + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + if (!ModelState.IsValid) + { + XException.InvalidArgs.Throw(); + } + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty( + model.Lang, + model.Email, + model.Password) + .AddNotNull(model.Device) + .AddEmailAddress(model.Email) + .ValidateGroupAsync(); + + // + // Get UserSelect By Param ... + var userSelectByParam = GetUserSelectByParam( + model, + excludes: new List { + XUserSelectBy.Email, + XUserSelectBy.MobileNumber + }); + + // + // Get Request ... + var result = await IdentityManager + .RequestConfirmEmail( + model.Lang, + model.Device, + userSelectByParam, + model.Password, + model.Email + ); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Request Reset Password Action + /// + /// + /// for this action you have to provide: + /// - Lang: client device currently used locale, such as: en-US. + /// - Device: user's client device which is an instance of XDevice. + /// - ReturnUrl: client application ResetPassword URL. + /// + /// Sample request: + /// + /// { + /// "lang": "fa-IR", + /// "returnUrl": "http://localhost/reset-password", + /// "device": { + /// "os":"Mac", + /// "browser":"Chrome", + /// "osVersion":"mac-os-x-15", + /// "userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36", + /// "deviceType":3, + /// "identifier":"[Mac]-[mac-os-x-15]-[3]-[Chrome]-[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36]", + /// "token":"96f400dd9aa5c57a8cec9d6f4775bad3" + /// } + /// } + /// + /// + /// an instance of XActionRequest class which provider requirement for action + /// an instance of XActionResponse + [AllowAnonymous] + [RequireXPowered] + [HttpPost("RequestResetPassword")] + public async Task> RequestResetPassword( + [FromBody] XActionRequest model + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + if (!ModelState.IsValid) + { + XException.InvalidArgs.Throw(); + } + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty( + model.Lang, + model.ReturnUrl) + .AddNotNull(model.Device) + .AddUrl(model.ReturnUrl) + .ValidateGroupAsync(); + + // + // Get UserSelect By Param ... + var userSelectByParam = GetUserSelectByParam(model); + + // + // Get Request ... + var result = await IdentityManager + .RequestResetPassword( + model.Lang, + model.Device, + userSelectByParam, + model.ReturnUrl + ); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Reset a User's Password + /// + /// + /// for this action you have to provide: + /// - Lang: client device currently used locale, such as: en-US. + /// - ActionToken: user's requested action token for this action. + /// - NewPassword: user's new password to change. + /// - Device: user's client device which is an instance of XDevice. + /// - ReturnUrl: client application Login URL. + /// + /// Sample request: + /// + /// { + /// "lang": "fa-IR", + /// "actionToken": "", + /// "newPassword": "", + /// "returnUrl": "http://localhost/login", + /// "device": { + /// "os":"Mac", + /// "browser":"Chrome", + /// "osVersion":"mac-os-x-15", + /// "userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36", + /// "deviceType":3, + /// "identifier":"[Mac]-[mac-os-x-15]-[3]-[Chrome]-[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36]", + /// "token":"96f400dd9aa5c57a8cec9d6f4775bad3" + /// } + /// } + /// + /// + /// an instance of XActionRequest class which provider requirement for action + /// + [AllowAnonymous] + [RequireXPowered] + [HttpPost("ResetPassword")] + public async Task ResetPassword( + [FromBody] XActionRequest model + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + if (!ModelState.IsValid) + { + XException.InvalidArgs.Throw(); + } + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty( + model.Lang, + model.ActionToken, + model.NewPassword, + model.ReturnUrl) + .AddNotNull(model.Device) + .ValidateGroupAsync(); + + // + // Get Request ... + await IdentityManager + .ResetPassword( + model.Lang, + model.Device, + model.ActionToken, + model.NewPassword, + model.ReturnUrl + ); + + // + return Ok(); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Request Registration Confirm + /// + /// + /// for this action you have to provide: + /// - a UserSelectedBy identifier which represet User. + /// - Lang: client device currently used locale, such as: en-US. + /// - Password: user's current active password. + /// - Device: user's client device which is an instance of XDevice. + /// - ReturnUrl: client application Confirm Registration URL. + /// + /// Sample request: + /// + /// { + /// "lang": "fa-IR", + /// "userId": "" + /// "password": "", + /// "returnUrl": "http://localhost/confirm", + /// "device": { + /// "os":"Mac", + /// "browser":"Chrome", + /// "osVersion":"mac-os-x-15", + /// "userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36", + /// "deviceType":3, + /// "identifier":"[Mac]-[mac-os-x-15]-[3]-[Chrome]-[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36]", + /// "token":"96f400dd9aa5c57a8cec9d6f4775bad3" + /// } + /// } + /// + /// + /// an instance of XActionRequest class which provider requirement for action + /// + [AllowAnonymous] + [RequireXPowered] + [HttpPost("RequestConfirmRegistration")] + public async Task RequestConfirmRegistration( + [FromBody] XActionRequest model + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + if (!ModelState.IsValid) + { + XException.InvalidArgs.Throw(); + } + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty( + model.Lang, + model.Password, + model.ReturnUrl) + .AddNotNull(model.Device) + .AddUrl(model.ReturnUrl) + .ValidateGroupAsync(); + + // + // Get UserSelect By Param ... + var userSelectByParam = GetUserSelectByParam(model); + + // + // Get Request ... + await IdentityManager + .RequestConfirmRegistration( + model.Lang, + model.Device, + userSelectByParam, + model.Password, + model.ReturnUrl + ); + + // + return Ok(); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + #endregion + + // + #region Registration ... + /// + /// Invite a User to Register on Dashboard + /// + /// + /// for this action you have to provide: + /// - Lang: client device currently used locale, such as: en-US. + /// - Email: which email address is going to invite. + /// - Device: user's client device which is an instance of XDevice. + /// - ReturnUrl: client application Registration URL. + /// notice in this step you had to provide all this informations as FormData + /// + /// Sample request: + /// + /// { + /// "lang": "fa-IR", + /// "email": "", + /// "returnUrl": "", + /// "device": { + /// "os":"Mac", + /// "browser":"Chrome", + /// "osVersion":"mac-os-x-15", + /// "userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36", + /// "deviceType":3, + /// "identifier":"[Mac]-[mac-os-x-15]-[3]-[Chrome]-[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36]", + /// "token":"96f400dd9aa5c57a8cec9d6f4775bad3" + /// } + /// } + /// + /// + /// an instance of XActionRequest class which provider requirement for action + /// an instance of XActionResponse + [RequireXPowered] + [HttpPost("InviteUser")] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledAdmin)] + [Authorize(Policy = XPolicies.EnabledAgent)] + public async Task> InviteUser( + [FromBody] XActionRequest model + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + if (!ModelState.IsValid) + { + XException.InvalidArgs.Throw(); + } + + // + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty( + model.Lang, + model.Email, + model.ReturnUrl) + .AddEmailAddress(model.Email) + .AddNotNull(model.Device) + .ValidateGroupAsync(); + + // + var result = await IdentityManager + .InviteUser( + model.Lang, + model.Device, + model.Email, + model.ReturnUrl); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var result = GetExceptionActionResult(ex); + return result; + } + } + + /// + /// Recieve some Basic Informations and Start Registration Proccess + /// if they Valid + /// + /// Registration Proccess Starts with Invoking this Action + /// + /// + /// for this action you have to provide: + /// - Lang: client device currently used locale, such as: en-US. + /// - FirstName: user's FirstName. + /// - LastName: user's LastName. + /// - DateOfBirth: user's dob date. + /// - MobileNumber: user's MobileNumber optional. + /// - Email: user's Email optional. + /// - ActionToken: user's requested action token for this action (if user invited). + /// - Device: user's client device which is an instance of XDevice. + /// + /// Sample request: + /// + /// { + /// "lang": "fa-IR", + /// "firstName": "", + /// "lastName": "", + /// "dateOfBirth": "", + /// "email": "", + /// "mobileNumber": "", + /// "actionToken": "", + /// "device": { + /// "os":"Mac", + /// "browser":"Chrome", + /// "osVersion":"mac-os-x-15", + /// "userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36", + /// "deviceType":3, + /// "identifier":"[Mac]-[mac-os-x-15]-[3]-[Chrome]-[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36]", + /// "token":"96f400dd9aa5c57a8cec9d6f4775bad3" + /// } + /// } + /// + /// + /// an instance of XActionRequest class which provider requirement for action + /// an instance of XActionResponse + [AllowAnonymous] + [RequireXPowered] + [HttpPost("RequestRegistration")] + public async Task> RequestRegistration( + [FromBody] XActionRequest model + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + if (!ModelState.IsValid) + { + XException.InvalidArgs.Throw(); + } + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty( + model.Lang, + model.FirstName, + model.LastName) + .AddNotNull( + model.Device, + model.DateOfBirth) + .ValidateGroupAsync(); + + // + // Check Date Of Birth ... + if (!model.DateOfBirth.HasValue) + { + XException.InvalidArgs.Throw(); ; + } + + // + // Validate Mobile Number if Exists ... + if (!model.MobileNumber.IsNullOrEmpty()) + { + ValidationProvider.MobileNumber(model.MobileNumber); + } + + // + // Validate Email Address if Exists ... + if (!model.Email.IsNullOrEmpty()) + { + ValidationProvider.EmailAddress(model.Email); + } + + // + // Get Request ... + var result = await IdentityManager + .RequestRegistration( + model.Lang, + model.Device, + model.ActionToken, + model.FirstName, + model.LastName, + model.DateOfBirth.GetValueOrDefault(), + model.MobileNumber, + model.Email + ); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + + // + return exResult; + } + } + + /// + /// Add User Account Informations + /// + /// + /// for this action you have to provide: + /// - Lang: client device currently used locale, such as: en-US. + /// - ActionToken: user's requested action token for this action. + /// - UserName: user name. + /// - Password: assigne password. + /// - Device: user's client device which is an instance of XDevice. + /// + /// Sample request: + /// + /// { + /// "lang": "fa-IR", + /// "userName": "", + /// "password": "", + /// "actionToken": "", + /// "device": { + /// "os":"Mac", + /// "browser":"Chrome", + /// "osVersion":"mac-os-x-15", + /// "userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36", + /// "deviceType":3, + /// "identifier":"[Mac]-[mac-os-x-15]-[3]-[Chrome]-[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36]", + /// "token":"96f400dd9aa5c57a8cec9d6f4775bad3" + /// } + /// } + /// + /// + /// an instance of XActionRequest class which provider requirement for action + /// an instance of XActionResponse + [AllowAnonymous] + [RequireXPowered] + [HttpPost("AddAccountInfo")] + public async Task> AddAccountInfo( + [FromBody] XActionRequest model + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + if (!ModelState.IsValid) + { + XException.InvalidArgs.Throw(); + } + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty( + model.Lang, + model.ActionToken, + model.UserName, + model.Password) + .AddNotNull(model.Device) + .ValidateGroupAsync(); + + // + // Get Request ... + var result = await IdentityManager + .AddAcountInfo( + model.Lang, + model.Device, + model.ActionToken, + model.UserName, + model.Password); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Upload and Attach a Profile Image for User. this is an Optional Step + /// + /// user's requested action token + /// an instance of IFormFile for user's Avatar + /// an instance of XActionResponse + [AllowAnonymous] + [RequireXPowered] + [HttpPost("AttachProfileImage")] + public async Task> AttachProfileImage( + [FromForm] string actionToken, [FromForm] IFormFile file + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty(actionToken) + .AddNotNull(file) + .ValidateGroupAsync(); + + // + var result = await IdentityManager + .AttachProfileImage(actionToken, file); + + // + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + return exResult; + } + } + + /// + /// Finish Registrationn Proccess + /// + /// + /// for this action you have to provide: + /// - a proper User Identifier such as: (UserId, UserName, Email or MobileNumber). + /// - Lang: client device currently used locale, such as: en-US. + /// - ActionToken: user's requested action token for this action. + /// - Password: assigne password. + /// - Device: user's client device which is an instance of XDevice. + /// - ReturnUrl: client application Login URL. + /// + /// Sample request: + /// + /// { + /// "lang": "fa-IR", + /// "userName": "", + /// "password": "", + /// "returnUrl": "", + /// "device": { + /// "os":"Mac", + /// "browser":"Chrome", + /// "osVersion":"mac-os-x-15", + /// "userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36", + /// "deviceType":3, + /// "identifier":"[Mac]-[mac-os-x-15]-[3]-[Chrome]-[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) x-framework-test/0.0.0 Chrome/87.0.4280.141 Electron/11.2.0 Safari/537.36]", + /// "token":"96f400dd9aa5c57a8cec9d6f4775bad3" + /// } + /// } + /// + /// + /// an instance of XActionRequest class which provider requirement for action + /// + [AllowAnonymous] + [RequireXPowered] + [HttpPost("FinishRegistration")] + public async Task FinishRegistration( + [FromBody] XActionRequest model + ) + { + // + // Do Action ... + try + { + // + // Validate Args ... + if (!ModelState.IsValid) + { + XException.InvalidArgs.Throw(); + } + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty( + model.Lang, + model.Password, + model.ReturnUrl) + .AddNotNull(model.Device) + .AddUrl(model.ReturnUrl) + .ValidateGroupAsync(); + + // + var userSelectByParam = IdentityManager + .GetUserSelectByParam(model); + ValidationProvider + .NotEmpty(userSelectByParam); + + // + // Get Request ... + await IdentityManager + .FinishRegistration( + model.Lang, + model.Device, + userSelectByParam, + model.Password, + model.ReturnUrl + ); + + // + return Ok(); + } + catch (Exception ex) + { + // + var exResult = GetExceptionActionResult(ex); + + // + return exResult; + } + } + #endregion + } +} \ No newline at end of file diff --git a/Controllers/Account/AccountController+Search.cs b/Controllers/Account/AccountController+Search.cs new file mode 100644 index 0000000..ace18e7 --- /dev/null +++ b/Controllers/Account/AccountController+Search.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using xCommons.Attributes; +using xCommons.Extensions; +using xIdentityHelper; +using xIdentityModels.Dtos; +using xModels.Dtos; +using static IdentityServer4.IdentityServerConstants; + +namespace xIds.Controllers +{ + /// + /// Actions which related to Search and Suggest Users for + /// Friendship or Detecting ... + /// + public partial class AccountController + { + // + #region Actions of Search ... + /// + /// Query Open To Search Users Profiles ... + /// Query all Users Which their Profiles is Open To Search ... + /// + /// how to filter results based on XQuery structure + /// an instance of XQueryResult of XUserProfileDto + [RequireXPowered] + [HttpGet("OpenToSearch/Query")] + [Authorize(Policy = LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + public async Task>> QueryOpenToSearchProfiles( + [FromQuery] XQuery query) + { + // + // Do Action ... + try + { + // + var userId = User.Identity.Name; + + // + var result = await IdentityManager.QueryOpenToSearchUsers( + userId, + query + ); + + return Ok(result + .ToDynamicObject()); + } + catch (Exception ex) + { + // + var exception = GetExceptionActionResult(ex); + return exception; + } + } + #endregion + } +} \ No newline at end of file diff --git a/Controllers/Account/AccountController+Test.cs b/Controllers/Account/AccountController+Test.cs new file mode 100644 index 0000000..f6608e0 --- /dev/null +++ b/Controllers/Account/AccountController+Test.cs @@ -0,0 +1,161 @@ +using System.Linq; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using xIdentityHelper; +using xCommons.Attributes; +using static IdentityServer4.IdentityServerConstants; + +namespace xIds.Controllers +{ + public partial class AccountController + { + + // + #region Test Actions ... + /// + /// API Read Scope + /// + /// string message + [RequireXPowered] + [Authorize(LocalApi.PolicyName)] + [HttpGet("Test/PassReadAccess")] + [Authorize(Policy = XPolicies.ReadAccess)] + public ActionResult PassReadAccess() + { + return Ok("Read Access Passed ..."); + } + + /// + /// API Write Scope + /// + /// string message + [RequireXPowered] + [Authorize(LocalApi.PolicyName)] + [HttpGet("Test/PassWriteAccess")] + [Authorize(Policy = XPolicies.WriteAccess)] + public ActionResult PassWriteAccess() + { + return Ok("Write Access Passed ..."); + } + + /// + /// API Admin Scope + /// + /// string message + [RequireXPowered] + [Authorize(LocalApi.PolicyName)] + [HttpGet("Test/PassAdminAccess")] + [Authorize(Policy = XPolicies.AdminAccess)] + public ActionResult PassAdminAccess() + { + return Ok("Admin Access Passed ..."); + } + + /// + /// API Manage Scop + /// + /// string message + [RequireXPowered] + [Authorize(LocalApi.PolicyName)] + [HttpGet("Test/PassManageAccess")] + [Authorize(Policy = XPolicies.ManageAccess)] + public ActionResult PassManageAccess() + { + return Ok("Manage Access Passed ..."); + } + + /// + /// a simple Action which returns a List of Authenticated User Claims + /// + /// string message which represent current user's claims + [RequireXPowered] + [HttpGet("Test/HiClaims")] + [Authorize(LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.User)] + public ActionResult HiClaims() + { + // + var result = new + { + name = User.Identity.Name, + claims = User.Claims.Select(c => new + { + c.Type, + c.Value + }) + }; + + // + return Ok(result); + } + + /// + /// a simple Hello User for Checking Authentication and Policy + /// + /// string message which contains authenticated user name + [RequireXPowered] + [HttpGet("Test/HiUser")] + [Authorize(LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.User)] + public ActionResult HiUser() + { + // + var result = $"Hi User: {User.Identity.Name} ..."; + + // + return Ok(result); + } + + /// + /// a simple Hello User for Checking Authentication and Policy + /// + /// string message which contains authenticated user name + [RequireXPowered] + [HttpGet("Test/HiEnabledUser")] + [Authorize(LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledUser)] + public ActionResult HiEnabledUser() + { + // + var result = $"Hi User: {User.Identity.Name} is Enabled ..."; + + // + return Ok(result); + } + + /// + /// a simple Hello User for Checking Authentication and Policy + /// + /// string message which contains authenticated user name + [RequireXPowered] + [HttpGet("Test/HiAdmin")] + [Authorize(LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.Admin)] + public ActionResult HiAdmin() + { + // + var result = $"Hi Admin: {User.Identity.Name} ..."; + + // + return Ok(result); + } + + /// + /// a simple Hello User for Checking Authentication and Policy + /// + /// string message which contains authenticated user name + [RequireXPowered] + [HttpGet("Test/HiEnabledAdmin")] + [Authorize(LocalApi.PolicyName)] + [Authorize(Policy = XPolicies.EnabledAdmin)] + public ActionResult HiEnabledAdmin() + { + // + var result = $"Hi Admin: {User.Identity.Name} is Enabled ..."; + + // + return Ok(result); + } + #endregion + } +} \ No newline at end of file diff --git a/Controllers/AccountController.cs b/Controllers/AccountController.cs new file mode 100644 index 0000000..76b7c00 --- /dev/null +++ b/Controllers/AccountController.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using xCommons.Configurations; +using xCommons.Providers; +using xIds.Controllers.Base; +using xIds.Interfaces; + +namespace xIds.Controllers +{ + /// + /// Provide all available tools for manipulating users and accounts + /// + [ApiController] + public partial class AccountController : XIBaseController + { + + public AccountController( + IXIdentityManager identityManager, + ILogger logger, + XAppConfiguration appConfiguration, + XValidationProvider validationProvider + ) : base( + logger, + appConfiguration, + identityManager, + validationProvider + ) + { } + } +} \ No newline at end of file diff --git a/Controllers/Base/XIBaseController.cs b/Controllers/Base/XIBaseController.cs new file mode 100644 index 0000000..a594e0d --- /dev/null +++ b/Controllers/Base/XIBaseController.cs @@ -0,0 +1,269 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using xCommons.Configurations; +using xCommons.Constants; +using xCommons.Controllers; +using xCommons.Extensions; +using xCommons.Providers; +using xExceptions.Constants; +using xIdentityModels.Constants; +using xIdentityModels.Models; +using xIds.Interfaces; + +namespace xIds.Controllers.Base +{ + public abstract class XIBaseController : XBaseController + { + public IXIdentityManager IdentityManager { get; } + + protected XIBaseController( + ILogger logger, + XAppConfiguration appConfiguration, + IXIdentityManager identityManager, + XValidationProvider validationProvider + ) : base( + logger, + appConfiguration, + validationProvider + ) + { + IdentityManager = identityManager; + } + + // + #region User Handlers NonActions ... + /// + /// Retrieve User Identifier Base on XActionRequest + /// + /// + /// + /// + /// + [NonAction] + public string GetUserSelectByParam( + XActionRequest model, + bool forceNotNull = true, + ICollection excludes = null) + { + // + // Validate Args ... + ValidationProvider.NotNull(model); + + // + var result = IdentityManager.GetUserSelectByParam( + model, + forceNotNull, + excludes); + + // + return result; + } + + /// + /// Retrieve Access Token + /// + /// + [NonAction] + public async Task GetAccessToken() + { + // + var accessToken = Request.Headers[XAuthorization.Header].ToString(); + if (accessToken.IsNullOrEmpty()) + { + accessToken = await HttpContext.GetTokenAsync(XAuthorization.AccessToken); + } + + // + if (accessToken + .ToNormalString() + .Contains(XAuthorization.TokenIdentifier.ToNormalString())) + { + accessToken = accessToken.Remove(0, XAuthorization.TokenIdentifier.Length); + } + + // + Logger.LogInformation($"Token: {accessToken}"); + return accessToken; + } + + /// + /// Retrieve Refresh Token + /// + /// + [NonAction] + public async Task GetRefreshToken() + { + // + var refreshToken = Request.Headers[XAuthorization.RefreshToken].ToString(); + if (refreshToken.IsNullOrEmpty()) + { + refreshToken = await HttpContext.GetTokenAsync(XAuthorization.RefreshToken); + } + + // + Logger.LogInformation($"Refresh Token: {refreshToken}"); + return refreshToken; + } + + /// + /// Retrieve Access Token Expiration Date + /// + /// + [NonAction] + public async Task GetTokenExpiresAt() + { + // + var expiresAtStr = Request.Headers[XAuthorization.ExpiresAt].ToString(); + if (expiresAtStr.IsNullOrEmpty()) + { + expiresAtStr = await HttpContext.GetTokenAsync(XAuthorization.ExpiresAt); + } + + // + var expiresAt = expiresAtStr.ConvertTo(); + + // + Logger.LogInformation($"Token ExpiresAt: {expiresAtStr}"); + return expiresAt; + } + + /// + /// Retrieve All Required Tokens + /// + /// + [NonAction] + public async Task RetrieveTokensAsXLoginResponse() + { + // + var accessToken = await GetAccessToken(); + var refreshToken = await GetRefreshToken(); + var expiresAt = await GetTokenExpiresAt(); + + // + return new XLoginResponse + { + AccessToken = accessToken, + RefreshToken = refreshToken, + ExpiresAt = expiresAt + }; + } + + /// + /// Retrieve All Required Tokens + /// + /// + [NonAction] + public async Task RetrieveTokensAsXTokenResponse() + { + // + var accessToken = await GetAccessToken(); + var refreshToken = await GetRefreshToken(); + var expiresAt = await GetTokenExpiresAt(); + + // + return new XTokenResponse + { + AccessToken = accessToken, + RefreshToken = refreshToken, + ExpiresAt = expiresAt + }; + } + + /// + /// Retrive UserInfo + /// + /// + [NonAction] + public async Task GetUserInfo() + { + // + var claims = User.Claims ?? null; + if (!claims.HasChild()) + { + return null; + } + + var xTokens = await RetrieveTokensAsXTokenResponse(); + var result = new XUserClaimsInfoDto( + xTokens.AccessToken, + xTokens.RefreshToken, + xTokens.ExpiresAt, + User.Claims + ); + + // + return result; + } + + /// + /// Validate User Authenticated and Retrieve User Info + /// + /// + [NonAction] + public async Task ValidateAndGetUserInfo() + { + // + if (!User.Identity.IsAuthenticated) + { + XException.NotAuthorized.Throw(); + } + + // + var result = await GetUserInfo(); + if (result.IsNull()) + { + XException.NotAuthorized.Throw(); + } + + // + return result; + } + #endregion + + // + #region NonActions ... + /// + /// Convert an Exception to Propper Error Result + /// + /// + /// + [NonAction] + public new ActionResult GetExceptionActionResult(Exception ex) + { + // + var exception = GetExceptionResult(ex); + var error = ex.Message.ToXError(); + + // + Logger.LogError($"exception: {exception}, error: {error}"); + + // + try + { + var xError = exception.Message.ToXError(); + var xException = (XException)xError.Id; + + // + switch (xException) + { + // + case XException.NotFound: + return NotFound(error); + + // + case XException.NotAuthorized: + return Unauthorized(error); + } + } + catch { } + + // + return BadRequest(error); + } + #endregion + } +} \ No newline at end of file diff --git a/Controllers/StartupController.cs b/Controllers/StartupController.cs new file mode 100644 index 0000000..a9c021d --- /dev/null +++ b/Controllers/StartupController.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using xCommons.Configurations; +using xCommons.Controllers; +using xCommons.Providers; + +namespace xIds.Controllers +{ + /// + /// Startup and Running Controller + /// + [Route("")] + [AllowAnonymous] + public class StartupController : XBaseController + { + public StartupController( + ILogger logger, + XAppConfiguration appConfiguration, + XValidationProvider validationProvider + ) : base( + logger, + appConfiguration, + validationProvider + ) + { } + + /// + /// Show Configured Welcome Message + /// + /// an string message + [HttpGet("")] + [AllowAnonymous] + public virtual ActionResult Index() + { + // + var controllerName = GetControllerName(); + var message = $"{AppConfiguration.WelcomeMessage}"; + + // + return Ok(message); + } + } +} \ No newline at end of file diff --git a/DI/XDIHelperExtension.cs b/DI/XDIHelperExtension.cs new file mode 100644 index 0000000..b964d19 --- /dev/null +++ b/DI/XDIHelperExtension.cs @@ -0,0 +1,420 @@ +using System.Collections.Generic; +using IdentityServer4; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using xCommons.Authorization; +using xCommons.Constants; +using xCommons.Extensions; +using xExceptions.Constants; +using xIdentityHelper; +using xIdentityModels; +using xIdentityModels.Configurations; +using xIds.Configurations; +using xIds.Constants; +using xIds.DbContext; +using xIds.Extensions; +using xIds.Helpers; +using xIds.Interfaces; +using xIds.Providers; +using xIds.Validators; +using xMessageService.DI; +using xMessageService.Interfaces; +using xStorageService.DI; +using xStorageService.Interfaces; + +namespace xIds.DI +{ + public static class XDIHelperExtension + { + /// + /// Register Application DataProvider + /// + /// + /// + public static void AddXDataProvider( + this IServiceCollection services, + IConfiguration configuration + ) + { + // + // Retrieve DbInfo ... + var dbInfo = configuration.GetXDbInfo(); + var identityDbConnectionString = configuration.GetConnectionString(ConnectionStringNames.IDENTITY_CONNECTION_NAME); + + // + // Register Db Context for EF ... + services.AddDbContext(builder => + { + // + builder.PrepareXDbContextOptionsBuilder( + dbInfo, + identityDbConnectionString + ); + }); + } + + /// + /// Extract Connection String From IConfiguration + /// + /// + /// + /// + public static string GetXConnectionString( + this IConfiguration config, + string connectionName = null + ) + { + // + if (connectionName.IsNullOrEmpty()) + { + connectionName = XDbProviderConfigurations.DEFAULT_CONNECTION_NAME; + } + + // + return config.GetConnectionString(connectionName); + } + + /// + /// Retrive XDataService Configurations + /// + /// + /// + /// + public static XDataServiceConfiguration GetXDataServiceConfiguration( + this IConfiguration source, + string connectionName = null + ) + { + // + var xDataServiceConfigSection = source + .GetSection(Constants.ConfigurationNodeNames.DATA_SERVICE_NODE_NAME); + var result = xDataServiceConfigSection.Get(); + if (result.IsNull()) + { + result = new XDataServiceConfiguration(); + } + + // + result.Provider = source.GetXDbProviderType(); + result.ConnectionString = source.GetXConnectionString(connectionName); + + // + return result; + } + + /// + /// Register XDataService Configuration + /// + /// + /// + /// + public static void AddXDataServiceConfiguration( + this IServiceCollection services, + IConfiguration configuration, + string connectionName = null + ) + { + // + var dataServiceConfiguration = configuration + .GetXDataServiceConfiguration(connectionName); + + // + services.AddSingleton(dataServiceConfiguration); + } + + /// + /// Register All Requirements For XIdentityServer Usage + /// + /// + /// + public static void AddXIdentityServerRequirements( + this IServiceCollection services, + IConfiguration configuration + ) + { + // + services.AddXDataProvider(configuration); + + // + // Register XIdentityMessageProvider ... + services.AddXIdentityMessageProvider(); + + // + // Register IdentityManager Service ... + var xIdentityManager = services.GetRegisteredService(); + if (xIdentityManager.IsNull()) + { + services.AddXIdentityManager(); + } + + // + // Check XMessage Service Registered or not and Register it if not ... + var xMessageProvider = services.GetRegisteredService(); + if (xMessageProvider.IsNull()) + { + services.AddXMessageService(configuration); + } + + // + // Check Storage Service Registered or not and Register it if not ... + var xStorageProvider = services.GetRegisteredService(); + if (xStorageProvider.IsNull()) + { + services.AddXStorageService(configuration); + } + + // + // Register XIdentityConfiguration ... + var xIdentityConfiguration = configuration.GetXIdentityConfiguration(); + services.AddSingleton(xIdentityConfiguration); + + // + // Register XIdentityHelper ... + services.AddSingleton(); + var xIdentityHelper = services.GetRegisteredService(); + + // + // Register Asp.net Identity ... + services.AddXIdentity(); + } + + /// + /// Add IdentityServer with Support of Ef + /// + /// + /// + public static void AddEfSupportXIdentityServer( + this IServiceCollection services, + IConfiguration configuration + ) + { + // + // Register All Requirements ... + services.AddXIdentityServerRequirements(configuration); + + // + // add DbSeeder Config ... + services.AddXDebSeederDescriptor(configuration); + + // + // Retrieve Certificate ... + var identityServerSigningCertificate = configuration.GetCertificate(CertificatesConfigurations.CertificateNames.IdentityServerSigning); + + // + // Retrieve DbInfo ... + var dbInfo = configuration.GetXDbInfo(); + + // + // Retrieve Connection String Names ... + var configDbConnectionString = configuration.GetConnectionString(ConnectionStringNames.CONFIGURATION_CONNECTION_NAME); + var persistedGrantDbConnectionString = configuration.GetConnectionString(ConnectionStringNames.PRESISTED_GRANTS_CONNECTION_NAME); + + // + // Register IdentityServer ... + services.AddIdentityServer(options => + { + // + options.Discovery.CustomEntries.Add("account_api", "~/account"); + + // + // options.Events.RaiseErrorEvents = true; + // options.Events.RaiseFailureEvents = true; + // options.Events.RaiseSuccessEvents = true; + // options.Events.RaiseInformationEvents = true; + }) + .LoadSigningCredentialFrom(identityServerSigningCertificate) + .AddXDbProvider(configuration) + .AddAspNetIdentity() + .AddProfileService() + .AddResourceOwnerValidator(); + } + + /// + /// force app to use IdentityServer + /// + /// + /// + public static void UseXIdentityServer( + this IApplicationBuilder app, + ILogger logger + ) + { + // + // Update XIdentityServer Configurations before Use Identity Server ... + app.SeedData(logger) + .GetAwaiter() + .GetResult(); + + // + // Use Identity Server ... + app.UseIdentityServer(); + } + + /// + /// Register Asp.net Identity as User Store + /// + /// + public static void AddXIdentity(this IServiceCollection services) + { + // + var xIdentityConfiguration = services.GetRegisteredService(); + if (xIdentityConfiguration.IsNull()) + { + XException.InvalidConfiguration.Throw(); + } + + // + var xIdentityHelper = services.GetRegisteredService(); + if (xIdentityHelper.IsNull()) + { + XException.InvalidConfiguration.Throw(); + } + + // + // Add Identity for User Persists ... + services.AddIdentity(options => + { + // + var mLockoutSpan = xIdentityHelper.GetLockoutTimeSpan(); + + // + // Lockout Setting ... + options.Lockout.DefaultLockoutTimeSpan = mLockoutSpan; + options.Lockout.MaxFailedAccessAttempts = xIdentityConfiguration.Policy.Lockout.MaxFailedAccessAttempts; + options.Lockout.AllowedForNewUsers = xIdentityConfiguration.Policy.Lockout.AllowedForNewUsers; + + // + // Password settings ... + options.Password.RequireDigit = xIdentityConfiguration.Policy.Password.RequireDigit; + options.Password.RequireLowercase = xIdentityConfiguration.Policy.Password.RequireLowercase; + options.Password.RequireNonAlphanumeric = xIdentityConfiguration.Policy.Password.RequireNonAlphanumeric; + options.Password.RequireUppercase = xIdentityConfiguration.Policy.Password.RequireUppercase; + options.Password.RequiredLength = xIdentityConfiguration.Policy.Password.RequiredLength; + options.Password.RequiredUniqueChars = xIdentityConfiguration.Policy.Password.RequiredUniqueChars; + + // + // SignIn settings ... + options.SignIn.RequireConfirmedEmail = xIdentityConfiguration.Policy.SignIn.RequireConfirmedEmail; + options.SignIn.RequireConfirmedPhoneNumber = xIdentityConfiguration.Policy.SignIn.RequireConfirmedPhoneNumber; + + // + // User settings ... + options.User.AllowedUserNameCharacters = xIdentityConfiguration.Policy.User.AllowedUserNameCharacters; + options.User.RequireUniqueEmail = xIdentityConfiguration.Policy.User.RequireUniqueEmail; + }) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + } + + /// + /// Register IdentityServer Based Authentication + /// + /// + /// + public static void AddXIdentityServerAuthentication( + this IServiceCollection services, + IConfiguration configuration + ) + { + // + services.AddXIdentityResourceConfiguration(configuration); + services.AddAuthentication() + .AddJwtBearer(options => + { + // + options.SaveToken = true; + options.RequireHttpsMetadata = false; + + // + options.ForwardDefault = XAuthentication.IDENTITY_SERVER_LOCAL_API; + }) + .AddLocalApi(); + } + + /// + /// Register Authorization Policies + /// + /// + /// + public static void AddXAuthorization( + this IServiceCollection services, + IDictionary policies = null + ) + { + // + services.AddAuthorization(options => + { + // + if (!policies.IsNull()) + { + // + // Fill Policies ... + using (var policiesEnumerator = policies.GetEnumerator()) + { + while (policiesEnumerator.MoveNext()) + { + var policyDescriptor = policiesEnumerator.Current; + options.AddPolicy(policyDescriptor.Key, policyDescriptor.Value); + } + } + } + + // + var xPolicies = XAuthorizationHelper.GetXAuthorizationPolicies(); + if (!xPolicies.IsNull()) + { + // + // Fill Policies ... + using (var policiesEnumerator = xPolicies.GetEnumerator()) + { + while (policiesEnumerator.MoveNext()) + { + var policyDescriptor = policiesEnumerator.Current; + options.AddPolicy(policyDescriptor.Key, policyDescriptor.Value); + } + } + } + + // + // Add Local APi Policy ... + options.AddPolicy(IdentityServerConstants.LocalApi.PolicyName, policy => + { + // + policy.AddAuthenticationSchemes(IdentityServerConstants.LocalApi.AuthenticationScheme); + policy.RequireAuthenticatedUser(); + }); + }); + + // + services.AddSingleton(); + } + + /// + /// Use Forward Headers Options for resolving behind a proxy issues ... + /// + /// + /// + public static void UseXForwardOptions(this IApplicationBuilder source) + { + // + var forwardOptions = new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto, + RequireHeaderSymmetry = false + }; + + // + forwardOptions.KnownNetworks.Clear(); + forwardOptions.KnownProxies.Clear(); + + // + // ref: https://github.com/aspnet/Docs/issues/2384 + source.UseForwardedHeaders(forwardOptions); + } + } +} \ No newline at end of file diff --git a/Db/.gitkeep b/Db/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/DbContext/XIdentityDbContext.cs b/DbContext/XIdentityDbContext.cs new file mode 100644 index 0000000..2b9628f --- /dev/null +++ b/DbContext/XIdentityDbContext.cs @@ -0,0 +1,57 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using xIdentityModels; +using xIdentityModels.Models; +using xIdentityModels.Navigations; + +namespace xIds.DbContext +{ + public class XIdentityDbContext : IdentityDbContext + { + // + #region Props/DbSets ... + public DbSet Tokens { get; set; } + public DbSet Devices { get; set; } + public DbSet Avatars { get; set; } + public DbSet BannedDevices { get; set; } + public DbSet Followers { get; set; } + public DbSet Followings { get; set; } + public DbSet VerificationRequests { get; set; } + #endregion + + public XIdentityDbContext(DbContextOptions options) : base(options) + { + // + try + { + Database.EnsureCreated(); + Database.Migrate(); + } + catch { } + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // + base.OnModelCreating(modelBuilder); + + // + // Fix XUser GUID Generator on Add ... + modelBuilder + .Entity() + .Property(e => e.Id) + .ValueGeneratedOnAdd(); + + // + // Handle Role List ... + modelBuilder.Entity(u => + { + u.HasMany(x => x.Roles) + .WithOne() + .HasForeignKey(ur => ur.UserId) + .IsRequired(); + }); + } + } +} \ No newline at end of file diff --git a/Extensions/DbExtensions.cs b/Extensions/DbExtensions.cs new file mode 100644 index 0000000..f90638e --- /dev/null +++ b/Extensions/DbExtensions.cs @@ -0,0 +1,168 @@ +using System; +using System.Reflection; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using MySql.EntityFrameworkCore.Infrastructure; +using xCommons.Extensions; +using xIdentityModels.Configurations; +using xIds.Configurations; +using xIds.Constants; +using xIds.Models; + +namespace xIds.Extensions +{ + public static class DbExtensions + { + /// + /// Retrieve Data Provider Type from Configurations + /// + /// + /// + public static XDbProviders GetXDbProviderType(this IConfiguration source) + { + // + var dbProvider = (source[$"{ConfigurationNodeNames.PROVIDER_NODE_NAME}"]) + .ToNormalString(); + + // + return dbProvider == ProviderType.SQLServer.ToNormalString() ? + XDbProviders.SQLServer : + dbProvider == ProviderType.SQLite.ToNormalString() ? + XDbProviders.SQLite : + dbProvider == ProviderType.MySQL.ToNormalString() ? + XDbProviders.MySQL : + XDbProviders.None; + } + + /// + /// Retrieve AspNet Identity Configurations + /// + /// + /// + public static XIdentityConfiguration GetXIdentityConfiguration(this IConfiguration source) + { + // + var xIdentityConfigSection = source + .GetSection(ConfigurationNodeNames.IDENTITY_NODE_NAME); + return xIdentityConfigSection.Get(); + } + + /// + /// Retrieve XIdentityResource Configuration from AppSettings + /// + /// + /// + public static XIdentityResourceConfiguration GetXIdentityResourceConfiguration(this IConfiguration configuration) + { + // + var xIdentityResourceSection = configuration.GetSection(ConfigurationNodeNames.IDENTITY_RESOURCE_NODE_NAME); + return xIdentityResourceSection.Get(); + } + + /// + /// Register XIdentityResourceConfiguration + /// + /// + /// + public static void AddXIdentityResourceConfiguration( + this IServiceCollection services, + IConfiguration configuration + ) + { + // + var xIdentityResourceConfiguration = configuration.GetXIdentityResourceConfiguration(); + if (!xIdentityResourceConfiguration.IsNull()) + { + services.AddSingleton(xIdentityResourceConfiguration); + } + } + + /// + /// Converts DbInfo Options Builder for MySql Usage + /// + /// + /// + public static Action GetMySqlOptionsBuilder(this XDbInfo source) + { + return ((Action)source.OptionsBuilder); + } + + /// + /// Converts DbInfo Options Builder for SQLite Usage + /// + /// + /// + public static Action GetSQLiteOptionsBuilder(this XDbInfo source) + { + return ((Action)source.OptionsBuilder); + } + + /// + /// Converts DbInfo Options Builder for SQLServer Usage + /// + /// + /// + public static Action GetSQLServerOptionsBuilder(this XDbInfo source) + { + return ((Action)source.OptionsBuilder); + } + + /// + /// Retrieve Required Informations to Register DbContexts into Di as XDbInfo instance + /// + /// + /// + public static XDbInfo GetXDbInfo(this IConfiguration configuration) + { + return new XDbInfo + { + ProviderType = configuration.GetXDbProviderType(), + MigrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name + }; + } + + /// + /// Prepare DbContextOptionsBuilder with Propper data to Support Configured DbProvider + /// + /// + /// + /// + public static void PrepareXDbContextOptionsBuilder( + this DbContextOptionsBuilder source, + XDbInfo dbInfo, + string connectionString + ) + { + // + if (dbInfo.ProviderType == XDbProviders.MySQL) + { + // + // Add Support for MySql ... + source.UseMySQL( + connectionString, + dbInfo.GetMySqlOptionsBuilder() + ); + } + else if (dbInfo.ProviderType == XDbProviders.SQLite) + { + // + // Add Support for SQLite ... + source.UseSqlite( + connectionString, + dbInfo.GetSQLiteOptionsBuilder() + ); + } + else if (dbInfo.ProviderType == XDbProviders.SQLServer) + { + // + // Add Support for SQLServer ... + source.UseSqlServer( + connectionString, + dbInfo.GetSQLServerOptionsBuilder() + ); + } + } + } +} \ No newline at end of file diff --git a/Extensions/IIdentityServerBuilderExtensions.cs b/Extensions/IIdentityServerBuilderExtensions.cs new file mode 100644 index 0000000..7b2933d --- /dev/null +++ b/Extensions/IIdentityServerBuilderExtensions.cs @@ -0,0 +1,117 @@ +using System.Reflection; +using System.Security.Cryptography.X509Certificates; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using xCommons.Extensions; +using xCommons.Models; +using xIds.Constants; + +namespace xIds.Extensions +{ + public static class IIdentityServerBuilderExtensions + { + /// + /// Add Support for Configured XDbProvider to IdentityServer + /// + /// + /// + /// + public static IIdentityServerBuilder AddXDbProvider( + this IIdentityServerBuilder source, + IConfiguration configuration + ) + { + // + // Retrieve DB Info ... + var dbInfo = configuration.GetXDbInfo(); + dbInfo.OptionsBuilder = (optionsBuilder) => + { + optionsBuilder.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); + optionsBuilder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); + }; + + // + // Retrieve Connection String Names ... + var identityDbConnectionString = configuration.GetConnectionString(ConnectionStringNames.IDENTITY_CONNECTION_NAME); + var configDbConnectionString = configuration.GetConnectionString(ConnectionStringNames.CONFIGURATION_CONNECTION_NAME); + var persistedGrantDbConnectionString = configuration.GetConnectionString(ConnectionStringNames.PRESISTED_GRANTS_CONNECTION_NAME); + + // + // Add Operational Store ... + source.AddOperationalStore(opt => + { + opt.ConfigureDbContext = + builder => + { + // + builder.PrepareXDbContextOptionsBuilder( + dbInfo, + persistedGrantDbConnectionString + ); + + // + builder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); + }; + + // + // this enables automatic token cleanup. this is optional. + opt.EnableTokenCleanup = true; + opt.TokenCleanupInterval = 3600; + }); + + // + // Add Configuration Store ... + source.AddConfigurationStore(opt => + { + opt.ConfigureDbContext = + builder => + { + // + builder.PrepareXDbContextOptionsBuilder( + dbInfo, + configDbConnectionString + ); + }; + }); + + // + return source; + } + + /// + /// Set Configured Certificate File to IdentityServer + /// + /// + /// + /// + public static IIdentityServerBuilder LoadSigningCredentialFrom( + this IIdentityServerBuilder builder, + XCertificate certificate + ) + { + // + if (!certificate.IsNull() && + !certificate.Path.IsNullOrEmpty() && + !certificate.Secret.IsNullOrEmpty() + ) + { + try + { + builder.AddSigningCredential(new X509Certificate2(certificate.Path, certificate.Secret)); + } + catch + { + builder.AddDeveloperSigningCredential(); + } + } + else + { + builder.AddDeveloperSigningCredential(); + } + + // + return builder; + } + } +} \ No newline at end of file diff --git a/Extensions/IQueryableExtensions.cs b/Extensions/IQueryableExtensions.cs new file mode 100644 index 0000000..1ac9f29 --- /dev/null +++ b/Extensions/IQueryableExtensions.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using xCommons.Extensions; +using xExceptions.Constants; +using xModels.Base; +using xIds.Constants; + +namespace xIds.Extensions +{ + public static class IQueryableExtensions + { + } +} \ No newline at end of file diff --git a/Extensions/IdentityExtensions.cs b/Extensions/IdentityExtensions.cs new file mode 100644 index 0000000..ffc7c42 --- /dev/null +++ b/Extensions/IdentityExtensions.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.DependencyInjection; +using xIds.Interfaces; +using xIds.Providers; + +namespace xIds.Extensions +{ + public static class IdentityExtensions + { + /// + /// Register IdentityManager + /// + /// + public static void AddXIdentityManager(this IServiceCollection source) + { + source.AddScoped(); + } + + /// + /// Register IdentityMessage Provider + /// + /// + public static void AddXIdentityMessageProvider( + this IServiceCollection source + ) + { + source.AddScoped(); + } + } +} \ No newline at end of file diff --git a/Extensions/XDbSeederExtensions.cs b/Extensions/XDbSeederExtensions.cs new file mode 100644 index 0000000..1d1d14d --- /dev/null +++ b/Extensions/XDbSeederExtensions.cs @@ -0,0 +1,428 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using IdentityServer4.EntityFramework.DbContexts; +using IdentityServer4.EntityFramework.Mappers; +using Microsoft.AspNetCore.Builder; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using xCommons.Extensions; +using xIdentityModels.Descriptors; +using xIdentityModels.Extensions; +using xIds.Constants; +using xIds.Interfaces; +using xIds.Models; + +namespace xIds.Extensions +{ + public static class XDbSeeder + { + /// + /// Retrieve DbSeedDescriptor from appSetting Configuration + /// + /// + /// + public static XDbSeedDescriptor GetXDbSeedDescriptor(this IConfiguration configuration) + { + // + var dbSeedSection = configuration.GetSection(ConfigurationNodeNames.DB_SEED_NODE_NAME); + return dbSeedSection.Get(); + } + + /// + /// Register XDbSeedDescriptor as Singleton Service + /// + /// + /// + public static void AddXDebSeederDescriptor( + this IServiceCollection services, + IConfiguration configuration + ) + { + // + var dbSeedDescriptor = configuration.GetXDbSeedDescriptor(); + if (!dbSeedDescriptor.IsNull()) + { + services.AddSingleton(dbSeedDescriptor); + } + else + { + Console.WriteLine("Error: DbSeeder Configuration not found in AppSetting ..."); + } + } + + /// + /// Add Base Required Data to XIdentitySer DbContexts + /// + /// + /// + /// + public static async Task SeedData( + this IApplicationBuilder app, + ILogger logger + ) + { + // + logger.LogInformation($"Start Add/Update XIdentityServer Descriptors ..."); + + // + // Create Scope ... + using (var scope = app.ApplicationServices.CreateScope()) + { + // + // Retrieve Required Configurations ... + var dbSeedDescriptor = scope.ServiceProvider.GetService(); + if (dbSeedDescriptor.IsNull()) + { + // + Console.WriteLine("DbSeeder is Null ..."); + return; + } + logger.LogInformation($"Update Exists Descriptors: {dbSeedDescriptor.UpdateExists.ToString()}"); + + // + // Retrieve Required Services ... + var identityProvider = scope.ServiceProvider.GetService(); + + // + // Retrieve DbContexts ... + var grantDbContext = scope.ServiceProvider.GetService(); + var configDbContext = scope.ServiceProvider.GetService(); + + // + #region Clients ... + // + logger.LogInformation($"Start Seeding Clients ..."); + + // + var dClients = dbSeedDescriptor.Clients; + var hasClient = configDbContext.Clients.Any(); + if ((!hasClient || + (hasClient && dbSeedDescriptor.UpdateExists)) && + dClients.HasChild()) + { + // + var clientEntities = dClients.Select(dc => dc.ToEntity()); + + // + foreach (var client in clientEntities) + { + // + logger.LogInformation($"Add/Update: {client.ClientId}"); + + // + // Retrieve Exists Client ... + var existsClient = configDbContext.Clients + .Include(c => c.AllowedCorsOrigins) + .Include(c => c.AllowedGrantTypes) + .Include(c => c.AllowedScopes) + .Include(c => c.Claims) + .Include(c => c.ClientSecrets) + .FirstOrDefault(cl => cl.ClientId == client.ClientId); + + // + var isExistsClient = !existsClient.IsNull(); + var isSameContent = isExistsClient && + existsClient.IsSameContent(client, propertyBlackList: new[] { nameof(existsClient.Id) }); + + // + if (isExistsClient) + { + // + if (!isSameContent) + { + // + logger.LogInformation($"Update Client: {existsClient.ClientId}"); + + // + existsClient = existsClient.UpdateData(client, propertyBlackList: new[] { nameof(existsClient.Id) }); + configDbContext.Clients.Update(existsClient); + } + } + else + { + // + logger.LogInformation($"Add Client: {client.ClientId}"); + + // + await configDbContext.Clients.AddAsync(client); + } + } + + // + // Save Changes on Config Db Context ... + await configDbContext.SaveChangesAsync(); + } + + // + logger.LogInformation($"Finish Seeding Clients ..."); + #endregion + + // + #region IdentityResources ... + // + logger.LogInformation($"Start Seeding Identity Resources ..."); + + // + var dIdentityResources = dbSeedDescriptor.IdentityResources; + var hasIdentityResources = configDbContext.IdentityResources.Any(); + if ((!hasIdentityResources || + (hasIdentityResources && dbSeedDescriptor.UpdateExists)) && + dIdentityResources.HasChild()) + { + // + var identityResourceEntities = dIdentityResources.Select(di => di.ToEntity()); + + // + foreach (var identityResource in identityResourceEntities) + { + // + logger.LogInformation($"Add/Update: {identityResource.DisplayName}"); + + // + // Retrieve Exists IdentityResource ... + var existsIdentityResource = configDbContext.IdentityResources + .AsNoTracking() + .FirstOrDefault(ir => ir.Name == identityResource.Name); + + // + var isExistsResource = !existsIdentityResource.IsNull(); + var isSameContent = isExistsResource && + existsIdentityResource.IsSameContent(identityResource, propertyBlackList: new[] { nameof(existsIdentityResource.Id) }); + + // + if (isExistsResource && + dbSeedDescriptor.UpdateExists) + { + // + if (!isSameContent) + { + // + logger.LogInformation($"Update IdentityResource: {identityResource.DisplayName}"); + + // + existsIdentityResource = existsIdentityResource.UpdateData(identityResource, propertyBlackList: new[] { nameof(existsIdentityResource.Id) }); + configDbContext.IdentityResources.Update(existsIdentityResource); + } + } + else + { + // + logger.LogInformation($"Add IdentityResource: {identityResource.DisplayName}"); + + // + await configDbContext.IdentityResources.AddAsync(identityResource); + } + } + + // + // Save Changes on Config Db Context ... + // await configDbContext.SaveChangesAsync(); + } + + // + logger.LogInformation($"Finish Seeding Identity Resources ..."); + #endregion + + // + #region Api Resources ... + // + logger.LogInformation($"Start Seeding Api Resources ..."); + + // + var dApiResources = dbSeedDescriptor.ApiResources; + var hasApiResources = configDbContext.ApiResources.Any(); + if ((!hasApiResources || + (hasApiResources && dbSeedDescriptor.UpdateExists)) && + dApiResources.HasChild()) + { + // + var apiResourceEntities = dApiResources.Select(da => da.ToEntity()); + + // + foreach (var apiResource in apiResourceEntities) + { + // + logger.LogInformation($"Add/Update: {apiResource.DisplayName}"); + + // + // Retrieve Exists ApiResource ... + var existsApiResource = configDbContext.ApiResources + .Include(r => r.Secrets) + .Include(r => r.Scopes) + .FirstOrDefault(ar => ar.Name == apiResource.Name); + + // + var isExistsResource = !existsApiResource.IsNull(); + var isSameContent = isExistsResource && + existsApiResource.IsSameContent(apiResource, propertyBlackList: new[] { nameof(existsApiResource.Id) }); + + // + if (isExistsResource && + dbSeedDescriptor.UpdateExists) + { + // + if (!isSameContent) + { + // + logger.LogInformation($"Update ApiResource: {existsApiResource.DisplayName}"); + + // + existsApiResource = existsApiResource.UpdateData(apiResource, propertyBlackList: new[] { nameof(existsApiResource.Id) }); + configDbContext.ApiResources.Update(existsApiResource); + } + } + else + { + // + logger.LogInformation($"Add ApiResource: {apiResource.DisplayName}"); + + // + await configDbContext.ApiResources.AddAsync(apiResource); + } + } + + // + // Save Changes on Config Db Context ... + // await configDbContext.SaveChangesAsync(); + } + + // + logger.LogInformation($"Finish Seeding Api Resources ..."); + #endregion + + // + #region Api Scopes ... + // + logger.LogInformation($"Start Seeding Scopes ..."); + + // + var dScopes = dbSeedDescriptor.ApiScopes; + var hasScope = configDbContext.ApiScopes.Any(); + if ((!hasScope || + (hasScope && dbSeedDescriptor.UpdateExists)) && + dScopes.HasChild()) + { + // + var apiScopeEntities = dScopes.Select(dc => dc.ToEntity()); + + // + foreach (var apiScope in apiScopeEntities) + { + // + logger.LogInformation($"Add/Update: {apiScope.DisplayName}"); + + // + // Retrieve Exists Client ... + var existsScope = configDbContext.ApiScopes + .FirstOrDefault(sc => sc.Name == apiScope.Name); + + // + var isExistsScope = !existsScope.IsNull(); + var isSameContent = isExistsScope && + existsScope.IsSameContent(apiScope, propertyBlackList: new[] { nameof(existsScope.Id) }); + + // + if (isExistsScope && + dbSeedDescriptor.UpdateExists) + { + // + if (!isSameContent) + { + // + logger.LogInformation($"Update ApiScope: {existsScope.Name}"); + + // + existsScope = existsScope.UpdateData(apiScope, propertyBlackList: new[] { nameof(existsScope.Id) }); + configDbContext.ApiScopes.Update(existsScope); + } + } + else + { + // + logger.LogInformation($"Add ApiScope: {apiScope.Name}"); + + // + await configDbContext.ApiScopes.AddAsync(apiScope); + } + } + + // + // Save Changes on Config Db Context ... + await configDbContext.SaveChangesAsync(); + } + + // + logger.LogInformation($"Finish Seeding Scopes ..."); + #endregion + + // + #region Users ... + // + logger.LogInformation($"Start Seeding Users ..."); + + // + var configUsers = dbSeedDescriptor.Users; + var hasUser = identityProvider.GetUsersDbSet().Any(); + if ((!hasUser || + (hasUser && dbSeedDescriptor.UpdateExists)) && + configUsers.HasChild()) + { + foreach (var user in configUsers) + { + // + logger.LogInformation($"Add/Update User: {user.UserName}"); + + // + var isUserExists = await identityProvider.IsUserExistsAsync(user.UserName); + if (isUserExists && + dbSeedDescriptor.UpdateExists) + { + // + var existsUser = await identityProvider.GetUserAsync(user.UserName); + var isSame = existsUser.IsSameAs(user); + if (!isSame) + { + // + // Update Entity ... + existsUser = existsUser.UpdateData(user, propertyWhiteList: new[] { + nameof (XIdentityUserDescriptor.FirstName), + nameof (XIdentityUserDescriptor.LastName), + nameof (XIdentityUserDescriptor.UserName), + nameof (XIdentityUserDescriptor.Email), + nameof (XIdentityUserDescriptor.EmailConfirmed), + nameof (XIdentityUserDescriptor.PhoneNumber), + nameof (XIdentityUserDescriptor.PhoneNumberConfirmed), + nameof (XIdentityUserDescriptor.IsEnable), + nameof (XIdentityUserDescriptor.IsBanned), + nameof (XIdentityUserDescriptor.DateOfBirth) + }, propertyValueProviders: new[] { + new KeyValuePair> ( + nameof (XIdentityUserDescriptor.DateOfBirth), + (user) => DateTime.Parse (user.DateOfBirth)) + }); + + // + await identityProvider.UpdateUserAsync(existsUser, false, false); + } + } + else + { + await identityProvider.CreateUserAsync(user); + } + } + } + + // + logger.LogInformation($"Finish Seeding Users ..."); + #endregion + } + + // + logger.LogInformation($"Finish Add/Update XIdentityServer Descriptors ..."); + } + } +} \ No newline at end of file diff --git a/Extensions/XModelExtensions.cs b/Extensions/XModelExtensions.cs new file mode 100644 index 0000000..789c960 --- /dev/null +++ b/Extensions/XModelExtensions.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using xCommons.Extensions; +using xDataService.Configuration; +using xIdentityModels.Models; + +namespace xIds.Extensions +{ + public static class XModelExtensions + { + /// + /// Determine a banned time is passed or not + /// + /// + /// + /// + public static bool IsDelayTimePassed( + this XBannedDevice source, + int delaySeconds + ) + { + // + var passedTime = source.BannedOn.AddSeconds(delaySeconds); + + // + var result = DateTime.UtcNow >= passedTime; + + // + return result; + } + + /// + /// Applying Filter to IQueryable + /// /// + /// + public static async Task> ApplyFilterAsync( + this IQueryable source, + string filter + ) where T : class + { + // + // Apply Filter ... + if (!filter.IsNullOrEmpty()) + { + // + var items = source.AsAsyncEnumerable(); + var filteredItems = new List(); + await + foreach (var item in items) + { + // + if (item.GetPropValues() + .ToNormalString() + .Contains(filter + .ToNormalString())) + { + // + filteredItems.Add(item); + } + } + + // + return filteredItems.AsQueryable(); + } + + // + return source; + } + + public static XDataServiceConfiguration ToXDataServiceConfig(this Configurations.XDataServiceConfiguration config) + { + // + var result = new XDataServiceConfiguration(); + + // + if (!config.IsNullOrDefault()) + { + // + result.PagingConfiguration.DefaultPageSize = config.PagingConfiguration.DefaultPageSize; + result.PagingConfiguration.MaxAvailablePageSize = config.PagingConfiguration.MaxAvailablePageSize; + result.PagingConfiguration.MinAvailablePageSize = config.PagingConfiguration.MinAvailablePageSize; + } + + // + return result; + } + } +} \ No newline at end of file diff --git a/Helpers/XIdentityHelper.cs b/Helpers/XIdentityHelper.cs new file mode 100644 index 0000000..a8b8fd1 --- /dev/null +++ b/Helpers/XIdentityHelper.cs @@ -0,0 +1,426 @@ +using System; +using System.IdentityModel.Tokens.Jwt; +using Microsoft.IdentityModel.Tokens; +using xCommons.Extensions; +using xCommons.Providers; +using xExceptions.Constants; +using xIdentityModels.Configurations; +using xIdentityModels.Extensions; +using xIdentityModels.Models; +using xIds.Interfaces; + +namespace xIds.Helpers +{ + public class XIdentityHelper : IXIdentityHelper + { + private readonly XIdentityConfiguration configurations; + private readonly JwtSecurityTokenHandler tokenHandler; + private readonly XValidationProvider dataValidationHelper; + + public XIdentityHelper( + XIdentityConfiguration configurations, + XValidationProvider dataValidationHelper + ) + { + // + this.configurations = configurations; + this.tokenHandler = new JwtSecurityTokenHandler(); + this.dataValidationHelper = dataValidationHelper; + } + + // + #region Configuration Parser ... + /// + /// Generate a Key for Signing JWT Tokens + /// + /// + public SymmetricSecurityKey GetTokenSecretKey() + { + // + dataValidationHelper + .NotEmpty(configurations.IdentitySecretKey); + + // + var result = new SymmetricSecurityKey(configurations + .IdentitySecretKey.ToBytes()); + + // + return result; + } + + /// + /// Generate Security Token Signing Key + /// + /// + public SigningCredentials GetTokenSigningCredentials() + { + // + var key = GetTokenSecretKey(); + dataValidationHelper + .NotNull(key); + + // + var result = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + // + return result; + } + + /// + /// Get Token Validation Parameters + /// + /// + public TokenValidationParameters GetTokenValidationParams() + { + // + var key = GetTokenSecretKey(); + var issuer = configurations.IdentityIssuer; + var audience = configurations.IdentityAudience; + + // + dataValidationHelper + .NotNull(key); + dataValidationHelper + .NotEmpty(issuer, audience); + + // + var result = new TokenValidationParameters + { + ValidIssuer = issuer, + ValidAudience = audience, + ValidateLifetime = true, + IssuerSigningKey = key + }; + + // + return result; + } + + /// + /// Get Account Lockout Time Span after Max Fail Reached + /// + /// + public TimeSpan GetLockoutTimeSpan() + { + // + dataValidationHelper + .NotNull(configurations.Policy, configurations.Policy.Lockout); + + // + var timeSpanString = configurations.Policy.Lockout.LockoutTimeSpanProvider; + dataValidationHelper + .NotEmpty(timeSpanString); + + // + var mExpireValue = 0; + var mExpireValueStr = timeSpanString.GetDigits(); + var mExpireUnit = timeSpanString.Replace(mExpireValueStr, ""); + dataValidationHelper + .NotEmpty(mExpireUnit); + dataValidationHelper + .NotEmpty(mExpireValueStr); + + // + var isParsed = int.TryParse(mExpireValueStr, out mExpireValue); + + // + switch (mExpireUnit) + { + case "d": + return TimeSpan.FromDays(mExpireValue); + + case "h": + return TimeSpan.FromHours(mExpireValue); + + case "m": + return TimeSpan.FromMinutes(mExpireValue); + + case "s": + return TimeSpan.FromSeconds(mExpireValue); + + case "ms": + default: + return TimeSpan.FromMilliseconds(mExpireValue); + } + } + + /// + /// Generate Security Token Descriptor for Tokenize another + /// + /// + public SecurityTokenDescriptor GetActionTokenDescriptor() + { + // + dataValidationHelper + .NotNull(configurations); + + // + var issuer = configurations.IdentityIssuer; + var audience = configurations.IdentityAudience; + dataValidationHelper + .NotEmpty(issuer, audience); + + // + var expDate = GetActionTokenExpirationDate(); + var signingCreds = GetTokenSigningCredentials(); + dataValidationHelper + .NotNull(expDate, signingCreds); + + // + var descriptor = new SecurityTokenDescriptor + { + Issuer = issuer, + Audience = audience, + Expires = expDate, + SigningCredentials = signingCreds + }; + + // + return descriptor; + } + + /// + /// Get Action Token Expiration Date + /// + /// + public DateTime GetActionTokenExpirationDate() + { + // + dataValidationHelper + .NotNull(configurations); + + // + var expDateProvider = configurations + .ActionTokenExpirationDateProvider; + dataValidationHelper + .NotEmpty(expDateProvider); + + // + var result = GetExpirationTime(expDateProvider); + dataValidationHelper + .NotNull(result); + + // + return result; + } + #endregion + + // + #region Action Token Handlers ... + public bool ValidateToken(string token) + { + // + // Validate Args ... + dataValidationHelper + .NotEmpty(token); + dataValidationHelper + .NotNull(tokenHandler); + + // + // Prepare Requirements ... + var tokenValidationParams = GetTokenValidationParams(); + dataValidationHelper + .NotNull(tokenValidationParams); + + // + var result = token + .ValidateToken(tokenHandler, tokenValidationParams); + + // + return result; + } + + /// + /// Converts a Token string to SecurityToken instance + /// + /// + /// + public SecurityToken ToSecurityToken(string token) + { + // + dataValidationHelper + .NotEmpty(token); + + // + // Validate token ... + var isTokenValid = ValidateToken(token); + if (!isTokenValid) + { + XException.InvalidToken.Throw(); + } + + // + // Prepare and Validate Requirements ... + var tokenValidationParams = GetTokenValidationParams(); + dataValidationHelper + .NotNull(tokenHandler, tokenValidationParams); + + // + var result = token + .ToSecurityToken(tokenHandler, tokenValidationParams); + dataValidationHelper + .NotNull(result); + + // + return result; + } + + /// + /// Convert an ActionRequest to Token Object + /// + /// + /// + public SecurityToken ToSecurityToken(XActionRequestToken request) + { + // + var tokenDescriptor = GetActionTokenDescriptor(); + + // + dataValidationHelper.NotNull(request, tokenHandler, tokenDescriptor); + + // + var result = request.ToJwtToken(tokenHandler, tokenDescriptor); + + // + return result; + } + + /// + /// Retrieve a Token Exiration Date + /// + /// + /// + public DateTime GetTokenExpirationDate(string token) + { + // + var tokenObj = ToSecurityToken(token); + dataValidationHelper.NotNull(tokenObj); + + // + var result = tokenObj.ValidTo; + + // + return result; + } + + public XActionRequestToken ParseActionRequestToken(string token) + { + // + dataValidationHelper + .NotEmpty(token); + + // + // Validate token ... + var isTokenValid = ValidateToken(token); + if (!isTokenValid) + { + XException.InvalidToken.Throw(); + } + + // + // Prepare and Validate Requirements ... + var tokenValidationParams = GetTokenValidationParams(); + dataValidationHelper + .NotNull(tokenHandler, tokenValidationParams); + + // + var result = token + .ParseActionRequestToken(tokenHandler, tokenValidationParams); + dataValidationHelper + .NotNull(result); + + // + return result; + } + + /// + /// Convert a JWT Security Token Object to + /// it's String Representation + /// + /// + /// + public string ToTokenString(JwtSecurityToken token) + { + // + dataValidationHelper.NotNull(token); + + // + var result = tokenHandler + .WriteToken(token); + + // + return result; + } + + /// + /// Convert an Action Token Object to it's String Representation + /// + /// + /// + public string ToTokenString(SecurityToken token) + { + // + // Validate Args ... + dataValidationHelper + .NotNull(token, tokenHandler); + + // + // Generate and return Result ... + var result = token.ToTokenString(tokenHandler); + dataValidationHelper + .NotEmpty(result); + + // + return result; + } + #endregion + + // + #region Private Actions ... + /// + /// Calculating Date Difference Provider String + /// and Generate Date based on UTC Time + /// + /// + /// + private DateTime GetExpirationTime(string expirationProvider) + { + // + dataValidationHelper + .NotEmpty(expirationProvider); + + // + var mExpireValue = 0; + var mExpireValueStr = expirationProvider.GetDigits(); + var mExpireUnit = expirationProvider.Replace(mExpireValueStr, ""); + dataValidationHelper + .NotEmpty(mExpireUnit); + dataValidationHelper + .NotEmpty(mExpireValueStr); + + // + var isParsed = int.TryParse(mExpireValueStr, out mExpireValue); + + // + switch (mExpireUnit) + { + case "d": + return DateTime.UtcNow.AddDays(mExpireValue); + + case "h": + return DateTime.UtcNow.AddHours(mExpireValue); + + case "m": + return DateTime.UtcNow.AddMinutes(mExpireValue); + + case "s": + return DateTime.UtcNow.AddSeconds(mExpireValue); + + case "ms": + default: + return DateTime.UtcNow.AddMilliseconds(mExpireValue); + } + } + #endregion + } +} \ No newline at end of file diff --git a/Interfaces/IXIdentityHelper.cs b/Interfaces/IXIdentityHelper.cs new file mode 100644 index 0000000..af48add --- /dev/null +++ b/Interfaces/IXIdentityHelper.cs @@ -0,0 +1,29 @@ +using System; +using System.IdentityModel.Tokens.Jwt; +using Microsoft.IdentityModel.Tokens; +using xIdentityModels.Models; + +namespace xIds.Interfaces +{ + public interface IXIdentityHelper + { + // + #region Configuration Parser ... + SymmetricSecurityKey GetTokenSecretKey(); + SigningCredentials GetTokenSigningCredentials(); + TokenValidationParameters GetTokenValidationParams(); + TimeSpan GetLockoutTimeSpan(); + #endregion + + // + #region Action Token Handlers ... + bool ValidateToken(string token); + SecurityToken ToSecurityToken(string token); + SecurityToken ToSecurityToken(XActionRequestToken request); + string ToTokenString(SecurityToken token); + string ToTokenString(JwtSecurityToken token); + DateTime GetTokenExpirationDate(string token); + XActionRequestToken ParseActionRequestToken(string token); + #endregion + } +} \ No newline at end of file diff --git a/Interfaces/IXIdentityManager.cs b/Interfaces/IXIdentityManager.cs new file mode 100644 index 0000000..e809adb --- /dev/null +++ b/Interfaces/IXIdentityManager.cs @@ -0,0 +1,506 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Security.Claims; +using System.Threading.Tasks; +using IdentityModel.Client; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using xCommons.Providers; +using xIdentityModels; +using xIdentityModels.Constants; +using xIdentityModels.Descriptors; +using xIdentityModels.Dtos; +using xIdentityModels.Models; +using xIdentityModels.Navigations; +using xIds.DbContext; +using xModels.Dtos; + +namespace xIds.Interfaces +{ + public interface IXIdentityManager + { + XIdentityDbContext DbContext { get; } + UserManager UserManager { get; } + ILogger Logger { get; } + SignInManager SignInManager { get; } + IXSecurityProvider SecurityProvider { get; } + RoleManager RoleManager { get; } + XValidationProvider ValidationProvider { get; } + + // + #region Class Getter (s) Methods ... + IQueryable GetUsersDbSet( + bool containsDetail = false + ); + IQueryable GetUsersFullDbSet(bool enableTracking = false); + #endregion + + // + #region Tools ... + string GetUserSelectByParam( + XActionRequest model, + bool forceNotNull = true, + ICollection excludes = null + ); + + Task ValidateUserExistsAndRetrieve( + string userSelectByParam, + bool containsDetail = false, + bool checkCanLoginPolicies = false, + bool checkIsBanned = true, + bool ignoreDisabledUser = false, + bool forceAdmin = false, + Exception exception = null + ); + #endregion + + // + #region Default Identity Preparation Actions ... + Task CreateIdentityRoles(); + Task CreateUserAsync(XIdentityUserDescriptor user); + #endregion + + // + #region Identity Actions ... + HttpClient GetHttpClient(); + Task RequestScopeAccessToken(string scope); + Task RequestDiscoveryDocument(); + Task Authenticate(XLoginRequest model); + Task Login(XLoginRequest model); + Task RefreshTokens(XTokenResponse model); + #endregion + + // + #region Role Handlers ... + Task IsRoleExistsAsync(string roleName); + Task CreateRoleAsync(string roleName); + Task GetRoleAsync(string roleName); + Task AddUserToRoleAsync(XUser user, string roleName); + Task GetRoleNameAsync(string roleId); + Task> GetRoleNamesAsync( + string userSelectByParam, + bool checkCanLoginPolicies = true, + bool checkIsBanned = true + ); + Task RemoveFromRoleAsync(XUser user, string roleName); + Task RemoveFromRolesAsync(XUser user, IEnumerable roleNames); + #endregion + + // + #region User Handlers ... + Task CanRegister(string userSelectByParam); + Task IsUserExistsAsync(string userSelectByParam); + Task GetUserAsync( + string userSelectByParam, + bool containsDetail = false); + + Task CreateUserAsync( + XUser user, + string password, + bool checkCanLoginPolicies = true, + bool checkIsBanned = true + ); + + Task UpdateUserAsync( + XUser user, + bool checkCanLoginPolicies = true, + bool checkIsBanned = true + ); + + Task> GetUserNamesAsync( + IEnumerable userIds + ); + + Task> GetUserNameIdsAsync( + XUserNameIdRequest model + ); + + Task ToJwtClaims( + XUser user, + bool checkCanLoginPolicies = true, + bool checkIsBanned = true + ); + + Task CheckPasswordSignInAsync( + XUser user, + string password, + XDevice device, + string language, + bool lockoutOnFailure, + bool isForced = true + ); + #endregion + + // + #region User Profile Handlers ... + Task GetUserProfileAsync( + string userSelectByParam, + string requestedUserSelectByParam, + bool forceCheckRequestedUser = true, + bool checkCanLoginPolicies = true, + bool checkIsBanned = true + ); + + Task> QueryUsers( + string requestedUserSelectByParam, + XQuery query + ); + + Task> QueryInRoleUsers( + string requestedUserSelectByParam, + string role, + XQuery query, + bool forceRole = false + ); + + Task> GetUserProfilesAsync( + ICollection ids, + string requestedUserSelectByParam, + bool forceCheckRequestedUser = true, + bool checkCanLoginPolicies = true, + bool checkIsBanned = true + ); + + Task> QueryAvatars( + string userSelectByParam, + XQuery query + ); + + Task ProfileUpdateAsync( + string userSelectByParam, + string requestedUserSelectByParam, + XProfileUpdateRequest request + ); + + Task FullProfileUpdateAsync( + string userSelectByParam, + string requestedUserSelectByParam, + XProfileUpdateRequest request + ); + + Task RequestConfirmMobile( + string lang, + XDevice device, + string userSelectByParam, + string password, + string mobileNumber = null + ); + + Task RequestConfirmEmail( + string lang, + XDevice device, + string userSelectByParam, + string password, + string emailAddress = null + ); + + Task RequestResetPassword( + string lang, + XDevice device, + string userSelectByParam, + string returnUrl + ); + + Task RequestConfirmRegistration( + string lang, + XDevice device, + string userSelectByParam, + string password, + string returnUrl + ); + + Task ChangePassword( + string lang, + XDevice device, + string userSelectByParam, + string password, + string newPassword, + string returnUrl + ); + + Task ResetPassword( + string lang, + XDevice device, + string actionHash, + string newPassword, + string returnUrl + ); + + Task AddAvatar( + string userSelectByParam, + IFormFile file + ); + + Task AddAvatars( + string userSelectByParam, + IFormFileCollection files + ); + + Task SetAvatar( + string userSelectByParam, + int profileImageId + ); + + Task RemoveAvatar( + string userSelectByParam, + ICollection profileImageIds + ); + + Task ConfirmRegistration( + string lang, + XDevice device, + string actionHash, + string returnUrl + ); + + Task ConfirmMobileNumber( + string lang, + XDevice device, + string registrationHash, + string verificationCode + ); + + Task ConfirmEmailAddress( + string lang, + XDevice device, + string registrationHash, + string verificationCode + ); + + Task IsConfirmedEmail(string userSelectByParam); + + Task IsConfirmedMobile(string userSelectByParam); + #endregion + + // + #region Registration Handlers ... + Task InviteUser( + string lang, + XDevice device, + string email, + string handlerUrl + ); + + Task RequestRegistration( + string lang, + XDevice device, + string invitationHash, + string firstName, + string lastName, + DateTime dateOfBirth, + string mobileNumber = null, + string emailAddress = null + ); + + Task AddAcountInfo( + string lang, + XDevice device, + string registrationHash, + string userName, + string password + ); + + Task AttachProfileImage( + string tokenHash, + IFormFile file + ); + + Task FinishRegistration( + string lang, + XDevice device, + string userSelectByParam, + string password, + string returnUrl + ); + #endregion + + // + #region Friendship Handlers ... + // + #region Actions ... + Task Follow( + string userSelectByParam, + string destUserSelectByParam + ); + + Task Cancel( + string userSelectByParam, + string destUserSelectByParam + ); + + Task UnFollowFollower( + string userSelectByParam, + string destUserSelectByParam + ); + + Task UnFollowFollowing( + string userSelectByParam, + string destUserSelectByParam + ); + + Task Block( + string userSelectByParam, + string destUserSelectByParam + ); + + Task UnBlock( + string userSelectByParam, + string destUserSelectByParam + ); + #endregion + + // + #region Request Handlers ... + Task AcceptRequest( + string userSelectByParam, + string destUserSelectByParam + ); + + Task RejectRequest( + string userSelectByParam, + string destUserSelectByParam + ); + #endregion + + // + #region Getters ... + Task IsFollower( + string userSelectByParam, + string destUserSelectByParam, + bool checkIsBanned = true, + bool checkCanLoginPolicies = true + ); + + Task GetFollowerState( + string userSelectByParam, + string destUserSelectByParam, + bool checkIsBanned = true, + bool checkCanLoginPolicies = true + ); + + Task GetFollower( + string userSelectByParam, + string destUserSelectByParam + ); + + Task> GetFollowers(string userSelectByParam); + Task> GetAllFollowers(string userSelectByParam); + Task> GetFollowersList(string userSelectByParam); + Task> QueryFollowers( + XQuery query, + string userSelectByParam + ); + + Task IsFollowing( + string userSelectByParam, + string destUserSelectByParam, + bool checkIsBanned = true, + bool checkCanLoginPolicies = true + ); + + Task GetFollowingState( + string userSelectByParam, + string destUserSelectByParam, + bool checkIsBanned = true, + bool checkCanLoginPolicies = true + ); + + Task GetFollowing( + string userSelectByParam, + string destUserSelectByParam + ); + + Task> GetFollowings(string userSelectByParam); + Task> GetAllFollowings(string userSelectByParam); + Task> GetFollowingList(string userSelectByParam); + Task> QueryFollowings( + XQuery query, + string userSelectByParam + ); + #endregion + + // + Task GetFriendshipInfo( + string userSelectByParam, + string destUserSelectByParam, + bool checkCanLoginPolicies = true, + bool checkIsBanned = true + ); + #endregion + + // + #region Admin Actions ... + Task> Ban( + string userSelectByParam, + XUserNameIdRequest model + ); + + Task> UnBan( + string userSelectByParam, + XUserNameIdRequest model + ); + + Task IsBanned( + string userSelectByParam, + string destUserSelectByParam + ); + #endregion + + // + #region Open Actions ... + // + #region OpenGet ... + Task OpenGet( + XOpenActionInnerDto model + ); + Task OpenGet( + string token, + string actionRequest + ); + #endregion + + Task GetUserInfo( + XOpenActionRequestDto dto + ); + + Task> GetUserInfos( + XOpenActionRequestDto dto + ); + #endregion + + // + #region Search ... + /// + /// Query Open To Search User Profiles ... + /// + /// requested user's identifier + /// how to filter results based on XQuery structure + /// an instance of XQueryResult of XUserProfileDto + Task> QueryOpenToSearchUsers( + string requestedUserSelectByParam, + XQuery query + ); + #endregion + + // + #region Query Service ... + /// + /// Retrieve Users as Query Model for Query Service ... + /// + /// + /// + /// + /// + /// + public Task> QueryUsers( + string requestedUserSelectByParam, + XQuery query, + string role = null, + bool forceRole = false + ); + #endregion + } +} \ No newline at end of file diff --git a/Interfaces/IXIdentityMessageProvider.cs b/Interfaces/IXIdentityMessageProvider.cs new file mode 100644 index 0000000..41456a3 --- /dev/null +++ b/Interfaces/IXIdentityMessageProvider.cs @@ -0,0 +1,16 @@ +namespace xIds.Interfaces +{ + public interface IXIdentityMessageProvider + { + string InviteMsg { get; } + string RegistrationApproveMsg { get; } + string RegisteredMsg { get; } + string VerificationCodeMsg { get; } + string ChangePasswordMsg { get; } + string PasswordChangedMsg { get; } + string NewDeviceLoggedInMsg { get; } + + bool IsReady(); + void PrepareMessages(string lang); + } +} \ No newline at end of file diff --git a/Interfaces/IXSecurityProvider.cs b/Interfaces/IXSecurityProvider.cs new file mode 100644 index 0000000..b12377a --- /dev/null +++ b/Interfaces/IXSecurityProvider.cs @@ -0,0 +1,10 @@ +namespace xIds.Interfaces +{ + public interface IXSecurityProvider + { + string Encrypt(string plainText); + string Decrypt(string cipherText); + string EncryptFromBytes(byte[] textBytes); + string DecryptFromBytes(byte[] cipherBytes); + } +} \ No newline at end of file diff --git a/Models/XDbInfo.cs b/Models/XDbInfo.cs new file mode 100644 index 0000000..7a1b36c --- /dev/null +++ b/Models/XDbInfo.cs @@ -0,0 +1,31 @@ +using System; +using xIds.Constants; + +namespace xIds.Models +{ + public class XDbInfo + { + private string _migrationAssembly; + public XDbProviders ProviderType { get; set; } + public string MigrationsAssembly + { + get + { + return _migrationAssembly; + } + + set + { + // + _migrationAssembly = value; + + // + OptionsBuilder = b => + { + b.MigrationsAssembly(value); + }; + } + } + public Action OptionsBuilder { get; set; } + } +} \ No newline at end of file diff --git a/Models/XDbSeedDescriptor.cs b/Models/XDbSeedDescriptor.cs new file mode 100644 index 0000000..d945214 --- /dev/null +++ b/Models/XDbSeedDescriptor.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using IdentityServer4.Models; +using xIdentityModels.Descriptors; + +namespace xIds.Models +{ + /// + /// the resource Descriptors for Using on DbSeed + /// + public class XDbSeedDescriptor + { + public bool UpdateExists { get; set; } + public ICollection ApiScopes { get; set; } + public ICollection ApiResources { get; set; } + public ICollection IdentityResources { get; set; } + public ICollection Clients { get; set; } + public ICollection Users { get; set; } + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..95db48b --- /dev/null +++ b/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace xIds +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} \ No newline at end of file diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 0000000..c250adf --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:50046", + "sslPort": 44363 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "xIdentityServerApi": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "applicationUrl": "https://localhost:4001;http://localhost:4000;https://0.0.0.0:4001;", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Providers/XIdentityManager.cs b/Providers/XIdentityManager.cs new file mode 100644 index 0000000..9762ad3 --- /dev/null +++ b/Providers/XIdentityManager.cs @@ -0,0 +1,187 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using xCommons.Extensions; +using xCommons.Providers; +using xExceptions.Constants; +using xIdentityModels; +using xIdentityModels.Configurations; +using xIdentityModels.Constants; +using xIdentityModels.Models; +using xMessageService.Interfaces; +using xStorageService.Interfaces; +using xIds.Configurations; +using xIds.DbContext; +using xIds.Interfaces; + +namespace xIds.Providers +{ + public partial class XIdentityManager : IXIdentityManager + { + // + private JsonSerializerSettings jsonSerializerSettings; + + // + public XIdentityDbContext DbContext { get; } + public UserManager UserManager { get; } + public IXIdentityHelper IdentityHelper { get; } + public ILogger Logger { get; } + public IXMessageProvider MessageProvider { get; } + public IXStorageProvider StorageProvider { get; } + public SignInManager SignInManager { get; } + public IXSecurityProvider SecurityProvider { get; } + public XIdentityConfiguration Configuration { get; } + public RoleManager RoleManager { get; } + public XValidationProvider ValidationProvider { get; } + public XDataServiceConfiguration DataConfiguration { get; } + public IXIdentityMessageProvider IdentityMessageProvider { get; } + public XIdentityResourceConfiguration IdentityResourceConfiguration { get; } + + public XIdentityManager( + XIdentityDbContext dbContext, + UserManager userManager, + IXIdentityHelper identityHelper, + ILogger logger, + IXMessageProvider messageProvider, + IXStorageProvider storageProvider, + SignInManager signInManager, + IXSecurityProvider securityProvider, + XIdentityConfiguration configuration, + RoleManager roleManager, + XValidationProvider validationProvider, + XDataServiceConfiguration dataConfiguration, + IXIdentityMessageProvider identityMessageProvider, + XIdentityResourceConfiguration identityResourceConfiguration + ) + { + // + Logger = logger; + DbContext = dbContext; + RoleManager = roleManager; + UserManager = userManager; + SignInManager = signInManager; + SecurityProvider = securityProvider; + Configuration = configuration; + IdentityHelper = identityHelper; + MessageProvider = messageProvider; + StorageProvider = storageProvider; + DataConfiguration = dataConfiguration; + ValidationProvider = validationProvider; + IdentityMessageProvider = identityMessageProvider; + IdentityResourceConfiguration = identityResourceConfiguration; + + // + this.jsonSerializerSettings = new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; + } + + // + #region Class Getter (s) Methods ... + /// + /// Retrieve User DB Set + /// + /// specifies returned object contains all Navigation Properties or not, default is false + /// + public IQueryable GetUsersDbSet( + bool containDetails = false + ) + { + // + var dbSet = containDetails ? GetUsersFullDbSet() : + UserManager.Users; + return dbSet; + } + + /// + /// Retrieve User Db Set Full Navigation Properties + /// + /// + /// + public IQueryable GetUsersFullDbSet(bool enableTracking = false) + { + // + var dbSet = UserManager.Users + .Include(u => u.Roles) + .Include(u => u.Devices) + .Include(u => u.Followers) + .Include(u => u.Followings) + .Include(u => u.Avatars); + + // + return dbSet.AsSplitQuery(); + } + #endregion + + // + #region Tools ... + /// + /// Get User SelectBy Param + /// + /// + /// + /// + /// + public string GetUserSelectByParam( + XActionRequest model, + bool forceNotNull = true, + ICollection excludes = null + ) + { + // + // Validate Args ... + ValidationProvider.NotNull(model); + + // + var id = model.UserId; + var userName = model.UserName; + var email = model.Email; + var phoneNumber = model.MobileNumber; + + // + if (excludes != null) + { + // + if (excludes.Contains(XUserSelectBy.ID)) + { + id = null; + } + if (excludes.Contains(XUserSelectBy.Email)) + { + email = null; + } + if (excludes.Contains(XUserSelectBy.MobileNumber)) + { + phoneNumber = null; + } + if (excludes.Contains(XUserSelectBy.Username)) + { + userName = null; + } + } + + var selectByParam = + id.IsNullOrEmpty() ? + userName.IsNullOrEmpty() ? + email.IsNullOrEmpty() ? + phoneNumber.IsNullOrEmpty() ? null : phoneNumber : email : userName : id; + + // + // Check result ... + if (forceNotNull && + selectByParam.IsNullOrEmpty()) + { + XException.InvalidData.Throw(); + } + + // + return selectByParam; + } + #endregion + } +} \ No newline at end of file diff --git a/Providers/XIdentityManager/XIdentityManager+Admin.cs b/Providers/XIdentityManager/XIdentityManager+Admin.cs new file mode 100644 index 0000000..e501391 --- /dev/null +++ b/Providers/XIdentityManager/XIdentityManager+Admin.cs @@ -0,0 +1,224 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using xCommons.Extensions; +using xIdentityModels.Dtos; + +namespace xIds.Providers +{ + public partial class XIdentityManager + { + #region Admin Actions ... + /// + /// Retrieve All UnBanned Users Identifiers + /// + /// caller user identifier + /// a collection of user identifiers + public async Task> GetAllUnbanned( + ICollection userSelectByParam + ) + { + // + var result = new List(); + if (!userSelectByParam.HasChild()) + { + return result; + } + + // + foreach (var uParam in userSelectByParam) + { + // + var destUser = await ValidateUserExistsAndRetrieve(uParam, checkIsBanned: false); + if (!destUser.IsBanned) + { + result.Add(uParam); + } + } + + // + return result; + } + + /// + /// Get All Banned Users Identifiers + /// + /// caller user identifier + /// a collection of user identifiers + public async Task> GetAllBanned( + ICollection userSelectByParam + ) + { + // + var result = new List(); + if (!userSelectByParam.HasChild()) + { + return result; + } + + // + foreach (var uParam in userSelectByParam) + { + // + var destUser = await ValidateUserExistsAndRetrieve(uParam, checkIsBanned: false); + if (destUser.IsBanned) + { + result.Add(uParam); + } + } + + // + return result; + } + + /// + /// Ban a Collection of Users + /// + /// caller user identifier + /// an instance of XUserNameIdRequest which represents user identifier list to Ban + /// a collection of user identifiers + public async Task> Ban( + string userSelectByParam, + XUserNameIdRequest model + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(userSelectByParam); + ValidationProvider.NotNull(model); + ValidationProvider.NotZeroChilds(model.Ids); + + // + var user = await ValidateUserExistsAndRetrieve( + userSelectByParam, + forceAdmin: true, + checkIsBanned: true, + containDetails: false, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + var result = new List(); + foreach (var uParam in model.Ids) + { + // + var destUser = await ValidateUserExistsAndRetrieve(uParam, checkIsBanned: false); + + // + var isBanned = destUser.IsBanned; + if (!isBanned) + { + // + destUser.IsBanned = true; + + // + await UpdateUserAsync( + user, + checkCanLoginPolicies: false, + checkIsBanned: false + ); + + // + result.Add(destUser.Id); + } + } + + // + return result; + } + + /// + /// UnBan a Collection of Users + /// + /// caller user identifier + /// an instance of XUserNameIdRequest which represents user identifier list to Ban + /// a collection of user identifiers + public async Task> UnBan( + string userSelectByParam, + XUserNameIdRequest model + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(userSelectByParam); + ValidationProvider.NotNull(model); + ValidationProvider.NotZeroChilds(model.Ids); + + // + var user = await ValidateUserExistsAndRetrieve( + userSelectByParam, + forceAdmin: true, + checkIsBanned: true, + containDetails: false, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + var result = new List(); + foreach (var uParam in model.Ids) + { + // + var destUser = await ValidateUserExistsAndRetrieve(uParam, checkIsBanned: false); + + // + var isBanned = destUser.IsBanned; + if (isBanned) + { + // + destUser.IsBanned = false; + + // + await UpdateUserAsync( + user, + checkCanLoginPolicies: false, + checkIsBanned: false + ); + + // + result.Add(destUser.Id); + } + } + + // + return result; + } + + /// + /// Detrmines a User is Banned or not + /// + /// caller user identifier + /// destination user identifier which checked is banned or not + /// a boolean value which represent user banned or not + public async Task IsBanned( + string userSelectByParam, + string destUserSelectByParam + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty( + userSelectByParam, + destUserSelectByParam + ); + + // + var user = await ValidateUserExistsAndRetrieve( + userSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: false, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + var destUser = await ValidateUserExistsAndRetrieve(destUserSelectByParam, checkIsBanned: false); + + // + var result = destUser.IsBanned; + + // + return result; + } + #endregion + } +} \ No newline at end of file diff --git a/Providers/XIdentityManager/XIdentityManager+Device.cs b/Providers/XIdentityManager/XIdentityManager+Device.cs new file mode 100644 index 0000000..86b20ad --- /dev/null +++ b/Providers/XIdentityManager/XIdentityManager+Device.cs @@ -0,0 +1,481 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using xCommons.Extensions; +using xExceptions.Constants; +using xIdentityModels; +using xIdentityModels.Extensions; +using xIdentityModels.Models; +using xIdentityModels.Navigations; +using xIds.Extensions; + +namespace xIds.Providers +{ + public partial class XIdentityManager + { + // + #region Device Actions ... + /// + /// Check a Device Exists or Not + /// + /// an instance of XDevice + /// a boolean value + public async Task IsDeviceExistsAsync( + XDevice device + ) + { + return await DbContext.Devices + .AnyAsync(d => d.IsSameAs(device)); + } + + /// + /// Add new Device + /// + /// an instance of XDevice + /// an instance of XDevice + public async Task AddDeviceAsync( + XDevice device + ) + { + // + var isDeviceExists = await IsDeviceExistsAsync(device); + if (isDeviceExists) + { + return null; + } + + // + await DbContext.Devices + .AddAsync(device); + + await DbContext.SaveChangesAsync(); + + return device; + } + + /// + /// Get a Device + /// + /// an instance of XDevice + /// an instance of XDevice + public Task GetDeviceAsync( + XDevice device + ) + { + // + return DbContext.Devices + .FirstOrDefaultAsync(d => + d.IsSameAs(device)); + } + + /// + /// Retrieve a List of User Related Devices + /// + /// user identifier + /// check a user can log in in system or not + /// check user is banned or not + /// a collection of XDevice instances which related to user + public async Task> GetUserDevices( + string userSelectByParam, + bool checkCanLoginPolicies = true, + bool checkIsBanned = true + ) + { + // + // Validate ARgs ... + ValidationProvider.NotEmpty(userSelectByParam); + + // + // Validate And Retrieve User ... + var user = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: true, + checkCanLoginPolicies: checkCanLoginPolicies, + checkIsBanned: checkIsBanned + ); + + // + // Check User Devices ... + if (!user.Devices.HasChild()) + { + return new List(); + } + + // + return user.Devices; + } + + /// + /// Determines a Device is Exists in User's Devices or not + /// + /// an instance of XUser + /// an instance of XDevice + /// a boolean value + public bool IsDeviceRelateDToUser( + XUser user, + XDevice device + ) + { + if (!user.Devices.HasChild()) + { + return false; + } + + // + var result = user.Devices.Any(d => d.IsSameAs(device)); + + // + return result; + } + + /// + /// Determines a Device is Exists in User's Devices or not + /// + /// user identifier + /// an instance of XDevice + /// check a user can log in in system or not + /// check user is banned or not + /// a boolean value + public async Task IsDeviceRelateDToUser( + string userSelectByParam, + XDevice device, + bool checkCanLoginPolicies = true, + bool checkIsBanned = true + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(userSelectByParam); + ValidationProvider.NotNull(device); + + // + var userDevices = await GetUserDevices( + userSelectByParam, + checkCanLoginPolicies: checkCanLoginPolicies, + checkIsBanned: checkIsBanned + ); + if (!userDevices.HasChild()) + { + return false; + } + + // + var result = userDevices.Any(d => d.IsSameAs(device)); + + // + return result; + } + + /// + /// Add a Device to a User Related Devices + /// + /// an instance of XUser + /// an instance of XDevice + /// a boolean value + public async Task AddUserRelatedDevice( + XUser user, + XDevice device + ) + { + // + var isExists = user.Devices.Any(d => + d.IsSameAs(device)); + if (isExists) + { + return true; + } + + // + user.Devices.Add(device); + try + { + var result = await UpdateUserAsync(user); + return result.Succeeded; + } + catch + { + throw XException.ActionFailed.ToException(); + } + } + + /// + /// Add a Device to a User + /// + /// user identifier + /// an instance of XDevice + /// check a user can log in in system or not + /// check user is banned or not + /// a boolean value + public async Task AddUserRelatedDevice( + string userSelectByParam, + XDevice device, + bool checkCanLoginPolicies = true, + bool checkIsBanned = true + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(userSelectByParam); + ValidationProvider.NotNull(device); + + // + // Check Device Related ... + var isDeviceRelatedToUser = await IsDeviceRelateDToUser( + userSelectByParam, + device, + checkCanLoginPolicies: checkCanLoginPolicies, + checkIsBanned: checkIsBanned); + if (isDeviceRelatedToUser) + { + return true; + } + + // + // Retrieve User ... + var user = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: true, + checkCanLoginPolicies: checkCanLoginPolicies, + checkIsBanned: checkIsBanned + ); + + // + // Add User Device ... + user.Devices.Add(device); + + // + var result = await UpdateUserAsync( + user, + checkCanLoginPolicies: false, + checkIsBanned: false + ); + + // + return result.Succeeded; + } + + /// + /// Remove a Device from a User Related Devices List + /// + /// an instance of XUser + /// an instance of XDevice + /// a boolean value + public async Task RemoveUserRelatedDevice( + XUser user, + XDevice device + ) + { + // + var isRelated = IsDeviceRelateDToUser(user, device); + if (!isRelated) + { + return false; + } + + // + var instanse = user.Devices + .FirstOrDefault(d => + d.IsSameAs(device)); + + // + user.Devices.Remove(instanse); + + // + var result = await UpdateUserAsync( + user, + checkCanLoginPolicies: false, + checkIsBanned: false + ); + + // + return result.Succeeded; + } + + /// + /// Remove a Device from a User Devices + /// + /// user identifier + /// an instance of XDevice + /// check a user can log in in system or not + /// check user is banned or not + /// a boolean value + public async Task RemoveUserRelatedDevice( + string userSelectByParam, + XDevice device, + bool checkCanLoginPolicies = true, + bool checkIsBanned = true + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(userSelectByParam); + ValidationProvider.NotNull(device); + + // + // Validate user exists and Retrieve it based on userselectbyparam ... + var user = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: true, + checkCanLoginPolicies: checkCanLoginPolicies, + checkIsBanned: checkIsBanned + ); + + // + return await RemoveUserRelatedDevice( + user, + device + ); + } + #endregion + + // + #region Banned Device Actions ... + /// + /// Check a Device is exists in Banned Devices or not + /// + /// an instance of XDevice + /// a boolean value + public async Task IsBannedDeviceExists( + XDevice device + ) + { + // + var result = false; + var bannedDeviceEnumerable = DbContext.BannedDevices.AsAsyncEnumerable(); + await + foreach (var bannedDevice in bannedDeviceEnumerable) + { + // + if (bannedDevice.Device.IsSameAs(device)) + { + // + result = true; + break; + } + } + + // + return result; + } + + /// + /// Find First Banned Device + /// + /// an instance of XDevice + /// an instance of XDevice + public async Task BannedDeviceFindOne( + XDevice device + ) + { + // + var result = await DbContext.BannedDevices + .FirstOrDefaultAsync(bd => + bd.Device.IsSameAs(device) + ); + + // + return result; + } + + /// + /// Remove Banned Device + /// + /// an instance of XBannedDevice + /// + public async Task BannedDeviceRemove( + XBannedDevice model + ) + { + // + var isExists = await IsBannedDeviceExists(model.Device); + if (!isExists) + { + return; + } + + // + var item = await BannedDeviceFindOne(model.Device); + if (item == null) + { + return; + } + + // + DbContext.BannedDevices.Remove(item); + await DbContext.SaveChangesAsync(); + } + + /// + /// Check a Device is Banned or not + /// + /// an instance of XDevice + /// a boolean value + private async Task IsDeviceBanned( + XDevice device + ) + { + // + var result = await IsBannedDeviceExists(device); + return result; + } + + /// + /// Retrieve Specific Banned Device + /// + /// an instance of XDevice + /// an instance of XBannedDevice + private async Task GetBannedDevice( + XDevice device + ) + { + // + var result = await BannedDeviceFindOne(device); + return result; + } + + /// + /// Is a Banned Device passed Banning Time + /// + /// an instance of XBannedDevice + /// a boolean value + private bool IsDelayTimePassed( + XBannedDevice bannedDevice + ) + { + // + // Validate Args ... + if (bannedDevice == null || + Configuration == null) + { + XException.InvalidArgs.Throw(); + } + + // + var isPassed = bannedDevice + .IsDelayTimePassed(Configuration.BannedDeviceTimeout); + + // + return isPassed; + } + + /// + /// Retrieve Passed Time from Banning Time of specific Device + /// + /// an instance of XBannedDevice + /// an instance of DateTime which represent Passed Time + private DateTime GetPassedTime( + XBannedDevice bannedDevice + ) + { + // + var result = bannedDevice.BannedOn + .AddSeconds(Configuration.BannedDeviceTimeout); + + // + return result; + } + #endregion + } +} \ No newline at end of file diff --git a/Providers/XIdentityManager/XIdentityManager+Friendship.cs b/Providers/XIdentityManager/XIdentityManager+Friendship.cs new file mode 100644 index 0000000..9efeb19 --- /dev/null +++ b/Providers/XIdentityManager/XIdentityManager+Friendship.cs @@ -0,0 +1,1680 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using xCommons.Extensions; +using xDataService.Extensions; +using xExceptions.Constants; +using xIdentityModels.Constants; +using xIdentityModels.Dtos; +using xIdentityModels.Navigations; +using xModels.Dtos; + +namespace xIds.Providers +{ + public partial class XIdentityManager + { + // + #region Friendship Actions ... + // + #region Actions ... + /// + /// Send a Follow Request for userSelectByParam to + /// destUserSelectByParam + /// + /// caller user identifier + /// dest user identifier + /// an instance of XFriendshipFollowing + public async Task Follow( + string userSelectByParam, + string destUserSelectByParam + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty( + userSelectByParam, + destUserSelectByParam + ); + + // + // Validate and Retrieve User ... + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: true, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + // Validate and Retrieve Dest User ... + var xDestUser = await ValidateUserExistsAndRetrieve( + destUserSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: true, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + // Validate Not Same Source and Dest Users ... + ValidateNotSameUsers(xUser, xDestUser); + + // + XFriendshipFollowing following = null; + XFriendshipFollower follower = null; + var isRequested = IsFollowRequested(xUser, xDestUser); + // + // Check Request Happens Or not ... + if (!isRequested) + { + // + following = new XFriendshipFollowing + { + UserId = xUser.Id, + DestId = xDestUser.Id, + State = XFriendshipState.Pending + }; + + // + follower = new XFriendshipFollower + { + UserId = xDestUser.Id, + DestId = xUser.Id, + State = XFriendshipState.Pending + }; + + // + xUser.Followings.Add(following); + xDestUser.Followers.Add(follower); + } + else + { + // + // if Request Done Before Must Check State ... + following = xUser.Followings.FirstOrDefault(f => f.DestId == xDestUser.Id); + follower = xDestUser.Followers.FirstOrDefault(f => f.DestId == xUser.Id); + if (following.IsNull() || + follower.IsNull()) + { + XException.NotFound.Throw(); + } + + // + switch (following.State) + { + case XFriendshipState.Accepted: + case XFriendshipState.Blocked: + throw XException.NotAllowed.ToException(); + + case XFriendshipState.Rejected: + following.State = XFriendshipState.Pending; + follower.State = XFriendshipState.Pending; + break; + } + } + + // + var canContinue = !following.IsNull() && !follower.IsNull(); + if (!canContinue) + { + XException.ActionFailed.Throw(); + } + + // + await UpdateUserAsync(xUser); + await UpdateUserAsync(xDestUser); + + // + var result = following; + + // + return result; + } + + /// + /// Cancel Following + /// + /// caller user identifier + /// dest user identifier + /// a boolean value + public async Task Cancel( + string userSelectByParam, + string destUserSelectByParam + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty( + userSelectByParam, + destUserSelectByParam + ); + + // + // Validate and Retrieve User ... + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: true, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + // Validate and Retrieve Dest User ... + var xDestUser = await ValidateUserExistsAndRetrieve( + destUserSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: true, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + // Validate Not Same Source and Dest Users ... + ValidateNotSameUsers(xUser, xDestUser); + + // + XFriendshipFollowing following = null; + XFriendshipFollower follower = null; + var isRequested = IsFollowRequested(xUser, xDestUser); + // + // Check Request Happens Or not ... + if (!isRequested) + { + return false; + } + + // + // if Request Done Before Must Check State ... + following = xUser.Followings.FirstOrDefault(f => f.DestId == xDestUser.Id); + follower = xDestUser.Followers.FirstOrDefault(f => f.DestId == xUser.Id); + if (following.IsNull() || + follower.IsNull()) + { + XException.NotFound.Throw(); + } + + // + switch (following.State) + { + case XFriendshipState.Accepted: + case XFriendshipState.Blocked: + throw XException.NotAllowed.ToException(); + + case XFriendshipState.Rejected: + case XFriendshipState.Pending: + DbContext.Followers.Remove(follower); + DbContext.Followings.Remove(following); + break; + } + + // + var canContinue = !following.IsNull() && !follower.IsNull(); + if (!canContinue) + { + XException.ActionFailed.Throw(); + } + + // + await DbContext.SaveChangesAsync(); + + // + return true; + } + + /// + /// userSelectByParam remove friendship by it's Follower + /// destUserSelectByParam + /// + /// caller user identifier + /// dest user identifier + /// + public async Task UnFollowFollower( + string userSelectByParam, + string destUserSelectByParam + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty( + userSelectByParam, + destUserSelectByParam + ); + + // + // Validate and Retrieve User ... + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: true, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + // Validate and Retrieve Dest User ... + var xDestUser = await ValidateUserExistsAndRetrieve( + destUserSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: true, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + // Validate Not Same Source and Dest Users ... + ValidateNotSameUsers(xUser, xDestUser); + ValidateIsFollowers(xUser, xDestUser); + + // + var follower = xUser.Followers + .FirstOrDefault(f => + f.DestId == xDestUser.Id && + f.State == XFriendshipState.Accepted); + var following = xDestUser.Followings + .FirstOrDefault(f => + f.DestId == xUser.Id && + f.State == XFriendshipState.Accepted); + if (follower.IsNull() || + following.IsNull()) + { + XException.NotFound.Throw(); + } + + // + DbContext.Followers.Remove(follower); + DbContext.Followings.Remove(following); + + // + await DbContext.SaveChangesAsync(); + } + + /// + /// userSelectByParam remove friendship by it's Followings + /// destUserSelectByParam + /// + /// caller user identifier + /// dest user identifier + /// + public async Task UnFollowFollowing( + string userSelectByParam, + string destUserSelectByParam + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty( + userSelectByParam, + destUserSelectByParam + ); + + // + // Validate and Retrieve User ... + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: true, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + // Validate and Retrieve Dest User ... + var xDestUser = await ValidateUserExistsAndRetrieve( + destUserSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: true, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + // Validate Not Same Source and Dest Users ... + ValidateNotSameUsers(xUser, xDestUser); + ValidateIsFollowings(xUser, xDestUser); + + // + var following = xUser.Followings + .FirstOrDefault(f => + f.DestId == xDestUser.Id && + f.State == XFriendshipState.Accepted); + var follower = xDestUser.Followers + .FirstOrDefault(f => + f.DestId == xUser.Id && + f.State == XFriendshipState.Accepted); + if (follower.IsNull() || + following.IsNull()) + { + XException.NotAllowed.Throw(); + } + + // + DbContext.Followers.Remove(follower); + DbContext.Followings.Remove(following); + + // + await DbContext.SaveChangesAsync(); + } + + /// + /// userSelectByParam Block it's follower destUserSelectByParam + /// + /// caller user identifier + /// dest user identifier + /// an instance of XFriendshipFollowing + public async Task Block( + string userSelectByParam, + string destUserSelectByParam + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty( + userSelectByParam, + destUserSelectByParam + ); + + // + // Validate and Retrieve User ... + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: true, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + // Validate and Retrieve Dest User ... + var xDestUser = await ValidateUserExistsAndRetrieve( + destUserSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: true, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + // Validate Not Same Source and Dest Users ... + ValidateNotSameUsers(xUser, xDestUser); + + // + // Validate XDestUser is Accepted Follower of XUser ... + ValidateIsFollowers(xUser, xDestUser); + + // + // Retrieve FriendShip Models ... + var following = xDestUser.Followings + .FirstOrDefault(f => + f.DestId == xUser.Id && + ( + f.State == XFriendshipState.Accepted + ) + ); + var follower = xUser.Followers + .FirstOrDefault(f => + f.DestId == xDestUser.Id && + ( + f.State == XFriendshipState.Accepted + ) + ); + if (following.IsNull() || + follower.IsNull()) + { + XException.NotAllowed.Throw(); + } + + // + follower.State = XFriendshipState.Blocked; + following.State = XFriendshipState.Blocked; + + // + DbContext.Followers.Update(follower); + DbContext.Followings.Update(following); + + // + await DbContext.SaveChangesAsync(); + + // + var result = following; + + // + return result; + } + + /// + /// userSelectByParam UnBlock it's follower destUserSelectByParam + /// + /// caller user identifier + /// dest user identifier + /// an instance of XFriendshipFollowing + public async Task UnBlock( + string userSelectByParam, + string destUserSelectByParam + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty( + userSelectByParam, + destUserSelectByParam + ); + + // + // Validate and Retrieve User ... + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: true, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + // Validate and Retrieve Dest User ... + var xDestUser = await ValidateUserExistsAndRetrieve( + destUserSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: true, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + // Validate Not Same Source and Dest Users ... + ValidateNotSameUsers(xUser, xDestUser); + + // + // Validate XDestUser is Accepted Follower of XUser ... + ValidateIsBlock(xUser, xDestUser); + + // + // Retrieve FriendShip Models ... + var following = xDestUser.Followings + .FirstOrDefault(f => + f.DestId == xUser.Id && + ( + f.State == XFriendshipState.Blocked + ) + ); + var follower = xUser.Followers + .FirstOrDefault(f => + f.DestId == xDestUser.Id && + ( + f.State == XFriendshipState.Blocked + ) + ); + if ( + following.IsNull() || + follower.IsNull() + ) + { + XException.NotAllowed.Throw(); + } + + // + follower.State = XFriendshipState.Accepted; + following.State = XFriendshipState.Accepted; + + // + DbContext.Followers.Update(follower); + DbContext.Followings.Update(following); + + // + await DbContext.SaveChangesAsync(); + + // + var result = following; + + // + return result; + } + #endregion + + // + #region Request Handlers ... + /// + /// userSelectByParam Accept Follow Request of + /// destUserSelectByParam + /// + /// caller user identifier + /// dest user identifier + /// an instance of XFriendshipFollower + public async Task AcceptRequest( + string userSelectByParam, + string destUserSelectByParam + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty( + userSelectByParam, + destUserSelectByParam + ); + + // + // Validate and Retrieve User ... + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: true, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + // Validate and Retrieve Dest User ... + var xDestUser = await ValidateUserExistsAndRetrieve( + destUserSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: true, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + // Validate Not Same Source and Dest Users ... + ValidateNotSameUsers(xUser, xDestUser); + + // + // Validate Request Sent and in Pending State ... + ValidateFollowingRequestForAccept(xUser, xDestUser); + + // + // Retrieve FriendShip Models ... + var following = xDestUser.Followings + .FirstOrDefault(f => + f.DestId == xUser.Id && + ( + f.State == XFriendshipState.Pending || + f.State == XFriendshipState.Rejected + ) + ); + var follower = xUser.Followers + .FirstOrDefault(f => + f.DestId == xDestUser.Id && + ( + f.State == XFriendshipState.Pending || + f.State == XFriendshipState.Rejected + ) + ); + if (following.IsNull() || + follower.IsNull()) + { + XException.NotAllowed.Throw(); + } + + // + follower.State = XFriendshipState.Accepted; + following.State = XFriendshipState.Accepted; + + // + await UpdateUserAsync(xUser); + await UpdateUserAsync(xDestUser); + + // + var result = follower; + + // + return result; + } + + /// + /// Reject a Follow Request of destUserSelectByParam + /// with userSelectByParam + /// + /// caller user identifier + /// dest user identifier + /// an instance of XFriendshipFollower + public async Task RejectRequest( + string userSelectByParam, + string destUserSelectByParam + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty( + userSelectByParam, + destUserSelectByParam + ); + + // + // Validate and Retrieve User ... + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: true, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + // Validate and Retrieve Dest User ... + var xDestUser = await ValidateUserExistsAndRetrieve( + destUserSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: true, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + // Validate Not Same Source and Dest Users ... + ValidateNotSameUsers(xUser, xDestUser); + + // + // Validate Request Sent and in Pending State ... + ValidateFollowingRequest(xUser, xDestUser); + + // + // Retrieve Pending Specified User Request Friendship models ... + var following = xDestUser.Followings + .FirstOrDefault(f => + f.DestId == xUser.Id && + f.State == XFriendshipState.Pending); + var follower = xUser.Followers + .FirstOrDefault(f => + f.DestId == xDestUser.Id && + f.State == XFriendshipState.Pending); + if (following.IsNull() || + follower.IsNull()) + { + XException.NotFound.Throw(); + } + + // + follower.State = XFriendshipState.Rejected; + following.State = XFriendshipState.Rejected; + + // + DbContext.Followers.Update(follower); + DbContext.Followings.Update(following); + + // + await DbContext.SaveChangesAsync(); + + // + var result = follower; + + // + return result; + } + #endregion + + // + #region Getters ... + /// + /// determines destUserSelectByParam is in followers of + /// userSelectByParam or not + /// + /// caller user identifier + /// dest user identifier + /// check user is banned or not, default is true + /// check a user can log in in system or not, default is true + /// a boolean value + public async Task IsFollower( + string userSelectByParam, + string destUserSelectByParam, + bool checkIsBanned = true, + bool checkCanLoginPolicies = true + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty( + userSelectByParam, + destUserSelectByParam + ); + + // + // Validate and Retrieve User ... + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + forceAdmin: false, + containDetails: true, + ignoreDisabledUser: false, + checkIsBanned: checkIsBanned, + checkCanLoginPolicies: checkCanLoginPolicies + ); + + // + // Validate and Retrieve Dest User ... + var xDestUser = await ValidateUserExistsAndRetrieve( + destUserSelectByParam, + forceAdmin: false, + containDetails: true, + ignoreDisabledUser: false, + checkIsBanned: checkIsBanned, + checkCanLoginPolicies: checkCanLoginPolicies + ); + + // + // Validate Not Same Source and Dest Users ... + ValidateNotSameUsers(xUser, xDestUser); + + // + var result = IsFollowRequested(xDestUser, xUser, XFriendshipState.Accepted); + + // + return result; + } + + /// + /// retrieve Friendship state of destUserSelectByParam + /// in Followers of userSelectByParam + /// + /// caller user identifier + /// dest user identifier + /// check user is banned or not, default is true + /// check a user can log in in system or not, default is true + /// a member of XFriendshipState + public async Task GetFollowerState( + string userSelectByParam, + string destUserSelectByParam, + bool checkIsBanned = true, + bool checkCanLoginPolicies = true + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty( + userSelectByParam, + destUserSelectByParam + ); + + // + // Validate and Retrieve User ... + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + forceAdmin: false, + containDetails: true, + ignoreDisabledUser: false, + checkIsBanned: checkIsBanned, + checkCanLoginPolicies: checkCanLoginPolicies + ); + + // + // Validate and Retrieve Dest User ... + var xDestUser = await ValidateUserExistsAndRetrieve( + destUserSelectByParam, + forceAdmin: false, + containDetails: true, + ignoreDisabledUser: false, + checkIsBanned: checkIsBanned, + checkCanLoginPolicies: checkCanLoginPolicies + ); + + // + // Validate Not Same Source and Dest Users ... + ValidateNotSameUsers(xUser, xDestUser); + + // + var isFollow = IsFollowRequested(xDestUser, xUser); + if (!isFollow) + { + return XFriendshipState.None; + } + + // + var follower = xUser.Followers.FirstOrDefault(f => f.DestId == xDestUser.Id); + if (follower.IsNull()) + { + XException.NotFound.Throw(); + } + + // + return follower.State; + } + + /// + /// get follower friendship model of destUserSelectByParam + /// in followers of userSelectByParam + /// + /// caller user identifier + /// dest user identifier + /// an instance of XFriendshipFollower + public async Task GetFollower( + string userSelectByParam, + string destUserSelectByParam + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty( + userSelectByParam, + destUserSelectByParam + ); + + // + // Validate and Retrieve User ... + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: true, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + // Validate and Retrieve Dest User ... + var xDestUser = await ValidateUserExistsAndRetrieve( + destUserSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: true, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + // Validate Not Same Source and Dest Users ... + ValidateNotSameUsers(xUser, xDestUser); + + // + var isFollow = IsFollowRequested(xDestUser, xUser); + if (!isFollow) + { + return null; + } + + // + var follower = xUser.Followers + .FirstOrDefault(f => f.DestId == xDestUser.Id); + if (follower.IsNull()) + { + XException.NotFound.Throw(); + } + + // + var result = follower; + + // + return result; + } + + /// + /// Retrieve Accepted Followers list of userSelectByParam + /// + /// caller user identifier + /// a collection of XFriendshipFollower instances + public async Task> GetFollowers( + string userSelectByParam + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(userSelectByParam); + + // + // Validate and Retrieve User ... + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: true, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + var result = xUser.Followers + .Where(f => f.State == XFriendshipState.Accepted); + + // + return result; + } + + /// + /// Retrieve all Followers list of userSelectByParam + /// + /// caller user identifier + /// a collection of XFriendshipFollower instances + public async Task> GetAllFollowers( + string userSelectByParam + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(userSelectByParam); + + // + // Validate and Retrieve User ... + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: true, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + var result = xUser.Followers; + + // + return result; + } + + /// + /// Read Specified User's Follower's List based on Query Model ... + /// + /// + /// + /// + public async Task> QueryFollowers( + XQuery query, + string userSelectByParam + ) + { + // + // Validate Args ... + ValidationProvider.NotNull(query); + ValidationProvider.NotEmpty(userSelectByParam); + + // + // Validate and Retrieve User ... + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + forceAdmin: false, + checkIsBanned: false, + containDetails: false, + ignoreDisabledUser: false, + checkCanLoginPolicies: false + ); + + // + // Prepare Query ... + + // + if (query.PageSize < DataConfiguration + .PagingConfiguration + .MinAvailablePageSize) + { + // + query.PageSize = DataConfiguration + .PagingConfiguration + .DefaultPageSize; + } + + // + if (query.PageSize > DataConfiguration + .PagingConfiguration + .MaxAvailablePageSize) + { + // + query.PageSize = DataConfiguration + .PagingConfiguration + .MaxAvailablePageSize; + } + + // + // Prepare Enumerator ... + Expression> whereClause = x => x.UserId == xUser.Id; + var enumerator = DbContext.Followers.AsAsyncEnumerable(); + var resultItems = new List(); + await foreach (var entity in enumerator) + { + // + var isApproved = whereClause.Compile()(entity); + if (isApproved) + { + // + // Convert Approved Entity to Dto ... + // then Validate it and Add it to Results ... + var dto = await ToDto(entity); + if (!dto.IsNull()) + { + resultItems.Add(dto); + } + } + } + + // + // Enumerable ... + var result = resultItems.AsEnumerable(); + + // + // Count Total Items ... + var totalItemsCount = result.Count(); + + // + // Apply Filter ... + if (!query.Filter.IsNullOrEmpty()) + { + result = result.ApplyFilter(query.Filter); + } + + // + // Count Filtered Items ... + var totalFilteredItemsCount = result.Count(); + + // + // Apply Paging ... + if (totalItemsCount > 0 && + totalFilteredItemsCount > 0) + { + // + // Apply Sorting ... + if (!query.SortBy.IsNullOrEmpty()) + { + // + // Since Sorting Based on Date is Very Important ... + // we have to do Sorting at first Time then + // try to apply Paging ... + result = result + .ApplySorting( + query.SortBy, + query.IsAscending) + .ToList(); + } + + // + // Apply Paging ... + result = result + .ApplyPaging( + query.Page, + query.PageSize) + .ToList(); + } + + // + // Calculating Page Count ... + int pagesCount = totalItemsCount / query.PageSize; + if (totalItemsCount % query.PageSize > 0) + { + pagesCount++; + } + + // + // Generate Result Object ... + var queryResult = new XQueryResult + { + Page = query.Page, + TotalPages = pagesCount, + PageSize = query.PageSize, + TotalItems = totalItemsCount, + Items = result.AsEnumerable(), + TotalFilteredItems = totalFilteredItemsCount + }; + + // + return queryResult; + } + + /// + /// determines destUserSelectByParam is in followings of + /// userSelectByParam or not + /// + /// caller user identifier + /// dest user identifier + /// check user is banned or not, default is true + /// check a user can log in in system or not, default is true + /// a boolean value + public async Task IsFollowing( + string userSelectByParam, + string destUserSelectByParam, + bool checkIsBanned = true, + bool checkCanLoginPolicies = true + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty( + userSelectByParam, + destUserSelectByParam + ); + + // + // Validate and Retrieve User ... + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + forceAdmin: false, + containDetails: true, + ignoreDisabledUser: false, + checkIsBanned: checkIsBanned, + checkCanLoginPolicies: checkCanLoginPolicies + ); + + // + // Validate and Retrieve Dest User ... + var xDestUser = await ValidateUserExistsAndRetrieve( + destUserSelectByParam, + forceAdmin: false, + containDetails: true, + ignoreDisabledUser: false, + checkIsBanned: checkIsBanned, + checkCanLoginPolicies: checkCanLoginPolicies + ); + + // + // Validate Not Same Source and Dest Users ... + ValidateNotSameUsers(xUser, xDestUser); + + // + var result = IsFollowRequested(xUser, xDestUser, XFriendshipState.Accepted); + + // + return result; + } + + /// + /// retrieve Frindship State of destUserSelectByParam + /// in Followings of userSelectByParam + /// + /// caller user identifier + /// dest user identifier + /// check user is banned or not, default is true + /// check a user can log in in system or not, default is true + /// a member of XFriendshipState + public async Task GetFollowingState( + string userSelectByParam, + string destUserSelectByParam, + bool checkIsBanned = true, + bool checkCanLoginPolicies = true + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty( + userSelectByParam, + destUserSelectByParam + ); + + // + // Validate and Retrieve User ... + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + forceAdmin: false, + containDetails: true, + ignoreDisabledUser: false, + checkIsBanned: checkIsBanned, + checkCanLoginPolicies: checkCanLoginPolicies + ); + + // + // Validate and Retrieve Dest User ... + var xDestUser = await ValidateUserExistsAndRetrieve( + destUserSelectByParam, + forceAdmin: false, + containDetails: true, + ignoreDisabledUser: false, + checkIsBanned: checkIsBanned, + checkCanLoginPolicies: checkCanLoginPolicies + ); + + // + // Validate Not Same Source and Dest Users ... + ValidateNotSameUsers(xUser, xDestUser); + + // + var isFollow = IsFollowRequested(xUser, xDestUser); + if (!isFollow) + { + return XFriendshipState.None; + } + + // + var following = xUser.Followings.FirstOrDefault(f => f.DestId == xDestUser.Id); + if (following.IsNull()) + { + XException.NotFound.Throw(); + } + + // + return following.State; + } + + /// + /// get following friendship model of destUserSelectByParam + /// in followings of userSelectByParam + /// + /// caller user identifier + /// dest user identifier + /// an instance of XFriendshipFollowing + public async Task GetFollowing( + string userSelectByParam, + string destUserSelectByParam + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty( + userSelectByParam, + destUserSelectByParam + ); + + // + // Validate and Retrieve User ... + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: true, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + // Validate and Retrieve Dest User ... + var xDestUser = await ValidateUserExistsAndRetrieve( + destUserSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: true, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + // Validate Not Same Source and Dest Users ... + ValidateNotSameUsers(xUser, xDestUser); + + // + var isFollow = IsFollowRequested(xUser, xDestUser); + if (!isFollow) + { + return null; + } + + // + var following = xUser.Followings.FirstOrDefault(f => f.DestId == xDestUser.Id); + if (following.IsNull()) + { + XException.NotFound.Throw(); + } + + // + var result = following; + + // + return result; + } + + /// + /// Retrieve accepted Followings List of userSelectByParam + /// + /// caller user identifier + /// a collection of XFriendshipFollowing instances + public async Task> GetFollowings( + string userSelectByParam + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(userSelectByParam); + + // + // Validate and Retrieve User ... + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: true, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + var result = xUser.Followings + .Where(f => f.State == XFriendshipState.Accepted); + + // + return result; + } + + /// + /// Retrieve all Followings List of userSelectByParam + /// + /// caller user identifier + /// a collection of XFriendshipFollowing instances + public async Task> GetAllFollowings(string userSelectByParam) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(userSelectByParam); + + // + // Validate and Retrieve User ... + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + forceAdmin: false, + checkIsBanned: true, + containDetails: true, + ignoreDisabledUser: false, + checkCanLoginPolicies: true + ); + + // + var result = xUser.Followings; + + // + return result; + } + + /// + /// Read Specified User's Following's List based on Query Model ... + /// + /// + /// + /// + public async Task> QueryFollowings( + XQuery query, + string userSelectByParam + ) + { + // + // Validate Args ... + ValidationProvider.NotNull(query); + ValidationProvider.NotEmpty(userSelectByParam); + + // + // Validate and Retrieve User ... + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + forceAdmin: false, + checkIsBanned: false, + containDetails: false, + ignoreDisabledUser: false, + checkCanLoginPolicies: false + ); + + // + // Prepare Query ... + + // + if (query.PageSize < DataConfiguration + .PagingConfiguration + .MinAvailablePageSize) + { + // + query.PageSize = DataConfiguration + .PagingConfiguration + .DefaultPageSize; + } + + // + if (query.PageSize > DataConfiguration + .PagingConfiguration + .MaxAvailablePageSize) + { + // + query.PageSize = DataConfiguration + .PagingConfiguration + .MaxAvailablePageSize; + } + + // + // Prepare Enumerator ... + Expression> whereClause = x => x.UserId == xUser.Id; + var enumerator = DbContext.Followings.AsAsyncEnumerable(); + var resultItems = new List(); + await foreach (var entity in enumerator) + { + // + var isApproved = whereClause.Compile()(entity); + if (isApproved) + { + // + // Convert Approved Entity to Dto ... + // then Validate it and Add it to Results ... + var dto = await ToDto(entity); + if (!dto.IsNull()) + { + resultItems.Add(dto); + } + } + } + + // + // Enumerable ... + var result = resultItems.AsEnumerable(); + + // + // Count Total Items ... + var totalItemsCount = result.Count(); + + // + // Apply Filter ... + if (!query.Filter.IsNullOrEmpty()) + { + result = result.ApplyFilter(query.Filter); + } + + // + // Count Filtered Items ... + var totalFilteredItemsCount = result.Count(); + + // + // Apply Paging ... + if (totalItemsCount > 0 && + totalFilteredItemsCount > 0) + { + // + // Apply Sorting ... + if (!query.SortBy.IsNullOrEmpty()) + { + // + // Since Sorting Based on Date is Very Important ... + // we have to do Sorting at first Time then + // try to apply Paging ... + result = result + .ApplySorting( + query.SortBy, + query.IsAscending) + .ToList(); + } + + // + // Apply Paging ... + result = result + .ApplyPaging( + query.Page, + query.PageSize) + .ToList(); + } + + // + // Calculating Page Count ... + int pagesCount = totalItemsCount / query.PageSize; + if (totalItemsCount % query.PageSize > 0) + { + pagesCount++; + } + + // + // Generate Result Object ... + var queryResult = new XQueryResult + { + Page = query.Page, + TotalPages = pagesCount, + PageSize = query.PageSize, + TotalItems = totalItemsCount, + Items = result.AsEnumerable(), + TotalFilteredItems = totalFilteredItemsCount + }; + + // + return queryResult; + } + #endregion + + // + #region Others ... + /// + /// Retrieve List Of All Followings User Id's + /// of userSelectByParam + /// + /// caller user identifier + /// a collection of user identifiers + public async Task> GetFollowingList( + string userSelectByParam + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(userSelectByParam); + + // + // Validate and Retrieve followings List ... + var followings = await GetFollowings(userSelectByParam); + + // + var result = followings.Select(f => f.DestId); + + // + return result.ToList(); + } + + /// + /// Retrieve List Of All Followers User Id's + /// of userSelectByParam + /// + /// caller user identifier + /// a collection of user identifiers + public async Task> GetFollowersList( + string userSelectByParam + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(userSelectByParam); + + // + // Validate and Retrieve followings List ... + var followers = await GetFollowers(userSelectByParam); + + // + var result = followers.Select(f => f.DestId); + + // + return result.ToList(); + } + + /// + /// retrieve Friendship Info Model for Specific User + /// + /// caller user identifier + /// dest user identifier + /// check a user can log in in system or not, default is true + /// check user is banned or not, default is true + /// an instance of XFriendshipInfoDto + public async Task GetFriendshipInfo( + string userSelectByParam, + string destUserSelectByParam, + bool checkCanLoginPolicies = true, + bool checkIsBanned = true + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty( + userSelectByParam, + destUserSelectByParam + ); + + // + // Validate and Retrieve Dest User ... + var xSourceUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + forceAdmin: false, + containDetails: true, + ignoreDisabledUser: false, + checkIsBanned: checkIsBanned, + checkCanLoginPolicies: checkCanLoginPolicies + ); + + // + // Validate and Retrieve Dest User ... + var xDestUser = await ValidateUserExistsAndRetrieve( + destUserSelectByParam, + forceAdmin: false, + containDetails: true, + ignoreDisabledUser: false, + checkIsBanned: checkIsBanned, + checkCanLoginPolicies: checkCanLoginPolicies + ); + + // + var isSame = xSourceUser.Id == xDestUser.Id; + var isFollower = false; + var isFollowing = false; + var followerState = XFriendshipState.None; + var followingState = XFriendshipState.None; + + // + if (!isSame) + { + // + isFollower = await IsFollower( + userSelectByParam, + destUserSelectByParam, + checkIsBanned: checkIsBanned, + checkCanLoginPolicies: checkCanLoginPolicies + ); + + // + isFollowing = await IsFollowing( + userSelectByParam, + destUserSelectByParam, + checkIsBanned: checkIsBanned, + checkCanLoginPolicies: checkCanLoginPolicies + ); + + // + followerState = await GetFollowerState( + userSelectByParam, + destUserSelectByParam, + checkIsBanned: checkIsBanned, + checkCanLoginPolicies: checkCanLoginPolicies + ); + + // + followingState = await GetFollowingState( + userSelectByParam, + destUserSelectByParam, + checkIsBanned: checkIsBanned, + checkCanLoginPolicies: checkCanLoginPolicies + ); + } + + // + var result = new XFriendshipInfoDto + { + UserId = xDestUser.Id, + IsFollower = isFollower, + IsFollowing = isFollowing, + FollowerState = followerState, + FollowingState = followingState, + Followers = xDestUser.Followers + .Count(f => f.State == XFriendshipState.Accepted), + Followings = xDestUser.Followings + .Count(f => f.State == XFriendshipState.Accepted) + }; + + // + return result; + } + #endregion + #endregion + } +} \ No newline at end of file diff --git a/Providers/XIdentityManager/XIdentityManager+Identity.cs b/Providers/XIdentityManager/XIdentityManager+Identity.cs new file mode 100644 index 0000000..405ac83 --- /dev/null +++ b/Providers/XIdentityManager/XIdentityManager+Identity.cs @@ -0,0 +1,342 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using IdentityModel.Client; +using Microsoft.Extensions.Logging; +using xCommons.Extensions; +using xExceptions.Constants; +using xExceptions.Models; +using xIdentityModels.Extensions; +using xIdentityModels.Models; +using xIds.Extensions; +using static IdentityModel.OidcConstants; + +namespace xIds.Providers +{ + public partial class XIdentityManager + { + // + #region Identity Actions ... + /// + /// Request for Discovery Document + /// + /// an instance of DiscoveryDocumentResponse + public async Task RequestDiscoveryDocument() + { + // + var httpClient = GetHttpClient(); + var result = httpClient + .GetDiscoveryDocumentAsync(IdentityResourceConfiguration.Authority) + .ContinueWith(docTask => + { + // + httpClient.Dispose(); + return docTask.Result; + }); + + // + return await result; + } + + /// + /// Request AccessToken for Specific XApiScope + /// + /// a member of XApiScope + /// an instance of TokenResponse + public async Task RequestScopeAccessToken( + string scope + ) + { + // + // Validate Args ... + if (scope.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + // Retrieve Disco Doc ... + var discoDoc = await RequestDiscoveryDocument(); + if (discoDoc.IsError) + { + XException.ActionFailed.Throw(); + } + + // + var request = new ClientCredentialsTokenRequest + { + Address = discoDoc.TokenEndpoint, + GrantType = GrantTypes.ClientCredentials, + ClientId = IdentityResourceConfiguration.ClientId, + ClientSecret = IdentityResourceConfiguration.ClientSecret, + Scope = scope + }; + + // + var httpClient = GetHttpClient(); + var result = await httpClient + .RequestClientCredentialsTokenAsync(request) + .ContinueWith(response => + { + // + httpClient.Dispose(); + return response.Result; + }); + + // + return result; + } + + /// + /// Authenticate a User + /// + /// an instance of XLoginRequest class which represent Authentication requirements + /// an instance of TokenResponse + public async Task Authenticate( + XLoginRequest model + ) + { + // + // Validate Args ... + ValidationProvider + .GroupValidationBuilder() + .AddNotNull(model) + .AddNotEmpty( + model.UserSelectBy, + model.Password + ) + .ValidateGroup(); + + // + // Retrieve Disco Doc ... + var discoDoc = await RequestDiscoveryDocument(); + if (discoDoc.IsError) + { + XException.ActionFailed.Throw(); + } + + // + // Create Request ... + var request = new PasswordTokenRequest + { + // + Address = discoDoc.TokenEndpoint, + + // + GrantType = GrantTypes.ClientCredentials, + ClientId = IdentityResourceConfiguration.ClientId, + ClientSecret = IdentityResourceConfiguration.ClientSecret, + + // + UserName = model.UserSelectBy, + Password = model.Password, + + // + // Pass Device to Request ... + Parameters = { { "force", false.ToJSON () } + } + }; + + // + var httpClient = GetHttpClient(); + var result = await httpClient + .RequestPasswordTokenAsync(request) + .ContinueWith(response => + { + // + httpClient.Dispose(); + return response.Result; + }); + + // + return result; + } + + /// + /// Do Login based on XLoginRequest + /// + /// an instance of XLoginRequest class which represent Authentication requirements + /// an instance of XLoginResponse + public async Task Login( + XLoginRequest model + ) + { + // + // Validate Args ... + ValidationProvider + .GroupValidationBuilder() + .AddNotNull(model, model.Device) + .AddNotEmpty( + model.UserSelectBy, + model.Password + ) + .ValidateGroup(); + + // + // Get Token Response ... + // + // Retrieve Disco Doc ... + var discoDoc = await RequestDiscoveryDocument(); + Logger.LogInformation($"discoDoc: {discoDoc.ToJSON()}"); + + if (discoDoc.IsError) + { + XException.ActionFailed.Throw(); + } + + // + // Create Request ... + var request = new PasswordTokenRequest + { + // + Address = discoDoc.TokenEndpoint, + + // + GrantType = GrantTypes.ClientCredentials, + ClientId = IdentityResourceConfiguration.ClientId, + ClientSecret = IdentityResourceConfiguration.ClientSecret, + + // + UserName = model.UserSelectBy, + Password = model.Password, + + // + // Pass Device to Request ... + Parameters = { { "force", false.ToJSON () }, + { "device", model.Device.ToJSON () }, + { "language", model.Language } + } + }; + + // + var httpClient = GetHttpClient(); + var authResponse = await httpClient + .RequestPasswordTokenAsync(request) + .ContinueWith(response => + { + // + httpClient.Dispose(); + return response.Result; + }); + + // + // Check If Response Has Error ... + Logger.LogInformation($"AuthResponse: {authResponse.ToJSON()}"); + + // + if (authResponse.IsError) + { + throw authResponse.GetException(); + } + + // + // Create XLoginResponse Model ... + var result = authResponse.CreateXLoginResponse(); + + // + // Get ans Set User Profile ... + result.Profile = await GetUserProfileAsync(model.UserSelectBy, model.UserSelectBy); + + // + return result; + } + + /// + /// Refresh Tokens + /// + /// Authentication Tokens, instance of XTokenResponse + /// an instance of XTokenResponse + public async Task RefreshTokens( + XTokenResponse model + ) + { + // + // Validate Args ... + ValidationProvider + .GroupValidationBuilder() + .AddNotNull(model) + .AddNotEmpty( + model.AccessToken, + model.RefreshToken + ) + .ValidateGroup(); + + // + var authResponse = await RequestDiscoveryDocument() + .ContinueWith((discoTask) => + { + // + var discoDoc = discoTask.Result; + if (discoDoc.IsError) + { + XException.ActionFailed.Throw(); + } + + // + using (var httpClient = GetHttpClient()) + { + return httpClient.RequestRefreshTokenAsync(new RefreshTokenRequest + { + // + Address = discoDoc.TokenEndpoint, + GrantType = GrantTypes.RefreshToken, + ClientId = IdentityResourceConfiguration.ClientId, + ClientSecret = IdentityResourceConfiguration.ClientSecret, + RefreshToken = model.RefreshToken + }).Result; + } + }); + + // + // Check If Response Has Error ... + if (authResponse.IsError) + { + // + XError error = authResponse.ErrorDescription.FromJSON(); + throw error.ToException(); + } + + // + // Create XLoginResponse Model ... + var result = authResponse.CreateXTokenResponse(); + + // + return result; + } + #endregion + + // + #region Requirements ... + /// + /// Get an Instance of Http Client + /// + /// an instance of HttpClient + public HttpClient GetHttpClient() + { + // + HttpClient httpClient = null; + httpClient = new HttpClient(); + var httpClientHandler = new HttpClientHandler() + { + ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => + { + // + Logger.LogInformation( + $"SSL Handler: {Environment.NewLine} sender: {sender}, {Environment.NewLine} cert: {cert}, {Environment.NewLine} chain: {chain}, {Environment.NewLine} sslPolicyErrors: {sslPolicyErrors}" + ); + + // + return true; + }, + ClientCertificateOptions = ClientCertificateOption.Manual, + }; + + // + httpClient = new HttpClient(httpClientHandler); + + // + return httpClient; + } + #endregion + } +} \ No newline at end of file diff --git a/Providers/XIdentityManager/XIdentityManager+MessageProvider.cs b/Providers/XIdentityManager/XIdentityManager+MessageProvider.cs new file mode 100644 index 0000000..9dbf317 --- /dev/null +++ b/Providers/XIdentityManager/XIdentityManager+MessageProvider.cs @@ -0,0 +1,341 @@ +using System; +using xCommons.Extensions; +using xExceptions.Constants; +using xIdentityModels.Navigations; +using xMessageService.Models; + +namespace xIds.Providers +{ + public partial class XIdentityManager + { + // + #region Message Provider ... + /// + /// Generate and Prepare an XMessage Instance for + /// User Invite Action + /// + /// specify destination language + /// reciever email address + /// invitation token + /// return url for invitation user to redirect + /// an instance of XMessage + private XMessage GetUserInviteMessage( + string lang, + string emailAddress, + string inviteToken, + string returnUrl + ) + { + // + ValidationProvider + .NotEmpty(lang, emailAddress, inviteToken, returnUrl); + ValidationProvider + .EmailAddress(emailAddress); + ValidationProvider + .Url(returnUrl); + + // + HandleMessageProviderPreperation(lang); + + // + var msgStr = IdentityMessageProvider.InviteMsg + + Environment.NewLine + + returnUrl + + "?t=" + + inviteToken; + + // + var message = new XMessage + { + Message = msgStr, + }; + message.Recievers.Add(emailAddress); + + // + return message; + } + + /// + /// Generate and Prepare an XMessage Instance for + /// New Device LoggedIn Action + /// + /// specify destination language + /// reciever email address + /// an instance of XDevice which represent user new Device + /// an instance of XMessage + private XMessage GetUserNewDeviceLoggedInMessage( + string lang, + string emailAddress, + XDevice device + ) + { + // + ValidationProvider + .NotEmpty(lang, emailAddress); + ValidationProvider + .EmailAddress(emailAddress); + ValidationProvider + .NotNull(device); + + // + HandleMessageProviderPreperation(lang); + + // + // Generate Specific Message ... + var msgStr = IdentityMessageProvider.NewDeviceLoggedInMsg + + Environment.NewLine + + nameof(XDevice.Os) + ": " + device.Os + + Environment.NewLine + + nameof(XDevice.OsVersion) + ": " + device.OsVersion + + Environment.NewLine + + nameof(XDevice.Browser) + ": " + device.Browser + + Environment.NewLine + + nameof(XDevice.UserAgent) + ": " + device.UserAgent; + + // + // Create XMessage Instance ... + var message = new XMessage + { + Message = msgStr, + }; + message.Recievers.Add(emailAddress); + + // + return message; + } + + /// + /// Generate and Prepare an XMessage Instance for + /// Verification Code Action + /// + /// specify destination language + /// an string which points to a user email address or mobile number + /// user verification code + /// an instance of XMessage + private XMessage GetVerificationCodeMessage( + string lang, + string mobileNumberOrEmail, + string verificationCode + ) + { + // + ValidationProvider + .NotEmpty(lang, mobileNumberOrEmail, verificationCode); + + // + HandleMessageProviderPreperation(lang); + + // + mobileNumberOrEmail = mobileNumberOrEmail.ToNormalString(); + + // + // Validate Mobile or Email ... + var isValidEmail = mobileNumberOrEmail.IsValidEmail(); + var isValidMobileNumber = mobileNumberOrEmail.IsValidMobileNumber(); + if (!isValidEmail && + !isValidMobileNumber) + { + XException.InvalidData.Throw(); + } + + // + // Prepare Message String ... + var msgStr = IdentityMessageProvider.VerificationCodeMsg + + Environment.NewLine + + verificationCode; + + // + // Create XMessage Instance ... + var message = new XMessage + { + Message = msgStr, + }; + message.Recievers.Add(mobileNumberOrEmail); + + // + return message; + } + + /// + /// Generate and Prepare an XMessage Instance for + /// Registration Approve Action + /// + /// specify destination language + /// reciever email address + /// a token which approved user action + /// return url for redirect + /// an instance of XMessage + private XMessage GetRegistrationConfirmMessage( + string lang, + string emailAddress, + string actionToken, + string returnUrl + ) + { + // + ValidationProvider + .NotEmpty(lang, emailAddress, actionToken, returnUrl); + ValidationProvider + .EmailAddress(emailAddress); + ValidationProvider + .Url(returnUrl); + + // + HandleMessageProviderPreperation(lang); + + // + // Prepare Message Str ... + var msgStr = IdentityMessageProvider.RegistrationApproveMsg + + Environment.NewLine + + returnUrl + + "?t=" + + actionToken; + + // + var message = new XMessage + { + Message = msgStr, + }; + message.Recievers.Add(emailAddress); + + // + return message; + } + + /// + /// Generate and Prepare an XMessage Instance for + /// Registration Finished Action + /// + /// specify destination language + /// reciever email address + /// return url for redirect + /// an instance of XMessage + private XMessage GetRegistrationFinishedMessage( + string lang, + string emailAddress, + string returnUrl + ) + { + // + ValidationProvider + .NotEmpty(lang, emailAddress, returnUrl); + ValidationProvider + .EmailAddress(emailAddress); + ValidationProvider + .Url(returnUrl); + + // + HandleMessageProviderPreperation(lang); + + // + var msgStr = IdentityMessageProvider.RegisteredMsg + + Environment.NewLine + + returnUrl; + + // + var message = new XMessage + { + Message = msgStr, + }; + message.Recievers.Add(emailAddress); + + // + return message; + } + + /// + /// Generate and Prepare an XMessage Instance for + /// Password Changed Action + /// + /// specify destination language + /// reciever email address + /// return url for redirect + /// specify throw exceptions on failure or not, default is true + /// an instance of XMessage + private XMessage GetPasswordChangedMessage( + string lang, + string emailAddress, + string returnUrl, + bool throwException = true + ) + { + // + if (throwException) + { + ValidationProvider + .NotEmpty(lang, emailAddress, returnUrl); + ValidationProvider + .EmailAddress(emailAddress); + ValidationProvider + .Url(returnUrl); + } + + // + HandleMessageProviderPreperation(lang); + + // + var msgStr = IdentityMessageProvider.PasswordChangedMsg + + (returnUrl.IsNullOrEmpty() ? + "" : + Environment.NewLine + + returnUrl); + + // + var message = new XMessage + { + Message = msgStr, + }; + message.Recievers.Add(emailAddress); + + // + return message; + } + + /// + /// Generate and Prepare an XMessage Instance for + /// Reset Password Action + /// + /// specify destination language + /// reciever email address + /// a token which approved user action + /// return url for redirect + /// an instance of XMessage + private XMessage GetResetPasswordMessage( + string lang, + string emailAddress, + string actionToken, + string returnUrl + ) + { + // + ValidationProvider + .NotEmpty(lang, emailAddress, actionToken, returnUrl); + ValidationProvider + .EmailAddress(emailAddress); + ValidationProvider + .Url(returnUrl); + + // + HandleMessageProviderPreperation(lang); + + // + // Prepare Message Str ... + var msgStr = IdentityMessageProvider.ChangePasswordMsg + + Environment.NewLine + + returnUrl + + "?t=" + + actionToken; + + // + var message = new XMessage + { + Message = msgStr, + }; + message.Recievers.Add(emailAddress); + + // + return message; + } + #endregion + } +} \ No newline at end of file diff --git a/Providers/XIdentityManager/XIdentityManager+Open.cs b/Providers/XIdentityManager/XIdentityManager+Open.cs new file mode 100644 index 0000000..cac2d35 --- /dev/null +++ b/Providers/XIdentityManager/XIdentityManager+Open.cs @@ -0,0 +1,211 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using xCommons.Extensions; +using xExceptions.Constants; +using xIdentityModels.Dtos; + +namespace xIds.Providers +{ + public partial class XIdentityManager + { + // + #region OpenGet ... + public async Task OpenGet( + XOpenActionInnerDto model + ) + { + // + // Validate Args ... + await ValidationProvider + .GroupValidationBuilder() + .AddNotNull(model) + .AddNotEmpty(model.Token) + .AddNotEmpty(model.Payload) + .ValidateGroupAsync(); + + // + return await OpenGet( + token: model.Token, + actionRequest: model.Payload + ); + } + + // + public async Task OpenGet( + string token, + string actionRequest + ) + { + // + // Validatr Args ... + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty(token) + .AddNotEmpty(actionRequest) + .ValidateGroupAsync(); + // + // Try to Decrypt Request ... + var requestModel = ParseModel( + token: token, + request: actionRequest + ); + if (requestModel.IsNull() || + requestModel.Action.IsNullOrEmpty()) + { + XException.BadRequest.Throw(); + } + + // + var responseJson = string.Empty; + switch (requestModel.Action) + { + // + case XOpenActions.GetUserInfo: + // + ValidationProvider.NotEmpty(requestModel.Payload); + var userInfo = await GetUserInfo(requestModel); + if (userInfo.IsNull()) + { + XException.InvalidData.Throw(); + } + + // + responseJson = userInfo.ToJSON(); + break; + + // + case XOpenActions.GetUserInfos: + ValidationProvider.NotEmpty(requestModel.Payload); + var userInfos = await GetUserInfos(requestModel); + if (userInfos.IsNull()) + { + XException.InvalidData.Throw(); + } + + // + responseJson = userInfos.ToJSON(); + break; + } + + // + // Encrypt Json Str ... + var responseStr = SecurityProvider.Encrypt(responseJson); + requestModel.Payload = responseStr; + requestModel.Timestamp = DateTime.UtcNow + .AddMinutes(5) + .ToTimestamp() + .ToString(); + + // + token = requestModel.ToOpenActionToken(); + requestModel.Checksum = token; + var result = new XOpenActionInnerDto + { + Token = token, + Payload = requestModel.ToJSON() + }; + + // + // Check Result ... + if (result.IsNull()) + { + XException.ActionFailed.Throw(); + } + + // + return result; + } + #endregion + + public async Task GetUserInfo( + XOpenActionRequestDto model + ) + { + // + // Retrieve User Profile ... + var userProfile = await GetUserProfileAsync( + checkIsBanned: false, + checkCanLoginPolicies: false, + requestedUserSelectByParam: "", + forceCheckRequestedUser: false, + userSelectByParam: model.Payload + ); + + // + // Generate a Dynamic Object ... + var result = new XOpenActionUserInfoDto + { + Username = userProfile.UserName, + Firstname = userProfile.FirstName, + Lastname = userProfile.LastName, + Avatar = userProfile.Avatar + }; + + // + return result; + } + + public async Task> GetUserInfos( + XOpenActionRequestDto model + ) + { + // + var ids = model.Payload.ParseListString().ToList(); + var profiles = await GetUserProfilesAsync( + ids: ids, + checkIsBanned: false, + checkCanLoginPolicies: false, + requestedUserSelectByParam: "", + forceCheckRequestedUser: false + ); + var result = profiles.Select(x => new XOpenActionUserInfoDto + { + Avatar = x.Avatar, + Username = x.UserName, + Lastname = x.LastName, + Firstname = x.FirstName, + }); + + // + return result; + } + + // + #region Private ... + private XOpenActionRequestDto ParseModel( + string token, + string request + ) + { + // + // Decript request to get Json string of request object ... + var result = new XOpenActionRequestDto(); + + // + var jsonStr = SecurityProvider.Decrypt(request); + result = jsonStr.FromJSON(); + if (result.IsNull() || + !result.Validate() || + token != result.Checksum + ) + { + // + result = null; + return result; + } + + // + var modelChecksum = result.ToOpenActionToken(); + result = + token == modelChecksum + ? result + : null; + + // + return result; + } + #endregion + } +} \ No newline at end of file diff --git a/Providers/XIdentityManager/XIdentityManager+Private.cs b/Providers/XIdentityManager/XIdentityManager+Private.cs new file mode 100644 index 0000000..8475f38 --- /dev/null +++ b/Providers/XIdentityManager/XIdentityManager+Private.cs @@ -0,0 +1,552 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using xCommons.Extensions; +using xCommons.Helpers; +using xExceptions.Constants; +using xIdentityModels; +using xIdentityModels.Constants; +using xIdentityModels.Dtos; +using xIdentityModels.Models; +using xIdentityModels.Navigations; + +namespace xIds.Providers +{ + public partial class XIdentityManager + { + // + #region Generators ... + /// + /// Generate a Random Number ... + /// + /// a long value + private long GenerateRandom() + { + return new Random().Next(100000, 999999); + } + #endregion + + // + #region UserSelectBy Actions ... + /// + /// Retrieve UserSelectByType based on Given Info + /// + /// a user identifier + /// a boolean value which specify the identifier + /// must be specific and not empty, default is true + /// a boolean value which specify the type + /// must be specific, default is false + /// a member of XUserSelectBy + private XUserSelectBy GetUserSelectByType( + string userSelectByParam, + bool forceUserSelectByParamNotEmpty = true, + bool forceUserSelectByTypeMustSpecified = false + ) + { + // + if (forceUserSelectByParamNotEmpty) + { + ValidationProvider.NotEmpty(userSelectByParam); + } + + // + var result = XUserSelectBy.NotSpecified; + var normalizedString = userSelectByParam.ToNormalString(); + if (userSelectByParam.IsValidEmail()) + { + // + // Find User by Email ... + result = XUserSelectBy.Email; + } + else if (userSelectByParam.IsValidMobileNumber()) + { + // + // Find User by Mobile Number ... + result = XUserSelectBy.MobileNumber; + } + else if (userSelectByParam.IsGuid()) + { + // + // Find User By it's ID ... + result = XUserSelectBy.ID; + } + else + { + // + // UserName ... + result = XUserSelectBy.Username; + } + + // + if (forceUserSelectByTypeMustSpecified && + result == XUserSelectBy.NotSpecified) + { + XException.InvalidData.Throw(); + } + + // + return result; + } + + /// + /// Retrieve UserSelectByParam based on XUser Object + /// + /// an instance of XUser + /// a boolean value which specify the identifier must nut empty, + /// and throw exception if it is empty, default is true + /// a collection of XUserSelectBy members which + /// exclude them from result, default is null + /// a user identifier + private string GetUserSelectByParam( + XUser user, + bool forceNotNull = true, + ICollection excludes = null + ) + { + // + // Validate Args ... + ValidationProvider.NotNull(user); + + // + var id = user.Id; + var userName = user.UserName; + var email = user.Email; + var phoneNumber = user.PhoneNumber; + + // + if (excludes != null) + { + if (excludes.Contains(XUserSelectBy.ID)) + { + id = null; + } + if (excludes.Contains(XUserSelectBy.Email)) + { + email = null; + } + if (excludes.Contains(XUserSelectBy.MobileNumber)) + { + phoneNumber = null; + } + if (excludes.Contains(XUserSelectBy.Username)) + { + userName = null; + } + } + + var selectByParam = + id.IsNullOrEmpty() ? + userName.IsNullOrEmpty() ? + email.IsNullOrEmpty() ? + phoneNumber.IsNullOrEmpty() ? null : phoneNumber : email : userName : id; + + // + // Check result ... + if (forceNotNull && + selectByParam.IsNullOrEmpty()) + { + XException.InvalidData.Throw(); + } + + // + return selectByParam; + } + + /// + /// Retrieve UserSelectByParam based on XActionRequestContext instance + /// + /// an instance of XActionRequestContext + /// a boolean value which specify the identifier must nut empty, + /// and throw exception if it is empty, default is true + /// a collection of XUserSelectBy members which + /// exclude them from result, default is null + /// a user identifier + private string GetUserSelectByParam( + XActionRequestContext context, + bool forceNotNull = true, + ICollection excludes = null + ) + { + // + // Validate Args ... + ValidationProvider.NotNull(context); + + // + // Generate User SelectBy ... + var id = context.UserId; + var userName = context.UserName; + var email = context.Email; + var phoneNumber = context.MobileNumber; + + // + if (excludes != null) + { + // + if (excludes.Contains(XUserSelectBy.ID)) + { + id = null; + } + if (excludes.Contains(XUserSelectBy.Email)) + { + email = null; + } + if (excludes.Contains(XUserSelectBy.MobileNumber)) + { + phoneNumber = null; + } + if (excludes.Contains(XUserSelectBy.Username)) + { + userName = null; + } + } + + var selectByParam = + id.IsNullOrEmpty() ? + userName.IsNullOrEmpty() ? + email.IsNullOrEmpty() ? + phoneNumber.IsNullOrEmpty() ? null : phoneNumber : email : userName : id; + + // + // Check param ... + if (forceNotNull && + selectByParam.IsNullOrEmpty()) + { + XException.InvalidData.Throw(); + } + + // + return selectByParam; + } + + /// + /// Retrieve UserSelectByParam based on XActionRequestToken instance + /// + /// an instance of XActionRequestToken which + /// provides required informations + /// a boolean value which specify the identifier must nut empty, + /// and throw exception if it is empty, default is true + /// a collection of XUserSelectBy members which + /// exclude them from result, default is null + /// a user identifier + private string GetUserSelectByParam( + XActionRequestToken request, + bool forceNotNull = true, + ICollection excludes = null + ) + { + // + // Validate Args ... + ValidationProvider.NotNull(request); + + // + // Generate User SelectBy ... + return GetUserSelectByParam( + context: request.Context, + forceNotNull: forceNotNull, + excludes: excludes + ); + } + + /// + /// Retrieve Available UserSelectByParams from given XUser + /// + /// an instance of XUser + /// + private ICollection GenerateUserSelectByParams( + XUser user + ) + { + // + if (user == null) + { + return null; + } + + // + var result = new List { + user.Id, + user.UserName, + user.PhoneNumber, + user.Email, + }; + + // + return result; + } + + /// + /// Retrieve Required XUserSelectByTypes + /// + /// a collection of XUserSelectBy members which + /// ignored in result + /// a collection of available XUserSelectBy members + private ICollection GenerateUserSelectByExcludes( + ICollection ignores + ) + { + // + var result = ObjectHelper + .ToEnumerableValues() + .Except(ignores); + + // + return result.ToList(); + } + #endregion + + // + #region XAction Actions ... + /// + /// Retrieve Action Result Response based on XToken + /// + /// an instance of XToken + /// an instance of XActionResponse + private XActionResponse GetActionResultResponse( + XToken xToken + ) + { + // + ValidationProvider.NotNull(xToken); + ValidationProvider.NotEmpty(xToken.Hash, xToken.Token); + + // + ValidateToken(xToken.Token); + + // + var expDate = GetTokenExpirationDate(xToken.Token); + ValidationProvider.NotNull(expDate); + + // + // Generate Result class ... + var result = new XActionResponse + { + Token = xToken.Hash, + Expiration = expDate + }; + + // + return result; + } + #endregion + + // + #region XIdentityMessage Actions ... + /// + /// Prepare Message Provider and Check it's State + /// + /// specify destination language + private void HandleMessageProviderPreperation( + string lang + ) + { + // + ValidationProvider + .NotEmpty(lang); + + // + // Prepare Identity Messages ... + // the PrepareMessageMethod check IsReady of Helper automatically ... + IdentityMessageProvider.PrepareMessages(lang); + + // + // Check Message Provider ... + var isMessageProviderReady = MessageProvider.IsReady(true); + if (!isMessageProviderReady) + { + XException.MessageServiceInitialFailed.Throw(); + } + } + #endregion + + // + #region Friendship Actions ... + /// + /// Check Follow Request Sent Before + /// from dest to source + /// + /// an instance of XUser + /// an instance of XUser + /// a boolean value + private bool IsFollowRequested( + XUser source, + XUser dest + ) + { + // + // Validate Args ... + ValidationProvider.NotNull(source, dest); + + // + var result = source.Followings.Any(f => f.DestId == dest.Id); + + // + return result; + } + + /// + /// Check Follow Request Sent Before + /// from dest to source + /// + /// an instance of XUser + /// an instance of XUser + /// a member of XFriendshipState + /// a boolean value + private bool IsFollowRequested( + XUser source, + XUser dest, + XFriendshipState state + ) + { + // + // Validate Args ... + ValidationProvider.NotNull(source, dest); + + // + var result = source.Followings + .Any(f => + f.DestId == dest.Id && + f.State == state); + + // + return result; + } + + /// + /// Converts to XFriendDto ... + /// + /// + /// + private async Task ToDto(XFriendshipFollower entity) + { + // + XFriendDto result = null; + + // + if (entity.IsNull()) + { + return result; + } + + // + // Reading Dest User Profile ... + var profile = await GetUserProfileAsync( + userSelectByParam: entity.DestId, + requestedUserSelectByParam: entity.UserId, + forceCheckRequestedUser: false, + checkCanLoginPolicies: false, + checkIsBanned: false + ); + + // + // Preparing Result ... + result = new XFriendDto + { + UserId = profile.UserId, + Avatar = profile.Avatar, + Username = profile.UserName, + Lastname = profile.LastName, + Firstname = profile.FirstName, + Type = XFriendshipType.Follower, + State = profile.FriendshipInfo.FollowerState, + }; + + // + return result; + } + + /// + /// Converts to XFriendDto ... + /// + /// + /// + private async Task ToDto(XFriendshipFollowing entity) + { + // + XFriendDto result = null; + + // + if (entity.IsNull()) + { + return result; + } + + // + // Reading Dest User Profile ... + var profile = await GetUserProfileAsync( + checkIsBanned: false, + checkCanLoginPolicies: false, + forceCheckRequestedUser: false, + userSelectByParam: entity.DestId, + requestedUserSelectByParam: entity.UserId + ); + + // + // Preparing Result ... + result = new XFriendDto + { + UserId = profile.UserId, + Avatar = profile.Avatar, + Username = profile.UserName, + Lastname = profile.LastName, + Firstname = profile.FirstName, + Type = XFriendshipType.Following, + State = profile.FriendshipInfo.FollowingState, + }; + + // + return result; + } + #endregion + + // + #region User Profile ... + /// + /// Count User's Page + /// + /// an integer value which represent page size + /// an inetger value which represent pages count + public async Task UserPagesCount( + int pageSize + ) + { + // + int count = await UserManager.Users.CountAsync(); + int pagesCount = count / pageSize; + + // + if (count % pageSize > 0) + { + pagesCount++; + } + + // + return pagesCount; + } + + /// + /// Count Avatar's Page + /// + /// an integer value which represent page size + /// an inetger value which represent pages count + public async Task ProfileImagePagesCount( + int pageSize + ) + { + // + int count = await DbContext.Avatars.CountAsync(); + int pagesCount = count / pageSize; + + // + if (count % pageSize > 0) + { + pagesCount++; + } + + // + return pagesCount; + } + #endregion + } +} \ No newline at end of file diff --git a/Providers/XIdentityManager/XIdentityManager+Profile.cs b/Providers/XIdentityManager/XIdentityManager+Profile.cs new file mode 100644 index 0000000..3eca00e --- /dev/null +++ b/Providers/XIdentityManager/XIdentityManager+Profile.cs @@ -0,0 +1,2016 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using xCommons.Extensions; +using xExceptions.Constants; +using xIdentityModels.Constants; +using xIdentityModels.Dtos; +using xIdentityModels.Models; +using xIdentityModels.Navigations; +using xModels.Dtos; +using xIds.Extensions; +using xIdentityModels.Extensions; +using xIdentityHelper; +using xIdentityHelper.Extensions; +using xDataService.Extensions; + +namespace xIds.Providers +{ + public partial class XIdentityManager + { + // + #region Retrieve Actions ... + /// + /// Retrieve User Profile + /// + /// a user identifier + /// requested user's identifier + /// specify check requested user's identifier not empty + /// and throw exception if it is, default true + /// check a user can log in in system or not, default is true + /// check user is banned or not, default is true + /// an instance of XUserProfileDto + public async Task GetUserProfileAsync( + string userSelectByParam, + string requestedUserSelectByParam, + bool forceCheckRequestedUser = true, + bool checkCanLoginPolicies = true, + bool checkIsBanned = true + ) + { + // + // Data Validation ... + if (forceCheckRequestedUser) + { + ValidationProvider.NotEmpty( + userSelectByParam, + requestedUserSelectByParam); + } + else + { + ValidationProvider.NotEmpty(userSelectByParam); + } + + // + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: true, + checkCanLoginPolicies: false, + checkIsBanned: false, + ignoreDisabledUser: true, + forceAdmin: false + ); + + // + var isAdmin = false; + var isSameUser = false; + if (forceCheckRequestedUser && + !requestedUserSelectByParam.IsNullOrEmpty()) + { + // + var xRequestedUser = await ValidateUserExistsAndRetrieve( + requestedUserSelectByParam, + containDetails: true, + checkCanLoginPolicies: false, + checkIsBanned: false, + ignoreDisabledUser: true, + forceAdmin: false + ); + + // + isSameUser = xUser.Id == xRequestedUser.Id; + + // + var xRequestUserRoleNames = await GetRoleNamesAsync( + userSelectByParam: requestedUserSelectByParam, + checkCanLoginPolicies: false, + checkIsBanned: false + ); + isAdmin = xRequestUserRoleNames.Contains(XUserRole.Admin.GetStringValue()); + } + + // + var roleNames = await GetRoleNamesAsync( + userSelectByParam, + checkIsBanned: checkIsBanned, + checkCanLoginPolicies: checkCanLoginPolicies + ); + + // + var xFriendshipInfo = + requestedUserSelectByParam.IsNullOrEmpty() + ? null + : await GetFriendshipInfo( + requestedUserSelectByParam, + userSelectByParam, + checkIsBanned: checkIsBanned, + checkCanLoginPolicies: checkCanLoginPolicies + ); + + // + var result = xUser.ToXProfileDto( + isAdmin: isAdmin, + roles: roleNames, + isSameUser: isSameUser, + friendship: xFriendshipInfo, + profileConfig: Configuration.Policy.Profile + ); + + // + return result; + } + + /// + /// Retrieve a Collection of User Profiles + /// + /// a collection of user identifiers + /// requested user's identifier + /// specify check requested user's identifier not empty + /// and throw exception if it is, default true + /// check a user can log in in system or not, default is true + /// check user is banned or not, default is true + /// an collection of XUserProfileDto instances + public async Task> GetUserProfilesAsync( + ICollection ids, + string requestedUserSelectByParam, + bool forceCheckRequestedUser = true, + bool checkCanLoginPolicies = true, + bool checkIsBanned = true + ) + { + // + // Data Validation ... + if (forceCheckRequestedUser) + { + ValidationProvider.NotEmpty(requestedUserSelectByParam); + } + ValidationProvider.NotZeroChilds(ids); + + // + var result = new List(); + foreach (var id in ids) + { + // + var xProfile = await GetUserProfileAsync( + id, + requestedUserSelectByParam, + checkIsBanned: checkIsBanned, + checkCanLoginPolicies: checkCanLoginPolicies, + forceCheckRequestedUser: forceCheckRequestedUser + ); + result.Add(xProfile); + } + + // + return result; + } + + /// + /// Query User Profiles + /// + /// requested user's identifier + /// how to filter results based on XQuery structure + /// an instance of XQueryResult of XUserProfileDto + public async Task> QueryUsers( + string requestedUserSelectByParam, + XQuery query + ) + { + // + // Validation ... + if (query.IsNull()) + { + XException.InvalidArgs.Throw(); + } + + // + // Normalize ... + query = query.NormalizeQuery(DataConfiguration.ToXDataServiceConfig()); + + // + // Since in Resourceable Entities we have to Search on Locales + // we Must Implement Senario Custom ... + var totalEntities = GetUsersDbSet() + .ToList() + .Where(u => !u.ContainsUserSelectByParam(requestedUserSelectByParam)) + .Select(u => u.Id) + .ToList(); + + // + var items = await GetUserProfilesAsync( + totalEntities, + requestedUserSelectByParam, + checkIsBanned: false, + checkCanLoginPolicies: false, + forceCheckRequestedUser: false + ); + + // + var totalItemsCount = items.Count(); + + // + // Apply Filter ... + if (!query.Filter.IsNullOrEmpty()) + { + // + items = items + .ApplyFilter(query.Filter); + } + int filteredItemsCount = items.Count(); + + // + // Count Pages ... + var totalPagesCount = query.CountPages(totalItemsCount); + var filteredPagesCount = query.CountPages(filteredItemsCount); + + // + // Apply Paging and Sorting ... + if (totalItemsCount > 0 && + filteredItemsCount > 0) + { + // + // Apply Sorting ... + items = items + .ToList() + .ApplySorting( + query.SortBy, + query.IsAscending + ); + + // + // Apply Paging ... + items = items + .ToList() + .ApplyPaging( + query.Page, + query.PageSize + ); + } + + // + // Generate Result Object ... + // Query = query, + var result = new XQueryResult + { + Items = items, + Page = query.Page, + PageSize = query.PageSize, + TotalPages = totalPagesCount, + TotalItems = totalItemsCount, + TotalFilteredPages = filteredPagesCount, + TotalFilteredItems = filteredItemsCount + }; + + // + return result; + } + + /// + /// Query In Roles User Profiles + /// + /// requested user's identifier + /// an string which represent user role + /// how to filter results based on XQuery structure + /// if it's true the user must has exact role, otherwise top level users also listed, default is false + /// an instance of XQueryResult of XUserProfileDto + public async Task> QueryInRoleUsers( + string requestedUserSelectByParam, + string role, + XQuery query, + bool forceRole = false + ) + { + // + // Validation ... + if (query.IsNull() || role.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + // Check Role Exists ... + var isExistsRole = await IsRoleExistsAsync(role); + if (!isExistsRole) + { + XException.NotFound.Throw(); + } + + // + // Normalize ... + query = query.NormalizeQuery(DataConfiguration.ToXDataServiceConfig()); + + // + #region Select Users Based on Specific Roles ... + var selectedUsers = new List(); + var usersEnumerator = GetUsersDbSet(query.ContainsDetail).AsAsyncEnumerable(); + await + foreach (var user in usersEnumerator) + { + // + if (user.ContainsUserSelectByParam(requestedUserSelectByParam)) + { + continue; + } + + // + // Set Default Value is True ... + var isInRole = false; + + // + // if Force Role ... + if (forceRole) + { + isInRole = await UserManager.IsInRoleAsync(user, role); + } + else + { + // + // Select User Top Role ... + var userRoles = await GetRoleNamesAsync( + checkIsBanned: false, + userSelectByParam: user.Id, + checkCanLoginPolicies: false + ); + isInRole = userRoles.HasRolePermissions(role); + } + + // + if (isInRole) + { + selectedUsers.Add(user.Id); + } + } + #endregion + + // + var items = await GetUserProfilesAsync( + selectedUsers, + requestedUserSelectByParam, + checkIsBanned: false, + checkCanLoginPolicies: false, + forceCheckRequestedUser: false + ); + + // + var totalItemsCount = items.Count(); + + // + // Apply Filter ... + if (!query.Filter.IsNullOrEmpty()) + { + // + items = items + .ApplyFilter(query.Filter); + } + int filteredItemsCount = items.Count(); + + // + // Count Pages ... + var totalPagesCount = query.CountPages(totalItemsCount); + var filteredPagesCount = query.CountPages(filteredItemsCount); + + // + // Apply Paging and Sorting ... + if (totalItemsCount > 0 && + filteredItemsCount > 0) + { + // + // Apply Sorting ... + items = items + .ToList() + .ApplySorting( + query.SortBy, + query.IsAscending + ); + + // + // Apply Paging ... + items = items + .ToList() + .ApplyPaging( + query.Page, + query.PageSize + ); + } + + // + // Generate Result Object ... + // Query = query, + var result = new XQueryResult + { + Items = items, + Page = query.Page, + PageSize = query.PageSize, + TotalPages = totalPagesCount, + TotalItems = totalItemsCount, + TotalFilteredPages = filteredPagesCount, + TotalFilteredItems = filteredItemsCount + }; + + // + return result; + } + + /// + /// Query Specified User's Profile Images + /// + /// a user identifier + /// how to filter results based on XQuery structure + /// an instance of XQueryResult of XProfileImage + public async Task> QueryAvatars( + string userSelectByParam, + XQuery query + ) + { + // + // Validation ... + if (query.IsNull()) + { + XException.InvalidArgs.Throw(); + } + + // + // Normalize ... + query = query.NormalizeQuery(DataConfiguration.ToXDataServiceConfig()); + + // + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: true, + checkCanLoginPolicies: true, + checkIsBanned: true + ); + + // + var items = query.ContainsDetail ? + DbContext.Avatars + .Include(pf => pf.User) + .AsEnumerable() : + DbContext.Avatars + .AsEnumerable(); + items = items + .Where(pfi => pfi.UserId == xUser.Id); + + // + var totalItemsCount = items.Count(); + + // + // Apply Filter ... + if (!query.Filter.IsNullOrEmpty()) + { + // + items = items + .ApplyFilter(query.Filter); + } + int filteredItemsCount = items.Count(); + + // + // Count Pages ... + var totalPagesCount = query.CountPages(totalItemsCount); + var filteredPagesCount = query.CountPages(filteredItemsCount); + + // + // Apply Paging and Sorting ... + if (totalItemsCount > 0 && + filteredItemsCount > 0) + { + // + // Apply Sorting ... + items = items + .ToList() + .ApplySorting( + query.SortBy, + query.IsAscending + ); + + // + // Apply Paging ... + items = items + .ToList() + .ApplyPaging( + query.Page, + query.PageSize + ); + } + + // + // Generate Result Object ... + // Query = query, + var result = new XQueryResult + { + Items = items, + Page = query.Page, + PageSize = query.PageSize, + TotalPages = totalPagesCount, + TotalItems = totalItemsCount, + TotalFilteredPages = filteredPagesCount, + TotalFilteredItems = filteredItemsCount + }; + + // + return result; + } + + /// + /// Check specific user Confirmed Email or not + /// + /// a user identifier + /// a boolean value + public async Task IsConfirmedEmail( + string userSelectByParam + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(userSelectByParam); + + // + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: false, + checkCanLoginPolicies: false, + checkIsBanned: false, + ignoreDisabledUser: true, + forceAdmin: false + ); + + // + return xUser.EmailConfirmed; + } + + /// + /// Ceck specific user Confirmed Mobile or not + /// + /// a user identifier + /// a boolean value + public async Task IsConfirmedMobile( + string userSelectByParam + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(userSelectByParam); + + // + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: false, + checkCanLoginPolicies: false, + checkIsBanned: false, + ignoreDisabledUser: true, + forceAdmin: false + ); + + // + return xUser.PhoneNumberConfirmed; + } + #endregion + + // + #region Update Actions ... + /// + /// Update User Profile (FirstName/LastName/DateOfBirth) ... + /// + /// a user identifier + /// requested user's identifier + /// user update info, an instance of XProfileUpdateRequest + /// an instance of XUserProfileDto + public async Task ProfileUpdateAsync( + string userSelectByParam, + string requestedUserSelectByParam, + XProfileUpdateRequest model + ) + { + // + // Data Validation ... + ValidationProvider.NotEmpty( + userSelectByParam, + requestedUserSelectByParam); + ValidationProvider.NotNull(model); + + // + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: true, + checkCanLoginPolicies: false, + checkIsBanned: false, + ignoreDisabledUser: true, + forceAdmin: false + ); + + // + var xRequestedUser = await ValidateUserExistsAndRetrieve( + requestedUserSelectByParam, + containDetails: true, + checkCanLoginPolicies: false, + checkIsBanned: false, + ignoreDisabledUser: true, + forceAdmin: false + ); + + // + await ValidateUserForDangerousAction(xUser, xRequestedUser); + + // + // FirstName ... + if (!model.FirstName.IsNullOrEmpty()) + { + xUser.FirstName = model.FirstName; + } + + // + // Last Name ... + if (!model.LastName.IsNullOrEmpty()) + { + xUser.LastName = model.LastName; + } + + // + // Date of Birth ... + if (model.DateOfBirth.HasValue) + { + xUser.DateOfBirth = model.DateOfBirth.Value; + } + + // + // Bio ... + xUser.Bio = model.Bio; + + // + // Cover Image ... + xUser.CoverImage = model.CoverImage; + + // + // Open To Search ... + xUser.OpenToSearch = model.OpenToSearch; + + // + var updateResult = await UpdateUserAsync(xUser); + if (!updateResult.Succeeded) + { + XException.ActionFailed.Throw(); + } + + // + var result = await GetUserProfileAsync(userSelectByParam, requestedUserSelectByParam); + return result; + } + + /// + /// Update User Profile + /// + /// a user identifier + /// requested user's identifier + /// user update info, an instance of XProfileUpdateRequest + /// an instance of XUserProfileDto + public async Task FullProfileUpdateAsync( + string userSelectByParam, + string requestedUserSelectByParam, + XProfileUpdateRequest model + ) + { + // + // Data Validation ... + ValidationProvider.NotEmpty( + userSelectByParam, + requestedUserSelectByParam); + ValidationProvider.NotNull(model); + + // + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: true, + checkCanLoginPolicies: false, + checkIsBanned: false, + ignoreDisabledUser: true, + forceAdmin: false + ); + + // + var xRequestedUser = await ValidateUserExistsAndRetrieve( + requestedUserSelectByParam, + containDetails: true, + checkCanLoginPolicies: false, + checkIsBanned: false, + ignoreDisabledUser: true, + forceAdmin: false + ); + + // + await ValidateUserForDangerousAction(xUser, xRequestedUser); + + // + #region Fill User with Models Value ... + // + // FirstName ... + xUser.FirstName = !model.FirstName.IsNullOrEmpty() ? + model.FirstName : + xUser.FirstName; + // + // LastName ... + xUser.LastName = !model.LastName.IsNullOrEmpty() ? + model.LastName : + xUser.LastName; + + // + // Email ... + xUser.Email = !model.Email.IsNullOrEmpty() ? + model.Email : + xUser.Email; + + // + // EmailConfirmed ... + if (model.EmailConfirmed.HasValue) + { + xUser.EmailConfirmed = model.EmailConfirmed.Value; + } + + // + // PhoneNumber ... + xUser.PhoneNumber = !model.PhoneNumber.IsNullOrEmpty() ? + model.PhoneNumber : + xUser.PhoneNumber; + + // + // PhoneNumberConfirmed ,,, + if (model.PhoneNumberConfirmed.HasValue) + { + xUser.PhoneNumberConfirmed = model.PhoneNumberConfirmed.Value; + } + + // + // DateOfBirth ... + if (model.DateOfBirth.HasValue) + { + xUser.DateOfBirth = model.DateOfBirth.Value; + } + + // + // CreationDate ... + if (model.CreationDate.HasValue) + { + xUser.CreationDate = model.CreationDate.Value; + } + + // + // LastLogin ... + if (model.LastLogin.HasValue) + { + xUser.LastLogin = model.LastLogin.Value; + } + + // + // Avatar ... + xUser.Avatar = !model.Avatar.IsNullOrEmpty() ? + model.Avatar : + xUser.Avatar; + + // + // Gender ... + if (model.Gender.HasValue) + { + xUser.Gender = model.Gender.Value; + } + + // + // IsEnable ... + if (model.IsEnable.HasValue) + { + xUser.IsEnable = model.IsEnable.Value; + } + + // + // IsBanned ... + if (model.IsBanned.HasValue) + { + xUser.IsBanned = model.IsBanned.Value; + } + + // + // Bio ... + xUser.Bio = model.Bio; + + // + // Cover Image ... + xUser.CoverImage = model.CoverImage; + + // + // Open To Search ... + xUser.OpenToSearch = model.OpenToSearch; + #endregion + + // + var updateResult = await UpdateUserAsync( + xUser, + checkCanLoginPolicies: false, + checkIsBanned: false + ); + if (!updateResult.Succeeded) + { + XException.ActionFailed.Throw(); + } + + // + // Handle Roles ... + + // + // Remove User from All Roles ... + if (xUser.Roles.HasChild()) + { + // + var roleNames = await GetRoleNamesAsync( + xUser.Id, + checkIsBanned: false, + checkCanLoginPolicies: false + ); + + // + foreach (var role in roleNames) + { + // + var isRoleExists = await IsRoleExistsAsync(role); + if (isRoleExists) + { + // + var isInRole = await UserManager.IsInRoleAsync(xUser, role); + if (!isInRole) + { + // + var removeRoleResult = await UserManager.RemoveFromRoleAsync(xUser, role); + if (!removeRoleResult.Succeeded) + { + XException.ActionFailed.Throw(); + } + } + } + } + } + + // + // Check Roles and Add to Exists ... + if (model.Roles.HasChild()) + { + // + // Check Exists Roles for Add ... + foreach (var role in model.Roles) + { + // + var isRoleExists = await IsRoleExistsAsync(role); + if (isRoleExists) + { + // + var isInRole = await UserManager.IsInRoleAsync(xUser, role); + if (!isInRole) + { + // + var addToRoleResult = await UserManager.AddToRoleAsync(xUser, role); + if (!addToRoleResult.Succeeded) + { + XException.ActionFailed.Throw(); + } + } + } + } + } + + // + var result = await GetUserProfileAsync( + userSelectByParam, + requestedUserSelectByParam, + checkCanLoginPolicies: false, + checkIsBanned: false + ); + return result; + } + #endregion + + // + #region Request For Actions ... + /// + /// Request Mobile Confirm + /// + /// specify destination language + /// an instance of XDevice + /// a user identifier + /// user's password + /// mobile number which need to confirm + /// an instance of XActionResponse + public async Task RequestConfirmMobile( + string lang, + XDevice device, + string userSelectByParam, + string password, + string mobileNumber = null + ) + { + // + // Validate Args ... + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty( + lang, + userSelectByParam, + password) + .AddNotNull(device) + .ValidateGroupAsync(); + + // + await ValidateDeviceForActions(device); + + // + // Prepare Request Context ... + var context = new XActionRequestContext + { + Lang = lang, + Device = device, + MobileNumber = mobileNumber + }; + + // + // Validate and Retrieve User ... + var user = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: false, + checkCanLoginPolicies: false, + checkIsBanned: false); + + // + context.UserName = user.UserName; + + // + await ValidateUserAndPassword( + userSelectByParam, + password, + false, + XException.ActionFailed.ToException()); + + // + ValidationProvider.MobileNumber(user.PhoneNumber); + if (!mobileNumber.IsNullOrEmpty() && + user.PhoneNumber == mobileNumber && + user.PhoneNumberConfirmed) + { + XException.MobileInUsed.Throw(); + } + + // + if (mobileNumber.IsNullOrEmpty()) + { + mobileNumber = user.PhoneNumber; + } + + // + // Remove All Prevoius Requests ... + await HandleRemoveExistsTokens(user.Id); + + // + // Prepare New Token Result , + // by Requesting an Action ... + var request = await ActionRequest( + lang, + device, + userSelectByParam, + XAction.RequestMobileVerificationCode, + context, + forceRenewToken: true + ); + + // + Logger.LogInformation($"ActionRequest: {request.ToJSON()}"); + + // + // Call Confirm Method of Registration ... + var result = await RequestMobileVerificationCode( + lang, + device, + request.Token, + mobileNumber, + checkMobileInUse: false); + + // + return result; + } + + /// + /// Request Email Confirma + /// + /// specify destination language + /// an instance of XDevice + /// a user identifier + /// user's password + /// email address which need to confirm + /// an instance of XActionResponse + public async Task RequestConfirmEmail( + string lang, + XDevice device, + string userSelectByParam, + string password, + string emailAddress = null + ) + { + // + // Validate Args ... + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty( + lang, + userSelectByParam, + password) + .AddNotNull(device) + .ValidateGroupAsync(); + + // + await ValidateDeviceForActions(device); + + // + // Prepare Request Context ... + var context = new XActionRequestContext + { + Lang = lang, + Device = device, + Email = emailAddress + }; + + // + // Validate and Retrieve User ... + var user = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: false, + checkCanLoginPolicies: false, + checkIsBanned: false); + + // + context.UserName = user.UserName; + + // + // Remove All Prevoius Requests ... + await HandleRemoveExistsTokens(user.Id); + + // + await ValidateUserAndPassword( + userSelectByParam, + password, + false, + XException.ActionFailed.ToException()); + + // + ValidationProvider.EmailAddress(user.Email); + if (!emailAddress.IsNullOrEmpty() && + user.Email == emailAddress && + user.EmailConfirmed) + { + XException.EmailInUsed.Throw(); + } + + // + if (emailAddress.IsNullOrEmpty()) + { + emailAddress = user.Email; + context.Email = user.Email; + } + + // + // Prepare New Token Result , + // by Requesting an Action ... + var request = await ActionRequest( + lang, + device, + userSelectByParam, + XAction.RequestEmailVerificationCode, + context, + forceRenewToken: true + ); + + // + // Call Confirm Method of Registration ... + var result = await RequestEmailVerificationCode( + lang, + device, + request.Token, + emailAddress, + checkEmailInUse: false); + + // + return result; + } + + /// + /// Request Reset Password + /// + /// specify destination language + /// an instance of XDevice + /// a user identifier + /// redirection url + /// an instance of XActionResponse + public async Task RequestResetPassword( + string lang, + XDevice device, + string userSelectByParam, + string returnUrl + ) + { + // + // Validate Args ... + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty( + lang, + userSelectByParam, + returnUrl) + .AddNotNull(device) + .AddUrl(returnUrl) + .ValidateGroupAsync(); + + // + await ValidateDeviceForActions(device); + + // + // Validate and Retrieve User ... + var user = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: false, + checkCanLoginPolicies: true, + checkIsBanned: true); + + // + // Prepare Request Context ... + var context = new XActionRequestContext + { + Lang = lang, + Device = device, + Email = user.Email, + UserName = user.UserName + }; + + // + // Remove All Prevoius Requests ... + await HandleCleanUserTokens(user); + + // + // Prepare New Token Result , + // by Requesting an Action ... + var result = await ActionRequest( + lang, + device, + user.Email, + XAction.RequestResetPassword, + context + ); + + // + // Prepare Message ... + var xMessage = GetResetPasswordMessage( + lang, + user.Email, + result.Token, + returnUrl); + + // + try + { + await MessageProvider.SendMailAsync(xMessage); + } + catch { } + + // + return result; + } + + /// + /// Request Registration Confrirm + /// + /// specify destination language + /// an instance of XDevice + /// a user identifier + /// user's password + /// redirection url + /// + public async Task RequestConfirmRegistration( + string lang, + XDevice device, + string userSelectByParam, + string password, + string returnUrl + ) + { + // + // Validate Args ... + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty( + lang, + userSelectByParam, + password, + returnUrl) + .AddNotNull(device) + .AddUrl(returnUrl) + .ValidateGroupAsync(); + + // + // Validate user and Retrieve it ... + var user = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: false, + checkCanLoginPolicies: false, + checkIsBanned: true, + ignoreDisabledUser: true); + + // + // Prepare Request Context ... + var context = new XActionRequestContext + { + Lang = lang, + Device = device, + Email = user.Email + }; + + // + await ValidateUserAndPassword( + userSelectByParam, + password, + false, + XException.ActionFailed.ToException()); + + // + // Remove All Prevoius Requests ... + await HandleRemoveExistsTokens(user.Id); + + // + // Prepare New Token Result , + // by Requesting an Action ... + var result = await ActionRequest( + lang, + device, + user.Email, + XAction.Finish, + context + ); + + // + // Prepare XMessage Instance ... + var xMessage = GetRegistrationConfirmMessage( + lang, + user.Email, + result.Token, + returnUrl); + + // + // Send Message ... + try + { + await MessageProvider.SendMailAsync(xMessage); + } + catch { } + } + #endregion + + // + #region Password Actions ... + /// + /// Change Password + /// + /// specify destination language + /// an instance of XDevice + /// a user identifier + /// user's password + /// user's new password + /// redirection url + /// + public async Task ChangePassword( + string lang, + XDevice device, + string userSelectByParam, + string password, + string newPassword, + string returnUrl + ) + { + // + // Validate Args ... + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty( + lang, + userSelectByParam, + password, + newPassword, + returnUrl) + .AddNotNull(device) + .ValidateGroupAsync(); + + // + // Password Unique ... + if (password == newPassword) + { + XException.PasswordsSame.Throw(); + } + + // + // Validate and Retrieve User ... + var user = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: false, + checkCanLoginPolicies: true, + checkIsBanned: true); + + // + await ValidateUserAndPassword( + userSelectByParam, + password); + + // + // Check Password Policies ... + await ValidatePasswordPolicies(user, newPassword); + + // + // Clean All User Tokens ... + await HandleCleanUserTokens(user); + + // + // Try to Change User Password ... + var passwordResetResult = await UserManager + .ChangePasswordAsync( + user, + password, + newPassword); + + // + // Check Result ... + if (!passwordResetResult.Succeeded) + { + XException.ActionFailed.Throw(); + } + + // + // Prepare Message ... + var xMessage = GetPasswordChangedMessage( + lang, + user.Email, + returnUrl, + throwException: false + ); + + // + // Send Message ... + try + { + await MessageProvider.SendMailAsync(xMessage); + } + catch { } + } + + /// + /// Reset User Password + /// + /// specify destination language + /// an instance of XDevice + /// a token which approved user action + /// user's new password + /// redirection url + /// + public async Task ResetPassword( + string lang, + XDevice device, + string actionToken, + string newPassword, + string returnUrl + ) + { + // + // Validate Args ... + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty( + lang, + actionToken, + newPassword, + returnUrl) + .AddNotNull(device) + .ValidateGroupAsync(); + + // + // Get Related Token to Registration Hash and Validate it, + // then Parse XActionRequest Instance from Token ... + var request = await ValidateAndParseActionHash(actionToken); + + // + // Get User Identifier ... + var userSelectByParam = GetUserSelectByParam(request); + + // + // Validate and Retrieve User ... + var user = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: false, + checkCanLoginPolicies: true, + checkIsBanned: true); + + // + // Check Password Policies ... + await ValidatePasswordPolicies(user, newPassword); + + // + // Clean All User Tokens ... + await HandleCleanUserTokens(user); + + // + // Try to Remove User Password ... + var removePasswordResult = await UserManager.RemovePasswordAsync(user); + if (!removePasswordResult.Succeeded) + { + XException.ActionFailed.Throw(); + } + + // + // try to Add New Password ... + var passwordResetResult = await UserManager.AddPasswordAsync(user, newPassword); + if (!passwordResetResult.Succeeded) + { + XException.ActionFailed.Throw(); + } + + // + // Prepare Message ... + var xMessage = GetPasswordChangedMessage( + lang, + user.Email, + returnUrl); + + // + // Send Message ... + try + { + await MessageProvider.SendMailAsync(xMessage); + } + catch { } + } + #endregion + + // + #region Profile Image Actions ... + /// + /// Add Profile Image to a User + /// + /// a user identifier + /// an specific File to upload, IFormFile + /// an instance of XUserProfileDto + public async Task AddAvatar( + string userSelectByParam, + IFormFile file + ) + { + // + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: true, + checkCanLoginPolicies: false, + checkIsBanned: false, + ignoreDisabledUser: true, + exception: XException.NotFound.ToException() + ); + + // + // Handle Saving File and Attach it to User ... + var saveResult = await StorageProvider.HandleProfileImageSave(file); + + // + var xProfileImage = new XProfileImage + { + UserId = xUser.Id, + Name = saveResult.FileName, + Path = saveResult.FilePath, + Thumb = saveResult.Thmbnail, + ThumbPath = saveResult.ThmbnailPath, + CreationDate = DateTime.UtcNow + }; + + // + xUser.Avatars.Add(xProfileImage); + var updateResult = await UpdateUserAsync( + xUser, + checkCanLoginPolicies: false, + checkIsBanned: false + ); + + // + if (!updateResult.Succeeded) + { + XException.ActionFailed.Throw(); + } + + // + xUser.Avatar = xProfileImage.ThumbPath; + updateResult = await UpdateUserAsync( + xUser, + checkCanLoginPolicies: false, + checkIsBanned: false + ); + + // + if (!updateResult.Succeeded) + { + XException.ActionFailed.Throw(); + } + + // + var result = await GetUserProfileAsync(userSelectByParam, userSelectByParam); + return result; + } + + /// + /// Add a Collection of Profile Images to a User + /// + /// a user identifier + /// a collection of Files to upload, IFormFileCollection + /// an instance of XUserProfileDto + public async Task AddAvatars( + string userSelectByParam, + IFormFileCollection files + ) + { + // + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: true, + checkCanLoginPolicies: false, + checkIsBanned: false, + ignoreDisabledUser: true, + exception: XException.NotFound.ToException() + ); + + // + foreach (var file in files) + { + // + // Handle Saving File and Attach it to User ... + var saveResult = await StorageProvider.HandleProfileImageSave(file); + + // + var xProfileImage = new XProfileImage + { + UserId = xUser.Id, + Name = saveResult.FileName, + Path = saveResult.FilePath, + Thumb = saveResult.Thmbnail, + ThumbPath = saveResult.ThmbnailPath, + CreationDate = DateTime.UtcNow + }; + + // + xUser.Avatars.Add(xProfileImage); + } + + // + var updateResult = await UpdateUserAsync( + xUser, + checkCanLoginPolicies: false, + checkIsBanned: false + ); + + // + if (!updateResult.Succeeded) + { + XException.ActionFailed.Throw(); + } + + // + var result = await GetUserProfileAsync(userSelectByParam, userSelectByParam); + return result; + } + + /// + /// Set Specific Profie Image as Current Profile Image + /// + /// a user identifier + /// an integer which reperesent AvatarId to set as current Avatar + /// an instance of XUserProfileDto + public async Task SetAvatar( + string userSelectByParam, + int id + ) + { + // + ValidationProvider.NotEmpty(userSelectByParam); + + // + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: true, + checkCanLoginPolicies: false, + checkIsBanned: false, + ignoreDisabledUser: true, + exception: XException.NotFound.ToException() + ); + + // + var xProfileImage = xUser.Avatars.FirstOrDefault(pf => pf.Id == id); + if (xProfileImage.IsNull()) + { + XException.NotFound.Throw(); + } + + // + xUser.Avatar = xProfileImage.ThumbPath; + var updateResult = await UpdateUserAsync( + xUser, + checkCanLoginPolicies: false, + checkIsBanned: false + ); + + // + if (!updateResult.Succeeded) + { + XException.ActionFailed.Throw(); + } + + // + var result = await GetUserProfileAsync(userSelectByParam, userSelectByParam); + return result; + } + + /// + /// Remove a Collection of User's Profile Images + /// + /// a user identifier + /// a comma seperated list of avatarIds to remove + /// an instance of XUserProfileDto + public async Task RemoveAvatar( + string userSelectByParam, + ICollection ids + ) + { + // + ValidationProvider.NotEmpty(userSelectByParam); + + // + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: true, + checkCanLoginPolicies: false, + checkIsBanned: false, + ignoreDisabledUser: true, + exception: XException.NotFound.ToException() + ); + + // + var xProfileImages = xUser.Avatars + .Where(pf => ids.Contains(pf.Id)); + if (xProfileImages.HasChild()) + { + // + xUser.Avatars = xUser.Avatars + .Except(xProfileImages) + .ToList(); + + // + // Creat a List of Physical Files to Remove ... + var xFilePathsForRemove = xProfileImages.Select(pf => pf.Thumb).ToList(); + xFilePathsForRemove.AddRange(xProfileImages.Select(pf => pf.Name)); + + // + StorageProvider.DeleteFiles( + xFilePathsForRemove, + forceFileExists: false); + + // + var isCurrentProfileImage = xProfileImages.Any(pfi => pfi.ThumbPath == xUser.Avatar); + if (isCurrentProfileImage) + { + xUser.Avatar = ""; + } + + // + await UpdateUserAsync(xUser); + } + + // + var result = await GetUserProfileAsync(userSelectByParam, userSelectByParam); + return result; + } + #endregion + + // + #region Confirmations ... + /// + /// Confirm Registration + /// + /// specify destination language + /// an instance of XDevice + /// a token which approved user action + /// redirection url + /// + public async Task ConfirmRegistration( + string lang, + XDevice device, + string actionToken, + string returnUrl + ) + { + // + // Validate Args ... + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty( + lang, + actionToken, + returnUrl) + .AddNotNull(device) + .ValidateGroupAsync(); + + // + // Check Device Validation ... + await ValidateDeviceForActions(device); + + // + // Get Related Token to Registration Hash and Validate it, + // then Parse XActionRequest Instance from Token ... + var request = await ValidateAndParseActionHash(actionToken); + if (request.Action != XAction.Finish) + { + XException.ActionFailed.Throw(); + } + + // + // Extract UserSelectByParam from XActionRequest ... + var userSelectByParam = GetUserSelectByParam( + request, + forceNotNull: true); + + // + // Validate user and Retrieve it ... + var user = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: false, + checkCanLoginPolicies: false, + ignoreDisabledUser: true, + checkIsBanned: true); + + // + // Make User Enabled ... + var xMessage = GetRegistrationFinishedMessage( + lang, + user.Email, + returnUrl); + + // + user.IsEnable = true; + user.EmailConfirmed = true; + + // + await UpdateUserAsync( + user, + checkCanLoginPolicies: false, + checkIsBanned: false + ); + + // + // Remove Token ... + await RemoveTokenByHash(actionToken); + + // + // Remove All Prevoius Requests ... + await HandleRemoveExistsTokens(user.Id); + + // + await RemoveVerificationCodeRequestByDevice(device); + + // + // Send Mail ... + try + { + await MessageProvider.SendMailAsync(xMessage); + } + catch { } + } + + /// + /// Confirm Mobile Number + /// + /// specify destination language + /// an instance of XDevice + /// a token which approved user action + /// recieved verification code + /// an instance of XActionResponse + public async Task ConfirmMobileNumber( + string lang, + XDevice device, + string actionToken, + string verificationCode + ) + { + // + // Validate Args ... + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty(lang, actionToken, verificationCode) + .AddNotNull(device) + .ValidateGroupAsync(); + + // + // Check Device Validation ... + await ValidateDeviceForActions(device); + + // + // Get Related Token to Registration Hash and Validate it, + // then Parse XActionRequest Instance from Token ... + var request = await ValidateAndParseActionHash(actionToken); + ValidationProvider.MobileNumber(request.Context.MobileNumber); + + // + // Validate two Device Must Same ... + ValidateRequestAndGiveDevices(device, request.Context.Device); + + // + // Check XActionRequest Action Must be Request Mobile Validation ... + if (request.Action != XAction.RequestMobileVerificationCode) + { + XException.InvalidData.Throw(); + } + + // + // Extract UserSelectByParam from XActionRequest ... + var userSelectByParam = GetUserSelectByParam(request); + + // + // since device must Requested before, + // Retrieve XVerification instance by Verification Code from Db + // and Validate it ... + XVerificationRequest xVerificationRequest = await ValidateAndRetrieveVerificationRequest(verificationCode); + + // + // Check User Added To Db or Not ... + // var isUserExists = await IsUserExistsAsync(userSelectByParam); + var isAddedUser = !request.Context.UserId.IsNullOrEmpty(); + if (isAddedUser) + { + // + // Validate User Exists and Retireve User Object ... + var user = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: false, + checkCanLoginPolicies: false, + checkIsBanned: false); + + // + // Update Exists User's Mobile Confirmation ... + user.PhoneNumberConfirmed = true; + + // + // Save Changes to Db ... + await UpdateUserAsync(user, checkCanLoginPolicies: false, checkIsBanned: false); + + // + request.Context.UserId = user.Id; + } + + // + // Update Request Context MobileNumber Verification Code + // and it's Confirmation Status ... + request.Context.MobileVerificationCode = verificationCode; + request.Context.MobileVerified = true; + + // + // Prepare New Token Result , + // by Requesting an Action ... + var result = await ActionRequest( + lang, + device, + userSelectByParam, + XAction.ConfirmMobileNumber, + request.Context, + forceCheckUserExists: isAddedUser + ); + + // + // Remove Previous Token ... + await RemoveTokenByHash(actionToken); + + try + { + // + // Remove XVerificationRequest instance from Db ... + await RemoveVerificationRequest(verificationCode); + } + catch { } + + // + // Return Result ... + return result; + } + + /// + /// Confirm Email Address + /// + /// specify destination language + /// an instance of XDevice + /// a token which approved user action + /// recieved verification code + /// an instance of XActionResponse + public async Task ConfirmEmailAddress( + string lang, + XDevice device, + string actionToken, + string verificationCode + ) + { + // + // Validate Args ... + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty(lang, actionToken, verificationCode) + .AddNotNull(device) + .ValidateGroupAsync(); + + // + // Check Device Validation ... + await ValidateDeviceForActions(device); + + // + // Get Related Token to Registration Hash and Validate it, + // then Parse XActionRequest Instance from Token ... + var request = await ValidateAndParseActionHash(actionToken); + ValidationProvider.EmailAddress(request.Context.Email); + + // + // Validate two Device Must Same ... + ValidateRequestAndGiveDevices(device, request.Context.Device); + + // + // Check XActionRequest Action Must be Request Mobile Validation ... + if (request.Action != XAction.RequestEmailVerificationCode) + { + XException.InvalidData.Throw(); + } + + // + // Extract UserSelectByParam from XActionRequest ... + var userSelectByParam = GetUserSelectByParam(request); + + // + // since device must Requested before, + // Retrieve XVerification instance by Verification Code from Db + // and Validate it ... + XVerificationRequest xVerificationRequest = await ValidateAndRetrieveVerificationRequest(verificationCode); + + // + // Check User Added To Db or Not ... + var isAddedUser = !request.Context.UserId.IsNullOrEmpty(); + if (isAddedUser) + { + // + // Validate User Exists and Retireve User Object ... + var user = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: false, + checkCanLoginPolicies: false, + checkIsBanned: false); + + // + // Update Exists User's Email Confirmation ... + user.EmailConfirmed = true; + + // + // Save Changes to Db ... + await UpdateUserAsync(user, checkCanLoginPolicies: false, checkIsBanned: false); + + // + request.Context.UserId = user.Id; + } + + // + // Update Request Context Email Address Verification Code + // and it's Confirmation Status ... + request.Context.EmailVerificationCode = verificationCode; + request.Context.EmailVerified = true; + + // + // Prepare New Token Result , + // by Requesting an Action ... + var result = await ActionRequest( + lang, + device, + userSelectByParam, + XAction.ConfirmEmailAddress, + request.Context, + forceCheckUserExists: isAddedUser + ); + + // + // Remove Previous Token ... + await RemoveTokenByHash(actionToken); + + // + // Remove XVerificationRequest instance from Db ... + await RemoveVerificationRequest(verificationCode); + + // + // Return Result ... + return result; + } + #endregion + } +} \ No newline at end of file diff --git a/Providers/XIdentityManager/XIdentityManager+Query.cs b/Providers/XIdentityManager/XIdentityManager+Query.cs new file mode 100644 index 0000000..915cc09 --- /dev/null +++ b/Providers/XIdentityManager/XIdentityManager+Query.cs @@ -0,0 +1,110 @@ +using System.Linq; +using System.Threading.Tasks; +using xCommons.Extensions; +using xExceptions.Constants; +using xDataService.Extensions; +using xIds.Extensions; +using xModels.Dtos; +using xIdentityModels.Extensions; + +namespace xIds.Providers +{ + public partial class XIdentityManager + { + /// + /// Retrieve Users as Query Model for Query Service ... + /// + /// + /// + /// + /// + /// + public async Task> QueryUsers( + string requestedUserSelectByParam, + XQuery query, + string role = null, + bool forceRole = false + ) + { + // + // Validation ... + if (query.IsNull()) + { + XException.InvalidArgs.Throw(); + } + + // + // Normalize ... + query = query.NormalizeQuery(DataConfiguration.ToXDataServiceConfig()); + + // + // Since in Resourceable Entities we have to Search on Locales + // we Must Implement Senario Custom ... + var items = GetUsersDbSet() + .ToList() + .Where(u => !u.ContainsUserSelectByParam(requestedUserSelectByParam)) + .ToList() + .AsEnumerable(); + + // + var totalItemsCount = items.Count(); + + // + // Apply Filter ... + if (!query.Filter.IsNullOrEmpty()) + { + // + items = items + .ApplyFilter(query.Filter); + } + int filteredItemsCount = items.Count(); + + // + // Count Pages ... + var totalPagesCount = query.CountPages(totalItemsCount); + var filteredPagesCount = query.CountPages(filteredItemsCount); + + // + // Apply Paging and Sorting ... + if (totalItemsCount > 0 && + filteredItemsCount > 0) + { + // + // Apply Sorting ... + items = items + .ToList() + .ApplySorting( + query.SortBy, + query.IsAscending + ); + + // + // Apply Paging ... + items = items + .ToList() + .ApplyPaging( + query.Page, + query.PageSize + ); + } + + // + // Generate Result Object ... + // Query = query, + var result = new XQueryResult + { + Items = items + .Select(u => u.Id), + Page = query.Page, + PageSize = query.PageSize, + TotalPages = totalPagesCount, + TotalItems = totalItemsCount, + TotalFilteredPages = filteredPagesCount, + TotalFilteredItems = filteredItemsCount + }; + + // + return result; + } + } +} \ No newline at end of file diff --git a/Providers/XIdentityManager/XIdentityManager+Registration.cs b/Providers/XIdentityManager/XIdentityManager+Registration.cs new file mode 100644 index 0000000..f399c03 --- /dev/null +++ b/Providers/XIdentityManager/XIdentityManager+Registration.cs @@ -0,0 +1,700 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using xCommons.Extensions; +using xExceptions.Constants; +using xIdentityModels; +using xIdentityModels.Constants; +using xIdentityModels.Models; +using xIdentityModels.Navigations; + +namespace xIds.Providers +{ + public partial class XIdentityManager + { + // + #region Registration Actions ... + /// + /// Invite a User to Register on Dashboard + /// + /// specify destination language + /// an instance of XDevice + /// which email address is going to invite + /// return url for invitation user to redirect + /// an instance of XActionResponse + public async Task InviteUser( + string lang, + XDevice device, + string email, + string returnUrl + ) + { + // + // Validate Args ... + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty( + lang, + email, + returnUrl + ) + .AddNotNull(device) + .AddEmailAddress(email) + .ValidateGroupAsync(); + + // + // Check Email is Unique ... + await ValidateUserNotExists(email); + + // + // Get Request Result ... + var result = await ActionRequest( + lang, + device, + email, + XAction.Invite, + forceCheckContext: false, + forceCheckUserExists: false); + + // + // Prepare Propper Message ... + var xMessage = GetUserInviteMessage(lang, email, result.Token, returnUrl); + + // + // Send Message ... + try + { + await MessageProvider.SendMailAsync(xMessage); + } + catch { } + + // + // Send Result ... + return result; + } + + /// + /// Recieve some Basic Informations and Start Registration Proccess + /// if they Valid + /// + /// Registration Proccess Starts with Invoking this Action + /// + /// specify destination language + /// an instance of XDevice + /// optional, if user invited, this is the invitation token + /// user's FirstName + /// user's LastName + /// user's dob date + /// user's Mobile Number + /// user's Email address + /// an instance of XActionResponse + public async Task RequestRegistration( + string lang, + XDevice device, + string invitationHash, + string firstName, + string lastName, + DateTime dateOfBirth, + string mobileNumber = null, + string emailAddress = null + ) + { + // + // Validate Args ... + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty( + lang, + firstName, + lastName + ) + .AddNotNull( + device, + dateOfBirth + ) + .ValidateGroupAsync(); + + // + // Validate Mobile Number if Exists ... + if (!mobileNumber.IsNullOrEmpty()) + { + // + ValidationProvider.MobileNumber(mobileNumber); + + // + // Check Mobile is Uniqsue ... + await ValidateUserNotExists(mobileNumber); + } + + // + // Validate Email Address if Exists ... + if (!emailAddress.IsNullOrEmpty()) + { + // + ValidationProvider.EmailAddress(emailAddress); + + // + // Check Email is Unique ... + await ValidateUserNotExists(emailAddress); + } + + // + // Get Related Token and Validate it and + // Check Related Configurations ... + await ValidateInvitationHash(invitationHash); + + // + // Check Device Validation ... + await ValidateDeviceForActions(device); + + // + // Validate DateOfBirth ... + ValidateDateOfBirth(dateOfBirth); + + // + // Define Empty Objects for Using ... + var userSelectByParam = ""; + var context = new XActionRequestContext(); + // + context.Lang = lang; + context.Device = device; + context.LastName = lastName; + context.FirstName = firstName; + context.DateOfBirth = dateOfBirth; + context.MobileNumber = mobileNumber ?? ""; + context.Email = !emailAddress.IsNullOrEmpty() ? emailAddress.ToNormalString() : ""; + + // + userSelectByParam = GetUserSelectByParam(context); + + // + // Token Data Parsing ... + if (!invitationHash.IsNullOrEmpty()) + { + // + // Get Related Token, Validate it and Parse Contains + // XAction Request and Validate the Request itself and + // Return it ... + var inviteRequest = await ValidateAndParseActionHash(invitationHash); + + // + // Extract User Select By Param and Type ... + userSelectByParam = GetUserSelectByParam(inviteRequest); + var userSelectBy = GetUserSelectByType(userSelectByParam); + + // + // Check Invitation Only done with Email Address ... + if (userSelectBy != XUserSelectBy.Email) + { + XException.InvalidData.Throw(); + } + + // + // Check Invited Email and Given Email to be Same ... + if (emailAddress.IsNullOrEmpty() || + (!emailAddress.IsNullOrEmpty() && + !emailAddress.IsValidEmail())) + { + XException.InvalidEmailAddress.Throw(); + } + var isSameEmail = inviteRequest.Context.Email.ToNormalString() == emailAddress.ToNormalString(); + if (!isSameEmail) + { + XException.EmailsSame.Throw(); + } + + // + // Update Context Email for Request Action ... + context.Email = userSelectByParam.ToNormalString(); + context.EmailVerified = true; + } + + // + var result = await ActionRequest( + lang, + device, + userSelectByParam, + XAction.Registration, + context, + forceCheckUserExists: false + ); + + // + // Remove Invitation Token ... + if (!invitationHash.IsNullOrEmpty()) + { + await RemoveTokenByHash(invitationHash); + } + + // + // Return Result ... + return result; + } + + /// + /// Add User Account Info + /// + /// specify destination language + /// an instance of XDevice + /// a token which approved user action + /// user name + /// assigne password + /// an instance of XActionResponse + public async Task AddAcountInfo( + string lang, + XDevice device, + string actionToken, + string userName, + string password + ) + { + // + // Validate Args ... + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty( + lang, + actionToken, + userName, + password) + .AddNotNull(device) + .ValidateGroupAsync(); + + // + // Check Device Validation ... + await ValidateDeviceForActions(device); + + // + // Validate a UserSelector is Available or not ... + await ValidateCanRegister(userName); + + // + // Check UserName is Unique ... + await ValidateUserNotExists(userName); + + // + // Get Related Token to Registration Hash and Validate it, + // then Parse XActionRequest Instance from Token ... + var request = await ValidateAndParseActionHash(actionToken); + + // + // Validate Requeired Data for Register User must be in Action Context ... + await ValidationProvider + .GroupValidationBuilder() + .AddNotNull( + request.Context, + request.Context.Device, + request.Context.DateOfBirth) + .AddNotEmpty( + request.Context.Email, + request.Context.MobileNumber, + request.Context.FirstName, + request.Context.LastName) + .AddEmailAddress(request.Context.Email) + .AddMobileNumber(request.Context.MobileNumber) + .ValidateGroupAsync(); + + // + // Validate two Device Must Same ... + ValidateRequestAndGivenDevices(device, request.Context.Device); + + // + // Extract UserSelectByParam from XActionRequest ... + var userSelectByParam = GetUserSelectByParam(request); + + // + // Validate Request Pointer User doesn't Exists ... + await ValidateUserNotExists(userSelectByParam); + + // + // Create an Empty Instance of XUser ... + var xUser = new XUser(); + + // + // Start Filling User Fields ... + xUser.UserName = userName; + xUser.Email = request.Context.Email; + xUser.PhoneNumber = request.Context.MobileNumber; + + // + xUser.FirstName = request.Context.FirstName; + xUser.LastName = request.Context.LastName; + xUser.DateOfBirth = request.Context.DateOfBirth; + + // + xUser.EmailConfirmed = request.Context.EmailVerified; + xUser.PhoneNumberConfirmed = request.Context.MobileVerified; + + // + xUser.CreationDate = DateTime.UtcNow; + xUser.LastLogin = null; + + // + #region Handling New User IsEnable State ... + // + // By Default New Users will Enables ... + xUser.IsEnable = true; + xUser.IsBanned = false; + + // + // Check Auto Enable Users ... + if (Configuration.RequireRegistrationConfirm) + { + xUser.IsEnable = false; + } + + // + // Check Auto Confirm Email Users ... + if (Configuration.AutoConfirmNewUsersEmail) + { + xUser.EmailConfirmed = true; + } + #endregion + + // + // Check Auto Confirm Phone Number Users ... + if (Configuration.AutoConfirmNewUsersPhoneNumber) + { + xUser.PhoneNumberConfirmed = true; + } + + // + // Validating given Password ... + await ValidatePasswordPolicies(xUser, password); + + // + // Now add new User to Database ... + var createUserResult = await CreateUserAsync(xUser, password); + if (!createUserResult.Succeeded) + { + XException.ActionFailed.Throw(); + } + + // + // Update Request UserId Value ... + request.Context.UserId = xUser.Id; + request.Context.UserName = xUser.UserName; + + // + #region Handling New User Role ... + // + // Now Try to Assign Default User role ... + var isContainsDefaultUserRole = !Configuration.NewUsersRole.IsNullOrEmpty(); + if (isContainsDefaultUserRole) + { + // + var newUserRoleName = Configuration.NewUsersRole; + var isNewUserRoleExists = await IsRoleExistsAsync(newUserRoleName); + // + // Make Sure New User role Exists ... + if (!isNewUserRoleExists) + { + // + // Create Default Roles ... + await CreateIdentityRoles(); + } + + // + // Assign User to Role ... + var assignNewUserToRoleResult = await AddUserToRoleAsync(xUser, newUserRoleName); + if (!assignNewUserToRoleResult.Succeeded) + { + // + // Delete User if Role Assignment Faild ... + await UserManager.DeleteAsync(xUser); + + // + // Thrown ActionFailed Error ... + XException.ActionFailed.Throw(); + } + } + #endregion + + // + #region Handle Profile Image ... + // Save and Attach Profile Image to User if it's Added Before ... + if (!request.Context.UserId.IsNullOrEmpty() && + !request.Context.Thubmnail.IsNull()) + { + // + xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: true, + checkCanLoginPolicies: false, + checkIsBanned: false, + ignoreDisabledUser: true, + exception: XException.NotFound.ToException() + ); + + // + // Handle Saving File and Attach it to User ... + var saveResult = await StorageProvider + .HandleProfileImageSave(request.Context.Thubmnail); + + // + var xProfileImage = new XProfileImage + { + UserId = request.Context.UserId, + Name = saveResult.FileName, + Path = saveResult.FilePath, + Thumb = saveResult.Thmbnail, + ThumbPath = saveResult.ThmbnailPath + }; + + // + xUser.Avatars.Add(xProfileImage); + var updateResult = await UpdateUserAsync( + xUser, + checkCanLoginPolicies: false, + checkIsBanned: false + ); + + // + if (updateResult.Succeeded) + { + // + xUser.Avatar = xProfileImage.ThumbPath; + await UpdateUserAsync( + xUser, + checkCanLoginPolicies: false, + checkIsBanned: false + ); + } + } + #endregion + + // + // Handle Attaching User device ... + var xUserDevice = await AddUserRelatedDevice( + userSelectByParam, + device, + checkCanLoginPolicies: false); + + // + // Prepare New Token Result Resource, + // by Requesting an Action ... + var result = await ActionRequest( + lang, + device, + userSelectByParam, + XAction.Finish, + request.Context, + forceCheckUserExists: false + ); + + // + // Remove Previous Token ... + await RemoveTokenByHash(actionToken); + + // + // Return Result ... + return result; + } + + /// + /// Attach Profile Image + /// + /// a token which approved user action + /// an instance of IFormFile for user's Avatar + /// an instance of XActionResponse + public async Task AttachProfileImage( + string actionToken, + IFormFile file + ) + { + // + // Validate Args ... + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty(actionToken) + .AddNotNull(file) + .ValidateGroupAsync(); + + // + // Get Related Token to Registration Hash and Validate it, + // then Parse XActionRequest Instance from Token ... + var request = await ValidateAndParseActionHash(actionToken); + + // + // Extract UserSelectByParam from XActionRequest ... + var userSelectByParam = GetUserSelectByParam(request); + if (userSelectByParam.IsNullOrEmpty()) + { + XException.InvalidData.Throw(); + } + + // + // Validate Given FormFile as Profile Image ... + StorageProvider.ValidateProfileImage(file); + + // + request.Context.Thubmnail = file; + + // + // Save and Attach Profile Image to User if it's Added Before ... + if (!request.Context.UserId.IsNullOrEmpty()) + { + // + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: true, + checkCanLoginPolicies: false, + checkIsBanned: false, + ignoreDisabledUser: true, + exception: XException.NotFound.ToException() + ); + + // + // Handle Saving File and Attach it to User ... + var saveResult = await StorageProvider.HandleProfileImageSave(file); + + // + var xProfileImage = new XProfileImage + { + UserId = request.Context.UserId, + Name = saveResult.FileName, + Path = saveResult.FilePath, + Thumb = saveResult.Thmbnail, + ThumbPath = saveResult.ThmbnailPath + }; + + // + xUser.Avatars.Add(xProfileImage); + var updateResult = await UpdateUserAsync( + xUser, + checkCanLoginPolicies: false, + checkIsBanned: false + ); + + // + if (updateResult.Succeeded) + { + // + xUser.Avatar = xProfileImage.ThumbPath; + await UpdateUserAsync( + xUser, + checkCanLoginPolicies: false, + checkIsBanned: false + ); + } + } + + // + // Remove Invitation Token ... + if (!actionToken.IsNullOrEmpty()) + { + await RemoveTokenByHash(actionToken); + } + + // + var result = await ActionRequest( + request.Context.Lang, + request.Context.Device, + userSelectByParam, + XAction.ProfileAction, + request.Context, + forceCheckUserExists: false + ); + + // + // Return Result ... + return result; + } + + /// + /// Finishing Registration + /// + /// specify destination language + /// an instance of XDevice + /// user identifier + /// assigne password + /// return url for invitation user to redirect + /// + public async Task FinishRegistration( + string lang, + XDevice device, + string userSelectByParam, + string password, + string returnUrl + ) + { + // + // Validate Args ... + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty(lang, userSelectByParam, password, returnUrl) + .AddNotNull(device) + .AddUrl(returnUrl) + .ValidateGroupAsync(); + + // + // Validate User Exists And Retrieve it ... + var xUser = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: false, + checkCanLoginPolicies: false, + checkIsBanned: false, + ignoreDisabledUser: true, + forceAdmin: false + ); + + // + #region Handling New User Email Notification Senario ... + // + // Here we Check if User is Enable and Approved Email + // send RegisteredMsg and if User is not Enable and + // Verify Registration value in Configuration is True + // send Verify Registration mail to user and handle it ... + if (Configuration.RequireRegistrationConfirm) + { + // + if (xUser.IsEnable) + { + // + // Send Registration Finished Message ... + // since in this step we need to prevent errors from + // going forward. add this try catch here ... + try + { + // + var xMessage = GetRegistrationFinishedMessage(lang, xUser.Email, returnUrl); + + // + await MessageProvider.SendMailAsync(xMessage); + } + catch { } + } + else + { + // + // Send Registration Confirm Message ... + // since in this step we need to prevent errors from + // going forward. add this try catch here ... + try + { + // + var request = await ActionRequest( + lang, + device, + userSelectByParam, + XAction.Finish, + forceCheckContext: false, + forceCheckUserExists: true); + + // + var xMessage = GetRegistrationConfirmMessage(lang, xUser.Email, request.Token, returnUrl); + + // + await MessageProvider.SendMailAsync(xMessage); + } + catch { } + } + } + + // // + // await RemoveTokenByDevice (device); + #endregion + } + #endregion + } +} \ No newline at end of file diff --git a/Providers/XIdentityManager/XIdentityManager+Request.cs b/Providers/XIdentityManager/XIdentityManager+Request.cs new file mode 100644 index 0000000..e4da3b1 --- /dev/null +++ b/Providers/XIdentityManager/XIdentityManager+Request.cs @@ -0,0 +1,722 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using xCommons.Extensions; +using xExceptions.Constants; +using xIdentityModels.Constants; +using xIdentityModels.Extensions; +using xIdentityModels.Models; +using xIdentityModels.Navigations; + +namespace xIds.Providers +{ + public partial class XIdentityManager + { + // + #region Action Handlers ... + /// + /// Request an Action + /// + /// specify destination language + /// an instance of XDevice + /// specified user's identifier + /// a member of XAction which represent Action Step + /// an instance of XActionRequestContext which provides required informations for specified step + /// specify checking context, default is false + /// specify check user exists, default is true + /// specify check user and device relation, default is false + /// specifies force renew Action Token if it's expired, default is false + /// an instance of XActionResponse + private async Task ActionRequest( + string lang, + XDevice device, + string userSelectByParam, + XAction step, + XActionRequestContext context = null, + bool forceCheckContext = false, + bool forceCheckUserExists = true, + bool forceCheckUserDevice = false, + bool forceRenewToken = false + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(lang); + ValidationProvider.NotNull(device); + + // + // Check Device not Banned ... + await ValidateDeviceForActions(device); + + // + // Check User Exists and Device Relation ... + if (forceCheckUserExists) + { + if (forceCheckUserDevice) + { + // + if (context.Device != null && + !device.IsSameAs(context.Device)) + { + XException.InvalidDevice.Throw(); + } + + // + await ValidateUserAndDeviceRelation(userSelectByParam, device); + } + else + { + await ValidateUserExistsAsync(userSelectByParam); + } + } + + // + XToken xToken = null; + XActionResponse result = null; + XActionRequestToken xRequest = null; + + // + var isExists = false; + var forceUserSelectByParamNotEmpty = true; + switch (step) + { + case XAction.Registration: + // + forceUserSelectByParamNotEmpty = false; + isExists = await IsTokenExistsByDeviceAndType(device, step); + + // + if (isExists) + { + xToken = await ValidateAndRetieveTokenByDeviceAndType(device, step); + } + break; + + default: + // + isExists = await IsTokenExistsByUserAndType(userSelectByParam, step); + + // + if (isExists) + { + xToken = await ValidateAndRetieveTokenByUserAndType(userSelectByParam, step); + } + break; + } + + // + if (isExists) + { + // + var isValidToken = IsValidToken(xToken.Token); + if (!isValidToken && !forceRenewToken) + { + switch (step) + { + case XAction.Registration: + case XAction.Invite: + forceRenewToken = true; + break; + + default: + throw XException.InvalidToken.ToException(); + } + } + + // + if (!isValidToken && forceRenewToken) + { + await ValidateAndHandleToken(xToken.Token); + return await ActionRequest( + lang, + device, + userSelectByParam, + step, + context, + forceCheckContext, + forceCheckUserExists, + forceCheckUserDevice, + forceRenewToken); + } + + // + xRequest = ValidateAndParseActionToken(xToken.Token); + + // + // Check Exists Context ... + if (forceCheckContext) + { + // + var isSameContext = xRequest.Context.IsSameAs(context); + if (!isSameContext) + { + XException.InvalidData.Throw(); + } + } + } + else + { + // + if (context != null) + { + // + var contextUserSelectByParam = GetUserSelectByParam( + context, + false, + new List { + XUserSelectBy.ID, + XUserSelectBy.Email, + XUserSelectBy.MobileNumber + }); + var contextLang = context.Lang; + var contextDevice = context.Device; + + // + // prevent from issues on Registration Request ... + if (forceUserSelectByParamNotEmpty) + { + var userSelectByType = GetUserSelectByType(userSelectByParam); + var contextUserSelectByType = GetUserSelectByType(contextUserSelectByParam); + if (contextUserSelectByType != userSelectByType) + { + // + var excludesList = GenerateUserSelectByExcludes(new List { userSelectByType }); + + // + contextUserSelectByParam = GetUserSelectByParam(context, false, excludesList); + } + } + + // + // Fix Device Not Same on Invitation and Request + if (step == XAction.Registration) + { + // + context.Device = device; + contextDevice = context.Device; + } + + // + // Validate Context Data must be Same as Request Data ... + if ((!contextUserSelectByParam.IsNullOrEmpty() && + contextUserSelectByParam != userSelectByParam) || + (!contextLang.IsNullOrEmpty() && + contextLang != lang) || + (contextDevice != null && + !contextDevice.IsSameAs(device))) + { + XException.InvalidData.Throw(); + } + } + else + { + // + context = new XActionRequestContext + { + Lang = lang, + Device = device + }; + + // + // Add Content ... + var userSelectByType = GetUserSelectByType( + userSelectByParam, + forceUserSelectByParamNotEmpty); + switch (userSelectByType) + { + case XUserSelectBy.MobileNumber: + context.MobileNumber = userSelectByParam; + break; + + case XUserSelectBy.Email: + context.Email = userSelectByParam; + break; + + case XUserSelectBy.Username: + context.UserName = userSelectByParam; + break; + + case XUserSelectBy.ID: + context.UserId = userSelectByParam; + break; + + case XUserSelectBy.NotSpecified: + default: + if (forceCheckUserExists) + { + XException.InvalidArgs.Throw(); + } + break; + } + } + + // + // Generate Registration Request ... + xRequest = new XActionRequestToken + { + Action = step, + Context = context + }; + + // + // Add User Id if Exists to Context ... + var isUserExists = await IsUserExistsAsync(userSelectByParam); + if (isUserExists) + { + // + var user = await GetUserAsync(userSelectByParam); + if (user != null) + { + xRequest.Context.UserId = user.Id; + } + } + + // + xRequest.Prepare(Configuration.IdentitySecretKey); + + // + xToken = await HashRequest( + xRequest, + forceUserSelectByParamNotEmpty); + } + + // + // When XToken Exists Passed XToken ... + result = GetActionResultResponse(xToken); + + // + if (result == null) + { + XException.ActionFailed.Throw(); + } + + // + return result; + } + + /// + /// Request a Mobile Verification Code + /// + /// specify destination language + /// an instance of XDevice + /// a token which approved user action + /// the mobile number which is going to request a verification code + /// check mobile number is in use or not, default is true + /// an instance of XActionResponse + private async Task RequestMobileVerificationCode( + string lang, + XDevice device, + string actionToken, + string mobileNumber, + bool checkMobileInUse = true + ) + { + // + // Validate Args ... + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty(lang, actionToken, mobileNumber) + .AddMobileNumber(mobileNumber) + .AddNotNull(device) + .ValidateGroupAsync(); + + // + // Check Device Validation ... + await ValidateDeviceForActions(device); + + // + // Check Mobile is Unique ... + if (checkMobileInUse) + { + var isUserExists = await IsUserExistsAsync(mobileNumber); + if (isUserExists) + { + // + var xUser = await GetUserAsync(mobileNumber); + + // + if (xUser.PhoneNumberConfirmed) + { + XException.MobileInUsed.Throw(); + } + } + } + + // + // Get Related Token to Registration Hash and Validate it, + // then Parse XActionRequest Instance from Token ... + var request = await ValidateAndParseActionHash(actionToken); + + // + // Validate two Device Must Same ... + ValidateRequestAndGiveDevices(device, request.Context.Device); + + // + // Extract UserSelectByParam from XActionRequest ... + var userSelectByParam = GetUserSelectByParam( + request, + forceNotNull: false, + excludes: new List { + XUserSelectBy.ID, + XUserSelectBy.Email, + XUserSelectBy.MobileNumber + }); + + // + // if it's null, means user doesn't have Invitation Token + // and there is no User Selector ... + if (userSelectByParam.IsNullOrEmpty()) + { + // + // Since Mobile is Unique so it can act as a + // UserSelector ... + userSelectByParam = mobileNumber; + } + + // + // Define empty Verification Code + // and Empty Verification Request ... + var verificationCode = ""; + XVerificationRequest xVerificationRequest = null; + + // + // Check Device Request Verification Code before or Not ... + var isDeviceRequested = await IsDeviceRequestedForVerificationCodeBefor(device); + if (isDeviceRequested) + { + // + // if Requested before, Retrieve XVerification instance by Device from Db + // and Validate it ... + xVerificationRequest = await ValidateAndRetrieveVerificationRequest(device); + + // + if (xVerificationRequest.VerificationCode.IsNullOrEmpty()) + { + // + verificationCode = GenerateRandom().ToString(); + xVerificationRequest.VerificationCode = verificationCode; + + // + await RemoveVerificationCodeRequestByDevice( + device, + saveChanges: false + ); + await AddVerificationRequest( + xVerificationRequest, + saveChanges: false + ); + + // + await DbContext.SaveChangesAsync(); + } + + // + // if XVerificationRequest is Valid, it must Contain an Alive Verification Code + // so retrieve it and assign it ... + verificationCode = xVerificationRequest.VerificationCode; + } + else + { + // + verificationCode = GenerateRandom().ToString(); + } + + // + // if it's First Verification Request of given Device + // Generate a new Verification Code ... + if (verificationCode.IsNullOrEmpty()) + { + verificationCode = GenerateRandom().ToString(); + } + + // + // Check Context Mobile and given Mobile ... + if (!request.Context.MobileNumber.IsNullOrEmpty() && + mobileNumber != request.Context.MobileNumber) + { + XException.InvalidMobileNumber.Throw(); + } + + // + // Check User Added To Db or Not ... + var isAddedUser = !request.Context.UserId.IsNullOrEmpty(); + if (isAddedUser) + { + // + // Validate User Exists and Retireve User Object ... + var user = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: false, + checkCanLoginPolicies: false, + checkIsBanned: false); + + // + // Check Mobiles Same ... + if (user.PhoneNumber == mobileNumber && + user.PhoneNumberConfirmed) + { + XException.MobilesSame.Throw(); + } + + // + // Update User Object's Mobile Number and + // Confirmation Status ... + user.PhoneNumber = mobileNumber; + user.PhoneNumberConfirmed = false; + + // + // Save Changes to Db ... + await UpdateUserAsync(user, checkCanLoginPolicies: false, checkIsBanned: false); + } + + // + // Prepare Verification Request by Checking Next Tries, + // Device Status, and etc, Update or Add XVerificationRequest to db + // and Retrieve the added Entity ... + xVerificationRequest = await HandleVerificationRequestPreparation(verificationCode, request); + + // + // Update Request Context MobileNumber + // and it's Confirmation Status ... + request.Context.MobileNumber = mobileNumber; + request.Context.MobileVerified = false; + + // + // Remove Previous Token ... + await RemoveTokenByHash(actionToken); + + // + // Prepare New Token Result Resource, + // by Requesting an Action ... + var result = await ActionRequest( + lang, + device, + userSelectByParam, + XAction.RequestMobileVerificationCode, + request.Context, + forceCheckUserExists: isAddedUser + ); + + // + // Prepare Message ... + var xMessage = GetVerificationCodeMessage(lang, mobileNumber, verificationCode); + + // + // Send Verification Code Message to User Using SMS Provider ... + await MessageProvider.SendSmsAsync(xMessage, throwException: false); + + // + // Return Result ... + return result; + } + + /// + /// Request a Email Verification Code + /// + /// specify destination language + /// an instance of XDevice + /// a token which approved user action + /// the email address which is going to request a verification code + /// check email address is in use or not, default is true + /// an instance of XActionResponse + public async Task RequestEmailVerificationCode( + string lang, + XDevice device, + string actionToken, + string emailAddress, + bool checkEmailInUse = true + ) + { + // + // Validate Args ... + await ValidationProvider + .GroupValidationBuilder() + .AddNotEmpty(lang, actionToken, emailAddress) + .AddEmailAddress(emailAddress) + .AddNotNull(device) + .ValidateGroupAsync(); + + // + // Check Device Validation ... + await ValidateDeviceForActions(device); + + // + // Check Email is Unique ... + if (checkEmailInUse) + { + var isUserExists = await IsUserExistsAsync(emailAddress); + if (isUserExists) + { + // + var xUser = await GetUserAsync(emailAddress); + + // + if (xUser.EmailConfirmed) + { + XException.EmailInUsed.Throw(); + } + } + } + + // + // Get Related Token to Registration Hash and Validate it, + // then Parse XActionRequest Instance from Token ... + var request = await ValidateAndParseActionHash(actionToken); + + // + // Validate two Device Must Same ... + ValidateRequestAndGiveDevices(device, request.Context.Device); + + // + // Extract UserSelectByParam from XActionRequest ... + var userSelectByParam = GetUserSelectByParam( + request, + forceNotNull: false, + excludes: new List { + XUserSelectBy.ID, + XUserSelectBy.Email, + XUserSelectBy.MobileNumber + }); + + // + // if it's null, means user doesn't have Invitation Token + // and there is no User Selector ... + if (userSelectByParam.IsNullOrEmpty()) + { + // + // Since Email is Unique so it can act as a + // UserSelector ... + userSelectByParam = emailAddress; + } + + // + // Define empty Verification Code + // and Empty Verification Request ... + var verificationCode = ""; + XVerificationRequest xVerificationRequest = null; + + // + // Check Device Request Verification Code before or Not ... + var isDeviceRequested = await IsDeviceRequestedForVerificationCodeBefor(device); + if (isDeviceRequested) + { + // + // if Requested before, Retrieve XVerification instance by Device from Db + // and Validate it ... + xVerificationRequest = await ValidateAndRetrieveVerificationRequest(device); + + // + if (xVerificationRequest.VerificationCode.IsNullOrEmpty()) + { + // + verificationCode = GenerateRandom().ToString(); + xVerificationRequest.VerificationCode = verificationCode; + + // + await RemoveVerificationCodeRequestByDevice( + device, + saveChanges: false + ); + await AddVerificationRequest( + xVerificationRequest, + saveChanges: true + ); + } + + // + // if XVerificationRequest is Valid, it must Contain an Alive Verification Code + // so retrieve it and assign it ... + verificationCode = xVerificationRequest.VerificationCode; + } + else + { + // + verificationCode = GenerateRandom().ToString(); + } + + // + // Check Context Email and given Email ... + if (!request.Context.Email.IsNullOrEmpty() && + emailAddress != request.Context.Email) + { + XException.InvalidEmailAddress.Throw(); + } + + // + // Check User Added To Db or Not ... + var isAddedUser = !request.Context.UserId.IsNullOrEmpty(); + if (isAddedUser) + { + // + // Validate User Exists and Retireve User Object ... + var user = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: false, + checkCanLoginPolicies: false, + checkIsBanned: false); + + // + // Check Email Same ... + if (user.Email == emailAddress && + user.EmailConfirmed) + { + XException.MobilesSame.Throw(); + } + + // + // Update User Object's Email and + // Confirmation Status ... + user.Email = emailAddress; + user.EmailConfirmed = false; + + // + // Save Changes to Db ... + await UpdateUserAsync( + user, + checkCanLoginPolicies: false, + checkIsBanned: false + ); + } + + // + // Prepare Verification Request by Checking Next Tries, + // Device Status, and etc, Update or Add XVerificationRequest to db + // and Retrieve the added Entity ... + xVerificationRequest = await HandleVerificationRequestPreparation(verificationCode, request); + + // + // Update Request Context Email + // and it's Confirmation Status ... + request.Context.Email = emailAddress; + request.Context.EmailVerified = false; + + // + // Prepare New Token Result Resource, + // by Requesting an Action ... + var result = await ActionRequest( + lang, + device, + userSelectByParam, + XAction.RequestEmailVerificationCode, + request.Context, + forceCheckUserExists: isAddedUser + ); + + // + // Remove Hash ... + await RemoveTokenByHash(actionToken); + + // + // Prepare Message ... + var xMessage = GetVerificationCodeMessage(lang, emailAddress, verificationCode); + + // + // Send Verification Code Message to User Using Mail Provider ... + await MessageProvider.SendMailAsync(xMessage, throwException: false); + + // + // Return Result ... + return result; + } + #endregion + } +} \ No newline at end of file diff --git a/Providers/XIdentityManager/XIdentityManager+Role.cs b/Providers/XIdentityManager/XIdentityManager+Role.cs new file mode 100644 index 0000000..deb765f --- /dev/null +++ b/Providers/XIdentityManager/XIdentityManager+Role.cs @@ -0,0 +1,273 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using xCommons.Extensions; +using xExceptions.Constants; +using xIdentityModels; + +namespace xIds.Providers +{ + public partial class XIdentityManager + { + // + #region Role Actions ... + /// + /// Check a Role exists or not + /// + /// role name + /// a boolean value + public async Task IsRoleExistsAsync( + string roleName + ) + { + // + if (roleName.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + return await RoleManager.RoleExistsAsync(roleName); + } + + /// + /// Create a new Role + /// + /// role name + /// an instance of IdentityResult + public async Task CreateRoleAsync( + string roleName + ) + { + // + var isExists = await RoleManager + .RoleExistsAsync(roleName); + if (isExists) + { + XException.Duplicate.Throw(); + } + + // + var result = await RoleManager + .CreateAsync(new IdentityRole(roleName)); + + // + return result; + } + + /// + /// Get a Role + /// + /// role name + /// an instance of IdentityRole + public async Task GetRoleAsync( + string roleName + ) + { + // + var isExists = await IsRoleExistsAsync(roleName); + if (!isExists) + { + XException.NotFound.Throw(); + } + + // + var result = await RoleManager + .FindByNameAsync(roleName); + + // + return result; + } + + /// + /// Remove a User From Role + /// + /// an instance of XUser + /// role name + /// an instance of IdentityResult + public async Task RemoveFromRoleAsync( + XUser user, + string roleName + ) + { + return await UserManager.RemoveFromRoleAsync(user, roleName); + } + + /// + /// Remove a User From Roles + /// + /// an instance of XUser + /// a collection of role names + /// an instance of IdentityResult + public async Task RemoveFromRolesAsync( + XUser user, + IEnumerable roleNames + ) + { + return await UserManager.RemoveFromRolesAsync(user, roleNames); + } + + /// + /// Assign a User to Specific Role + /// + /// an instance of XUser + /// role name + /// an instance of IdentityResult + public async Task AddUserToRoleAsync( + XUser user, + string roleName + ) + { + // + if (user == null || + roleName.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + var userSelectByParam = GetUserSelectByParam(user); + var isExistsUser = await IsUserExistsAsync(userSelectByParam); + var isExistsRole = await IsRoleExistsAsync(roleName); + if (!isExistsUser || !isExistsRole) + { + XException.NotFound.Throw(); + } + + // + var result = await UserManager + .AddToRoleAsync(user, roleName); + + // + return result; + } + + /// + /// Retrieve Role Name by it's ID + /// + /// role id + /// role name as string + public async Task GetRoleNameAsync( + string roleId + ) + { + // + // Validate Args ... + if (roleId.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + var role = await RoleManager + .FindByIdAsync(roleId); + + // + return role.Name.ToNormalString(); + } + + /// + /// Get a List Of User Roles + /// + /// user identifier + /// + /// + /// a collection of role names + public async Task> GetRoleNamesAsync( + string userSelectByParam, + bool checkCanLoginPolicies = true, + bool checkIsBanned = true + ) + { + // + // Validate Args ... + if (userSelectByParam.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + var user = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: true, + checkCanLoginPolicies: checkCanLoginPolicies, + checkIsBanned: checkIsBanned + ); + + // + var result = new List(); + foreach (var role in user.Roles) + { + // + var roleName = await GetRoleNameAsync(role.RoleId); + roleName = roleName.ToNormalString(); + + // + result.Add(roleName); + } + + // + return result; + } + + /// + /// Check a User is Adminr not + /// + /// user identifier + /// a boolean value + public async Task IsAdmin( + string userSelectByParam + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(userSelectByParam); + + // + var user = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: true, + checkCanLoginPolicies: true); + + // + var result = await IsAdmin(user); + + // + return result; + } + + /// + /// Check a User is Adminr not + /// + /// an instance of XUser + /// a boolean value + public async Task IsAdmin( + XUser user + ) + { + // + // Validate Args ... + ValidationProvider.NotNull(user); + ValidationProvider.NotEmpty(user.Id); + + // + // Retrieve Admin Role ... + // TODO: Fix this ... + var adminRole = "admin"; + // XUserRole.Admin + // .GetStringValue () + // .ToNormalString (); + + // + // Check Admin Role Exists in User Roles or not ... + var userRoleNames = await GetRoleNamesAsync(user.Id); + var isContainsAdminRole = userRoleNames.Contains(adminRole); + + // + // Return Result ... + return isContainsAdminRole; + } + #endregion + } +} \ No newline at end of file diff --git a/Providers/XIdentityManager/XIdentityManager+Search.cs b/Providers/XIdentityManager/XIdentityManager+Search.cs new file mode 100644 index 0000000..8e5baf6 --- /dev/null +++ b/Providers/XIdentityManager/XIdentityManager+Search.cs @@ -0,0 +1,119 @@ +using System.Threading.Tasks; +using xCommons.Extensions; +using xExceptions.Constants; +using xIdentityModels.Dtos; +using xModels.Dtos; +using xDataService.Extensions; +using xIds.Extensions; +using System.Linq; +using xIdentityModels.Extensions; + +namespace xIds.Providers +{ + public partial class XIdentityManager + { + // + #region Actions ... + /// + /// Query User Profiles + /// + /// requested user's identifier + /// how to filter results based on XQuery structure + /// an instance of XQueryResult of XUserProfileDto + public async Task> QueryOpenToSearchUsers( + string requestedUserSelectByParam, + XQuery query + ) + { + // + // Validation ... + if (query.IsNull()) + { + XException.InvalidArgs.Throw(); + } + + // + // Normalize ... + query = query.NormalizeQuery(DataConfiguration.ToXDataServiceConfig()); + + // + // Since in Resourceable Entities we have to Search on Locales + // we Must Implement Senario Custom ... + var totalEntities = GetUsersDbSet() + .Where(u => u.OpenToSearch) + .ToList() + .Where(u => !u.ContainsUserSelectByParam(requestedUserSelectByParam)) + .Select(u => u.Id) + .ToList(); + + // + var items = await GetUserProfilesAsync( + totalEntities, + requestedUserSelectByParam, + checkIsBanned: false, + checkCanLoginPolicies: false, + forceCheckRequestedUser: false + ); + + // + var totalItemsCount = items.Count(); + + // + // Apply Filter ... + if (!query.Filter.IsNullOrEmpty()) + { + // + items = items + .ApplyFilter(query.Filter); + } + int filteredItemsCount = items.Count(); + + // + // Count Pages ... + var totalPagesCount = query.CountPages(totalItemsCount); + var filteredPagesCount = query.CountPages(filteredItemsCount); + + // + // Apply Paging and Sorting ... + if (totalItemsCount > 0 && + filteredItemsCount > 0) + { + // + // Apply Sorting ... + items = items + .ToList() + .ApplySorting( + query.SortBy, + query.IsAscending + ); + + // + // Apply Paging ... + items = items + .ToList() + .ApplyPaging( + query.Page, + query.PageSize + ); + } + + // + // Generate Result Object ... + // Query = query, + var result = new XQueryResult + { + Items = items, + Page = query.Page, + PageSize = query.PageSize, + TotalPages = totalPagesCount, + TotalItems = totalItemsCount, + TotalFilteredPages = filteredPagesCount, + TotalFilteredItems = filteredItemsCount + }; + + // + return result; + } + #endregion + } +} \ No newline at end of file diff --git a/Providers/XIdentityManager/XIdentityManager+Seed.cs b/Providers/XIdentityManager/XIdentityManager+Seed.cs new file mode 100644 index 0000000..2cb1584 --- /dev/null +++ b/Providers/XIdentityManager/XIdentityManager+Seed.cs @@ -0,0 +1,132 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using xCommons.Extensions; +using xExceptions.Constants; +using xIdentityModels.Descriptors; +using xIdentityModels.Extensions; + +namespace xIds.Providers +{ + public partial class XIdentityManager + { + // + #region Default Identity Preparation Actions ... + /// + /// Create Default Roles based on Identity Configuration + /// + /// + public async Task CreateIdentityRoles() + { + // + if (Configuration == null || + Configuration.IdentityRoles == null || + Configuration.IdentityRoles.Count == 0) + { + return; + } + + // + // Check New Users Role exists in IdentityRole or not ... + if (!Configuration.NewUsersRole.IsNullOrEmpty()) + { + // + // Ensure NewUsersRole Exists in IdentityRoles ... + var isNewUserRoleExistsInIdentityRoles = Configuration + .IdentityRoles + .Contains(Configuration.NewUsersRole); + if (!isNewUserRoleExistsInIdentityRoles) + { + XException.InvalidArgs.Throw(); + } + } + + // + // loop through all roles and create them one by one ... + foreach (var roleName in Configuration.IdentityRoles) + { + // + var isRoleExists = await RoleManager.RoleExistsAsync(roleName); + if (!isRoleExists) + { + // + var result = await RoleManager.CreateAsync(new IdentityRole(roleName)); + } + } + } + + /// + /// Create a user Based on User Descriptor + /// + /// an instance of XIdentityUserDescriptor + /// + public async Task CreateUserAsync( + XIdentityUserDescriptor user + ) + { + // + // Validate Args ... + ValidationProvider.NotNull(user); + + // + var isExistsRole = await RoleManager.RoleExistsAsync(user.Role); + if (!isExistsRole) + { + // + // Check Assigned role to user exists in app roles ... + if (!Configuration + .IdentityRoles + .Contains(user.Role)) + { + XException.InvalidArgs.Throw(); + } + + // + // Ensure All Roles Created ... + await CreateIdentityRoles(); + } + + // + // Check User Exists ... + var isExistsUser = await IsUserExistsAsync(user.UserName); + if (!isExistsUser) + { + // + // Create a DashboardUser instance based on Configuration Data ... + var userEntity = user.ToXUser(); + + // + // Try to Create User ... + var userCreateResult = await CreateUserAsync( + userEntity, + user.Password + ); + if (!userCreateResult.Succeeded) + { + XException.ActionFailed.Throw(); + } + + // + // Assign Admin User to it's Role ... + var roleAssignResult = await UserManager.AddToRoleAsync(userEntity, user.Role); + if (!roleAssignResult.Succeeded) + { + XException.ActionFailed.Throw(); + } + + // + var claims = await ToJwtClaims( + userEntity, + checkCanLoginPolicies: true, + checkIsBanned: true); + + // + var addClaimsResult = await UserManager.AddClaimsAsync(userEntity, claims); + if (!addClaimsResult.Succeeded) + { + XException.ActionFailed.Throw(); + } + } + } + #endregion + } +} \ No newline at end of file diff --git a/Providers/XIdentityManager/XIdentityManager+Token.cs b/Providers/XIdentityManager/XIdentityManager+Token.cs new file mode 100644 index 0000000..d622004 --- /dev/null +++ b/Providers/XIdentityManager/XIdentityManager+Token.cs @@ -0,0 +1,742 @@ +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using xCommons.Extensions; +using xCommons.Helpers; +using xExceptions.Constants; +using xIdentityModels; +using xIdentityModels.Constants; +using xIdentityModels.Extensions; +using xIdentityModels.Models; +using xIdentityModels.Navigations; + +namespace xIds.Providers +{ + public partial class XIdentityManager + { + // + #region Token Actions ... + /// + /// Check a Token Entity Exists or not + /// + /// an instance of XDevice + /// specifies Action Step by a member of XAction + /// a boolean value + private async Task IsTokenExistsByDeviceAndType( + XDevice device, + XAction type + ) + { + // + ValidationProvider.NotNull(device); + + // + var isExists = false; + var enumerator = DbContext.Tokens.AsAsyncEnumerable(); + await + foreach (var token in enumerator) + { + // + if (token.Type == type && + token.Device.IsSameAs(device)) + { + isExists = true; + break; + } + } + + // + return isExists; + } + + /// + /// Check a Token Entity Exists or not + /// + /// specified user's identifier + /// specifies Action Step by a member of XAction + /// a boolean value + private async Task IsTokenExistsByUserAndType( + string userSelectByParam, + XAction type + ) + { + // + ValidationProvider.NotEmpty(userSelectByParam); + + // + var isExists = false; + var tokensEnumerator = DbContext.Tokens.AsAsyncEnumerable(); + await + foreach (var token in tokensEnumerator) + { + // + if (token.Type == type && + token.UserSelectByParam + .ToNormalString() == userSelectByParam + .ToNormalString()) + { + // + isExists = true; + break; + } + } + + // + return isExists; + } + + /// + /// Check a Token Entity Exists or not + /// + /// represent specified action token + /// a boolean value + private async Task IsTokenExistsByToken( + string token + ) + { + // + var result = await DbContext.Tokens + .AnyAsync(xt => xt.Token == token); + + // + return result; + } + + /// + /// Check a Token Entity Exists or not + /// + /// represent specified action token hash + /// a boolean value + private async Task IsTokenExistsByHash( + string hash + ) + { + // + var result = await DbContext.Tokens + .AnyAsync(xt => xt.Hash == hash); + + // + return result; + } + + /// + /// Check a Token Entity Exists or not + /// + /// an instance of XToken + /// a boolean value + private async Task IsTokenExistsByToken( + XToken item + ) + { + // + var result = false; + var tokenEnumerator = DbContext.Tokens + .AsAsyncEnumerable(); + await + foreach (var token in tokenEnumerator) + { + // + if (token.Hash == item.Hash && + token.Token == item.Token && + token.Type == item.Type && + token.UserSelectByParam + .ToNormalString() == item.UserSelectByParam + .ToNormalString() && + token.Device.IsSameAs(item.Device)) + { + // + result = true; + break; + } + } + + // + return result; + } + + /// + /// Check a Token Entity Exists or not + /// + /// specifies token id + /// a boolean value + private async Task IsTokenExistsById( + int id + ) + { + return await DbContext.Tokens.AnyAsync(t => t.Id == id); + } + + /// + /// Retrieve Token Entity + /// + /// an instance of XDevice + /// specifies Action Step by a member of XAction + /// an instance of XToken + private async Task GetTokenByDeviceAndType( + XDevice device, + XAction type + ) + { + // + var isExists = await IsTokenExistsByDeviceAndType(device, type); + if (!isExists) + { + return null; + } + + // + XToken item = null; + var enumerator = DbContext.Tokens.AsAsyncEnumerable(); + await + foreach (var token in enumerator) + { + // + if (token.Type == type && + token.Device.IsSameAs(device)) + { + // + item = token; + break; + } + } + + // + return item; + } + + /// + /// Retrieve Token Entity + /// + /// specified user's identifier + /// specifies Action Step by a member of XAction + /// an instance of XToken + private async Task GetTokenByUserAndType( + string userSelectByParam, + XAction type + ) + { + // + var isExists = await IsTokenExistsByUserAndType(userSelectByParam, type); + if (!isExists) + { + return null; + } + + // + XToken item = null; + var enumerator = DbContext.Tokens.AsAsyncEnumerable(); + await + foreach (var token in enumerator) + { + // + if (token.Type == type && + token.UserSelectByParam + .ToNormalString() == userSelectByParam + .ToNormalString()) + { + // + item = token; + break; + } + } + + // + return item; + } + + /// + /// Retrieve a Token Entity + /// + /// represent specified action token + /// an instance of XToken + private async Task GetTokenByToken( + string token + ) + { + // + var isExists = await IsTokenExistsByToken(token); + if (!isExists) + { + return null; + } + + // + var result = await DbContext.Tokens + .FirstOrDefaultAsync(xt => xt.Token == token); + + // + return result; + } + + /// + /// Retrieve Related Hash to a Token + /// + /// represent specified action token + /// hash string + private async Task GetRelatedHashByToken( + string token + ) + { + // + var item = await GetTokenByToken(token); + if (item == null) + { + return null; + } + + // + return item.Hash; + } + + /// + /// Retrieve a Token Entity + /// + /// represent specified action token hash + /// an instance of XToken + private async Task GetTokenByHash( + string hash + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(hash); + + // + var isExists = await IsTokenExistsByHash(hash); + if (!isExists) + { + return null; + } + + // + var item = await DbContext.Tokens + .FirstOrDefaultAsync(t => t.Hash == hash); + + // + return item; + } + + /// + /// Retrieve Related Token to a Hash string + /// + /// represent specified action token hash + /// token string + private async Task GetRelatedTokenByHash( + string hash + ) + { + // + var item = await GetTokenByHash(hash); + if (item == null) + { + return null; + } + + // + return item.Token; + } + + /// + /// Retrieve a Token Expiration Date + /// + /// represent specified action token + /// an instance of DateTime + private DateTime GetTokenExpirationDate( + string token + ) + { + // + ValidationProvider.NotEmpty(token); + + // + var result = IdentityHelper.GetTokenExpirationDate(token); + + // + return result; + } + + /// + /// Remove a Token Entity + /// + /// represent specified action token + /// + private async Task RemoveTokenByToken( + string token + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(token); + + // + var isExists = await IsTokenExistsByToken(token); + if (!isExists) + { + return; + } + + // + var item = await GetTokenByToken(token); + if (item == null) + { + return; + } + + // + DbContext.Tokens.Remove(item); + await DbContext.SaveChangesAsync(); + } + + /// + /// Remove a Token Entity + /// + /// represent specified action token hash + /// + private async Task RemoveTokenByHash( + string hash + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(hash); + + // + var isExists = await IsTokenExistsByHash(hash); + if (!isExists) + { + return; + } + + // + var item = await GetTokenByHash(hash); + if (item == null) + { + return; + } + + // + DbContext.Tokens.Remove(item); + await DbContext.SaveChangesAsync(); + } + + /// + /// Remove a Token Entity + /// + /// an instance of XDevice + /// + private async Task RemoveTokenByDevice( + XDevice device + ) + { + // + // Validate Args ... + ValidationProvider.NotNull(device); + + // + XToken items = null; + var enumerator = DbContext.Tokens.AsAsyncEnumerable(); + await + foreach (var token in enumerator) + { + // + if (token.Device.IsSameAs(device)) + { + // + items = token; + break; + } + } + + // + if (!items.IsNull()) + { + DbContext.Tokens.RemoveRange(items); + await DbContext.SaveChangesAsync(); + } + } + + /// + /// add a token Entity + /// + /// an instance of XToken + /// an instance of XToken + private async Task AddTokenAsync( + XToken item + ) + { + // + var isExist = await IsTokenExistsByToken(item); + if (isExist) + { + return await GetTokenByHash(item.Hash); + } + + // + var entry = await DbContext.Tokens.AddAsync(item); + await DbContext.SaveChangesAsync(); + + // + return entry.Entity; + } + + /// + /// Hash a Request Token + /// + /// an instance of XActionRequestToken + /// + /// + /// an instance of XToken + private async Task HashRequest( + XActionRequestToken request, + bool forceNotNullUserSelectByParam = true, + bool forceDeviceNotNull = true + ) + { + // + // Validate Args ... + var secretKey = Configuration.IdentitySecretKey; + ValidationProvider.NotNull(request); + ValidationProvider.NotEmpty(secretKey); + + // + ValidateActionRequest(request); + + // + var type = request.Action; + var device = request.Context.Device; + var tokenObject = ToSecurityToken(request); + var tokenString = ToTokenString(tokenObject); + var tokenHash = ToHash(tokenString); + var userSelectByParam = GetUserSelectByParam(request, forceNotNullUserSelectByParam); + + // + if (forceNotNullUserSelectByParam) + { + ValidationProvider.NotEmpty(userSelectByParam); + } + + // + ValidationProvider.NotEmpty(tokenString, tokenHash); + + // + if (forceDeviceNotNull) + { + ValidationProvider.NotNull(device); + } + + // + ValidationProvider.NotNull(tokenObject); + + // + var xToken = new XToken + { + Type = type, + Device = device, + Hash = tokenHash, + Token = tokenString, + UserSelectByParam = userSelectByParam + }; + + // + var addedToken = await AddTokenAsync(xToken); + + // + return addedToken; + } + + /// + /// Generate Token Hash string + /// + /// represent specified action token + /// check token validation parameters, default is true + /// hash string + private string ToHash( + string token, + bool checkValidation = true + ) + { + // + ValidationProvider.NotEmpty(token); + + // + if (checkValidation) + { + ValidateToken(token); + } + + // + var result = token.ToMd5String().ToNormalString(); + + // + return result; + } + + /// + /// Generate Token string + /// + /// represent specified action token + /// token string + private string ToTokenString( + SecurityToken token + ) + { + // + ValidationProvider.NotNull(token, IdentityHelper); + + // + var result = IdentityHelper.ToTokenString(token); + + // + return result; + } + + /// + /// Generate Token string + /// + /// represent specified action token + /// token string + private string ToTokenString( + JwtSecurityToken token + ) + { + // + ValidationProvider.NotNull(token, IdentityHelper); + + // + var result = IdentityHelper.ToTokenString(token); + + // + return result; + } + + /// + /// Generate Security Token + /// + /// represent specified action token + /// an instance of SecurityToken + private SecurityToken ToSecurityToken( + string token + ) + { + // + ValidationProvider.NotEmpty(token); + + // + var result = IdentityHelper.ToSecurityToken(token); + + // + return result; + } + + /// + /// Generate Security Token + /// + /// an instance of XActionRequestToken + /// an instance of SecurityToken + private SecurityToken ToSecurityToken( + XActionRequestToken request + ) + { + // + ValidationProvider.NotNull(request); + + // + ValidateActionRequest(request); + + // + var result = IdentityHelper.ToSecurityToken(request); + if (result == null) + { + XException.InvalidToken.Throw(); + } + + // + return result; + } + + /// + /// Cleanup all exists tokens related to specified User + /// + /// specified user's identifier + /// + private async Task HandleRemoveExistsTokens( + string userSelectByParam + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(userSelectByParam); + + // + var xTokens = new List(); + var xTokenEnumerator = DbContext.Tokens.AsAsyncEnumerable(); + await + foreach (var xToken in xTokenEnumerator) + { + // + if (xToken.UserSelectByParam == userSelectByParam && + ObjectHelper.ToEnumerableValues().Contains(xToken.Type)) + { + xTokens.Add(xToken); + } + } + + // + if (xTokens.HasChild()) + { + DbContext.Tokens.RemoveRange(xTokens); + await DbContext.SaveChangesAsync(); + } + } + + /// + /// Cleanup all tokens related to specified User + /// + /// a collection of user identifiers + /// + private async Task HandleCleanUserTokens( + ICollection userSelectByParams + ) + { + // + // Validate Args ... + if (!userSelectByParams.HasChild()) + { + return; + } + + // + foreach (var userSelectByParam in userSelectByParams) + { + await HandleRemoveExistsTokens(userSelectByParam); + } + } + + /// + /// Cleanup all tokens related to specified User + /// + /// an instance of XUser + /// + private async Task HandleCleanUserTokens( + XUser user + ) + { + // + if (user == null) + { + return; + } + + // + var userSelectByParams = GenerateUserSelectByParams(user); + + // + await HandleCleanUserTokens(userSelectByParams); + } + #endregion + } +} \ No newline at end of file diff --git a/Providers/XIdentityManager/XIdentityManager+User.cs b/Providers/XIdentityManager/XIdentityManager+User.cs new file mode 100644 index 0000000..5f441ec --- /dev/null +++ b/Providers/XIdentityManager/XIdentityManager+User.cs @@ -0,0 +1,503 @@ +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using IdentityModel; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using xCommons.Constants; +using xCommons.Extensions; +using xExceptions.Constants; +using xIdentityModels; +using xIdentityModels.Constants; +using xIdentityModels.Dtos; +using xIdentityModels.Extensions; +using xIdentityModels.Navigations; + +namespace xIds.Providers +{ + public partial class XIdentityManager + { + // + #region User Handlers ... + /// + /// Check a User Exists or not + /// + /// specified user's identifier + /// a boolean value + public async Task IsUserExistsAsync( + string userSelectByParam + ) + { + // + // Validate Args ... + if (userSelectByParam.IsNullOrEmpty()) + { + return false; + } + + // + var selectType = GetUserSelectByType(userSelectByParam); + + // + var result = false; + XUser user = null; + switch (selectType) + { + case XUserSelectBy.MobileNumber: + user = await GetUsersDbSet() + .Where(ur => ur.PhoneNumber == userSelectByParam) + .FirstOrDefaultAsync(); + break; + + case XUserSelectBy.Email: + user = await UserManager + .FindByEmailAsync(userSelectByParam); + break; + + case XUserSelectBy.Username: + user = await UserManager + .FindByNameAsync(userSelectByParam); + break; + + case XUserSelectBy.ID: + default: + user = await UserManager + .FindByIdAsync(userSelectByParam); + break; + } + + // + result = user != null; + + // + return result; + } + + /// + /// Check a User Selector is Available for Registration or Not + /// + /// specified user's identifier + /// a boolean value + public async Task CanRegister( + string userSelectByParam + ) + { + // + // Validate ARgs ... + ValidationProvider.NotEmpty(userSelectByParam); + + // + var selectByType = GetUserSelectByType(userSelectByParam); + + // + if (selectByType == XUserSelectBy.Username && + !ValidateUserName(userSelectByParam)) + { + return false; + } + + // + var isExists = await IsUserExistsAsync(userSelectByParam); + if (isExists) + { + return false; + } + + // + return true; + } + + /// + /// Get a User object + /// + /// specified user's identifier + /// specifies returned object contains all Navigation Properties or not, default is false + /// an instance of XUser + public async Task GetUserAsync( + string userSelectByParam, + bool containDetails = false + ) + { + // + var isExists = await IsUserExistsAsync(userSelectByParam); + if (!isExists) + { + XException.NotFound.Throw(); + } + + // + // Fill User ... + var selectType = GetUserSelectByType(userSelectByParam); + XUser user = null; + var usersStore = GetUsersDbSet(containDetails); + + // + // Select Type ... + switch (selectType) + { + case XUserSelectBy.MobileNumber: + user = await usersStore + .FirstOrDefaultAsync(ur => ur.PhoneNumber == + userSelectByParam); + break; + + case XUserSelectBy.Email: + user = await usersStore + .FirstOrDefaultAsync(ur => ur.Email == + userSelectByParam); + break; + + case XUserSelectBy.Username: + user = await usersStore + .FirstOrDefaultAsync(ur => ur.UserName == + userSelectByParam); + break; + + case XUserSelectBy.ID: + default: + user = await usersStore + .FirstOrDefaultAsync(ur => ur.Id == + userSelectByParam); + break; + } + + // + return user; + } + + /// + /// Retrieve User Names based on UserIds + /// + /// a collection of user identifiers + /// a collection of Usernames + public async Task> GetUserNamesAsync( + IEnumerable userIds + ) + { + // + // Validate Args ... + if (userIds == null || + userIds.Count() == 0) + { + return null; + } + + // + var result = await GetUsersDbSet() + .Where( + xu => + userIds + .Contains(xu.Id)) + .Select(xt => xt.UserName) + .ToListAsync(); + + // + return result; + } + + /// + /// get user ids and retrieve corresponding user names + /// + /// an instance of XUserNameIdRequest which represent required UserIds collection + /// a collection of XUserNameIdResponse instances + public async Task> GetUserNameIdsAsync( + XUserNameIdRequest model + ) + { + // + // Validate Args ... + if (model.IsNull() || !model.Ids.HasChild()) + { + return null; + } + + // + var result = await GetUsersDbSet() + .Where( + xu => + model.Ids.Contains(xu.Id) || + model.Ids.Contains(xu.Email) || + model.Ids.Contains(xu.UserName) || + model.Ids.Contains(xu.PhoneNumber)) + .Select(xt => new XUserNameIdResponse + { + Id = xt.Id, + UserName = xt.UserName, + }) + .ToListAsync(); + + // + return result; + } + + /// + /// Create a User + /// + /// an instance of XUser + /// user's password + /// check a user can log in in system or not, default is false + /// check user is banned or not, default is false + /// an instance of IdentityResult + public async Task CreateUserAsync( + XUser user, + string password, + bool checkCanLoginPolicies = false, + bool checkIsBanned = false + ) + { + // + if (user == null || + password.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + // Get User Select By param + var userSelectByParam = GetUserSelectByParam(user, excludes: new List { XUserSelectBy.ID }); + + // + // Check User Created before or not ... + var isExists = await IsUserExistsAsync(userSelectByParam); + if (isExists) + { + XException.Duplicate.Throw(); + } + + // + var result = await UserManager.CreateAsync(user, password); + if (result.Succeeded) + { + // + var claims = await ToJwtClaims( + user, + checkCanLoginPolicies: checkCanLoginPolicies, + checkIsBanned: checkIsBanned); + + // + var addClaimsResult = await UserManager.AddClaimsAsync(user, claims); + + // + return addClaimsResult; + } + + // + return result; + } + + /// + /// Update a User Information + /// + /// an instance of XUser + /// check a user can log in in system or not, default is true + /// check user is banned or not, default is true + /// an instance of IdentityResult + public async Task UpdateUserAsync( + XUser user, + bool checkCanLoginPolicies = true, + bool checkIsBanned = true + ) + { + // + var updateResult = await this.UserManager.UpdateAsync(user); + if (updateResult.Succeeded) + { + // + var claims = await ToJwtClaims( + user, + checkCanLoginPolicies: checkCanLoginPolicies, + checkIsBanned: checkIsBanned + ); + var existsClaims = await UserManager.GetClaimsAsync(user); + + // + await UserManager.RemoveClaimsAsync(user, existsClaims); + var addClaimsResult = await UserManager.AddClaimsAsync(user, claims); + + // + return addClaimsResult; + } + + // + return updateResult; + } + + /// + /// Generate JWT Claims froma DashboardUser object + /// + /// an instance of XUser + /// check a user can log in in system or not, default is true + /// check user is banned or not, default is true + /// a collection of Claim instances + public async Task ToJwtClaims( + XUser user, + bool checkCanLoginPolicies = true, + bool checkIsBanned = true + ) + { + // + // Get New Instance of Claims ... + var claims = new List { + // + // Mapping to User.Identity.Name ... + new Claim (JwtClaimTypes.Name, user.UserName), + + // + // Mapping to User.Identity.Name ... + new Claim (JwtRegisteredClaimNames.UniqueName, user.UserName), + + // + // Other Usefull User Related Data ... + new Claim (JwtClaimTypes.GivenName, user.FirstName), + new Claim (JwtClaimTypes.FamilyName, user.LastName), + + // + // User Gendre ... + new Claim (JwtClaimTypes.Gender, user.Gender.ToString (), ClaimValueTypes.Integer), + + // + // User Profile Picture ... + new Claim (JwtClaimTypes.Picture, !user.Avatar.IsNullOrEmpty () ? + user.Avatar.ToString () : + ""), + + // + // The Unique Identifier for Each Token ... + new Claim (JwtRegisteredClaimNames.Jti, Guid.NewGuid ().ToString ()), + + // + // Add isEnable and isBanned feature ... + new Claim ( + XCustomClaims.IsEnabled, + user.IsEnable.ToString (), + ClaimValueTypes.Boolean + ), + new Claim ( + XCustomClaims.IsBanned, + user.IsBanned.ToString (), + ClaimValueTypes.Boolean + ), + }; + + // + #region Set Claims based on Profile Policies ... + // + if (Configuration.Policy.Profile.ContainsDateOfBirth) + { + claims.Add(new Claim(JwtRegisteredClaimNames.Birthdate, user.DateOfBirth.ToString())); + } + + // + if (Configuration.Policy.Profile.ContainsEmail) + { + claims.Add(new Claim(JwtClaimTypes.Email, user.Email)); + claims.Add(new Claim( + JwtClaimTypes.EmailVerified, + user.EmailConfirmed.ToString(), + ClaimValueTypes.Boolean)); + } + + // + if (Configuration.Policy.Profile.ContainsPhoneNumber) + { + claims.Add(new Claim(JwtClaimTypes.PhoneNumber, user.PhoneNumber)); + claims.Add(new Claim( + JwtClaimTypes.PhoneNumberVerified, + user.PhoneNumberConfirmed.ToString(), + ClaimValueTypes.Boolean)); + } + + // + if (Configuration.Policy.Profile.ContainsRoles) + { + var roleNames = await GetRoleNamesAsync( + user.UserName, + checkCanLoginPolicies: checkCanLoginPolicies, + checkIsBanned: checkIsBanned); + roleNames.ToList() + .ForEach(rn => + { + claims.Add(new Claim(JwtClaimTypes.Role, rn)); + }); + } + + // + if (Configuration.Policy.Profile.ContainsLastLogin) + { + claims.Add(new Claim( + JwtClaimTypes.AuthenticationTime, + user.LastLogin.ToString(), + ClaimValueTypes.DateTime)); + } + #endregion + + // + // Return Result ... + return claims.ToArray(); + } + + /// + /// Check User and Password relation + /// + /// an instance of XUser + /// user's password + /// an instance of XDevice + /// specify destination language + /// specify account lockout on failure log in + /// specify log in force without checking XDevice and lang relation + /// an instance of SignInResult + public async Task CheckPasswordSignInAsync( + XUser user, + string password, + XDevice device, + string language, + bool lockoutOnFailure, + bool isForced = true + ) + { + // + var result = await SignInManager.CheckPasswordSignInAsync( + user, password, lockoutOnFailure + ); + + // + if (result.Succeeded && isForced) + { + // + var isExistsDevice = user.Devices.Any(d => d.IsSameAs(device)); + if (!isExistsDevice && !user.Email.IsNullOrEmpty() && user.EmailConfirmed) + { + // + user.Devices.Add(device); + + // + var updateUserResult = await UpdateUserAsync(user); + if (updateUserResult.Succeeded) + { + // + try + { + // + var xMessage = GetUserNewDeviceLoggedInMessage( + language, + user.Email, + device + ); + + // + await MessageProvider.SendMailAsync(xMessage); + } + catch { } + } + } + } + + // + return result; + } + #endregion + } +} \ No newline at end of file diff --git a/Providers/XIdentityManager/XIdentityManager+Validators.cs b/Providers/XIdentityManager/XIdentityManager+Validators.cs new file mode 100644 index 0000000..6df78f2 --- /dev/null +++ b/Providers/XIdentityManager/XIdentityManager+Validators.cs @@ -0,0 +1,1520 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using xCommons.Extensions; +using xExceptions.Constants; +using xIdentityModels; +using xIdentityModels.Constants; +using xIdentityModels.Extensions; +using xIdentityModels.Models; +using xIdentityModels.Navigations; + +namespace xIds.Providers +{ + public partial class XIdentityManager + { + // + #region User Validators ... + /// + /// Validate User Name for Registration based on Policies + /// + /// provided user name + /// a boolean value + private bool ValidateUserName( + string userName + ) + { + // + // Check min/max Length ... + if (userName.IsNullOrEmpty() || + userName.Length < Configuration.Policy.User.MinLength || + userName.Length > Configuration.Policy.User.MaxLength) + { + return false; + } + + // + // Check Allowed UserName Characters ... + if (!UserManager.Options.User.AllowedUserNameCharacters.IsNullOrEmpty() && + userName.Any(c => !UserManager.Options.User.AllowedUserNameCharacters.Contains(c))) + { + return false; + } + + // + // Check invalid UserNames ... + if (Configuration.Policy.User.InvalidUserNames + .HasChild() && Configuration.Policy.User.InvalidUserNames + .Any(u => userName + .ToNormalString() + .Contains(u.ToNormalString()))) + { + return false; + } + + // + return true; + } + + /// + /// Validate a User Exists + /// + /// specifies user identifier + /// specify Excetion to thrown on failure + /// + private async Task ValidateUserExistsAsync( + string userSelectByParam, + Exception exception = null + ) + { + // + ValidationProvider + .NotEmpty(userSelectByParam); + + // + if (exception == null) + { + exception = XException.NotFound.ToException(); + } + + // + // Check User Exists ... + var isExists = await IsUserExistsAsync(userSelectByParam); + if (!isExists) + { + throw exception; + } + } + + /// + /// Validate a User Not Exists + /// + /// specifies user identifier + /// specify Excetion to thrown on failure + /// + private async Task ValidateUserNotExists( + string userSelectByParam, + Exception exception = null + ) + { + // + ValidationProvider + .NotEmpty(userSelectByParam); + + // + if (exception == null) + { + exception = XException.UserRegisteredBefore.ToException(); + } + + // + // Check User Exists ... + var isExists = await IsUserExistsAsync(userSelectByParam); + if (isExists) + { + throw exception; + } + } + + /// + /// Validate User Exists and Some Usefull Checks + /// + /// specifies user identifier + /// specifies returned object contains all Navigation Properties or not, default is false + /// check a user can log in in system or not, default is false + /// check user is banned or not, default is true + /// specify do not thrown Exception if user is Disabled, default is false + /// specify requested person is Admin role, default is false + /// specify Excetion to thrown on failure + /// an instance of XUser + public async Task ValidateUserExistsAndRetrieve( + string userSelectByParam, + bool containDetails = false, + bool checkCanLoginPolicies = false, + bool checkIsBanned = true, + bool ignoreDisabledUser = false, + bool forceAdmin = false, + Exception exception = null + ) + { + // + ValidationProvider + .NotEmpty(userSelectByParam); + ValidationProvider.NotNull( + Configuration, + Configuration.Policy, + Configuration.Policy.SignIn); + + // + // Prepare Default Exception ... + if (exception == null) + { + exception = XException.InvalidData.ToException(); + } + + // + // Check User Exists ... + await ValidateUserExistsAsync(userSelectByParam); + + // + // Retrieve User ... + var result = await GetUserAsync(userSelectByParam, containDetails); + if (result == null) + { + throw exception; + } + + // + // Check Force Admin ... + if (forceAdmin) + { + // + var isAdmin = await IsAdmin(result); + if (!isAdmin) + { + XException.NotAuthorized.Throw(); + } + } + + // + // Check User not Bann ... + if (checkIsBanned && + result.IsBanned) + { + XException.UserBanned.Throw(); + } + + // + var requireConfirmedEmail = Configuration.Policy.SignIn.RequireConfirmedEmail; + var requireConfirmedPhoneNumber = Configuration.Policy.SignIn.RequireConfirmedPhoneNumber; + var requireEnabled = Configuration.Policy.SignIn.RequiredEnabled; + + // + var isEnabled = result.IsEnable; + var isMobileConfirmed = result.PhoneNumberConfirmed; + var isEmailConfirmed = result.EmailConfirmed; + + // + if (checkCanLoginPolicies) + { + // + // Check IsEnabled ... + if (!ignoreDisabledUser && + requireEnabled && + !isEnabled) + { + XException.UserDisabled.Throw(); + } + + // + // Check Email Confirmed ... + if (requireConfirmedEmail && + !isEmailConfirmed) + { + XException.EmailNotConfirmed.Throw(); + } + + // + // Check Mobile Confirmed ... + if (requireConfirmedPhoneNumber && + !isMobileConfirmed) + { + XException.MobileNotConfirmed.Throw(); + } + } + + // + // Return Result ... + return result; + } + + /// + /// Validate a Device for User Actions + /// + /// an instance of XDevice + /// + private async Task ValidateDeviceForActions( + XDevice device + ) + { + // + ValidationProvider + .NotNull(device); + + // + // Check is Device Banned or not ... + var isDeviceBanned = await IsDeviceBanned(device); + if (isDeviceBanned) + { + // + // Retrieve Banned Device ... + var xBannedDevice = await GetBannedDevice(device); + if (xBannedDevice == null) + { + XException.InvalidArgs.Throw(); + } + + // + // Check Banned Device Time out Passed or not ... + var isDelayTimePassed = IsDelayTimePassed(xBannedDevice); + if (!isDelayTimePassed) + { + // + var passedTime = GetPassedTime(xBannedDevice); + + // + XException.DeviceBanned + .AddContentToException(passedTime.ToString()); + } + + // + // if Delay Passed of Banned Device + // the device must Removed ... + await BannedDeviceRemove(xBannedDevice); + } + } + + /// + /// Validate a DateOfBirth for Registration + /// + /// users dob date, an instance of DateTime + /// specify Excetion to thrown on failure + /// + private void ValidateDateOfBirth( + DateTime dateOfBirth, + Exception exception = null + ) + { + // + // Validate Args ... + ValidationProvider.NotNull(dateOfBirth); + + // + if (exception.IsNull()) + { + exception = XException.InvalidDate.ToException(); + } + + // + var currentDate = DateTime.UtcNow; + if (dateOfBirth >= currentDate) + { + throw exception; + } + + // + var zeroDate = (new DateTime(1, 1, 1)).ToUniversalTime(); + var minAge = Configuration.Policy.User.MinAgeForRegistration; + var maxAge = Configuration.Policy.User.MaxAgeForRegistration; + + // + var timeSpan = currentDate - dateOfBirth; + var age = (zeroDate + timeSpan).Year - 1; + + // + if (age > maxAge || age < minAge) + { + throw exception; + } + } + + /// + /// Validate a Given User Can Registere + /// + /// specifies user identifier + /// + private async Task ValidateCanRegister( + string userSelectByParam + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(userSelectByParam); + + // + var canRegister = await CanRegister(userSelectByParam); + if (!canRegister) + { + XException.Unavailable.Throw(); + } + } + + /// + /// Validate Password by Gicen Policies + /// + /// an instance of XUser + /// user's password + /// specify Excetion to thrown on failure + /// + private async Task ValidatePasswordPolicies( + XUser user, + string password, + Exception exception = null + ) + { + // + // Validate Args ... + ValidationProvider.NotNull(user); + ValidationProvider.NotEmpty(password); + + // + // Prepare Default Exception ... + if (exception == null) + { + exception = XException.InvalidPassword.ToException(); + } + + // + // Check new Password Validation ... + var passwordValidator = new PasswordValidator(); + var checkPasswordResult = await passwordValidator.ValidateAsync(UserManager, user, password); + if (!checkPasswordResult.Succeeded) + { + throw exception; + } + } + + /// + /// Validate a User for Dangerous Actions + /// + /// an instance of XUser + /// specified which user request Action by an instance of XUser + /// + private async Task ValidateUserForDangerousAction( + XUser user, + XUser requester + ) + { + // + // Validate ARgs ... + ValidationProvider.NotNull(user, requester); + + // + var requesterRoleNames = await GetRoleNamesAsync(requester.Id); + + // + var isSame = user.Id == requester.Id; + var isRequesterAdmin = requesterRoleNames.Any(r => r.ToNormalString() == "admin"); + if (!isSame && !isRequesterAdmin) + { + XException.NotAllowed.Throw(); + } + } + #endregion + + // + #region Device Validators ... + /// + /// Validate a Device for Specified User + /// + /// specifies user identifier + /// an instance of XDevice + /// specify Excetion to thrown on failure + /// + private async Task ValidateUserAndDeviceRelation( + string userSelectByParam, + XDevice device, + Exception exception = null + ) + { + // + ValidationProvider.NotEmpty(userSelectByParam); + ValidationProvider.NotNull(device); + + // + if (exception == null) + { + exception = XException.InvalidDevice.ToException(); + } + + // + var user = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: true); + + // + var result = await IsDeviceRelateDToUser( + userSelectByParam, + device + ); + if (!result) + { + throw exception; + } + } + #endregion + + // + #region Token Validators ... + /// + /// Validate a Token + /// + /// token string + /// specify Excetion to thrown on failure + private void ValidateToken( + string token, + Exception exception = null + ) + { + // + ValidationProvider.NotEmpty(token); + + // + if (exception == null) + { + exception = XException.InvalidToken.ToException(); + } + + // + var result = IdentityHelper.ValidateToken(token); + if (!result) + { + throw exception; + } + } + + /// + /// Validate a Token is Specified to a Device and User + /// + /// an instance of XDevice + /// specifies Action type by a member of XAction + /// specify Excetion to thrown on failure + /// an instance of XToken + private async Task ValidateAndRetieveTokenByDeviceAndType( + XDevice device, + XAction type, + Exception exception = null + ) + { + // + ValidationProvider.NotNull(device); + + // + // Prepare Default Exception ... + if (exception == null) + { + exception = XException.NotFound.ToException(); + } + + // + var isExists = await IsTokenExistsByDeviceAndType(device, type); + if (!isExists) + { + throw exception; + } + + // + var item = await GetTokenByDeviceAndType(device, type); + if (item == null) + { + throw exception; + } + + // + return item; + } + + /// + /// Validate a Token By User and Specified Type + /// + /// specifies user identifier + /// specifies Action type by a member of XAction + /// specify Excetion to thrown on failure + /// an instance of XToken + private async Task ValidateAndRetieveTokenByUserAndType( + string userSelectByParam, + XAction type, + Exception exception = null + ) + { + // + ValidationProvider.NotEmpty(userSelectByParam); + + // + // Prepare Default Exception ... + if (exception == null) + { + exception = XException.InvalidData.ToException(); + } + + // + var isExists = await IsTokenExistsByUserAndType(userSelectByParam, type); + if (!isExists) + { + throw exception; + } + + // + var item = await GetTokenByUserAndType(userSelectByParam, type); + if (item == null) + { + throw exception; + } + + // + return item; + } + + /// + /// Check Token Validation + /// + /// token string + /// a boolean value + private bool IsValidToken( + string token + ) + { + // + ValidationProvider.NotEmpty(token); + + // + var result = IdentityHelper.ValidateToken(token); + + // + return result; + } + + /// + /// Retrieve Action Request Based on Action Token + /// + /// token string + /// specify Excetion to thrown on failure + /// an instance of XActionRequestToken + private XActionRequestToken ValidateAndParseActionToken( + string token, + Exception exception = null + ) + { + // + ValidationProvider.NotEmpty(token); + + // + // Prepare Default Exception ... + if (exception == null) + { + exception = XException.InvalidToken.ToException(); + } + + // + ValidateToken(token); + + // + var result = IdentityHelper.ParseActionRequestToken(token); + if (result == null) + { + throw exception; + } + + // + ValidateActionRequest(result); + + // + return result; + } + + /// + /// Parse Action Request from Token + /// + /// token string + /// an instance of XActionRequestToken + private XActionRequestToken ParseActionRequest( + string token + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(token); + + // + var result = IdentityHelper + .ParseActionRequestToken(token); + + // + return result; + } + + /// + /// Parse Action Hash by Corresponding Token + /// + /// token hash string + /// an instance of XActionRequestToken + private async Task ParseActionHash( + string hash + ) + { + // + ValidationProvider.NotEmpty(hash); + + // + var isHashExists = await IsTokenExistsByHash(hash); + if (!isHashExists) + { + return null; + } + + // + var xToken = await GetTokenByHash(hash); + if (xToken == null) + { + return null; + } + + // + var isValidToken = IsValidToken(xToken.Token); + if (!isValidToken) + { + return null; + } + + // + var result = ParseActionRequest(xToken.Token); + + // + return result; + } + + /// + /// Validate and Parse Action Hash + /// + /// token hash string + /// specifies chack token Validation Parameter, default is true + /// specifies check action Validations, default is true + /// specify Excetion to thrown on failure + /// an instance of XActionRequestToken + private async Task ValidateAndParseActionHash( + string hash, + bool forceTokenValidation = true, + bool forceActionValidation = true, + Exception exception = null + ) + { + // + ValidationProvider.NotEmpty(hash); + + // + // Prepare Default Exception ... + if (exception == null) + { + exception = XException.InvalidToken.ToException(); + } + + // + var token = await ValidateAndRetrieveRelatedToken(hash, forceTokenValidation); + + // + var result = await ParseActionHash(hash); + if (result == null) + { + throw exception; + } + + // + if (forceActionValidation) + { + ValidateActionRequest(result); + } + + // + return result; + } + + /// + /// Validate Action Request + /// + /// an instance of XActionRequestToken + /// specify Excetion to thrown on failure + private void ValidateActionRequest( + XActionRequestToken request, + Exception exception = null + ) + { + // + ValidationProvider + .NotNull(request); + + // + // Prepare Default Exception ... + if (exception == null) + { + exception = XException.InvalidData.ToException(); + } + + // + var isValid = IsValidActionRequest(request); + if (!isValid) + { + throw exception; + } + } + + /// + /// Check Action Request Validation + /// + /// an instance of XActionRequestToken + /// a boolean value + private bool IsValidActionRequest( + XActionRequestToken request + ) + { + // + // Validate Args ... + if (request == null || + Configuration.IdentitySecretKey.IsNullOrEmpty()) + { + XException.InvalidArgs.Throw(); + } + + // + var result = request.Validate(Configuration.IdentitySecretKey); + + // + return result; + } + + /// + /// Check Token and Act Based on it + /// + /// token string + /// specify Excetion to thrown on Issue failure + /// specify Excetion to thrown on failure + /// + private async Task ValidateAndHandleToken( + string token, + bool issueExcepion = false, + Exception exception = null + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(token); + + // + // Prepare Default Exception ... + if (exception == null) + { + exception = XException.InvalidToken.ToException(); + } + + // + var isValidToken = IsValidToken(token); + + // + if (!isValidToken) + { + // + await RemoveTokenByToken(token); + + // + if (issueExcepion) + { + throw exception; + } + } + } + + /// + /// Validate a Hash Exists or not + /// + /// token hash string + /// specify Excetion to thrown on failure + /// + private async Task ValidateHashExists( + string hash, + Exception exception = null + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(hash); + + // + // Prepare Default Exception ... + if (exception == null) + { + exception = XException.InvalidToken.ToException(); + } + + // + var isExists = await IsTokenExistsByHash(hash); + if (!isExists) + { + throw exception; + } + } + + /// + /// Validate a Token Exists + /// + /// token string + /// specify Excetion to thrown on failure + /// + private async Task ValidateTokenExists( + string token, + Exception exception = null + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(token); + + // + // Prepare Default Exception ... + if (exception == null) + { + exception = XException.InvalidToken.ToException(); + } + + // + var isExists = await IsTokenExistsByToken(token); + if (!isExists) + { + throw exception; + } + } + + /// + /// Validate a Hash and Retrieve it's Corresponding Token + /// + /// token hash string + /// specifies chack token Validation Parameter, default is true + /// specify Excetion to thrown on Issue failure + /// specify Excetion to thrown on failure + /// token string + private async Task ValidateAndRetrieveRelatedToken( + string hash, + bool forceTokenValidation = true, + bool issueExcepion = false, + Exception exception = null + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(hash); + + // + // Prepare Default Exception ... + if (exception == null) + { + exception = XException.InvalidToken.ToException(); + } + + // + await ValidateHashExists(hash); + + // + var token = await GetRelatedTokenByHash(hash); + if (token.IsNullOrEmpty()) + { + throw exception; + } + + // + if (forceTokenValidation) + { + await ValidateAndHandleToken(token, issueExcepion); + } + + // + return token; + } + + /// + /// Validate a Token and Retrieve it's Corresponding Hash + /// + /// token string + /// specifies chack token Validation Parameter, default is true + /// specify Excetion to thrown on failure + /// hash string + private async Task ValidateAndRetrieveRelatedHash( + string token, + bool forceTokenValidation = true, + Exception exception = null + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty(token); + + // + // Prepare Default Exception ... + if (exception == null) + { + exception = XException.InvalidToken.ToException(); + } + + // + if (forceTokenValidation) + { + await ValidateAndHandleToken(token); + } + + // + await ValidateTokenExists(token); + + // + var hash = await GetRelatedHashByToken(token); + if (hash.IsNullOrEmpty()) + { + throw exception; + } + + // + return hash; + } + + /// + /// Validate Invitation Hash + /// + /// registration invitation hash string + /// thrown Exception if user Exists, default is true + /// + private async Task ValidateInvitationHash( + string invitationHash, + bool forceUserExists = true + ) + { + // + // Validate Args ... + ValidationProvider.NotNull(Configuration); + + // + if (Configuration.RegistrationJustWithInvite && + invitationHash.IsNullOrEmpty()) + { + XException.NotAllowed.Throw(); + } + + // + // Validate Invitation Token if it isn't null ... + if (!invitationHash.IsNullOrEmpty()) + { + // + await ValidateHashExists(invitationHash); + + // + // Token Validation also done in this task ... + var relatedToken = await ValidateAndRetrieveRelatedToken(invitationHash); + + // + var inviteAction = ValidateAndParseActionToken(relatedToken); + ValidationProvider.NotNull(inviteAction); + + // + var inviteUerSelectByParam = GetUserSelectByParam(inviteAction); + var inviteUserSelectBy = GetUserSelectByType(inviteUerSelectByParam); + if (inviteUserSelectBy != XUserSelectBy.Email) + { + XException.InvalidToken.Throw(); + } + + // + if (forceUserExists) + { + // + await ValidateUserNotExists( + inviteUerSelectByParam, + XException.EmailInUsed.ToException()); + } + } + } + #endregion + + // + #region Profile Validators ... + /// + /// Validate a Request is Corresponding to Specific Device + /// + /// an instance of XDevice + /// specify requested device by an instance of XDevice + /// specify Excetion to thrown on failure + private void ValidateRequestAndGiveDevices( + XDevice device, + XDevice requestDevice, + Exception exception = null + ) + { + // + // Validate Args ... + ValidationProvider.NotNull(device, requestDevice); + + // + // Prepare Default Exception ... + if (exception == null) + { + exception = XException.InvalidDevice.ToException(); + } + + // + var isDevicesSame = device.IsSameAs(requestDevice); + + // + if (!isDevicesSame) + { + throw exception; + } + } + + /// + /// Check Validation of a User SelectBy Param and Password + /// + /// specifies user identifier + /// user's password + /// check a user can log in in system or not, default is true + /// specify Excetion to thrown on failure + /// + private async Task ValidateUserAndPassword( + string userSelectByParam, + string password, + bool checkCanLoginPolicies = true, + Exception exception = null + ) + { + // + // Validate Args ... + ValidationProvider.NotEmpty( + userSelectByParam, + password + ); + + // + // Prepare Default Exception ... + if (exception == null) + { + exception = XException.LoginFailed.ToException(); + } + + // + // Retrieve User and Validate User Exists ... + var user = await ValidateUserExistsAndRetrieve( + userSelectByParam, + containDetails: false, + checkCanLoginPolicies: checkCanLoginPolicies, + ignoreDisabledUser: false, + checkIsBanned: true, + exception: exception); + + // + // Check Given Password Follows Policies ... + await ValidatePasswordPolicies(user, password, exception); + + // + // Check Password for User ... + var checkPasswordResult = await UserManager.CheckPasswordAsync(user, password); + if (!checkPasswordResult) + { + throw exception; + } + } + #endregion + + // + #region Friendship Validators ... + /// + /// Validate Friendship Requested Before or not + /// + /// specifies a user to check as source by an instance of XUser + /// specifies a user to check as dest by an instance of XUser + private void ValidateRequestBefore( + XUser source, + XUser dest + ) + { + // + // Validate Args ... + ValidationProvider.NotNull(source, dest); + + // + var isRequested = IsFollowRequested(source, dest); + if (!isRequested) + { + XException.NotFound.Throw(); + } + } + + /// + /// Validate a Friendship Not Requested Before + /// + /// specifies a user to check as source by an instance of XUser + /// specifies a user to check as dest by an instance of XUser + private void ValidateNotRequestBefore( + XUser source, + XUser dest + ) + { + // + // Validate Args ... + ValidationProvider.NotNull(source, dest); + + // + var isRequested = IsFollowRequested(source, dest); + if (isRequested) + { + XException.Duplicate.Throw(); + } + } + + /// + /// Validate User Can Send Following Request to Dest User + /// + /// specifies a user to check as dest by an instance of XUser + /// specifies a user to check as source by an instance of XUser + private void ValidateFollowingRequest( + XUser accepterUser, + XUser requesterUser + ) + { + // + // Validate Args ... + ValidationProvider.NotNull(accepterUser, requesterUser); + + // + var isRequest = IsFollowRequested(requesterUser, accepterUser, XFriendshipState.Pending); + if (!isRequest) + { + XException.NotFound.Throw(); + } + } + + /// + /// Validate a User can Accept a Following Request or Not + /// + /// specifies a user to check as dest by an instance of XUser + /// specifies a user to check as source by an instance of XUser + private void ValidateFollowingRequestForAccept( + XUser accepterUser, + XUser requesterUser + ) + { + // + // Validate Args ... + ValidationProvider.NotNull(accepterUser, requesterUser); + + // + var isRequestPending = IsFollowRequested(requesterUser, accepterUser, XFriendshipState.Pending); + var isRequestRejected = IsFollowRequested(requesterUser, accepterUser, XFriendshipState.Rejected); + if (!isRequestPending && + !isRequestRejected) + { + XException.NotFound.Throw(); + } + } + + /// + /// Validate a Request is not Passed for Same Users + /// + /// specifies a user to check as source by an instance of XUser + /// specifies a user to check as dest by an instance of XUser + private void ValidateNotSameUsers( + XUser source, + XUser dest + ) + { + // + // Validate ARgs ... + ValidationProvider.NotNull(source, dest); + + // + var isSame = source.Id == dest.Id; + if (isSame) + { + XException.ActionFailed.Throw(); + } + } + + /// + /// Check a user is in Followers of another + /// + /// specifies a user to check as source by an instance of XUser + /// specifies a user to check as dest by an instance of XUser + private void ValidateIsFollowers( + XUser source, + XUser dest + ) + { + // + // Validate Args ... + ValidationProvider.NotNull(source, dest); + + // + ValidateNotSameUsers(source, dest); + ValidateRequestBefore(dest, source); + + // + var isFollower = IsFollowRequested(dest, source, XFriendshipState.Accepted) || + IsFollowRequested(dest, source, XFriendshipState.Blocked); + if (!isFollower) + { + XException.NotFound.Throw(); + } + } + + /// + /// Check a user is in Followings of another + /// + /// specifies a user to check as source by an instance of XUser + /// specifies a user to check as dest by an instance of XUser + private void ValidateIsFollowings( + XUser source, + XUser dest + ) + { + // + // Validate Args ... + ValidationProvider.NotNull(source, dest); + + // + ValidateNotSameUsers(source, dest); + ValidateRequestBefore(source, dest); + + // + var isFollowing = IsFollowRequested(source, dest, XFriendshipState.Accepted); + if (!isFollowing) + { + XException.NotFound.Throw(); + } + } + + /// + /// Validate a User Blocked another + /// + /// specifies a user to check as source by an instance of XUser + /// specifies a user to check as dest by an instance of XUser + private void ValidateIsBlock( + XUser source, + XUser dest + ) + { + // + // Validate Args ... + ValidationProvider.NotNull(source, dest); + + // + ValidateNotSameUsers(source, dest); + ValidateRequestBefore(dest, source); + + // + var isFollower = IsFollowRequested(dest, source, XFriendshipState.Blocked); + if (!isFollower) + { + XException.NotFound.Throw(); + } + } + #endregion + + // + #region XActionRequest Validators ... + /// + /// Validate a Device for Request + /// + /// an instance of XDevice + /// specify requested device by an instance of XDevice + /// specify Excetion to thrown on failure + private void ValidateRequestAndGivenDevices( + XDevice device, + XDevice requestDevice, + Exception exception = null + ) + { + // + // Validate Args ... + ValidationProvider.NotNull(device, requestDevice); + + // + // Prepare Default Exception ... + if (exception == null) + { + exception = XException.InvalidDevice.ToException(); + } + + // + var isDevicesSame = device.IsSameAs(requestDevice); + + // + if (!isDevicesSame) + { + throw exception; + } + } + #endregion + + // + #region XVerification Request ... + /// + /// Validate Verification Request Exists by Device + /// + /// an instance of XDevice + /// specify Excetion to thrown on failure + /// + private async Task ValidateVerificationRequestExists( + XDevice device, + Exception exception = null + ) + { + // + ValidationProvider.NotNull(device); + + // + if (exception == null) + { + exception = XException.InvalidDevice.ToException(); + } + + // + var isExists = await IsDeviceRequestedForVerificationCodeBefor(device); + if (!isExists) + { + throw exception; + } + } + + /// + /// Validate Verification Request Exists by VerificationCode + /// + /// Confirm Verification Code + /// specify Excetion to thrown on failure + /// + private async Task ValidateVerificationRequestExists( + string verificationCode, + Exception exception = null + ) + { + // + ValidationProvider.NotEmpty(verificationCode); + + // + if (exception == null) + { + exception = XException.InvalidVerificationCode.ToException(); + } + + // + var isExists = await IsVerificationCodeRequested(verificationCode); + if (!isExists) + { + throw exception; + } + } + + /// + /// Validate and Retrive Verification Request for Given Device + /// + /// an instance of XDevice + /// specify Excetion to thrown on failure + /// an instance of XVerificationRequest + private async Task ValidateAndRetrieveVerificationRequest( + XDevice device, + Exception exception = null + ) + { + // + ValidationProvider.NotNull(device); + + // + if (exception == null) + { + exception = XException.InvalidDevice.ToException(); + } + + // + await ValidateVerificationRequestExists(device); + + // + var result = await GetVerificationRequest(device); + if (result == null) + { + throw exception; + } + + // + return result; + } + + /// + /// Validate and Retrieve Verification Request for Given VerificationCode + /// + /// Confirm Verification Code + /// specify Excetion to thrown on failure + /// an instance of XVerificationRequest + private async Task ValidateAndRetrieveVerificationRequest( + string verificationCode, + Exception exception = null + ) + { + // + ValidationProvider.NotEmpty(verificationCode); + + // + if (exception == null) + { + exception = XException.InvalidVerificationCode.ToException(); + } + + // + await ValidateVerificationRequestExists(verificationCode); + + // + var result = await GetVerificationRequest(verificationCode); + if (result == null) + { + throw exception; + } + + // + return result; + } + + /// + /// Handle Prepare Verification Request + /// + /// Confirm Verification Code + /// an instance of XActionRequestToken + /// an instance of XVerificationRequest + private async Task HandleVerificationRequestPreparation( + string verificationCode, + XActionRequestToken request + ) + { + // + ValidationProvider + .NotEmpty(verificationCode); + + // + // Check Action Request Validation ... + ValidateActionRequest(request); + + // + var device = request.Context.Device; + + // + // Check is Device Requested Before or not ... + XVerificationRequest xVerificationRequest = null; + var isDeviceRequested = await IsDeviceRequestedForVerificationCodeBefor(device); + if (!isDeviceRequested) + { + // + xVerificationRequest = new XVerificationRequest + { + VerificationCode = verificationCode, + NumberOfTries = 1, + LastTryOn = DateTime.UtcNow, + Device = device, + Request = request + }; + + // + await AddVerificationRequest(xVerificationRequest); + } + else + { + // + // Get Verification Request ... + xVerificationRequest = await ValidateAndRetrieveVerificationRequest(device); + } + + // + return xVerificationRequest; + } + #endregion + } +} \ No newline at end of file diff --git a/Providers/XIdentityManager/XIdentityManager+VerificationCode.cs b/Providers/XIdentityManager/XIdentityManager+VerificationCode.cs new file mode 100644 index 0000000..0e9d45a --- /dev/null +++ b/Providers/XIdentityManager/XIdentityManager+VerificationCode.cs @@ -0,0 +1,220 @@ +using System.Threading.Tasks; +using xCommons.Extensions; +using xIdentityModels.Extensions; +using xIdentityModels.Models; +using xIdentityModels.Navigations; + +namespace xIds.Providers +{ + public partial class XIdentityManager + { + // + #region Verification Code Actions ... + /// + /// Check a Device Requeste for Verification Code before or not + /// + /// an instance of XDevice + /// a boolean value + private async Task IsDeviceRequestedForVerificationCodeBefor( + XDevice device + ) + { + // + var result = false; + var enumerator = DbContext.VerificationRequests + .AsAsyncEnumerable(); + await + foreach (var entity in enumerator) + { + // + if (entity.Device.IsSameAs(device)) + { + // + result = true; + break; + } + } + + // + return result; + } + + /// + /// Check a Verification Code Requested before or not + /// + /// Confirm Verification Code + /// a boolean value + private async Task IsVerificationCodeRequested( + string verificationCode + ) + { + // + // Validate Args ... + if (verificationCode.IsNullOrEmpty()) + { + return false; + } + + // + var result = false; + var enumerator = DbContext.VerificationRequests + .AsAsyncEnumerable(); + await + foreach (var entity in enumerator) + { + // + if (entity.VerificationCode == verificationCode) + { + // + result = true; + break; + } + } + + // + return result; + } + + /// + /// Retrieve Verification Request + /// + /// an instance of XDevice + /// an instance of XVerificationRequest + private async Task GetVerificationRequest( + XDevice device + ) + { + // + XVerificationRequest result = null; + var enumerator = DbContext.VerificationRequests + .AsAsyncEnumerable(); + await + foreach (var entity in enumerator) + { + // + if (entity.Device.IsSameAs(device)) + { + // + result = entity; + break; + } + } + + // + return result; + } + + /// + /// Retrieve Verification Request + /// + /// Confirm Verification Code + /// an instance of XVerificationRequest + private async Task GetVerificationRequest( + string verificationCode + ) + { + // + XVerificationRequest result = null; + var enumerator = DbContext.VerificationRequests + .AsAsyncEnumerable(); + await + foreach (var entity in enumerator) + { + // + if (entity.VerificationCode == verificationCode) + { + // + result = entity; + break; + } + } + + // + return result; + } + + /// + /// Remove Exists Verification Code Request + /// + /// an instance of XDevice + /// save changes on DbContext, default is true + /// + private async Task RemoveVerificationCodeRequestByDevice( + XDevice device, + bool saveChanges = true + ) + { + // + if (!await IsDeviceRequestedForVerificationCodeBefor(device)) + { + return; + } + + // + var xvr = await GetVerificationRequest(device); + DbContext.VerificationRequests.Remove(xvr); + + // + if (saveChanges) + { + await DbContext.SaveChangesAsync(); + } + } + + /// + /// Remove Exists Verification Code Request + /// + /// Confirm Verification Code + /// save changes on DbContext, default is true + /// + private async Task RemoveVerificationRequest( + string verificationCode, + bool saveChanges = true + ) + { + // + ValidationProvider.NotEmpty(verificationCode); + + // + var request = await GetVerificationRequest(verificationCode); + DbContext.VerificationRequests.Remove(request); + + // + if (saveChanges) + { + await DbContext.SaveChangesAsync(); + } + } + + /// + /// Add Verification Code Request + /// + /// an instance of XVerificationRequest + /// save changes on DbContext, default is true + /// an instance of XVerificationRequest + private async Task AddVerificationRequest( + XVerificationRequest request, + bool saveChanges = true + ) + { + // + if (await IsDeviceRequestedForVerificationCodeBefor(request.Device)) + { + await RemoveVerificationCodeRequestByDevice(request.Device); + } + + // + var entry = await DbContext.VerificationRequests.AddAsync(request); + + // + if (saveChanges) + { + await DbContext.SaveChangesAsync(); + } + + // + return entry.Entity; + } + #endregion + } +} \ No newline at end of file diff --git a/Providers/XIdentityMessageProvider.cs b/Providers/XIdentityMessageProvider.cs new file mode 100644 index 0000000..7144645 --- /dev/null +++ b/Providers/XIdentityMessageProvider.cs @@ -0,0 +1,95 @@ +using System.Linq; +using Microsoft.Extensions.Logging; +using xCommons.Extensions; +using xExceptions.Constants; +using xIdentityModels.Configurations; +using xIds.Constants; +using xIds.Interfaces; + +namespace xIds.Providers +{ + public class XIdentityMessageProvider : IXIdentityMessageProvider + { + // + public string InviteMsg { get; set; } + public string RegistrationApproveMsg { get; set; } + public string RegisteredMsg { get; set; } + public string VerificationCodeMsg { get; set; } + public string ChangePasswordMsg { get; set; } + public string PasswordChangedMsg { get; set; } + public string NewDeviceLoggedInMsg { get; set; } + + private readonly ILogger logger; + private readonly XIdentityConfiguration identityConfiguration; + + public XIdentityMessageProvider( + ILogger logger, + XIdentityConfiguration identityConfiguration + ) + { + this.logger = logger; + this.identityConfiguration = identityConfiguration; + } + + /// + /// Determine All things is Ready or Not + /// + /// + public bool IsReady() + { + return !InviteMsg.IsNullOrEmpty() && + !RegistrationApproveMsg.IsNullOrEmpty() && + !RegisteredMsg.IsNullOrEmpty() && + !VerificationCodeMsg.IsNullOrEmpty() && + !ChangePasswordMsg.IsNullOrEmpty() && + !PasswordChangedMsg.IsNullOrEmpty() && + !NewDeviceLoggedInMsg.IsNullOrEmpty(); + } + + /// + /// Retrieve a Translated Value Based on Given Lang + /// + /// + /// + /// + private string GetStringResource(string resourceTitle, string lang) + { + // + var item = identityConfiguration.IdentityMessages + .FirstOrDefault(sr => + sr.Language.ToNormalString() == lang.ToNormalString() && + sr.ResourceTitle.ToNormalString() == resourceTitle.ToNormalString()); + // + if (item == null) + { + return null; + } + + // + return item.TranslatedValue; + } + + /// + /// Prepare Helper Class and Fill Messages + /// with Specific Language + /// + /// + public void PrepareMessages(string lang) + { + // + InviteMsg = GetStringResource(IdentityMessagesKeys.InviteMsg, lang); + RegisteredMsg = GetStringResource(IdentityMessagesKeys.RegisteredMsg, lang); + ChangePasswordMsg = GetStringResource(IdentityMessagesKeys.ChangePasswordMsg, lang); + PasswordChangedMsg = GetStringResource(IdentityMessagesKeys.PasswordChangedMsg, lang); + VerificationCodeMsg = GetStringResource(IdentityMessagesKeys.VerificationCodeMsg, lang); + NewDeviceLoggedInMsg = GetStringResource(IdentityMessagesKeys.NewDeviceLoggedInMsg, lang); + RegistrationApproveMsg = GetStringResource(IdentityMessagesKeys.RegistrationApproveMsg, lang); + + // + if (!IsReady()) + { + XException.InvalidArgs.Throw(); + } + } + } +} \ No newline at end of file diff --git a/Providers/XIdentityProfileService.cs b/Providers/XIdentityProfileService.cs new file mode 100644 index 0000000..a37cd96 --- /dev/null +++ b/Providers/XIdentityProfileService.cs @@ -0,0 +1,51 @@ +using System.Linq; +using System.Threading.Tasks; +using IdentityServer4.Extensions; +using IdentityServer4.Models; +using IdentityServer4.Services; +using Microsoft.AspNetCore.Identity; +using xCommons.Extensions; +using xIdentityModels; +using xIds.Interfaces; + +namespace xIds.Providers +{ + public class XIdentityProfileService : IProfileService + { + private readonly IXIdentityManager identityManager; + private readonly IUserClaimsPrincipalFactory userClaimsPrincipalFacotry; + + public XIdentityProfileService( + IXIdentityManager identityManager, + IUserClaimsPrincipalFactory userClaimsPrincipalFacotry + ) + { + this.identityManager = identityManager; + this.userClaimsPrincipalFacotry = userClaimsPrincipalFacotry; + } + + public async Task GetProfileDataAsync(ProfileDataRequestContext context) + { + // + var sub = context.Subject.GetSubjectId(); + var user = await identityManager.GetUserAsync(sub, true); + var principal = await userClaimsPrincipalFacotry.CreateAsync(user); + + // + var claims = principal.Claims.ToList(); + + // + context.IssuedClaims = claims; + } + + public async Task IsActiveAsync(IsActiveContext context) + { + // + var sub = context.Subject.GetSubjectId(); + var user = await identityManager.GetUserAsync(sub); + + // + context.IsActive = !user.IsNull() && user.IsEnable && !user.IsBanned; + } + } +} \ No newline at end of file diff --git a/Providers/XSecurityProvider.cs b/Providers/XSecurityProvider.cs new file mode 100644 index 0000000..d2dde37 --- /dev/null +++ b/Providers/XSecurityProvider.cs @@ -0,0 +1,168 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using xCommons.Extensions; +using xCommons.Providers; +using xIds.Configurations; +using xIds.Interfaces; + +namespace xIds.Providers +{ + public class XSecurityProvider : IXSecurityProvider + { + // + #region Props ... + private readonly string secretKey; + private readonly byte[] iv; + private readonly byte[] key; + private readonly XValidationProvider validationProvider; + #endregion + + // + #region Constructor ... + public XSecurityProvider( + XIdentityResourceConfiguration identityConfiguration, + XValidationProvider validationProvider + ) + { + // + this.secretKey = identityConfiguration.XRevisionSecretKey.ToMd5String(); + this.validationProvider = validationProvider; + + // + var ivBytes = new byte[16]; + var keyBytes = new byte[32]; + var secretBytes = Encoding.UTF8.GetBytes(this.secretKey); + + // + Array.Copy( + secretBytes, + ivBytes, + secretBytes.Length < ivBytes.Length ? + secretBytes.Length : + ivBytes.Length + ); + this.iv = ivBytes; + + // + Array.Copy( + secretBytes, + keyBytes, + secretBytes.Length < keyBytes.Length ? + secretBytes.Length : + keyBytes.Length + ); + this.key = keyBytes; + + // Encoding.UTF8.GetBytes (this.secretKey); + } + #endregion + + // + #region Actions ... + public string Encrypt(string plainText) + { + // + validationProvider.NotEmpty(plainText); + + // + var textBytes = Encoding.UTF8.GetBytes(plainText); + var result = EncryptFromBytes(textBytes); + + // + return result; + } + + public string Decrypt(string cipherText) + { + // + validationProvider.NotEmpty(cipherText); + + // + var cipherBytes = Convert.FromBase64String(cipherText); + var result = DecryptFromBytes(cipherBytes); + + // + return result; + } + + public string EncryptFromBytes(byte[] textBytes) + { + // + // Validate Args ... + validationProvider.NotNull(textBytes); + + // Declare the string used to hold + // the decrypted text. + byte[] encryptedBytes = null; + + // Create an RijndaelManaged object + // with the specified key and IV. + using (var rijAlg = new RijndaelManaged()) + { + //Settings + rijAlg.Mode = CipherMode.CBC; + rijAlg.Padding = PaddingMode.PKCS7; + // rijAlg.FeedbackSize = 128; + + rijAlg.Key = key; + rijAlg.IV = iv; + + // + using (var encryptor = rijAlg.CreateEncryptor(rijAlg.Key, rijAlg.IV)) + { + encryptedBytes = encryptor.TransformFinalBlock(textBytes, 0, textBytes.Length); + } + } + + // + return Convert.ToBase64String(encryptedBytes); + } + + public string DecryptFromBytes(byte[] cipherBytes) + { + // + // Validate Args ... + validationProvider.NotNull(cipherBytes); + + // Declare the string used to hold + // the decrypted text. + string plaintext = null; + + // Create an RijndaelManaged object + // with the specified key and IV. + using (var rijAlg = new RijndaelManaged()) + { + //Settings + rijAlg.Mode = CipherMode.CBC; + rijAlg.Padding = PaddingMode.PKCS7; + // rijAlg.FeedbackSize = 128; + + rijAlg.Key = key; + rijAlg.IV = iv; + + // Create a decrytor to perform the stream transform. + var decryptor = rijAlg.CreateDecryptor(rijAlg.Key, rijAlg.IV); + + // Create the streams used for decryption. + using (var msDecrypt = new MemoryStream(cipherBytes)) + { + using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) + { + using (var srDecrypt = new StreamReader(csDecrypt)) + { + // Read the decrypted bytes from the decrypting stream + // and place them in a string. + plaintext = srDecrypt.ReadToEnd(); + } + } + } + } + + // + return plaintext; + } + #endregion + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..faf7c3d --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# xSaherElm.xIds + +this project is an Identity Server for xSaherElm projects which provides Authentication and +Authorization of User's. + +## Maintainer + +Hadi Khazaee asl + +[https://www.saherelm.ir](https://www.saherelm.ir) + +[hadi_khazaee_asl@yahoo.com](mailto:hadi_khazaee_asl@yahoo.com) diff --git a/Startup.cs b/Startup.cs new file mode 100644 index 0000000..3c14006 --- /dev/null +++ b/Startup.cs @@ -0,0 +1,123 @@ +using System; +using System.IO; +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using xCommons.Configurations; +using xCommons.Extensions; +using xStorageService.DI; +using xIds.Constants; +using xIds.DI; +using xIds.Interfaces; +using xIds.Providers; + +namespace xIds +{ + public class Startup + { + public IConfiguration Configuration { get; } + + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public void ConfigureServices(IServiceCollection services) + { + // + // Register Validation Provider and all XCommons Module Services ... + services.AddXCommons(); + + // + // Register App Configuration ... + services.AddXAppConfiguration(Configuration); + var appConfiguration = services.GetRegisteredService(); + + // + // Register XDataService Configuration ... + services.AddXDataServiceConfiguration(Configuration, ConnectionStringNames.IDENTITY_CONNECTION_NAME); + + // + // Register Allowed Origins ... + services.AddXCors(appConfiguration.AllowedOrigins); + + // + // Register Swagger ... + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + services.AddXSwagger(Configuration, xmlFilePath: xmlPath); + + // + // Register IdentityServer to DI ... + // services.AddInMemoryXIdentityServer (Configuration); + services.AddEfSupportXIdentityServer(Configuration); + + // + services.AddControllers() + .AddNewtonsoftJson(x => x.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore); + + // + // Register Authentication ... + services.AddXIdentityServerAuthentication(Configuration); + + // + // Register Authorization ... + services.AddXAuthorization(); + + // + // Register Security Provider ... + services.AddSingleton(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger logger) + { + // + if (env.IsDevelopment()) + { + // + app.UseDeveloperExceptionPage(); + + // + // app.UseXRequestLogger(); + } + + // + // Use Swagger Middleware ... + app.UseXSwagger(); + + // + app.UseHttpsRedirection(); + app.UseXForwardOptions(); + + // + // Using Cors ... + app.UseXCors(); + + // + // Force App to Use IdentityServer ... + app.UseXIdentityServer(logger); + + // + // Force app to Use Storage Service ... + app.UseXStorageService(); + + // + app.UseRouting(); + + // + app.UseAuthentication(); + app.UseAuthorization(); + + // + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + }); + } + } +} diff --git a/Stores/XPersistedGrantStore.cs b/Stores/XPersistedGrantStore.cs new file mode 100644 index 0000000..a2af0f6 --- /dev/null +++ b/Stores/XPersistedGrantStore.cs @@ -0,0 +1,151 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using IdentityServer4.EntityFramework.DbContexts; +using IdentityServer4.EntityFramework.Mappers; +using IdentityServer4.Models; +using IdentityServer4.Stores; +using Microsoft.EntityFrameworkCore; + +namespace xIds.Stores +{ + public class XPersistedGrantStore : IPersistedGrantStore + { + private readonly PersistedGrantDbContext dbContext; + + public XPersistedGrantStore(PersistedGrantDbContext dbContext) + { + this.dbContext = dbContext; + } + + public async Task> GetAllAsync(string subjectId) + { + // + var result = (await dbContext.PersistedGrants + .Where(g => g.SubjectId == subjectId) + .AsNoTracking() + .Select(res => res.ToModel()) + .ToListAsync()).AsEnumerable(); + + // + return result; + } + + public async Task GetAsync(string key) + { + // + var result = await dbContext.PersistedGrants + .FirstOrDefaultAsync(g => g.Key == key); + + // + return result.ToModel(); + } + + public async Task RemoveAllAsync(string subjectId, string clientId) + { + // + var items = await dbContext.PersistedGrants + .Where(g => + g.SubjectId == subjectId && + g.ClientId == clientId) + .ToListAsync(); + + // + dbContext.PersistedGrants.RemoveRange(items); + + // + await dbContext.SaveChangesAsync(); + } + + public async Task RemoveAllAsync(string subjectId, string clientId, string type) + { + // + var items = await dbContext.PersistedGrants + .Where(g => + g.SubjectId == subjectId && + g.ClientId == clientId && + g.Type == type) + .ToListAsync(); + + // + dbContext.PersistedGrants.RemoveRange(items); + + // + await dbContext.SaveChangesAsync(); + } + + public async Task RemoveAsync(string key) + { + // + var items = await dbContext.PersistedGrants + .Where(g => g.Key == key) + .ToListAsync(); + + // + dbContext.PersistedGrants.RemoveRange(items); + + // + await dbContext.SaveChangesAsync(); + } + + public async Task IsExistsAsync(string key) + { + return await this.dbContext.PersistedGrants.AnyAsync(g => g.Key == key); + } + + public async Task IsExistsAsync(PersistedGrant grant) + { + return await IsExistsAsync(grant.Key); + } + + public async Task AddOrUpdateAsync(PersistedGrant grant) + { + // + var isExists = await IsExistsAsync(grant); + if (isExists) + { + await UpdateAsync(grant); + } + else + { + await AddAsync(grant); + } + } + + public async Task AddAsync(PersistedGrant grant) + { + // + var item = grant.ToEntity(); + await dbContext.PersistedGrants.AddAsync(item); + await dbContext.SaveChangesAsync(); + } + + public async Task UpdateAsync(PersistedGrant grant) + { + // + var item = grant.ToEntity(); + var entity = dbContext.Attach(item); + entity.State = EntityState.Modified; + await dbContext.SaveChangesAsync(); + } + + public async Task StoreAsync(PersistedGrant grant) + { + await AddOrUpdateAsync(grant); + } + + // + // TODO: Complete this ... + public Task> GetAllAsync(PersistedGrantFilter filter) + { + throw new System.NotImplementedException(); + } + + // + // TODO: Complete this ... + public Task RemoveAllAsync(PersistedGrantFilter filter) + { + throw new System.NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Validators/XResourceOwnerPasswordValidator.cs b/Validators/XResourceOwnerPasswordValidator.cs new file mode 100644 index 0000000..b4ece99 --- /dev/null +++ b/Validators/XResourceOwnerPasswordValidator.cs @@ -0,0 +1,193 @@ +using System; +using System.Threading.Tasks; +using IdentityServer4.Events; +using IdentityServer4.Models; +using IdentityServer4.Services; +using IdentityServer4.Validation; +using Microsoft.Extensions.Logging; +using xCommons.Extensions; +using xExceptions.Constants; +using xIdentityModels.Navigations; +using xIds.Interfaces; +using static IdentityModel.OidcConstants; + +namespace xIds.Validators +{ + public class XResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator + { + private readonly IXIdentityManager identityManager; + private readonly IEventService events; + private readonly ILogger logger; + + public XResourceOwnerPasswordValidator( + IXIdentityManager identityProvider, + IEventService events, + ILogger logger + ) + { + this.identityManager = identityProvider; + this.events = events; + this.logger = logger; + } + + /// + /// Validates the resource owner password credential + /// by providing UserName/Email or PhoneNumber + /// + /// The context. + /// + public virtual async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) + { + // + var clientId = context.Request?.Client?.ClientId; + var xUser = await identityManager.GetUserAsync(context.UserName, true); + + // + // Check Force Data ... + var isForced = true; + var isForcedStr = context.Request?.Raw["force"]; + if (!isForcedStr.IsNullOrEmpty()) + { + isForced = isForcedStr.FromJSON(); + } + + // + XDevice device = null; + var language = context.Request?.Raw["language"]; + var deviceStr = context.Request?.Raw["device"]; + if (deviceStr.IsNullOrEmpty() && isForced) + { + // + logger.LogError($"Device not Found ..."); + + // + await events + .RaiseAsync( + new UserLoginFailureEvent( + xUser.UserName, + XException.InvalidDevice.ToXError().Message, + false, + clientId + )); + return; + } + device = isForced ? deviceStr.FromJSON() : null; + + // + XException exception; + if (!xUser.IsNull()) + { + // + var result = await identityManager + .CheckPasswordSignInAsync( + xUser, + context.Password, + device, + language, + true, + isForced + ); + if (result.Succeeded) + { + // + logger.LogInformation("Credentials validated for username: {username}", xUser.UserName); + + // + // Update User Last Login ... + xUser.LastLogin = DateTime.UtcNow; + await identityManager.UpdateUserAsync(xUser); + + // + await events.RaiseAsync( + new UserLoginSuccessEvent( + xUser.UserName, xUser.Id, xUser.UserName, false, clientId + )); + + // + context.Result = new GrantValidationResult(xUser.Id, AuthenticationMethods.Password); + return; + } + else if (result.IsLockedOut) + { + // + logger.LogInformation("Authentication failed for username: {username}, reason: locked out", xUser.UserName); + + // + exception = XException.AccountLockedOut; + await events + .RaiseAsync( + new UserLoginFailureEvent( + xUser.UserName, + exception.ToXError().Message, + false, + clientId + )); + } + else if (result.IsNotAllowed) + { + // + logger.LogInformation("Authentication failed for username: {username}, reason: not allowed", xUser.UserName); + + // + exception = XException.NotAllowed; + await events + .RaiseAsync( + new UserLoginFailureEvent( + xUser.UserName, + exception.ToXError().Message, + false, + clientId + )); + } + else + { + // + logger.LogInformation("Authentication failed for username: {username}, reason: invalid credentials", xUser.UserName); + + // + exception = XException.LoginFailed; + await events + .RaiseAsync( + new UserLoginFailureEvent( + xUser.UserName, + exception.ToXError().Message, + false, + clientId + )); + + // + + } + } + else + { + // + logger.LogInformation("No user found matching username: {username}", xUser.UserName); + + // + exception = XException.InvalidUserName; + await events + .RaiseAsync( + new UserLoginFailureEvent( + xUser.UserName, + exception.ToXError().Message, + false, + clientId + )); + } + + // + Exception ecs = exception.ToException(); + if (exception == XException.AccountLockedOut && xUser.LockoutEnd.HasValue) + { + // + var passedTime = xUser.LockoutEnd.Value.UtcDateTime; + ecs = XException.AccountLockedOut + .AddContentToException(passedTime.ToString()); + } + + // + context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, ecs.Message); + } + } +} \ No newline at end of file diff --git a/appsettings.Development.json b/appsettings.Development.json new file mode 100644 index 0000000..d894880 --- /dev/null +++ b/appsettings.Development.json @@ -0,0 +1,177 @@ +{ + "AllowedOrigins": [ + "https://localhost:5001", + "https://localhost:6001", + "https://saherelm.ir", + "https://api.saherelm.ir", + "https://saherelmhub.ir", + "https://api.saherelmhub.ir", + "https://192.168.1.15:5001", + "https://192.168.1.110:5001" + ], + "IdentityResourceConfiguration": { + "Authority": "https://localhost:4001", + "ApiName": "xSaherElmAPI", + "ApiSecret": "s@H@1694056", + "ClientId": "xSaherElmAPIClient", + "ClientSecret": "s@H@1694056", + "XPoweredValue": "SaherElmITCenter", + "XRevisionSecretKey": "SaherElmITCenter@1694056" + }, + "StorageConfiguration": { + "Authority": "https://localhost:4001", + "IdentityAuthority": "https://localhost:4001", + "Storage": "Storage", + "Temp": "Temp", + "Thumb": "Thumb", + "Uploads": "Uploads", + "Widgets": "Widgets", + "Image": "Image", + "Audio": "Audio", + "Video": "Video", + "Document": "Document", + "FilePrefix": "xSaherElm_", + "ThumbPrefix": "Thumb_", + "ThumbSize": 512, + "ThumbQuality": 72, + "MaxFileSize": 41943040 + }, + "DbSeeder": { + "UpdateExists": false, + "Clients": [ + { + "ClientId": "xSaherElmAPIClient", + "ClientName": "Web Client of xSaherElm API", + "ClientSecrets": [ + { + "Description": null, + "Value": "NKaAKXeE9Ws+VVGGHsmM9DUO84cla4+hvx8swP1uuEg=", + "Expiration": null, + "Type": "SharedSecret" + } + ], + "Enabled": true, + "RedirectUris": [ + "https://localhost:5001/signin-oidc" + ], + "AllowedGrantTypes": [ + "hybrid", + "password", + "client_credentials" + ], + "AllowedScopes": [ + "IdentityServerApi", + "openid", + "profile", + "XAPI.read", + "XAPI.write", + "XAPI.admin", + "manage", + "offline_access" + ], + "AllowedCorsOrigins": [ + "https://localhost:5001", + "https://localhost:6001", + "http://saherelm.ir", + "https://saherelm.ir", + "http://saherelmhub.ir", + "https://saherelmhub.ir" + ], + "AllowOfflineAccess": true, + "RefreshTokenUsage": 1, + "AccessTokenType": 1, + "RefreshTokenExpiration": 0, + "AbsoluteRefreshTokenLifetime": 2592000, + "SlidingRefreshTokenLifetime": 1296000, + "UpdateAccessTokenClaimsOnRefresh": true, + "IdentityTokenLifetime": 300, + "AccessTokenLifetime": 3600, + "AuthorizationCodeLifetime": 300, + "DeviceCodeLifetime": 300 + } + ], + "IdentityResources": [ + "openid", + "profile", + "email", + "role" + ], + "ApiResources": [ + { + "Name": "IdentityServerApi" + }, + { + "Name": "xSaherElmAPI", + "DisplayName": "xSaherElm API", + "Description": "Protected by IdentityServer API Access", + "ApiSecrets": [ + { + "Description": null, + "Value": "NKaAKXeE9Ws+VVGGHsmM9DUO84cla4+hvx8swP1uuEg=", + "Expiration": null, + "Type": "SharedSecret" + } + ], + "Enabled": true, + "Scopes": [ + "openid", + "profile", + "XAPI.read", + "XAPI.write", + "XAPI.admin", + "manage" + ], + "UserClaims": [ + "name", + "role", + "unique_name", + "gender", + "birthdate", + "picture", + "email", + "phone_number", + "email_verified", + "phone_number_verified", + "is_enabled", + "is_banned", + "creation_date", + "last_login", + "given_name", + "family_name" + ] + } + ], + "ApiScopes": [ + { + "Name": "IdentityServerApi" + }, + { + "Name": "xSaherElmAPI" + }, + { + "Name": "XAPI.read", + "DisplayName": "Read Access for XAPI" + }, + { + "Name": "XAPI.write", + "DisplayName": "Write Access for XAPI" + }, + { + "Name": "XAPI.admin", + "DisplayName": "Admin Access for XAPI" + }, + { + "Name": "manage", + "DisplayName": "Admin Access for All APIs" + } + ] + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..69ed0cd --- /dev/null +++ b/appsettings.json @@ -0,0 +1,397 @@ +{ + "Version": "0.1", + "XPoweredValue": "SaherElmITCenter", + "Name": "xIdentityServer", + "WelcomeMessage": "Welcome to xSaherElm Project's xIdentityServer", + "AllowedOrigins": [ + "https://saherelm.ir", + "https://saherelmhub.ir", + "https://api.saherelm.ir", + "https://api.saherelmhub.ir" + ], + "SwaggerConfiguration": { + "Version": "v1.0", + "Title": "xSaherElm Identity Server API", + "Description": "Complete API Documentation", + "Contact": { + "Name": "Hadi Khazaee Asl", + "Email": "hadi_khazaee_asl@yahoo.com", + "Url": "https://www.saherelm.ir" + } + }, + "DbProvider": "SQLite", + "ConnectionStrings": { + "IdentityDb": "Filename=./Db/XIdentity.db;Foreign Keys=False", + "ConfigurationDb": "Filename=./Db/XConfigurations.db", + "PersistedGrantDb": "Filename=./Db/XPersistedGrants.db" + }, + "DataServiceConfiguration": { + "PagingConfiguration": { + "DefaultPageSize": 20, + "MaxAvailablePageSize": 400, + "MinAvailablePageSize": 20 + } + }, + "Certificates": { + "IdentityServerSigning": { + "Path": "Certificate/xIds.pfx", + "Secret": "s@1694056" + } + }, + "IdentityConfiguration": { + "IdentityIssuer": "SaherElm Identity Server", + "IdentityAudience": "SaherElmIdentityAPI", + "IdentitySecretKey": "SaherElmITCenter@1694056", + "ActionTokenExpirationDateProvider": "20m", + "IdentityRoles": [ + "admin", + "responsible_manager", + "journalist", + "chief_clerk", + "chief_clerk_assistant", + "actuary", + "actuary_assistant", + "reporter", + "reporter_assistant", + "agent", + "user" + ], + "AutoConfirmNewUsersEmail": false, + "AutoConfirmNewUsersPhoneNumber": false, + "RegistrationJustWithInvite": false, + "RequireRegistrationConfirm": true, + "MaxNumberOfVerificationCodeSend": 3, + "DeleyBetweenTwoVerificationCode": 180, + "NewUsersRole": "user", + "Policy": { + "Lockout": { + "LockoutTimeSpanProvider": "15m", + "MaxFailedAccessAttempts": 5, + "AllowedForNewUsers": true + }, + "Password": { + "RequireDigit": true, + "RequiredLength": 6, + "RequiredUniqueChars": 1, + "RequireLowercase": true, + "RequireNonAlphanumeric": true, + "RequireUppercase": true + }, + "SignIn": { + "RequiredEnabled": true, + "RequireConfirmedEmail": true, + "RequireConfirmedPhoneNumber": false + }, + "User": { + "AllowedUserNameCharacters": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._", + "RequireUniqueEmail": true, + "MinLength": 6, + "MaxLength": 20, + "MinAgeForRegistration": 18, + "MaxAgeForRegistration": 60, + "InvalidUserNames": [ + "saherelm", + "admin", + "responsible_manager", + "journalist", + "chief_clerk", + "chief_clerk_assistant", + "actuary", + "actuary_assistant", + "reporter", + "reporter_assistant", + "agent", + "user" + ] + }, + "Profile": { + "ContainsEmail": false, + "ContainsPhoneNumber": false, + "ContainsRoles": true, + "ContainsLastLogin": false, + "ContainsDateOfBirth": false, + "ContainsCreationDate": true + } + }, + "IdentityMessages": [ + { + "Language": "fa-IR", + "ResourceTitle": "user_invite_msg", + "TranslatedValue": "شما دعوت شده اید تا در سامانه ثبت نام کنید" + }, + { + "Language": "en-US", + "ResourceTitle": "user_invite_msg", + "TranslatedValue": "You are Invited to Register at Dashboard" + }, + { + "Language": "fa-IR", + "ResourceTitle": "registration_approve_msg", + "TranslatedValue": "شما باید حساب کاربری خود را با ورود به پیوند زیر فعال کنید" + }, + { + "Language": "en-US", + "ResourceTitle": "registration_approve_msg", + "TranslatedValue": "you must activate your account by going to this link" + }, + { + "Language": "fa-IR", + "ResourceTitle": "registered_msg", + "TranslatedValue": "ثبت نام شما در سامانه با موفقیت پایان یافت" + }, + { + "Language": "en-US", + "ResourceTitle": "registered_msg", + "TranslatedValue": "you successfully registered at Dashboard" + }, + { + "Language": "fa-IR", + "ResourceTitle": "verification_code_msg", + "TranslatedValue": "کد اعتبار سنجی شما در سامانه" + }, + { + "Language": "en-US", + "ResourceTitle": "verification_code_msg", + "TranslatedValue": "Your Verification Code at Dashboard" + }, + { + "Language": "fa-IR", + "ResourceTitle": "change_password_msg", + "TranslatedValue": "برای تغییر کلمه عبور پیوند زیر را دنبال کنید" + }, + { + "Language": "en-US", + "ResourceTitle": "change_password_msg", + "TranslatedValue": "for change password please follow this link" + }, + { + "Language": "fa-IR", + "ResourceTitle": "password_changed_msg", + "TranslatedValue": "کلمه عبور شما در سامانه تغییر یافت" + }, + { + "Language": "en-US", + "ResourceTitle": "password_changed_msg", + "TranslatedValue": "your password at Dashboard changed" + }, + { + "Language": "fa-IR", + "ResourceTitle": "new_device_logged_in_msg", + "TranslatedValue": "ابزار جدیدی با مشخصات ذیل در سامانه با حساب کاربری شما وارد شده است" + }, + { + "Language": "en-US", + "ResourceTitle": "new_device_logged_in_msg", + "TranslatedValue": "new device with following details loggedin with your account in Dashboard" + } + ] + }, + "IdentityResourceConfiguration": { + "Authority": "https://172.18.0.151", + "ApiName": "xSaherElmAPI", + "ApiSecret": "s@H@1694056", + "ClientId": "xSaherElmAPIClient", + "ClientSecret": "s@H@1694056", + "XPoweredValue": "SaherElmITCenter", + "XRevisionSecretKey": "SaherElmITCenter@1694056" + }, + "StorageConfiguration": { + "Authority": "https://ids.saherelm.ir", + "IdentityAuthority": "https://ids.saherelm.ir", + "Storage": "Storage", + "Temp": "Temp", + "Thumb": "Thumb", + "Uploads": "Uploads", + "Widgets": "Widgets", + "Image": "Image", + "Audio": "Audio", + "Video": "Video", + "Document": "Document", + "FilePrefix": "xSaherElm_", + "ThumbPrefix": "Thumb_", + "ThumbSize": 512, + "ThumbQuality": 72, + "MaxFileSize": 41943040 + }, + "MessageConfiguration": { + "MailConfigurations": { + "gmail": { + "Title": "SaherElm Dashboard", + "Host": "smtp.gmail.com", + "Port": 587, + "EnableSsl": true, + "Username": "saherelm@gmail.com", + "Password": "vzuz gtrk tndb ctbc" + } + }, + "SmsConfigurations": { + "yoursms": { + "ServiceUrl": "http://sms.yourprovider.ir", + "Username": "user", + "Password": "psw", + "LineNumber": "linenumber", + "UserApiKey": "", + "SecretKey": "" + } + } + }, + "DbSeeder": { + "UpdateExists": false, + "Clients": [ + { + "ClientId": "xSaherElmAPIClient", + "ClientName": "Web Client of xMabsut API", + "ClientSecrets": [ + { + "Description": null, + "Value": "NKaAKXeE9Ws+VVGGHsmM9DUO84cla4+hvx8swP1uuEg=", + "Expiration": null, + "Type": "SharedSecret" + } + ], + "Enabled": true, + "RedirectUris": ["https://localhost:5001/signin-oidc"], + "AllowedGrantTypes": [ + "hybrid", + "password", + "client_credentials" + ], + "AllowedScopes": [ + "IdentityServerApi", + "openid", + "profile", + "XAPI.read", + "XAPI.write", + "XAPI.admin", + "manage", + "offline_access" + ], + "AllowedCorsOrigins": [ + "http://saherelm.ir", + "https://saherelm.ir", + "http://saherelmhub.ir", + "https://saherelmhub.ir" + ], + "AllowOfflineAccess": true, + "RefreshTokenUsage": 1, + "AccessTokenType": 1, + "RefreshTokenExpiration": 0, + "AbsoluteRefreshTokenLifetime": 2592000, + "SlidingRefreshTokenLifetime": 1296000, + "UpdateAccessTokenClaimsOnRefresh": true, + "IdentityTokenLifetime": 300, + "AccessTokenLifetime": 3600, + "AuthorizationCodeLifetime": 300, + "DeviceCodeLifetime": 300 + } + ], + "IdentityResources": ["openid", "profile", "email", "role"], + "ApiResources": [ + { + "Name": "IdentityServerApi" + }, + { + "Name": "xSaherElmAPI", + "DisplayName": "xSaherElm API", + "Description": "Protected by IdentityServer API Access", + "ApiSecrets": [ + { + "Description": null, + "Value": "NKaAKXeE9Ws+VVGGHsmM9DUO84cla4+hvx8swP1uuEg=", + "Expiration": null, + "Type": "SharedSecret" + } + ], + "Enabled": true, + "Scopes": [ + "openid", + "profile", + "XAPI.read", + "XAPI.write", + "XAPI.admin", + "manage" + ], + "UserClaims": [ + "name", + "role", + "unique_name", + "gender", + "birthdate", + "picture", + "email", + "phone_number", + "email_verified", + "phone_number_verified", + "is_enabled", + "is_banned", + "creation_date", + "last_login", + "given_name", + "family_name" + ] + } + ], + "ApiScopes": [ + { + "Name": "IdentityServerApi" + }, + { + "Name": "xSaherElmAPI" + }, + { + "Name": "XAPI.read", + "DisplayName": "Read Access for XAPI" + }, + { + "Name": "XAPI.write", + "DisplayName": "Write Access for XAPI" + }, + { + "Name": "XAPI.admin", + "DisplayName": "Admin Access for XAPI" + }, + { + "Name": "manage", + "DisplayName": "Admin Access for All APIs" + } + ], + "Users": [ + { + "Id": "b5713f9f-34f4-49b2-9680-b71e2b2de969", + "FirstName": "Hadi", + "LastName": "Khazaee Asl", + "UserName": "admin", + "Email": "hadi_khazaee_asl@yahoo.com", + "EmailConfirmed": true, + "PhoneNumber": "+989121694056", + "PhoneNumberConfirmed": true, + "Password": "s@H@1694056", + "Role": "admin", + "DateOfBirth": "1982-09-17 20:30:00.0000000", + "IsEnable": true + }, + { + "Id": "83b09f5e-edd2-4e9a-8c5f-c4c129ee9d52", + "FirstName": "Hadi", + "LastName": "Khazaee Asl", + "UserName": "agent", + "Email": "hadi.khazaee.asl@gmail.com", + "EmailConfirmed": true, + "PhoneNumber": "+989101879789", + "PhoneNumberConfirmed": true, + "Password": "a@A@1694056", + "Role": "agent", + "DateOfBirth": "1982-09-17 20:30:00.0000000", + "IsEnable": true + } + ] + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..807eec5 --- /dev/null +++ b/nuget.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/tempkey.jwk b/tempkey.jwk new file mode 100644 index 0000000..9508d98 --- /dev/null +++ b/tempkey.jwk @@ -0,0 +1 @@ +{"alg":"RS256","d":"ArwBivGf5QZR7tala9B6BmHJOVbPp_MQCGa1h9bXRhzZjpSC2Ppq3pFMLvu089GXvbgjgR5o_ORyP9OIKtvHkNKtwMV_z_Om3C864pAr4i1RdRtc5pwYNhFEZkEY8bZ6ne4KzTcqSZGw9rXjSWTna95bifHwM9m_UGtcHwljJnd-SXJjxRUWFTkCut1u1ACmhvuQeEQrHrJbL5pVBli80FinQzqG4CNGpk9pjiBPRfenp1ctzBSggIsX_lmCLHkvXPxl9Yy4i01DdOLCJUtYmf49TjP-MWUTz9IccrMJoX2iuh9iHaCafx5qq7k8L42DFR1tQS4hp3sKPQ3kXFqfkQ","dp":"TP1dStEZx2EeZF5Sz3PZJEqxFHe5Xa3zmhiGCe2Jm2wRZ04XOxzd2xw_cUoftAxemeMpRXq7KMarrD4WMFswX1yLK3LVO9WpdvEBBR7f-SIpVbKYdhP3P1zZqwudih6hbjaPd03G4CesbuActwtirc0323nxUXuZbTvkLztbYyc","dq":"sEXhyXCs-mDNNM_qKRCh7zIztQfc8LEFYXckZSk_1mnDA3WNQRqXXdXbLw0JkBBTHlALZoue8lfTyMfjqFkpSrl06qMKVU3SkF69omvemnuNeCKMFqItEkcynRuqf3vYsurxM6bXf57WdgDc708ZftWdXl9oo0baFh74yIlgsBk","e":"AQAB","kid":"38954A2B1CBC6B8ADFC020D8A81E7947","kty":"RSA","n":"rKTQ4bS9E8h_i7v-okGQPhqJ5V9xovOqCoqWlBGJ2osETHi05itedHMdK99t4Q8usI5cDkiI9AW1F59R-hFeXEyYxhrHoiYuU2HtstG-e5xFp2qLlbO_bGNCeLlEROnigWVMRYp7fsL3trb_VGTTTeFFn6-iuPVvhFvhBcxBEuisLtxmsywT58ucxdQa-2CDnA4UmGp8zI0BZPYGAdEXoCqlobtURdiSW8X1rxk96dHTy3sDzwofd-SWlJHcxz4DP6lCHTfxk3cxBY0KYTJbOhyzASiV8wJ0oMFQBeSB_5U4r0tOBIYZ_d_KwE_LQ-oxw-XN3Uv_qsy9w_0cy_FqEw","p":"1qnzbvQ2EdPz_gD4pVvf5NtoxqqDKxblhhWLYYFUQTEYmLvsoGh62oYv1F1e_CJm3DgwoDBzkrcAITdBmmYwqxcr7K3J_IZIN0R-tfLrqoE4L8pOwF3lvqGo7w5kzpaI3ofrgEfGjvypq698r3AqnqQ8saKKHgEvqJQ5aM1OyEc","q":"zeNyx4l6aBr4rhcw3lUmOTtxpcVMlo1WH5McjaUMFsh7Nd33nf9dB9gJlddu79SBwBXMVASWZ_bBzFCclb5viuvgvQgr1nbJq8hm3I52j9ZH1H4Nh05DYW2aiDo3HFf3FqMng3_mesu26cJrjBhir5Pxhx3OgBfuihCoOZs_gdU","qi":"i1RttjN0gRVILTzVdBZdBieXh5GYDiDcSvTy-rf4pg-6JBs5p7DyGFr9Fu5k5HZmzdqBecHTov-pZFC5PdDIUAU2DXaSWK0-UFhbNBxDgm8CcT_pW3_DjCve162Zb8QcZRLcg1jzovlQVwzw897ixJOlhm4JRDNKts_IHL6R4Do"} \ No newline at end of file diff --git a/xIds.csproj b/xIds.csproj new file mode 100644 index 0000000..68b7b8d --- /dev/null +++ b/xIds.csproj @@ -0,0 +1,73 @@ + + + + netcoreapp3.1 + xSaherElm.xIds + 1.0.0 + Hadi Khazaee Asl + SaherElm IT Center + + this project is an Identity Server for xSaherElm projects which provides Authentication and + Authorization of User's. + + + + false + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + true + true + $(NoWarn);1591 + + + + + + + \ No newline at end of file