Activities of "cbogner85"

Answer

Hello,

I'm already using LeptonX and have imeplemented custom themes. What I'm trying to accomplish: enable different themes based on the current tenant (only visible to this specific tenant) and set a different DefaultStyle based on the current tenant. The documentation shows how to hide styles, but in WebModule we don't have access to the current tenant as it runs at application startup and not at every page request.

Best regards

Question
  • ABP Framework version: v7.2.2
  • UI type: MVC
  • DB provider: EF Core

Hello,

I'm trying to implement customized themes for some tenants (they need their own branding) and hide the default themes for them. Therefore, I created new LeptonX themes and added them in WebModule:

options.Styles.Add("tenant1-light",
	new LeptonXThemeStyle(
	LocalizableString.Create<LeptonXResource>("Theme:light"), "bi bi-sun-fill"));
options.Styles.Add("tenant1-dark",
	new LeptonXThemeStyle(
	LocalizableString.Create<LeptonXResource>("Theme:dark"), "bi bi-moon-fill"));
options.Styles.Add("tenant2-light",
	new LeptonXThemeStyle(
	LocalizableString.Create<LeptonXResource>("Theme:light"), "bi bi-sun-fill"));
options.Styles.Add("tenant2-dark",
	new LeptonXThemeStyle(
	LocalizableString.Create<LeptonXResource>("Theme:dark"), "bi bi-moon-fill"));

Since I don't want users to see the customized themes of other tenants (and also not the default styles!), I have overriden Themes\LeptonX\Components\Common\GeneralSettings\Default.cshtml and Themes\LeptonX\Components\Common\MobileGeneralSettings\Default.cshtml to only display themes of the current tenant.

It works, but how can I change the default theme per tenant? Normally, DefaultStyle is set in WebModule:

options.DefaultStyle = LeptonXStyleNames.Light;

I need a tenant-specific DefaultStyle (e.g. "tenant1-light" for tenant1, "tenant2-light" for tenant2). With my current approach, when a tenant1 user opens the page for the first time, it displays the default light style (although this is not visible in the user's theme list) instead of the tenant1-light style . He can switch to tenant1-light manually and this is persisted, however, when he changes the language, it falls back to default light style.

Additionally, with my approach the "System" theme doesn't work, as it uses default light/dark instead of my customized ones.

Is there a better approach to implement tenant-specific themes?

Thanks in advance and best regards

Same here. Hopefully this gets fixed soon.

Hi @maliming!

Thank you for pointing me to the right direction.

I found out that the issue occurs in UserLookupService > FindByUserId. It seems that sometimes the tenant filter is not working correctly, although I changed the tenant.

I worked around by disabling multi-tenancy filter for the user lookup:

       // Fix CmsUser Insert
        TUser localUser;

        using (_dataFilter.Disable<IMultiTenant>())
        {
            localUser = await _userRepository.FindAsync(id, cancellationToken: cancellationToken);
        }
          

Seems a bit quick and dirty, but on the other hand, the id is unique anyway, so I don't see a problem doing the search globally. Since that change the issue is gone.

Therefore I'll close this issue.

hi @maliming,

I can't change to the host, as it would only find host users. I don't want to disable multi-tenancy, I absolutely need all items to be multi-tenant. I only want to let the users login from a unified login page without having to select the tenant from tenantbox or having different subdomains for each tenant.

the function of tenantId = await _extendedOrganizationUnitAppService.GetTenantByUsername(LoginInput.UserNameOrEmailAddress); is quite simple:

  public async Task<Guid?> GetTenantByUsername(string userName)
  {
            var user = await _identityUserRepository.FindByNormalizedUserNameAsync(userName.ToUpper());
            if (user != null)
            {
                return user.TenantId;
            }
            else
            {
                return null;
            }
            
}

Every few days one I got the issue again after a couple of days without the issue. It's really strange. A user can't login from the unified login page, because a duplicate Id insertion into table CmsUsers is tried and the result is an error 500. When I change his password and try to login, I can reproduce the issue. When I login for one time using the tenant specific URL (with subdomain), it works and after that, the unified login works again, too. Therefore I think that the tenant (null, from the unified page) is stored somewhere and is used during CmsUser search, although I changed it in my custom login method... of course I tried in private mode of the browser to ensure the tenant isn't stored somewhere in the cookies.

Where does CmsUser search/ insert during login process takes place? Could you point me to the source file? Maybe overriding it to customize it to my needs is easier than finding the cause of that issue.

Thanks

Hi @maliming,

sure, this is my LoginModel extension:

  [Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(AccountPageModel), typeof(LoginModel))]
    public class CustomLoginModel : LoginModel
    {
        private readonly IDataFilter _dataFilter;
        private readonly ICurrentTenant _currentTenant;
        private readonly IExtendedOrganizationUnitAppService _extendedOrganizationUnitAppService;
        public CustomLoginModel(IAuthenticationSchemeProvider schemeProvider,
            IOptions<AbpAccountOptions> accountOptions,
            IAbpRecaptchaValidatorFactory recaptchaValidatorFactory,
            IAccountExternalProviderAppService accountExternalProviderAppService,
            ICurrentPrincipalAccessor currentPrincipalAccessor,
            IOptions<IdentityOptions> identityOptions,
            IOptionsSnapshot<reCAPTCHAOptions> reCaptchaOptions,
            ICurrentTenant currentTenant,
            IExtendedOrganizationUnitAppService extendedOrganizationUnitAppService,
            IDataFilter dataFilter) : base(schemeProvider, accountOptions, recaptchaValidatorFactory, accountExternalProviderAppService, currentPrincipalAccessor, identityOptions, reCaptchaOptions)
        {
            _dataFilter = dataFilter;
            _currentTenant = currentTenant;
            _extendedOrganizationUnitAppService = extendedOrganizationUnitAppService;
        }

        public override async Task<IActionResult> OnPostAsync(string action)
        {

            try
            {
                await ReCaptchaVerification();
            }
            catch (UserFriendlyException e)
            {
                if (e is ScoreBelowThresholdException)
                {
                    var onScoreBelowThresholdResult = OnRecaptchaScoreBelowThreshold();
                    if (onScoreBelowThresholdResult != null)
                    {
                        return await onScoreBelowThresholdResult;
                    }
                }

                Alerts.Danger(GetLocalizeExceptionMessage(e));
                return Page();
            }

            ValidateModel();

            await IdentityOptions.SetAsync();

            var localLoginResult = await CheckLocalLoginAsync();
            if (localLoginResult != null)
            {
                return localLoginResult;
            }

            IsSelfRegistrationEnabled = await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled);

            Volo.Abp.Identity.IdentityUser user;

            Guid? tenantId;

            // disable TenantFilter
            using (_dataFilter.Disable<IMultiTenant>())
            {
                // read tenant from username (unique for all tenants, by overwritten RegisterModel)
                // if we login with disabled IMultiTenant DataFilter, we have issues with claims
                // therefore we read tenant from username, then switch the current tenant and login

                await ReplaceEmailToUsernameOfInputIfNeeds();
                tenantId = await _extendedOrganizationUnitAppService.GetTenantByUsername(LoginInput.UserNameOrEmailAddress);
            }
            
            // change the current tenant
            using (CurrentTenant.Change(tenantId))
            {

                IsLinkLogin = await VerifyLinkTokenAsync();


                var result = await SignInManager.PasswordSignInAsync(
                LoginInput.UserNameOrEmailAddress,
                LoginInput.Password,
                LoginInput.RememberMe,
                true
            );

                await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext
                {
                    Identity = IdentitySecurityLogIdentityConsts.Identity,
                    Action = result.ToIdentitySecurityLogAction(),
                    UserName = LoginInput.UserNameOrEmailAddress
                });

                if (result.RequiresTwoFactor)
                {

                    return RedirectToPage("./SendSecurityCode", new
                    {
                        returnUrl = base.ReturnUrl,
                        returnUrlHash = base.ReturnUrlHash,
                        rememberMe = LoginInput.RememberMe,
                        linkUserId = base.LinkUserId,
                        linkTenantId = base.LinkTenantId,
                        linkToken = base.LinkToken
                    });
                }

                if (result.IsLockedOut)
                {
                    return RedirectToPage("./LockedOut", new
                    {
                        returnUrl = ReturnUrl,
                        returnUrlHash = ReturnUrlHash
                    });
                }

                if (result.IsNotAllowed)
                {
                    var notAllowedUser = await GetIdentityUser(LoginInput.UserNameOrEmailAddress);
                    if (notAllowedUser.IsActive && await UserManager.CheckPasswordAsync(notAllowedUser, LoginInput.Password))
                    {
                        await StoreConfirmUser(notAllowedUser);
                        return RedirectToPage("./ConfirmUser", new
                        {
                            returnUrl = ReturnUrl,
                            returnUrlHash = ReturnUrlHash
                        });
                    }

                    Alerts.Danger(L["LoginIsNotAllowed"]);
                    return Page();
                }

                if (!result.Succeeded)
                {
                    Alerts.Danger(L["InvalidUserNameOrPassword"]);
                    return Page();
                }

                user = await GetIdentityUser(LoginInput.UserNameOrEmailAddress);
            }

            if (IsLinkLogin)
            {
                using (CurrentPrincipalAccessor.Change(await SignInManager.CreateUserPrincipalAsync(user)))
                {
                    await IdentityLinkUserAppService.LinkAsync(new LinkUserInput
                    {
                        UserId = LinkUserId.Value,
                        TenantId = LinkTenantId,
                        Token = LinkToken
                    });

                    await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext
                    {
                        Identity = IdentitySecurityLogIdentityConsts.Identity,
                        Action = IdentityProSecurityLogActionConsts.LinkUser,
                        UserName = user.UserName,
                        ExtraProperties =
                        {
                            { IdentityProSecurityLogActionConsts.LinkTargetTenantId, LinkTenantId },
                            { IdentityProSecurityLogActionConsts.LinkTargetUserId, LinkUserId }
                        }
                    });

                    using (CurrentTenant.Change(LinkTenantId))
                    {
                        var targetUser = await UserManager.GetByIdAsync(LinkUserId.Value);
                        using (CurrentPrincipalAccessor.Change(await SignInManager.CreateUserPrincipalAsync(targetUser)))
                        {
                            await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext
                            {
                                Identity = IdentitySecurityLogIdentityConsts.Identity,
                                Action = IdentityProSecurityLogActionConsts.LinkUser,
                                UserName = targetUser.UserName,
                                ExtraProperties =
                                {
                                    { IdentityProSecurityLogActionConsts.LinkTargetTenantId, targetUser.TenantId },
                                    { IdentityProSecurityLogActionConsts.LinkTargetUserId, targetUser.Id }
                                }
                            });
                        }
                    }

                    return RedirectToPage("./LinkLogged", new
                    {
                        returnUrl = ReturnUrl,
                        returnUrlHash = ReturnUrlHash,
                        TargetLinkUserId = LinkUserId,
                        TargetLinkTenantId = LinkTenantId
                    });
                }
            }

            return RedirectSafely(ReturnUrl, ReturnUrlHash);
        }


    }
  • ABP Framework version: v6.0.0
  • UI type: MVC
  • DB provider: EF Core
  • Tiered (MVC) or Identity Server Separated (Angular): no

Hello,

as my users always complained about tenant-specific URLs, I decided to develop a unified login. Therefore, I extended all required methods such as LoginModel, ForgotPasswordModel, ResetPasswordModel and so on to work without multi-tenancy.

For the LoginModel, I disable multi-tenancy filter, get the tenant ID by the inserted user name (which is unique for all tenants), change CurrentTenant to the user's tenant and then perform PasswordSignIn.

Everything works almost as expected, but I got a strange issue: in some very rare cases, when users try to login from the unified login page, they receive an error 500, because a duplicate CmsUser insertion is tried. I can see from the sql server logs that it tries to find the cms user with tenant null:

exec sp_executesql N'SELECT TOP(1) [c].[Id], [c].[ConcurrencyStamp], [c].[Email], [c].[EmailConfirmed], [c].[ExtraProperties], [c].[IsActive], [c].[Name], [c].[PhoneNumber], [c].[PhoneNumberConfirmed], [c].[Surname], [c].[TenantId], [c].[UserName]
FROM [CmsUsers] AS [c]
WHERE ((@__ef_filter__p_0 = CAST(1 AS bit)) OR ([c].[TenantId] = @__ef_filter__CurrentTenantId_1)) AND ([c].[Id] = @__id_0)
ORDER BY [c].[Id]',N'@__ef_filter__p_0 bit,@__ef_filter__CurrentTenantId_1 uniqueidentifier,@__id_0 uniqueidentifier',@__ef_filter__p_0=0,@__ef_filter__CurrentTenantId_1=null,@__id_0='[userid]'

Important part: it searches for __CurrentTenantId_1=null although I changed the tenant.

Of course this doesn't return a user (as the user is tenant user and not host user) and therefore it tries to insert a new CmsUser, (interestingly with the correct switched tenantid!). This if course produces an error because of the insertion of a duplicate Id.

I don't understand why this happens only in rare cases and not always. But I want to figure out and therefore, I need to know where this CmsUser Inserting during login process takes place. I couldn't find it from the sources. Maybe you could point me to the right direction to help me fix it.

Thanks in advance!

https://github.com/abpframework/abp/issues/13999

If somebody else also struggles with this, it seems that it will be fixed in 6.0 final. Thanks.

Btw. I think it's related this issue, therefore it seems to be a new feature, not bad at all, but there should be a possibility to disable it, since we might want the users to set their SocialSecurityNumber, but not their UserLevel, Department, Income,...

Hi @mahmut.gundogdu

I think you misunderstood me.

I'm using MVC and since 6.0.0-rc.3, the users of my application can change the ExtraProperties in their profile settings. I just want to disable this, as I'm using ExtraProperties that should be set only by administrators.

In Module Extensions Documentation there is also a sample where a User Type (Regular/ Moderator/ Superuser) is set by ExtraProperties. Of course we wouldn't want the user to set himself as Superuser ;-) I just implemented a similar logic.

Best regards Claus

Showing 11 to 20 of 41 entries
Made with ❤️ on ABP v8.2.0-preview Updated on March 25, 2024, 15:11