Activities of "enisn"

Hi,

Thank you for reporting this issue. The behavior you’re experiencing is related to Windows Smart App Control, a security feature in Windows 11 that blocks applications it cannot verify. Since ABP Studio currently includes unsigned DLLs, Smart App Control flags the executable (Volo.App.Studio.UI.Host.exe) as potentially unsafe and prevents it from launching.

At this time:

Smart App Control can only be enabled or disabled during Windows installation. If it is enabled, unsigned applications may be blocked.

We are actively working on digitally signing ABP Framework DLLs to ensure compatibility with Smart App Control. This will be available in upcoming releases.

The current version of ABP Studio will remain affected until the signing process is completed.

Workarounds:

If you need immediate access, you can disable Smart App Control (note: this must be done during Windows installation; it cannot be toggled afterward).

Alternatively, you may run ABP Studio on a system where Smart App Control is not enabled.

We understand the inconvenience and appreciate your patience while we finalize the signing process. Your feedback helps us prioritize this fix, and we’ll update you as soon as the signed version is available.

You're absolutely right - there's currently no built-in support for prorated upgrades/downgrades in the ABP SaaS + Payment modules. Let me explain why this is happening and how to fix it.

When you call _subscriptionAppService.CreateSubscriptionAsync(), it creates a brand new Stripe subscription every time. Looking at the source code, it simply creates a new PaymentRequest without checking if the tenant already has an active subscription.

This is why you're seeing:

  • Full charge for original edition → Full charge for new edition
  • No proration between the two

The Solution

  • You need to create a custom service that updates the existing Stripe subscription instead of creating a new one. Here's how:
public interface ISubscriptionChangeAppService : IApplicationService
{
    Task<PaymentRequestWithDetailsDto?> ChangeSubscriptionAsync(Guid newEditionId, Guid tenantId);
}
public class SubscriptionChangeAppService : YourProjectnameAppService, ISubscriptionChangeAppService
{
    protected IPaymentRequestRepository PaymentRequestRepository { get; }
    protected IPlanRepository PlanRepository { get; }
    protected ITenantRepository TenantRepository { get; }
    protected EditionManager EditionManager { get; }
    public SubscriptionChangeAppService(
        IPaymentRequestRepository paymentRequestRepository,
        IPlanRepository planRepository,
        ITenantRepository tenantRepository,
        EditionManager editionManager)
    {
        PaymentRequestRepository = paymentRequestRepository;
        PlanRepository = planRepository;
        TenantRepository = tenantRepository;
        EditionManager = editionManager;
    }
    public virtual async Task<PaymentRequestWithDetailsDto?> ChangeSubscriptionAsync(Guid newEditionId, Guid tenantId)
    {
        var edition = await EditionManager.GetEditionForSubscriptionAsync(newEditionId);
        
        // Get Stripe price ID for the new edition
        var gatewayPlan = await PlanRepository.GetGatewayPlanAsync(edition.PlanId, StripeConsts.GatewayName);
        var newPriceId = gatewayPlan.ExternalId;
        // Find existing subscription payment request
        var tenant = await TenantRepository.GetAsync(tenantId);
        
        // Option 1: Get from tenant's ExtraProperties if you store subscription ID there
        // Option 2: Query payment requests by tenant and find one with ExternalSubscriptionId
        var existingPaymentRequests = await PaymentRequestRepository.GetListByTenantId(tenantId); // There is no this query in the repository, you'll need to add this kind of query.
        
        var existingSubscription = existingPaymentRequests
            .FirstOrDefault(pr => !pr.ExternalSubscriptionId.IsNullOrEmpty() && 
                                   pr.State == PaymentRequestState.Completed);
        
        if (existingSubscription == null || existingSubscription.ExternalSubscriptionId.IsNullOrEmpty())
        {
            // No existing subscription, create new one
            return await SubscriptionAppService.CreateSubscriptionAsync(newEditionId, tenantId);
        }
        // UPDATE existing Stripe subscription (this handles proration automatically)
        var subscriptionService = new SubscriptionService();
        var subscription = await subscriptionService.GetAsync(existingSubscription.ExternalSubscriptionId);
        
        var existingItemId = subscription.Items.Data[0].Id;
        var options = new SubscriptionUpdateOptions
        {
            Items = new List<SubscriptionItemOptions>
            {
                new SubscriptionItemOptions
                {
                    Id = existingItemId,  // Required to update existing item
                    Price = newPriceId,   // New plan's Stripe price ID
                }
            }
            // Stripe automatically prorates by default!
        };
        await subscriptionService.UpdateAsync(existingSubscription.ExternalSubscriptionId, options);
        
        // IMPORTANT: Update the PaymentRequest with the new EditionId
        // This ensures the webhook handler knows to update the tenant's edition
        existingSubscription.ExtraProperties[EditionConsts.EditionIdParameterName] = newEditionId.ToString();
        await PaymentRequestRepository.UpdateAsync(existingSubscription);
        // The Stripe webhook will trigger SubscriptionUpdatedHandler
        // which will update the tenant's EditionId automatically
        return null; // No payment needed - Stripe handles proration in next invoice
    }
}
  • Update Your Page
public async Task<IActionResult> OnPostChangeEditionAsync(Guid editionId)
{
    try
    {
        var result = await _subscriptionChangeAppService.ChangeSubscriptionAsync(editionId, _currentTenant.Id);
        if (result != null && result.RequiresPayment && result.PaymentRequestId != Guid.Empty)
        {
            return LocalRedirectPreserveMethod($"/Payment/GatewaySelection?paymentRequestId={result.PaymentRequestId}");
        }
        Alerts.Success("Plan changed successfully! Stripe will process the prorated amount.");
        return RedirectToPage();
    }
    catch (UserFriendlyException ex)
    {
        Alerts.Danger(ex.Message);
        return RedirectToPage();
    }
}

It seems you added following packages to your backend application:

Volo.CmsKit.Pro.Domain
Volo.CmsKit.Pro.Domain.Shared
Volo.CmsKit.Pro.EntityFrameworkCore
Volo.CmsKit.Pro.HttpApi

But Application layer is missing, this is where AppServices are located. Canyou add Volo.CmsKit.Pro.Applicaton package to your project and try again?

For the "http/https"problem on OIDC with AuthServer, this approach by microsoft might help: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0#when-it-isnt-possible-to-add-forwarded-headers-and-all-requests-are-secure

app.Use((context, next) =>
{
    context.Request.Scheme = "https";
    return next(context);
});
Answer

Hi,

We could reproduce this problem, the problem occured in the module. The problem will be fixed in a patch version of v10.0.x

Hi,

That module is Preview now, and it's not stable yet. Breaking changes might happen.

Is there anything specific we need to do or enable in order to start using and testing these AI modules in a real (but controlled) project?

No need for a custom flag or enabling something in your project. ABP AI features based on Microsoft's packages and they're on -preview version mostly, this is the important point to know. If Microsoft.Extensions.AI makes a breaking-cahnge or drop some features during preview period, so it'll affect to our features too. That's why we haven't announced them yet.

Are there any important current limitations or “known gaps” we should be aware of when adopting the preview (e.g. missing scenarios, not-yet-implemented providers, configuration restrictions)?

In our scenario, we are trying to move forward with firm steps. So, essential features like RAG and Vector Store options are not implemented yet. We're waiting for the stability of the infrastructure and waiting Microsoft's next steps, like Agent Framework, which was announced during Dotnet Conf this year. We closely follow updates from this, and our AI modules will be based on the Agent Framework in the near future.

Roadmap / planning What is the current roadmap for these AI modules?

As I mentioned, we'll be based on Agent Framework soon, and also RAG and Vector Store Options, MCP Server building infrastructure etc. will be implemented. But we're still waiting the stable packages from Microsoft first to continue further.

Can we configure a custom chat client that calls this external API endpoint using the ConfigureChatClient configuration?

Yes, but if that external APIis compatible with OpenAI API standards, you can connect it as an OpenAI Client. Or an alternative scenario, if that external API is built with ABP and uses the AI Management module, you can directly connect it by using HttpApi.Client package as always.

Do you have a minimal example (or guidance) for implementing a custom chat client that forwards requests to an arbitrary HTTP endpoint?

This section might be the scenario that you're looking for. But both applications should use the AI Management module.

https://abp.io/docs/latest/modules/ai-management#scenario-3-ai-management-client-with-remote-execution

perhaps only from host, not tenant?

When I checked the source code, I can see that it doesn't matter if it's from tenant or host side. It gets TenantId as parameter and that payment request can be paid from anyone even the user is not authenticated, you can just redirect to the payment page and when that created PaymentRequest is paid, that subscribtion starts. In the Domain layer of Saas module, you can see SubscriptionCreatedHandler.cs handles the susbcription started event from payment module and applies everything. You can customize that point to change logic according to your custom business requirements.

If webhooks implemented properly (working properly for Stripe when configured) you can handle events from SubscriptionCanceledEto and SubscriptionUpdatedEto like they're already subscribed in the Saas module.

_paymentRequestAppService.CreateAsync, but how does that fit into subscriptions?

Yes! Payment module itself supports subscriptions without Saas module dependency. You can just pass PaymentType = PaymentType.Subscription while creating a payment request, and that's it.

   public virtual async Task<IActionResult> OnPostAsync()
    {
        var paymentRequest = await PaymentRequestAppService.CreateAsync(
            new PaymentRequestCreateDto()
            {
                Products =
                {
                    new PaymentRequestProductCreateDto
                    {
                        PaymentType = PaymentType.Subscription,
                        // ...
                    }
                }
            });

        return LocalRedirectPreserveMethod("/Payment/GatewaySelection?paymentRequestId=" + paymentRequest.Id);
    }

Later, you can track with DistributedEvents published by Payment Module itself. https://abp.io/docs/latest/modules/payment#distributed-events

public class SubscriptionCreatedHandler :
    IDistributedEventHandler<SubscriptionCreatedEto>,
    ITransientDependency
{

    public async Task HandleEventAsync(SubscriptionCreatedEto eventData)
    {
    }
}

Then you can implement your own logic without depending on Tenant & Edition. You can keep your custom data in ExtraProperties of PaymentRequest to keep your product or service data while starting or stopping the subscription logic

It seems it's not a bug or a problem on leptonx side. It seems it's how it was designed at the first time.

You support ticket is refunded

We dicussed on live, and decided to use the same database across applications to handle Users, Permissions and Settings logic in a consistency. In the more complex scenarios, micro-service template can be used as a best practise

Hi,

// ... other code ...

indicates other unimportant code blocks that is obvious what they do. If you need a key solution, I can share the exact entire class with you.

Here what you need to do:

File: MerkeziSSO.DbMigrator/appsettings.json

Add this configuration section (merge with existing config):

{
  "ConnectionStrings": {
    "Default": "..."
  },
  "OpenIddict": {
    "Applications": {
      "TestClient_App": {
        "ClientId": "TestClient_App",
        "ClientSecret": "1q2w3E*",
        "RootUrl": "https://localhost:44305"
      },
      "TestClient_Swagger": {
        "ClientId": "TestClient_Swagger",
        "RootUrl": "https://localhost:44308"
      }
    }
  }
}

The entire file of OpenIddictDataSeedContributor.cs

public class OpenIddictDataSeedContributor : IDataSeedContributor, ITransientDependency
{
    private readonly IConfiguration _configuration;
    private readonly IOpenIddictApplicationRepository _openIddictApplicationRepository;
    private readonly IAbpApplicationManager _applicationManager;
    private readonly IOpenIddictScopeRepository _openIddictScopeRepository;
    private readonly IOpenIddictScopeManager _scopeManager;

    public OpenIddictDataSeedContributor(
        IConfiguration configuration,
        IOpenIddictApplicationRepository openIddictApplicationRepository,
        IAbpApplicationManager applicationManager,
        IOpenIddictScopeRepository openIddictScopeRepository,
        IOpenIddictScopeManager scopeManager)
    {
        _configuration = configuration;
        _openIddictApplicationRepository = openIddictApplicationRepository;
        _applicationManager = applicationManager;
        _openIddictScopeRepository = openIddictScopeRepository;
        _scopeManager = scopeManager;
    }

    public async Task SeedAsync(DataSeedContext context)
    {
        await CreateApiScopesAsync();
        await CreateClientsAsync();
    }

    private async Task CreateApiScopesAsync()
    {
        // Create a scope for your API application
        if (await _openIddictScopeRepository.FindByNameAsync("TestClient") == null)
        {
            await _scopeManager.CreateAsync(new OpenIddictScopeDescriptor
            {
                Name = "TestClient",  // This is your API resource scope
                DisplayName = "Test Client API",
                Description = "Test Client API Scope",
                Resources = { "TestClient" }  // API resource name
            });
        }

        // MerkeziSSO scope (for accessing SSO's own APIs)
        if (await _openIddictScopeRepository.FindByNameAsync("MerkeziSSO") == null)
        {
            await _scopeManager.CreateAsync(new OpenIddictScopeDescriptor
            {
                Name = "MerkeziSSO",
                DisplayName = "Merkezi SSO API",
                Resources = { "MerkeziSSO" }
            });
        }
    }

    private async Task CreateClientsAsync()
    {
        var commonScopes = new List<string>
        {
            OpenIddictConstants.Permissions.Scopes.Address,
            OpenIddictConstants.Permissions.Scopes.Email,
            OpenIddictConstants.Permissions.Scopes.Phone,
            OpenIddictConstants.Permissions.Scopes.Profile,
            OpenIddictConstants.Permissions.Scopes.Roles,
            "MerkeziSSO",  // Custom scopes are just strings
            "TestClient"   // Important! Your API scope
        };

        // Client for your Blazor/React App
        var webClientId = _configuration["OpenIddict:Applications:TestClient_App:ClientId"];
        if (!string.IsNullOrWhiteSpace(webClientId))
        {
            var webClientRootUrl = _configuration["OpenIddict:Applications:TestClient_App:RootUrl"]!.TrimEnd('/');
            
            await CreateApplicationAsync(
                name: webClientId!,
                type: OpenIddictConstants.ClientTypes.Confidential,
                consentType: OpenIddictConstants.ConsentTypes.Implicit,
                displayName: "Test Client Application",
                secret: _configuration["OpenIddict:Applications:TestClient_App:ClientSecret"] ?? "1q2w3E*",
                grantTypes: new List<string>
                {
                    OpenIddictConstants.GrantTypes.AuthorizationCode,
                    OpenIddictConstants.GrantTypes.RefreshToken
                },
                scopes: commonScopes,
                clientUri: webClientRootUrl,
                redirectUri: $"{webClientRootUrl}/signin-oidc",
                postLogoutRedirectUri: $"{webClientRootUrl}/signout-callback-oidc"
            );
        }

        // Swagger client for API testing
        var swaggerClientId = _configuration["OpenIddict:Applications:TestClient_Swagger:ClientId"];
        if (!string.IsNullOrWhiteSpace(swaggerClientId))
        {
            var swaggerRootUrl = _configuration["OpenIddict:Applications:TestClient_Swagger:RootUrl"]?.TrimEnd('/');
            
            await CreateApplicationAsync(
                name: swaggerClientId!,
                type: OpenIddictConstants.ClientTypes.Public,
                consentType: OpenIddictConstants.ConsentTypes.Implicit,
                displayName: "Test Client Swagger",
                secret: null,
                grantTypes: new List<string>
                {
                    OpenIddictConstants.GrantTypes.AuthorizationCode
                },
                scopes: commonScopes,
                clientUri: swaggerRootUrl,
                redirectUri: $"{swaggerRootUrl}/swagger/oauth2-redirect.html"
            );
        }
    }

    private async Task CreateApplicationAsync(
        string name,
        string type,
        string consentType,
        string displayName,
        string? secret,
        List<string> grantTypes,
        List<string> scopes,
        string? clientUri = null,
        string? redirectUri = null,
        string? postLogoutRedirectUri = null)
    {
        var client = await _openIddictApplicationRepository.FindByClientIdAsync(name);

        var application = new AbpApplicationDescriptor
        {
            ClientId = name,
            ClientType = type,
            ClientSecret = secret,
            ConsentType = consentType,
            DisplayName = displayName,
            ClientUri = clientUri,
        };

        // Add grant type permissions
        foreach (var grantType in grantTypes)
        {
            if (grantType == OpenIddictConstants.GrantTypes.AuthorizationCode)
            {
                application.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode);
                application.Permissions.Add(OpenIddictConstants.Permissions.ResponseTypes.Code);
                application.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Authorization);
                application.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Token);
            }

            if (grantType == OpenIddictConstants.GrantTypes.RefreshToken)
            {
                application.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.RefreshToken);
                application.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Token);
            }

            application.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Revocation);
            application.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Introspection);
        }

        if (!string.IsNullOrWhiteSpace(redirectUri) || !string.IsNullOrWhiteSpace(postLogoutRedirectUri))
        {
            application.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.EndSession);
        }

        // Add scope permissions
        var buildInScopes = new[]
        {
            OpenIddictConstants.Permissions.Scopes.Address,
            OpenIddictConstants.Permissions.Scopes.Email,
            OpenIddictConstants.Permissions.Scopes.Phone,
            OpenIddictConstants.Permissions.Scopes.Profile,
            OpenIddictConstants.Permissions.Scopes.Roles
        };

        foreach (var scope in scopes)
        {
            if (buildInScopes.Contains(scope))
            {
                application.Permissions.Add(scope);
            }
            else
            {
                application.Permissions.Add(OpenIddictConstants.Permissions.Prefixes.Scope + scope);
            }
        }

        // Add redirect URIs
        if (!string.IsNullOrWhiteSpace(redirectUri))
        {
            if (Uri.TryCreate(redirectUri, UriKind.Absolute, out var uri))
            {
                application.RedirectUris.Add(uri);
            }
        }

        if (!string.IsNullOrWhiteSpace(postLogoutRedirectUri))
        {
            if (Uri.TryCreate(postLogoutRedirectUri, UriKind.Absolute, out var uri))
            {
                application.PostLogoutRedirectUris.Add(uri);
            }
        }

        if (client == null)
        {
            await _applicationManager.CreateAsync(application);
        }
        else
        {
            await _applicationManager.UpdateAsync(client.ToModel(), application);
        }
    }
}

It should be compiled without an error right know:


If you go with Solution C:

And also corrected the compilation errors in MerkeziSSO.Domain/Identity/PermissionClaimsPrincipalContributor.cs

using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.DependencyInjection;
using Volo.Abp.PermissionManagement;
using Volo.Abp.Security.Claims;

namespace MerkeziSSO.Identity
{
    public class PermissionClaimsPrincipalContributor : IAbpClaimsPrincipalContributor, ITransientDependency
    {
        private readonly IPermissionManager _permissionManager;

        public PermissionClaimsPrincipalContributor(IPermissionManager permissionManager)
        {
            _permissionManager = permissionManager;
        }

        public async Task ContributeAsync(AbpClaimsPrincipalContributorContext context)
        {
            var identity = context.ClaimsPrincipal.Identities.FirstOrDefault();
            if (identity == null)
            {
                return;
            }

            var userId = context.ClaimsPrincipal.FindUserId();
            
            if (userId.HasValue)
            {
                // GetAllAsync with provider name and key
                var permissions = await _permissionManager.GetAllAsync(
                    "U",  // User provider name (UserPermissionValueProvider.ProviderName)
                    userId.Value.ToString()
                );
                
                //  Filter granted permissions and add as claims
                foreach (var permission in permissions.Where(p => p.IsGranted))
                {
                    identity.AddClaim(new Claim("permission", permission.Name));
                }
            }
        }
    }
}

Also fixed version of TestClient.HttpApi.Host/Permissions/ClaimPermissionValueProvider.cs

using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Authorization.Permissions;

namespace TestClient.Permissions
{
    public class ClaimPermissionValueProvider : PermissionValueProvider
    {
        public const string ProviderName = "Claim";

        public override string Name => ProviderName;

        public ClaimPermissionValueProvider(IPermissionStore permissionStore)
            : base(permissionStore)
        {
        }

        public override Task<PermissionGrantResult> CheckAsync(PermissionValueCheckContext context)
        {
            var permissionClaims = context.Principal?
                .FindAll("permission")
                .Select(c => c.Value)
                .ToList();

            if (permissionClaims != null && permissionClaims.Contains(context.Permission.Name))
            {
                return Task.FromResult(PermissionGrantResult.Granted);
            }

            return Task.FromResult(PermissionGrantResult.Undefined);
        }

        public override Task<MultiplePermissionGrantResult> CheckAsync(PermissionValuesCheckContext context)
        {
            var permissionClaims = context.Principal?
                .FindAll("permission")
                .Select(c => c.Value)
                .ToHashSet();

            var result = new MultiplePermissionGrantResult();
            
            if (permissionClaims != null)
            {
                foreach (var permission in context.Permissions)
                {
                    result.Result[permission.Name] = permissionClaims.Contains(permission.Name)
                        ? PermissionGrantResult.Granted
                        : PermissionGrantResult.Undefined;
                }
            }

            return Task.FromResult(result);
        }
    }
}

And do not forget to register it in TestClientHttpApiHostModule.cs file:

public override void ConfigureServices(ServiceConfigurationContext context)
{
    // ... existing code ...
    
    // 👇 Add this configuration
    Configure<AbpPermissionOptions>(options =>
    {
        options.ValueProviders.Add<ClaimPermissionValueProvider>();
    });
}

Since your project using OpenIdDict module not IdentityServer, We'll need these extra steps:

  • MerkeziSSO.AuthServer/Identity/PermissionOpenIddictClaimsPrincipalHandler.cs
public class PermissionOpenIddictClaimsPrincipalHandler : IAbpOpenIddictClaimsPrincipalHandler, ITransientDependency
    {
        public Task HandleAsync(AbpOpenIddictClaimsPrincipalHandlerContext context)
        {
            // Set destinations for permission claims
            foreach (var claim in context.Principal.Claims)
            {
                if (claim.Type == "permission")
                {
                    // Add permission claims to access token
                    claim.SetDestinations(OpenIddictConstants.Destinations.AccessToken);
                }
            }

            return Task.CompletedTask;
        }
    }
  • MerkeziSSOAuthServerModule.cs

In ConfigureServices method, add this:

public override void ConfigureServices(ServiceConfigurationContext context)
{
    // ... your existing configuration ...
    
    // Register the OpenIddict claims handler
    Configure<AbpOpenIddictClaimsPrincipalOptions>(options =>
    {
        options.ClaimsPrincipalHandlers.Add<PermissionOpenIddictClaimsPrincipalHandler>();
    });
}

Example token payload:

{
  "iss": "https://localhost:44387",
  "aud": ["TestClient"],
  "sub": "39f69c84-2862-bcba-ca19-3a0dbbbfe559",
  "email": "admin@abp.io",
  "role": "admin",
  "permission": [
    "AbpIdentity.Users",
    "AbpIdentity.Users.Create",
    "AbpIdentity.Users.Update"
  ],
  "exp": 1729565400
}

I personally do not suggest this way considering the token sizes is going to be huge. But it shows the approach. You may combine different approaches according to your requirements.


I highly recommend to you to analyze the OpenIdDict module if you need to customize: https://github.com/abpframework/abp/tree/dev/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers

The module and ABP's AuthServer template is designed to be used standalone Authentication Servers like Keycloak, Auth0, authentik etc... But the problem is user itself and ICurrentUser, and their permissions. If you're able to connect the same database at least for identity module it'll work.

But same logic can be applied for PermissionManagement and SettingManagement module, too. If you want to manage permissions and setting globally across the system.

Showing 1 to 10 of 787 entries
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.2.0-preview. Updated on February 17, 2026, 09:10
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.