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