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 @@

{{ headerEdit }}

Name - + You must enter a value @@ -34,10 +34,10 @@

{{ headerEdit }}

- -
@@ -47,6 +47,7 @@

{{ headerEdit }}

@@ -56,6 +57,7 @@

{{ headerEdit }}

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", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.Navigation("User") + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", b => + { + b.Navigation("DeletionProcesses"); + + b.Navigation("Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.Navigation("AuditLog"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20240213195907_AddModificationsRequestedByIdentityDeletion.cs b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20240213195907_AddModificationsRequestedByIdentityDeletion.cs new file mode 100644 index 0000000000..2be86f0f04 --- /dev/null +++ b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/20240213195907_AddModificationsRequestedByIdentityDeletion.cs @@ -0,0 +1,138 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backbone.Modules.Devices.Infrastructure.Database.Postgres.Migrations +{ + /// + public partial class AddModificationsRequestedByIdentityDeletion : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Environment", + table: "PnsRegistrations", + type: "integer", + nullable: false, + oldClrType: typeof(int), + oldType: "integer", + oldDefaultValue: 1); + + migrationBuilder.AddColumn( + name: "DeletionGracePeriodEndsAt", + table: "Identities", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "Status", + table: "Identities", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "TierIdBeforeDeletion", + table: "Identities", + type: "character(20)", + unicode: false, + fixedLength: true, + maxLength: 20, + nullable: true); + + migrationBuilder.CreateTable( + name: "IdentityDeletionProcesses", + columns: table => new + { + Id = table.Column(type: "character(20)", unicode: false, fixedLength: true, maxLength: 20, nullable: false), + Status = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + ApprovalReminder1SentAt = table.Column(type: "timestamp with time zone", nullable: true), + ApprovalReminder2SentAt = table.Column(type: "timestamp with time zone", nullable: true), + ApprovalReminder3SentAt = table.Column(type: "timestamp with time zone", nullable: true), + ApprovedAt = table.Column(type: "timestamp with time zone", nullable: true), + ApprovedByDevice = table.Column(type: "character(20)", unicode: false, fixedLength: true, maxLength: 20, nullable: true), + GracePeriodEndsAt = table.Column(type: "timestamp with time zone", nullable: true), + GracePeriodReminder1SentAt = table.Column(type: "timestamp with time zone", nullable: true), + GracePeriodReminder2SentAt = table.Column(type: "timestamp with time zone", nullable: true), + GracePeriodReminder3SentAt = table.Column(type: "timestamp with time zone", nullable: true), + IdentityAddress = table.Column(type: "character(36)", unicode: false, fixedLength: true, maxLength: 36, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_IdentityDeletionProcesses", x => x.Id); + table.ForeignKey( + name: "FK_IdentityDeletionProcesses_Identities_IdentityAddress", + column: x => x.IdentityAddress, + principalTable: "Identities", + principalColumn: "Address"); + }); + + migrationBuilder.CreateTable( + name: "IdentityDeletionProcessAuditLog", + columns: table => new + { + Id = table.Column(type: "character(20)", unicode: false, fixedLength: true, maxLength: 20, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + Message = table.Column(type: "text", nullable: false), + IdentityAddressHash = table.Column(type: "bytea", nullable: false), + DeviceIdHash = table.Column(type: "bytea", nullable: true), + OldStatus = table.Column(type: "integer", nullable: true), + NewStatus = table.Column(type: "integer", nullable: false), + IdentityDeletionProcessId = table.Column(type: "character(20)", unicode: false, fixedLength: true, maxLength: 20, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_IdentityDeletionProcessAuditLog", x => x.Id); + table.ForeignKey( + name: "FK_IdentityDeletionProcessAuditLog_IdentityDeletionProcesses_I~", + column: x => x.IdentityDeletionProcessId, + principalTable: "IdentityDeletionProcesses", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_IdentityDeletionProcessAuditLog_IdentityDeletionProcessId", + table: "IdentityDeletionProcessAuditLog", + column: "IdentityDeletionProcessId"); + + migrationBuilder.CreateIndex( + name: "IX_IdentityDeletionProcesses_IdentityAddress", + table: "IdentityDeletionProcesses", + column: "IdentityAddress"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "IdentityDeletionProcessAuditLog"); + + migrationBuilder.DropTable( + name: "IdentityDeletionProcesses"); + + migrationBuilder.DropColumn( + name: "DeletionGracePeriodEndsAt", + table: "Identities"); + + migrationBuilder.DropColumn( + name: "Status", + table: "Identities"); + + migrationBuilder.DropColumn( + name: "TierIdBeforeDeletion", + table: "Identities"); + + migrationBuilder.AlterColumn( + name: "Environment", + table: "PnsRegistrations", + type: "integer", + nullable: false, + defaultValue: 1, + oldClrType: typeof(int), + oldType: "integer"); + } + } +} diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/DevicesDbContextModelSnapshot.cs b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/DevicesDbContextModelSnapshot.cs index d523655328..d4f0a25b5f 100644 --- a/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/DevicesDbContextModelSnapshot.cs +++ b/Modules/Devices/src/Devices.Infrastructure.Database.Postgres/Migrations/DevicesDbContextModelSnapshot.cs @@ -42,9 +42,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsFixedLength(); b.Property("Environment") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(1); + .HasColumnType("integer"); b.Property("Handle") .IsRequired() @@ -91,7 +89,26 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Tiers"); }); - modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.ApplicationUser", b => + 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"); @@ -150,26 +167,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AspNetUsers", (string)null); }); - 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.Device", b => + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => { b.Property("Id") .HasMaxLength(20) @@ -210,7 +208,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Devices"); }); - modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identity", b => + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", b => { b.Property("Address") .HasMaxLength(36) @@ -225,6 +223,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("DeletionGracePeriodEndsAt") + .HasColumnType("timestamp with time zone"); + b.Property("IdentityVersion") .HasColumnType("smallint"); @@ -232,17 +233,124 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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") @@ -598,20 +706,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AspNetUserTokens", (string)null); }); - modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.ApplicationUser", b => + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", b => { - b.HasOne("Backbone.Modules.Devices.Domain.Entities.Device", "Device") + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Device", "Device") .WithOne("User") - .HasForeignKey("Backbone.Modules.Devices.Domain.Entities.ApplicationUser", "DeviceId") + .HasForeignKey("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", "DeviceId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("Device"); }); - modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Device", b => + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => { - b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identity", "Identity") + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", "Identity") .WithMany("Devices") .HasForeignKey("IdentityAddress") .OnDelete(DeleteBehavior.Cascade) @@ -620,6 +728,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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) @@ -664,7 +786,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => { - b.HasOne("Backbone.Modules.Devices.Domain.Entities.ApplicationUser", null) + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -673,7 +795,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { - b.HasOne("Backbone.Modules.Devices.Domain.Entities.ApplicationUser", null) + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -688,7 +810,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Backbone.Modules.Devices.Domain.Entities.ApplicationUser", null) + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -697,24 +819,31 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => { - b.HasOne("Backbone.Modules.Devices.Domain.Entities.ApplicationUser", null) + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); - modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Device", b => + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => { b.Navigation("User") .IsRequired(); }); - modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identity", b => + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", b => { + b.Navigation("DeletionProcesses"); + b.Navigation("Devices"); }); + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.Navigation("AuditLog"); + }); + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => { b.Navigation("Authorizations"); diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20231124134205_PnsRegistrationAddDevicePushIdentifier.Designer.cs b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20231124134205_PnsRegistrationAddDevicePushIdentifier.Designer.cs index e1a7fc1355..6d4124e91c 100644 --- a/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20231124134205_PnsRegistrationAddDevicePushIdentifier.Designer.cs +++ b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20231124134205_PnsRegistrationAddDevicePushIdentifier.Designer.cs @@ -200,9 +200,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("char(20)") .IsFixedLength(); - b.Property("DeletionCertificate") - .HasColumnType("varbinary(max)"); - b.Property("IdentityAddress") .IsRequired() .HasMaxLength(36) diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20231127172833_AddMaxIdentitiesToClients.Designer.cs b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20231127172833_AddMaxIdentitiesToClients.Designer.cs index 0edb570542..cab7c96a19 100644 --- a/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20231127172833_AddMaxIdentitiesToClients.Designer.cs +++ b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20231127172833_AddMaxIdentitiesToClients.Designer.cs @@ -200,9 +200,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("char(20)") .IsFixedLength(); - b.Property("DeletionCertificate") - .HasColumnType("varbinary(max)"); - b.Property("IdentityAddress") .IsRequired() .HasMaxLength(36) diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20240213195858_AddModificationsRequestedByIdentityDeletion.Designer.cs b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20240213195858_AddModificationsRequestedByIdentityDeletion.Designer.cs new file mode 100644 index 0000000000..10721f5f75 --- /dev/null +++ b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20240213195858_AddModificationsRequestedByIdentityDeletion.Designer.cs @@ -0,0 +1,869 @@ +// +using System; +using Backbone.Modules.Devices.Infrastructure.Persistence.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Backbone.Modules.Devices.Infrastructure.Database.SqlServer.Migrations +{ + [DbContext(typeof(DevicesDbContext))] + [Migration("20240213195858_AddModificationsRequestedByIdentityDeletion")] + partial class AddModificationsRequestedByIdentityDeletion + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Aggregates.PushNotifications.PnsRegistration", b => + { + b.Property("DeviceId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("AppId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("DevicePushIdentifier") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("Environment") + .HasColumnType("int"); + + b.Property("Handle") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(true) + .HasColumnType("nvarchar(200)") + .IsFixedLength(false); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("char(36)") + .IsFixedLength(); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("DeviceId"); + + b.ToTable("PnsRegistrations"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Aggregates.Tier.Tier", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("Name") + .IsRequired() + .HasMaxLength(30) + .IsUnicode(true) + .HasColumnType("nvarchar(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("char(20)") + .IsFixedLength(); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Challenges", "Challenges", t => + { + t.ExcludeFromMigrations(); + }); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("LastLoginAt") + .HasColumnType("datetime2"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("UserName") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedByDevice") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("IdentityAddress") + .IsRequired() + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("char(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("char(36)") + .IsFixedLength(); + + b.Property("ClientId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeletionGracePeriodEndsAt") + .HasColumnType("datetime2"); + + b.Property("IdentityVersion") + .HasColumnType("tinyint"); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TierId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("TierIdBeforeDeletion") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(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("char(20)") + .IsFixedLength(); + + b.Property("ApprovalReminder1SentAt") + .HasColumnType("datetime2"); + + b.Property("ApprovalReminder2SentAt") + .HasColumnType("datetime2"); + + b.Property("ApprovalReminder3SentAt") + .HasColumnType("datetime2"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApprovedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("GracePeriodEndsAt") + .HasColumnType("datetime2"); + + b.Property("GracePeriodReminder1SentAt") + .HasColumnType("datetime2"); + + b.Property("GracePeriodReminder2SentAt") + .HasColumnType("datetime2"); + + b.Property("GracePeriodReminder3SentAt") + .HasColumnType("datetime2"); + + b.Property("IdentityAddress") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("char(36)") + .IsFixedLength(); + + b.Property("Status") + .HasColumnType("int"); + + 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("char(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceIdHash") + .HasColumnType("varbinary(max)"); + + b.Property("IdentityAddressHash") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("IdentityDeletionProcessId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NewStatus") + .HasColumnType("int"); + + b.Property("OldStatus") + .HasColumnType("int"); + + 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("nvarchar(450)"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ClientSecret") + .HasColumnType("nvarchar(max)"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DefaultTier") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayNames") + .HasColumnType("nvarchar(max)"); + + b.Property("JsonWebKeySet") + .HasColumnType("nvarchar(max)"); + + b.Property("MaxIdentities") + .HasColumnType("int"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("nvarchar(max)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("RedirectUris") + .HasColumnType("nvarchar(max)"); + + b.Property("Requirements") + .HasColumnType("nvarchar(max)"); + + b.Property("Settings") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique() + .HasFilter("[ClientId] IS NOT NULL"); + + b.HasIndex("DefaultTier"); + + b.ToTable("OpenIddictApplications", (string)null); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationId") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("Scopes") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Descriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayNames") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("Resources") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique() + .HasFilter("[Name] IS NOT NULL"); + + b.ToTable("OpenIddictScopes", (string)null); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationId") + .HasColumnType("nvarchar(450)"); + + b.Property("AuthorizationId") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("ExpirationDate") + .HasColumnType("datetime2"); + + b.Property("Payload") + .HasColumnType("nvarchar(max)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("RedemptionDate") + .HasColumnType("datetime2"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique() + .HasFilter("[ReferenceId] IS NOT NULL"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (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", b => + { + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => + { + b.Navigation("User") + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", b => + { + b.Navigation("DeletionProcesses"); + + b.Navigation("Devices"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.Navigation("AuditLog"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20240213195858_AddModificationsRequestedByIdentityDeletion.cs b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20240213195858_AddModificationsRequestedByIdentityDeletion.cs new file mode 100644 index 0000000000..77f6f651fb --- /dev/null +++ b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/20240213195858_AddModificationsRequestedByIdentityDeletion.cs @@ -0,0 +1,138 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backbone.Modules.Devices.Infrastructure.Database.SqlServer.Migrations +{ + /// + public partial class AddModificationsRequestedByIdentityDeletion : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Environment", + table: "PnsRegistrations", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int", + oldDefaultValue: 1); + + migrationBuilder.AddColumn( + name: "DeletionGracePeriodEndsAt", + table: "Identities", + type: "datetime2", + nullable: true); + + migrationBuilder.AddColumn( + name: "Status", + table: "Identities", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "TierIdBeforeDeletion", + table: "Identities", + type: "char(20)", + unicode: false, + fixedLength: true, + maxLength: 20, + nullable: true); + + migrationBuilder.CreateTable( + name: "IdentityDeletionProcesses", + columns: table => new + { + Id = table.Column(type: "char(20)", unicode: false, fixedLength: true, maxLength: 20, nullable: false), + Status = table.Column(type: "int", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + ApprovalReminder1SentAt = table.Column(type: "datetime2", nullable: true), + ApprovalReminder2SentAt = table.Column(type: "datetime2", nullable: true), + ApprovalReminder3SentAt = table.Column(type: "datetime2", nullable: true), + ApprovedAt = table.Column(type: "datetime2", nullable: true), + ApprovedByDevice = table.Column(type: "char(20)", unicode: false, fixedLength: true, maxLength: 20, nullable: true), + GracePeriodEndsAt = table.Column(type: "datetime2", nullable: true), + GracePeriodReminder1SentAt = table.Column(type: "datetime2", nullable: true), + GracePeriodReminder2SentAt = table.Column(type: "datetime2", nullable: true), + GracePeriodReminder3SentAt = table.Column(type: "datetime2", nullable: true), + IdentityAddress = table.Column(type: "char(36)", unicode: false, fixedLength: true, maxLength: 36, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_IdentityDeletionProcesses", x => x.Id); + table.ForeignKey( + name: "FK_IdentityDeletionProcesses_Identities_IdentityAddress", + column: x => x.IdentityAddress, + principalTable: "Identities", + principalColumn: "Address"); + }); + + migrationBuilder.CreateTable( + name: "IdentityDeletionProcessAuditLog", + columns: table => new + { + Id = table.Column(type: "char(20)", unicode: false, fixedLength: true, maxLength: 20, nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + Message = table.Column(type: "nvarchar(max)", nullable: false), + IdentityAddressHash = table.Column(type: "varbinary(max)", nullable: false), + DeviceIdHash = table.Column(type: "varbinary(max)", nullable: true), + OldStatus = table.Column(type: "int", nullable: true), + NewStatus = table.Column(type: "int", nullable: false), + IdentityDeletionProcessId = table.Column(type: "char(20)", unicode: false, fixedLength: true, maxLength: 20, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_IdentityDeletionProcessAuditLog", x => x.Id); + table.ForeignKey( + name: "FK_IdentityDeletionProcessAuditLog_IdentityDeletionProcesses_IdentityDeletionProcessId", + column: x => x.IdentityDeletionProcessId, + principalTable: "IdentityDeletionProcesses", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_IdentityDeletionProcessAuditLog_IdentityDeletionProcessId", + table: "IdentityDeletionProcessAuditLog", + column: "IdentityDeletionProcessId"); + + migrationBuilder.CreateIndex( + name: "IX_IdentityDeletionProcesses_IdentityAddress", + table: "IdentityDeletionProcesses", + column: "IdentityAddress"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "IdentityDeletionProcessAuditLog"); + + migrationBuilder.DropTable( + name: "IdentityDeletionProcesses"); + + migrationBuilder.DropColumn( + name: "DeletionGracePeriodEndsAt", + table: "Identities"); + + migrationBuilder.DropColumn( + name: "Status", + table: "Identities"); + + migrationBuilder.DropColumn( + name: "TierIdBeforeDeletion", + table: "Identities"); + + migrationBuilder.AlterColumn( + name: "Environment", + table: "PnsRegistrations", + type: "int", + nullable: false, + defaultValue: 1, + oldClrType: typeof(int), + oldType: "int"); + } + } +} diff --git a/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/ApplicationDbContextModelSnapshot.cs b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/ApplicationDbContextModelSnapshot.cs index 031660c404..cccac00d0f 100644 --- a/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Modules/Devices/src/Devices.Infrastructure.Database.SqlServer/Migrations/ApplicationDbContextModelSnapshot.cs @@ -42,9 +42,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsFixedLength(); b.Property("Environment") - .ValueGeneratedOnAdd() - .HasColumnType("int") - .HasDefaultValue(1); + .HasColumnType("int"); b.Property("Handle") .IsRequired() @@ -91,7 +89,26 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Tiers"); }); - modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.ApplicationUser", b => + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Challenge", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Challenges", "Challenges", t => + { + t.ExcludeFromMigrations(); + }); + }); + + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", b => { b.Property("Id") .HasColumnType("nvarchar(450)"); @@ -151,26 +168,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AspNetUsers", (string)null); }); - modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Challenge", b => - { - b.Property("Id") - .HasMaxLength(20) - .IsUnicode(false) - .HasColumnType("char(20)") - .IsFixedLength(); - - b.Property("ExpiresAt") - .HasColumnType("datetime2"); - - b.HasKey("Id"); - - b.ToTable("Challenges", "Challenges", t => - { - t.ExcludeFromMigrations(); - }); - }); - - modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Device", b => + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => { b.Property("Id") .HasMaxLength(20) @@ -211,7 +209,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Devices"); }); - modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identity", b => + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", b => { b.Property("Address") .HasMaxLength(36) @@ -226,6 +224,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("datetime2"); + b.Property("DeletionGracePeriodEndsAt") + .HasColumnType("datetime2"); + b.Property("IdentityVersion") .HasColumnType("tinyint"); @@ -233,17 +234,124 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("varbinary(max)"); + b.Property("Status") + .HasColumnType("int"); + b.Property("TierId") .HasMaxLength(20) .IsUnicode(false) .HasColumnType("char(20)") .IsFixedLength(); + b.Property("TierIdBeforeDeletion") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(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("char(20)") + .IsFixedLength(); + + b.Property("ApprovalReminder1SentAt") + .HasColumnType("datetime2"); + + b.Property("ApprovalReminder2SentAt") + .HasColumnType("datetime2"); + + b.Property("ApprovalReminder3SentAt") + .HasColumnType("datetime2"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApprovedByDevice") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("GracePeriodEndsAt") + .HasColumnType("datetime2"); + + b.Property("GracePeriodReminder1SentAt") + .HasColumnType("datetime2"); + + b.Property("GracePeriodReminder2SentAt") + .HasColumnType("datetime2"); + + b.Property("GracePeriodReminder3SentAt") + .HasColumnType("datetime2"); + + b.Property("IdentityAddress") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("char(36)") + .IsFixedLength(); + + b.Property("Status") + .HasColumnType("int"); + + 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("char(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DeviceIdHash") + .HasColumnType("varbinary(max)"); + + b.Property("IdentityAddressHash") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("IdentityDeletionProcessId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NewStatus") + .HasColumnType("int"); + + b.Property("OldStatus") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("IdentityDeletionProcessId"); + + b.ToTable("IdentityDeletionProcessAuditLog", (string)null); + }); + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => { b.Property("Id") @@ -603,20 +711,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AspNetUserTokens", (string)null); }); - modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.ApplicationUser", b => + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", b => { - b.HasOne("Backbone.Modules.Devices.Domain.Entities.Device", "Device") + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Device", "Device") .WithOne("User") - .HasForeignKey("Backbone.Modules.Devices.Domain.Entities.ApplicationUser", "DeviceId") + .HasForeignKey("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", "DeviceId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); b.Navigation("Device"); }); - modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Device", b => + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => { - b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identity", "Identity") + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", "Identity") .WithMany("Devices") .HasForeignKey("IdentityAddress") .OnDelete(DeleteBehavior.Cascade) @@ -625,6 +733,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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) @@ -669,7 +791,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => { - b.HasOne("Backbone.Modules.Devices.Domain.Entities.ApplicationUser", null) + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -678,7 +800,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { - b.HasOne("Backbone.Modules.Devices.Domain.Entities.ApplicationUser", null) + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -693,7 +815,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Backbone.Modules.Devices.Domain.Entities.ApplicationUser", null) + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -702,24 +824,31 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => { - b.HasOne("Backbone.Modules.Devices.Domain.Entities.ApplicationUser", null) + b.HasOne("Backbone.Modules.Devices.Domain.Entities.Identities.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); - modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Device", b => + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Device", b => { b.Navigation("User") .IsRequired(); }); - modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identity", b => + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.Identity", b => { + b.Navigation("DeletionProcesses"); + b.Navigation("Devices"); }); + modelBuilder.Entity("Backbone.Modules.Devices.Domain.Entities.Identities.IdentityDeletionProcess", b => + { + b.Navigation("AuditLog"); + }); + modelBuilder.Entity("Backbone.Modules.Devices.Infrastructure.OpenIddict.CustomOpenIddictEntityFrameworkCoreApplication", b => { b.Navigation("Authorizations"); diff --git a/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/DevicesDbContext.cs b/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/DevicesDbContext.cs index 3a4f8547b0..bb5be4eaf1 100644 --- a/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/DevicesDbContext.cs +++ b/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/DevicesDbContext.cs @@ -7,6 +7,7 @@ using Backbone.Modules.Devices.Domain.Aggregates.PushNotifications.Handles; using Backbone.Modules.Devices.Domain.Aggregates.Tier; using Backbone.Modules.Devices.Domain.Entities; +using Backbone.Modules.Devices.Domain.Entities.Identities; using Backbone.Modules.Devices.Infrastructure.Persistence.Database.EntityConfigurations; using Backbone.Modules.Devices.Infrastructure.Persistence.Database.ValueConverters; using Backbone.Tooling.Extensions; @@ -119,16 +120,16 @@ private List GetAppIdsForWhichNoConfigurationExists(string platform, ICo { var query = PnsRegistrations.FromSqlRaw( Database.IsNpgsql() - ? $""" - SELECT "AppId" - FROM "Devices"."PnsRegistrations" - WHERE "Handle" LIKE '{platform}%' - """ - : $""" - SELECT "AppId" - FROM [Devices].[PnsRegistrations] - WHERE Handle LIKE '{platform}%' - """); + ? $""" + SELECT "AppId" + FROM "Devices"."PnsRegistrations" + WHERE "Handle" LIKE '{platform}%' + """ + : $""" + SELECT "AppId" + FROM [Devices].[PnsRegistrations] + WHERE Handle LIKE '{platform}%' + """); return query .Where(x => !supportedAppIds.Contains(x.AppId)) @@ -153,6 +154,10 @@ protected override void ConfigureConventions(ModelConfigurationBuilder configura .HaveMaxLength(TierId.MAX_LENGTH).HaveConversion(); configurationBuilder.Properties().AreUnicode().AreFixedLength(false) .HaveMaxLength(TierName.MAX_LENGTH).HaveConversion(); + configurationBuilder.Properties().AreUnicode(false).AreFixedLength() + .HaveMaxLength(IdentityDeletionProcessId.MAX_LENGTH).HaveConversion(); + configurationBuilder.Properties().AreUnicode(false).AreFixedLength() + .HaveMaxLength(IdentityDeletionProcessAuditLogEntryId.MAX_LENGTH).HaveConversion(); configurationBuilder.Properties().AreUnicode().AreFixedLength(false) .HaveMaxLength(200).HaveConversion(); diff --git a/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/EntityConfigurations/DeviceEntityTypeConfiguration.cs b/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/EntityConfigurations/DeviceEntityTypeConfiguration.cs index 5568afa986..dec4a3ca69 100644 --- a/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/EntityConfigurations/DeviceEntityTypeConfiguration.cs +++ b/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/EntityConfigurations/DeviceEntityTypeConfiguration.cs @@ -1,4 +1,4 @@ -using Backbone.Modules.Devices.Domain.Entities; +using Backbone.Modules.Devices.Domain.Entities.Identities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/EntityConfigurations/IdentityDeletionProcessEntityTypeConfiguration.cs b/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/EntityConfigurations/IdentityDeletionProcessEntityTypeConfiguration.cs new file mode 100644 index 0000000000..578fa18247 --- /dev/null +++ b/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/EntityConfigurations/IdentityDeletionProcessEntityTypeConfiguration.cs @@ -0,0 +1,34 @@ +using Backbone.Modules.Devices.Domain.Entities.Identities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Backbone.Modules.Devices.Infrastructure.Persistence.Database.EntityConfigurations; + +public class IdentityDeletionProcessEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("IdentityDeletionProcesses"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Status); + builder.Property(x => x.CreatedAt); + builder.Property(x => x.ApprovalReminder1SentAt); + builder.Property(x => x.ApprovalReminder2SentAt); + builder.Property(x => x.ApprovalReminder3SentAt); + } +} + +public class IdentityDeletionProcessAuditLogEntryEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("IdentityDeletionProcessAuditLog"); + builder.HasKey(x => x.Id); + builder.Property(x => x.DeviceIdHash); + builder.Property(x => x.IdentityAddressHash); + builder.Property(x => x.CreatedAt); + builder.Property(x => x.Message); + builder.Property(x => x.NewStatus); + builder.Property(x => x.OldStatus); + } +} diff --git a/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/EntityConfigurations/IdentityEntityTypeConfiguration.cs b/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/EntityConfigurations/IdentityEntityTypeConfiguration.cs index 1551829234..25bdcc6810 100644 --- a/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/EntityConfigurations/IdentityEntityTypeConfiguration.cs +++ b/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/EntityConfigurations/IdentityEntityTypeConfiguration.cs @@ -1,4 +1,4 @@ -using Backbone.Modules.Devices.Domain.Entities; +using Backbone.Modules.Devices.Domain.Entities.Identities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/EntityConfigurations/UserEntityTypeConfiguration.cs b/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/EntityConfigurations/UserEntityTypeConfiguration.cs index ac033424a3..84cf3b416c 100644 --- a/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/EntityConfigurations/UserEntityTypeConfiguration.cs +++ b/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/EntityConfigurations/UserEntityTypeConfiguration.cs @@ -1,5 +1,5 @@ -using Backbone.DevelopmentKit.Identity.ValueObjects; -using Backbone.Modules.Devices.Domain.Entities; +using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Devices.Domain.Entities.Identities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/QueryableExtensions/DeviceQueryableExtensions.cs b/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/QueryableExtensions/DeviceQueryableExtensions.cs index 8c36423ee8..9e439dd5d6 100644 --- a/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/QueryableExtensions/DeviceQueryableExtensions.cs +++ b/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/QueryableExtensions/DeviceQueryableExtensions.cs @@ -1,6 +1,6 @@ -using Backbone.BuildingBlocks.Application.Abstractions.Exceptions; +using Backbone.BuildingBlocks.Application.Abstractions.Exceptions; using Backbone.DevelopmentKit.Identity.ValueObjects; -using Backbone.Modules.Devices.Domain.Entities; +using Backbone.Modules.Devices.Domain.Entities.Identities; using Microsoft.EntityFrameworkCore; namespace Backbone.Modules.Devices.Infrastructure.Persistence.Database.QueryableExtensions; diff --git a/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/QueryableExtensions/IdentityQueryableExtensions.cs b/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/QueryableExtensions/IdentityQueryableExtensions.cs index ca2150e60c..4707c69d2b 100644 --- a/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/QueryableExtensions/IdentityQueryableExtensions.cs +++ b/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/QueryableExtensions/IdentityQueryableExtensions.cs @@ -1,6 +1,6 @@ -using Backbone.BuildingBlocks.Application.Abstractions.Exceptions; +using Backbone.BuildingBlocks.Application.Abstractions.Exceptions; using Backbone.DevelopmentKit.Identity.ValueObjects; -using Backbone.Modules.Devices.Domain.Entities; +using Backbone.Modules.Devices.Domain.Entities.Identities; using Microsoft.EntityFrameworkCore; namespace Backbone.Modules.Devices.Infrastructure.Persistence.Database.QueryableExtensions; diff --git a/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/ValueConverters/IdentityDeletionProcessAuditLogEntryIdEntityFrameworkValueConverter.cs b/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/ValueConverters/IdentityDeletionProcessAuditLogEntryIdEntityFrameworkValueConverter.cs new file mode 100644 index 0000000000..e5896ba34f --- /dev/null +++ b/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/ValueConverters/IdentityDeletionProcessAuditLogEntryIdEntityFrameworkValueConverter.cs @@ -0,0 +1,37 @@ +using Backbone.Modules.Devices.Domain.Aggregates.Tier; +using Backbone.Modules.Devices.Domain.Entities.Identities; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Backbone.Modules.Devices.Infrastructure.Persistence.Database.ValueConverters; + +public class IdentityDeletionProcessAuditLogEntryIdEntityFrameworkValueConverter : ValueConverter +{ + public IdentityDeletionProcessAuditLogEntryIdEntityFrameworkValueConverter() : this(null) + { + } + + public IdentityDeletionProcessAuditLogEntryIdEntityFrameworkValueConverter(ConverterMappingHints? mappingHints) + : base( + id => id.Value, + value => IdentityDeletionProcessAuditLogEntryId.Create(value).Value, + mappingHints + ) + { + } +} + +public class NullableIdentityDeletionProcessAuditLogEntryIdEntityFrameworkValueConverter : ValueConverter +{ + public NullableIdentityDeletionProcessAuditLogEntryIdEntityFrameworkValueConverter() : this(null) + { + } + + public NullableIdentityDeletionProcessAuditLogEntryIdEntityFrameworkValueConverter(ConverterMappingHints? mappingHints) + : base( + id => id == null ? null : id.Value, + value => value == null ? null : IdentityDeletionProcessAuditLogEntryId.Create(value).Value, + mappingHints + ) + { + } +} diff --git a/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/ValueConverters/IdentityDeletionProcessIdEntityFrameworkValueConverter.cs b/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/ValueConverters/IdentityDeletionProcessIdEntityFrameworkValueConverter.cs new file mode 100644 index 0000000000..7ad8a8ce92 --- /dev/null +++ b/Modules/Devices/src/Devices.Infrastructure/Persistence/Database/ValueConverters/IdentityDeletionProcessIdEntityFrameworkValueConverter.cs @@ -0,0 +1,37 @@ +using Backbone.Modules.Devices.Domain.Aggregates.Tier; +using Backbone.Modules.Devices.Domain.Entities.Identities; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Backbone.Modules.Devices.Infrastructure.Persistence.Database.ValueConverters; + +public class IdentityDeletionProcessIdEntityFrameworkValueConverter : ValueConverter +{ + public IdentityDeletionProcessIdEntityFrameworkValueConverter() : this(null) + { + } + + public IdentityDeletionProcessIdEntityFrameworkValueConverter(ConverterMappingHints? mappingHints) + : base( + id => id.Value, + value => IdentityDeletionProcessId.Create(value).Value, + mappingHints + ) + { + } +} + +public class NullableIdentityDeletionProcessIdEntityFrameworkValueConverter : ValueConverter +{ + public NullableIdentityDeletionProcessIdEntityFrameworkValueConverter() : this(null) + { + } + + public NullableIdentityDeletionProcessIdEntityFrameworkValueConverter(ConverterMappingHints? mappingHints) + : base( + id => id == null ? null : id.Value, + value => value == null ? null : IdentityDeletionProcessId.Create(value).Value, + mappingHints + ) + { + } +} diff --git a/Modules/Devices/src/Devices.Infrastructure/Persistence/Repository/IdentitiesRepository.cs b/Modules/Devices/src/Devices.Infrastructure/Persistence/Repository/IdentitiesRepository.cs index 7dd930fcff..608b2a7747 100644 --- a/Modules/Devices/src/Devices.Infrastructure/Persistence/Repository/IdentitiesRepository.cs +++ b/Modules/Devices/src/Devices.Infrastructure/Persistence/Repository/IdentitiesRepository.cs @@ -5,13 +5,14 @@ using Backbone.DevelopmentKit.Identity.ValueObjects; using Backbone.Modules.Devices.Application; using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository; -using Backbone.Modules.Devices.Domain.Entities; +using Backbone.Modules.Devices.Domain.Entities.Identities; using Backbone.Modules.Devices.Infrastructure.Persistence.Database; using Backbone.Modules.Devices.Infrastructure.Persistence.Database.QueryableExtensions; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; namespace Backbone.Modules.Devices.Infrastructure.Persistence.Repository; + public class IdentitiesRepository : IIdentitiesRepository { private readonly DbSet _identities; @@ -52,6 +53,13 @@ public async Task Exists(IdentityAddress address, CancellationToken cancel .AnyAsync(i => i.Address == address, cancellationToken); } + public async Task> FindAllWithDeletionProcessInStatus(DeletionProcessStatus status, CancellationToken cancellationToken, bool track = false) + { + return await (track ? _identities : _readonlyIdentities) + .IncludeAll(_dbContext) + .Where(i => i.DeletionProcesses.Any(d => d.Status == status)) + .ToListAsync(cancellationToken); + } public async Task CountByClientId(string clientId, CancellationToken cancellationToken) { diff --git a/Modules/Devices/test/Devices.Application.Tests/TestDataGenerator.cs b/Modules/Devices/test/Devices.Application.Tests/TestDataGenerator.cs index 04cea47323..221e7537c6 100644 --- a/Modules/Devices/test/Devices.Application.Tests/TestDataGenerator.cs +++ b/Modules/Devices/test/Devices.Application.Tests/TestDataGenerator.cs @@ -1,5 +1,6 @@ using Backbone.Modules.Devices.Domain.Aggregates.Tier; -using Backbone.Modules.Devices.Domain.Entities; +using Backbone.Modules.Devices.Domain.Entities.Identities; +using Backbone.Tooling; using static Backbone.UnitTestTools.Data.TestDataGenerator; namespace Backbone.Modules.Devices.Application.Tests; @@ -20,4 +21,43 @@ public static Identity CreateIdentity() CreateRandomTierId(), 1); } + + public static Identity CreateIdentityWithOneDevice() + { + var identity = new Identity( + CreateRandomDeviceId(), + CreateRandomIdentityAddress(), + CreateRandomBytes(), + CreateRandomTierId(), + 1); + identity.Devices.Add(new Device(identity)); + + return identity; + } + + public static Identity CreateIdentityWithApprovedDeletionProcess(DateTime approvalDate) + { + var currentDateTime = SystemTime.UtcNow; + + var identity = CreateIdentityWithOneDevice(); + SystemTime.Set(approvalDate); + identity.StartDeletionProcessAsOwner(identity.Devices[0].Id); + + SystemTime.Set(currentDateTime); + + return identity; + } + + public static Identity CreateIdentityWithDeletionProcessWaitingForApproval(DateTime deletionProcessStartedAt) + { + var currentDateTime = SystemTime.UtcNow; + + var identity = CreateIdentityWithOneDevice(); + SystemTime.Set(deletionProcessStartedAt); + identity.StartDeletionProcessAsSupport(); + + SystemTime.Set(currentDateTime); + + return identity; + } } diff --git a/Modules/Devices/test/Devices.Application.Tests/Tests/Devices/Commands/DeleteDevice/HandlerTests.cs b/Modules/Devices/test/Devices.Application.Tests/Tests/Devices/Commands/DeleteDevice/HandlerTests.cs index ff0a3ce93b..dcd8f5f608 100644 --- a/Modules/Devices/test/Devices.Application.Tests/Tests/Devices/Commands/DeleteDevice/HandlerTests.cs +++ b/Modules/Devices/test/Devices.Application.Tests/Tests/Devices/Commands/DeleteDevice/HandlerTests.cs @@ -3,6 +3,7 @@ using Backbone.Modules.Devices.Application.Devices.Commands.DeleteDevice; 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 FakeItEasy; using FluentAssertions; @@ -17,8 +18,6 @@ public class HandlerTests public async Task Deletes_unOnboarded_device_owned_by_identity() { // Arrange - var startTime = SystemTime.UtcNow; - var identity = TestDataGenerator.CreateIdentity(); var unOnboardedDevice = CreateUnOnboardedDevice(identity); var onboardedDevice = CreateOnboardedDevice(identity); @@ -38,12 +37,14 @@ public async Task Deletes_unOnboarded_device_owned_by_identity() DeviceId = unOnboardedDevice.Id }; + var utcNow = DateTime.Parse("2000-01-01"); + // Act await handler.Handle(deleteDeviceCommand, CancellationToken.None); // Assert unOnboardedDevice.DeletedAt.Should().NotBeNull(); - unOnboardedDevice.DeletedAt.Should().BeAfter(startTime); + unOnboardedDevice.DeletedAt.Should().BeAfter(utcNow); unOnboardedDevice.DeletedByDevice.Should().Be(onboardedDevice.Id); A.CallTo(() => mockIdentitiesRepository.Update( diff --git a/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Commands/ApproveDeletionProcess/HandlerTests.cs b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Commands/ApproveDeletionProcess/HandlerTests.cs new file mode 100644 index 0000000000..534187ddb6 --- /dev/null +++ b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Commands/ApproveDeletionProcess/HandlerTests.cs @@ -0,0 +1,141 @@ +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.Modules.Devices.Application.Identities.Commands.ApproveDeletionProcess; +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.Identities; +using Backbone.Tooling; +using Backbone.UnitTestTools.Extensions; +using FakeItEasy; +using FluentAssertions; +using Xunit; +using static Backbone.UnitTestTools.Data.TestDataGenerator; + +namespace Backbone.Modules.Devices.Application.Tests.Tests.Identities.Commands.ApproveDeletionProcess; + +public class HandlerTests +{ + [Fact] + public async void Happy_path() + { + // Arrange + var utcNow = DateTime.Parse("2000-01-01"); + SystemTime.Set(utcNow); + + var identity = TestDataGenerator.CreateIdentityWithDeletionProcessWaitingForApproval(DateTime.Parse("2000-01-10")); + var deletionProcess = identity.GetDeletionProcessInStatus(DeletionProcessStatus.WaitingForApproval)!; + var device = identity.Devices[0]; + + var fakeUserContext = A.Fake(); + A.CallTo(() => fakeUserContext.GetAddress()).Returns(identity.Address); + A.CallTo(() => fakeUserContext.GetDeviceId()).Returns(device.Id); + + var mockIdentitiesRepository = A.Fake(); + A.CallTo(() => mockIdentitiesRepository.FindByAddress(identity.Address, A._, A._)) + .Returns(identity); + + var handler = CreateHandler(mockIdentitiesRepository, fakeUserContext); + + // Act + var response = await handler.Handle(new ApproveDeletionProcessCommand(deletionProcess.Id), CancellationToken.None); + + // Assert + A.CallTo(() => mockIdentitiesRepository.Update(A.That.Matches(i => + i.Address == identity.Address + && i.Status == IdentityStatus.ToBeDeleted + && i.TierId == Tier.QUEUED_FOR_DELETION.Id + && i.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.Approved)!.ApprovedAt == DateTime.Parse("2000-01-01") + && i.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.Approved)!.GracePeriodEndsAt == DateTime.Parse("2000-01-31") + && i.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.Approved)!.ApprovedByDevice == device.Id + ), A._)) + .MustHaveHappenedOnceExactly(); + + response.Id.Should().Be(deletionProcess.Id); + response.ApprovedAt.Should().Be(utcNow); + response.ApprovedByDevice.Should().Be(device.Id); + response.Status.Should().Be(DeletionProcessStatus.Approved); + } + + [Fact] + public void Throws_when_given_identity_does_not_exist() + { + // Arrange + var address = CreateRandomIdentityAddress(); + var fakeIdentitiesRepository = A.Fake(); + var fakeUserContext = A.Fake(); + A.CallTo(() => fakeUserContext.GetAddress()).Returns(address); + + A.CallTo(() => fakeIdentitiesRepository.FindByAddress(address, A._, A._)).Returns(null); + + var handler = CreateHandler(fakeIdentitiesRepository, fakeUserContext); + + // Act + var acting = async () => await handler.Handle(new ApproveDeletionProcessCommand("some-deletion-process-id"), CancellationToken.None); + + // Assert + acting.Should().AwaitThrowAsync().Which.Message.Should().Contain("Identity"); + } + + [Fact] + public void Throws_when_deletion_process_does_not_exist() + { + // Arrange + var utcNow = DateTime.Parse("2000-01-01"); + SystemTime.Set(utcNow); + + var identity = TestDataGenerator.CreateIdentityWithDeletionProcessWaitingForApproval(DateTime.Parse("2000-01-10")); + var identityDevice = identity.Devices[0]; + + var fakeUserContext = A.Fake(); + A.CallTo(() => fakeUserContext.GetAddress()).Returns(identity.Address); + A.CallTo(() => fakeUserContext.GetDeviceId()).Returns(identityDevice.Id); + + var mockIdentitiesRepository = A.Fake(); + A.CallTo(() => mockIdentitiesRepository.FindByAddress(identity.Address, A._, A._)) + .Returns(identity); + + var handler = CreateHandler(mockIdentitiesRepository, fakeUserContext); + + // Act + var acting = async () => await handler.Handle(new ApproveDeletionProcessCommand("IDP00000000000000001"), CancellationToken.None); + + // Assert + acting.Should().AwaitThrowAsync().Which.Code.Should().Be("error.platform.recordNotFound"); + } + + [Fact] + public void Throws_when_deletion_process_is_not_waiting_for_approval() + { + // Arrange + var utcNow = DateTime.Parse("2000-01-01"); + SystemTime.Set(utcNow); + + var identity = TestDataGenerator.CreateIdentityWithApprovedDeletionProcess(DateTime.Parse("2000-01-10")); + var identityDevice = identity.Devices[0]; + + var fakeUserContext = A.Fake(); + A.CallTo(() => fakeUserContext.GetAddress()).Returns(identity.Address); + A.CallTo(() => fakeUserContext.GetDeviceId()).Returns(identityDevice.Id); + + var mockIdentitiesRepository = A.Fake(); + A.CallTo(() => mockIdentitiesRepository.FindByAddress(identity.Address, A._, A._)) + .Returns(identity); + + var handler = CreateHandler(mockIdentitiesRepository, fakeUserContext); + + // Act + var acting = async () => await handler.Handle(new ApproveDeletionProcessCommand(identity.DeletionProcesses.FirstOrDefault()!.Id), CancellationToken.None); + + // Assert + acting.Should().AwaitThrowAsync().Which.Code.Should().Be("error.platform.validation.device.noDeletionProcessWithRequiredStatusExists"); + } + + private static Handler CreateHandler(IIdentitiesRepository identitiesRepository, IUserContext userContext, IEventBus eventBus = null) + { + eventBus ??= A.Fake(); + return new Handler(identitiesRepository, userContext, eventBus); + } +} diff --git a/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Commands/SendDeletionProcessApprovalReminders/HandlerTests.cs b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Commands/SendDeletionProcessApprovalReminders/HandlerTests.cs new file mode 100644 index 0000000000..4f86c4d68d --- /dev/null +++ b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Commands/SendDeletionProcessApprovalReminders/HandlerTests.cs @@ -0,0 +1,197 @@ +using Backbone.BuildingBlocks.Application.PushNotifications; +using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Devices.Application.Identities.Commands.SendDeletionProcessApprovalReminders; +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 FakeItEasy; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Backbone.Modules.Devices.Application.Tests.Tests.Identities.Commands.SendDeletionProcessApprovalReminders; + +public class HandlerTests +{ + [Fact] + public async Task No_identities_with_a_deletion_process_waiting_for_approval_exists() + { + // Arrange + var mockIdentitiesRepository = A.Fake(); + var mockPushNotificationSender = A.Fake(); + + A.CallTo(() => mockIdentitiesRepository.FindAllWithDeletionProcessInStatus(A._, A._, A._)) + .Returns(new List()); + + var handler = CreateHandler(mockIdentitiesRepository, mockPushNotificationSender); + + // Act + await handler.Handle(new SendDeletionProcessApprovalRemindersCommand(), CancellationToken.None); + + // Assert + A.CallTo(() => mockIdentitiesRepository.Update(A._, A._)) + .MustNotHaveHappened(); + A.CallTo(() => mockPushNotificationSender.SendNotification(A._, A._, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Sends_first_reminder() + { + // Arrange + var identity = TestDataGenerator.CreateIdentityWithDeletionProcessWaitingForApproval(deletionProcessStartedAt: DateTime.Parse("2000-01-01")); + + var utcNow = DateTime.Parse("2000-01-02"); + SystemTime.Set(utcNow); + + var mockIdentitiesRepository = A.Fake(); + var mockPushNotificationSender = A.Fake(); + + A.CallTo(() => mockIdentitiesRepository.FindAllWithDeletionProcessInStatus(DeletionProcessStatus.WaitingForApproval, A._, A._)) + .Returns(new List { identity }); + + var handler = CreateHandler(mockIdentitiesRepository, mockPushNotificationSender); + + // Act + await handler.Handle(new SendDeletionProcessApprovalRemindersCommand(), CancellationToken.None); + + // Assert + A.CallTo(() => mockPushNotificationSender.SendNotification(identity.Address, A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => mockIdentitiesRepository.Update(A.That.Matches(i => + i.Address == identity.Address + && i.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.WaitingForApproval)!.ApprovalReminder1SentAt == utcNow + ), A._)).MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task Sends_second_reminder() + { + // Arrange + var identity = TestDataGenerator.CreateIdentityWithDeletionProcessWaitingForApproval(deletionProcessStartedAt: DateTime.Parse("2000-01-01")); + identity.DeletionProcessApprovalReminder1Sent(); + + var utcNow = DateTime.Parse("2000-01-06"); + SystemTime.Set(utcNow); + + var mockIdentitiesRepository = A.Fake(); + var mockPushNotificationSender = A.Fake(); + + A.CallTo(() => mockIdentitiesRepository.FindAllWithDeletionProcessInStatus(DeletionProcessStatus.WaitingForApproval, A._, A._)) + .Returns(new List { identity }); + + var handler = CreateHandler(mockIdentitiesRepository, mockPushNotificationSender); + + // Act + await handler.Handle(new SendDeletionProcessApprovalRemindersCommand(), CancellationToken.None); + + // Assert + A.CallTo(() => mockPushNotificationSender.SendNotification(identity.Address, A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => mockIdentitiesRepository.Update(A.That.Matches(i => + i.Address == identity.Address + && i.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.WaitingForApproval)!.ApprovalReminder2SentAt == utcNow + ), A._)) + .MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task Sends_third_reminder() + { + // Arrange + var identity = TestDataGenerator.CreateIdentityWithDeletionProcessWaitingForApproval(deletionProcessStartedAt: DateTime.Parse("2000-01-01")); + identity.DeletionProcessApprovalReminder1Sent(); + identity.DeletionProcessApprovalReminder2Sent(); + + var utcNow = DateTime.Parse("2000-01-09"); + SystemTime.Set(utcNow); + + var mockIdentitiesRepository = A.Fake(); + var mockPushNotificationSender = A.Fake(); + + A.CallTo(() => mockIdentitiesRepository.FindAllWithDeletionProcessInStatus(DeletionProcessStatus.WaitingForApproval, A._, A._)) + .Returns(new List { identity }); + + var handler = CreateHandler(mockIdentitiesRepository, mockPushNotificationSender); + + // Act + await handler.Handle(new SendDeletionProcessApprovalRemindersCommand(), CancellationToken.None); + + // Assert + A.CallTo(() => mockPushNotificationSender.SendNotification(identity.Address, A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => mockIdentitiesRepository.Update(A.That.Matches(i => + i.Address == identity.Address + && i.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.WaitingForApproval)!.ApprovalReminder3SentAt == utcNow + ), A._)) + .MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task Does_not_send_reminder_1_when_2_has_to_be_sent_as_well() + { + // Arrange + var identity = TestDataGenerator.CreateIdentityWithDeletionProcessWaitingForApproval(deletionProcessStartedAt: DateTime.Parse("2000-01-01")); + + var utcNow = DateTime.Parse("2000-01-06"); + SystemTime.Set(utcNow); + + var mockIdentitiesRepository = A.Fake(); + var mockPushNotificationSender = A.Fake(); + + A.CallTo(() => mockIdentitiesRepository.FindAllWithDeletionProcessInStatus(DeletionProcessStatus.WaitingForApproval, A._, A._)) + .Returns(new List { identity }); + + var handler = CreateHandler(mockIdentitiesRepository, mockPushNotificationSender); + + // Act + await handler.Handle(new SendDeletionProcessApprovalRemindersCommand(), CancellationToken.None); + + // Assert + A.CallTo(() => mockPushNotificationSender.SendNotification(identity.Address, A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => mockIdentitiesRepository.Update(A.That.Matches(i => + i.Address == identity.Address + && i.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.WaitingForApproval)!.ApprovalReminder1SentAt == null + && i.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.WaitingForApproval)!.ApprovalReminder2SentAt == utcNow + ), A._)) + .MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task Does_not_send_reminder_1_and_2_when_3_has_to_be_sent_as_well() + { + // Arrange + var identity = TestDataGenerator.CreateIdentityWithDeletionProcessWaitingForApproval(deletionProcessStartedAt: DateTime.Parse("2000-01-01")); + + var utcNow = DateTime.Parse("2000-01-09"); + SystemTime.Set(utcNow); + + var mockIdentitiesRepository = A.Fake(); + var mockPushNotificationSender = A.Fake(); + + A.CallTo(() => mockIdentitiesRepository.FindAllWithDeletionProcessInStatus(DeletionProcessStatus.WaitingForApproval, A._, A._)) + .Returns(new List { identity }); + + var handler = CreateHandler(mockIdentitiesRepository, mockPushNotificationSender); + + // Act + await handler.Handle(new SendDeletionProcessApprovalRemindersCommand(), CancellationToken.None); + + // Assert + A.CallTo(() => mockPushNotificationSender.SendNotification(identity.Address, A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => mockIdentitiesRepository.Update(A.That.Matches(i => + i.Address == identity.Address + && i.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.WaitingForApproval)!.ApprovalReminder1SentAt == null + && i.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.WaitingForApproval)!.ApprovalReminder2SentAt == null + && i.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.WaitingForApproval)!.ApprovalReminder3SentAt == utcNow + ), A._)) + .MustHaveHappenedOnceExactly(); + } + + private static Handler CreateHandler(IIdentitiesRepository identitiesRepository, IPushNotificationSender pushNotificationSender) + { + return new Handler(identitiesRepository, pushNotificationSender, A.Fake>()); + } +} diff --git a/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Commands/SendDeletionProcessGracePeriodReminders/HandlerTests.cs b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Commands/SendDeletionProcessGracePeriodReminders/HandlerTests.cs new file mode 100644 index 0000000000..cc5e3a434a --- /dev/null +++ b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Commands/SendDeletionProcessGracePeriodReminders/HandlerTests.cs @@ -0,0 +1,196 @@ +using Backbone.BuildingBlocks.Application.PushNotifications; +using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Devices.Application.Identities.Commands.SendDeletionProcessGracePeriodReminders; +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 FakeItEasy; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Backbone.Modules.Devices.Application.Tests.Tests.Identities.Commands.SendDeletionProcessGracePeriodReminders; +public class HandlerTests +{ + [Fact] + public async Task No_identities_with_an_approved_deletion_process_exist() + { + // Arrange + var mockIdentitiesRepository = A.Fake(); + var mockPushNotificationSender = A.Fake(); + + A.CallTo(() => mockIdentitiesRepository.FindAllWithDeletionProcessInStatus(A._, A._, A._)) + .Returns(new List()); + + var handler = CreateHandler(mockIdentitiesRepository, mockPushNotificationSender); + + // Act + await handler.Handle(new SendDeletionProcessGracePeriodRemindersCommand(), CancellationToken.None); + + // Assert + A.CallTo(() => mockIdentitiesRepository.Update(A._, A._)) + .MustNotHaveHappened(); + A.CallTo(() => mockPushNotificationSender.SendNotification(A._, A._, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Sends_first_reminder() + { + // Arrange + var identity = TestDataGenerator.CreateIdentityWithApprovedDeletionProcess(approvalDate: DateTime.Parse("2000-01-01")); + + var utcNow = DateTime.Parse("2000-01-11"); + SystemTime.Set(utcNow); + + var mockIdentitiesRepository = A.Fake(); + var mockPushNotificationSender = A.Fake(); + + A.CallTo(() => mockIdentitiesRepository.FindAllWithDeletionProcessInStatus(DeletionProcessStatus.Approved, A._, A._)) + .Returns(new List { identity }); + + var handler = CreateHandler(mockIdentitiesRepository, mockPushNotificationSender); + + // Act + await handler.Handle(new SendDeletionProcessGracePeriodRemindersCommand(), CancellationToken.None); + + // Assert + A.CallTo(() => mockPushNotificationSender.SendNotification(identity.Address, A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => mockIdentitiesRepository.Update(A.That.Matches(i => + i.Address == identity.Address + && i.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.Approved)!.GracePeriodReminder1SentAt == utcNow + ), A._)).MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task Sends_second_reminder() + { + // Arrange + var identity = TestDataGenerator.CreateIdentityWithApprovedDeletionProcess(approvalDate: DateTime.Parse("2000-01-01")); + identity.DeletionGracePeriodReminder1Sent(); + + var utcNow = DateTime.Parse("2000-01-21"); + SystemTime.Set(utcNow); + + var mockIdentitiesRepository = A.Fake(); + var mockPushNotificationSender = A.Fake(); + + A.CallTo(() => mockIdentitiesRepository.FindAllWithDeletionProcessInStatus(DeletionProcessStatus.Approved, A._, A._)) + .Returns(new List { identity }); + + var handler = CreateHandler(mockIdentitiesRepository, mockPushNotificationSender); + + // Act + await handler.Handle(new SendDeletionProcessGracePeriodRemindersCommand(), CancellationToken.None); + + // Assert + A.CallTo(() => mockPushNotificationSender.SendNotification(identity.Address, A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => mockIdentitiesRepository.Update(A.That.Matches(i => + i.Address == identity.Address + && i.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.Approved)!.GracePeriodReminder2SentAt == utcNow + ), A._)) + .MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task Sends_third_reminder() + { + // Arrange + var identity = TestDataGenerator.CreateIdentityWithApprovedDeletionProcess(approvalDate: DateTime.Parse("2000-01-01")); + identity.DeletionGracePeriodReminder1Sent(); + identity.DeletionGracePeriodReminder2Sent(); + + var utcNow = DateTime.Parse("2000-01-26"); + SystemTime.Set(utcNow); + + var mockIdentitiesRepository = A.Fake(); + var mockPushNotificationSender = A.Fake(); + + A.CallTo(() => mockIdentitiesRepository.FindAllWithDeletionProcessInStatus(DeletionProcessStatus.Approved, A._, A._)) + .Returns(new List { identity }); + + var handler = CreateHandler(mockIdentitiesRepository, mockPushNotificationSender); + + // Act + await handler.Handle(new SendDeletionProcessGracePeriodRemindersCommand(), CancellationToken.None); + + // Assert + A.CallTo(() => mockPushNotificationSender.SendNotification(identity.Address, A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => mockIdentitiesRepository.Update(A.That.Matches(i => + i.Address == identity.Address + && i.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.Approved)!.GracePeriodReminder3SentAt == utcNow + ), A._)) + .MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task Does_not_send_reminder_1_when_2_has_to_be_sent_as_well() + { + // Arrange + var identity = TestDataGenerator.CreateIdentityWithApprovedDeletionProcess(approvalDate: DateTime.Parse("2000-01-01")); + + var utcNow = DateTime.Parse("2000-01-21"); + SystemTime.Set(utcNow); + + var mockIdentitiesRepository = A.Fake(); + var mockPushNotificationSender = A.Fake(); + + A.CallTo(() => mockIdentitiesRepository.FindAllWithDeletionProcessInStatus(DeletionProcessStatus.Approved, A._, A._)) + .Returns(new List { identity }); + + var handler = CreateHandler(mockIdentitiesRepository, mockPushNotificationSender); + + // Act + await handler.Handle(new SendDeletionProcessGracePeriodRemindersCommand(), CancellationToken.None); + + // Assert + A.CallTo(() => mockPushNotificationSender.SendNotification(identity.Address, A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => mockIdentitiesRepository.Update(A.That.Matches(i => + i.Address == identity.Address + && i.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.Approved)!.GracePeriodReminder1SentAt == null + && i.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.Approved)!.GracePeriodReminder2SentAt == utcNow + ), A._)) + .MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task Does_not_send_reminder_1_and_2_when_3_has_to_be_sent_as_well() + { + // Arrange + var identity = TestDataGenerator.CreateIdentityWithApprovedDeletionProcess(approvalDate: DateTime.Parse("2000-01-01")); + + var utcNow = DateTime.Parse("2000-01-26"); + SystemTime.Set(utcNow); + + var mockIdentitiesRepository = A.Fake(); + var mockPushNotificationSender = A.Fake(); + + A.CallTo(() => mockIdentitiesRepository.FindAllWithDeletionProcessInStatus(DeletionProcessStatus.Approved, A._, A._)) + .Returns(new List { identity }); + + var handler = CreateHandler(mockIdentitiesRepository, mockPushNotificationSender); + + // Act + await handler.Handle(new SendDeletionProcessGracePeriodRemindersCommand(), CancellationToken.None); + + // Assert + A.CallTo(() => mockPushNotificationSender.SendNotification(identity.Address, A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => mockIdentitiesRepository.Update(A.That.Matches(i => + i.Address == identity.Address + && i.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.Approved)!.GracePeriodReminder1SentAt == null + && i.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.Approved)!.GracePeriodReminder2SentAt == null + && i.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.Approved)!.GracePeriodReminder3SentAt == utcNow + ), A._)) + .MustHaveHappenedOnceExactly(); + } + + private static Handler CreateHandler(IIdentitiesRepository identitiesRepository, IPushNotificationSender pushNotificationSender) + { + return new Handler(identitiesRepository, pushNotificationSender, A.Fake>()); + } +} diff --git a/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Commands/StartDeletionProcessAsOwner/HandlerTests.cs b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Commands/StartDeletionProcessAsOwner/HandlerTests.cs new file mode 100644 index 0000000000..64a264bd3b --- /dev/null +++ b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Commands/StartDeletionProcessAsOwner/HandlerTests.cs @@ -0,0 +1,84 @@ +using Backbone.BuildingBlocks.Application.Abstractions.Exceptions; +using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus; +using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext; +using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Devices.Application.Extensions; +using Backbone.Modules.Devices.Application.Identities.Commands.StartDeletionProcessAsOwner; +using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository; +using Backbone.Modules.Devices.Domain.Entities.Identities; +using Backbone.UnitTestTools.Extensions; +using FakeItEasy; +using FluentAssertions; +using Xunit; +using static Backbone.UnitTestTools.Data.TestDataGenerator; + +namespace Backbone.Modules.Devices.Application.Tests.Tests.Identities.Commands.StartDeletionProcessAsOwner; + +public class HandlerTests +{ + [Fact] + public async Task Happy_path() + { + // Arrange + var activeIdentity = TestDataGenerator.CreateIdentityWithOneDevice(); + var activeDevice = activeIdentity.Devices[0]; + + var mockIdentitiesRepository = A.Fake(); + var fakeUserContext = A.Fake(); + + A.CallTo(() => mockIdentitiesRepository.FindByAddress(A._, A._, A._)) + .Returns(activeIdentity); + A.CallTo(() => fakeUserContext.GetAddressOrNull()).Returns(activeIdentity.Address); + A.CallTo(() => fakeUserContext.GetDeviceId()).Returns(activeDevice.Id); + + var handler = CreateHandler(mockIdentitiesRepository, fakeUserContext); + + // Act + var command = new StartDeletionProcessAsOwnerCommand(); + var response = await handler.Handle(command, CancellationToken.None); + + // Assert + response.Should().NotBeNull(); + response.ApprovedByDevice.Should().NotBeNull(); + + A.CallTo(() => mockIdentitiesRepository.Update( + A.That.Matches( + i => i.Address == activeIdentity.Address && + i.DeletionProcesses.Count == 1 && + i.DeletionProcesses[0].Id == response.Id && + i.DeletionProcesses[0].AuditLog.Count == 1), + A._)) + .MustHaveHappenedOnceExactly(); + } + + [Fact] + public void Cannot_start_when_given_identity_does_not_exist() + { + // Arrange + var address = CreateRandomIdentityAddress(); + + var fakeIdentitiesRepository = A.Fake(); + var fakeUserContext = A.Fake(); + + A.CallTo(() => fakeIdentitiesRepository.FindByAddress( + A._, + A._, + A._)) + .Returns(null); + A.CallTo(() => fakeUserContext.GetAddressOrNull()).Returns(address); + + var handler = CreateHandler(fakeIdentitiesRepository, fakeUserContext); + + // Act + var command = new StartDeletionProcessAsOwnerCommand(); + var acting = async () => await handler.Handle(command, CancellationToken.None); + + // Assert + acting.Should().AwaitThrowAsync().Which.Message.Should().Contain("Identity"); + } + + private static Handler CreateHandler(IIdentitiesRepository identitiesRepository, IUserContext userContext) + { + return new Handler(identitiesRepository, userContext, A.Dummy()); + } +} diff --git a/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Commands/StartDeletionProcessAsSupport/HandlerTests.cs b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Commands/StartDeletionProcessAsSupport/HandlerTests.cs new file mode 100644 index 0000000000..9f97322140 --- /dev/null +++ b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Commands/StartDeletionProcessAsSupport/HandlerTests.cs @@ -0,0 +1,100 @@ +using Backbone.BuildingBlocks.Application.Abstractions.Exceptions; +using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus; +using Backbone.Modules.Devices.Application.Identities.Commands.StartDeletionProcessAsSupport; +using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository; +using Backbone.Modules.Devices.Application.IntegrationEvents.Outgoing; +using Backbone.Modules.Devices.Domain.Entities.Identities; +using Backbone.UnitTestTools.Extensions; +using FakeItEasy; +using FluentAssertions; +using Xunit; +using static Backbone.UnitTestTools.Data.TestDataGenerator; + +namespace Backbone.Modules.Devices.Application.Tests.Tests.Identities.Commands.StartDeletionProcessAsSupport; + +public class HandlerTests +{ + [Fact] + public async Task Happy_path() + { + // Arrange + var activeIdentity = TestDataGenerator.CreateIdentityWithOneDevice(); + + var mockIdentitiesRepository = A.Fake(); + + A.CallTo(() => mockIdentitiesRepository.FindByAddress(activeIdentity.Address, A._, A._)) + .Returns(activeIdentity); + + var handler = CreateHandler(mockIdentitiesRepository); + + // Act + var response = await handler.Handle(new StartDeletionProcessAsSupportCommand(activeIdentity.Address), CancellationToken.None); + + // Assert + response.Should().NotBeNull(); + response.Status.Should().Be(DeletionProcessStatus.WaitingForApproval); + + A.CallTo(() => mockIdentitiesRepository.Update( + A.That.Matches( + i => i.Address == activeIdentity.Address && + i.DeletionProcesses.Count == 1 && + i.DeletionProcesses[0].Id == response.Id && + i.DeletionProcesses[0].AuditLog.Count == 1), + A._)) + .MustHaveHappenedOnceExactly(); + } + + [Fact] + public async void Publishes_IdentityDeletionProcessStartedEvent() + { + // Arrange + var activeIdentity = TestDataGenerator.CreateIdentityWithOneDevice(); + + var fakeIdentitiesRepository = A.Fake(); + var mockEventBus = A.Fake(); + + A.CallTo(() => fakeIdentitiesRepository.FindByAddress(activeIdentity.Address, A._, A._)) + .Returns(activeIdentity); + + var handler = CreateHandler(fakeIdentitiesRepository, mockEventBus); + + // Act + var response = await handler.Handle(new StartDeletionProcessAsSupportCommand(activeIdentity.Address), CancellationToken.None); + + // Assert + A.CallTo(() => mockEventBus.Publish( + A.That.Matches( + e => e.Address == activeIdentity.Address && + e.DeletionProcessId == response.Id)) + ).MustHaveHappenedOnceExactly(); + } + + [Fact] + public void Cannot_start_when_given_identity_does_not_exist() + { + // Arrange + var address = CreateRandomIdentityAddress(); + + var fakeIdentitiesRepository = A.Fake(); + + A.CallTo(() => fakeIdentitiesRepository.FindByAddress( + address, + A._, + A._)) + .Returns(null); + + var handler = CreateHandler(fakeIdentitiesRepository); + + // Act + var acting = async () => await handler.Handle(new StartDeletionProcessAsSupportCommand(address), CancellationToken.None); + + // Assert + acting.Should().AwaitThrowAsync().Which.Message.Should().Contain("Identity"); + } + + private static Handler CreateHandler(IIdentitiesRepository identitiesRepository, IEventBus eventBus = null) + { + eventBus ??= A.Fake(); + return new Handler(identitiesRepository, eventBus); + } +} diff --git a/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Commands/UpdateIdentity/HandlerTests.cs b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Commands/UpdateIdentity/HandlerTests.cs index 4ad8a79f11..6eb51967c3 100644 --- a/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Commands/UpdateIdentity/HandlerTests.cs +++ b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Commands/UpdateIdentity/HandlerTests.cs @@ -6,7 +6,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 Backbone.UnitTestTools.Extensions; using FakeItEasy; using FluentAssertions; diff --git a/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/IntegrationEvents/IdentityDeletionProcessStartedIntegrationEventHandlerTests.cs b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/IntegrationEvents/IdentityDeletionProcessStartedIntegrationEventHandlerTests.cs new file mode 100644 index 0000000000..3fdaa06ee4 --- /dev/null +++ b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/IntegrationEvents/IdentityDeletionProcessStartedIntegrationEventHandlerTests.cs @@ -0,0 +1,30 @@ +using Backbone.BuildingBlocks.Application.PushNotifications; +using Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.DeletionProcess; +using Backbone.Modules.Devices.Application.IntegrationEvents.Incoming.IdentityDeletionProcessStarted; +using Backbone.Modules.Devices.Application.IntegrationEvents.Outgoing; +using Backbone.Modules.Devices.Domain.Entities.Identities; +using FakeItEasy; +using Xunit; + +namespace Backbone.Modules.Devices.Application.Tests.Tests.Identities.IntegrationEvents; + +public class IdentityDeletionProcessStartedIntegrationEventHandlerTests +{ + [Fact] + public async Task Sends_push_notification() + { + // Arrange + var mockPushNotificationSender = A.Fake(); + var handler = new IdentityDeletionProcessStartedIntegrationEventHandler(mockPushNotificationSender); + var identity = TestDataGenerator.CreateIdentity(); + var identityDeletionProcessStartedIntegrationEvent = new IdentityDeletionProcessStartedIntegrationEvent(identity.Address, IdentityDeletionProcess.StartAsSupport(identity.Address).Id); + + // Act + await handler.Handle(identityDeletionProcessStartedIntegrationEvent); + + // Assert + A.CallTo(() => mockPushNotificationSender.SendNotification(identity.Address, + A._, CancellationToken.None) + ).MustHaveHappenedOnceExactly(); + } +} diff --git a/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/GetIdentity/FindByAddressStubRepository.cs b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/GetIdentity/FindByAddressStubRepository.cs index 4aa6e7e57a..a0481a6490 100644 --- a/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/GetIdentity/FindByAddressStubRepository.cs +++ b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/GetIdentity/FindByAddressStubRepository.cs @@ -1,8 +1,8 @@ -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.Application.Infrastructure.Persistence.Repository; -using Backbone.Modules.Devices.Domain.Entities; +using Backbone.Modules.Devices.Domain.Entities.Identities; namespace Backbone.Modules.Devices.Application.Tests.Tests.Identities.Queries.GetIdentity; @@ -20,6 +20,11 @@ public Task Exists(IdentityAddress address, CancellationToken cancellation throw new NotImplementedException(); } + public Task> FindAllWithDeletionProcessInStatus(DeletionProcessStatus status, CancellationToken cancellationToken, bool track = false) + { + throw new NotImplementedException(); + } + public Task CountByClientId(string clientId, CancellationToken cancellationToken) { throw new NotImplementedException(); @@ -35,6 +40,11 @@ public Task> FindAll(PaginationFilter paginationFil throw new NotImplementedException(); } + public Task> FindAllWithDeletionProcessWaitingForApproval(CancellationToken cancellationToken, bool track = false) + { + throw new NotImplementedException(); + } + public Task> FindAllDevicesOfIdentity(IdentityAddress identity, IEnumerable ids, PaginationFilter paginationFilter, CancellationToken cancellationToken) { throw new NotImplementedException(); diff --git a/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/GetIdentity/HandlerTests.cs b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/GetIdentity/HandlerTests.cs index 628a4dea82..59ec2b1078 100644 --- a/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/GetIdentity/HandlerTests.cs +++ b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/GetIdentity/HandlerTests.cs @@ -2,7 +2,7 @@ using Backbone.DevelopmentKit.Identity.ValueObjects; using Backbone.Modules.Devices.Application.Identities.Queries.GetIdentity; using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository; -using Backbone.Modules.Devices.Domain.Entities; +using Backbone.Modules.Devices.Domain.Entities.Identities; using Backbone.UnitTestTools.Extensions; using FakeItEasy; using FluentAssertions; diff --git a/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/ListIdentities/FindAllStubRepository.cs b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/ListIdentities/FindAllStubRepository.cs index dd686d2a57..d7bacd07aa 100644 --- a/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/ListIdentities/FindAllStubRepository.cs +++ b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/ListIdentities/FindAllStubRepository.cs @@ -1,8 +1,8 @@ -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.Application.Infrastructure.Persistence.Repository; -using Backbone.Modules.Devices.Domain.Entities; +using Backbone.Modules.Devices.Domain.Entities.Identities; namespace Backbone.Modules.Devices.Application.Tests.Tests.Identities.Queries.ListIdentities; @@ -20,6 +20,11 @@ public Task Exists(IdentityAddress address, CancellationToken cancellation throw new NotImplementedException(); } + public Task> FindAllWithDeletionProcessInStatus(DeletionProcessStatus status, CancellationToken cancellationToken, bool track = false) + { + throw new NotImplementedException(); + } + public Task CountByClientId(string clientId, CancellationToken cancellationToken) { throw new NotImplementedException(); @@ -35,6 +40,11 @@ public Task> FindAll(PaginationFilter paginationFil return Task.FromResult(_identities); } + public Task> FindAllWithDeletionProcessWaitingForApproval(CancellationToken cancellationToken, bool track = false) + { + throw new NotImplementedException(); + } + public Task> FindAllDevicesOfIdentity(IdentityAddress identity, IEnumerable ids, PaginationFilter paginationFilter, CancellationToken cancellationToken) { throw new NotImplementedException(); diff --git a/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/ListIdentities/HandlerTests.cs b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/ListIdentities/HandlerTests.cs index f826f3f495..7eab6f59c8 100644 --- a/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/ListIdentities/HandlerTests.cs +++ b/Modules/Devices/test/Devices.Application.Tests/Tests/Identities/Queries/ListIdentities/HandlerTests.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.Modules.Devices.Application.Identities.Queries.ListIdentities; -using Backbone.Modules.Devices.Domain.Entities; +using Backbone.Modules.Devices.Domain.Entities.Identities; using FluentAssertions; using FluentAssertions.Execution; using Xunit; diff --git a/Modules/Devices/test/Devices.Domain.Tests/Devices.Domain.Tests.csproj b/Modules/Devices/test/Devices.Domain.Tests/Devices.Domain.Tests.csproj index 094b4b4782..f69891ec48 100644 --- a/Modules/Devices/test/Devices.Domain.Tests/Devices.Domain.Tests.csproj +++ b/Modules/Devices/test/Devices.Domain.Tests/Devices.Domain.Tests.csproj @@ -6,6 +6,7 @@ + @@ -23,5 +24,4 @@ - diff --git a/Modules/Devices/test/Devices.Domain.Tests/Domain/ApplicationUserTests.cs b/Modules/Devices/test/Devices.Domain.Tests/Domain/ApplicationUserTests.cs index 7255dcb63f..74b1e94d76 100644 --- a/Modules/Devices/test/Devices.Domain.Tests/Domain/ApplicationUserTests.cs +++ b/Modules/Devices/test/Devices.Domain.Tests/Domain/ApplicationUserTests.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Backbone.Modules.Devices.Domain.Entities; +using Backbone.Modules.Devices.Domain.Entities.Identities; using FluentAssertions; using Xunit; diff --git a/Modules/Devices/test/Devices.Domain.Tests/Domain/DeviceTests.cs b/Modules/Devices/test/Devices.Domain.Tests/Domain/DeviceTests.cs index fbe85016dc..2964b2a2cc 100644 --- a/Modules/Devices/test/Devices.Domain.Tests/Domain/DeviceTests.cs +++ b/Modules/Devices/test/Devices.Domain.Tests/Domain/DeviceTests.cs @@ -1,5 +1,6 @@ using Backbone.BuildingBlocks.Domain; using Backbone.Modules.Devices.Domain.Entities; +using Backbone.Modules.Devices.Domain.Entities.Identities; using FluentAssertions; using Xunit; diff --git a/Modules/Devices/test/Devices.Domain.Tests/Identities/ApproveDeletionProcessTests.cs b/Modules/Devices/test/Devices.Domain.Tests/Identities/ApproveDeletionProcessTests.cs new file mode 100644 index 0000000000..a34b8cff91 --- /dev/null +++ b/Modules/Devices/test/Devices.Domain.Tests/Identities/ApproveDeletionProcessTests.cs @@ -0,0 +1,86 @@ +using Backbone.BuildingBlocks.Domain; +using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Devices.Domain.Aggregates.Tier; +using Backbone.Modules.Devices.Domain.Entities.Identities; +using Backbone.Modules.Devices.Domain.Tests.Identities.TestDoubles; +using Backbone.Tooling; +using FluentAssertions; +using Xunit; + +namespace Backbone.Modules.Devices.Domain.Tests.Identities; + +public class ApproveDeletionProcessTests +{ + [Fact] + public void Approve_deletion_process_waiting_for_approval() + { + // Arrange + var currentDateTime = DateTime.Parse("2000-01-01"); + SystemTime.Set(currentDateTime); + var identity = CreateIdentityWithDeletionProcessWaitingForApproval(); + + // Act + identity.ApproveDeletionProcess(identity.GetDeletionProcessInStatus(DeletionProcessStatus.WaitingForApproval)!.Id, DeviceId.Parse("DVC")); + + // Assert + identity.Status.Should().Be(IdentityStatus.ToBeDeleted); + identity.DeletionGracePeriodEndsAt.Should().Be(DateTime.Parse("2000-01-31")); + identity.TierId.Should().Be(Tier.QUEUED_FOR_DELETION.Id); + var deletionProcess = identity.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.Approved)!; + AssertAuditLogEntryWasCreated(deletionProcess); + } + + [Fact] + public void Throws_when_deletion_process_does_not_exist() + { + // Arrange + var identity = CreateIdentity(); + var identityDeletionProcessId = IdentityDeletionProcessId.Create("IDP00000000000000001").Value; + + // Act + var acting = () => identity.ApproveDeletionProcess(identityDeletionProcessId, DeviceId.Parse("DVC")); + + // Assert + acting.Should().Throw().Which.Code.Should().Be("error.platform.recordNotFound"); + } + + [Fact] + public void Throws_when_deletion_process_is_not_in_status_waiting_for_approval() + { + // Arrange + var identity = CreateIdentity(); + var deletionProcess = identity.StartDeletionProcessAsOwner(DeviceId.Parse("DVC")); + + // Act + var acting = () => identity.ApproveDeletionProcess(deletionProcess.Id, DeviceId.Parse("DVC")); + + // Assert + acting.Should().Throw().Which.Code.Should().Be("error.platform.validation.device.noDeletionProcessWithRequiredStatusExists"); + } + + private static void AssertAuditLogEntryWasCreated(IdentityDeletionProcess deletionProcess) + { + deletionProcess.AuditLog.Should().HaveCount(2); + + var auditLogEntry = deletionProcess.AuditLog[1]; + auditLogEntry.ProcessId.Should().Be(deletionProcess.Id); + auditLogEntry.CreatedAt.Should().Be(SystemTime.UtcNow); + auditLogEntry.IdentityAddressHash.Should().BeEquivalentTo(new byte[] { 1, 2, 3 }); + auditLogEntry.OldStatus.Should().Be(DeletionProcessStatus.WaitingForApproval); + auditLogEntry.NewStatus.Should().Be(DeletionProcessStatus.Approved); + } + + private static Identity CreateIdentity() + { + var address = IdentityAddress.Create(Array.Empty(), "id1"); + return new Identity("", address, Array.Empty(), TierId.Generate(), 1); + } + + private static Identity CreateIdentityWithDeletionProcessWaitingForApproval() + { + var identity = CreateIdentity(); + Hasher.SetHasher(new DummyHasher(new byte[] { 1, 2, 3 })); + identity.StartDeletionProcessAsSupport(); + return identity; + } +} diff --git a/Modules/Devices/test/Devices.Domain.Tests/Identities/DeletionGracePeriodReminderTests.cs b/Modules/Devices/test/Devices.Domain.Tests/Identities/DeletionGracePeriodReminderTests.cs new file mode 100644 index 0000000000..b9e75d121d --- /dev/null +++ b/Modules/Devices/test/Devices.Domain.Tests/Identities/DeletionGracePeriodReminderTests.cs @@ -0,0 +1,140 @@ +using Backbone.BuildingBlocks.Domain; +using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Devices.Domain.Aggregates.Tier; +using Backbone.Modules.Devices.Domain.Entities.Identities; +using Backbone.Modules.Devices.Domain.Tests.Identities.TestDoubles; +using Backbone.Tooling; +using FluentAssertions; +using Xunit; + +namespace Backbone.Modules.Devices.Domain.Tests.Identities; + +public class DeletionGracePeriodReminderTests : IDisposable +{ + [Fact] + public void DeletionGracePeriodReminder1Sent_updates_GracePeriodReminder1SentAt() + { + // Arrange + var currentDateTime = DateTime.Parse("2000-01-01"); + SystemTime.Set(currentDateTime); + var identity = CreateIdentityWithApprovedDeletionProcess(); + + // Act + identity.DeletionGracePeriodReminder1Sent(); + + // Assert + var deletionProcess = identity.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.Approved)!; + AssertAuditLogEntryWasCreated(deletionProcess); + deletionProcess.GracePeriodReminder1SentAt.Should().Be(currentDateTime); + } + + [Fact] + public void DeletionGracePeriodReminder1Sent_fails_when_no_approved_deletion_process_exists() + { + // Arrange + SystemTime.Set(DateTime.Parse("2000-01-01")); + var identity = CreateIdentity(); + + // Act + var acting = identity.DeletionGracePeriodReminder1Sent; + + // Assert + acting.Should().Throw().Which.Code.Should().Be("error.platform.validation.device.noDeletionProcessWithRequiredStatusExists"); + } + + [Fact] + public void DeletionGracePeriodReminder2Sent_updates_GracePeriodReminder2SentAt() + { + // Arrange + var currentDateTime = DateTime.Parse("2000-01-01"); + SystemTime.Set(currentDateTime); + var identity = CreateIdentityWithApprovedDeletionProcess(); + + // Act + identity.DeletionGracePeriodReminder2Sent(); + + // Assert + var deletionProcess = identity.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.Approved)!; + AssertAuditLogEntryWasCreated(deletionProcess); + deletionProcess.GracePeriodReminder2SentAt.Should().Be(currentDateTime); + } + + + [Fact] + public void DeletionGracePeriodReminder2Sent_fails_when_no_approved_deletion_process_exists() + { + // Arrange + SystemTime.Set(DateTime.Parse("2000-01-01")); + var identity = CreateIdentity(); + + // Act + var acting = identity.DeletionGracePeriodReminder2Sent; + + // Assert + acting.Should().Throw().Which.Code.Should().Be("error.platform.validation.device.noDeletionProcessWithRequiredStatusExists"); + } + + [Fact] + public void DeletionGracePeriodReminder3Sent_updates_GracePeriodReminder3SentAt() + { + // Arrange + var currentDateTime = DateTime.Parse("2000-01-01"); + SystemTime.Set(currentDateTime); + var identity = CreateIdentityWithApprovedDeletionProcess(); + + // Act + identity.DeletionGracePeriodReminder3Sent(); + + // Assert + var deletionProcess = identity.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.Approved)!; + AssertAuditLogEntryWasCreated(deletionProcess); + deletionProcess.GracePeriodReminder3SentAt.Should().Be(currentDateTime); + } + + + [Fact] + public void DeletionGracePeriodReminder3Sent_fails_when_no_approved_deletion_process_exists() + { + // Arrange + SystemTime.Set(DateTime.Parse("2000-01-01")); + var identity = CreateIdentity(); + + // Act + var acting = identity.DeletionGracePeriodReminder3Sent; + + // Assert + acting.Should().Throw().Which.Code.Should().Be("error.platform.validation.device.noDeletionProcessWithRequiredStatusExists"); + } + + private static void AssertAuditLogEntryWasCreated(IdentityDeletionProcess deletionProcess) + { + deletionProcess.AuditLog.Should().HaveCount(2); + + var auditLogEntry = deletionProcess.AuditLog[1]; + auditLogEntry.ProcessId.Should().Be(deletionProcess.Id); + auditLogEntry.CreatedAt.Should().Be(SystemTime.UtcNow); + auditLogEntry.IdentityAddressHash.Should().BeEquivalentTo(new byte[] { 1, 2, 3 }); + auditLogEntry.OldStatus.Should().Be(DeletionProcessStatus.Approved); + auditLogEntry.NewStatus.Should().Be(DeletionProcessStatus.Approved); + } + + private static Identity CreateIdentityWithApprovedDeletionProcess() + { + var identity = CreateIdentity(); + Hasher.SetHasher(new DummyHasher(new byte[] { 1, 2, 3 })); + identity.StartDeletionProcessAsOwner(new Device(identity).Id); + + return identity; + } + + private static Identity CreateIdentity() + { + var address = IdentityAddress.Create(Array.Empty(), "id1"); + return new Identity("", address, Array.Empty(), TierId.Generate(), 1); + } + + public void Dispose() + { + Hasher.Reset(); + } +} diff --git a/Modules/Devices/test/Devices.Domain.Tests/Identities/DeletionProcessApprovalReminderTests.cs b/Modules/Devices/test/Devices.Domain.Tests/Identities/DeletionProcessApprovalReminderTests.cs new file mode 100644 index 0000000000..f2f0ecc79e --- /dev/null +++ b/Modules/Devices/test/Devices.Domain.Tests/Identities/DeletionProcessApprovalReminderTests.cs @@ -0,0 +1,158 @@ +using Backbone.BuildingBlocks.Domain; +using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Devices.Domain.Aggregates.Tier; +using Backbone.Modules.Devices.Domain.Entities.Identities; +using Backbone.Modules.Devices.Domain.Tests.Identities.TestDoubles; +using Backbone.Tooling; +using FluentAssertions; +using Xunit; +using static Backbone.UnitTestTools.Data.TestDataGenerator; + +namespace Backbone.Modules.Devices.Domain.Tests.Identities; + +public class DeletionProcessApprovalReminderTests +{ + [Fact] + public void DeletionProcessApprovalReminder1Sent_updates_ApprovalReminder1SentAt() + { + // Arrange + var currentDateTime = DateTime.Parse("2000-01-01"); + SystemTime.Set(currentDateTime); + var identity = CreateIdentityWithDeletionProcessWaitingForApproval(); + + // Act + identity.DeletionProcessApprovalReminder1Sent(); + + // Assert + var deletionProcess = identity.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.WaitingForApproval)!; + AssertAuditLogEntryWasCreated(deletionProcess); + deletionProcess.ApprovalReminder1SentAt.Should().Be(currentDateTime); + } + + [Fact] + public void DeletionProcessApprovalReminder1Sent_fails_when_no_deletion_process_waiting_for_approval_exists() + { + // Arrange + SystemTime.Set(DateTime.Parse("2000-01-01")); + var identity = CreateIdentity(); + + // Act + var acting = identity.DeletionProcessApprovalReminder1Sent; + + // Assert + acting.Should().Throw().Which.Code.Should().Be("error.platform.validation.device.noDeletionProcessWithRequiredStatusExists"); + } + + [Fact] + public void DeletionProcessApprovalReminder2Sent_updates_ApprovalReminder2SentAt() + { + // Arrange + var currentDateTime = DateTime.Parse("2000-01-01"); + SystemTime.Set(currentDateTime); + var identity = CreateIdentityWithDeletionProcessWaitingForApproval(); + + // Act + identity.DeletionProcessApprovalReminder2Sent(); + + // Assert + var deletionProcess = identity.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.WaitingForApproval)!; + AssertAuditLogEntryWasCreated(deletionProcess); + deletionProcess.ApprovalReminder2SentAt.Should().Be(currentDateTime); + } + + + [Fact] + public void DeletionProcessApprovalReminder2Sent_fails_when_no_deletion_process_waiting_for_approval_exists() + { + // Arrange + SystemTime.Set(DateTime.Parse("2000-01-01")); + var identity = CreateIdentity(); + + // Act + var acting = identity.DeletionProcessApprovalReminder2Sent; + + // Assert + acting.Should().Throw().Which.Code.Should().Be("error.platform.validation.device.noDeletionProcessWithRequiredStatusExists"); + } + + [Fact] + public void DeletionProcessApprovalReminder3Sent_updates_ApprovalReminder3SentAt() + { + // Arrange + var currentDateTime = DateTime.Parse("2000-01-01"); + SystemTime.Set(currentDateTime); + var identity = CreateIdentityWithDeletionProcessWaitingForApproval(); + + // Act + identity.DeletionProcessApprovalReminder3Sent(); + + // Assert + var deletionProcess = identity.DeletionProcesses.FirstOrDefault(d => d.Status == DeletionProcessStatus.WaitingForApproval)!; + AssertAuditLogEntryWasCreated(deletionProcess); + deletionProcess.ApprovalReminder3SentAt.Should().Be(currentDateTime); + } + + + [Fact] + public void DeletionProcessApprovalReminder3Sent_fails_when_no_deletion_process_waiting_for_approval_exists() + { + // Arrange + SystemTime.Set(DateTime.Parse("2000-01-01")); + var identity = CreateIdentity(); + + // Act + var acting = identity.DeletionProcessApprovalReminder3Sent; + + // Assert + acting.Should().Throw().Which.Code.Should().Be("error.platform.validation.device.noDeletionProcessWithRequiredStatusExists"); + } + + [Fact] + public void GetEndOfApprovalPeriod_returns_expected_date() + { + // Arrange + SystemTime.Set(DateTime.Parse("2000-01-01")); + IdentityDeletionConfiguration.MaxApprovalTime = 10; + + var deletionProcess = IdentityDeletionProcess.StartAsOwner(CreateRandomIdentityAddress(), CreateRandomDeviceId()); + + // Act + var endOfApprovalPeriod = deletionProcess.GetEndOfApprovalPeriod(); + + // Assert + endOfApprovalPeriod.Should().Be(DateTime.Parse("2000-01-11")); + } + + private static void AssertAuditLogEntryWasCreated(IdentityDeletionProcess deletionProcess) + { + deletionProcess.AuditLog.Should().HaveCount(2); + + var auditLogEntry = deletionProcess.AuditLog[1]; + auditLogEntry.ProcessId.Should().Be(deletionProcess.Id); + auditLogEntry.CreatedAt.Should().Be(SystemTime.UtcNow); + auditLogEntry.IdentityAddressHash.Should().BeEquivalentTo(new byte[] { 1, 2, 3 }); + auditLogEntry.OldStatus.Should().Be(DeletionProcessStatus.WaitingForApproval); + auditLogEntry.NewStatus.Should().Be(DeletionProcessStatus.WaitingForApproval); + } + + private static Identity CreateIdentityWithDeletionProcessWaitingForApproval() + { + var identity = CreateIdentity(); + Hasher.SetHasher(new DummyHasher(new byte[] { 1, 2, 3 })); + identity.StartDeletionProcessAsSupport(); + + return identity; + } + + private static Identity CreateIdentity() + { + var address = IdentityAddress.Create(Array.Empty(), "id1"); + return new Identity("", address, Array.Empty(), TierId.Generate(), 1); + } + + [Fact] + public void Dispose() + { + Hasher.Reset(); + } +} diff --git a/Modules/Devices/test/Devices.Domain.Tests/Identities/StartDeletionProcessAsOwnerTests.cs b/Modules/Devices/test/Devices.Domain.Tests/Identities/StartDeletionProcessAsOwnerTests.cs new file mode 100644 index 0000000000..5e5ad1d674 --- /dev/null +++ b/Modules/Devices/test/Devices.Domain.Tests/Identities/StartDeletionProcessAsOwnerTests.cs @@ -0,0 +1,94 @@ +using Backbone.BuildingBlocks.Domain; +using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Devices.Domain.Aggregates.Tier; +using Backbone.Modules.Devices.Domain.Entities.Identities; +using Backbone.Modules.Devices.Domain.Tests.Identities.TestDoubles; +using Backbone.Tooling; +using FluentAssertions; +using Xunit; + +namespace Backbone.Modules.Devices.Domain.Tests.Identities; + +public class StartDeletionProcessAsOwnerTests : IDisposable +{ + [Fact] + public void Start_deletion_process() + { + // Arrange + SystemTime.Set(DateTime.Parse("2000-01-01")); + var activeIdentity = CreateIdentity(); + var activeDevice = DeviceId.Parse("DVC"); + + Hasher.SetHasher(new DummyHasher(new byte[] { 1, 2, 3 })); + + // Act + var deletionProcess = activeIdentity.StartDeletionProcessAsOwner(activeDevice); + + // Assert + activeIdentity.DeletionGracePeriodEndsAt.Should().Be(DateTime.Parse("2000-01-31")); + activeIdentity.TierId!.Value.Should().Be(Tier.QUEUED_FOR_DELETION.Id.Value); + activeIdentity.Status.Should().Be(IdentityStatus.ToBeDeleted); + + AssertDeletionProcessWasStarted(activeIdentity); + deletionProcess.Status.Should().Be(DeletionProcessStatus.Approved); + deletionProcess.ApprovedAt.Should().Be(SystemTime.UtcNow); + deletionProcess.ApprovedByDevice.Should().Be(activeDevice); + deletionProcess.GracePeriodEndsAt.Should().Be(DateTime.Parse("2000-01-31")); + + AssertAuditLogEntryWasCreated(deletionProcess); + var auditLogEntry = deletionProcess.AuditLog[0]; + auditLogEntry.Message.Should().Be("The deletion process was started by the owner. It was automatically approved."); + auditLogEntry.DeviceIdHash.Should().BeEquivalentTo(new byte[] { 1, 2, 3 }); + auditLogEntry.NewStatus.Should().Be(DeletionProcessStatus.Approved); + } + + [Fact] + public void Only_one_active_deletion_process_is_allowed_when_started() + { + // Arrange + var activeIdentity = CreateIdentity(); + var activeDevice = DeviceId.Parse("DVC"); + + activeIdentity.StartDeletionProcessAsOwner(activeDevice); + + // Act + var acting = () => activeIdentity.StartDeletionProcessAsOwner(activeDevice); + + // Assert + acting.Should().Throw().Which.Code.Should().Be("error.platform.validation.device.onlyOneActiveDeletionProcessAllowed"); + } + + private static void AssertDeletionProcessWasStarted(Identity activeIdentity) + { + activeIdentity.DeletionProcesses.Should().HaveCount(1); + var deletionProcess = activeIdentity.DeletionProcesses[0]; + deletionProcess.Should().NotBeNull(); + + deletionProcess.Id.Should().NotBeNull(); + deletionProcess.Id.Value.Should().HaveLength(20); + + deletionProcess.CreatedAt.Should().Be(SystemTime.UtcNow); + + deletionProcess.AuditLog.Should().HaveCount(1); + } + + private static void AssertAuditLogEntryWasCreated(IdentityDeletionProcess deletionProcess) + { + var auditLogEntry = deletionProcess.AuditLog[0]; + auditLogEntry.ProcessId.Should().Be(deletionProcess.Id); + auditLogEntry.CreatedAt.Should().Be(SystemTime.UtcNow); + auditLogEntry.IdentityAddressHash.Should().BeEquivalentTo(new byte[] { 1, 2, 3 }); + auditLogEntry.OldStatus.Should().BeNull(); + } + + private static Identity CreateIdentity() + { + var address = IdentityAddress.Create(Array.Empty(), "id1"); + return new Identity("", address, Array.Empty(), TierId.Generate(), 1); + } + + public void Dispose() + { + Hasher.Reset(); + } +} diff --git a/Modules/Devices/test/Devices.Domain.Tests/Identities/StartDeletionProcessAsSupportTests.cs b/Modules/Devices/test/Devices.Domain.Tests/Identities/StartDeletionProcessAsSupportTests.cs new file mode 100644 index 0000000000..373ebd526d --- /dev/null +++ b/Modules/Devices/test/Devices.Domain.Tests/Identities/StartDeletionProcessAsSupportTests.cs @@ -0,0 +1,80 @@ +using Backbone.BuildingBlocks.Domain; +using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Devices.Domain.Aggregates.Tier; +using Backbone.Modules.Devices.Domain.Entities.Identities; +using Backbone.Modules.Devices.Domain.Tests.Identities.TestDoubles; +using Backbone.Tooling; +using FluentAssertions; +using Xunit; + +namespace Backbone.Modules.Devices.Domain.Tests.Identities; + +public class StartDeletionProcessAsSupportTests +{ + [Fact] + public void Start_deletion_process() + { + // Arrange + SystemTime.Set(DateTime.Parse("2000-01-01")); + var activeIdentity = CreateIdentity(); + + Hasher.SetHasher(new DummyHasher(new byte[] { 1, 2, 3 })); + + // Act + var deletionProcess = activeIdentity.StartDeletionProcessAsSupport(); + + // Assert + AssertDeletionProcessWasStarted(activeIdentity); + deletionProcess.Status.Should().Be(DeletionProcessStatus.WaitingForApproval); + + AssertAuditLogEntryWasCreated(deletionProcess); + var auditLogEntry = deletionProcess.AuditLog[0]; + auditLogEntry.Message.Should().Be("The deletion process was started by support. It is now waiting for approval."); + auditLogEntry.DeviceIdHash.Should().BeNull(); + auditLogEntry.NewStatus.Should().Be(DeletionProcessStatus.WaitingForApproval); + } + + [Fact] + public void Only_one_active_deletion_process_is_allowed_when_started() + { + // Arrange + var activeIdentity = CreateIdentity(); + + activeIdentity.StartDeletionProcessAsSupport(); + + // Act + var acting = activeIdentity.StartDeletionProcessAsSupport; + + // Assert + acting.Should().Throw().Which.Code.Should().Be("error.platform.validation.device.onlyOneActiveDeletionProcessAllowed"); + } + + private static void AssertDeletionProcessWasStarted(Identity activeIdentity) + { + activeIdentity.DeletionProcesses.Should().HaveCount(1); + var deletionProcess = activeIdentity.DeletionProcesses[0]; + deletionProcess.Should().NotBeNull(); + + deletionProcess.Id.Should().NotBeNull(); + deletionProcess.Id.Value.Should().HaveLength(20); + + deletionProcess.CreatedAt.Should().Be(SystemTime.UtcNow); + + deletionProcess.AuditLog.Should().HaveCount(1); + } + + private static void AssertAuditLogEntryWasCreated(IdentityDeletionProcess deletionProcess) + { + var auditLogEntry = deletionProcess.AuditLog[0]; + auditLogEntry.ProcessId.Should().Be(deletionProcess.Id); + auditLogEntry.CreatedAt.Should().Be(SystemTime.UtcNow); + auditLogEntry.IdentityAddressHash.Should().BeEquivalentTo(new byte[] { 1, 2, 3 }); + auditLogEntry.OldStatus.Should().BeNull(); + } + + private static Identity CreateIdentity() + { + var address = IdentityAddress.Create(Array.Empty(), "id1"); + return new Identity("", address, Array.Empty(), TierId.Generate(), 1); + } +} diff --git a/Modules/Devices/test/Devices.Domain.Tests/Identities/TestDoubles/DummyHasher.cs b/Modules/Devices/test/Devices.Domain.Tests/Identities/TestDoubles/DummyHasher.cs new file mode 100644 index 0000000000..699bafa79c --- /dev/null +++ b/Modules/Devices/test/Devices.Domain.Tests/Identities/TestDoubles/DummyHasher.cs @@ -0,0 +1,23 @@ +using Backbone.Modules.Devices.Domain.Entities.Identities; + +namespace Backbone.Modules.Devices.Domain.Tests.Identities.TestDoubles; + +public class DummyHasher : IHasher +{ + private readonly byte[] _bytes; + + public DummyHasher() + { + _bytes = new byte[] { 1, 2, 3 }; + } + + public DummyHasher(byte[] bytes) + { + _bytes = bytes; + } + + public byte[] HashUtf8(string input) + { + return _bytes; + } +} diff --git a/Modules/Devices/test/Devices.Domain.Tests/TestDataGenerator.cs b/Modules/Devices/test/Devices.Domain.Tests/TestDataGenerator.cs index a6dc0e9489..922a2e5c67 100644 --- a/Modules/Devices/test/Devices.Domain.Tests/TestDataGenerator.cs +++ b/Modules/Devices/test/Devices.Domain.Tests/TestDataGenerator.cs @@ -1,5 +1,5 @@ using Backbone.Modules.Devices.Domain.Aggregates.Tier; -using Backbone.Modules.Devices.Domain.Entities; +using Backbone.Modules.Devices.Domain.Entities.Identities; using static Backbone.UnitTestTools.Data.TestDataGenerator; namespace Backbone.Modules.Devices.Domain.Tests; diff --git a/Modules/Devices/test/Devices.Domain.Tests/Tiers/TierTests.cs b/Modules/Devices/test/Devices.Domain.Tests/Tiers/TierTests.cs index 1ca71dc66b..75935ab636 100644 --- a/Modules/Devices/test/Devices.Domain.Tests/Tiers/TierTests.cs +++ b/Modules/Devices/test/Devices.Domain.Tests/Tiers/TierTests.cs @@ -27,7 +27,18 @@ public void Basic_Tier_cannot_be_deleted() // Assert error.Should().NotBeNull(); - error.Should().BeEquivalentTo(DomainErrors.CannotDeleteBasicTier()); + error!.Code.Should().Be("error.platform.validation.device.basicTierCannotBeDeleted"); + } + + [Fact] + public void Queued_for_deletion_tier_cannot_be_deleted() + { + // Act + var error = Tier.QUEUED_FOR_DELETION.CanBeDeleted(clientsCount: 0, identitiesCount: 0); + + // Assert + error.Should().NotBeNull(); + error!.Code.Should().Be("error.platform.validation.device.queuedForDeletionTierCannotBeDeleted"); } [Fact] @@ -40,7 +51,7 @@ public void Tier_with_related_identities_cannot_be_deleted() var error = tier.CanBeDeleted(clientsCount: 0, identitiesCount: 1); // Assert - error.Should().Be(DomainErrors.CannotDeleteUsedTier("")); + error!.Code.Should().Be("error.platform.validation.device.usedTierCannotBeDeleted"); error!.Message.Should().Contain("Tier is assigned to one or more Identities"); } @@ -54,7 +65,7 @@ public void Tier_with_related_clients_cannot_be_deleted() var error = tier.CanBeDeleted(clientsCount: 1, identitiesCount: 0); // Assert - error.Should().Be(DomainErrors.CannotDeleteUsedTier("")); + error!.Code.Should().Be("error.platform.validation.device.usedTierCannotBeDeleted"); error!.Message.Should().Contain("The Tier is used as the default Tier by one or more clients."); } diff --git a/Modules/Quotas/src/Quotas.Application/IntegrationEvents/Incoming/TierOfIdentityChanged/TierOfIdentityChangedIntegrationEvent.cs b/Modules/Quotas/src/Quotas.Application/IntegrationEvents/Incoming/TierOfIdentityChanged/TierOfIdentityChangedIntegrationEvent.cs index 1df3697522..2db6fb11c9 100644 --- a/Modules/Quotas/src/Quotas.Application/IntegrationEvents/Incoming/TierOfIdentityChanged/TierOfIdentityChangedIntegrationEvent.cs +++ b/Modules/Quotas/src/Quotas.Application/IntegrationEvents/Incoming/TierOfIdentityChanged/TierOfIdentityChangedIntegrationEvent.cs @@ -3,7 +3,7 @@ namespace Backbone.Modules.Quotas.Application.IntegrationEvents.Incoming.TierOfIdentityChanged; public class TierOfIdentityChangedIntegrationEvent : IntegrationEvent { - public required string OldTier { get; set; } - public required string NewTier { get; set; } + public required string OldTierId { get; set; } + public required string NewTierId { get; set; } public required string IdentityAddress { get; set; } } diff --git a/Modules/Quotas/src/Quotas.Application/IntegrationEvents/Incoming/TierOfIdentityChanged/TierOfIdentityChangedIntegrationEventHandler.cs b/Modules/Quotas/src/Quotas.Application/IntegrationEvents/Incoming/TierOfIdentityChanged/TierOfIdentityChangedIntegrationEventHandler.cs index 88543dca11..76e24876c4 100644 --- a/Modules/Quotas/src/Quotas.Application/IntegrationEvents/Incoming/TierOfIdentityChanged/TierOfIdentityChangedIntegrationEventHandler.cs +++ b/Modules/Quotas/src/Quotas.Application/IntegrationEvents/Incoming/TierOfIdentityChanged/TierOfIdentityChangedIntegrationEventHandler.cs @@ -22,7 +22,7 @@ public TierOfIdentityChangedIntegrationEventHandler(IIdentitiesRepository identi public async Task Handle(TierOfIdentityChangedIntegrationEvent @event) { var identity = await _identitiesRepository.Find(@event.IdentityAddress, CancellationToken.None, track: true) ?? throw new NotFoundException(nameof(Identity)); - var newTier = await _tiersRepository.Find(@event.NewTier, CancellationToken.None, track: true) ?? throw new NotFoundException(nameof(Tier)); + var newTier = await _tiersRepository.Find(@event.NewTierId, CancellationToken.None, track: true) ?? throw new NotFoundException(nameof(Tier)); await identity.ChangeTier(newTier, _metricCalculatorFactory, CancellationToken.None); diff --git a/Modules/Quotas/src/Quotas.Application/Tiers/Commands/SeedQueuedForDeletionTier/Handler.cs b/Modules/Quotas/src/Quotas.Application/Tiers/Commands/SeedQueuedForDeletionTier/Handler.cs new file mode 100644 index 0000000000..973fe5247b --- /dev/null +++ b/Modules/Quotas/src/Quotas.Application/Tiers/Commands/SeedQueuedForDeletionTier/Handler.cs @@ -0,0 +1,39 @@ +using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus; +using Backbone.Modules.Quotas.Application.Infrastructure.Persistence.Repository; +using Backbone.Modules.Quotas.Application.IntegrationEvents.Outgoing; +using Backbone.Modules.Quotas.Domain.Aggregates.Tiers; +using MediatR; + +namespace Backbone.Modules.Quotas.Application.Tiers.Commands.SeedQueuedForDeletionTier; + +public class Handler : IRequestHandler +{ + private readonly ITiersRepository _tiersRepository; + private readonly IMetricsRepository _metricsRepository; + private readonly IEventBus _eventBus; + + public Handler(ITiersRepository tiersRepository, IMetricsRepository metricsRepository, IEventBus eventBus) + { + _tiersRepository = tiersRepository; + _metricsRepository = metricsRepository; + _eventBus = eventBus; + } + + public async Task Handle(SeedQueuedForDeletionTierCommand request, CancellationToken cancellationToken) + { + var queuedForDeletionTier = await _tiersRepository.Find(Tier.QUEUED_FOR_DELETION.Id, CancellationToken.None, true); + + if (queuedForDeletionTier == null) + { + queuedForDeletionTier = new Tier(new TierId(Tier.QUEUED_FOR_DELETION.Id), Tier.QUEUED_FOR_DELETION.Name); + await _tiersRepository.Add(queuedForDeletionTier, CancellationToken.None); + } + + var metrics = await _metricsRepository.FindAll(CancellationToken.None); + var addedQuotas = queuedForDeletionTier.AddQuotaForAllMetricsOnQueuedForDeletion(metrics); + await _tiersRepository.Update(queuedForDeletionTier, CancellationToken.None); + + foreach (var quota in addedQuotas.ToList()) + _eventBus.Publish(new QuotaCreatedForTierIntegrationEvent(queuedForDeletionTier.Id, quota.Id)); + } +} diff --git a/Modules/Quotas/src/Quotas.Application/Tiers/Commands/SeedQueuedForDeletionTier/SeedQueuedForDeletionTierCommand.cs b/Modules/Quotas/src/Quotas.Application/Tiers/Commands/SeedQueuedForDeletionTier/SeedQueuedForDeletionTierCommand.cs new file mode 100644 index 0000000000..342ea80b23 --- /dev/null +++ b/Modules/Quotas/src/Quotas.Application/Tiers/Commands/SeedQueuedForDeletionTier/SeedQueuedForDeletionTierCommand.cs @@ -0,0 +1,7 @@ +using MediatR; + +namespace Backbone.Modules.Quotas.Application.Tiers.Commands.SeedQueuedForDeletionTier; + +public class SeedQueuedForDeletionTierCommand : IRequest +{ +} diff --git a/Modules/Quotas/src/Quotas.Domain/Aggregates/Tiers/Tier.cs b/Modules/Quotas/src/Quotas.Domain/Aggregates/Tiers/Tier.cs index d1c9f322e5..1a56ab2201 100644 --- a/Modules/Quotas/src/Quotas.Domain/Aggregates/Tiers/Tier.cs +++ b/Modules/Quotas/src/Quotas.Domain/Aggregates/Tiers/Tier.cs @@ -7,11 +7,13 @@ namespace Backbone.Modules.Quotas.Domain.Aggregates.Tiers; public class Tier { + public static readonly Tier QUEUED_FOR_DELETION = new(new TierId("TIR00000000000000001"), "Queued For Deletion"); + public Tier(TierId id, string name) { Id = id; Name = name; - Quotas = new(); + Quotas = new List(); } public TierId Id { get; } @@ -20,20 +22,20 @@ public Tier(TierId id, string name) public Result CreateQuota(MetricKey metricKey, int max, QuotaPeriod period) { + if (IsQueuedForDeletionTier()) + return Result.Failure(DomainErrors.CannotCreateOrDeleteQuotaForQueuedForDeletionTier()); + if (max < 0) return Result.Failure(DomainErrors.MaxValueCannotBeLowerThanZero()); - if (TierQuotaAlreadyExists(metricKey, period)) - return Result.Failure(DomainErrors.DuplicateQuota()); - - var quotaDefinition = new TierQuotaDefinition(metricKey, max, period); - Quotas.Add(quotaDefinition); - - return Result.Success(quotaDefinition); + return AddTierQuotaDefinition(metricKey, max, period); } public Result DeleteQuota(string tierQuotaDefinitionId) { + if (IsQueuedForDeletionTier()) + return Result.Failure(DomainErrors.CannotCreateOrDeleteQuotaForQueuedForDeletionTier()); + var quotaDefinition = Quotas.FirstOrDefault(q => q.Id == tierQuotaDefinitionId); if (quotaDefinition == null) @@ -44,6 +46,39 @@ public Result DeleteQuota(string tierQuotaDe return Result.Success(quotaDefinition.Id); } + public IEnumerable AddQuotaForAllMetricsOnQueuedForDeletion(IEnumerable metrics) + { + if (!IsQueuedForDeletionTier()) + throw new InvalidOperationException("Method can only be called for the 'Queued for Deletion' tier"); + + var missingMetrics = metrics.Where(metric => Quotas.All(q => q.MetricKey.Value != metric.Key.Value)); + + var addedQuotas = new List(); + foreach (var metric in missingMetrics) + { + var result = AddTierQuotaDefinition(metric.Key, 0, QuotaPeriod.Total); + addedQuotas.Add(result.Value); + } + + return addedQuotas; + } + + private Result AddTierQuotaDefinition(MetricKey metricKey, int max, QuotaPeriod period) + { + if (TierQuotaAlreadyExists(metricKey, period)) + return Result.Failure(DomainErrors.DuplicateQuota()); + + var quotaDefinition = new TierQuotaDefinition(metricKey, max, period); + Quotas.Add(quotaDefinition); + + return Result.Success(quotaDefinition); + } + + private bool IsQueuedForDeletionTier() + { + return Id == QUEUED_FOR_DELETION.Id; + } + private bool TierQuotaAlreadyExists(MetricKey metricKey, QuotaPeriod period) { return Quotas.Any(q => q.MetricKey == metricKey && q.Period == period); diff --git a/Modules/Quotas/src/Quotas.Domain/DomainErrors.cs b/Modules/Quotas/src/Quotas.Domain/DomainErrors.cs index 2ae38d83d2..0f0d2b8e43 100644 --- a/Modules/Quotas/src/Quotas.Domain/DomainErrors.cs +++ b/Modules/Quotas/src/Quotas.Domain/DomainErrors.cs @@ -2,6 +2,7 @@ using Backbone.Modules.Quotas.Domain.Aggregates.Metrics; namespace Backbone.Modules.Quotas.Domain; + public static class DomainErrors { public static DomainError UnsupportedMetricKey() @@ -18,4 +19,9 @@ public static DomainError DuplicateQuota() { return new DomainError("error.platform.quotas.duplicateQuota", "A quota targeting the same metric and period already exists."); } + + public static DomainError CannotCreateOrDeleteQuotaForQueuedForDeletionTier() + { + return new DomainError("error.platform.quotas.cannotCreateOrDeleteQuotaOnQueuedForDeletionTier", "Quotas cannot be manually managed for the 'Queued for Deletion' tier."); + } } diff --git a/Modules/Quotas/test/Quotas.Domain.Tests/Tests/Tiers/TierTests.cs b/Modules/Quotas/test/Quotas.Domain.Tests/Tests/Tiers/TierTests.cs index cabb0f7356..5b44a1d109 100644 --- a/Modules/Quotas/test/Quotas.Domain.Tests/Tests/Tiers/TierTests.cs +++ b/Modules/Quotas/test/Quotas.Domain.Tests/Tests/Tiers/TierTests.cs @@ -1,9 +1,10 @@ using Backbone.Modules.Quotas.Domain.Aggregates.Identities; +using Backbone.Modules.Quotas.Domain.Aggregates.Metrics; using Backbone.Modules.Quotas.Domain.Aggregates.Tiers; using Backbone.UnitTestTools.Extensions; using FluentAssertions; +using Newtonsoft.Json; using Xunit; -using MetricKey = Backbone.Modules.Quotas.Domain.Aggregates.Metrics.MetricKey; namespace Backbone.Modules.Quotas.Domain.Tests.Tests.Tiers; @@ -33,6 +34,18 @@ public void Can_create_quota_on_tier() tier.Quotas.Should().HaveCount(1); } + [Fact] + public void Cannot_create_quota_for_queued_for_deletion_tier() + { + // Arrange & Act + var tier = Tier.QUEUED_FOR_DELETION.Clone(); + var result = tier.CreateQuota(MetricKey.NumberOfSentMessages, 5, QuotaPeriod.Month); + + // Assert + result.IsFailure.Should().BeTrue(); + result.Error.Code.Should().Be("error.platform.quotas.cannotCreateOrDeleteQuotaOnQueuedForDeletionTier"); + } + [Fact] public void Can_delete_quota_on_tier() { @@ -47,6 +60,25 @@ public void Can_delete_quota_on_tier() tier.Quotas.Should().HaveCount(0); } + [Fact] + public void Cannot_delete_quota_on_queued_for_deletion_tier() + { + // Arrange + var metrics = new List + { + new(MetricKey.NumberOfRelationships, "Number of Relationships") + }; + var tier = Tier.QUEUED_FOR_DELETION.Clone(); + var addedQuotas = tier.AddQuotaForAllMetricsOnQueuedForDeletion(metrics); + + // Act + var result = tier.DeleteQuota(addedQuotas.First().Id); + + // Assert + result.IsFailure.Should().BeTrue(); + result.Error.Code.Should().Be("error.platform.quotas.cannotCreateOrDeleteQuotaOnQueuedForDeletionTier"); + } + [Fact] public void Does_only_delete_quota_with_given_id() { @@ -95,4 +127,72 @@ public void Creating_a_quota_with_duplicate_quota_metric_period_throws_domain_ex result.IsSuccess.Should().BeFalse(); result.Error.Code.Should().Be("error.platform.quotas.duplicateQuota"); } + + [Fact] + public void AddQuotaForAllMetricsOnQueuedForDeletion_can_only_be_called_on_queued_for_deletion_tier() + { + // Arrange + var metrics = new List + { + new(MetricKey.NumberOfRelationships, "Number of Relationships") + }; + var tier = new Tier(new TierId("SomeTierId"), "some tier"); + + // Act + Action act = () => tier.AddQuotaForAllMetricsOnQueuedForDeletion(metrics); + + // Assert + act.Should().Throw().Which.Message.Should().Be("Method can only be called for the 'Queued for Deletion' tier"); + } + + [Fact] + public void AddQuotaForAllMetricsOnQueuedForDeletion_adds_quotas() + { + // Arrange + var tier = Tier.QUEUED_FOR_DELETION.Clone(); + + var metrics = new List + { + new(MetricKey.NumberOfSentMessages, "Number of Sent Messages") + }; + + // Act + var addedQuotas = tier.AddQuotaForAllMetricsOnQueuedForDeletion(metrics).ToList(); + + // Assert + tier.Quotas.Should().HaveCount(1); + addedQuotas.Should().HaveCount(1); + addedQuotas.First().MetricKey.Should().Be(MetricKey.NumberOfSentMessages); + } + + [Fact] + public void AddQuotaForAllMetricsOnQueuedForDeletion_only_creates_missing_quotas() + { + // Arrange + var metrics = new List + { + new(MetricKey.NumberOfSentMessages, "Number of Sent Messages") + }; + + var tier = Tier.QUEUED_FOR_DELETION.Clone(); + tier.AddQuotaForAllMetricsOnQueuedForDeletion(metrics); + metrics.Add(new Metric(MetricKey.NumberOfRelationships, "Number of Relationships")); + + // Act + var addedQuotas = tier.AddQuotaForAllMetricsOnQueuedForDeletion(metrics).ToList(); + + // Assert + addedQuotas.Should().HaveCount(1); + tier.Quotas.Should().HaveCount(2); + } +} + + +file static class TierExtensions +{ + public static Tier Clone(this Tier tier) + { + var serialized = JsonConvert.SerializeObject(tier); + return JsonConvert.DeserializeObject(serialized)!; + } } diff --git a/Modules/Relationships/src/Relationships.Application/Extensions/IEventBusExtensions.cs b/Modules/Relationships/src/Relationships.Application/Extensions/IEventBusExtensions.cs new file mode 100644 index 0000000000..4079c5a8e8 --- /dev/null +++ b/Modules/Relationships/src/Relationships.Application/Extensions/IEventBusExtensions.cs @@ -0,0 +1,11 @@ +using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus; + +namespace Backbone.Modules.Relationships.Application.Extensions; + +public static class IEventBusExtensions +{ + public static IEventBus AddRelationshipsIntegrationEventSubscriptions(this IEventBus eventBus) + { + return eventBus; + } +} diff --git a/Modules/Relationships/src/Relationships.Application/Infrastructure/Persistence/Repository/IRelationshipsRepository.cs b/Modules/Relationships/src/Relationships.Application/Infrastructure/Persistence/Repository/IRelationshipsRepository.cs index f7f0c0e803..216010af93 100644 --- a/Modules/Relationships/src/Relationships.Application/Infrastructure/Persistence/Repository/IRelationshipsRepository.cs +++ b/Modules/Relationships/src/Relationships.Application/Infrastructure/Persistence/Repository/IRelationshipsRepository.cs @@ -1,4 +1,5 @@ -using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.Persistence.Database; +using System.Linq.Expressions; +using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.Persistence.Database; using Backbone.BuildingBlocks.Application.Pagination; using Backbone.DevelopmentKit.Identity.ValueObjects; using Backbone.Modules.Relationships.Common; @@ -16,6 +17,6 @@ Task> FindChangesWithIds(IEnumerable FindRelationshipChange(RelationshipChangeId id, IdentityAddress identityAddress, CancellationToken cancellationToken, bool track = false); Task Add(Relationship relationship, CancellationToken cancellationToken); Task Update(Relationship relationship); - + Task> FindRelationships(Expression> filter, CancellationToken cancellationToken); Task RelationshipBetweenTwoIdentitiesExists(IdentityAddress identityAddressA, IdentityAddress identityAddressB, CancellationToken cancellationToken); } diff --git a/Modules/Relationships/src/Relationships.Application/IntegrationEvents/RelationshipChangeCompletedIntegrationEvent.cs b/Modules/Relationships/src/Relationships.Application/IntegrationEvents/Outgoing/RelationshipChangeCompletedIntegrationEvent.cs similarity index 98% rename from Modules/Relationships/src/Relationships.Application/IntegrationEvents/RelationshipChangeCompletedIntegrationEvent.cs rename to Modules/Relationships/src/Relationships.Application/IntegrationEvents/Outgoing/RelationshipChangeCompletedIntegrationEvent.cs index 14fa3d3d57..7449f06fe5 100644 --- a/Modules/Relationships/src/Relationships.Application/IntegrationEvents/RelationshipChangeCompletedIntegrationEvent.cs +++ b/Modules/Relationships/src/Relationships.Application/IntegrationEvents/Outgoing/RelationshipChangeCompletedIntegrationEvent.cs @@ -1,7 +1,7 @@ using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus.Events; using Backbone.Modules.Relationships.Domain.Entities; -namespace Backbone.Modules.Relationships.Application.IntegrationEvents; +namespace Backbone.Modules.Relationships.Application.IntegrationEvents.Outgoing; public class RelationshipChangeCompletedIntegrationEvent : IntegrationEvent { diff --git a/Modules/Relationships/src/Relationships.Application/IntegrationEvents/RelationshipChangeCreatedIntegrationEvent.cs b/Modules/Relationships/src/Relationships.Application/IntegrationEvents/Outgoing/RelationshipChangeCreatedIntegrationEvent.cs similarity index 98% rename from Modules/Relationships/src/Relationships.Application/IntegrationEvents/RelationshipChangeCreatedIntegrationEvent.cs rename to Modules/Relationships/src/Relationships.Application/IntegrationEvents/Outgoing/RelationshipChangeCreatedIntegrationEvent.cs index 931465a87e..bb3227e347 100644 --- a/Modules/Relationships/src/Relationships.Application/IntegrationEvents/RelationshipChangeCreatedIntegrationEvent.cs +++ b/Modules/Relationships/src/Relationships.Application/IntegrationEvents/Outgoing/RelationshipChangeCreatedIntegrationEvent.cs @@ -1,7 +1,7 @@ using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus.Events; using Backbone.Modules.Relationships.Domain.Entities; -namespace Backbone.Modules.Relationships.Application.IntegrationEvents; +namespace Backbone.Modules.Relationships.Application.IntegrationEvents.Outgoing; public class RelationshipChangeCreatedIntegrationEvent : IntegrationEvent { public RelationshipChangeCreatedIntegrationEvent(RelationshipChange change) : base($"{change.Id}/Created") diff --git a/Modules/Relationships/src/Relationships.Application/IntegrationEvents/RelationshipTemplateCreatedIntegrationEvent.cs b/Modules/Relationships/src/Relationships.Application/IntegrationEvents/Outgoing/RelationshipTemplateCreatedIntegrationEvent.cs similarity index 97% rename from Modules/Relationships/src/Relationships.Application/IntegrationEvents/RelationshipTemplateCreatedIntegrationEvent.cs rename to Modules/Relationships/src/Relationships.Application/IntegrationEvents/Outgoing/RelationshipTemplateCreatedIntegrationEvent.cs index 0da061968b..9417aedb5b 100644 --- a/Modules/Relationships/src/Relationships.Application/IntegrationEvents/RelationshipTemplateCreatedIntegrationEvent.cs +++ b/Modules/Relationships/src/Relationships.Application/IntegrationEvents/Outgoing/RelationshipTemplateCreatedIntegrationEvent.cs @@ -1,7 +1,7 @@ using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus.Events; using Backbone.Modules.Relationships.Domain.Entities; -namespace Backbone.Modules.Relationships.Application.IntegrationEvents; +namespace Backbone.Modules.Relationships.Application.IntegrationEvents.Outgoing; public class RelationshipTemplateCreatedIntegrationEvent : IntegrationEvent { public RelationshipTemplateCreatedIntegrationEvent(RelationshipTemplate template) : base($"{template.Id}/Created") diff --git a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/CreateRelationshipTemplate/Handler.cs b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/CreateRelationshipTemplate/Handler.cs index 253b2ef020..02c31622e3 100644 --- a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/CreateRelationshipTemplate/Handler.cs +++ b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/CreateRelationshipTemplate/Handler.cs @@ -2,7 +2,7 @@ using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus; using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext; using Backbone.Modules.Relationships.Application.Infrastructure.Persistence.Repository; -using Backbone.Modules.Relationships.Application.IntegrationEvents; +using Backbone.Modules.Relationships.Application.IntegrationEvents.Outgoing; using Backbone.Modules.Relationships.Domain.Entities; using MediatR; diff --git a/Modules/Relationships/src/Relationships.Application/Relationships/Commands/AcceptRelationshipChangeRequest/Handler.cs b/Modules/Relationships/src/Relationships.Application/Relationships/Commands/AcceptRelationshipChangeRequest/Handler.cs index dacd269cba..667cc35109 100644 --- a/Modules/Relationships/src/Relationships.Application/Relationships/Commands/AcceptRelationshipChangeRequest/Handler.cs +++ b/Modules/Relationships/src/Relationships.Application/Relationships/Commands/AcceptRelationshipChangeRequest/Handler.cs @@ -2,7 +2,7 @@ using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus; using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext; using Backbone.Modules.Relationships.Application.Infrastructure.Persistence.Repository; -using Backbone.Modules.Relationships.Application.IntegrationEvents; +using Backbone.Modules.Relationships.Application.IntegrationEvents.Outgoing; using Backbone.Modules.Relationships.Domain.Entities; using MediatR; diff --git a/Modules/Relationships/src/Relationships.Application/Relationships/Commands/CreateRelationship/Handler.cs b/Modules/Relationships/src/Relationships.Application/Relationships/Commands/CreateRelationship/Handler.cs index 31502f74cd..4da7a7bc9a 100644 --- a/Modules/Relationships/src/Relationships.Application/Relationships/Commands/CreateRelationship/Handler.cs +++ b/Modules/Relationships/src/Relationships.Application/Relationships/Commands/CreateRelationship/Handler.cs @@ -3,7 +3,7 @@ using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus; using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext; using Backbone.Modules.Relationships.Application.Infrastructure.Persistence.Repository; -using Backbone.Modules.Relationships.Application.IntegrationEvents; +using Backbone.Modules.Relationships.Application.IntegrationEvents.Outgoing; using Backbone.Modules.Relationships.Domain.Entities; using MediatR; diff --git a/Modules/Relationships/src/Relationships.Application/Relationships/Commands/RejectRelationshipChangeRequest/Handler.cs b/Modules/Relationships/src/Relationships.Application/Relationships/Commands/RejectRelationshipChangeRequest/Handler.cs index 51269887f9..72183827e5 100644 --- a/Modules/Relationships/src/Relationships.Application/Relationships/Commands/RejectRelationshipChangeRequest/Handler.cs +++ b/Modules/Relationships/src/Relationships.Application/Relationships/Commands/RejectRelationshipChangeRequest/Handler.cs @@ -2,7 +2,7 @@ using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus; using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext; using Backbone.Modules.Relationships.Application.Infrastructure.Persistence.Repository; -using Backbone.Modules.Relationships.Application.IntegrationEvents; +using Backbone.Modules.Relationships.Application.IntegrationEvents.Outgoing; using Backbone.Modules.Relationships.Domain.Entities; using MediatR; diff --git a/Modules/Relationships/src/Relationships.Application/Relationships/Commands/RevokeRelationshipChangeRequest/Handler.cs b/Modules/Relationships/src/Relationships.Application/Relationships/Commands/RevokeRelationshipChangeRequest/Handler.cs index b853e7d5c4..368dca9ee8 100644 --- a/Modules/Relationships/src/Relationships.Application/Relationships/Commands/RevokeRelationshipChangeRequest/Handler.cs +++ b/Modules/Relationships/src/Relationships.Application/Relationships/Commands/RevokeRelationshipChangeRequest/Handler.cs @@ -2,7 +2,7 @@ using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus; using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext; using Backbone.Modules.Relationships.Application.Infrastructure.Persistence.Repository; -using Backbone.Modules.Relationships.Application.IntegrationEvents; +using Backbone.Modules.Relationships.Application.IntegrationEvents.Outgoing; using Backbone.Modules.Relationships.Domain.Entities; using MediatR; diff --git a/Modules/Relationships/src/Relationships.ConsumerApi/RelationshipsModule.cs b/Modules/Relationships/src/Relationships.ConsumerApi/RelationshipsModule.cs index d00ece3fe8..980f5a44da 100644 --- a/Modules/Relationships/src/Relationships.ConsumerApi/RelationshipsModule.cs +++ b/Modules/Relationships/src/Relationships.ConsumerApi/RelationshipsModule.cs @@ -47,5 +47,6 @@ public override void ConfigureServices(IServiceCollection services, IConfigurati public override void ConfigureEventBus(IEventBus eventBus) { + eventBus.AddRelationshipsIntegrationEventSubscriptions(); } } diff --git a/Modules/Relationships/src/Relationships.Domain/Entities/Relationship.cs b/Modules/Relationships/src/Relationships.Domain/Entities/Relationship.cs index 8a9a14ae76..c25c20e398 100644 --- a/Modules/Relationships/src/Relationships.Domain/Entities/Relationship.cs +++ b/Modules/Relationships/src/Relationships.Domain/Entities/Relationship.cs @@ -1,4 +1,5 @@ -using Backbone.BuildingBlocks.Domain; +using System.Linq.Expressions; +using Backbone.BuildingBlocks.Domain; using Backbone.DevelopmentKit.Identity.ValueObjects; using Backbone.Modules.Relationships.Domain.Errors; using Backbone.Modules.Relationships.Domain.Ids; @@ -145,4 +146,11 @@ private void EnsureCanBeTerminated() if (existingChange != null) throw new DomainException(DomainErrors.PendingChangeAlreadyExists(existingChange.Id)); } + + #region Selectors + public static Expression> HasParticipant(string identity) + { + return r => r.From == identity || r.To == identity; + } + #endregion } diff --git a/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/Repository/RelationshipsRepository.cs b/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/Repository/RelationshipsRepository.cs index be700a155f..54dbfd60bf 100644 --- a/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/Repository/RelationshipsRepository.cs +++ b/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/Repository/RelationshipsRepository.cs @@ -1,4 +1,5 @@ -using Backbone.BuildingBlocks.Application.Abstractions.Exceptions; +using System.Linq.Expressions; +using Backbone.BuildingBlocks.Application.Abstractions.Exceptions; using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.Persistence.Database; using Backbone.BuildingBlocks.Application.Extensions; using Backbone.BuildingBlocks.Application.Pagination; @@ -136,6 +137,11 @@ public async Task RelationshipBetweenTwoIdentitiesExists(IdentityAddress i r.Status != RelationshipStatus.Revoked) .AnyAsync(cancellationToken); } + + public async Task> FindRelationships(Expression> filter, CancellationToken cancellationToken) + { + return await _relationships.Where(filter).ToListAsync(cancellationToken); + } } internal static partial class RelationshipRepositoryLogs diff --git a/Modules/Relationships/test/Relationships.Application.Tests/Tests/RelationshipTemplates/Commands/CreateRelationshipTemplate/HandlerTests.cs b/Modules/Relationships/test/Relationships.Application.Tests/Tests/RelationshipTemplates/Commands/CreateRelationshipTemplate/HandlerTests.cs index 7149e43b03..3332610dd1 100644 --- a/Modules/Relationships/test/Relationships.Application.Tests/Tests/RelationshipTemplates/Commands/CreateRelationshipTemplate/HandlerTests.cs +++ b/Modules/Relationships/test/Relationships.Application.Tests/Tests/RelationshipTemplates/Commands/CreateRelationshipTemplate/HandlerTests.cs @@ -3,7 +3,7 @@ using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext; using Backbone.DevelopmentKit.Identity.ValueObjects; using Backbone.Modules.Relationships.Application.Infrastructure.Persistence.Repository; -using Backbone.Modules.Relationships.Application.IntegrationEvents; +using Backbone.Modules.Relationships.Application.IntegrationEvents.Outgoing; using Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.CreateRelationshipTemplate; using Backbone.Modules.Relationships.Domain.Entities; using FakeItEasy; diff --git a/Modules/Synchronization/src/Synchronization.Application/Extensions/IEventBusExtensions.cs b/Modules/Synchronization/src/Synchronization.Application/Extensions/IEventBusExtensions.cs index b8e5d892b5..2f2db15f99 100644 --- a/Modules/Synchronization/src/Synchronization.Application/Extensions/IEventBusExtensions.cs +++ b/Modules/Synchronization/src/Synchronization.Application/Extensions/IEventBusExtensions.cs @@ -1,4 +1,5 @@ using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus; +using Backbone.Modules.Synchronization.Application.IntegrationEvents.Incoming.IdentityDeletionProcessStarted; using Backbone.Modules.Synchronization.Application.IntegrationEvents.Incoming.MessageCreated; using Backbone.Modules.Synchronization.Application.IntegrationEvents.Incoming.RelationshipChangeCompleted; using Backbone.Modules.Synchronization.Application.IntegrationEvents.Incoming.RelationshipChangeCreated; @@ -11,6 +12,7 @@ public static IEventBus AddSynchronizationIntegrationEventSubscriptions(this IEv { SubscribeToMessagesEvents(eventBus); SubscribeToRelationshipsEvents(eventBus); + SubscribeToIdentitiesEvents(eventBus); return eventBus; } @@ -18,6 +20,7 @@ public static IEventBus AddSynchronizationIntegrationEventSubscriptions(this IEv private static void SubscribeToMessagesEvents(IEventBus eventBus) { eventBus.Subscribe(); + eventBus.Subscribe(); // eventBus.Subscribe(); // this is temporaryly disabled to avoid an external event flood when the same message is sent to many recipients (s. JSSNMSHDD-2174) } @@ -26,4 +29,8 @@ private static void SubscribeToRelationshipsEvents(IEventBus eventBus) eventBus.Subscribe(); eventBus.Subscribe(); } + + private static void SubscribeToIdentitiesEvents(IEventBus eventBus) + { + } } diff --git a/Modules/Synchronization/src/Synchronization.Application/IntegrationEvents/Incoming/IdentityDeletionProcessStarted/IdentityDeletionProcessStartedIntegrationEvent.cs b/Modules/Synchronization/src/Synchronization.Application/IntegrationEvents/Incoming/IdentityDeletionProcessStarted/IdentityDeletionProcessStartedIntegrationEvent.cs new file mode 100644 index 0000000000..9c6da7173d --- /dev/null +++ b/Modules/Synchronization/src/Synchronization.Application/IntegrationEvents/Incoming/IdentityDeletionProcessStarted/IdentityDeletionProcessStartedIntegrationEvent.cs @@ -0,0 +1,15 @@ +using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus.Events; + +namespace Backbone.Modules.Synchronization.Application.IntegrationEvents.Incoming.IdentityDeletionProcessStarted; + +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/Synchronization/src/Synchronization.Application/IntegrationEvents/Incoming/IdentityDeletionProcessStarted/IdentityDeletionProcessStartedIntegrationEventHandler.cs b/Modules/Synchronization/src/Synchronization.Application/IntegrationEvents/Incoming/IdentityDeletionProcessStarted/IdentityDeletionProcessStartedIntegrationEventHandler.cs new file mode 100644 index 0000000000..a9582bce07 --- /dev/null +++ b/Modules/Synchronization/src/Synchronization.Application/IntegrationEvents/Incoming/IdentityDeletionProcessStarted/IdentityDeletionProcessStartedIntegrationEventHandler.cs @@ -0,0 +1,39 @@ +using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus; +using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Synchronization.Application.Infrastructure; +using Backbone.Modules.Synchronization.Application.IntegrationEvents.Outgoing; +using Backbone.Modules.Synchronization.Domain.Entities.Sync; +using Microsoft.Extensions.Logging; + +namespace Backbone.Modules.Synchronization.Application.IntegrationEvents.Incoming.IdentityDeletionProcessStarted; + +public class IdentityDeletionProcessStartedIntegrationEventHandler : IIntegrationEventHandler +{ + private readonly ISynchronizationDbContext _dbContext; + private readonly IEventBus _eventBus; + private readonly ILogger _logger; + + public IdentityDeletionProcessStartedIntegrationEventHandler(ISynchronizationDbContext dbContext, IEventBus eventBus, ILogger logger) + { + _dbContext = dbContext; + _eventBus = eventBus; + _logger = logger; + } + + public async Task Handle(IdentityDeletionProcessStartedIntegrationEvent integrationEvent) + { +#pragma warning disable IDE0037 + var payload = new { DeletionProcessId = integrationEvent.DeletionProcessId }; +#pragma warning restore IDE0037 + try + { + var externalEvent = await _dbContext.CreateExternalEvent(IdentityAddress.Parse(integrationEvent.Address), ExternalEventType.IdentityDeletionProcessStarted, payload); + _eventBus.Publish(new ExternalEventCreatedIntegrationEvent(externalEvent)); + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while processing an integration event."); + throw; + } + } +} diff --git a/Modules/Synchronization/src/Synchronization.Application/IntegrationEvents/Incoming/MessageCreated/MessageCreatedIntegrationEventHandler.cs b/Modules/Synchronization/src/Synchronization.Application/IntegrationEvents/Incoming/MessageCreated/MessageCreatedIntegrationEventHandler.cs index e885e047d5..058ad659e2 100644 --- a/Modules/Synchronization/src/Synchronization.Application/IntegrationEvents/Incoming/MessageCreated/MessageCreatedIntegrationEventHandler.cs +++ b/Modules/Synchronization/src/Synchronization.Application/IntegrationEvents/Incoming/MessageCreated/MessageCreatedIntegrationEventHandler.cs @@ -29,7 +29,9 @@ private async Task CreateExternalEvents(MessageCreatedIntegrationEvent integrati { foreach (var recipient in integrationEvent.Recipients) { - var payload = new { integrationEvent.Id }; +#pragma warning disable IDE0037 + var payload = new { Id = integrationEvent.Id }; +#pragma warning restore IDE0037 try { var externalEvent = await _dbContext.CreateExternalEvent(IdentityAddress.Parse(recipient), ExternalEventType.MessageReceived, payload); diff --git a/Modules/Synchronization/src/Synchronization.Application/IntegrationEvents/Incoming/RelationshipChangeCompleted/RelationshipChangeCompletedIntegrationEventHandler.cs b/Modules/Synchronization/src/Synchronization.Application/IntegrationEvents/Incoming/RelationshipChangeCompleted/RelationshipChangeCompletedIntegrationEventHandler.cs index 6e88a4a741..c6d693e314 100644 --- a/Modules/Synchronization/src/Synchronization.Application/IntegrationEvents/Incoming/RelationshipChangeCompleted/RelationshipChangeCompletedIntegrationEventHandler.cs +++ b/Modules/Synchronization/src/Synchronization.Application/IntegrationEvents/Incoming/RelationshipChangeCompleted/RelationshipChangeCompletedIntegrationEventHandler.cs @@ -26,7 +26,9 @@ public async Task Handle(RelationshipChangeCompletedIntegrationEvent integration private async Task CreateExternalEvent(RelationshipChangeCompletedIntegrationEvent integrationEvent) { - var payload = new { integrationEvent.RelationshipId, integrationEvent.ChangeId }; +#pragma warning disable IDE0037 + var payload = new { RelationshipId = integrationEvent.RelationshipId, ChangeId = integrationEvent.ChangeId }; +#pragma warning restore IDE0037 try { var owner = integrationEvent.ChangeResult switch diff --git a/Modules/Synchronization/src/Synchronization.Application/IntegrationEvents/Incoming/RelationshipChangeCreated/RelationshipChangeCreatedIntegrationEventHandler.cs b/Modules/Synchronization/src/Synchronization.Application/IntegrationEvents/Incoming/RelationshipChangeCreated/RelationshipChangeCreatedIntegrationEventHandler.cs index fd6a1b1839..c175365026 100644 --- a/Modules/Synchronization/src/Synchronization.Application/IntegrationEvents/Incoming/RelationshipChangeCreated/RelationshipChangeCreatedIntegrationEventHandler.cs +++ b/Modules/Synchronization/src/Synchronization.Application/IntegrationEvents/Incoming/RelationshipChangeCreated/RelationshipChangeCreatedIntegrationEventHandler.cs @@ -26,7 +26,9 @@ public async Task Handle(RelationshipChangeCreatedIntegrationEvent integrationEv private async Task CreateExternalEvent(RelationshipChangeCreatedIntegrationEvent integrationEvent) { - var payload = new { integrationEvent.RelationshipId, integrationEvent.ChangeId }; +#pragma warning disable IDE0037 + var payload = new { RelationshipId = integrationEvent.RelationshipId, ChangeId = integrationEvent.ChangeId }; +#pragma warning restore IDE0037 try { var externalEvent = await _dbContext.CreateExternalEvent(integrationEvent.ChangeRecipient, ExternalEventType.RelationshipChangeCreated, payload); diff --git a/Modules/Synchronization/src/Synchronization.Domain/Entities/Sync/ExternalEvent.cs b/Modules/Synchronization/src/Synchronization.Domain/Entities/Sync/ExternalEvent.cs index f2fb26fbb0..e18a5f9822 100644 --- a/Modules/Synchronization/src/Synchronization.Domain/Entities/Sync/ExternalEvent.cs +++ b/Modules/Synchronization/src/Synchronization.Domain/Entities/Sync/ExternalEvent.cs @@ -51,8 +51,9 @@ public void SyncFailed(SyncError error) public enum ExternalEventType { - MessageReceived, - MessageDelivered, - RelationshipChangeCreated, - RelationshipChangeCompleted + MessageReceived = 0, + MessageDelivered = 1, + RelationshipChangeCreated = 2, + RelationshipChangeCompleted = 3, + IdentityDeletionProcessStarted = 4 } diff --git a/Modules/Synchronization/test/Synchronization.Application.Tests/Tests/IntegrationEvents/IdentityDeletionProcessStartedIntegrationEventHandlerTests.cs b/Modules/Synchronization/test/Synchronization.Application.Tests/Tests/IntegrationEvents/IdentityDeletionProcessStartedIntegrationEventHandlerTests.cs new file mode 100644 index 0000000000..19b6b4b8ca --- /dev/null +++ b/Modules/Synchronization/test/Synchronization.Application.Tests/Tests/IntegrationEvents/IdentityDeletionProcessStartedIntegrationEventHandlerTests.cs @@ -0,0 +1,44 @@ +using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus; +using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Synchronization.Application.Infrastructure; +using Backbone.Modules.Synchronization.Application.IntegrationEvents.Incoming.IdentityDeletionProcessStarted; +using Backbone.Modules.Synchronization.Application.IntegrationEvents.Outgoing; +using Backbone.Modules.Synchronization.Domain.Entities.Sync; +using FakeItEasy; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Backbone.Modules.Synchronization.Application.Tests.Tests.IntegrationEvents; + +public class IdentityDeletionProcessStartedIntegrationEventHandlerTests +{ + [Fact] + public async Task Creates_an_external_event() + { + // Arrange + var identityAddress = TestDataGenerator.CreateRandomIdentityAddress(); + var identityDeletionProcessStartedIntegrationEvent = new IdentityDeletionProcessStartedIntegrationEvent(identityAddress, "some-deletion-process-id"); + + var fakeDbContext = A.Fake(); + var mockEventBus = A.Fake(); + + var externalEvent = new ExternalEvent(ExternalEventType.IdentityDeletionProcessStarted, IdentityAddress.Parse(identityAddress), 1, + new { identityDeletionProcessStartedIntegrationEvent.DeletionProcessId }); + + A.CallTo(() => fakeDbContext.CreateExternalEvent( + A.That.Matches(i => i.StringValue == identityAddress), + ExternalEventType.IdentityDeletionProcessStarted, + A._) + ).Returns(externalEvent); + + var handler = new IdentityDeletionProcessStartedIntegrationEventHandler(fakeDbContext, mockEventBus, A.Fake>()); + + // Act + await handler.Handle(identityDeletionProcessStartedIntegrationEvent); + + // Handle + A.CallTo(() => mockEventBus.Publish( + A.That.Matches(e => e.Owner == externalEvent.Owner && e.EventId == externalEvent.Id)) + ).MustHaveHappenedOnceExactly(); + } +} diff --git a/Modules/Tokens/src/Tokens.Application/Tokens.Application.csproj b/Modules/Tokens/src/Tokens.Application/Tokens.Application.csproj index 1cf4eea348..c0987cbe38 100644 --- a/Modules/Tokens/src/Tokens.Application/Tokens.Application.csproj +++ b/Modules/Tokens/src/Tokens.Application/Tokens.Application.csproj @@ -1,4 +1,7 @@ + + enable + diff --git a/Modules/Tokens/src/Tokens.ConsumerApi/Tokens.ConsumerApi.csproj b/Modules/Tokens/src/Tokens.ConsumerApi/Tokens.ConsumerApi.csproj index da58671fb9..c658ee6a2d 100644 --- a/Modules/Tokens/src/Tokens.ConsumerApi/Tokens.ConsumerApi.csproj +++ b/Modules/Tokens/src/Tokens.ConsumerApi/Tokens.ConsumerApi.csproj @@ -1,4 +1,4 @@ - + enable diff --git a/Modules/Tokens/src/Tokens.Domain/Tokens.Domain.csproj b/Modules/Tokens/src/Tokens.Domain/Tokens.Domain.csproj index 757c63fd6c..4158fd3752 100644 --- a/Modules/Tokens/src/Tokens.Domain/Tokens.Domain.csproj +++ b/Modules/Tokens/src/Tokens.Domain/Tokens.Domain.csproj @@ -1,4 +1,4 @@ - + enable diff --git a/Modules/Tokens/src/Tokens.Infrastructure/Tokens.Infrastructure.csproj b/Modules/Tokens/src/Tokens.Infrastructure/Tokens.Infrastructure.csproj index 36d93e7350..9f4b5022ad 100644 --- a/Modules/Tokens/src/Tokens.Infrastructure/Tokens.Infrastructure.csproj +++ b/Modules/Tokens/src/Tokens.Infrastructure/Tokens.Infrastructure.csproj @@ -1,4 +1,7 @@ + + enable +