diff --git a/.ci/appsettings.override.postgres.docker.json b/.ci/appsettings.override.postgres.docker.json index 2705554bee..749cedff01 100644 --- a/.ci/appsettings.override.postgres.docker.json +++ b/.ci/appsettings.override.postgres.docker.json @@ -19,6 +19,14 @@ } }, "Modules": { + "Announcements": { + "Infrastructure": { + "SqlDatabase": { + "Provider": "Postgres", + "ConnectionString": "User ID=announcements;Password=Passw0rd;Server=postgres;Port=5432;Database=enmeshed;" + } + } + }, "Challenges": { "Infrastructure": { "SqlDatabase": { @@ -97,10 +105,7 @@ }, "Tags": { "Application": { - "SupportedLanguages": [ - "de", - "en" - ], + "SupportedLanguages": ["de", "en"], "TagsForAttributeValueTypes": { "IdentityFileReference": { "schulabschluss": { @@ -166,6 +171,9 @@ } }, "Tokens": { + "Application": { + "DidDomainName": "localhost" + }, "Infrastructure": { "SqlDatabase": { "Provider": "Postgres", diff --git a/.ci/appsettings.override.postgres.local.json b/.ci/appsettings.override.postgres.local.json index 7321d4ae9c..7e4cb7fc92 100644 --- a/.ci/appsettings.override.postgres.local.json +++ b/.ci/appsettings.override.postgres.local.json @@ -19,6 +19,14 @@ } }, "Modules": { + "Announcements": { + "Infrastructure": { + "SqlDatabase": { + "Provider": "Postgres", + "ConnectionString": "User ID=announcements;Password=Passw0rd;Server=localhost;Port=5432;Database=enmeshed;" + } + } + }, "Challenges": { "Infrastructure": { "SqlDatabase": { @@ -97,10 +105,7 @@ }, "Tags": { "Application": { - "SupportedLanguages": [ - "de", - "en" - ], + "SupportedLanguages": ["de", "en"], "TagsForAttributeValueTypes": { "IdentityFileReference": { "schulabschluss": { @@ -166,6 +171,9 @@ } }, "Tokens": { + "Application": { + "DidDomainName": "localhost" + }, "Infrastructure": { "SqlDatabase": { "Provider": "Postgres", diff --git a/.ci/appsettings.override.sqlserver.docker.json b/.ci/appsettings.override.sqlserver.docker.json index a6102ed26e..050034be42 100644 --- a/.ci/appsettings.override.sqlserver.docker.json +++ b/.ci/appsettings.override.sqlserver.docker.json @@ -19,6 +19,14 @@ } }, "Modules": { + "Announcements": { + "Infrastructure": { + "SqlDatabase": { + "Provider": "SqlServer", + "ConnectionString": "Server=sqlserver;Database=enmeshed;User Id=announcements;Password=Passw0rd;TrustServerCertificate=True" + } + } + }, "Challenges": { "Infrastructure": { "SqlDatabase": { @@ -97,10 +105,7 @@ }, "Tags": { "Application": { - "SupportedLanguages": [ - "de", - "en" - ], + "SupportedLanguages": ["de", "en"], "TagsForAttributeValueTypes": { "IdentityFileReference": { "schulabschluss": { @@ -166,6 +171,9 @@ } }, "Tokens": { + "Application": { + "DidDomainName": "localhost" + }, "Infrastructure": { "SqlDatabase": { "Provider": "SqlServer", diff --git a/.ci/appsettings.override.sqlserver.local.json b/.ci/appsettings.override.sqlserver.local.json index 74b61bb4e9..eb601335ed 100644 --- a/.ci/appsettings.override.sqlserver.local.json +++ b/.ci/appsettings.override.sqlserver.local.json @@ -19,6 +19,14 @@ } }, "Modules": { + "Announcements": { + "Infrastructure": { + "SqlDatabase": { + "Provider": "SqlServer", + "ConnectionString": "Server=localhost;Database=enmeshed;User Id=announcements;Password=Passw0rd;TrustServerCertificate=True" + } + } + }, "Challenges": { "Infrastructure": { "SqlDatabase": { @@ -97,10 +105,7 @@ }, "Tags": { "Application": { - "SupportedLanguages": [ - "de", - "en" - ], + "SupportedLanguages": ["de", "en"], "TagsForAttributeValueTypes": { "IdentityFileReference": { "schulabschluss": { @@ -166,6 +171,9 @@ } }, "Tokens": { + "Application": { + "DidDomainName": "localhost" + }, "Infrastructure": { "SqlDatabase": { "Provider": "SqlServer", diff --git a/.ci/compose.test.yml b/.ci/compose.test.yml index a4234d74b3..a9e78bdb7f 100644 --- a/.ci/compose.test.yml +++ b/.ci/compose.test.yml @@ -2,7 +2,7 @@ services: consumer-api: container_name: consumer-api-test hostname: consumer-api - image: consumer-api:0.0.1 + image: ghcr.io/nmshd/backbone-consumer-api:0.0.1 environment: - ASPNETCORE_ENVIRONMENT=Development ports: @@ -21,7 +21,7 @@ services: admin-ui: container_name: admin-ui-test hostname: admin-ui - image: admin-ui:0.0.1 + image: ghcr.io/nmshd/backbone-admin-ui:0.0.1 environment: - ASPNETCORE_ENVIRONMENT=Development ports: @@ -37,7 +37,7 @@ services: event-handler-service: container_name: event-handler-service-test - image: event-handler-service:0.0.1 + image: ghcr.io/nmshd/backbone-event-handler:0.0.1 environment: - ASPNETCORE_ENVIRONMENT=Development depends_on: @@ -51,7 +51,7 @@ services: sse-server: container_name: sse-server-test - image: sse-server:0.0.1 + image: ghcr.io/nmshd/backbone-sse-server:0.0.1 environment: - ASPNETCORE_ENVIRONMENT=Development depends_on: @@ -65,7 +65,7 @@ services: database-migrator: container_name: database-migrator-test - image: database-migrator:0.0.1 + image: ghcr.io/nmshd/backbone-database-migrator:0.0.1 depends_on: seed-database: condition: service_completed_successfully @@ -101,7 +101,7 @@ services: admin-cli: container_name: admin-cli-test - image: admin-cli:0.0.1 + image: ghcr.io/nmshd/backbone-admin-cli:0.0.1 depends_on: consumer-api: condition: service_healthy diff --git a/.github/mergify.yml b/.github/mergify.yml index da9e009014..f4b1edc580 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -1,6 +1,7 @@ pull_request_rules: - name: update pull request conditions: - - label!=wip + - label != wip + - author != renovate[bot] actions: update: diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 4a53eb10d4..c3d8425e06 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -22,7 +22,14 @@ { // the docker images in this list are created and used by our pipelines and can therefore not to be updated by renovate matchDatasources: ["docker"], - packageNames: ["consumer-api", "admin-ui", "event-handler-service", "sse-server", "database-migrator", "admin-cli"], + packageNames: [ + "ghcr.io/nmshd/backbone-consumer-api", + "ghcr.io/nmshd/backbone-admin-ui", + "ghcr.io/nmshd/backbone-event-handler", + "ghcr.io/nmshd/backbone-sse-server", + "ghcr.io/nmshd/backbone-database-migrator", + "ghcr.io/nmshd/backbone-admin-cli" + ], enabled: false }, { diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2550d319a8..7197c3f8b5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Setup Flutter - uses: subosito/flutter-action@v2.17.0 + uses: subosito/flutter-action@v2.18.0 - name: Run checks run: ./.ci/aui/runChecks.sh @@ -105,7 +105,7 @@ jobs: image: database-migrator - dockerfile: Applications/EventHandlerService/src/EventHandlerService/Dockerfile - image: event-handler-service + image: event-handler - dockerfile: Applications/FilesSanityCheck/src/FilesSanityCheck/Dockerfile image: files-sanity-check @@ -153,7 +153,7 @@ jobs: cache-from: type=local,src=/tmp/.buildx-${{ matrix.image }}-cache cache-to: type=local,mode=max,dest=/tmp/.buildx-${{ matrix.image }}-cache-new outputs: type=docker,dest=/tmp/${{ matrix.image }}.tar - tags: ${{ matrix.image }}:0.0.1 + tags: ghcr.io/nmshd/backbone-${{ matrix.image }}:0.0.1 - name: Upload artifact uses: actions/upload-artifact@v4 @@ -306,6 +306,8 @@ jobs: NMSHD_TEST_CLIENTSECRET: test NMSHD_TEST_BASEURL_ADMIN_API: http://localhost:5173 NMSHD_TEST_ADMIN_API_KEY: test + APPSETTINGS_OVERRIDE_LOCATION: ${{ github.workspace }}/backbone/.ci/appsettings.override.${{matrix.database}}.docker.json + BACKBONE_VERSION: 0.0.1 run: npm run test:local:lokijs - name: Save Docker Logs diff --git a/.gitignore b/.gitignore index 10ef12e4cd..2a88651cc0 100644 --- a/.gitignore +++ b/.gitignore @@ -244,7 +244,6 @@ Generated_Code/ # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ -Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ diff --git a/.run/SSE Server.run.xml b/.run/SSE Server.run.xml index de915ad00d..d0e1fcdab8 100644 --- a/.run/SSE Server.run.xml +++ b/.run/SSE Server.run.xml @@ -1,17 +1,17 @@  - - - + + + \ No newline at end of file diff --git a/Applications/AdminApi/src/AdminApi/AdminApi.csproj b/Applications/AdminApi/src/AdminApi/AdminApi.csproj index 6ea3418160..d96ef9d937 100644 --- a/Applications/AdminApi/src/AdminApi/AdminApi.csproj +++ b/Applications/AdminApi/src/AdminApi/AdminApi.csproj @@ -8,12 +8,11 @@ - - - + + @@ -29,6 +28,8 @@ + + diff --git a/Applications/AdminApi/src/AdminApi/Configuration/AdminConfiguration.cs b/Applications/AdminApi/src/AdminApi/Configuration/AdminConfiguration.cs index 2a2d8e5a8a..2dd378f8ca 100644 --- a/Applications/AdminApi/src/AdminApi/Configuration/AdminConfiguration.cs +++ b/Applications/AdminApi/src/AdminApi/Configuration/AdminConfiguration.cs @@ -10,8 +10,6 @@ public class AdminConfiguration public CorsConfiguration Cors { get; set; } = new(); - public SwaggerUiConfiguration SwaggerUi { get; set; } = new(); - [Required] public AdminInfrastructureConfiguration Infrastructure { get; set; } = new(); @@ -30,12 +28,6 @@ public class CorsConfiguration public bool AccessControlAllowCredentials { get; set; } = false; } - public class SwaggerUiConfiguration - { - [Required] - public bool Enabled { get; set; } = false; - } - public class AdminInfrastructureConfiguration { [Required] @@ -46,6 +38,9 @@ public class AdminInfrastructureConfiguration public class ModulesConfiguration { + [Required] + public AnnouncementsConfiguration Announcements { get; set; } = new(); + [Required] public DevicesConfiguration Devices { get; set; } = new(); diff --git a/Applications/AdminApi/src/AdminApi/Configuration/AnnouncementsConfiguration.cs b/Applications/AdminApi/src/AdminApi/Configuration/AnnouncementsConfiguration.cs new file mode 100644 index 0000000000..3749efb2e2 --- /dev/null +++ b/Applications/AdminApi/src/AdminApi/Configuration/AnnouncementsConfiguration.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +using Backbone.Modules.Announcements.Application; + +namespace Backbone.AdminApi.Configuration; + +public class AnnouncementsConfiguration +{ + [Required] + public ApplicationOptions Application { get; set; } = new(); + + [Required] + public InfrastructureConfiguration Infrastructure { get; set; } = new(); + + public class InfrastructureConfiguration + { + [Required] + public SqlDatabaseConfiguration SqlDatabase { get; set; } = new(); + + public class SqlDatabaseConfiguration + { + [Required] + [MinLength(1)] + [RegularExpression("SqlServer|Postgres")] + public string Provider { get; set; } = string.Empty; + + [Required] + [MinLength(1)] + public string ConnectionString { get; set; } = string.Empty; + + [Required] + public bool EnableHealthCheck { get; set; } = true; + } + } +} diff --git a/Applications/AdminApi/src/AdminApi/Controllers/AnnouncementsController.cs b/Applications/AdminApi/src/AdminApi/Controllers/AnnouncementsController.cs new file mode 100644 index 0000000000..24a364a3ef --- /dev/null +++ b/Applications/AdminApi/src/AdminApi/Controllers/AnnouncementsController.cs @@ -0,0 +1,31 @@ +using Backbone.BuildingBlocks.API.Mvc; +using Backbone.Modules.Announcements.Application.Announcements.Commands.CreateAnnouncement; +using Backbone.Modules.Announcements.Application.Announcements.Queries.GetAllAnnouncements; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Backbone.AdminApi.Controllers; + +[Route("api/v1/[controller]")] +[Authorize("ApiKey")] +public class AnnouncementsController : ApiControllerBase +{ + public AnnouncementsController(IMediator mediator) : base(mediator) + { + } + + [HttpPost] + public async Task CreateAnnouncement(CreateAnnouncementCommand request, CancellationToken cancellationToken) + { + var response = await _mediator.Send(request, cancellationToken); + return Created(response); + } + + [HttpGet] + public async Task ListAnnouncements(CancellationToken cancellationToken) + { + var response = await _mediator.Send(new GetAllAnnouncementsQuery(), cancellationToken); + return Ok(response); + } +} diff --git a/Applications/AdminApi/src/AdminApi/Controllers/LogsController.cs b/Applications/AdminApi/src/AdminApi/Controllers/LogsController.cs deleted file mode 100644 index defb65557f..0000000000 --- a/Applications/AdminApi/src/AdminApi/Controllers/LogsController.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Backbone.BuildingBlocks.API.Mvc; -using Backbone.BuildingBlocks.API.Mvc.ControllerAttributes; -using Backbone.BuildingBlocks.Application.Abstractions.Exceptions; -using MediatR; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using ApplicationException = Backbone.BuildingBlocks.Application.Abstractions.Exceptions.ApplicationException; - -namespace Backbone.AdminApi.Controllers; - -[Route("api/v1/[controller]")] -[Authorize("ApiKey")] -public class LogsController : ApiControllerBase -{ - private readonly ILoggerFactory _loggerFactory; - - public LogsController( - IMediator mediator, ILoggerFactory logger) : base(mediator) - { - _loggerFactory = logger; - } - - [HttpPost] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesError(StatusCodes.Status400BadRequest)] - public IActionResult CreateLog(LogRequest request) - { - var logger = _loggerFactory.CreateLogger(request.Category); - - switch (request.LogLevel) - { - case LogLevel.Trace: - logger.LogTrace(request.MessageTemplate, request.Arguments); - break; - case LogLevel.Debug: - logger.LogDebug(request.MessageTemplate, request.Arguments); - break; - case LogLevel.Information: - case LogLevel.Log: - logger.LogInformation(request.MessageTemplate, request.Arguments); - break; - case LogLevel.Warning: - logger.LogWarning(request.MessageTemplate, request.Arguments); - break; - case LogLevel.Error: - logger.LogError(request.MessageTemplate, request.Arguments); - break; - case LogLevel.Critical: - logger.LogCritical(request.MessageTemplate, request.Arguments); - break; - default: - throw new ApplicationException(GenericApplicationErrors.Validation.InvalidPropertyValue(nameof(request.LogLevel))); - } - - return NoContent(); - } -} - -public class LogRequest -{ - public required LogLevel LogLevel { get; set; } - public required string Category { get; set; } - public required string MessageTemplate { get; set; } - public object[] Arguments { get; set; } = []; -} - -public enum LogLevel -{ - Trace, - Debug, - Information, - Log, - Warning, - Error, - Critical -} diff --git a/Applications/AdminApi/src/AdminApi/Dockerfile b/Applications/AdminApi/src/AdminApi/Dockerfile index 9508c092c7..69f03916f4 100644 --- a/Applications/AdminApi/src/AdminApi/Dockerfile +++ b/Applications/AdminApi/src/AdminApi/Dockerfile @@ -55,29 +55,39 @@ RUN apt update && apt upgrade --yes WORKDIR /src COPY ["Directory.Build.props", "."] COPY ["Modules/Directory.Build.props", "Modules/"] + COPY ["Applications/AdminApi/src/AdminApi/AdminApi.csproj", "Applications/AdminApi/src/AdminApi/"] +COPY ["Applications/AdminApi/src/AdminApi.Infrastructure.Database.Postgres/AdminApi.Infrastructure.Database.Postgres.csproj", "Applications/AdminApi/src/AdminApi.Infrastructure.Database.Postgres/"] +COPY ["Applications/AdminApi/src/AdminApi.Infrastructure/AdminApi.Infrastructure.csproj", "Applications/AdminApi/src/AdminApi.Infrastructure/"] +COPY ["Applications/AdminApi/src/AdminApi.Infrastructure.Database.SqlServer/AdminApi.Infrastructure.Database.SqlServer.csproj", "Applications/AdminApi/src/AdminApi.Infrastructure.Database.SqlServer/"] + COPY ["BuildingBlocks/src/BuildingBlocks.API/BuildingBlocks.API.csproj", "BuildingBlocks/src/BuildingBlocks.API/"] -COPY ["Modules/Devices/src/Devices.Domain/Devices.Domain.csproj", "Modules/Devices/src/Devices.Domain/"] COPY ["BuildingBlocks/src/BuildingBlocks.Domain/BuildingBlocks.Domain.csproj", "BuildingBlocks/src/BuildingBlocks.Domain/"] COPY ["BuildingBlocks/src/Tooling/Tooling.csproj", "BuildingBlocks/src/Tooling/"] COPY ["BuildingBlocks/src/DevelopmentKit.Identity/DevelopmentKit.Identity.csproj", "BuildingBlocks/src/DevelopmentKit.Identity/"] -COPY ["Modules/Devices/src/Devices.Infrastructure/Devices.Infrastructure.csproj", "Modules/Devices/src/Devices.Infrastructure/"] COPY ["BuildingBlocks/src/BuildingBlocks.Infrastructure/BuildingBlocks.Infrastructure.csproj", "BuildingBlocks/src/BuildingBlocks.Infrastructure/"] COPY ["BuildingBlocks/src/BuildingBlocks.Application.Abstractions/BuildingBlocks.Application.Abstractions.csproj", "BuildingBlocks/src/BuildingBlocks.Application.Abstractions/"] -COPY ["Modules/Devices/src/Devices.Application/Devices.Application.csproj", "Modules/Devices/src/Devices.Application/"] COPY ["BuildingBlocks/src/BuildingBlocks.Application/BuildingBlocks.Application.csproj", "BuildingBlocks/src/BuildingBlocks.Application/"] -COPY ["Common/src/Common.Infrastructure/Common.Infrastructure.csproj", "Common/src/Common.Infrastructure/"] COPY ["BuildingBlocks/src/Crypto/Crypto.csproj", "BuildingBlocks/src/Crypto/"] +COPY ["Common/src/Common.Infrastructure/Common.Infrastructure.csproj", "Common/src/Common.Infrastructure/"] +COPY ["Infrastructure/Infrastructure.csproj", "Infrastructure/"] + +COPY ["Modules/Announcements/src/Announcements.Application/Announcements.Application.csproj", "Modules/Announcements/src/Announcements.Application/"] +COPY ["Modules/Announcements/src/Announcements.Domain/Announcements.Domain.csproj", "Modules/Announcements/src/Announcements.Domain/"] +COPY ["Modules/Announcements/src/Announcements.Infrastructure/Announcements.Infrastructure.csproj", "Modules/Announcements/src/Announcements.Infrastructure/"] + +COPY ["Modules/Devices/src/Devices.Domain/Devices.Domain.csproj", "Modules/Devices/src/Devices.Domain/"] +COPY ["Modules/Devices/src/Devices.Infrastructure/Devices.Infrastructure.csproj", "Modules/Devices/src/Devices.Infrastructure/"] +COPY ["Modules/Devices/src/Devices.Application/Devices.Application.csproj", "Modules/Devices/src/Devices.Application/"] + COPY ["Modules/Challenges/src/Challenges.Application/Challenges.Application.csproj", "Modules/Challenges/src/Challenges.Application/"] COPY ["Modules/Challenges/src/Challenges.Domain/Challenges.Domain.csproj", "Modules/Challenges/src/Challenges.Domain/"] COPY ["Modules/Challenges/src/Challenges.Infrastructure/Challenges.Infrastructure.csproj", "Modules/Challenges/src/Challenges.Infrastructure/"] -COPY ["Infrastructure/Infrastructure.csproj", "Infrastructure/"] + COPY ["Modules/Quotas/src/Quotas.Application/Quotas.Application.csproj", "Modules/Quotas/src/Quotas.Application/"] COPY ["Modules/Quotas/src/Quotas.Domain/Quotas.Domain.csproj", "Modules/Quotas/src/Quotas.Domain/"] COPY ["Modules/Quotas/src/Quotas.Infrastructure/Quotas.Infrastructure.csproj", "Modules/Quotas/src/Quotas.Infrastructure/"] -COPY ["Applications/AdminApi/src/AdminApi.Infrastructure.Database.Postgres/AdminApi.Infrastructure.Database.Postgres.csproj", "Applications/AdminApi/src/AdminApi.Infrastructure.Database.Postgres/"] -COPY ["Applications/AdminApi/src/AdminApi.Infrastructure/AdminApi.Infrastructure.csproj", "Applications/AdminApi/src/AdminApi.Infrastructure/"] -COPY ["Applications/AdminApi/src/AdminApi.Infrastructure.Database.SqlServer/AdminApi.Infrastructure.Database.SqlServer.csproj", "Applications/AdminApi/src/AdminApi.Infrastructure.Database.SqlServer/"] + RUN dotnet restore /p:ContinuousIntegrationBuild=true "Applications/AdminApi/src/AdminApi/AdminApi.csproj" COPY . . @@ -88,7 +98,7 @@ RUN dotnet publish /p:ContinuousIntegrationBuild=true /p:UseAppHost=false --no-r RUN dotnet publish /p:ContinuousIntegrationBuild=true --configuration Release --output /app/publish/health "/src/Applications/HealthCheck/src/HealthCheck.csproj" #### Build Flutter Admin UI #### -FROM ghcr.io/cirruslabs/flutter:3.24.5 AS flutter-build-env +FROM ghcr.io/cirruslabs/flutter:3.27.0 AS flutter-build-env COPY Applications/AdminUi /src WORKDIR /src diff --git a/Applications/AdminApi/src/AdminApi/Extensions/AnnouncementsServiceCollectionExtensions.cs b/Applications/AdminApi/src/AdminApi/Extensions/AnnouncementsServiceCollectionExtensions.cs new file mode 100644 index 0000000000..c5bcf81256 --- /dev/null +++ b/Applications/AdminApi/src/AdminApi/Extensions/AnnouncementsServiceCollectionExtensions.cs @@ -0,0 +1,22 @@ +using Backbone.AdminApi.Configuration; +using Backbone.Modules.Announcements.Application.Extensions; +using Backbone.Modules.Announcements.Infrastructure.Persistence.Database; + +namespace Backbone.AdminApi.Extensions; + +public static class AnnouncementsServiceCollectionExtensions +{ + public static IServiceCollection AddAnnouncements(this IServiceCollection services, + AnnouncementsConfiguration configuration) + { + services.AddApplication(); + + services.AddDatabase(options => + { + options.ConnectionString = configuration.Infrastructure.SqlDatabase.ConnectionString; + options.Provider = configuration.Infrastructure.SqlDatabase.Provider; + }); + + return services; + } +} diff --git a/Applications/AdminApi/src/AdminApi/Extensions/IServiceCollectionExtensions.cs b/Applications/AdminApi/src/AdminApi/Extensions/IServiceCollectionExtensions.cs index 2b9f41d97f..68577ebaac 100644 --- a/Applications/AdminApi/src/AdminApi/Extensions/IServiceCollectionExtensions.cs +++ b/Applications/AdminApi/src/AdminApi/Extensions/IServiceCollectionExtensions.cs @@ -159,15 +159,6 @@ private static object GetPropertyValue(object source, string propertyPath) return source; } - public static IServiceCollection AddCustomSwaggerWithUi(this IServiceCollection services) - { - services - .AddEndpointsApiExplorer() - .AddSwaggerGen(); - - return services; - } - public static IServiceCollection AddOData(this IServiceCollection services) { var builder = new ODataConventionModelBuilder() diff --git a/Applications/AdminApi/src/AdminApi/Extensions/QuotasServiceCollectionExtensions.cs b/Applications/AdminApi/src/AdminApi/Extensions/QuotasServiceCollectionExtensions.cs index 10e4582af6..e9a9b2f240 100644 --- a/Applications/AdminApi/src/AdminApi/Extensions/QuotasServiceCollectionExtensions.cs +++ b/Applications/AdminApi/src/AdminApi/Extensions/QuotasServiceCollectionExtensions.cs @@ -17,7 +17,6 @@ public static IServiceCollection AddQuotas(this IServiceCollection services, options.Provider = configuration.Infrastructure.SqlDatabase.Provider; }); - return services; } } diff --git a/Applications/AdminApi/src/AdminApi/Program.cs b/Applications/AdminApi/src/AdminApi/Program.cs index 8fe5f79377..df48cc16e0 100644 --- a/Applications/AdminApi/src/AdminApi/Program.cs +++ b/Applications/AdminApi/src/AdminApi/Program.cs @@ -109,12 +109,10 @@ static void ConfigureServices(IServiceCollection services, IConfiguration config .AddDatabase(parsedConfiguration.Infrastructure.SqlDatabase) .AddDevices(configuration.GetSection("Modules:Devices")) .AddQuotas(parsedConfiguration.Modules.Quotas) + .AddAnnouncements(parsedConfiguration.Modules.Announcements) .AddChallenges(parsedConfiguration.Modules.Challenges) .AddHealthChecks(); - if (parsedConfiguration.SwaggerUi.Enabled) - services.AddCustomSwaggerWithUi(); - services .AddOpenIddict() .AddCore(options => @@ -189,9 +187,6 @@ static void Configure(WebApplication app) policies.AddCustomHeader("Access-Control-Allow-Credentials", "true"); }); - if (configuration.SwaggerUi.Enabled) - app.UseSwagger().UseSwaggerUI(); - if (app.Environment.IsDevelopment()) IdentityModelEventSource.ShowPII = true; diff --git a/Applications/AdminApi/src/AdminApi/appsettings.json b/Applications/AdminApi/src/AdminApi/appsettings.json index e7d3310ff0..29f83dc734 100644 --- a/Applications/AdminApi/src/AdminApi/appsettings.json +++ b/Applications/AdminApi/src/AdminApi/appsettings.json @@ -3,15 +3,25 @@ "Authentication": { "JwtLifetimeInSeconds": 300 }, - "SwaggerUi": { - "Enabled": false - }, "Infrastructure": { "EventBus": { "SubscriptionClientName": "adminui" } }, "Modules": { + "Announcements": { + "Application": { + "Pagination": { + "DefaultPageSize": 50, + "MaxPageSize": 200 + } + }, + "Infrastructure": { + "SqlDatabase": { + "EnableHealthCheck": true + } + } + }, "Challenges": { "Infrastructure": { "SqlDatabase": { diff --git a/Applications/AdminApi/test/AdminApi.Tests.Integration/AdminApi.Tests.Integration.csproj b/Applications/AdminApi/test/AdminApi.Tests.Integration/AdminApi.Tests.Integration.csproj index 5a77045ba3..0aff0f9d0d 100644 --- a/Applications/AdminApi/test/AdminApi.Tests.Integration/AdminApi.Tests.Integration.csproj +++ b/Applications/AdminApi/test/AdminApi.Tests.Integration/AdminApi.Tests.Integration.csproj @@ -15,7 +15,7 @@ - + @@ -36,5 +36,5 @@ Always - + diff --git a/Applications/AdminApi/test/AdminApi.Tests.Integration/Assertions/ApiResponseAssertions.cs b/Applications/AdminApi/test/AdminApi.Tests.Integration/Assertions/ApiResponseAssertions.cs index cc3d05c76b..3b6852fbb4 100644 --- a/Applications/AdminApi/test/AdminApi.Tests.Integration/Assertions/ApiResponseAssertions.cs +++ b/Applications/AdminApi/test/AdminApi.Tests.Integration/Assertions/ApiResponseAssertions.cs @@ -1,8 +1,8 @@ -using Backbone.AdminApi.Tests.Integration.Support; +using System.Text.Json; +using Backbone.AdminApi.Tests.Integration.Support; using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types; using FluentAssertions.Execution; using FluentAssertions.Primitives; -using Newtonsoft.Json; namespace Backbone.AdminApi.Tests.Integration.Assertions; @@ -25,7 +25,7 @@ public async Task ComplyWithSchema(string because = "", params object[] becauseA if (Subject.Result != null) { - var resultJson = JsonConvert.SerializeObject(Subject.Result); + var resultJson = JsonSerializer.Serialize(Subject.Result); var (isValid, errors) = await JsonValidator.ValidateJsonSchema(resultJson); assertion.ForCondition(_ => isValid) diff --git a/Applications/AdminApi/test/AdminApi.Tests.Integration/Features/Announcements/GET.feature b/Applications/AdminApi/test/AdminApi.Tests.Integration/Features/Announcements/GET.feature new file mode 100644 index 0000000000..5475e2a227 --- /dev/null +++ b/Applications/AdminApi/test/AdminApi.Tests.Integration/Features/Announcements/GET.feature @@ -0,0 +1,10 @@ +@Integration +Feature: GET /Announcements + +User gets all Announcements + + Scenario: Get all Announcement + Given an existing Announcement a + When a GET request is sent to the /Announcements endpoint + Then the response status code is 200 (OK) + And the response contains the Announcement a diff --git a/Applications/AdminApi/test/AdminApi.Tests.Integration/Features/Announcements/POST.feature b/Applications/AdminApi/test/AdminApi.Tests.Integration/Features/Announcements/POST.feature new file mode 100644 index 0000000000..95244aa5ed --- /dev/null +++ b/Applications/AdminApi/test/AdminApi.Tests.Integration/Features/Announcements/POST.feature @@ -0,0 +1,14 @@ +@Integration +Feature: POST /Announcements + +User create an Announcement + + Scenario: Creating an Announcement + When a POST request is sent to the /Announcements endpoint with a valid content + Then the response status code is 201 (Created) + And the response contains an Announcement + + Scenario: Trying to create an Announcement without an English translation + When a POST request is sent to the /Announcements endpoint without an English translation + Then the response status code is 400 (Bad Request) + And the response content contains an error with the error code "error.platform.validation.invalidPropertyValue" diff --git a/Applications/AdminApi/test/AdminApi.Tests.Integration/Features/Logs/POST.feature b/Applications/AdminApi/test/AdminApi.Tests.Integration/Features/Logs/POST.feature deleted file mode 100644 index 59dd07a9c2..0000000000 --- a/Applications/AdminApi/test/AdminApi.Tests.Integration/Features/Logs/POST.feature +++ /dev/null @@ -1,13 +0,0 @@ -@Integration -Feature: POST Log - -UI Creates a Log - -Scenario: Creating a Log - When a POST request is sent to the /Logs endpoint - Then the response status code is 204 (No Content) - -Scenario: Creating a Log with an invalid Log Level fails - When a POST request is sent to the /Logs endpoint with an invalid Log Level - Then the response status code is 400 (Bad Request) - And the response content contains an error with the error code "error.platform.validation.invalidPropertyValue" diff --git a/Applications/AdminApi/test/AdminApi.Tests.Integration/StepDefinitions/AnnouncementsStepDefinitions.cs b/Applications/AdminApi/test/AdminApi.Tests.Integration/StepDefinitions/AnnouncementsStepDefinitions.cs new file mode 100644 index 0000000000..89f35268ca --- /dev/null +++ b/Applications/AdminApi/test/AdminApi.Tests.Integration/StepDefinitions/AnnouncementsStepDefinitions.cs @@ -0,0 +1,135 @@ +using Backbone.AdminApi.Sdk.Endpoints.Announcements.Types; +using Backbone.AdminApi.Sdk.Endpoints.Announcements.Types.Requests; +using Backbone.AdminApi.Sdk.Endpoints.Announcements.Types.Responses; +using Backbone.AdminApi.Tests.Integration.Configuration; +using Backbone.AdminApi.Tests.Integration.Extensions; +using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types; +using Backbone.Tooling; +using Microsoft.Extensions.Options; + +namespace Backbone.AdminApi.Tests.Integration.StepDefinitions; + +[Binding] +[Scope(Feature = "GET /Announcements")] +[Scope(Feature = "POST /Announcements")] +internal class AnnouncementsStepDefinitions : BaseStepDefinitions +{ + private ApiResponse? _announcementResponse; + private ApiResponse? _announcementsResponse; + private Announcement? _givenAnnouncement; + + public AnnouncementsStepDefinitions(HttpClientFactory factory, IOptions options) : base(factory, options) + { + } + + [Given(@"an existing Announcement a")] + public async Task GivenAnExistingAnnouncementA() + { + var response = await _client.Announcements.CreateAnnouncement(new CreateAnnouncementRequest + { + Texts = + [ + new CreateAnnouncementRequestText + { + Language = "en", + Title = "Title", + Body = "Body" + } + ], + Severity = AnnouncementSeverity.Medium, + ExpiresAt = DateTime.UtcNow + }); + + _givenAnnouncement = response.Result; + } + + [When(@"a GET request is sent to the /Announcements endpoint")] + public async Task WhenAGETRequestIsSentToTheAnnouncementsEndpoint() + { + _announcementsResponse = await _client.Announcements.GetAllAnnouncements(); + } + + [When(@"a POST request is sent to the /Announcements endpoint with a valid content")] + public async Task WhenAPOSTRequestIsSentToTheAnnouncementsEndpointWithAValidContent() + { + _announcementResponse = await _client.Announcements.CreateAnnouncement(new CreateAnnouncementRequest + { + Severity = AnnouncementSeverity.High, + ExpiresAt = SystemTime.UtcNow.AddDays(1), + Texts = + [ + new CreateAnnouncementRequestText + { + Language = "en", + Title = "Title", + Body = "Body" + } + ] + }); + } + + [When(@"a POST request is sent to the /Announcements endpoint without an English translation")] + public async Task WhenAPOSTRequestIsSentToTheAnnouncementsEndpointWithoutAnEnglishTranslation() + { + _announcementResponse = await _client.Announcements.CreateAnnouncement(new CreateAnnouncementRequest + { + Severity = AnnouncementSeverity.High, + ExpiresAt = SystemTime.UtcNow.AddDays(1), + Texts = + [ + new CreateAnnouncementRequestText + { + Language = "de", + Title = "Titel", + Body = "Inhalt" + } + ] + }); + } + + [Then(@"the response status code is (\d+) \(.+\)")] + public void ThenTheResponseStatusCodeIs(int expectedStatusCode) + { + if (_announcementResponse != null) + ((int)_announcementResponse.Status).Should().Be(expectedStatusCode); + + if (_announcementsResponse != null) + ((int)_announcementsResponse.Status).Should().Be(expectedStatusCode); + } + + [Then(@"the response contains an Announcement")] + public async Task ThenTheResponseContainsAnAnnouncement() + { + _announcementResponse!.Result.Should().NotBeNull(); + await _announcementResponse.Should().ComplyWithSchema(); + } + + [Then(@"the response contains a list of Announcements")] + public async Task ThenTheResponseContainsAListOfAnnouncements() + { + _announcementsResponse!.Result.Should().NotBeNull(); + await _announcementsResponse.Should().ComplyWithSchema(); + } + + [Then(@"the response contains the Announcement a")] + public void ThenTheResponseContainsTheAnnouncementA() + { + _announcementsResponse!.Result.Should().ContainSingle(a => a.Id == _givenAnnouncement!.Id); + } + + [Then(@"the response content contains an error with the error code ""([^""]+)""")] + public void ThenTheResponseContentIncludesAnErrorWithTheErrorCode(string errorCode) + { + if (_announcementResponse != null) + { + _announcementResponse!.Error.Should().NotBeNull(); + _announcementResponse.Error!.Code.Should().Be(errorCode); + } + + if (_announcementsResponse != null) + { + _announcementsResponse.Error.Should().NotBeNull(); + _announcementsResponse.Error!.Code.Should().Be(errorCode); + } + } +} diff --git a/Applications/AdminApi/test/AdminApi.Tests.Integration/StepDefinitions/LogsStepDefinitions.cs b/Applications/AdminApi/test/AdminApi.Tests.Integration/StepDefinitions/LogsStepDefinitions.cs deleted file mode 100644 index 783320df09..0000000000 --- a/Applications/AdminApi/test/AdminApi.Tests.Integration/StepDefinitions/LogsStepDefinitions.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Backbone.AdminApi.Sdk.Endpoints.Logs.Types.Requests; -using Backbone.AdminApi.Tests.Integration.Configuration; -using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types; -using Microsoft.Extensions.Options; - -namespace Backbone.AdminApi.Tests.Integration.StepDefinitions; - -[Binding] -[Scope(Feature = "POST Log")] -internal class LogsStepDefinitions : BaseStepDefinitions -{ - private ApiResponse? _postResponse; - - public LogsStepDefinitions(HttpClientFactory factory, IOptions options) : base(factory, options) - { - } - - [When("a POST request is sent to the /Logs endpoint")] - public async Task WhenAPOSTRequestIsSentToTheLogsEndpoint() - { - _postResponse = await _client.Logs.CreateLog(new LogRequest - { - LogLevel = LogLevel.Trace, - Category = "Test Category", - MessageTemplate = "The log request {0} has the following description: {1}", - Arguments = ["Request Name", "Request Description"] - }); - } - - [When("a POST request is sent to the /Logs endpoint with an invalid Log Level")] - public async Task WhenAPOSTRequestIsSentToTheLogsEndpointWithAnInvalidLogLevel() - { - _postResponse = await _client.Logs.CreateLog(new LogRequest - { - LogLevel = (LogLevel)16, - Category = "Test Category", - MessageTemplate = "The log request {0} has the following description: {1}", - Arguments = ["Request Name", "Request Description"] - }); - } - - [Then(@"the response status code is (\d+) \(.+\)")] - public void ThenTheResponseStatusCodeIs(int expectedStatusCode) - { - if (_postResponse != null) - ((int)_postResponse!.Status).Should().Be(expectedStatusCode); - } - - [Then(@"the response content contains an error with the error code ""([^""]+)""")] - public void ThenTheResponseContentIncludesAnErrorWithTheErrorCode(string errorCode) - { - _postResponse!.Error.Should().NotBeNull(); - _postResponse.Error!.Code.Should().Be(errorCode); - } -} diff --git a/Applications/AdminUi/apps/admin_ui/.gitignore b/Applications/AdminUi/apps/admin_ui/.gitignore index 29a3a5017f..717abbc27d 100644 --- a/Applications/AdminUi/apps/admin_ui/.gitignore +++ b/Applications/AdminUi/apps/admin_ui/.gitignore @@ -30,6 +30,7 @@ migrate_working_dir/ .pub-cache/ .pub/ /build/ +pubspec_overrides.yaml # Symbolication related app.*.symbols diff --git a/Applications/AdminUi/apps/admin_ui/pubspec_overrides.yaml b/Applications/AdminUi/apps/admin_ui/pubspec_overrides.yaml deleted file mode 100644 index 4026bd53fa..0000000000 --- a/Applications/AdminUi/apps/admin_ui/pubspec_overrides.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# melos_managed_dependency_overrides: admin_api_sdk,admin_api_types -dependency_overrides: - admin_api_sdk: - path: ../../packages/admin_api_sdk - admin_api_types: - path: ../../packages/admin_api_types diff --git a/Applications/AdminUi/packages/admin_api_sdk/pubspec_overrides.yaml b/Applications/AdminUi/packages/admin_api_sdk/pubspec_overrides.yaml deleted file mode 100644 index 76f93a465b..0000000000 --- a/Applications/AdminUi/packages/admin_api_sdk/pubspec_overrides.yaml +++ /dev/null @@ -1,4 +0,0 @@ -# melos_managed_dependency_overrides: admin_api_types -dependency_overrides: - admin_api_types: - path: ../admin_api_types diff --git a/Applications/ConsumerApi/src/Configuration/BackboneConfiguration.cs b/Applications/ConsumerApi/src/Configuration/BackboneConfiguration.cs index 0ec14cdad7..497845e18e 100644 --- a/Applications/ConsumerApi/src/Configuration/BackboneConfiguration.cs +++ b/Applications/ConsumerApi/src/Configuration/BackboneConfiguration.cs @@ -18,8 +18,6 @@ public class BackboneConfiguration public CorsConfiguration Cors { get; set; } = new(); - public SwaggerUiConfiguration SwaggerUi { get; set; } = new(); - [Required] public BackboneInfrastructureConfiguration Infrastructure { get; set; } = new(); @@ -41,14 +39,6 @@ public class CorsConfiguration public string ExposedHeaders { get; set; } = ""; } - public class SwaggerUiConfiguration - { - [Required] - public bool Enabled { get; set; } = false; - - public string TokenUrl { get; set; } = ""; - } - public class BackboneInfrastructureConfiguration { [Required] diff --git a/Applications/ConsumerApi/src/ConsumerApi.csproj b/Applications/ConsumerApi/src/ConsumerApi.csproj index 1580acf630..0843297ee7 100644 --- a/Applications/ConsumerApi/src/ConsumerApi.csproj +++ b/Applications/ConsumerApi/src/ConsumerApi.csproj @@ -13,10 +13,9 @@ - - - + + @@ -30,6 +29,7 @@ + diff --git a/Applications/ConsumerApi/src/Controllers/AuthorizationController.cs b/Applications/ConsumerApi/src/Controllers/AuthorizationController.cs index 8809c3d458..e30f5a06e4 100644 --- a/Applications/ConsumerApi/src/Controllers/AuthorizationController.cs +++ b/Applications/ConsumerApi/src/Controllers/AuthorizationController.cs @@ -49,7 +49,7 @@ public async Task Exchange() ApplicationErrors.Authentication.InvalidOAuthRequest("missing username")); var user = await _userManager.FindByNameAsync(request.Username!); - if (user == null) + if (user == null || user.Device.Identity.IsGracePeriodOver) return InvalidUserCredentials(); if (request.Password.IsNullOrEmpty()) diff --git a/Applications/ConsumerApi/src/DevicesDbContextSeeder.cs b/Applications/ConsumerApi/src/DevicesDbContextSeeder.cs index 64305fcc65..dc56496b0e 100644 --- a/Applications/ConsumerApi/src/DevicesDbContextSeeder.cs +++ b/Applications/ConsumerApi/src/DevicesDbContextSeeder.cs @@ -6,7 +6,6 @@ using Backbone.Modules.Devices.Domain.Aggregates.Tier; using Backbone.Modules.Devices.Infrastructure.Persistence.Database; using MediatR; -using Microsoft.EntityFrameworkCore; namespace Backbone.ConsumerApi; @@ -26,11 +25,9 @@ public async Task SeedAsync(DevicesDbContext context) private async Task SeedEverything(DevicesDbContext context) { - await context.Database.EnsureCreatedAsync(); - await SeedBasicTier(context); await SeedQueuedForDeletionTier(); - await SeedApplicationUsers(context); + await SeedApplicationUsers(); } private static async Task GetBasicTier(DevicesDbContext context) @@ -38,11 +35,8 @@ private async Task SeedEverything(DevicesDbContext context) return await context.Tiers.GetBasicTier(CancellationToken.None); } - private async Task SeedApplicationUsers(DevicesDbContext context) + private async Task SeedApplicationUsers() { - if (await context.Users.AnyAsync()) - return; - await _mediator.Send(new SeedTestUsersCommand()); } diff --git a/Applications/ConsumerApi/src/Extensions/IServiceCollectionExtensions.cs b/Applications/ConsumerApi/src/Extensions/IServiceCollectionExtensions.cs index 685a89f2df..8fc76489bb 100644 --- a/Applications/ConsumerApi/src/Extensions/IServiceCollectionExtensions.cs +++ b/Applications/ConsumerApi/src/Extensions/IServiceCollectionExtensions.cs @@ -14,10 +14,8 @@ using Backbone.Modules.Devices.Infrastructure.Persistence.Database; using Backbone.Modules.Devices.Infrastructure.PushNotifications.Connectors.Sse; using FluentValidation; -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.OpenApi.Models; using OpenIddict.Validation.AspNetCore; using PublicKey = Backbone.Modules.Devices.Application.Devices.DTOs.PublicKey; @@ -167,39 +165,6 @@ public static IServiceCollection AddCustomFluentValidation(this IServiceCollecti return services; } - - public static IServiceCollection AddCustomSwaggerUi(this IServiceCollection services, - BackboneConfiguration.SwaggerUiConfiguration configuration) - { - services - .AddEndpointsApiExplorer() - .AddSwaggerGen(c => - { - var securityScheme = new OpenApiSecurityScheme - { - Type = SecuritySchemeType.OAuth2, - Flows = new OpenApiOAuthFlows { Password = new OpenApiOAuthFlow { TokenUrl = new Uri(configuration.TokenUrl) } }, - Name = "Authorization", - In = ParameterLocation.Header, - Scheme = "bearer", - UnresolvedReference = false, - BearerFormat = "JWT", - Reference = new OpenApiReference - { - Id = JwtBearerDefaults.AuthenticationScheme, - Type = ReferenceType.SecurityScheme - } - }; - - c.AddSecurityDefinition(securityScheme.Reference.Id, securityScheme); - c.AddSecurityRequirement(new OpenApiSecurityRequirement - { - { securityScheme, Array.Empty() } - }); - }); - - return services; - } } public class SseServerHealthCheck : IHealthCheck diff --git a/Applications/ConsumerApi/src/Program.cs b/Applications/ConsumerApi/src/Program.cs index 0f2df63a56..da4caf932e 100644 --- a/Applications/ConsumerApi/src/Program.cs +++ b/Applications/ConsumerApi/src/Program.cs @@ -11,6 +11,8 @@ using Backbone.ConsumerApi.Configuration; using Backbone.ConsumerApi.Extensions; using Backbone.Infrastructure.EventBus; +using Backbone.Modules.Announcements.ConsumerApi; +using Backbone.Modules.Announcements.Infrastructure.Persistence.Database; using Backbone.Modules.Challenges.ConsumerApi; using Backbone.Modules.Challenges.Infrastructure.Persistence.Database; using Backbone.Modules.Devices.ConsumerApi; @@ -102,6 +104,7 @@ static WebApplication CreateApp(string[] args) if ((app.Environment.IsLocal() || app.Environment.IsDevelopment()) && app.Configuration.GetValue("RunMigrations")) { app + .MigrateDbContext() .MigrateDbContext() .MigrateDbContext() .MigrateDbContext() @@ -141,6 +144,7 @@ static void ConfigureServices(IServiceCollection services, IConfiguration config services.AddTransient(); services + .AddModule(configuration) .AddModule(configuration) .AddModule(configuration) .AddModule(configuration) @@ -162,9 +166,6 @@ static void ConfigureServices(IServiceCollection services, IConfiguration config .AddCustomFluentValidation() .AddCustomOpenIddict(parsedConfiguration.Authentication); - if (parsedConfiguration.SwaggerUi.Enabled) - services.AddCustomSwaggerUi(parsedConfiguration.SwaggerUi); - services.Configure(options => { options.ForwardedHeaders = @@ -202,9 +203,6 @@ static void Configure(WebApplication app) var configuration = app.Services.GetRequiredService>().Value; - if (configuration.SwaggerUi.Enabled) - app.UseSwagger().UseSwaggerUI(); - if (app.Environment.IsDevelopment()) IdentityModelEventSource.ShowPII = true; diff --git a/Applications/ConsumerApi/src/QuotasDbContextSeeder.cs b/Applications/ConsumerApi/src/QuotasDbContextSeeder.cs index 9fd20aa713..9aa99625ef 100644 --- a/Applications/ConsumerApi/src/QuotasDbContextSeeder.cs +++ b/Applications/ConsumerApi/src/QuotasDbContextSeeder.cs @@ -21,8 +21,6 @@ public async Task SeedAsync(QuotasDbContext context) private async Task SeedEverything(QuotasDbContext context) { - await context.Database.EnsureCreatedAsync(); - await SeedQueuedForDeletionTierMetrics(); } diff --git a/Applications/ConsumerApi/src/appsettings.json b/Applications/ConsumerApi/src/appsettings.json index bb10720f50..eaab20ca3d 100644 --- a/Applications/ConsumerApi/src/appsettings.json +++ b/Applications/ConsumerApi/src/appsettings.json @@ -3,9 +3,6 @@ "Authentication": { "JwtLifetimeInSeconds": 300 }, - "SwaggerUi": { - "Enabled": false - }, "Infrastructure": { "EventBus": { "SubscriptionClientName": "consumerapi", @@ -20,6 +17,19 @@ } }, "Modules": { + "Announcements": { + "Application": { + "Pagination": { + "DefaultPageSize": 50, + "MaxPageSize": 200 + } + }, + "Infrastructure": { + "SqlDatabase": { + "EnableHealthCheck": true + } + } + }, "Challenges": { "Infrastructure": { "SqlDatabase": { diff --git a/Applications/ConsumerApi/src/http/Tokens/Create Token.bru b/Applications/ConsumerApi/src/http/Tokens/Create Token.bru index d824ae2b52..87ef189443 100644 --- a/Applications/ConsumerApi/src/http/Tokens/Create Token.bru +++ b/Applications/ConsumerApi/src/http/Tokens/Create Token.bru @@ -13,6 +13,7 @@ post { body:json { { "content": "AAAA", - "expiresAt": "2024-12-17" + "expiresAt": "2024-12-17", + "forIdentity": "did:e:localhost:dids:8234cca0160ff05c785636" } } diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/ConsumerApi.Tests.Integration.csproj b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/ConsumerApi.Tests.Integration.csproj index 173014666d..97be10a47d 100644 --- a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/ConsumerApi.Tests.Integration.csproj +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/ConsumerApi.Tests.Integration.csproj @@ -15,7 +15,7 @@ - + diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Challenges/POST.feature b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Challenges/POST.feature index 68fbab1356..d4cf042591 100644 --- a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Challenges/POST.feature +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Challenges/POST.feature @@ -4,7 +4,7 @@ Feature: POST /Challenges User creates a Challenge Scenario: Creating a Challenge as an anonymous user - When an anonymous user sends a POST request is sent to the /Challenges endpoint + When an anonymous user sends a POST request to the /Challenges endpoint Then the response status code is 201 (Created) And the response contains a Challenge And the Challenge has an expiration date in the future diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Devices/POST.feature b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Devices/POST.feature index 5d0874657b..1ab45b568f 100644 --- a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Devices/POST.feature +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Devices/POST.feature @@ -9,3 +9,18 @@ User creates a Device When i sends a POST request to the /Devices endpoint with a valid signature on c Then the response status code is 201 (Created) And the response contains a Device + + Scenario: Registering a backup Device + Given Identity i + And a Challenge c created by i + When i sends a POST request to the /Devices endpoint with a valid signature on c as a backup Device + Then the response status code is 201 (Created) + And the response contains a Device + And the created Device is a backup Device + + Scenario: Registering a second backup Device is not possible + Given an Identity i with a Device d1 and a backup Device d2 + And a Challenge c created by i + When i sends a POST request to the /Devices endpoint with a valid signature on c as a backup Device + Then the response status code is 400 (Bad Request) + And the response content contains an error with the error code "error.platform.validation.device.onlyOneBackupDeviceCanExist" diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Identities/IsDeleted/GET.feature b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Identities/IsDeleted/GET.feature new file mode 100644 index 0000000000..656071fd64 --- /dev/null +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Identities/IsDeleted/GET.feature @@ -0,0 +1,12 @@ +@Integration +Feature: GET /Identities/IsDeleted + +User wants to know whether its identity was deleted + + Scenario: Asking whether a not-yet-deleted identity was deleted + Given an Identity i with a Device d + And an active deletion process for i exists + When an anonymous user sends a GET request to the /Identities/IsDeleted endpoint with d.Username + Then the response status code is 200 (OK) + And the response says that the identity was not deleted + And the deletion date is not set diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/ChallengesStepDefinitions.cs b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/ChallengesStepDefinitions.cs index 51041aef7f..1ddfbbff7c 100644 --- a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/ChallengesStepDefinitions.cs +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/ChallengesStepDefinitions.cs @@ -48,7 +48,7 @@ public async Task GivenAChallengeCreatedByAnAnonymousUser(string challengeName) #region When - [When("an anonymous user sends a POST request is sent to the /Challenges endpoint")] + [When("an anonymous user sends a POST request to the /Challenges endpoint")] public async Task WhenAnAnonymousUserSendsAPostRequestIsSentToTheChallengesEndpoint() { var client = _clientPool.Anonymous; diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/DevicesStepDefinitions.cs b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/DevicesStepDefinitions.cs index 02713c1487..75d203e329 100644 --- a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/DevicesStepDefinitions.cs +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/DevicesStepDefinitions.cs @@ -1,6 +1,8 @@ -using Backbone.ConsumerApi.Sdk; +using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types; +using Backbone.ConsumerApi.Sdk; using Backbone.ConsumerApi.Sdk.Authentication; using Backbone.ConsumerApi.Sdk.Endpoints.Devices.Types.Requests; +using Backbone.ConsumerApi.Sdk.Endpoints.Devices.Types.Responses; using Backbone.ConsumerApi.Tests.Integration.Configuration; using Backbone.ConsumerApi.Tests.Integration.Contexts; using Backbone.ConsumerApi.Tests.Integration.Helpers; @@ -24,6 +26,8 @@ internal class DevicesStepDefinitions private readonly ResponseContext _responseContext; private readonly ClientPool _clientPool; + private ApiResponse? _registerDeviceResponse; + public DevicesStepDefinitions(ChallengesContext challengesContext, ResponseContext responseContext, HttpClientFactory factory, IOptions httpConfiguration, ClientPool clientPool) { @@ -51,8 +55,17 @@ public async Task GivenAnIdentityWithADeviceAndAnUnonboardedDevice(string identi { var client = await Client.CreateForNewIdentity(_httpClient, _clientCredentials, DEVICE_PASSWORD); _clientPool.Add(client).ForIdentity(identityName).AndDevice(onboardedDeviceName); - var clientForUnOnboardedDevice = await client.OnboardNewDevice("Passw0rd"); - _clientPool.Add(clientForUnOnboardedDevice).ForIdentity(identityName).AndDevice(unonboardedDeviceName); + var clientForBackupDevice = await client.OnboardNewDevice("Passw0rd"); + _clientPool.Add(clientForBackupDevice).ForIdentity(identityName).AndDevice(unonboardedDeviceName); + } + + [Given($"an Identity {RegexFor.SINGLE_THING} with a Device {RegexFor.SINGLE_THING} and a backup Device {RegexFor.SINGLE_THING}")] + public async Task GivenAnIdentityWithADeviceAndABackupDevice(string identityName, string onboardedDeviceName, string backupDeviceName) + { + var client = await Client.CreateForNewIdentity(_httpClient, _clientCredentials, DEVICE_PASSWORD); + _clientPool.Add(client).ForIdentity(identityName).AndDevice(onboardedDeviceName); + var clientForBackupDevice = await client.OnboardNewBackupDevice("Passw0rd"); + _clientPool.Add(clientForBackupDevice).ForIdentity(identityName).AndDevice(backupDeviceName); } [Given($"an Identity {RegexFor.SINGLE_THING} with Devices {RegexFor.LIST_OF_THINGS}")] @@ -82,10 +95,25 @@ public async Task WhenIdentitySendsAPostRequestToTheDevicesEndpointWithASignedCh var identity = _clientPool.FirstForIdentityName(identityName); var signedChallenge = CreateSignedChallenge(identity, _challengesContext.Challenges[challengeName]); - _responseContext.WhenResponse = await identity.Devices.RegisterDevice(new RegisterDeviceRequest + _responseContext.WhenResponse = _registerDeviceResponse = await identity.Devices.RegisterDevice(new RegisterDeviceRequest + { + DevicePassword = DEVICE_PASSWORD, + SignedChallenge = signedChallenge, + IsBackupDevice = false + }); + } + + [When($"{RegexFor.SINGLE_THING} sends a POST request to the /Devices endpoint with a valid signature on {RegexFor.SINGLE_THING} as a backup Device")] + public async Task WhenIdentitySendsAPostRequestToTheDevicesEndpointWithASignedChallengeAsABackupDevice(string identityName, string challengeName) + { + var identity = _clientPool.FirstForIdentityName(identityName); + var signedChallenge = CreateSignedChallenge(identity, _challengesContext.Challenges[challengeName]); + + _responseContext.WhenResponse = _registerDeviceResponse = await identity.Devices.RegisterDevice(new RegisterDeviceRequest { DevicePassword = DEVICE_PASSWORD, - SignedChallenge = signedChallenge + SignedChallenge = signedChallenge, + IsBackupDevice = true }); } @@ -174,5 +202,11 @@ public async Task ThenTheBackboneHasPersistedAsTheNewCommunicationLanguageOfDevi response.Result!.First().CommunicationLanguage.Should().Be(_communicationLanguage); } + [Then("the created Device is a backup Device")] + public void ThenTheCreatedDeviceIsABackupDevice() + { + _registerDeviceResponse!.Result!.IsBackupDevice.Should().BeTrue(); + } + #endregion } diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/IdentitiesStepDefinitions.cs b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/IdentitiesStepDefinitions.cs index d98d291ff2..698342500f 100644 --- a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/IdentitiesStepDefinitions.cs +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/IdentitiesStepDefinitions.cs @@ -1,8 +1,10 @@ using Backbone.BuildingBlocks.SDK.Crypto; +using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types; using Backbone.ConsumerApi.Sdk; using Backbone.ConsumerApi.Sdk.Authentication; using Backbone.ConsumerApi.Sdk.Endpoints.Devices.Types; using Backbone.ConsumerApi.Sdk.Endpoints.Identities.Types.Requests; +using Backbone.ConsumerApi.Sdk.Endpoints.Identities.Types.Responses; using Backbone.ConsumerApi.Tests.Integration.Configuration; using Backbone.ConsumerApi.Tests.Integration.Contexts; using Backbone.ConsumerApi.Tests.Integration.Helpers; @@ -27,6 +29,8 @@ internal class IdentitiesStepDefinitions private readonly ChallengesContext _challengesContext; private readonly ClientPool _clientPool; + private ApiResponse? _isDeletedResponse; + public IdentitiesStepDefinitions(ResponseContext responseContext, ChallengesContext challengesContext, ClientPool clientPool, HttpClientFactory factory, IOptions httpConfiguration) { @@ -91,5 +95,31 @@ public async Task WhenAPostRequestIsSentToTheIdentitiesEndpointWithAValidSignatu _responseContext.WhenResponse = await _clientPool.Anonymous.Identities.CreateIdentity(createIdentityPayload); } + [When($@"an anonymous user sends a GET request to the /Identities/IsDeleted endpoint with {RegexFor.SINGLE_THING}.Username")] + public async Task WhenAnAnonymousUserSendsAGETRequestToTheIdentitiesIsDeletedEndpointWithDUsername(string deviceName) + { + var client = _clientPool.GetForDeviceName(deviceName); + + var device = await client.Devices.GetActiveDevice(); + + _responseContext.WhenResponse = _isDeletedResponse = await _clientPool.Anonymous.Identities.IsDeleted(device.Result!.Username); + } + + #endregion + + #region Then + + [Then(@"the response says that the identity was not deleted")] + public void ThenTheResponseSaysThatTheIdentityWasNotDeleted() + { + _isDeletedResponse!.Result!.IsDeleted.Should().BeFalse(); + } + + [Then(@"the deletion date is not set")] + public void ThenTheDeletionDateIsNotSet() + { + _isDeletedResponse!.Result!.DeletionDate.Should().BeNull(); + } + #endregion } diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Performance/package-lock.json b/Applications/ConsumerApi/test/ConsumerApi.Tests.Performance/package-lock.json index 7bfbdc93bd..50fa3cc972 100644 --- a/Applications/ConsumerApi/test/ConsumerApi.Tests.Performance/package-lock.json +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Performance/package-lock.json @@ -22,9 +22,9 @@ "copy-webpack-plugin": "12.0.2", "eslint": "^8.57.0", "papaparse": "^5.4.1", - "prettier": "3.3.3", + "prettier": "3.4.2", "typescript": "5.7.2", - "webpack": "5.96.1", + "webpack": "5.97.1", "webpack-cli": "5.1.4", "webpack-glob-entries": "1.0.1" } @@ -2481,148 +2481,163 @@ "dev": true }, "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", - "dev": true + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", - "dev": true + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", - "dev": true + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", - "dev": true + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, + "license": "MIT", "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", - "dev": true + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, + "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -2674,13 +2689,15 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/abbrev": { "version": "1.1.1", @@ -5068,10 +5085,11 @@ } }, "node_modules/prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true, + "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, @@ -5946,17 +5964,17 @@ } }, "node_modules/webpack": { - "version": "5.96.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", - "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "dev": true, "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.14.0", "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Performance/package.json b/Applications/ConsumerApi/test/ConsumerApi.Tests.Performance/package.json index 23cda8d939..8a79410ff0 100644 --- a/Applications/ConsumerApi/test/ConsumerApi.Tests.Performance/package.json +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Performance/package.json @@ -17,9 +17,9 @@ "copy-webpack-plugin": "12.0.2", "eslint": "^8.57.0", "papaparse": "^5.4.1", - "prettier": "3.3.3", + "prettier": "3.4.2", "typescript": "5.7.2", - "webpack": "5.96.1", + "webpack": "5.97.1", "webpack-cli": "5.1.4", "webpack-glob-entries": "1.0.1" }, diff --git a/Applications/DatabaseMigrator/src/DatabaseMigrator/DatabaseMigrator.csproj b/Applications/DatabaseMigrator/src/DatabaseMigrator/DatabaseMigrator.csproj index 7d5f29e80d..1df4458cf4 100644 --- a/Applications/DatabaseMigrator/src/DatabaseMigrator/DatabaseMigrator.csproj +++ b/Applications/DatabaseMigrator/src/DatabaseMigrator/DatabaseMigrator.csproj @@ -14,14 +14,14 @@ - + - - + + @@ -32,6 +32,9 @@ + + + diff --git a/Applications/DatabaseMigrator/src/DatabaseMigrator/Dockerfile b/Applications/DatabaseMigrator/src/DatabaseMigrator/Dockerfile index 8c04e7e1ca..6e9233e97d 100644 --- a/Applications/DatabaseMigrator/src/DatabaseMigrator/Dockerfile +++ b/Applications/DatabaseMigrator/src/DatabaseMigrator/Dockerfile @@ -6,61 +6,79 @@ WORKDIR /src COPY ["Directory.Build.props", "."] COPY ["Modules/Directory.Build.props", "Modules/"] + COPY ["Applications/DatabaseMigrator/src/DatabaseMigrator/DatabaseMigrator.csproj", "Applications/DatabaseMigrator/src/DatabaseMigrator/"] + COPY ["Applications/AdminApi/src/AdminApi.Infrastructure/AdminApi.Infrastructure.csproj", "Applications/AdminApi/src/AdminApi.Infrastructure/"] COPY ["Applications/AdminApi/src/AdminApi.Infrastructure.Database.Postgres/AdminApi.Infrastructure.Database.Postgres.csproj", "Applications/AdminApi/src/AdminApi.Infrastructure.Database.Postgres/"] COPY ["Applications/AdminApi/src/AdminApi.Infrastructure.Database.SqlServer/AdminApi.Infrastructure.Database.SqlServer.csproj", "Applications/AdminApi/src/AdminApi.Infrastructure.Database.SqlServer/"] +COPY ["Applications/AdminApi/src/AdminApi.Infrastructure.Database.SqlServer/AdminApi.Infrastructure.Database.SqlServer.csproj", "Applications/AdminApi/src/AdminApi.Infrastructure.Database.SqlServer/"] +COPY ["Applications/AdminApi/src/AdminApi.Infrastructure.Database.Postgres/AdminApi.Infrastructure.Database.Postgres.csproj", "Applications/AdminApi/src/AdminApi.Infrastructure.Database.Postgres/"] + COPY ["BuildingBlocks/src/BuildingBlocks.Infrastructure/BuildingBlocks.Infrastructure.csproj", "BuildingBlocks/src/BuildingBlocks.Infrastructure/"] COPY ["BuildingBlocks/src/BuildingBlocks.Application.Abstractions/BuildingBlocks.Application.Abstractions.csproj", "BuildingBlocks/src/BuildingBlocks.Application.Abstractions/"] COPY ["BuildingBlocks/src/DevelopmentKit.Identity/DevelopmentKit.Identity.csproj", "BuildingBlocks/src/DevelopmentKit.Identity/"] COPY ["BuildingBlocks/src/BuildingBlocks.Domain/BuildingBlocks.Domain.csproj", "BuildingBlocks/src/BuildingBlocks.Domain/"] COPY ["BuildingBlocks/src/Tooling/Tooling.csproj", "BuildingBlocks/src/Tooling/"] -COPY ["Modules/Tokens/src/Tokens.Infrastructure.Database.SqlServer/Tokens.Infrastructure.Database.SqlServer.csproj", "Modules/Tokens/src/Tokens.Infrastructure.Database.SqlServer/"] -COPY ["Modules/Tokens/src/Tokens.Infrastructure/Tokens.Infrastructure.csproj", "Modules/Tokens/src/Tokens.Infrastructure/"] -COPY ["Modules/Tokens/src/Tokens.Application/Tokens.Application.csproj", "Modules/Tokens/src/Tokens.Application/"] COPY ["BuildingBlocks/src/BuildingBlocks.Application/BuildingBlocks.Application.csproj", "BuildingBlocks/src/BuildingBlocks.Application/"] +COPY ["BuildingBlocks/src/Crypto/Crypto.csproj", "BuildingBlocks/src/Crypto/"] + COPY ["Common/src/Common.Infrastructure/Common.Infrastructure.csproj", "Common/src/Common.Infrastructure/"] -COPY ["Modules/Tokens/src/Tokens.Domain/Tokens.Domain.csproj", "Modules/Tokens/src/Tokens.Domain/"] -COPY ["Modules/Tokens/src/Tokens.Infrastructure.Database.Postgres/Tokens.Infrastructure.Database.Postgres.csproj", "Modules/Tokens/src/Tokens.Infrastructure.Database.Postgres/"] -COPY ["Modules/Synchronization/src/Synchronization.Infrastructure.Database.SqlServer/Synchronization.Infrastructure.Database.SqlServer.csproj", "Modules/Synchronization/src/Synchronization.Infrastructure.Database.SqlServer/"] -COPY ["Modules/Synchronization/src/Synchronization.Infrastructure/Synchronization.Infrastructure.csproj", "Modules/Synchronization/src/Synchronization.Infrastructure/"] -COPY ["Modules/Synchronization/src/Synchronization.Application/Synchronization.Application.csproj", "Modules/Synchronization/src/Synchronization.Application/"] -COPY ["Modules/Synchronization/src/Synchronization.Domain/Synchronization.Domain.csproj", "Modules/Synchronization/src/Synchronization.Domain/"] -COPY ["Modules/Synchronization/src/Synchronization.Infrastructure.Database.Postgres/Synchronization.Infrastructure.Database.Postgres.csproj", "Modules/Synchronization/src/Synchronization.Infrastructure.Database.Postgres/"] + +COPY ["Modules/Announcements/src/Announcements.Infrastructure/Announcements.Infrastructure.csproj", "Modules/Announcements/src/Announcements.Infrastructure/"] +COPY ["Modules/Announcements/src/Announcements.Application/Announcements.Application.csproj", "Modules/Announcements/src/Announcements.Application/"] +COPY ["Modules/Announcements/src/Announcements.Domain/Announcements.Domain.csproj", "Modules/Announcements/src/Announcements.Domain/"] +COPY ["Modules/Announcements/src/Announcements.Infrastructure.Database.SqlServer/Announcements.Infrastructure.Database.SqlServer.csproj", "Modules/Announcements/src/Announcements.Infrastructure.Database.SqlServer/"] +COPY ["Modules/Announcements/src/Announcements.Infrastructure.Database.Postgres/Announcements.Infrastructure.Database.Postgres.csproj", "Modules/Announcements/src/Announcements.Infrastructure.Database.Postgres/"] + COPY ["Modules/Challenges/src/Challenges.Infrastructure/Challenges.Infrastructure.csproj", "Modules/Challenges/src/Challenges.Infrastructure/"] COPY ["Modules/Challenges/src/Challenges.Application/Challenges.Application.csproj", "Modules/Challenges/src/Challenges.Application/"] COPY ["Modules/Challenges/src/Challenges.Domain/Challenges.Domain.csproj", "Modules/Challenges/src/Challenges.Domain/"] +COPY ["Modules/Challenges/src/Challenges.Infrastructure.Database.SqlServer/Challenges.Infrastructure.Database.SqlServer.csproj", "Modules/Challenges/src/Challenges.Infrastructure.Database.SqlServer/"] +COPY ["Modules/Challenges/src/Challenges.Infrastructure.Database.Postgres/Challenges.Infrastructure.Database.Postgres.csproj", "Modules/Challenges/src/Challenges.Infrastructure.Database.Postgres/"] + COPY ["Modules/Devices/src/Devices.Infrastructure/Devices.Infrastructure.csproj", "Modules/Devices/src/Devices.Infrastructure/"] COPY ["Modules/Devices/src/Devices.Application/Devices.Application.csproj", "Modules/Devices/src/Devices.Application/"] -COPY ["BuildingBlocks/src/Crypto/Crypto.csproj", "BuildingBlocks/src/Crypto/"] COPY ["Modules/Devices/src/Devices.Domain/Devices.Domain.csproj", "Modules/Devices/src/Devices.Domain/"] +COPY ["Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Devices.Infrastructure.Database.SqlServer.csproj", "Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/"] +COPY ["Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Devices.Infrastructure.Database.Postgres.csproj", "Modules/Devices/src/Devices.Infrastructure.Database.Postgres/"] + COPY ["Modules/Files/src/Files.Infrastructure/Files.Infrastructure.csproj", "Modules/Files/src/Files.Infrastructure/"] COPY ["Modules/Files/src/Files.Application/Files.Application.csproj", "Modules/Files/src/Files.Application/"] COPY ["Modules/Files/src/Files.Domain/Files.Domain.csproj", "Modules/Files/src/Files.Domain/"] +COPY ["Modules/Files/src/Files.Infrastructure.Database.SqlServer/Files.Infrastructure.Database.SqlServer.csproj", "Modules/Files/src/Files.Infrastructure.Database.SqlServer/"] +COPY ["Modules/Files/src/Files.Infrastructure.Database.Postgres/Files.Infrastructure.Database.Postgres.csproj", "Modules/Files/src/Files.Infrastructure.Database.Postgres/"] + COPY ["Modules/Messages/src/Messages.Infrastructure/Messages.Infrastructure.csproj", "Modules/Messages/src/Messages.Infrastructure/"] COPY ["Modules/Messages/src/Messages.Application/Messages.Application.csproj", "Modules/Messages/src/Messages.Application/"] COPY ["Modules/Messages/src/Messages.Domain/Messages.Domain.csproj", "Modules/Messages/src/Messages.Domain/"] +COPY ["Modules/Messages/src/Messages.Infrastructure.Database.SqlServer/Messages.Infrastructure.Database.SqlServer.csproj", "Modules/Messages/src/Messages.Infrastructure.Database.SqlServer/"] +COPY ["Modules/Messages/src/Messages.Infrastructure.Database.Postgres/Messages.Infrastructure.Database.Postgres.csproj", "Modules/Messages/src/Messages.Infrastructure.Database.Postgres/"] + COPY ["Modules/Quotas/src/Quotas.Infrastructure/Quotas.Infrastructure.csproj", "Modules/Quotas/src/Quotas.Infrastructure/"] COPY ["Modules/Quotas/src/Quotas.Application/Quotas.Application.csproj", "Modules/Quotas/src/Quotas.Application/"] COPY ["Modules/Quotas/src/Quotas.Domain/Quotas.Domain.csproj", "Modules/Quotas/src/Quotas.Domain/"] +COPY ["Modules/Quotas/src/Quotas.Infrastructure.Database.SqlServer/Quotas.Infrastructure.Database.SqlServer.csproj", "Modules/Quotas/src/Quotas.Infrastructure.Database.SqlServer/"] +COPY ["Modules/Quotas/src/Quotas.Infrastructure.Database.Postgres/Quotas.Infrastructure.Database.Postgres.csproj", "Modules/Quotas/src/Quotas.Infrastructure.Database.Postgres/"] + COPY ["Modules/Relationships/src/Relationships.Infrastructure/Relationships.Infrastructure.csproj", "Modules/Relationships/src/Relationships.Infrastructure/"] COPY ["Modules/Relationships/src/Relationships.Application/Relationships.Application.csproj", "Modules/Relationships/src/Relationships.Application/"] COPY ["Modules/Relationships/src/Relationships.Common/Relationships.Common.csproj", "Modules/Relationships/src/Relationships.Common/"] COPY ["Modules/Relationships/src/Relationships.Domain/Relationships.Domain.csproj", "Modules/Relationships/src/Relationships.Domain/"] COPY ["Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Relationships.Infrastructure.Database.SqlServer.csproj", "Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/"] COPY ["Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Relationships.Infrastructure.Database.Postgres.csproj", "Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/"] -COPY ["Modules/Quotas/src/Quotas.Infrastructure.Database.SqlServer/Quotas.Infrastructure.Database.SqlServer.csproj", "Modules/Quotas/src/Quotas.Infrastructure.Database.SqlServer/"] -COPY ["Modules/Quotas/src/Quotas.Infrastructure.Database.Postgres/Quotas.Infrastructure.Database.Postgres.csproj", "Modules/Quotas/src/Quotas.Infrastructure.Database.Postgres/"] -COPY ["Modules/Messages/src/Messages.Infrastructure.Database.SqlServer/Messages.Infrastructure.Database.SqlServer.csproj", "Modules/Messages/src/Messages.Infrastructure.Database.SqlServer/"] -COPY ["Modules/Messages/src/Messages.Infrastructure.Database.Postgres/Messages.Infrastructure.Database.Postgres.csproj", "Modules/Messages/src/Messages.Infrastructure.Database.Postgres/"] -COPY ["Modules/Files/src/Files.Infrastructure.Database.SqlServer/Files.Infrastructure.Database.SqlServer.csproj", "Modules/Files/src/Files.Infrastructure.Database.SqlServer/"] -COPY ["Modules/Files/src/Files.Infrastructure.Database.Postgres/Files.Infrastructure.Database.Postgres.csproj", "Modules/Files/src/Files.Infrastructure.Database.Postgres/"] -COPY ["Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Devices.Infrastructure.Database.SqlServer.csproj", "Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/"] -COPY ["Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Devices.Infrastructure.Database.Postgres.csproj", "Modules/Devices/src/Devices.Infrastructure.Database.Postgres/"] -COPY ["Modules/Challenges/src/Challenges.Infrastructure.Database.SqlServer/Challenges.Infrastructure.Database.SqlServer.csproj", "Modules/Challenges/src/Challenges.Infrastructure.Database.SqlServer/"] -COPY ["Modules/Challenges/src/Challenges.Infrastructure.Database.Postgres/Challenges.Infrastructure.Database.Postgres.csproj", "Modules/Challenges/src/Challenges.Infrastructure.Database.Postgres/"] -COPY ["Applications/AdminApi/src/AdminApi.Infrastructure.Database.SqlServer/AdminApi.Infrastructure.Database.SqlServer.csproj", "Applications/AdminApi/src/AdminApi.Infrastructure.Database.SqlServer/"] -COPY ["Applications/AdminApi/src/AdminApi.Infrastructure.Database.Postgres/AdminApi.Infrastructure.Database.Postgres.csproj", "Applications/AdminApi/src/AdminApi.Infrastructure.Database.Postgres/"] + +COPY ["Modules/Synchronization/src/Synchronization.Infrastructure.Database.SqlServer/Synchronization.Infrastructure.Database.SqlServer.csproj", "Modules/Synchronization/src/Synchronization.Infrastructure.Database.SqlServer/"] +COPY ["Modules/Synchronization/src/Synchronization.Infrastructure/Synchronization.Infrastructure.csproj", "Modules/Synchronization/src/Synchronization.Infrastructure/"] +COPY ["Modules/Synchronization/src/Synchronization.Application/Synchronization.Application.csproj", "Modules/Synchronization/src/Synchronization.Application/"] +COPY ["Modules/Synchronization/src/Synchronization.Domain/Synchronization.Domain.csproj", "Modules/Synchronization/src/Synchronization.Domain/"] +COPY ["Modules/Synchronization/src/Synchronization.Infrastructure.Database.Postgres/Synchronization.Infrastructure.Database.Postgres.csproj", "Modules/Synchronization/src/Synchronization.Infrastructure.Database.Postgres/"] + +COPY ["Modules/Tokens/src/Tokens.Infrastructure.Database.SqlServer/Tokens.Infrastructure.Database.SqlServer.csproj", "Modules/Tokens/src/Tokens.Infrastructure.Database.SqlServer/"] +COPY ["Modules/Tokens/src/Tokens.Infrastructure/Tokens.Infrastructure.csproj", "Modules/Tokens/src/Tokens.Infrastructure/"] +COPY ["Modules/Tokens/src/Tokens.Application/Tokens.Application.csproj", "Modules/Tokens/src/Tokens.Application/"] +COPY ["Modules/Tokens/src/Tokens.Domain/Tokens.Domain.csproj", "Modules/Tokens/src/Tokens.Domain/"] +COPY ["Modules/Tokens/src/Tokens.Infrastructure.Database.Postgres/Tokens.Infrastructure.Database.Postgres.csproj", "Modules/Tokens/src/Tokens.Infrastructure.Database.Postgres/"] RUN dotnet restore /p:ContinuousIntegrationBuild=true "Applications/DatabaseMigrator/src/DatabaseMigrator/DatabaseMigrator.csproj" diff --git a/Applications/DatabaseMigrator/src/DatabaseMigrator/IServiceCollectionExtensions.cs b/Applications/DatabaseMigrator/src/DatabaseMigrator/IServiceCollectionExtensions.cs index af7ae760b3..8f5a6236d9 100644 --- a/Applications/DatabaseMigrator/src/DatabaseMigrator/IServiceCollectionExtensions.cs +++ b/Applications/DatabaseMigrator/src/DatabaseMigrator/IServiceCollectionExtensions.cs @@ -4,6 +4,13 @@ public static class IServiceCollectionExtensions { public static void AddAllDbContexts(this IServiceCollection services, SqlDatabaseConfiguration databaseConfiguration) { + Modules.Announcements.Infrastructure.Persistence.Database.IServiceCollectionExtensions.AddDatabase(services, options => + { + options.Provider = databaseConfiguration.Provider; + options.ConnectionString = databaseConfiguration.ConnectionString; + options.CommandTimeout = databaseConfiguration.CommandTimeout; + }); + Modules.Challenges.Infrastructure.Persistence.IServiceCollectionExtensions.AddDatabase(services, options => { options.Provider = databaseConfiguration.Provider; diff --git a/Applications/DatabaseMigrator/src/DatabaseMigrator/MigrationReader.cs b/Applications/DatabaseMigrator/src/DatabaseMigrator/MigrationReader.cs index 3aadc54a35..f589931a6e 100644 --- a/Applications/DatabaseMigrator/src/DatabaseMigrator/MigrationReader.cs +++ b/Applications/DatabaseMigrator/src/DatabaseMigrator/MigrationReader.cs @@ -1,4 +1,5 @@ using Backbone.AdminApi.Infrastructure.Persistence.Database; +using Backbone.Modules.Announcements.Infrastructure.Persistence.Database; using Backbone.Modules.Challenges.Infrastructure.Persistence.Database; using Backbone.Modules.Devices.Infrastructure.Persistence.Database; using Backbone.Modules.Files.Infrastructure.Persistence.Database; @@ -26,7 +27,8 @@ public class MigrationReader typeof(TokensDbContext), typeof(MessagesDbContext), typeof(QuotasDbContext), - typeof(AdminApiDbContext) + typeof(AdminApiDbContext), + typeof(AnnouncementsDbContext) ]; public MigrationReader(DbContextProvider dbContextProvider) diff --git a/Applications/DatabaseMigrator/test/DatabaseMigrator.Tests/DatabaseMigrator.Tests.csproj b/Applications/DatabaseMigrator/test/DatabaseMigrator.Tests/DatabaseMigrator.Tests.csproj index 54d5ae5af4..7d57b3fe71 100644 --- a/Applications/DatabaseMigrator/test/DatabaseMigrator.Tests/DatabaseMigrator.Tests.csproj +++ b/Applications/DatabaseMigrator/test/DatabaseMigrator.Tests/DatabaseMigrator.Tests.csproj @@ -13,7 +13,7 @@ - + diff --git a/Applications/EventHandlerService/src/EventHandlerService/appsettings.json b/Applications/EventHandlerService/src/EventHandlerService/appsettings.json index e3f0a7d4e1..8a42f8eb43 100644 --- a/Applications/EventHandlerService/src/EventHandlerService/appsettings.json +++ b/Applications/EventHandlerService/src/EventHandlerService/appsettings.json @@ -10,6 +10,14 @@ } }, "Modules": { + "Announcements": { + "Application": { + "Pagination": { + "DefaultPageSize": 50, + "MaxPageSize": 200 + } + } + }, "Challenges": {}, "Quotas": { "Application": { diff --git a/Applications/IdentityDeletionJobs/src/Job.IdentityDeletion/Workers/ActualDeletionWorker.cs b/Applications/IdentityDeletionJobs/src/Job.IdentityDeletion/Workers/ActualDeletionWorker.cs index b13819b9fc..78b72ba49a 100644 --- a/Applications/IdentityDeletionJobs/src/Job.IdentityDeletion/Workers/ActualDeletionWorker.cs +++ b/Applications/IdentityDeletionJobs/src/Job.IdentityDeletion/Workers/ActualDeletionWorker.cs @@ -2,7 +2,9 @@ using Backbone.BuildingBlocks.Application.PushNotifications; using Backbone.BuildingBlocks.Domain.Errors; using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Devices.Application.Identities.Commands.HandleCompletedDeletionProcess; using Backbone.Modules.Devices.Application.Identities.Commands.TriggerRipeDeletionProcesses; +using Backbone.Modules.Devices.Application.Identities.Queries.GetIdentity; using Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.DeletionProcess; using CSharpFunctionalExtensions; using MediatR; @@ -78,10 +80,16 @@ await _pushNotificationSender.SendNotification( private async Task Delete(IdentityAddress identityAddress) { + var identity = await _mediator.Send(new GetIdentityQuery(identityAddress.Value)); + foreach (var identityDeleter in _identityDeleters) { await identityDeleter.Delete(identityAddress); } + + var usernames = identity.Devices.Select(d => d.Username); + + await _mediator.Send(new HandleCompletedDeletionProcessCommand(identityAddress.Value, usernames)); } private void LogErroringDeletionTriggers(IEnumerable>> erroringDeletionTriggers) diff --git a/Applications/IdentityDeletionJobs/src/Job.IdentityDeletion/appsettings.json b/Applications/IdentityDeletionJobs/src/Job.IdentityDeletion/appsettings.json index e3f0a7d4e1..8a42f8eb43 100644 --- a/Applications/IdentityDeletionJobs/src/Job.IdentityDeletion/appsettings.json +++ b/Applications/IdentityDeletionJobs/src/Job.IdentityDeletion/appsettings.json @@ -10,6 +10,14 @@ } }, "Modules": { + "Announcements": { + "Application": { + "Pagination": { + "DefaultPageSize": 50, + "MaxPageSize": 200 + } + } + }, "Challenges": {}, "Quotas": { "Application": { diff --git a/Applications/IdentityDeletionJobs/test/Job.IdentityDeletion.Tests/Tests/ActualDeletionWorkerTests.cs b/Applications/IdentityDeletionJobs/test/Job.IdentityDeletion.Tests/Tests/ActualDeletionWorkerTests.cs index feb5834646..7f4a1381bd 100644 --- a/Applications/IdentityDeletionJobs/test/Job.IdentityDeletion.Tests/Tests/ActualDeletionWorkerTests.cs +++ b/Applications/IdentityDeletionJobs/test/Job.IdentityDeletion.Tests/Tests/ActualDeletionWorkerTests.cs @@ -4,7 +4,10 @@ using Backbone.DevelopmentKit.Identity.ValueObjects; using Backbone.Job.IdentityDeletion.Workers; using Backbone.Modules.Devices.Application.Identities.Commands.TriggerRipeDeletionProcesses; +using Backbone.Modules.Devices.Application.Identities.Queries.GetIdentity; using Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.DeletionProcess; +using Backbone.Modules.Devices.Domain.Aggregates.Tier; +using Backbone.Modules.Devices.Domain.Entities.Identities; using Backbone.Modules.Relationships.Application.Relationships.Queries.FindRelationshipsOfIdentity; using CSharpFunctionalExtensions; using FakeItEasy; @@ -37,9 +40,9 @@ public async Task Calls_Deleters_For_Each_Identity() { // Arrange var fakeMediator = A.Fake(); - var identityAddress1 = CreateRandomIdentityAddress(); - var identityAddress2 = CreateRandomIdentityAddress(); - SetupRipeDeletionProcessesCommand(fakeMediator, identityAddress1, identityAddress2); + var identity1 = CreateIdentity(); + var identity2 = CreateIdentity(); + SetupRipeDeletionProcessesCommand(fakeMediator, identity1.Address, identity2.Address); var mockIdentityDeleter = A.Fake(); var worker = CreateWorker(fakeMediator, [mockIdentityDeleter]); @@ -47,12 +50,18 @@ public async Task Calls_Deleters_For_Each_Identity() A.CallTo(() => fakeMediator.Send(A._, A._)) .Returns(new FindRelationshipsOfIdentityResponse([])); + A.CallTo(() => fakeMediator.Send(A.That.Matches(q => q.Address == identity1.Address.Value), A._)) + .Returns(new GetIdentityResponse(identity1)); + + A.CallTo(() => fakeMediator.Send(A.That.Matches(q => q.Address == identity2.Address.Value), A._)) + .Returns(new GetIdentityResponse(identity2)); + // Act await worker.StartProcessing(CancellationToken.None); // Assert - A.CallTo(() => mockIdentityDeleter.Delete(identityAddress1)).MustHaveHappenedOnceExactly(); - A.CallTo(() => mockIdentityDeleter.Delete(identityAddress2)).MustHaveHappenedOnceExactly(); + A.CallTo(() => mockIdentityDeleter.Delete(identity1.Address)).MustHaveHappenedOnceExactly(); + A.CallTo(() => mockIdentityDeleter.Delete(identity2.Address)).MustHaveHappenedOnceExactly(); } [Fact] @@ -60,12 +69,17 @@ public async Task Sends_push_notification_to_each_deleted_identity() { // Arrange var fakeMediator = A.Fake(); - var identityAddress1 = CreateRandomIdentityAddress(); - var identityAddress2 = CreateRandomIdentityAddress(); - var identityAddress3 = CreateRandomIdentityAddress(); - SetupRipeDeletionProcessesCommand(fakeMediator, identityAddress1, identityAddress2, identityAddress3); + var identity1 = CreateIdentity(); + var identity2 = CreateIdentity(); + SetupRipeDeletionProcessesCommand(fakeMediator, identity1.Address, identity2.Address); A.CallTo(() => fakeMediator.Send(A._, A._)).Returns(new FindRelationshipsOfIdentityResponse([])); + A.CallTo(() => fakeMediator.Send(A.That.Matches(q => q.Address == identity1.Address.Value), A._)) + .Returns(new GetIdentityResponse(identity1)); + + A.CallTo(() => fakeMediator.Send(A.That.Matches(q => q.Address == identity2.Address.Value), A._)) + .Returns(new GetIdentityResponse(identity2)); + var mockPushNotificationSender = A.Fake(); var worker = CreateWorker(fakeMediator, [], mockPushNotificationSender); @@ -73,7 +87,7 @@ public async Task Sends_push_notification_to_each_deleted_identity() await worker.StartProcessing(CancellationToken.None); // Assert - foreach (var identityAddress in new[] { identityAddress1, identityAddress2, identityAddress3 }) + foreach (var identityAddress in new[] { identity1.Address, identity2.Address }) { A.CallTo(() => mockPushNotificationSender.SendNotification( A._, @@ -99,4 +113,15 @@ private static ActualDeletionWorker CreateWorker(IMediator mediator, var logger = A.Dummy>(); return new ActualDeletionWorker(hostApplicationLifetime, identityDeleters, mediator, pushNotificationSender, logger); } + + private static Identity CreateIdentity() + { + return new Identity( + CreateRandomDeviceId(), + CreateRandomIdentityAddress(), + CreateRandomBytes(), + TierId.Generate(), + 1, + CommunicationLanguage.DEFAULT_LANGUAGE); + } } diff --git a/Applications/SseServer/src/SseServer/Controllers/SseController.cs b/Applications/SseServer/src/SseServer/Controllers/SseController.cs index 569a06bbb6..deb1806f1f 100644 --- a/Applications/SseServer/src/SseServer/Controllers/SseController.cs +++ b/Applications/SseServer/src/SseServer/Controllers/SseController.cs @@ -1,5 +1,4 @@ -using Backbone.BuildingBlocks.API; -using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext; +using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext; using Backbone.Modules.Devices.Application.PushNotifications.Commands.DeleteDeviceRegistration; using Backbone.Modules.Devices.Application.PushNotifications.Commands.UpdateDeviceRegistration; using MediatR; @@ -25,7 +24,7 @@ public SseController(IEventQueue eventQueue, IUserContext userContext, IMediator [HttpGet("/api/v1/sse")] [Authorize] - public async Task Subscribe() + public async Task Subscribe(CancellationToken cancellationToken) { var address = _userContext.GetAddress().Value; @@ -34,7 +33,7 @@ await _mediator.Send(new UpdateDeviceRegistrationCommand Handle = "sse-handle", // this is just some dummy value; the SSE connector doesn't use it AppId = "sse-client", // this is just some dummy value; the SSE connector doesn't use it Platform = "sse" - }); + }, cancellationToken); Response.StatusCode = 200; Response.Headers.CacheControl = "no-cache"; @@ -56,8 +55,11 @@ await _mediator.Send(new UpdateDeviceRegistrationCommand } catch (ClientAlreadyRegisteredException) { - return BadRequest(HttpError.ForProduction("error.platform.sseClientAlreadyRegistered", - "An SSE client for your identity is already registered. You can only register once per identity.", "")); + // if it is already registered, everything is fine + } + catch (OperationCanceledException) + { + // this is expected when the client disconnects } catch (Exception ex) { @@ -66,10 +68,9 @@ await _mediator.Send(new UpdateDeviceRegistrationCommand finally { _eventQueue.Deregister(address); - await _mediator.Send(new DeleteDeviceRegistrationCommand()); + // we must NOT pass the cancellation token here, because otherwise the device registration would not be deleted in case the request was cancelled + await _mediator.Send(new DeleteDeviceRegistrationCommand(), CancellationToken.None); } - - return Ok(); } } diff --git a/Applications/SseServer/src/SseServer/Extensions/IServiceCollectionExtensions.cs b/Applications/SseServer/src/SseServer/Extensions/IServiceCollectionExtensions.cs index 8058bc0495..0a93c34720 100644 --- a/Applications/SseServer/src/SseServer/Extensions/IServiceCollectionExtensions.cs +++ b/Applications/SseServer/src/SseServer/Extensions/IServiceCollectionExtensions.cs @@ -65,7 +65,7 @@ public static void AddCustomAspNetCore(this IServiceCollection services, options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; }); - services.AddAuthentication().AddJwtBearer(options => + services.AddAuthentication().AddJwtBearer("default", options => { var privateKeyBytes = Convert.FromBase64String(configuration.Authentication.JwtSigningCertificate); #pragma warning disable SYSLIB0057 // The constructor is obsolete. But I didn't manage to get the suggested alternative to work. @@ -75,7 +75,13 @@ public static void AddCustomAspNetCore(this IServiceCollection services, options.TokenValidationParameters.ValidateIssuer = false; options.TokenValidationParameters.ValidateAudience = false; }); - services.AddAuthorization(); + + services.AddAuthorizationBuilder() + .AddDefaultPolicy("default", policy => + { + policy.AddAuthenticationSchemes("default"); + policy.RequireAuthenticatedUser(); + }); services.AddHttpContextAccessor(); diff --git a/Applications/SseServer/src/SseServer/Program.cs b/Applications/SseServer/src/SseServer/Program.cs index 8f88f495eb..6138e63518 100644 --- a/Applications/SseServer/src/SseServer/Program.cs +++ b/Applications/SseServer/src/SseServer/Program.cs @@ -13,7 +13,6 @@ using Backbone.Tooling.Extensions; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Logging; using Serilog; using Serilog.Exceptions; using Serilog.Exceptions.Core; @@ -75,7 +74,7 @@ static WebApplication CreateApp(string[] args) ) .UseServiceProviderFactory(new AutofacServiceProviderFactory()); - ConfigureServices(builder.Services, builder.Configuration); + ConfigureServices(builder.Services, builder.Configuration, builder.Environment); var app = builder.Build(); Configure(app); @@ -88,7 +87,7 @@ static WebApplication CreateApp(string[] args) return app; } -static void ConfigureServices(IServiceCollection services, IConfiguration configuration) +static void ConfigureServices(IServiceCollection services, IConfiguration configuration, IWebHostEnvironment environment) { services.ConfigureAndValidate(configuration.Bind); @@ -117,6 +116,8 @@ static void ConfigureServices(IServiceCollection services, IConfiguration config options.KnownProxies.Clear(); }); + services.AddCustomIdentity(environment); + services.AddPushNotifications(parsedConfiguration.Modules.Devices.Infrastructure.PushNotifications); } @@ -141,16 +142,11 @@ static void Configure(WebApplication app) .AddCustomHeader("X-Frame-Options", "Deny") ); - if (app.Environment.IsDevelopment()) - IdentityModelEventSource.ShowPII = true; - app.UseAuthentication().UseAuthorization(); app.MapControllers(); app.MapHealthChecks("/health"); - - app.UseResponseCaching(); } static void LoadConfiguration(WebApplicationBuilder webApplicationBuilder, string[] strings) diff --git a/Applications/SseServer/src/SseServer/SseServer.csproj b/Applications/SseServer/src/SseServer/SseServer.csproj index 19a740078f..5df9da8c92 100644 --- a/Applications/SseServer/src/SseServer/SseServer.csproj +++ b/Applications/SseServer/src/SseServer/SseServer.csproj @@ -6,8 +6,8 @@ - - + + diff --git a/Backbone.Tests.ArchUnit/Backbone.Tests.ArchUnit.csproj b/Backbone.Tests.ArchUnit/Backbone.Tests.ArchUnit.csproj index 61a73fe378..606d9f0136 100644 --- a/Backbone.Tests.ArchUnit/Backbone.Tests.ArchUnit.csproj +++ b/Backbone.Tests.ArchUnit/Backbone.Tests.ArchUnit.csproj @@ -8,7 +8,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Backbone.sln b/Backbone.sln index 7ec94c5c54..5905be68c5 100644 --- a/Backbone.sln +++ b/Backbone.sln @@ -235,7 +235,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Files.Infrastructure.Tests" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Messages.Domain.Tests", "Modules\Messages\test\Messages.Domain.Tests\Messages.Domain.Tests.csproj", "{83A86879-670B-4F22-8835-EE1D0AB49AA9}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PerformanceSnapshotCreator", "Applications\ConsumerApi\test\ConsumerApi.Tests.Performance\tools\snapshot-creator\ConsumerApi.Tests.Performance.SnapshotCreator.csproj", "{B74B8655-8A94-4520-8BAB-212D7123EDB4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsumerApi.Tests.Performance.SnapshotCreator", "Applications\ConsumerApi\test\ConsumerApi.Tests.Performance\tools\snapshot-creator\ConsumerApi.Tests.Performance.SnapshotCreator.csproj", "{B74B8655-8A94-4520-8BAB-212D7123EDB4}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tokens.Domain.Tests", "Modules\Tokens\test\Tokens.Domain.Tests\Tokens.Domain.Tests.csproj", "{EDCB84BE-54C3-4CAD-977E-45EEBEFA1402}" EndProject @@ -368,7 +368,25 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsumerApi.Sdk", "Sdks\Con EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{1BFEE2F6-D514-4458-994A-3DD106405990}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DatabaseMigrator.Tests", "Applications\DatabaseMigrator\test\DatabaseMigrator.Tests\DatabaseMigrator.Tests.csproj", "{553F1C8B-E099-4856-9416-67C103D3D194}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DatabaseMigrator.Tests", "Applications\DatabaseMigrator\test\DatabaseMigrator.Tests\DatabaseMigrator.Tests.csproj", "{553F1C8B-E099-4856-9416-67C103D3D194}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Announcements", "Announcements", "{DB8BF59A-7BA3-42B5-8CD7-4F9F2FD17AA2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D67D404E-1FE6-4CC6-B0F8-20DB87B9F23A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{399BA0C2-C130-4C8E-8F2D-8BB45AB9FD1A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Announcements.ConsumerApi", "Modules\Announcements\src\Announcements.ConsumerApi\Announcements.ConsumerApi.csproj", "{8D3E9953-BF24-4E8D-A337-90A5DE0B43D1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Announcements.Domain", "Modules\Announcements\src\Announcements.Domain\Announcements.Domain.csproj", "{5AEE6DCE-F448-4593-8897-6C8511D7F3F2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Announcements.Infrastructure", "Modules\Announcements\src\Announcements.Infrastructure\Announcements.Infrastructure.csproj", "{AF71E018-9D16-4C13-9646-6FACC474F138}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Announcements.Infrastructure.Database.Postgres", "Modules\Announcements\src\Announcements.Infrastructure.Database.Postgres\Announcements.Infrastructure.Database.Postgres.csproj", "{4176C717-C052-4A41-A8D1-4BA321205471}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Announcements.Infrastructure.Database.SqlServer", "Modules\Announcements\src\Announcements.Infrastructure.Database.SqlServer\Announcements.Infrastructure.Database.SqlServer.csproj", "{CAC08F1F-4187-46A8-8B77-7DD7A1022D66}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Announcements.Application", "Modules\Announcements\src\Announcements.Application\Announcements.Application.csproj", "{69F91E3A-3F97-4F08-A451-223B5BF12A07}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tags", "Tags", "{2E8887F1-55BD-4751-A522-0377C80DC70B}" EndProject @@ -384,6 +402,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tags.Infrastructure", "Modu EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Files.Domain.Tests", "Modules\Files\test\Files.Domain.Tests\Files.Domain.Tests.csproj", "{30402564-3CAA-4CB1-A0D5-1BF157BB5B65}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Announcements.Application.Tests", "Modules\Announcements\test\Announcements.Application.Tests\Announcements.Application.Tests.csproj", "{74CDB906-8BC9-42F7-A3FA-2B675A240D51}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -822,6 +842,30 @@ Global {553F1C8B-E099-4856-9416-67C103D3D194}.Debug|Any CPU.Build.0 = Debug|Any CPU {553F1C8B-E099-4856-9416-67C103D3D194}.Release|Any CPU.ActiveCfg = Release|Any CPU {553F1C8B-E099-4856-9416-67C103D3D194}.Release|Any CPU.Build.0 = Release|Any CPU + {8D3E9953-BF24-4E8D-A337-90A5DE0B43D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D3E9953-BF24-4E8D-A337-90A5DE0B43D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D3E9953-BF24-4E8D-A337-90A5DE0B43D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D3E9953-BF24-4E8D-A337-90A5DE0B43D1}.Release|Any CPU.Build.0 = Release|Any CPU + {5AEE6DCE-F448-4593-8897-6C8511D7F3F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5AEE6DCE-F448-4593-8897-6C8511D7F3F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5AEE6DCE-F448-4593-8897-6C8511D7F3F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5AEE6DCE-F448-4593-8897-6C8511D7F3F2}.Release|Any CPU.Build.0 = Release|Any CPU + {AF71E018-9D16-4C13-9646-6FACC474F138}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF71E018-9D16-4C13-9646-6FACC474F138}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF71E018-9D16-4C13-9646-6FACC474F138}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF71E018-9D16-4C13-9646-6FACC474F138}.Release|Any CPU.Build.0 = Release|Any CPU + {4176C717-C052-4A41-A8D1-4BA321205471}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4176C717-C052-4A41-A8D1-4BA321205471}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4176C717-C052-4A41-A8D1-4BA321205471}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4176C717-C052-4A41-A8D1-4BA321205471}.Release|Any CPU.Build.0 = Release|Any CPU + {CAC08F1F-4187-46A8-8B77-7DD7A1022D66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CAC08F1F-4187-46A8-8B77-7DD7A1022D66}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CAC08F1F-4187-46A8-8B77-7DD7A1022D66}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CAC08F1F-4187-46A8-8B77-7DD7A1022D66}.Release|Any CPU.Build.0 = Release|Any CPU + {69F91E3A-3F97-4F08-A451-223B5BF12A07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {69F91E3A-3F97-4F08-A451-223B5BF12A07}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69F91E3A-3F97-4F08-A451-223B5BF12A07}.Release|Any CPU.ActiveCfg = Release|Any CPU + {69F91E3A-3F97-4F08-A451-223B5BF12A07}.Release|Any CPU.Build.0 = Release|Any CPU {04A11F4A-6D3E-484E-89EC-041D223CED21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {04A11F4A-6D3E-484E-89EC-041D223CED21}.Debug|Any CPU.Build.0 = Debug|Any CPU {04A11F4A-6D3E-484E-89EC-041D223CED21}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -842,6 +886,10 @@ Global {30402564-3CAA-4CB1-A0D5-1BF157BB5B65}.Debug|Any CPU.Build.0 = Debug|Any CPU {30402564-3CAA-4CB1-A0D5-1BF157BB5B65}.Release|Any CPU.ActiveCfg = Release|Any CPU {30402564-3CAA-4CB1-A0D5-1BF157BB5B65}.Release|Any CPU.Build.0 = Release|Any CPU + {74CDB906-8BC9-42F7-A3FA-2B675A240D51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {74CDB906-8BC9-42F7-A3FA-2B675A240D51}.Debug|Any CPU.Build.0 = Debug|Any CPU + {74CDB906-8BC9-42F7-A3FA-2B675A240D51}.Release|Any CPU.ActiveCfg = Release|Any CPU + {74CDB906-8BC9-42F7-A3FA-2B675A240D51}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1011,6 +1059,15 @@ Global {D28F10DA-33BD-4F08-B736-1B7AB53026BD} = {CA03350A-2013-4D18-B68D-8C0F680DBA39} {1BFEE2F6-D514-4458-994A-3DD106405990} = {A82D9F44-8B80-4803-BA97-7D737DEF6A98} {553F1C8B-E099-4856-9416-67C103D3D194} = {1BFEE2F6-D514-4458-994A-3DD106405990} + {DB8BF59A-7BA3-42B5-8CD7-4F9F2FD17AA2} = {0EAF57B8-E97C-469E-A74B-596D78C978B2} + {D67D404E-1FE6-4CC6-B0F8-20DB87B9F23A} = {DB8BF59A-7BA3-42B5-8CD7-4F9F2FD17AA2} + {399BA0C2-C130-4C8E-8F2D-8BB45AB9FD1A} = {DB8BF59A-7BA3-42B5-8CD7-4F9F2FD17AA2} + {8D3E9953-BF24-4E8D-A337-90A5DE0B43D1} = {D67D404E-1FE6-4CC6-B0F8-20DB87B9F23A} + {5AEE6DCE-F448-4593-8897-6C8511D7F3F2} = {D67D404E-1FE6-4CC6-B0F8-20DB87B9F23A} + {AF71E018-9D16-4C13-9646-6FACC474F138} = {D67D404E-1FE6-4CC6-B0F8-20DB87B9F23A} + {4176C717-C052-4A41-A8D1-4BA321205471} = {D67D404E-1FE6-4CC6-B0F8-20DB87B9F23A} + {CAC08F1F-4187-46A8-8B77-7DD7A1022D66} = {D67D404E-1FE6-4CC6-B0F8-20DB87B9F23A} + {69F91E3A-3F97-4F08-A451-223B5BF12A07} = {D67D404E-1FE6-4CC6-B0F8-20DB87B9F23A} {2E8887F1-55BD-4751-A522-0377C80DC70B} = {0EAF57B8-E97C-469E-A74B-596D78C978B2} {2A9D40E4-AB8F-49B5-878B-A8C7763EE609} = {2E8887F1-55BD-4751-A522-0377C80DC70B} {04A11F4A-6D3E-484E-89EC-041D223CED21} = {2A9D40E4-AB8F-49B5-878B-A8C7763EE609} @@ -1018,6 +1075,7 @@ Global {3A107CCD-A7E9-448B-82DF-0FD87D1ABA9E} = {2A9D40E4-AB8F-49B5-878B-A8C7763EE609} {98C16B16-7ECE-4E23-8D6C-2CA372EC310C} = {2A9D40E4-AB8F-49B5-878B-A8C7763EE609} {30402564-3CAA-4CB1-A0D5-1BF157BB5B65} = {2D0BC8E9-ED6B-49D9-937C-1616ED40FB3E} + {74CDB906-8BC9-42F7-A3FA-2B675A240D51} = {399BA0C2-C130-4C8E-8F2D-8BB45AB9FD1A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1F3BD2C6-7CB3-450F-A21A-23EA520D5B7A} diff --git a/BuildingBlocks/src/BuildingBlocks.API/AspNetCoreIdentityCustomizations/CustomSigninManager.cs b/BuildingBlocks/src/BuildingBlocks.API/AspNetCoreIdentityCustomizations/CustomSigninManager.cs index 13813ae2f8..bce72cd555 100644 --- a/BuildingBlocks/src/BuildingBlocks.API/AspNetCoreIdentityCustomizations/CustomSigninManager.cs +++ b/BuildingBlocks/src/BuildingBlocks.API/AspNetCoreIdentityCustomizations/CustomSigninManager.cs @@ -18,7 +18,8 @@ public CustomSigninManager( IAuthenticationSchemeProvider schemes, IUserConfirmation confirmation) : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) - { } + { + } public override async Task CheckPasswordSignInAsync(ApplicationUser user, string password, bool lockoutOnFailure) @@ -35,7 +36,7 @@ public override async Task CheckPasswordSignInAsync(ApplicationUse private async Task UpdateLastLoginDate(ApplicationUser user) { - user.LoginOccurred(); + user.Device.LoginOccurred(); await UserManager.UpdateAsync(user); } } diff --git a/BuildingBlocks/src/BuildingBlocks.API/AspNetCoreIdentityCustomizations/CustomUserStore.cs b/BuildingBlocks/src/BuildingBlocks.API/AspNetCoreIdentityCustomizations/CustomUserStore.cs index 77ec912a87..f9a7490672 100644 --- a/BuildingBlocks/src/BuildingBlocks.API/AspNetCoreIdentityCustomizations/CustomUserStore.cs +++ b/BuildingBlocks/src/BuildingBlocks.API/AspNetCoreIdentityCustomizations/CustomUserStore.cs @@ -9,7 +9,9 @@ namespace Backbone.BuildingBlocks.API.AspNetCoreIdentityCustomizations; public class CustomUserStore : UserStore { - public CustomUserStore(DevicesDbContext context, IdentityErrorDescriber? describer = null) : base(context, describer) { } + public CustomUserStore(DevicesDbContext context, IdentityErrorDescriber? describer = null) : base(context, describer) + { + } public override async Task FindByIdAsync(string userId, CancellationToken cancellationToken = default) { @@ -34,4 +36,11 @@ public CustomUserStore(DevicesDbContext context, IdentityErrorDescriber? describ return user; } + + public override Task UpdateAsync(ApplicationUser user, CancellationToken cancellationToken = new CancellationToken()) + { + Context.Attach(user.Device); + Context.Update(user.Device); + return base.UpdateAsync(user, cancellationToken); + } } diff --git a/BuildingBlocks/src/BuildingBlocks.API/Mvc/ModelBinders/GenericArrayModelBinder.cs b/BuildingBlocks/src/BuildingBlocks.API/Mvc/ModelBinders/GenericArrayModelBinder.cs index c86ca565dc..4eee6518b5 100644 --- a/BuildingBlocks/src/BuildingBlocks.API/Mvc/ModelBinders/GenericArrayModelBinder.cs +++ b/BuildingBlocks/src/BuildingBlocks.API/Mvc/ModelBinders/GenericArrayModelBinder.cs @@ -18,7 +18,7 @@ public class GenericArrayModelBinder : IModelBinder public Task BindModelAsync(ModelBindingContext bindingContext) { var elementType = bindingContext.ModelType.GetElementType()!; - var templates = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType))!; + var items = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType))!; var query = bindingContext.HttpContext.Request.Query; for (var i = 0; ; i++) @@ -29,7 +29,7 @@ public Task BindModelAsync(ModelBindingContext bindingContext) var queryValueFound = false; foreach (var property in properties) { - var key = $"templates.{i}.{property.Name.ToLower()}"; + var key = $"{bindingContext.ModelName}.{i}.{property.Name.ToLower()}"; if (!query.TryGetValue(key, out var queryValue)) continue; @@ -49,14 +49,14 @@ public Task BindModelAsync(ModelBindingContext bindingContext) if (!queryValueFound) break; - templates.Add(instance); + items.Add(instance); } - var resultArray = Array.CreateInstance(elementType, templates.Count); + var resultArray = Array.CreateInstance(elementType, items.Count); - for (var i = 0; i < templates.Count; i++) + for (var i = 0; i < items.Count; i++) { - resultArray.SetValue(templates[i], i); + resultArray.SetValue(items[i], i); } bindingContext.Result = ModelBindingResult.Success(resultArray); diff --git a/BuildingBlocks/src/BuildingBlocks.Application/FluentValidation/TwoLetterIsoLanguageValidator.cs b/BuildingBlocks/src/BuildingBlocks.Application/FluentValidation/TwoLetterIsoLanguageValidator.cs new file mode 100644 index 0000000000..fc3d81f85a --- /dev/null +++ b/BuildingBlocks/src/BuildingBlocks.Application/FluentValidation/TwoLetterIsoLanguageValidator.cs @@ -0,0 +1,32 @@ +using System.Globalization; +using Backbone.BuildingBlocks.Application.Abstractions.Exceptions; +using FluentValidation; +using FluentValidation.Validators; + +namespace Backbone.BuildingBlocks.Application.FluentValidation; + +public class TwoLetterIsoLanguageValidator : IPropertyValidator +{ + // ReSharper disable once StaticMemberInGenericType + private static readonly string[] VALID_LANGUAGES = CultureInfo.GetCultures(CultureTypes.AllCultures & ~CultureTypes.NeutralCultures).Select(c => c.TwoLetterISOLanguageName).ToArray(); + + public bool IsValid(ValidationContext context, string value) + { + return VALID_LANGUAGES.Contains(value); + } + + public string GetDefaultMessageTemplate(string errorCode) + { + return "This language is not a valid two letter ISO language name."; + } + + public string Name { get; } = GenericApplicationErrors.Validation.InvalidPropertyValue().Code; +} + +public static class TwoLetterIsoLanguageValidatorRuleBuilderExtensions +{ + public static IRuleBuilderOptions TwoLetterIsoLanguage(this IRuleBuilder ruleBuilder) + { + return ruleBuilder.SetValidator(new TwoLetterIsoLanguageValidator()); + } +} diff --git a/BuildingBlocks/src/BuildingBlocks.Application/PushNotifications/SendPushNotificationFilter.cs b/BuildingBlocks/src/BuildingBlocks.Application/PushNotifications/SendPushNotificationFilter.cs index c69e112ac1..728026aee7 100644 --- a/BuildingBlocks/src/BuildingBlocks.Application/PushNotifications/SendPushNotificationFilter.cs +++ b/BuildingBlocks/src/BuildingBlocks.Application/PushNotifications/SendPushNotificationFilter.cs @@ -27,4 +27,13 @@ public static SendPushNotificationFilter AllDevicesOfExcept(IdentityAddress reci ExcludedDevices = [.. deviceIds] }; } + + public static SendPushNotificationFilter AllDevicesOfAllIdentities() + { + return new SendPushNotificationFilter + { + IncludedIdentities = [], + ExcludedDevices = [] + }; + } } diff --git a/BuildingBlocks/src/BuildingBlocks.Infrastructure/BuildingBlocks.Infrastructure.csproj b/BuildingBlocks/src/BuildingBlocks.Infrastructure/BuildingBlocks.Infrastructure.csproj index c81420e66e..c079e705e4 100644 --- a/BuildingBlocks/src/BuildingBlocks.Infrastructure/BuildingBlocks.Infrastructure.csproj +++ b/BuildingBlocks/src/BuildingBlocks.Infrastructure/BuildingBlocks.Infrastructure.csproj @@ -19,7 +19,7 @@ - + diff --git a/BuildingBlocks/src/BuildingBlocks.SDK/BuildingBlocks.SDK.csproj b/BuildingBlocks/src/BuildingBlocks.SDK/BuildingBlocks.SDK.csproj index a7ca9608cc..a13e5fb4cf 100644 --- a/BuildingBlocks/src/BuildingBlocks.SDK/BuildingBlocks.SDK.csproj +++ b/BuildingBlocks/src/BuildingBlocks.SDK/BuildingBlocks.SDK.csproj @@ -1,6 +1,6 @@  - + diff --git a/BuildingBlocks/src/BuildingBlocks.SDK/Endpoints/Common/EndpointClient.cs b/BuildingBlocks/src/BuildingBlocks.SDK/Endpoints/Common/EndpointClient.cs index dbe4f23d4e..fa88533e00 100644 --- a/BuildingBlocks/src/BuildingBlocks.SDK/Endpoints/Common/EndpointClient.cs +++ b/BuildingBlocks/src/BuildingBlocks.SDK/Endpoints/Common/EndpointClient.cs @@ -50,20 +50,20 @@ public async Task> PostUnauthenticated(string url, object? req .Execute(); } - public async Task> Get(string url, object? requestContent = null, PaginationFilter? pagination = null) + public async Task> Get(string url, NameValueCollection? queryParameters = null, PaginationFilter? pagination = null) { return await Request(HttpMethod.Get, url) .Authenticate() .WithPagination(pagination) - .WithJson(requestContent) + .AddQueryParameters(queryParameters) .Execute(); } - public async Task> GetUnauthenticated(string url, object? requestContent = null, PaginationFilter? pagination = null) + public async Task> GetUnauthenticated(string url, NameValueCollection? queryParameters = null, PaginationFilter? pagination = null) { return await Request(HttpMethod.Get, url) .WithPagination(pagination) - .WithJson(requestContent) + .AddQueryParameters(queryParameters) .Execute(); } @@ -201,7 +201,9 @@ public RequestBuilder WithMultipartForm(MultipartContent content) public RequestBuilder WithPagination(PaginationFilter? pagination) { - if (pagination != null) AddQueryParameters(pagination); + if (pagination != null) + AddQueryParameters(pagination); + return this; } @@ -233,7 +235,14 @@ public RequestBuilder AddQueryParameter(string key, object value) public RequestBuilder AddQueryParameters(IQueryParameterStorage parameters) { - _queryParameters.Add(parameters.ToQueryParameters()); + return AddQueryParameters(parameters.ToQueryParameters()); + } + + public RequestBuilder AddQueryParameters(NameValueCollection? parameters) + { + if (parameters != null) + _queryParameters.Add(parameters); + return this; } diff --git a/BuildingBlocks/src/Crypto/Crypto.csproj b/BuildingBlocks/src/Crypto/Crypto.csproj index 7bfd0569b1..31d48ab380 100644 --- a/BuildingBlocks/src/Crypto/Crypto.csproj +++ b/BuildingBlocks/src/Crypto/Crypto.csproj @@ -5,7 +5,7 @@ - + diff --git a/BuildingBlocks/src/UnitTestTools/UnitTestTools.csproj b/BuildingBlocks/src/UnitTestTools/UnitTestTools.csproj index 6324db29ea..0f1ae8f00e 100644 --- a/BuildingBlocks/src/UnitTestTools/UnitTestTools.csproj +++ b/BuildingBlocks/src/UnitTestTools/UnitTestTools.csproj @@ -2,7 +2,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/BuildingBlocks/test/BuildingBlocks.API.Tests/BuildingBlocks.API.Tests.csproj b/BuildingBlocks/test/BuildingBlocks.API.Tests/BuildingBlocks.API.Tests.csproj index 0bbd811403..d5712198db 100644 --- a/BuildingBlocks/test/BuildingBlocks.API.Tests/BuildingBlocks.API.Tests.csproj +++ b/BuildingBlocks/test/BuildingBlocks.API.Tests/BuildingBlocks.API.Tests.csproj @@ -6,7 +6,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/BuildingBlocks/test/UnitTestTools.Tests/UnitTestTools.Tests.csproj b/BuildingBlocks/test/UnitTestTools.Tests/UnitTestTools.Tests.csproj index bfc6ceea8c..898d8e15bd 100644 --- a/BuildingBlocks/test/UnitTestTools.Tests/UnitTestTools.Tests.csproj +++ b/BuildingBlocks/test/UnitTestTools.Tests/UnitTestTools.Tests.csproj @@ -6,7 +6,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Directory.Build.props b/Directory.Build.props index 9ec8741c83..b5402a3d49 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -26,7 +26,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index 3a41f6853d..25f6676291 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -3,8 +3,8 @@ - - + + diff --git a/Infrastructure/Logging/LogHelper.cs b/Infrastructure/Logging/LogHelper.cs index fecd08c099..a666482042 100644 --- a/Infrastructure/Logging/LogHelper.cs +++ b/Infrastructure/Logging/LogHelper.cs @@ -44,12 +44,12 @@ public static void EnrichFromRequest(IDiagnosticContext diagnosticContext, HttpC diagnosticContext.Set("QueryString", request.QueryString.Value); } - diagnosticContext.Set("ContentType", httpContext.Response.ContentType); + if (httpContext.Response.ContentType != null) + diagnosticContext.Set("ContentType", httpContext.Response.ContentType); var endpoint = httpContext.GetEndpoint(); - if (endpoint != null) - { + + if (endpoint?.DisplayName != null) diagnosticContext.Set("EndpointName", endpoint.DisplayName); - } } } diff --git a/Modules/Announcements/src/Announcements.Application/Announcements.Application.csproj b/Modules/Announcements/src/Announcements.Application/Announcements.Application.csproj new file mode 100644 index 0000000000..e7edb86307 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Application/Announcements.Application.csproj @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/Modules/Announcements/src/Announcements.Application/Announcements/Commands/CreateAnnouncement/CreateAnnouncementCommand.cs b/Modules/Announcements/src/Announcements.Application/Announcements/Commands/CreateAnnouncement/CreateAnnouncementCommand.cs new file mode 100644 index 0000000000..4c87092302 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Application/Announcements/Commands/CreateAnnouncement/CreateAnnouncementCommand.cs @@ -0,0 +1,19 @@ +using Backbone.Modules.Announcements.Application.Announcements.DTOs; +using Backbone.Modules.Announcements.Domain.Entities; +using MediatR; + +namespace Backbone.Modules.Announcements.Application.Announcements.Commands.CreateAnnouncement; + +public class CreateAnnouncementCommand : IRequest +{ + public required AnnouncementSeverity Severity { get; set; } + public required List Texts { get; set; } + public DateTime? ExpiresAt { get; set; } +} + +public class CreateAnnouncementCommandText +{ + public required string Language { get; set; } + public required string Title { get; set; } + public required string Body { get; set; } +} diff --git a/Modules/Announcements/src/Announcements.Application/Announcements/Commands/CreateAnnouncement/Handler.cs b/Modules/Announcements/src/Announcements.Application/Announcements/Commands/CreateAnnouncement/Handler.cs new file mode 100644 index 0000000000..304bf3a4f4 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Application/Announcements/Commands/CreateAnnouncement/Handler.cs @@ -0,0 +1,27 @@ +using Backbone.Modules.Announcements.Application.Announcements.DTOs; +using Backbone.Modules.Announcements.Application.Infrastructure.Persistence.Repository; +using Backbone.Modules.Announcements.Domain.Entities; +using MediatR; + +namespace Backbone.Modules.Announcements.Application.Announcements.Commands.CreateAnnouncement; + +public class Handler : IRequestHandler +{ + private readonly IAnnouncementsRepository _announcementsRepository; + + public Handler(IAnnouncementsRepository announcementsRepository) + { + _announcementsRepository = announcementsRepository; + } + + public async Task Handle(CreateAnnouncementCommand request, CancellationToken cancellationToken) + { + var texts = request.Texts.Select(t => new AnnouncementText(AnnouncementLanguage.Parse(t.Language), t.Title, t.Body)).ToList(); + + var announcement = new Announcement(request.Severity, texts, request.ExpiresAt); + + await _announcementsRepository.Add(announcement, cancellationToken); + + return new AnnouncementDTO(announcement); + } +} diff --git a/Modules/Announcements/src/Announcements.Application/Announcements/Commands/CreateAnnouncement/Validator.cs b/Modules/Announcements/src/Announcements.Application/Announcements/Commands/CreateAnnouncement/Validator.cs new file mode 100644 index 0000000000..9a3f535422 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Application/Announcements/Commands/CreateAnnouncement/Validator.cs @@ -0,0 +1,29 @@ +using Backbone.BuildingBlocks.Application.Abstractions.Exceptions; +using Backbone.BuildingBlocks.Application.FluentValidation; +using Backbone.Modules.Announcements.Domain.Entities; +using FluentValidation; + +namespace Backbone.Modules.Announcements.Application.Announcements.Commands.CreateAnnouncement; + +public class Validator : AbstractValidator +{ + public Validator() + { + RuleFor(x => x.Texts) + .Must(x => x.Any(t => t.Language == AnnouncementLanguage.DEFAULT_LANGUAGE.Value)) + .WithErrorCode(GenericApplicationErrors.Validation.InvalidPropertyValue().Code) + .WithMessage("There must be a text for English."); + + RuleForEach(x => x.Texts).SetValidator(new CreateAnnouncementCommandTextValidator()); + } +} + +public class CreateAnnouncementCommandTextValidator : AbstractValidator +{ + public CreateAnnouncementCommandTextValidator() + { + RuleFor(x => x.Language).TwoLetterIsoLanguage(); + RuleFor(x => x.Title).DetailedNotEmpty(); + RuleFor(x => x.Body).DetailedNotEmpty(); + } +} diff --git a/Modules/Announcements/src/Announcements.Application/Announcements/DTOs/AnnouncementDTO.cs b/Modules/Announcements/src/Announcements.Application/Announcements/DTOs/AnnouncementDTO.cs new file mode 100644 index 0000000000..f1753b0782 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Application/Announcements/DTOs/AnnouncementDTO.cs @@ -0,0 +1,22 @@ +using Backbone.Modules.Announcements.Domain.Entities; + +namespace Backbone.Modules.Announcements.Application.Announcements.DTOs +{ + public class AnnouncementDTO + { + public AnnouncementDTO(Announcement announcement) + { + Id = announcement.Id; + CreatedAt = announcement.CreatedAt; + ExpiresAt = announcement.ExpiresAt; + Severity = announcement.Severity; + Texts = announcement.Texts.Select(t => new AnnouncementTextDTO(t)); + } + + public string Id { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? ExpiresAt { get; set; } + public AnnouncementSeverity Severity { get; set; } + public IEnumerable Texts { get; set; } + } +} diff --git a/Modules/Announcements/src/Announcements.Application/Announcements/DTOs/AnnouncementTextDTO.cs b/Modules/Announcements/src/Announcements.Application/Announcements/DTOs/AnnouncementTextDTO.cs new file mode 100644 index 0000000000..5697add779 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Application/Announcements/DTOs/AnnouncementTextDTO.cs @@ -0,0 +1,17 @@ +using Backbone.Modules.Announcements.Domain.Entities; + +namespace Backbone.Modules.Announcements.Application.Announcements.DTOs; + +public class AnnouncementTextDTO +{ + public AnnouncementTextDTO(AnnouncementText announcementText) + { + Language = announcementText.Language.Value; + Title = announcementText.Title; + Body = announcementText.Body; + } + + public string Language { get; set; } + public string Title { get; set; } + public string Body { get; set; } +} diff --git a/Modules/Announcements/src/Announcements.Application/Announcements/DTOs/SingleLanguageAnnouncementDTO.cs b/Modules/Announcements/src/Announcements.Application/Announcements/DTOs/SingleLanguageAnnouncementDTO.cs new file mode 100644 index 0000000000..0a3a75257c --- /dev/null +++ b/Modules/Announcements/src/Announcements.Application/Announcements/DTOs/SingleLanguageAnnouncementDTO.cs @@ -0,0 +1,29 @@ +using Backbone.Modules.Announcements.Domain.Entities; + +namespace Backbone.Modules.Announcements.Application.Announcements.DTOs; + +/** + * This class is used when a user's device asks for announcements. In that case we return + * just the translation for the device's communication language. + */ +public class SingleLanguageAnnouncementDTO +{ + public SingleLanguageAnnouncementDTO(Announcement announcement, AnnouncementLanguage language) + { + Id = announcement.Id.Value; + CreatedAt = announcement.CreatedAt; + ExpiresAt = announcement.ExpiresAt; + Severity = announcement.Severity; + + var textInLanguage = announcement.Texts.SingleOrDefault(t => t.Language == language) ?? + announcement.Texts.Single(t => t.Language == AnnouncementLanguage.DEFAULT_LANGUAGE); + + Text = new AnnouncementTextDTO(textInLanguage); + } + + public string Id { get; } + public DateTime CreatedAt { get; } + public DateTime? ExpiresAt { get; } + public AnnouncementSeverity Severity { get; } + public AnnouncementTextDTO Text { get; } +} diff --git a/Modules/Announcements/src/Announcements.Application/Announcements/Queries/GetAllAnnouncements/GetAllAnnouncementsQuery.cs b/Modules/Announcements/src/Announcements.Application/Announcements/Queries/GetAllAnnouncements/GetAllAnnouncementsQuery.cs new file mode 100644 index 0000000000..a7867373ee --- /dev/null +++ b/Modules/Announcements/src/Announcements.Application/Announcements/Queries/GetAllAnnouncements/GetAllAnnouncementsQuery.cs @@ -0,0 +1,5 @@ +using MediatR; + +namespace Backbone.Modules.Announcements.Application.Announcements.Queries.GetAllAnnouncements; + +public class GetAllAnnouncementsQuery : IRequest; diff --git a/Modules/Announcements/src/Announcements.Application/Announcements/Queries/GetAllAnnouncements/GetAllAnnouncementsResponse.cs b/Modules/Announcements/src/Announcements.Application/Announcements/Queries/GetAllAnnouncements/GetAllAnnouncementsResponse.cs new file mode 100644 index 0000000000..29c9a2c0e0 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Application/Announcements/Queries/GetAllAnnouncements/GetAllAnnouncementsResponse.cs @@ -0,0 +1,12 @@ +using Backbone.BuildingBlocks.Application.CQRS.BaseClasses; +using Backbone.Modules.Announcements.Application.Announcements.DTOs; +using Backbone.Modules.Announcements.Domain.Entities; + +namespace Backbone.Modules.Announcements.Application.Announcements.Queries.GetAllAnnouncements; + +public class GetAllAnnouncementsResponse : CollectionResponseBase +{ + public GetAllAnnouncementsResponse(IEnumerable items) : base(items.Select(a => new AnnouncementDTO(a))) + { + } +} diff --git a/Modules/Announcements/src/Announcements.Application/Announcements/Queries/GetAllAnnouncements/Handler.cs b/Modules/Announcements/src/Announcements.Application/Announcements/Queries/GetAllAnnouncements/Handler.cs new file mode 100644 index 0000000000..a966537d33 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Application/Announcements/Queries/GetAllAnnouncements/Handler.cs @@ -0,0 +1,21 @@ +using Backbone.Modules.Announcements.Application.Infrastructure.Persistence.Repository; +using MediatR; + +namespace Backbone.Modules.Announcements.Application.Announcements.Queries.GetAllAnnouncements; + +public class Handler : IRequestHandler +{ + private readonly IAnnouncementsRepository _announcementsRepository; + + public Handler(IAnnouncementsRepository announcementsRepository) + { + _announcementsRepository = announcementsRepository; + } + + public async Task Handle(GetAllAnnouncementsQuery request, CancellationToken cancellationToken) + { + var announcements = await _announcementsRepository.FindAll(cancellationToken); + + return new GetAllAnnouncementsResponse(announcements); + } +} diff --git a/Modules/Announcements/src/Announcements.Application/Announcements/Queries/GetAllAnnouncementsInLanguage/GetAllAnnouncementsInLanguageQuery.cs b/Modules/Announcements/src/Announcements.Application/Announcements/Queries/GetAllAnnouncementsInLanguage/GetAllAnnouncementsInLanguageQuery.cs new file mode 100644 index 0000000000..c02a4604ec --- /dev/null +++ b/Modules/Announcements/src/Announcements.Application/Announcements/Queries/GetAllAnnouncementsInLanguage/GetAllAnnouncementsInLanguageQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace Backbone.Modules.Announcements.Application.Announcements.Queries.GetAllAnnouncementsInLanguage; + +public class GetAllAnnouncementsInLanguageQuery : IRequest +{ + public required string Language { get; set; } +} diff --git a/Modules/Announcements/src/Announcements.Application/Announcements/Queries/GetAllAnnouncementsInLanguage/GetAllAnnouncementsInLanguageResponse.cs b/Modules/Announcements/src/Announcements.Application/Announcements/Queries/GetAllAnnouncementsInLanguage/GetAllAnnouncementsInLanguageResponse.cs new file mode 100644 index 0000000000..228b758a05 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Application/Announcements/Queries/GetAllAnnouncementsInLanguage/GetAllAnnouncementsInLanguageResponse.cs @@ -0,0 +1,12 @@ +using Backbone.BuildingBlocks.Application.CQRS.BaseClasses; +using Backbone.Modules.Announcements.Application.Announcements.DTOs; +using Backbone.Modules.Announcements.Domain.Entities; + +namespace Backbone.Modules.Announcements.Application.Announcements.Queries.GetAllAnnouncementsInLanguage; + +public class GetAllAnnouncementsInLanguageResponse : CollectionResponseBase +{ + public GetAllAnnouncementsInLanguageResponse(IEnumerable items, AnnouncementLanguage language) : base(items.Select(a => new SingleLanguageAnnouncementDTO(a, language))) + { + } +} diff --git a/Modules/Announcements/src/Announcements.Application/Announcements/Queries/GetAllAnnouncementsInLanguage/Handler.cs b/Modules/Announcements/src/Announcements.Application/Announcements/Queries/GetAllAnnouncementsInLanguage/Handler.cs new file mode 100644 index 0000000000..f5937d6ac3 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Application/Announcements/Queries/GetAllAnnouncementsInLanguage/Handler.cs @@ -0,0 +1,24 @@ +using Backbone.Modules.Announcements.Application.Infrastructure.Persistence.Repository; +using Backbone.Modules.Announcements.Domain.Entities; +using MediatR; + +namespace Backbone.Modules.Announcements.Application.Announcements.Queries.GetAllAnnouncementsInLanguage; + +public class Handler : IRequestHandler +{ + private readonly IAnnouncementsRepository _announcementsRepository; + + public Handler(IAnnouncementsRepository announcementsRepository) + { + _announcementsRepository = announcementsRepository; + } + + public async Task Handle(GetAllAnnouncementsInLanguageQuery request, CancellationToken cancellationToken) + { + var announcements = await _announcementsRepository.FindAll(cancellationToken); + + var expectedLanguage = AnnouncementLanguage.Parse(request.Language); + + return new GetAllAnnouncementsInLanguageResponse(announcements, expectedLanguage); + } +} diff --git a/Modules/Announcements/src/Announcements.Application/Announcements/Queries/GetAllAnnouncementsInLanguage/Validator.cs b/Modules/Announcements/src/Announcements.Application/Announcements/Queries/GetAllAnnouncementsInLanguage/Validator.cs new file mode 100644 index 0000000000..61d9dbee1c --- /dev/null +++ b/Modules/Announcements/src/Announcements.Application/Announcements/Queries/GetAllAnnouncementsInLanguage/Validator.cs @@ -0,0 +1,12 @@ +using Backbone.BuildingBlocks.Application.FluentValidation; +using FluentValidation; + +namespace Backbone.Modules.Announcements.Application.Announcements.Queries.GetAllAnnouncementsInLanguage; + +public class Validator : AbstractValidator +{ + public Validator() + { + RuleFor(x => x.Language).DetailedNotEmpty().TwoLetterIsoLanguage(); + } +} diff --git a/Modules/Announcements/src/Announcements.Application/ApplicationOptions.cs b/Modules/Announcements/src/Announcements.Application/ApplicationOptions.cs new file mode 100644 index 0000000000..7e23cd42c1 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Application/ApplicationOptions.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace Backbone.Modules.Announcements.Application; + +public class ApplicationOptions +{ + [Required] + public PaginationOptions Pagination { get; set; } = new(); +} + +public class PaginationOptions +{ + [Required] + [Range(1, 1000)] + public int MaxPageSize { get; set; } + + [Required] + [Range(1, 1000)] + public int DefaultPageSize { get; set; } +} diff --git a/Modules/Announcements/src/Announcements.Application/Extensions/IServiceCollectionExtensions.cs b/Modules/Announcements/src/Announcements.Application/Extensions/IServiceCollectionExtensions.cs new file mode 100644 index 0000000000..0d6c5b9c25 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Application/Extensions/IServiceCollectionExtensions.cs @@ -0,0 +1,20 @@ +using Backbone.BuildingBlocks.Application.MediatR; +using Backbone.Modules.Announcements.Application.Announcements.Commands.CreateAnnouncement; +using FluentValidation; +using Microsoft.Extensions.DependencyInjection; + +namespace Backbone.Modules.Announcements.Application.Extensions; + +public static class IServiceCollectionExtensions +{ + public static void AddApplication(this IServiceCollection services) + { + services.AddMediatR(c => c + .RegisterServicesFromAssemblyContaining() + .AddOpenBehavior(typeof(LoggingBehavior<,>)) + .AddOpenBehavior(typeof(RequestValidationBehavior<,>)) + .AddOpenBehavior(typeof(QuotaEnforcerBehavior<,>)) + ); + services.AddValidatorsFromAssembly(typeof(Validator).Assembly); + } +} diff --git a/Modules/Announcements/src/Announcements.Application/Infrastructure/Persistence/Repository/IAnnouncementsRepository.cs b/Modules/Announcements/src/Announcements.Application/Infrastructure/Persistence/Repository/IAnnouncementsRepository.cs new file mode 100644 index 0000000000..33f83e52b2 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Application/Infrastructure/Persistence/Repository/IAnnouncementsRepository.cs @@ -0,0 +1,8 @@ +using Backbone.Modules.Announcements.Domain.Entities; + +namespace Backbone.Modules.Announcements.Application.Infrastructure.Persistence.Repository; +public interface IAnnouncementsRepository +{ + Task Add(Announcement announcement, CancellationToken cancellationToken); + Task> FindAll(CancellationToken cancellationToken); +} diff --git a/Modules/Announcements/src/Announcements.ConsumerApi/Announcements.ConsumerApi.csproj b/Modules/Announcements/src/Announcements.ConsumerApi/Announcements.ConsumerApi.csproj new file mode 100644 index 0000000000..0649af4b12 --- /dev/null +++ b/Modules/Announcements/src/Announcements.ConsumerApi/Announcements.ConsumerApi.csproj @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/Modules/Announcements/src/Announcements.ConsumerApi/AnnouncementsModule.cs b/Modules/Announcements/src/Announcements.ConsumerApi/AnnouncementsModule.cs new file mode 100644 index 0000000000..d0a46778f2 --- /dev/null +++ b/Modules/Announcements/src/Announcements.ConsumerApi/AnnouncementsModule.cs @@ -0,0 +1,41 @@ +using Backbone.BuildingBlocks.API; +using Backbone.BuildingBlocks.API.Extensions; +using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus; +using Backbone.Modules.Announcements.Application; +using Backbone.Modules.Announcements.Application.Extensions; +using Backbone.Modules.Announcements.Infrastructure.Persistence.Database; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Backbone.Modules.Announcements.ConsumerApi; + +public class AnnouncementsModule : AbstractModule +{ + public override string Name => "Announcements"; + + public override void ConfigureServices(IServiceCollection services, IConfigurationSection configuration) + { + services.ConfigureAndValidate(options => configuration.GetSection("Application").Bind(options)); + services.ConfigureAndValidate(configuration.Bind); + + var parsedConfiguration = services.BuildServiceProvider().GetRequiredService>().Value; + + services.AddApplication(); + + services.AddDatabase(dbOptions => + { + dbOptions.Provider = parsedConfiguration.Infrastructure.SqlDatabase.Provider; + dbOptions.ConnectionString = parsedConfiguration.Infrastructure.SqlDatabase.ConnectionString; + }); + + if (parsedConfiguration.Infrastructure.SqlDatabase.EnableHealthCheck) + services.AddSqlDatabaseHealthCheck(Name, + parsedConfiguration.Infrastructure.SqlDatabase.Provider, + parsedConfiguration.Infrastructure.SqlDatabase.ConnectionString); + } + + public override void ConfigureEventBus(IEventBus eventBus) + { + } +} diff --git a/Modules/Announcements/src/Announcements.ConsumerApi/Configuration.cs b/Modules/Announcements/src/Announcements.ConsumerApi/Configuration.cs new file mode 100644 index 0000000000..d6f7684715 --- /dev/null +++ b/Modules/Announcements/src/Announcements.ConsumerApi/Configuration.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +using Backbone.Modules.Announcements.Application; + +namespace Backbone.Modules.Announcements.ConsumerApi; + +public class Configuration +{ + [Required] + public ApplicationOptions Application { get; set; } = new(); + + [Required] + public InfrastructureConfiguration Infrastructure { get; set; } = new(); + + public class InfrastructureConfiguration + { + [Required] + public SqlDatabaseConfiguration SqlDatabase { get; set; } = new(); + + public class SqlDatabaseConfiguration + { + [Required] + [MinLength(1)] + [RegularExpression("SqlServer|Postgres")] + public string Provider { get; set; } = string.Empty; + + [Required] + [MinLength(1)] + public string ConnectionString { get; set; } = string.Empty; + + [Required] + public bool EnableHealthCheck { get; set; } = true; + } + } +} diff --git a/Modules/Announcements/src/Announcements.ConsumerApi/Controllers/AnnouncementsController.cs b/Modules/Announcements/src/Announcements.ConsumerApi/Controllers/AnnouncementsController.cs new file mode 100644 index 0000000000..e3b4f19170 --- /dev/null +++ b/Modules/Announcements/src/Announcements.ConsumerApi/Controllers/AnnouncementsController.cs @@ -0,0 +1,23 @@ +using Backbone.BuildingBlocks.API.Mvc; +using Backbone.Modules.Announcements.Application.Announcements.Queries.GetAllAnnouncementsInLanguage; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Backbone.Modules.Announcements.ConsumerApi.Controllers; + +[Route("api/v1/[controller]")] +[Authorize("OpenIddict.Validation.AspNetCore")] +public class AnnouncementsController : ApiControllerBase +{ + public AnnouncementsController(IMediator mediator) : base(mediator) + { + } + + [HttpGet] + public async Task GetAllAnnouncements([FromQuery] string language) + { + var announcements = await _mediator.Send(new GetAllAnnouncementsInLanguageQuery { Language = language }); + return Ok(announcements); + } +} diff --git a/Modules/Announcements/src/Announcements.Domain/Announcements.Domain.csproj b/Modules/Announcements/src/Announcements.Domain/Announcements.Domain.csproj new file mode 100644 index 0000000000..3717185dc4 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Domain/Announcements.Domain.csproj @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Modules/Announcements/src/Announcements.Domain/DomainErrors.cs b/Modules/Announcements/src/Announcements.Domain/DomainErrors.cs new file mode 100644 index 0000000000..a866816579 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Domain/DomainErrors.cs @@ -0,0 +1,11 @@ +using Backbone.BuildingBlocks.Domain.Errors; + +namespace Backbone.Modules.Announcements.Domain; + +public class DomainErrors +{ + public static DomainError InvalidAnnouncementLanguage() + { + return new DomainError("error.platform.validation.invalidAnnouncementLanguage", "The Announcement Language must be a valid two letter ISO code."); + } +} diff --git a/Modules/Announcements/src/Announcements.Domain/DomainEvents/Outgoing/AnnouncementCreatedDomainEvent.cs b/Modules/Announcements/src/Announcements.Domain/DomainEvents/Outgoing/AnnouncementCreatedDomainEvent.cs new file mode 100644 index 0000000000..aec067119e --- /dev/null +++ b/Modules/Announcements/src/Announcements.Domain/DomainEvents/Outgoing/AnnouncementCreatedDomainEvent.cs @@ -0,0 +1,32 @@ +using Backbone.BuildingBlocks.Domain.Events; +using Backbone.Modules.Announcements.Domain.Entities; + +namespace Backbone.Modules.Announcements.Domain.DomainEvents.Outgoing; + +public class AnnouncementCreatedDomainEvent : DomainEvent +{ + public AnnouncementCreatedDomainEvent(Announcement announcement) : base($"{announcement.Id}/Created") + { + Id = announcement.Id.Value; + Severity = announcement.Severity.ToString(); + Texts = announcement.Texts.Select(t => new AnnouncementCreatedDomainEventText(t)).ToList(); + } + + public string Id { get; } + public string Severity { get; } + public List Texts { get; } +} + +public class AnnouncementCreatedDomainEventText +{ + public AnnouncementCreatedDomainEventText(AnnouncementText announcementText) + { + Language = announcementText.Language.Value; + Title = announcementText.Title; + Body = announcementText.Body; + } + + public string Language { get; } + public string Title { get; } + public string Body { get; } +} diff --git a/Modules/Announcements/src/Announcements.Domain/Entities/Announcement.cs b/Modules/Announcements/src/Announcements.Domain/Entities/Announcement.cs new file mode 100644 index 0000000000..3c4c491940 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Domain/Entities/Announcement.cs @@ -0,0 +1,41 @@ +using Backbone.BuildingBlocks.Domain; +using Backbone.Modules.Announcements.Domain.DomainEvents.Outgoing; +using Backbone.Tooling; + +namespace Backbone.Modules.Announcements.Domain.Entities; + +public class Announcement : Entity +{ + // ReSharper disable once UnusedMember.Local + private Announcement() + { + // This constructor is for EF Core only; initializing the properties with null is therefore not a problem + Id = null!; + Texts = null!; + } + + public Announcement(AnnouncementSeverity severity, List texts, DateTime? expiresAt) + { + Id = AnnouncementId.New(); + CreatedAt = SystemTime.UtcNow; + ExpiresAt = expiresAt; + Severity = severity; + Texts = texts; + + RaiseDomainEvent(new AnnouncementCreatedDomainEvent(this)); + } + + public AnnouncementId Id { get; } + public DateTime CreatedAt { get; } + public DateTime? ExpiresAt { get; } + public AnnouncementSeverity Severity { get; } + + public List Texts { get; } +} + +public enum AnnouncementSeverity +{ + Low, + Medium, + High +} diff --git a/Modules/Announcements/src/Announcements.Domain/Entities/AnnouncementId.cs b/Modules/Announcements/src/Announcements.Domain/Entities/AnnouncementId.cs new file mode 100644 index 0000000000..c76deaf661 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Domain/Entities/AnnouncementId.cs @@ -0,0 +1,33 @@ +using Backbone.BuildingBlocks.Domain; +using Backbone.BuildingBlocks.Domain.StronglyTypedIds.Records; + +namespace Backbone.Modules.Announcements.Domain.Entities; + +public record AnnouncementId : StronglyTypedId +{ + public const int MAX_LENGTH = DEFAULT_MAX_LENGTH; + private const string PREFIX = "ANC"; + private static readonly StronglyTypedIdHelpers UTILS = new(PREFIX, DEFAULT_VALID_CHARS, MAX_LENGTH); + + public AnnouncementId(string stringValue) : base(stringValue) + { + } + + public static AnnouncementId Parse(string stringValue) + { + UTILS.Validate(stringValue); + + return new AnnouncementId(stringValue); + } + + public static bool IsValid(string stringValue) + { + return UTILS.IsValid(stringValue); + } + + public static AnnouncementId New() + { + var stringValue = StringUtils.Generate(DEFAULT_VALID_CHARS, DEFAULT_MAX_LENGTH_WITHOUT_PREFIX); + return new AnnouncementId(PREFIX + stringValue); + } +} diff --git a/Modules/Announcements/src/Announcements.Domain/Entities/AnnouncementLanguage.cs b/Modules/Announcements/src/Announcements.Domain/Entities/AnnouncementLanguage.cs new file mode 100644 index 0000000000..a84be3d51d --- /dev/null +++ b/Modules/Announcements/src/Announcements.Domain/Entities/AnnouncementLanguage.cs @@ -0,0 +1,38 @@ +using System.Globalization; +using Backbone.BuildingBlocks.Domain.Errors; +using Backbone.BuildingBlocks.Domain.Exceptions; + +namespace Backbone.Modules.Announcements.Domain.Entities; + +public record AnnouncementLanguage +{ + public static readonly AnnouncementLanguage DEFAULT_LANGUAGE = new("en"); + private static readonly CultureInfo[] CULTURES = CultureInfo.GetCultures(CultureTypes.AllCultures & ~CultureTypes.NeutralCultures); + + public const int LENGTH = 2; + + private AnnouncementLanguage(string value) + { + Value = value; + } + + public string Value { get; } + + public static AnnouncementLanguage Parse(string value) + { + var validationResult = Validate(value); + + if (validationResult != null) + throw new DomainException(validationResult); + + return new AnnouncementLanguage(value); + } + + public static DomainError? Validate(string value) + { + if (CULTURES.All(c => c.TwoLetterISOLanguageName != value)) + return DomainErrors.InvalidAnnouncementLanguage(); + + return null; + } +} diff --git a/Modules/Announcements/src/Announcements.Domain/Entities/AnnouncementText.cs b/Modules/Announcements/src/Announcements.Domain/Entities/AnnouncementText.cs new file mode 100644 index 0000000000..2fc9256156 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Domain/Entities/AnnouncementText.cs @@ -0,0 +1,29 @@ +using Backbone.BuildingBlocks.Domain; + +namespace Backbone.Modules.Announcements.Domain.Entities; + +public class AnnouncementText : Entity +{ + // ReSharper disable once UnusedMember.Local + private AnnouncementText() + { + // This constructor is for EF Core only; initializing the properties with null is therefore not a problem + AnnouncementId = null!; + Language = null!; + Title = null!; + Body = null!; + } + + public AnnouncementText(AnnouncementLanguage language, string title, string body) + { + AnnouncementId = null!; // will be set by EF Core + Language = language; + Title = title; + Body = body; + } + + public AnnouncementId AnnouncementId { get; } + public AnnouncementLanguage Language { get; set; } + public string Title { get; set; } + public string Body { get; set; } +} diff --git a/Modules/Announcements/src/Announcements.Infrastructure.Database.Postgres/Announcements.Infrastructure.Database.Postgres.csproj b/Modules/Announcements/src/Announcements.Infrastructure.Database.Postgres/Announcements.Infrastructure.Database.Postgres.csproj new file mode 100644 index 0000000000..7850953be6 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Infrastructure.Database.Postgres/Announcements.Infrastructure.Database.Postgres.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Modules/Announcements/src/Announcements.Infrastructure.Database.Postgres/Migrations/20241030151920_Init.Designer.cs b/Modules/Announcements/src/Announcements.Infrastructure.Database.Postgres/Migrations/20241030151920_Init.Designer.cs new file mode 100644 index 0000000000..78fc859bc6 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Infrastructure.Database.Postgres/Migrations/20241030151920_Init.Designer.cs @@ -0,0 +1,94 @@ +// +using System; +using Backbone.Modules.Announcements.Infrastructure.Persistence.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backbone.Modules.Announcements.Infrastructure.Database.Postgres.Migrations +{ + [DbContext(typeof(AnnouncementsDbContext))] + [Migration("20241030151920_Init")] + partial class Init + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Announcements") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backbone.Modules.Announcements.Domain.Entities.Announcement", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Severity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Announcements", "Announcements"); + }); + + modelBuilder.Entity("Backbone.Modules.Announcements.Domain.Entities.AnnouncementText", b => + { + b.Property("AnnouncementId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("Language") + .HasMaxLength(2) + .IsUnicode(false) + .HasColumnType("character(2)") + .IsFixedLength(); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("AnnouncementId", "Language"); + + b.ToTable("AnnouncementText", "Announcements"); + }); + + modelBuilder.Entity("Backbone.Modules.Announcements.Domain.Entities.AnnouncementText", b => + { + b.HasOne("Backbone.Modules.Announcements.Domain.Entities.Announcement", null) + .WithMany("Texts") + .HasForeignKey("AnnouncementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Announcements.Domain.Entities.Announcement", b => + { + b.Navigation("Texts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Modules/Announcements/src/Announcements.Infrastructure.Database.Postgres/Migrations/20241030151920_Init.cs b/Modules/Announcements/src/Announcements.Infrastructure.Database.Postgres/Migrations/20241030151920_Init.cs new file mode 100644 index 0000000000..f3f94b936c --- /dev/null +++ b/Modules/Announcements/src/Announcements.Infrastructure.Database.Postgres/Migrations/20241030151920_Init.cs @@ -0,0 +1,67 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backbone.Modules.Announcements.Infrastructure.Database.Postgres.Migrations +{ + /// + public partial class Init : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "Announcements"); + + migrationBuilder.CreateTable( + name: "Announcements", + schema: "Announcements", + columns: table => new + { + Id = table.Column(type: "character(20)", unicode: false, fixedLength: true, maxLength: 20, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + ExpiresAt = table.Column(type: "timestamp with time zone", nullable: true), + Severity = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Announcements", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AnnouncementText", + schema: "Announcements", + columns: table => new + { + AnnouncementId = table.Column(type: "character(20)", unicode: false, fixedLength: true, maxLength: 20, nullable: false), + Language = table.Column(type: "character(2)", unicode: false, fixedLength: true, maxLength: 2, nullable: false), + Title = table.Column(type: "text", nullable: false), + Body = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AnnouncementText", x => new { x.AnnouncementId, x.Language }); + table.ForeignKey( + name: "FK_AnnouncementText_Announcements_AnnouncementId", + column: x => x.AnnouncementId, + principalSchema: "Announcements", + principalTable: "Announcements", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AnnouncementText", + schema: "Announcements"); + + migrationBuilder.DropTable( + name: "Announcements", + schema: "Announcements"); + } + } +} diff --git a/Modules/Announcements/src/Announcements.Infrastructure.Database.Postgres/Migrations/AnnouncementsDbContextModelSnapshot.cs b/Modules/Announcements/src/Announcements.Infrastructure.Database.Postgres/Migrations/AnnouncementsDbContextModelSnapshot.cs new file mode 100644 index 0000000000..323ea7b3b8 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Infrastructure.Database.Postgres/Migrations/AnnouncementsDbContextModelSnapshot.cs @@ -0,0 +1,91 @@ +// +using System; +using Backbone.Modules.Announcements.Infrastructure.Persistence.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backbone.Modules.Announcements.Infrastructure.Database.Postgres.Migrations +{ + [DbContext(typeof(AnnouncementsDbContext))] + partial class AnnouncementsDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Announcements") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backbone.Modules.Announcements.Domain.Entities.Announcement", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Severity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Announcements", "Announcements"); + }); + + modelBuilder.Entity("Backbone.Modules.Announcements.Domain.Entities.AnnouncementText", b => + { + b.Property("AnnouncementId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("Language") + .HasMaxLength(2) + .IsUnicode(false) + .HasColumnType("character(2)") + .IsFixedLength(); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("AnnouncementId", "Language"); + + b.ToTable("AnnouncementText", "Announcements"); + }); + + modelBuilder.Entity("Backbone.Modules.Announcements.Domain.Entities.AnnouncementText", b => + { + b.HasOne("Backbone.Modules.Announcements.Domain.Entities.Announcement", null) + .WithMany("Texts") + .HasForeignKey("AnnouncementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Announcements.Domain.Entities.Announcement", b => + { + b.Navigation("Texts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Modules/Announcements/src/Announcements.Infrastructure.Database.SqlServer/Announcements.Infrastructure.Database.SqlServer.csproj b/Modules/Announcements/src/Announcements.Infrastructure.Database.SqlServer/Announcements.Infrastructure.Database.SqlServer.csproj new file mode 100644 index 0000000000..ffe2061abd --- /dev/null +++ b/Modules/Announcements/src/Announcements.Infrastructure.Database.SqlServer/Announcements.Infrastructure.Database.SqlServer.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Modules/Announcements/src/Announcements.Infrastructure.Database.SqlServer/Migrations/20241030151918_Init.Designer.cs b/Modules/Announcements/src/Announcements.Infrastructure.Database.SqlServer/Migrations/20241030151918_Init.Designer.cs new file mode 100644 index 0000000000..926a2401cc --- /dev/null +++ b/Modules/Announcements/src/Announcements.Infrastructure.Database.SqlServer/Migrations/20241030151918_Init.Designer.cs @@ -0,0 +1,94 @@ +// +using System; +using Backbone.Modules.Announcements.Infrastructure.Persistence.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Backbone.Modules.Announcements.Infrastructure.Database.SqlServer.Migrations +{ + [DbContext(typeof(AnnouncementsDbContext))] + [Migration("20241030151918_Init")] + partial class Init + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Announcements") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Backbone.Modules.Announcements.Domain.Entities.Announcement", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("Severity") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Announcements", "Announcements"); + }); + + modelBuilder.Entity("Backbone.Modules.Announcements.Domain.Entities.AnnouncementText", b => + { + b.Property("AnnouncementId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("Language") + .HasMaxLength(2) + .IsUnicode(false) + .HasColumnType("char(2)") + .IsFixedLength(); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AnnouncementId", "Language"); + + b.ToTable("AnnouncementText", "Announcements"); + }); + + modelBuilder.Entity("Backbone.Modules.Announcements.Domain.Entities.AnnouncementText", b => + { + b.HasOne("Backbone.Modules.Announcements.Domain.Entities.Announcement", null) + .WithMany("Texts") + .HasForeignKey("AnnouncementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Announcements.Domain.Entities.Announcement", b => + { + b.Navigation("Texts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Modules/Announcements/src/Announcements.Infrastructure.Database.SqlServer/Migrations/20241030151918_Init.cs b/Modules/Announcements/src/Announcements.Infrastructure.Database.SqlServer/Migrations/20241030151918_Init.cs new file mode 100644 index 0000000000..0740713279 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Infrastructure.Database.SqlServer/Migrations/20241030151918_Init.cs @@ -0,0 +1,67 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backbone.Modules.Announcements.Infrastructure.Database.SqlServer.Migrations +{ + /// + public partial class Init : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "Announcements"); + + migrationBuilder.CreateTable( + name: "Announcements", + schema: "Announcements", + columns: table => new + { + Id = table.Column(type: "char(20)", unicode: false, fixedLength: true, maxLength: 20, nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + ExpiresAt = table.Column(type: "datetime2", nullable: true), + Severity = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Announcements", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AnnouncementText", + schema: "Announcements", + columns: table => new + { + AnnouncementId = table.Column(type: "char(20)", unicode: false, fixedLength: true, maxLength: 20, nullable: false), + Language = table.Column(type: "char(2)", unicode: false, fixedLength: true, maxLength: 2, nullable: false), + Title = table.Column(type: "nvarchar(max)", nullable: false), + Body = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AnnouncementText", x => new { x.AnnouncementId, x.Language }); + table.ForeignKey( + name: "FK_AnnouncementText_Announcements_AnnouncementId", + column: x => x.AnnouncementId, + principalSchema: "Announcements", + principalTable: "Announcements", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AnnouncementText", + schema: "Announcements"); + + migrationBuilder.DropTable( + name: "Announcements", + schema: "Announcements"); + } + } +} diff --git a/Modules/Announcements/src/Announcements.Infrastructure.Database.SqlServer/Migrations/AnnouncementsDbContextModelSnapshot.cs b/Modules/Announcements/src/Announcements.Infrastructure.Database.SqlServer/Migrations/AnnouncementsDbContextModelSnapshot.cs new file mode 100644 index 0000000000..1c8b00bc89 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Infrastructure.Database.SqlServer/Migrations/AnnouncementsDbContextModelSnapshot.cs @@ -0,0 +1,91 @@ +// +using System; +using Backbone.Modules.Announcements.Infrastructure.Persistence.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Backbone.Modules.Announcements.Infrastructure.Database.SqlServer.Migrations +{ + [DbContext(typeof(AnnouncementsDbContext))] + partial class AnnouncementsDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Announcements") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Backbone.Modules.Announcements.Domain.Entities.Announcement", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("Severity") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Announcements", "Announcements"); + }); + + modelBuilder.Entity("Backbone.Modules.Announcements.Domain.Entities.AnnouncementText", b => + { + b.Property("AnnouncementId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("Language") + .HasMaxLength(2) + .IsUnicode(false) + .HasColumnType("char(2)") + .IsFixedLength(); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AnnouncementId", "Language"); + + b.ToTable("AnnouncementText", "Announcements"); + }); + + modelBuilder.Entity("Backbone.Modules.Announcements.Domain.Entities.AnnouncementText", b => + { + b.HasOne("Backbone.Modules.Announcements.Domain.Entities.Announcement", null) + .WithMany("Texts") + .HasForeignKey("AnnouncementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Announcements.Domain.Entities.Announcement", b => + { + b.Navigation("Texts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Modules/Announcements/src/Announcements.Infrastructure/Announcements.Infrastructure.csproj b/Modules/Announcements/src/Announcements.Infrastructure/Announcements.Infrastructure.csproj new file mode 100644 index 0000000000..e0f7050ac5 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Infrastructure/Announcements.Infrastructure.csproj @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/Modules/Announcements/src/Announcements.Infrastructure/Persistence/Database/AnnouncementsDbContext.cs b/Modules/Announcements/src/Announcements.Infrastructure/Persistence/Database/AnnouncementsDbContext.cs new file mode 100644 index 0000000000..bd2461fbbd --- /dev/null +++ b/Modules/Announcements/src/Announcements.Infrastructure/Persistence/Database/AnnouncementsDbContext.cs @@ -0,0 +1,42 @@ +using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus; +using Backbone.BuildingBlocks.Infrastructure.Persistence.Database; +using Backbone.Modules.Announcements.Domain.Entities; +using Backbone.Modules.Announcements.Infrastructure.Persistence.Database.ValueConverters; +using Microsoft.EntityFrameworkCore; + +namespace Backbone.Modules.Announcements.Infrastructure.Persistence.Database; + +public class AnnouncementsDbContext : AbstractDbContextBase +{ + public AnnouncementsDbContext() + { + } + + public AnnouncementsDbContext(DbContextOptions options, IEventBus eventBus) : base(options, eventBus) + { + } + + public AnnouncementsDbContext(DbContextOptions options, IServiceProvider serviceProvider, IEventBus eventBus) : base(options, eventBus, serviceProvider) + { + } + + public virtual DbSet Announcements { get; set; } = null!; + + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) + { + base.ConfigureConventions(configurationBuilder); + + configurationBuilder.Properties().AreUnicode(false).AreFixedLength().HaveMaxLength(AnnouncementId.MAX_LENGTH).HaveConversion(); + configurationBuilder.Properties().AreUnicode(false).AreFixedLength().HaveMaxLength(AnnouncementLanguage.LENGTH) + .HaveConversion(); + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + builder.HasDefaultSchema("Announcements"); + + builder.ApplyConfigurationsFromAssembly(typeof(AnnouncementsDbContext).Assembly); + } +} diff --git a/Modules/Announcements/src/Announcements.Infrastructure/Persistence/Database/EntityTypeConfigurations/AnnouncementEntityTypeConfiguration.cs b/Modules/Announcements/src/Announcements.Infrastructure/Persistence/Database/EntityTypeConfigurations/AnnouncementEntityTypeConfiguration.cs new file mode 100644 index 0000000000..1d49dea3a2 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Infrastructure/Persistence/Database/EntityTypeConfigurations/AnnouncementEntityTypeConfiguration.cs @@ -0,0 +1,21 @@ +using Backbone.BuildingBlocks.Infrastructure.Persistence.Database.EntityTypeConfigurations; +using Backbone.Modules.Announcements.Domain.Entities; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Backbone.Modules.Announcements.Infrastructure.Persistence.Database.EntityTypeConfigurations; + +public class AnnouncementEntityTypeConfiguration : EntityEntityTypeConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + base.Configure(builder); + + builder.HasKey(a => a.Id); + + builder.Property(a => a.CreatedAt); + builder.Property(a => a.ExpiresAt); + builder.Property(a => a.Severity); + + builder.HasMany(a => a.Texts).WithOne(); + } +} diff --git a/Modules/Announcements/src/Announcements.Infrastructure/Persistence/Database/EntityTypeConfigurations/AnnouncementsTextEntityTypeConfiguration.cs b/Modules/Announcements/src/Announcements.Infrastructure/Persistence/Database/EntityTypeConfigurations/AnnouncementsTextEntityTypeConfiguration.cs new file mode 100644 index 0000000000..6103683463 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Infrastructure/Persistence/Database/EntityTypeConfigurations/AnnouncementsTextEntityTypeConfiguration.cs @@ -0,0 +1,18 @@ +using Backbone.BuildingBlocks.Infrastructure.Persistence.Database.EntityTypeConfigurations; +using Backbone.Modules.Announcements.Domain.Entities; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Backbone.Modules.Announcements.Infrastructure.Persistence.Database.EntityTypeConfigurations; + +public class AnnouncementsTextEntityTypeConfiguration : EntityEntityTypeConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + base.Configure(builder); + + builder.HasKey(a => new { a.AnnouncementId, a.Language }); + + builder.Property(a => a.Title); + builder.Property(a => a.Body); + } +} diff --git a/Modules/Announcements/src/Announcements.Infrastructure/Persistence/Database/IServiceCollectionExtensions.cs b/Modules/Announcements/src/Announcements.Infrastructure/Persistence/Database/IServiceCollectionExtensions.cs new file mode 100644 index 0000000000..eec4784f91 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Infrastructure/Persistence/Database/IServiceCollectionExtensions.cs @@ -0,0 +1,70 @@ +using Backbone.Modules.Announcements.Application.Infrastructure.Persistence.Repository; +using Backbone.Modules.Announcements.Infrastructure.Persistence.Repository; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.Extensions.DependencyInjection; + +namespace Backbone.Modules.Announcements.Infrastructure.Persistence.Database; + +public static class IServiceCollectionExtensions +{ + private const string SQLSERVER = "SqlServer"; + private const string SQLSERVER_MIGRATIONS_ASSEMBLY = "Backbone.Modules.Announcements.Infrastructure.Database.SqlServer"; + private const string POSTGRES = "Postgres"; + private const string POSTGRES_MIGRATIONS_ASSEMBLY = "Backbone.Modules.Announcements.Infrastructure.Database.Postgres"; + + public static void AddDatabase(this IServiceCollection services, Action setupOptions) + { + var options = new DbOptions(); + setupOptions.Invoke(options); + + services.AddDatabase(options); + } + + public static void AddDatabase(this IServiceCollection services, DbOptions options) + { + services + .AddDbContext(dbContextOptions => + { + switch (options.Provider) + { + case SQLSERVER: + dbContextOptions.UseSqlServer(options.ConnectionString, sqlOptions => + { + sqlOptions.CommandTimeout(options.CommandTimeout); + sqlOptions.MigrationsAssembly(SQLSERVER_MIGRATIONS_ASSEMBLY); + sqlOptions.EnableRetryOnFailure(options.RetryOptions.MaxRetryCount, TimeSpan.FromSeconds(options.RetryOptions.MaxRetryDelayInSeconds), null); + sqlOptions.MigrationsHistoryTable(HistoryRepository.DefaultTableName, "Announcements"); + }); + break; + case POSTGRES: + dbContextOptions.UseNpgsql(options.ConnectionString, sqlOptions => + { + sqlOptions.CommandTimeout(options.CommandTimeout); + sqlOptions.MigrationsAssembly(POSTGRES_MIGRATIONS_ASSEMBLY); + sqlOptions.EnableRetryOnFailure(options.RetryOptions.MaxRetryCount, TimeSpan.FromSeconds(options.RetryOptions.MaxRetryDelayInSeconds), null); + sqlOptions.MigrationsHistoryTable(HistoryRepository.DefaultTableName, "Announcements"); + }); + break; + default: + throw new Exception($"Unsupported database provider: {options.Provider}"); + } + }); + + services.AddTransient(); + } + + public class DbOptions + { + public string Provider { get; set; } = null!; + public string ConnectionString { get; set; } = null!; + public int CommandTimeout { get; set; } = 20; + public RetryOptions RetryOptions { get; set; } = new(); + } + + public class RetryOptions + { + public byte MaxRetryCount { get; set; } = 15; + public int MaxRetryDelayInSeconds { get; set; } = 30; + } +} diff --git a/Modules/Announcements/src/Announcements.Infrastructure/Persistence/Database/ValueConverters/AnnouncementIdEntityFrameworkValueConverter.cs b/Modules/Announcements/src/Announcements.Infrastructure/Persistence/Database/ValueConverters/AnnouncementIdEntityFrameworkValueConverter.cs new file mode 100644 index 0000000000..1cc0c201fd --- /dev/null +++ b/Modules/Announcements/src/Announcements.Infrastructure/Persistence/Database/ValueConverters/AnnouncementIdEntityFrameworkValueConverter.cs @@ -0,0 +1,20 @@ +using Backbone.Modules.Announcements.Domain.Entities; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Backbone.Modules.Announcements.Infrastructure.Persistence.Database.ValueConverters; + +public class AnnouncementIdEntityFrameworkValueConverter : ValueConverter +{ + public AnnouncementIdEntityFrameworkValueConverter() : this(new ConverterMappingHints()) + { + } + + public AnnouncementIdEntityFrameworkValueConverter(ConverterMappingHints mappingHints) + : base( + id => id.Value, + value => AnnouncementId.Parse(value), + mappingHints + ) + { + } +} diff --git a/Modules/Announcements/src/Announcements.Infrastructure/Persistence/Database/ValueConverters/AnnouncementLanguageEntityFrameworkValueConverter.cs b/Modules/Announcements/src/Announcements.Infrastructure/Persistence/Database/ValueConverters/AnnouncementLanguageEntityFrameworkValueConverter.cs new file mode 100644 index 0000000000..e35fcbb86a --- /dev/null +++ b/Modules/Announcements/src/Announcements.Infrastructure/Persistence/Database/ValueConverters/AnnouncementLanguageEntityFrameworkValueConverter.cs @@ -0,0 +1,20 @@ +using Backbone.Modules.Announcements.Domain.Entities; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Backbone.Modules.Announcements.Infrastructure.Persistence.Database.ValueConverters; + +public class AnnouncementLanguageEntityFrameworkValueConverter : ValueConverter +{ + public AnnouncementLanguageEntityFrameworkValueConverter() : this(new ConverterMappingHints()) + { + } + + public AnnouncementLanguageEntityFrameworkValueConverter(ConverterMappingHints mappingHints) + : base( + id => id.Value, + value => AnnouncementLanguage.Parse(value), + mappingHints + ) + { + } +} diff --git a/Modules/Announcements/src/Announcements.Infrastructure/Persistence/IServiceCollectionExtensions.cs b/Modules/Announcements/src/Announcements.Infrastructure/Persistence/IServiceCollectionExtensions.cs new file mode 100644 index 0000000000..b4e1624363 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Infrastructure/Persistence/IServiceCollectionExtensions.cs @@ -0,0 +1,25 @@ +using Backbone.Modules.Announcements.Infrastructure.Persistence.Database; +using Microsoft.Extensions.DependencyInjection; + +namespace Backbone.Modules.Announcements.Infrastructure.Persistence; + +public static class IServiceCollectionExtensions +{ + public static void AddPersistence(this IServiceCollection services, Action setupOptions) + { + var options = new PersistenceOptions(); + setupOptions.Invoke(options); + + services.AddPersistence(options); + } + + public static void AddPersistence(this IServiceCollection services, PersistenceOptions options) + { + services.AddDatabase(options.DbOptions); + } +} + +public class PersistenceOptions +{ + public Database.IServiceCollectionExtensions.DbOptions DbOptions { get; set; } = new(); +} diff --git a/Modules/Announcements/src/Announcements.Infrastructure/Persistence/Repository/AnnouncementsRepository.cs b/Modules/Announcements/src/Announcements.Infrastructure/Persistence/Repository/AnnouncementsRepository.cs new file mode 100644 index 0000000000..09788dd055 --- /dev/null +++ b/Modules/Announcements/src/Announcements.Infrastructure/Persistence/Repository/AnnouncementsRepository.cs @@ -0,0 +1,33 @@ +using Backbone.BuildingBlocks.Application.Extensions; +using Backbone.Modules.Announcements.Application.Infrastructure.Persistence.Repository; +using Backbone.Modules.Announcements.Domain.Entities; +using Backbone.Modules.Announcements.Infrastructure.Persistence.Database; +using Microsoft.EntityFrameworkCore; + +namespace Backbone.Modules.Announcements.Infrastructure.Persistence.Repository; + +public class AnnouncementsRepository : IAnnouncementsRepository +{ + private readonly AnnouncementsDbContext _dbContext; + private readonly DbSet _announcements; + private readonly IQueryable _readOnlyAnnouncements; + + public AnnouncementsRepository(AnnouncementsDbContext dbContext) + { + _dbContext = dbContext; + _announcements = dbContext.Announcements; + _readOnlyAnnouncements = dbContext.Announcements.AsNoTracking(); + } + + public async Task Add(Announcement announcement, CancellationToken cancellationToken) + { + _announcements.Add(announcement); + + await _dbContext.SaveChangesAsync(cancellationToken); + } + + public Task> FindAll(CancellationToken cancellationToken) + { + return _readOnlyAnnouncements.IncludeAll(_dbContext).ToListAsync(cancellationToken); + } +} diff --git a/Modules/Announcements/test/Announcements.Application.Tests/Announcements.Application.Tests.csproj b/Modules/Announcements/test/Announcements.Application.Tests/Announcements.Application.Tests.csproj new file mode 100644 index 0000000000..3e3cf13b83 --- /dev/null +++ b/Modules/Announcements/test/Announcements.Application.Tests/Announcements.Application.Tests.csproj @@ -0,0 +1,21 @@ + + + + false + true + + + + + + + + + + + + + + + + diff --git a/Modules/Announcements/test/Announcements.Application.Tests/Tests/Announcements/Commands/CreateAnnouncement/ValidatorTests.cs b/Modules/Announcements/test/Announcements.Application.Tests/Tests/Announcements/Commands/CreateAnnouncement/ValidatorTests.cs new file mode 100644 index 0000000000..2f32d745aa --- /dev/null +++ b/Modules/Announcements/test/Announcements.Application.Tests/Tests/Announcements/Commands/CreateAnnouncement/ValidatorTests.cs @@ -0,0 +1,83 @@ +using Backbone.Modules.Announcements.Application.Announcements.Commands.CreateAnnouncement; +using Backbone.Modules.Announcements.Domain.Entities; +using FluentValidation.TestHelper; + +namespace Backbone.Modules.Announcements.Application.Tests.Tests.Announcements.Commands.CreateAnnouncement; + +public class ValidatorTests +{ + [Fact] + public void Accepts_a_valid_object() + { + // Arrange + var validator = new Validator(); + + // Act + var validationResult = validator.TestValidate(CreateCommand()); + + // Assert + validationResult.ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public void Expects_an_english_text() + { + // Arrange + var validator = new Validator(); + + // Act + var validationResult = validator.TestValidate(CreateCommand(languages: ["de"])); + + // Assert + validationResult.ShouldHaveValidationErrorFor(c => c.Texts).WithErrorMessage("There must be a text for English."); + } + + [Fact] + public void Expects_two_letter_language_codes() + { + // Arrange + var validator = new Validator(); + + // Act + var validationResult = validator.TestValidate(CreateCommand(languages: ["eng", "e"])); + + // Assert + validationResult.ShouldHaveValidationErrorFor("Texts[0].Language").WithErrorCode("error.platform.validation.invalidPropertyValue") + .WithErrorMessage("This language is not a valid two letter ISO language name."); + validationResult.ShouldHaveValidationErrorFor("Texts[1].Language").WithErrorCode("error.platform.validation.invalidPropertyValue") + .WithErrorMessage("This language is not a valid two letter ISO language name."); + } + + [Fact] + public void Expects_non_empty_title_and_body() + { + // Arrange + var validator = new Validator(); + + // Act + var validationResult = validator.TestValidate(CreateCommand(title: "", body: null)); + + // Assert + validationResult.ShouldHaveValidationErrorFor("Texts[0].Title").WithErrorCode("error.platform.validation.invalidPropertyValue"); + validationResult.ShouldHaveValidationErrorFor("Texts[0].Body").WithErrorCode("error.platform.validation.invalidPropertyValue"); + } + + private static CreateAnnouncementCommand CreateCommand(List? languages = null, string title = "Test Title", string? body = "Test Body") + { + languages ??= ["en", "de"]; + + var command = new CreateAnnouncementCommand + { + ExpiresAt = null, + Severity = AnnouncementSeverity.Low, + Texts = languages.Select(l => new CreateAnnouncementCommandText + { + Language = l, + Title = title, + Body = body! + }).ToList() + }; + + return command; + } +} diff --git a/Modules/Challenges/src/Challenges.Infrastructure/Persistence/IServiceCollectionExtensions.cs b/Modules/Challenges/src/Challenges.Infrastructure/Persistence/IServiceCollectionExtensions.cs index b8a7363823..4928fe8a4e 100644 --- a/Modules/Challenges/src/Challenges.Infrastructure/Persistence/IServiceCollectionExtensions.cs +++ b/Modules/Challenges/src/Challenges.Infrastructure/Persistence/IServiceCollectionExtensions.cs @@ -1,6 +1,6 @@ using Backbone.Modules.Challenges.Application.Infrastructure.Persistence.Repository; using Backbone.Modules.Challenges.Infrastructure.Persistence.Database; -using Backbone.Modules.Challenges.Infrastructure.Persistence.Database.Repository; +using Backbone.Modules.Challenges.Infrastructure.Persistence.Repository; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.Extensions.DependencyInjection; diff --git a/Modules/Challenges/src/Challenges.Infrastructure/Persistence/Database/Repository/ChallengesRepository.cs b/Modules/Challenges/src/Challenges.Infrastructure/Persistence/Repository/ChallengesRepository.cs similarity index 94% rename from Modules/Challenges/src/Challenges.Infrastructure/Persistence/Database/Repository/ChallengesRepository.cs rename to Modules/Challenges/src/Challenges.Infrastructure/Persistence/Repository/ChallengesRepository.cs index 2fd2127c00..5905927890 100644 --- a/Modules/Challenges/src/Challenges.Infrastructure/Persistence/Database/Repository/ChallengesRepository.cs +++ b/Modules/Challenges/src/Challenges.Infrastructure/Persistence/Repository/ChallengesRepository.cs @@ -3,9 +3,11 @@ using Backbone.Modules.Challenges.Application.Infrastructure.Persistence.Repository; using Backbone.Modules.Challenges.Domain.Entities; using Backbone.Modules.Challenges.Domain.Ids; +using Backbone.Modules.Challenges.Infrastructure.Persistence.Database; using Microsoft.EntityFrameworkCore; -namespace Backbone.Modules.Challenges.Infrastructure.Persistence.Database.Repository; +namespace Backbone.Modules.Challenges.Infrastructure.Persistence.Repository; + public class ChallengesRepository : IChallengesRepository { private readonly DbSet _challenges; @@ -16,6 +18,7 @@ public ChallengesRepository(ChallengesDbContext dbContext) _challenges = dbContext.Challenges; _dbContext = dbContext; } + public async Task Find(ChallengeId id, CancellationToken cancellationToken) { return await _challenges diff --git a/Modules/Challenges/src/Challenges.Jobs.Cleanup/Program.cs b/Modules/Challenges/src/Challenges.Jobs.Cleanup/Program.cs index ab26e04ce7..0a73ce3720 100644 --- a/Modules/Challenges/src/Challenges.Jobs.Cleanup/Program.cs +++ b/Modules/Challenges/src/Challenges.Jobs.Cleanup/Program.cs @@ -4,7 +4,7 @@ using Backbone.Modules.Challenges.Application.Extensions; using Backbone.Modules.Challenges.Application.Infrastructure.Persistence.Repository; using Backbone.Modules.Challenges.Infrastructure.Persistence.Database; -using Backbone.Modules.Challenges.Infrastructure.Persistence.Database.Repository; +using Backbone.Modules.Challenges.Infrastructure.Persistence.Repository; using Microsoft.EntityFrameworkCore; namespace Backbone.Modules.Challenges.Jobs.Cleanup; diff --git a/Modules/Devices/src/Devices.Application/ApplicationErrors.cs b/Modules/Devices/src/Devices.Application/ApplicationErrors.cs index a300be17a7..9967cff514 100644 --- a/Modules/Devices/src/Devices.Application/ApplicationErrors.cs +++ b/Modules/Devices/src/Devices.Application/ApplicationErrors.cs @@ -62,5 +62,11 @@ public static ApplicationError ClientReachedIdentitiesLimit() return new ApplicationError("error.platform.validation.device.clientReachedIdentitiesLimit", "The client's Identity limit has been reached. A new Identity cannot be created with this client."); } + + public static ApplicationError BackupDeviceAlreadyExists() + { + return new ApplicationError("error.platform.validation.device.onlyOneBackupDeviceCanExist", + "Only one backup device can be created per identity."); + } } } diff --git a/Modules/Devices/src/Devices.Application/DTOs/IdentityDeletionProcessDetailsDTO.cs b/Modules/Devices/src/Devices.Application/DTOs/IdentityDeletionProcessDetailsDTO.cs index e99e5df897..1a91cce51c 100644 --- a/Modules/Devices/src/Devices.Application/DTOs/IdentityDeletionProcessDetailsDTO.cs +++ b/Modules/Devices/src/Devices.Application/DTOs/IdentityDeletionProcessDetailsDTO.cs @@ -67,7 +67,7 @@ public IdentityDeletionProcessAuditLogEntryDTO(IdentityDeletionProcessAuditLogEn public string Id { get; set; } public DateTime CreatedAt { get; set; } public DeletionProcessStatus? OldStatus { get; set; } - public DeletionProcessStatus NewStatus { get; set; } + public DeletionProcessStatus? NewStatus { get; set; } public Dictionary AdditionalData { get; set; } public MessageKey MessageKey { get; set; } } diff --git a/Modules/Devices/src/Devices.Application/Devices/Commands/DeleteDevice/Handler.cs b/Modules/Devices/src/Devices.Application/Devices/Commands/DeleteDevice/Handler.cs index 39abd3bb4d..c7d4eedecd 100644 --- a/Modules/Devices/src/Devices.Application/Devices/Commands/DeleteDevice/Handler.cs +++ b/Modules/Devices/src/Devices.Application/Devices/Commands/DeleteDevice/Handler.cs @@ -26,10 +26,11 @@ public async Task Handle(DeleteDeviceCommand request, CancellationToken cancella var deviceId = DeviceId.Parse(request.DeviceId); var deviceThatIsBeingDeleted = await _identitiesRepository.GetDeviceById(deviceId, cancellationToken, track: true) ?? throw new NotFoundException(nameof(Device)); - deviceThatIsBeingDeleted.MarkAsDeleted(_userContext.GetDeviceId(), _userContext.GetAddress()); - await _identitiesRepository.Update(deviceThatIsBeingDeleted, cancellationToken); + deviceThatIsBeingDeleted.EnsureCanBeDeleted(_userContext.GetAddress()); - _logger.MarkedDeviceAsDeleted(); + await _identitiesRepository.DeleteDevice(deviceThatIsBeingDeleted, cancellationToken); + + _logger.DeviceDeleted(); } } @@ -37,8 +38,8 @@ internal static partial class DeleteDeviceLogs { [LoggerMessage( EventId = 776010, - EventName = "Devices.MarkDeviceAsDeleted.MarkedDeviceAsDeleted", + EventName = "Devices.DeleteDevice.DeviceDeleted", Level = LogLevel.Information, - Message = "Successfully marked the device as deleted.")] - public static partial void MarkedDeviceAsDeleted(this ILogger logger); + Message = "The device was deleted.")] + public static partial void DeviceDeleted(this ILogger logger); } diff --git a/Modules/Devices/src/Devices.Application/Devices/Commands/RegisterDevice/Handler.cs b/Modules/Devices/src/Devices.Application/Devices/Commands/RegisterDevice/Handler.cs index 80444ab32b..c43c527915 100644 --- a/Modules/Devices/src/Devices.Application/Devices/Commands/RegisterDevice/Handler.cs +++ b/Modules/Devices/src/Devices.Application/Devices/Commands/RegisterDevice/Handler.cs @@ -9,6 +9,7 @@ using Backbone.Modules.Devices.Domain.Entities.Identities; using MediatR; using Microsoft.Extensions.Logging; +using ApplicationException = Backbone.BuildingBlocks.Application.Abstractions.Exceptions.ApplicationException; namespace Backbone.Modules.Devices.Application.Devices.Commands.RegisterDevice; @@ -31,18 +32,21 @@ public async Task Handle(RegisterDeviceCommand command, { var identity = await _identitiesRepository.FindByAddress(_userContext.GetAddress(), cancellationToken, track: true) ?? throw new NotFoundException(nameof(Identity)); + if (command.IsBackupDevice && await _identitiesRepository.HasBackupDevice(identity.Address, cancellationToken)) + throw new ApplicationException(ApplicationErrors.Devices.BackupDeviceAlreadyExists()); + await _challengeValidator.Validate(command.SignedChallenge, PublicKey.FromBytes(identity.PublicKey)); _logger.LogTrace("Successfully validated challenge."); var communicationLanguageResult = CommunicationLanguage.Create(command.CommunicationLanguage); - var newDevice = identity.AddDevice(communicationLanguageResult.Value, _userContext.GetDeviceId()); + var newDevice = identity.AddDevice(communicationLanguageResult.Value, _userContext.GetDeviceId(), command.IsBackupDevice); await _identitiesRepository.UpdateWithNewDevice(identity, command.DevicePassword); _logger.CreatedDevice(); - return new RegisterDeviceResponse(newDevice.User); + return new RegisterDeviceResponse(newDevice); } } diff --git a/Modules/Devices/src/Devices.Application/Devices/Commands/RegisterDevice/RegisterDeviceCommand.cs b/Modules/Devices/src/Devices.Application/Devices/Commands/RegisterDevice/RegisterDeviceCommand.cs index 4db880cb98..1d30bf77c1 100644 --- a/Modules/Devices/src/Devices.Application/Devices/Commands/RegisterDevice/RegisterDeviceCommand.cs +++ b/Modules/Devices/src/Devices.Application/Devices/Commands/RegisterDevice/RegisterDeviceCommand.cs @@ -8,4 +8,5 @@ public class RegisterDeviceCommand : IRequest public required string DevicePassword { get; set; } public required string CommunicationLanguage { get; set; } public required SignedChallengeDTO SignedChallenge { get; set; } + public required bool IsBackupDevice { get; set; } } diff --git a/Modules/Devices/src/Devices.Application/Devices/Commands/RegisterDevice/RegisterDeviceResponse.cs b/Modules/Devices/src/Devices.Application/Devices/Commands/RegisterDevice/RegisterDeviceResponse.cs index 2775faedc7..be795371f3 100644 --- a/Modules/Devices/src/Devices.Application/Devices/Commands/RegisterDevice/RegisterDeviceResponse.cs +++ b/Modules/Devices/src/Devices.Application/Devices/Commands/RegisterDevice/RegisterDeviceResponse.cs @@ -4,16 +4,18 @@ namespace Backbone.Modules.Devices.Application.Devices.Commands.RegisterDevice; public class RegisterDeviceResponse { - public RegisterDeviceResponse(ApplicationUser user) + public RegisterDeviceResponse(Device device) { - Id = user.DeviceId; - Username = user.UserName!; - CreatedByDevice = user.Device.CreatedByDevice; - CreatedAt = user.Device.CreatedAt; + Id = device.Id.Value; + Username = device.User.UserName!; + CreatedByDevice = device.CreatedByDevice.Value; + CreatedAt = device.CreatedAt; + IsBackupDevice = device.IsBackupDevice; } public string Id { get; set; } public string Username { get; set; } public DateTime CreatedAt { get; set; } public string CreatedByDevice { get; set; } + public bool IsBackupDevice { get; set; } } diff --git a/Modules/Devices/src/Devices.Application/Devices/Commands/UpdateActiveDevice/Validator.cs b/Modules/Devices/src/Devices.Application/Devices/Commands/UpdateActiveDevice/Validator.cs new file mode 100644 index 0000000000..e05a003dda --- /dev/null +++ b/Modules/Devices/src/Devices.Application/Devices/Commands/UpdateActiveDevice/Validator.cs @@ -0,0 +1,13 @@ +using Backbone.BuildingBlocks.Application.FluentValidation; +using Backbone.Modules.Devices.Domain; +using FluentValidation; + +namespace Backbone.Modules.Devices.Application.Devices.Commands.UpdateActiveDevice; + +public class Validator : AbstractValidator +{ + public Validator() + { + RuleFor(c => c.CommunicationLanguage).TwoLetterIsoLanguage().WithErrorCode(DomainErrors.InvalidDeviceCommunicationLanguage().Code); + } +} diff --git a/Modules/Devices/src/Devices.Application/Devices/DTOs/DeviceDTO.cs b/Modules/Devices/src/Devices.Application/Devices/DTOs/DeviceDTO.cs index ea9e655fe5..759cc26052 100644 --- a/Modules/Devices/src/Devices.Application/Devices/DTOs/DeviceDTO.cs +++ b/Modules/Devices/src/Devices.Application/Devices/DTOs/DeviceDTO.cs @@ -12,6 +12,7 @@ public DeviceDTO(Device device) CreatedByDevice = device.CreatedByDevice; LastLogin = new LastLoginInformation { Time = device.User.LastLoginAt }; CommunicationLanguage = device.CommunicationLanguage; + IsBackupDevice = device.IsBackupDevice; } public string Id { get; set; } @@ -20,6 +21,7 @@ public DeviceDTO(Device device) public string CreatedByDevice { get; set; } public LastLoginInformation LastLogin { get; set; } public string CommunicationLanguage { get; set; } + public bool IsBackupDevice { get; set; } } public class LastLoginInformation diff --git a/Modules/Devices/src/Devices.Application/DomainEvents/Incoming/AnnouncementCreated/AnnouncementCreatedDomainEventHandler.cs b/Modules/Devices/src/Devices.Application/DomainEvents/Incoming/AnnouncementCreated/AnnouncementCreatedDomainEventHandler.cs new file mode 100644 index 0000000000..123ac6c17e --- /dev/null +++ b/Modules/Devices/src/Devices.Application/DomainEvents/Incoming/AnnouncementCreated/AnnouncementCreatedDomainEventHandler.cs @@ -0,0 +1,24 @@ +using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus; +using Backbone.BuildingBlocks.Application.PushNotifications; +using Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.Announcements; +using Backbone.Modules.Devices.Domain.DomainEvents.Incoming.AnnouncementCreated; + +namespace Backbone.Modules.Devices.Application.DomainEvents.Incoming.AnnouncementCreated; + +public class AnnouncementCreatedDomainEventHandler : IDomainEventHandler +{ + private readonly IPushNotificationSender _pushSenderService; + + public AnnouncementCreatedDomainEventHandler(IPushNotificationSender pushSenderService) + { + _pushSenderService = pushSenderService; + } + + public async Task Handle(AnnouncementCreatedDomainEvent @event) + { + var pushNotificationTexts = @event.Texts.ToDictionary(k => k.Language, k => new NotificationText(k.Title, k.Body) { Title = k.Title, Body = k.Body }); + + await _pushSenderService.SendNotification(new NewAnnouncementPushNotification { AnnouncementId = @event.Id }, SendPushNotificationFilter.AllDevicesOfAllIdentities(), pushNotificationTexts, + CancellationToken.None); + } +} diff --git a/Modules/Devices/src/Devices.Application/DomainEvents/Incoming/BackupDeviceUsed/BackupDeviceUsedDomainEventHandler.cs b/Modules/Devices/src/Devices.Application/DomainEvents/Incoming/BackupDeviceUsed/BackupDeviceUsedDomainEventHandler.cs new file mode 100644 index 0000000000..67764931e7 --- /dev/null +++ b/Modules/Devices/src/Devices.Application/DomainEvents/Incoming/BackupDeviceUsed/BackupDeviceUsedDomainEventHandler.cs @@ -0,0 +1,21 @@ +using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus; +using Backbone.BuildingBlocks.Application.PushNotifications; +using Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.Device; +using Backbone.Modules.Devices.Domain.DomainEvents.Outgoing; + +namespace Backbone.Modules.Devices.Application.DomainEvents.Incoming.BackupDeviceUsed; + +public class BackupDeviceUsedDomainEventHandler : IDomainEventHandler +{ + private readonly IPushNotificationSender _pushNotificationSender; + + public BackupDeviceUsedDomainEventHandler(IPushNotificationSender pushNotificationSender) + { + _pushNotificationSender = pushNotificationSender; + } + + public async Task Handle(BackupDeviceUsedDomainEvent @event) + { + await _pushNotificationSender.SendNotification(new BackupDeviceUsedPushNotification(), SendPushNotificationFilter.AllDevicesOf(@event.IdentityAddress), CancellationToken.None); + } +} diff --git a/Modules/Devices/src/Devices.Application/Extensions/IEventBusExtensions.cs b/Modules/Devices/src/Devices.Application/Extensions/IEventBusExtensions.cs index 20ecccc02c..271dbdb2de 100644 --- a/Modules/Devices/src/Devices.Application/Extensions/IEventBusExtensions.cs +++ b/Modules/Devices/src/Devices.Application/Extensions/IEventBusExtensions.cs @@ -1,7 +1,10 @@ using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus; +using Backbone.Modules.Devices.Application.DomainEvents.Incoming.AnnouncementCreated; +using Backbone.Modules.Devices.Application.DomainEvents.Incoming.BackupDeviceUsed; using Backbone.Modules.Devices.Application.DomainEvents.Incoming.DatawalletModificationCreated; using Backbone.Modules.Devices.Application.DomainEvents.Incoming.ExternalEventCreated; using Backbone.Modules.Devices.Application.DomainEvents.Incoming.IdentityDeletionProcessStarted; +using Backbone.Modules.Devices.Domain.DomainEvents.Incoming.AnnouncementCreated; using Backbone.Modules.Devices.Domain.DomainEvents.Incoming.DatawalletModificationCreated; using Backbone.Modules.Devices.Domain.DomainEvents.Incoming.ExternalEventCreated; using Backbone.Modules.Devices.Domain.DomainEvents.Outgoing; @@ -12,10 +15,22 @@ public static class IEventBusExtensions { public static void AddDevicesDomainEventSubscriptions(this IEventBus eventBus) { - SubscribeToSynchronizationEvents(eventBus); + eventBus.SubscribeToAnnouncementsEvents(); + eventBus.SubscribeToDevicesEvents(); + eventBus.SubscribeToSynchronizationEvents(); } - private static void SubscribeToSynchronizationEvents(IEventBus eventBus) + private static void SubscribeToAnnouncementsEvents(this IEventBus eventBus) + { + eventBus.Subscribe(); + } + + private static void SubscribeToDevicesEvents(this IEventBus eventBus) + { + eventBus.Subscribe(); + } + + private static void SubscribeToSynchronizationEvents(this IEventBus eventBus) { eventBus.Subscribe(); eventBus.Subscribe(); diff --git a/Modules/Devices/src/Devices.Application/Identities/Commands/HandleCompletedDeletionProcess/HandleCompletedDeletionProcessCommand.cs b/Modules/Devices/src/Devices.Application/Identities/Commands/HandleCompletedDeletionProcess/HandleCompletedDeletionProcessCommand.cs new file mode 100644 index 0000000000..8945bfed43 --- /dev/null +++ b/Modules/Devices/src/Devices.Application/Identities/Commands/HandleCompletedDeletionProcess/HandleCompletedDeletionProcessCommand.cs @@ -0,0 +1,15 @@ +using MediatR; + +namespace Backbone.Modules.Devices.Application.Identities.Commands.HandleCompletedDeletionProcess; + +public class HandleCompletedDeletionProcessCommand : IRequest +{ + public HandleCompletedDeletionProcessCommand(string identityAddress, IEnumerable usernames) + { + IdentityAddress = identityAddress; + Usernames = usernames; + } + + public string IdentityAddress { get; } + public IEnumerable Usernames { get; } +} diff --git a/Modules/Devices/src/Devices.Application/Identities/Commands/HandleCompletedDeletionProcess/Handler.cs b/Modules/Devices/src/Devices.Application/Identities/Commands/HandleCompletedDeletionProcess/Handler.cs new file mode 100644 index 0000000000..4a3ba15670 --- /dev/null +++ b/Modules/Devices/src/Devices.Application/Identities/Commands/HandleCompletedDeletionProcess/Handler.cs @@ -0,0 +1,39 @@ +using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository; +using Backbone.Modules.Devices.Domain.Entities.Identities; +using MediatR; + +namespace Backbone.Modules.Devices.Application.Identities.Commands.HandleCompletedDeletionProcess; + +public class Handler : IRequestHandler +{ + private readonly IIdentitiesRepository _identitiesRepository; + + public Handler(IIdentitiesRepository identitiesRepository) + { + _identitiesRepository = identitiesRepository; + } + + public async Task Handle(HandleCompletedDeletionProcessCommand request, CancellationToken cancellationToken) + { + await _identitiesRepository.AddDeletionProcessAuditLogEntry(IdentityDeletionProcessAuditLogEntry.DeletionCompleted(request.IdentityAddress)); + + await AssociateUsernames(request, cancellationToken); + } + + private async Task AssociateUsernames(HandleCompletedDeletionProcessCommand request, CancellationToken cancellationToken) + { + var identityAddressHash = Hasher.HashUtf8(request.IdentityAddress); + + var auditLogEntries = await _identitiesRepository.GetIdentityDeletionProcessAuditLogs(l => l.IdentityAddressHash == identityAddressHash, CancellationToken.None, track: true); + + var auditLogEntriesArray = auditLogEntries.ToArray(); + + foreach (var auditLogEntry in auditLogEntriesArray) + { + auditLogEntry.AssociateUsernames(request.Usernames.Select(Username.Parse)); + } + + await _identitiesRepository.Update(auditLogEntriesArray, cancellationToken); + } +} diff --git a/Modules/Devices/src/Devices.Application/Identities/Commands/HandleCompletedDeletionProcess/Validator.cs b/Modules/Devices/src/Devices.Application/Identities/Commands/HandleCompletedDeletionProcess/Validator.cs new file mode 100644 index 0000000000..9207e08229 --- /dev/null +++ b/Modules/Devices/src/Devices.Application/Identities/Commands/HandleCompletedDeletionProcess/Validator.cs @@ -0,0 +1,15 @@ +using Backbone.BuildingBlocks.Application.Extensions; +using Backbone.BuildingBlocks.Application.FluentValidation; +using Backbone.DevelopmentKit.Identity.ValueObjects; +using FluentValidation; + +namespace Backbone.Modules.Devices.Application.Identities.Commands.HandleCompletedDeletionProcess; + +public class Validator : AbstractValidator +{ + public Validator() + { + RuleFor(c => c.IdentityAddress).ValidId(); + RuleFor(c => c.Usernames).DetailedNotEmpty(); + } +} diff --git a/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsOwner/Handler.cs b/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsOwner/Handler.cs index 732503b184..abb7d6ebbf 100644 --- a/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsOwner/Handler.cs +++ b/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsOwner/Handler.cs @@ -27,7 +27,7 @@ public async Task Handle(StartDeletionProce { var identity = await _identitiesRepository.FindByAddress(_userContext.GetAddress(), cancellationToken, true) ?? throw new NotFoundException(nameof(Identity)); - var deletionProcess = identity.StartDeletionProcessAsOwner(_userContext.GetDeviceId()); + var deletionProcess = identity.StartDeletionProcessAsOwner(_userContext.GetDeviceId(), request.LengthOfGracePeriodInDays); try { diff --git a/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsOwner/StartDeletionProcessAsOwnerCommand.cs b/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsOwner/StartDeletionProcessAsOwnerCommand.cs index ed65e0a036..5f0085cde4 100644 --- a/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsOwner/StartDeletionProcessAsOwnerCommand.cs +++ b/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsOwner/StartDeletionProcessAsOwnerCommand.cs @@ -2,4 +2,7 @@ namespace Backbone.Modules.Devices.Application.Identities.Commands.StartDeletionProcessAsOwner; -public class StartDeletionProcessAsOwnerCommand : IRequest; +public class StartDeletionProcessAsOwnerCommand : IRequest +{ + public double? LengthOfGracePeriodInDays { get; set; } +} diff --git a/Modules/Devices/src/Devices.Application/Identities/Queries/GetDeletionProcessesAuditLogs/Handler.cs b/Modules/Devices/src/Devices.Application/Identities/Queries/GetDeletionProcessesAuditLogs/Handler.cs index 1cf6d20517..029c697acc 100644 --- a/Modules/Devices/src/Devices.Application/Identities/Queries/GetDeletionProcessesAuditLogs/Handler.cs +++ b/Modules/Devices/src/Devices.Application/Identities/Queries/GetDeletionProcessesAuditLogs/Handler.cs @@ -15,7 +15,10 @@ public Handler(IIdentitiesRepository identityRepository) public async Task Handle(GetDeletionProcessesAuditLogsQuery request, CancellationToken cancellationToken) { - var identityDeletionProcessAuditLogEntries = await _identityRepository.GetIdentityDeletionProcessAuditLogsByAddress(Hasher.HashUtf8(request.IdentityAddress), cancellationToken); + var addressHash = Hasher.HashUtf8(request.IdentityAddress); + + var identityDeletionProcessAuditLogEntries = await _identityRepository.GetIdentityDeletionProcessAuditLogs(l => l.IdentityAddressHash == addressHash, cancellationToken); + return new GetDeletionProcessesAuditLogsResponse(identityDeletionProcessAuditLogEntries.OrderBy(e => e.CreatedAt)); } } diff --git a/Modules/Devices/src/Devices.Application/Identities/Queries/IsIdentityOfUserDeleted/Handler.cs b/Modules/Devices/src/Devices.Application/Identities/Queries/IsIdentityOfUserDeleted/Handler.cs new file mode 100644 index 0000000000..37be9ea8b0 --- /dev/null +++ b/Modules/Devices/src/Devices.Application/Identities/Queries/IsIdentityOfUserDeleted/Handler.cs @@ -0,0 +1,44 @@ +using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository; +using Backbone.Modules.Devices.Domain.Entities.Identities; +using MediatR; + +namespace Backbone.Modules.Devices.Application.Identities.Queries.IsIdentityOfUserDeleted; + +public class Handler : IRequestHandler +{ + private readonly IIdentitiesRepository _identitiesRepository; + + public Handler(IIdentitiesRepository identitiesRepository) + { + _identitiesRepository = identitiesRepository; + } + + public async Task Handle(IsIdentityOfUserDeletedQuery request, CancellationToken cancellationToken) + { + var identity = await _identitiesRepository.FindFirst(Identity.HasUser(request.Username), cancellationToken); + + bool isDeleted; + DateTime? deletionGracePeriodEndsAt; + + if (identity != null) + { + isDeleted = identity.IsGracePeriodOver; + deletionGracePeriodEndsAt = identity.IsGracePeriodOver ? identity.DeletionGracePeriodEndsAt : null; + } + else + { + var auditLogEntries = await _identitiesRepository.GetIdentityDeletionProcessAuditLogs( + IdentityDeletionProcessAuditLogEntry.IsAssociatedToUser(Username.Parse(request.Username)), + cancellationToken); + + var deletionCompletedAuditLogEntry = auditLogEntries.FirstOrDefault(l => l.MessageKey == MessageKey.DeletionCompleted); + + isDeleted = deletionCompletedAuditLogEntry != null; + deletionGracePeriodEndsAt = deletionCompletedAuditLogEntry?.CreatedAt; + } + + return new IsIdentityOfUserDeletedResponse(isDeleted, deletionGracePeriodEndsAt); + + } +} diff --git a/Modules/Devices/src/Devices.Application/Identities/Queries/IsIdentityOfUserDeleted/IsIdentityOfUserDeletedQuery.cs b/Modules/Devices/src/Devices.Application/Identities/Queries/IsIdentityOfUserDeleted/IsIdentityOfUserDeletedQuery.cs new file mode 100644 index 0000000000..b5c22c92dc --- /dev/null +++ b/Modules/Devices/src/Devices.Application/Identities/Queries/IsIdentityOfUserDeleted/IsIdentityOfUserDeletedQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace Backbone.Modules.Devices.Application.Identities.Queries.IsIdentityOfUserDeleted; + +public class IsIdentityOfUserDeletedQuery : IRequest +{ + public required string Username { get; init; } +} diff --git a/Modules/Devices/src/Devices.Application/Identities/Queries/IsIdentityOfUserDeleted/IsIdentityOfUserDeletedResponse.cs b/Modules/Devices/src/Devices.Application/Identities/Queries/IsIdentityOfUserDeleted/IsIdentityOfUserDeletedResponse.cs new file mode 100644 index 0000000000..d94fc90363 --- /dev/null +++ b/Modules/Devices/src/Devices.Application/Identities/Queries/IsIdentityOfUserDeleted/IsIdentityOfUserDeletedResponse.cs @@ -0,0 +1,13 @@ +namespace Backbone.Modules.Devices.Application.Identities.Queries.IsIdentityOfUserDeleted; + +public class IsIdentityOfUserDeletedResponse +{ + public IsIdentityOfUserDeletedResponse(bool isDeleted, DateTime? deletionDate) + { + IsDeleted = isDeleted; + DeletionDate = deletionDate; + } + + public bool IsDeleted { get; set; } + public DateTime? DeletionDate { get; set; } +} diff --git a/Modules/Devices/src/Devices.Application/Identities/Queries/IsIdentityOfUserDeleted/Validator.cs b/Modules/Devices/src/Devices.Application/Identities/Queries/IsIdentityOfUserDeleted/Validator.cs new file mode 100644 index 0000000000..7df6aabed5 --- /dev/null +++ b/Modules/Devices/src/Devices.Application/Identities/Queries/IsIdentityOfUserDeleted/Validator.cs @@ -0,0 +1,13 @@ +using Backbone.BuildingBlocks.Application.Extensions; +using Backbone.DevelopmentKit.Identity.ValueObjects; +using FluentValidation; + +namespace Backbone.Modules.Devices.Application.Identities.Queries.IsIdentityOfUserDeleted; + +public class Validator : AbstractValidator +{ + public Validator() + { + RuleFor(x => x.Username).ValidId(); + } +} diff --git a/Modules/Devices/src/Devices.Application/Infrastructure/Persistence/Repository/IIdentitiesRepository.cs b/Modules/Devices/src/Devices.Application/Infrastructure/Persistence/Repository/IIdentitiesRepository.cs index 3f829a38bc..b4fe595614 100644 --- a/Modules/Devices/src/Devices.Application/Infrastructure/Persistence/Repository/IIdentitiesRepository.cs +++ b/Modules/Devices/src/Devices.Application/Infrastructure/Persistence/Repository/IIdentitiesRepository.cs @@ -16,6 +16,7 @@ public interface IIdentitiesRepository Task> FindAllWithDeletionProcessInStatus(DeletionProcessStatus status, CancellationToken cancellationToken, bool track = false); Task CountByClientId(string clientId, CancellationToken cancellationToken); Task> Find(Expression> filter, CancellationToken cancellationToken, bool track = false); + Task FindFirst(Expression> filter, CancellationToken cancellationToken, bool track = false); Task Delete(Expression> filter, CancellationToken cancellationToken); Task Add(Identity identity, string password); @@ -27,15 +28,22 @@ public interface IIdentitiesRepository Task> FindAllDevicesOfIdentity(IdentityAddress identity, IEnumerable ids, PaginationFilter paginationFilter, CancellationToken cancellationToken); Task GetDeviceById(DeviceId deviceId, CancellationToken cancellationToken, bool track = false); + Task> GetDevicesByIds(IEnumerable deviceIds, CancellationToken cancellationToken, bool track = false); Task Update(Device device, CancellationToken cancellationToken); Task FindDevices(Expression> filter, Expression> selector, CancellationToken cancellationToken, bool track = false); + Task HasBackupDevice(IdentityAddress identity, CancellationToken cancellationToken); + Task DeleteDevice(Device device, CancellationToken cancellationToken); #endregion #region Deletion Process Audit Logs - Task> GetIdentityDeletionProcessAuditLogsByAddress(byte[] identityAddressHash, CancellationToken cancellationToken); + Task> GetIdentityDeletionProcessAuditLogs(Expression> filter, + CancellationToken cancellationToken, bool track = false); + Task AddDeletionProcessAuditLogEntry(IdentityDeletionProcessAuditLogEntry auditLogEntry); + Task Update(IEnumerable auditLogEntries, CancellationToken cancellationToken); + #endregion } diff --git a/Modules/Devices/src/Devices.Application/Infrastructure/PushNotifications/Announcements/NewAnnouncementPushNotification.cs b/Modules/Devices/src/Devices.Application/Infrastructure/PushNotifications/Announcements/NewAnnouncementPushNotification.cs new file mode 100644 index 0000000000..efecf7f64c --- /dev/null +++ b/Modules/Devices/src/Devices.Application/Infrastructure/PushNotifications/Announcements/NewAnnouncementPushNotification.cs @@ -0,0 +1,8 @@ +using Backbone.BuildingBlocks.Application.PushNotifications; + +namespace Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.Announcements; + +public class NewAnnouncementPushNotification : IPushNotification +{ + public required string AnnouncementId { get; set; } +} diff --git a/Modules/Devices/src/Devices.Application/Infrastructure/PushNotifications/Device/BackupDeviceUsedPushNotification.cs b/Modules/Devices/src/Devices.Application/Infrastructure/PushNotifications/Device/BackupDeviceUsedPushNotification.cs new file mode 100644 index 0000000000..02d365413f --- /dev/null +++ b/Modules/Devices/src/Devices.Application/Infrastructure/PushNotifications/Device/BackupDeviceUsedPushNotification.cs @@ -0,0 +1,5 @@ +using Backbone.BuildingBlocks.Application.PushNotifications; + +namespace Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.Device; + +public class BackupDeviceUsedPushNotification : IPushNotification; diff --git a/Modules/Devices/src/Devices.Application/Users/Commands/SeedTestUsers/Handler.cs b/Modules/Devices/src/Devices.Application/Users/Commands/SeedTestUsers/Handler.cs index 81446d8519..a3553a6118 100644 --- a/Modules/Devices/src/Devices.Application/Users/Commands/SeedTestUsers/Handler.cs +++ b/Modules/Devices/src/Devices.Application/Users/Commands/SeedTestUsers/Handler.cs @@ -1,5 +1,6 @@ using Backbone.DevelopmentKit.Identity.ValueObjects; using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository; +using Backbone.Modules.Devices.Domain.Aggregates.Tier; using Backbone.Modules.Devices.Domain.Entities.Identities; using MediatR; using Microsoft.Extensions.Options; @@ -11,6 +12,8 @@ public class Handler : IRequestHandler private readonly ApplicationOptions _applicationOptions; private readonly IIdentitiesRepository _identitiesRepository; private readonly ITiersRepository _tiersRepository; + private Tier? _basicTier; + private CancellationToken _cancellationToken; public Handler(IIdentitiesRepository identitiesRepository, ITiersRepository tiersRepository, IOptions applicationOptions) { @@ -21,12 +24,23 @@ public Handler(IIdentitiesRepository identitiesRepository, ITiersRepository tier public async Task Handle(SeedTestUsersCommand request, CancellationToken cancellationToken) { - var basicTier = await _tiersRepository.FindBasicTier(cancellationToken); + _cancellationToken = cancellationToken; + _basicTier = (await _tiersRepository.FindBasicTier(cancellationToken))!; - var identityA = Identity.CreateTestIdentity(IdentityAddress.Create([1, 1, 1, 1, 1], _applicationOptions.DidDomainName), [1, 1, 1, 1, 1], basicTier!.Id, "USRa"); - var identityB = Identity.CreateTestIdentity(IdentityAddress.Create([2, 2, 2, 2, 2], _applicationOptions.DidDomainName), [2, 2, 2, 2, 2], basicTier.Id, "USRb"); + await CreateIdentityIfNecessary([1, 1, 1, 1, 1], "USRa", "Aaaaaaaa1!"); + await CreateIdentityIfNecessary([2, 2, 2, 2, 2], "USRb", "Bbbbbbbb1!"); + } + + private async Task CreateIdentityIfNecessary(byte[] publicKey, string username, string password) + { + var address = IdentityAddress.Create(publicKey, _applicationOptions.DidDomainName); + + var identityExists = await _identitiesRepository.Exists(address, _cancellationToken); - await _identitiesRepository.Add(identityA, "Aaaaaaaa1!"); - await _identitiesRepository.Add(identityB, "Bbbbbbbb1!"); + if (!identityExists) + { + var identity = Identity.CreateTestIdentity(address, publicKey, _basicTier!.Id, username); + await _identitiesRepository.Add(identity, password); + } } } diff --git a/Modules/Devices/src/Devices.ConsumerApi/Controllers/DevicesController.cs b/Modules/Devices/src/Devices.ConsumerApi/Controllers/DevicesController.cs index 93b671e421..2e7f7601ea 100644 --- a/Modules/Devices/src/Devices.ConsumerApi/Controllers/DevicesController.cs +++ b/Modules/Devices/src/Devices.ConsumerApi/Controllers/DevicesController.cs @@ -12,6 +12,7 @@ using Backbone.Modules.Devices.Application.Devices.DTOs; using Backbone.Modules.Devices.Application.Devices.Queries.GetActiveDevice; using Backbone.Modules.Devices.Application.Devices.Queries.ListDevices; +using Backbone.Modules.Devices.Domain.Entities.Identities; using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -40,9 +41,10 @@ public async Task RegisterDevice(RegisterDeviceRequest request, C { var command = new RegisterDeviceCommand { - CommunicationLanguage = request.CommunicationLanguage ?? "en", + CommunicationLanguage = request.CommunicationLanguage ?? CommunicationLanguage.DEFAULT_LANGUAGE.Value, SignedChallenge = request.SignedChallenge, - DevicePassword = request.DevicePassword + DevicePassword = request.DevicePassword, + IsBackupDevice = request.IsBackupDevice ?? false }; var response = await _mediator.Send(command, cancellationToken); @@ -109,4 +111,5 @@ public class RegisterDeviceRequest public required string DevicePassword { get; set; } public string? CommunicationLanguage { get; set; } public required SignedChallengeDTO SignedChallenge { get; set; } + public bool? IsBackupDevice { get; set; } } diff --git a/Modules/Devices/src/Devices.ConsumerApi/Controllers/IdentitiesController.cs b/Modules/Devices/src/Devices.ConsumerApi/Controllers/IdentitiesController.cs index 1638df6a3a..36ebd9856e 100644 --- a/Modules/Devices/src/Devices.ConsumerApi/Controllers/IdentitiesController.cs +++ b/Modules/Devices/src/Devices.ConsumerApi/Controllers/IdentitiesController.cs @@ -12,6 +12,8 @@ using Backbone.Modules.Devices.Application.Identities.Queries.GetDeletionProcessAsOwner; using Backbone.Modules.Devices.Application.Identities.Queries.GetDeletionProcessesAsOwner; using Backbone.Modules.Devices.Application.Identities.Queries.GetOwnIdentity; +using Backbone.Modules.Devices.Application.Identities.Queries.IsIdentityOfUserDeleted; +using Backbone.Modules.Devices.Domain.Entities.Identities; using Backbone.Modules.Devices.Infrastructure.OpenIddict; using MediatR; using Microsoft.AspNetCore.Authorization; @@ -52,7 +54,7 @@ public async Task CreateIdentity(CreateIdentityRequest request, C DevicePassword = request.DevicePassword, IdentityPublicKey = request.IdentityPublicKey, IdentityVersion = request.IdentityVersion, - CommunicationLanguage = request.DeviceCommunicationLanguage ?? "en", + CommunicationLanguage = request.DeviceCommunicationLanguage ?? CommunicationLanguage.DEFAULT_LANGUAGE.Value, SignedChallenge = new SignedChallengeDTO { Challenge = request.SignedChallenge.Challenge, @@ -68,9 +70,10 @@ public async Task CreateIdentity(CreateIdentityRequest request, C [HttpPost("Self/DeletionProcesses")] [ProducesResponseType(typeof(HttpResponseEnvelopeResult), StatusCodes.Status201Created)] [ProducesError(StatusCodes.Status400BadRequest)] - public async Task StartDeletionProcess(CancellationToken cancellationToken) + public async Task StartDeletionProcess(StartDeletionProcessAsOwnerCommand? request, CancellationToken cancellationToken) { - var response = await _mediator.Send(new StartDeletionProcessAsOwnerCommand(), cancellationToken); + request ??= new StartDeletionProcessAsOwnerCommand(); + var response = await _mediator.Send(request, cancellationToken); return Created("", response); } @@ -128,6 +131,16 @@ public async Task GetOwnIdentity(CancellationToken cancellationTo var response = await _mediator.Send(new GetOwnIdentityQuery(), cancellationToken); return Ok(response); } + + [HttpGet("IsDeleted")] + [AllowAnonymous] + [ProducesResponseType(typeof(IsIdentityOfUserDeletedResponse), StatusCodes.Status200OK)] + [ProducesError(StatusCodes.Status200OK)] + public async Task IsIdentityOfUserDeleted([FromQuery(Name = "username")] string username, CancellationToken cancellationToken) + { + var response = await _mediator.Send(new IsIdentityOfUserDeletedQuery { Username = username }, cancellationToken); + return Ok(response); + } } public class CreateIdentityRequest diff --git a/Modules/Devices/src/Devices.Domain/DomainEvents/Incoming/AnnouncementCreated/AnnouncementCreatedDomainEvent.cs b/Modules/Devices/src/Devices.Domain/DomainEvents/Incoming/AnnouncementCreated/AnnouncementCreatedDomainEvent.cs new file mode 100644 index 0000000000..921986f5aa --- /dev/null +++ b/Modules/Devices/src/Devices.Domain/DomainEvents/Incoming/AnnouncementCreated/AnnouncementCreatedDomainEvent.cs @@ -0,0 +1,17 @@ +using Backbone.BuildingBlocks.Domain.Events; + +namespace Backbone.Modules.Devices.Domain.DomainEvents.Incoming.AnnouncementCreated; + +public class AnnouncementCreatedDomainEvent : DomainEvent +{ + public required string Id { get; set; } + public required string Severity { get; set; } + public required List Texts { get; set; } +} + +public class AnnouncementCreatedDomainEventText +{ + public required string Language { get; set; } + public required string Title { get; set; } + public required string Body { get; set; } +} diff --git a/Modules/Devices/src/Devices.Domain/DomainEvents/Outgoing/BackupDeviceUsedDomainEvent.cs b/Modules/Devices/src/Devices.Domain/DomainEvents/Outgoing/BackupDeviceUsedDomainEvent.cs new file mode 100644 index 0000000000..08991944bb --- /dev/null +++ b/Modules/Devices/src/Devices.Domain/DomainEvents/Outgoing/BackupDeviceUsedDomainEvent.cs @@ -0,0 +1,14 @@ +using Backbone.BuildingBlocks.Domain.Events; +using Backbone.DevelopmentKit.Identity.ValueObjects; + +namespace Backbone.Modules.Devices.Domain.DomainEvents.Outgoing; + +public class BackupDeviceUsedDomainEvent : DomainEvent +{ + public BackupDeviceUsedDomainEvent(IdentityAddress identityAddress) + { + IdentityAddress = identityAddress; + } + + public IdentityAddress IdentityAddress { get; set; } +} diff --git a/Modules/Devices/src/Devices.Domain/Entities/Identities/ApplicationUser.cs b/Modules/Devices/src/Devices.Domain/Entities/Identities/ApplicationUser.cs index 3f20c8a54c..9be3cf3c75 100644 --- a/Modules/Devices/src/Devices.Domain/Entities/Identities/ApplicationUser.cs +++ b/Modules/Devices/src/Devices.Domain/Entities/Identities/ApplicationUser.cs @@ -46,7 +46,7 @@ public Device Device public bool HasLoggedIn => LastLoginAt.HasValue; - public void LoginOccurred() + internal void LoginOccurred() { LastLoginAt = SystemTime.UtcNow; } diff --git a/Modules/Devices/src/Devices.Domain/Entities/Identities/CommunicationLanguage.cs b/Modules/Devices/src/Devices.Domain/Entities/Identities/CommunicationLanguage.cs index 51dbf0ad84..200528df48 100644 --- a/Modules/Devices/src/Devices.Domain/Entities/Identities/CommunicationLanguage.cs +++ b/Modules/Devices/src/Devices.Domain/Entities/Identities/CommunicationLanguage.cs @@ -9,7 +9,6 @@ public record CommunicationLanguage public static readonly CommunicationLanguage DEFAULT_LANGUAGE = new("en"); private static readonly CultureInfo[] CULTURES = CultureInfo.GetCultures(CultureTypes.AllCultures & ~CultureTypes.NeutralCultures); - public string Value { get; } public const int LENGTH = 2; private CommunicationLanguage(string value) @@ -17,6 +16,8 @@ private CommunicationLanguage(string value) Value = value; } + public string Value { get; } + public static Result Create(string value) { var validationResult = Validate(value); diff --git a/Modules/Devices/src/Devices.Domain/Entities/Identities/Device.cs b/Modules/Devices/src/Devices.Domain/Entities/Identities/Device.cs index f6fcbcb00d..5d6e5650e1 100644 --- a/Modules/Devices/src/Devices.Domain/Entities/Identities/Device.cs +++ b/Modules/Devices/src/Devices.Domain/Entities/Identities/Device.cs @@ -3,6 +3,7 @@ using Backbone.BuildingBlocks.Domain.Errors; using Backbone.BuildingBlocks.Domain.Exceptions; using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Devices.Domain.DomainEvents.Outgoing; using Backbone.Tooling; namespace Backbone.Modules.Devices.Domain.Entities.Identities; @@ -21,12 +22,16 @@ private Device() CommunicationLanguage = null!; } + /** + * This constructor is only used for creating test devices. + */ private Device(Identity identity, CommunicationLanguage communicationLanguage, string username) { Id = DeviceId.New(); CreatedAt = SystemTime.UtcNow; CreatedByDevice = Id; CommunicationLanguage = communicationLanguage; + IsBackupDevice = false; User = new ApplicationUser(this, username); @@ -34,12 +39,13 @@ private Device(Identity identity, CommunicationLanguage communicationLanguage, s IdentityAddress = null!; } - public Device(Identity identity, CommunicationLanguage communicationLanguage, DeviceId? createdByDevice = null) + public Device(Identity identity, CommunicationLanguage communicationLanguage, DeviceId? createdByDevice = null, bool isBackupDevice = false) { Id = DeviceId.New(); CreatedAt = SystemTime.UtcNow; CreatedByDevice = createdByDevice ?? Id; CommunicationLanguage = communicationLanguage; + IsBackupDevice = isBackupDevice; User = new ApplicationUser(this); @@ -69,13 +75,17 @@ public Device(Identity identity, CommunicationLanguage communicationLanguage, De public DeviceId CreatedByDevice { get; set; } - public DateTime? DeletedAt { get; set; } - public DeviceId? DeletedByDevice { get; set; } + public bool IsBackupDevice { get; private set; } public bool IsOnboarded => User.HasLoggedIn; - public static Expression> IsNotDeleted => - device => device.DeletedAt == null && device.DeletedByDevice == null; + public void EnsureCanBeDeleted(IdentityAddress addressOfActiveIdentity) + { + var error = CanBeDeletedBy(addressOfActiveIdentity); + + if (error != null) + throw new DomainException(error); + } private DomainError? CanBeDeletedBy(IdentityAddress addressOfActiveIdentity) { @@ -100,19 +110,26 @@ public bool Update(CommunicationLanguage communicationLanguage) return hasChanges; } - public void MarkAsDeleted(DeviceId deletedByDevice, IdentityAddress addressOfActiveIdentity) + public void LoginOccurred() { - var error = CanBeDeletedBy(addressOfActiveIdentity); - - if (error != null) - throw new DomainException(error); + if (IsBackupDevice) + { + IsBackupDevice = false; + RaiseDomainEvent(new BackupDeviceUsedDomainEvent(IdentityAddress)); + } - DeletedAt = SystemTime.UtcNow; - DeletedByDevice = deletedByDevice; + User.LoginOccurred(); } public static Device CreateTestDevice(Identity identity, CommunicationLanguage communicationLanguage, string username) { return new Device(identity, communicationLanguage, username); } + + #region Expressions + + public static Expression> IsBackup => + device => device.IsBackupDevice; + + #endregion } diff --git a/Modules/Devices/src/Devices.Domain/Entities/Identities/Hasher.cs b/Modules/Devices/src/Devices.Domain/Entities/Identities/Hasher.cs index 0e67e7f10c..8bca59de43 100644 --- a/Modules/Devices/src/Devices.Domain/Entities/Identities/Hasher.cs +++ b/Modules/Devices/src/Devices.Domain/Entities/Identities/Hasher.cs @@ -40,9 +40,9 @@ public static void Reset() internal class HasherImpl : IHasher { private static readonly byte[] SALT = SHA256.HashData("enmeshed_identity_deletion_log"u8.ToArray()); + public byte[] HashUtf8(string input) { - // Salt: SHA128 von "enmeshed_identity_deletion_log" var hash = KeyDerivation.Pbkdf2(input, SALT, KeyDerivationPrf.HMACSHA256, 100_000, 32); return hash; } diff --git a/Modules/Devices/src/Devices.Domain/Entities/Identities/Identity.cs b/Modules/Devices/src/Devices.Domain/Entities/Identities/Identity.cs index f60a8e6ce6..ab8a1387be 100644 --- a/Modules/Devices/src/Devices.Domain/Entities/Identities/Identity.cs +++ b/Modules/Devices/src/Devices.Domain/Entities/Identities/Identity.cs @@ -95,14 +95,16 @@ private set public IdentityStatus Status { get; private set; } + public bool IsGracePeriodOver => DeletionGracePeriodEndsAt != null && DeletionGracePeriodEndsAt < SystemTime.UtcNow; + public bool IsNew() { return Devices.Count < 1; } - public Device AddDevice(CommunicationLanguage communicationLanguage, DeviceId createdByDevice) + public Device AddDevice(CommunicationLanguage communicationLanguage, DeviceId createdByDevice, bool isBackupDevice) { - var newDevice = new Device(this, communicationLanguage, createdByDevice); + var newDevice = new Device(this, communicationLanguage, createdByDevice, isBackupDevice); Devices.Add(newDevice); return newDevice; } @@ -128,14 +130,14 @@ public IdentityDeletionProcess StartDeletionProcessAsSupport() return deletionProcess; } - public IdentityDeletionProcess StartDeletionProcessAsOwner(DeviceId asDevice) + public IdentityDeletionProcess StartDeletionProcessAsOwner(DeviceId asDevice, double? lengthOfGracePeriodInDays = null) { EnsureNoActiveProcessExists(); EnsureIdentityOwnsDevice(asDevice); TierIdBeforeDeletion = TierId; - var deletionProcess = IdentityDeletionProcess.StartAsOwner(Address, asDevice); + var deletionProcess = IdentityDeletionProcess.StartAsOwner(Address, asDevice, lengthOfGracePeriodInDays); _deletionProcesses.Add(deletionProcess); DeletionGracePeriodEndsAt = deletionProcess.GracePeriodEndsAt; @@ -278,16 +280,6 @@ public void DeletionGracePeriodReminder3Sent() return DeletionProcesses.FirstOrDefault(x => x.Status == deletionProcessStatus); } - public static Expression> HasAddress(IdentityAddress address) - { - return i => i.Address == address.ToString(); - } - - public static Expression> IsReadyForDeletion() - { - return i => i.Status == IdentityStatus.ToBeDeleted && i.DeletionGracePeriodEndsAt != null && i.DeletionGracePeriodEndsAt < SystemTime.UtcNow; - } - public IdentityDeletionProcess CancelDeletionProcessAsOwner(IdentityDeletionProcessId deletionProcessId, DeviceId cancelledByDeviceId) { EnsureIdentityOwnsDevice(cancelledByDeviceId); @@ -329,6 +321,25 @@ public static Identity CreateTestIdentity(IdentityAddress address, byte[] public { return new Identity("test", address, publicKey, tierId, 1, CommunicationLanguage.DEFAULT_LANGUAGE, username); } + + #region Expressions + + public static Expression> HasAddress(IdentityAddress address) + { + return i => i.Address == address.ToString(); + } + + public static Expression> IsReadyForDeletion() + { + return i => i.Status == IdentityStatus.ToBeDeleted && i.DeletionGracePeriodEndsAt != null && i.DeletionGracePeriodEndsAt < SystemTime.UtcNow; + } + + public static Expression> HasUser(string username) + { + return i => i.Devices.Any(d => d.User.UserName == username); + } + + #endregion } public enum DeletionProcessStatus diff --git a/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcess.cs b/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcess.cs index 9ae73b819f..94a5fa7dfe 100644 --- a/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcess.cs +++ b/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcess.cs @@ -31,13 +31,13 @@ private IdentityDeletionProcess(IdentityAddress createdBy, DeletionProcessStatus RaiseDomainEvent(new IdentityDeletionProcessStartedDomainEvent(createdBy, Id, null)); } - private IdentityDeletionProcess(IdentityAddress createdBy, DeviceId createdByDevice) + private IdentityDeletionProcess(IdentityAddress createdBy, DeviceId createdByDevice, double? lengthOfGracePeriodInDays) { Id = IdentityDeletionProcessId.Generate(); IdentityAddress = null!; CreatedAt = SystemTime.UtcNow; - ApproveInternally(createdBy, createdByDevice); + ApproveInternally(createdBy, createdByDevice, lengthOfGracePeriodInDays); _auditLog = [IdentityDeletionProcessAuditLogEntry.ProcessStartedByOwner(Id, createdBy, createdByDevice)]; } @@ -81,9 +81,9 @@ public static IdentityDeletionProcess StartAsSupport(IdentityAddress createdBy) return new IdentityDeletionProcess(createdBy, DeletionProcessStatus.WaitingForApproval); } - public static IdentityDeletionProcess StartAsOwner(IdentityAddress createdBy, DeviceId createdByDeviceId) + public static IdentityDeletionProcess StartAsOwner(IdentityAddress createdBy, DeviceId createdByDeviceId, double? lengthOfGracePeriodInDays) { - return new IdentityDeletionProcess(createdBy, createdByDeviceId); + return new IdentityDeletionProcess(createdBy, createdByDeviceId, lengthOfGracePeriodInDays); } public bool IsActive() @@ -150,11 +150,12 @@ public void Approve(IdentityAddress address, DeviceId approvedByDevice) _auditLog.Add(IdentityDeletionProcessAuditLogEntry.ProcessApproved(Id, address, approvedByDevice)); } - private void ApproveInternally(IdentityAddress address, DeviceId createdByDevice) + private void ApproveInternally(IdentityAddress address, DeviceId createdByDevice, double? lengthOfGracePeriodInDays = null) { + lengthOfGracePeriodInDays ??= IdentityDeletionConfiguration.Instance.LengthOfGracePeriodInDays; ApprovedAt = SystemTime.UtcNow; ApprovedByDevice = createdByDevice; - GracePeriodEndsAt = SystemTime.UtcNow.AddDays(IdentityDeletionConfiguration.Instance.LengthOfGracePeriodInDays); + GracePeriodEndsAt = SystemTime.UtcNow.AddDays(lengthOfGracePeriodInDays.Value); ChangeStatus(DeletionProcessStatus.Approved, address, address); } diff --git a/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcessAuditLogEntry.cs b/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcessAuditLogEntry.cs index 23b3095389..d3476a0d69 100644 --- a/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcessAuditLogEntry.cs +++ b/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcessAuditLogEntry.cs @@ -1,4 +1,5 @@ -using Backbone.BuildingBlocks.Domain; +using System.Linq.Expressions; +using Backbone.BuildingBlocks.Domain; using Backbone.DevelopmentKit.Identity.ValueObjects; using Backbone.Tooling; @@ -16,7 +17,7 @@ private IdentityDeletionProcessAuditLogEntry() } private IdentityDeletionProcessAuditLogEntry(IdentityDeletionProcessId? processId, MessageKey messageKey, byte[] identityAddressHash, byte[]? deviceIdHash, DeletionProcessStatus? oldStatus, - DeletionProcessStatus newStatus, Dictionary? additionalData = null) + DeletionProcessStatus? newStatus, Dictionary? additionalData = null) { Id = IdentityDeletionProcessAuditLogEntryId.Generate(); ProcessId = processId; @@ -36,8 +37,9 @@ private IdentityDeletionProcessAuditLogEntry(IdentityDeletionProcessId? processI public byte[] IdentityAddressHash { get; } public byte[]? DeviceIdHash { get; } public DeletionProcessStatus? OldStatus { get; } - public DeletionProcessStatus NewStatus { get; } + public DeletionProcessStatus? NewStatus { get; } public Dictionary? AdditionalData { get; } + public List? UsernameHashesBase64 { get; private set; } public static IdentityDeletionProcessAuditLogEntry ProcessStartedByOwner(IdentityDeletionProcessId processId, IdentityAddress identityAddress, DeviceId deviceId) { @@ -209,6 +211,32 @@ public static IdentityDeletionProcessAuditLogEntry DataDeleted(IdentityAddress i } ); } + + public static IdentityDeletionProcessAuditLogEntry DeletionCompleted(IdentityAddress identityAddress) + { + return new IdentityDeletionProcessAuditLogEntry( + processId: null, + messageKey: MessageKey.DeletionCompleted, + identityAddressHash: Hasher.HashUtf8(identityAddress.Value), + deviceIdHash: null, + oldStatus: DeletionProcessStatus.Deleting, + newStatus: null + ); + } + + public void AssociateUsernames(IEnumerable usernames) + { + UsernameHashesBase64 = usernames + .Select(u => Hasher.HashUtf8(u.Value.Trim())) + .Select(Convert.ToBase64String) + .ToList(); + } + + public static Expression> IsAssociatedToUser(Username username) + { + var usernameHashBase64 = Convert.ToBase64String(Hasher.HashUtf8(username.Value.Trim())); + return logEntry => logEntry.UsernameHashesBase64 != null && logEntry.UsernameHashesBase64.Contains(usernameHashBase64); + } } public enum MessageKey @@ -226,5 +254,6 @@ public enum MessageKey GracePeriodReminder1Sent = 11, GracePeriodReminder2Sent = 12, GracePeriodReminder3Sent = 13, - DataDeleted = 14 + DataDeleted = 14, + DeletionCompleted = 15 } diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20241120145133_AddIsBackupDevicePropertyToDevice.Designer.cs b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20241120145133_AddIsBackupDevicePropertyToDevice.Designer.cs new file mode 100644 index 0000000000..7a922aabf5 --- /dev/null +++ b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20241120145133_AddIsBackupDevicePropertyToDevice.Designer.cs @@ -0,0 +1,928 @@ +// +using System; +using Backbone.Modules.Devices.Infrastructure.Persistence.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backbone.Modules.Devices.Infrastructure.Database.Postgres.Migrations +{ + [DbContext(typeof(DevicesDbContext))] + [Migration("20241120145133_AddIsBackupDevicePropertyToDevice")] + partial class AddIsBackupDevicePropertyToDevice + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Devices") + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Aggregates.PushNotifications.PnsRegistration", b => + { + b.Property("DeviceId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("AppId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DevicePushIdentifier") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("Environment") + .HasColumnType("integer"); + + b.Property("Handle") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(true) + .HasColumnType("character varying(200)") + .IsFixedLength(false); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("DeviceId"); + + b.ToTable("PnsRegistrations", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Aggregates.Tier.Tier", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("CanBeManuallyAssigned") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("CanBeUsedAsDefaultForClient") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Name") + .IsRequired() + .HasMaxLength(30) + .IsUnicode(true) + .HasColumnType("character varying(30)") + .IsFixedLength(false); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Tiers", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Challenge", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Challenges", "Challenges", t => + { + t.ExcludeFromMigrations(); + }); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("UserName") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("CommunicationLanguage") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2) + .IsUnicode(false) + .HasColumnType("character(2)") + .HasDefaultValue("en") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByDevice") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("IsBackupDevice") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("IdentityAddress"); + + b.ToTable("Devices", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", b => + { + b.Property("Address") + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("ClientId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletionGracePeriodEndsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IdentityVersion") + .HasColumnType("smallint"); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TierId") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("TierIdBeforeDeletion") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.HasKey("Address"); + + b.HasIndex("ClientId"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("ClientId"), "hash"); + + b.HasIndex("TierId"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("TierId"), "hash"); + + b.ToTable("Identities", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("ApprovalReminder1SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovalReminder2SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovalReminder3SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CancelledByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletionStartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GracePeriodEndsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GracePeriodReminder1SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GracePeriodReminder2SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GracePeriodReminder3SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("RejectedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RejectedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IdentityAddress"); + + b.HasIndex(new[] { "IdentityAddress" }, "IX_only_one_active_deletion_process") + .IsUnique() + .HasFilter("\"Status\" = 1"); + + b.ToTable("IdentityDeletionProcesses", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcessAuditLogEntry", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("AdditionalData") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceIdHash") + .HasColumnType("bytea"); + + b.Property("IdentityAddressHash") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("IdentityDeletionProcessId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("MessageKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("NewStatus") + .HasColumnType("integer"); + + b.Property("OldStatus") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IdentityDeletionProcessId"); + + b.ToTable("IdentityDeletionProcessAuditLog", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ClientSecret") + .HasColumnType("text"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultTier") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("MaxIdentities") + .HasColumnType("integer"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedirectUris") + .HasColumnType("text"); + + b.Property("Requirements") + .HasColumnType("text"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.HasIndex("DefaultTier"); + + b.ToTable("OpenIddictApplications", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Scopes") + .HasColumnType("text"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Descriptions") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Resources") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("AuthorizationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedemptionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Device", "Device") + .WithOne("User") + .HasForeignKey("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", "DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", "Identity") + .WithMany("Devices") + .HasForeignKey("IdentityAddress") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Identity"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", null) + .WithMany("DeletionProcesses") + .HasForeignKey("IdentityAddress") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcessAuditLogEntry", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", null) + .WithMany("AuditLog") + .HasForeignKey("IdentityDeletionProcessId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Aggregates.Tier.Tier", null) + .WithMany() + .HasForeignKey("DefaultTier") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.Navigation("User") + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", b => + { + b.Navigation("DeletionProcesses"); + + b.Navigation("Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.Navigation("AuditLog"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20241120145133_AddIsBackupDevicePropertyToDevice.cs b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20241120145133_AddIsBackupDevicePropertyToDevice.cs new file mode 100644 index 0000000000..2a77622ec8 --- /dev/null +++ b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20241120145133_AddIsBackupDevicePropertyToDevice.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backbone.Modules.Devices.Infrastructure.Database.Postgres.Migrations +{ + /// + public partial class AddIsBackupDevicePropertyToDevice : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsBackupDevice", + schema: "Devices", + table: "Devices", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsBackupDevice", + schema: "Devices", + table: "Devices"); + } + } +} diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20241128082925_AddUsernameHashesBase64Column.Designer.cs b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20241128082925_AddUsernameHashesBase64Column.Designer.cs new file mode 100644 index 0000000000..8cc5ace4da --- /dev/null +++ b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20241128082925_AddUsernameHashesBase64Column.Designer.cs @@ -0,0 +1,929 @@ +// +using System; +using System.Collections.Generic; +using Backbone.Modules.Devices.Infrastructure.Persistence.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backbone.Modules.Devices.Infrastructure.Database.Postgres.Migrations +{ + [DbContext(typeof(DevicesDbContext))] + [Migration("20241128082925_AddUsernameHashesBase64Column")] + partial class AddUsernameHashesBase64Column + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Devices") + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Aggregates.PushNotifications.PnsRegistration", b => + { + b.Property("DeviceId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("AppId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DevicePushIdentifier") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("Environment") + .HasColumnType("integer"); + + b.Property("Handle") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(true) + .HasColumnType("character varying(200)") + .IsFixedLength(false); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("DeviceId"); + + b.ToTable("PnsRegistrations", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Aggregates.Tier.Tier", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("CanBeManuallyAssigned") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("CanBeUsedAsDefaultForClient") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Name") + .IsRequired() + .HasMaxLength(30) + .IsUnicode(true) + .HasColumnType("character varying(30)") + .IsFixedLength(false); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Tiers", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Challenge", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Challenges", "Challenges", t => + { + t.ExcludeFromMigrations(); + }); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("UserName") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("CommunicationLanguage") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2) + .IsUnicode(false) + .HasColumnType("character(2)") + .HasDefaultValue("en") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByDevice") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.HasKey("Id"); + + b.HasIndex("IdentityAddress"); + + b.ToTable("Devices", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", b => + { + b.Property("Address") + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("ClientId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletionGracePeriodEndsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IdentityVersion") + .HasColumnType("smallint"); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TierId") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("TierIdBeforeDeletion") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.HasKey("Address"); + + b.HasIndex("ClientId"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("ClientId"), "hash"); + + b.HasIndex("TierId"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("TierId"), "hash"); + + b.ToTable("Identities", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("ApprovalReminder1SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovalReminder2SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovalReminder3SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CancelledByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletionStartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GracePeriodEndsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GracePeriodReminder1SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GracePeriodReminder2SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GracePeriodReminder3SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("RejectedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RejectedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IdentityAddress"); + + b.HasIndex(new[] { "IdentityAddress" }, "IX_only_one_active_deletion_process") + .IsUnique() + .HasFilter("\"Status\" = 1"); + + b.ToTable("IdentityDeletionProcesses", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcessAuditLogEntry", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("AdditionalData") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceIdHash") + .HasColumnType("bytea"); + + b.Property("IdentityAddressHash") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("IdentityDeletionProcessId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("MessageKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("NewStatus") + .HasColumnType("integer"); + + b.Property("OldStatus") + .HasColumnType("integer"); + + b.PrimitiveCollection>("UsernameHashesBase64") + .HasColumnType("text[]"); + + b.HasKey("Id"); + + b.HasIndex("IdentityDeletionProcessId"); + + b.ToTable("IdentityDeletionProcessAuditLog", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ClientSecret") + .HasColumnType("text"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultTier") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("MaxIdentities") + .HasColumnType("integer"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedirectUris") + .HasColumnType("text"); + + b.Property("Requirements") + .HasColumnType("text"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.HasIndex("DefaultTier"); + + b.ToTable("OpenIddictApplications", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Scopes") + .HasColumnType("text"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Descriptions") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Resources") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("AuthorizationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedemptionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Device", "Device") + .WithOne("User") + .HasForeignKey("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", "DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", "Identity") + .WithMany("Devices") + .HasForeignKey("IdentityAddress") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Identity"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", null) + .WithMany("DeletionProcesses") + .HasForeignKey("IdentityAddress") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcessAuditLogEntry", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", null) + .WithMany("AuditLog") + .HasForeignKey("IdentityDeletionProcessId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Aggregates.Tier.Tier", null) + .WithMany() + .HasForeignKey("DefaultTier") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.Navigation("User") + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", b => + { + b.Navigation("DeletionProcesses"); + + b.Navigation("Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.Navigation("AuditLog"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20241128082925_AddUsernameHashesBase64Column.cs b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20241128082925_AddUsernameHashesBase64Column.cs new file mode 100644 index 0000000000..bdb6aad9ef --- /dev/null +++ b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20241128082925_AddUsernameHashesBase64Column.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backbone.Modules.Devices.Infrastructure.Database.Postgres.Migrations +{ + /// + public partial class AddUsernameHashesBase64Column : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "NewStatus", + schema: "Devices", + table: "IdentityDeletionProcessAuditLog", + type: "integer", + nullable: true, + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AddColumn>( + name: "UsernameHashesBase64", + schema: "Devices", + table: "IdentityDeletionProcessAuditLog", + type: "text[]", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "UsernameHashesBase64", + schema: "Devices", + table: "IdentityDeletionProcessAuditLog"); + + migrationBuilder.AlterColumn( + name: "NewStatus", + schema: "Devices", + table: "IdentityDeletionProcessAuditLog", + type: "integer", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + } + } +} diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20241211083753_RemoveSoftDeleteColumnsFromDevicesTable.Designer.cs b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20241211083753_RemoveSoftDeleteColumnsFromDevicesTable.Designer.cs new file mode 100644 index 0000000000..b55568cf22 --- /dev/null +++ b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20241211083753_RemoveSoftDeleteColumnsFromDevicesTable.Designer.cs @@ -0,0 +1,923 @@ +// +using System; +using System.Collections.Generic; +using Backbone.Modules.Devices.Infrastructure.Persistence.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Backbone.Modules.Devices.Infrastructure.Database.Postgres.Migrations +{ + [DbContext(typeof(DevicesDbContext))] + [Migration("20241211083753_RemoveSoftDeleteColumnsFromDevicesTable")] + partial class RemoveSoftDeleteColumnsFromDevicesTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Devices") + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Aggregates.PushNotifications.PnsRegistration", b => + { + b.Property("DeviceId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("AppId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DevicePushIdentifier") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("Environment") + .HasColumnType("integer"); + + b.Property("Handle") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(true) + .HasColumnType("character varying(200)") + .IsFixedLength(false); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("DeviceId"); + + b.ToTable("PnsRegistrations", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Aggregates.Tier.Tier", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("CanBeManuallyAssigned") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("CanBeUsedAsDefaultForClient") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("Name") + .IsRequired() + .HasMaxLength(30) + .IsUnicode(true) + .HasColumnType("character varying(30)") + .IsFixedLength(false); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Tiers", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Challenge", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Challenges", "Challenges", t => + { + t.ExcludeFromMigrations(); + }); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("UserName") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("CommunicationLanguage") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2) + .IsUnicode(false) + .HasColumnType("character(2)") + .HasDefaultValue("en") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByDevice") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("IsBackupDevice") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("IdentityAddress"); + + b.ToTable("Devices", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", b => + { + b.Property("Address") + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("ClientId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletionGracePeriodEndsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IdentityVersion") + .HasColumnType("smallint"); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TierId") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("TierIdBeforeDeletion") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.HasKey("Address"); + + b.HasIndex("ClientId"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("ClientId"), "hash"); + + b.HasIndex("TierId"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("TierId"), "hash"); + + b.ToTable("Identities", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("ApprovalReminder1SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovalReminder2SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovalReminder3SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ApprovedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CancelledByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletionStartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GracePeriodEndsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GracePeriodReminder1SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GracePeriodReminder2SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("GracePeriodReminder3SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("RejectedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RejectedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IdentityAddress"); + + b.HasIndex(new[] { "IdentityAddress" }, "IX_only_one_active_deletion_process") + .IsUnique() + .HasFilter("\"Status\" = 1"); + + b.ToTable("IdentityDeletionProcesses", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcessAuditLogEntry", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("AdditionalData") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceIdHash") + .HasColumnType("bytea"); + + b.Property("IdentityAddressHash") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("IdentityDeletionProcessId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("MessageKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("NewStatus") + .HasColumnType("integer"); + + b.Property("OldStatus") + .HasColumnType("integer"); + + b.PrimitiveCollection>("UsernameHashesBase64") + .HasColumnType("text[]"); + + b.HasKey("Id"); + + b.HasIndex("IdentityDeletionProcessId"); + + b.ToTable("IdentityDeletionProcessAuditLog", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ClientSecret") + .HasColumnType("text"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultTier") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("MaxIdentities") + .HasColumnType("integer"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedirectUris") + .HasColumnType("text"); + + b.Property("Requirements") + .HasColumnType("text"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.HasIndex("DefaultTier"); + + b.ToTable("OpenIddictApplications", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Scopes") + .HasColumnType("text"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Descriptions") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Resources") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("AuthorizationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedemptionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Device", "Device") + .WithOne("User") + .HasForeignKey("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", "DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", "Identity") + .WithMany("Devices") + .HasForeignKey("IdentityAddress") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Identity"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", null) + .WithMany("DeletionProcesses") + .HasForeignKey("IdentityAddress") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcessAuditLogEntry", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", null) + .WithMany("AuditLog") + .HasForeignKey("IdentityDeletionProcessId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Aggregates.Tier.Tier", null) + .WithMany() + .HasForeignKey("DefaultTier") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.Navigation("User") + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", b => + { + b.Navigation("DeletionProcesses"); + + b.Navigation("Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.Navigation("AuditLog"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20241211083753_RemoveSoftDeleteColumnsFromDevicesTable.cs b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20241211083753_RemoveSoftDeleteColumnsFromDevicesTable.cs new file mode 100644 index 0000000000..ac5191b766 --- /dev/null +++ b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20241211083753_RemoveSoftDeleteColumnsFromDevicesTable.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backbone.Modules.Devices.Infrastructure.Database.Postgres.Migrations +{ + /// + public partial class RemoveSoftDeleteColumnsFromDevicesTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("""DELETE FROM "Devices"."Devices" WHERE "DeletedAt" IS NOT NULL"""); + + migrationBuilder.DropColumn( + name: "DeletedAt", + schema: "Devices", + table: "Devices"); + + migrationBuilder.DropColumn( + name: "DeletedByDevice", + schema: "Devices", + table: "Devices"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DeletedAt", + schema: "Devices", + table: "Devices", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "DeletedByDevice", + schema: "Devices", + table: "Devices", + type: "character(20)", + unicode: false, + fixedLength: true, + maxLength: 20, + nullable: true); + } + } +} diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/DevicesDbContextModelSnapshot.cs b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/DevicesDbContextModelSnapshot.cs index afe4e7e765..654c0ddbb0 100644 --- a/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/DevicesDbContextModelSnapshot.cs +++ b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/DevicesDbContextModelSnapshot.cs @@ -1,5 +1,6 @@ // using System; +using System.Collections.Generic; using Backbone.Modules.Devices.Infrastructure.Persistence.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -18,7 +19,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Devices") - .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("ProductVersion", "9.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -205,15 +206,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character(20)") .IsFixedLength(); - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeletedByDevice") - .HasMaxLength(20) - .IsUnicode(false) - .HasColumnType("character(20)") - .IsFixedLength(); - b.Property("IdentityAddress") .IsRequired() .HasMaxLength(80) @@ -221,6 +213,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(80)") .IsFixedLength(false); + b.Property("IsBackupDevice") + .HasColumnType("boolean"); + b.HasKey("Id"); b.HasIndex("IdentityAddress"); @@ -396,12 +391,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("NewStatus") + b.Property("NewStatus") .HasColumnType("integer"); b.Property("OldStatus") .HasColumnType("integer"); + b.PrimitiveCollection>("UsernameHashesBase64") + .HasColumnType("text[]"); + b.HasKey("Id"); b.HasIndex("IdentityDeletionProcessId"); diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20241120145125_AddIsBackupDevicePropertyToDevice.Designer.cs b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20241120145125_AddIsBackupDevicePropertyToDevice.Designer.cs new file mode 100644 index 0000000000..2874774550 --- /dev/null +++ b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20241120145125_AddIsBackupDevicePropertyToDevice.Designer.cs @@ -0,0 +1,931 @@ +// +using System; +using Backbone.Modules.Devices.Infrastructure.Persistence.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Backbone.Modules.Devices.Infrastructure.Database.SqlServer.Migrations +{ + [DbContext(typeof(DevicesDbContext))] + [Migration("20241120145125_AddIsBackupDevicePropertyToDevice")] + partial class AddIsBackupDevicePropertyToDevice + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Devices") + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Aggregates.PushNotifications.PnsRegistration", b => + { + b.Property("DeviceId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("AppId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DevicePushIdentifier") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("Environment") + .HasColumnType("int"); + + b.Property("Handle") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(true) + .HasColumnType("nvarchar(200)") + .IsFixedLength(false); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("DeviceId"); + + b.ToTable("PnsRegistrations", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Aggregates.Tier.Tier", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CanBeManuallyAssigned") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("CanBeUsedAsDefaultForClient") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("Name") + .IsRequired() + .HasMaxLength(30) + .IsUnicode(true) + .HasColumnType("nvarchar(30)") + .IsFixedLength(false); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Tiers", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Challenge", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Challenges", "Challenges", t => + { + t.ExcludeFromMigrations(); + }); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("LastLoginAt") + .HasColumnType("datetime2"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("UserName") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CommunicationLanguage") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2) + .IsUnicode(false) + .HasColumnType("char(2)") + .HasDefaultValue("en") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedByDevice") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("IsBackupDevice") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("IdentityAddress"); + + b.ToTable("Devices", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", b => + { + b.Property("Address") + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("ClientId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeletionGracePeriodEndsAt") + .HasColumnType("datetime2"); + + b.Property("IdentityVersion") + .HasColumnType("tinyint"); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TierId") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("TierIdBeforeDeletion") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.HasKey("Address"); + + b.HasIndex("ClientId") + .HasAnnotation("Npgsql:IndexMethod", "hash"); + + b.HasIndex("TierId") + .HasAnnotation("Npgsql:IndexMethod", "hash"); + + b.ToTable("Identities", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("ApprovalReminder1SentAt") + .HasColumnType("datetime2"); + + b.Property("ApprovalReminder2SentAt") + .HasColumnType("datetime2"); + + b.Property("ApprovalReminder3SentAt") + .HasColumnType("datetime2"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApprovedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CancelledAt") + .HasColumnType("datetime2"); + + b.Property("CancelledByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeletionStartedAt") + .HasColumnType("datetime2"); + + b.Property("GracePeriodEndsAt") + .HasColumnType("datetime2"); + + b.Property("GracePeriodReminder1SentAt") + .HasColumnType("datetime2"); + + b.Property("GracePeriodReminder2SentAt") + .HasColumnType("datetime2"); + + b.Property("GracePeriodReminder3SentAt") + .HasColumnType("datetime2"); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("RejectedAt") + .HasColumnType("datetime2"); + + b.Property("RejectedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("IdentityAddress"); + + b.HasIndex(new[] { "IdentityAddress" }, "IX_only_one_active_deletion_process") + .IsUnique() + .HasFilter("\"Status\" = 1"); + + b.ToTable("IdentityDeletionProcesses", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcessAuditLogEntry", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("AdditionalData") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceIdHash") + .HasColumnType("varbinary(max)"); + + b.Property("IdentityAddressHash") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("IdentityDeletionProcessId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("MessageKey") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NewStatus") + .HasColumnType("int"); + + b.Property("OldStatus") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("IdentityDeletionProcessId"); + + b.ToTable("IdentityDeletionProcessAuditLog", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ClientSecret") + .HasColumnType("nvarchar(max)"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DefaultTier") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayNames") + .HasColumnType("nvarchar(max)"); + + b.Property("JsonWebKeySet") + .HasColumnType("nvarchar(max)"); + + b.Property("MaxIdentities") + .HasColumnType("int"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("nvarchar(max)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("RedirectUris") + .HasColumnType("nvarchar(max)"); + + b.Property("Requirements") + .HasColumnType("nvarchar(max)"); + + b.Property("Settings") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique() + .HasFilter("[ClientId] IS NOT NULL"); + + b.HasIndex("DefaultTier"); + + b.ToTable("OpenIddictApplications", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationId") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("Scopes") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Descriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayNames") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("Resources") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique() + .HasFilter("[Name] IS NOT NULL"); + + b.ToTable("OpenIddictScopes", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationId") + .HasColumnType("nvarchar(450)"); + + b.Property("AuthorizationId") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("ExpirationDate") + .HasColumnType("datetime2"); + + b.Property("Payload") + .HasColumnType("nvarchar(max)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("RedemptionDate") + .HasColumnType("datetime2"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique() + .HasFilter("[ReferenceId] IS NOT NULL"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Device", "Device") + .WithOne("User") + .HasForeignKey("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", "DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", "Identity") + .WithMany("Devices") + .HasForeignKey("IdentityAddress") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Identity"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", null) + .WithMany("DeletionProcesses") + .HasForeignKey("IdentityAddress") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcessAuditLogEntry", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", null) + .WithMany("AuditLog") + .HasForeignKey("IdentityDeletionProcessId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Aggregates.Tier.Tier", null) + .WithMany() + .HasForeignKey("DefaultTier") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.Navigation("User") + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", b => + { + b.Navigation("DeletionProcesses"); + + b.Navigation("Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.Navigation("AuditLog"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20241120145125_AddIsBackupDevicePropertyToDevice.cs b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20241120145125_AddIsBackupDevicePropertyToDevice.cs new file mode 100644 index 0000000000..9b85196150 --- /dev/null +++ b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20241120145125_AddIsBackupDevicePropertyToDevice.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backbone.Modules.Devices.Infrastructure.Database.SqlServer.Migrations +{ + /// + public partial class AddIsBackupDevicePropertyToDevice : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsBackupDevice", + schema: "Devices", + table: "Devices", + type: "bit", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsBackupDevice", + schema: "Devices", + table: "Devices"); + } + } +} diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20241128082923_AddUsernameHashesBase64Column.Designer.cs b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20241128082923_AddUsernameHashesBase64Column.Designer.cs new file mode 100644 index 0000000000..4b7203f822 --- /dev/null +++ b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20241128082923_AddUsernameHashesBase64Column.Designer.cs @@ -0,0 +1,931 @@ +// +using System; +using Backbone.Modules.Devices.Infrastructure.Persistence.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Backbone.Modules.Devices.Infrastructure.Database.SqlServer.Migrations +{ + [DbContext(typeof(DevicesDbContext))] + [Migration("20241128082923_AddUsernameHashesBase64Column")] + partial class AddUsernameHashesBase64Column + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Devices") + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Aggregates.PushNotifications.PnsRegistration", b => + { + b.Property("DeviceId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("AppId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DevicePushIdentifier") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("Environment") + .HasColumnType("int"); + + b.Property("Handle") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(true) + .HasColumnType("nvarchar(200)") + .IsFixedLength(false); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("DeviceId"); + + b.ToTable("PnsRegistrations", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Aggregates.Tier.Tier", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CanBeManuallyAssigned") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("CanBeUsedAsDefaultForClient") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("Name") + .IsRequired() + .HasMaxLength(30) + .IsUnicode(true) + .HasColumnType("nvarchar(30)") + .IsFixedLength(false); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Tiers", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Challenge", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Challenges", "Challenges", t => + { + t.ExcludeFromMigrations(); + }); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("LastLoginAt") + .HasColumnType("datetime2"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("UserName") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CommunicationLanguage") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2) + .IsUnicode(false) + .HasColumnType("char(2)") + .HasDefaultValue("en") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedByDevice") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.HasKey("Id"); + + b.HasIndex("IdentityAddress"); + + b.ToTable("Devices", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", b => + { + b.Property("Address") + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("ClientId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeletionGracePeriodEndsAt") + .HasColumnType("datetime2"); + + b.Property("IdentityVersion") + .HasColumnType("tinyint"); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TierId") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("TierIdBeforeDeletion") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.HasKey("Address"); + + b.HasIndex("ClientId") + .HasAnnotation("Npgsql:IndexMethod", "hash"); + + b.HasIndex("TierId") + .HasAnnotation("Npgsql:IndexMethod", "hash"); + + b.ToTable("Identities", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("ApprovalReminder1SentAt") + .HasColumnType("datetime2"); + + b.Property("ApprovalReminder2SentAt") + .HasColumnType("datetime2"); + + b.Property("ApprovalReminder3SentAt") + .HasColumnType("datetime2"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApprovedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CancelledAt") + .HasColumnType("datetime2"); + + b.Property("CancelledByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeletionStartedAt") + .HasColumnType("datetime2"); + + b.Property("GracePeriodEndsAt") + .HasColumnType("datetime2"); + + b.Property("GracePeriodReminder1SentAt") + .HasColumnType("datetime2"); + + b.Property("GracePeriodReminder2SentAt") + .HasColumnType("datetime2"); + + b.Property("GracePeriodReminder3SentAt") + .HasColumnType("datetime2"); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("RejectedAt") + .HasColumnType("datetime2"); + + b.Property("RejectedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("IdentityAddress"); + + b.HasIndex(new[] { "IdentityAddress" }, "IX_only_one_active_deletion_process") + .IsUnique() + .HasFilter("\"Status\" = 1"); + + b.ToTable("IdentityDeletionProcesses", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcessAuditLogEntry", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("AdditionalData") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceIdHash") + .HasColumnType("varbinary(max)"); + + b.Property("IdentityAddressHash") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("IdentityDeletionProcessId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("MessageKey") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NewStatus") + .HasColumnType("int"); + + b.Property("OldStatus") + .HasColumnType("int"); + + b.PrimitiveCollection("UsernameHashesBase64") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("IdentityDeletionProcessId"); + + b.ToTable("IdentityDeletionProcessAuditLog", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ClientSecret") + .HasColumnType("nvarchar(max)"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DefaultTier") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayNames") + .HasColumnType("nvarchar(max)"); + + b.Property("JsonWebKeySet") + .HasColumnType("nvarchar(max)"); + + b.Property("MaxIdentities") + .HasColumnType("int"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("nvarchar(max)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("RedirectUris") + .HasColumnType("nvarchar(max)"); + + b.Property("Requirements") + .HasColumnType("nvarchar(max)"); + + b.Property("Settings") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique() + .HasFilter("[ClientId] IS NOT NULL"); + + b.HasIndex("DefaultTier"); + + b.ToTable("OpenIddictApplications", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationId") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("Scopes") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Descriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayNames") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("Resources") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique() + .HasFilter("[Name] IS NOT NULL"); + + b.ToTable("OpenIddictScopes", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationId") + .HasColumnType("nvarchar(450)"); + + b.Property("AuthorizationId") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("ExpirationDate") + .HasColumnType("datetime2"); + + b.Property("Payload") + .HasColumnType("nvarchar(max)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("RedemptionDate") + .HasColumnType("datetime2"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique() + .HasFilter("[ReferenceId] IS NOT NULL"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Device", "Device") + .WithOne("User") + .HasForeignKey("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", "DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", "Identity") + .WithMany("Devices") + .HasForeignKey("IdentityAddress") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Identity"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", null) + .WithMany("DeletionProcesses") + .HasForeignKey("IdentityAddress") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcessAuditLogEntry", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", null) + .WithMany("AuditLog") + .HasForeignKey("IdentityDeletionProcessId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Aggregates.Tier.Tier", null) + .WithMany() + .HasForeignKey("DefaultTier") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.Navigation("User") + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", b => + { + b.Navigation("DeletionProcesses"); + + b.Navigation("Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.Navigation("AuditLog"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20241128082923_AddUsernameHashesBase64Column.cs b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20241128082923_AddUsernameHashesBase64Column.cs new file mode 100644 index 0000000000..169737415a --- /dev/null +++ b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20241128082923_AddUsernameHashesBase64Column.cs @@ -0,0 +1,50 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backbone.Modules.Devices.Infrastructure.Database.SqlServer.Migrations +{ + /// + public partial class AddUsernameHashesBase64Column : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "NewStatus", + schema: "Devices", + table: "IdentityDeletionProcessAuditLog", + type: "int", + nullable: true, + oldClrType: typeof(int), + oldType: "int"); + + migrationBuilder.AddColumn( + name: "UsernameHashesBase64", + schema: "Devices", + table: "IdentityDeletionProcessAuditLog", + type: "nvarchar(max)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "UsernameHashesBase64", + schema: "Devices", + table: "IdentityDeletionProcessAuditLog"); + + migrationBuilder.AlterColumn( + name: "NewStatus", + schema: "Devices", + table: "IdentityDeletionProcessAuditLog", + type: "int", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "int", + oldNullable: true); + } + } +} diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20241211083751_RemoveSoftDeleteColumnsFromDevicesTable.Designer.cs b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20241211083751_RemoveSoftDeleteColumnsFromDevicesTable.Designer.cs new file mode 100644 index 0000000000..6153c825c2 --- /dev/null +++ b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20241211083751_RemoveSoftDeleteColumnsFromDevicesTable.Designer.cs @@ -0,0 +1,925 @@ +// +using System; +using Backbone.Modules.Devices.Infrastructure.Persistence.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Backbone.Modules.Devices.Infrastructure.Database.SqlServer.Migrations +{ + [DbContext(typeof(DevicesDbContext))] + [Migration("20241211083751_RemoveSoftDeleteColumnsFromDevicesTable")] + partial class RemoveSoftDeleteColumnsFromDevicesTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Devices") + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Aggregates.PushNotifications.PnsRegistration", b => + { + b.Property("DeviceId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("AppId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DevicePushIdentifier") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("Environment") + .HasColumnType("int"); + + b.Property("Handle") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(true) + .HasColumnType("nvarchar(200)") + .IsFixedLength(false); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("DeviceId"); + + b.ToTable("PnsRegistrations", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Aggregates.Tier.Tier", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CanBeManuallyAssigned") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("CanBeUsedAsDefaultForClient") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("Name") + .IsRequired() + .HasMaxLength(30) + .IsUnicode(true) + .HasColumnType("nvarchar(30)") + .IsFixedLength(false); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Tiers", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Challenge", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Challenges", "Challenges", t => + { + t.ExcludeFromMigrations(); + }); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("LastLoginAt") + .HasColumnType("datetime2"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("UserName") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CommunicationLanguage") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2) + .IsUnicode(false) + .HasColumnType("char(2)") + .HasDefaultValue("en") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedByDevice") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("IsBackupDevice") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("IdentityAddress"); + + b.ToTable("Devices", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", b => + { + b.Property("Address") + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("ClientId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeletionGracePeriodEndsAt") + .HasColumnType("datetime2"); + + b.Property("IdentityVersion") + .HasColumnType("tinyint"); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TierId") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("TierIdBeforeDeletion") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.HasKey("Address"); + + b.HasIndex("ClientId") + .HasAnnotation("Npgsql:IndexMethod", "hash"); + + b.HasIndex("TierId") + .HasAnnotation("Npgsql:IndexMethod", "hash"); + + b.ToTable("Identities", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("ApprovalReminder1SentAt") + .HasColumnType("datetime2"); + + b.Property("ApprovalReminder2SentAt") + .HasColumnType("datetime2"); + + b.Property("ApprovalReminder3SentAt") + .HasColumnType("datetime2"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApprovedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CancelledAt") + .HasColumnType("datetime2"); + + b.Property("CancelledByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeletionStartedAt") + .HasColumnType("datetime2"); + + b.Property("GracePeriodEndsAt") + .HasColumnType("datetime2"); + + b.Property("GracePeriodReminder1SentAt") + .HasColumnType("datetime2"); + + b.Property("GracePeriodReminder2SentAt") + .HasColumnType("datetime2"); + + b.Property("GracePeriodReminder3SentAt") + .HasColumnType("datetime2"); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("RejectedAt") + .HasColumnType("datetime2"); + + b.Property("RejectedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("IdentityAddress"); + + b.HasIndex(new[] { "IdentityAddress" }, "IX_only_one_active_deletion_process") + .IsUnique() + .HasFilter("\"Status\" = 1"); + + b.ToTable("IdentityDeletionProcesses", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcessAuditLogEntry", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("AdditionalData") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceIdHash") + .HasColumnType("varbinary(max)"); + + b.Property("IdentityAddressHash") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("IdentityDeletionProcessId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("MessageKey") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NewStatus") + .HasColumnType("int"); + + b.Property("OldStatus") + .HasColumnType("int"); + + b.PrimitiveCollection("UsernameHashesBase64") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("IdentityDeletionProcessId"); + + b.ToTable("IdentityDeletionProcessAuditLog", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ClientSecret") + .HasColumnType("nvarchar(max)"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DefaultTier") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayNames") + .HasColumnType("nvarchar(max)"); + + b.Property("JsonWebKeySet") + .HasColumnType("nvarchar(max)"); + + b.Property("MaxIdentities") + .HasColumnType("int"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("nvarchar(max)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("RedirectUris") + .HasColumnType("nvarchar(max)"); + + b.Property("Requirements") + .HasColumnType("nvarchar(max)"); + + b.Property("Settings") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique() + .HasFilter("[ClientId] IS NOT NULL"); + + b.HasIndex("DefaultTier"); + + b.ToTable("OpenIddictApplications", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationId") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("Scopes") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Descriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayNames") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("Resources") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique() + .HasFilter("[Name] IS NOT NULL"); + + b.ToTable("OpenIddictScopes", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationId") + .HasColumnType("nvarchar(450)"); + + b.Property("AuthorizationId") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("ExpirationDate") + .HasColumnType("datetime2"); + + b.Property("Payload") + .HasColumnType("nvarchar(max)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("RedemptionDate") + .HasColumnType("datetime2"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique() + .HasFilter("[ReferenceId] IS NOT NULL"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", "Devices"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", "Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Device", "Device") + .WithOne("User") + .HasForeignKey("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", "DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", "Identity") + .WithMany("Devices") + .HasForeignKey("IdentityAddress") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Identity"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", null) + .WithMany("DeletionProcesses") + .HasForeignKey("IdentityAddress") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcessAuditLogEntry", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", null) + .WithMany("AuditLog") + .HasForeignKey("IdentityDeletionProcessId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Aggregates.Tier.Tier", null) + .WithMany() + .HasForeignKey("DefaultTier") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.Navigation("User") + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", b => + { + b.Navigation("DeletionProcesses"); + + b.Navigation("Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.Navigation("AuditLog"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20241211083751_RemoveSoftDeleteColumnsFromDevicesTable.cs b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20241211083751_RemoveSoftDeleteColumnsFromDevicesTable.cs new file mode 100644 index 0000000000..5534128414 --- /dev/null +++ b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20241211083751_RemoveSoftDeleteColumnsFromDevicesTable.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backbone.Modules.Devices.Infrastructure.Database.SqlServer.Migrations +{ + /// + public partial class RemoveSoftDeleteColumnsFromDevicesTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("""DELETE FROM [Devices].[Devices] WHERE "DeletedAt" IS NOT NULL"""); + + migrationBuilder.DropColumn( + name: "DeletedAt", + schema: "Devices", + table: "Devices"); + + migrationBuilder.DropColumn( + name: "DeletedByDevice", + schema: "Devices", + table: "Devices"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DeletedAt", + schema: "Devices", + table: "Devices", + type: "datetime2", + nullable: true); + + migrationBuilder.AddColumn( + name: "DeletedByDevice", + schema: "Devices", + table: "Devices", + type: "char(20)", + unicode: false, + fixedLength: true, + maxLength: 20, + nullable: true); + } + } +} diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/DevicesDbContextModelSnapshot.cs b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/DevicesDbContextModelSnapshot.cs index 021391f517..84ac70c716 100644 --- a/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/DevicesDbContextModelSnapshot.cs +++ b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/DevicesDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Devices") - .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("ProductVersion", "9.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -206,15 +206,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("char(20)") .IsFixedLength(); - b.Property("DeletedAt") - .HasColumnType("datetime2"); - - b.Property("DeletedByDevice") - .HasMaxLength(20) - .IsUnicode(false) - .HasColumnType("char(20)") - .IsFixedLength(); - b.Property("IdentityAddress") .IsRequired() .HasMaxLength(80) @@ -222,6 +213,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("varchar(80)") .IsFixedLength(false); + b.Property("IsBackupDevice") + .HasColumnType("bit"); + b.HasKey("Id"); b.HasIndex("IdentityAddress"); @@ -395,12 +389,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("nvarchar(max)"); - b.Property("NewStatus") + b.Property("NewStatus") .HasColumnType("int"); b.Property("OldStatus") .HasColumnType("int"); + b.PrimitiveCollection("UsernameHashesBase64") + .HasColumnType("nvarchar(max)"); + b.HasKey("Id"); b.HasIndex("IdentityDeletionProcessId"); diff --git a/Modules/Devices/src/Devices.Infrastructure/Devices.Infrastructure.csproj b/Modules/Devices/src/Devices.Infrastructure/Devices.Infrastructure.csproj index a20ab9b63f..c861b3a5fc 100644 --- a/Modules/Devices/src/Devices.Infrastructure/Devices.Infrastructure.csproj +++ b/Modules/Devices/src/Devices.Infrastructure/Devices.Infrastructure.csproj @@ -2,7 +2,7 @@ - + diff --git a/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/QueryableExtensions/DeviceQueryableExtensions.cs b/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/QueryableExtensions/DeviceQueryableExtensions.cs index d83692001a..35d3be6c3a 100644 --- a/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/QueryableExtensions/DeviceQueryableExtensions.cs +++ b/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/QueryableExtensions/DeviceQueryableExtensions.cs @@ -1,6 +1,5 @@ using Backbone.DevelopmentKit.Identity.ValueObjects; using Backbone.Modules.Devices.Domain.Entities.Identities; -using Microsoft.EntityFrameworkCore; namespace Backbone.Modules.Devices.Infrastructure.Persistence.Database.QueryableExtensions; @@ -16,18 +15,8 @@ public static IQueryable OfIdentity(this IQueryable query, Ident return query.Where(d => d.IdentityAddress == address); } - public static IQueryable NotDeleted(this IQueryable query) - { - return query.Where(Device.IsNotDeleted); - } - public static IQueryable WithIdIn(this IQueryable query, IEnumerable ids) { return query.Where(d => ids.Contains(d.Id)); } - - public static IQueryable IncludeUser(this IQueryable query) - { - return query.Include(d => d.User); - } } diff --git a/Modules/Devices/src/Devices.Infrastructure/Persistence/Repository/IdentitiesRepository.cs b/Modules/Devices/src/Devices.Infrastructure/Persistence/Repository/IdentitiesRepository.cs index 4a23e9ab99..690c704251 100644 --- a/Modules/Devices/src/Devices.Infrastructure/Persistence/Repository/IdentitiesRepository.cs +++ b/Modules/Devices/src/Devices.Infrastructure/Persistence/Repository/IdentitiesRepository.cs @@ -53,10 +53,34 @@ public async Task FindDevices(Expression> filter, Exp .ToArrayAsync(cancellationToken); } - public async Task> GetIdentityDeletionProcessAuditLogsByAddress(byte[] identityAddressHash, CancellationToken cancellationToken) + public async Task HasBackupDevice(IdentityAddress identity, CancellationToken cancellationToken) { - return await _readonlyIdentityDeletionProcessAuditLogs - .Where(auditLog => auditLog.IdentityAddressHash == identityAddressHash) + return await _readonlyDevices + .OfIdentity(identity) + .Where(Device.IsBackup) + .AnyAsync(cancellationToken); + } + + public async Task DeleteDevice(Device device, CancellationToken cancellationToken) + { + _devices.Remove(device); + await _dbContext.SaveChangesAsync(cancellationToken); + } + + public async Task> GetIdentityDeletionProcessAuditLogs(Expression> filter, + CancellationToken cancellationToken, bool track = false) + { + // Clearing the change tracker needs to be done because in case of the actual identity deletion, the deletion + // process including all its audit log entries is read first. Then the deletion process is deleted without the + // change tracker being involved. This leads to auditLogEntry.ProcessId being set to null in the database (because + // of the foreign key configuration). But the change tracker does not know about that. + // Later on during the actual deletion we want to update all existing audit log entries to set the usernames. + // And when trying to save the updated audit log entries, EF Core tries to save the process id as well, which is + // impossible, because the deletion process was deleted already. + _dbContext.ChangeTracker.Clear(); + + return await (track ? _identityDeletionProcessAuditLogs : _readonlyIdentityDeletionProcessAuditLogs) + .Where(filter) .ToListAsync(cancellationToken); } @@ -97,7 +121,6 @@ public async Task UpdateWithNewDevice(Identity identity, string password) public async Task> FindAllDevicesOfIdentity(IdentityAddress identity, IEnumerable ids, PaginationFilter paginationFilter, CancellationToken cancellationToken) { var query = _readonlyDevices - .NotDeleted() .IncludeAll(_dbContext) .OfIdentity(identity); @@ -110,11 +133,18 @@ public async Task> FindAllDevicesOfIdentity(IdentityA public async Task GetDeviceById(DeviceId deviceId, CancellationToken cancellationToken, bool track = false) { return await (track ? _devices : _readonlyDevices) - .NotDeleted() .IncludeAll(_dbContext) .FirstOrDefaultAsync(d => d.Id == deviceId, cancellationToken); } + public async Task> GetDevicesByIds(IEnumerable deviceIds, CancellationToken cancellationToken, bool track = false) + { + return await (track ? _devices : _readonlyDevices) + .IncludeAll(_dbContext) + .Where(d => deviceIds.Contains(d.Id)) + .ToListAsync(cancellationToken); + } + public async Task Update(Device device, CancellationToken cancellationToken) { _devices.Update(device); @@ -144,6 +174,14 @@ public async Task> Find(Expression> f .ToListAsync(cancellationToken); } + public async Task FindFirst(Expression> filter, CancellationToken cancellationToken, bool track = false) + { + return await (track ? _identities : _readonlyIdentities) + .IncludeAll(_dbContext) + .Where(filter) + .FirstOrDefaultAsync(cancellationToken); + } + public async Task Delete(Expression> filter, CancellationToken cancellationToken) { await _identities.Where(filter).ExecuteDeleteAsync(cancellationToken); @@ -154,4 +192,10 @@ public async Task AddDeletionProcessAuditLogEntry(IdentityDeletionProcessAuditLo _identityDeletionProcessAuditLogs.Add(auditLogEntry); await _dbContext.SaveChangesAsync(); } + + public async Task Update(IEnumerable auditLogEntries, CancellationToken cancellationToken) + { + _identityDeletionProcessAuditLogs.UpdateRange(auditLogEntries); + await _dbContext.SaveChangesAsync(cancellationToken); + } } diff --git a/Modules/Devices/src/Devices.Infrastructure/PushNotifications/Connectors/Sse/SseMessageBuilder.cs b/Modules/Devices/src/Devices.Infrastructure/PushNotifications/Connectors/Sse/SseMessageBuilder.cs index 9e541cbab6..2f3a6d8197 100644 --- a/Modules/Devices/src/Devices.Infrastructure/PushNotifications/Connectors/Sse/SseMessageBuilder.cs +++ b/Modules/Devices/src/Devices.Infrastructure/PushNotifications/Connectors/Sse/SseMessageBuilder.cs @@ -1,5 +1,6 @@ using System.Net.Http.Json; using System.Text.Json.Serialization; +using System.Web; namespace Backbone.Modules.Devices.Infrastructure.PushNotifications.Connectors.Sse; @@ -16,7 +17,7 @@ public SseMessageBuilder(string recipient, string eventName) public HttpRequestMessage Build() { - var request = new HttpRequestMessage(HttpMethod.Post, $"{_recipient}/events") + var request = new HttpRequestMessage(HttpMethod.Post, $"{HttpUtility.UrlEncode(_recipient)}/events") { Content = JsonContent.Create(new EventPayload(_eventName)) }; diff --git a/Modules/Devices/src/Devices.Infrastructure/PushNotifications/PushService.cs b/Modules/Devices/src/Devices.Infrastructure/PushNotifications/PushService.cs index f6188e26df..c5f426885f 100644 --- a/Modules/Devices/src/Devices.Infrastructure/PushNotifications/PushService.cs +++ b/Modules/Devices/src/Devices.Infrastructure/PushNotifications/PushService.cs @@ -56,7 +56,8 @@ public async Task SendNotification(IPushNotification notification, SendPushNotif private async Task FindDevices(SendPushNotificationFilter filter, CancellationToken cancellationToken) { var result = await _identitiesRepository.FindDevices( - d => filter.IncludedIdentities.Contains(d.IdentityAddress) && !filter.ExcludedDevices.Contains(d.Id), + d => (filter.IncludedIdentities.Count == 0 || filter.IncludedIdentities.Contains(d.IdentityAddress)) && + !filter.ExcludedDevices.Contains(d.Id), d => new DeviceWithOnlyIdAndCommunicationLanguage { Id = d.Id, CommunicationLanguage = d.CommunicationLanguage }, cancellationToken ); @@ -82,7 +83,11 @@ private async Task SendNotificationInternal(IPushNotification notification, Devi .Select(r => { var device = devices.First(d => d.Id == r.DeviceId); - return pnsConnector.Send(r, notification, notificationTexts[device.CommunicationLanguage]); + + if (!notificationTexts.TryGetValue(device.CommunicationLanguage, out var notificationText)) + notificationText = notificationTexts[CommunicationLanguage.DEFAULT_LANGUAGE]; + + return pnsConnector.Send(r, notification, notificationText); }); var sendResults = await Task.WhenAll(sendTasks); diff --git a/Modules/Devices/src/Devices.Infrastructure/Translations/Resources.de.resx b/Modules/Devices/src/Devices.Infrastructure/Translations/Resources.de.resx index 90856d2804..7968443917 100644 --- a/Modules/Devices/src/Devices.Infrastructure/Translations/Resources.de.resx +++ b/Modules/Devices/src/Devices.Infrastructure/Translations/Resources.de.resx @@ -78,4 +78,10 @@ Test + + Wiederherstellungsgerät benutzt + + + Ihr Wiederherstellungsgerät wurde benutzt. + diff --git a/Modules/Devices/src/Devices.Infrastructure/Translations/Resources.it.resx b/Modules/Devices/src/Devices.Infrastructure/Translations/Resources.it.resx index 4f29b8fc08..025a92933a 100644 --- a/Modules/Devices/src/Devices.Infrastructure/Translations/Resources.it.resx +++ b/Modules/Devices/src/Devices.Infrastructure/Translations/Resources.it.resx @@ -1,7 +1,7 @@  - + @@ -78,4 +78,10 @@ Questa è una notifica push di prova. + + Dispositivo di recupero usato + + + Il dispositivo di backup è stato utilizzato. + diff --git a/Modules/Devices/src/Devices.Infrastructure/Translations/Resources.pt.resx b/Modules/Devices/src/Devices.Infrastructure/Translations/Resources.pt.resx index 58f6ebf3f3..540a7e3481 100644 --- a/Modules/Devices/src/Devices.Infrastructure/Translations/Resources.pt.resx +++ b/Modules/Devices/src/Devices.Infrastructure/Translations/Resources.pt.resx @@ -1,126 +1,126 @@  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Há atualizações disponíveis na aplicação. - + Atualizações Disponíveis @@ -129,10 +129,10 @@ Processo de eliminação aprovado - + Isto é uma notificação de teste. - + Teste @@ -147,34 +147,40 @@ Um processo de eliminação foi cancelado - + A sua identidade será excluída dentro de alguns dias. O cancelamento ainda é possível. - + A sua identidade será excluída - + Um processo de eliminação foi iniciado para uma das suas identidades. - + O processo de eliminação foi iniciado - + Há um processo de eliminação para a sua identidade que aguarda a sua aprovação. Se não o aprovar dentro de alguns dias, o processo será cancelado. - + Processo de eliminação a aguardar aprovação - + A eliminação da sua identidade foi concluída. - + A sua identidade foi eliminada - + Estão disponíveis novas atualizações na aplicação. - + Atualização recebida + + Dispositivo de recuperação utilizado + + + O seu dispositivo de cópia de segurança foi utilizado. + diff --git a/Modules/Devices/src/Devices.Infrastructure/Translations/Resources.resx b/Modules/Devices/src/Devices.Infrastructure/Translations/Resources.resx index 820db36955..5639d7f986 100644 --- a/Modules/Devices/src/Devices.Infrastructure/Translations/Resources.resx +++ b/Modules/Devices/src/Devices.Infrastructure/Translations/Resources.resx @@ -60,45 +60,45 @@ : and then encoded with base64 encoding. --> - + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + @@ -177,4 +177,10 @@ Test + + Backup Device used + + + Your backup device was used. + diff --git a/Modules/Devices/test/Devices.Domain.Tests/Domain/ApplicationUserTests.cs b/Modules/Devices/test/Devices.Domain.Tests/Domain/ApplicationUserTests.cs deleted file mode 100644 index ed2275c728..0000000000 --- a/Modules/Devices/test/Devices.Domain.Tests/Domain/ApplicationUserTests.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Backbone.Modules.Devices.Domain.Entities.Identities; - -namespace Backbone.Modules.Devices.Domain.Tests.Domain; - -public class ApplicationUserTests : AbstractTestsBase -{ - [Fact] - public void Login_occurred() - { - // Arrange - var identity = TestDataGenerator.CreateIdentity(); - var device = new Device(identity, CommunicationLanguage.DEFAULT_LANGUAGE); - - var user = new ApplicationUser(device); - - // Act - user.LoginOccurred(); - - // Assert - user.HasLoggedIn.Should().BeTrue(); - } -} diff --git a/Modules/Devices/test/Devices.Domain.Tests/Domain/DeviceTests.cs b/Modules/Devices/test/Devices.Domain.Tests/Domain/DeviceTests.cs index 01b73e6257..748411e7a2 100644 --- a/Modules/Devices/test/Devices.Domain.Tests/Domain/DeviceTests.cs +++ b/Modules/Devices/test/Devices.Domain.Tests/Domain/DeviceTests.cs @@ -1,5 +1,7 @@ using Backbone.BuildingBlocks.Domain.Exceptions; +using Backbone.Modules.Devices.Domain.DomainEvents.Outgoing; using Backbone.Modules.Devices.Domain.Entities.Identities; +using Backbone.Tooling; namespace Backbone.Modules.Devices.Domain.Tests.Domain; @@ -55,8 +57,6 @@ public void IsOnboarded_returns_false_if_user_has_never_logged_in_before() var identity = TestDataGenerator.CreateIdentity(); var device = new Device(identity, CommunicationLanguage.DEFAULT_LANGUAGE); - device.User = new ApplicationUser(device); - // Act var isOnboarded = device.IsOnboarded; @@ -71,8 +71,7 @@ public void IsOnboarded_returns_true_if_user_has_been_used_to_login() var identity = TestDataGenerator.CreateIdentity(); var device = new Device(identity, CommunicationLanguage.DEFAULT_LANGUAGE); - device.User = new ApplicationUser(device); - device.User.LoginOccurred(); + device.LoginOccurred(); // Act var isOnboarded = device.IsOnboarded; @@ -82,20 +81,19 @@ public void IsOnboarded_returns_true_if_user_has_been_used_to_login() } [Fact] - public void An_unOnboarded_device_can_be_deleted() + public void An_unonboarded_device_can_be_deleted() { // Arrange var identity = TestDataGenerator.CreateIdentityWithoutDevice(); - var activeDevice = CreateOnboardedDevice(identity); + CreateOnboardedDevice(identity); var unOnboardedDevice = CreateUnonboardedDevice(identity); // Act - unOnboardedDevice.MarkAsDeleted(activeDevice.Id, identity.Address); + var acting = () => unOnboardedDevice.EnsureCanBeDeleted(identity.Address); // Assert - unOnboardedDevice.DeletedAt.Should().NotBeNull(); - unOnboardedDevice.DeletedByDevice.Should().Be(activeDevice.Id); + acting.Should().NotThrow(); } [Fact] @@ -104,14 +102,14 @@ public void An_onboarded_device_cannot_be_deleted() // Arrange var identity = TestDataGenerator.CreateIdentity(); - var activeDevice = CreateOnboardedDevice(identity); + CreateOnboardedDevice(identity); var onboardedDevice = CreateOnboardedDevice(identity); // Act - var action = () => onboardedDevice.MarkAsDeleted(activeDevice.Id, identity.Address); + var acting = () => onboardedDevice.EnsureCanBeDeleted(identity.Address); // Assert - var domainException = action.Should().Throw().Which; + var domainException = acting.Should().Throw().Which; domainException.Code.Should().Be("error.platform.validation.device.deviceCannotBeDeleted"); } @@ -122,26 +120,76 @@ public void A_device_not_owned_by_active_identity_cannot_be_deleted() var activeIdentity = TestDataGenerator.CreateIdentity(); var otherIdentity = TestDataGenerator.CreateIdentityWithoutDevice(); - var activeDevice = CreateOnboardedDevice(activeIdentity); - var unOnboardedDeviceOfOtherIdentity = CreateUnonboardedDevice(otherIdentity); + CreateOnboardedDevice(activeIdentity); + var unonboardedDeviceOfOtherIdentity = CreateUnonboardedDevice(otherIdentity); // Act - var action = () => unOnboardedDeviceOfOtherIdentity.MarkAsDeleted(activeDevice.Id, activeIdentity.Address); + var action = () => unonboardedDeviceOfOtherIdentity.EnsureCanBeDeleted(activeIdentity.Address); // Assert var domainException = action.Should().Throw().Which; domainException.Code.Should().Be("error.platform.validation.device.deviceCannotBeDeleted"); } + [Fact] + public void A_backup_device_can_be_marked_as_used() + { + // Arrange + var activeIdentity = TestDataGenerator.CreateIdentity(); + var device = CreateBackupDevice(activeIdentity); + + // Act + device.LoginOccurred(); + + // Assert + device.IsBackupDevice.Should().BeFalse(); + } + + [Fact] + public void When_a_device_logs_in_the_last_login_date_is_set() + { + // Arrange + var identity = TestDataGenerator.CreateIdentity(); + var device = new Device(identity, CommunicationLanguage.DEFAULT_LANGUAGE); + var utcNow = DateTime.UtcNow; + SystemTime.Set(utcNow); + + // Act + device.LoginOccurred(); + + // Assert + device.User.LastLoginAt.Should().Be(utcNow); + } + + [Fact] + public void When_a_backup_device_logs_in_a_BackupDeviceUsedDomainEvent_is_raised() + { + // Arrange + var identity = TestDataGenerator.CreateIdentity(); + var device = identity.AddDevice(CommunicationLanguage.DEFAULT_LANGUAGE, identity.Devices.First().Id, true); + + // Act + device.LoginOccurred(); + + // Assert + var domainEvent = device.Should().HaveASingleDomainEvent(); + domainEvent.IdentityAddress.Should().Be(identity.Address); + } + private static Device CreateUnonboardedDevice(Identity identity) { - return identity.AddDevice(CommunicationLanguage.DEFAULT_LANGUAGE, identity.Devices.First().Id); + return identity.AddDevice(CommunicationLanguage.DEFAULT_LANGUAGE, identity.Devices.First().Id, false); } private static Device CreateOnboardedDevice(Identity identity) { - var activeDevice = new Device(identity, CommunicationLanguage.DEFAULT_LANGUAGE); - activeDevice.User.LoginOccurred(); - return activeDevice; + var device = new Device(identity, CommunicationLanguage.DEFAULT_LANGUAGE); + device.LoginOccurred(); + return device; + } + + private static Device CreateBackupDevice(Identity identity) + { + return new Device(identity, CommunicationLanguage.DEFAULT_LANGUAGE, null, true); } } diff --git a/Modules/Devices/test/Devices.Domain.Tests/Identities/ApproveDeletionProcessTests.cs b/Modules/Devices/test/Devices.Domain.Tests/Identities/ApproveDeletionProcessTests.cs index 78f9a1c942..76cf470375 100644 --- a/Modules/Devices/test/Devices.Domain.Tests/Identities/ApproveDeletionProcessTests.cs +++ b/Modules/Devices/test/Devices.Domain.Tests/Identities/ApproveDeletionProcessTests.cs @@ -128,4 +128,10 @@ private static Identity CreateIdentityWithDeletionProcessWaitingForApproval() identity.StartDeletionProcessAsSupport(); return identity; } + + public override void Dispose() + { + base.Dispose(); + Hasher.Reset(); + } } diff --git a/Modules/Devices/test/Devices.Domain.Tests/Identities/DeletionGracePeriodReminderTests.cs b/Modules/Devices/test/Devices.Domain.Tests/Identities/DeletionGracePeriodReminderTests.cs index dc5ff06243..1b36894737 100644 --- a/Modules/Devices/test/Devices.Domain.Tests/Identities/DeletionGracePeriodReminderTests.cs +++ b/Modules/Devices/test/Devices.Domain.Tests/Identities/DeletionGracePeriodReminderTests.cs @@ -9,12 +9,6 @@ namespace Backbone.Modules.Devices.Domain.Tests.Identities; public class DeletionGracePeriodReminderTests : AbstractTestsBase { - public override void Dispose() - { - Hasher.Reset(); - base.Dispose(); - } - [Fact] public void DeletionGracePeriodReminder1Sent_updates_GracePeriodReminder1SentAt() { @@ -139,4 +133,10 @@ private static Identity CreateIdentity() var address = IdentityAddress.Create([], "prod.enmeshed.eu"); return new Identity("", address, [], TierId.Generate(), 1, CommunicationLanguage.DEFAULT_LANGUAGE); } + + public override void Dispose() + { + base.Dispose(); + Hasher.Reset(); + } } diff --git a/Modules/Devices/test/Devices.Domain.Tests/Identities/DeletionProcessApprovalReminderTests.cs b/Modules/Devices/test/Devices.Domain.Tests/Identities/DeletionProcessApprovalReminderTests.cs index f3d3213049..15c211314e 100644 --- a/Modules/Devices/test/Devices.Domain.Tests/Identities/DeletionProcessApprovalReminderTests.cs +++ b/Modules/Devices/test/Devices.Domain.Tests/Identities/DeletionProcessApprovalReminderTests.cs @@ -131,10 +131,9 @@ private static Identity CreateIdentity() return new Identity("", address, [], TierId.Generate(), 1, CommunicationLanguage.DEFAULT_LANGUAGE); } - [Fact] public override void Dispose() { - Hasher.Reset(); base.Dispose(); + Hasher.Reset(); } } diff --git a/Modules/Devices/test/Devices.Domain.Tests/Identities/DeletionProcessGracePeriodTests.cs b/Modules/Devices/test/Devices.Domain.Tests/Identities/DeletionProcessGracePeriodTests.cs index 0719bd71c5..45ab6a6860 100644 --- a/Modules/Devices/test/Devices.Domain.Tests/Identities/DeletionProcessGracePeriodTests.cs +++ b/Modules/Devices/test/Devices.Domain.Tests/Identities/DeletionProcessGracePeriodTests.cs @@ -7,12 +7,6 @@ namespace Backbone.Modules.Devices.Domain.Tests.Identities; public class DeletionProcessGracePeriodTests : AbstractTestsBase { - public override void Dispose() - { - Hasher.Reset(); - base.Dispose(); - } - [Fact] public void DeletionGracePeriodReminder1Sent_updates_GracePeriodReminder1SentAt() { @@ -128,4 +122,10 @@ private static Identity CreateIdentityWithApprovedDeletionProcess() return identity; } + + public override void Dispose() + { + base.Dispose(); + Hasher.Reset(); + } } diff --git a/Modules/Devices/test/Devices.Domain.Tests/Identities/IdentityDeletionProcessAuditLogEntryTests.cs b/Modules/Devices/test/Devices.Domain.Tests/Identities/IdentityDeletionProcessAuditLogEntryTests.cs new file mode 100644 index 0000000000..3afe98a1b2 --- /dev/null +++ b/Modules/Devices/test/Devices.Domain.Tests/Identities/IdentityDeletionProcessAuditLogEntryTests.cs @@ -0,0 +1,54 @@ +using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Devices.Domain.Entities.Identities; + +namespace Backbone.Modules.Devices.Domain.Tests.Identities; + +public class IdentityDeletionProcessAuditLogEntryTests : AbstractTestsBase +{ + [Fact] + public void AssociateUsernames_happy_path() + { + // Arrange + var auditLogEntry = IdentityDeletionProcessAuditLogEntry.DeletionCompleted(CreateRandomIdentityAddress()); + + // Act + auditLogEntry.AssociateUsernames([Username.Parse("USR1111111111111111"), Username.Parse("USR2222222222222222")]); + + // Assert + auditLogEntry.UsernameHashesBase64.Should().HaveCount(2); + auditLogEntry.UsernameHashesBase64.Should().Contain("jj3azydpiPwK4iFxo/wpCP6pP5Yf5MStnu/hyzMUZ14=", "FQao/LArcVbFzRs4RgYCU5JycRx9zmJMxY5ApJ0Nk8E="); + } + + [Fact] + public void IsAssociatedToUser_returns_true_if_user_is_associated() + { + // Arrange + var username1 = Username.Parse("USR1111111111111111"); + var username2 = Username.Parse("USR2222222222222222"); + var auditLogEntry = IdentityDeletionProcessAuditLogEntry.DeletionCompleted(CreateRandomIdentityAddress()); + auditLogEntry.AssociateUsernames([username1, username2]); + + // Act + var resultForUsername1 = IdentityDeletionProcessAuditLogEntry.IsAssociatedToUser(username1).Compile()(auditLogEntry); + var resultForUsername2 = IdentityDeletionProcessAuditLogEntry.IsAssociatedToUser(username2).Compile()(auditLogEntry); + + // Assert + resultForUsername1.Should().BeTrue(); + resultForUsername2.Should().BeTrue(); + } + + [Fact] + public void IsAssociatedToUser_returns_false_if_user_is_not_associated() + { + // Arrange + var unassociatedUsername = Username.Parse("USR3333333333333333"); + var auditLogEntry = IdentityDeletionProcessAuditLogEntry.DeletionCompleted(CreateRandomIdentityAddress()); + auditLogEntry.AssociateUsernames([Username.Parse("USR1111111111111111"), Username.Parse("USR2222222222222222")]); + + // Act + var result = IdentityDeletionProcessAuditLogEntry.IsAssociatedToUser(unassociatedUsername).Compile()(auditLogEntry); + + // Assert + result.Should().BeFalse(); + } +} diff --git a/Modules/Devices/test/Devices.Domain.Tests/Identities/IsGracePeriodOverTests.cs b/Modules/Devices/test/Devices.Domain.Tests/Identities/IsGracePeriodOverTests.cs new file mode 100644 index 0000000000..214652bddd --- /dev/null +++ b/Modules/Devices/test/Devices.Domain.Tests/Identities/IsGracePeriodOverTests.cs @@ -0,0 +1,35 @@ +using Backbone.Modules.Devices.Domain.Entities.Identities; +using Backbone.Tooling; + +namespace Backbone.Modules.Devices.Domain.Tests.Identities; + +public class IsGracePeriodOverTests : AbstractTestsBase +{ + [Fact] + public void Returns_false_if_grace_period_is_not_over() + { + // Arrange + var identity = TestDataGenerator.CreateIdentityWithApprovedDeletionProcess(); + + // Act + var result = identity.IsGracePeriodOver; + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void Returns_true_if_grace_period_is_over() + { + // Arrange + var identity = TestDataGenerator.CreateIdentityWithApprovedDeletionProcess(); + + SystemTime.Set(SystemTime.UtcNow.AddDays(IdentityDeletionConfiguration.Instance.LengthOfGracePeriodInDays + 1)); + + // Act + var result = identity.IsGracePeriodOver; + + // Assert + result.Should().BeTrue(); + } +} diff --git a/Modules/Devices/test/Devices.Domain.Tests/Identities/RejectDeletionProcessTests.cs b/Modules/Devices/test/Devices.Domain.Tests/Identities/RejectDeletionProcessTests.cs index f20e681f06..a73bfd09dd 100644 --- a/Modules/Devices/test/Devices.Domain.Tests/Identities/RejectDeletionProcessTests.cs +++ b/Modules/Devices/test/Devices.Domain.Tests/Identities/RejectDeletionProcessTests.cs @@ -105,4 +105,10 @@ private static Identity CreateIdentityWithDeletionProcessWaitingForApproval() identity.StartDeletionProcessAsSupport(); return identity; } + + public override void Dispose() + { + base.Dispose(); + Hasher.Reset(); + } } diff --git a/Modules/Devices/test/Devices.Domain.Tests/Identities/StartDeletionProcessAsOwnerTests.cs b/Modules/Devices/test/Devices.Domain.Tests/Identities/StartDeletionProcessAsOwnerTests.cs index 55076cc41a..8c350ea874 100644 --- a/Modules/Devices/test/Devices.Domain.Tests/Identities/StartDeletionProcessAsOwnerTests.cs +++ b/Modules/Devices/test/Devices.Domain.Tests/Identities/StartDeletionProcessAsOwnerTests.cs @@ -10,12 +10,6 @@ namespace Backbone.Modules.Devices.Domain.Tests.Identities; public class StartDeletionProcessAsOwnerTests : AbstractTestsBase { - public override void Dispose() - { - Hasher.Reset(); - base.Dispose(); - } - [Fact] public void Start_deletion_process() { @@ -105,6 +99,22 @@ public void Raises_domain_events() identityToBeDeletedDomainEvent.IdentityAddress.Should().Be(activeIdentity.Address); } + [Fact] + public void Passing_a_lengthOfDeletionGracePeriod_overrides_the_configured_value() + { + //Arrange + var activeIdentity = TestDataGenerator.CreateIdentity(); + var activeDevice = activeIdentity.Devices[0]; + SystemTime.Set("2000-01-01"); + + //Act + activeIdentity.StartDeletionProcessAsOwner(activeDevice.Id, 1); + + // Assert + activeIdentity.DeletionGracePeriodEndsAt.Should().Be(DateTime.Parse("2000-01-02")); + activeIdentity.DeletionProcesses.First().GracePeriodEndsAt.Should().Be(DateTime.Parse("2000-01-02")); + } + private static void AssertDeletionProcessWasStarted(Identity activeIdentity) { activeIdentity.DeletionProcesses.Should().HaveCount(1); @@ -133,4 +143,10 @@ private static Identity CreateIdentity() var address = IdentityAddress.Create([], "prod.enmeshed.eu"); return new Identity("", address, [], TierId.Generate(), 1, CommunicationLanguage.DEFAULT_LANGUAGE); } + + public override void Dispose() + { + base.Dispose(); + Hasher.Reset(); + } } diff --git a/Modules/Devices/test/Devices.Domain.Tests/Identities/StartDeletionProcessAsSupportTests.cs b/Modules/Devices/test/Devices.Domain.Tests/Identities/StartDeletionProcessAsSupportTests.cs index 4a4aa40a4d..437ccc09c3 100644 --- a/Modules/Devices/test/Devices.Domain.Tests/Identities/StartDeletionProcessAsSupportTests.cs +++ b/Modules/Devices/test/Devices.Domain.Tests/Identities/StartDeletionProcessAsSupportTests.cs @@ -92,4 +92,10 @@ private static Identity CreateIdentity() var address = IdentityAddress.Create([], "prod.enmeshed.eu"); return new Identity("", address, [], TierId.Generate(), 1, CommunicationLanguage.DEFAULT_LANGUAGE); } + + public override void Dispose() + { + base.Dispose(); + Hasher.Reset(); + } } diff --git a/Modules/Devices/test/Devices.Infrastructure.Tests/Tests/PushNotifications/NotificationTexts/PushNotificationTextProviderTests.cs b/Modules/Devices/test/Devices.Infrastructure.Tests/Tests/PushNotifications/NotificationTexts/PushNotificationTextProviderTests.cs index c97402365e..480da3cd3e 100644 --- a/Modules/Devices/test/Devices.Infrastructure.Tests/Tests/PushNotifications/NotificationTexts/PushNotificationTextProviderTests.cs +++ b/Modules/Devices/test/Devices.Infrastructure.Tests/Tests/PushNotifications/NotificationTexts/PushNotificationTextProviderTests.cs @@ -1,5 +1,6 @@ using Backbone.BuildingBlocks.Application.PushNotifications; using Backbone.Modules.Devices.Application.Infrastructure.PushNotifications; +using Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.Announcements; using Backbone.Modules.Devices.Domain.Entities.Identities; using Backbone.Modules.Devices.Infrastructure.PushNotifications.NotificationTexts; @@ -60,7 +61,8 @@ private static IEnumerable GetNotificationTypes() return typeof(TestPushNotification) .Assembly.GetTypes() - .Where(t => t.IsAssignableTo(typeof(IPushNotification)) && !t.IsInterface); + .Where(t => t.IsAssignableTo(typeof(IPushNotification)) && !t.IsInterface) + .Where(t => t != typeof(NewAnnouncementPushNotification)); // `NewAnnouncementPushNotification` does not have a translatable text and must therefore be excluded } private class AllSupportedLanguagesExceptEnglishCrossJoinedWithNotificationTypes : TheoryData diff --git a/Modules/Directory.Build.props b/Modules/Directory.Build.props index 30aefeae66..5ef47d7ee9 100644 --- a/Modules/Directory.Build.props +++ b/Modules/Directory.Build.props @@ -41,7 +41,7 @@ all - + @@ -51,7 +51,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Modules/Relationships/src/Relationships.Application/Identities/IdentityDeleter.Tests.cs b/Modules/Relationships/src/Relationships.Application/Identities/IdentityDeleter.Tests.cs index 7bc65902e9..7bd59b7c74 100644 --- a/Modules/Relationships/src/Relationships.Application/Identities/IdentityDeleter.Tests.cs +++ b/Modules/Relationships/src/Relationships.Application/Identities/IdentityDeleter.Tests.cs @@ -1,6 +1,7 @@ using Backbone.BuildingBlocks.Application.Identities; using Backbone.Modules.Relationships.Application.Relationships.Commands.DecomposeAndAnonymizeRelationshipsOfIdentity; using Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.AnonymizeRelationshipTemplateAllocationsAllocatedByIdentity; +using Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.AnonymizeRelationshipTemplatesForIdentity; using Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.DeleteRelationshipTemplatesOfIdentity; using FakeItEasy; using MediatR; @@ -28,6 +29,9 @@ public async Task Deleter_calls_correct_command() A.CallTo(() => mockMediator.Send( A.That.Matches(i => i.IdentityAddress == identityAddress), A._)).MustHaveHappenedOnceExactly(); + A.CallTo(() => mockMediator.Send( + A.That.Matches(i => i.IdentityAddress == identityAddress), + A._)).MustHaveHappenedOnceExactly(); A.CallTo(() => mockMediator.Send( A.That.Matches(i => i.IdentityAddress == identityAddress), A._)).MustHaveHappenedOnceExactly(); diff --git a/Modules/Relationships/src/Relationships.Application/Identities/IdentityDeleter.cs b/Modules/Relationships/src/Relationships.Application/Identities/IdentityDeleter.cs index 0f5d0b023e..9df8b9239f 100644 --- a/Modules/Relationships/src/Relationships.Application/Identities/IdentityDeleter.cs +++ b/Modules/Relationships/src/Relationships.Application/Identities/IdentityDeleter.cs @@ -2,6 +2,7 @@ using Backbone.DevelopmentKit.Identity.ValueObjects; using Backbone.Modules.Relationships.Application.Relationships.Commands.DecomposeAndAnonymizeRelationshipsOfIdentity; using Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.AnonymizeRelationshipTemplateAllocationsAllocatedByIdentity; +using Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.AnonymizeRelationshipTemplatesForIdentity; using Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.DeleteRelationshipTemplatesOfIdentity; using MediatR; @@ -23,6 +24,7 @@ public async Task Delete(IdentityAddress identityAddress) await _mediator.Send(new DecomposeAndAnonymizeRelationshipsOfIdentityCommand(identityAddress)); await _deletionProcessLogger.LogDeletion(identityAddress, "Relationships"); await _mediator.Send(new DeleteRelationshipTemplatesOfIdentityCommand(identityAddress)); + await _mediator.Send(new AnonymizeRelationshipTemplatesForIdentityCommand(identityAddress)); await _deletionProcessLogger.LogDeletion(identityAddress, "RelationshipTemplates"); await _mediator.Send(new AnonymizeRelationshipTemplateAllocationsAllocatedByIdentityCommand(identityAddress)); await _deletionProcessLogger.LogDeletion(identityAddress, "RelationshipTemplateAllocations"); diff --git a/Modules/Relationships/src/Relationships.Application/Infrastructure/Persistence/Repository/IRelationshipTemplatesRepository.cs b/Modules/Relationships/src/Relationships.Application/Infrastructure/Persistence/Repository/IRelationshipTemplatesRepository.cs index 7b845a82ce..2f41f02727 100644 --- a/Modules/Relationships/src/Relationships.Application/Infrastructure/Persistence/Repository/IRelationshipTemplatesRepository.cs +++ b/Modules/Relationships/src/Relationships.Application/Infrastructure/Persistence/Repository/IRelationshipTemplatesRepository.cs @@ -12,9 +12,12 @@ public interface IRelationshipTemplatesRepository Task> FindTemplates(IEnumerable queryItems, IdentityAddress activeIdentity, PaginationFilter paginationFilter, CancellationToken cancellationToken, bool track = false); + Task> FindTemplates(Expression> filter, CancellationToken cancellationToken); + Task Find(RelationshipTemplateId id, IdentityAddress identityAddress, CancellationToken cancellationToken, bool track = false); Task Add(RelationshipTemplate template, CancellationToken cancellationToken); Task Update(RelationshipTemplate template); + Task Update(IEnumerable templates, CancellationToken cancellationToken); Task Delete(Expression> filter, CancellationToken cancellationToken); Task Delete(RelationshipTemplate template, CancellationToken cancellationToken); diff --git a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/AnonymizeRelationshipTemplatesForIdentityCommand.cs b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/AnonymizeRelationshipTemplatesForIdentityCommand.cs new file mode 100644 index 0000000000..266548a344 --- /dev/null +++ b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/AnonymizeRelationshipTemplatesForIdentityCommand.cs @@ -0,0 +1,13 @@ +using MediatR; + +namespace Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.AnonymizeRelationshipTemplatesForIdentity; + +public class AnonymizeRelationshipTemplatesForIdentityCommand : IRequest +{ + public AnonymizeRelationshipTemplatesForIdentityCommand(string identityAddress) + { + IdentityAddress = identityAddress; + } + + public string IdentityAddress { get; set; } +} diff --git a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/Handler.cs b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/Handler.cs new file mode 100644 index 0000000000..282c568cc4 --- /dev/null +++ b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/Handler.cs @@ -0,0 +1,29 @@ +using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Relationships.Application.Infrastructure.Persistence.Repository; +using Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates; +using MediatR; +using Microsoft.Extensions.Options; + +namespace Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.AnonymizeRelationshipTemplatesForIdentity; + +public class Handler : IRequestHandler +{ + private readonly IRelationshipTemplatesRepository _relationshipTemplatesRepository; + private readonly ApplicationOptions _applicationOptions; + + public Handler(IRelationshipTemplatesRepository relationshipTemplatesRepository, IOptions options) + { + _relationshipTemplatesRepository = relationshipTemplatesRepository; + _applicationOptions = options.Value; + } + + public async Task Handle(AnonymizeRelationshipTemplatesForIdentityCommand request, CancellationToken cancellationToken) + { + var relationshipTemplates = (await _relationshipTemplatesRepository.FindTemplates(RelationshipTemplate.IsFor(IdentityAddress.Parse(request.IdentityAddress)), cancellationToken)).ToList(); + + foreach (var relationshipTemplate in relationshipTemplates) + relationshipTemplate.AnonymizeForIdentity(_applicationOptions.DidDomainName); + + await _relationshipTemplatesRepository.Update(relationshipTemplates, cancellationToken); + } +} diff --git a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/Validator.cs b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/Validator.cs new file mode 100644 index 0000000000..f68d8dfe19 --- /dev/null +++ b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/AnonymizeRelationshipTemplatesForIdentity/Validator.cs @@ -0,0 +1,14 @@ +using Backbone.BuildingBlocks.Application.Extensions; +using Backbone.DevelopmentKit.Identity.ValueObjects; +using FluentValidation; + +namespace Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.AnonymizeRelationshipTemplatesForIdentity; + +public class Validator : AbstractValidator +{ + public Validator() + { + RuleFor(c => c.IdentityAddress) + .ValidId(); + } +} diff --git a/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.AnonymizeForIdentityTests.cs b/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.AnonymizeForIdentityTests.cs new file mode 100644 index 0000000000..7e8b5919fb --- /dev/null +++ b/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.AnonymizeForIdentityTests.cs @@ -0,0 +1,43 @@ +using Backbone.BuildingBlocks.Domain.Exceptions; +using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.UnitTestTools.Extensions; + +namespace Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates; + +public class RelationshipTemplateAnonymizeForIdentityTests : AbstractTestsBase +{ + private const string DID_DOMAIN_NAME = "localhost"; + + [Fact] + public void Personalized_template_can_be_anonymized() + { + // Arrange + var creatorIdentityAddress = CreateRandomIdentityAddress(); + var forIdentityAddress = CreateRandomIdentityAddress(); + var deviceId = CreateRandomDeviceId(); + byte[] content = [1, 1, 1, 1, 1, 1, 1, 1]; + var relationshipTemplate = new RelationshipTemplate(creatorIdentityAddress, deviceId, null, null, content, forIdentityAddress); + + // Act + relationshipTemplate.AnonymizeForIdentity(DID_DOMAIN_NAME); + + // Assert + relationshipTemplate.ForIdentity.Should().Be(IdentityAddress.GetAnonymized(DID_DOMAIN_NAME)); + } + + [Fact] + public void Non_personalized_template_can_not_be_anonymized() + { + // Arrange + var creatorIdentityAddress = CreateRandomIdentityAddress(); + var deviceId = CreateRandomDeviceId(); + byte[] content = [1, 1, 1, 1, 1, 1, 1, 1]; + var relationshipTemplate = new RelationshipTemplate(creatorIdentityAddress, deviceId, null, _dateTimeNow, content); + + // Act + var acting = () => relationshipTemplate.AnonymizeForIdentity(DID_DOMAIN_NAME); + + // Assert + acting.Should().Throw().WithError("error.platform.validation.relationship.relationshipTemplateNotPersonalized"); + } +} diff --git a/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.cs b/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.cs index 7e16fd2e95..a78f4464b9 100644 --- a/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.cs +++ b/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.cs @@ -50,7 +50,7 @@ public RelationshipTemplate(IdentityAddress createdBy, DeviceId createdByDevice, public DateTime CreatedAt { get; set; } - public IdentityAddress? ForIdentity { get; set; } + public IdentityAddress? ForIdentity { get; private set; } public byte[]? Password { get; set; } public List Allocations { get; set; } = []; @@ -69,6 +69,15 @@ public void AllocateFor(IdentityAddress identity, DeviceId device) Allocations.Add(new RelationshipTemplateAllocation(Id, identity, device)); } + public void AnonymizeForIdentity(string didDomainName) + { + EnsureIsPersonalized(); + + var anonymousIdentity = IdentityAddress.GetAnonymized(didDomainName); + + ForIdentity = anonymousIdentity; + } + public bool IsAllocatedBy(IdentityAddress identity) { return Allocations.Any(x => x.AllocatedBy == identity); @@ -87,6 +96,11 @@ public void EnsureCanBeDeletedBy(IdentityAddress identityAddress) if (CreatedBy != identityAddress) throw new DomainActionForbiddenException(); } + public void EnsureIsPersonalized() + { + if (ForIdentity == null) throw new DomainException(DomainErrors.RelationshipTemplateNotPersonalized()); + } + #region Expressions public static Expression> HasId(RelationshipTemplateId id) @@ -117,5 +131,10 @@ public static Expression> CanBeCollectedWithPas a.AllocatedBy == activeIdentity); // if the template has already been allocated by the active identity, it doesn't need to pass the password again; } + public static Expression> IsFor(IdentityAddress identityAddress) + { + return relationshipTemplate => relationshipTemplate.ForIdentity == identityAddress; + } + #endregion } diff --git a/Modules/Relationships/src/Relationships.Domain/Aggregates/Relationships/Relationship.DecomposeDueToIdentityDeletionTests.cs b/Modules/Relationships/src/Relationships.Domain/Aggregates/Relationships/Relationship.DecomposeDueToIdentityDeletionTests.cs index 5edb8fa3af..02fefecaeb 100644 --- a/Modules/Relationships/src/Relationships.Domain/Aggregates/Relationships/Relationship.DecomposeDueToIdentityDeletionTests.cs +++ b/Modules/Relationships/src/Relationships.Domain/Aggregates/Relationships/Relationship.DecomposeDueToIdentityDeletionTests.cs @@ -30,19 +30,6 @@ public void Decomposition_can_be_performed_from_multiple_statuses(RelationshipSt relationship.ToHasDecomposed.Should().BeFalse(); } - [Fact] - public void Decomposition_can_not_be_called_by_the_same_identity_twice() - { - // Arrange - var relationship = CreateRelationshipDecomposedByFrom(IDENTITY_1, IDENTITY_2); - - // Act - var acting = () => relationship.DecomposeDueToIdentityDeletion(IDENTITY_1, DID_DOMAIN_NAME); - - // Assert - acting.Should().Throw().WithError("error.platform.validation.relationship.relationshipAlreadyDecomposed"); - } - [Fact] public void Decomposition_can_not_be_performed_by_other_identities() { diff --git a/Modules/Relationships/src/Relationships.Domain/Aggregates/Relationships/Relationship.cs b/Modules/Relationships/src/Relationships.Domain/Aggregates/Relationships/Relationship.cs index f4c8c926f7..8f60d65976 100644 --- a/Modules/Relationships/src/Relationships.Domain/Aggregates/Relationships/Relationship.cs +++ b/Modules/Relationships/src/Relationships.Domain/Aggregates/Relationships/Relationship.cs @@ -306,7 +306,8 @@ public void Decompose(IdentityAddress activeIdentity, DeviceId activeDevice) public void DecomposeDueToIdentityDeletion(IdentityAddress identityToBeDeleted, string didDomainName) { EnsureHasParticipant(identityToBeDeleted); - EnsureRelationshipNotDecomposedBy(identityToBeDeleted); + + if (From == identityToBeDeleted && FromHasDecomposed || To == identityToBeDeleted && ToHasDecomposed) return; if (Status is RelationshipStatus.DeletionProposed) DecomposeAsSecondParticipant(identityToBeDeleted, null, RelationshipAuditLogEntryReason.DecompositionDueToIdentityDeletion); diff --git a/Modules/Relationships/src/Relationships.Domain/DomainErrors.cs b/Modules/Relationships/src/Relationships.Domain/DomainErrors.cs index 61fce6d94d..6652e1e392 100644 --- a/Modules/Relationships/src/Relationships.Domain/DomainErrors.cs +++ b/Modules/Relationships/src/Relationships.Domain/DomainErrors.cs @@ -82,4 +82,10 @@ public static DomainError RelationshipTemplateNotAllocated() return new DomainError("error.platform.validation.relationship.relationshipTemplateNotAllocated", "The relationship template has to be allocated before it can be used to establish a relationship. Send a GET request to the /RelationshipTemplates/{id} endpoint to allocate the template."); } + + public static DomainError RelationshipTemplateNotPersonalized() + { + return new DomainError("error.platform.validation.relationship.relationshipTemplateNotPersonalized", + "The relationship template has to be personalized."); + } } diff --git a/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/Repository/RelationshipTemplatesRepository.cs b/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/Repository/RelationshipTemplatesRepository.cs index 4afa5fac1b..82ac567f55 100644 --- a/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/Repository/RelationshipTemplatesRepository.cs +++ b/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/Repository/RelationshipTemplatesRepository.cs @@ -79,12 +79,23 @@ public async Task> FindTemplates(IEnume return templates; } + public async Task> FindTemplates(Expression> filter, CancellationToken cancellationToken) + { + return await _templates.Where(filter).ToListAsync(cancellationToken); + } + public async Task Update(RelationshipTemplate template) { _templates.Update(template); await _dbContext.SaveChangesAsync(); } + public async Task Update(IEnumerable templates, CancellationToken cancellationToken) + { + _templates.UpdateRange(templates); + await _dbContext.SaveChangesAsync(cancellationToken); + } + public async Task> FindRelationshipTemplateAllocations(Expression> filter, CancellationToken cancellationToken) { diff --git a/Modules/Tokens/src/Tokens.Application/ApplicationOptions.cs b/Modules/Tokens/src/Tokens.Application/ApplicationOptions.cs index 7137094dd6..7dd53fb8c2 100644 --- a/Modules/Tokens/src/Tokens.Application/ApplicationOptions.cs +++ b/Modules/Tokens/src/Tokens.Application/ApplicationOptions.cs @@ -6,6 +6,11 @@ public class ApplicationOptions { [Required] public PaginationOptions Pagination { get; set; } = new(); + + [Required] + [MinLength(3)] + [MaxLength(45)] + public string DidDomainName { get; set; } = null!; } public class PaginationOptions diff --git a/Modules/Tokens/src/Tokens.Application/Identities/IdentityDeleter.cs b/Modules/Tokens/src/Tokens.Application/Identities/IdentityDeleter.cs index 895709831b..836bd57dff 100644 --- a/Modules/Tokens/src/Tokens.Application/Identities/IdentityDeleter.cs +++ b/Modules/Tokens/src/Tokens.Application/Identities/IdentityDeleter.cs @@ -1,5 +1,6 @@ using Backbone.BuildingBlocks.Application.Identities; using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Tokens.Application.Tokens.Commands.AnonymizeTokensForIdentity; using Backbone.Modules.Tokens.Application.Tokens.Commands.DeleteTokensOfIdentity; using MediatR; @@ -19,6 +20,7 @@ public IdentityDeleter(IMediator mediator, IDeletionProcessLogger deletionProces public async Task Delete(IdentityAddress identityAddress) { await _mediator.Send(new DeleteTokensOfIdentityCommand(identityAddress)); + await _mediator.Send(new AnonymizeTokensForIdentityCommand(identityAddress)); await _deletionProcessLogger.LogDeletion(identityAddress, "Tokens"); } } diff --git a/Modules/Tokens/src/Tokens.Application/Infrastructure/Persistence/Repository/ITokensRepository.cs b/Modules/Tokens/src/Tokens.Application/Infrastructure/Persistence/Repository/ITokensRepository.cs index 57ee498cfc..60beb94dfa 100644 --- a/Modules/Tokens/src/Tokens.Application/Infrastructure/Persistence/Repository/ITokensRepository.cs +++ b/Modules/Tokens/src/Tokens.Application/Infrastructure/Persistence/Repository/ITokensRepository.cs @@ -14,7 +14,10 @@ public interface ITokensRepository Task> FindTokens(IEnumerable queryItems, IdentityAddress activeIdentity, PaginationFilter paginationFilter, CancellationToken cancellationToken, bool track = false); + Task> FindTokens(Expression> filter, CancellationToken cancellationToken, bool track = false); + Task Find(TokenId tokenId, IdentityAddress? activeIdentity, CancellationToken cancellationToken, bool track = false); + Task Update(IEnumerable tokens, CancellationToken cancellationToken); Task DeleteTokens(Expression> filter, CancellationToken cancellationToken); Task DeleteToken(Token token, CancellationToken cancellationToken); } diff --git a/Modules/Tokens/src/Tokens.Application/Tokens/Commands/AnonymizeTokensForIdentity/AnonymizeTokensForIdentityCommand.cs b/Modules/Tokens/src/Tokens.Application/Tokens/Commands/AnonymizeTokensForIdentity/AnonymizeTokensForIdentityCommand.cs new file mode 100644 index 0000000000..ed7cb1109c --- /dev/null +++ b/Modules/Tokens/src/Tokens.Application/Tokens/Commands/AnonymizeTokensForIdentity/AnonymizeTokensForIdentityCommand.cs @@ -0,0 +1,13 @@ +using MediatR; + +namespace Backbone.Modules.Tokens.Application.Tokens.Commands.AnonymizeTokensForIdentity; + +public class AnonymizeTokensForIdentityCommand : IRequest +{ + public AnonymizeTokensForIdentityCommand(string identityAddress) + { + IdentityAddress = identityAddress; + } + + public string IdentityAddress { get; set; } +} diff --git a/Modules/Tokens/src/Tokens.Application/Tokens/Commands/AnonymizeTokensForIdentity/Handler.cs b/Modules/Tokens/src/Tokens.Application/Tokens/Commands/AnonymizeTokensForIdentity/Handler.cs new file mode 100644 index 0000000000..6cb30e44b2 --- /dev/null +++ b/Modules/Tokens/src/Tokens.Application/Tokens/Commands/AnonymizeTokensForIdentity/Handler.cs @@ -0,0 +1,29 @@ +using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Tokens.Application.Infrastructure.Persistence.Repository; +using Backbone.Modules.Tokens.Domain.Entities; +using MediatR; +using Microsoft.Extensions.Options; + +namespace Backbone.Modules.Tokens.Application.Tokens.Commands.AnonymizeTokensForIdentity; + +public class Handler : IRequestHandler +{ + private readonly ITokensRepository _tokensRepository; + private readonly ApplicationOptions _applicationOptions; + + public Handler(ITokensRepository tokensRepository, IOptions applicationOptions) + { + _tokensRepository = tokensRepository; + _applicationOptions = applicationOptions.Value; + } + + public async Task Handle(AnonymizeTokensForIdentityCommand request, CancellationToken cancellationToken) + { + var tokens = (await _tokensRepository.FindTokens(Token.IsFor(IdentityAddress.Parse(request.IdentityAddress)), cancellationToken)).ToList(); + + foreach (var token in tokens) + token.AnonymizeForIdentity(_applicationOptions.DidDomainName); + + await _tokensRepository.Update(tokens, cancellationToken); + } +} diff --git a/Modules/Tokens/src/Tokens.Application/Tokens/Commands/AnonymizeTokensForIdentity/Validator.cs b/Modules/Tokens/src/Tokens.Application/Tokens/Commands/AnonymizeTokensForIdentity/Validator.cs new file mode 100644 index 0000000000..f3908cc542 --- /dev/null +++ b/Modules/Tokens/src/Tokens.Application/Tokens/Commands/AnonymizeTokensForIdentity/Validator.cs @@ -0,0 +1,14 @@ +using Backbone.BuildingBlocks.Application.Extensions; +using Backbone.DevelopmentKit.Identity.ValueObjects; +using FluentValidation; + +namespace Backbone.Modules.Tokens.Application.Tokens.Commands.AnonymizeTokensForIdentity; + +public class Validator : AbstractValidator +{ + public Validator() + { + RuleFor(c => c.IdentityAddress) + .ValidId(); + } +} diff --git a/Modules/Tokens/src/Tokens.ConsumerApi/Controllers/TokensController.cs b/Modules/Tokens/src/Tokens.ConsumerApi/Controllers/TokensController.cs index 8e037fa8bc..c4c2b766ac 100644 --- a/Modules/Tokens/src/Tokens.ConsumerApi/Controllers/TokensController.cs +++ b/Modules/Tokens/src/Tokens.ConsumerApi/Controllers/TokensController.cs @@ -24,12 +24,10 @@ namespace Backbone.Modules.Tokens.ConsumerApi.Controllers; public class TokensController : ApiControllerBase { private readonly ApplicationOptions _options; - private readonly JsonSerializerOptions _jsonSerializerOptions; - public TokensController(IMediator mediator, IOptions options, IOptions jsonOptions) : base(mediator) + public TokensController(IMediator mediator, IOptions options) : base(mediator) { _options = options.Value; - _jsonSerializerOptions = jsonOptions.Value.JsonSerializerOptions; } [HttpPost] @@ -52,28 +50,14 @@ public async Task GetToken([FromRoute] string id, [FromQuery] byt [HttpGet] [ProducesResponseType(typeof(PagedHttpResponseEnvelope), StatusCodes.Status200OK)] - public async Task ListTokens([FromQuery] PaginationFilter paginationFilter, [FromQuery] string? tokens, + public async Task ListTokens([FromQuery] PaginationFilter paginationFilter, [FromQuery] ListTokensQueryItem[]? tokens, [FromQuery] IEnumerable ids, CancellationToken cancellationToken) { - List? tokenQueryItems; + // We keep this code for backwards compatibility reasons. In a few months the `templates` + // parameter will become required, and the fallback to `ids` will be removed. + tokens = tokens is { Length: > 0 } ? tokens : ids.Select(id => new ListTokensQueryItem { Id = id }).ToArray(); - if (tokens != null) - { - try - { - tokenQueryItems = JsonSerializer.Deserialize>(tokens, _jsonSerializerOptions); - } - catch (JsonException ex) - { - throw new ApplicationException(GenericApplicationErrors.Validation.InputCannotBeParsed(ex.Message)); - } - } - else - { - tokenQueryItems = ids.Select(id => new ListTokensQueryItem { Id = id }).ToList(); - } - - var request = new ListTokensQuery(paginationFilter, tokenQueryItems); + var request = new ListTokensQuery(paginationFilter, tokens); paginationFilter.PageSize ??= _options.Pagination.DefaultPageSize; diff --git a/Modules/Tokens/src/Tokens.Domain/DomainErrors.cs b/Modules/Tokens/src/Tokens.Domain/DomainErrors.cs new file mode 100644 index 0000000000..3498ca67bf --- /dev/null +++ b/Modules/Tokens/src/Tokens.Domain/DomainErrors.cs @@ -0,0 +1,12 @@ +using Backbone.BuildingBlocks.Domain.Errors; + +namespace Backbone.Modules.Tokens.Domain; + +public class DomainErrors +{ + public static DomainError TokenNotPersonalized() + { + return new DomainError("error.platform.validation.token.tokenNotPersonalized", + "The token has to be personalized."); + } +} diff --git a/Modules/Tokens/src/Tokens.Domain/Entities/Token.cs b/Modules/Tokens/src/Tokens.Domain/Entities/Token.cs index dc63d63e34..b347d57fc4 100644 --- a/Modules/Tokens/src/Tokens.Domain/Entities/Token.cs +++ b/Modules/Tokens/src/Tokens.Domain/Entities/Token.cs @@ -43,7 +43,7 @@ public Token(IdentityAddress createdBy, DeviceId createdByDevice, byte[] content public IdentityAddress CreatedBy { get; set; } public DeviceId CreatedByDevice { get; set; } - public IdentityAddress? ForIdentity { get; set; } + public IdentityAddress? ForIdentity { get; private set; } public byte[]? Password { get; set; } public byte[] Content { get; private set; } @@ -58,11 +58,25 @@ public bool CanBeCollectedUsingPassword(IdentityAddress? address, byte[]? passwo CreatedBy == address; // The owner shouldn't need a password to get the template } + public void AnonymizeForIdentity(string didDomainName) + { + EnsureIsPersonalized(); + + var anonymousIdentity = IdentityAddress.GetAnonymized(didDomainName); + + ForIdentity = anonymousIdentity; + } + public void EnsureCanBeDeletedBy(IdentityAddress identityAddress) { if (CreatedBy != identityAddress) throw new DomainActionForbiddenException(); } + public void EnsureIsPersonalized() + { + if (ForIdentity == null) throw new DomainException(DomainErrors.TokenNotPersonalized()); + } + #region Expressions public static Expression> IsNotExpired => @@ -91,5 +105,10 @@ public static Expression> CanBeCollectedWithPassword(IdentityA token.CreatedBy == address; // The owner shouldn't need a password to get the template } + public static Expression> IsFor(IdentityAddress identityAddress) + { + return token => token.ForIdentity == identityAddress; + } + #endregion } diff --git a/Modules/Tokens/src/Tokens.Infrastructure/Persistence/Repository/TokensRepository.cs b/Modules/Tokens/src/Tokens.Infrastructure/Persistence/Repository/TokensRepository.cs index 3bb69076b3..5a6ee192c3 100644 --- a/Modules/Tokens/src/Tokens.Infrastructure/Persistence/Repository/TokensRepository.cs +++ b/Modules/Tokens/src/Tokens.Infrastructure/Persistence/Repository/TokensRepository.cs @@ -48,6 +48,13 @@ public async Task> FindTokens(IEnumerable> FindTokens(Expression> filter, CancellationToken cancellationToken, bool track = false) + { + return await (track ? _tokensDbSet : _readonlyTokensDbSet) + .Where(filter) + .ToListAsync(cancellationToken); + } + public async Task Find(TokenId id, IdentityAddress? activeIdentity, CancellationToken cancellationToken, bool track = false) { var token = await _readonlyTokensDbSet @@ -86,6 +93,12 @@ public async Task Add(Token token) await _dbContext.SaveChangesAsync(); } + public async Task Update(IEnumerable tokens, CancellationToken cancellationToken) + { + _tokensDbSet.UpdateRange(tokens); + await _dbContext.SaveChangesAsync(cancellationToken); + } + public async Task DeleteTokens(Expression> filter, CancellationToken cancellationToken) { await _tokensDbSet.Where(filter).ExecuteDeleteAsync(cancellationToken); diff --git a/Modules/Tokens/test/Tokens.Domain.Tests/Tests/Token.AnonymizeForIdentityTests.cs b/Modules/Tokens/test/Tokens.Domain.Tests/Tests/Token.AnonymizeForIdentityTests.cs new file mode 100644 index 0000000000..6159567c0c --- /dev/null +++ b/Modules/Tokens/test/Tokens.Domain.Tests/Tests/Token.AnonymizeForIdentityTests.cs @@ -0,0 +1,46 @@ +using Backbone.BuildingBlocks.Domain.Exceptions; +using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Tokens.Domain.Entities; +using Backbone.UnitTestTools.Extensions; + +namespace Backbone.Modules.Tokens.Domain.Tests.Tests; + +public class TokenAnonymizeForIdentityTests : AbstractTestsBase +{ + private const string DID_DOMAIN_NAME = "localhost"; + + [Fact] + public void Personalized_token_can_be_anonymized() + { + // Arrange + var creatorIdentityAddress = CreateRandomIdentityAddress(); + var forIdentityAddress = CreateRandomIdentityAddress(); + var deviceId = CreateRandomDeviceId(); + byte[] content = [1, 1, 1, 1, 1, 1, 1, 1]; + var expiresAt = _dateTimeTomorrow; + var relationshipTemplate = new Token(creatorIdentityAddress, deviceId, content, expiresAt, forIdentityAddress); + + // Act + relationshipTemplate.AnonymizeForIdentity(DID_DOMAIN_NAME); + + // Assert + relationshipTemplate.ForIdentity.Should().Be(IdentityAddress.GetAnonymized(DID_DOMAIN_NAME)); + } + + [Fact] + public void Non_personalized_token_can_not_be_anonymized() + { + // Arrange + var creatorIdentityAddress = CreateRandomIdentityAddress(); + var deviceId = CreateRandomDeviceId(); + byte[] content = [1, 1, 1, 1, 1, 1, 1, 1]; + var expiresAt = _dateTimeTomorrow; + var relationshipTemplate = new Token(creatorIdentityAddress, deviceId, content, expiresAt); + + // Act + var acting = () => relationshipTemplate.AnonymizeForIdentity(DID_DOMAIN_NAME); + + // Assert + acting.Should().Throw().WithError("error.platform.validation.token.tokenNotPersonalized"); + } +} diff --git a/Sdks/AdminApi.Sdk/src/Client.cs b/Sdks/AdminApi.Sdk/src/Client.cs index c961e3a593..161d1d7dd2 100644 --- a/Sdks/AdminApi.Sdk/src/Client.cs +++ b/Sdks/AdminApi.Sdk/src/Client.cs @@ -1,10 +1,10 @@ using System.Text.Json; using Backbone.AdminApi.Sdk.Authentication; +using Backbone.AdminApi.Sdk.Endpoints.Announcements; using Backbone.AdminApi.Sdk.Endpoints.ApiKeyValidation; using Backbone.AdminApi.Sdk.Endpoints.Challenges; using Backbone.AdminApi.Sdk.Endpoints.Clients; using Backbone.AdminApi.Sdk.Endpoints.Identities; -using Backbone.AdminApi.Sdk.Endpoints.Logs; using Backbone.AdminApi.Sdk.Endpoints.Messages; using Backbone.AdminApi.Sdk.Endpoints.Metrics; using Backbone.AdminApi.Sdk.Endpoints.Relationships; @@ -26,9 +26,9 @@ private Client(HttpClient httpClient, string apiKey) var endpointClient = new EndpointClient(httpClient, authenticator, jsonSerializerOptions); ApiKeyValidation = new ApiKeyValidationEndpoint(endpointClient); + Announcements = new AnnouncementsEndpoint(endpointClient); Clients = new ClientsEndpoint(endpointClient); Identities = new IdentitiesEndpoint(endpointClient); - Logs = new LogsEndpoint(endpointClient); Metrics = new MetricsEndpoint(endpointClient); Relationships = new RelationshipsEndpoint(endpointClient); Tiers = new TiersEndpoint(endpointClient); @@ -37,9 +37,9 @@ private Client(HttpClient httpClient, string apiKey) } public ApiKeyValidationEndpoint ApiKeyValidation { get; } + public AnnouncementsEndpoint Announcements { get; } public ClientsEndpoint Clients { get; } public IdentitiesEndpoint Identities { get; } - public LogsEndpoint Logs { get; } public MetricsEndpoint Metrics { get; } public RelationshipsEndpoint Relationships { get; } public TiersEndpoint Tiers { get; } diff --git a/Sdks/AdminApi.Sdk/src/Endpoints/Announcements/AnnouncementsEndpoint.cs b/Sdks/AdminApi.Sdk/src/Endpoints/Announcements/AnnouncementsEndpoint.cs new file mode 100644 index 0000000000..d1c14c8c8e --- /dev/null +++ b/Sdks/AdminApi.Sdk/src/Endpoints/Announcements/AnnouncementsEndpoint.cs @@ -0,0 +1,20 @@ +using Backbone.AdminApi.Sdk.Endpoints.Announcements.Types; +using Backbone.AdminApi.Sdk.Endpoints.Announcements.Types.Requests; +using Backbone.AdminApi.Sdk.Endpoints.Announcements.Types.Responses; +using Backbone.BuildingBlocks.SDK.Endpoints.Common; +using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types; + +namespace Backbone.AdminApi.Sdk.Endpoints.Announcements; + +public class AnnouncementsEndpoint(EndpointClient client) : AdminApiEndpoint(client) +{ + public async Task> CreateAnnouncement(CreateAnnouncementRequest request) + { + return await _client.Post($"api/{API_VERSION}/Announcements", request); + } + + public async Task> GetAllAnnouncements() + { + return await _client.Get($"api/{API_VERSION}/Announcements"); + } +} diff --git a/Sdks/AdminApi.Sdk/src/Endpoints/Announcements/Types/Announcement.cs b/Sdks/AdminApi.Sdk/src/Endpoints/Announcements/Types/Announcement.cs new file mode 100644 index 0000000000..fbc4b62952 --- /dev/null +++ b/Sdks/AdminApi.Sdk/src/Endpoints/Announcements/Types/Announcement.cs @@ -0,0 +1,29 @@ +using System.Text.Json.Serialization; + +namespace Backbone.AdminApi.Sdk.Endpoints.Announcements.Types; + +public class Announcement +{ + public required string Id { get; set; } + public required DateTime CreatedAt { get; set; } + public required DateTime? ExpiresAt { get; set; } + + [JsonConverter(typeof(JsonStringEnumConverter))] + public required AnnouncementSeverity Severity { get; set; } + + public required IEnumerable Texts { get; set; } +} + +public class AnnouncementText +{ + public required string Language { get; set; } + public required string Title { get; set; } + public required string Body { get; set; } +} + +public enum AnnouncementSeverity +{ + Low, + Medium, + High +} diff --git a/Sdks/AdminApi.Sdk/src/Endpoints/Announcements/Types/Requests/CreateAnnouncementRequest.cs b/Sdks/AdminApi.Sdk/src/Endpoints/Announcements/Types/Requests/CreateAnnouncementRequest.cs new file mode 100644 index 0000000000..78408298af --- /dev/null +++ b/Sdks/AdminApi.Sdk/src/Endpoints/Announcements/Types/Requests/CreateAnnouncementRequest.cs @@ -0,0 +1,15 @@ +namespace Backbone.AdminApi.Sdk.Endpoints.Announcements.Types.Requests; + +public class CreateAnnouncementRequest +{ + public required AnnouncementSeverity Severity { get; set; } + public required List Texts { get; set; } + public DateTime? ExpiresAt { get; set; } +} + +public class CreateAnnouncementRequestText +{ + public required string Language { get; set; } + public required string Title { get; set; } + public required string Body { get; set; } +} diff --git a/Sdks/AdminApi.Sdk/src/Endpoints/Announcements/Types/Responses/GetAllAnnouncementsResponse.cs b/Sdks/AdminApi.Sdk/src/Endpoints/Announcements/Types/Responses/GetAllAnnouncementsResponse.cs new file mode 100644 index 0000000000..8ec3fc4e70 --- /dev/null +++ b/Sdks/AdminApi.Sdk/src/Endpoints/Announcements/Types/Responses/GetAllAnnouncementsResponse.cs @@ -0,0 +1,5 @@ +using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types; + +namespace Backbone.AdminApi.Sdk.Endpoints.Announcements.Types.Responses; + +public class GetAllAnnouncementsResponse : EnumerableResponseBase; diff --git a/Sdks/AdminApi.Sdk/src/Endpoints/Logs/LogsEndpoint.cs b/Sdks/AdminApi.Sdk/src/Endpoints/Logs/LogsEndpoint.cs deleted file mode 100644 index 734921ec31..0000000000 --- a/Sdks/AdminApi.Sdk/src/Endpoints/Logs/LogsEndpoint.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Backbone.AdminApi.Sdk.Endpoints.Logs.Types.Requests; -using Backbone.BuildingBlocks.SDK.Endpoints.Common; -using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types; - -namespace Backbone.AdminApi.Sdk.Endpoints.Logs; - -public class LogsEndpoint(EndpointClient client) : AdminApiEndpoint(client) -{ - public async Task> CreateLog(LogRequest request) - { - return await _client.Post($"api/{API_VERSION}/Logs", request); - } -} diff --git a/Sdks/AdminApi.Sdk/src/Endpoints/Logs/Types/Requests/LogRequest.cs b/Sdks/AdminApi.Sdk/src/Endpoints/Logs/Types/Requests/LogRequest.cs deleted file mode 100644 index 566c3103df..0000000000 --- a/Sdks/AdminApi.Sdk/src/Endpoints/Logs/Types/Requests/LogRequest.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Backbone.AdminApi.Sdk.Endpoints.Logs.Types.Requests; - -public class LogRequest -{ - public required LogLevel LogLevel { get; set; } - public required string Category { get; set; } - public required string MessageTemplate { get; set; } - public object[] Arguments { get; set; } = []; -} - -public enum LogLevel -{ - Trace, - Debug, - Information, - Log, - Warning, - Error, - Critical -} diff --git a/Sdks/ConsumerApi.Sdk/src/Client.cs b/Sdks/ConsumerApi.Sdk/src/Client.cs index 3a3b9de046..d9dd5abcbe 100644 --- a/Sdks/ConsumerApi.Sdk/src/Client.cs +++ b/Sdks/ConsumerApi.Sdk/src/Client.cs @@ -2,6 +2,7 @@ using Backbone.BuildingBlocks.SDK.Crypto; using Backbone.BuildingBlocks.SDK.Endpoints.Common; using Backbone.ConsumerApi.Sdk.Authentication; +using Backbone.ConsumerApi.Sdk.Endpoints.Announcements; using Backbone.ConsumerApi.Sdk.Endpoints.Challenges; using Backbone.ConsumerApi.Sdk.Endpoints.Datawallets; using Backbone.ConsumerApi.Sdk.Endpoints.Devices; @@ -39,6 +40,7 @@ private Client(HttpClient httpClient, Configuration configuration, DeviceData? d DeviceData = deviceData; IdentityData = identityData; + Announcements = new AnnouncementsEndpoint(endpointClient); Challenges = new ChallengesEndpoint(endpointClient); Datawallet = new DatawalletEndpoint(endpointClient); Devices = new DevicesEndpoint(endpointClient); @@ -58,6 +60,7 @@ private Client(HttpClient httpClient, Configuration configuration, DeviceData? d public IdentityData? IdentityData { get; } // ReSharper disable UnusedAutoPropertyAccessor.Global + public AnnouncementsEndpoint Announcements { get; } public ChallengesEndpoint Challenges { get; } public DatawalletEndpoint Datawallet { get; } public DevicesEndpoint Devices { get; } @@ -200,6 +203,16 @@ public static async Task CreateForNewIdentity(HttpClient httpClient, Cli } public async Task OnboardNewDevice(string password) + { + return await OnboardNewDevice(password, false); + } + + public async Task OnboardNewBackupDevice(string password) + { + return await OnboardNewDevice(password, true); + } + + private async Task OnboardNewDevice(string password, bool isBackupDevice) { if (DeviceData == null) throw new Exception("The device data is missing. This is probably because you're using an unauthenticated client. In order to onboard a new device, the client needs to be authenticated."); @@ -209,7 +222,8 @@ public async Task OnboardNewDevice(string password) var createDeviceResponse = await Devices.RegisterDevice(new RegisterDeviceRequest { DevicePassword = password, - SignedChallenge = signedChallenge + SignedChallenge = signedChallenge, + IsBackupDevice = isBackupDevice }); if (createDeviceResponse.IsError) diff --git a/Sdks/ConsumerApi.Sdk/src/Endpoints/Announcements/AnnouncementsEndpoint.cs b/Sdks/ConsumerApi.Sdk/src/Endpoints/Announcements/AnnouncementsEndpoint.cs new file mode 100644 index 0000000000..13d9bedfa2 --- /dev/null +++ b/Sdks/ConsumerApi.Sdk/src/Endpoints/Announcements/AnnouncementsEndpoint.cs @@ -0,0 +1,21 @@ +using Backbone.BuildingBlocks.SDK.Endpoints.Common; +using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types; +using Backbone.ConsumerApi.Sdk.Endpoints.Announcements.Types.Responses; + +namespace Backbone.ConsumerApi.Sdk.Endpoints.Announcements; + +public class AnnouncementsEndpoint : ConsumerApiEndpoint +{ + public AnnouncementsEndpoint(EndpointClient client) : base(client) + { + } + + public async Task> ListAnnouncements(string language, PaginationFilter? pagination = null) + { + return await _client.Request(HttpMethod.Get, $"api/{API_VERSION}/Announcements") + .Authenticate() + .WithPagination(pagination) + .AddQueryParameter("language", language) + .Execute(); + } +} diff --git a/Sdks/ConsumerApi.Sdk/src/Endpoints/Announcements/Types/Announcement.cs b/Sdks/ConsumerApi.Sdk/src/Endpoints/Announcements/Types/Announcement.cs new file mode 100644 index 0000000000..3fa2fe849c --- /dev/null +++ b/Sdks/ConsumerApi.Sdk/src/Endpoints/Announcements/Types/Announcement.cs @@ -0,0 +1,29 @@ +using System.Text.Json.Serialization; + +namespace Backbone.ConsumerApi.Sdk.Endpoints.Announcements.Types; + +public class Announcement +{ + public required string Id { get; set; } + public required DateTime CreatedAt { get; set; } + public DateTime? ExpiresAt { get; set; } + + [JsonConverter(typeof(JsonStringEnumConverter))] + public required AnnouncementSeverity Severity { get; set; } + + public required AnnouncementText Text { get; set; } +} + +public class AnnouncementText +{ + public required string Language { get; set; } + public required string Title { get; set; } + public required string Body { get; set; } +} + +public enum AnnouncementSeverity +{ + Low, + Medium, + High +} diff --git a/Sdks/ConsumerApi.Sdk/src/Endpoints/Announcements/Types/Responses/ListMessagesResponse.cs b/Sdks/ConsumerApi.Sdk/src/Endpoints/Announcements/Types/Responses/ListMessagesResponse.cs new file mode 100644 index 0000000000..e9fa8ae830 --- /dev/null +++ b/Sdks/ConsumerApi.Sdk/src/Endpoints/Announcements/Types/Responses/ListMessagesResponse.cs @@ -0,0 +1,5 @@ +using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types; + +namespace Backbone.ConsumerApi.Sdk.Endpoints.Announcements.Types.Responses; + +public class ListAnnouncementsResponse : EnumerableResponseBase; diff --git a/Sdks/ConsumerApi.Sdk/src/Endpoints/Devices/Types/Device.cs b/Sdks/ConsumerApi.Sdk/src/Endpoints/Devices/Types/Device.cs index e1256eb8bc..d2cd517924 100644 --- a/Sdks/ConsumerApi.Sdk/src/Endpoints/Devices/Types/Device.cs +++ b/Sdks/ConsumerApi.Sdk/src/Endpoints/Devices/Types/Device.cs @@ -8,6 +8,7 @@ public class Device public required string CreatedByDevice { get; set; } public required LastLoginInformation LastLogin { get; set; } public required string CommunicationLanguage { get; set; } + public required bool IsBackupDevice { get; set; } } public class LastLoginInformation diff --git a/Sdks/ConsumerApi.Sdk/src/Endpoints/Devices/Types/Requests/RegisterDeviceRequest.cs b/Sdks/ConsumerApi.Sdk/src/Endpoints/Devices/Types/Requests/RegisterDeviceRequest.cs index b12e2db881..bf7dd9b1bb 100644 --- a/Sdks/ConsumerApi.Sdk/src/Endpoints/Devices/Types/Requests/RegisterDeviceRequest.cs +++ b/Sdks/ConsumerApi.Sdk/src/Endpoints/Devices/Types/Requests/RegisterDeviceRequest.cs @@ -4,4 +4,6 @@ public class RegisterDeviceRequest { public required string DevicePassword { get; set; } public required SignedChallenge SignedChallenge { get; set; } + public string? CommunicationLanguage { get; set; } = null; + public required bool IsBackupDevice { get; set; } } diff --git a/Sdks/ConsumerApi.Sdk/src/Endpoints/Devices/Types/Responses/RegisterDeviceResponse.cs b/Sdks/ConsumerApi.Sdk/src/Endpoints/Devices/Types/Responses/RegisterDeviceResponse.cs index fb3d2585d0..7c7f41622a 100644 --- a/Sdks/ConsumerApi.Sdk/src/Endpoints/Devices/Types/Responses/RegisterDeviceResponse.cs +++ b/Sdks/ConsumerApi.Sdk/src/Endpoints/Devices/Types/Responses/RegisterDeviceResponse.cs @@ -6,4 +6,5 @@ public class RegisterDeviceResponse public required string Username { get; set; } public required DateTime CreatedAt { get; set; } public required string CreatedByDevice { get; set; } + public required bool IsBackupDevice { get; set; } } diff --git a/Sdks/ConsumerApi.Sdk/src/Endpoints/Identities/IdentitiesEndpoint.cs b/Sdks/ConsumerApi.Sdk/src/Endpoints/Identities/IdentitiesEndpoint.cs index 59271de8d4..626b200a20 100644 --- a/Sdks/ConsumerApi.Sdk/src/Endpoints/Identities/IdentitiesEndpoint.cs +++ b/Sdks/ConsumerApi.Sdk/src/Endpoints/Identities/IdentitiesEndpoint.cs @@ -1,4 +1,5 @@ -using Backbone.BuildingBlocks.SDK.Endpoints.Common; +using System.Collections.Specialized; +using Backbone.BuildingBlocks.SDK.Endpoints.Common; using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types; using Backbone.ConsumerApi.Sdk.Endpoints.Identities.Types; using Backbone.ConsumerApi.Sdk.Endpoints.Identities.Types.Requests; @@ -37,4 +38,9 @@ public async Task> CancelDeletionProc { return await _client.Put($"api/{API_VERSION}/Identities/Self/DeletionProcesses/{id}/Cancel"); } + + public async Task> IsDeleted(string username) + { + return await _client.GetUnauthenticated($"api/{API_VERSION}/Identities/IsDeleted", new NameValueCollection { { "username", username } }); + } } diff --git a/Sdks/ConsumerApi.Sdk/src/Endpoints/Identities/Types/Responses/IsDeletedResponse.cs b/Sdks/ConsumerApi.Sdk/src/Endpoints/Identities/Types/Responses/IsDeletedResponse.cs new file mode 100644 index 0000000000..5e41a153bf --- /dev/null +++ b/Sdks/ConsumerApi.Sdk/src/Endpoints/Identities/Types/Responses/IsDeletedResponse.cs @@ -0,0 +1,7 @@ +namespace Backbone.ConsumerApi.Sdk.Endpoints.Identities.Types.Responses; + +public class IsDeletedResponse +{ + public bool IsDeleted { get; set; } + public DateTime? DeletionDate { get; set; } +} diff --git a/Sdks/ConsumerApi.Sdk/src/Endpoints/Tokens/TokensEndpoint.cs b/Sdks/ConsumerApi.Sdk/src/Endpoints/Tokens/TokensEndpoint.cs index d36f871584..47729d3b34 100644 --- a/Sdks/ConsumerApi.Sdk/src/Endpoints/Tokens/TokensEndpoint.cs +++ b/Sdks/ConsumerApi.Sdk/src/Endpoints/Tokens/TokensEndpoint.cs @@ -25,12 +25,23 @@ public async Task> ListTokens(PaginationFilter? public async Task> ListTokens(IEnumerable queryItems, PaginationFilter? pagination = null) { - return await _client + var request = _client .Request(HttpMethod.Get, $"api/{API_VERSION}/Tokens") .Authenticate() - .WithPagination(pagination) - .AddQueryParameter("tokens", queryItems) - .Execute(); + .WithPagination(pagination); + + var i = 0; + foreach (var queryItem in queryItems) + { + request.AddQueryParameter($"tokens.{i}.id", queryItem.Id); + + if (queryItem.Password != null) + request.AddQueryParameter($"tokens.{i}.password", queryItem.Password); + + i++; + } + + return await request.Execute(); } public async Task> GetTokenUnauthenticated(string id) diff --git a/appsettings.override.json b/appsettings.override.json index 8b0db92412..7d623edf05 100644 --- a/appsettings.override.json +++ b/appsettings.override.json @@ -10,10 +10,6 @@ "ExposedHeaders": "", "AccessControlAllowCredentials": true }, - "SwaggerUi": { - "TokenUrl": "http://localhost:5000/connect/token", - "Enabled": true - }, "Infrastructure": { "EventBus": { "Vendor": "RabbitMQ", // possible values: InMemory, RabbitMQ, GoogleCloud, Azure @@ -34,6 +30,15 @@ } }, "Modules": { + "Announcements": { + "Infrastructure": { + "SqlDatabase": { + "Provider": "Postgres", + "ConnectionString": "User ID=postgres;Password=admin;Server=localhost;Port=5432;Database=enmeshed;" // postgres + // "ConnectionString": "Server=localhost;Database=enmeshed;User Id=sa;Password=Passw0rd;TrustServerCertificate=True" // sqlserver + } + } + }, "Challenges": { "Infrastructure": { "SqlDatabase": { @@ -155,10 +160,7 @@ }, "Tags": { "Application": { - "SupportedLanguages": [ - "de", - "en" - ], + "SupportedLanguages": ["de", "en"], "TagsForAttributeValueTypes": { "IdentityFileReference": { "schulabschluss": { @@ -224,6 +226,9 @@ } }, "Tokens": { + "Application": { + "DidDomainName": "localhost" + }, "Infrastructure": { "SqlDatabase": { "Provider": "Postgres", diff --git a/docker-compose/adminui.appsettings.override.json b/docker-compose/adminui.appsettings.override.json index fe96ab1915..671e472318 100644 --- a/docker-compose/adminui.appsettings.override.json +++ b/docker-compose/adminui.appsettings.override.json @@ -7,9 +7,6 @@ "ExposedHeaders": "", "AccessControlAllowCredentials": true }, - "SwaggerUi": { - "Enabled": true - }, "Infrastructure": { "EventBus": { "Vendor": "RabbitMQ", // possible values: InMemory, RabbitMQ, GoogleCloud, Azure diff --git a/docker-compose/appsettings.override.json b/docker-compose/appsettings.override.json index 5d756619b6..2cc6146066 100644 --- a/docker-compose/appsettings.override.json +++ b/docker-compose/appsettings.override.json @@ -9,10 +9,6 @@ "ExposedHeaders": "", "AccessControlAllowCredentials": true }, - "SwaggerUi": { - "TokenUrl": "http://localhost:8082/connect/token", - "Enabled": true - }, "Infrastructure": { "EventBus": { "Vendor": "RabbitMQ", // possible values: InMemory, RabbitMQ, GoogleCloud, Azure diff --git a/helm/templates/consumerapi/httproute.yaml b/helm/templates/consumerapi/httproute.yaml index d59b0da7c2..d662563720 100644 --- a/helm/templates/consumerapi/httproute.yaml +++ b/helm/templates/consumerapi/httproute.yaml @@ -10,9 +10,11 @@ spec: - name: {{ .Values.consumerapi.httpRoute.parentRefName }} namespace: {{ .Values.consumerapi.httpRoute.parentRefNamespace }} sectionName: {{ .Values.consumerapi.httpRoute.parentRefSectionName }} - {{- with .Values.consumerapi.httpRoute.hostnames }} hostnames: - {{- toYaml . | nindent 4 }} + {{- if .Values.consumerapi.httpRoute.hostnamesOverride }} + {{- toYaml .Values.consumerapi.httpRoute.hostnamesOverride | nindent 4 }} + {{- else }} + - {{ .Values.global.defaultHostname }} {{- end }} rules: - matches: diff --git a/helm/templates/sseserver/httproute.yaml b/helm/templates/sseserver/httproute.yaml index ddeb03b630..713ffc33ee 100644 --- a/helm/templates/sseserver/httproute.yaml +++ b/helm/templates/sseserver/httproute.yaml @@ -11,9 +11,11 @@ spec: - name: {{ .Values.sseserver.httpRoute.parentRefName }} namespace: {{ .Values.sseserver.httpRoute.parentRefNamespace }} sectionName: {{ .Values.sseserver.httpRoute.parentRefSectionName }} - {{- with .Values.sseserver.httpRoute.hostnames }} hostnames: - {{- toYaml . | nindent 4 }} + {{- if .Values.consumerapi.httpRoute.hostnamesOverride }} + {{- toYaml .Values.consumerapi.httpRoute.hostnamesOverride | nindent 4 }} + {{- else }} + - {{ .Values.global.defaultHostname }} {{- end }} rules: - matches: diff --git a/helm/values.yaml b/helm/values.yaml index 4e9f0c4cb2..01b06f93d7 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -90,13 +90,8 @@ adminui: podAnnotations: {} # env - environment variables for the Admin UI container (see https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container/) - env: - - name: Database__ConnectionString - valueFrom: - secretKeyRef: - name: "devices-sql-connectionstring" - key: VALUE - + env: [] + # the nodeSelector for the pods deployed by the Deployment (see https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector) nodeSelector: {} @@ -113,28 +108,6 @@ adminui: targetCPUUtilizationPercentage: 80 targetMemoryUtilizationPercentage: "" - # httpRoute - the configuration of the HttpRoute for the Consumer API - httpRoute: - # enabled - whether to enable the HttpRoute for the Consumer API - enabled: false - # parentRefName - the name of the gateway this Route wants to be attached to - parentRefName: "" - # parentRefNamespace - # the namespace of the gateway this Route wants to be attached to - parentRefNamespace: "" - hostnames: [] - - # backendConfig - only applicable if .Values.global.provider is set to "GoogleCloud"; see https://cloud.google.com/kubernetes-engine/docs/how-to/ingress-configuration#direct_health for a description of `HttpRoute`s` - backendConfig: - healthCheck: - # checkIntervalSec - time from the start of one prober's check to the start of its next check - checkIntervalSec: 15 - # timeoutSec - the amount of time that Google Cloud waits for a response to a probe - timeoutSec: 15 - # healthyThreshold - the number of consecutive successful checks required to mark a backend as healthy - healthyThreshold: 1 - # unhealthyThreshold - the number of consecutive failed checks required to mark a backend as unhealthy - unhealthyThreshold: 2 - #=========================== Consumer API =========================== consumerapi: @@ -199,8 +172,8 @@ consumerapi: parentRefNamespace: "" # parentRefSectionName - the name of the section in the gateway this Route wants to be attached to parentRefSectionName: "" - # hostnames - the hostnames the Consumer API should be reachable under - hostnames: [] + # hostnamesOverride - the hostnames the Consumer API should be reachable under; if empty, the `.global.defaultHostname` property is used + hostnamesOverride: [] ingress: # enabled - whether to enable the Ingress for the Consumer API @@ -225,7 +198,7 @@ consumerapi: targetCPUUtilizationPercentage: 80 targetMemoryUtilizationPercentage: "" - # backendConfig - only applicable if .Values.global.provider is set to "GoogleCloud"; see https://cloud.google.com/kubernetes-engine/docs/how-to/ingress-configuration#direct_health for a description of `HttpRoute`s` + # backendConfig - only applicable if .Values.global.provider is set to "GoogleCloud"; see https://cloud.google.com/kubernetes-engine/docs/how-to/ingress-configuration#direct_health for a description of `BackendConfig`s backendConfig: healthCheck: # checkIntervalSec - time from the start of one prober's check to the start of its next check @@ -523,8 +496,8 @@ sseserver: parentRefNamespace: "" # parentRefSectionName - the name of the section in the gateway this Route wants to be attached to parentRefSectionName: "" - # hostnames - the hostnames the Consumer API should be reachable under - hostnames: [] + # hostnamesOverride - the hostnames the SSE Server should be reachable under; if empty, the `.global.defaultHostname` property is used + hostnamesOverride: [] ingress: # enabled - whether to enable the Ingress for the Consumer API @@ -542,7 +515,7 @@ sseserver: # the affinity for the pods deployed by the Deployment (see https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#NodeAffinity) affinity: {} - # backendConfig - only applicable if .Values.global.provider is set to "GoogleCloud"; see https://cloud.google.com/kubernetes-engine/docs/how-to/ingress-configuration#direct_health for a description of `HttpRoute`s` + # backendConfig - only applicable if .Values.global.provider is set to "GoogleCloud"; see https://cloud.google.com/kubernetes-engine/docs/how-to/ingress-configuration#direct_health for a description of `BackendConfig`s backendConfig: healthCheck: # checkIntervalSec - time from the start of one prober's check to the start of its next check @@ -555,6 +528,9 @@ sseserver: unhealthyThreshold: 2 global: + # defaultHostname - the default hostname the services should be reachable under (this can be overriden in the individual service configurations) + defaultHostname: "" + # useBuiltInEventbus - if a development event bus should be created, set this to true; if you want to use an existing one, set it to false useBuiltInEventbus: false @@ -585,11 +561,6 @@ global: allowedOrigins: "" # allowedMethods - a semicolon-separated list of headers that are exposed to the client; the specified headers are recommended, because they are used by the official Enmeshed app exposedHeaders: "X-Request-Time;X-Response-Time;X-Response-Duration-ms;X-Trace-Id" - swaggerUi: - # enabled - whether the Swagger UI should be enabled - enabled: false - # tokenUrl - the URL from which the Swagger UI can obtain a JWT; it usually follows the pattern "/connect/token" - tokenUrl: "" infrastructure: eventBus: # vendor - possible values: "RabbitMQ"/"Azure"/"GoogleCloud" @@ -614,6 +585,18 @@ global: # connectionString - should be set via environment variable connectionString: "" modules: + announcements: + application: + # pagination - the following options describe the default and maximum page size for the different endpoints + pagination: + defaultPageSize: 50 + maxPageSize: 200 + infrastructure: + sqlDatabase: + # provider - possible values: "Postgres"/"SqlServer" + provider: "" + # connectionString - should be set via environment variable + connectionString: "" challenges: infrastructure: sqlDatabase: @@ -623,8 +606,8 @@ global: connectionString: "" devices: application: - # addressPrefix - the prefix that should be used when generating Identity Addresses; the official Enmeshed App currently only supports "id1" - addressPrefix: id1 + # didDomainName - the didDomainName that should be used when generating Identity Addresses + didDomainName: "" # pagination - the following options describe the default and maximum page size for the different endpoints pagination: defaultPageSize: 50 @@ -691,6 +674,8 @@ global: # containerName: "" messages: application: + # didDomainName - the didDomainName that should be used when generating Identity Addresses + didDomainName: "" # maxNumberOfUnreceivedMessagesFromOneSender - if this number is exceeded, the Consumer API will not accept any more messages from the sender to the recipient in order to prevent spam maxNumberOfUnreceivedMessagesFromOneSender: 20 # pagination - the following options describe the default and maximum page size for the different endpoints @@ -717,6 +702,8 @@ global: connectionString: "" relationships: application: + # didDomainName - the didDomainName that should be used when generating Identity Addresses + didDomainName: "" # pagination - the following options describe the default and maximum page size for the different endpoints pagination: defaultPageSize: 50 @@ -741,6 +728,8 @@ global: connectionString: "" tokens: application: + # didDomainName - the didDomainName that should be used when generating Identity Addresses + didDomainName: "" # pagination - the following options describe the default and maximum page size for the different endpoints pagination: defaultPageSize: 50 diff --git a/scripts/sql/postgres/setup.sql b/scripts/sql/postgres/setup.sql index 90cdcd5e5a..5514b9dcd6 100644 --- a/scripts/sql/postgres/setup.sql +++ b/scripts/sql/postgres/setup.sql @@ -14,6 +14,7 @@ DROP SCHEMA "Tokens" cascade; DROP SCHEMA "Quotas" cascade; */ +CREATE SCHEMA IF NOT EXISTS "Announcements"; CREATE SCHEMA IF NOT EXISTS "Challenges"; CREATE SCHEMA IF NOT EXISTS "Devices"; CREATE SCHEMA IF NOT EXISTS "Files"; @@ -38,6 +39,16 @@ BEGIN END $$; +DO +$$ +BEGIN + IF NOT EXISTS (SELECT usename FROM pg_user WHERE usename = 'announcements') THEN + CREATE USER "announcements" WITH password 'Passw0rd'; + RAISE NOTICE 'User "announcements" created'; + END IF; +END +$$; + DO $$ BEGIN @@ -130,6 +141,7 @@ $$; /*+++++++++++++++++++++++++++++++++++++++++++++++++ Authorizations +++++++++++++++++++++++++++++++++++++++++++++++++*/ +GRANT USAGE ON SCHEMA "Announcements" TO announcements; GRANT USAGE ON SCHEMA "Challenges" TO challenges; GRANT USAGE ON SCHEMA "Devices" TO devices; GRANT USAGE ON SCHEMA "Files" TO files; @@ -140,6 +152,7 @@ GRANT USAGE ON SCHEMA "Synchronization" TO synchronization; GRANT USAGE ON SCHEMA "Tokens" TO tokens; GRANT USAGE ON SCHEMA "AdminUi" TO "adminUi"; +ALTER DEFAULT PRIVILEGES IN SCHEMA "Announcements" GRANT ALL ON TABLES TO announcements; ALTER DEFAULT PRIVILEGES IN SCHEMA "Challenges" GRANT ALL ON TABLES TO challenges; ALTER DEFAULT PRIVILEGES IN SCHEMA "Devices" GRANT ALL ON TABLES TO devices; ALTER DEFAULT PRIVILEGES IN SCHEMA "Files" GRANT ALL ON TABLES TO files; @@ -226,6 +239,13 @@ GRANT USAGE ON SCHEMA "Tokens" TO quotas; GRANT SELECT ON ALL TABLES IN SCHEMA "Tokens" TO quotas; ALTER DEFAULT PRIVILEGES IN SCHEMA "Tokens" GRANT SELECT ON TABLES TO quotas; +CREATE TABLE IF NOT EXISTS "Announcements"."__EFMigrationsHistory" +( + "MigrationId" character varying(150) COLLATE pg_catalog."default" NOT NULL, + "ProductVersion" character varying(32) COLLATE pg_catalog."default" NOT NULL, + CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId") +); +ALTER TABLE IF EXISTS "Announcements"."__EFMigrationsHistory" OWNER to announcements; CREATE TABLE IF NOT EXISTS "Challenges"."__EFMigrationsHistory" ( @@ -301,7 +321,8 @@ ALTER TABLE IF EXISTS "AdminUi"."__EFMigrationsHistory" OWNER to "adminUi"; /*+++++++++++++++++++++++++++++++++++++++++++++++++ Schema Owners ++++++++++++++++++++++++++++++++++++++++++++++++++*/ -GRANT challenges TO "nmshdAdmin";; +GRANT challenges TO "announcements"; +GRANT challenges TO "nmshdAdmin"; GRANT devices TO "nmshdAdmin"; GRANT messages TO "nmshdAdmin"; GRANT synchronization TO "nmshdAdmin"; diff --git a/scripts/sql/sqlserver/setup.sql b/scripts/sql/sqlserver/setup.sql index 96341d0846..03e091836a 100644 --- a/scripts/sql/sqlserver/setup.sql +++ b/scripts/sql/sqlserver/setup.sql @@ -1,6 +1,13 @@ /* Server Configuration */ IF NOT EXISTS(SELECT * FROM sys.server_principals +WHERE name = 'announcements') +BEGIN + CREATE LOGIN announcements WITH PASSWORD = 'Passw0rd' + PRINT 'Login "announcements" created' ; +END +IF NOT EXISTS(SELECT * +FROM sys.server_principals WHERE name = 'challenges') BEGIN CREATE LOGIN challenges WITH PASSWORD = 'Passw0rd' @@ -86,6 +93,14 @@ BEGIN PRINT 'Schema "Challenges" created' ; END +IF NOT EXISTS ( SELECT * +FROM sys.schemas +WHERE name = N'Announcements' ) +BEGIN + EXEC('CREATE SCHEMA [Announcements]') + PRINT 'Schema "Announcements" created' ; +END + IF NOT EXISTS ( SELECT * FROM sys.schemas WHERE name = N'Devices' ) @@ -151,6 +166,14 @@ BEGIN END /*+++++++++++++++++++++++++++++++++++++++++++++++++++++ Users ++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ +IF NOT EXISTS (SELECT * +FROM sys.database_principals +WHERE name = 'announcements') +BEGIN + CREATE USER announcements FOR LOGIN announcements + PRINT 'User "announcements" created' ; +END + IF NOT EXISTS (SELECT * FROM sys.database_principals WHERE name = 'challenges') @@ -229,6 +252,7 @@ GO /*+++++++++++++++++++++++++++++++++++++++++++++++++ Authorizations +++++++++++++++++++++++++++++++++++++++++++++++++*/ PRINT 'Start changing authorizations' ; +GRANT SELECT, INSERT, UPDATE, DELETE, EXECUTE, REFERENCES, VIEW DEFINITION ON SCHEMA::Announcements TO announcements; GRANT SELECT, INSERT, UPDATE, DELETE, EXECUTE, REFERENCES, VIEW DEFINITION ON SCHEMA::Challenges TO challenges; GRANT SELECT, INSERT, UPDATE, DELETE, EXECUTE, REFERENCES, VIEW DEFINITION ON SCHEMA::Devices TO devices; GRANT SELECT, INSERT, UPDATE, DELETE, EXECUTE, REFERENCES, VIEW DEFINITION ON SCHEMA::Files TO files; diff --git a/scripts/windows/efcore/add_migration.ps1 b/scripts/windows/efcore/add_migration.ps1 index d7199d9937..45ced64ac4 100644 --- a/scripts/windows/efcore/add_migration.ps1 +++ b/scripts/windows/efcore/add_migration.ps1 @@ -1,5 +1,5 @@ Param( - [parameter(Mandatory)][ValidateSet("AdminApi", "Challenges", "Devices", "Files", "Messages", "Quotas", "Relationships", "Synchronization", "Tokens")] $moduleName, + [parameter(Mandatory)][ValidateSet("AdminApi", "Announcements", "Challenges", "Devices", "Files", "Messages", "Quotas", "Relationships", "Synchronization", "Tokens")] $moduleName, [parameter(Mandatory)] $migrationName, [parameter(Mandatory)][ValidateSet("s", "p", "SqlServer", "Postgres", "")] $provider ) diff --git a/scripts/windows/efcore/remove_migration.ps1 b/scripts/windows/efcore/remove_migration.ps1 index 1efc5ffc33..846689677e 100644 --- a/scripts/windows/efcore/remove_migration.ps1 +++ b/scripts/windows/efcore/remove_migration.ps1 @@ -1,5 +1,5 @@ Param( - [parameter(Mandatory)][ValidateSet("AdminApi", "Challenges", "Devices", "Files", "Messages", "Quotas", "Relationships", "Synchronization", "Tokens")] $moduleName, + [parameter(Mandatory)][ValidateSet("AdminApi", "Announcements", "Challenges", "Devices", "Files", "Messages", "Quotas", "Relationships", "Synchronization", "Tokens")] $moduleName, [parameter(Mandatory)][ValidateSet("SqlServer", "Postgres", "")] $provider ) diff --git a/scripts/windows/efcore/update_local_database.ps1 b/scripts/windows/efcore/update_local_database.ps1 index 172b965386..5e252803f8 100644 --- a/scripts/windows/efcore/update_local_database.ps1 +++ b/scripts/windows/efcore/update_local_database.ps1 @@ -1,5 +1,5 @@ Param( - [parameter(Mandatory)][ValidateSet("AdminApi", "Challenges", "Devices", "Files", "Messages", "Quotas", "Relationships", "Synchronization", "Tokens")] $moduleName, + [parameter(Mandatory)][ValidateSet("AdminApi", "Announcements", "Challenges", "Devices", "Files", "Messages", "Quotas", "Relationships", "Synchronization", "Tokens")] $moduleName, [parameter(Mandatory)] $migrationName, [parameter(Mandatory)][ValidateSet("SqlServer", "Postgres", "")] $provider )