I know, but we can't do that, the link will be just: abp.io/account/login
what's VerifyBaseUrl? I am using v3 and do we need to set up the Site key and Site secret for each tenant? it's working in production, but it doesn't work in my local
no, only registration has the the tenant info:
Registration: Always start with a link that contains tenant info. For example, send the user a link with the tenant embedded.
the login page doesn't have the tenant info:
Login: Iterate through all tenants to check whether the email exists in any tenant. If a matching username/email + password combination is found in a tenant, log the user in.
For privacy protection of tenants, we cannot display the tenant list on the login and registration pages. A possible solution:
Registration: Always start with a link that contains tenant info. For example, send the user a link with the tenant embedded.
Login: Iterate through all tenants to check whether the email exists in any tenant. If a matching username/email + password combination is found in a tenant, log the user in.
If the same credentials are valid in multiple tenants, prompt the user to choose which tenant to log into.
any implementation suggestions?
ok, but AbpReCaptchaOptions could not be found
Works correctly in production server, but in local development environment, siteverify always returns a low score, causing validation to fail with error message: ** Verification failed, score below threshold**
I have added localhost in the domain list on the google reCAPTCHA key page
I have a hangfilre job: public class HourlyBackgroundWorker : HangfireBackgroundWorkerBase { private readonly ILogger<HourlyBackgroundWorker> _logger; private readonly IUserFileFeedbackService _userFileFeedbackService; private readonly IDataConnectionUserAnswerService _dataConnectionUserAnswerService; private readonly IDataConnectionUserAnswerRepository _dataConnectionUserAnswerRepository; private readonly IFeedbackFilesUpdaterService _feedbackFilesUpdaterService;
public HourlyBackgroundWorker(ILogger<HourlyBackgroundWorker> logger,
IUserFileFeedbackService userFileFeedbackService,
IDataConnectionUserAnswerService dataConnectionUserAnswerService,
IDataConnectionUserAnswerRepository dataConnectionUserAnswerRepository,
IFeedbackFilesUpdaterService feedbackFilesUpdaterService)
{
RecurringJobId = nameof(HourlyBackgroundWorker);
CronExpression = Cron.Hourly();
_logger = logger;
_userFileFeedbackService = userFileFeedbackService;
_dataConnectionUserAnswerService = dataConnectionUserAnswerService;
_dataConnectionUserAnswerRepository = dataConnectionUserAnswerRepository;
_feedbackFilesUpdaterService = feedbackFilesUpdaterService;
}
public override async Task DoWorkAsync(CancellationToken cancellationToken = default)
{
using var uow = LazyServiceProvider.LazyGetRequiredService<IUnitOfWorkManager>().Begin();
_logger.LogInformation("Hangfire:HourlyBackgroundWorker::UpdateFeedbacks");
await _feedbackFilesUpdaterService.UpdateAllFeedbacksAsync();
}
Is Hangfire multi-tenant aware by default? does this hourly job runs for each tenant? i.e. runs once per tenant?
I ended up creating my own distributed lock using redis, and it worked
sorry, had to create 3 posts, it was too long, so if you checked the screenshot, the types are correct. but still not working,
using System.IO; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Tapp.EntityFrameworkCore; using Tapp.Localization; using Tapp.MultiTenancy; using Tapp.Permissions; using Tapp.Web.Menus; using Microsoft.OpenApi.Models; using Volo.Abp; using Volo.Abp.Studio; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.UI; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonX; using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonX.Bundling; using Volo.Abp.LeptonX.Shared; using Volo.Abp.Autofac; using Volo.Abp.AutoMapper; using Volo.Abp.Modularity; using Volo.Abp.PermissionManagement; using Volo.Abp.PermissionManagement.Web; using Volo.Abp.UI.Navigation.Urls; using Volo.Abp.UI; using Volo.Abp.UI.Navigation; using Volo.Abp.VirtualFileSystem; using Volo.Abp.Identity.Web; using Volo.Abp.FeatureManagement; using OpenIddict.Server.AspNetCore; using OpenIddict.Validation.AspNetCore; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Commercial; using Tapp.Web.HealthChecks; using Volo.Abp.Account.Admin.Web; using Volo.Abp.Account.Public.Web; using Volo.Abp.Account.Public.Web.ExternalProviders; using Volo.Abp.Account.Pro.Public.Web.Shared; using Volo.Abp.AuditLogging.Web; using Volo.Abp.LanguageManagement; using Volo.Abp.TextTemplateManagement.Web; using Volo.Saas.Host; using Volo.Abp.Gdpr.Web; using Volo.Abp.Gdpr.Web.Extensions; using Volo.Abp.OpenIddict.Pro.Web; using System; using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Google; using Microsoft.AspNetCore.Authentication.MicrosoftAccount; using Microsoft.AspNetCore.Authentication.Twitter; using Microsoft.AspNetCore.Extensions.DependencyInjection; using Volo.Abp.Account.Web; using Volo.Abp.AspNetCore.Mvc.UI.Bundling; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Toolbars; using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.Identity; using Volo.Abp.Swashbuckle; using Volo.Abp.OpenIddict; using Volo.Abp.Security.Claims; using Volo.Abp.SettingManagement.Web; using Volo.Abp.Studio.Client.AspNetCore; using Medallion.Threading; using StackExchange.Redis; using Medallion.Threading.Redis; using Volo.Abp.DistributedLocking;
namespace Tapp.Web;
[DependsOn( typeof(AbpDistributedLockingModule), typeof(TappHttpApiModule), typeof(TappApplicationModule), typeof(TappEntityFrameworkCoreModule), typeof(AbpAutofacModule), typeof(AbpStudioClientAspNetCoreModule), typeof(AbpIdentityWebModule), typeof(AbpAspNetCoreMvcUiLeptonXThemeModule), typeof(AbpAccountPublicWebOpenIddictModule), typeof(AbpAuditLoggingWebModule), typeof(SaasHostWebModule), typeof(AbpAccountAdminWebModule), typeof(AbpOpenIddictProWebModule), typeof(LanguageManagementWebModule), typeof(TextTemplateManagementWebModule), typeof(AbpGdprWebModule), typeof(AbpFeatureManagementWebModule), typeof(AbpSwashbuckleModule), typeof(AbpAspNetCoreSerilogModule) )] public class TappWebModule : AbpModule { public override void PreConfigureServices(ServiceConfigurationContext context) { var hostingEnvironment = context.Services.GetHostingEnvironment(); var configuration = context.Services.GetConfiguration();
context.Services.PreConfigure<AbpMvcDataAnnotationsLocalizationOptions>(options =>
{
options.AddAssemblyResource(
typeof(TappResource),
typeof(TappDomainModule).Assembly,
typeof(TappDomainSharedModule).Assembly,
typeof(TappApplicationModule).Assembly,
typeof(TappApplicationContractsModule).Assembly,
typeof(TappWebModule).Assembly
);
});
PreConfigure<OpenIddictBuilder>(builder =>
{
builder.AddValidation(options =>
{
options.AddAudiences("Tapp");
options.UseLocalServer();
options.UseAspNetCore();
});
});
if (!hostingEnvironment.IsDevelopment())
{
PreConfigure<AbpOpenIddictAspNetCoreOptions>(options =>
{
options.AddDevelopmentEncryptionAndSigningCertificate = false;
});
PreConfigure<OpenIddictServerBuilder>(serverBuilder =>
{
serverBuilder.AddProductionEncryptionAndSigningCertificate("openiddict.pfx", configuration["AuthServer:CertificatePassPhrase"]!);
serverBuilder.SetIssuer(new Uri(configuration["AuthServer:Authority"]!));
});
}
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
var hostingEnvironment = context.Services.GetHostingEnvironment();
var configuration = context.Services.GetConfiguration();
if (!configuration.GetValue<bool>("App:DisablePII"))
{
Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
Microsoft.IdentityModel.Logging.IdentityModelEventSource.LogCompleteSecurityArtifact = true;
}
if (!configuration.GetValue<bool>("AuthServer:RequireHttpsMetadata"))
{
Configure<OpenIddictServerAspNetCoreOptions>(options =>
{
options.DisableTransportSecurityRequirement = true;
});
Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedProto;
});
}
context.Services.AddSingleton<IDistributedLockProvider>(sp =>
{
var connection = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]!);
return new RedisDistributedSynchronizationProvider(connection.GetDatabase());
});
ConfigureBundles();
ConfigureUrls(configuration);
ConfigurePages(configuration);
ConfigureImpersonation(context, configuration);
ConfigureHealthChecks(context);
ConfigureExternalProviders(context);
ConfigureCookieConsent(context);
ConfigureAuthentication(context);
ConfigureAutoMapper();
ConfigureVirtualFileSystem(hostingEnvironment);
ConfigureNavigationServices();
ConfigureAutoApiControllers();
ConfigureSwaggerServices(context.Services);
ConfigureTheme();
Configure<PermissionManagementOptions>(options =>
{
options.IsDynamicPermissionStoreEnabled = true;
});
}
private void ConfigureCookieConsent(ServiceConfigurationContext context)
{
context.Services.AddAbpCookieConsent(options =>
{
options.IsEnabled = true;
options.CookiePolicyUrl = "/CookiePolicy";
options.PrivacyPolicyUrl = "/PrivacyPolicy";
});
}
private void ConfigureTheme()
{
Configure<LeptonXThemeOptions>(options =>
{
options.DefaultStyle = LeptonXStyleNames.System;
});
Configure<LeptonXThemeMvcOptions>(options =>
{
options.ApplicationLayout = LeptonXMvcLayouts.SideMenu;
});
}
private void ConfigureHealthChecks(ServiceConfigurationContext context)
{
context.Services.AddTappHealthChecks();
}
private void ConfigureBundles()
{
Configure<AbpBundlingOptions>(options =>
{
options.StyleBundles.Configure(
LeptonXThemeBundles.Styles.Global,
bundle =>
{
bundle.AddFiles("/global-scripts.js");
bundle.AddFiles("/global-styles.css");
}
);
});
}
private void ConfigurePages(IConfiguration configuration)
{
Configure<RazorPagesOptions>(options =>
{
options.Conventions.AuthorizePage("/HostDashboard", TappPermissions.Dashboard.Host);
options.Conventions.AuthorizePage("/TenantDashboard", TappPermissions.Dashboard.Tenant);
});
}
private void ConfigureUrls(IConfiguration configuration)
{
Configure<AppUrlOptions>(options =>
{
options.Applications["MVC"].RootUrl = configuration["App:SelfUrl"];
});
}
private void ConfigureAuthentication(ServiceConfigurationContext context)
{
context.Services.ForwardIdentityAuthenticationForBearer(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
context.Services.Configure<AbpClaimsPrincipalFactoryOptions>(options =>
{
options.IsDynamicClaimsEnabled = true;
});
}
private void ConfigureImpersonation(ServiceConfigurationContext context, IConfiguration configuration)
{
context.Services.Configure<AbpSaasHostWebOptions>(options =>
{
options.EnableTenantImpersonation = true;
});
context.Services.Configure<AbpIdentityWebOptions>(options =>
{
options.EnableUserImpersonation = true;
});
context.Services.Configure<AbpAccountOptions>(options =>
{
options.TenantAdminUserName = "admin";
options.ImpersonationTenantPermission = SaasHostPermissions.Tenants.Impersonation;
options.ImpersonationUserPermission = IdentityPermissions.Users.Impersonation;
});
}
private void ConfigureAutoMapper()
{
Configure<AbpAutoMapperOptions>(options =>
{
options.AddMaps<TappWebModule>();
});
}
private void ConfigureVirtualFileSystem(IWebHostEnvironment hostingEnvironment)
{
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<TappWebModule>();
if (hostingEnvironment.IsDevelopment())
{
options.FileSets.ReplaceEmbeddedByPhysical<TappDomainSharedModule>(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}Tapp.Domain.Shared", Path.DirectorySeparatorChar)));
options.FileSets.ReplaceEmbeddedByPhysical<TappDomainModule>(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}Tapp.Domain", Path.DirectorySeparatorChar)));
options.FileSets.ReplaceEmbeddedByPhysical<TappApplicationContractsModule>(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}Tapp.Application.Contracts", Path.DirectorySeparatorChar)));
options.FileSets.ReplaceEmbeddedByPhysical<TappApplicationModule>(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}Tapp.Application", Path.DirectorySeparatorChar)));
options.FileSets.ReplaceEmbeddedByPhysical<TappHttpApiModule>(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}..{0}src{0}Tapp.HttpApi", Path.DirectorySeparatorChar)));
options.FileSets.ReplaceEmbeddedByPhysical<TappWebModule>(hostingEnvironment.ContentRootPath);
}
});
}
private void ConfigureNavigationServices()
{
Configure<AbpNavigationOptions>(options =>
{
options.MenuContributors.Add(new TappMenuContributor());
});
Configure<AbpToolbarOptions>(options =>
{
options.Contributors.Add(new TappToolbarContributor());
});
}
private void ConfigureAutoApiControllers()
{
Configure<AbpAspNetCoreMvcOptions>(options =>
{
options.ConventionalControllers.Create(typeof(TappApplicationModule).Assembly);
});
}
private void ConfigureSwaggerServices(IServiceCollection services)
{
services.AddAbpSwaggerGen(
options =>
{
options.SwaggerDoc("v1", new OpenApiInfo { Title = "Tapp API", Version = "v1" });
options.DocInclusionPredicate((docName, description) => true);
options.CustomSchemaIds(type => type.FullName);
}
);
}
private void ConfigureExternalProviders(ServiceConfigurationContext context)
{
context.Services.AddAuthentication()
.AddGoogle(GoogleDefaults.AuthenticationScheme, options =>
{
options.ClaimActions.MapJsonKey(AbpClaimTypes.Picture, "picture");
})
.WithDynamicOptions<GoogleOptions, GoogleHandler>(
GoogleDefaults.AuthenticationScheme,
options =>
{
options.WithProperty(x => x.ClientId);
options.WithProperty(x => x.ClientSecret, isSecret: true);
}
)
.AddMicrosoftAccount(MicrosoftAccountDefaults.AuthenticationScheme, options =>
{
//Personal Microsoft accounts as an example.
options.AuthorizationEndpoint = "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize";
options.TokenEndpoint = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
options.ClaimActions.MapCustomJson("picture", _ => "https://graph.microsoft.com/v1.0/me/photo/$value");
options.SaveTokens = true;
})
.WithDynamicOptions<MicrosoftAccountOptions, MicrosoftAccountHandler>(
MicrosoftAccountDefaults.AuthenticationScheme,
options =>
{
options.WithProperty(x => x.ClientId);
options.WithProperty(x => x.ClientSecret, isSecret: true);
}
)
.AddTwitter(TwitterDefaults.AuthenticationScheme, options =>
{
options.ClaimActions.MapJsonKey(AbpClaimTypes.Picture,"profile_image_url_https");
options.RetrieveUserDetails = true;
})
.WithDynamicOptions<TwitterOptions, TwitterHandler>(
TwitterDefaults.AuthenticationScheme,
options =>
{
options.WithProperty(x => x.ConsumerKey);
options.WithProperty(x => x.ConsumerSecret, isSecret: true);
}
);
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
var env = context.GetEnvironment();
app.UseForwardedHeaders();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAbpRequestLocalization();
if (!env.IsDevelopment())
{
app.UseErrorPage();
app.UseHsts();
}
app.UseAbpCookieConsent();
app.UseCorrelationId();
app.MapAbpStaticAssets();
app.UseAbpStudioLink();
app.UseRouting();
app.UseAbpSecurityHeaders();
app.UseAuthentication();
app.UseAbpOpenIddictValidation();
if (MultiTenancyConsts.IsEnabled)
{
app.UseMultiTenancy();
}
app.UseUnitOfWork();
app.UseDynamicClaims();
app.UseAuthorization();
app.UseSwagger();
app.UseAbpSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "Tapp API");
});
app.UseAuditing();
app.UseAbpSerilogEnrichers();
app.UseConfiguredEndpoints();
}
}