diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 37f6b402b0..87653e30e0 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -586,6 +586,10 @@ } ] }, + { + "text": "Application URLs", + "path": "framework/infrastructure/app-urls.md" + }, { "text": "Background Jobs", "items": [ diff --git a/docs/en/framework/architecture/multi-tenancy/index.md b/docs/en/framework/architecture/multi-tenancy/index.md index 11ac3f7af1..23c8316ee2 100644 --- a/docs/en/framework/architecture/multi-tenancy/index.md +++ b/docs/en/framework/architecture/multi-tenancy/index.md @@ -357,6 +357,8 @@ context.Services ``` +The configuration above resolves the current tenant from the incoming request (inbound). To make the **outbound** URLs your application generates — such as the password reset link inside an Account email — point to the tenant's subdomain as well, configure `AppUrlOptions`. See [Application URLs](../../infrastructure/app-urls.md#multi-tenant-aware-urls). + ##### Custom Tenant Resolvers You can add implement your custom tenant resolver and configure the `AbpTenantResolveOptions` in your module's `ConfigureServices` method as like below: diff --git a/docs/en/framework/infrastructure/app-urls.md b/docs/en/framework/infrastructure/app-urls.md new file mode 100644 index 0000000000..cc7d634800 --- /dev/null +++ b/docs/en/framework/infrastructure/app-urls.md @@ -0,0 +1,164 @@ +```json +//[doc-seo] +{ + "Description": "Configure cross-application URLs in ABP with AppUrlOptions and IAppUrlProvider, including multi-tenant subdomain templates and redirect URL validation." +} +``` + +# Application URLs + +ABP provides the `AppUrlOptions` options class and the `IAppUrlProvider` service to centrally configure and resolve URLs that point to **other applications** in your solution (for example, an MVC/Razor Pages UI, an Auth Server, an HTTP API host, etc.). They are typically used when code in one application needs to build a link that targets another — like the Account module putting a **password reset link** into an email. + +* Defines `AppUrlOptions` to register the **root URL** and named relative URLs of each application. +* Provides `IAppUrlProvider` to **resolve** those URLs at runtime, with optional **tenant-aware** placeholder substitution. +* Supports **subdomain-style templates** (e.g. `https://{0}.example.com`) that produce per-tenant URLs without extra code. +* Maintains a `RedirectAllowedUrls` list used by `IAppUrlProvider.IsRedirectAllowedUrlAsync` to validate redirect targets. + +> `AppUrlOptions` is defined in the `Volo.Abp.UI.Navigation` package, which comes pre-installed with the [application startup template](../../solution-templates/layered-web-application). + +## Configuring Application URLs + +`AppUrlOptions` exposes a dictionary of **applications**, each with a `RootUrl` and a set of named `Urls`. + +**Example: Set the root URL and a named URL for the MVC application** + +```csharp +Configure(options => +{ + options.Applications["MVC"].RootUrl = "https://my-app.com"; + options.Applications["MVC"].Urls["MyPage"] = "my-page"; +}); +``` + +* `"MVC"` is the **application key**. Some modules (such as Account) register their URLs under a known key — `"MVC"` is the default for the **server-side UI**. You can use any key you want for your own applications. +* `RootUrl` is the **base URL** of that application. +* `Urls[urlName]` is a **relative path** appended to `RootUrl`. The final URL is built as `RootUrl.EnsureEndsWith('/') + Urls[urlName]`, so the relative path should **not** start with a `/`. When `RootUrl` is `null`, the value of `Urls[urlName]` is returned as-is. + +The Account module, for example, **pre-registers** its URLs in its application module: + +**Example: How the Account module registers the password reset URL** + +```csharp +Configure(options => +{ + options.Applications["MVC"].Urls[AccountUrlNames.PasswordReset] = "Account/ResetPassword"; +}); +``` + +> So configuring `Applications["MVC"].RootUrl` in your own module is usually enough to make password reset and similar Account email links point to the right host. + +### Defaults in the application startup template + +The ABP **application startup template** wires `Applications["MVC"].RootUrl` to the `App:SelfUrl` setting and seeds `RedirectAllowedUrls` from `App:RedirectAllowedUrls`: + +```csharp +Configure(options => +{ + options.Applications["MVC"].RootUrl = configuration["App:SelfUrl"]; + options.RedirectAllowedUrls.AddRange( + configuration["App:RedirectAllowedUrls"]?.Split(',') ?? Array.Empty()); +}); +``` + +> This is why Account email links point to your **host URL** out of the box: they reuse `App:SelfUrl`. If that default isn't what you want — for example, in a subdomain-based **multi-tenant** setup — override `Applications["MVC"].RootUrl` with the template you need (see [Multi-Tenant Aware URLs](#multi-tenant-aware-urls)). + +## Using `IAppUrlProvider` + +[Inject](../fundamentals/dependency-injection.md) the `IAppUrlProvider` service into any class that needs to build a cross-application URL. + +**Example: Resolve a root URL and a named URL of the MVC application** + +```csharp +public class MyNotificationSender : ITransientDependency +{ + private readonly IAppUrlProvider _appUrlProvider; + + public MyNotificationSender(IAppUrlProvider appUrlProvider) + { + _appUrlProvider = appUrlProvider; + } + + public async Task SendAsync() + { + var rootUrl = await _appUrlProvider.GetUrlAsync("MVC"); + var pageUrl = await _appUrlProvider.GetUrlAsync("MVC", "MyPage"); + } +} +``` + +* `GetUrlAsync(appName)` returns the configured `RootUrl` for the given application. +* `GetUrlAsync(appName, urlName)` returns the **combined URL** described above. +* `GetUrlAsync(...)` throws an `AbpException` when the resolved URL is `null` or empty (e.g. both `RootUrl` and `Urls[urlName]` are unset). Use `GetUrlOrNullAsync(...)` if you'd rather get `null` and decide what to do yourself. +* `NormalizeUrlAsync(url)` applies tenant placeholder substitution to a URL string that you already have. Useful when the URL doesn't come from `AppUrlOptions`. + +## Multi-Tenant Aware URLs + +If your solution uses **subdomain-based** multi-tenancy (see the [Domain/Subdomain Tenant Resolver](../architecture/multi-tenancy/index.md#domainsubdomain-tenant-resolver)), you'll usually want the **outbound URLs** you generate (email links, redirects) to also be tenant-aware — otherwise the link in a password reset email won't point to the tenant's subdomain. + +`AppUrlOptions` supports the following **placeholders** in any URL value. They are substituted by `IAppUrlProvider` based on the **current tenant**: + +| Placeholder | Replaced with | +| --- | --- | +| `{0}` | Current tenant **name** | +| `{%{{{ {{tenantName}} }}}%}` | Current tenant **name** | +| `{%{{{ {{tenantId}} }}}%}` | Current tenant **id** | + +The `{0}` placeholder uses the **same convention** as `AddDomainTenantResolver("{0}.example.com")`, so a typical subdomain-tenant setup looks like this: + +**Example: Tenant-aware Account email links via a subdomain template** + +```csharp +Configure(options => +{ + options.AddDomainTenantResolver("{0}.example.com"); +}); + +Configure(options => +{ + options.Applications["MVC"].RootUrl = "https://{0}.example.com"; +}); +``` + +With this configuration, password reset emails sent to a tenant whose name is `acme` will contain a link starting with `https://acme.example.com/`, matching the tenant's subdomain. + +### Host (no tenant) Fallback + +When there is **no current tenant** (host-side request), the placeholder **and the dot following it** are removed together: + +| Template | Tenant `acme` | Host (no tenant) | +| --- | --- | --- | +| `https://{0}.example.com` | `https://acme.example.com` | `https://example.com` | +| `https://{%{{{ {{tenantId}} }}}%}.example.com` | `https://3a21....example.com` | `https://example.com` | + +A single subdomain-style template like the ones above therefore works for **both** tenant and host scenarios without extra configuration. + +> If your subdomain is based on the tenant **id** rather than the name, use `https://{%{{{ {{tenantId}} }}}%}.example.com`. The resolver's `{0}` placeholder accepts both name and id when finding a tenant, but `AppUrlOptions` substitutes `{0}` with the tenant **name**; if those two don't match, switch to the explicit `{%{{{ {{tenantId}} }}}%}` form on the `AppUrlOptions` side. + +## Redirect Allowed URLs + +`AppUrlOptions.RedirectAllowedUrls` is a list of URL entries used by `IAppUrlProvider.IsRedirectAllowedUrlAsync(url)` to decide whether a redirect target is allowed. A URL is allowed when it satisfies **either** of: + +* **Prefix match**: the URL string **starts with** a configured entry (case-insensitive). +* **Subdomain match**: the URL and the entry have the **same scheme** and **port**, and the URL's host **ends with** `.{entry-host}`. + +**Example: Register allowed redirect URLs (including a wildcard)** + +```csharp +Configure(options => +{ + options.RedirectAllowedUrls.Add("https://my-app.com"); + options.RedirectAllowedUrls.Add("https://admin.my-app.com"); + + options.RedirectAllowedUrls.Add("https://*.my-app.com"); +}); +``` + +* A **plain entry** like `https://my-app.com` allows any URL that starts with that prefix, plus any subdomain of `my-app.com`. +* A **wildcard entry** like `https://*.my-app.com` allows any subdomain of `my-app.com`; the `*.` is stripped before the subdomain check. +* Entries also go through **tenant placeholder substitution**, so `https://{0}.my-app.com` is resolved to the current tenant's URL first (e.g. `https://acme.my-app.com`) and then compared. Use the wildcard form when you need to allow *any* tenant subdomain regardless of the current tenant. + +## See Also + +* [Multi-Tenancy](../architecture/multi-tenancy/index.md) +* [Account Module](../../modules/account.md) +* [Emailing](emailing.md) diff --git a/docs/en/framework/infrastructure/emailing.md b/docs/en/framework/infrastructure/emailing.md index a9304b2e57..ff7ab152df 100644 --- a/docs/en/framework/infrastructure/emailing.md +++ b/docs/en/framework/infrastructure/emailing.md @@ -265,3 +265,4 @@ So, don't confuse if you don't receive emails on DEBUG mode. Emails will be sent ## See Also * [MailKit integration for sending emails](./mail-kit.md) +* [Application URLs](./app-urls.md) — for building cross-application links inside email content (e.g. password reset links). diff --git a/docs/en/guides/ms-multi-tenant-domain-resolving.md b/docs/en/guides/ms-multi-tenant-domain-resolving.md index 7603491f56..c8fdaae84c 100644 --- a/docs/en/guides/ms-multi-tenant-domain-resolving.md +++ b/docs/en/guides/ms-multi-tenant-domain-resolving.md @@ -68,6 +68,8 @@ This configuration will allow subdomain tenant resolving. Ex, if you have a tena **For angular application**, you don't need to add anything since we will be overriding the angular environment via kubernetes values file. +> The configuration above resolves the current tenant from the incoming request. To make outbound URLs generated through `IAppUrlProvider` (Account email links, redirects, etc.) also tenant-aware, configure `AppUrlOptions` with the same `{0}` template. See [Application URLs](../framework/infrastructure/app-urls.md#multi-tenant-aware-urls). + ## Configuring AuthServer When the tenant try to login from an application (Ex `https://volosoft.angular.mystore.dev`) it will be redirected to AuthServer (`https://volosoft.authserver.mystore.dev`) and you will be seeing a **HTTP 400 error** related to `invalid redirect_uri`. If you check the authserver application logs (under Logs/logs.txt file or console logs), you will notice that the `https://volosoft.angular.mystore.dev` is not a valid redirect_uri since it is not been seeded by the OpenIddictDataSeeder. Only the **host** applications, gateways and microservice URLs are seeded (like `https://angular.mystore.dev`). diff --git a/docs/en/modules/account.md b/docs/en/modules/account.md index 3fc2c8e3aa..844e00e06f 100644 --- a/docs/en/modules/account.md +++ b/docs/en/modules/account.md @@ -43,6 +43,8 @@ Social/external login buttons becomes visible if you setup it. See the *Social/E ![account-module-forgot-password](../images/account-module-forgot-password.png) +> The host part of the password reset link is built from `AppUrlOptions.Applications["MVC"].RootUrl`. Configure it if the default `App:SelfUrl` isn't what you want users to see in emails — for example, when you use subdomain-based multi-tenancy and want the link to point to the tenant's subdomain. See [Application URLs](../framework/infrastructure/app-urls.md). + ### Account Management `/Account/Manage` page is used to change password and personal information of the user.