committed by
GitHub
81 changed files with 8426 additions and 12 deletions
File diff suppressed because it is too large
@ -0,0 +1,77 @@ |
|||
using System; |
|||
using Microsoft.EntityFrameworkCore.Migrations; |
|||
|
|||
#nullable disable |
|||
|
|||
namespace LY.MicroService.Applications.Single.EntityFrameworkCore.MySql.Migrations |
|||
{ |
|||
/// <inheritdoc />
|
|||
public partial class AddGdprModule : Migration |
|||
{ |
|||
/// <inheritdoc />
|
|||
protected override void Up(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.CreateTable( |
|||
name: "AbpGdprRequests", |
|||
columns: table => new |
|||
{ |
|||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"), |
|||
UserId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"), |
|||
CreationTime = table.Column<DateTime>(type: "datetime(6)", nullable: false), |
|||
ReadyTime = table.Column<DateTime>(type: "datetime(6)", nullable: false), |
|||
ExtraProperties = table.Column<string>(type: "longtext", nullable: false) |
|||
.Annotation("MySql:CharSet", "utf8mb4"), |
|||
ConcurrencyStamp = table.Column<string>(type: "varchar(40)", maxLength: 40, nullable: false) |
|||
.Annotation("MySql:CharSet", "utf8mb4") |
|||
}, |
|||
constraints: table => |
|||
{ |
|||
table.PrimaryKey("PK_AbpGdprRequests", x => x.Id); |
|||
}) |
|||
.Annotation("MySql:CharSet", "utf8mb4"); |
|||
|
|||
migrationBuilder.CreateTable( |
|||
name: "AbpGdprInfos", |
|||
columns: table => new |
|||
{ |
|||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"), |
|||
RequestId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"), |
|||
Data = table.Column<string>(type: "longtext", nullable: false) |
|||
.Annotation("MySql:CharSet", "utf8mb4"), |
|||
Provider = table.Column<string>(type: "varchar(256)", maxLength: 256, nullable: false) |
|||
.Annotation("MySql:CharSet", "utf8mb4") |
|||
}, |
|||
constraints: table => |
|||
{ |
|||
table.PrimaryKey("PK_AbpGdprInfos", x => x.Id); |
|||
table.ForeignKey( |
|||
name: "FK_AbpGdprInfos_AbpGdprRequests_RequestId", |
|||
column: x => x.RequestId, |
|||
principalTable: "AbpGdprRequests", |
|||
principalColumn: "Id", |
|||
onDelete: ReferentialAction.Cascade); |
|||
}) |
|||
.Annotation("MySql:CharSet", "utf8mb4"); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_AbpGdprInfos_RequestId", |
|||
table: "AbpGdprInfos", |
|||
column: "RequestId"); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_AbpGdprRequests_UserId", |
|||
table: "AbpGdprRequests", |
|||
column: "UserId"); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
protected override void Down(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.DropTable( |
|||
name: "AbpGdprInfos"); |
|||
|
|||
migrationBuilder.DropTable( |
|||
name: "AbpGdprRequests"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,26 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks> |
|||
<AssemblyName>LINGYUN.Abp.Gdpr.Application.Contracts</AssemblyName> |
|||
<PackageId>LINGYUN.Abp.Gdpr.Application.Contracts</PackageId> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<Nullable>enable</Nullable> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.Authorization.Abstractions" /> |
|||
<PackageReference Include="Volo.Abp.Ddd.Application.Contracts" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\LINGYUN.Abp.Gdpr.Domain.Shared\LINGYUN.Abp.Gdpr.Domain.Shared.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,14 @@ |
|||
using Volo.Abp.Application; |
|||
using Volo.Abp.Authorization; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpAuthorizationAbstractionsModule), |
|||
typeof(AbpDddApplicationContractsModule), |
|||
typeof(AbpGdprDomainSharedModule) |
|||
)] |
|||
public class AbpGdprApplicationContractsModule : AbpModule |
|||
{ |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
namespace LINGYUN.Abp.Gdpr; |
|||
public static class GdprRemoteServiceConsts |
|||
{ |
|||
public const string RemoteServiceName = "Gdpr"; |
|||
public const string ModuleName = "gdpr"; |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Auditing; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
public class GdprRequestDto : EntityDto<Guid>, IHasCreationTime |
|||
{ |
|||
public DateTime CreationTime { get; set; } |
|||
public DateTime ReadyTime { get; set; } |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
public class GdprRequestGetListInput : PagedAndSortedResultRequestDto |
|||
{ |
|||
public string? CreationTime { get; set; } |
|||
public string? ReadyTime { get; set; } |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.Content; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
public interface IGdprRequestAppService : IApplicationService |
|||
{ |
|||
/// <summary>
|
|||
/// 获取个人数据请求详情
|
|||
/// </summary>
|
|||
/// <param name="id"></param>
|
|||
/// <returns></returns>
|
|||
Task<GdprRequestDto> GetAsync(Guid id); |
|||
/// <summary>
|
|||
/// 获取个人数据请求列表
|
|||
/// </summary>
|
|||
/// <param name="input"></param>
|
|||
/// <returns></returns>
|
|||
Task<PagedResultDto<GdprRequestDto>> GetListAsync(GdprRequestGetListInput input); |
|||
/// <summary>
|
|||
/// 下载已收集的用户数据
|
|||
/// </summary>
|
|||
/// <param name="requestId"></param>
|
|||
/// <returns></returns>
|
|||
Task<IRemoteStreamContent> DownloadPersonalDataAsync(Guid requestId); |
|||
/// <summary>
|
|||
/// 删除个人数据请求
|
|||
/// </summary>
|
|||
/// <param name="id"></param>
|
|||
/// <returns></returns>
|
|||
Task DeleteAsync(Guid id); |
|||
/// <summary>
|
|||
/// 准备用户数据
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
Task PreparePersonalDataAsync(); |
|||
/// <summary>
|
|||
/// 删除用户数据
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
Task DeletePersonalDataAsync(); |
|||
/// <summary>
|
|||
/// 删除用户账户
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
Task DeletePersonalAccountAsync(); |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,28 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>net9.0</TargetFrameworks> |
|||
<AssemblyName>LINGYUN.Abp.Gdpr.Application</AssemblyName> |
|||
<PackageId>LINGYUN.Abp.Gdpr.Application</PackageId> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<Nullable>enable</Nullable> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.Ddd.Application" /> |
|||
</ItemGroup> |
|||
|
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\..\framework\exporter\LINGYUN.Abp.Exporter.Core\LINGYUN.Abp.Exporter.Core.csproj" /> |
|||
<ProjectReference Include="..\LINGYUN.Abp.Gdpr.Application.Contracts\LINGYUN.Abp.Gdpr.Application.Contracts.csproj" /> |
|||
<ProjectReference Include="..\LINGYUN.Abp.Gdpr.Domain\LINGYUN.Abp.Gdpr.Domain.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,26 @@ |
|||
using LINGYUN.Abp.Exporter; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.Application; |
|||
using Volo.Abp.AutoMapper; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpDddApplicationModule), |
|||
typeof(AbpGdprDomainModule), |
|||
typeof(AbpExporterCoreModule), |
|||
typeof(AbpGdprApplicationContractsModule) |
|||
)] |
|||
public class AbpGdprApplicationModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.AddAutoMapperObjectMapper<AbpGdprApplicationModule>(); |
|||
|
|||
Configure<AbpAutoMapperOptions>(options => |
|||
{ |
|||
options.AddMaps<AbpGdprApplicationModule>(validate: true); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using AutoMapper; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
public class GdprApplicationMapperProfile : Profile |
|||
{ |
|||
public GdprApplicationMapperProfile() |
|||
{ |
|||
CreateMap<GdprRequest, GdprRequestDto>(); |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using LINGYUN.Abp.Gdpr.Localization; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
public abstract class GdprApplicationServiceBase : ApplicationService |
|||
{ |
|||
protected GdprApplicationServiceBase() |
|||
{ |
|||
LocalizationResource = typeof(GdprResource); |
|||
ObjectMapperContext = typeof(AbpGdprApplicationModule); |
|||
} |
|||
} |
|||
@ -0,0 +1,211 @@ |
|||
using LINGYUN.Abp.Exporter; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Microsoft.Extensions.Caching.Distributed; |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Linq.Expressions; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Caching; |
|||
using Volo.Abp.Content; |
|||
using Volo.Abp.EventBus.Distributed; |
|||
using Volo.Abp.Gdpr; |
|||
using Volo.Abp.Json; |
|||
using Volo.Abp.Users; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
[Authorize] |
|||
public class GdprRequestAppService( |
|||
IJsonSerializer jsonSerializer, |
|||
IExporterProvider exporterProvider, |
|||
IOptions<AbpGdprOptions> gdprOptions, |
|||
IDistributedEventBus distributedEventBus, |
|||
IDistributedCache<GdprRequestCacheItem> gdprRequestCache, |
|||
IGdprRequestRepository gdprRequestRepository |
|||
) : GdprApplicationServiceBase, |
|||
IGdprRequestAppService |
|||
{ |
|||
public async virtual Task DeleteAsync(Guid id) |
|||
{ |
|||
var gdprRequest = await gdprRequestRepository.GetAsync(id); |
|||
|
|||
await gdprRequestRepository.DeleteAsync(gdprRequest); |
|||
} |
|||
|
|||
public async virtual Task DeletePersonalAccountAsync() |
|||
{ |
|||
var userId = CurrentUser.GetId(); |
|||
|
|||
await DeletePersonalDataAsync(); |
|||
|
|||
await distributedEventBus.PublishAsync( |
|||
new GdprUserAccountDeletionRequestedEto() |
|||
{ |
|||
UserId = userId, |
|||
}); |
|||
} |
|||
|
|||
public async virtual Task DeletePersonalDataAsync() |
|||
{ |
|||
var userId = CurrentUser.GetId(); |
|||
|
|||
var specification = new Volo.Abp.Specifications.ExpressionSpecification<GdprRequest>(x => x.UserId == userId); |
|||
|
|||
var totalCount = await gdprRequestRepository.GetCountAsync(specification); |
|||
var gdprRequests = await gdprRequestRepository.GetListAsync(specification, maxResultCount: totalCount); |
|||
|
|||
await gdprRequestRepository.DeleteManyAsync(gdprRequests); |
|||
|
|||
await CurrentUnitOfWork!.SaveChangesAsync(); |
|||
|
|||
await distributedEventBus.PublishAsync( |
|||
new GdprUserDataDeletionRequestedEto() |
|||
{ |
|||
UserId = userId, |
|||
}); |
|||
} |
|||
|
|||
public async virtual Task<GdprRequestDto> GetAsync(Guid id) |
|||
{ |
|||
var gdprRequest = await gdprRequestRepository.GetAsync(id); |
|||
|
|||
return ObjectMapper.Map<GdprRequest, GdprRequestDto>(gdprRequest); |
|||
} |
|||
|
|||
public async virtual Task<PagedResultDto<GdprRequestDto>> GetListAsync(GdprRequestGetListInput input) |
|||
{ |
|||
Expression<Func<GdprRequest, bool>> expression = x => x.UserId == CurrentUser.GetId(); |
|||
|
|||
if (!input.CreationTime.IsNullOrWhiteSpace()) |
|||
{ |
|||
var times = input.CreationTime.Split(';'); |
|||
if (times.Length >= 1 && DateTime.TryParse(times[0], out var beginCreationTime)) |
|||
{ |
|||
expression = expression.And(x => x.CreationTime >= beginCreationTime); |
|||
} |
|||
if (times.Length >= 2 && DateTime.TryParse(times[1], out var endCreationTime)) |
|||
{ |
|||
expression = expression.And(x => x.CreationTime <= endCreationTime); |
|||
} |
|||
} |
|||
if (!input.ReadyTime.IsNullOrWhiteSpace()) |
|||
{ |
|||
var times = input.ReadyTime.Split(';'); |
|||
if (times.Length >= 1 && DateTime.TryParse(times[0], out var beginReadyTime)) |
|||
{ |
|||
expression = expression.And(x => x.ReadyTime >= beginReadyTime); |
|||
} |
|||
if (times.Length >= 2 && DateTime.TryParse(times[1], out var endReadyTime)) |
|||
{ |
|||
expression = expression.And(x => x.ReadyTime <= endReadyTime); |
|||
} |
|||
} |
|||
|
|||
var specification = new Volo.Abp.Specifications.ExpressionSpecification<GdprRequest>(expression); |
|||
|
|||
var totalCount = await gdprRequestRepository.GetCountAsync(specification); |
|||
var gdprRequests = await gdprRequestRepository.GetListAsync(specification, |
|||
input.Sorting, input.MaxResultCount, input.SkipCount); |
|||
|
|||
return new PagedResultDto<GdprRequestDto>(totalCount, |
|||
ObjectMapper.Map<List<GdprRequest>, List<GdprRequestDto>>(gdprRequests)); |
|||
} |
|||
|
|||
public async virtual Task<IRemoteStreamContent> DownloadPersonalDataAsync(Guid requestId) |
|||
{ |
|||
var userId = CurrentUser.GetId(); |
|||
var cacheKey = GdprRequestCacheItem.CalculateCacheKey(userId, requestId); |
|||
var cacheItem = await gdprRequestCache.GetAsync(cacheKey); |
|||
if (cacheItem == null) |
|||
{ |
|||
var gdprRequest = await gdprRequestRepository.GetAsync(requestId); |
|||
if (Clock.Now < gdprRequest.ReadyTime) |
|||
{ |
|||
throw new BusinessException(GdprErrorCodes.DataNotPreparedYet) |
|||
.WithData(nameof(GdprRequest.ReadyTime), gdprRequest.ReadyTime.ToString("yyyy-MM-dd HH:mm:ss")); |
|||
} |
|||
|
|||
cacheItem = new GdprRequestCacheItem(userId, requestId) |
|||
{ |
|||
Infos = gdprRequest.Infos.Select(x => new GdprInfoCacheItem(x.Provider, x.Data)).ToList() |
|||
}; |
|||
|
|||
await gdprRequestCache.SetAsync(cacheKey, cacheItem, new DistributedCacheEntryOptions |
|||
{ |
|||
AbsoluteExpirationRelativeToNow = gdprOptions.Value.RequestTimeInterval |
|||
}); |
|||
} |
|||
|
|||
return await DownloadUserData(cacheItem); |
|||
} |
|||
|
|||
public async virtual Task PreparePersonalDataAsync() |
|||
{ |
|||
var userId = CurrentUser.GetId(); |
|||
|
|||
await ValidationRequestTimeInterval(userId); |
|||
|
|||
var gdprRequest = new GdprRequest( |
|||
GuidGenerator.Create(), |
|||
userId, |
|||
Clock.Now.Add(gdprOptions.Value.MinutesForDataPreparation)); |
|||
|
|||
await gdprRequestRepository.InsertAsync(gdprRequest); |
|||
|
|||
await CurrentUnitOfWork!.SaveChangesAsync(); |
|||
|
|||
await distributedEventBus.PublishAsync( |
|||
new GdprUserDataRequestedEto() |
|||
{ |
|||
RequestId = gdprRequest.Id, |
|||
UserId = gdprRequest.UserId, |
|||
}); |
|||
} |
|||
|
|||
protected async virtual Task<IRemoteStreamContent> DownloadUserData(GdprRequestCacheItem cacheItem) |
|||
{ |
|||
var fileName = $"{L["PersonalData"]}.xlsx"; |
|||
|
|||
var emptyValue = !L["PersonalData:Empty"].ResourceNotFound ? L["PersonalData:Empty"] : "None"; |
|||
|
|||
var personalData = new List<Dictionary<string, string>>(); |
|||
|
|||
foreach (var gdprInfo in cacheItem.Infos) |
|||
{ |
|||
var gdprData = new Dictionary<string, string>(); |
|||
var gdprInfoData = jsonSerializer.Deserialize<Dictionary<string, string>>(gdprInfo.Data); |
|||
|
|||
foreach (var gdprDataItem in gdprInfoData) |
|||
{ |
|||
// 导出数据本地化
|
|||
var gdprDataKey = gdprDataItem.Key; |
|||
var gdprDataLocalizaztionKey = L[$"PersonalData:{gdprDataKey.ToPascalCase()}"]; |
|||
if (!gdprDataLocalizaztionKey.ResourceNotFound) |
|||
{ |
|||
gdprDataKey = gdprDataLocalizaztionKey.Value; |
|||
} |
|||
gdprData[gdprDataKey] = gdprDataItem.Value.IsNullOrWhiteSpace() ? emptyValue : gdprDataItem.Value; |
|||
} |
|||
personalData.Add(gdprData); |
|||
} |
|||
var stream = await exporterProvider.ExportAsync(personalData); |
|||
|
|||
return new RemoteStreamContent(stream, fileName); |
|||
} |
|||
|
|||
protected async Task ValidationRequestTimeInterval(Guid userId) |
|||
{ |
|||
var latestRequestTime = await gdprRequestRepository.FindLatestRequestTimeAsync(userId); |
|||
if (latestRequestTime.HasValue && (Clock.Now - latestRequestTime) < gdprOptions.Value.RequestTimeInterval) |
|||
{ |
|||
var nextRequestTime = latestRequestTime.Value.Add(gdprOptions.Value.RequestTimeInterval); |
|||
|
|||
throw new BusinessException(GdprErrorCodes.PersonalDataRequestAlreadyDays) |
|||
.WithData(nameof(AbpGdprOptions.RequestTimeInterval), nextRequestTime.ToString("yyyy-MM-dd HH:mm:ss")); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,27 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>net9.0</TargetFrameworks> |
|||
<AssemblyName>LINGYUN.Abp.Gdpr.Domain.Identity</AssemblyName> |
|||
<PackageId>LINGYUN.Abp.Gdpr.Domain.Identity</PackageId> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<Nullable>enable</Nullable> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<None Remove="LINGYUN\Abp\Gdpr\Identity\Localization\Resources\*.json" /> |
|||
<EmbeddedResource Include="LINGYUN\Abp\Gdpr\Identity\Localization\Resources\*.json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\identity\LINGYUN.Abp.Identity.Domain\LINGYUN.Abp.Identity.Domain.csproj" /> |
|||
<ProjectReference Include="..\LINGYUN.Abp.Gdpr.Domain\LINGYUN.Abp.Gdpr.Domain.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,36 @@ |
|||
using LINGYUN.Abp.Gdpr.Localization; |
|||
using LINGYUN.Abp.Identity; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.VirtualFileSystem; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr.Identity; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpGdprDomainModule), |
|||
typeof(AbpIdentityDomainModule))] |
|||
public class AbpGdprDomainIdentityModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpVirtualFileSystemOptions>(options => |
|||
{ |
|||
options.FileSets.AddEmbedded<AbpGdprDomainIdentityModule>(); |
|||
}); |
|||
|
|||
Configure<AbpLocalizationOptions>(options => |
|||
{ |
|||
options.Resources |
|||
.Get<GdprResource>() |
|||
.AddVirtualJson("/LINGYUN/Abp/Gdpr/Identity/Localization/Resources"); |
|||
}); |
|||
|
|||
Configure<AbpGdprOptions>(options => |
|||
{ |
|||
// 用户数据提供者
|
|||
options.GdprUserDataProviders.Add(new AbpGdprIdentityUserDataProvider()); |
|||
// 用户账户提供者
|
|||
options.GdprUserAccountProviders.Add(new AbpGdprIdentityUserAccountProvider()); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Identity; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr.Identity; |
|||
|
|||
/// <summary>
|
|||
/// 身份标识模块用户账户提供者
|
|||
/// </summary>
|
|||
public class AbpGdprIdentityUserAccountProvider : GdprUserAccountProviderBase |
|||
{ |
|||
public const string ProviderName = "Identity"; |
|||
|
|||
public override string Name => ProviderName; |
|||
|
|||
public async override Task DeleteAsync(GdprDeleteUserAccountContext context) |
|||
{ |
|||
var identityUserManager = context.ServiceProvider.GetRequiredService<IdentityUserManager>(); |
|||
|
|||
var identityUser = await identityUserManager.GetByIdAsync(context.UserId); |
|||
|
|||
(await identityUserManager.DeleteAsync(identityUser)).CheckErrors(); |
|||
} |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
using Microsoft.AspNetCore.Identity; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Auditing; |
|||
using Volo.Abp.Domain.ChangeTracking; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.Gdpr; |
|||
using Volo.Abp.Identity; |
|||
using Volo.Abp.Users; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr.Identity; |
|||
|
|||
/// <summary>
|
|||
/// 身份标识模块用户数据提供者
|
|||
/// </summary>
|
|||
public class AbpGdprIdentityUserDataProvider: GdprUserDataProviderBase |
|||
{ |
|||
public const string ProviderName = "Identity"; |
|||
|
|||
public override string Name => ProviderName; |
|||
|
|||
public async override Task DeleteAsync(GdprDeleteUserDataContext context) |
|||
{ |
|||
var identityUserRepository = context.ServiceProvider.GetRequiredService<IIdentityUserRepository>(); |
|||
var identityUserManager = context.ServiceProvider.GetRequiredService<IdentityUserManager>(); |
|||
|
|||
var identityUser = await identityUserManager.GetByIdAsync(context.UserId); |
|||
|
|||
identityUser.Name = ""; |
|||
identityUser.Surname = ""; |
|||
identityUser.SetIsActive(false); |
|||
|
|||
await identityUserRepository.EnsureCollectionLoadedAsync(identityUser, u => u.Claims); |
|||
|
|||
identityUser.Claims.Clear(); |
|||
|
|||
// TODO: 应设置为空字符串, 但是abp框架不允许Email为空
|
|||
(await identityUserManager.SetEmailAsync(identityUser, $"{identityUser.UserName}@abp.io")).CheckErrors(); |
|||
(await identityUserManager.SetPhoneNumberAsync(identityUser, "")).CheckErrors(); |
|||
|
|||
(await identityUserManager.UpdateAsync(identityUser)).CheckErrors(); |
|||
} |
|||
|
|||
[DisableEntityChangeTracking] |
|||
public async override Task PorepareAsync(GdprPrepareUserDataContext context) |
|||
{ |
|||
var identityUserRepository = context.ServiceProvider.GetRequiredService<IIdentityUserRepository>(); |
|||
var identityUser = await identityUserRepository.GetAsync(context.UserId); |
|||
|
|||
var gdprDataInfo = new GdprDataInfo |
|||
{ |
|||
{ nameof(UserData.UserName), identityUser.UserName }, |
|||
{ nameof(UserData.Name), identityUser.Name }, |
|||
{ nameof(UserData.Surname), identityUser.Surname }, |
|||
{ nameof(UserData.Email), identityUser.Email }, |
|||
{ nameof(UserData.PhoneNumber), identityUser.PhoneNumber }, |
|||
{ nameof(IHasCreationTime.CreationTime), identityUser.CreationTime.ToString("yyyy-MM-dd HH:mm:ss") }, |
|||
}; |
|||
|
|||
foreach (var identityUserClaim in identityUser.Claims) |
|||
{ |
|||
gdprDataInfo.Add(identityUserClaim.ClaimType, identityUserClaim.ClaimValue); |
|||
} |
|||
|
|||
await DispatchPrepareUserDataAsync(context, gdprDataInfo); |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
{ |
|||
"culture": "en", |
|||
"texts": { |
|||
"PersonalData:UserName": "User Name", |
|||
"PersonalData:Name": "Name", |
|||
"PersonalData:Surname": "Surname", |
|||
"PersonalData:Email": "Email", |
|||
"PersonalData:PhoneNumber": "Phone Number", |
|||
"PersonalData:CreationTime": "Registration Time", |
|||
"PersonalData:AvatarUrl": "Avatar", |
|||
"PersonalData:Picture": "Picture", |
|||
"PersonalData:Website": "Website" |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
{ |
|||
"culture": "zh-Hans", |
|||
"texts": { |
|||
"PersonalData:UserName": "用户名", |
|||
"PersonalData:Name": "名称", |
|||
"PersonalData:Surname": "姓氏", |
|||
"PersonalData:Email": "邮件地址", |
|||
"PersonalData:PhoneNumber": "手机号", |
|||
"PersonalData:CreationTime": "注册时间", |
|||
"PersonalData:AvatarUrl": "头像", |
|||
"PersonalData:Picture": "头像", |
|||
"PersonalData:Website": "个人页" |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,28 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks> |
|||
<AssemblyName>LINGYUN.Abp.Gdpr.Domain.Shared</AssemblyName> |
|||
<PackageId>LINGYUN.Abp.Gdpr.Domain.Shared</PackageId> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<Nullable>enable</Nullable> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<None Remove="LINGYUN\Abp\Gdpr\Localization\Resources\*.json" /> |
|||
<EmbeddedResource Include="LINGYUN\Abp\Gdpr\Localization\Resources\*.json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.Features" /> |
|||
<PackageReference Include="Volo.Abp.Ddd.Domain.Shared" /> |
|||
<PackageReference Include="Volo.Abp.Gdpr.Abstractions" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,38 @@ |
|||
using LINGYUN.Abp.Gdpr.Localization; |
|||
using Volo.Abp.Domain; |
|||
using Volo.Abp.Features; |
|||
using Volo.Abp.Gdpr; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.Localization.ExceptionHandling; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.VirtualFileSystem; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpFeaturesModule), |
|||
typeof(AbpDddDomainSharedModule), |
|||
typeof(AbpGdprAbstractionsModule) |
|||
)] |
|||
public class AbpGdprDomainSharedModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpVirtualFileSystemOptions>(options => |
|||
{ |
|||
options.FileSets.AddEmbedded<AbpGdprDomainSharedModule>(); |
|||
}); |
|||
|
|||
Configure<AbpLocalizationOptions>(options => |
|||
{ |
|||
options.Resources |
|||
.Add<GdprResource>("en") |
|||
.AddVirtualJson("/LINGYUN/Abp/Gdpr/Localization/Resources"); |
|||
}); |
|||
|
|||
Configure<AbpExceptionLocalizationOptions>(options => |
|||
{ |
|||
options.MapCodeNamespace(GdprErrorCodes.Namespace, typeof(GdprResource)); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
public static class GdprErrorCodes |
|||
{ |
|||
public const string Namespace = "Gdpr"; |
|||
/// <summary>
|
|||
/// 你已经提交了一个个人数据下载请求, 请在 {RequestTimeInterval} 后再次尝试.
|
|||
/// </summary>
|
|||
public const string PersonalDataRequestAlreadyDays = Namespace + ":010001"; |
|||
/// <summary>
|
|||
/// 你的个人数据正在准备中, 请在 {ReadyTime} 后再次尝试下载.
|
|||
/// </summary>
|
|||
public const string DataNotPreparedYet = Namespace + ":010002"; |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
public class GdprInfoCacheItem |
|||
{ |
|||
public string Data { get; set; } |
|||
public string Provider { get; set; } |
|||
public GdprInfoCacheItem() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public GdprInfoCacheItem(string provider, string data) |
|||
{ |
|||
Data = data; |
|||
Provider = provider; |
|||
} |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
public class GdprInfoConsts |
|||
{ |
|||
public static int MaxProviderLength { get; set; } = 256; |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.MultiTenancy; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
[Serializable] |
|||
[IgnoreMultiTenancy] |
|||
public class GdprRequestCacheItem |
|||
{ |
|||
private const string CacheKeyFormat = "uid:{0};rid:{1}"; |
|||
|
|||
public Guid UserId { get; set; } |
|||
public Guid RequestId { get; set; } |
|||
public List<GdprInfoCacheItem> Infos { get; set; } |
|||
public GdprRequestCacheItem() |
|||
{ |
|||
Infos = new List<GdprInfoCacheItem>(); |
|||
} |
|||
|
|||
public GdprRequestCacheItem(Guid userId, Guid requestId) |
|||
{ |
|||
UserId = userId; |
|||
RequestId = requestId; |
|||
|
|||
Infos = new List<GdprInfoCacheItem>(); |
|||
} |
|||
|
|||
public static string CalculateCacheKey(Guid userId, Guid requestId) |
|||
{ |
|||
return string.Format(CacheKeyFormat, userId, requestId); |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using System; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
/// <summary>
|
|||
/// 用户账户删除请求事件传输对象
|
|||
/// </summary>
|
|||
[Serializable] |
|||
public class GdprUserAccountDeletionRequestedEto |
|||
{ |
|||
public Guid UserId { get; set; } |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
using Volo.Abp.Localization; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr.Localization; |
|||
|
|||
[LocalizationResourceName("AbpGdpr")] |
|||
public class GdprResource |
|||
{ |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
{ |
|||
"culture": "en", |
|||
"texts": { |
|||
"Gdpr:010001": "You have submitted a personal data download request, please try again after {RequestTimeInterval}.", |
|||
"Gdpr:010002": "Your personal data is being prepared, please try to download again after {ReadyTime}.", |
|||
"DisplayName:PersonalData": "Personal Data", |
|||
"DisplayName:Provider": "Provider", |
|||
"DisplayName:UserId": "User Id", |
|||
"DisplayName:CreationTime": "Creation Time", |
|||
"DisplayName:ReadyTime": "Ready Time", |
|||
"PersonalData:Empty": "None", |
|||
"Preparing": "Preparing", |
|||
"Download": "Download", |
|||
"RequestPersonalData": "Request Personal Data", |
|||
"DeletePersonalData": "Delete Personal Data", |
|||
"DeletePersonalAccount": "Delete Account", |
|||
"RequestedSuccessfully": "Requested Successfully", |
|||
"DeletePersonalAccountWarning": "This action will delete your account (including your personal data) and you will no longer be able to log in to the app!", |
|||
"PersonalAccountDeleteRequestReceived": "Your account deletion request is being processed and your personal data will be deleted at the end of the deletion process.", |
|||
"DeletePersonalDataWarning": "This action will delete your account personal data, and the deletion of some data may affect the page experience!", |
|||
"PersonalDataDeleteRequestReceived": "Your personal data deletion request is being processed, and you may need to log back into the app after the deletion process is complete.", |
|||
"PersonalDataPrepareRequestReceived": "Your personal data request is being processed and if the data is ready, you can download it on this page.", |
|||
"Accept": "Accept", |
|||
"PersonalData": "Personal Data", |
|||
"CookiePolicy": "Cookie Policy", |
|||
"PrivacyPolicy": "Privacy Policy", |
|||
"ThisWebsiteUsesCookie": "This website uses cookies to ensure you get the best experience on the website.", |
|||
"CookieConsentAgreePolicy": "If you continue to browse, then you agree to our {0}.", |
|||
"CookieConsentAgreePolicies": "If you continue to browse, then you agree to our {0} and {1}." |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
{ |
|||
"culture": "zh-Hans", |
|||
"texts": { |
|||
"Gdpr:010001": "你已经提交了一个个人数据下载请求, 请在 {RequestTimeInterval} 后再次尝试.", |
|||
"Gdpr:010002": "你的个人数据正在准备中, 请在 {ReadyTime} 后再次尝试下载.", |
|||
"DisplayName:PersonalData": "个人数据", |
|||
"DisplayName:Provider": "收集模块", |
|||
"DisplayName:UserId": "用户Id", |
|||
"DisplayName:CreationTime": "请求时间", |
|||
"DisplayName:ReadyTime": "就绪时间", |
|||
"PersonalData:Empty": "无", |
|||
"Preparing": "准备中", |
|||
"Download": "下载", |
|||
"RequestPersonalData": "请求个人数据", |
|||
"DeletePersonalData": "删除个人数据", |
|||
"DeletePersonalAccount": "注销账号", |
|||
"RequestedSuccessfully": "请求成功", |
|||
"DeletePersonalAccountWarning": "此操作将会删除你的账户(包括你的个人数据), 并且你将不能再登录到应用程序!", |
|||
"PersonalAccountDeleteRequestReceived": "你的账户删除请求正在处理中, 在删除过程结束后,你的个人数据将被删除.", |
|||
"DeletePersonalDataWarning": "此操作将会删除你的账户个人数据, 某些数据被删除可能会影响页面体验!", |
|||
"PersonalDataDeleteRequestReceived": "你的个人数据删除请求正在处理中, 在删除过程结束后, 你可能需要重新登录应用程序.", |
|||
"PersonalDataPrepareRequestReceived": "你的个人数据请求正在处理中, 如果数据准备就绪, 你可以在这个页面下载它.", |
|||
"Accept": "接受", |
|||
"PersonalData": "个人数据", |
|||
"CookiePolicy": "Cookie政策", |
|||
"PrivacyPolicy": "隐私政策", |
|||
"ThisWebsiteUsesCookie": "本网站使用cookie以确保您在网站上获得最佳体验.", |
|||
"CookieConsentAgreePolicy": "如果您继续浏览, 即表示您同意我们的 {0}.", |
|||
"CookieConsentAgreePolicies": "如果您继续浏览, 则表示您同意我们的 {0} 和 {1}." |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,26 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>net9.0</TargetFrameworks> |
|||
<AssemblyName>LINGYUN.Abp.Gdpr.Domain</AssemblyName> |
|||
<PackageId>LINGYUN.Abp.Gdpr.Domain</PackageId> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<Nullable>enable</Nullable> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.AutoMapper" /> |
|||
<PackageReference Include="Volo.Abp.Ddd.Domain" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\LINGYUN.Abp.Gdpr.Domain.Shared\LINGYUN.Abp.Gdpr.Domain.Shared.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,44 @@ |
|||
using System; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
/// <summary>
|
|||
/// AbpCookie同意选项
|
|||
/// See: https://abp.io/docs/latest/modules/gdpr#abpcookieconsentoptions
|
|||
/// </summary>
|
|||
public class AbpCookieConsentOptions |
|||
{ |
|||
/// <summary>
|
|||
/// 启用或禁用Cookie Consent特征
|
|||
/// </summary>
|
|||
public bool IsEnabled { get; set; } |
|||
/// <summary>
|
|||
/// 定义 Cookie 策略页面 URL.<br />
|
|||
/// 设置后, "Cookie 策略"页面 URL 会自动添加到 Cookie 同意声明中.<br />
|
|||
/// 因此, 用户可以在接受 cookie 同意之前检查 cookie 政策.<br />
|
|||
/// 您可以将其设置为本地地址或完整 URL, 例如:<br />
|
|||
/// /CookiePolicy<br />
|
|||
/// https://example.com/cookie-policy
|
|||
/// </summary>
|
|||
public string CookiePolicyUrl { get; set; } |
|||
/// <summary>
|
|||
/// 定义隐私政策页面 URL.<br />
|
|||
/// 设置后, "隐私政策"页面 URL 会自动添加到 Cookie 同意声明中.<br />
|
|||
/// 因此, 用户可以在接受 cookie 同意之前查看隐私政策.<br />
|
|||
/// 您可以将其设置为本地地址或完整 URL, 例如:<br />
|
|||
/// /PrivacyPolicy<br />
|
|||
/// https://example.com/privacy-policy
|
|||
/// </summary>
|
|||
public string PrivacyPolicyUrl { get; set; } |
|||
/// <summary>
|
|||
/// 定义 Cookie 同意的 Cookie 过期时间.<br />
|
|||
/// 默认情况下, 当接受 Cookie 同意时, 它会将 Cookie 设置为有效期为 6 个月.
|
|||
/// </summary>
|
|||
public TimeSpan Expiration { get; set; } |
|||
public AbpCookieConsentOptions() |
|||
{ |
|||
IsEnabled = true; |
|||
CookiePolicyUrl = "/CookiePolicy"; |
|||
PrivacyPolicyUrl = "/PrivacyPolicy"; |
|||
Expiration = TimeSpan.FromDays(180); |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.AutoMapper; |
|||
using Volo.Abp.Domain; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpAutoMapperModule), |
|||
typeof(AbpDddDomainModule), |
|||
typeof(AbpGdprDomainSharedModule) |
|||
)] |
|||
public class AbpGdprDomainModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
var configuration = context.Services.GetConfiguration(); |
|||
|
|||
Configure<AbpGdprOptions>(configuration.GetSection("Gdpr")); |
|||
Configure<AbpCookieConsentOptions>(configuration.GetSection("CookieConsent")); |
|||
|
|||
context.Services.AddAutoMapperObjectMapper<AbpGdprDomainModule>(); |
|||
|
|||
Configure<AbpAutoMapperOptions>(options => |
|||
{ |
|||
options.AddMaps<AbpGdprDomainModule>(validate: true); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
/// <summary>
|
|||
/// AbpGdpr选项
|
|||
/// See: https://abp.io/docs/latest/modules/gdpr#abpgdproptions
|
|||
/// </summary>
|
|||
public class AbpGdprOptions |
|||
{ |
|||
/// <summary>
|
|||
/// 用于指示允许的请求时间间隔.<br />
|
|||
/// 如果要增加或减少个人数据请求间隔,可以配置此属性.<br />
|
|||
/// 默认情况下, 用户可以每天请求一次其个人数据<br />
|
|||
/// 默认值: 1 天
|
|||
/// </summary>
|
|||
public TimeSpan RequestTimeInterval { get; set; } |
|||
/// <summary>
|
|||
/// 由于 GDPR 模块旨在支持分布式方案, 因此收集和准备个人数据应该需要一段时间.
|
|||
/// 如果要根据应用程序的大小增加或减少数据准备时间, 可以配置此属性.<br />
|
|||
/// 默认值: 60 分钟
|
|||
/// </summary>
|
|||
public TimeSpan MinutesForDataPreparation { get; set; } |
|||
/// <summary>
|
|||
/// 用户数据提供者列表
|
|||
/// </summary>
|
|||
public IList<IGdprUserDataProvider> GdprUserDataProviders { get; } |
|||
/// <summary>
|
|||
/// 用户账户提供者列表
|
|||
/// </summary>
|
|||
public IList<IGdprUserAccountProvider> GdprUserAccountProviders { get; } |
|||
public AbpGdprOptions() |
|||
{ |
|||
RequestTimeInterval = TimeSpan.FromDays(1); |
|||
MinutesForDataPreparation = TimeSpan.FromMinutes(60); |
|||
|
|||
GdprUserDataProviders = new List<IGdprUserDataProvider>(); |
|||
GdprUserAccountProviders = new List<IGdprUserAccountProvider>(); |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
namespace LINGYUN.Abp.Gdpr; |
|||
public static class GdprDbProterties |
|||
{ |
|||
public static string DbTablePrefix { get; set; } = "AbpGdpr"; |
|||
|
|||
public static string? DbSchema { get; set; } = null; |
|||
|
|||
|
|||
public const string ConnectionStringName = "AbpGdpr"; |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using System; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Gdpr; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
public class GdprDeleteUserAccountContext(IServiceProvider serviceProvider) : GdprUserDataProviderContext, IServiceProviderAccessor |
|||
{ |
|||
public IServiceProvider ServiceProvider { get; } = serviceProvider; |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using System; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Gdpr; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
public class GdprDeleteUserDataContext(IServiceProvider serviceProvider) : GdprUserDataProviderContext, IServiceProviderAccessor |
|||
{ |
|||
public IServiceProvider ServiceProvider { get; } = serviceProvider; |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using AutoMapper; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
public class GdprDomainMapperProfile : Profile |
|||
{ |
|||
public GdprDomainMapperProfile() |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
using System; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Domain.Entities; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
/// <summary>
|
|||
/// 表示用户的个人数据
|
|||
/// See: https://abp.io/docs/latest/modules/gdpr#gdprinfo
|
|||
/// </summary>
|
|||
public class GdprInfo : Entity<Guid> |
|||
{ |
|||
/// <summary>
|
|||
/// GDPR 请求的 ID
|
|||
/// </summary>
|
|||
public virtual Guid RequestId { get; protected set; } |
|||
|
|||
/// <summary>
|
|||
/// 用于存储个人数据
|
|||
/// </summary>
|
|||
public virtual string Data { get; protected set; } |
|||
|
|||
/// <summary>
|
|||
/// 表示收集个人数据的模块
|
|||
/// </summary>
|
|||
public virtual string Provider { get; protected set; } |
|||
|
|||
protected GdprInfo() |
|||
{ |
|||
} |
|||
|
|||
public GdprInfo( |
|||
Guid id, |
|||
Guid requestId, |
|||
string data, |
|||
string provider) |
|||
: base(id) |
|||
{ |
|||
RequestId = requestId; |
|||
Data = Check.NotNullOrWhiteSpace(data, nameof(data)); |
|||
Provider = Check.NotNullOrWhiteSpace(provider, nameof(provider), GdprInfoConsts.MaxProviderLength); |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using System; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Gdpr; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
public class GdprPrepareUserDataContext(Guid requestId, IServiceProvider serviceProvider) : GdprUserDataProviderContext, IServiceProviderAccessor |
|||
{ |
|||
public IServiceProvider ServiceProvider { get; } = serviceProvider; |
|||
public Guid RequestId { get; } = requestId; |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Collections.ObjectModel; |
|||
using Volo.Abp.Auditing; |
|||
using Volo.Abp.Domain.Entities; |
|||
using Volo.Abp.Guids; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
/// <summary>
|
|||
/// 表示用户发出的 GDPR 请求
|
|||
/// See: https://abp.io/docs/latest/modules/gdpr#gdprrequest
|
|||
/// </summary>
|
|||
public class GdprRequest : AggregateRoot<Guid>, IHasCreationTime |
|||
{ |
|||
///// TODO: Abp Framework GDPR模块不支持多租户
|
|||
///// <summary>
|
|||
///// 租户Id
|
|||
///// </summary>
|
|||
///// public virtual Guid? TenantId { get; protected set; }
|
|||
|
|||
/// <summary>
|
|||
/// 发出请求的用户的 ID
|
|||
/// </summary>
|
|||
public virtual Guid UserId { get; protected set; } |
|||
/// <summary>
|
|||
/// 创建时间
|
|||
/// </summary>
|
|||
public virtual DateTime CreationTime { get; protected set; } |
|||
/// <summary>
|
|||
/// 数据准备过程的结束时间
|
|||
/// </summary>
|
|||
public virtual DateTime ReadyTime { get; protected set; } |
|||
/// <summary>
|
|||
/// 收集的用户个人数据
|
|||
/// </summary>
|
|||
public virtual ICollection<GdprInfo> Infos { get; protected set; } |
|||
protected GdprRequest() |
|||
{ |
|||
Infos = new Collection<GdprInfo>(); |
|||
} |
|||
|
|||
public GdprRequest( |
|||
Guid id, |
|||
Guid userId, |
|||
DateTime readyTime) |
|||
: base(id) |
|||
{ |
|||
UserId = userId; |
|||
ReadyTime = readyTime; |
|||
Infos = new Collection<GdprInfo>(); |
|||
} |
|||
|
|||
public void AddData( |
|||
IGuidGenerator guidGenerator, |
|||
string data, |
|||
string provider) |
|||
{ |
|||
Infos.Add( |
|||
new GdprInfo( |
|||
guidGenerator.Create(), |
|||
Id, |
|||
data, |
|||
provider)); |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
public abstract class GdprUserAccountProviderBase : IGdprUserAccountProvider |
|||
{ |
|||
public abstract string Name { get; } |
|||
|
|||
public abstract Task DeleteAsync(GdprDeleteUserAccountContext context); |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.EventBus.Distributed; |
|||
using Volo.Abp.Gdpr; |
|||
using Volo.Abp.Guids; |
|||
using Volo.Abp.Json; |
|||
using Volo.Abp.Uow; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
public class GdprUserDataPreparedEventHandler( |
|||
IGdprRequestRepository gdprRequestRepository, |
|||
IJsonSerializer jsonSerializer, |
|||
IGuidGenerator guidGenerator |
|||
): IDistributedEventHandler<GdprUserDataPreparedEto>, |
|||
ITransientDependency |
|||
{ |
|||
[UnitOfWork] |
|||
public async virtual Task HandleEventAsync(GdprUserDataPreparedEto eventData) |
|||
{ |
|||
if (eventData.Data.Any()) |
|||
{ |
|||
var gdprRequest = await gdprRequestRepository.FindAsync(eventData.RequestId); |
|||
if (gdprRequest != null) |
|||
{ |
|||
var data = jsonSerializer.Serialize(eventData.Data); |
|||
|
|||
gdprRequest.AddData( |
|||
guidGenerator, |
|||
data, |
|||
eventData.Provider); |
|||
|
|||
await gdprRequestRepository.UpdateAsync(gdprRequest); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.EventBus.Distributed; |
|||
using Volo.Abp.Gdpr; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
public abstract class GdprUserDataProviderBase: IGdprUserDataProvider |
|||
{ |
|||
public abstract string Name { get; } |
|||
|
|||
public abstract Task DeleteAsync(GdprDeleteUserDataContext context); |
|||
public abstract Task PorepareAsync(GdprPrepareUserDataContext context); |
|||
/// <summary>
|
|||
/// 发布个人数据
|
|||
/// </summary>
|
|||
/// <param name="context"></param>
|
|||
/// <param name="gdprDataInfo">个人数据</param>
|
|||
/// <returns></returns>
|
|||
protected async virtual Task DispatchPrepareUserDataAsync(GdprPrepareUserDataContext context, GdprDataInfo gdprDataInfo) |
|||
{ |
|||
var distributedEventBus = context.ServiceProvider.GetRequiredService<IDistributedEventBus>(); |
|||
|
|||
var gdprUserData = new GdprUserDataPreparedEto |
|||
{ |
|||
RequestId = context.RequestId, |
|||
Provider = Name, |
|||
Data = gdprDataInfo, |
|||
}; |
|||
|
|||
await distributedEventBus.PublishAsync(gdprUserData); |
|||
} |
|||
} |
|||
@ -0,0 +1,71 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.EventBus.Distributed; |
|||
using Volo.Abp.Gdpr; |
|||
using Volo.Abp.Uow; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
/// <summary>
|
|||
/// 用户个人数据收集事件监听器<br />
|
|||
/// See: https://abp.io/docs/latest/modules/gdpr#gdpruserdatarequestedeto
|
|||
/// </summary>
|
|||
/// <param name="options"></param>
|
|||
/// <param name="serviceScopeFactory"></param>
|
|||
public class GdprUserDataRequestedEventHandler( |
|||
IOptions<AbpGdprOptions> options, |
|||
IServiceScopeFactory serviceScopeFactory |
|||
) : IDistributedEventHandler<GdprUserDataRequestedEto>, |
|||
IDistributedEventHandler<GdprUserDataDeletionRequestedEto>, |
|||
IDistributedEventHandler<GdprUserAccountDeletionRequestedEto>, |
|||
ITransientDependency |
|||
{ |
|||
public async virtual Task HandleEventAsync(GdprUserDataRequestedEto eventData) |
|||
{ |
|||
using var scope = serviceScopeFactory.CreateScope(); |
|||
|
|||
var context = new GdprPrepareUserDataContext(eventData.RequestId, scope.ServiceProvider) |
|||
{ |
|||
UserId = eventData.UserId, |
|||
}; |
|||
|
|||
foreach (var gdprUserDataProvider in options.Value.GdprUserDataProviders) |
|||
{ |
|||
await gdprUserDataProvider.PorepareAsync(context); |
|||
} |
|||
} |
|||
|
|||
[UnitOfWork] |
|||
public async virtual Task HandleEventAsync(GdprUserDataDeletionRequestedEto eventData) |
|||
{ |
|||
using var scope = serviceScopeFactory.CreateScope(); |
|||
|
|||
var context = new GdprDeleteUserDataContext(scope.ServiceProvider) |
|||
{ |
|||
UserId = eventData.UserId, |
|||
}; |
|||
|
|||
foreach (var gdprUserDataProvider in options.Value.GdprUserDataProviders) |
|||
{ |
|||
await gdprUserDataProvider.DeleteAsync(context); |
|||
} |
|||
} |
|||
|
|||
[UnitOfWork] |
|||
public async virtual Task HandleEventAsync(GdprUserAccountDeletionRequestedEto eventData) |
|||
{ |
|||
using var scope = serviceScopeFactory.CreateScope(); |
|||
|
|||
var context = new GdprDeleteUserAccountContext(scope.ServiceProvider) |
|||
{ |
|||
UserId = eventData.UserId, |
|||
}; |
|||
|
|||
foreach (var gdprUserAccountProvider in options.Value.GdprUserAccountProviders) |
|||
{ |
|||
await gdprUserAccountProvider.DeleteAsync(context); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.Specifications; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
public interface IGdprRequestRepository : IBasicRepository<GdprRequest, Guid> |
|||
{ |
|||
Task<DateTime?> FindLatestRequestTimeAsync( |
|||
Guid userId, |
|||
CancellationToken cancellationToken = default); |
|||
|
|||
Task<int> GetCountAsync( |
|||
ISpecification<GdprRequest> specification, |
|||
CancellationToken cancellationToken = default); |
|||
|
|||
Task<List<GdprRequest>> GetListAsync( |
|||
ISpecification<GdprRequest> specification, |
|||
string? sorting = $"{nameof(GdprRequest.CreationTime)} DESC", |
|||
int maxResultCount = 10, |
|||
int skipCount = 0, |
|||
CancellationToken cancellationToken = default); |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
/// <summary>
|
|||
/// 用户个人账户提供者
|
|||
/// </summary>
|
|||
public interface IGdprUserAccountProvider |
|||
{ |
|||
/// <summary>
|
|||
/// 提供者名称
|
|||
/// </summary>
|
|||
string Name { get; } |
|||
/// <summary>
|
|||
/// 删除用户账户
|
|||
/// </summary>
|
|||
/// <param name="context"></param>
|
|||
/// <returns></returns>
|
|||
Task DeleteAsync(GdprDeleteUserAccountContext context); |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
/// <summary>
|
|||
/// 用户个人数据提供者<br />
|
|||
/// 实现: https://abp.io/docs/latest/modules/gdpr#gdpruserdatarequestedeto
|
|||
/// </summary>
|
|||
public interface IGdprUserDataProvider |
|||
{ |
|||
/// <summary>
|
|||
/// 提供者名称
|
|||
/// </summary>
|
|||
string Name { get; } |
|||
/// <summary>
|
|||
/// 准备用户数据
|
|||
/// </summary>
|
|||
Task PorepareAsync(GdprPrepareUserDataContext context); |
|||
/// <summary>
|
|||
/// 删除用户数据
|
|||
/// </summary>
|
|||
/// <param name="context"></param>
|
|||
/// <returns></returns>
|
|||
Task DeleteAsync(GdprDeleteUserDataContext context); |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,25 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>net9.0</TargetFrameworks> |
|||
<AssemblyName>LINGYUN.Abp.Gdpr.EntityFrameworkCore</AssemblyName> |
|||
<PackageId>LINGYUN.Abp.Gdpr.EntityFrameworkCore</PackageId> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<Nullable>enable</Nullable> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.EntityFrameworkCore" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\LINGYUN.Abp.Gdpr.Domain\LINGYUN.Abp.Gdpr.Domain.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,22 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.EntityFrameworkCore; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr.EntityFrameworkCore; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpGdprDomainModule), |
|||
typeof(AbpEntityFrameworkCoreModule) |
|||
)] |
|||
public class AbpGdprEntityFrameworkCoreModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.AddAbpDbContext<GdprDbContext>(options => |
|||
{ |
|||
options.AddRepository<GdprRequest, EfCoreGdprRequestRepository>(); |
|||
|
|||
options.AddDefaultRepositories<IGdprDbContext>(); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
using Microsoft.EntityFrameworkCore; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Linq.Dynamic.Core; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Domain.Repositories.EntityFrameworkCore; |
|||
using Volo.Abp.EntityFrameworkCore; |
|||
using Volo.Abp.Specifications; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr.EntityFrameworkCore; |
|||
|
|||
public class EfCoreGdprRequestRepository(IDbContextProvider<IGdprDbContext> dbContextProvider) : |
|||
EfCoreRepository<IGdprDbContext, GdprRequest, Guid>(dbContextProvider), |
|||
IGdprRequestRepository |
|||
{ |
|||
public async virtual Task<DateTime?> FindLatestRequestTimeAsync(Guid userId, CancellationToken cancellationToken = default) |
|||
{ |
|||
return await (await GetQueryableAsync()) |
|||
.Where(x => x.UserId == userId) |
|||
.OrderByDescending(x => x.CreationTime) |
|||
.Select(x => x.CreationTime) |
|||
.FirstOrDefaultAsync(GetCancellationToken(cancellationToken)); |
|||
} |
|||
|
|||
public async virtual Task<int> GetCountAsync(ISpecification<GdprRequest> specification, CancellationToken cancellationToken = default) |
|||
{ |
|||
return await (await GetQueryableAsync()) |
|||
.Where(specification.ToExpression()) |
|||
.CountAsync(GetCancellationToken(cancellationToken)); |
|||
} |
|||
|
|||
public async virtual Task<List<GdprRequest>> GetListAsync( |
|||
ISpecification<GdprRequest> specification, |
|||
string? sorting = $"{nameof(GdprRequest.CreationTime)} DESC", |
|||
int maxResultCount = 10, |
|||
int skipCount = 0, |
|||
CancellationToken cancellationToken = default) |
|||
{ |
|||
return await(await GetQueryableAsync()) |
|||
.Where(specification.ToExpression()) |
|||
.OrderBy(sorting.IsNullOrWhiteSpace() ? $"{nameof(GdprRequest.CreationTime)} DESC" : sorting) |
|||
.PageBy(skipCount, maxResultCount) |
|||
.ToListAsync(GetCancellationToken(cancellationToken)); |
|||
} |
|||
|
|||
public async override Task<IQueryable<GdprRequest>> WithDetailsAsync() |
|||
{ |
|||
return (await base.WithDetailsAsync()).Include(x => x.Infos); |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
using Microsoft.EntityFrameworkCore; |
|||
using Volo.Abp.Data; |
|||
using Volo.Abp.EntityFrameworkCore; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr.EntityFrameworkCore; |
|||
|
|||
[ConnectionStringName(GdprDbProterties.ConnectionStringName)] |
|||
public class GdprDbContext(DbContextOptions<GdprDbContext> options) : |
|||
AbpDbContext<GdprDbContext>(options), IGdprDbContext |
|||
{ |
|||
public virtual DbSet<GdprRequest> Requests { get; set; } |
|||
|
|||
protected override void OnModelCreating(ModelBuilder modelBuilder) |
|||
{ |
|||
base.OnModelCreating(modelBuilder); |
|||
|
|||
modelBuilder.ConfigureGdpr(); ; |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
using Microsoft.EntityFrameworkCore; |
|||
using System; |
|||
using Volo.Abp; |
|||
using Volo.Abp.EntityFrameworkCore.Modeling; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr.EntityFrameworkCore; |
|||
public static class GdprDbContextModelCreatingExtensions |
|||
{ |
|||
public static void ConfigureGdpr( |
|||
this ModelBuilder builder, |
|||
Action<GdprModelBuilderConfigurationOptions>? optionsAction = null) |
|||
{ |
|||
Check.NotNull(builder, nameof(builder)); |
|||
|
|||
var options = new GdprModelBuilderConfigurationOptions( |
|||
GdprDbProterties.DbTablePrefix, |
|||
GdprDbProterties.DbSchema |
|||
); |
|||
optionsAction?.Invoke(options); |
|||
|
|||
builder.Entity<GdprRequest>(b => |
|||
{ |
|||
b.ToTable(options.TablePrefix + "Requests", options.Schema); |
|||
|
|||
b.HasMany(p => p.Infos) |
|||
.WithOne() |
|||
.HasForeignKey(fk => fk.RequestId) |
|||
.IsRequired(true); |
|||
|
|||
b.HasIndex(p => p.UserId); |
|||
|
|||
b.ConfigureByConvention(); |
|||
}); |
|||
builder.Entity<GdprInfo>(b => |
|||
{ |
|||
b.ToTable(options.TablePrefix + "Infos", options.Schema); |
|||
|
|||
b.Property(p => p.Provider) |
|||
.HasColumnName(nameof(GdprInfo.Provider)) |
|||
.HasMaxLength(GdprInfoConsts.MaxProviderLength) |
|||
.IsRequired(); |
|||
|
|||
b.Property(p => p.Data) |
|||
.HasColumnName(nameof(GdprInfo.Data)) |
|||
.IsRequired(); |
|||
|
|||
b.HasIndex(p => p.RequestId); |
|||
|
|||
b.ConfigureByConvention(); |
|||
}); |
|||
|
|||
builder.TryConfigureObjectExtensions<GdprDbContext>(); |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using JetBrains.Annotations; |
|||
using Volo.Abp.EntityFrameworkCore.Modeling; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr.EntityFrameworkCore; |
|||
public class GdprModelBuilderConfigurationOptions( |
|||
[NotNull] string tablePrefix = "", |
|||
[CanBeNull] string? schema = null) : |
|||
AbpModelBuilderConfigurationOptions(tablePrefix, schema) |
|||
{ |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using Microsoft.EntityFrameworkCore; |
|||
using Volo.Abp.Data; |
|||
using Volo.Abp.EntityFrameworkCore; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr.EntityFrameworkCore; |
|||
|
|||
[ConnectionStringName(GdprDbProterties.ConnectionStringName)] |
|||
public interface IGdprDbContext : IEfCoreDbContext |
|||
{ |
|||
DbSet<GdprRequest> Requests { get; } |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,25 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>net9.0</TargetFrameworks> |
|||
<AssemblyName>LINGYUN.Abp.Gdpr.HttpApi</AssemblyName> |
|||
<PackageId>LINGYUN.Abp.Gdpr.HttpApi</PackageId> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<Nullable>enable</Nullable> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.AspNetCore.Mvc" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\LINGYUN.Abp.Gdpr.Application.Contracts\LINGYUN.Abp.Gdpr.Application.Contracts.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,41 @@ |
|||
using LINGYUN.Abp.Gdpr.Localization; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc.Localization; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.Validation.Localization; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpAspNetCoreMvcModule), |
|||
typeof(AbpGdprApplicationContractsModule) |
|||
)] |
|||
public class AbpGdprHttpApiModule : AbpModule |
|||
{ |
|||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
PreConfigure<IMvcBuilder>(mvcBuilder => |
|||
{ |
|||
mvcBuilder.AddApplicationPartIfNotExists(typeof(AbpGdprHttpApiModule).Assembly); |
|||
}); |
|||
|
|||
PreConfigure<AbpMvcDataAnnotationsLocalizationOptions>(options => |
|||
{ |
|||
options.AddAssemblyResource( |
|||
typeof(GdprResource), |
|||
typeof(AbpGdprApplicationContractsModule).Assembly); |
|||
}); |
|||
} |
|||
|
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpLocalizationOptions>(options => |
|||
{ |
|||
options.Resources |
|||
.Get<GdprResource>() |
|||
.AddBaseTypes(typeof(AbpValidationResource)); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,69 @@ |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.Content; |
|||
|
|||
namespace LINGYUN.Abp.Gdpr; |
|||
|
|||
[Authorize] |
|||
[Controller] |
|||
[Area(GdprRemoteServiceConsts.ModuleName)] |
|||
[RemoteService(Name = GdprRemoteServiceConsts.RemoteServiceName)] |
|||
[Route($"api/{GdprRemoteServiceConsts.ModuleName}/requests")] |
|||
public class GdprRequestController( |
|||
IGdprRequestAppService service |
|||
) : AbpControllerBase, |
|||
IGdprRequestAppService |
|||
{ |
|||
[HttpDelete] |
|||
[Route("{id}")] |
|||
public virtual Task DeleteAsync(Guid id) |
|||
{ |
|||
return service.DeleteAsync(id); |
|||
} |
|||
|
|||
[HttpDelete] |
|||
[Route("personal-account")] |
|||
public virtual Task DeletePersonalAccountAsync() |
|||
{ |
|||
return service.DeletePersonalAccountAsync(); |
|||
} |
|||
|
|||
[HttpDelete] |
|||
[Route("personal-data")] |
|||
public virtual Task DeletePersonalDataAsync() |
|||
{ |
|||
return service.DeletePersonalDataAsync(); |
|||
} |
|||
|
|||
[HttpGet] |
|||
[Route("{id}")] |
|||
public virtual Task<GdprRequestDto> GetAsync(Guid id) |
|||
{ |
|||
return service.GetAsync(id); |
|||
} |
|||
|
|||
[HttpGet] |
|||
public virtual Task<PagedResultDto<GdprRequestDto>> GetListAsync(GdprRequestGetListInput input) |
|||
{ |
|||
return service.GetListAsync(input); |
|||
} |
|||
|
|||
[HttpGet] |
|||
[Route("personal-data/download/{requestId}")] |
|||
public virtual Task<IRemoteStreamContent> DownloadPersonalDataAsync(Guid requestId) |
|||
{ |
|||
return service.DownloadPersonalDataAsync(requestId); |
|||
} |
|||
|
|||
[HttpPost] |
|||
[Route("personal-data/prepare")] |
|||
public virtual Task PreparePersonalDataAsync() |
|||
{ |
|||
return service.PreparePersonalDataAsync(); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue