Merge branch 'dev' into abp-prerelease-compatibility

pull/4782/head
Yunus Emre Kalkan 5 years ago
commit 6a2d71882e

@ -0,0 +1,66 @@
# BLOB Storing Minio Provider
BLOB Storing Minio Provider can store BLOBs in [MinIO Object storage](https://min.io/).
> Read the [BLOB Storing document](Blob-Storing.md) to understand how to use the BLOB storing system. This document only covers how to configure containers to use a Minio BLOB as the storage provider.
## Installation
Use the ABP CLI to add [Volo.Abp.BlobStoring.Minio](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Minio) NuGet package to your project:
* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed before.
* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.BlobStoring.Minio` package.
* Run `abp add-package Volo.Abp.BlobStoring.Minio` command.
If you want to do it manually, install the [Volo.Abp.BlobStoring.Minio](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Minio) NuGet package to your project and add `[DependsOn(typeof(AbpBlobStoringMinioModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project.
## Configuration
Configuration is done in the `ConfigureServices` method of your [module](Module-Development-Basics.md) class, as explained in the [BLOB Storing document](Blob-Storing.md).
**Example: Configure to use the minio storage provider by default**
````csharp
Configure<AbpBlobStoringOptions>(options =>
{
options.Containerscontainer.UseMinio(minio =>
{
minio.EndPoint = "your minio endPoint";
minio.AccessKey = "your minio accessKey";
minio.SecretKey = "your minio secretKey";
minio.BucketName = "your minio bucketName";
});
});
````
> See the [BLOB Storing document](Blob-Storing.md) to learn how to configure this provider for a specific container.
### Options
* **EndPoint** (string): URL to object storage service. Please refer to MinIO Client SDK for .NET: https://docs.min.io/docs/dotnet-client-quickstart-guide.html
* **AccessKey** (string): Access key is the user ID that uniquely identifies your account.
* **SecretKey** (string): Secret key is the password to your account.
* **BucketName** (string): You can specify the bucket name in MinIO. If this is not specified, it uses the name of the BLOB container defined with the `BlogContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)).MinIO is the defacto standard for S3 compatibility, So MinIO has some **rules for naming bucket**. The [following rules](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html) apply for naming MinIO buckets:
* Bucket names must be between **3** and **63** characters long.
* Bucket names can consist only of **lowercase** letters, numbers, dots (.), and hyphens (-).
* Bucket names must begin and end with a letter or number.
* Bucket names must not be formatted as an IP address (for example, 192.168.5.4).
* Bucket names can't begin with **xn--** (for buckets created after February 2020).
* Bucket names must be unique within a partition.
* Buckets used with Amazon S3 Transfer Acceleration can't have dots (.) in their names. For more information about transfer acceleration, see Amazon S3 Transfer Acceleration.
* **WithSSL** (bool): Default value is `false`,Chain to MinIO Client object to use https instead of http.
* **CreateContainerIfNotExists** (bool): Default value is `false`, If a bucket does not exist in minio, `MinioBlobProvider` will try to create it.
## Minio Blob Name Calculator
Minio Blob Provider organizes BLOB name and implements some conventions. The full name of a BLOB is determined by the following rules by default:
* Appends `host` string if [current tenant](Multi-Tenancy.md) is `null` (or multi-tenancy is disabled for the container - see the [BLOB Storing document](Blob-Storing.md) to learn how to disable multi-tenancy for a container).
* Appends `tenants/<tenant-id>` string if current tenant is not `null`.
* Appends the BLOB name.
## Other Services
* `MinioBlobProvider` is the main service that implements the Minio BLOB storage provider, if you want to override/replace it via [dependency injection](Dependency-Injection.md) (don't replace `IBlobProvider` interface, but replace `MinioBlobProvider` class).
* `IMinioBlobNameCalculator` is used to calculate the full BLOB name (that is explained above). It is implemented by the `DefaultMinioBlobNameCalculator` by default.

@ -19,6 +19,8 @@ The ABP Framework has already the following storage provider implementations;
* [File System](Blob-Storing-File-System.md): Stores BLOBs in a folder of the local file system, as standard files.
* [Database](Blob-Storing-Database.md): Stores BLOBs in a database.
* [Azure](Blob-Storing-Azure.md): Stores BLOBs on the [Azure BLOB storage](https://azure.microsoft.com/en-us/services/storage/blobs/).
* [Aliyun](Blob-Storing-Aliyun.md): Stores BLOBs on the [Aliyun Blob storage](https://help.aliyun.com/product/31815.html).
* [Ninio](Blob-Storing-Minio.md): Stores BLOBs on the [MinIO Object storage](https://min.io/).
More providers will be implemented by the time. You can [request](https://github.com/abpframework/abp/issues/new) it for your favorite provider or [create it yourself](Blob-Storing-Custom-Provider.md) and [contribute](Contribution/Index.md) to the ABP Framework.

@ -43,6 +43,12 @@ To be able to run the solution from source code, following tools should be insta
* [ElasticSearch](https://www.elastic.co/downloads/elasticsearch) 6.6+
* [Kibana](https://www.elastic.co/downloads/kibana) 6.6+ (optional, recommended to show logs)
### Running Infrastructure
* Docker-compose is used to run the pre requirements with ease as default. If you don't have it, you can download and start using [Docker for Windows](https://docs.docker.com/docker-for-windows/) from [here](https://docs.docker.com/docker-for-windows/install/) on windows environment.
* Run the command `docker-compose -f docker-compose.infrastructure.yml -f docker-compose.infrastructure.override.yml up -d` at `MicroserviceDemo` directory or run the powershell script `__Run_Infrastructure.ps1` located at `MicroserviceDemo/_run` directory.
* If you don't want to use docker for pre required services and install them on your local development, you need to update `appsettings.json` files of the projects in the MicroserviceDemo solution accordingly.
### Open & Build the Visual Studio Solution
* Open the `samples\MicroserviceDemo\MicroserviceDemo.sln` in Visual Studio 2017 (15.9.0+).

@ -220,6 +220,10 @@
"text": "Aliyun Provider",
"path": "Blob-Storing-Aliyun.md"
},
{
"text": "Minio Provider",
"path": "Blob-Storing-Minio.md"
},
{
"text": "Create a Custom Provider",
"path": "Blob-Storing-Custom-Provider.md"

@ -0,0 +1,66 @@
# BLOB Storing Minio 提供程序
BLOB Storing Minio提供程序帮助你存储对象到 [MinIO Object storage](https://min.io/),
> 阅读[BLOB存储文档](Blob-Storing.md)了解如何使用BLOB存储系统, 本文档仅介绍如何为容器配置Minio提供程序,
## 安装
使用 ABP CLI 来安装 [Volo.Abp.BlobStoring.Minio](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Minio) NuGet 包到你的项目:
* 如果你没有安装ABP CLI,请先安装 [ABP CLI](https://docs.abp.io/en/abp/latest/CLI),
* 在要添加 `Volo.Abp.BlobStoring.Minio` 包的 `.csproj` 文件目录打开命令行,
* 执行 `abp add-package Volo.Abp.BlobStoring.Minio` 命令,
如果你要手动安装, 通过NuGet安装 [Volo.Abp.BlobStoring.Minio](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Minio) 到你的项目,然后添加 `[DependsOn(typeof(AbpBlobStoringMinioModule))]` 特性到你的 [ABP module](Module-Development-Basics.md) 类上,
## 配置
配置在你的[module](Module-Development-Basics.md)类中的`ConfigureServices`方法中完成,
**例: 配置使用Minio存储**
````csharp
Configure<AbpBlobStoringOptions>(options =>
{
options.Containerscontainer.UseMinio(minio =>
{
minio.EndPoint = "你的 minio endPoint";
minio.AccessKey = "你的 minio accessKey";
minio.SecretKey = "你的 minio secretKey";
minio.BucketName = "你的 minio bucketName";
});
});
````
> 参阅[BLOB存储文档](Blob-Storing.md) 学习如何为指定容器配置提供程序,
### 选项
* **EndPoint** (string): 你的Minio对象存储服务的URL, 查看文档https://docs.min.io/docs/dotnet-client-quickstart-guide.html
* **AccessKey** (string): Access key是唯一标识你的账户的用户ID,
* **SecretKey** (string): Access key是唯一标识你的账户的用户ID
* **BucketName** (string):你可以指定bucket名称,如果没有指定,将使用 `BlogContainerName` 属性定义的BLOB容器的名称(查阅[BLOB storing document](Blob-Storing.md)),MinIO完全兼容S3标准,所以有一些 **bucket命名规则**,必须符合[规则](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html):
* Bucket名字必须 **3** 到 **63** 字符长度.
* Bucket名字必须是 **小写** 的字母,数字,点号(.), 横杠 (-),
* Bucket名字必须是以数字和字母开头和结尾,
* Bucket名字不能是ip (例如, 192.168.5.4),
* Bucket名字不能以 **xn--** 开头, (2020年2月以后创建),
* Bucket名字必须区块唯一
* Buckets如果使用Amazon S3加速传输名字不能有点号(.),
* **WithSSL** (bool): 默认 `false`,代表使用HTTPS,
* **CreateContainerIfNotExists** (bool): 默认 `false`,如果不存在bucket, `MinioBlobProvider` 将会创建一个,
## Minio BLOB 名称计算器
默认情况下BLOB的全名由以下规则确定:
* 如果当前租户为 `null`(或容器禁用多租户 - 请参阅[BLOB存储文档](Blob-Storing.md) 了解如何禁用容器的多租户),则追加 `host` 字符串,
* 如果当前租户不为 `null`,则追加 `tenants/<tenant-id>` 字符串,
* 追加 BLOB 名称,
## 其他服务
* `MinioBlobProvider` 是实现Minio BLOB存储提供程序的主要服务,如果你想要通过[依赖注入](Dependency-Injection.md)覆盖/替换它(不要替换 `IBlobProvider` 接口,而是替换 `MinioBlobProvider` 类).
* `IMinioBlobNameCalculator` 服务用于计算文件路径. 默认实现是 `DefaultMinioBlobNameCalculator`. 如果你想自定义文件路径计算,可以替换/覆盖它.

@ -19,6 +19,8 @@ ABP框架已经有以下存储提供程序的实现;
* [File System](Blob-Storing-File-System.md):将BLOB作为标准文件存储在本地文件系统的文件夹中.
* [Database](Blob-Storing-Database.md): 将BLOB存储在数据库中.
* [Azure](Blob-Storing-Azure.md): 将BLOG存储在 [Azure BLOB storage](https://azure.microsoft.com/en-us/services/storage/blobs/)中.
* [Aliyun](Blob-Storing-Aliyun.md): 将BLOB存储在[Aliyun Blob storage](https://help.aliyun.com/product/31815.html)中.
* [Ninio](Blob-Storing-Minio.md): 将BLOB存储在[MinIO Object storage](https://min.io/)中.
以后会实现更多的提供程序,你可以为自己喜欢的提供程序创建[请求](https://github.com/abpframework/abp/issues/new),或者你也可以[自己实现](Blob-Storing-Custom-Provider.md)它并[贡献](Contribution/Index.md)到ABP框架.

@ -214,6 +214,10 @@
"text": "Aliyun提供程序",
"path": "Blob-Storing-Aliyun.md"
},
{
"text": "Minio提供程序",
"path": "Blob-Storing-Minio.md"
},
{
"text": "创建自定义提供程序",
"path": "Blob-Storing-Custom-Provider.md"

@ -1,5 +1,7 @@
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Packages.BootstrapDatepicker;
using Volo.Abp.AspNetCore.Mvc.UI.Packages.JQueryValidation;
using Volo.Abp.AspNetCore.Mvc.UI.Packages.Timeago;
using Volo.Abp.Localization;
using Volo.Abp.Modularity;
@ -12,6 +14,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Packages
{
Configure<AbpLocalizationOptions>(options =>
{
//BootstrapDatepicker
options.AddLanguagesMapOrUpdate(BootstrapDatepickerScriptContributor.PackageName,
new NameValue("zh-Hans", "zh-CN"),
new NameValue("zh-Hant", "zh-TW"));
@ -19,6 +22,16 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Packages
options.AddLanguageFilesMapOrUpdate(BootstrapDatepickerScriptContributor.PackageName,
new NameValue("zh-Hans", "zh-CN"),
new NameValue("zh-Hant", "zh-TW"));
//Timeago
options.AddLanguageFilesMapOrUpdate(TimeagoScriptContributor.PackageName,
new NameValue("zh-Hans", "zh-CN"),
new NameValue("zh-Hant", "zh-TW"));
//JQueryValidation
options.AddLanguageFilesMapOrUpdate(JQueryValidationScriptContributor.PackageName,
new NameValue("zh-Hans", "zh"),
new NameValue("zh-Hant", "zh_TW"));
});
}
}

@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Globalization;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Packages.JQuery;
using Volo.Abp.Localization;
@ -19,26 +18,12 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.BootstrapDatepicker
public override void ConfigureDynamicResources(BundleConfigurationContext context)
{
var cultureName = CultureInfo.CurrentUICulture.DateTimeFormat.Calendar.AlgorithmType ==
CalendarAlgorithmType.LunarCalendar
? "en"
: CultureInfo.CurrentUICulture.Name;
TryAddCultureFile(context, cultureName);
}
protected virtual bool TryAddCultureFile(BundleConfigurationContext context, string cultureName)
{
var fileName = context.LocalizationOptions.GetLanguageFilesMap(PackageName, cultureName);
var fileName = context.LocalizationOptions.GetCurrentUICultureLanguageFilesMap(PackageName);
var filePath = $"/libs/bootstrap-datepicker/locales/bootstrap-datepicker.{fileName}.min.js";
if (!context.FileProvider.GetFileInfo(filePath).Exists)
if (context.FileProvider.GetFileInfo(filePath).Exists)
{
return false;
context.Files.AddIfNotContains(filePath);
}
context.Files.AddIfNotContains(filePath);
return true;
}
}
}

@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Globalization;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Packages.JQuery;
using Volo.Abp.Localization;
@ -10,8 +9,6 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.JQueryValidation
[DependsOn(typeof(JQueryScriptContributor))]
public class JQueryValidationScriptContributor : BundleContributor
{
public const string DefaultLocalizationFolder = "/libs/jquery-validation/localization/";
public const string PackageName = "jquery-validation";
public override void ConfigureBundle(BundleConfigurationContext context)
@ -21,41 +18,12 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.JQueryValidation
public override void ConfigureDynamicResources(BundleConfigurationContext context)
{
//TODO: Can we optimize these points:
// - Can we get rid of context.FileProvider.GetFileInfo call?
// - What if the same Contributor is used twice for a page.
// Duplication is prevented by the bundle manager, however the logic below will execute twice
var cultureName = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName.Replace('-', '_');
var fileName = context.LocalizationOptions.GetLanguageFilesMap(PackageName, cultureName);
if (TryAddCultureFile(context, fileName))
{
return;
}
if (!cultureName.Contains("_"))
{
return;
}
fileName = context.LocalizationOptions.GetLanguageFilesMap(PackageName,
cultureName.Substring(0, cultureName.IndexOf('_')));
TryAddCultureFile(context, fileName);
}
protected virtual bool TryAddCultureFile(BundleConfigurationContext context, string cultureName)
{
var filePath = DefaultLocalizationFolder + "messages_" + cultureName + ".js";
var fileInfo = context.FileProvider.GetFileInfo(filePath);
if (!fileInfo.Exists)
var fileName = context.LocalizationOptions.GetCurrentUICultureLanguageFilesMap(PackageName);
var filePath = $"/libs/jquery-validation/localization/messages_{fileName}.js";
if (context.FileProvider.GetFileInfo(filePath).Exists)
{
return false;
context.Files.AddIfNotContains(filePath);
}
context.Files.AddIfNotContains(filePath);
return true;
}
}
}

@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Globalization;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Packages.JQuery;
using Volo.Abp.Localization;
using Volo.Abp.Modularity;
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Timeago
@ -9,6 +9,8 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Timeago
[DependsOn(typeof(JQueryScriptContributor))]
public class TimeagoScriptContributor : BundleContributor
{
public const string PackageName = "jquery.timeago";
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddIfNotContains("/libs/timeago/jquery.timeago.js");
@ -16,17 +18,11 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Timeago
public override void ConfigureDynamicResources(BundleConfigurationContext context)
{
var cultureName = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
if (cultureName.StartsWith("en"))
{
return;
}
var cultureFileName = $"/libs/timeago/locales/jquery.timeago.{cultureName}.js";
if (context.FileProvider.GetFileInfo(cultureFileName).Exists)
var fileName = context.LocalizationOptions.GetCurrentUICultureLanguageFilesMap(PackageName);
var filePath = $"/libs/timeago/locales/jquery.timeago.{fileName}.js";
if (context.FileProvider.GetFileInfo(filePath).Exists)
{
context.Files.Add(cultureFileName);
context.Files.Add(filePath);
}
}
}

@ -299,6 +299,13 @@
column.targets = i;
}
if (!column.render && column.dataFormat) {
var render = datatables.defaultRenderers[column.dataFormat];
if (render) {
column.render = render;
}
}
if (column.rowAction) {
customizeRowActionColumn(column);
}
@ -330,18 +337,23 @@
}
};
var ISOStringToDateTimeLocaleString = function (format) {
return function(data) {
var date = luxon
.DateTime
.fromISO(data, {
locale: abp.localization.currentCulture.name
});
return format ? date.toLocaleString(format) : date.toLocaleString();
};
};
datatables.defaultRenderers['date'] = function (value) {
return luxon
.DateTime
.fromISO(value, { locale: abp.localization.currentCulture.name })
.toLocaleString();
return (ISOStringToDateTimeLocaleString())(value);
};
datatables.defaultRenderers['datetime'] = function (value) {
return luxon
.DateTime
.fromISO(value, { locale: abp.localization.currentCulture.name })
.toLocaleString(luxon.DateTime.DATETIME_SHORT);
return (ISOStringToDateTimeLocaleString(luxon.DateTime.DATETIME_SHORT))(value);
};
/************************************************************************

@ -27,7 +27,7 @@
var getFilters = function ($widgetWrapperDiv) {
var filters = {};
if (opts.filterForm) {
opts.filterForm.each(function() {
filters = $.extend(filters, opts.filterForm.serializeFormToObject());
@ -47,7 +47,7 @@
};
var init = function () {
opts.wrapper.find('.abp-widget-wrapper').each(function () {
opts.wrapper.findWithSelf('.abp-widget-wrapper').each(function () {
var $widgetWrapperDiv = $(this);
var widgetName = $widgetWrapperDiv.attr('data-widget-name');
var widgetApiClass = abp.widgets[widgetName];
@ -62,7 +62,7 @@
};
var refresh = function () {
opts.wrapper.find('.abp-widget-wrapper').each(function () {
opts.wrapper.findWithSelf('.abp-widget-wrapper').each(function () {
var $widgetWrapperDiv = $(this);
var refreshUrl = $widgetWrapperDiv.attr('data-refresh-url');
@ -104,4 +104,4 @@
return publicApi;
};
})(jQuery);
})(jQuery);

@ -270,7 +270,7 @@ namespace Volo.Abp.AspNetCore.Mvc
return parameterInfo.Name;
}
return modelNameProvider.Name;
return modelNameProvider.Name ?? parameterInfo.Name;
}
private static string GetRootPath([NotNull] Type controllerType, [CanBeNull] ConventionalControllerSetting setting)

@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.AspNetCore.WebClientInfo;
using Volo.Abp.Auditing;
using Volo.Abp.DependencyInjection;
@ -35,14 +36,15 @@ namespace Volo.Abp.AspNetCore.Auditing
context.AuditInfo.Url = BuildUrl(httpContext);
}
var clientInfoProvider = context.ServiceProvider.GetRequiredService<IWebClientInfoProvider>();
if (context.AuditInfo.ClientIpAddress == null)
{
context.AuditInfo.ClientIpAddress = GetClientIpAddress(httpContext);
context.AuditInfo.ClientIpAddress = clientInfoProvider.ClientIpAddress;
}
if (context.AuditInfo.BrowserInfo == null)
{
context.AuditInfo.BrowserInfo = GetBrowserInfo(httpContext);
context.AuditInfo.BrowserInfo = clientInfoProvider.BrowserInfo;
}
//TODO: context.AuditInfo.ClientName
@ -62,24 +64,6 @@ namespace Volo.Abp.AspNetCore.Auditing
}
}
protected virtual string GetBrowserInfo(HttpContext httpContext)
{
return httpContext.Request?.Headers?["User-Agent"];
}
protected virtual string GetClientIpAddress(HttpContext httpContext)
{
try
{
return httpContext.Connection?.RemoteIpAddress?.ToString();
}
catch (Exception ex)
{
Logger.LogException(ex, LogLevel.Warning);
return null;
}
}
protected virtual string BuildUrl(HttpContext httpContext)
{
//TODO: Add options to include/exclude query, schema and host

@ -0,0 +1,73 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.WebClientInfo;
using Volo.Abp.Clients;
using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;
using Volo.Abp.SecurityLog;
using Volo.Abp.Timing;
using Volo.Abp.Tracing;
using Volo.Abp.Users;
namespace Volo.Abp.AspNetCore.SecurityLog
{
[Dependency(ReplaceServices = true)]
public class AspNetCoreSecurityLogManager : DefaultSecurityLogManager
{
protected ILogger<AspNetCoreSecurityLogManager> Logger { get; }
protected IClock Clock { get; }
protected ICurrentUser CurrentUser { get; }
protected ICurrentTenant CurrentTenant { get; }
protected ICurrentClient CurrentClient { get; }
protected IHttpContextAccessor HttpContextAccessor { get; }
protected ICorrelationIdProvider CorrelationIdProvider { get; }
protected IWebClientInfoProvider WebClientInfoProvider { get; }
public AspNetCoreSecurityLogManager(
IOptions<AbpSecurityLogOptions> securityLogOptions,
ISecurityLogStore securityLogStore,
ILogger<AspNetCoreSecurityLogManager> logger,
IClock clock,
ICurrentUser currentUser,
ICurrentTenant currentTenant,
ICurrentClient currentClient,
IHttpContextAccessor httpContextAccessor,
ICorrelationIdProvider correlationIdProvider,
IWebClientInfoProvider webClientInfoProvider)
: base(securityLogOptions, securityLogStore)
{
Logger = logger;
Clock = clock;
CurrentUser = currentUser;
CurrentTenant = currentTenant;
CurrentClient = currentClient;
HttpContextAccessor = httpContextAccessor;
CorrelationIdProvider = correlationIdProvider;
WebClientInfoProvider = webClientInfoProvider;
}
protected override async Task<SecurityLogInfo> CreateAsync()
{
var securityLogInfo = await base.CreateAsync();
securityLogInfo.CreationTime = Clock.Now;
securityLogInfo.TenantId = CurrentTenant.Id;
securityLogInfo.TenantName = CurrentTenant.Name;
securityLogInfo.UserId = CurrentUser.Id;
securityLogInfo.UserName = CurrentUser.UserName;
securityLogInfo.ClientId = CurrentClient.Id;
securityLogInfo.CorrelationId = CorrelationIdProvider.Get();
securityLogInfo.ClientIpAddress = WebClientInfoProvider.ClientIpAddress;
securityLogInfo.BrowserInfo = WebClientInfoProvider.BrowserInfo;
return securityLogInfo;
}
}
}

@ -0,0 +1,43 @@
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.WebClientInfo
{
public class HttpContextWebClientInfoProvider : IWebClientInfoProvider, ITransientDependency
{
protected ILogger<HttpContextWebClientInfoProvider> Logger { get; }
protected IHttpContextAccessor HttpContextAccessor { get; }
public HttpContextWebClientInfoProvider(
ILogger<HttpContextWebClientInfoProvider> logger,
IHttpContextAccessor httpContextAccessor)
{
Logger = logger;
HttpContextAccessor = httpContextAccessor;
}
public string BrowserInfo => GetBrowserInfo();
public string ClientIpAddress => GetClientIpAddress();
protected virtual string GetBrowserInfo()
{
return HttpContextAccessor.HttpContext?.Request?.Headers?["User-Agent"];
}
protected virtual string GetClientIpAddress()
{
try
{
return HttpContextAccessor.HttpContext?.Connection?.RemoteIpAddress?.ToString();
}
catch (Exception ex)
{
Logger.LogException(ex, LogLevel.Warning);
return null;
}
}
}
}

@ -0,0 +1,9 @@
namespace Volo.Abp.AspNetCore.WebClientInfo
{
public interface IWebClientInfoProvider
{
string BrowserInfo { get; }
string ClientIpAddress { get; }
}
}

@ -1,11 +1,12 @@
using System;
using Volo.Abp.EventBus;
using Volo.Abp.MultiTenancy;
namespace Volo.Abp.Domain.Entities.Events.Distributed
{
[Serializable]
[GenericEventName(Postfix = ".Created")]
public class EntityCreatedEto<TEntityEto>
public class EntityCreatedEto<TEntityEto> : IEventDataMayHaveTenantId
{
public TEntityEto Entity { get; set; }
@ -13,5 +14,17 @@ namespace Volo.Abp.Domain.Entities.Events.Distributed
{
Entity = entity;
}
public virtual bool IsMultiTenant(out Guid? tenantId)
{
if (Entity is IMultiTenant multiTenantEntity)
{
tenantId = multiTenantEntity.TenantId;
return true;
}
tenantId = null;
return false;
}
}
}

@ -1,11 +1,12 @@
using System;
using Volo.Abp.EventBus;
using Volo.Abp.MultiTenancy;
namespace Volo.Abp.Domain.Entities.Events.Distributed
{
[Serializable]
[GenericEventName(Postfix = ".Deleted")]
public class EntityDeletedEto<TEntityEto>
public class EntityDeletedEto<TEntityEto> : IEventDataMayHaveTenantId
{
public TEntityEto Entity { get; set; }
@ -13,5 +14,17 @@ namespace Volo.Abp.Domain.Entities.Events.Distributed
{
Entity = entity;
}
public virtual bool IsMultiTenant(out Guid? tenantId)
{
if (Entity is IMultiTenant multiTenantEntity)
{
tenantId = multiTenantEntity.TenantId;
return true;
}
tenantId = null;
return false;
}
}
}
}

@ -1,11 +1,12 @@
using System;
using Volo.Abp.EventBus;
using Volo.Abp.MultiTenancy;
namespace Volo.Abp.Domain.Entities.Events.Distributed
{
[Serializable]
[GenericEventName(Postfix = ".Updated")]
public class EntityUpdatedEto<TEntityEto>
public class EntityUpdatedEto<TEntityEto> : IEventDataMayHaveTenantId
{
public TEntityEto Entity { get; set; }
@ -13,5 +14,17 @@ namespace Volo.Abp.Domain.Entities.Events.Distributed
{
Entity = entity;
}
public virtual bool IsMultiTenant(out Guid? tenantId)
{
if (Entity is IMultiTenant multiTenantEntity)
{
tenantId = multiTenantEntity.TenantId;
return true;
}
tenantId = null;
return false;
}
}
}
}

@ -1,5 +1,6 @@
using System;
using Volo.Abp.EventBus;
using Volo.Abp.MultiTenancy;
namespace Volo.Abp.Domain.Entities.Events
{
@ -8,7 +9,7 @@ namespace Volo.Abp.Domain.Entities.Events
/// </summary>
/// <typeparam name="TEntity">Entity type</typeparam>
[Serializable]
public class EntityEventData<TEntity> : IEventDataWithInheritableGenericArgument
public class EntityEventData<TEntity> : IEventDataWithInheritableGenericArgument, IEventDataMayHaveTenantId
{
/// <summary>
/// Related entity with this event.
@ -28,5 +29,17 @@ namespace Volo.Abp.Domain.Entities.Events
{
return new object[] { Entity };
}
public virtual bool IsMultiTenant(out Guid? tenantId)
{
if (Entity is IMultiTenant multiTenantEntity)
{
tenantId = multiTenantEntity.TenantId;
return true;
}
tenantId = null;
return false;
}
}
}
}

@ -11,6 +11,7 @@ using Microsoft.Extensions.Options;
using Volo.Abp.Domain.Entities;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.DependencyInjection;
using Volo.Abp.Guids;
namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
{
@ -29,9 +30,12 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
private readonly IDbContextProvider<TDbContext> _dbContextProvider;
private readonly Lazy<AbpEntityOptions<TEntity>> _entityOptionsLazy;
protected virtual IGuidGenerator GuidGenerator { get; set; }
public EfCoreRepository(IDbContextProvider<TDbContext> dbContextProvider)
{
_dbContextProvider = dbContextProvider;
GuidGenerator = SimpleGuidGenerator.Instance;
_entityOptionsLazy = new Lazy<AbpEntityOptions<TEntity>>(
() => ServiceProvider
@ -40,9 +44,11 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
.GetOrNull<TEntity>() ?? AbpEntityOptions<TEntity>.Empty
);
}
public override async Task<TEntity> InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
{
CheckAndSetId(entity);
var savedEntity = DbSet.Add(entity).Entity;
if (autoSave)
@ -66,7 +72,7 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
return updatedEntity;
}
public override async Task DeleteAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
{
DbSet.Remove(entity);
@ -110,7 +116,7 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
}
public override async Task<TEntity> FindAsync(
Expression<Func<TEntity, bool>> predicate,
Expression<Func<TEntity, bool>> predicate,
bool includeDetails = true,
CancellationToken cancellationToken = default)
{
@ -188,16 +194,38 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
return query;
}
protected virtual void CheckAndSetId(TEntity entity)
{
if (entity is IEntity<Guid> entityWithGuidId)
{
TrySetGuidId(entityWithGuidId);
}
}
protected virtual void TrySetGuidId(IEntity<Guid> entity)
{
if (entity.Id != default)
{
return;
}
EntityHelper.TrySetId(
entity,
() => GuidGenerator.Create(),
true
);
}
}
public class EfCoreRepository<TDbContext, TEntity, TKey> : EfCoreRepository<TDbContext, TEntity>,
public class EfCoreRepository<TDbContext, TEntity, TKey> : EfCoreRepository<TDbContext, TEntity>,
IEfCoreRepository<TEntity, TKey>,
ISupportsExplicitLoading<TEntity, TKey>
where TDbContext : IEfCoreDbContext
where TEntity : class, IEntity<TKey>
{
public EfCoreRepository(IDbContextProvider<TDbContext> dbContextProvider)
public EfCoreRepository(IDbContextProvider<TDbContext> dbContextProvider)
: base(dbContextProvider)
{

@ -0,0 +1,28 @@
using System;
namespace Volo.Abp.EventBus
{
/// <summary>
/// An event data object (or event transfer object) can implement this interface
/// to indicate that this event may be related to a tenant.
///
/// If an event data class is always related to a tenant, then directly implement the
/// <see cref="IsMultiTenant"/> interface instead of this one.
///
/// This interface is typically implemented by generic event handlers where the generic
/// parameter may implement <see cref="IsMultiTenant"/> or not.
/// </summary>
public interface IEventDataMayHaveTenantId
{
/// <summary>
/// Returns true if this event data has a Tenant Id information.
/// If so, it should set the <see cref="tenantId"/> our parameter.
/// Otherwise, the <see cref="tenantId"/> our parameter value should not be informative
/// (it will be null as expected, but doesn't indicate a tenant with null tenant id).
/// </summary>
/// <param name="tenantId">
/// The tenant id that is set if this method returns true.
/// </param>
bool IsMultiTenant(out Guid? tenantId);
}
}

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace Volo.Abp.Localization
@ -24,6 +25,11 @@ namespace Volo.Abp.Localization
: language;
}
public static string GetCurrentUICultureLanguagesMap(this AbpLocalizationOptions localizationOptions, string packageName)
{
return GetLanguagesMap(localizationOptions, packageName, CultureInfo.CurrentUICulture.Name);
}
public static AbpLocalizationOptions AddLanguageFilesMapOrUpdate(this AbpLocalizationOptions localizationOptions,
string packageName, params NameValue[] maps)
{
@ -43,6 +49,11 @@ namespace Volo.Abp.Localization
: language;
}
public static string GetCurrentUICultureLanguageFilesMap(this AbpLocalizationOptions localizationOptions, string packageName)
{
return GetLanguageFilesMap(localizationOptions, packageName, CultureInfo.CurrentUICulture.Name);
}
private static void AddOrUpdate(IDictionary<string, List<NameValue>> maps, string packageName, NameValue value)
{
if (maps.TryGetValue(packageName, out var existMaps))

@ -50,6 +50,7 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
LocalEventBus = NullLocalEventBus.Instance;
DistributedEventBus = NullDistributedEventBus.Instance;
EntityChangeEventHelper = NullEntityChangeEventHelper.Instance;
GuidGenerator = SimpleGuidGenerator.Instance;
}
public override async Task<TEntity> InsertAsync(

@ -0,0 +1,21 @@
namespace Volo.Abp.SecurityLog
{
public class AbpSecurityLogOptions
{
/// <summary>
/// Default: true.
/// </summary>
public bool IsEnabled { get; set; }
/// <summary>
/// The name of the application or service writing security log.
/// Default: null.
/// </summary>
public string ApplicationName { get; set; }
public AbpSecurityLogOptions()
{
IsEnabled = true;
}
}
}

@ -0,0 +1,42 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.SecurityLog
{
public class DefaultSecurityLogManager : ISecurityLogManager, ITransientDependency
{
protected AbpSecurityLogOptions SecurityLogOptions { get; }
protected ISecurityLogStore SecurityLogStore { get; }
public DefaultSecurityLogManager(
IOptions<AbpSecurityLogOptions> securityLogOptions,
ISecurityLogStore securityLogStore)
{
SecurityLogStore = securityLogStore;
SecurityLogOptions = securityLogOptions.Value;
}
public async Task SaveAsync(Action<SecurityLogInfo> saveAction = null)
{
if (!SecurityLogOptions.IsEnabled)
{
return;
}
var securityLogInfo = await CreateAsync();
saveAction?.Invoke(securityLogInfo);
await SecurityLogStore.SaveAsync(securityLogInfo);
}
protected virtual Task<SecurityLogInfo> CreateAsync()
{
return Task.FromResult(new SecurityLogInfo
{
ApplicationName = SecurityLogOptions.ApplicationName
});
}
}
}

@ -0,0 +1,10 @@
using System;
using System.Threading.Tasks;
namespace Volo.Abp.SecurityLog
{
public interface ISecurityLogManager
{
Task SaveAsync(Action<SecurityLogInfo> saveAction = null);
}
}

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Volo.Abp.SecurityLog
{
public interface ISecurityLogStore
{
Task SaveAsync(SecurityLogInfo securityLogInfo);
}
}

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
namespace Volo.Abp.SecurityLog
{
[Serializable]
public class SecurityLogInfo
{
public string ApplicationName { get; set; }
public string Identity { get; set; }
public string Action { get; set; }
public Dictionary<string, object> ExtraProperties { get; }
public Guid? UserId { get; set; }
public string UserName { get; set; }
public Guid? TenantId { get; set; }
public string TenantName { get; set; }
public string ClientId { get; set; }
public string CorrelationId { get; set; }
public string ClientIpAddress { get; set; }
public string BrowserInfo { get; set; }
public DateTime CreationTime { get; set; }
public SecurityLogInfo()
{
ExtraProperties = new Dictionary<string, object>();
}
public override string ToString()
{
return $"SECURITY LOG: [{ApplicationName} - {Identity} - {Action}]";
}
}
}

@ -0,0 +1,30 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.SecurityLog
{
public class SimpleSecurityLogStore : ISecurityLogStore, ITransientDependency
{
public ILogger<SimpleSecurityLogStore> Logger { get; set; }
protected AbpSecurityLogOptions SecurityLogOptions { get; }
public SimpleSecurityLogStore(ILogger<SimpleSecurityLogStore> logger, IOptions<AbpSecurityLogOptions> securityLogOptions)
{
Logger = logger;
SecurityLogOptions = securityLogOptions.Value;
}
public Task SaveAsync(SecurityLogInfo securityLogInfo)
{
if (!SecurityLogOptions.IsEnabled)
{
return Task.CompletedTask;
}
Logger.LogInformation(securityLogInfo.ToString());
return Task.CompletedTask;
}
}
}

@ -64,5 +64,15 @@ namespace Volo.Abp.MongoDB.Repositories
person.Phones.Count.ShouldBe(1);
person.Phones.Any(p => p.PersonId == person.Id && p.Number == "1234567890").ShouldBeTrue();
}
[Fact]
public async Task Insert_Should_Set_Guid_Id()
{
var person = new Person(Guid.Empty, "New Person", 35);
await PersonRepository.InsertAsync(person);
person.Id.ShouldNotBe(Guid.Empty);
}
}
}

@ -1,4 +1,5 @@
using Volo.Abp.Modularity;
using Volo.Abp.SecurityLog;
namespace Volo.Abp.Security
{
@ -8,6 +9,12 @@ namespace Volo.Abp.Security
)]
public class AbpSecurityTestModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpSecurityLogOptions>(x =>
{
x.ApplicationName = "AbpSecurityTest";
});
}
}
}

@ -0,0 +1,45 @@
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using Volo.Abp.SecurityLog;
using Volo.Abp.Testing;
using Xunit;
namespace Volo.Abp.Security.SecurityLog
{
public class SecurityLogManager_Tests : AbpIntegratedTest<AbpSecurityTestModule>
{
private readonly ISecurityLogManager _securityLogManager;
private ISecurityLogStore _auditingStore;
public SecurityLogManager_Tests()
{
_securityLogManager = GetRequiredService<ISecurityLogManager>();
}
protected override void AfterAddApplication(IServiceCollection services)
{
_auditingStore = Substitute.For<ISecurityLogStore>();
services.AddSingleton(_auditingStore);
}
[Fact]
public async Task SaveAsync()
{
await _securityLogManager.SaveAsync(securityLog =>
{
securityLog.Identity = "Test";
securityLog.Action = "Test-Action";
securityLog.UserName = "Test-User";
});
await _auditingStore.Received().SaveAsync(Arg.Is<SecurityLogInfo>(log =>
log.ApplicationName == "AbpSecurityTest" &&
log.Identity == "Test" &&
log.Action == "Test-Action" &&
log.UserName == "Test-User"));
}
}
}

@ -14,7 +14,10 @@ using System.Security.Principal;
using System.Threading.Tasks;
using Volo.Abp.Account.Settings;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Identity;
using Volo.Abp.Identity.AspNetCore;
using Volo.Abp.MultiTenancy;
using Volo.Abp.SecurityLog;
using Volo.Abp.Settings;
using Volo.Abp.Uow;
@ -129,6 +132,13 @@ namespace Volo.Abp.Account.Web.Pages.Account
true
);
await LocalEventBus.PublishAsync(new IdentitySecurityLogEvent
{
Identity = IdentitySecurityLogIdentityConsts.Identity,
Action = result.ToIdentitySecurityLogAction(),
UserName = LoginInput.UserNameOrEmailAddress
});
if (result.RequiresTwoFactor)
{
return RedirectToPage("./SendSecurityCode", new

@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Identity;
namespace Volo.Abp.Account.Web.Pages.Account
{
@ -19,6 +20,12 @@ namespace Volo.Abp.Account.Web.Pages.Account
public override async Task<IActionResult> OnGetAsync()
{
await LocalEventBus.PublishAsync(new IdentitySecurityLogEvent
{
Identity = IdentitySecurityLogIdentityConsts.Identity,
Action = IdentitySecurityLogActionConsts.Logout
});
await SignInManager.SignOutAsync();
var logoutId = Request.Query["logoutId"].ToString();

@ -6,7 +6,10 @@ using Volo.Abp.Account.Localization;
using Volo.Abp.Account.Settings;
using Volo.Abp.Account.Web.Areas.Account.Controllers.Models;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.EventBus.Local;
using Volo.Abp.Identity;
using Volo.Abp.Identity.AspNetCore;
using Volo.Abp.SecurityLog;
using Volo.Abp.Settings;
using Volo.Abp.Validation;
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
@ -26,13 +29,21 @@ namespace Volo.Abp.Account.Web.Areas.Account.Controllers
protected IdentityUserManager UserManager { get; }
protected ISettingProvider SettingProvider { get; }
public AccountController(SignInManager<IdentityUser> signInManager, IdentityUserManager userManager, ISettingProvider settingProvider)
protected ILocalEventBus LocalEventBus { get; }
public AccountController(
SignInManager<IdentityUser> signInManager,
IdentityUserManager userManager,
ISettingProvider settingProvider,
ISecurityLogManager securityLogManager,
ILocalEventBus localEventBus)
{
LocalizationResource = typeof(AccountResource);
SignInManager = signInManager;
UserManager = userManager;
SettingProvider = settingProvider;
LocalEventBus = localEventBus;
}
[HttpPost]
@ -44,19 +55,33 @@ namespace Volo.Abp.Account.Web.Areas.Account.Controllers
ValidateLoginInfo(login);
await ReplaceEmailToUsernameOfInputIfNeeds(login);
return GetAbpLoginResult(await SignInManager.PasswordSignInAsync(
var signInResult = await SignInManager.PasswordSignInAsync(
login.UserNameOrEmailAddress,
login.Password,
login.RememberMe,
true
));
);
await LocalEventBus.PublishAsync(new IdentitySecurityLogEvent
{
Identity = IdentitySecurityLogIdentityConsts.Identity,
Action = signInResult.ToIdentitySecurityLogAction(),
UserName = login.UserNameOrEmailAddress
});
return GetAbpLoginResult(signInResult);
}
[HttpGet]
[Route("logout")]
public virtual async Task Logout()
{
await LocalEventBus.PublishAsync(new IdentitySecurityLogEvent
{
Identity = IdentitySecurityLogIdentityConsts.Identity,
Action = IdentitySecurityLogActionConsts.Logout
});
await SignInManager.SignOutAsync();
}

@ -1,11 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Account.Localization;
using Volo.Abp.AspNetCore.Mvc.UI.RazorPages;
using Volo.Abp.EventBus.Local;
using Volo.Abp.Identity;
using IdentityUser = Volo.Abp.Identity.IdentityUser;
@ -15,6 +15,7 @@ namespace Volo.Abp.Account.Web.Pages.Account
{
public SignInManager<IdentityUser> SignInManager { get; set; }
public IdentityUserManager UserManager { get; set; }
public ILocalEventBus LocalEventBus { get; set; }
protected AccountPageModel()
{

@ -38,15 +38,12 @@
</label>
</div>
<abp-button type="submit" button-type="Primary" name="Action" value="Login" class="btn-block btn-lg mt-3">@L["Login"]</abp-button>
@if (Model.ShowCancelButton)
{
<abp-button type="submit" button-type="Secondary" formnovalidate="formnovalidate" name="Action" value="Cancel" class="btn-block btn-lg mt-3">@L["Cancel"]</abp-button>
}
</form>
</div>
@if (Model.ShowCancelButton)
{
<div class="card-footer text-center border-0">
<abp-button type="button" button-type="Link" name="Action" value="Cancel" class="px-2 py-0">@L["Cancel"]</abp-button>
</div>
}
</div>
}

@ -13,11 +13,12 @@ using System.Threading.Tasks;
using Volo.Abp.Account.Settings;
using Volo.Abp.Auditing;
using Volo.Abp.Identity;
using Volo.Abp.Identity.AspNetCore;
using Volo.Abp.Security.Claims;
using Volo.Abp.Settings;
using Volo.Abp.Uow;
using Volo.Abp.Validation;
using IdentityUser = Volo.Abp.Identity.IdentityUser;
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
namespace Volo.Abp.Account.Web.Pages.Account
{
@ -97,6 +98,13 @@ namespace Volo.Abp.Account.Web.Pages.Account
true
);
await LocalEventBus.PublishAsync(new IdentitySecurityLogEvent
{
Identity = IdentitySecurityLogIdentityConsts.Identity,
Action = result.ToIdentitySecurityLogAction(),
UserName = LoginInput.UserNameOrEmailAddress
});
if (result.RequiresTwoFactor)
{
return RedirectToPage("./SendSecurityCode", new
@ -184,6 +192,15 @@ namespace Volo.Abp.Account.Web.Pages.Account
bypassTwoFactor: true
);
if (!result.Succeeded)
{
await LocalEventBus.PublishAsync(new IdentitySecurityLogEvent
{
Identity = IdentitySecurityLogIdentityConsts.IdentityExternal,
Action = "Login" + result
});
}
if (result.IsLockedOut)
{
throw new UserFriendlyException("Cannot proceed because user is locked out!");
@ -206,6 +223,14 @@ namespace Volo.Abp.Account.Web.Pages.Account
var user = await CreateExternalUserAsync(info);
await SignInManager.SignInAsync(user, false);
await LocalEventBus.PublishAsync(new IdentitySecurityLogEvent
{
Identity = IdentitySecurityLogIdentityConsts.IdentityExternal,
Action = result.ToIdentitySecurityLogAction(),
UserName = user.Name
});
return RedirectSafely(returnUrl, returnUrlHash);
}

@ -1,5 +1,6 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Identity;
namespace Volo.Abp.Account.Web.Pages.Account
{
@ -15,6 +16,12 @@ namespace Volo.Abp.Account.Web.Pages.Account
public virtual async Task<IActionResult> OnGetAsync()
{
await LocalEventBus.PublishAsync(new IdentitySecurityLogEvent
{
Identity = IdentitySecurityLogIdentityConsts.Identity,
Action = IdentitySecurityLogActionConsts.Logout
});
await SignInManager.SignOutAsync();
if (ReturnUrl != null)
{

@ -1,6 +1,6 @@
@model Volo.CmsKit.Web.Pages.CmsKit.Shared.Components.ReactionSelection.ReactionSelectionViewModel
<span class="cms-reaction-selection" data-entity-type="@Model.EntityType" data-entity-id="@Model.EntityId">
<div class="cms-reaction-selection-available-reactions">
<div>
Pick a reaction:
@foreach (var reaction in Model.Reactions)
{
@ -9,7 +9,7 @@
</span>
}
</div>
<div class="cms-reaction-selection-reactions">
<div>
Current reactions:
@foreach (var reaction in Model.Reactions.Where(r => r.Count > 0))
{

@ -8,6 +8,7 @@ using Volo.CmsKit.Reactions;
namespace Volo.CmsKit.Web.Pages.CmsKit.Shared.Components.ReactionSelection
{
[ViewComponent(Name = "CmsReactionSelection")]
[Widget(
ScriptTypes = new[] {typeof(ReactionSelectionScriptBundleContributor)},
StyleTypes = new[] {typeof(ReactionSelectionStyleBundleContributor)}

@ -1,25 +1,46 @@
(function () {
(function ($) {
$(document).ready(function () {
function initReactionSelection() {
var $wrapper = $(this);
var $availableReactions = $wrapper.find('.cms-reaction-selection-available-reactions');
abp.widgets.CmsReactionSelection = function ($widget) {
$wrapper.find('.cms-reaction-icon').each(function () {
var $icon = $(this);
$icon.click(function () {
var methodName = $icon.hasClass('cms-reaction-icon-selected') ? 'delete' : 'create';
volo.cmsKit.reactions.reactionPublic[methodName]({
entityType: $wrapper.attr('data-entity-type'),
entityId: $wrapper.attr('data-entity-id'),
reactionName: $icon.attr('data-name')
}).then(function () {
location.reload(); //TODO: JUST TESTING !!!!!!!!
function getFilters() {
return {};
}
function refresh(filters) {
location.reload(); //TODO: JUST TESTING !!!!!!!!
}
function init(filters) {
var $wrapper = $widget.find('.cms-reaction-selection');
$widget.find('.cms-reaction-icon').each(function () {
var $icon = $(this);
$icon.click(function () {
var methodName = $icon.hasClass('cms-reaction-icon-selected') ? 'delete' : 'create';
volo.cmsKit.reactions.reactionPublic[methodName]({
entityType: $wrapper.attr('data-entity-type'),
entityId: $wrapper.attr('data-entity-id'),
reactionName: $icon.attr('data-name')
}).then(function () {
refresh();
});
});
});
}
return {
init: init,
refresh: refresh,
getFilters : getFilters
};
};
$('.abp-widget-wrapper[data-widget-name="CmsReactionSelection"]').each(function () {
var widgetManager = new abp.WidgetManager({
wrapper: $(this),
});
}
$('.cms-reaction-selection').each(initReactionSelection);
widgetManager.init();
});
});
})();
})(jQuery);

@ -0,0 +1,37 @@
using Microsoft.AspNetCore.Identity;
namespace Volo.Abp.Identity.AspNetCore
{
public static class SignInResultExtensions
{
public static string ToIdentitySecurityLogAction(this SignInResult result)
{
if (result.Succeeded)
{
return IdentitySecurityLogActionConsts.LoginSucceeded;
}
if (result.IsLockedOut)
{
return IdentitySecurityLogActionConsts.LoginLockedout;
}
if (result.RequiresTwoFactor)
{
return IdentitySecurityLogActionConsts.LoginRequiresTwoFactor;
}
if (result.IsNotAllowed)
{
return IdentitySecurityLogActionConsts.LoginNotAllowed;
}
if (!result.Succeeded)
{
return IdentitySecurityLogActionConsts.LoginFailed;
}
return IdentitySecurityLogActionConsts.LoginFailed;
}
}
}

@ -0,0 +1,33 @@
namespace Volo.Abp.Identity
{
public class IdentitySecurityLogActionConsts
{
public static string LoginSucceeded { get; set; } = "LoginSucceeded";
public static string LoginLockedout { get; set; } = "LoginLockedout";
public static string LoginNotAllowed { get; set; } = "LoginNotAllowed";
public static string LoginRequiresTwoFactor { get; set; } = "LoginRequiresTwoFactor";
public static string LoginFailed { get; set; } = "LoginFailed";
public static string LoginInvalidUserName { get; set; } = "LoginInvalidUserName";
public static string LoginInvalidUserNameOrPassword { get; set; } = "LoginInvalidUserNameOrPassword";
public static string Logout { get; set; } = "Logout";
public static string ChangeUserName { get; set; } = "ChangeUserName";
public static string ChangeEmail { get; set; } = "ChangeEmail";
public static string ChangePhoneNumber { get; set; } = "ChangePhoneNumber";
public static string ChangePassword { get; set; } = "ChangePassword";
public static string TwoFactorEnabled { get; set; } = "TwoFactorEnabled";
public static string TwoFactorDisabled { get; set; } = "TwoFactorDisabled";
}
}

@ -0,0 +1,52 @@
namespace Volo.Abp.Identity
{
public class IdentitySecurityLogConsts
{
/// <summary>
/// Default value: 96
/// </summary>
public static int MaxApplicationNameLength { get; set; } = 96;
/// <summary>
/// Default value: 96
/// </summary>
public static int MaxIdentityLength { get; set; } = 96;
/// <summary>
/// Default value: 96
/// </summary>
public static int MaxActionLength { get; set; } = 96;
/// <summary>
/// Default value: 256
/// </summary>
public static int MaxUserNameLength { get; set; } = 256;
/// <summary>
/// Default value: 64
/// </summary>
public static int MaxTenantNameLength { get; set; } = 64;
/// <summary>
/// Default value: 64
/// </summary>
public static int MaxClientIpAddressLength { get; set; } = 64;
/// <summary>
/// Default value: 64
/// </summary>
public static int MaxClientIdLength { get; set; } = 64;
/// <summary>
/// Default value: 64
/// </summary>
public static int MaxCorrelationIdLength { get; set; } = 64;
/// <summary>
/// Default value: 512
/// </summary>
public static int MaxBrowserInfoLength { get; set; } = 512;
}
}

@ -0,0 +1,11 @@
namespace Volo.Abp.Identity
{
public static class IdentitySecurityLogIdentityConsts
{
public static string Identity { get; set; } = "Identity";
public static string IdentityExternal { get; set; } = "IdentityExternal";
public static string IdentityTwoFactor { get; set; } = "IdentityTwoFactor";
}
}

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Globalization;
using Microsoft.Extensions.Localization;
using Volo.Abp.Identity;
using Volo.Abp.Localization;
using Volo.Abp.Text.Formatting;
namespace Microsoft.AspNetCore.Identity
@ -48,12 +49,15 @@ namespace Microsoft.AspNetCore.Identity
if (!localizedString.ResourceNotFound)
{
var englishLocalizedString = localizer.WithCulture(CultureInfo.GetCultureInfo("en"))[key];
if (!englishLocalizedString.ResourceNotFound)
using (CultureHelper.Use(CultureInfo.GetCultureInfo("en")))
{
if (FormattedStringValueExtracter.IsMatch(error.Description, englishLocalizedString.Value, out var values))
var englishLocalizedString = localizer[key];
if (!englishLocalizedString.ResourceNotFound)
{
return string.Format(localizedString.Value, values.Cast<object>().ToArray());
if (FormattedStringValueExtracter.IsMatch(error.Description, englishLocalizedString.Value, out var values))
{
return string.Format(localizedString.Value, values.Cast<object>().ToArray());
}
}
}
}

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;
namespace Volo.Abp.Identity
{
public interface IIdentitySecurityLogRepository : IBasicRepository<IdentitySecurityLog, Guid>
{
Task<List<IdentitySecurityLog>> GetListAsync(
string sorting = null,
int maxResultCount = 50,
int skipCount = 0,
DateTime? startTime = null,
DateTime? endTime = null,
string applicationName = null,
string identity = null,
string action = null,
Guid? userId = null,
string userName = null,
string clientId = null,
string correlationId = null,
bool includeDetails = false,
CancellationToken cancellationToken = default);
Task<long> GetCountAsync(
DateTime? startTime = null,
DateTime? endTime = null,
string applicationName = null,
string identity = null,
string action = null,
Guid? userId = null,
string userName = null,
string clientId = null,
string correlationId = null,
CancellationToken cancellationToken = default);
Task<IdentitySecurityLog> GetByUserIdAsync(
Guid id,
Guid userId,
bool includeDetails = false,
CancellationToken cancellationToken = default);
}
}

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy;
using Volo.Abp.SecurityLog;
namespace Volo.Abp.Identity
{
public class IdentitySecurityLog : AggregateRoot<Guid>, IMultiTenant
{
public Guid? TenantId { get; protected set; }
public string ApplicationName { get; protected set; }
public string Identity { get; protected set; }
public string Action { get; protected set; }
public Guid? UserId { get; protected set; }
public string UserName { get; protected set; }
public string TenantName { get; protected set; }
public string ClientId { get; protected set; }
public string CorrelationId { get; protected set; }
public string ClientIpAddress { get; protected set; }
public string BrowserInfo { get; protected set; }
public DateTime CreationTime { get; protected set; }
protected IdentitySecurityLog()
{
ExtraProperties = new Dictionary<string, object>();
}
public IdentitySecurityLog(IGuidGenerator guidGenerator, SecurityLogInfo securityLogInfo)
{
Id = guidGenerator.Create();
TenantId = securityLogInfo.TenantId;
TenantName = securityLogInfo.TenantName.Truncate(IdentitySecurityLogConsts.MaxTenantNameLength);
ApplicationName = securityLogInfo.ApplicationName.Truncate(IdentitySecurityLogConsts.MaxApplicationNameLength);
Identity = securityLogInfo.Identity.Truncate(IdentitySecurityLogConsts.MaxIdentityLength);
Action = securityLogInfo.Action.Truncate(IdentitySecurityLogConsts.MaxActionLength);
UserId = securityLogInfo.UserId;
UserName = securityLogInfo.UserName.Truncate(IdentitySecurityLogConsts.MaxUserNameLength);
CreationTime = securityLogInfo.CreationTime;
ClientIpAddress = securityLogInfo.ClientIpAddress.Truncate(IdentitySecurityLogConsts.MaxClientIpAddressLength);
ClientId = securityLogInfo.ClientId.Truncate(IdentitySecurityLogConsts.MaxClientIdLength);
CorrelationId = securityLogInfo.CorrelationId.Truncate(IdentitySecurityLogConsts.MaxCorrelationIdLength);
BrowserInfo = securityLogInfo.BrowserInfo.Truncate(IdentitySecurityLogConsts.MaxBrowserInfoLength);
ExtraProperties = securityLogInfo.ExtraProperties;
}
}
}

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using Volo.Abp.MultiTenancy;
namespace Volo.Abp.Identity
{
public class IdentitySecurityLogEvent : IMultiTenant
{
public Guid? TenantId { get; set; }
public string Identity { get; set; }
public string Action { get; set; }
public string UserName { get; set; }
public string ClientId { get; set; }
public Dictionary<string, object> ExtraProperties { get; }
public IdentitySecurityLogEvent()
{
ExtraProperties = new Dictionary<string, object>();
}
public virtual IdentitySecurityLogEvent WithProperty(string key, object value)
{
ExtraProperties[key] = value;
return this;
}
}
}

@ -0,0 +1,86 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus;
using Volo.Abp.Security.Claims;
using Volo.Abp.SecurityLog;
using Volo.Abp.Uow;
using Volo.Abp.Users;
namespace Volo.Abp.Identity
{
public class IdentitySecurityLogHandler : ILocalEventHandler<IdentitySecurityLogEvent>, ITransientDependency
{
protected ISecurityLogManager SecurityLogManager { get; }
protected IdentityUserManager UserManager { get; }
protected ICurrentPrincipalAccessor CurrentPrincipalAccessor { get; }
protected IUserClaimsPrincipalFactory<IdentityUser> UserClaimsPrincipalFactory { get; }
protected ICurrentUser CurrentUser { get; }
public IdentitySecurityLogHandler(
ISecurityLogManager securityLogManager,
IdentityUserManager userManager,
ICurrentPrincipalAccessor currentPrincipalAccessor,
IUserClaimsPrincipalFactory<IdentityUser> userClaimsPrincipalFactory,
ICurrentUser currentUser)
{
SecurityLogManager = securityLogManager;
UserManager = userManager;
CurrentPrincipalAccessor = currentPrincipalAccessor;
UserClaimsPrincipalFactory = userClaimsPrincipalFactory;
CurrentUser = currentUser;
}
public async Task HandleEventAsync(IdentitySecurityLogEvent eventData)
{
Action<SecurityLogInfo> securityLogAction = securityLog =>
{
securityLog.Identity = eventData.Identity;
securityLog.Action = eventData.Action;
if (securityLog.UserName.IsNullOrWhiteSpace())
{
securityLog.UserName = eventData.UserName;
}
if (securityLog.ClientId.IsNullOrWhiteSpace())
{
securityLog.ClientId = eventData.ClientId;
}
foreach (var property in eventData.ExtraProperties)
{
securityLog.ExtraProperties[property.Key] = property.Value;
}
};
if (CurrentUser.IsAuthenticated)
{
await SecurityLogManager.SaveAsync(securityLogAction);
}
else
{
if (eventData.UserName.IsNullOrWhiteSpace())
{
await SecurityLogManager.SaveAsync(securityLogAction);
}
else
{
var user = await UserManager.FindByNameAsync(eventData.UserName);
if (user != null)
{
using (CurrentPrincipalAccessor.Change(await UserClaimsPrincipalFactory.CreateAsync(user)))
{
await SecurityLogManager.SaveAsync(securityLogAction);
}
}
else
{
await SecurityLogManager.SaveAsync(securityLogAction);
}
}
}
}
}
}

@ -0,0 +1,49 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Guids;
using Volo.Abp.SecurityLog;
using Volo.Abp.Uow;
namespace Volo.Abp.Identity
{
[Dependency(ReplaceServices = true)]
public class IdentitySecurityLogStore : ISecurityLogStore, ITransientDependency
{
public ILogger<IdentitySecurityLogStore> Logger { get; set; }
protected AbpSecurityLogOptions SecurityLogOptions { get; }
protected IIdentitySecurityLogRepository IdentitySecurityLogRepository { get; }
protected IGuidGenerator GuidGenerator { get; }
protected IUnitOfWorkManager UnitOfWorkManager { get; }
public IdentitySecurityLogStore(
ILogger<IdentitySecurityLogStore> logger,
IOptions<AbpSecurityLogOptions> securityLogOptions,
IIdentitySecurityLogRepository identitySecurityLogRepository,
IGuidGenerator guidGenerator,
IUnitOfWorkManager unitOfWorkManager)
{
Logger = logger;
SecurityLogOptions = securityLogOptions.Value;
IdentitySecurityLogRepository = identitySecurityLogRepository;
GuidGenerator = guidGenerator;
UnitOfWorkManager = unitOfWorkManager;
}
public async Task SaveAsync(SecurityLogInfo securityLogInfo)
{
if (!SecurityLogOptions.IsEnabled)
{
return;
}
using (var uow = UnitOfWorkManager.Begin(requiresNew: true))
{
await IdentitySecurityLogRepository.InsertAsync(new IdentitySecurityLog(GuidGenerator, securityLogInfo));
await uow.CompleteAsync();
}
}
}
}

@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
namespace Volo.Abp.Identity.EntityFrameworkCore
{
public class EFCoreIdentitySecurityLogRepository : EfCoreRepository<IIdentityDbContext, IdentitySecurityLog, Guid>, IIdentitySecurityLogRepository
{
public EFCoreIdentitySecurityLogRepository(IDbContextProvider<IIdentityDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
public async Task<List<IdentitySecurityLog>> GetListAsync(
string sorting = null,
int maxResultCount = 50,
int skipCount = 0,
DateTime? startTime = null,
DateTime? endTime = null,
string applicationName = null,
string identity = null,
string action = null,
Guid? userId = null,
string userName = null,
string clientId = null,
string correlationId = null,
bool includeDetails = false,
CancellationToken cancellationToken = default)
{
var query = GetListQuery(
startTime,
endTime,
applicationName,
identity,
action,
userId,
userName,
clientId,
correlationId
);
return await query.OrderBy(sorting ?? nameof(IdentitySecurityLog.CreationTime) + " desc")
.PageBy(skipCount, maxResultCount)
.ToListAsync(GetCancellationToken(cancellationToken));
}
public async Task<long> GetCountAsync(
DateTime? startTime = null,
DateTime? endTime = null,
string applicationName = null,
string identity = null,
string action = null,
Guid? userId = null,
string userName = null,
string clientId = null,
string correlationId = null,
CancellationToken cancellationToken = default)
{
var query = GetListQuery(
startTime,
endTime,
applicationName,
identity,
action,
userId,
userName,
clientId,
correlationId
);
return await query.LongCountAsync(GetCancellationToken(cancellationToken));
}
public async Task<IdentitySecurityLog> GetByUserIdAsync(Guid id, Guid userId, bool includeDetails = false, CancellationToken cancellationToken = default)
{
return await DbSet.FirstOrDefaultAsync(x => x.Id == id && x.UserId == userId, GetCancellationToken(cancellationToken));
}
protected virtual IQueryable<IdentitySecurityLog> GetListQuery(
DateTime? startTime = null,
DateTime? endTime = null,
string applicationName = null,
string identity = null,
string action = null,
Guid? userId = null,
string userName = null,
string clientId = null,
string correlationId = null)
{
return DbSet.AsNoTracking()
.WhereIf(startTime.HasValue, securityLog => securityLog.CreationTime >= startTime)
.WhereIf(endTime.HasValue, securityLog => securityLog.CreationTime >= endTime)
.WhereIf(!applicationName.IsNullOrWhiteSpace(), securityLog => securityLog.ApplicationName == applicationName)
.WhereIf(!identity.IsNullOrWhiteSpace(), securityLog => securityLog.Identity == identity)
.WhereIf(!action.IsNullOrWhiteSpace(), securityLog => securityLog.Action == action)
.WhereIf(userId.HasValue, securityLog => securityLog.UserId == userId)
.WhereIf(!userName.IsNullOrWhiteSpace(), securityLog => securityLog.UserName == userName)
.WhereIf(!clientId.IsNullOrWhiteSpace(), securityLog => securityLog.ClientId == clientId)
.WhereIf(!correlationId.IsNullOrWhiteSpace(), securityLog => securityLog.CorrelationId == correlationId);
}
}
}

@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Data;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.SecurityLog;
namespace Volo.Abp.Identity.EntityFrameworkCore
{
@ -14,5 +15,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore
DbSet<IdentityClaimType> ClaimTypes { get; set; }
DbSet<OrganizationUnit> OrganizationUnits { get; set; }
DbSet<IdentitySecurityLog> IdentitySecurityLogs { get; set; }
}
}

@ -18,6 +18,8 @@ namespace Volo.Abp.Identity.EntityFrameworkCore
public DbSet<OrganizationUnit> OrganizationUnits { get; set; }
public DbSet<IdentitySecurityLog> IdentitySecurityLogs { get; set; }
public IdentityDbContext(DbContextOptions<IdentityDbContext> options)
: base(options)
{
@ -31,4 +33,4 @@ namespace Volo.Abp.Identity.EntityFrameworkCore
builder.ConfigureIdentity();
}
}
}
}

@ -204,6 +204,32 @@ namespace Volo.Abp.Identity.EntityFrameworkCore
b.HasIndex(ou => new {ou.UserId, ou.OrganizationUnitId});
});
builder.Entity<IdentitySecurityLog>(b =>
{
b.ToTable(options.TablePrefix + "SecurityLogs", options.Schema);
b.ConfigureByConvention();
b.Property(x => x.TenantName).HasMaxLength(IdentitySecurityLogConsts.MaxTenantNameLength);
b.Property(x => x.ApplicationName).HasMaxLength(IdentitySecurityLogConsts.MaxApplicationNameLength);
b.Property(x => x.Identity).HasMaxLength(IdentitySecurityLogConsts.MaxIdentityLength);
b.Property(x => x.Action).HasMaxLength(IdentitySecurityLogConsts.MaxActionLength);
b.Property(x => x.UserName).HasMaxLength(IdentitySecurityLogConsts.MaxUserNameLength);
b.Property(x => x.ClientIpAddress).HasMaxLength(IdentitySecurityLogConsts.MaxClientIpAddressLength);
b.Property(x => x.ClientId).HasMaxLength(IdentitySecurityLogConsts.MaxClientIdLength);
b.Property(x => x.CorrelationId).HasMaxLength(IdentitySecurityLogConsts.MaxCorrelationIdLength);
b.Property(x => x.BrowserInfo).HasMaxLength(IdentitySecurityLogConsts.MaxBrowserInfoLength);
b.HasIndex(x => new { x.TenantId, x.ApplicationName });
b.HasIndex(x => new { x.TenantId, x.Identity });
b.HasIndex(x => new { x.TenantId, x.Action });
b.HasIndex(x => new { x.TenantId, x.UserId });
});
}
}
}
}

@ -15,6 +15,8 @@ namespace Volo.Abp.Identity.MongoDB
public IMongoCollection<OrganizationUnit> OrganizationUnits => Collection<OrganizationUnit>();
public IMongoCollection<IdentitySecurityLog> IdentitySecurityLogs => Collection<IdentitySecurityLog>();
protected override void CreateModel(IMongoModelBuilder modelBuilder)
{
base.CreateModel(modelBuilder);
@ -22,4 +24,4 @@ namespace Volo.Abp.Identity.MongoDB
modelBuilder.ConfigureIdentity();
}
}
}
}

@ -36,6 +36,11 @@ namespace Volo.Abp.Identity.MongoDB
{
b.CollectionName = options.CollectionPrefix + "OrganizationUnits";
});
builder.Entity<IdentitySecurityLog>(b =>
{
b.CollectionName = options.CollectionPrefix + "SecurityLogs";
});
}
}
}
}

@ -18,6 +18,7 @@ namespace Volo.Abp.Identity.MongoDB
options.AddRepository<IdentityRole, MongoIdentityRoleRepository>();
options.AddRepository<IdentityClaimType, MongoIdentityRoleRepository>();
options.AddRepository<OrganizationUnit, MongoIdentityRoleRepository>();
options.AddRepository<IdentitySecurityLog, MongoIdentitySecurityLogRepository>();
});
}
}

@ -14,5 +14,7 @@ namespace Volo.Abp.Identity.MongoDB
IMongoCollection<IdentityClaimType> ClaimTypes { get; }
IMongoCollection<OrganizationUnit> OrganizationUnits { get; }
IMongoCollection<IdentitySecurityLog> IdentitySecurityLogs { get; }
}
}
}

@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using Volo.Abp.Domain.Repositories.MongoDB;
using Volo.Abp.MongoDB;
namespace Volo.Abp.Identity.MongoDB
{
public class MongoIdentitySecurityLogRepository :
MongoDbRepository<IAbpIdentityMongoDbContext, IdentitySecurityLog, Guid>, IIdentitySecurityLogRepository
{
public MongoIdentitySecurityLogRepository(IMongoDbContextProvider<IAbpIdentityMongoDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
public async Task<List<IdentitySecurityLog>> GetListAsync(
string sorting = null,
int maxResultCount = 50,
int skipCount = 0,
DateTime? startTime = null,
DateTime? endTime = null,
string applicationName = null,
string identity = null,
string action = null,
Guid? userId = null,
string userName = null,
string clientId = null,
string correlationId = null,
bool includeDetails = false,
CancellationToken cancellationToken = default)
{
var query = GetListQuery(
startTime,
endTime,
applicationName,
identity,
action,
userId,
userName,
clientId,
correlationId
);
return await query.OrderBy(sorting ?? nameof(IdentitySecurityLog.CreationTime) + " desc")
.As<IMongoQueryable<IdentitySecurityLog>>()
.PageBy<IdentitySecurityLog, IMongoQueryable<IdentitySecurityLog>>(skipCount, maxResultCount)
.ToListAsync(GetCancellationToken(cancellationToken));
}
public async Task<long> GetCountAsync(
DateTime? startTime = null,
DateTime? endTime = null,
string applicationName = null,
string identity = null,
string action = null,
Guid? userId = null,
string userName = null,
string clientId = null,
string correlationId = null,
CancellationToken cancellationToken = default)
{
var query = GetListQuery(
startTime,
endTime,
applicationName,
identity,
action,
userId,
userName,
clientId,
correlationId
);
return await query.As<IMongoQueryable<IdentitySecurityLog>>()
.LongCountAsync(GetCancellationToken(cancellationToken));
}
public async Task<IdentitySecurityLog> GetByUserIdAsync(Guid id, Guid userId, bool includeDetails = false,
CancellationToken cancellationToken = default)
{
return await GetMongoQueryable().FirstOrDefaultAsync(x => x.Id == id && x.UserId == userId,
GetCancellationToken(cancellationToken));
}
protected virtual IQueryable<IdentitySecurityLog> GetListQuery(
DateTime? startTime = null,
DateTime? endTime = null,
string applicationName = null,
string identity = null,
string action = null,
Guid? userId = null,
string userName = null,
string clientId = null,
string correlationId = null)
{
return GetMongoQueryable()
.WhereIf(startTime.HasValue, securityLog => securityLog.CreationTime >= startTime)
.WhereIf(endTime.HasValue, securityLog => securityLog.CreationTime >= endTime)
.WhereIf(!applicationName.IsNullOrWhiteSpace(),
securityLog => securityLog.ApplicationName == applicationName)
.WhereIf(!identity.IsNullOrWhiteSpace(), securityLog => securityLog.Identity == identity)
.WhereIf(!action.IsNullOrWhiteSpace(), securityLog => securityLog.Action == action)
.WhereIf(userId.HasValue, securityLog => securityLog.UserId == userId)
.WhereIf(!userName.IsNullOrWhiteSpace(), securityLog => securityLog.UserName == userName)
.WhereIf(!clientId.IsNullOrWhiteSpace(), securityLog => securityLog.ClientId == clientId)
.WhereIf(!correlationId.IsNullOrWhiteSpace(),
securityLog => securityLog.CorrelationId == correlationId);
}
}
}

@ -0,0 +1,7 @@
namespace Volo.Abp.Identity.EntityFrameworkCore
{
public class IdentitySecurityLogRepository_Tests : IdentitySecurityLogRepository_Tests<AbpIdentityEntityFrameworkCoreTestModule>
{
}
}

@ -0,0 +1,10 @@
using Xunit;
namespace Volo.Abp.Identity.MongoDB
{
[Collection(MongoTestCollection.Name)]
public class IdentitySecurityLogRepository_Tests : IdentitySecurityLogRepository_Tests<AbpIdentityMongoDbTestModule>
{
}
}

@ -1,9 +1,10 @@
using System;
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Guids;
using Volo.Abp.SecurityLog;
namespace Volo.Abp.Identity
{
@ -14,6 +15,7 @@ namespace Volo.Abp.Identity
private readonly IIdentityClaimTypeRepository _identityClaimTypeRepository;
private readonly IIdentityRoleRepository _roleRepository;
private readonly IOrganizationUnitRepository _organizationUnitRepository;
private readonly IIdentitySecurityLogRepository _identitySecurityLogRepository;
private readonly ILookupNormalizer _lookupNormalizer;
private readonly IdentityTestData _testData;
private readonly OrganizationUnitManager _organizationUnitManager;
@ -31,6 +33,7 @@ namespace Volo.Abp.Identity
IIdentityClaimTypeRepository identityClaimTypeRepository,
IIdentityRoleRepository roleRepository,
IOrganizationUnitRepository organizationUnitRepository,
IIdentitySecurityLogRepository identitySecurityLogRepository,
ILookupNormalizer lookupNormalizer,
IdentityTestData testData,
OrganizationUnitManager organizationUnitManager)
@ -43,6 +46,7 @@ namespace Volo.Abp.Identity
_testData = testData;
_organizationUnitRepository = organizationUnitRepository;
_organizationUnitManager = organizationUnitManager;
_identitySecurityLogRepository = identitySecurityLogRepository;
}
public async Task Build()
@ -51,6 +55,7 @@ namespace Volo.Abp.Identity
await AddOrganizationUnits();
await AddUsers();
await AddClaimTypes();
await AddSecurityLogs();
}
private async Task AddRoles()
@ -69,7 +74,7 @@ namespace Volo.Abp.Identity
}
/* Creates OU tree as shown below:
*
*
* - OU1
* - OU11
* - OU111
@ -138,5 +143,30 @@ namespace Volo.Abp.Identity
var ou = await _organizationUnitRepository.InsertAsync(new OrganizationUnit(_guidGenerator.Create(), displayName, parentId) { Code = code });
return ou;
}
private async Task AddSecurityLogs()
{
await _identitySecurityLogRepository.InsertAsync(new IdentitySecurityLog(_guidGenerator, new SecurityLogInfo
{
ApplicationName = "Test-ApplicationName",
Identity = "Test-Identity",
Action = "Test-Action",
UserId = _testData.UserJohnId,
UserName = "john.nash",
CreationTime = new DateTime(2020, 01, 01, 10, 0, 0)
}));
await _identitySecurityLogRepository.InsertAsync(new IdentitySecurityLog(_guidGenerator, new SecurityLogInfo
{
ApplicationName = "Test-ApplicationName",
Identity = "Test-Identity",
Action = "Test-Action",
UserId = _testData.UserDavidId,
UserName = "david",
CreationTime = new DateTime(2020, 01, 02, 10, 0, 0)
}));
}
}
}
}

@ -0,0 +1,36 @@
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.Modularity;
using Xunit;
namespace Volo.Abp.Identity
{
public abstract class IdentitySecurityLogRepository_Tests<TStartupModule> : AbpIdentityTestBase<TStartupModule>
where TStartupModule : IAbpModule
{
protected IIdentitySecurityLogRepository RoleRepository { get; }
protected IdentityTestData TestData { get; }
protected IdentitySecurityLogRepository_Tests()
{
RoleRepository = GetRequiredService<IIdentitySecurityLogRepository>();
TestData = GetRequiredService<IdentityTestData>();
}
[Fact]
public async Task GetListAsync()
{
var logs = await RoleRepository.GetListAsync();
logs.ShouldNotBeEmpty();
logs.ShouldContain(x => x.ApplicationName == "Test-ApplicationName" && x.UserId == TestData.UserJohnId);
logs.ShouldContain(x => x.ApplicationName == "Test-ApplicationName" && x.UserId == TestData.UserDavidId);
}
[Fact]
public async Task GetCountAsync()
{
var count = await RoleRepository.GetCountAsync();
count.ShouldBe(2);
}
}
}

@ -96,8 +96,5 @@
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"resolutions": {
"@ngx-validate/core": "^0.0.8"
}
}

@ -15,7 +15,7 @@ describe('AccountService', () => {
beforeEach(() => (spectator = createHttp()));
it('should send a GET to find tenant', () => {
spectator.get(Store).selectSnapshot.andReturn('https://abp.io');
spectator.inject(Store).selectSnapshot.andReturn('https://abp.io');
spectator.service.findTenant('test').subscribe();
spectator.expectOne(
'https://abp.io/api/abp/multi-tenancy/tenants/by-name/test',
@ -30,7 +30,7 @@ describe('AccountService', () => {
password: 'test1234',
appName: 'Angular',
} as RegisterRequest;
spectator.get(Store).selectSnapshot.andReturn('https://abp.io');
spectator.inject(Store).selectSnapshot.andReturn('https://abp.io');
spectator.service.register(mock).subscribe();
const req = spectator.expectOne('https://abp.io/api/account/register', HttpMethod.POST);
expect(req.request.body).toEqual(mock);

@ -1,4 +1,6 @@
<nz-tree
[nzBeforeDrop]="beforeDrop"
[nzDraggable]="draggable"
[nzCheckStrictly]="checkStrictly"
[nzCheckable]="checkable"
[nzCheckedKeys]="checkedKeys"
@ -7,6 +9,7 @@
[nzExpandedKeys]="expandedKeys"
(nzExpandChange)="onExpandedKeysChange($event)"
(nzCheckBoxChange)="onCheckboxChange($event)"
(nzOnDrop)="onDrop($event)"
></nz-tree>
<ng-template #treeTemplate let-node>
<div

@ -7,6 +7,10 @@ import {
TemplateRef,
ViewEncapsulation,
} from '@angular/core';
import { NzFormatEmitEvent, NzFormatBeforeDropEvent } from 'ng-zorro-antd/tree';
import { of } from 'rxjs';
export type DropEvent = NzFormatEmitEvent & { pos: number };
@Component({
selector: 'abp-tree',
@ -18,10 +22,14 @@ import {
encapsulation: ViewEncapsulation.None,
})
export class TreeComponent {
dropPosition: number;
@ContentChild('menu') menu: TemplateRef<any>;
@Output() readonly checkedKeysChange = new EventEmitter();
@Output() readonly expandedKeysChange = new EventEmitter<string[]>();
@Output() readonly selectedNodeChange = new EventEmitter();
@Output() readonly dropOver = new EventEmitter<DropEvent>();
@Input() draggable: boolean;
@Input() checkable: boolean;
@Input() checkStrictly: boolean;
@Input() checkedKeys = [];
@ -29,6 +37,10 @@ export class TreeComponent {
@Input() expandedKeys: string[] = [];
@Input() selectedNode: any;
@Input() isNodeSelected = node => this.selectedNode?.id === node.key;
@Input() beforeDrop = (event: NzFormatBeforeDropEvent) => {
this.dropPosition = event.pos;
return of(false);
};
onSelectedNodeChange(node) {
this.selectedNode = node.origin.entity;
@ -44,4 +56,12 @@ export class TreeComponent {
this.expandedKeys = [...event.keys];
this.expandedKeysChange.emit(event.keys);
}
onDrop(event: DropEvent) {
event.event.stopPropagation();
event.event.preventDefault();
event.pos = this.dropPosition;
this.dropOver.emit(event);
}
}

@ -21,8 +21,8 @@ describe('ApiInterceptor', () => {
beforeEach(() => {
spectator = createService();
interceptor = spectator.service;
store = spectator.get(Store);
oauthService = spectator.get(OAuthService);
store = spectator.inject(Store);
oauthService = spectator.inject(OAuthService);
});
it('should add headers to http request', done => {

@ -14,7 +14,7 @@ describe('ApplicationConfigurationService', () => {
beforeEach(() => (spectator = createHttp()));
it('should send a GET to application-configuration API', () => {
spectator.get(Store).selectSnapshot.andReturn('https://abp.io');
spectator.inject(Store).selectSnapshot.andReturn('https://abp.io');
spectator.service.getConfiguration().subscribe();
spectator.expectOne('https://abp.io/api/abp/application-configuration', HttpMethod.GET);
});

@ -22,13 +22,13 @@ describe('AuthGuard', () => {
});
it('should return true when user logged in', () => {
spectator.get(OAuthService).hasValidAccessToken.andReturn(true);
spectator.inject(OAuthService).hasValidAccessToken.andReturn(true);
expect(guard.canActivate(null, null)).toBe(true);
});
it('should return navigate to login page with redirectUrl state', () => {
const router = spectator.get(Router);
spectator.get(OAuthService).hasValidAccessToken.andReturn(false);
const router = spectator.inject(Router);
spectator.inject(OAuthService).hasValidAccessToken.andReturn(false);
expect(guard.canActivate(null, { url: '/' } as any)).toBe(true);
expect(router.navigate).toHaveBeenCalledWith(['/account/login'], {

@ -102,7 +102,7 @@ describe('ConfigStateService', () => {
beforeEach(() => {
spectator = createService();
service = spectator.service;
store = spectator.get(Store);
store = spectator.inject(Store);
});
test('should have the all ConfigState static methods', () => {
const reg = /(?<=static )(.*)(?=\()/gm;

@ -110,9 +110,9 @@ describe('ConfigState', () => {
beforeEach(() => {
spectator = createService();
store = spectator.get(Store);
store = spectator.inject(Store);
service = spectator.service;
state = new ConfigState(spectator.get(HttpClient), store);
state = new ConfigState(spectator.inject(HttpClient), store);
});
describe('#getAll', () => {
@ -250,7 +250,7 @@ describe('ConfigState', () => {
dispatchArg = a;
return of(a);
});
const httpClient = spectator.get(HttpClient);
const httpClient = spectator.inject(HttpClient);
httpClient.get.andReturn(res$);
state.addData({ patchState, dispatch } as any).subscribe();

@ -167,8 +167,8 @@ describe('DynamicLayoutComponent', () => {
beforeEach(async () => {
spectator = createComponent();
store = spectator.get(Store);
const routesService = spectator.get(RoutesService);
store = spectator.inject(Store);
const routesService = spectator.inject(RoutesService);
routesService.add(routes);
store.reset(storeData);

@ -26,12 +26,12 @@ describe('LocaleProvider', () => {
describe('#LOCALE_ID', () => {
test('should equal to currentLang', async () => {
spectator = createComponent();
const localizationService = spectator.get(LocalizationService);
const localizationService = spectator.inject(LocalizationService);
expect(spectator.get(LOCALE_ID).valueOf()).toBe(localesMapping['en-US'] || 'en-US');
expect(spectator.inject(LOCALE_ID).valueOf()).toBe(localesMapping['en-US'] || 'en-US');
(localizationService as any).currentLang = 'tr';
expect(spectator.get(LOCALE_ID).valueOf()).toBe(localesMapping['tr'] || 'tr');
expect(spectator.inject(LOCALE_ID).valueOf()).toBe(localesMapping['tr'] || 'tr');
});
});
});

@ -12,8 +12,8 @@ describe('LocalizationPipe', () => {
beforeEach(() => {
spectator = createService();
pipe = spectator.get(LocalizationPipe);
store = spectator.get(Store);
pipe = spectator.inject(LocalizationPipe);
store = spectator.inject(Store);
});
it('should call getLocalization selector', () => {

@ -18,7 +18,7 @@ describe('LocalizationService', () => {
beforeEach(() => {
spectator = createService();
store = spectator.get(Store);
store = spectator.inject(Store);
service = spectator.service;
});
@ -52,7 +52,7 @@ describe('LocalizationService', () => {
describe('#registerLocale', () => {
it('should return registerLocale and then call setRouteReuse', () => {
const router = spectator.get(Router);
const router = spectator.inject(Router);
const shouldReuseRoute = () => true;
router.routeReuseStrategy = { shouldReuseRoute } as any;

@ -42,7 +42,7 @@ describe('PermissionDirective', () => {
});
it('should do nothing when condition is undefined', () => {
const spy = jest.spyOn(spectator.get(Store), 'select');
const spy = jest.spyOn(spectator.inject(Store), 'select');
grantedPolicy$.next(false);
expect(spy.mock.calls).toHaveLength(0);
});

@ -52,7 +52,7 @@ describe('PermissionGuard', () => {
spectator = createService();
guard = spectator.service;
routes = spectator.inject(RoutesService);
store = spectator.get(Store);
store = spectator.inject(Store);
});
it('should return true when the grantedPolicy is true', done => {

@ -13,7 +13,7 @@ describe('ProfileStateService', () => {
beforeEach(() => {
spectator = createService();
service = spectator.service;
store = spectator.get(Store);
store = spectator.inject(Store);
});
test('should have the all ProfileState static methods', () => {
const reg = /(?<=static )(.*)(?=\()/gm;

@ -14,14 +14,14 @@ describe('ProfileService', () => {
beforeEach(() => (spectator = createHttp()));
it('should send a GET to my-profile API', () => {
spectator.get(Store).selectSnapshot.andReturn('https://abp.io');
spectator.inject(Store).selectSnapshot.andReturn('https://abp.io');
spectator.service.get().subscribe();
spectator.expectOne('https://abp.io/api/identity/my-profile', HttpMethod.GET);
});
it('should send a POST to change-password API', () => {
const mock = { currentPassword: 'test', newPassword: 'test' };
spectator.get(Store).selectSnapshot.andReturn('https://abp.io');
spectator.inject(Store).selectSnapshot.andReturn('https://abp.io');
spectator.service.changePassword(mock).subscribe();
const req = spectator.expectOne(
'https://abp.io/api/identity/my-profile/change-password',
@ -38,7 +38,7 @@ describe('ProfileService', () => {
surname: 'Doe',
phoneNumber: '+123456',
};
spectator.get(Store).selectSnapshot.andReturn('https://abp.io');
spectator.inject(Store).selectSnapshot.andReturn('https://abp.io');
spectator.service.update(mock).subscribe();
const req = spectator.expectOne('https://abp.io/api/identity/my-profile', HttpMethod.PUT);
expect(req.request.body).toEqual(mock);

@ -26,7 +26,7 @@ describe('ProfileState', () => {
beforeEach(() => {
spectator = createService();
profileService = spectator.get(ProfileService);
profileService = spectator.inject(ProfileService);
state = new ProfileState(profileService);
});

@ -25,7 +25,7 @@ describe('ReplaceableComponentsState', () => {
});
it('should add a component to the state', () => {
const store = spectator.get(Store);
const store = spectator.inject(Store);
expect(store.selectSnapshot(ReplaceableComponentsState.getAll)).toEqual([]);
store.dispatch(new AddReplaceableComponent({ component: DummyComponent, key: 'Dummy' }));
expect(store.selectSnapshot(ReplaceableComponentsState.getComponent('Dummy'))).toEqual({
@ -35,7 +35,7 @@ describe('ReplaceableComponentsState', () => {
});
it('should replace a exist component', () => {
const store = spectator.get(Store);
const store = spectator.inject(Store);
store.dispatch(new AddReplaceableComponent({ component: DummyComponent, key: 'Dummy' }));
store.dispatch(new AddReplaceableComponent({ component: null, key: 'Dummy' }));
expect(store.selectSnapshot(ReplaceableComponentsState.getComponent('Dummy'))).toEqual({
@ -47,7 +47,7 @@ describe('ReplaceableComponentsState', () => {
it('should call reloadRoute when reload parameter is given as true to AddReplaceableComponent', async () => {
const spy = jest.spyOn(router, 'navigateByUrl');
const store = spectator.get(Store);
const store = spectator.inject(Store);
store.dispatch(new AddReplaceableComponent({ component: DummyComponent, key: 'Dummy' }));
store.dispatch(new AddReplaceableComponent({ component: null, key: 'Dummy' }, true));

@ -20,7 +20,7 @@ describe('HttpClient testing', () => {
beforeEach(() => {
spectator = createHttp();
store = spectator.get(Store);
store = spectator.inject(Store);
store.reset({
ConfigState: {
environment: {

@ -17,7 +17,7 @@ describe('SessionStateService', () => {
beforeEach(() => {
spectator = createService();
service = spectator.service;
store = spectator.get(Store);
store = spectator.inject(Store);
});
test('should have the all SessionState static methods', () => {
const reg = /(?<=static )(.*)(?=\()/gm;

@ -16,7 +16,7 @@ describe('FeatureManagementStateService', () => {
beforeEach(() => {
spectator = createService();
service = spectator.service;
store = spectator.get(Store);
store = spectator.inject(Store);
});
test('should have the all FeatureManagementState static methods', () => {

@ -13,7 +13,7 @@ describe('IdentityStateService', () => {
beforeEach(() => {
spectator = createService();
service = spectator.service;
store = spectator.get(Store);
store = spectator.inject(Store);
});
test('should have the all IdentityState static methods', () => {

@ -16,7 +16,7 @@ describe('PermissionManagementStateService', () => {
beforeEach(() => {
spectator = createService();
service = spectator.service;
store = spectator.get(Store);
store = spectator.inject(Store);
});
test('should have the all PermissionManagementState static methods', () => {
const reg = /(?<=static )(.*)(?=\()/gm;

@ -16,7 +16,7 @@ describe('TenantManagementStateService', () => {
beforeEach(() => {
spectator = createService();
service = spectator.service;
store = spectator.get(Store);
store = spectator.inject(Store);
});
test('should have the all TenantManagementState static methods', () => {

@ -126,8 +126,8 @@ describe('Form Prop Utils', () => {
function* getInjected(spectator: SpectatorService<ExtensionsService>) {
yield spectator.service;
yield spectator.get(EXTENSIONS_IDENTIFIER);
yield spectator.get(LocalizationService);
yield spectator.inject(EXTENSIONS_IDENTIFIER);
yield spectator.inject(LocalizationService);
}
interface Foo {

@ -15,8 +15,8 @@ describe('AppendContentToken', () => {
beforeEach(() => (spectator = createComponent()));
it('should insert a style element to the DOM', () => {
spectator.get(THEME_SHARED_APPEND_CONTENT);
expect(spectator.get(DomInsertionService).has(styles)).toBe(true);
spectator.inject(THEME_SHARED_APPEND_CONTENT);
expect(spectator.inject(DomInsertionService).has(styles)).toBe(true);
});
it('should be loaded the chart.js', done => {
@ -25,6 +25,6 @@ describe('AppendContentToken', () => {
done();
});
spectator.get(THEME_SHARED_APPEND_CONTENT);
spectator.inject(THEME_SHARED_APPEND_CONTENT);
});
});

@ -54,8 +54,8 @@ describe('BreadcrumbComponent', () => {
beforeEach(() => {
spectator = createRouting();
routes = spectator.get(RoutesService);
store = spectator.get(Store);
routes = spectator.inject(RoutesService);
store = spectator.inject(Store);
});
it('should display the breadcrumb', async () => {

@ -52,7 +52,7 @@ describe('ErrorHandler', () => {
beforeEach(() => {
spectator = createService();
service = spectator.service;
store = spectator.get(Store);
store = spectator.inject(Store);
store.selectSnapshot = jest.fn(() => '/x');
});
@ -189,7 +189,7 @@ describe('ErrorHandler', () => {
test('should call error method of ConfirmationService when authenticated error occurs with _AbpErrorFormat header', done => {
spectator
.get(Actions)
.inject(Actions)
.pipe(ofActionDispatched(Navigate))
.subscribe(({ path, queryParams, extras }) => {
expect(path).toEqual(['/account/login']);
@ -281,7 +281,7 @@ describe('ErrorHandler with custom error component', () => {
beforeEach(() => {
spectator = createService();
service = spectator.service;
store = spectator.get(Store);
store = spectator.inject(Store);
store.selectSnapshot = jest.fn(() => '/x');
});

@ -22,7 +22,7 @@ describe('LoaderBarComponent', () => {
spectator = createHost('<abp-loader-bar></abp-loader-bar>');
spectator.component.intervalPeriod = 1;
spectator.component.stopDelay = 1;
router = spectator.get(Router);
router = spectator.inject(Router);
(router as any).events = events$;
});
@ -34,7 +34,7 @@ describe('LoaderBarComponent', () => {
it('should increase the progressLevel', done => {
spectator.detectChanges();
spectator.get(Store).dispatch(new StartLoader(new HttpRequest('GET', 'test')));
spectator.inject(Store).dispatch(new StartLoader(new HttpRequest('GET', 'test')));
spectator.detectChanges();
setTimeout(() => {
expect(spectator.component.progressLevel > 0).toBeTruthy();
@ -44,7 +44,7 @@ describe('LoaderBarComponent', () => {
test.skip('should be interval unsubscribed', done => {
spectator.detectChanges();
spectator.get(Store).dispatch(new StartLoader(new HttpRequest('GET', 'test')));
spectator.inject(Store).dispatch(new StartLoader(new HttpRequest('GET', 'test')));
expect(spectator.component.interval.closed).toBe(false);
timer(400).subscribe(() => {
@ -73,7 +73,7 @@ describe('LoaderBarComponent', () => {
(router as any).events.next(new NavigationStart(1, 'test'));
expect(spectator.component.interval.closed).toBe(false);
spectator.get(Store).dispatch(new StopLoader(new HttpRequest('GET', 'test')));
spectator.inject(Store).dispatch(new StopLoader(new HttpRequest('GET', 'test')));
expect(spectator.component.progressLevel).toBe(100);
timer(2).subscribe(() => {

@ -23,7 +23,7 @@ describe('ModalContainerComponent', () => {
let foo = document.querySelector('div.foo');
expect(foo).toBeNull();
const cfResolver = spectator.get(ComponentFactoryResolver);
const cfResolver = spectator.inject(ComponentFactoryResolver);
const factory = cfResolver.resolveComponentFactory(TestComponent);
componentRef = spectator.component.container.createComponent(factory);

@ -78,7 +78,7 @@ describe('ModalComponent', () => {
});
afterEach(() => {
const modalService = spectator.get(ModalService);
const modalService = spectator.inject(ModalService);
modalService.clearModal();
});
@ -120,7 +120,7 @@ describe('ModalComponent', () => {
});
it('should open the confirmation popup and works correct', async () => {
const confirmationService = spectator.get(ConfirmationService);
const confirmationService = spectator.inject(ConfirmationService);
const warnSpy = jest.spyOn(confirmationService, 'warn');
await wait0ms();

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save