Hi,
Good catch — the post-process workaround needs to drop empty groups after filtering, otherwise a group whose features are all restricted to other providers stays in the result with an empty list. Here is the updated snippet:
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IFeatureAppService))]
public class MyFeatureAppService : FeatureAppService
{
public MyFeatureAppService(
IFeatureManager featureManager,
IFeatureDefinitionManager featureDefinitionManager,
IOptions<FeatureManagementOptions> options)
: base(featureManager, featureDefinitionManager, options)
{
}
public override async Task<GetFeatureListResultDto> GetAsync(string providerName, string providerKey)
{
var result = await base.GetAsync(providerName, providerKey);
foreach (var group in result.Groups)
{
var filtered = new List<FeatureDto>();
foreach (var feature in group.Features)
{
var definition = await FeatureDefinitionManager.GetAsync(feature.Name);
if (!definition.AllowedProviders.Any() || definition.AllowedProviders.Contains(providerName))
{
filtered.Add(feature);
}
}
group.Features = filtered;
}
result.Groups = result.Groups.Where(g => g.Features.Any()).ToList();
return result;
}
}
The framework fix in https://github.com/abpframework/abp/pull/25462 filters inline during enumeration, so the existing empty-group check in FeatureAppService.GetAsync already handles this — once you upgrade you can drop the override.
Thanks
Hi,
ABP's CAPTCHA is Google reCAPTCHA (v2 and v3) wrapped via the Owl.reCAPTCHA NuGet package. The services are registered as public DI dependencies in the Volo.Abp.Account.Pro.Public.Web module, so you can reuse them on any custom Razor Page. No middleware, no extra module to depend on (your *.Web project already references the Account Pro Web module).
SiteKey / SiteSecret / Version / Score all come from ABP settings (Abp.Account.Captcha.*), the same place the built-in Login/Register pages read from. So your custom page automatically uses the same keys you configured under Settings → Account → reCAPTCHA in the admin UI.
Here is a minimal working example. I built and verified it in a fresh MVC + Basic Theme app on ABP 10.4.
Pages/MyForm.cshtml.cs:
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Owl.reCAPTCHA;
using Volo.Abp.Account.Public.Web.Security.Recaptcha;
using Volo.Abp.Account.Security.Recaptcha;
using Volo.Abp.Account.Settings;
using Volo.Abp.AspNetCore.Mvc.UI.RazorPages;
using Volo.Abp.Settings;
namespace MyApp.Web.Pages;
public class MyFormModel : AbpPageModel
{
[BindProperty]
public InputModel Input { get; set; }
public int ReCaptchaVersion { get; set; }
public IAbpRecaptchaValidatorFactory RecaptchaValidatorFactory =>
LazyServiceProvider.LazyGetRequiredService<IAbpRecaptchaValidatorFactory>();
public IOptionsSnapshot<reCAPTCHAOptions> ReCaptchaOptions =>
LazyServiceProvider.LazyGetRequiredService<IOptionsSnapshot<reCAPTCHAOptions>>();
public virtual async Task<IActionResult> OnGetAsync()
{
ReCaptchaVersion = await SettingProvider.GetAsync<int>(AccountSettingNames.Captcha.Version);
await ReCaptchaOptions.SetAsync(ReCaptchaVersion == 3 ? reCAPTCHAConsts.V3 : reCAPTCHAConsts.V2);
return Page();
}
public virtual async Task<IActionResult> OnPostAsync()
{
ReCaptchaVersion = await SettingProvider.GetAsync<int>(AccountSettingNames.Captcha.Version);
await ReCaptchaOptions.SetAsync(ReCaptchaVersion == 3 ? reCAPTCHAConsts.V3 : reCAPTCHAConsts.V2);
var validator = await RecaptchaValidatorFactory.CreateAsync();
await validator.ValidateAsync(HttpContext.Request.Form[RecaptchaValidatorBase.RecaptchaResponseKey]);
Alerts.Success("CAPTCHA verified. Message: " + Input.Message);
return Page();
}
public class InputModel
{
[Required]
public string Message { get; set; }
}
}
Pages/MyForm.cshtml:
@page
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using Owl.reCAPTCHA.v2.TagHelpers
@using Owl.reCAPTCHA.v3.TagHelpers
@using Volo.Abp.Account.Public.Web.Security.Recaptcha
@model MyApp.Web.Pages.MyFormModel
@section scripts {
@if (Model.ReCaptchaVersion == 3)
{
<recaptcha-script-v3 />
<recaptcha-script-v3-js action="myform" execute="false" />
<script>
$("#myForm").submit(function (e) {
e.preventDefault();
var form = $(this);
grecaptcha.reExecute(function (token) {
form.find("input[type=hidden][data-captcha=true]").val(token);
form[0].submit();
});
});
</script>
}
else
{
<recaptcha-script-v2 />
<script>
function recaptchaCallback(token) {
$('#@RecaptchaValidatorBase.RecaptchaResponseKey').val(token);
$('#myForm button').removeAttr("disabled");
}
</script>
}
}
<h2>My Custom Form</h2>
<form id="myForm" method="post">
<input type="hidden" data-captcha="true"
name="@RecaptchaValidatorBase.RecaptchaResponseKey"
id="@RecaptchaValidatorBase.RecaptchaResponseKey" />
<div class="mb-3">
<label asp-for="Input.Message" class="form-label"></label>
<input asp-for="Input.Message" class="form-control" />
<span asp-validation-for="Input.Message" class="text-danger"></span>
</div>
@if (Model.ReCaptchaVersion == 2)
{
<div class="mb-3">
<recaptcha-div-v2 callback="recaptchaCallback" />
</div>
<button type="submit" class="btn btn-primary" disabled>Submit</button>
}
else
{
<button type="submit" class="btn btn-primary">Submit</button>
}
</form>
A few notes:
Pages/_ViewImports.cshtml generated by the ABP template already has @addTagHelper *, Owl.reCAPTCHA, so the recaptcha tag helpers work out of the box.IAbpRecaptchaValidatorFactory.CreateAsync() returns a V2 or V3 validator based on the Abp.Account.Captcha.Version setting. You don't pick the version manually in code.RecaptchaValidatorBase.RecaptchaResponseKey is the constant "g-recaptcha-response" — the standard hidden field name Google expects. Both the cshtml and the POST handler use this constant so they stay in sync.await ReCaptchaOptions.SetAsync(...) is what injects SiteKey / SiteSecret from ABP settings into Owl.reCAPTCHA at request time. If you forget this, the tag helpers won't render with your configured keys.ValidateAsync throws UserFriendlyException on failure, and ABP's exception filter renders it as an error alert automatically — no try/catch needed in the simple case.Abp.Account.Captcha.Score, default 0.5). If your custom page sees ScoreBelowThresholdException, lower this value under Settings → Account → reCAPTCHA → Score (e.g. 0.3), especially when testing on localhost where Google tends to return lower scores.If you want to read the built-in reference implementations, look at these files in the Volo.Abp.Account.Pro.Public.Web module: Pages/Account/Login.cshtml(.cs), Pages/Account/Register.cshtml(.cs), and Pages/Account/ForgotPassword.cshtml(.cs). The example above is essentially the same pattern as Register.cshtml.
Thanks
Your question ticket has been refunded.
Thanks.
Hi,
Thanks for the report — you're right, this is a framework-level bug. The Feature Management UI/API should respect FeatureDefinition.AllowedProviders to match the runtime behavior in IFeatureChecker. We'll fix it in the next version: https://github.com/abpframework/abp/pull/25462
For now, you can work around it on the backend by overriding FeatureAppService.GetAsync to filter features whose AllowedProviders does not include the current providerName. Add this class to your HttpApi.Host project:
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Features;
using Volo.Abp.FeatureManagement;
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IFeatureAppService))]
public class MyFeatureAppService : FeatureAppService
{
public MyFeatureAppService(
IFeatureManager featureManager,
IFeatureDefinitionManager featureDefinitionManager,
IOptions<FeatureManagementOptions> options)
: base(featureManager, featureDefinitionManager, options)
{
}
public override async Task<GetFeatureListResultDto> GetAsync(string providerName, string providerKey)
{
var result = await base.GetAsync(providerName, providerKey);
foreach (var group in result.Groups)
{
var filtered = new List<FeatureDto>();
foreach (var feature in group.Features)
{
var definition = await FeatureDefinitionManager.GetAsync(feature.Name);
if (!definition.AllowedProviders.Any() || definition.AllowedProviders.Contains(providerName))
{
filtered.Add(feature);
}
}
group.Features = filtered;
}
return result;
}
}
The Edition Features dialog will then hide features that are restricted to other providers.
Thanks
Hi,
Before suggesting any change, I'd like to confirm a few things, because the error message you posted is raised from a very specific code path and the URL you mentioned doesn't match it.
First, one fact about how this works in AI Management: Volo.AIManagement.Workspaces.Workspace.Consume is a resource permission, granted per-workspace instance. The grants are stored in AbpResourcePermissionGrants, which is multi-tenant aware — every grant row carries a TenantId, and queries are automatically filtered by the current tenant. So a grant written under the host context (TenantId = NULL) is not visible when a tenant user checks the permission, and vice versa.
A few things would help me give you a precise answer:
1) The exact request that returns the error.
Open browser DevTools → Network tab, reproduce the issue, and tell me which HTTP request returns the Authorization failed!...Consume response. The URL http://localhost:4200/ai-management/workspaces is the workspaces list page, and its route guard / list endpoint requires AIManagement.Workspaces (a regular permission), not Workspace.Consume. The Consume check only fires from endpoints that actually consume a workspace — chat completion, embeddings, OpenAI-compatible chat/model/validation paths, and similar. So something on the page is hitting one of those endpoints — most likely a child component initializing against a workspace, or a click into a specific one. Knowing the exact failing request URL will pin this down.
2) The actual grant rows, the user's tenant, and the workspace config.
Please run these on your API database and share the result. Note that ResourceKey stores the workspace Id (GUID as string), not the name — so filter by Name and inspect the rows:
SELECT Id, Name, ProviderName, ProviderKey, TenantId, ResourceName, ResourceKey
FROM AbpResourcePermissionGrants
WHERE Name = 'Volo.AIManagement.Workspaces.Workspace.Consume';
SELECT Id, UserName, TenantId FROM AbpUsers WHERE UserName = 'test2';
SELECT Id, Name, RequiredPermissionName FROM AbpWorkspaces WHERE Name = 'PatientAssistant';
This tells us (a) whether the grants were really persisted — the modal showing the checkbox is not proof, (b) the TenantId on the grant rows (NULL = host, otherwise a tenant), (c) the TenantId of test2, and (d) whether the workspace has a RequiredPermissionName set. If RequiredPermissionName is not null, the check path is different — it goes through a regular permission check (IsGrantedAnyAsync) before the resource permission check, and the Consume grant is bypassed if that one is granted.
3) Where you granted the permission.
Were you logged in as the host admin when you opened the workspace's permission modal, or were you operating inside a tenant (for example via Saas → Tenants → "Login with admin")? And the "Patient" role you granted to — is it a host-level role or a tenant-level role? These are separate records in AbpRoles even if they share the same name.
While we're at it, the answers to your other questions:
role claim in the token, and the user-based provider key comes from the sub claim. If you recently assigned a new role to test2 (e.g. just added Patient), test2 needs to re-login so the new role appears in the token; otherwise the role-based grant won't be matched. If you only changed the resource permission on an existing role, re-login is not needed.Once I have the three items above I can give you the exact fix.
Thanks
It could be a network issue or a problem with the ABPIO website.
Thanks.
: )
Hi,
Thanks for the clear repro. Confirmed it's a LeptonX issue with the custom-select overlay — the native <select> is hidden, so Blazorise's is-invalid class never reaches the visible proxy element, and the .invalid-feedback text stays hidden because it's no longer a sibling of the marked element.
While we ship a proper fix in the next release, please drop this small CSS into your project's global stylesheet (e.g. wwwroot/global-styles.css, or any css that gets bundled after the LeptonX theme):
.custom-select-wrapper:has(> select.form-select.is-invalid) .custom-select-display {
border-color: var(--bs-form-invalid-border-color, #c00d49) !important;
}
.custom-select-wrapper:has(> select.form-select.is-invalid) ~ .invalid-feedback {
display: block;
}
.custom-select-wrapper:has(> select.form-select.is-valid) .custom-select-display {
border-color: var(--bs-form-valid-border-color, #4fbf67) !important;
}
.custom-select-wrapper:has(> select.form-select.is-valid) ~ .valid-feedback {
display: block;
}
That gives the proxy a red border and shows the <ValidationError /> message exactly like TextInput does. We'll address it in LeptonX itself so the workaround can be removed in a future release.
Thanks.