diff --git a/build/common.ps1 b/build/common.ps1 index e4ba6a8adc..06fa20b35e 100644 --- a/build/common.ps1 +++ b/build/common.ps1 @@ -28,6 +28,7 @@ $solutionPaths = ( "../samples/BookStore-Modular/modules/book-management", "../samples/BookStore-Modular/application", "../samples/DashboardDemo", + "../samples/EfCoreMigrationDemo", "../samples/MicroserviceDemo", "../samples/RabbitMqEventBus", "../abp_io/AbpIoLocalization" diff --git a/docs/en/Application-Services.md b/docs/en/Application-Services.md index 3fb52377b3..f7f7675f46 100644 --- a/docs/en/Application-Services.md +++ b/docs/en/Application-Services.md @@ -132,6 +132,8 @@ The `CreateAsync` method above manually creates a `Book` entity from given `Crea However, in many cases, it's very practical to use **auto object mapping** to set properties of an object from a similar object. ABP provides an [object to object mapping](Object-To-Object-Mapping.md) infrastructure to make this even easier. +Object to object mapping provides abstractions and it is implemented by the [AutoMapper](https://automapper.org/) library by default. + Let's create another method to get a book. First, define the method in the `IBookAppService` interface: ````csharp @@ -146,7 +148,6 @@ public interface IBookAppService : IApplicationService `BookDto` is a simple [DTO](Data-Transfer-Objects.md) class defined as below: ````csharp -[AbpAutoMapFrom(typeof(Book))] //Defines the mapping public class BookDto { public Guid Id { get; set; } @@ -159,21 +160,38 @@ public class BookDto } ```` -* `BookDto` defines `[AbpAutoMapFrom(typeof(Book))]` attribute to create the object mapping from `Book` to `BookDto`. +AutoMapper requires to create a mapping [profile class](https://docs.automapper.org/en/stable/Configuration.html#profile-instances). Example: -Then you can implement the `GetAsync` method as shown below: +````csharp +public class MyProfile : Profile +{ + public MyProfile() + { + CreateMap(); + } +} +```` + +You should then register profiles using the `AbpAutoMapperOptions`: ````csharp -public async Task GetAsync(Guid id) +[DependsOn(typeof(AbpAutoMapperModule))] +public class MyModule : AbpModule { - var book = await _bookRepository.GetAsync(id); - return book.MapTo(); + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + //Add all mappings defined in the assembly of the MyModule class + options.AddMaps(); + }); + } } ```` -`MapTo` extension method converts `Book` object to `BookDto` object by copying all properties with the same naming. +`AddMaps` registers all profile classes defined in the assembly of the given class, typically your module class. It also registers for the [attribute mapping](https://docs.automapper.org/en/stable/Attribute-mapping.html). -An alternative to the `MapTo` is using the `IObjectMapper` service: +Then you can implement the `GetAsync` method as shown below: ````csharp public async Task GetAsync(Guid id) @@ -183,8 +201,6 @@ public async Task GetAsync(Guid id) } ```` -While the second syntax is a bit harder to write, it better works if you write unit tests. - See the [object to object mapping document](Object-To-Object-Mapping.md) for more. ## Validation @@ -250,7 +266,6 @@ public interface ICrudAppService< DTO classes used in this example are `BookDto` and `CreateUpdateBookDto`: ````csharp -[AbpAutoMapFrom(typeof(Book))] public class BookDto : AuditedEntityDto { public string Name { get; set; } @@ -260,7 +275,6 @@ public class BookDto : AuditedEntityDto public float Price { get; set; } } -[AbpAutoMapTo(typeof(Book))] public class CreateUpdateBookDto { [Required] @@ -275,6 +289,19 @@ public class CreateUpdateBookDto } ```` +[Profile](https://docs.automapper.org/en/stable/Configuration.html#profile-instances) class of DTO class. + +```csharp +public class MyProfile : Profile +{ + public MyProfile() + { + CreateMap(); + CreateMap(); + } +} +``` + * `CreateUpdateBookDto` is shared by create and update operations, but you could use separated DTO classes as well. And finally, the `BookAppService` implementation is very simple: diff --git a/docs/en/Background-Jobs-Hangfire.md b/docs/en/Background-Jobs-Hangfire.md index 588fcace9e..622558ac68 100644 --- a/docs/en/Background-Jobs-Hangfire.md +++ b/docs/en/Background-Jobs-Hangfire.md @@ -39,5 +39,46 @@ public class YourModule : AbpModule ```` ## Configuration +You can install any storage for Hangfire. The most common one is SQL Server (see the [Hangfire.SqlServer](https://www.nuget.org/packages/Hangfire.SqlServer) NuGet package). -TODO... \ No newline at end of file +After you have installed these NuGet packages, you need to configure your project to use Hangfire. + +1.First, we change the `Module` class (example: `HttpApiHostModule`) to add Hangfire configuration of the storage and connection string in the `ConfigureServices` method: + + +````csharp + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + var hostingEnvironment = context.Services.GetHostingEnvironment(); + + //... other configarations + + ConfigureHangfire(context, configuration); + } + + private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration) + { + context.Services.AddHangfire(config => + { + config.UseSqlServerStorage(configuration.GetConnectionString("Default")); + }); + } +```` + +2. We need to add `UseHangfireServer` call in the `OnApplicationInitialization` method in `Module` class + +If you want to use hangfire's dashboard, you can add it, too: by `UseHangfireDashboard` + +````csharp + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + var app = context.GetApplicationBuilder(); + + // ... others + + app.UseHangfireServer(); + app.UseHangfireDashboard(); + + } +```` diff --git a/docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md b/docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md new file mode 100644 index 0000000000..1d650f46b1 --- /dev/null +++ b/docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md @@ -0,0 +1,178 @@ +# How to Use the Azure Active Directory Authentication for MVC / Razor Page Applications + +## Introduction + +This post demonstrates how to integrate AzureAD to an ABP application that enables users to sign in using OAuth 2.0 with credentials from Azure Active Directory. + +Adding Azure Active Directory is pretty straightforward in ABP framework. Couple of configurations needs to be done correctly. + +There will be two samples of connections for better coverage; + +- **AddAzureAD** (Microsoft.AspNetCore.Authentication.AzureAD.UI package) +- **AddOpenIdConnect** (Default Microsoft.AspNetCore.Authentication.OpenIdConnect package) + + + +## Sample Code + +https://github.com/abpframework/abp-samples/tree/master/aspnet-core/BookStore-AzureAD + + + +## Setup + +Update your `appsettings.json` in your **.Web** application and add the following section filled with your AzureAD application settings. + +````xml + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "", + "Domain": "domain.onmicrosoft.com", + "CallbackPath": "/signin-azuread-oidc" + } +```` + + + +## AddAzureAD + +#### **Update your `appsettings.json`** + +Install `Microsoft.AspNetCore.Authentication.AzureAD.UI` package to your **.Web** application. + +In your **.Web** application, add the following section filled with your AzureAD application settings. Modify `ConfigureAuthentication` method of your **BookStoreWebModule** with the following: + +````xml +private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) + { + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); + context.Services.AddAuthentication() + .AddIdentityServerAuthentication(options => + { + options.Authority = configuration["AuthServer:Authority"]; + options.RequireHttpsMetadata = false; + options.ApiName = "Acme.BookStore"; + }) + .AddAzureAD(options => configuration.Bind("AzureAd", options)); + + context.Services.Configure(AzureADDefaults.OpenIdScheme, options => + { + options.Authority = options.Authority + "/v2.0/"; + options.ClientId = configuration["AzureAd:ClientId"]; + options.CallbackPath = configuration["AzureAd:CallbackPath"]; + options.ResponseType = OpenIdConnectResponseType.IdToken; + options.RequireHttpsMetadata = false; + + options.TokenValidationParameters.ValidateIssuer = false; + options.GetClaimsFromUserInfoEndpoint = true; + options.SaveTokens = true; + options.SignInScheme = IdentityConstants.ExternalScheme; + + options.Scope.Add("email"); + }); + } +```` + + + +## AddOpenIdConnect + +Modify `ConfigureAuthentication` method of your **BookStoreWebModule** with the following: + +````xml +private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) + { + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); + + context.Services.AddAuthentication() + .AddIdentityServerAuthentication(options => + { + options.Authority = configuration["AuthServer:Authority"]; + options.RequireHttpsMetadata = false; + options.ApiName = "BookStore"; + }) + .AddOpenIdConnect("AzureOpenId", "AzureAD", options => + { + options.Authority = "https://login.microsoftonline.com/" + configuration["AzureAd:TenantId"]; + options.ClientId = configuration["AzureAd:ClientId"]; + options.ResponseType = OpenIdConnectResponseType.CodeIdToken; + options.CallbackPath = configuration["AzureAd:CallbackPath"]; + options.RequireHttpsMetadata = false; + options.SaveTokens = true; + options.GetClaimsFromUserInfoEndpoint = true; + }); + } +```` + + + +# FAQ + +* Help! `GetExternalLoginInfoAsync` returns `null`! + + * There can be 2 reasons for this; + + 1. You are trying to authenticate against wrong scheme. Check if you set **SignInScheme** to `IdentityConstants.ExternalScheme`: + + ````xml + options.SignInScheme = IdentityConstants.ExternalScheme; + ```` + + 2. Your `ClaimTypes.NameIdentifier` is `null`. Check if you added claim mapping: + + ````xml + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); + ```` + + +* Help! I keep getting ***AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application*** error! + + * If you set your **CallbackPath** in appsettings as: + + ````xml + "AzureAd": { + ... + "CallbackPath": "/signin-azuread-oidc" + } + ```` + + your **Redirect URI** of your application in azure portal must be with domain like `https://localhost:44320/signin-azuread-oidc`, not only `/signin-azuread-oidc`. + +* Help! I am getting ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** error! + + + * This occurs when you use Azure Authority **v2.0 endpoint** without requesting `email` scope. [Abp checks unique email to create user](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). Simply add + + ````xml + options.Scope.Add("email"); + ```` + + to your openid configuration. + +* How can I **debug/watch** which claims I get before they get mapped? + + * You can add a simple event under openid configuration to debug before mapping like: + + ````xml + options.Events.OnTokenValidated = (async context => + { + var claimsFromOidcProvider = context.Principal.Claims.ToList(); + await Task.CompletedTask; + }); + ```` + +* I want to debug further, how can I implement my custom **SignInManager**? + + * You can check [Customizing SignInManager in Abp Framework](link here) post. + +* I want to add extra properties to user while they are being created. How can I do that? + + * You can check [Customizing Login Page in Abp Framework]() post. + +* Why can't I see **External Register Page** after I sign in from external provider for the first time? + + * ABP framework automatically registers your user with supported email claim from your external authentication provider. You can change this behavior by [Customizing Login Page in Abp Framework](will be link here). diff --git a/docs/en/How-To/Customize-Login-Page-MVC.md b/docs/en/How-To/Customize-Login-Page-MVC.md new file mode 100644 index 0000000000..49b4b4c39e --- /dev/null +++ b/docs/en/How-To/Customize-Login-Page-MVC.md @@ -0,0 +1,113 @@ +# How to Customize the Login Page for MVC / Razor Page Applications + +When you create a new application using the [application startup template](../Startup-Templates/Application.md), source code of the login page will not be inside your solution, so you can not directly change it. The login page comes from the [Account Module](../Modules/Account) that is used a [NuGet package](https://www.nuget.org/packages/Volo.Abp.Account.Web) reference. + +This document explains how to customize the login page for your own application. + +## Create a Login PageModel + +Create a new class inheriting from the [LoginModel](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs) of the Account module. + +````csharp +public class CustomLoginModel : LoginModel +{ + public CustomLoginModel( + Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemeProvider, + Microsoft.Extensions.Options.IOptions accountOptions) + : base(schemeProvider, accountOptions) + { + } +} +```` + +> Naming convention is important here. If your class name doesn't end with `LoginModel`, you need to manually replace the `LoginModel` using the [dependency injection](../Dependency-Injection.md) system. + +Then you can override any method you need and add new methods and properties needed by the UI. + +## Overriding the Login Page UI + +Create folder named **Account** under **Pages** directory and create a **Login.cshtml** under this folder. It will automatically override the `Login.cshtml` file defined in the Account Module thanks to the [Virtual File System](../Virtual-File-System.md). + +A good way to customize a page is to copy its source code. [Click here](https://github.com/abpframework/abp/blob/dev/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml) for the source code of the login page. At the time this document has been written, the source code was like below: + +````xml +@page +@using Volo.Abp.Account.Settings +@using Volo.Abp.Settings +@model Acme.BookStore.Web.Pages.Account.CustomLoginModel +@inherits Volo.Abp.Account.Web.Pages.Account.AccountPage +@inject Volo.Abp.Settings.ISettingProvider SettingProvider +@if (Model.EnableLocalLogin) +{ +
+
+

@L["Login"]

+ @if (await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled)) + { + + @L["AreYouANewUser"] + @L["Register"] + + } +
+ + +
+ + + +
+
+ + + +
+
+ +
+ @L["Login"] +
+
+ + +
+} + +@if (Model.VisibleExternalProviders.Any()) +{ +
+

@L["UseAnotherServiceToLogIn"]

+
+ + + @foreach (var provider in Model.VisibleExternalProviders) + { + + } +
+
+} + +@if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any()) +{ +
+ @L["InvalidLoginRequest"] + @L["ThereAreNoLoginSchemesConfiguredForThisClient"] +
+} +```` + +Just changed the `@model` to `Acme.BookStore.Web.Pages.Account.CustomLoginModel` to use the customized `PageModel` class. You can change it however your application needs. + +## The Source Code + +You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization). + +## See Also + +* [ASP.NET Core (MVC / Razor Pages) User Interface Customization Guide](../UI/AspNetCore/Customization-User-Interface). \ No newline at end of file diff --git a/docs/en/How-To/Customize-SignIn-Manager.md b/docs/en/How-To/Customize-SignIn-Manager.md new file mode 100644 index 0000000000..8c9733a6af --- /dev/null +++ b/docs/en/How-To/Customize-SignIn-Manager.md @@ -0,0 +1,84 @@ +# Customize the SignInManager + +## Introduction + +ABP Framework uses Microsoft Identity underneath hence supports customization as much as Microsoft Identity does. + +## Sample Code + +https://github.com/abpframework/abp-samples/blob/master/aspnet-core/BookStore-AzureAD/src/Acme.BookStore.Web/CustomSignInManager.cs + +## Creating CustomSignInManager + +To create your own custom SignIn Manager, you need to inherit `SignInManager`. + +````xml +public class CustomSignInManager : SignInManager +{ + public CustomSigninManager( + UserManager userManager, + IHttpContextAccessor contextAccessor, + IUserClaimsPrincipalFactory claimsFactory, + IOptions optionsAccessor, + ILogger> logger, + IAuthenticationSchemeProvider schemes, + IUserConfirmation confirmation) + : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) + { + } +} +```` + + + +## Overriding Methods + +Afterwards you can override a method like `GetExternalLoginInfoAsync`: + +````xml +public override async Task GetExternalLoginInfoAsync(string expectedXsrf = null) +{ + var auth = await Context.AuthenticateAsync(IdentityConstants.ExternalScheme); + var items = auth?.Properties?.Items; + if (auth?.Principal == null || items == null || !items.ContainsKey("LoginProvider")) + { + return null; + } + + if (expectedXsrf != null) + { + if (!items.ContainsKey("XsrfId")) + { + return null; + } + var userId = items[XsrfKey] as string; + if (userId != expectedXsrf) + { + return null; + } + var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier); + var provider = items[LoginProviderKey] as string; + if (providerKey == null || provider == null) + { + return null; + } + + var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName ?? provider; + return new ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName) + { + AuthenticationTokens = auth.Properties.GetTokens() + }; +} +```` + + + +## Registering to DI + +You need to register your Custom SignIn Manager to DI to activate it. Inside the `.Web` project, locate the `ApplicationNameWebModule` and add the following under `ConfigureServices` method: + +````xml +context.Services +.GetObject() + .AddSignInManager(); +```` \ No newline at end of file diff --git a/docs/en/How-To/Index.md b/docs/en/How-To/Index.md new file mode 100644 index 0000000000..d6b31fa7ed --- /dev/null +++ b/docs/en/How-To/Index.md @@ -0,0 +1,9 @@ +# "How To" Guides + +This section contains "how to" guides for some specific questions frequently asked. While some of them are common development tasks and not directly related to the ABP Framework, we think it is useful to have some concrete examples those directly work with your ABP based applications. + +## Authentication + +* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md) +* [How to Use the Azure Active Directory Authentication for MVC / Razor Page Applications](Azure-Active-Directory-Authentication-MVC.md) +* [Customize the SignInManager](Customize-SignIn-Manager.md) (as an example of customization) \ No newline at end of file diff --git a/docs/en/UI/Angular/Content-Security-Strategy.md b/docs/en/UI/Angular/Content-Security-Strategy.md new file mode 100644 index 0000000000..21d794a1af --- /dev/null +++ b/docs/en/UI/Angular/Content-Security-Strategy.md @@ -0,0 +1,56 @@ +# ContentSecurityStrategy + +`ContentSecurityStrategy` is an abstract class exposed by @abp/ng.core package. It helps you mark inline scripts or styles as safe in terms of [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy). + + + + +## API + + +### constructor(public nonce?: string) + +- `nonce` enables whitelisting inline script or styles in order to avoid using `unsafe-inline` in [script-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#Unsafe_inline_script) and [style-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src#Unsafe_inline_styles) directives. + + +### applyCSP(element: HTMLScriptElement | HTMLStyleElement): void + +This method maps the aforementioned properties to the given `element`. + + + + +## LooseContentSecurityPolicy + +`LooseContentSecurityPolicy` is a class that extends `ContentSecurityStrategy`. It requires `nonce` and marks given `