Thanks for looking into this.
A few questions:
So based on the AI reply, it sounds like I may still be able to use ISubscriptionAppService.CreateSubscriptionAsync to change subscription, but perhaps only from host, not tenant? It also seems to indicate I could create a payment intent through _paymentRequestAppService.CreateAsync, but how does that fit into subscriptions? I would think if I wanted to maintain the subscription renewal and other supported aspects, I would need to go through the subscription app service.
Related to this: I don't see a method in ISubscriptionAppService which allows for the deletion/cancellation of a subscription. I will also need something to this effect since I will need to allow users to cancel in a self-service portal.
Hi, I need to create a self-service upgrade/downgrade edition page. I need help understanding how this works, since I couldn't find any publicly available documentation on this. I presume there is already a system in place to accommodate this, since when upgrading, the charge would need to be prorated. But I'm guessing that these systems are probably tied in to manual edition changing by an admin.
In my self-service registration page, I use _subscriptionAppService.CreateSubscriptionAsync() to create the payment request and then direct the user to the Stripe page to process the payment and start the subscription. I also already have the source code from the StripePaymentGateway so I can make modifications there if necessary to get this working.
I wanted to see if there was a streamlined way of doing this. I could probably find a way to do it by directly modifying the payment gateway code and directly interface with Stripe, but my concern is that I may inadvertently bypass something in the Payment module which could cause additional problems, and I am also concerned I could miss something and cause an issue with the charge amount, subscription renewal date (since that is abstracted and I cannot access it afaik), etc.
If there is some endpoint I could access that would help with this, that would be greatly appreciated.
Thanks for the help on this. I was able to implement the custom Stripe gateway and pass the email in through an extra property.
Ok, that makes sense. I am not using any tenant resolvers like subdomain tenant resolver, so I decided to just set the tenant cookie when logging in. This seems to have resolved my issue.
Thanks for your help.
Ok, I made a lot of progress towards figuring out why this is occurring, but I'm not sure how to fix it with my current implementation (will explain below)
First, I created the fresh template solution to attempt to recreate the problem, and of course, it worked in the new project. However, I didn't have anything that was intentionally blocking the ChangePassword page, so I started testing a bunch of different configurations to see if I could get it to work.
I found out that in my existing project, using the default login model rather than my overridden login model, it worked as expected. I further tracked this down to the tenant switcher section. In my custom login page, I removed the tenant switcher, instead opting to infer the tenant from the email entered and use that to switch to the corresponding tenant in the code. See my custom login model OnPostAsync:
public override async Task<IActionResult> OnPostAsync(string action)
{
try
{
if (LoginInput.UserNameOrEmailAddress == "admin")
{
return await base.OnPostAsync(action);
}
Guid tenantId = await _multiTenancyAppService.GetTenantIdByEmailAsync(LoginInput.UserNameOrEmailAddress);
if (tenantId == Guid.Empty)
{
ModelState.AddModelError(string.Empty, "Account not found. Please register if you do not have an account.");
return Page();
}
using (CurrentTenant.Change(tenantId))
{
return await base.OnPostAsync(action);
}
}
catch (UserFriendlyException ex)
{
ViewData["ErrorMessage"] = ex.Message;
await base.OnGetAsync();
return Page();
}
}
That specific section:
using (CurrentTenant.Change(tenantId))
{
return await base.OnPostAsync(action);
}
works normally to log the user in to their tenant without requiring them to switch tenants in the login page (I want my users to be as multitenancy-unaware as possible)
However, that doesn't work with the change password page. If I have the tenant null and log into a normal account, it will properly infer the tenant by the email, change tenant in code, and login. If I have the tenant null and attempt to log into an account that has ShouldChangePasswordOnNextLogin set to true, it kicks me back to login page. If I manually switch the tenant using the tenant switcher in the UI, then it works.
So here is the question: Is this a bug? Why does using (CurrentTenant.Change(tenantId)) work for logged in, but not for being redirected to the /ChangePassword page?
It will take me a little while to create a new project and add the necessary code to recreate this. I will try to get to this tomorrow.
Thank you for that. I attempted it, but it didn't work, so I tried removing all of my authorization code (so I can access host pages and am not locked out of anything by default) and tried to see if I can at least get to the ChangePassword page, but it still isn't working. At this point, I don't think there is anything on my end that is preventing this, though I could be wrong. The main thing that is different is that I am creating users through my own appservice:
public async Task UpsertUserAsync(Guid userId, string email, string? password, bool forceChangePassword)
{
if (userId != Guid.Empty)
{
IdentityUserDto existingUser = await _identityUserAppService.GetAsync(userId);
if (existingUser == null)
{
throw new UserFriendlyException("User not found.");
}
string username = existingUser.UserName == "admin" ? "admin" : email;
IdentityUserUpdateDto updateDto = new IdentityUserUpdateDto
{
UserName = username,
Email = email,
ShouldChangePasswordOnNextLogin = forceChangePassword,
IsActive = true,
EmailConfirmed = true,
PhoneNumberConfirmed = true,
RoleNames = ["admin"]
};
await _identityUserAppService.UpdateAsync(userId, updateDto);
return;
}
// Make sure email is unique
bool isEmailUnique = await IsEmailUnique(email);
if (!isEmailUnique)
{
throw new UserFriendlyException("Email address is already registered. Please use another email address.");
}
await _identityUserAppService.CreateAsync(new IdentityUserCreateDto
{
UserName = email,
Email = email,
Password = password,
ShouldChangePasswordOnNextLogin = forceChangePassword,
IsActive = true,
EmailConfirmed = true,
PhoneNumberConfirmed = true,
RoleNames = ["admin"]
});
}
Could there be something wrong with how I am creating the users here which might impact their ability to reach that page? There are some things I will likely need to resolve here like verifying emails, etc, but as a proof of concept, I think this should work, and it does when ShouldChangePasswordOnNextLogin is set to false, but not when it is set to true.
Let me know if I can provide any more info.
I have modified a fair amount at this point, so I am not able to get the "Administration" section to show up in Host, only when logged in. I have var administration = context.Menu.GetAdministration(); in my menu contributor, and I have commented out the code i provided above that authorizes the whole app. It allows me to access Host but I still cannot access Administration.
Here is my MenuContributor:
private static Task ConfigureMainMenuAsync(MenuConfigurationContext context)
{
var l = context.GetLocalizer<ArmadaResource>();
var currentUser = context.ServiceProvider.GetRequiredService<ICurrentUser>();
var currentTenant = context.ServiceProvider.GetRequiredService<ICurrentTenant>();
bool isHostAdmin = currentUser.IsAuthenticated && currentTenant.Id == null;
//Home
context.Menu.AddItem(
new ApplicationMenuItem(
ArmadaIOMenus.Home,
l["Menu:Home"],
"~/",
icon: "fa fa-home",
order: 1
)
);
//HostDashboard
context.Menu.AddItem(
new ApplicationMenuItem(
ArmadaIOMenus.HostDashboard,
l["Menu:Dashboard"],
"~/HostDashboard",
icon: "fa fa-line-chart",
order: 2
).RequirePermissions(ArmadaIOPermissions.Dashboard.Host)
);
//TenantDashboard
//context.Menu.AddItem(
// new ApplicationMenuItem(
// ArmadaIOMenus.TenantDashboard,
// l["Menu:Dashboard"],
// "~/Dashboard",
// icon: "fa fa-line-chart",
// order: 2
// ).RequirePermissions(ArmadaIOPermissions.Dashboard.Tenant)
//);
context.Menu.AddItem(
new ApplicationMenuItem(
ArmadaIOMenus.Forms,
l[ArmadaIOMenus.Forms],
url: "/Forms",
icon: "fas fa-file",
order: 1
)
);
context.Menu.AddItem(
new ApplicationMenuItem(
ArmadaIOMenus.Calendar,
l[ArmadaIOMenus.Calendar],
url: "/Calendar",
icon: "fas fa-calendar-alt",
order: 2
)
);
context.Menu.AddItem(
new ApplicationMenuItem(
ArmadaIOMenus.Settings,
l[ArmadaIOMenus.Settings],
icon: "fas fa-gear",
order: 2
).AddItem(
new ApplicationMenuItem(
ArmadaIOMenus.GeneralSettings,
l[ArmadaIOMenus.GeneralSettings],
url: "/Settings"
)
).AddItem(
new ApplicationMenuItem(
ArmadaIOMenus.CompanyLocations,
l[ArmadaIOMenus.CompanyLocations],
url: "/Settings/CompanyLocations"
)
).AddItem(
new ApplicationMenuItem(
ArmadaIOMenus.Drivers,
l[ArmadaIOMenus.Drivers],
url: "/Settings/Drivers"
)
).AddItem(
new ApplicationMenuItem(
ArmadaIOMenus.LimitedItems,
l[ArmadaIOMenus.LimitedItems],
url: "/LimitedResources"
)
).AddItem(
new ApplicationMenuItem(
ArmadaIOMenus.Payments,
l[ArmadaIOMenus.Payments],
url: "/Settings/Payments/StripeAccount"
)
).AddItem(
new ApplicationMenuItem(
ArmadaIOMenus.Users,
l[ArmadaIOMenus.Users],
url: "/Settings/Users"
)
)
);
//Saas
context.Menu.SetSubItemOrder(SaasHostMenuNames.GroupName, 3);
//Administration
var administration = context.Menu.GetAdministration();
//if (!isHostAdmin)
//{
// context.Menu.Items.Remove(administration);
//}
administration.Order = 6;
//Administration->Identity
administration.SetSubItemOrder(IdentityMenuNames.GroupName, 2);
//Administration->OpenIddict
administration.SetSubItemOrder(OpenIddictProMenus.GroupName, 3);
//Administration->Language Management
administration.SetSubItemOrder(LanguageManagementMenuNames.GroupName, 4);
//Administration->Text Template Management
administration.SetSubItemOrder(TextTemplateManagementMainMenuNames.GroupName, 5);
//Administration->Audit Logs
administration.SetSubItemOrder(AbpAuditLoggingMainMenuNames.GroupName, 6);
//Administration->Settings
administration.SetSubItemOrder(SettingManagementMenuNames.GroupName, 7);
return Task.CompletedTask;
}
However, why do you think I should move away from my existing solution? I would at least like to be able to get the path to the resetpassword page so I can whitelist it as an option in case I need it.
Thanks for sending the source code.
Where do I put this file so that it overrides the default? And how do I actually populate that field in StripePaymentGateway with the registration email? My confusion stems from the fact that I am only interfacing with _subscriptionAppService.CreateSubscriptionAsync() to create the PaymentRequest, and then passing the PaymentRequest.Id to the GatewaySelection URL. If I extend StripePaymentGateway, I have no idea how to interface with this in a meaningful way.
Any guidance you can provide would be greatly appreciated.