Merge branch 'dev' into liangshiwei/identity

pull/15956/head
liangshiwei 3 years ago
commit e911d9fddc

@ -208,7 +208,7 @@
"WhenShouldIRenewMyLicense": "When should I renew my license?",
"WhenShouldIRenewMyLicenseExplanation": "If you renew your license within <strong>1 month</strong> after your license expires, the following discounts will be applied: Team License {0}; Business License {1}; Enterprise License {2}. However, if you renew your license after <strong>1 month</strong> since the expiry date of your license, the renewal price will be the same as the license purchase price and there will be no discount on your renewal.",
"TrialPlan": "Do you have a trial plan?",
"TrialPlanExplanation": "It has a 14 days trial period for the ABP Commercial team license. For more information visit <a href={0} target='_blank'>here</a>. Furthermore, for the Team licenses we provide a 30 days money-back guarantee. You can just request a refund in the first 30 days. For the Business and Enterprise licenses, we provide 60% refund in 30 days. This is because Business and Enterprise licenses include the full source code of all the modules and the themes.",
"TrialPlanExplanation": "No, there is no trial version for ABP Commercial. You can check the community edition to understand the code quality and approaches. We also offer a 30-day money-back guarantee for the Team license, no questions asked! You can request a refund within the first 30 days. We provide a 60% refund within 30 days for Business and Enterprise licenses. This is because the Business and Enterprise licenses contain the full source-code of all the modules and themes.",
"DoYouAcceptBankWireTransfer": "Do you accept bank wire transfers?",
"DoYouAcceptBankWireTransferExplanation": "Yes, we accept bank wire transfers.<br/>After sending the license fee via bank transfer, send your receipt and requested license type to accounting@volosoft.com.<br/>Our international bank account information:",
"HowToUpgrade": "How to upgrade existing applications when a new version is available?",

@ -184,6 +184,8 @@
"Layout_MetaDescription": "ABP Community is an environment where people can share posts about ABP framework and follows the projects.",
"Index_Page_CommunityIntroduction": "This is a hub for ABP Framework, .NET and software development. You can read the articles, watch the video tutorials, get informed about ABPs development progress and ABP-related events, help other developers and share your expertise with the ABP community.",
"TagsInArticle": "Tags in article",
"IConsentToMedium": "I consent to the publication of this post at https://medium.com/volosoft."
"IConsentToMedium": "I consent to the publication of this post at https://medium.com/volosoft.",
"SearchResultsFor": "Search results for <span class=\"fw-bold\">\"{0}\"</span>",
"SeeMoreVideos": "See more videos"
}
}

@ -361,7 +361,7 @@ public class BookAppService :
GetListPolicyName = BookStorePermissions.Books.Default;
CreatePolicyName = BookStorePermissions.Books.Create;
UpdatePolicyName = BookStorePermissions.Books.Edit;
DeletePolicyName = BookStorePermissions.Books.Create;
DeletePolicyName = BookStorePermissions.Books.Delete;
}
public override async Task<BookDto> GetAsync(Guid id)

@ -201,4 +201,4 @@ You may use observables in combination with [AsyncPipe](https://angular.io/guide
<input type="text" name="search" [(ngModel)]="list.filter">
```
ABP doesn't have a built-in filtering mechanism. You need to implement yourself and handle `filter` property in backend.
ABP doesn't have a built-in filtering mechanism. You need to implement it yourself and handle the `filter` property in the backend.

@ -104,6 +104,7 @@ There are more options of a menu item (the constructor of the `ApplicationMenuIt
* `target` (`string`): Target of the menu item. Can be `null` (default), "\_*blank*", "\_*self*", "\_*parent*", "\_*top*" or a frame name for web applications.
* `elementId` (`string`): Can be used to render the element with a specific HTML `id` attribute.
* `cssClass` (`string`): Additional string classes for the menu item.
* `groupName` (`string`): Can be used to group menu items.
### Authorization
@ -179,6 +180,58 @@ userMenu.Icon = "fa fa-users";
> `context.Menu` gives you ability to access to all the menu items those have been added by the previous menu contributors.
### Menu Groups
You can define groups and associate menu items with a group.
Example:
```csharp
using System.Threading.Tasks;
using MyProject.Localization;
using Volo.Abp.UI.Navigation;
namespace MyProject.Web.Menus
{
public class MyProjectMenuContributor : IMenuContributor
{
public async Task ConfigureMenuAsync(MenuConfigurationContext context)
{
if (context.Menu.Name == StandardMenus.Main)
{
await ConfigureMainMenuAsync(context);
}
}
private async Task ConfigureMainMenuAsync(MenuConfigurationContext context)
{
var l = context.GetLocalizer<MyProjectResource>();
context.Menu.AddGroup(
new ApplicationMenuGroup(
name: "Main",
displayName: l["Main"]
)
)
context.Menu.AddItem(
new ApplicationMenuItem("MyProject.Crm", l["Menu:CRM"], groupName: "Main")
.AddItem(new ApplicationMenuItem(
name: "MyProject.Crm.Customers",
displayName: l["Menu:Customers"],
url: "/crm/customers")
).AddItem(new ApplicationMenuItem(
name: "MyProject.Crm.Orders",
displayName: l["Menu:Orders"],
url: "/crm/orders")
)
);
}
}
}
```
> The UI theme will decide whether to render the groups or not, and if it decides to render, the way it's rendered is up to the theme. Only the LeptonX theme implements the menu group.
## Standard Menus
A menu is a **named** component. An application may contain more than one menus with different, unique names. There are two pre-defined standard menus:
@ -233,4 +286,3 @@ namespace MyProject.Web.Pages
}
}
```

@ -104,6 +104,7 @@ There are more options of a menu item (the constructor of the `ApplicationMenuIt
* `target` (`string`): Target of the menu item. Can be `null` (default), "\_*blank*", "\_*self*", "\_*parent*", "\_*top*" or a frame name for web applications.
* `elementId` (`string`): Can be used to render the element with a specific HTML `id` attribute.
* `cssClass` (`string`): Additional string classes for the menu item.
* `groupName` (`string`): Can be used to group menu items.
### Authorization
@ -160,6 +161,58 @@ userMenu.Icon = "fa fa-users";
> `context.Menu` gives you ability to access to all the menu items those have been added by the previous menu contributors.
### Menu Groups
You can define groups and associate menu items with a group.
Example:
```csharp
using System.Threading.Tasks;
using MyProject.Localization;
using Volo.Abp.UI.Navigation;
namespace MyProject.Web.Menus
{
public class MyProjectMenuContributor : IMenuContributor
{
public async Task ConfigureMenuAsync(MenuConfigurationContext context)
{
if (context.Menu.Name == StandardMenus.Main)
{
await ConfigureMainMenuAsync(context);
}
}
private async Task ConfigureMainMenuAsync(MenuConfigurationContext context)
{
var l = context.GetLocalizer<MyProjectResource>();
context.Menu.AddGroup(
new ApplicationMenuGroup(
name: "Main",
displayName: l["Main"]
)
)
context.Menu.AddItem(
new ApplicationMenuItem("MyProject.Crm", l["Menu:CRM"], groupName: "Main")
.AddItem(new ApplicationMenuItem(
name: "MyProject.Crm.Customers",
displayName: l["Menu:Customers"],
url: "/crm/customers")
).AddItem(new ApplicationMenuItem(
name: "MyProject.Crm.Orders",
displayName: l["Menu:Orders"],
url: "/crm/orders")
)
);
}
}
}
```
> The UI theme will decide whether to render the groups or not, and if it decides to render, the way it's rendered is up to the theme. Only the LeptonX theme implements the menu group.
## Standard Menus
A menu is a **named** component. An application may contain more than one menus with different, unique names. There are two pre-defined standard menus:

@ -367,7 +367,7 @@ namespace Acme.BookStore.Books
GetListPolicyName = BookStorePermissions.Books.Default;
CreatePolicyName = BookStorePermissions.Books.Create;
UpdatePolicyName = BookStorePermissions.Books.Edit;
DeletePolicyName = BookStorePermissions.Books.Create;
DeletePolicyName = BookStorePermissions.Books.Delete;
}
public override async Task<BookDto> GetAsync(Guid id)

@ -0,0 +1,56 @@
using System;
using IdentityModel.Client;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Authentication.Cookies;
public static class CookieAuthenticationOptionsExtensions
{
/// <summary>
/// Introspect access token on validating the principal.
/// </summary>
/// <param name="options"></param>
/// <param name="oidcAuthenticationScheme"></param>
/// <returns></returns>
public static CookieAuthenticationOptions IntrospectAccessToken(this CookieAuthenticationOptions options, string oidcAuthenticationScheme = "oidc")
{
var originalHandler = options.Events.OnValidatePrincipal;
options.Events.OnValidatePrincipal = async principalContext =>
{
originalHandler?.Invoke(principalContext);
if (principalContext.Principal != null && principalContext.Principal.Identity != null && principalContext.Principal.Identity.IsAuthenticated)
{
var accessToken = principalContext.Properties.GetTokenValue("access_token");
if (!accessToken.IsNullOrWhiteSpace())
{
var openIdConnectOptions = principalContext.HttpContext.RequestServices.GetRequiredService<IOptionsMonitor<OpenIdConnectOptions>>().Get(oidcAuthenticationScheme);
if (openIdConnectOptions.Configuration == null && openIdConnectOptions.ConfigurationManager != null)
{
openIdConnectOptions.Configuration = await openIdConnectOptions.ConfigurationManager.GetConfigurationAsync(principalContext.HttpContext.RequestAborted);
}
var response = await openIdConnectOptions.Backchannel.IntrospectTokenAsync(new TokenIntrospectionRequest
{
Address = openIdConnectOptions.Configuration?.IntrospectionEndpoint ?? openIdConnectOptions.Authority.EnsureEndsWith('/') + "connect/introspect",
ClientId = openIdConnectOptions.ClientId,
ClientSecret = openIdConnectOptions.ClientSecret,
Token = accessToken
});
if (response.IsActive)
{
return;
}
}
principalContext.RejectPrincipal();
await principalContext.HttpContext.SignOutAsync(principalContext.Scheme.Name);
}
};
return options;
}
}

@ -12,11 +12,13 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.AspNetCore.Components.Web\Volo.Abp.AspNetCore.Components.Web.csproj" />
<ProjectReference Include="..\Volo.Abp.AspNetCore.Mvc.Contracts\Volo.Abp.AspNetCore.Mvc.Contracts.csproj" />
<ProjectReference Include="..\Volo.Abp.EventBus\Volo.Abp.EventBus.csproj" />
<ProjectReference Include="..\Volo.Abp.Http.Client\Volo.Abp.Http.Client.csproj" />
<ProjectReference Include="..\Volo.Abp.AspNetCore.SignalR\Volo.Abp.AspNetCore.SignalR.csproj" />
<ProjectReference Include="..\Volo.Abp.AspNetCore.Components.Web\Volo.Abp.AspNetCore.Components.Web.csproj" />
<ProjectReference Include="..\Volo.Abp.AspNetCore.Mvc.Contracts\Volo.Abp.AspNetCore.Mvc.Contracts.csproj" />
<ProjectReference Include="..\Volo.Abp.EventBus\Volo.Abp.EventBus.csproj" />
<ProjectReference Include="..\Volo.Abp.Http.Client\Volo.Abp.Http.Client.csproj" />
<ProjectReference Include="..\Volo.Abp.AspNetCore.SignalR\Volo.Abp.AspNetCore.SignalR.csproj" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="$(MicrosoftAspNetCorePackageVersion)" />
<PackageReference Include="IdentityModel" Version="6.0.0" />
</ItemGroup>
</Project>

@ -0,0 +1,23 @@
using System.Threading.Tasks;
using Volo.Abp.AspNetCore.Components.Web.Configuration;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.Components.WebAssembly.Configuration;
[Dependency(ReplaceServices = true)]
public class BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService :
ICurrentApplicationConfigurationCacheResetService,
ITransientDependency
{
private readonly WebAssemblyCachedApplicationConfigurationClient _webAssemblyCachedApplicationConfigurationClient;
public BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService(WebAssemblyCachedApplicationConfigurationClient webAssemblyCachedApplicationConfigurationClient)
{
_webAssemblyCachedApplicationConfigurationClient = webAssemblyCachedApplicationConfigurationClient;
}
public async Task ResetAsync()
{
await _webAssemblyCachedApplicationConfigurationClient.InitializeAsync();
}
}

@ -1,16 +1,28 @@
using System.Collections.Generic;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Packages.Core;
using Volo.Abp.Modularity;
using Volo.Abp.Localization;
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Select2;
[DependsOn(typeof(CoreScriptContributor))]
public class Select2ScriptContributor : BundleContributor
{
public const string PackageName = "select2";
public override void ConfigureBundle(BundleConfigurationContext context)
{
//TODO: Add select2.full.min.js or localize!
//TODO: Add select2.full.min.js
context.Files.AddIfNotContains("/libs/select2/js/select2.min.js");
}
public override void ConfigureDynamicResources(BundleConfigurationContext context)
{
var fileName = context.LazyServiceProvider.LazyGetRequiredService<IOptions<AbpLocalizationOptions>>().Value.GetCurrentUICultureLanguageFilesMap(PackageName);
var filePath = $"/libs/select2/js/i18n/{fileName}.js";
if (context.FileProvider.GetFileInfo(filePath).Exists)
{
context.Files.AddIfNotContains(filePath);
}
}
}

@ -105,6 +105,7 @@
$select.select2({
ajax: {
url: url,
delay: 250,
dataType: "json",
data: function (params) {
let query = {};
@ -132,6 +133,7 @@
width: '100%',
dropdownParent: parentSelector ? $(parentSelector) : $('body'),
allowClear: allowClear,
language: abp.localization.currentCulture.cultureName,
placeholder: {
id: '-1',
text: placeholder

@ -15,7 +15,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="6.4.0" />
<PackageReference Include="Autofac" Version="7.0.0" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Autofac.Extras.DynamicProxy" Version="6.0.1" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="$(MicrosoftPackageVersion)" />

@ -1,8 +1,11 @@

using System;
namespace Volo.Abp.Cli.Auth;
public class LoginInfo
{
public Guid? Id { get; set; }
public string Name { get; set; }
public string Surname { get; set; }

@ -1,5 +1,4 @@
using System;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System.Text;
using System.Threading.Tasks;
@ -41,7 +40,7 @@ public class LoginInfoCommand : IConsoleCommand, ITransientDependency
var sb = new StringBuilder();
sb.AppendLine("");
sb.AppendLine($"Login info:");
sb.AppendLine("Login info:");
sb.AppendLine($"Name: {loginInfo.Name}");
sb.AppendLine($"Surname: {loginInfo.Surname}");
sb.AppendLine($"Username: {loginInfo.Username}");

@ -209,7 +209,22 @@ public static class AbpStringExtensions
return str;
}
return str.Substring(0, pos) + replace + str.Substring(pos + search.Length);
var searchLength = search.Length;
var replaceLength = replace.Length;
var newLength = str.Length - searchLength + replaceLength;
Span<char> buffer = newLength <= 1024 ? stackalloc char[newLength] : new char[newLength];
// Copy the part of the original string before the search term
str.AsSpan(0, pos).CopyTo(buffer);
// Copy the replacement text
replace.AsSpan().CopyTo(buffer.Slice(pos));
// Copy the remainder of the original string
str.AsSpan(pos + searchLength).CopyTo(buffer.Slice(pos + replaceLength));
return buffer.ToString();
}
/// <summary>

@ -19,8 +19,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="$(MicrosoftEntityFrameworkCorePackageVersion)" />
<PackageReference Include="Devart.Data.Oracle.EFCore" Version="9.16.1434" />
<PackageReference Include="Devart.Data.Oracle.EFCore" Version="10.1.134.7" />
</ItemGroup>
</Project>

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Volo.Abp.Data;
@ -5,7 +6,7 @@ using Volo.Abp.UI.Navigation;
namespace Volo.Abp.UI.Navigation;
public class ApplicationMenu : IHasMenuItems
public class ApplicationMenu : IHasMenuItems, IHasMenuGroups
{
/// <summary>
/// Unique name of the menu in the application.
@ -31,6 +32,10 @@ public class ApplicationMenu : IHasMenuItems
[NotNull]
public ApplicationMenuItemList Items { get; }
/// <inheritdoc cref="IHasMenuGroups.Groups"/>
[NotNull]
public ApplicationMenuGroupList Groups { get; }
/// <summary>
/// Can be used to store a custom object related to this menu.
/// </summary>
@ -47,6 +52,7 @@ public class ApplicationMenu : IHasMenuItems
DisplayName = displayName ?? Name;
Items = new ApplicationMenuItemList();
Groups = new ApplicationMenuGroupList();
}
/// <summary>
@ -60,6 +66,17 @@ public class ApplicationMenu : IHasMenuItems
return this;
}
/// <summary>
/// Adds a <see cref="ApplicationMenuGroup"/> to <see cref="Groups"/>.
/// </summary>
/// <param name="group"><see cref="ApplicationMenuGroup"/> to be added</param>
/// <returns>This <see cref="ApplicationMenu"/> object</returns>
public ApplicationMenu AddGroup([NotNull] ApplicationMenuGroup group)
{
Groups.Add(group);
return this;
}
/// <summary>
/// Adds a custom data item to <see cref="CustomData"/> with given key &amp; value.
/// </summary>

@ -64,4 +64,54 @@ public static class ApplicationMenuExtensions
return menuWithItems;
}
[NotNull]
public static ApplicationMenuGroup GetMenuGroup(
[NotNull] this IHasMenuGroups menuWithGroups,
string groupName)
{
var menuGroup = menuWithGroups.GetMenuGroupOrNull(groupName);
if (menuGroup == null)
{
throw new AbpException($"Could not find a group item with given name: {groupName}");
}
return menuGroup;
}
[CanBeNull]
public static ApplicationMenuGroup GetMenuGroupOrNull(
[NotNull] this IHasMenuGroups menuWithGroups,
string menuGroupName)
{
Check.NotNull(menuWithGroups, nameof(menuWithGroups));
return menuWithGroups.Groups.FirstOrDefault(group => group.Name == menuGroupName);
}
public static bool TryRemoveMenuGroup(
[NotNull] this IHasMenuGroups menuWithGroups,
string menuGroupName)
{
Check.NotNull(menuWithGroups, nameof(menuWithGroups));
return menuWithGroups.Groups.RemoveAll(group => group.Name == menuGroupName) > 0;
}
[NotNull]
public static IHasMenuGroups SetMenuGroupOrder(
[NotNull] this IHasMenuGroups menuWithGroups,
string menuGroupName,
int order)
{
Check.NotNull(menuWithGroups, nameof(menuWithGroups));
var menuGroup = menuWithGroups.GetMenuGroupOrNull(menuGroupName);
if (menuGroup != null)
{
menuGroup.Order = order;
}
return menuWithGroups;
}
}

@ -0,0 +1,67 @@
using JetBrains.Annotations;
namespace Volo.Abp.UI.Navigation;
public class ApplicationMenuGroup
{
private string _displayName;
/// <summary>
/// Default <see cref="Order"/> value of a group item.
/// </summary>
public const int DefaultOrder = 1000;
/// <summary>
/// Unique name of the group in the application.
/// </summary>
[NotNull]
public string Name { get; }
/// <summary>
/// Display name of the group.
/// </summary>
[NotNull]
public string DisplayName {
get { return _displayName; }
set {
Check.NotNullOrWhiteSpace(value, nameof(value));
_displayName = value;
}
}
/// <summary>
/// Can be used to render the element with a specific Id for DOM selections.
/// </summary>
public string ElementId { get; set; }
/// <summary>
/// The Display order of the group.
/// Default value: 1000.
/// </summary>
public int Order { get; set; }
public ApplicationMenuGroup(
[NotNull] string name,
[NotNull] string displayName,
string elementId = null,
int order = DefaultOrder)
{
Check.NotNullOrWhiteSpace(name, nameof(name));
Check.NotNullOrWhiteSpace(displayName, nameof(displayName));
Name = name;
DisplayName = displayName;
ElementId = elementId;
Order = order;
}
private string GetDefaultElementId()
{
return "MenuGroup_" + Name;
}
public override string ToString()
{
return $"[ApplicationMenuGroup] Name = {Name}";
}
}

@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Linq;
namespace Volo.Abp.UI.Navigation;
public class ApplicationMenuGroupList: List<ApplicationMenuGroup>
{
public ApplicationMenuGroupList()
{
}
public ApplicationMenuGroupList(int capacity)
: base(capacity)
{
}
public ApplicationMenuGroupList(IEnumerable<ApplicationMenuGroup> collection)
: base(collection)
{
}
public void Normalize()
{
Order();
}
private void Order()
{
var orderedItems = this.OrderBy(item => item.Order).ToArray();
Clear();
AddRange(orderedItems);
}
}

@ -92,6 +92,11 @@ public class ApplicationMenuItem : IHasMenuItems, IHasSimpleStateCheckers<Applic
/// </summary>
public string CssClass { get; set; }
/// <summary>
/// Can be used to group menu items.
/// </summary>
public string GroupName { get; set; }
public ApplicationMenuItem(
[NotNull] string name,
[NotNull] string displayName,
@ -101,6 +106,7 @@ public class ApplicationMenuItem : IHasMenuItems, IHasSimpleStateCheckers<Applic
string target = null,
string elementId = null,
string cssClass = null,
string groupName = null,
string requiredPermissionName = null)
{
Check.NotNullOrWhiteSpace(name, nameof(name));
@ -114,6 +120,7 @@ public class ApplicationMenuItem : IHasMenuItems, IHasSimpleStateCheckers<Applic
Target = target;
ElementId = elementId ?? GetDefaultElementId();
CssClass = cssClass;
GroupName = groupName;
RequiredPermissionName = requiredPermissionName;
StateCheckers = new List<ISimpleStateChecker<ApplicationMenuItem>>();
Items = new ApplicationMenuItemList();

@ -0,0 +1,9 @@
namespace Volo.Abp.UI.Navigation;
public interface IHasMenuGroups
{
/// <summary>
/// Menu groups.
/// </summary>
ApplicationMenuGroupList Groups { get; }
}

@ -96,6 +96,7 @@ public class MenuManager : IMenuManager, ITransientDependency
}
NormalizeMenu(menu);
NormalizeMenuGroup(menu);
return menu;
}
@ -159,4 +160,23 @@ public class MenuManager : IMenuManager, ITransientDependency
menuWithItems.Items.Normalize();
}
protected virtual void NormalizeMenuGroup(ApplicationMenu applicationMenu)
{
foreach (var menuGroup in applicationMenu.Items.Where(x => !x.GroupName.IsNullOrWhiteSpace()).GroupBy(x => x.GroupName))
{
var group = applicationMenu.GetMenuGroupOrNull(menuGroup.First().GroupName);
if (group != null)
{
continue;
}
foreach (var menuItem in menuGroup)
{
menuItem.GroupName = null;
}
}
applicationMenu.Groups.Normalize();
}
}

@ -16,6 +16,7 @@ public class AbpUiNavigationTestModule : AbpModule
options.MenuContributors.Add(new MenuManager_Tests.TestMenuContributor1());
options.MenuContributors.Add(new MenuManager_Tests.TestMenuContributor2());
options.MenuContributors.Add(new MenuManager_Tests.TestMenuContributor3());
options.MenuContributors.Add(new MenuManager_Tests.TestMenuContributor4());
options.MainMenuNames.Add(MenuManager_Tests.TestMenuContributor3.MenuName);
});

@ -46,7 +46,7 @@ public class MenuManager_Tests : AbpIntegratedTest<AbpUiNavigationTestModule>
mainMenu.Name.ShouldBe(StandardMenus.Main);
mainMenu.DisplayName.ShouldBe("Main Menu");
mainMenu.Items.Count.ShouldBe(2);
mainMenu.Items.Count.ShouldBe(5);
mainMenu.Items[0].Name.ShouldBe("Dashboard");
mainMenu.Items[1].Name.ShouldBe(DefaultMenuNames.Application.Main.Administration);
mainMenu.Items[1].Items[0].Name.ShouldBe("Administration.UserManagement");
@ -63,12 +63,29 @@ public class MenuManager_Tests : AbpIntegratedTest<AbpUiNavigationTestModule>
mainMenu.Name.ShouldBe(StandardMenus.Main);
mainMenu.Items.Count.ShouldBe(3);
mainMenu.Items.Count.ShouldBe(6);
mainMenu.Items.ShouldContain(x => x.Name == "Products");
mainMenu.Items.ShouldContain(x => x.Name == "Dashboard");
}
[Fact]
public async Task GetMainMenuAsync_GroupMenuItems()
{
var mainMenu = await _menuManager.GetMainMenuAsync();
mainMenu.Name.ShouldBe(StandardMenus.Main);
mainMenu.Items.Count.ShouldBe(6);
mainMenu.Items[2].GroupName.ShouldBe("Layouts");
mainMenu.Items[3].GroupName.ShouldBe("Layouts");
mainMenu.Items[4].GroupName.ShouldBe(null); // No group defined
var layoutsGroup = mainMenu.GetMenuGroup("Layouts");
layoutsGroup.Name.ShouldBe("Layouts");
layoutsGroup.DisplayName.ShouldBe("Layouts");
}
/* Adds menu items:
* - Administration
* - User Management
@ -149,4 +166,29 @@ public class MenuManager_Tests : AbpIntegratedTest<AbpUiNavigationTestModule>
return Task.CompletedTask;
}
}
/* Adds group and menu items:
* - Layouts
* - Toolbars
* - Page Header
*/
public class TestMenuContributor4 : IMenuContributor
{
public Task ConfigureMenuAsync(MenuConfigurationContext context)
{
if (context.Menu.Name != StandardMenus.Main)
{
return Task.CompletedTask;
}
context.Menu.AddGroup(new ApplicationMenuGroup("Layouts", "Layouts"));
context.Menu.AddItem(new ApplicationMenuItem("Toolbars", "Toolbars", url: "/layouts/toolbars", groupName: "Layouts"));
context.Menu.AddItem(new ApplicationMenuItem("PageHeader", "Page Header", url: "/layouts/page-header", groupName: "Layouts"));
context.Menu.AddItem(new ApplicationMenuItem("Branding", "Branding", url: "/layouts/branding", groupName: "NotDefinedGroup"));
return Task.CompletedTask;
}
}
}

@ -125,4 +125,10 @@ public interface IIdentityUserRepository : IBasicRepository<IdentityUser, Guid>
bool includeDetails = true,
CancellationToken cancellationToken = default
);
Task<List<IdentityUser>> GetListByIdsAsync(
IEnumerable<Guid> ids,
bool includeDetails = false,
CancellationToken cancellationToken = default
);
}

@ -20,6 +20,7 @@ public class EfCoreIdentityLinkUserRepository : EfCoreRepository<IIdentityDbCont
public virtual async Task<IdentityLinkUser> FindAsync(IdentityLinkUserInfo sourceLinkUserInfo, IdentityLinkUserInfo targetLinkUserInfo, CancellationToken cancellationToken = default)
{
return await (await GetDbSetAsync())
.AsNoTracking()
.OrderBy(x => x.Id).FirstOrDefaultAsync(x =>
x.SourceUserId == sourceLinkUserInfo.UserId && x.SourceTenantId == sourceLinkUserInfo.TenantId &&
x.TargetUserId == targetLinkUserInfo.UserId && x.TargetTenantId == targetLinkUserInfo.TenantId ||
@ -31,7 +32,8 @@ public class EfCoreIdentityLinkUserRepository : EfCoreRepository<IIdentityDbCont
public virtual async Task<List<IdentityLinkUser>> GetListAsync(IdentityLinkUserInfo linkUserInfo, List<IdentityLinkUserInfo> excludes = null,
CancellationToken cancellationToken = default)
{
IQueryable<IdentityLinkUser> query = (await GetDbSetAsync())
var query = (await GetDbSetAsync())
.AsNoTracking()
.Where(x =>
x.SourceUserId == linkUserInfo.UserId && x.SourceTenantId == linkUserInfo.TenantId ||
x.TargetUserId == linkUserInfo.UserId && x.TargetTenantId == linkUserInfo.TenantId);
@ -51,7 +53,7 @@ public class EfCoreIdentityLinkUserRepository : EfCoreRepository<IIdentityDbCont
public virtual async Task DeleteAsync(IdentityLinkUserInfo linkUserInfo, CancellationToken cancellationToken = default)
{
var linkUsers = await (await GetDbSetAsync()).Where(x =>
var linkUsers = await (await GetDbSetAsync()).AsNoTracking().Where(x =>
x.SourceUserId == linkUserInfo.UserId && x.SourceTenantId == linkUserInfo.TenantId ||
x.TargetUserId == linkUserInfo.UserId && x.TargetTenantId == linkUserInfo.TenantId)
.ToListAsync(cancellationToken: GetCancellationToken(cancellationToken));

@ -348,4 +348,12 @@ public class EfCoreIdentityUserRepository : EfCoreRepository<IIdentityDbContext,
GetCancellationToken(cancellationToken)
);
}
public virtual async Task<List<IdentityUser>> GetListByIdsAsync(IEnumerable<Guid> ids, bool includeDetails = false, CancellationToken cancellationToken = default)
{
return await (await GetDbSetAsync())
.IncludeDetails(includeDetails)
.Where(x => ids.Contains(x.Id))
.ToListAsync(GetCancellationToken(cancellationToken));
}
}

@ -316,4 +316,11 @@ public class MongoIdentityUserRepository : MongoDbRepository<IAbpIdentityMongoDb
GetCancellationToken(cancellationToken)
);
}
public virtual async Task<List<IdentityUser>> GetListByIdsAsync(IEnumerable<Guid> ids, bool includeDetails = false, CancellationToken cancellationToken = default)
{
return await (await GetMongoQueryableAsync(cancellationToken))
.Where(x => ids.Contains(x.Id))
.ToListAsync(GetCancellationToken(cancellationToken));
}
}

@ -18,3 +18,4 @@ export * from './routes.service';
export * from './session-state.service';
export * from './subscription.service';
export * from './track-by.service';
export * from './local-storage.service';

@ -0,0 +1,28 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class AbpLocalStorageService implements Storage {
constructor() {}
[name: string]: any;
get length(): number {
return localStorage.length;
}
clear(): void {
localStorage.clear();
}
getItem(key: string): string {
return localStorage.getItem(key);
}
key(index: number): string {
return localStorage.key(index);
}
removeItem(key: string): void {
localStorage.removeItem(key);
}
setItem(key: string, value: string): void {
localStorage.setItem(key, value);
}
}

@ -5,6 +5,7 @@ import { Session } from '../models/session';
import { CurrentTenantDto } from '../proxy/volo/abp/asp-net-core/mvc/multi-tenancy/models';
import { InternalStore } from '../utils/internal-store-utils';
import { ConfigStateService } from './config-state.service';
import { AbpLocalStorageService } from './local-storage.service';
@Injectable({
providedIn: 'root',
@ -13,16 +14,19 @@ export class SessionStateService {
private readonly store = new InternalStore({} as Session.State);
private updateLocalStorage = () => {
localStorage.setItem('abpSession', JSON.stringify(this.store.state));
this.localStorageService.setItem('abpSession', JSON.stringify(this.store.state));
};
constructor(private configState: ConfigStateService) {
constructor(
private configState: ConfigStateService,
private localStorageService: AbpLocalStorageService,
) {
this.init();
this.setInitialLanguage();
}
private init() {
const session = localStorage.getItem('abpSession');
const session = this.localStorageService.getItem('abpSession');
if (session) {
this.store.set(JSON.parse(session));
}

@ -0,0 +1,52 @@
import { TestBed } from '@angular/core/testing';
import { AbpLocalStorageService } from '../services/local-storage.service';
describe('LocalStorageService', () => {
let service: AbpLocalStorageService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(AbpLocalStorageService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should be called getItem', () => {
const spy = jest.spyOn(service, 'getItem');
service.getItem('test');
expect(spy).toHaveBeenCalled();
});
it('should be called setItem', () => {
const spy = jest.spyOn(service, 'setItem');
service.setItem('test', 'value');
expect(spy).toHaveBeenCalled();
});
it('should be called removeItem', () => {
const spy = jest.spyOn(service, 'removeItem');
service.removeItem('test');
expect(spy).toHaveBeenCalled();
});
it('should be called clear', () => {
const spy = jest.spyOn(service, 'clear');
service.clear();
expect(spy).toHaveBeenCalled();
});
it('should be called key', () => {
const spy = jest.spyOn(service, 'key');
service.key(0);
expect(spy).toHaveBeenCalled();
});
it('should be called length', () => {
const spy = jest.spyOn(service, 'length', 'get');
service.length;
expect(spy).toHaveBeenCalled();
});
});

@ -2,14 +2,14 @@ import { APP_INITIALIZER, ModuleWithProviders, NgModule, Provider } from '@angul
import { CommonModule } from '@angular/common';
import { OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
import {
AbpLocalStorageService,
ApiInterceptor,
AuthGuard,
AuthService,
CHECK_AUTHENTICATION_STATE_FN_KEY,
noop,
PIPE_TO_LOGIN_FN_KEY
PIPE_TO_LOGIN_FN_KEY,
} from '@abp/ng.core';
import { storageFactory } from './utils/storage.factory';
import { AbpOAuthService } from './services';
import { OAuthConfigurationHandler } from './handlers/oauth-configuration.handler';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
@ -59,7 +59,7 @@ export class AbpOAuthModule {
useFactory: noop,
},
OAuthModule.forRoot().providers as Provider[],
{ provide: OAuthStorage, useFactory: storageFactory },
{ provide: OAuthStorage, useClass: AbpLocalStorageService },
],
};
}

@ -12,10 +12,10 @@ export const NavigateToManageProfileProvider: Provider = {
console.warn('The oAuthConfig env is missing on environment.ts');
return;
}
window.open(
`${env.oAuthConfig.issuer}/Account/Manage?returnUrl=${window.location.href}`,
'_self',
);
const { issuer } = env.oAuthConfig;
const path = issuer.endsWith('/') ? issuer : `${issuer}/`;
window.open(`${path}Account/Manage?returnUrl=${window.location.href}`, '_self');
};
},
};

@ -9,6 +9,7 @@ import {
import { Observable, of } from 'rxjs';
import { filter, switchMap, tap } from 'rxjs/operators';
import {
AbpLocalStorageService,
ConfigStateService,
EnvironmentService,
HttpErrorReporterService,
@ -29,6 +30,7 @@ export abstract class AuthFlowStrategy {
protected oAuthService: OAuthService2;
protected oAuthConfig!: AuthConfig;
protected sessionState: SessionStateService;
protected localStorageService: AbpLocalStorageService;
protected tenantKey: string;
abstract checkIfInternalAuth(queryParams?: Params): boolean;
@ -50,6 +52,7 @@ export abstract class AuthFlowStrategy {
this.configState = injector.get(ConfigStateService);
this.oAuthService = injector.get(OAuthService2);
this.sessionState = injector.get(SessionStateService);
this.localStorageService = injector.get(AbpLocalStorageService);
this.oAuthConfig = this.environment.getEnvironment().oAuthConfig || {};
this.tenantKey = injector.get(TENANT_KEY);

@ -4,7 +4,7 @@ import { Params, Router } from '@angular/router';
import { from, Observable, pipe } from 'rxjs';
import { HttpHeaders } from '@angular/common/http';
import { AuthFlowStrategy } from './auth-flow-strategy';
import { pipeToLogin, removeRememberMe, setRememberMe } from '../utils/auth-utils';
import { pipeToLogin, removeRememberMe } from '../utils/auth-utils';
import { LoginParams } from '@abp/ng.core';
import { clearOAuthStorage } from '../utils/clear-o-auth-storage';
@ -33,7 +33,7 @@ export class AuthPasswordFlowStrategy extends AuthFlowStrategy {
this.refreshToken();
} else {
this.oAuthService.logOut();
removeRememberMe();
removeRememberMe(this.localStorageService);
this.configState.refreshAppState().subscribe();
}
});
@ -74,7 +74,7 @@ export class AuthPasswordFlowStrategy extends AuthFlowStrategy {
switchMap(() => this.configState.refreshAppState()),
tap(() => {
router.navigateByUrl('/');
removeRememberMe();
removeRememberMe(this.localStorageService);
}),
);
}
@ -82,7 +82,7 @@ export class AuthPasswordFlowStrategy extends AuthFlowStrategy {
protected refreshToken() {
return this.oAuthService.refreshToken().catch(() => {
clearOAuthStorage();
removeRememberMe();
removeRememberMe(this.localStorageService);
});
}
}

@ -7,7 +7,7 @@ import {
ConfigStateService,
LoginParams,
PipeToLoginFn,
SetTokenResponseToStorageFn,
AbpLocalStorageService,
} from '@abp/ng.core';
const cookieKey = 'rememberMe';
@ -19,25 +19,28 @@ export const pipeToLogin: PipeToLoginFn = function (
) {
const configState = injector.get(ConfigStateService);
const router = injector.get(Router);
const localStorage = injector.get(AbpLocalStorageService);
return pipe(
switchMap(() => configState.refreshAppState()),
tap(() => {
setRememberMe(params.rememberMe);
setRememberMe(params.rememberMe, localStorage);
if (params.redirectUrl) router.navigate([params.redirectUrl]);
}),
);
};
export function setRememberMe(remember: boolean | undefined) {
removeRememberMe();
localStorage.setItem(storageKey, 'true');
export function setRememberMe(
remember: boolean | undefined,
localStorageService: AbpLocalStorageService,
) {
removeRememberMe(localStorageService);
localStorageService.setItem(storageKey, 'true');
document.cookie = `${cookieKey}=true; path=/${
remember ? ' ;expires=Fri, 31 Dec 9999 23:59:59 GMT' : ''
}`;
}
export function removeRememberMe() {
localStorage.removeItem(storageKey);
export function removeRememberMe(localStorageService: AbpLocalStorageService) {
localStorageService.removeItem(storageKey);
document.cookie = cookieKey + '= ; path=/; expires = Thu, 01 Jan 1970 00:00:00 GMT';
}

@ -12,3 +12,5 @@ export * from './toast-container/toast-container.component';
export * from './toast/toast.component';
export * from './password/password.component';
export * from './card/index';
export * from './checkbox/checkbox.component';
export * from './form-input/form-input.component';

@ -4,6 +4,7 @@ using Blazorise.Bootstrap5;
using Blazorise.Icons.FontAwesome;
using Medallion.Threading;
using Medallion.Threading.Redis;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
@ -163,6 +164,7 @@ public class MyProjectNameBlazorModule : AbpModule
.AddCookie("Cookies", options =>
{
options.ExpireTimeSpan = TimeSpan.FromDays(365);
options.IntrospectAccessToken();
})
.AddAbpOpenIdConnect("oidc", options =>
{

@ -6,7 +6,9 @@ using Microsoft.Extensions.DependencyInjection;
using Volo.Abp;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.Sqlite;
using Volo.Abp.FeatureManagement;
using Volo.Abp.Modularity;
using Volo.Abp.PermissionManagement;
using Volo.Abp.Uow;
namespace MyCompanyName.MyProjectName.EntityFrameworkCore;
@ -22,7 +24,18 @@ public class MyProjectNameEntityFrameworkCoreTestModule : AbpModule
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<FeatureManagementOptions>(options =>
{
options.SaveStaticFeaturesToDatabase = false;
options.IsDynamicFeatureStoreEnabled = false;
});
Configure<PermissionManagementOptions>(options =>
{
options.SaveStaticPermissionsToDatabase = false;
options.IsDynamicPermissionStoreEnabled = false;
});
context.Services.AddAlwaysDisableUnitOfWorkTransaction();
ConfigureInMemorySqlite(context.Services);
}

Loading…
Cancel
Save