From 6f5db8d039fd0a5aa731768d218a0a9418bb460b Mon Sep 17 00:00:00 2001 From: Musa Demir Date: Wed, 18 Nov 2020 01:06:27 +0300 Subject: [PATCH 01/13] add documents about how to sing in without specifying tenant (mvc) --- ...e-Mvc-Sign-In-Without-Specifying-Tenant.md | 243 ++++++++++++++++++ .../images/login-page-with-tenant-change.png | Bin 0 -> 23659 bytes .../login-page-without-tenant-change.png | Bin 0 -> 20251 bytes 3 files changed, 243 insertions(+) create mode 100644 docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md create mode 100644 docs/en/images/login-page-with-tenant-change.png create mode 100644 docs/en/images/login-page-without-tenant-change.png diff --git a/docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md b/docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md new file mode 100644 index 00000000..3d1cfb29 --- /dev/null +++ b/docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md @@ -0,0 +1,243 @@ +## Sign In Without Specifying Tenant + +Normally, **ASP.NET Zero** uses tenant information in login transactions. This document shows you how to implement tenant information independent login. + +***Important Note:*** *To implement that, your user's email addresses have to be unique. Otherwise, that solution may not works properly.* + +#### Updating LogInManager + +* First of all, open `LogInManager`. *(It is located in **aspnet-core\src\\[YOURAPPNAME].Application\Authorization** folder.)* + +* Add lines of codes shown below + + ``````csharp + UserStore _userStore + public LogInManager( + //.... + UserStore userStore + ){ + _userStore = userStore; + } + + [UnitOfWork] + public async Task> LoginAsync(UserLoginInfo login) + { + var result = await LoginAsyncInternal(login); + await SaveLoginAttemptAsync(result, result.Tenant.Name, login.ProviderKey + "@" + login.LoginProvider); + return result; + } + + protected async Task> LoginAsyncInternal(UserLoginInfo login) + { + if (login == null || login.LoginProvider.IsNullOrEmpty() || login.ProviderKey.IsNullOrEmpty()) + { + throw new ArgumentException("login"); + } + using (UnitOfWorkManager.Current.DisableFilter(AbpDataFilters.MayHaveTenant)) + { + var user = await _userStore.FindAsync(login); + if (user == null) + { + return new AbpLoginResult(AbpLoginResultType.UnknownExternalLogin); + } + //Get and check tenant + Tenant tenant = null; + if (!MultiTenancyConfig.IsEnabled) + { + tenant = await GetDefaultTenantAsync(); + } + else if (user.TenantId.HasValue) + { + tenant = await TenantRepository.FirstOrDefaultAsync(t => t.Id == user.TenantId); + if (tenant == null) + { + return new AbpLoginResult(AbpLoginResultType.InvalidTenancyName); + } + if (!tenant.IsActive) + { + return new AbpLoginResult(AbpLoginResultType.TenantIsNotActive, tenant); + } + } + return await CreateLoginResultAsync(user, tenant); + } + } + `````` + + Then, your `LogInManager` will be able to use given user's tenant to try log in. + + + + #### Updating UserManager + +* Go to `UserManager`. *(It is located in **aspnet-core\src\\[YOURAPPNAME].Core\Authorization\Users** folder.)* + +* And add following lines of codes + + ```csharp +public async Task TryGetTenantIdOfUser(string userEmail) + { + using (_unitOfWorkManager.Current.DisableFilter(AbpDataFilters.MayHaveTenant)) + { + var user = await Users.SingleOrDefaultAsync(u => u.EmailAddress == userEmail.Trim()); + return user?.TenantId; + } + } + ``` + + + + #### Updating AccountController + +* Then, go to `AccountController`. *(It is located in **aspnet-core\src\\[YOURAPPNAME].Mvc\Controllers** folder.)* + +* Replace the function named `GetTenancyNameOrNull` with the following content + + ```csharp + private async Task GetTenancyNameOrNull(string email) + { + var tenantId = await _userManager.TryGetTenantIdOfUser(email); + if (!tenantId.HasValue) + { + return null; + } + return _tenantCache.GetOrNull(tenantId.Value)?.TenancyName; + } + ``` + +* Replace the function named `Login(LoginViewModel loginModel, string returnUrl = "", string returnUrlHash = "", string ss = "") ` with the following content + + ```csharp + [HttpPost] + [UnitOfWork] + public virtual async Task Login(LoginViewModel loginModel, string returnUrl = "", string returnUrlHash = "", string ss = "") + { + returnUrl = NormalizeReturnUrl(returnUrl); + if (!string.IsNullOrWhiteSpace(returnUrlHash)) + { + returnUrl = returnUrl + returnUrlHash; + } + + if (UseCaptchaOnLogin()) + { + await _recaptchaValidator.ValidateAsync(HttpContext.Request.Form[RecaptchaValidator.RecaptchaResponseKey]); + } + + var loginResult = await GetLoginResultAsync(loginModel.UsernameOrEmailAddress, loginModel.Password, await GetTenancyNameOrNull(loginModel.UsernameOrEmailAddress));//use new GetTenancyNameOrNull method that you add previously + if (loginResult?.Tenant?.Id != AbpSession.TenantId) + { + SetTenantIdCookie(loginResult?.Tenant?.Id); + CurrentUnitOfWork.SetTenantId(loginResult?.Tenant?.Id); + } + + if (!string.IsNullOrEmpty(ss) && ss.Equals("true", StringComparison.OrdinalIgnoreCase) && loginResult.Result == AbpLoginResultType.Success) + { + loginResult.User.SetSignInToken(); + returnUrl = AddSingleSignInParametersToReturnUrl(returnUrl, loginResult.User.SignInToken, loginResult.User.Id, loginResult.User.TenantId); + } + + if (_settingManager.GetSettingValue(AppSettings.UserManagement.AllowOneConcurrentLoginPerUser)) + { + await _userManager.UpdateSecurityStampAsync(loginResult.User); + } + + if (loginResult.User.ShouldChangePasswordOnNextLogin) + { + loginResult.User.SetNewPasswordResetCode(); + return Json(new AjaxResponse + { + TargetUrl = Url.Action( + "ResetPassword", + new ResetPasswordViewModel + { + TenantId = AbpSession.TenantId, + UserId = loginResult.User.Id, + ResetCode = loginResult.User.PasswordResetCode, + ReturnUrl = returnUrl, + SingleSignIn = ss + }) + }); + } + + var signInResult = await _signInManager.SignInOrTwoFactorAsync(loginResult, loginModel.RememberMe); + if (signInResult.RequiresTwoFactor) + { + return Json(new AjaxResponse + { + TargetUrl = Url.Action( + "SendSecurityCode", + new + { + returnUrl = returnUrl, + rememberMe = loginModel.RememberMe + }) + }); + } + + Debug.Assert(signInResult.Succeeded); + await UnitOfWorkManager.Current.SaveChangesAsync(); + + return Json(new AjaxResponse { TargetUrl = returnUrl }); + } + ``` + +* Replace the function named `ExternalLoginCallback(string returnUrl, string remoteError = null, string ss = "") ` with the following content + + ```csharp + [UnitOfWork] + public virtual async Task ExternalLoginCallback(string returnUrl, string remoteError = null, string ss = "") + { + returnUrl = NormalizeReturnUrl(returnUrl); + if (remoteError != null) + { + Logger.Error("Remote Error in ExternalLoginCallback: " + remoteError); + throw new UserFriendlyException(L("CouldNotCompleteLoginOperation")); + } + var externalLoginInfo = await _signInManager.GetExternalLoginInfoAsync(); + if (externalLoginInfo == null) + { + Logger.Warn("Could not get information from external login."); + return RedirectToAction(nameof(Login)); + } + var loginResult = await _logInManager.LoginAsync(externalLoginInfo);//use new login method that you add previously + switch (loginResult.Result) + { + case AbpLoginResultType.Success: + { + await _signInManager.SignInAsync(loginResult.Identity, false); + if (!string.IsNullOrEmpty(ss) && ss.Equals("true", StringComparison.OrdinalIgnoreCase) && loginResult.Result == AbpLoginResultType.Success) + { + loginResult.User.SetSignInToken(); + returnUrl = AddSingleSignInParametersToReturnUrl(returnUrl, loginResult.User.SignInToken, loginResult.User.Id, loginResult.User.TenantId); + } + return Redirect(returnUrl); + } + case AbpLoginResultType.UnknownExternalLogin: + return await RegisterForExternalLogin(externalLoginInfo); + default: + throw _abpLoginResultTypeHelper.CreateExceptionForFailedLoginAttempt( + loginResult.Result, + externalLoginInfo.Principal.FindFirstValue(ClaimTypes.Email) ?? externalLoginInfo.ProviderKey, + loginResult.Tenant?.Name + ); + } + } + ``` + +Then your project will be able to use without specifying tenant. + +#### More + +For a more stable UI, you can remove the tenant selection model used for login operations. + +Go to **aspnet-core\src\}[YOURAPPNAME].Web.Mvc\Views\Account\Login.cshtml** and add following code part + +```csharp +@{ + ViewBag.DisableTenantChange = true; +} +``` + + + + + diff --git a/docs/en/images/login-page-with-tenant-change.png b/docs/en/images/login-page-with-tenant-change.png new file mode 100644 index 0000000000000000000000000000000000000000..034183fcb80c0e8680bb33683a995210ed0a63cc GIT binary patch literal 23659 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 literal 0 HcmV?d00001 diff --git a/docs/en/images/login-page-without-tenant-change.png b/docs/en/images/login-page-without-tenant-change.png new file mode 100644 index 0000000000000000000000000000000000000000..1cfca470de570d54e98d4efc35002ffeda5018ed GIT binary patch literal 20251 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 Date: Wed, 18 Nov 2020 01:06:34 +0300 Subject: [PATCH 02/13] dd documents about how to sing in without specifying tenant (angular) --- ...gular-Sign-In-Without-Specifying-Tenant.md | 313 ++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md diff --git a/docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md b/docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md new file mode 100644 index 00000000..7b8fec34 --- /dev/null +++ b/docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md @@ -0,0 +1,313 @@ +## Sign In Without Specifying Tenant + +Normally, **ASP.NET Zero** uses tenant information in login transactions. This document shows you how to implement tenant information independent login. + +***Important Note:*** *To implement that, your user's email addresses have to be unique. Otherwise, that solution may not works properly.* + +#### Updating LogInManager + +* First of all, open `LogInManager`. *(It is located in **aspnet-core\src\\[YOURAPPNAME].Application\Authorization** folder.)* + +* Add lines of codes shown below + + ``````csharp + UserStore _userStore + public LogInManager( + //.... + UserStore userStore + ){ + _userStore = userStore; + } + + [UnitOfWork] + public async Task> LoginAsync(UserLoginInfo login) + { + var result = await LoginAsyncInternal(login); + await SaveLoginAttemptAsync(result, result.Tenant.Name, login.ProviderKey + "@" + login.LoginProvider); + return result; + } + + protected async Task> LoginAsyncInternal(UserLoginInfo login) + { + if (login == null || login.LoginProvider.IsNullOrEmpty() || login.ProviderKey.IsNullOrEmpty()) + { + throw new ArgumentException("login"); + } + using (UnitOfWorkManager.Current.DisableFilter(AbpDataFilters.MayHaveTenant)) + { + var user = await _userStore.FindAsync(login); + if (user == null) + { + return new AbpLoginResult(AbpLoginResultType.UnknownExternalLogin); + } + //Get and check tenant + Tenant tenant = null; + if (!MultiTenancyConfig.IsEnabled) + { + tenant = await GetDefaultTenantAsync(); + } + else if (user.TenantId.HasValue) + { + tenant = await TenantRepository.FirstOrDefaultAsync(t => t.Id == user.TenantId); + if (tenant == null) + { + return new AbpLoginResult(AbpLoginResultType.InvalidTenancyName); + } + if (!tenant.IsActive) + { + return new AbpLoginResult(AbpLoginResultType.TenantIsNotActive, tenant); + } + } + return await CreateLoginResultAsync(user, tenant); + } + } + `````` + + Then, your `LogInManager` will be able to use given user's tenant to try log in. + + + + #### Updating UserManager + +* Go to `UserManager`. *(It is located in **aspnet-core\src\\[YOURAPPNAME].Core\Authorization\Users** folder.)* + +* And add following lines of codes + + ```csharp +public async Task TryGetTenantIdOfUser(string userEmail) + { + using (_unitOfWorkManager.Current.DisableFilter(AbpDataFilters.MayHaveTenant)) + { + var user = await Users.SingleOrDefaultAsync(u => u.EmailAddress == userEmail.Trim()); + return user?.TenantId; + } + } + ``` + + + + #### Updating TokenAuthController + +* Then, go to `TokenAuthController`. *(It is located in **aspnet-core\src\[YOURAPPNAME].Web.Core\Controllers** folder.)* + +* Replace the function named `GetTenancyNameOrNull` with the following content + + ```csharp + private async Task GetTenancyNameOrNull(string email) + { + var tenantId = await _userManager.TryGetTenantIdOfUser(email); + if (!tenantId.HasValue) + { + return null; + } + return _tenantCache.GetOrNull(tenantId.Value)?.TenancyName; + } + ``` + +* Replace the function named `Authenticate([FromBody] AuthenticateModel model) ` with the following content + + ```csharp + [HttpPost] + public async Task Authenticate([FromBody] AuthenticateModel model) + { + if (UseCaptchaOnLogin()) + { + await ValidateReCaptcha(model.CaptchaResponse); + } + + var loginResult = await GetLoginResultAsync( + model.UserNameOrEmailAddress, + model.Password, + await GetTenancyNameOrNull(model.UserNameOrEmailAddress)//use new GetTenancyNameOrNull method that you add previously + ); + + var returnUrl = model.ReturnUrl; + if (model.SingleSignIn.HasValue && model.SingleSignIn.Value && + loginResult.Result == AbpLoginResultType.Success) + { + loginResult.User.SetSignInToken(); + returnUrl = AddSingleSignInParametersToReturnUrl(model.ReturnUrl, loginResult.User.SignInToken, + loginResult.User.Id, loginResult.User.TenantId); + } + + //Password reset + if (loginResult.User.ShouldChangePasswordOnNextLogin) + { + loginResult.User.SetNewPasswordResetCode(); + return new AuthenticateResultModel + { + ShouldResetPassword = true, + PasswordResetCode = loginResult.User.PasswordResetCode, + UserId = loginResult.User.Id, + ReturnUrl = returnUrl + }; + } + + //Two factor auth + await _userManager.InitializeOptionsAsync(loginResult.Tenant?.Id); + + string twoFactorRememberClientToken = null; + if (await IsTwoFactorAuthRequiredAsync(loginResult, model)) + { + if (model.TwoFactorVerificationCode.IsNullOrEmpty()) + { + //Add a cache item which will be checked in SendTwoFactorAuthCode to prevent sending unwanted two factor code to users. + _cacheManager + .GetTwoFactorCodeCache() + .Set( + loginResult.User.ToUserIdentifier().ToString(), + new TwoFactorCodeCacheItem() + ); + return new AuthenticateResultModel + { + RequiresTwoFactorVerification = true, + UserId = loginResult.User.Id, + TwoFactorAuthProviders = await _userManager.GetValidTwoFactorProvidersAsync(loginResult.User), + ReturnUrl = returnUrl + }; + } + twoFactorRememberClientToken = await TwoFactorAuthenticateAsync(loginResult.User, model); + } + + // One Concurrent Login + if (AllowOneConcurrentLoginPerUser()) + { + await _userManager.UpdateSecurityStampAsync(loginResult.User); + await _securityStampHandler.SetSecurityStampCacheItem(loginResult.User.TenantId, loginResult.User.Id, + loginResult.User.SecurityStamp); + loginResult.Identity.ReplaceClaim(new Claim(AppConsts.SecurityStampKey, + loginResult.User.SecurityStamp)); + } + + var refreshToken = CreateRefreshToken(await CreateJwtClaims(loginResult.Identity, loginResult.User, + tokenType: TokenType.RefreshToken)); + + var accessToken = CreateAccessToken(await CreateJwtClaims(loginResult.Identity, loginResult.User, + refreshTokenKey: refreshToken.key)); + + return new AuthenticateResultModel + { + AccessToken = accessToken, + ExpireInSeconds = (int) _configuration.AccessTokenExpiration.TotalSeconds, + RefreshToken = refreshToken.token, + RefreshTokenExpireInSeconds = (int) _configuration.RefreshTokenExpiration.TotalSeconds, + EncryptedAccessToken = GetEncryptedAccessToken(accessToken), + TwoFactorRememberClientToken = twoFactorRememberClientToken, + UserId = loginResult.User.Id, + ReturnUrl = returnUrl + }; + } + ``` + +* Replace the function named `ExternalAuthenticate([FromBody] ExternalAuthenticateModel model) ` with the following content + + ```csharp + [HttpPost] + public async Task ExternalAuthenticate([FromBody] ExternalAuthenticateModel model) + { + var externalUser = await GetExternalUserInfo(model); + var loginResult = await _logInManager.LoginAsync(new UserLoginInfo(model.AuthProvider, model.ProviderKey, model.AuthProvider)); + switch (loginResult.Result) + { + case AbpLoginResultType.Success: + { + var refreshToken = CreateRefreshToken(await CreateJwtClaims(loginResult.Identity, loginResult.User, + tokenType: TokenType.RefreshToken)); + var accessToken = CreateAccessToken(await CreateJwtClaims(loginResult.Identity, loginResult.User, + refreshTokenKey: refreshToken.key)); + var returnUrl = model.ReturnUrl; + + if (model.SingleSignIn.HasValue && model.SingleSignIn.Value && + loginResult.Result == AbpLoginResultType.Success) + { + loginResult.User.SetSignInToken(); + returnUrl = AddSingleSignInParametersToReturnUrl(model.ReturnUrl, loginResult.User.SignInToken, + loginResult.User.Id, loginResult.User.TenantId); + } + + return new ExternalAuthenticateResultModel + { + AccessToken = accessToken, + EncryptedAccessToken = GetEncryptedAccessToken(accessToken), + ExpireInSeconds = (int) _configuration.AccessTokenExpiration.TotalSeconds, + ReturnUrl = returnUrl, + RefreshToken = refreshToken.token, + RefreshTokenExpireInSeconds = (int) _configuration.RefreshTokenExpiration.TotalSeconds + }; + } + case AbpLoginResultType.UnknownExternalLogin: + { + var newUser = await RegisterExternalUserAsync(externalUser); + if (!newUser.IsActive) + { + return new ExternalAuthenticateResultModel + { + WaitingForActivation = true + }; + } + //Try to login again with newly registered user! + loginResult = await _logInManager.LoginAsync( + new UserLoginInfo(model.AuthProvider, model.ProviderKey, model.AuthProvider) + ); + + if (loginResult.Result != AbpLoginResultType.Success) + { + throw _abpLoginResultTypeHelper.CreateExceptionForFailedLoginAttempt( + loginResult.Result, + model.ProviderKey, + loginResult?.Tenant?.Name + ); + } + + var refreshToken = CreateRefreshToken(await CreateJwtClaims(loginResult.Identity, + loginResult.User, tokenType: TokenType.RefreshToken) + ); + + var accessToken = CreateAccessToken(await CreateJwtClaims(loginResult.Identity, + loginResult.User, refreshTokenKey: refreshToken.key)); + + return new ExternalAuthenticateResultModel + { + AccessToken = accessToken, + EncryptedAccessToken = GetEncryptedAccessToken(accessToken), + ExpireInSeconds = (int) _configuration.AccessTokenExpiration.TotalSeconds, + RefreshToken = refreshToken.token, + RefreshTokenExpireInSeconds = (int) _configuration.RefreshTokenExpiration.TotalSeconds + }; + } + default: + { + throw _abpLoginResultTypeHelper.CreateExceptionForFailedLoginAttempt( + loginResult.Result, + model.ProviderKey, + loginResult?.Tenant?.Name + ); + } + } + } + ``` + +Then your project will be able to use without specifying tenant. + +#### More + +For a more stable UI, you can remove the tenant selection model used for login operations. + +Go to **aspnet-zero-core\angular\src\account\account.component.ts** and add add **login** to **tenantChangeDisabledRoutes** + +```typescript + tenantChangeDisabledRoutes: string[] = [ + 'select-edition', + 'buy', + 'upgrade', + 'extend', + 'register-tenant' + //... + 'login'//add login + ]; +``` + + + + + From e548af8900998bcfbbb7442376cf2cb1a9d469ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0smail=20=C3=87A=C4=9EDA=C5=9E?= Date: Wed, 13 Jan 2021 09:56:42 +0300 Subject: [PATCH 03/13] Update docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md --- docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md b/docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md index 7b8fec34..e3e0b5b9 100644 --- a/docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md +++ b/docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md @@ -1,6 +1,6 @@ ## Sign In Without Specifying Tenant -Normally, **ASP.NET Zero** uses tenant information in login transactions. This document shows you how to implement tenant information independent login. +Normally, **ASP.NET Zero** uses tenant information during the login process. This document shows you how to implement the login process without tenant information. ***Important Note:*** *To implement that, your user's email addresses have to be unique. Otherwise, that solution may not works properly.* @@ -310,4 +310,3 @@ Go to **aspnet-zero-core\angular\src\account\account.component.ts** and add add - From 8eeb9acf42896821d64d26aac4f28ceabea2d239 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0smail=20=C3=87A=C4=9EDA=C5=9E?= Date: Wed, 13 Jan 2021 09:56:47 +0300 Subject: [PATCH 04/13] Update docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md --- docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md b/docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md index e3e0b5b9..de8d6e13 100644 --- a/docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md +++ b/docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md @@ -2,7 +2,7 @@ Normally, **ASP.NET Zero** uses tenant information during the login process. This document shows you how to implement the login process without tenant information. -***Important Note:*** *To implement that, your user's email addresses have to be unique. Otherwise, that solution may not works properly.* +***Important Note:*** *Your user's email addresses have to be unique to implement this solution. Otherwise, this solution may not work correctly.* #### Updating LogInManager From 6093fdbcaba8f20976791e7dabf86cb76abb170c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0smail=20=C3=87A=C4=9EDA=C5=9E?= Date: Wed, 13 Jan 2021 09:56:53 +0300 Subject: [PATCH 05/13] Update docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md --- docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md b/docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md index de8d6e13..a0d4846f 100644 --- a/docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md +++ b/docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md @@ -8,7 +8,7 @@ Normally, **ASP.NET Zero** uses tenant information during the login process. Thi * First of all, open `LogInManager`. *(It is located in **aspnet-core\src\\[YOURAPPNAME].Application\Authorization** folder.)* -* Add lines of codes shown below +* Add lines shown below ``````csharp UserStore _userStore From 0bf9e3f2da041db4f0934c10e388e29cac58bc8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0smail=20=C3=87A=C4=9EDA=C5=9E?= Date: Wed, 13 Jan 2021 09:56:58 +0300 Subject: [PATCH 06/13] Update docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md --- docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md b/docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md index a0d4846f..6429f833 100644 --- a/docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md +++ b/docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md @@ -63,7 +63,7 @@ Normally, **ASP.NET Zero** uses tenant information during the login process. Thi } `````` - Then, your `LogInManager` will be able to use given user's tenant to try log in. + Then, your `LogInManager` will be able to use given user's tenant for login process. From b8aee4865e6df1e3234c4fe1a8c2de0e24749042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0smail=20=C3=87A=C4=9EDA=C5=9E?= Date: Wed, 13 Jan 2021 09:57:04 +0300 Subject: [PATCH 07/13] Update docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md --- docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md b/docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md index 6429f833..ec4570c3 100644 --- a/docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md +++ b/docs/en/Core-Angular-Sign-In-Without-Specifying-Tenant.md @@ -71,7 +71,7 @@ Normally, **ASP.NET Zero** uses tenant information during the login process. Thi * Go to `UserManager`. *(It is located in **aspnet-core\src\\[YOURAPPNAME].Core\Authorization\Users** folder.)* -* And add following lines of codes +* And add following lines; ```csharp public async Task TryGetTenantIdOfUser(string userEmail) From 186ca94bef75ca4c88c26961089882e16c5f17ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0smail=20=C3=87A=C4=9EDA=C5=9E?= Date: Wed, 13 Jan 2021 09:57:13 +0300 Subject: [PATCH 08/13] Update docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md --- docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md b/docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md index 3d1cfb29..49bffc95 100644 --- a/docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md +++ b/docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md @@ -2,7 +2,7 @@ Normally, **ASP.NET Zero** uses tenant information in login transactions. This document shows you how to implement tenant information independent login. -***Important Note:*** *To implement that, your user's email addresses have to be unique. Otherwise, that solution may not works properly.* +***Important Note:*** *Your user's email addresses have to be unique to implement this solution. Otherwise, this solution may not work correctly.* #### Updating LogInManager @@ -240,4 +240,3 @@ Go to **aspnet-core\src\}[YOURAPPNAME].Web.Mvc\Views\Account\Login.cshtml** and - From 66332a95f726013124486534935d4c4bd2392111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0smail=20=C3=87A=C4=9EDA=C5=9E?= Date: Wed, 13 Jan 2021 09:57:17 +0300 Subject: [PATCH 09/13] Update docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md --- docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md b/docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md index 49bffc95..c67ad4b5 100644 --- a/docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md +++ b/docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md @@ -8,7 +8,7 @@ Normally, **ASP.NET Zero** uses tenant information in login transactions. This d * First of all, open `LogInManager`. *(It is located in **aspnet-core\src\\[YOURAPPNAME].Application\Authorization** folder.)* -* Add lines of codes shown below +* Add lines shown below ``````csharp UserStore _userStore From ca19ab7cae1acd4736dcf50aba88b6507628a4d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0smail=20=C3=87A=C4=9EDA=C5=9E?= Date: Wed, 13 Jan 2021 09:57:22 +0300 Subject: [PATCH 10/13] Update docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md --- docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md b/docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md index c67ad4b5..0991d1ca 100644 --- a/docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md +++ b/docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md @@ -63,7 +63,7 @@ Normally, **ASP.NET Zero** uses tenant information in login transactions. This d } `````` - Then, your `LogInManager` will be able to use given user's tenant to try log in. + Then, your `LogInManager` will be able to use given user's tenant for login process. From c48f3904c4593a32181eaf3d493f902d2e0e874a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0smail=20=C3=87A=C4=9EDA=C5=9E?= Date: Wed, 13 Jan 2021 09:57:27 +0300 Subject: [PATCH 11/13] Update docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md --- docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md b/docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md index 0991d1ca..a56f016a 100644 --- a/docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md +++ b/docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md @@ -71,7 +71,7 @@ Normally, **ASP.NET Zero** uses tenant information in login transactions. This d * Go to `UserManager`. *(It is located in **aspnet-core\src\\[YOURAPPNAME].Core\Authorization\Users** folder.)* -* And add following lines of codes +* And add following lines; ```csharp public async Task TryGetTenantIdOfUser(string userEmail) From 08857efdb6683db7eabcf1a6e33acdb2f77cd282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0smail=20=C3=87A=C4=9EDA=C5=9E?= Date: Wed, 13 Jan 2021 09:57:38 +0300 Subject: [PATCH 12/13] Update docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md --- docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md b/docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md index a56f016a..ffff8452 100644 --- a/docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md +++ b/docs/en/Core-Mvc-Sign-In-Without-Specifying-Tenant.md @@ -223,7 +223,7 @@ public async Task TryGetTenantIdOfUser(string userEmail) } ``` -Then your project will be able to use without specifying tenant. +Then your users will be able to login without specifying a tenant. #### More From 5667ed4d520575b10ce8bea5f0bfcbe1b1e5d0f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0smail=20=C3=87A=C4=9EDA=C5=9E?= Date: Wed, 13 Jan 2021 10:00:09 +0300 Subject: [PATCH 13/13] added tutorial to main menu --- docs/en/nav-aspnet-core-angular.json | 4 ++++ docs/en/nav-aspnet-core-mvc.json | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/docs/en/nav-aspnet-core-angular.json b/docs/en/nav-aspnet-core-angular.json index 574f0a54..e0771f6c 100644 --- a/docs/en/nav-aspnet-core-angular.json +++ b/docs/en/nav-aspnet-core-angular.json @@ -536,6 +536,10 @@ { "text": "File Upload", "path": "Core-Angular-File-Upload-Tutorial.md" + }, + { + "text": "Sign In Without Specifying Tenant", + "path": "Core-Angular-Sign-In-Without-Specifying-Tenant.md" } ] }, diff --git a/docs/en/nav-aspnet-core-mvc.json b/docs/en/nav-aspnet-core-mvc.json index 437c1e8f..3873cae4 100644 --- a/docs/en/nav-aspnet-core-mvc.json +++ b/docs/en/nav-aspnet-core-mvc.json @@ -595,6 +595,10 @@ { "text": "File Upload", "path": "Core-Mvc-File-Upload-Tutorial.md " + }, + { + "text": "Sign In Without Specifying Tenant", + "path": "Core-Mvc-Sign-In-Without-Specifying-Tenant.md" } ] },