diff --git a/AdminUi/src/AdminUi/ClientApp/src/app/components/quotas/tier/create-tier-dialog/create-tier-dialog.component.ts b/AdminUi/src/AdminUi/ClientApp/src/app/components/quotas/tier/create-tier-dialog/create-tier-dialog.component.ts
index aaf775e6b2..ede04b90aa 100644
--- a/AdminUi/src/AdminUi/ClientApp/src/app/components/quotas/tier/create-tier-dialog/create-tier-dialog.component.ts
+++ b/AdminUi/src/AdminUi/ClientApp/src/app/components/quotas/tier/create-tier-dialog/create-tier-dialog.component.ts
@@ -29,7 +29,8 @@ export class CreateTierDialogComponent {
name: "",
quotas: [],
isDeletable: false,
- numberOfIdentities: 0
+ numberOfIdentities: 0,
+ isReadOnly: false
} as Tier;
}
@@ -58,7 +59,8 @@ export class CreateTierDialogComponent {
name: data.result.name,
quotas: [],
numberOfIdentities: data.result.numberOfIdentities,
- isDeletable: data.result.isDeletable
+ isDeletable: data.result.isDeletable,
+ isReadOnly: false
} as Tier;
this.snackBar.open("Successfully added tier.", "Dismiss", {
diff --git a/AdminUi/src/AdminUi/ClientApp/src/app/components/quotas/tier/tier-edit/tier-edit.component.html b/AdminUi/src/AdminUi/ClientApp/src/app/components/quotas/tier/tier-edit/tier-edit.component.html
index 1c7976c893..cac746bda5 100644
--- a/AdminUi/src/AdminUi/ClientApp/src/app/components/quotas/tier/tier-edit/tier-edit.component.html
+++ b/AdminUi/src/AdminUi/ClientApp/src/app/components/quotas/tier/tier-edit/tier-edit.component.html
@@ -17,7 +17,7 @@
Name
-
+
You must enter a value
@@ -34,10 +34,10 @@
-
@@ -47,6 +47,7 @@
@@ -56,6 +57,7 @@
diff --git a/AdminUi/src/AdminUi/ClientApp/src/app/components/quotas/tier/tier-edit/tier-edit.component.ts b/AdminUi/src/AdminUi/ClientApp/src/app/components/quotas/tier/tier-edit/tier-edit.component.ts
index 62e9abc6f7..3b9e31620f 100644
--- a/AdminUi/src/AdminUi/ClientApp/src/app/components/quotas/tier/tier-edit/tier-edit.component.ts
+++ b/AdminUi/src/AdminUi/ClientApp/src/app/components/quotas/tier/tier-edit/tier-edit.component.ts
@@ -1,15 +1,15 @@
+import { SelectionModel } from "@angular/cdk/collections";
import { Component } from "@angular/core";
import { MatDialog, MatDialogRef } from "@angular/material/dialog";
import { MatSnackBar } from "@angular/material/snack-bar";
import { ActivatedRoute, Router } from "@angular/router";
+import { Observable, forkJoin } from "rxjs";
+import { ConfirmationDialogComponent } from "src/app/components/shared/confirmation-dialog/confirmation-dialog.component";
import { QuotasService, TierQuota } from "src/app/services/quotas-service/quotas.service";
import { Tier, TierService } from "src/app/services/tier-service/tier.service";
+import { HttpErrorResponseWrapper } from "src/app/utils/http-error-response-wrapper";
import { HttpResponseEnvelope } from "src/app/utils/http-response-envelope";
import { AssignQuotaData, AssignQuotasDialogComponent } from "../../assign-quotas-dialog/assign-quotas-dialog.component";
-import { SelectionModel } from "@angular/cdk/collections";
-import { ConfirmationDialogComponent } from "src/app/components/shared/confirmation-dialog/confirmation-dialog.component";
-import { Observable, forkJoin } from "rxjs";
-import { HttpErrorResponseWrapper } from "src/app/utils/http-error-response-wrapper";
@Component({
selector: "app-tier-edit",
@@ -24,7 +24,6 @@ export class TierEditComponent {
public selectionQuotas: SelectionModel;
public quotasTableDisplayedColumns: string[];
public tierId?: string;
- public disabled: boolean;
public editMode: boolean;
public tier: Tier;
public loading: boolean;
@@ -47,12 +46,12 @@ export class TierEditComponent {
this.quotasTableDisplayedColumns = ["select", "metricName", "max", "period"];
this.editMode = false;
this.loading = true;
- this.disabled = false;
this.tier = {
id: "",
name: "",
quotas: [],
isDeletable: false,
+ isReadOnly: false,
numberOfIdentities: 0
} as Tier;
}
@@ -76,7 +75,6 @@ export class TierEditComponent {
this.tierService.getTierById(this.tierId!).subscribe({
next: (data: HttpResponseEnvelope) => {
this.tier = data.result;
- this.tier.isDeletable = this.tier.name !== "Basic";
},
complete: () => (this.loading = false),
error: (err: any) => {
@@ -246,4 +244,16 @@ export class TierEditComponent {
}
return `${this.selectionQuotas.isSelected(row) ? "deselect" : "select"} row ${index + 1}`;
}
+
+ public isNameInputDisabled(): boolean {
+ return this.editMode || this.tier.isReadOnly;
+ }
+
+ public isQuotaDeletionDisabled(): boolean {
+ return this.selectionQuotas.selected.length === 0 || this.tier.isReadOnly;
+ }
+
+ public isQuotaAssignmentDisabled(): boolean {
+ return this.tier.id === "" || this.tier.isReadOnly;
+ }
}
diff --git a/AdminUi/src/AdminUi/ClientApp/src/app/services/tier-service/tier.service.ts b/AdminUi/src/AdminUi/ClientApp/src/app/services/tier-service/tier.service.ts
index 43836a4070..80edcc45ba 100644
--- a/AdminUi/src/AdminUi/ClientApp/src/app/services/tier-service/tier.service.ts
+++ b/AdminUi/src/AdminUi/ClientApp/src/app/services/tier-service/tier.service.ts
@@ -1,6 +1,6 @@
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
-import { Observable } from "rxjs";
+import { Observable, map } from "rxjs";
import { HttpResponseEnvelope } from "src/app/utils/http-response-envelope";
import { PagedHttpResponseEnvelope } from "src/app/utils/paged-http-response-envelope";
import { environment } from "src/environments/environment";
@@ -11,6 +11,9 @@ import { TierQuota } from "../quotas-service/quotas.service";
})
export class TierService {
private readonly apiUrl: string;
+ private static readonly QUEUED_FOR_DELETION_TIER_ID = "TIR00000000000000001";
+ private static readonly BASIC_TIER_NAME = "Basic";
+
public constructor(private readonly http: HttpClient) {
this.apiUrl = `${environment.apiUrl}/Tiers`;
}
@@ -20,7 +23,13 @@ export class TierService {
}
public getTierById(id: string): Observable> {
- return this.http.get>(`${this.apiUrl}/${id}`);
+ return this.http.get>(`${this.apiUrl}/${id}`).pipe(
+ map((responseEnvelope: HttpResponseEnvelope) => {
+ responseEnvelope.result.isDeletable = responseEnvelope.result.name !== TierService.BASIC_TIER_NAME && responseEnvelope.result.id !== TierService.QUEUED_FOR_DELETION_TIER_ID;
+ responseEnvelope.result.isReadOnly = responseEnvelope.result.id === TierService.QUEUED_FOR_DELETION_TIER_ID;
+ return responseEnvelope;
+ })
+ );
}
public createTier(tier: Tier): Observable> {
@@ -41,6 +50,7 @@ export interface Tier {
name: string;
quotas: TierQuota[];
isDeletable: boolean;
+ isReadOnly: boolean;
numberOfIdentities: number;
}
diff --git a/AdminUi/src/AdminUi/Controllers/IdentitiesController.cs b/AdminUi/src/AdminUi/Controllers/IdentitiesController.cs
index 9af1e1da81..667a7c218e 100644
--- a/AdminUi/src/AdminUi/Controllers/IdentitiesController.cs
+++ b/AdminUi/src/AdminUi/Controllers/IdentitiesController.cs
@@ -1,8 +1,9 @@
-using Backbone.AdminUi.Infrastructure.Persistence.Database;
+using Backbone.BuildingBlocks.API;
using Backbone.BuildingBlocks.API.Mvc;
using Backbone.BuildingBlocks.API.Mvc.ControllerAttributes;
-using Backbone.Modules.Devices.Application;
using Backbone.Modules.Devices.Application.Devices.DTOs;
+using Backbone.Modules.Devices.Application.Identities.Commands.CreateIdentity;
+using Backbone.Modules.Devices.Application.Identities.Commands.StartDeletionProcessAsSupport;
using Backbone.Modules.Devices.Application.Identities.Commands.UpdateIdentity;
using Backbone.Modules.Quotas.Application.DTOs;
using Backbone.Modules.Quotas.Application.Identities.Commands.CreateQuotaForIdentity;
@@ -11,7 +12,6 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Options;
using GetIdentityQueryDevices = Backbone.Modules.Devices.Application.Identities.Queries.GetIdentity.GetIdentityQuery;
using GetIdentityQueryQuotas = Backbone.Modules.Quotas.Application.Identities.Queries.GetIdentity.GetIdentityQuery;
using GetIdentityResponseDevices = Backbone.Modules.Devices.Application.Identities.Queries.GetIdentity.GetIdentityResponse;
@@ -23,14 +23,8 @@ namespace Backbone.AdminUi.Controllers;
[Authorize("ApiKey")]
public class IdentitiesController : ApiControllerBase
{
- private readonly AdminUiDbContext _adminUiDbContext;
- private readonly ApplicationOptions _options;
-
- public IdentitiesController(
- IMediator mediator, IOptions options, AdminUiDbContext adminUiDbContext) : base(mediator)
+ public IdentitiesController(IMediator mediator) : base(mediator)
{
- _adminUiDbContext = adminUiDbContext;
- _options = options.Value;
}
[HttpPost("{identityAddress}/Quotas")]
@@ -86,6 +80,40 @@ public async Task UpdateIdentity([FromRoute] string identityAddre
await _mediator.Send(command, cancellationToken);
return NoContent();
}
+
+ [HttpPost]
+ [ProducesResponseType(typeof(HttpResponseEnvelopeResult), StatusCodes.Status201Created)]
+ [ProducesError(StatusCodes.Status400BadRequest)]
+ public async Task CreateIdentity(CreateIdentityRequest request, CancellationToken cancellationToken)
+ {
+ var command = new CreateIdentityCommand
+ {
+ ClientId = request.ClientId,
+ DevicePassword = request.DevicePassword,
+ IdentityPublicKey = request.IdentityPublicKey,
+ IdentityVersion = request.IdentityVersion,
+ SignedChallenge = new SignedChallengeDTO
+ {
+ Challenge = request.SignedChallenge.Challenge,
+ Signature = request.SignedChallenge.Signature
+ },
+ ShouldValidateChallenge = false
+ };
+
+ var response = await _mediator.Send(command, cancellationToken);
+
+ return Created(response);
+ }
+
+ [HttpPost("{address}/DeletionProcesses")]
+ [ProducesResponseType(StatusCodes.Status201Created)]
+ [ProducesError(StatusCodes.Status400BadRequest)]
+ [ProducesError(StatusCodes.Status404NotFound)]
+ public async Task StartDeletionProcessAsSupport([FromRoute] string address, CancellationToken cancellationToken)
+ {
+ var response = await _mediator.Send(new StartDeletionProcessAsSupportCommand(address), cancellationToken);
+ return Created("", response);
+ }
}
public class CreateQuotaForIdentityRequest
@@ -94,6 +122,7 @@ public class CreateQuotaForIdentityRequest
public int Max { get; set; }
public QuotaPeriod Period { get; set; }
}
+
public class UpdateIdentityTierRequest
{
public string TierId { get; set; }
@@ -104,16 +133,26 @@ public class GetIdentityResponse
public string Address { get; set; }
public string ClientId { get; set; }
public byte[] PublicKey { get; set; }
-
public string TierId { get; set; }
-
public DateTime CreatedAt { get; set; }
-
public byte IdentityVersion { get; set; }
-
public int NumberOfDevices { get; set; }
-
public IEnumerable Devices { get; set; }
-
public IEnumerable Quotas { get; set; }
}
+
+public class CreateIdentityRequest
+{
+ public required string ClientId { get; set; }
+ public required string ClientSecret { get; set; }
+ public required byte[] IdentityPublicKey { get; set; }
+ public required string DevicePassword { get; set; }
+ public required byte IdentityVersion { get; set; }
+ public required CreateIdentityRequestSignedChallenge SignedChallenge { get; set; }
+}
+
+public class CreateIdentityRequestSignedChallenge
+{
+ public required string Challenge { get; set; }
+ public required byte[] Signature { get; set; }
+}
diff --git a/AdminUi/test/AdminUi.Tests.Integration/API/IdentitiesApi.cs b/AdminUi/test/AdminUi.Tests.Integration/API/IdentitiesApi.cs
index 8092afeb17..6748017071 100644
--- a/AdminUi/test/AdminUi.Tests.Integration/API/IdentitiesApi.cs
+++ b/AdminUi/test/AdminUi.Tests.Integration/API/IdentitiesApi.cs
@@ -27,4 +27,14 @@ internal async Task DeleteIndividualQuota(string identityAddress,
{
return await GetOData>("/Identities?$expand=Tier", requestConfiguration);
}
+
+ internal async Task> StartDeletionProcess(string identityAddress, RequestConfiguration requestConfiguration)
+ {
+ return await Post($"/Identities/{identityAddress}/DeletionProcesses", requestConfiguration);
+ }
+
+ internal async Task> CreateIdentity(RequestConfiguration requestConfiguration)
+ {
+ return await Post("/Identities", requestConfiguration);
+ }
}
diff --git a/AdminUi/test/AdminUi.Tests.Integration/Features/IdentityDeletionProcess/POST.feature b/AdminUi/test/AdminUi.Tests.Integration/Features/IdentityDeletionProcess/POST.feature
new file mode 100644
index 0000000000..ed0b7c0e40
--- /dev/null
+++ b/AdminUi/test/AdminUi.Tests.Integration/Features/IdentityDeletionProcess/POST.feature
@@ -0,0 +1,17 @@
+@Integration
+Feature: POST Identities/{id}/DeletionProcess
+
+Support starts a deletion process
+
+Scenario: Starting a deletion process as support
+ Given an Identity i
+ When a POST request is sent to the /Identities/{i.id}/DeletionProcesses endpoint
+ Then the response status code is 201 (Created)
+ And the response contains a Deletion Process
+
+Scenario: There can only be one active deletion process
+ Given an Identity i
+ And an active deletion process for Identity i exists
+ When a POST request is sent to the /Identities/{i.id}/DeletionProcesses endpoint
+ Then the response status code is 400 (Bad Request)
+ And the response content includes an error with the error code "error.platform.validation.device.onlyOneActiveDeletionProcessAllowed"
diff --git a/AdminUi/test/AdminUi.Tests.Integration/Models/CreateIdentityRequest.cs b/AdminUi/test/AdminUi.Tests.Integration/Models/CreateIdentityRequest.cs
new file mode 100644
index 0000000000..618180cfd8
--- /dev/null
+++ b/AdminUi/test/AdminUi.Tests.Integration/Models/CreateIdentityRequest.cs
@@ -0,0 +1,17 @@
+namespace Backbone.AdminUi.Tests.Integration.Models;
+
+public class CreateIdentityRequest
+{
+ public string ClientId { get; set; }
+ public string ClientSecret { get; set; }
+ public string IdentityPublicKey { get; set; }
+ public string DevicePassword { get; set; }
+ public byte IdentityVersion { get; set; }
+ public CreateIdentityRequestSignedChallenge SignedChallenge { get; set; }
+}
+
+public class CreateIdentityRequestSignedChallenge
+{
+ public string Challenge { get; set; }
+ public string Signature { get; set; }
+}
diff --git a/AdminUi/test/AdminUi.Tests.Integration/Models/CreateIdentityResponse.cs b/AdminUi/test/AdminUi.Tests.Integration/Models/CreateIdentityResponse.cs
new file mode 100644
index 0000000000..53d7f18b20
--- /dev/null
+++ b/AdminUi/test/AdminUi.Tests.Integration/Models/CreateIdentityResponse.cs
@@ -0,0 +1,16 @@
+namespace Backbone.AdminUi.Tests.Integration.Models;
+
+public class CreateIdentityResponse
+{
+ public string Address { get; set; }
+ public DateTime CreatedAt { get; set; }
+
+ public CreateIdentityResponseDevice Device { get; set; }
+}
+
+public class CreateIdentityResponseDevice
+{
+ public string Id { get; set; }
+ public string Username { get; set; }
+ public DateTime CreatedAt { get; set; }
+}
diff --git a/AdminUi/test/AdminUi.Tests.Integration/Models/StartDeletionProcessAsSupportResponse.cs b/AdminUi/test/AdminUi.Tests.Integration/Models/StartDeletionProcessAsSupportResponse.cs
new file mode 100644
index 0000000000..781867ea04
--- /dev/null
+++ b/AdminUi/test/AdminUi.Tests.Integration/Models/StartDeletionProcessAsSupportResponse.cs
@@ -0,0 +1,8 @@
+namespace Backbone.AdminUi.Tests.Integration.Models;
+
+public class StartDeletionProcessAsSupportResponse
+{
+ public string Id { get; set; }
+ public string Status { get; set; }
+ public DateTime CreatedAt { get; set; }
+}
diff --git a/AdminUi/test/AdminUi.Tests.Integration/StepDefinitions/IdentitiesApiStepDefinitions.cs b/AdminUi/test/AdminUi.Tests.Integration/StepDefinitions/IdentitiesApiStepDefinitions.cs
index d4559d4c7c..54309bf638 100644
--- a/AdminUi/test/AdminUi.Tests.Integration/StepDefinitions/IdentitiesApiStepDefinitions.cs
+++ b/AdminUi/test/AdminUi.Tests.Integration/StepDefinitions/IdentitiesApiStepDefinitions.cs
@@ -1,29 +1,77 @@
using Backbone.AdminUi.Tests.Integration.API;
using Backbone.AdminUi.Tests.Integration.Extensions;
using Backbone.AdminUi.Tests.Integration.Models;
-using Backbone.AdminUi.Tests.Integration.TestData;
+using Backbone.Crypto;
+using Backbone.Crypto.Abstractions;
+using Newtonsoft.Json;
namespace Backbone.AdminUi.Tests.Integration.StepDefinitions;
[Binding]
[Scope(Feature = "GET Identities")]
+[Scope(Feature = "POST Identities/{id}/DeletionProcess")]
internal class IdentitiesApiStepDefinitions : BaseStepDefinitions
{
private readonly IdentitiesApi _identitiesApi;
+ private readonly ISignatureHelper _signatureHelper;
private ODataResponse>? _identityOverviewsResponse;
private HttpResponse? _identityResponse;
+ private HttpResponse? _createIdentityResponse;
+ private HttpResponse? _identityDeletionProcessResponse;
private string _existingIdentity;
- public IdentitiesApiStepDefinitions(IdentitiesApi identitiesApi)
+ public IdentitiesApiStepDefinitions(IdentitiesApi identitiesApi, ISignatureHelper signatureHelper)
{
_identitiesApi = identitiesApi;
+ _signatureHelper = signatureHelper;
_existingIdentity = string.Empty;
}
+
+ [Given("an active deletion process for Identity i exists")]
+ public async Task GivenAnActiveDeletionProcessForIdentityAExists()
+ {
+ await _identitiesApi.StartDeletionProcess(_createIdentityResponse!.Content.Result!.Address, _requestConfiguration);
+ }
+
[Given(@"an Identity i")]
- public void GivenAnIdentity()
+ public async Task GivenAnIdentityI()
{
- _existingIdentity = Identities.IDENTITY_A;
+ var keyPair = _signatureHelper.CreateKeyPair();
+
+ dynamic publicKey = new
+ {
+ pub = keyPair.PublicKey.Base64Representation,
+ alg = 3
+ };
+
+ var createIdentityRequest = new CreateIdentityRequest()
+ {
+ ClientId = "test",
+ ClientSecret = "test",
+ DevicePassword = "test",
+ IdentityPublicKey = (ConvertibleString.FromUtf8(JsonConvert.SerializeObject(publicKey)) as ConvertibleString)!.Base64Representation,
+ IdentityVersion = 1,
+ SignedChallenge = new CreateIdentityRequestSignedChallenge
+ {
+ Challenge = "string.Empty",
+ Signature = "some-dummy-signature"
+ }
+ };
+
+ var requestConfiguration = _requestConfiguration.Clone();
+ requestConfiguration.ContentType = "application/json";
+ requestConfiguration.SetContent(createIdentityRequest);
+
+ _createIdentityResponse = await _identitiesApi.CreateIdentity(requestConfiguration);
+ _createIdentityResponse.IsSuccessStatusCode.Should().BeTrue();
+ _existingIdentity = _createIdentityResponse.Content.Result!.Address;
+ }
+
+ [When("a POST request is sent to the /Identities/{i.id}/DeletionProcesses endpoint")]
+ public async Task WhenAPOSTRequestIsSentToTheIdentitiesIdDeletionProcessesEndpoint()
+ {
+ _identityDeletionProcessResponse = await _identitiesApi.StartDeletionProcess(_createIdentityResponse!.Content.Result!.Address, _requestConfiguration);
}
[When(@"a GET request is sent to the /Identities endpoint")]
@@ -59,6 +107,14 @@ public void ThenTheResponseContainsAListOfIdentities()
_identityOverviewsResponse!.AssertContentCompliesWithSchema();
}
+ [Then(@"the response contains a Deletion Process")]
+ public void ThenTheResponseContainsADeletionProcess()
+ {
+ _identityDeletionProcessResponse!.Content.Result.Should().NotBeNull();
+ _identityDeletionProcessResponse!.AssertContentTypeIs("application/json");
+ _identityDeletionProcessResponse!.AssertContentCompliesWithSchema();
+ }
+
[Then(@"the response contains Identity i")]
public void ThenTheResponseContainsAnIdentity()
{
@@ -82,6 +138,12 @@ public void ThenTheResponseStatusCodeIs(int expectedStatusCode)
var actualStatusCode = (int)_identityOverviewsResponse!.StatusCode;
actualStatusCode.Should().Be(expectedStatusCode);
}
+
+ if (_identityDeletionProcessResponse != null)
+ {
+ var actualStatusCode = (int)_identityDeletionProcessResponse!.StatusCode;
+ actualStatusCode.Should().Be(expectedStatusCode);
+ }
}
[Then(@"the response content includes an error with the error code ""([^""]+)""")]
@@ -92,5 +154,11 @@ public void ThenTheResponseContentIncludesAnErrorWithTheErrorCode(string errorCo
_identityResponse!.Content.Error.Should().NotBeNull();
_identityResponse.Content.Error!.Code.Should().Be(errorCode);
}
+
+ if (_identityDeletionProcessResponse != null)
+ {
+ _identityDeletionProcessResponse!.Content.Error.Should().NotBeNull();
+ _identityDeletionProcessResponse.Content.Error!.Code.Should().Be(errorCode);
+ }
}
}
diff --git a/AdminUi/test/AdminUi.Tests.Integration/StepDefinitions/IndividualQuotaStepDefinitions.cs b/AdminUi/test/AdminUi.Tests.Integration/StepDefinitions/IndividualQuotaStepDefinitions.cs
index a6942d3a07..c40f0f61c6 100644
--- a/AdminUi/test/AdminUi.Tests.Integration/StepDefinitions/IndividualQuotaStepDefinitions.cs
+++ b/AdminUi/test/AdminUi.Tests.Integration/StepDefinitions/IndividualQuotaStepDefinitions.cs
@@ -1,7 +1,9 @@
using Backbone.AdminUi.Tests.Integration.API;
using Backbone.AdminUi.Tests.Integration.Extensions;
using Backbone.AdminUi.Tests.Integration.Models;
-using Backbone.AdminUi.Tests.Integration.TestData;
+using Backbone.Crypto;
+using Backbone.Crypto.Abstractions;
+using Newtonsoft.Json;
namespace Backbone.AdminUi.Tests.Integration.StepDefinitions;
@@ -14,25 +16,27 @@ internal class IndividualQuotaStepDefinitions : BaseStepDefinitions
private string _identityAddress;
private string _quotaId;
private HttpResponse? _response;
+ private readonly ISignatureHelper _signatureHelper;
private HttpResponse? _deleteResponse;
- public IndividualQuotaStepDefinitions(IdentitiesApi identitiesApi)
+ public IndividualQuotaStepDefinitions(IdentitiesApi identitiesApi, ISignatureHelper signatureHelper)
{
_identitiesApi = identitiesApi;
+ _signatureHelper = signatureHelper;
_identityAddress = string.Empty;
_quotaId = string.Empty;
}
[Given(@"an Identity i")]
- public void GivenAnIdentity()
+ public async Task GivenAnIdentityI()
{
- _identityAddress = Identities.IDENTITY_A;
+ await CreateIdentity();
}
[Given(@"an Identity i with an IndividualQuota q")]
public async Task GivenAnIdentityIWithAnIndividualQuotaQ()
{
- _identityAddress = Identities.IDENTITY_A;
+ await CreateIdentity();
var createIndividualQuotaRequest = new CreateIndividualQuotaRequest()
{
MetricKey = "NumberOfSentMessages",
@@ -138,4 +142,40 @@ public void ThenTheResponseContentIncludesAnErrorWithTheErrorCode(string errorCo
_deleteResponse.Content!.Error.Code.Should().Be(errorCode);
}
}
+
+ private async Task CreateIdentity()
+ {
+ var keyPair = _signatureHelper.CreateKeyPair();
+
+ dynamic publicKey = new
+ {
+ pub = keyPair.PublicKey.Base64Representation,
+ alg = 3
+ };
+
+ var createIdentityRequest = new CreateIdentityRequest()
+ {
+ ClientId = "test",
+ ClientSecret = "test",
+ DevicePassword = "test",
+ IdentityPublicKey = (ConvertibleString.FromUtf8(JsonConvert.SerializeObject(publicKey)) as ConvertibleString)!.Base64Representation,
+ IdentityVersion = 1,
+ SignedChallenge = new CreateIdentityRequestSignedChallenge
+ {
+ Challenge = "string.Empty",
+ Signature = "some-dummy-signature"
+ }
+ };
+
+ var requestConfiguration = _requestConfiguration.Clone();
+ requestConfiguration.ContentType = "application/json";
+ requestConfiguration.SetContent(createIdentityRequest);
+
+ var createIdentityResponse = await _identitiesApi.CreateIdentity(requestConfiguration);
+ createIdentityResponse.IsSuccessStatusCode.Should().BeTrue();
+ _identityAddress = createIdentityResponse.Content.Result!.Address;
+
+ // allow the event queue to trigger the creation of this Identity on the Quotas module
+ Thread.Sleep(2000);
+ }
}
diff --git a/AdminUi/test/AdminUi.Tests.Integration/Support/Dependencies.cs b/AdminUi/test/AdminUi.Tests.Integration/Support/Dependencies.cs
index db7a3d560c..dcb160a694 100644
--- a/AdminUi/test/AdminUi.Tests.Integration/Support/Dependencies.cs
+++ b/AdminUi/test/AdminUi.Tests.Integration/Support/Dependencies.cs
@@ -1,5 +1,7 @@
using Backbone.AdminUi.Tests.Integration.API;
using Backbone.AdminUi.Tests.Integration.Configuration;
+using Backbone.Crypto.Abstractions;
+using Backbone.Crypto.Implementations;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using SolidToken.SpecFlow.DependencyInjection;
@@ -24,6 +26,8 @@ public static IServiceCollection CreateServices()
);
services.AddSingleton(new HttpClientFactory(new CustomWebApplicationFactory()));
+ services.AddSingleton(SignatureHelper.CreateEd25519WithRawKeyFormat());
+
services.AddTransient();
services.AddTransient();
services.AddTransient();
diff --git a/BuildingBlocks/src/BuildingBlocks.API/AspNetCoreIdentityCustomizations/CustomSigninManager.cs b/BuildingBlocks/src/BuildingBlocks.API/AspNetCoreIdentityCustomizations/CustomSigninManager.cs
index 000f2ea9de..afeaf78a5a 100644
--- a/BuildingBlocks/src/BuildingBlocks.API/AspNetCoreIdentityCustomizations/CustomSigninManager.cs
+++ b/BuildingBlocks/src/BuildingBlocks.API/AspNetCoreIdentityCustomizations/CustomSigninManager.cs
@@ -1,4 +1,4 @@
-using Backbone.Modules.Devices.Domain.Entities;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
diff --git a/BuildingBlocks/src/BuildingBlocks.API/AspNetCoreIdentityCustomizations/CustomUserStore.cs b/BuildingBlocks/src/BuildingBlocks.API/AspNetCoreIdentityCustomizations/CustomUserStore.cs
index 75d03877d1..4da3b0a24d 100644
--- a/BuildingBlocks/src/BuildingBlocks.API/AspNetCoreIdentityCustomizations/CustomUserStore.cs
+++ b/BuildingBlocks/src/BuildingBlocks.API/AspNetCoreIdentityCustomizations/CustomUserStore.cs
@@ -1,5 +1,5 @@
using System.Linq.Expressions;
-using Backbone.Modules.Devices.Domain.Entities;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
using Backbone.Modules.Devices.Infrastructure.Persistence.Database;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
diff --git a/BuildingBlocks/src/BuildingBlocks.API/Extensions/ServiceCollectionExtensions.cs b/BuildingBlocks/src/BuildingBlocks.API/Extensions/ServiceCollectionExtensions.cs
index c2966515ed..e94b6346e6 100644
--- a/BuildingBlocks/src/BuildingBlocks.API/Extensions/ServiceCollectionExtensions.cs
+++ b/BuildingBlocks/src/BuildingBlocks.API/Extensions/ServiceCollectionExtensions.cs
@@ -1,5 +1,5 @@
-using Backbone.BuildingBlocks.API.AspNetCoreIdentityCustomizations;
-using Backbone.Modules.Devices.Domain.Entities;
+using Backbone.BuildingBlocks.API.AspNetCoreIdentityCustomizations;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
using Backbone.Modules.Devices.Infrastructure.Persistence.Database;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
diff --git a/BuildingBlocks/src/BuildingBlocks.API/Mvc/ExceptionFilters/CustomExceptionFilter.cs b/BuildingBlocks/src/BuildingBlocks.API/Mvc/ExceptionFilters/CustomExceptionFilter.cs
index 24991af71d..ec2a7901e7 100644
--- a/BuildingBlocks/src/BuildingBlocks.API/Mvc/ExceptionFilters/CustomExceptionFilter.cs
+++ b/BuildingBlocks/src/BuildingBlocks.API/Mvc/ExceptionFilters/CustomExceptionFilter.cs
@@ -137,7 +137,7 @@ private HttpError CreateHttpErrorForDomainException(DomainException domainExcept
return quotaExhautedException.ExhaustedMetricStatuses.Select(m => new
{
#pragma warning disable IDE0037
- MetricKey = m.MetricKey,
+ MetricKey = m.MetricKey.Value,
IsExhaustedUntil = m.IsExhaustedUntil
#pragma warning restore IDE0037
});
diff --git a/BuildingBlocks/src/BuildingBlocks.Application.Abstractions/Exceptions/GenericApplicationErrors.cs b/BuildingBlocks/src/BuildingBlocks.Application.Abstractions/Exceptions/GenericApplicationErrors.cs
index eb227d5f67..3e9da5b3d1 100644
--- a/BuildingBlocks/src/BuildingBlocks.Application.Abstractions/Exceptions/GenericApplicationErrors.cs
+++ b/BuildingBlocks/src/BuildingBlocks.Application.Abstractions/Exceptions/GenericApplicationErrors.cs
@@ -25,7 +25,7 @@ public static ApplicationError Forbidden()
public static ApplicationError QuotaExhausted()
{
return new ApplicationError("error.platform.quotaExhausted",
- "You are not allowed to perform this action because one or more quotas have been exhausted.");
+ "You cannot to perform this action because one or more quotas are exhausted.");
}
public static class Validation
diff --git a/BuildingBlocks/src/DevelopmentKit.Identity/ValueObjects/IdentityAddress.cs b/BuildingBlocks/src/DevelopmentKit.Identity/ValueObjects/IdentityAddress.cs
index d29c352b8b..13af71abc6 100644
--- a/BuildingBlocks/src/DevelopmentKit.Identity/ValueObjects/IdentityAddress.cs
+++ b/BuildingBlocks/src/DevelopmentKit.Identity/ValueObjects/IdentityAddress.cs
@@ -108,9 +108,9 @@ public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo?
#region Operators
- public static implicit operator string(IdentityAddress deviceId)
+ public static implicit operator string(IdentityAddress identityAddress)
{
- return deviceId.StringValue;
+ return identityAddress.StringValue;
}
public static implicit operator IdentityAddress(string stringValue)
diff --git a/BuildingBlocks/src/UnitTestTools/Extensions/NonGenericAsyncFunctionAssertionsExtensions.cs b/BuildingBlocks/src/UnitTestTools/Extensions/NonGenericAsyncFunctionAssertionsExtensions.cs
index 9076f90ed6..92b38656ef 100644
--- a/BuildingBlocks/src/UnitTestTools/Extensions/NonGenericAsyncFunctionAssertionsExtensions.cs
+++ b/BuildingBlocks/src/UnitTestTools/Extensions/NonGenericAsyncFunctionAssertionsExtensions.cs
@@ -10,4 +10,11 @@ public static ExceptionAssertions AwaitThrowAsync(this N
{
return assertions.ThrowAsync(because, becauseArgs).WaitAsync(CancellationToken.None).Result;
}
+
+ public static ExceptionAssertions AwaitThrowAsync(this GenericAsyncFunctionAssertions assertions, string because = "",
+ params object[] becauseArgs)
+ where TException : Exception
+ {
+ return assertions.ThrowAsync(because, becauseArgs).WaitAsync(CancellationToken.None).Result;
+ }
}
diff --git a/ConsumerApi.Tests.Integration/API/BaseApi.cs b/ConsumerApi.Tests.Integration/API/BaseApi.cs
index a5567bc46d..f99ab868bf 100644
--- a/ConsumerApi.Tests.Integration/API/BaseApi.cs
+++ b/ConsumerApi.Tests.Integration/API/BaseApi.cs
@@ -32,6 +32,11 @@ protected async Task> Post(string endpoint, RequestConfigurat
return await ExecuteRequest(HttpMethod.Post, endpoint, requestConfiguration);
}
+ protected async Task Post(string endpoint, RequestConfiguration requestConfiguration)
+ {
+ return await ExecuteRequest(HttpMethod.Post, endpoint, requestConfiguration);
+ }
+
protected async Task> Put(string endpoint, RequestConfiguration requestConfiguration)
{
return await ExecuteRequest(HttpMethod.Put, endpoint, requestConfiguration);
diff --git a/ConsumerApi.Tests.Integration/API/IdentitiesApi.cs b/ConsumerApi.Tests.Integration/API/IdentitiesApi.cs
index cc3a3fc399..694cca5504 100644
--- a/ConsumerApi.Tests.Integration/API/IdentitiesApi.cs
+++ b/ConsumerApi.Tests.Integration/API/IdentitiesApi.cs
@@ -6,6 +6,11 @@ internal class IdentitiesApi : BaseApi
{
public IdentitiesApi(HttpClientFactory factory) : base(factory) { }
+ internal async Task> StartDeletionProcess(RequestConfiguration requestConfiguration)
+ {
+ return await Post("/Identities/Self/DeletionProcesses", requestConfiguration);
+ }
+
internal async Task> CreateIdentity(RequestConfiguration requestConfiguration)
{
return await Post("/Identities", requestConfiguration);
diff --git a/ConsumerApi.Tests.Integration/ConsumerApi.Tests.Integration.csproj b/ConsumerApi.Tests.Integration/ConsumerApi.Tests.Integration.csproj
index 52278caa2d..43a52e15fd 100644
--- a/ConsumerApi.Tests.Integration/ConsumerApi.Tests.Integration.csproj
+++ b/ConsumerApi.Tests.Integration/ConsumerApi.Tests.Integration.csproj
@@ -51,5 +51,4 @@
%(RelativeDir)%(Filename).feature$(DefaultLanguageSourceExtension)
-
diff --git a/ConsumerApi.Tests.Integration/CustomWebApplicationFactory.cs b/ConsumerApi.Tests.Integration/CustomWebApplicationFactory.cs
index 80822cd67a..53014f6014 100644
--- a/ConsumerApi.Tests.Integration/CustomWebApplicationFactory.cs
+++ b/ConsumerApi.Tests.Integration/CustomWebApplicationFactory.cs
@@ -1,6 +1,13 @@
-using Backbone.Tooling.Extensions;
+using Backbone.Crypto;
+using Backbone.Crypto.Abstractions;
+using Backbone.Crypto.Implementations;
+using Backbone.Tooling.Extensions;
+using Google;
using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
namespace Backbone.ConsumerApi.Tests.Integration;
diff --git a/ConsumerApi.Tests.Integration/Features/Identities/Self/DeletionProcess/POST.feature b/ConsumerApi.Tests.Integration/Features/Identities/Self/DeletionProcess/POST.feature
new file mode 100644
index 0000000000..a97618bc07
--- /dev/null
+++ b/ConsumerApi.Tests.Integration/Features/Identities/Self/DeletionProcess/POST.feature
@@ -0,0 +1,16 @@
+@Integration
+Feature: POST Identities/Self/DeletionProcess
+
+User starts a deletion process
+
+// Scenario: Starting a deletion process
+// Given no active deletion process for the identity exists
+// When a POST request is sent to the /Identities/Self/DeletionProcesses endpoint
+// Then the response status code is 201 (Created)
+// And the response contains a Deletion Process
+
+// Scenario: There can only be one active deletion process
+// Given an active deletion process for the identity exists
+// When a POST request is sent to the /Identities/Self/DeletionProcesses endpoint
+// Then the response status code is 400 (Bad Request)
+// And the response content includes an error with the error code "error.platform.validation.device.onlyOneActiveDeletionProcessAllowed"
diff --git a/ConsumerApi.Tests.Integration/Hooks/Hooks.cs b/ConsumerApi.Tests.Integration/Hooks/Hooks.cs
index 30f86d8e48..d2a89c1ecd 100644
--- a/ConsumerApi.Tests.Integration/Hooks/Hooks.cs
+++ b/ConsumerApi.Tests.Integration/Hooks/Hooks.cs
@@ -4,8 +4,6 @@ namespace Backbone.ConsumerApi.Tests.Integration.Hooks;
[Binding]
public sealed class Hooks
{
- // For additional details on SpecFlow hooks see http://go.specflow.org/doc-hooks
-
[BeforeTestRun(Order = 0)]
public static void BeforeTestRun()
{
diff --git a/ConsumerApi.Tests.Integration/Models/StartDeletionProcessAsSupportResponse.cs b/ConsumerApi.Tests.Integration/Models/StartDeletionProcessAsSupportResponse.cs
new file mode 100644
index 0000000000..acb239ca1a
--- /dev/null
+++ b/ConsumerApi.Tests.Integration/Models/StartDeletionProcessAsSupportResponse.cs
@@ -0,0 +1,10 @@
+using Backbone.Modules.Devices.Domain.Entities.Identities;
+
+namespace Backbone.ConsumerApi.Tests.Integration.Models;
+
+public class StartDeletionProcessAsSupportResponse
+{
+ public string Id { get; set; }
+ public DeletionProcessStatus Status { get; set; }
+ public DateTime CreatedAt { get; set; }
+}
diff --git a/ConsumerApi.Tests.Integration/Models/StartDeletionProcessResponse.cs b/ConsumerApi.Tests.Integration/Models/StartDeletionProcessResponse.cs
new file mode 100644
index 0000000000..3abde3cb51
--- /dev/null
+++ b/ConsumerApi.Tests.Integration/Models/StartDeletionProcessResponse.cs
@@ -0,0 +1,13 @@
+namespace Backbone.ConsumerApi.Tests.Integration.Models;
+
+public class StartDeletionProcessResponse
+{
+ public string Id { get; set; }
+ public string Status { get; set; }
+ public DateTime CreatedAt { get; set; }
+
+ public DateTime ApprovedAt { get; set; }
+ public string ApprovedByDevice { get; set; }
+
+ public DateTime GracePeriodEndsAt { get; set; }
+}
diff --git a/ConsumerApi.Tests.Integration/StepDefinitions/IdentitiesApiStepDefinitions.cs b/ConsumerApi.Tests.Integration/StepDefinitions/IdentitiesApiStepDefinitions.cs
index fe703536bb..705cb5152b 100644
--- a/ConsumerApi.Tests.Integration/StepDefinitions/IdentitiesApiStepDefinitions.cs
+++ b/ConsumerApi.Tests.Integration/StepDefinitions/IdentitiesApiStepDefinitions.cs
@@ -1,18 +1,18 @@
-using Backbone.ConsumerApi.Tests.Integration.API;
+using Backbone.ConsumerApi.Tests.Integration.API;
using Backbone.ConsumerApi.Tests.Integration.Configuration;
using Backbone.ConsumerApi.Tests.Integration.Extensions;
using Backbone.ConsumerApi.Tests.Integration.Models;
-using Backbone.Crypto;
using Backbone.Crypto.Abstractions;
using Microsoft.Extensions.Options;
-using Newtonsoft.Json;
namespace Backbone.ConsumerApi.Tests.Integration.StepDefinitions;
[Binding]
+[Scope(Feature = "POST Identities/Self/DeletionProcess")]
[Scope(Feature = "POST Identity")]
internal class IdentitiesApiStepDefinitions : BaseStepDefinitions
{
+ private HttpResponse? _response;
private HttpResponse? _identityResponse;
private HttpResponse? _challengeResponse;
@@ -20,23 +20,68 @@ public IdentitiesApiStepDefinitions(IOptions httpConfiguratio
base(httpConfiguration, signatureHelper, challengesApi, identitiesApi, devicesApi)
{ }
+ [Given("no active deletion process for the identity exists")]
+ public void GivenNoActiveDeletionProcessForTheUserExists()
+ {
+ }
+
+ [Given("an active deletion process for the identity exists")]
+ public async Task GivenAnActiveDeletionProcessForTheUserExists()
+ {
+ var requestConfiguration = new RequestConfiguration();
+ requestConfiguration.SupplementWith(_requestConfiguration);
+ requestConfiguration.Authenticate = true;
+ requestConfiguration.AuthenticationParameters.Username = "USRa";
+ requestConfiguration.AuthenticationParameters.Password = "a";
+
+ await _identitiesApi.StartDeletionProcess(requestConfiguration);
+ }
+
+ [When("a POST request is sent to the /Identities/Self/DeletionProcesses endpoint")]
+ public async Task WhenAPOSTRequestIsSentToTheIdentitiesSelfDeletionProcessEndpoint()
+ {
+ var requestConfiguration = new RequestConfiguration();
+ requestConfiguration.SupplementWith(_requestConfiguration);
+ requestConfiguration.Authenticate = true;
+ requestConfiguration.AuthenticationParameters.Username = "USRa";
+ requestConfiguration.AuthenticationParameters.Password = "a";
+
+ _response = await _identitiesApi.StartDeletionProcess(requestConfiguration);
+ }
+
+ [Then(@"the response content includes an error with the error code ""([^""]*)""")]
+ public void ThenTheResponseContentIncludesAnErrorWithTheErrorCode(string errorCode)
+ {
+ _response!.Content.Should().NotBeNull();
+ _response.Content!.Error.Should().NotBeNull();
+ _response.Content.Error!.Code.Should().Be(errorCode);
+ }
+
+ [Then("the response contains a Deletion Process")]
+ public void ThenTheResponseContainsADeletionProcess()
+ {
+ _response!.Content.Should().NotBeNull();
+ _response!.Content.Result.Should().NotBeNull();
+ _response!.AssertContentCompliesWithSchema();
+ }
+
[Given(@"a Challenge c")]
public async Task GivenAChallengeC()
{
- await CreateChallenge();
+ _challengeResponse = await CreateChallenge();
}
[When(@"a POST request is sent to the /Identities endpoint with a valid signature on c")]
public async Task WhenAPOSTRequestIsSentToTheIdentitiesEndpoint()
{
- _identityResponse = await CreateIdentity();
+ _identityResponse = await CreateIdentity(_challengeResponse!.Content.Result);
}
[Given(@"an Identity i")]
public async Task GivenAnIdentityI()
{
_challengeResponse = await CreateChallenge();
- _identityResponse = await CreateIdentity();
+ _identityResponse = await CreateIdentity(_challengeResponse.Content.Result);
}
[Then(@"the response contains a CreateIdentityResponse")]
@@ -51,7 +96,16 @@ public void ThenTheResponseContainsACreateIdentityResponse()
[Then(@"the response status code is (\d+) \(.+\)")]
public void ThenTheResponseStatusCodeIs(int expectedStatusCode)
{
- var actualStatusCode = (int)_identityResponse!.StatusCode;
- actualStatusCode.Should().Be(expectedStatusCode);
+ if (_identityResponse != null)
+ {
+ var actualStatusCode = (int)_identityResponse!.StatusCode;
+ actualStatusCode.Should().Be(expectedStatusCode);
+ }
+
+ if (_response != null)
+ {
+ var actualStatusCode = (int)_response!.StatusCode;
+ actualStatusCode.Should().Be(expectedStatusCode);
+ }
}
}
diff --git a/ConsumerApi/Controllers/AuthorizationController.cs b/ConsumerApi/Controllers/AuthorizationController.cs
index 416cf5af25..fdec765ee9 100644
--- a/ConsumerApi/Controllers/AuthorizationController.cs
+++ b/ConsumerApi/Controllers/AuthorizationController.cs
@@ -2,7 +2,7 @@
using Backbone.BuildingBlocks.Application.Abstractions.Exceptions;
using Backbone.ConsumerApi.Mvc;
using Backbone.Modules.Devices.Application;
-using Backbone.Modules.Devices.Domain.Entities;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
using Backbone.Tooling.Extensions;
using MediatR;
using Microsoft.AspNetCore;
diff --git a/ConsumerApi/DevicesDbContextSeeder.cs b/ConsumerApi/DevicesDbContextSeeder.cs
index d5a99b07a0..81ebb8b8d4 100644
--- a/ConsumerApi/DevicesDbContextSeeder.cs
+++ b/ConsumerApi/DevicesDbContextSeeder.cs
@@ -1,5 +1,6 @@
using Backbone.BuildingBlocks.API.Extensions;
using Backbone.Modules.Devices.Application.Extensions;
+using Backbone.Modules.Devices.Application.Tiers.Commands.CreateQueuedForDeletionTier;
using Backbone.Modules.Devices.Application.Tiers.Commands.CreateTier;
using Backbone.Modules.Devices.Application.Users.Commands.SeedTestUsers;
using Backbone.Modules.Devices.Domain.Aggregates.Tier;
@@ -28,6 +29,7 @@ private async Task SeedEverything(DevicesDbContext context)
await context.Database.EnsureCreatedAsync();
await SeedBasicTier(context);
+ await SeedQueuedForDeletionTier();
await SeedApplicationUsers(context);
await AddBasicTierToIdentities(context);
}
@@ -53,6 +55,11 @@ private async Task SeedBasicTier(DevicesDbContext context)
}
}
+ private async Task SeedQueuedForDeletionTier()
+ {
+ await _mediator.Send(new CreateQueuedForDeletionTierCommand());
+ }
+
private async Task AddBasicTierToIdentities(DevicesDbContext context)
{
var basicTier = await GetBasicTier(context);
diff --git a/ConsumerApi/QuotasDbContextSeeder.cs b/ConsumerApi/QuotasDbContextSeeder.cs
index efe86790eb..7af7c1ce0e 100644
--- a/ConsumerApi/QuotasDbContextSeeder.cs
+++ b/ConsumerApi/QuotasDbContextSeeder.cs
@@ -1,8 +1,10 @@
using Backbone.BuildingBlocks.API.Extensions;
using Backbone.Modules.Devices.Infrastructure.Persistence.Database;
+using Backbone.Modules.Quotas.Application.Tiers.Commands.SeedQueuedForDeletionTier;
using Backbone.Modules.Quotas.Domain.Aggregates.Identities;
using Backbone.Modules.Quotas.Domain.Aggregates.Tiers;
using Backbone.Modules.Quotas.Infrastructure.Persistence.Database;
+using MediatR;
using Microsoft.EntityFrameworkCore;
namespace Backbone.ConsumerApi;
@@ -10,16 +12,24 @@ namespace Backbone.ConsumerApi;
public class QuotasDbContextSeeder : IDbSeeder
{
private readonly DevicesDbContext _devicesDbContext;
+ private readonly IMediator _mediator;
- public QuotasDbContextSeeder(DevicesDbContext devicesDbContext)
+ public QuotasDbContextSeeder(DevicesDbContext devicesDbContext, IMediator mediator)
{
_devicesDbContext = devicesDbContext;
+ _mediator = mediator;
}
public async Task SeedAsync(QuotasDbContext context)
{
await SeedTier(context);
await AddTierToIdentities(context);
+ await EnsureQueuedForDeletionTierWithQuotas();
+ }
+
+ private async Task EnsureQueuedForDeletionTierWithQuotas()
+ {
+ await _mediator.Send(new SeedQueuedForDeletionTierCommand());
}
private async Task AddTierToIdentities(QuotasDbContext context)
diff --git a/Modules/Devices/src/Devices.AdminCli/ServiceLocator.cs b/Modules/Devices/src/Devices.AdminCli/ServiceLocator.cs
index 17d3a8a583..aa4ca7ad6d 100644
--- a/Modules/Devices/src/Devices.AdminCli/ServiceLocator.cs
+++ b/Modules/Devices/src/Devices.AdminCli/ServiceLocator.cs
@@ -1,6 +1,6 @@
-using Backbone.BuildingBlocks.Application.QuotaCheck;
+using Backbone.BuildingBlocks.Application.QuotaCheck;
using Backbone.Modules.Devices.Application.Extensions;
-using Backbone.Modules.Devices.Domain.Entities;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
using Backbone.Modules.Devices.Infrastructure.OpenIddict;
using Backbone.Modules.Devices.Infrastructure.Persistence;
using Backbone.Modules.Devices.Infrastructure.Persistence.Database;
diff --git a/Modules/Devices/src/Devices.Application/ApplicationErrors.cs b/Modules/Devices/src/Devices.Application/ApplicationErrors.cs
index 5e9ce4b522..a9097e62fe 100644
--- a/Modules/Devices/src/Devices.Application/ApplicationErrors.cs
+++ b/Modules/Devices/src/Devices.Application/ApplicationErrors.cs
@@ -66,4 +66,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 class Identities
+ {
+ public static ApplicationError CanOnlyStartDeletionProcessForOwnIdentity()
+ {
+ return new ApplicationError("error.platform.validation.identity.canOnlyStartDeletionProcessForOwnIdentity", "You can only start a deletion process for your own identity.");
+ }
+ }
}
diff --git a/Modules/Devices/src/Devices.Application/DTOs/IdentitySummaryDTO.cs b/Modules/Devices/src/Devices.Application/DTOs/IdentitySummaryDTO.cs
index 448422af7a..e8551bd675 100644
--- a/Modules/Devices/src/Devices.Application/DTOs/IdentitySummaryDTO.cs
+++ b/Modules/Devices/src/Devices.Application/DTOs/IdentitySummaryDTO.cs
@@ -1,6 +1,6 @@
-using Backbone.DevelopmentKit.Identity.ValueObjects;
+using Backbone.DevelopmentKit.Identity.ValueObjects;
using Backbone.Modules.Devices.Application.Devices.DTOs;
-using Backbone.Modules.Devices.Domain.Entities;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
namespace Backbone.Modules.Devices.Application.DTOs;
diff --git a/Modules/Devices/src/Devices.Application/Devices/Commands/ChangePassword/Handler.cs b/Modules/Devices/src/Devices.Application/Devices/Commands/ChangePassword/Handler.cs
index 60581f0fd7..ac627ea1d3 100644
--- a/Modules/Devices/src/Devices.Application/Devices/Commands/ChangePassword/Handler.cs
+++ b/Modules/Devices/src/Devices.Application/Devices/Commands/ChangePassword/Handler.cs
@@ -1,8 +1,8 @@
-using Backbone.BuildingBlocks.Application.Abstractions.Exceptions;
+using Backbone.BuildingBlocks.Application.Abstractions.Exceptions;
using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext;
using Backbone.DevelopmentKit.Identity.ValueObjects;
using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository;
-using Backbone.Modules.Devices.Domain.Entities;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
using MediatR;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
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 8197da4f32..abda78be05 100644
--- a/Modules/Devices/src/Devices.Application/Devices/Commands/DeleteDevice/Handler.cs
+++ b/Modules/Devices/src/Devices.Application/Devices/Commands/DeleteDevice/Handler.cs
@@ -3,6 +3,7 @@
using Backbone.DevelopmentKit.Identity.ValueObjects;
using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository;
using Backbone.Modules.Devices.Domain.Entities;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
using MediatR;
using Microsoft.Extensions.Logging;
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 b766974937..22cddc7dca 100644
--- a/Modules/Devices/src/Devices.Application/Devices/Commands/RegisterDevice/Handler.cs
+++ b/Modules/Devices/src/Devices.Application/Devices/Commands/RegisterDevice/Handler.cs
@@ -7,7 +7,7 @@
using Backbone.DevelopmentKit.Identity.ValueObjects;
using Backbone.Modules.Devices.Application.Devices.DTOs;
using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository;
-using Backbone.Modules.Devices.Domain.Entities;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
using MediatR;
using Microsoft.Extensions.Logging;
diff --git a/Modules/Devices/src/Devices.Application/Devices/DTOs/DeviceDTO.cs b/Modules/Devices/src/Devices.Application/Devices/DTOs/DeviceDTO.cs
index 245254e310..fe026d6816 100644
--- a/Modules/Devices/src/Devices.Application/Devices/DTOs/DeviceDTO.cs
+++ b/Modules/Devices/src/Devices.Application/Devices/DTOs/DeviceDTO.cs
@@ -1,7 +1,7 @@
using AutoMapper;
using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.Mapping;
using Backbone.DevelopmentKit.Identity.ValueObjects;
-using Backbone.Modules.Devices.Domain.Entities;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
namespace Backbone.Modules.Devices.Application.Devices.DTOs;
diff --git a/Modules/Devices/src/Devices.Application/Devices/Queries/GetActiveDevice/Handler.cs b/Modules/Devices/src/Devices.Application/Devices/Queries/GetActiveDevice/Handler.cs
index 9550edf9db..36bdc7dff6 100644
--- a/Modules/Devices/src/Devices.Application/Devices/Queries/GetActiveDevice/Handler.cs
+++ b/Modules/Devices/src/Devices.Application/Devices/Queries/GetActiveDevice/Handler.cs
@@ -3,7 +3,7 @@
using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext;
using Backbone.Modules.Devices.Application.Devices.DTOs;
using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository;
-using Backbone.Modules.Devices.Domain.Entities;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
using MediatR;
namespace Backbone.Modules.Devices.Application.Devices.Queries.GetActiveDevice;
diff --git a/Modules/Devices/src/Devices.Application/Extensions/IEventBusExtensions.cs b/Modules/Devices/src/Devices.Application/Extensions/IEventBusExtensions.cs
index 845ee14ace..b6bca76c51 100644
--- a/Modules/Devices/src/Devices.Application/Extensions/IEventBusExtensions.cs
+++ b/Modules/Devices/src/Devices.Application/Extensions/IEventBusExtensions.cs
@@ -1,6 +1,8 @@
using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus;
using Backbone.Modules.Devices.Application.IntegrationEvents.Incoming.DatawalletModificationCreated;
using Backbone.Modules.Devices.Application.IntegrationEvents.Incoming.ExternalEventCreated;
+using Backbone.Modules.Devices.Application.IntegrationEvents.Incoming.IdentityDeletionProcessStarted;
+using Backbone.Modules.Devices.Application.IntegrationEvents.Outgoing;
namespace Backbone.Modules.Devices.Application.Extensions;
@@ -15,5 +17,6 @@ private static void SubscribeToSynchronizationEvents(IEventBus eventBus)
{
eventBus.Subscribe();
eventBus.Subscribe();
+ eventBus.Subscribe();
}
}
diff --git a/Modules/Devices/src/Devices.Application/Extensions/TierQueryableExtensions.cs b/Modules/Devices/src/Devices.Application/Extensions/TierQueryableExtensions.cs
index 355872d147..7a761a624d 100644
--- a/Modules/Devices/src/Devices.Application/Extensions/TierQueryableExtensions.cs
+++ b/Modules/Devices/src/Devices.Application/Extensions/TierQueryableExtensions.cs
@@ -8,7 +8,6 @@ public static class TierQueryableExtensions
public static async Task GetBasicTier(this IQueryable query, CancellationToken cancellationToken)
{
var basicTier = await query.FirstOrDefaultAsync(t => t.Name == TierName.BASIC_DEFAULT_NAME, cancellationToken);
-
return basicTier;
}
}
diff --git a/Modules/Devices/src/Devices.Application/Identities/Commands/ApproveDeletionProcess/ApproveDeletionProcessCommand.cs b/Modules/Devices/src/Devices.Application/Identities/Commands/ApproveDeletionProcess/ApproveDeletionProcessCommand.cs
new file mode 100644
index 0000000000..ea0b10c16e
--- /dev/null
+++ b/Modules/Devices/src/Devices.Application/Identities/Commands/ApproveDeletionProcess/ApproveDeletionProcessCommand.cs
@@ -0,0 +1,13 @@
+using MediatR;
+
+namespace Backbone.Modules.Devices.Application.Identities.Commands.ApproveDeletionProcess;
+
+public class ApproveDeletionProcessCommand : IRequest
+{
+ public ApproveDeletionProcessCommand(string deletionProcessId)
+ {
+ DeletionProcessId = deletionProcessId;
+ }
+
+ public string DeletionProcessId { get; set; }
+}
diff --git a/Modules/Devices/src/Devices.Application/Identities/Commands/ApproveDeletionProcess/ApproveDeletionProcessResponse.cs b/Modules/Devices/src/Devices.Application/Identities/Commands/ApproveDeletionProcess/ApproveDeletionProcessResponse.cs
new file mode 100644
index 0000000000..125b4b27e7
--- /dev/null
+++ b/Modules/Devices/src/Devices.Application/Identities/Commands/ApproveDeletionProcess/ApproveDeletionProcessResponse.cs
@@ -0,0 +1,21 @@
+using Backbone.Modules.Devices.Domain.Entities.Identities;
+
+namespace Backbone.Modules.Devices.Application.Identities.Commands.ApproveDeletionProcess;
+
+public class ApproveDeletionProcessResponse
+{
+ public ApproveDeletionProcessResponse(IdentityDeletionProcess deletionProcess)
+ {
+ Id = deletionProcess.Id;
+ Status = deletionProcess.Status;
+ CreatedAt = deletionProcess.CreatedAt;
+ ApprovedAt = deletionProcess.ApprovedAt!.Value;
+ ApprovedByDevice = deletionProcess.ApprovedByDevice;
+ }
+
+ public string Id { get; }
+ public DeletionProcessStatus Status { get; }
+ public DateTime CreatedAt { get; }
+ public DateTime ApprovedAt { get; }
+ public string ApprovedByDevice { get; }
+}
diff --git a/Modules/Devices/src/Devices.Application/Identities/Commands/ApproveDeletionProcess/Handler.cs b/Modules/Devices/src/Devices.Application/Identities/Commands/ApproveDeletionProcess/Handler.cs
new file mode 100644
index 0000000000..eb532933f8
--- /dev/null
+++ b/Modules/Devices/src/Devices.Application/Identities/Commands/ApproveDeletionProcess/Handler.cs
@@ -0,0 +1,47 @@
+using Backbone.BuildingBlocks.Application.Abstractions.Exceptions;
+using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus;
+using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext;
+using Backbone.BuildingBlocks.Domain;
+using Backbone.BuildingBlocks.Domain.Errors;
+using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository;
+using Backbone.Modules.Devices.Application.IntegrationEvents.Outgoing;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
+using MediatR;
+
+namespace Backbone.Modules.Devices.Application.Identities.Commands.ApproveDeletionProcess;
+
+public class Handler : IRequestHandler
+{
+ private readonly IIdentitiesRepository _identitiesRepository;
+ private readonly IUserContext _userContext;
+ private readonly IEventBus _eventBus;
+
+ public Handler(IIdentitiesRepository identitiesRepository, IUserContext userContext, IEventBus eventBus)
+ {
+ _identitiesRepository = identitiesRepository;
+ _userContext = userContext;
+ _eventBus = eventBus;
+ }
+
+ public async Task Handle(ApproveDeletionProcessCommand request, CancellationToken cancellationToken)
+ {
+ var identity = await _identitiesRepository.FindByAddress(_userContext.GetAddress(), cancellationToken, track: true) ?? throw new NotFoundException(nameof(Identity));
+
+ var identityDeletionProcessIdResult = IdentityDeletionProcessId.Create(request.DeletionProcessId);
+
+ if (identityDeletionProcessIdResult.IsFailure)
+ throw new DomainException(identityDeletionProcessIdResult.Error);
+
+ var identityDeletionProcessId = identityDeletionProcessIdResult.Value;
+
+ var oldTierId = identity.TierId;
+ var deletionProcess = identity.ApproveDeletionProcess(identityDeletionProcessId, _userContext.GetDeviceId());
+ var newTierId = identity.TierId;
+
+ await _identitiesRepository.Update(identity, cancellationToken);
+
+ _eventBus.Publish(new TierOfIdentityChangedIntegrationEvent(identity, oldTierId, newTierId));
+
+ return new ApproveDeletionProcessResponse(deletionProcess);
+ }
+}
diff --git a/Modules/Devices/src/Devices.Application/Identities/Commands/CreateIdentity/CreateIdentityCommand.cs b/Modules/Devices/src/Devices.Application/Identities/Commands/CreateIdentity/CreateIdentityCommand.cs
index 71f91e504f..838333bcf2 100644
--- a/Modules/Devices/src/Devices.Application/Identities/Commands/CreateIdentity/CreateIdentityCommand.cs
+++ b/Modules/Devices/src/Devices.Application/Identities/Commands/CreateIdentity/CreateIdentityCommand.cs
@@ -10,4 +10,5 @@ public class CreateIdentityCommand : IRequest
public string DevicePassword { get; set; }
public byte IdentityVersion { get; set; }
public SignedChallengeDTO SignedChallenge { get; set; }
+ public bool ShouldValidateChallenge { get; set; } = true; // Used to avoid challenge validation when creating Identities through the AdminApi for Integration tests purposes.
}
diff --git a/Modules/Devices/src/Devices.Application/Identities/Commands/CreateIdentity/Handler.cs b/Modules/Devices/src/Devices.Application/Identities/Commands/CreateIdentity/Handler.cs
index f875b36cb7..306a609198 100644
--- a/Modules/Devices/src/Devices.Application/Identities/Commands/CreateIdentity/Handler.cs
+++ b/Modules/Devices/src/Devices.Application/Identities/Commands/CreateIdentity/Handler.cs
@@ -4,7 +4,7 @@
using Backbone.Modules.Devices.Application.Devices.DTOs;
using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository;
using Backbone.Modules.Devices.Application.IntegrationEvents.Outgoing;
-using Backbone.Modules.Devices.Domain.Entities;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
using MediatR;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -33,7 +33,8 @@ public Handler(ChallengeValidator challengeValidator, ILogger logger, I
public async Task Handle(CreateIdentityCommand command, CancellationToken cancellationToken)
{
var publicKey = PublicKey.FromBytes(command.IdentityPublicKey);
- await _challengeValidator.Validate(command.SignedChallenge, publicKey);
+ if (command.ShouldValidateChallenge)
+ await _challengeValidator.Validate(command.SignedChallenge, publicKey);
_logger.LogTrace("Challenge sucessfully validated.");
diff --git a/Modules/Devices/src/Devices.Application/Identities/Commands/SendDeletionProcessApprovalReminder/Handler.cs b/Modules/Devices/src/Devices.Application/Identities/Commands/SendDeletionProcessApprovalReminder/Handler.cs
new file mode 100644
index 0000000000..7927c159ef
--- /dev/null
+++ b/Modules/Devices/src/Devices.Application/Identities/Commands/SendDeletionProcessApprovalReminder/Handler.cs
@@ -0,0 +1,73 @@
+using Backbone.BuildingBlocks.Application.Abstractions.Exceptions;
+using Backbone.BuildingBlocks.Application.PushNotifications;
+using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository;
+using Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.DeletionProcess;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
+using Backbone.Tooling;
+using MediatR;
+
+namespace Backbone.Modules.Devices.Application.Identities.Commands.SendDeletionProcessApprovalReminder;
+
+public class Handler : IRequestHandler
+{
+ private readonly IIdentitiesRepository _identitiesRepository;
+ private readonly IPushNotificationSender _pushNotificationSender;
+
+ public Handler(IIdentitiesRepository identitiesRepository, IPushNotificationSender pushNotificationSender)
+ {
+ _identitiesRepository = identitiesRepository;
+ _pushNotificationSender = pushNotificationSender;
+ }
+
+ public async Task Handle(SendDeletionProcessApprovalReminderCommand request, CancellationToken cancellationToken)
+ {
+ var identitiesWithDeletionProcessWaitingForApproval = await _identitiesRepository.FindAllWithDeletionProcessInStatus(DeletionProcessStatus.WaitingForApproval, cancellationToken, track: true);
+
+ foreach (var identity in identitiesWithDeletionProcessWaitingForApproval)
+ {
+ var waitingForApprovalDeletionProcess = identity.GetDeletionProcessInStatus(DeletionProcessStatus.WaitingForApproval) ?? throw new NotFoundException(nameof(IdentityDeletionProcess));
+ var endOfApprovalPeriod = waitingForApprovalDeletionProcess.CreatedAt.AddDays(IdentityDeletionConfiguration.MaxApprovalTime);
+ var daysUntilApprovalPeriodEnds = (endOfApprovalPeriod - SystemTime.UtcNow).Days;
+
+ if (waitingForApprovalDeletionProcess.ApprovalReminder3SentAt != null) continue;
+
+ if (daysUntilApprovalPeriodEnds <= IdentityDeletionConfiguration.ApprovalReminder3.Time)
+ {
+ await SendReminder3(identity, daysUntilApprovalPeriodEnds, cancellationToken);
+ continue;
+ }
+
+ if (waitingForApprovalDeletionProcess.ApprovalReminder2SentAt != null) continue;
+ if (daysUntilApprovalPeriodEnds <= IdentityDeletionConfiguration.ApprovalReminder2.Time)
+ {
+ await SendReminder2(identity, daysUntilApprovalPeriodEnds, cancellationToken);
+ continue;
+ }
+
+ if (waitingForApprovalDeletionProcess.ApprovalReminder1SentAt == null && daysUntilApprovalPeriodEnds <= IdentityDeletionConfiguration.ApprovalReminder1.Time)
+ {
+ await SendReminder1(identity, daysUntilApprovalPeriodEnds, cancellationToken);
+ }
+ }
+ }
+
+ private async Task SendReminder3(Identity identity, int daysUntilApprovalPeriodEnds, CancellationToken cancellationToken)
+ {
+ await _pushNotificationSender.SendNotification(identity.Address, new DeletionProcessWaitingForApprovalReminderPushNotification(daysUntilApprovalPeriodEnds), cancellationToken);
+ identity.DeletionProcessApprovalReminder3Sent();
+ await _identitiesRepository.Update(identity, cancellationToken);
+ }
+
+ private async Task SendReminder2(Identity identity, int daysUntilApprovalPeriodEnds, CancellationToken cancellationToken)
+ {
+ await _pushNotificationSender.SendNotification(identity.Address, new DeletionProcessWaitingForApprovalReminderPushNotification(daysUntilApprovalPeriodEnds), cancellationToken);
+ identity.DeletionProcessApprovalReminder2Sent();
+ await _identitiesRepository.Update(identity, cancellationToken);
+ }
+ private async Task SendReminder1(Identity identity, int daysUntilApprovalPeriodEnds, CancellationToken cancellationToken)
+ {
+ await _pushNotificationSender.SendNotification(identity.Address, new DeletionProcessWaitingForApprovalReminderPushNotification(daysUntilApprovalPeriodEnds), cancellationToken);
+ identity.DeletionProcessApprovalReminder1Sent();
+ await _identitiesRepository.Update(identity, cancellationToken);
+ }
+}
diff --git a/Modules/Devices/src/Devices.Application/Identities/Commands/SendDeletionProcessApprovalReminder/SendDeletionProcessApprovalReminderCommand.cs b/Modules/Devices/src/Devices.Application/Identities/Commands/SendDeletionProcessApprovalReminder/SendDeletionProcessApprovalReminderCommand.cs
new file mode 100644
index 0000000000..d61524c0ef
--- /dev/null
+++ b/Modules/Devices/src/Devices.Application/Identities/Commands/SendDeletionProcessApprovalReminder/SendDeletionProcessApprovalReminderCommand.cs
@@ -0,0 +1,5 @@
+using MediatR;
+
+namespace Backbone.Modules.Devices.Application.Identities.Commands.SendDeletionProcessApprovalReminder;
+
+public class SendDeletionProcessApprovalReminderCommand : IRequest;
diff --git a/Modules/Devices/src/Devices.Application/Identities/Commands/SendDeletionProcessApprovalReminders/Handler.cs b/Modules/Devices/src/Devices.Application/Identities/Commands/SendDeletionProcessApprovalReminders/Handler.cs
new file mode 100644
index 0000000000..69b1bd3c63
--- /dev/null
+++ b/Modules/Devices/src/Devices.Application/Identities/Commands/SendDeletionProcessApprovalReminders/Handler.cs
@@ -0,0 +1,86 @@
+using Backbone.BuildingBlocks.Application.Abstractions.Exceptions;
+using Backbone.BuildingBlocks.Application.PushNotifications;
+using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository;
+using Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.DeletionProcess;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
+using Backbone.Tooling;
+using MediatR;
+using Microsoft.Extensions.Logging;
+
+namespace Backbone.Modules.Devices.Application.Identities.Commands.SendDeletionProcessApprovalReminders;
+
+public class Handler : IRequestHandler
+{
+ private readonly IIdentitiesRepository _identitiesRepository;
+ private readonly IPushNotificationSender _pushNotificationSender;
+ private readonly ILogger _logger;
+
+
+ public Handler(IIdentitiesRepository identitiesRepository, IPushNotificationSender pushNotificationSender, ILogger logger)
+ {
+ _identitiesRepository = identitiesRepository;
+ _pushNotificationSender = pushNotificationSender;
+ _logger = logger;
+ }
+
+ public async Task Handle(SendDeletionProcessApprovalRemindersCommand request, CancellationToken cancellationToken)
+ {
+ var identities = await _identitiesRepository.FindAllWithDeletionProcessInStatus(DeletionProcessStatus.WaitingForApproval, cancellationToken, track: true);
+
+ _logger.LogTrace("Processing identities with deletion process in status 'Waiting for Approval' ...");
+
+ foreach (var identity in identities)
+ {
+ var deletionProcess = identity.GetDeletionProcessInStatus(DeletionProcessStatus.WaitingForApproval) ?? throw new NotFoundException(nameof(IdentityDeletionProcess));
+ var endOfApprovalPeriod = deletionProcess.GetEndOfApprovalPeriod();
+ var daysUntilApprovalPeriodEnds = (endOfApprovalPeriod - SystemTime.UtcNow).Days;
+
+ if (deletionProcess.ApprovalReminder3SentAt != null)
+ {
+ _logger.LogTrace($"Identity '{identity.Address}': No Approval reminder sent.");
+ continue;
+ }
+
+ if (daysUntilApprovalPeriodEnds <= IdentityDeletionConfiguration.ApprovalReminder3.Time)
+ {
+ await SendReminder3(identity, daysUntilApprovalPeriodEnds, deletionProcess.Id, cancellationToken);
+ continue;
+ }
+
+ if (deletionProcess.ApprovalReminder2SentAt != null) continue;
+ if (daysUntilApprovalPeriodEnds <= IdentityDeletionConfiguration.ApprovalReminder2.Time)
+ {
+ await SendReminder2(identity, daysUntilApprovalPeriodEnds, deletionProcess.Id, cancellationToken);
+ continue;
+ }
+
+ if (deletionProcess.ApprovalReminder1SentAt == null && daysUntilApprovalPeriodEnds <= IdentityDeletionConfiguration.ApprovalReminder1.Time)
+ {
+ await SendReminder1(identity, daysUntilApprovalPeriodEnds, deletionProcess.Id, cancellationToken);
+ }
+ }
+ }
+
+ private async Task SendReminder3(Identity identity, int daysUntilApprovalPeriodEnds, IdentityDeletionProcessId deletionProcessId, CancellationToken cancellationToken)
+ {
+ await _pushNotificationSender.SendNotification(identity.Address, new DeletionProcessWaitingForApprovalReminderPushNotification(daysUntilApprovalPeriodEnds), cancellationToken);
+ identity.DeletionProcessApprovalReminder3Sent();
+ await _identitiesRepository.Update(identity, cancellationToken);
+ _logger.LogTrace($"Identity '{identity.Address}': Approval reminder 3 sent for deletion process '{deletionProcessId}'");
+ }
+
+ private async Task SendReminder2(Identity identity, int daysUntilApprovalPeriodEnds, IdentityDeletionProcessId deletionProcessId, CancellationToken cancellationToken)
+ {
+ await _pushNotificationSender.SendNotification(identity.Address, new DeletionProcessWaitingForApprovalReminderPushNotification(daysUntilApprovalPeriodEnds), cancellationToken);
+ identity.DeletionProcessApprovalReminder2Sent();
+ await _identitiesRepository.Update(identity, cancellationToken);
+ _logger.LogTrace($"Identity '{identity.Address}': Approval reminder 2 sent for deletion process '{deletionProcessId}'");
+ }
+ private async Task SendReminder1(Identity identity, int daysUntilApprovalPeriodEnds, IdentityDeletionProcessId deletionProcessId, CancellationToken cancellationToken)
+ {
+ await _pushNotificationSender.SendNotification(identity.Address, new DeletionProcessWaitingForApprovalReminderPushNotification(daysUntilApprovalPeriodEnds), cancellationToken);
+ identity.DeletionProcessApprovalReminder1Sent();
+ await _identitiesRepository.Update(identity, cancellationToken);
+ _logger.LogTrace($"Identity '{identity.Address}': Approval reminder 1 sent for deletion process '{deletionProcessId}'");
+ }
+}
diff --git a/Modules/Devices/src/Devices.Application/Identities/Commands/SendDeletionProcessApprovalReminders/SendDeletionProcessApprovalRemindersCommand.cs b/Modules/Devices/src/Devices.Application/Identities/Commands/SendDeletionProcessApprovalReminders/SendDeletionProcessApprovalRemindersCommand.cs
new file mode 100644
index 0000000000..34d01ea0f5
--- /dev/null
+++ b/Modules/Devices/src/Devices.Application/Identities/Commands/SendDeletionProcessApprovalReminders/SendDeletionProcessApprovalRemindersCommand.cs
@@ -0,0 +1,5 @@
+using MediatR;
+
+namespace Backbone.Modules.Devices.Application.Identities.Commands.SendDeletionProcessApprovalReminders;
+
+public class SendDeletionProcessApprovalRemindersCommand : IRequest;
diff --git a/Modules/Devices/src/Devices.Application/Identities/Commands/SendDeletionProcessGracePeriodReminders/Handler.cs b/Modules/Devices/src/Devices.Application/Identities/Commands/SendDeletionProcessGracePeriodReminders/Handler.cs
new file mode 100644
index 0000000000..4ced4a4a4a
--- /dev/null
+++ b/Modules/Devices/src/Devices.Application/Identities/Commands/SendDeletionProcessGracePeriodReminders/Handler.cs
@@ -0,0 +1,84 @@
+using Backbone.BuildingBlocks.Application.Abstractions.Exceptions;
+using Backbone.BuildingBlocks.Application.PushNotifications;
+using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository;
+using Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.DeletionProcess;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
+using Backbone.Tooling;
+using MediatR;
+using Microsoft.Extensions.Logging;
+
+namespace Backbone.Modules.Devices.Application.Identities.Commands.SendDeletionProcessGracePeriodReminders;
+
+public class Handler : IRequestHandler
+{
+ private readonly IIdentitiesRepository _identitiesRepository;
+ private readonly IPushNotificationSender _pushSender;
+ private readonly ILogger _logger;
+
+ public Handler(IIdentitiesRepository identitiesRepository, IPushNotificationSender pushSender, ILogger logger)
+ {
+ _identitiesRepository = identitiesRepository;
+ _pushSender = pushSender;
+ _logger = logger;
+ }
+
+ public async Task Handle(SendDeletionProcessGracePeriodRemindersCommand request, CancellationToken cancellationToken)
+ {
+ var identities = await _identitiesRepository.FindAllWithDeletionProcessInStatus(DeletionProcessStatus.Approved, cancellationToken, track: true);
+
+ _logger.LogTrace("Processing identities with deletion process in status 'Approved' ...");
+
+ foreach (var identity in identities)
+ {
+ var deletionProcess = identity.GetDeletionProcessInStatus(DeletionProcessStatus.Approved) ?? throw new NotFoundException(nameof(IdentityDeletionProcess));
+ var daysToDeletion = (deletionProcess.GracePeriodEndsAt!.Value - SystemTime.UtcNow).Days;
+
+ if (deletionProcess.GracePeriodReminder3SentAt != null)
+ {
+ _logger.LogTrace($"Identity '{identity.Address}': No Grace period reminder sent.");
+ continue;
+ }
+
+ if (daysToDeletion <= IdentityDeletionConfiguration.GracePeriodNotification3.Time)
+ {
+ await SendReminder3(identity, daysToDeletion, deletionProcess.Id, cancellationToken);
+ continue;
+ }
+
+ if (deletionProcess.GracePeriodReminder2SentAt != null) continue;
+ if (daysToDeletion <= IdentityDeletionConfiguration.GracePeriodNotification2.Time)
+ {
+ await SendReminder2(identity, daysToDeletion, deletionProcess.Id, cancellationToken);
+ continue;
+ }
+
+ if (deletionProcess.GracePeriodReminder1SentAt == null && daysToDeletion <= IdentityDeletionConfiguration.GracePeriodNotification1.Time)
+ {
+ await SendReminder1(identity, daysToDeletion, deletionProcess.Id, cancellationToken);
+ }
+ }
+ }
+
+ private async Task SendReminder3(Identity identity, int daysToDeletion, IdentityDeletionProcessId deletionProcessId, CancellationToken cancellationToken)
+ {
+ await _pushSender.SendNotification(identity.Address, new DeletionProcessGracePeriodNotification(daysToDeletion), cancellationToken);
+ identity.DeletionGracePeriodReminder3Sent();
+ await _identitiesRepository.Update(identity, cancellationToken);
+ _logger.LogTrace($"Identity '{identity.Address}': Grace period reminder 3 sent for deletion process '{deletionProcessId}'");
+ }
+
+ private async Task SendReminder2(Identity identity, int daysToDeletion, IdentityDeletionProcessId deletionProcessId, CancellationToken cancellationToken)
+ {
+ await _pushSender.SendNotification(identity.Address, new DeletionProcessGracePeriodNotification(daysToDeletion), cancellationToken);
+ identity.DeletionGracePeriodReminder2Sent();
+ await _identitiesRepository.Update(identity, cancellationToken);
+ _logger.LogTrace($"Identity '{identity.Address}': Grace period reminder 2 sent for deletion process '{deletionProcessId}'");
+ }
+ private async Task SendReminder1(Identity identity, int daysToDeletion, IdentityDeletionProcessId deletionProcessId, CancellationToken cancellationToken)
+ {
+ await _pushSender.SendNotification(identity.Address, new DeletionProcessGracePeriodNotification(daysToDeletion), cancellationToken);
+ identity.DeletionGracePeriodReminder1Sent();
+ await _identitiesRepository.Update(identity, cancellationToken);
+ _logger.LogTrace($"Identity '{identity.Address}': Grace period reminder 1 sent for deletion process '{deletionProcessId}'");
+ }
+}
diff --git a/Modules/Devices/src/Devices.Application/Identities/Commands/SendDeletionProcessGracePeriodReminders/SendDeletionProcessGracePeriodRemindersCommand.cs b/Modules/Devices/src/Devices.Application/Identities/Commands/SendDeletionProcessGracePeriodReminders/SendDeletionProcessGracePeriodRemindersCommand.cs
new file mode 100644
index 0000000000..cc664399d5
--- /dev/null
+++ b/Modules/Devices/src/Devices.Application/Identities/Commands/SendDeletionProcessGracePeriodReminders/SendDeletionProcessGracePeriodRemindersCommand.cs
@@ -0,0 +1,6 @@
+using MediatR;
+
+namespace Backbone.Modules.Devices.Application.Identities.Commands.SendDeletionProcessGracePeriodReminders;
+public class SendDeletionProcessGracePeriodRemindersCommand : IRequest
+{
+}
diff --git a/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsOwner/Handler.cs b/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsOwner/Handler.cs
new file mode 100644
index 0000000000..278ae764aa
--- /dev/null
+++ b/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsOwner/Handler.cs
@@ -0,0 +1,38 @@
+using Backbone.BuildingBlocks.Application.Abstractions.Exceptions;
+using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus;
+using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext;
+using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository;
+using Backbone.Modules.Devices.Application.IntegrationEvents.Outgoing;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
+using MediatR;
+
+namespace Backbone.Modules.Devices.Application.Identities.Commands.StartDeletionProcessAsOwner;
+
+public class Handler : IRequestHandler
+{
+ private readonly IIdentitiesRepository _identitiesRepository;
+ private readonly IUserContext _userContext;
+ private readonly IEventBus _eventBus;
+
+ public Handler(IIdentitiesRepository identitiesRepository, IUserContext userContext, IEventBus eventBus)
+ {
+ _identitiesRepository = identitiesRepository;
+ _userContext = userContext;
+ _eventBus = eventBus;
+ }
+
+ public async Task Handle(StartDeletionProcessAsOwnerCommand request, CancellationToken cancellationToken)
+ {
+ var identity = await _identitiesRepository.FindByAddress(_userContext.GetAddress(), cancellationToken, true) ?? throw new NotFoundException(nameof(Identity));
+
+ var oldTierId = identity.TierId;
+ var deletionProcess = identity.StartDeletionProcessAsOwner(_userContext.GetDeviceId());
+ var newTierId = identity.TierId;
+
+ _eventBus.Publish(new TierOfIdentityChangedIntegrationEvent(identity, oldTierId, newTierId));
+
+ await _identitiesRepository.Update(identity, cancellationToken);
+
+ return new StartDeletionProcessAsOwnerResponse(deletionProcess);
+ }
+}
diff --git a/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsOwner/StartDeletionProcessAsOwnerCommand.cs b/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsOwner/StartDeletionProcessAsOwnerCommand.cs
new file mode 100644
index 0000000000..d38fff3de1
--- /dev/null
+++ b/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsOwner/StartDeletionProcessAsOwnerCommand.cs
@@ -0,0 +1,7 @@
+using MediatR;
+
+namespace Backbone.Modules.Devices.Application.Identities.Commands.StartDeletionProcessAsOwner;
+
+public class StartDeletionProcessAsOwnerCommand : IRequest
+{
+}
diff --git a/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsOwner/StartDeletionProcessAsOwnerResponse.cs b/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsOwner/StartDeletionProcessAsOwnerResponse.cs
new file mode 100644
index 0000000000..933c7bb01a
--- /dev/null
+++ b/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsOwner/StartDeletionProcessAsOwnerResponse.cs
@@ -0,0 +1,26 @@
+using Backbone.DevelopmentKit.Identity.ValueObjects;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
+
+namespace Backbone.Modules.Devices.Application.Identities.Commands.StartDeletionProcessAsOwner;
+
+public class StartDeletionProcessAsOwnerResponse
+{
+ public StartDeletionProcessAsOwnerResponse(IdentityDeletionProcess deletionProcess)
+ {
+ Id = deletionProcess.Id;
+ Status = deletionProcess.Status;
+ CreatedAt = deletionProcess.CreatedAt;
+ ApprovedAt = deletionProcess.ApprovedAt.GetValueOrDefault();
+ ApprovedByDevice = deletionProcess.ApprovedByDevice;
+ GracePeriodEndsAt = deletionProcess.GracePeriodEndsAt.GetValueOrDefault();
+ }
+
+ public string Id { get; set; }
+ public DeletionProcessStatus Status { get; set; }
+ public DateTime CreatedAt { get; set; }
+
+ public DateTime ApprovedAt { get; set; }
+ public DeviceId ApprovedByDevice { get; set; }
+
+ public DateTime GracePeriodEndsAt { get; set; }
+}
diff --git a/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsSupport/Handler.cs b/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsSupport/Handler.cs
new file mode 100644
index 0000000000..403d7b8271
--- /dev/null
+++ b/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsSupport/Handler.cs
@@ -0,0 +1,32 @@
+using Backbone.BuildingBlocks.Application.Abstractions.Exceptions;
+using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus;
+using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository;
+using Backbone.Modules.Devices.Application.IntegrationEvents.Outgoing;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
+using MediatR;
+
+namespace Backbone.Modules.Devices.Application.Identities.Commands.StartDeletionProcessAsSupport;
+
+public class Handler : IRequestHandler
+{
+ private readonly IIdentitiesRepository _identitiesRepository;
+ private readonly IEventBus _eventBus;
+
+ public Handler(IIdentitiesRepository identitiesRepository, IEventBus eventBus)
+ {
+ _identitiesRepository = identitiesRepository;
+ _eventBus = eventBus;
+ }
+
+ public async Task Handle(StartDeletionProcessAsSupportCommand request, CancellationToken cancellationToken)
+ {
+ var identity = await _identitiesRepository.FindByAddress(request.IdentityAddress, cancellationToken, true) ?? throw new NotFoundException(nameof(Identity));
+ var deletionProcess = identity.StartDeletionProcessAsSupport();
+
+ await _identitiesRepository.Update(identity, cancellationToken);
+
+ _eventBus.Publish(new IdentityDeletionProcessStartedIntegrationEvent(identity.Address, deletionProcess.Id));
+
+ return new StartDeletionProcessAsSupportResponse(deletionProcess);
+ }
+}
diff --git a/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsSupport/StartDeletionProcessAsSupportCommand.cs b/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsSupport/StartDeletionProcessAsSupportCommand.cs
new file mode 100644
index 0000000000..305a78d3f4
--- /dev/null
+++ b/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsSupport/StartDeletionProcessAsSupportCommand.cs
@@ -0,0 +1,13 @@
+using MediatR;
+
+namespace Backbone.Modules.Devices.Application.Identities.Commands.StartDeletionProcessAsSupport;
+
+public class StartDeletionProcessAsSupportCommand : IRequest
+{
+ public StartDeletionProcessAsSupportCommand(string identityAddress)
+ {
+ IdentityAddress = identityAddress;
+ }
+
+ public string IdentityAddress { get; set; }
+}
diff --git a/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsSupport/StartDeletionProcessAsSupportResponse.cs b/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsSupport/StartDeletionProcessAsSupportResponse.cs
new file mode 100644
index 0000000000..0cb2f504df
--- /dev/null
+++ b/Modules/Devices/src/Devices.Application/Identities/Commands/StartDeletionProcessAsSupport/StartDeletionProcessAsSupportResponse.cs
@@ -0,0 +1,17 @@
+using Backbone.Modules.Devices.Domain.Entities.Identities;
+
+namespace Backbone.Modules.Devices.Application.Identities.Commands.StartDeletionProcessAsSupport;
+
+public class StartDeletionProcessAsSupportResponse
+{
+ public StartDeletionProcessAsSupportResponse(IdentityDeletionProcess deletionProcess)
+ {
+ Id = deletionProcess.Id;
+ Status = deletionProcess.Status;
+ CreatedAt = deletionProcess.CreatedAt;
+ }
+
+ public string Id { get; set; }
+ public DeletionProcessStatus Status { get; set; }
+ public DateTime CreatedAt { get; set; }
+}
diff --git a/Modules/Devices/src/Devices.Application/Identities/Commands/UpdateIdentity/Handler.cs b/Modules/Devices/src/Devices.Application/Identities/Commands/UpdateIdentity/Handler.cs
index 2789acebbb..cafe619df0 100644
--- a/Modules/Devices/src/Devices.Application/Identities/Commands/UpdateIdentity/Handler.cs
+++ b/Modules/Devices/src/Devices.Application/Identities/Commands/UpdateIdentity/Handler.cs
@@ -3,7 +3,7 @@
using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository;
using Backbone.Modules.Devices.Application.IntegrationEvents.Outgoing;
using Backbone.Modules.Devices.Domain.Aggregates.Tier;
-using Backbone.Modules.Devices.Domain.Entities;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
using MediatR;
using ApplicationException = Backbone.BuildingBlocks.Application.Abstractions.Exceptions.ApplicationException;
@@ -38,6 +38,6 @@ public async Task Handle(UpdateIdentityCommand request, CancellationToken cancel
identity.ChangeTier(newTier.Id);
await _identitiesRepository.Update(identity, cancellationToken);
- _eventBus.Publish(new TierOfIdentityChangedIntegrationEvent(identity, oldTier, newTier));
+ _eventBus.Publish(new TierOfIdentityChangedIntegrationEvent(identity, oldTier.Id, newTier.Id));
}
}
diff --git a/Modules/Devices/src/Devices.Application/Identities/Queries/GetIdentity/GetIdentityResponse.cs b/Modules/Devices/src/Devices.Application/Identities/Queries/GetIdentity/GetIdentityResponse.cs
index 87997fe1b4..23ddad3dcb 100644
--- a/Modules/Devices/src/Devices.Application/Identities/Queries/GetIdentity/GetIdentityResponse.cs
+++ b/Modules/Devices/src/Devices.Application/Identities/Queries/GetIdentity/GetIdentityResponse.cs
@@ -1,5 +1,5 @@
using Backbone.Modules.Devices.Application.DTOs;
-using Backbone.Modules.Devices.Domain.Entities;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
namespace Backbone.Modules.Devices.Application.Identities.Queries.GetIdentity;
public class GetIdentityResponse : IdentitySummaryDTO
diff --git a/Modules/Devices/src/Devices.Application/Identities/Queries/GetIdentity/Handler.cs b/Modules/Devices/src/Devices.Application/Identities/Queries/GetIdentity/Handler.cs
index fcbfc47bfd..8c0a2c8505 100644
--- a/Modules/Devices/src/Devices.Application/Identities/Queries/GetIdentity/Handler.cs
+++ b/Modules/Devices/src/Devices.Application/Identities/Queries/GetIdentity/Handler.cs
@@ -1,6 +1,6 @@
-using Backbone.BuildingBlocks.Application.Abstractions.Exceptions;
+using Backbone.BuildingBlocks.Application.Abstractions.Exceptions;
using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository;
-using Backbone.Modules.Devices.Domain.Entities;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
using MediatR;
namespace Backbone.Modules.Devices.Application.Identities.Queries.GetIdentity;
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 df09538f4f..be4fc0d2b1 100644
--- a/Modules/Devices/src/Devices.Application/Infrastructure/Persistence/Repository/IIdentitiesRepository.cs
+++ b/Modules/Devices/src/Devices.Application/Infrastructure/Persistence/Repository/IIdentitiesRepository.cs
@@ -1,7 +1,7 @@
-using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.Persistence.Database;
+using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.Persistence.Database;
using Backbone.BuildingBlocks.Application.Pagination;
using Backbone.DevelopmentKit.Identity.ValueObjects;
-using Backbone.Modules.Devices.Domain.Entities;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
namespace Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository;
@@ -10,10 +10,9 @@ public interface IIdentitiesRepository
#region Identities
Task> FindAll(PaginationFilter paginationFilter, CancellationToken cancellationToken);
Task Update(Identity identity, CancellationToken cancellationToken);
-#nullable enable
Task FindByAddress(IdentityAddress address, CancellationToken cancellationToken, bool track = false);
-#nullable disable
Task Exists(IdentityAddress address, CancellationToken cancellationToken);
+ Task> FindAllWithDeletionProcessInStatus(DeletionProcessStatus status, CancellationToken cancellationToken, bool track = false);
Task CountByClientId(string clientId, CancellationToken cancellationToken);
#endregion
diff --git a/Modules/Devices/src/Devices.Application/Infrastructure/PushNotifications/DeletionProcess/DeletionProcessGracePeriodNotification.cs b/Modules/Devices/src/Devices.Application/Infrastructure/PushNotifications/DeletionProcess/DeletionProcessGracePeriodNotification.cs
new file mode 100644
index 0000000000..5ba8449a51
--- /dev/null
+++ b/Modules/Devices/src/Devices.Application/Infrastructure/PushNotifications/DeletionProcess/DeletionProcessGracePeriodNotification.cs
@@ -0,0 +1,4 @@
+namespace Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.DeletionProcess;
+
+[NotificationText(Title = "Your Identity will be deleted", Body = "Your Identity will be deleted in a few days. You can still cancel up to this point.")]
+public record DeletionProcessGracePeriodNotification(int DaysUntilDeletion);
diff --git a/Modules/Devices/src/Devices.Application/Infrastructure/PushNotifications/DeletionProcess/DeletionProcessStartedPushNotification.cs b/Modules/Devices/src/Devices.Application/Infrastructure/PushNotifications/DeletionProcess/DeletionProcessStartedPushNotification.cs
new file mode 100644
index 0000000000..07721dadb8
--- /dev/null
+++ b/Modules/Devices/src/Devices.Application/Infrastructure/PushNotifications/DeletionProcess/DeletionProcessStartedPushNotification.cs
@@ -0,0 +1,6 @@
+namespace Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.DeletionProcess;
+
+[NotificationText(Title = "Deletion process started", Body = "A Deletion Process was started for your Identity.")]
+public record DeletionProcessStartedPushNotification
+{
+}
diff --git a/Modules/Devices/src/Devices.Application/Infrastructure/PushNotifications/DeletionProcess/DeletionProcessWaitingForApprovalReminderPushNotification.cs b/Modules/Devices/src/Devices.Application/Infrastructure/PushNotifications/DeletionProcess/DeletionProcessWaitingForApprovalReminderPushNotification.cs
new file mode 100644
index 0000000000..0f96f437a8
--- /dev/null
+++ b/Modules/Devices/src/Devices.Application/Infrastructure/PushNotifications/DeletionProcess/DeletionProcessWaitingForApprovalReminderPushNotification.cs
@@ -0,0 +1,5 @@
+namespace Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.DeletionProcess;
+
+[NotificationText(Title = "Deletion process waiting for approval.",
+ Body = "There is a deletion process for your identity that waits for your approval. If you don't approve it within a few days, the process will be terminated.")]
+public record DeletionProcessWaitingForApprovalReminderPushNotification(int DaysUntilApprovalPeriodEnds);
diff --git a/Modules/Devices/src/Devices.Application/IntegrationEvents/Incoming/IdentityDeletionProcessStarted/IdentityDeletionProcessStartedIntegrationEventHandler.cs b/Modules/Devices/src/Devices.Application/IntegrationEvents/Incoming/IdentityDeletionProcessStarted/IdentityDeletionProcessStartedIntegrationEventHandler.cs
new file mode 100644
index 0000000000..36f8526ec2
--- /dev/null
+++ b/Modules/Devices/src/Devices.Application/IntegrationEvents/Incoming/IdentityDeletionProcessStarted/IdentityDeletionProcessStartedIntegrationEventHandler.cs
@@ -0,0 +1,21 @@
+using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus;
+using Backbone.BuildingBlocks.Application.PushNotifications;
+using Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.DeletionProcess;
+using Backbone.Modules.Devices.Application.IntegrationEvents.Outgoing;
+
+namespace Backbone.Modules.Devices.Application.IntegrationEvents.Incoming.IdentityDeletionProcessStarted;
+
+public class IdentityDeletionProcessStartedIntegrationEventHandler : IIntegrationEventHandler
+{
+ private readonly IPushNotificationSender _pushNotificationSender;
+
+ public IdentityDeletionProcessStartedIntegrationEventHandler(IPushNotificationSender pushNotificationSender)
+ {
+ _pushNotificationSender = pushNotificationSender;
+ }
+
+ public async Task Handle(IdentityDeletionProcessStartedIntegrationEvent @event)
+ {
+ await _pushNotificationSender.SendNotification(@event.Address, new DeletionProcessStartedPushNotification(), CancellationToken.None);
+ }
+}
diff --git a/Modules/Devices/src/Devices.Application/IntegrationEvents/Outgoing/IdentityCreatedIntegrationEvent.cs b/Modules/Devices/src/Devices.Application/IntegrationEvents/Outgoing/IdentityCreatedIntegrationEvent.cs
index fccba59b22..acc671886c 100644
--- a/Modules/Devices/src/Devices.Application/IntegrationEvents/Outgoing/IdentityCreatedIntegrationEvent.cs
+++ b/Modules/Devices/src/Devices.Application/IntegrationEvents/Outgoing/IdentityCreatedIntegrationEvent.cs
@@ -1,5 +1,5 @@
-using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus.Events;
-using Backbone.Modules.Devices.Domain.Entities;
+using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus.Events;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
namespace Backbone.Modules.Devices.Application.IntegrationEvents.Outgoing;
public class IdentityCreatedIntegrationEvent : IntegrationEvent
diff --git a/Modules/Devices/src/Devices.Application/IntegrationEvents/Outgoing/IdentityDeletionProcessStartedIntegrationEvent.cs b/Modules/Devices/src/Devices.Application/IntegrationEvents/Outgoing/IdentityDeletionProcessStartedIntegrationEvent.cs
new file mode 100644
index 0000000000..4cb438ae39
--- /dev/null
+++ b/Modules/Devices/src/Devices.Application/IntegrationEvents/Outgoing/IdentityDeletionProcessStartedIntegrationEvent.cs
@@ -0,0 +1,15 @@
+using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus.Events;
+
+namespace Backbone.Modules.Devices.Application.IntegrationEvents.Outgoing;
+
+public class IdentityDeletionProcessStartedIntegrationEvent : IntegrationEvent
+{
+ public IdentityDeletionProcessStartedIntegrationEvent(string identityAddress, string deletionProcessId) : base($"{identityAddress}/DeletionProcessStarted/{deletionProcessId}")
+ {
+ DeletionProcessId = deletionProcessId;
+ Address = identityAddress;
+ }
+
+ public string Address { get; private set; }
+ public string DeletionProcessId { get; private set; }
+}
diff --git a/Modules/Devices/src/Devices.Application/IntegrationEvents/Outgoing/TierOfIdentityChangedIntegrationEvent.cs b/Modules/Devices/src/Devices.Application/IntegrationEvents/Outgoing/TierOfIdentityChangedIntegrationEvent.cs
index b37dfadb6c..de691b0145 100644
--- a/Modules/Devices/src/Devices.Application/IntegrationEvents/Outgoing/TierOfIdentityChangedIntegrationEvent.cs
+++ b/Modules/Devices/src/Devices.Application/IntegrationEvents/Outgoing/TierOfIdentityChangedIntegrationEvent.cs
@@ -1,18 +1,18 @@
-using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus.Events;
+using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus.Events;
using Backbone.Modules.Devices.Domain.Aggregates.Tier;
-using Backbone.Modules.Devices.Domain.Entities;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
namespace Backbone.Modules.Devices.Application.IntegrationEvents.Outgoing;
public class TierOfIdentityChangedIntegrationEvent : IntegrationEvent
{
- public TierOfIdentityChangedIntegrationEvent(Identity identity, Tier oldTier, Tier newTier) : base($"{identity.Address}/TierOfIdentityChanged")
+ public TierOfIdentityChangedIntegrationEvent(Identity identity, TierId oldTierIdId, TierId newTierIdId) : base($"{identity.Address}/TierOfIdentityChanged")
{
- OldTier = oldTier.Id;
- NewTier = newTier.Id;
+ OldTierId = oldTierIdId;
+ NewTierId = newTierIdId;
IdentityAddress = identity.Address;
}
- public string OldTier { get; set; }
- public string NewTier { get; set; }
+ public string OldTierId { get; set; }
+ public string NewTierId { get; set; }
public string IdentityAddress { get; set; }
}
diff --git a/Modules/Devices/src/Devices.Application/Tiers/Commands/CreateQueuedForDeletionTier/CreateQueuedForDeletionTierCommand.cs b/Modules/Devices/src/Devices.Application/Tiers/Commands/CreateQueuedForDeletionTier/CreateQueuedForDeletionTierCommand.cs
new file mode 100644
index 0000000000..c7385ff60d
--- /dev/null
+++ b/Modules/Devices/src/Devices.Application/Tiers/Commands/CreateQueuedForDeletionTier/CreateQueuedForDeletionTierCommand.cs
@@ -0,0 +1,7 @@
+using MediatR;
+
+namespace Backbone.Modules.Devices.Application.Tiers.Commands.CreateQueuedForDeletionTier;
+
+public class CreateQueuedForDeletionTierCommand : IRequest
+{
+}
diff --git a/Modules/Devices/src/Devices.Application/Tiers/Commands/CreateQueuedForDeletionTier/Handler.cs b/Modules/Devices/src/Devices.Application/Tiers/Commands/CreateQueuedForDeletionTier/Handler.cs
new file mode 100644
index 0000000000..9fc44531f8
--- /dev/null
+++ b/Modules/Devices/src/Devices.Application/Tiers/Commands/CreateQueuedForDeletionTier/Handler.cs
@@ -0,0 +1,21 @@
+using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository;
+using Backbone.Modules.Devices.Domain.Aggregates.Tier;
+using MediatR;
+
+namespace Backbone.Modules.Devices.Application.Tiers.Commands.CreateQueuedForDeletionTier;
+
+public class Handler : IRequestHandler
+{
+ private readonly ITiersRepository _tiersRepository;
+
+ public Handler(ITiersRepository tiersRepository)
+ {
+ _tiersRepository = tiersRepository;
+ }
+
+ public async Task Handle(CreateQueuedForDeletionTierCommand request, CancellationToken cancellationToken)
+ {
+ if (!await _tiersRepository.ExistsWithId(TierId.Create(Tier.QUEUED_FOR_DELETION.Id).Value, CancellationToken.None))
+ await _tiersRepository.AddAsync(Tier.QUEUED_FOR_DELETION, cancellationToken);
+ }
+}
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 79919686ad..5c5701b13a 100644
--- a/Modules/Devices/src/Devices.Application/Users/Commands/SeedTestUsers/Handler.cs
+++ b/Modules/Devices/src/Devices.Application/Users/Commands/SeedTestUsers/Handler.cs
@@ -1,7 +1,7 @@
using Backbone.DevelopmentKit.Identity.ValueObjects;
using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Database;
using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository;
-using Backbone.Modules.Devices.Domain.Entities;
+using Backbone.Modules.Devices.Domain.Entities.Identities;
using Backbone.Tooling;
using MediatR;
using Microsoft.AspNetCore.Identity;
diff --git a/Modules/Devices/src/Devices.ConsumerApi/Controllers/IdentitiesController.cs b/Modules/Devices/src/Devices.ConsumerApi/Controllers/IdentitiesController.cs
index 74fb3e4513..ac33756002 100644
--- a/Modules/Devices/src/Devices.ConsumerApi/Controllers/IdentitiesController.cs
+++ b/Modules/Devices/src/Devices.ConsumerApi/Controllers/IdentitiesController.cs
@@ -3,7 +3,9 @@
using Backbone.BuildingBlocks.API.Mvc.ControllerAttributes;
using Backbone.BuildingBlocks.Application.Abstractions.Exceptions;
using Backbone.Modules.Devices.Application.Devices.DTOs;
+using Backbone.Modules.Devices.Application.Identities.Commands.ApproveDeletionProcess;
using Backbone.Modules.Devices.Application.Identities.Commands.CreateIdentity;
+using Backbone.Modules.Devices.Application.Identities.Commands.StartDeletionProcessAsOwner;
using Backbone.Modules.Devices.Infrastructure.OpenIddict;
using MediatR;
using Microsoft.AspNetCore.Authorization;
@@ -53,22 +55,41 @@ public async Task CreateIdentity(CreateIdentityRequest request, C
var response = await _mediator.Send(command, cancellationToken);
+ return Created(response);
+ }
+
+ [HttpPost("Self/DeletionProcesses")]
+ [ProducesResponseType(StatusCodes.Status201Created)]
+ [ProducesError(StatusCodes.Status400BadRequest)]
+ public async Task StartDeletionProcess(CancellationToken cancellationToken)
+ {
+ var response = await _mediator.Send(new StartDeletionProcessAsOwnerCommand(), cancellationToken);
return Created("", response);
}
+
+ [HttpPut("Self/DeletionProcesses/{id}/Approve")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesError(StatusCodes.Status400BadRequest)]
+ [ProducesError(StatusCodes.Status404NotFound)]
+ public async Task ApproveDeletionProcess([FromRoute] string id, CancellationToken cancellationToken)
+ {
+ var response = await _mediator.Send(new ApproveDeletionProcessCommand(id), cancellationToken);
+ return Ok(response);
+ }
}
public class CreateIdentityRequest
{
- public string ClientId { get; set; }
- public string ClientSecret { get; set; }
- public byte[] IdentityPublicKey { get; set; }
- public string DevicePassword { get; set; }
- public byte IdentityVersion { get; set; }
- public CreateIdentityRequestSignedChallenge SignedChallenge { get; set; }
+ public required string ClientId { get; set; }
+ public required string ClientSecret { get; set; }
+ public required byte[] IdentityPublicKey { get; set; }
+ public required string DevicePassword { get; set; }
+ public required byte IdentityVersion { get; set; }
+ public required CreateIdentityRequestSignedChallenge SignedChallenge { get; set; }
}
public class CreateIdentityRequestSignedChallenge
{
- public string Challenge { get; set; }
- public byte[] Signature { get; set; }
+ public required string Challenge { get; set; }
+ public required byte[] Signature { get; set; }
}
diff --git a/Modules/Devices/src/Devices.Domain/Aggregates/Tier/Tier.cs b/Modules/Devices/src/Devices.Domain/Aggregates/Tier/Tier.cs
index a85fe6d9c8..7d0f3dcedd 100644
--- a/Modules/Devices/src/Devices.Domain/Aggregates/Tier/Tier.cs
+++ b/Modules/Devices/src/Devices.Domain/Aggregates/Tier/Tier.cs
@@ -4,31 +4,36 @@ namespace Backbone.Modules.Devices.Domain.Aggregates.Tier;
public class Tier
{
+ public static readonly Tier QUEUED_FOR_DELETION = new(TierId.Create("TIR00000000000000001").Value, TierName.Create("Queued for Deletion").Value);
+
public Tier(TierName name)
{
Id = TierId.Generate();
Name = name;
}
+ private Tier(TierId id, TierName name)
+ {
+ Id = id;
+ Name = name;
+ }
+
public TierId Id { get; }
public TierName Name { get; }
public DomainError? CanBeDeleted(int clientsCount, int identitiesCount)
{
if (clientsCount > 0)
- {
return DomainErrors.CannotDeleteUsedTier($"The Tier is used as the default Tier by one or more clients. A Tier cannot be deleted if it is the default Tier of a Client ({clientsCount} found).");
- }
if (identitiesCount > 0)
- {
return DomainErrors.CannotDeleteUsedTier($"The Tier is assigned to one or more Identities. A Tier cannot be deleted if it is assigned to an Identity ({identitiesCount} found).");
- }
if (IsBasicTier())
- {
return DomainErrors.CannotDeleteBasicTier();
- }
+
+ if (IsQueuedForDeletionTier())
+ return DomainErrors.CannotDeleteQueuedForDeletionTier();
return null;
}
@@ -37,4 +42,9 @@ public bool IsBasicTier()
{
return Name == TierName.BASIC_DEFAULT_NAME;
}
+
+ public bool IsQueuedForDeletionTier()
+ {
+ return Id == QUEUED_FOR_DELETION.Id;
+ }
}
diff --git a/Modules/Devices/src/Devices.Domain/Aggregates/Tier/TierId.cs b/Modules/Devices/src/Devices.Domain/Aggregates/Tier/TierId.cs
index 6b76647430..f757a2445b 100644
--- a/Modules/Devices/src/Devices.Domain/Aggregates/Tier/TierId.cs
+++ b/Modules/Devices/src/Devices.Domain/Aggregates/Tier/TierId.cs
@@ -8,7 +8,6 @@ namespace Backbone.Modules.Devices.Domain.Aggregates.Tier;
public record TierId : StronglyTypedId
{
public const int MAX_LENGTH = DEFAULT_MAX_LENGTH;
-
private const string PREFIX = "TIR";
private static readonly StronglyTypedIdHelpers UTILS = new(PREFIX, DEFAULT_VALID_CHARS, MAX_LENGTH);
diff --git a/Modules/Devices/src/Devices.Domain/Aggregates/Tier/TierName.cs b/Modules/Devices/src/Devices.Domain/Aggregates/Tier/TierName.cs
index 0efb5cab41..0b7aa5670e 100644
--- a/Modules/Devices/src/Devices.Domain/Aggregates/Tier/TierName.cs
+++ b/Modules/Devices/src/Devices.Domain/Aggregates/Tier/TierName.cs
@@ -6,6 +6,7 @@ namespace Backbone.Modules.Devices.Domain.Aggregates.Tier;
public record TierName
{
public static readonly TierName BASIC_DEFAULT_NAME = new("Basic");
+
public string Value { get; }
public const int MIN_LENGTH = 3;
public const int MAX_LENGTH = 30;
diff --git a/Modules/Devices/src/Devices.Domain/DomainErrors.cs b/Modules/Devices/src/Devices.Domain/DomainErrors.cs
index 0c2d324a5d..47528986c9 100644
--- a/Modules/Devices/src/Devices.Domain/DomainErrors.cs
+++ b/Modules/Devices/src/Devices.Domain/DomainErrors.cs
@@ -20,9 +20,17 @@ public static DomainError InvalidPnsPlatform(string reason = "")
public static DomainError CannotDeleteBasicTier(string reason = "")
{
- var formattedReason = string.IsNullOrEmpty(reason) ? "" : $" ({reason})";
- return new DomainError("error.platform.validation.device.basicTierCannotBeDeleted",
- string.IsNullOrEmpty(reason) ? $"The Basic Tier cannot be deleted {formattedReason}." : reason);
+ return new DomainError("error.platform.validation.device.basicTierCannotBeDeleted", "The 'Basic' Tier cannot be deleted.");
+ }
+
+ public static DomainError CannotDeleteQueuedForDeletionTier()
+ {
+ return new DomainError("error.platform.validation.device.queuedForDeletionTierCannotBeDeleted", "The 'Queued for Deletion' Tier cannot be deleted.");
+ }
+
+ public static DomainError CannotChangeTierQueuedForDeletion()
+ {
+ return new DomainError("error.platform.validation.device.queuedForDeletionTierCannotBeManuallyAssignedOrUnassigned", "The Identity's Tier cannot be be changed from or to the 'Queued for Deletion' Tier.");
}
public static DomainError CannotDeleteUsedTier(string reason = "")
@@ -31,4 +39,14 @@ public static DomainError CannotDeleteUsedTier(string reason = "")
return new DomainError("error.platform.validation.device.usedTierCannotBeDeleted",
string.IsNullOrEmpty(reason) ? $"The Tier cannot be deleted {formattedReason}" : reason);
}
+
+ public static DomainError OnlyOneActiveDeletionProcessAllowed()
+ {
+ return new DomainError("error.platform.validation.device.onlyOneActiveDeletionProcessAllowed", "Only one active deletion process is allowed.");
+ }
+
+ public static DomainError NoDeletionProcessWithRequiredStatusExists()
+ {
+ return new DomainError("error.platform.validation.device.noDeletionProcessWithRequiredStatusExists", "The deletion process does not have the correct status to perform this action.");
+ }
}
diff --git a/Modules/Devices/src/Devices.Domain/Entities/ApplicationUser.cs b/Modules/Devices/src/Devices.Domain/Entities/Identities/ApplicationUser.cs
similarity index 94%
rename from Modules/Devices/src/Devices.Domain/Entities/ApplicationUser.cs
rename to Modules/Devices/src/Devices.Domain/Entities/Identities/ApplicationUser.cs
index b816d01bb6..3e763214a8 100644
--- a/Modules/Devices/src/Devices.Domain/Entities/ApplicationUser.cs
+++ b/Modules/Devices/src/Devices.Domain/Entities/Identities/ApplicationUser.cs
@@ -2,7 +2,7 @@
using Backbone.Tooling;
using Microsoft.AspNetCore.Identity;
-namespace Backbone.Modules.Devices.Domain.Entities;
+namespace Backbone.Modules.Devices.Domain.Entities.Identities;
public class ApplicationUser : IdentityUser
{
diff --git a/Modules/Devices/src/Devices.Domain/Entities/Device.cs b/Modules/Devices/src/Devices.Domain/Entities/Identities/Device.cs
similarity index 97%
rename from Modules/Devices/src/Devices.Domain/Entities/Device.cs
rename to Modules/Devices/src/Devices.Domain/Entities/Identities/Device.cs
index e3819f51b0..631a213a81 100644
--- a/Modules/Devices/src/Devices.Domain/Entities/Device.cs
+++ b/Modules/Devices/src/Devices.Domain/Entities/Identities/Device.cs
@@ -4,7 +4,7 @@
using Backbone.DevelopmentKit.Identity.ValueObjects;
using Backbone.Tooling;
-namespace Backbone.Modules.Devices.Domain.Entities;
+namespace Backbone.Modules.Devices.Domain.Entities.Identities;
public class Device
{
diff --git a/Modules/Devices/src/Devices.Domain/Entities/Identities/Hasher.cs b/Modules/Devices/src/Devices.Domain/Entities/Identities/Hasher.cs
new file mode 100644
index 0000000000..da0035cdb3
--- /dev/null
+++ b/Modules/Devices/src/Devices.Domain/Entities/Identities/Hasher.cs
@@ -0,0 +1,50 @@
+using System.Diagnostics;
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.AspNetCore.Cryptography.KeyDerivation;
+
+namespace Backbone.Modules.Devices.Domain.Entities.Identities;
+
+public interface IHasher
+{
+ byte[] HashUtf8(string input);
+}
+
+public static class Hasher
+{
+ private static readonly ThreadLocal> GET_HASHER = new(() => () => new HasherImpl());
+
+ public static void SetHasher(IHasher hasher)
+ {
+ var stackTrace = new StackTrace();
+ var callerType = stackTrace.GetFrame(1)!.GetMethod()!.DeclaringType;
+
+ if (callerType is { Namespace: not null } && !callerType.Namespace.Contains("Test"))
+ {
+ throw new NotSupportedException("You can't call this method from a Non-Test-class");
+ }
+
+ GET_HASHER.Value = () => hasher;
+ }
+
+ public static byte[] HashUtf8(string input)
+ {
+ return GET_HASHER.Value!().HashUtf8(input);
+ }
+
+ public static void Reset()
+ {
+ GET_HASHER.Value = () => new HasherImpl();
+ }
+}
+
+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
new file mode 100644
index 0000000000..3f143a5d84
--- /dev/null
+++ b/Modules/Devices/src/Devices.Domain/Entities/Identities/Identity.cs
@@ -0,0 +1,180 @@
+using Backbone.BuildingBlocks.Domain;
+using Backbone.BuildingBlocks.Domain.Errors;
+using Backbone.DevelopmentKit.Identity.ValueObjects;
+using Backbone.Modules.Devices.Domain.Aggregates.Tier;
+using Backbone.Tooling;
+
+namespace Backbone.Modules.Devices.Domain.Entities.Identities;
+
+public class Identity
+{
+ private readonly List _deletionProcesses;
+
+ public Identity(string? clientId, IdentityAddress address, byte[] publicKey, TierId tierId, byte identityVersion)
+ {
+ ClientId = clientId;
+ Address = address;
+ PublicKey = publicKey;
+ IdentityVersion = identityVersion;
+ CreatedAt = SystemTime.UtcNow;
+ Devices = new List();
+ TierId = tierId;
+ Status = IdentityStatus.Active;
+ _deletionProcesses = new List();
+ }
+
+ public string? ClientId { get; private set; }
+
+ public IdentityAddress Address { get; private set; }
+ public byte[] PublicKey { get; private set; }
+ public DateTime CreatedAt { get; private set; }
+
+ public List Devices { get; }
+
+ public byte IdentityVersion { get; private set; }
+
+ public TierId? TierIdBeforeDeletion { get; private set; }
+ public TierId? TierId { get; private set; }
+
+ public IReadOnlyList DeletionProcesses => _deletionProcesses;
+
+ public DateTime? DeletionGracePeriodEndsAt { get; private set; }
+
+ public IdentityStatus Status { get; private set; }
+
+ public bool IsNew()
+ {
+ return Devices.Count < 1;
+ }
+
+ public void ChangeTier(TierId id)
+ {
+ if (id == Tier.QUEUED_FOR_DELETION.Id || TierId == Tier.QUEUED_FOR_DELETION.Id)
+ throw new DomainException(DomainErrors.CannotChangeTierQueuedForDeletion());
+
+ if (TierId == id)
+ throw new DomainException(GenericDomainErrors.NewAndOldParametersMatch("TierId"));
+
+ TierId = id;
+ }
+
+ public IdentityDeletionProcess StartDeletionProcessAsSupport()
+ {
+ EnsureNoActiveProcessExists();
+
+ var deletionProcess = IdentityDeletionProcess.StartAsSupport(Address);
+ _deletionProcesses.Add(deletionProcess);
+
+ return deletionProcess;
+ }
+
+ public IdentityDeletionProcess StartDeletionProcessAsOwner(DeviceId asDevice)
+ {
+ EnsureNoActiveProcessExists();
+
+ TierIdBeforeDeletion = TierId;
+
+ var deletionProcess = IdentityDeletionProcess.StartAsOwner(Address, asDevice);
+ _deletionProcesses.Add(deletionProcess);
+
+ DeletionGracePeriodEndsAt = deletionProcess.GracePeriodEndsAt;
+ TierId = Tier.QUEUED_FOR_DELETION.Id;
+ Status = IdentityStatus.ToBeDeleted;
+
+ return deletionProcess;
+ }
+
+ public void DeletionProcessApprovalReminder1Sent()
+ {
+ EnsureDeletionProcessInStatusExists(DeletionProcessStatus.WaitingForApproval);
+
+ var deletionProcess = GetDeletionProcessInStatus(DeletionProcessStatus.WaitingForApproval)!;
+ deletionProcess.ApprovalReminder1Sent(Address);
+ }
+
+ public void DeletionProcessApprovalReminder2Sent()
+ {
+ EnsureDeletionProcessInStatusExists(DeletionProcessStatus.WaitingForApproval);
+
+ var deletionProcess = GetDeletionProcessInStatus(DeletionProcessStatus.WaitingForApproval)!;
+ deletionProcess.ApprovalReminder2Sent(Address);
+ }
+
+ public void DeletionProcessApprovalReminder3Sent()
+ {
+ EnsureDeletionProcessInStatusExists(DeletionProcessStatus.WaitingForApproval);
+
+ var deletionProcess = GetDeletionProcessInStatus(DeletionProcessStatus.WaitingForApproval)!;
+ deletionProcess.ApprovalReminder3Sent(Address);
+ }
+
+ public IdentityDeletionProcess ApproveDeletionProcess(IdentityDeletionProcessId deletionProcessId, DeviceId deviceId)
+ {
+ var deletionProcess = DeletionProcesses.FirstOrDefault(x => x.Id == deletionProcessId) ?? throw new DomainException(GenericDomainErrors.NotFound(nameof(IdentityDeletionProcess)));
+
+ deletionProcess.Approve(Address, deviceId);
+
+ Status = IdentityStatus.ToBeDeleted;
+ DeletionGracePeriodEndsAt = deletionProcess.GracePeriodEndsAt;
+ TierId = Tier.QUEUED_FOR_DELETION.Id;
+
+ return deletionProcess;
+ }
+
+ private void EnsureDeletionProcessInStatusExists(DeletionProcessStatus status)
+ {
+ var deletionProcess = DeletionProcesses.Any(d => d.Status == status);
+
+ if (!deletionProcess)
+ throw new DomainException(DomainErrors.NoDeletionProcessWithRequiredStatusExists());
+ }
+
+ private void EnsureNoActiveProcessExists()
+ {
+ var activeProcessExists = DeletionProcesses.Any(d => d.IsActive());
+
+ if (activeProcessExists)
+ throw new DomainException(DomainErrors.OnlyOneActiveDeletionProcessAllowed());
+ }
+
+ public void DeletionGracePeriodReminder1Sent()
+ {
+ EnsureDeletionProcessInStatusExists(DeletionProcessStatus.Approved);
+
+ var deletionProcess = GetDeletionProcessInStatus(DeletionProcessStatus.Approved)!;
+ deletionProcess.GracePeriodReminder1Sent(Address);
+ }
+
+ public void DeletionGracePeriodReminder2Sent()
+ {
+ EnsureDeletionProcessInStatusExists(DeletionProcessStatus.Approved);
+
+ var deletionProcess = DeletionProcesses.First(d => d.Status == DeletionProcessStatus.Approved);
+ deletionProcess.GracePeriodReminder2Sent(Address);
+ }
+
+ public void DeletionGracePeriodReminder3Sent()
+ {
+ EnsureDeletionProcessInStatusExists(DeletionProcessStatus.Approved);
+
+ var deletionProcess = DeletionProcesses.First(d => d.Status == DeletionProcessStatus.Approved);
+ deletionProcess.GracePeriodReminder3Sent(Address);
+ }
+
+ public IdentityDeletionProcess? GetDeletionProcessInStatus(DeletionProcessStatus deletionProcessStatus)
+ {
+ return DeletionProcesses.FirstOrDefault(x => x.Status == deletionProcessStatus);
+ }
+}
+
+public enum DeletionProcessStatus
+{
+ WaitingForApproval = 0,
+ Approved = 1
+}
+
+public enum IdentityStatus
+{
+ Active = 0,
+ ToBeDeleted = 1
+}
diff --git a/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionConfiguration.cs b/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionConfiguration.cs
new file mode 100644
index 0000000000..93f04a5806
--- /dev/null
+++ b/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionConfiguration.cs
@@ -0,0 +1,41 @@
+namespace Backbone.Modules.Devices.Domain.Entities.Identities;
+
+public class IdentityDeletionConfiguration
+{
+ public static int MaxApprovalTime { get; set; } = 10;
+ public static int LengthOfGracePeriod { get; set; } = 30;
+ public static GracePeriodNotificationConfiguration GracePeriodNotification1 { get; set; } = new()
+ {
+ Time = 20
+ };
+ public static GracePeriodNotificationConfiguration GracePeriodNotification2 { get; set; } = new()
+ {
+ Time = 10
+ };
+ public static GracePeriodNotificationConfiguration GracePeriodNotification3 { get; set; } = new()
+ {
+ Time = 5
+ };
+ public static ApprovalReminderNotificationConfiguration ApprovalReminder1 { get; set; } = new()
+ {
+ Time = 10
+ };
+ public static ApprovalReminderNotificationConfiguration ApprovalReminder2 { get; set; } = new()
+ {
+ Time = 5
+ };
+ public static ApprovalReminderNotificationConfiguration ApprovalReminder3 { get; set; } = new()
+ {
+ Time = 2
+ };
+}
+
+public class GracePeriodNotificationConfiguration
+{
+ public int Time { get; set; }
+}
+
+public class ApprovalReminderNotificationConfiguration
+{
+ public int Time { get; set; }
+}
diff --git a/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcess.cs b/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcess.cs
new file mode 100644
index 0000000000..c3c0fd41dc
--- /dev/null
+++ b/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcess.cs
@@ -0,0 +1,135 @@
+using Backbone.BuildingBlocks.Domain;
+using Backbone.DevelopmentKit.Identity.ValueObjects;
+using Backbone.Tooling;
+
+namespace Backbone.Modules.Devices.Domain.Entities.Identities;
+
+public class IdentityDeletionProcess
+{
+ private readonly List _auditLog;
+
+ // EF Core needs the empty constructor
+#pragma warning disable CS8618
+ // ReSharper disable once UnusedMember.Local
+ private IdentityDeletionProcess()
+#pragma warning restore CS8618
+ {
+ }
+
+ public static IdentityDeletionProcess StartAsSupport(IdentityAddress createdBy)
+ {
+ return new IdentityDeletionProcess(createdBy, DeletionProcessStatus.WaitingForApproval);
+ }
+
+ public static IdentityDeletionProcess StartAsOwner(IdentityAddress createdBy, DeviceId createdByDeviceId)
+ {
+ return new IdentityDeletionProcess(createdBy, createdByDeviceId);
+ }
+
+ private IdentityDeletionProcess(IdentityAddress createdBy, DeletionProcessStatus status)
+ {
+ Id = IdentityDeletionProcessId.Generate();
+ CreatedAt = SystemTime.UtcNow;
+ Status = status;
+
+ _auditLog = new List
+ {
+ IdentityDeletionProcessAuditLogEntry.ProcessStartedBySupport(Id, createdBy)
+ };
+ }
+
+ private IdentityDeletionProcess(IdentityAddress createdBy, DeviceId createdByDevice)
+ {
+ Id = IdentityDeletionProcessId.Generate();
+ CreatedAt = SystemTime.UtcNow;
+
+ Approve(createdByDevice);
+
+ _auditLog = new List
+ {
+ IdentityDeletionProcessAuditLogEntry.ProcessStartedByOwner(Id, createdBy, createdByDevice)
+ };
+ }
+
+ private void Approve(DeviceId createdByDevice)
+ {
+ Status = DeletionProcessStatus.Approved;
+ ApprovedAt = SystemTime.UtcNow;
+ ApprovedByDevice = createdByDevice;
+ GracePeriodEndsAt = SystemTime.UtcNow.AddDays(IdentityDeletionConfiguration.LengthOfGracePeriod);
+ }
+
+ public IdentityDeletionProcessId Id { get; }
+ public IReadOnlyList AuditLog => _auditLog;
+ public DeletionProcessStatus Status { get; private set; }
+ public DateTime CreatedAt { get; }
+
+ public DateTime? ApprovalReminder1SentAt { get; private set; }
+ public DateTime? ApprovalReminder2SentAt { get; private set; }
+ public DateTime? ApprovalReminder3SentAt { get; private set; }
+
+ public DateTime? ApprovedAt { get; private set; }
+ public DeviceId? ApprovedByDevice { get; private set; }
+
+ public DateTime? GracePeriodEndsAt { get; private set; }
+
+ public DateTime? GracePeriodReminder1SentAt { get; private set; }
+ public DateTime? GracePeriodReminder2SentAt { get; private set; }
+ public DateTime? GracePeriodReminder3SentAt { get; private set; }
+
+
+ public bool IsActive()
+ {
+ return Status is DeletionProcessStatus.Approved or DeletionProcessStatus.WaitingForApproval;
+ }
+
+ public DateTime GetEndOfApprovalPeriod()
+ {
+ return CreatedAt.AddDays(IdentityDeletionConfiguration.MaxApprovalTime);
+ }
+
+ public void ApprovalReminder1Sent(IdentityAddress address)
+ {
+ ApprovalReminder1SentAt = SystemTime.UtcNow;
+ _auditLog.Add(IdentityDeletionProcessAuditLogEntry.ApprovalReminder1Sent(Id, address));
+ }
+
+ public void ApprovalReminder2Sent(IdentityAddress address)
+ {
+ ApprovalReminder2SentAt = SystemTime.UtcNow;
+ _auditLog.Add(IdentityDeletionProcessAuditLogEntry.ApprovalReminder2Sent(Id, address));
+ }
+
+ public void ApprovalReminder3Sent(IdentityAddress address)
+ {
+ ApprovalReminder3SentAt = SystemTime.UtcNow;
+ _auditLog.Add(IdentityDeletionProcessAuditLogEntry.ApprovalReminder3Sent(Id, address));
+ }
+
+ public void GracePeriodReminder1Sent(IdentityAddress address)
+ {
+ GracePeriodReminder1SentAt = SystemTime.UtcNow;
+ _auditLog.Add(IdentityDeletionProcessAuditLogEntry.GracePeriodReminder1Sent(Id, address));
+ }
+
+ public void GracePeriodReminder2Sent(IdentityAddress address)
+ {
+ GracePeriodReminder2SentAt = SystemTime.UtcNow;
+ _auditLog.Add(IdentityDeletionProcessAuditLogEntry.GracePeriodReminder2Sent(Id, address));
+ }
+
+ public void GracePeriodReminder3Sent(IdentityAddress address)
+ {
+ GracePeriodReminder3SentAt = SystemTime.UtcNow;
+ _auditLog.Add(IdentityDeletionProcessAuditLogEntry.GracePeriodReminder3Sent(Id, address));
+ }
+
+ public void Approve(IdentityAddress address, DeviceId approvedByDevice)
+ {
+ if (Status != DeletionProcessStatus.WaitingForApproval)
+ throw new DomainException(DomainErrors.NoDeletionProcessWithRequiredStatusExists());
+
+ Approve(approvedByDevice);
+ _auditLog.Add(IdentityDeletionProcessAuditLogEntry.ProcessApproved(Id, address, approvedByDevice));
+ }
+}
diff --git a/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcessAuditLogEntry.cs b/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcessAuditLogEntry.cs
new file mode 100644
index 0000000000..5ea3f8e88d
--- /dev/null
+++ b/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcessAuditLogEntry.cs
@@ -0,0 +1,81 @@
+using Backbone.DevelopmentKit.Identity.ValueObjects;
+using Backbone.Tooling;
+
+namespace Backbone.Modules.Devices.Domain.Entities.Identities;
+
+public class IdentityDeletionProcessAuditLogEntry
+{
+ public static IdentityDeletionProcessAuditLogEntry ProcessStartedByOwner(IdentityDeletionProcessId processId, IdentityAddress identityAddress, DeviceId deviceId)
+ {
+ return new IdentityDeletionProcessAuditLogEntry(processId, "The deletion process was started by the owner. It was automatically approved.", Hasher.HashUtf8(identityAddress.StringValue), Hasher.HashUtf8(deviceId.StringValue), null, DeletionProcessStatus.Approved);
+ }
+
+ public static IdentityDeletionProcessAuditLogEntry ProcessStartedBySupport(IdentityDeletionProcessId processId, IdentityAddress identityAddress)
+ {
+ return new IdentityDeletionProcessAuditLogEntry(processId, "The deletion process was started by support. It is now waiting for approval.", Hasher.HashUtf8(identityAddress.StringValue), null, null, DeletionProcessStatus.WaitingForApproval);
+ }
+
+ public static IdentityDeletionProcessAuditLogEntry ProcessApproved(IdentityDeletionProcessId processId, IdentityAddress identityAddress, DeviceId deviceId)
+ {
+ return new IdentityDeletionProcessAuditLogEntry(processId, "The deletion process was approved.", Hasher.HashUtf8(identityAddress.StringValue), Hasher.HashUtf8(deviceId.StringValue), DeletionProcessStatus.WaitingForApproval, DeletionProcessStatus.Approved);
+ }
+
+ public static IdentityDeletionProcessAuditLogEntry ApprovalReminder1Sent(IdentityDeletionProcessId processId, IdentityAddress identityAddress)
+ {
+ return new IdentityDeletionProcessAuditLogEntry(processId, "The first approval reminder notification has been sent.", Hasher.HashUtf8(identityAddress.StringValue), null, DeletionProcessStatus.WaitingForApproval, DeletionProcessStatus.WaitingForApproval);
+ }
+
+ public static IdentityDeletionProcessAuditLogEntry ApprovalReminder2Sent(IdentityDeletionProcessId processId, IdentityAddress identityAddress)
+ {
+ return new IdentityDeletionProcessAuditLogEntry(processId, "The second approval reminder notification has been sent.", Hasher.HashUtf8(identityAddress.StringValue), null, DeletionProcessStatus.WaitingForApproval, DeletionProcessStatus.WaitingForApproval);
+ }
+
+ public static IdentityDeletionProcessAuditLogEntry ApprovalReminder3Sent(IdentityDeletionProcessId processId, IdentityAddress identityAddress)
+ {
+ return new IdentityDeletionProcessAuditLogEntry(processId, "The third approval reminder notification has been sent.", Hasher.HashUtf8(identityAddress.StringValue), null, DeletionProcessStatus.WaitingForApproval, DeletionProcessStatus.WaitingForApproval);
+ }
+
+ public static IdentityDeletionProcessAuditLogEntry GracePeriodReminder1Sent(IdentityDeletionProcessId processId, IdentityAddress identityAddress)
+ {
+ return new IdentityDeletionProcessAuditLogEntry(processId, "The first grace period reminder notification has been sent.", Hasher.HashUtf8(identityAddress.StringValue), null, DeletionProcessStatus.Approved, DeletionProcessStatus.Approved);
+ }
+
+ public static IdentityDeletionProcessAuditLogEntry GracePeriodReminder2Sent(IdentityDeletionProcessId processId, IdentityAddress identityAddress)
+ {
+ return new IdentityDeletionProcessAuditLogEntry(processId, "The second grace period reminder notification has been sent.", Hasher.HashUtf8(identityAddress.StringValue), null, DeletionProcessStatus.Approved, DeletionProcessStatus.Approved);
+ }
+
+ public static IdentityDeletionProcessAuditLogEntry GracePeriodReminder3Sent(IdentityDeletionProcessId processId, IdentityAddress identityAddress)
+ {
+ return new IdentityDeletionProcessAuditLogEntry(processId, "The third grace period reminder notification has been sent.", Hasher.HashUtf8(identityAddress.StringValue), null, DeletionProcessStatus.Approved, DeletionProcessStatus.Approved);
+ }
+
+ // EF Core needs the empty constructor
+#pragma warning disable CS8618
+ // ReSharper disable once UnusedMember.Local
+ private IdentityDeletionProcessAuditLogEntry()
+#pragma warning restore CS8618
+ {
+ }
+
+ private IdentityDeletionProcessAuditLogEntry(IdentityDeletionProcessId processId, string message, byte[] identityAddressHash, byte[]? deviceIdHash, DeletionProcessStatus? oldStatus, DeletionProcessStatus newStatus)
+ {
+ Id = IdentityDeletionProcessAuditLogEntryId.Generate();
+ ProcessId = processId;
+ CreatedAt = SystemTime.UtcNow;
+ Message = message;
+ IdentityAddressHash = identityAddressHash;
+ DeviceIdHash = deviceIdHash;
+ OldStatus = oldStatus;
+ NewStatus = newStatus;
+ }
+
+ public IdentityDeletionProcessAuditLogEntryId Id { get; }
+ public IdentityDeletionProcessId ProcessId { get; }
+ public DateTime CreatedAt { get; }
+ public string Message { get; }
+ public byte[] IdentityAddressHash { get; }
+ public byte[]? DeviceIdHash { get; }
+ public DeletionProcessStatus? OldStatus { get; }
+ public DeletionProcessStatus NewStatus { get; }
+}
diff --git a/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcessAuditLogEntryId.cs b/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcessAuditLogEntryId.cs
new file mode 100644
index 0000000000..1deca9c1b8
--- /dev/null
+++ b/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcessAuditLogEntryId.cs
@@ -0,0 +1,33 @@
+using Backbone.BuildingBlocks.Domain;
+using Backbone.BuildingBlocks.Domain.Errors;
+using Backbone.BuildingBlocks.Domain.StronglyTypedIds.Records;
+using CSharpFunctionalExtensions;
+
+namespace Backbone.Modules.Devices.Domain.Entities.Identities;
+
+public record IdentityDeletionProcessAuditLogEntryId : StronglyTypedId
+{
+ public const int MAX_LENGTH = DEFAULT_MAX_LENGTH;
+
+ private const string PREFIX = "IDA";
+
+ private static readonly StronglyTypedIdHelpers UTILS = new(PREFIX, DEFAULT_VALID_CHARS, MAX_LENGTH);
+
+ private IdentityDeletionProcessAuditLogEntryId(string value) : base(value) { }
+
+ public static IdentityDeletionProcessAuditLogEntryId Generate()
+ {
+ var randomPart = StringUtils.Generate(DEFAULT_VALID_CHARS, DEFAULT_MAX_LENGTH_WITHOUT_PREFIX);
+ return new IdentityDeletionProcessAuditLogEntryId(PREFIX + randomPart);
+ }
+
+ public static Result Create(string value)
+ {
+ var validationError = UTILS.Validate(value);
+
+ if (validationError != null)
+ return Result.Failure(validationError);
+
+ return Result.Success(new IdentityDeletionProcessAuditLogEntryId(value));
+ }
+}
diff --git a/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcessId.cs b/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcessId.cs
new file mode 100644
index 0000000000..0e55677e45
--- /dev/null
+++ b/Modules/Devices/src/Devices.Domain/Entities/Identities/IdentityDeletionProcessId.cs
@@ -0,0 +1,33 @@
+using Backbone.BuildingBlocks.Domain;
+using Backbone.BuildingBlocks.Domain.Errors;
+using Backbone.BuildingBlocks.Domain.StronglyTypedIds.Records;
+using CSharpFunctionalExtensions;
+
+namespace Backbone.Modules.Devices.Domain.Entities.Identities;
+
+public record IdentityDeletionProcessId : StronglyTypedId
+{
+ public const int MAX_LENGTH = DEFAULT_MAX_LENGTH;
+
+ private const string PREFIX = "IDP";
+
+ private static readonly StronglyTypedIdHelpers UTILS = new(PREFIX, DEFAULT_VALID_CHARS, MAX_LENGTH);
+
+ private IdentityDeletionProcessId(string value) : base(value) { }
+
+ public static IdentityDeletionProcessId Generate()
+ {
+ var randomPart = StringUtils.Generate(DEFAULT_VALID_CHARS, DEFAULT_MAX_LENGTH_WITHOUT_PREFIX);
+ return new IdentityDeletionProcessId(PREFIX + randomPart);
+ }
+
+ public static Result Create(string value)
+ {
+ var validationError = UTILS.Validate(value);
+
+ if (validationError != null)
+ return Result.Failure(validationError);
+
+ return Result.Success(new IdentityDeletionProcessId(value));
+ }
+}
diff --git a/Modules/Devices/src/Devices.Domain/Entities/Identity.cs b/Modules/Devices/src/Devices.Domain/Entities/Identity.cs
deleted file mode 100644
index adf03d9364..0000000000
--- a/Modules/Devices/src/Devices.Domain/Entities/Identity.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-using Backbone.BuildingBlocks.Domain;
-using Backbone.BuildingBlocks.Domain.Errors;
-using Backbone.DevelopmentKit.Identity.ValueObjects;
-using Backbone.Modules.Devices.Domain.Aggregates.Tier;
-using Backbone.Tooling;
-
-namespace Backbone.Modules.Devices.Domain.Entities;
-
-public class Identity
-{
- public Identity(string? clientId, IdentityAddress address, byte[] publicKey, TierId tierId, byte identityVersion)
- {
- ClientId = clientId;
- Address = address;
- PublicKey = publicKey;
- IdentityVersion = identityVersion;
- CreatedAt = SystemTime.UtcNow;
- Devices = new List();
- TierId = tierId;
- }
-
- public string? ClientId { get; set; }
-
- public IdentityAddress Address { get; set; }
- public byte[] PublicKey { get; set; }
- public DateTime CreatedAt { get; set; }
-
- public List Devices { get; set; }
-
- public byte IdentityVersion { get; set; }
-
- public TierId? TierId { get; set; }
-
- public bool IsNew()
- {
- return Devices.Count < 1;
- }
-
- public void ChangeTier(TierId id)
- {
- if (TierId == id)
- {
- throw new DomainException(GenericDomainErrors.NewAndOldParametersMatch("TierId"));
- }
-
- TierId = id;
- }
-}
diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20231124134222_PnsRegistrationAddDevicePushIdentifier.Designer.cs b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20231124134222_PnsRegistrationAddDevicePushIdentifier.Designer.cs
index 7e1e5c39f6..77b7f38434 100644
--- a/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20231124134222_PnsRegistrationAddDevicePushIdentifier.Designer.cs
+++ b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20231124134222_PnsRegistrationAddDevicePushIdentifier.Designer.cs
@@ -199,9 +199,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
.HasColumnType("character(20)")
.IsFixedLength();
- b.Property("DeletionCertificate")
- .HasColumnType("bytea");
-
b.Property("IdentityAddress")
.IsRequired()
.HasMaxLength(36)
diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20231127171049_AddMaxIdentitiesToClients.Designer.cs b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20231127171049_AddMaxIdentitiesToClients.Designer.cs
index 089dde0883..8d5a597ce8 100644
--- a/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20231127171049_AddMaxIdentitiesToClients.Designer.cs
+++ b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20231127171049_AddMaxIdentitiesToClients.Designer.cs
@@ -199,9 +199,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
.HasColumnType("character(20)")
.IsFixedLength();
- b.Property("DeletionCertificate")
- .HasColumnType("bytea");
-
b.Property("IdentityAddress")
.IsRequired()
.HasMaxLength(36)
diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20240213195907_AddModificationsRequestedByIdentityDeletion.Designer.cs b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20240213195907_AddModificationsRequestedByIdentityDeletion.Designer.cs
new file mode 100644
index 0000000000..5cc8926e75
--- /dev/null
+++ b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20240213195907_AddModificationsRequestedByIdentityDeletion.Designer.cs
@@ -0,0 +1,864 @@
+//
+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("20240213195907_AddModificationsRequestedByIdentityDeletion")]
+ partial class AddModificationsRequestedByIdentityDeletion
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.1")
+ .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(36)
+ .IsUnicode(false)
+ .HasColumnType("character(36)")
+ .IsFixedLength();
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("DeviceId");
+
+ b.ToTable("PnsRegistrations");
+ });
+
+ modelBuilder.Entity("Backbone.Modules.Devices.Domain.Aggregates.Tier.Tier", b =>
+ {
+ b.Property("Id")
+ .HasMaxLength(20)
+ .IsUnicode(false)
+ .HasColumnType("character(20)")
+ .IsFixedLength();
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(30)
+ .IsUnicode(true)
+ .HasColumnType("character varying(30)")
+ .IsFixedLength(false);
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.ToTable("Tiers");
+ });
+
+ 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", (string)null);
+ });
+
+ modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b =>
+ {
+ b.Property("Id")
+ .HasMaxLength(20)
+ .IsUnicode(false)
+ .HasColumnType("character(20)")
+ .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(36)
+ .IsUnicode(false)
+ .HasColumnType("character(36)")
+ .IsFixedLength();
+
+ b.HasKey("Id");
+
+ b.HasIndex("IdentityAddress");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", b =>
+ {
+ b.Property("Address")
+ .HasMaxLength(36)
+ .IsUnicode(false)
+ .HasColumnType("character(36)")
+ .IsFixedLength();
+
+ 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")
+ .HasMaxLength(20)
+ .IsUnicode(false)
+ .HasColumnType("character(20)")
+ .IsFixedLength();
+
+ b.Property("TierIdBeforeDeletion")
+ .HasMaxLength(20)
+ .IsUnicode(false)
+ .HasColumnType("character(20)")
+ .IsFixedLength();
+
+ b.HasKey("Address");
+
+ b.ToTable("Identities");
+ });
+
+ 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("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("GracePeriodEndsAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("GracePeriodReminder1SentAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("GracePeriodReminder2SentAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("GracePeriodReminder3SentAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IdentityAddress")
+ .HasMaxLength(36)
+ .IsUnicode(false)
+ .HasColumnType("character(36)")
+ .IsFixedLength();
+
+ b.Property("Status")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("IdentityAddress");
+
+ b.ToTable("IdentityDeletionProcesses", (string)null);
+ });
+
+ modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcessAuditLogEntry", b =>
+ {
+ b.Property("Id")
+ .HasMaxLength(20)
+ .IsUnicode(false)
+ .HasColumnType("character(20)")
+ .IsFixedLength();
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DeviceIdHash")
+ .HasColumnType("bytea");
+
+ b.Property("IdentityAddressHash")
+ .IsRequired()
+ .HasColumnType("bytea");
+
+ b.Property("IdentityDeletionProcessId")
+ .HasMaxLength(20)
+ .IsUnicode(false)
+ .HasColumnType("character(20)")
+ .IsFixedLength();
+
+ b.Property("Message")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("NewStatus")
+ .HasColumnType("integer");
+
+ b.Property("OldStatus")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("IdentityDeletionProcessId");
+
+ b.ToTable("IdentityDeletionProcessAuditLog", (string)null);
+ });
+
+ modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("text");
+
+ b.Property("ApplicationType")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("ClientId")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("ClientSecret")
+ .HasColumnType("text");
+
+ b.Property("ClientType")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("ConcurrencyToken")
+ .IsConcurrencyToken()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("ConsentType")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DefaultTier")
+ .IsRequired()
+ .HasMaxLength(20)
+ .IsUnicode(false)
+ .HasColumnType("character(20)")
+ .IsFixedLength();
+
+ b.Property("DisplayName")
+ .HasColumnType("text");
+
+ b.Property("DisplayNames")
+ .HasColumnType("text");
+
+ b.Property("JsonWebKeySet")
+ .HasColumnType("text");
+
+ b.Property("MaxIdentities")
+ .HasColumnType("integer");
+
+ b.Property("Permissions")
+ .HasColumnType("text");
+
+ b.Property("PostLogoutRedirectUris")
+ .HasColumnType("text");
+
+ b.Property("Properties")
+ .HasColumnType("text");
+
+ b.Property("RedirectUris")
+ .HasColumnType("text");
+
+ b.Property("Requirements")
+ .HasColumnType("text");
+
+ b.Property("Settings")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ClientId")
+ .IsUnique();
+
+ b.HasIndex("DefaultTier");
+
+ b.ToTable("OpenIddictApplications", (string)null);
+ });
+
+ modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("text");
+
+ b.Property("ApplicationId")
+ .HasColumnType("text");
+
+ b.Property("ConcurrencyToken")
+ .IsConcurrencyToken()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Properties")
+ .HasColumnType("text");
+
+ b.Property("Scopes")
+ .HasColumnType("text");
+
+ b.Property("Status")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Subject")
+ .HasMaxLength(400)
+ .HasColumnType("character varying(400)");
+
+ b.Property("Type")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ApplicationId", "Status", "Subject", "Type");
+
+ b.ToTable("OpenIddictAuthorizations", (string)null);
+ });
+
+ modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreScope", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("text");
+
+ b.Property("ConcurrencyToken")
+ .IsConcurrencyToken()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("Descriptions")
+ .HasColumnType("text");
+
+ b.Property("DisplayName")
+ .HasColumnType("text");
+
+ b.Property("DisplayNames")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)");
+
+ b.Property("Properties")
+ .HasColumnType("text");
+
+ b.Property("Resources")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.ToTable("OpenIddictScopes", (string)null);
+ });
+
+ modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("text");
+
+ b.Property("ApplicationId")
+ .HasColumnType("text");
+
+ b.Property("AuthorizationId")
+ .HasColumnType("text");
+
+ b.Property("ConcurrencyToken")
+ .IsConcurrencyToken()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("CreationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ExpirationDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Payload")
+ .HasColumnType("text");
+
+ b.Property("Properties")
+ .HasColumnType("text");
+
+ b.Property("RedemptionDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ReferenceId")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Status")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Subject")
+ .HasMaxLength(400)
+ .HasColumnType("character varying(400)");
+
+ b.Property("Type")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AuthorizationId");
+
+ b.HasIndex("ReferenceId")
+ .IsUnique();
+
+ b.HasIndex("ApplicationId", "Status", "Subject", "Type");
+
+ b.ToTable("OpenIddictTokens", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex");
+
+ b.ToTable("AspNetRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("text");
+
+ b.Property("ClaimValue")
+ .HasColumnType("text");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("text");
+
+ b.Property("ClaimValue")
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("text");
+
+ b.Property("ProviderKey")
+ .HasColumnType("text");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.Property("RoleId")
+ .HasColumnType("text");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.Property("LoginProvider")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .HasColumnType("text");
+
+ b.Property("Value")
+ .HasColumnType("text");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens", (string)null);
+ });
+
+ modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", b =>
+ {
+ b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Device", "Device")
+ .WithOne("User")
+ .HasForeignKey("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", "DeviceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Device");
+ });
+
+ modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b =>
+ {
+ b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", "Identity")
+ .WithMany("Devices")
+ .HasForeignKey("IdentityAddress")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Identity");
+ });
+
+ modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b =>
+ {
+ b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", null)
+ .WithMany("DeletionProcesses")
+ .HasForeignKey("IdentityAddress");
+ });
+
+ modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcessAuditLogEntry", b =>
+ {
+ b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", null)
+ .WithMany("AuditLog")
+ .HasForeignKey("IdentityDeletionProcessId");
+ });
+
+ modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b =>
+ {
+ b.HasOne("Backbone.Modules.Devices.Domain.Aggregates.Tier.Tier", null)
+ .WithMany()
+ .HasForeignKey("DefaultTier")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b =>
+ {
+ b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", "Application")
+ .WithMany("Authorizations")
+ .HasForeignKey("ApplicationId");
+
+ b.Navigation("Application");
+ });
+
+ modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreToken", b =>
+ {
+ b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", "Application")
+ .WithMany("Tokens")
+ .HasForeignKey("ApplicationId");
+
+ b.HasOne("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", "Authorization")
+ .WithMany("Tokens")
+ .HasForeignKey("AuthorizationId");
+
+ b.Navigation("Application");
+
+ b.Navigation("Authorization");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken