From 8c9375103f7a265d09ff647b8a03c4ba01ba0b2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Vetni=C4=87?= <62119280+NikolaVetnic@users.noreply.github.com> Date: Fri, 11 Oct 2024 10:35:12 +0200 Subject: [PATCH] Consumer API: Password-protected RelationshipTemplates (#900) * feat: support password protected relationship templates, wip * test: remove redundant scenarios * test: move step definitions to correct places and only use SINGLE_THING_OR_DEFAULT where it's fitting * test: rename SINGLE_THING_OR_DEFAULT to OPTIONAL_SINGLE_THING to better reflect its purpose * test: revert change to ThenTheResponseStatusCodeIs step definition * test: use GetBytes extension method to convert string to bytes * feat: when establishing a relationship, make sure the template is allocated by the active identity * chore: fix a nullability issue * refactor: expose RelationshipTemplate.CanBeCollectedWithPassword as simple method * test: use Convert.FromBase64String again * refactor: renaming and simplifcations * fix: change implementation of CanBeCollectedWithPassword2 to not compare arrays with equals operator * test: fix assertion * chore: rename scenario * test: remove redundant test case * test: state explicitly which relations templates are expected * test: tidy up * feat: update relationship template validators * chore: update method name * chore: rename method * chore: clean up message * chore: remove redundant method * feat: update sdk to allow for password protected templates * fix: add endpoint expected by transport tests * fix: add temporary endpoint to satisfy transport tests * feat: address pr change requests * fix: address issues causing tests to fail * chore: fix formatting * feat: catch and process json parsing exception * fix: revert manually edited snapshots * feat: add migration limiting the password length * test: limit the scope of resharper warning disabling * test: improve property names * refactor: rename RelationshipTemplateQuery to RelationshipTemplateQueryItem in SDK * refactor: don't set error message and code when using NumberOfBytes extension method * fix: allow camelCase in GetAll query string  Conflicts:  Modules/Relationships/src/Relationships.ConsumerApi/Controllers/RelationshipTemplatesController.cs * refactor: add curly braces for better readability * refactor: use generic application error in case the input cannot be parsed * fix: fix merge error * chore: delete redundant methods added by merge * chore: fix formatting * feat: revert to state before adding the password field * feat: add password with required length in a single migration * chore: remove comment * refactor: add migration "the correct way" * refactor: use new MAX_PASSWORD_LENGTH constant in ListRelationshipTemplatesQuery Validator * feat: validate password max length in GetRelationshipTemplateQuery * refactor: remove database configuration from db context * chore: fix formatting --------- Co-authored-by: Timo Notheisen Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .../IServiceCollectionExtensions.cs | 3 +- .../IServiceCollectionExtensions.cs | 3 +- .../RelationshipTemplates/GET.feature | 47 +++ .../RelationshipTemplates/POST.feature | 11 + .../RelationshipTemplates/{id}/GET.feature | 40 +-- .../Helpers/RegexFor.cs | 1 + .../RelationshipTemplatesStepDefinitions.cs | 114 +++++++- .../IServiceCollectionExtensions.cs | 3 +- .../Exceptions/GenericApplicationErrors.cs | 5 + .../NumberOfBytesValidator.cs | 2 +- .../ApplicationErrors.cs | 9 + .../IRelationshipTemplatesRepository.cs | 5 +- .../CreateRelationshipTemplateCommand.cs | 1 + .../CreateRelationshipTemplate/Handler.cs | 3 +- .../Validator.Tests.cs | 57 +++- .../CreateRelationshipTemplate/Validator.cs | 5 +- .../GetRelationshipTemplateQuery.cs | 1 + .../GetRelationshipTemplate/Handler.cs | 15 +- .../GetRelationshipTemplate/Validator.cs | 3 + .../ListRelationshipTemplates/Handler.cs | 3 +- .../ListRelationshipTemplatesQuery.cs | 12 +- ...sValidator.Tests.cs => Validator.Tests.cs} | 22 +- .../ListRelationshipTemplates/Validator.cs | 20 +- .../RelationshipTemplatesController.cs | 35 ++- .../RelationshipTemplate.cs | 35 ++- .../src/Relationships.Domain/DomainErrors.cs | 6 + ...PasswordToRelationshipTemplate.Designer.cs | 268 ++++++++++++++++++ ...81142_AddPasswordToRelationshipTemplate.cs | 31 ++ .../RelationshipsDbContextModelSnapshot.cs | 6 +- ...PasswordToRelationshipTemplate.Designer.cs | 266 +++++++++++++++++ ...81556_AddPasswordToRelationshipTemplate.cs | 31 ++ .../RelationshipsDbContextModelSnapshot.cs | 6 +- ...tionshipTemplateEntityTypeConfiguration.cs | 4 + .../RelationshipTemplatesRepository.cs | 22 +- .../RelationshipTemplatesEndpoint.cs | 23 +- .../CreateRelationshipTemplateRequest.cs | 1 + .../Requests/RelationshipTemplateQueryItem.cs | 7 + 37 files changed, 1035 insertions(+), 91 deletions(-) create mode 100644 Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/RelationshipTemplates/GET.feature rename Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/ListRelationshipTemplates/{ListRelationshipTemplatesValidator.Tests.cs => Validator.Tests.cs} (64%) create mode 100644 Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Migrations/20241011081142_AddPasswordToRelationshipTemplate.Designer.cs create mode 100644 Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Migrations/20241011081142_AddPasswordToRelationshipTemplate.cs create mode 100644 Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Migrations/20241011081556_AddPasswordToRelationshipTemplate.Designer.cs create mode 100644 Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Migrations/20241011081556_AddPasswordToRelationshipTemplate.cs create mode 100644 Sdks/ConsumerApi.Sdk/src/Endpoints/RelationshipTemplates/Types/Requests/RelationshipTemplateQueryItem.cs diff --git a/Applications/AdminApi/src/AdminApi/Extensions/IServiceCollectionExtensions.cs b/Applications/AdminApi/src/AdminApi/Extensions/IServiceCollectionExtensions.cs index 8fa8858208..2b9f41d97f 100644 --- a/Applications/AdminApi/src/AdminApi/Extensions/IServiceCollectionExtensions.cs +++ b/Applications/AdminApi/src/AdminApi/Extensions/IServiceCollectionExtensions.cs @@ -6,6 +6,7 @@ using Backbone.AdminApi.Infrastructure.DTOs; using Backbone.BuildingBlocks.API; using Backbone.BuildingBlocks.API.Mvc.ExceptionFilters; +using Backbone.BuildingBlocks.Application.Abstractions.Exceptions; using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext; using Backbone.Modules.Devices.Application.Devices.Commands.RegisterDevice; using Backbone.Modules.Devices.Application.Devices.DTOs; @@ -55,7 +56,7 @@ public static IServiceCollection AddCustomAspNetCore(this IServiceCollection ser : $"'{nameOfPropertyWithError}': {firstErrorMessage}"; context.HttpContext.Response.ContentType = "application/json"; var responsePayload = new HttpResponseEnvelopeError( - HttpError.ForProduction("error.platform.inputCannotBeParsed", formattedMessage, + HttpError.ForProduction(GenericApplicationErrors.Validation.InputCannotBeParsed().Code, formattedMessage, "")); // TODO: add docs return new BadRequestObjectResult(responsePayload); }; diff --git a/Applications/ConsumerApi/src/Extensions/IServiceCollectionExtensions.cs b/Applications/ConsumerApi/src/Extensions/IServiceCollectionExtensions.cs index 55305006ed..a336599673 100644 --- a/Applications/ConsumerApi/src/Extensions/IServiceCollectionExtensions.cs +++ b/Applications/ConsumerApi/src/Extensions/IServiceCollectionExtensions.cs @@ -4,6 +4,7 @@ using System.Text.Json.Serialization; using Backbone.BuildingBlocks.API; using Backbone.BuildingBlocks.API.Mvc.ExceptionFilters; +using Backbone.BuildingBlocks.Application.Abstractions.Exceptions; using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext; using Backbone.ConsumerApi.Configuration; using Backbone.Infrastructure.UserContext; @@ -46,7 +47,7 @@ public static IServiceCollection AddCustomAspNetCore(this IServiceCollection ser : $"'{nameOfPropertyWithError}': {firstErrorMessage}"; context.HttpContext.Response.ContentType = "application/json"; var responsePayload = new HttpResponseEnvelopeError( - HttpError.ForProduction("error.platform.inputCannotBeParsed", formattedMessage, + HttpError.ForProduction(GenericApplicationErrors.Validation.InputCannotBeParsed().Code, formattedMessage, "")); // TODO: add docs return new BadRequestObjectResult(responsePayload); }; diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/RelationshipTemplates/GET.feature b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/RelationshipTemplates/GET.feature new file mode 100644 index 0000000000..6f825a614b --- /dev/null +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/RelationshipTemplates/GET.feature @@ -0,0 +1,47 @@ +@Integration +Feature: GET /RelationshipTemplates + +User requests Relationship Templates + + Scenario Outline: Requesting a list of Relationship Templates in a variety of scenarios + Given Identities i1, i2, i3 and i4 + And the following Relationship Templates + | templateName | templateOwner | forIdentity | password | + | rt1 | i1 | - | - | + | rt2 | i2 | - | - | + | rt3 | i1 | - | - | + | rt4 | i2 | - | - | + | rt5 | i1 | - | password | + | rt6 | i1 | - | password | + | rt7 | i2 | - | password | + | rt8 | i2 | - | password | + | rt9 | i1 | i1 | - | + | rt10 | i2 | i3 | - | + | rt11 | i2 | i2 | - | + | rt12 | i2 | i3 | - | + | rt13 | i2 | i3 | password | + | rt14 | i2 | i3 | password | + When sends a GET request to the /RelationshipTemplate endpoint with the following payloads + | templateName | passwordOnGet | + | rt1 | - | + | rt2 | - | + | rt3 | password | + | rt4 | password | + | rt5 | password | + | rt6 | - | + | rt7 | password | + | rt8 | - | + | rt9 | - | + | rt10 | - | + | rt11 | - | + | rt12 | - | + | rt13 | password | + | rt14 | wordpass | + Then the response contains Relationship Template(s) + + Examples: + | activeIdentity | retreivedTemplates | + | i1 | rt1, rt2, rt3, rt4, rt5, rt6, rt7, rt9 | + | i2 | rt1, rt2, rt3, rt4, rt5, rt7, rt8, rt10, rt11, rt12, rt13, rt14 | + | i3 | rt1, rt2, rt3, rt4, rt5, rt7, rt10, rt12, rt13 | + | i4 | rt1, rt2, rt3, rt4, rt5, rt7 | diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/RelationshipTemplates/POST.feature b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/RelationshipTemplates/POST.feature index c569a6c8b6..02b8277a45 100644 --- a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/RelationshipTemplates/POST.feature +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/RelationshipTemplates/POST.feature @@ -8,3 +8,14 @@ User creates a Relationship Template When i sends a POST request to the /RelationshipTemplates endpoint Then the response status code is 201 (Created) And the response contains a RelationshipMetadata + + Scenario: Creating a relationship template with a password + Given Identity i + When i sends a POST request to the /RelationshipTemplates endpoint with the password "my-password" + Then the response status code is 201 (Created) + And the response contains a RelationshipMetadata + + Scenario: Create a personalized Relationship Template with a password + Given Identities i1 and i2 + When i1 sends a POST request to the /RelationshipTemplate endpoint with password "my-password" and forIdentity i2 + Then the response status code is 201 (Created) diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/RelationshipTemplates/{id}/GET.feature b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/RelationshipTemplates/{id}/GET.feature index cb51d48565..4bfdea34f0 100644 --- a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/RelationshipTemplates/{id}/GET.feature +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/RelationshipTemplates/{id}/GET.feature @@ -3,22 +3,26 @@ Feature: GET /RelationshipTemplates/{id} User requests a Relationship Template - Scenario: Requesting a Relationship Template with ForIdentity created for me - Given Identities i1 and i2 - And Relationship Template rt created by i1 where ForIdentity is the address of i2 - When i2 sends a GET request to the /RelationshipTemplates/{id} endpoint with rt.Id - Then the response status code is 200 (Ok) - And the response contains a rt + Scenario Outline: Requesting a Relationship Template in a variety of scenarios + Given Identities + And Relationship Template rt created by with password "" and forIdentity + When sends a GET request to the /RelationshipTemplate/rt.Id endpoint with password "" + Then the response status code is - Scenario: Requesting a Relationship Template with ForIdentity created by me - Given Identities i1 and i2 - And Relationship Template rt created by i1 where ForIdentity is the address of i2 - When i1 sends a GET request to the /RelationshipTemplates/{id} endpoint with rt.Id - Then the response status code is 200 (Ok) - And the response contains a rt - - Scenario: Requesting a Relationship Template with ForIdentity created for someone else - Given Identities i1, i2 and i3 - And Relationship Template rt created by i1 where ForIdentity is the address of i2 - When i3 sends a GET request to the /RelationshipTemplates/{id} endpoint with rt.Id - Then the response status code is 404 (Not Found) + Examples: + | givenIdentities | templateOwner | forIdentity | password | activeIdentity | passwordOnGet | responseStatusCode | description | + | i | i | - | - | i | - | 200 (OK) | owner tries to get | + | i1 and i2 | i1 | - | - | i2 | - | 200 (OK) | non-owner tries to get | + | i | i | - | - | i | password | 200 (OK) | owner passes password even though none is set | + | i1 and i2 | i1 | - | - | i2 | password | 200 (OK) | non-owner identity passes password even though none is set | + | i | i | - | password | i | password | 200 (OK) | owner passes correct password | + | i | i | - | password | i | - | 200 (OK) | owner doesn't pass password, even though one is set | + | i1 and i2 | i1 | - | password | i2 | password | 200 (OK) | non-owner identity passes correct password | + | i1 and i2 | i1 | - | password | i2 | - | 404 (Not Found) | non-owner identity passes no password even though one is set | + | i | i | i | - | i | - | 200 (OK) | owner is forIdentity and tries to get | + | i1 and i2 | i1 | i2 | - | i1 | - | 200 (OK) | non-owner is forIdentity, creator tries to get | + | i1 and i2 | i1 | i1 | - | i2 | - | 404 (Not Found) | owner is forIdentity and non-owner tries to get | + | i1 and i2 | i1 | i2 | - | i2 | - | 200 (OK) | non-owner is forIdentity and tries to get | + | i1 and i2 | i1 | i2 | password | i2 | password | 200 (OK) | non-owner is forIdentity and tries to get with correct password | + | i1 and i2 | i1 | i2 | password | i2 | wordpass | 404 (Not Found) | non-owner is forIdentity and tries to get with incorrect password | + | i1, i2 and i3 | i1 | i2 | password | i3 | password | 404 (Not Found) | non-owner is forIdentity, and thirdParty tries to get | diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Helpers/RegexFor.cs b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Helpers/RegexFor.cs index 7befe8a431..4e13e6b09e 100644 --- a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Helpers/RegexFor.cs +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Helpers/RegexFor.cs @@ -3,5 +3,6 @@ public static class RegexFor { public const string SINGLE_THING = "([a-zA-Z0-9]+)"; + public const string OPTIONAL_SINGLE_THING = "([a-zA-Z0-9-]+)"; public const string LIST_OF_THINGS = "([a-zA-Z0-9, ]+)"; } diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/RelationshipTemplatesStepDefinitions.cs b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/RelationshipTemplatesStepDefinitions.cs index a9913a2d0d..a1f37d92f7 100644 --- a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/RelationshipTemplatesStepDefinitions.cs +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/RelationshipTemplatesStepDefinitions.cs @@ -1,6 +1,11 @@ -using Backbone.ConsumerApi.Sdk.Endpoints.RelationshipTemplates.Types.Requests; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types; +using Backbone.ConsumerApi.Sdk.Endpoints.RelationshipTemplates.Types.Requests; +using Backbone.ConsumerApi.Sdk.Endpoints.RelationshipTemplates.Types.Responses; using Backbone.ConsumerApi.Tests.Integration.Contexts; using Backbone.ConsumerApi.Tests.Integration.Helpers; +using TechTalk.SpecFlow.Assist; namespace Backbone.ConsumerApi.Tests.Integration.StepDefinitions; @@ -13,6 +18,8 @@ internal class RelationshipTemplatesStepDefinitions private readonly RelationshipTemplatesContext _relationshipTemplatesContext; private readonly ClientPool _clientPool; + private ApiResponse? _listRelationshipTemplatesResponse; + public RelationshipTemplatesStepDefinitions(ResponseContext responseContext, RelationshipTemplatesContext relationshipTemplatesContext, ClientPool clientPool) { _responseContext = responseContext; @@ -24,18 +31,37 @@ public RelationshipTemplatesStepDefinitions(ResponseContext responseContext, Rel #region Given - [Given($"Relationship Template {RegexFor.SINGLE_THING} created by {RegexFor.SINGLE_THING} where ForIdentity is the address of {RegexFor.SINGLE_THING}")] - public async Task GivenRelationshipTemplateCreatedByIWhereForIdentityIsTheAddressOfI(string relationshipTemplateName, string identityName, string forIdentityName) + [Given($@"Relationship Template {RegexFor.SINGLE_THING} created by {RegexFor.SINGLE_THING} with password ""([^""]*)"" and forIdentity {RegexFor.OPTIONAL_SINGLE_THING}")] + public async Task GivenRelationshipTemplateCreatedByTokenOwnerWithPasswordAndForIdentity(string relationshipTemplateName, string identityName, string passwordString, string forIdentityName) { var client = _clientPool.FirstForIdentityName(identityName); - var forClient = _clientPool.FirstForIdentityName(forIdentityName); + var forClient = forIdentityName != "-" ? _clientPool.FirstForIdentityName(forIdentityName).IdentityData!.Address : null; + var password = passwordString.Trim() != "-" ? Convert.FromBase64String(passwordString.Trim()) : null; var response = await client.RelationshipTemplates - .CreateTemplate(new CreateRelationshipTemplateRequest { Content = TestData.SOME_BYTES, ForIdentity = forClient.IdentityData!.Address }); + .CreateTemplate(new CreateRelationshipTemplateRequest { Content = TestData.SOME_BYTES, ForIdentity = forClient, Password = password }); _relationshipTemplatesContext.CreateRelationshipTemplatesResponses[relationshipTemplateName] = response.Result!; } + [Given(@"the following Relationship Templates")] + public async Task GivenRelationshipTemplatesWithTheFollowingProperties(Table table) + { + var relationshipTemplatePropertiesSet = table.CreateSet(); + + foreach (var relationshipTemplateProperties in relationshipTemplatePropertiesSet) + { + var client = _clientPool.FirstForIdentityName(relationshipTemplateProperties.TemplateOwner); + var forClient = relationshipTemplateProperties.ForIdentity != "-" ? _clientPool.FirstForIdentityName(relationshipTemplateProperties.ForIdentity).IdentityData!.Address : null; + var password = relationshipTemplateProperties.Password.Trim() != "-" ? Convert.FromBase64String(relationshipTemplateProperties.Password.Trim()) : null; + + var response = await client.RelationshipTemplates + .CreateTemplate(new CreateRelationshipTemplateRequest { Content = TestData.SOME_BYTES, ForIdentity = forClient, Password = password }); + + _relationshipTemplatesContext.CreateRelationshipTemplatesResponses[relationshipTemplateProperties.TemplateName] = response.Result!; + } + } + #endregion #region When @@ -45,19 +71,89 @@ public async Task WhenIdentitySendsAPostRequestToTheRelationshipTemplatesEndpoin { var client = _clientPool.FirstForIdentityName(identityName); - _responseContext.WhenResponse = await client.RelationshipTemplates.CreateTemplate(new CreateRelationshipTemplateRequest { Content = TestData.SOME_BYTES }); + _responseContext.WhenResponse = await client.RelationshipTemplates.CreateTemplate( + new CreateRelationshipTemplateRequest { Content = TestData.SOME_BYTES }); + } + + [When($@"{RegexFor.SINGLE_THING} sends a POST request to the /RelationshipTemplates endpoint with the password ""(.*)""")] + public async Task WhenIdentitySendsAPostRequestToTheRelationshipTemplatesEndpointWithThePassword(string identityName, string passwordString) + { + var client = _clientPool.FirstForIdentityName(identityName); + var password = Encoding.UTF8.GetBytes(passwordString); + + _responseContext.WhenResponse = await client.RelationshipTemplates.CreateTemplate( + new CreateRelationshipTemplateRequest { Content = TestData.SOME_BYTES, Password = password }); + } + + [When($@"{RegexFor.SINGLE_THING} sends a POST request to the /RelationshipTemplate endpoint with password ""(.*)"" and forIdentity {RegexFor.SINGLE_THING}")] + public async Task WhenIdentitySendsAPostRequestToTheRelationshipTemplatesEndpointWithPasswordAndForIdentity(string identityName, string passwordString, string forIdentityName) + { + var client = _clientPool.FirstForIdentityName(identityName); + var forClient = _clientPool.FirstForIdentityName(forIdentityName); + var password = Encoding.UTF8.GetBytes(passwordString); + + _responseContext.WhenResponse = await client.RelationshipTemplates.CreateTemplate( + new CreateRelationshipTemplateRequest { Content = TestData.SOME_BYTES, ForIdentity = forClient.IdentityData!.Address, Password = password }); } - [When($"{RegexFor.SINGLE_THING} sends a GET request to the /RelationshipTemplates/{{id}} endpoint with {RegexFor.SINGLE_THING}.Id")] - public async Task WhenISendsAGetRequestToTheRelationshipTemplatesIdEndpointWithId(string identityName, string relationshipTemplateName) + [When($@"{RegexFor.SINGLE_THING} sends a GET request to the /RelationshipTemplate/{RegexFor.SINGLE_THING}.Id endpoint with password ""([^""]*)""")] + public async Task WhenIdentitySendsAGetRequestToTheRelationshipTemplatesIdEndpointWithPassword(string identityName, string relationshipTemplateName, string password) { var client = _clientPool.FirstForIdentityName(identityName); var relationshipTemplateId = _relationshipTemplatesContext.CreateRelationshipTemplatesResponses[relationshipTemplateName].Id; - _responseContext.WhenResponse = await client.RelationshipTemplates.GetTemplate(relationshipTemplateId); + _responseContext.WhenResponse = password != "-" + ? await client.RelationshipTemplates.GetTemplate(relationshipTemplateId, Convert.FromBase64String(password.Trim())) + : await client.RelationshipTemplates.GetTemplate(relationshipTemplateId); } + [When($@"{RegexFor.SINGLE_THING} sends a GET request to the /RelationshipTemplate endpoint with the following payloads")] + public async Task WhenISendsAGETRequestToTheRelationshipTemplateEndpointWithTheFollowingPayloads(string identityName, Table table) + { + var client = _clientPool.FirstForIdentityName(identityName); + + var getRequestPayloadSet = table.CreateSet(); + + var queryItems = getRequestPayloadSet.Select(payload => + { + var relationshipTemplateId = _relationshipTemplatesContext.CreateRelationshipTemplatesResponses[payload.TemplateName].Id; + var password = payload.PasswordOnGet == "-" ? null : Convert.FromBase64String(payload.PasswordOnGet.Trim()); + + return new RelationshipTemplateQueryItem { Id = relationshipTemplateId, Password = password }; + }).ToList(); + + _responseContext.WhenResponse = _listRelationshipTemplatesResponse = await client.RelationshipTemplates.ListTemplates(queryItems); + } #endregion + + #region Then + + [Then($@"the response contains Relationship Template\(s\) {RegexFor.LIST_OF_THINGS}")] + public void ThenTheResponseContainsRelationshipTemplates(string relationshipTemplateNames) + { + var relationshipTemplates = relationshipTemplateNames.Split(',').Select(item => _relationshipTemplatesContext.CreateRelationshipTemplatesResponses[item.Trim()]).ToList(); + _listRelationshipTemplatesResponse!.Result!.Should().BeEquivalentTo(relationshipTemplates, options => options.WithStrictOrdering()); + } + + #endregion +} + +// ReSharper disable once ClassNeverInstantiated.Local +[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] +file class RelationshipTemplateProperties +{ + public required string TemplateName { get; set; } + public required string TemplateOwner { get; set; } + public required string ForIdentity { get; set; } + public required string Password { get; set; } +} + +// ReSharper disable once ClassNeverInstantiated.Local +[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] +file class GetRequestPayload +{ + public required string TemplateName { get; set; } + public required string PasswordOnGet { get; set; } } diff --git a/Applications/SseServer/src/SseServer/Extensions/IServiceCollectionExtensions.cs b/Applications/SseServer/src/SseServer/Extensions/IServiceCollectionExtensions.cs index a1e466091c..031ed467de 100644 --- a/Applications/SseServer/src/SseServer/Extensions/IServiceCollectionExtensions.cs +++ b/Applications/SseServer/src/SseServer/Extensions/IServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using System.Text.Json.Serialization; using Backbone.BuildingBlocks.API; using Backbone.BuildingBlocks.API.Mvc.ExceptionFilters; +using Backbone.BuildingBlocks.Application.Abstractions.Exceptions; using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext; using Backbone.Infrastructure.UserContext; using Backbone.Modules.Devices.Application.Devices.Commands.RegisterDevice; @@ -38,7 +39,7 @@ public static void AddCustomAspNetCore(this IServiceCollection services, : $"'{nameOfPropertyWithError}': {firstErrorMessage}"; context.HttpContext.Response.ContentType = "application/json"; var responsePayload = new HttpResponseEnvelopeError( - HttpError.ForProduction("error.platform.inputCannotBeParsed", formattedMessage, + HttpError.ForProduction(GenericApplicationErrors.Validation.InputCannotBeParsed().Code, formattedMessage, "")); // TODO: add docs return new BadRequestObjectResult(responsePayload); }; diff --git a/BuildingBlocks/src/BuildingBlocks.Application.Abstractions/Exceptions/GenericApplicationErrors.cs b/BuildingBlocks/src/BuildingBlocks.Application.Abstractions/Exceptions/GenericApplicationErrors.cs index 5f4c66cbb1..9c49f9ddc4 100644 --- a/BuildingBlocks/src/BuildingBlocks.Application.Abstractions/Exceptions/GenericApplicationErrors.cs +++ b/BuildingBlocks/src/BuildingBlocks.Application.Abstractions/Exceptions/GenericApplicationErrors.cs @@ -47,5 +47,10 @@ public static ApplicationError InvalidPageSize(int? maxPageSize = null) return new ApplicationError("error.platform.validation.pagination.invalidPageSize", message); } + + public static ApplicationError InputCannotBeParsed(string reason = "The input cannot be parsed.") + { + return new ApplicationError("error.platform.inputCannotBeParsed", reason); + } } } diff --git a/BuildingBlocks/src/BuildingBlocks.Application/FluentValidation/NumberOfBytesValidator.cs b/BuildingBlocks/src/BuildingBlocks.Application/FluentValidation/NumberOfBytesValidator.cs index 7c4e3c23b6..44a32e8861 100644 --- a/BuildingBlocks/src/BuildingBlocks.Application/FluentValidation/NumberOfBytesValidator.cs +++ b/BuildingBlocks/src/BuildingBlocks.Application/FluentValidation/NumberOfBytesValidator.cs @@ -59,7 +59,7 @@ public static class NumberOfBytesValidatorRuleBuilderExtensions .WithErrorCode(GenericApplicationErrors.Validation.InvalidPropertyValue().Code); } - public static IRuleBuilderOptions NumberOfBytes(this IRuleBuilder ruleBuilder, + public static IRuleBuilderOptions NumberOfBytes(this IRuleBuilder ruleBuilder, int numberOfBytes) { return ruleBuilder diff --git a/Modules/Relationships/src/Relationships.Application/ApplicationErrors.cs b/Modules/Relationships/src/Relationships.Application/ApplicationErrors.cs index 808b4d20f3..9782c0b896 100644 --- a/Modules/Relationships/src/Relationships.Application/ApplicationErrors.cs +++ b/Modules/Relationships/src/Relationships.Application/ApplicationErrors.cs @@ -12,4 +12,13 @@ public static ApplicationError PeerIsToBeDeleted() "Cannot establish relationship with the owner of the template because the owner is in status 'ToBeDeleted'."); } } + + public static class RelationshipTemplate + { + public static ApplicationError InvalidRelationshipTemplateQueryItem() + { + return new ApplicationError("error.platform.validation.relationship.invalidRelationshipTemplateQueryItem", + "RelationshipTemplateQueryItem object is expected to have Id property containing the RelationshipTemplateId and optionally a Password property."); + } + } } diff --git a/Modules/Relationships/src/Relationships.Application/Infrastructure/Persistence/Repository/IRelationshipTemplatesRepository.cs b/Modules/Relationships/src/Relationships.Application/Infrastructure/Persistence/Repository/IRelationshipTemplatesRepository.cs index 5feaab0daa..2628ab72bd 100644 --- a/Modules/Relationships/src/Relationships.Application/Infrastructure/Persistence/Repository/IRelationshipTemplatesRepository.cs +++ b/Modules/Relationships/src/Relationships.Application/Infrastructure/Persistence/Repository/IRelationshipTemplatesRepository.cs @@ -2,14 +2,16 @@ using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.Persistence.Database; using Backbone.BuildingBlocks.Application.Pagination; using Backbone.DevelopmentKit.Identity.ValueObjects; +using Backbone.Modules.Relationships.Application.RelationshipTemplates.Queries.ListRelationshipTemplates; using Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates; namespace Backbone.Modules.Relationships.Application.Infrastructure.Persistence.Repository; public interface IRelationshipTemplatesRepository { - Task> FindTemplatesWithIds(IEnumerable ids, IdentityAddress identityAddress, PaginationFilter paginationFilter, + Task> FindTemplatesWithIds(IEnumerable queryItems, IdentityAddress activeIdentity, PaginationFilter paginationFilter, CancellationToken cancellationToken, bool track = false); + Task Find(RelationshipTemplateId id, IdentityAddress identityAddress, CancellationToken cancellationToken, bool track = false); Task Add(RelationshipTemplate template, CancellationToken cancellationToken); Task Update(RelationshipTemplate template); @@ -21,5 +23,4 @@ Task> FindTemplatesWithIds(IEnumerable< Task UpdateRelationshipTemplateAllocations(List templateAllocations, CancellationToken cancellationToken); #endregion RelationshipTemplateAllocations - } diff --git a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/CreateRelationshipTemplate/CreateRelationshipTemplateCommand.cs b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/CreateRelationshipTemplate/CreateRelationshipTemplateCommand.cs index 35af51593b..9d49648091 100644 --- a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/CreateRelationshipTemplate/CreateRelationshipTemplateCommand.cs +++ b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/CreateRelationshipTemplate/CreateRelationshipTemplateCommand.cs @@ -10,4 +10,5 @@ public class CreateRelationshipTemplateCommand : IRequest Handle(CreateRelationshipT request.MaxNumberOfAllocations, request.ExpiresAt, request.Content, - forIdentity); + forIdentity, + request.Password); await _relationshipTemplatesRepository.Add(template, cancellationToken); diff --git a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/CreateRelationshipTemplate/Validator.Tests.cs b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/CreateRelationshipTemplate/Validator.Tests.cs index 444e99b191..e95c925910 100644 --- a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/CreateRelationshipTemplate/Validator.Tests.cs +++ b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/CreateRelationshipTemplate/Validator.Tests.cs @@ -5,6 +5,7 @@ using Xunit; namespace Backbone.Modules.Relationships.Application.RelationshipTemplates.Commands.CreateRelationshipTemplate; + public class ValidatorTests : AbstractTestsBase { [Fact] @@ -15,7 +16,13 @@ public void Happy_Path_with_optional_parameters() // Act var validationResult = validator.TestValidate( - new CreateRelationshipTemplateCommand { ExpiresAt = DateTime.UtcNow.AddDays(1), MaxNumberOfAllocations = 1, Content = [1], ForIdentity = TestDataGenerator.CreateRandomIdentityAddress() }); + new CreateRelationshipTemplateCommand + { + ExpiresAt = DateTime.UtcNow.AddDays(1), + MaxNumberOfAllocations = 1, + Content = [1], + ForIdentity = TestDataGenerator.CreateRandomIdentityAddress(), Password = [1] + }); // Assert validationResult.ShouldNotHaveAnyValidationErrors(); @@ -29,7 +36,10 @@ public void Happy_Path_without_optional_parameters() // Act var validationResult = validator.TestValidate( - new CreateRelationshipTemplateCommand { Content = [1] }); + new CreateRelationshipTemplateCommand + { + Content = [1] + }); // Assert validationResult.ShouldNotHaveAnyValidationErrors(); @@ -43,7 +53,11 @@ public void Fails_when_ExpiresAt_is_invalid() // Act var validationResult = validator.TestValidate( - new CreateRelationshipTemplateCommand() { ExpiresAt = DateTime.UtcNow.AddDays(-1), Content = [1] }); + new CreateRelationshipTemplateCommand + { + ExpiresAt = DateTime.UtcNow.AddDays(-1), + Content = [1] + }); // Assert validationResult.ShouldHaveValidationErrorForItem(nameof(CreateRelationshipTemplateCommand.ExpiresAt), "error.platform.validation.invalidPropertyValue", "'Expires At' must be in the future."); @@ -57,10 +71,15 @@ public void Fails_when_MaxNumberOfAllocations_is_invalid() // Act var validationResult = validator.TestValidate( - new CreateRelationshipTemplateCommand() { MaxNumberOfAllocations = 0, Content = [1] }); + new CreateRelationshipTemplateCommand + { + MaxNumberOfAllocations = 0, + Content = [1] + }); // Assert - validationResult.ShouldHaveValidationErrorForItem(nameof(CreateRelationshipTemplateCommand.MaxNumberOfAllocations), "error.platform.validation.invalidPropertyValue", "'Max Number Of Allocations' must be greater than '0'."); + validationResult.ShouldHaveValidationErrorForItem(nameof(CreateRelationshipTemplateCommand.MaxNumberOfAllocations), "error.platform.validation.invalidPropertyValue", + "'Max Number Of Allocations' must be greater than '0'."); } [Fact] @@ -71,9 +90,35 @@ public void Fails_when_ForIdentity_is_invalid() // Act var validationResult = validator.TestValidate( - new CreateRelationshipTemplateCommand() { Content = [1], ForIdentity = "some-address" }); + new CreateRelationshipTemplateCommand + { + Content = [1], + ForIdentity = "some-address" + }); // Assert validationResult.ShouldHaveValidationErrorForId(nameof(CreateRelationshipTemplateCommand.ForIdentity)); } + + [Fact] + public void Fails_when_Password_is_too_long() + { + // Arrange + var validator = new Validator(); + + var password = new byte[250]; + new Random().NextBytes(password); + + // Act + var validationResult = validator.TestValidate( + new CreateRelationshipTemplateCommand + { + Content = [1], + Password = password + }); + + // Assert + validationResult.ShouldHaveValidationErrorForItem(nameof(CreateRelationshipTemplateCommand.Password), "error.platform.validation.invalidPropertyValue", + "'Password' must be between 0 and 200 bytes long. You entered 250 bytes."); + } } diff --git a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/CreateRelationshipTemplate/Validator.cs b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/CreateRelationshipTemplate/Validator.cs index 55930ec1ba..c8b3bb896c 100644 --- a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/CreateRelationshipTemplate/Validator.cs +++ b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Commands/CreateRelationshipTemplate/Validator.cs @@ -18,10 +18,13 @@ public Validator() .GreaterThan(0).WithErrorCode(GenericApplicationErrors.Validation.InvalidPropertyValue().Code).When(c => c.MaxNumberOfAllocations != null); RuleFor(c => c.ExpiresAt) - .GreaterThan(SystemTime.UtcNow).WithMessage("'{PropertyName}' must be in the future.").WithErrorCode(GenericApplicationErrors.Validation.InvalidPropertyValue().Code).When(c => c.ExpiresAt != null); + .GreaterThan(SystemTime.UtcNow).WithMessage("'{PropertyName}' must be in the future.").WithErrorCode(GenericApplicationErrors.Validation.InvalidPropertyValue().Code) + .When(c => c.ExpiresAt != null); RuleFor(c => c.ForIdentity) .ValidId() .When(c => c.ForIdentity != null); + + RuleFor(c => c.Password).NumberOfBytes(0, 200); } } diff --git a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/GetRelationshipTemplate/GetRelationshipTemplateQuery.cs b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/GetRelationshipTemplate/GetRelationshipTemplateQuery.cs index 53be7337a2..113b0438d6 100644 --- a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/GetRelationshipTemplate/GetRelationshipTemplateQuery.cs +++ b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/GetRelationshipTemplate/GetRelationshipTemplateQuery.cs @@ -6,4 +6,5 @@ namespace Backbone.Modules.Relationships.Application.RelationshipTemplates.Queri public class GetRelationshipTemplateQuery : IRequest { public required string Id { get; set; } + public byte[]? Password { get; set; } } diff --git a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/GetRelationshipTemplate/Handler.cs b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/GetRelationshipTemplate/Handler.cs index 454305f741..f2e9269597 100644 --- a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/GetRelationshipTemplate/Handler.cs +++ b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/GetRelationshipTemplate/Handler.cs @@ -20,13 +20,22 @@ public Handler(IUserContext userContext, IRelationshipTemplatesRepository relati public async Task Handle(GetRelationshipTemplateQuery request, CancellationToken cancellationToken) { - var template = await _relationshipTemplatesRepository.Find(RelationshipTemplateId.Parse(request.Id), _userContext.GetAddress(), cancellationToken, true) ?? - throw new NotFoundException(nameof(RelationshipTemplate)); + var template = await GetRelationshipTemplate(request.Id, request.Password, cancellationToken); template.AllocateFor(_userContext.GetAddress(), _userContext.GetDeviceId()); - await _relationshipTemplatesRepository.Update(template); return new RelationshipTemplateDTO(template); } + + private async Task GetRelationshipTemplate(string relationshipTemplateId, byte[]? password, CancellationToken cancellationToken) + { + var relationshipTemplate = await _relationshipTemplatesRepository.Find(RelationshipTemplateId.Parse(relationshipTemplateId), _userContext.GetAddress(), cancellationToken, true) ?? + throw new NotFoundException(nameof(RelationshipTemplate)); + + if (!relationshipTemplate.CanBeCollectedUsingPassword(_userContext.GetAddress(), password)) + throw new NotFoundException(nameof(RelationshipTemplate)); + + return relationshipTemplate; + } } diff --git a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/GetRelationshipTemplate/Validator.cs b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/GetRelationshipTemplate/Validator.cs index 55115a2b92..b5643fa6d0 100644 --- a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/GetRelationshipTemplate/Validator.cs +++ b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/GetRelationshipTemplate/Validator.cs @@ -1,4 +1,5 @@ using Backbone.BuildingBlocks.Application.Extensions; +using Backbone.BuildingBlocks.Application.FluentValidation; using Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates; using FluentValidation; @@ -9,5 +10,7 @@ public class Validator : AbstractValidator public Validator() { RuleFor(x => x.Id).ValidId(); + + RuleFor(x => x.Password).NumberOfBytes(1, RelationshipTemplate.MAX_PASSWORD_LENGTH).When(x => x.Password != null); } } diff --git a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/ListRelationshipTemplates/Handler.cs b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/ListRelationshipTemplates/Handler.cs index bf103bc64c..dfe1db7947 100644 --- a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/ListRelationshipTemplates/Handler.cs +++ b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/ListRelationshipTemplates/Handler.cs @@ -1,6 +1,5 @@ using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext; using Backbone.Modules.Relationships.Application.Infrastructure.Persistence.Repository; -using Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates; using MediatR; namespace Backbone.Modules.Relationships.Application.RelationshipTemplates.Queries.ListRelationshipTemplates; @@ -18,7 +17,7 @@ public Handler(IUserContext userContext, IRelationshipTemplatesRepository relati public async Task Handle(ListRelationshipTemplatesQuery request, CancellationToken cancellationToken) { - var dbPaginationResult = await _relationshipTemplatesRepository.FindTemplatesWithIds(request.Ids.Select(RelationshipTemplateId.Parse), _userContext.GetAddress(), request.PaginationFilter, + var dbPaginationResult = await _relationshipTemplatesRepository.FindTemplatesWithIds(request.QueryItems, _userContext.GetAddress(), request.PaginationFilter, cancellationToken, track: false); return new ListRelationshipTemplatesResponse(dbPaginationResult, request.PaginationFilter); diff --git a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/ListRelationshipTemplates/ListRelationshipTemplatesQuery.cs b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/ListRelationshipTemplates/ListRelationshipTemplatesQuery.cs index ecaa4ba976..da2db692e4 100644 --- a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/ListRelationshipTemplates/ListRelationshipTemplatesQuery.cs +++ b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/ListRelationshipTemplates/ListRelationshipTemplatesQuery.cs @@ -5,12 +5,18 @@ namespace Backbone.Modules.Relationships.Application.RelationshipTemplates.Queri public class ListRelationshipTemplatesQuery : IRequest { - public ListRelationshipTemplatesQuery(PaginationFilter paginationFilter, IEnumerable? ids) + public ListRelationshipTemplatesQuery(PaginationFilter paginationFilter, IEnumerable? queries) { PaginationFilter = paginationFilter; - Ids = ids == null ? [] : ids.ToList(); + QueryItems = queries == null ? [] : queries.ToList(); } public PaginationFilter PaginationFilter { get; set; } - public List Ids { get; set; } + public List QueryItems { get; set; } +} + +public class RelationshipTemplateQueryItem +{ + public required string Id { get; set; } + public byte[]? Password { get; set; } } diff --git a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/ListRelationshipTemplates/ListRelationshipTemplatesValidator.Tests.cs b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/ListRelationshipTemplates/Validator.Tests.cs similarity index 64% rename from Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/ListRelationshipTemplates/ListRelationshipTemplatesValidator.Tests.cs rename to Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/ListRelationshipTemplates/Validator.Tests.cs index b8ab48d109..c20521b2fa 100644 --- a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/ListRelationshipTemplates/ListRelationshipTemplatesValidator.Tests.cs +++ b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/ListRelationshipTemplates/Validator.Tests.cs @@ -3,43 +3,41 @@ using Backbone.UnitTestTools.BaseClasses; using Backbone.UnitTestTools.FluentValidation; using FluentValidation.TestHelper; +using Microsoft.EntityFrameworkCore.Metadata.Internal; using Xunit; namespace Backbone.Modules.Relationships.Application.RelationshipTemplates.Queries.ListRelationshipTemplates; -public class ListRelationshipTemplatesValidatorTests : AbstractTestsBase +public class ValidatorTests : AbstractTestsBase { [Fact] - public void Happy_path() + public void Happy_path_with_password() { // Arrange var validator = new Validator(); // Act - var validationResult = validator.TestValidate(new ListRelationshipTemplatesQuery(new PaginationFilter(), [RelationshipTemplateId.New()])); + var validationResult = validator.TestValidate(new ListRelationshipTemplatesQuery(new PaginationFilter(), new[] { new RelationshipTemplateQueryItem() { Id = RelationshipTemplateId.New(), Password = [1, 2, 3] } })); // Assert validationResult.ShouldNotHaveAnyValidationErrors(); } [Fact] - public void Fails_when_Ids_is_null() + public void Happy_path_without_password() { // Arrange var validator = new Validator(); // Act - var validationResult = validator.TestValidate(new ListRelationshipTemplatesQuery(new PaginationFilter(), null)); + var validationResult = validator.TestValidate(new ListRelationshipTemplatesQuery(new PaginationFilter(), new[] { new RelationshipTemplateQueryItem() { Id = RelationshipTemplateId.New() } })); // Assert - validationResult.ShouldHaveValidationErrorForItem( - propertyName: nameof(ListRelationshipTemplatesQuery.Ids), - expectedErrorCode: "error.platform.validation.invalidPropertyValue", - expectedErrorMessage: "'Ids' must not be empty."); + validationResult.ShouldNotHaveAnyValidationErrors(); } [Fact] - public void Fails_when_Ids_is_empty() + public void Fails_when_Queries_is_empty() { // Arrange var validator = new Validator(); @@ -49,8 +47,8 @@ public void Fails_when_Ids_is_empty() // Assert validationResult.ShouldHaveValidationErrorForItem( - propertyName: nameof(ListRelationshipTemplatesQuery.Ids), + propertyName: nameof(ListRelationshipTemplatesQuery.QueryItems), expectedErrorCode: "error.platform.validation.invalidPropertyValue", - expectedErrorMessage: "'Ids' must not be empty."); + expectedErrorMessage: "'Query Items' must not be empty."); } } diff --git a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/ListRelationshipTemplates/Validator.cs b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/ListRelationshipTemplates/Validator.cs index ab13585141..3ac120caed 100644 --- a/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/ListRelationshipTemplates/Validator.cs +++ b/Modules/Relationships/src/Relationships.Application/RelationshipTemplates/Queries/ListRelationshipTemplates/Validator.cs @@ -1,4 +1,3 @@ -using Backbone.BuildingBlocks.Application.Abstractions.Exceptions; using Backbone.BuildingBlocks.Application.Extensions; using Backbone.BuildingBlocks.Application.FluentValidation; using Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates; @@ -11,11 +10,22 @@ public class Validator : AbstractValidator { public Validator() { - RuleFor(q => q.Ids) + RuleFor(q => q.QueryItems) .Cascade(CascadeMode.Stop) - .DetailedNotNull() - .Must(ids => ids.Count > 0).WithErrorCode(GenericApplicationErrors.Validation.InvalidPropertyValue().Code).WithMessage("'Ids' must not be empty."); + .DetailedNotEmpty(); - RuleForEach(x => x.Ids).ValidId(); + RuleForEach(x => x.QueryItems) + .Cascade(CascadeMode.Stop) + .ChildRules(queryItems => + { + queryItems + .RuleFor(query => query.Id) + .ValidId(); + + queryItems + .RuleFor(query => query.Password) + .NumberOfBytes(1, RelationshipTemplate.MAX_PASSWORD_LENGTH) + .When(query => query.Password != null); + }); } } diff --git a/Modules/Relationships/src/Relationships.ConsumerApi/Controllers/RelationshipTemplatesController.cs b/Modules/Relationships/src/Relationships.ConsumerApi/Controllers/RelationshipTemplatesController.cs index 590713fc49..1b1d304fbd 100644 --- a/Modules/Relationships/src/Relationships.ConsumerApi/Controllers/RelationshipTemplatesController.cs +++ b/Modules/Relationships/src/Relationships.ConsumerApi/Controllers/RelationshipTemplatesController.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using Backbone.BuildingBlocks.API; using Backbone.BuildingBlocks.API.Mvc; using Backbone.BuildingBlocks.API.Mvc.ControllerAttributes; @@ -22,34 +23,52 @@ namespace Backbone.Modules.Relationships.ConsumerApi.Controllers; public class RelationshipTemplatesController : ApiControllerBase { private readonly ApplicationOptions _options; + private readonly JsonSerializerOptions _jsonSerializerOptions; - public RelationshipTemplatesController(IMediator mediator, IOptions options) : base(mediator) + public RelationshipTemplatesController(IMediator mediator, IOptions options, IOptions jsonOptions) : base(mediator) { _options = options.Value; + _jsonSerializerOptions = jsonOptions.Value.JsonSerializerOptions; } [HttpGet("{id}")] [ProducesResponseType(typeof(HttpResponseEnvelopeResult), StatusCodes.Status200OK)] [ProducesError(StatusCodes.Status404NotFound)] - public async Task GetById(string id, CancellationToken cancellationToken) + public async Task GetById(string id, [FromQuery] byte[]? password, CancellationToken cancellationToken) { - var template = await _mediator.Send(new GetRelationshipTemplateQuery { Id = id }, cancellationToken); + var template = await _mediator.Send(new GetRelationshipTemplateQuery { Id = id, Password = password }, cancellationToken); return Ok(template); } [HttpGet] [ProducesResponseType(typeof(PagedHttpResponseEnvelope), StatusCodes.Status200OK)] - public async Task GetAll([FromQuery] PaginationFilter paginationFilter, - [FromQuery] IEnumerable ids, CancellationToken cancellationToken) + public async Task GetAll([FromQuery] PaginationFilter paginationFilter, [FromQuery] string? templates, [FromQuery] IEnumerable ids, CancellationToken cancellationToken) { - var request = new ListRelationshipTemplatesQuery(paginationFilter, ids); + List? relationshipTemplateQueryItems; + + if (templates != null) + { + try + { + relationshipTemplateQueryItems = JsonSerializer.Deserialize>(templates, _jsonSerializerOptions); + } + catch (JsonException ex) + { + throw new ApplicationException(GenericApplicationErrors.Validation.InputCannotBeParsed(ex.Message)); + } + } + else + { + relationshipTemplateQueryItems = ids.Select(id => new RelationshipTemplateQueryItem { Id = id }).ToList(); + } + + var request = new ListRelationshipTemplatesQuery(paginationFilter, relationshipTemplateQueryItems); request.PaginationFilter.PageSize ??= _options.Pagination.DefaultPageSize; if (paginationFilter.PageSize > _options.Pagination.MaxPageSize) - throw new ApplicationException( - GenericApplicationErrors.Validation.InvalidPageSize(_options.Pagination.MaxPageSize)); + throw new ApplicationException(GenericApplicationErrors.Validation.InvalidPageSize(_options.Pagination.MaxPageSize)); var template = await _mediator.Send(request, cancellationToken); return Paged(template); diff --git a/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.cs b/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.cs index f41ddf2f9f..67ca001ad7 100644 --- a/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.cs +++ b/Modules/Relationships/src/Relationships.Domain/Aggregates/RelationshipTemplates/RelationshipTemplate.cs @@ -9,6 +9,8 @@ namespace Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates public class RelationshipTemplate : Entity { + public const int MAX_PASSWORD_LENGTH = 200; + // ReSharper disable once UnusedMember.Local private RelationshipTemplate() { @@ -18,7 +20,8 @@ private RelationshipTemplate() CreatedByDevice = null!; } - public RelationshipTemplate(IdentityAddress createdBy, DeviceId createdByDevice, int? maxNumberOfAllocations, DateTime? expiresAt, byte[] content, IdentityAddress? forIdentity = null) + public RelationshipTemplate(IdentityAddress createdBy, DeviceId createdByDevice, int? maxNumberOfAllocations, DateTime? expiresAt, byte[] content, IdentityAddress? forIdentity = null, + byte[]? password = null) { Id = RelationshipTemplateId.New(); CreatedAt = SystemTime.UtcNow; @@ -29,13 +32,14 @@ public RelationshipTemplate(IdentityAddress createdBy, DeviceId createdByDevice, ExpiresAt = expiresAt; Content = content; ForIdentity = forIdentity; + Password = password; RaiseDomainEvent(new RelationshipTemplateCreatedDomainEvent(this)); } public RelationshipTemplateId Id { get; set; } - public ICollection Relationships { get; set; } = new List(); + public ICollection Relationships { get; set; } = []; public IdentityAddress CreatedBy { get; set; } public DeviceId CreatedByDevice { get; set; } @@ -46,6 +50,7 @@ public RelationshipTemplate(IdentityAddress createdBy, DeviceId createdByDevice, public DateTime CreatedAt { get; set; } public IdentityAddress? ForIdentity { get; set; } + public byte[]? Password { get; set; } public List Allocations { get; set; } = []; @@ -54,7 +59,7 @@ public void AllocateFor(IdentityAddress identity, DeviceId device) if (identity == CreatedBy) return; - if (Allocations.Any(x => x.AllocatedBy == identity)) + if (IsAllocatedBy(identity)) return; if (Allocations.Count == MaxNumberOfAllocations) @@ -63,6 +68,23 @@ public void AllocateFor(IdentityAddress identity, DeviceId device) Allocations.Add(new RelationshipTemplateAllocation(Id, identity, device)); } + public bool IsAllocatedBy(IdentityAddress identity) + { + return Allocations.All(x => x.AllocatedBy != identity); + } + + public bool CanBeCollectedUsingPassword(IdentityAddress address, byte[]? password) + { + return Password == null || password != null && Password.SequenceEqual(password) || CreatedBy == address; + } + + #region Expressions + + public static Expression> HasId(RelationshipTemplateId id) + { + return r => r.Id == id; + } + public static Expression> WasCreatedBy(IdentityAddress identityAddress) { return r => r.CreatedBy == identityAddress.ToString(); @@ -72,4 +94,11 @@ public static Expression> CanBeCollectedBy(Iden { return relationshipTemplate => relationshipTemplate.ForIdentity == null || relationshipTemplate.ForIdentity == address || relationshipTemplate.CreatedBy == address; } + + public static Expression> CanBeCollectedWithPassword(IdentityAddress address, byte[]? password) + { + return relationshipTemplate => relationshipTemplate.Password == null || relationshipTemplate.Password == password || relationshipTemplate.CreatedBy == address; + } + + #endregion } diff --git a/Modules/Relationships/src/Relationships.Domain/DomainErrors.cs b/Modules/Relationships/src/Relationships.Domain/DomainErrors.cs index a230e7e458..61fce6d94d 100644 --- a/Modules/Relationships/src/Relationships.Domain/DomainErrors.cs +++ b/Modules/Relationships/src/Relationships.Domain/DomainErrors.cs @@ -76,4 +76,10 @@ public static DomainError RelationshipAlreadyDecomposed() return new DomainError("error.platform.validation.relationship.relationshipAlreadyDecomposed", "You already decomposed this Relationship."); } + + public static DomainError RelationshipTemplateNotAllocated() + { + return new DomainError("error.platform.validation.relationship.relationshipTemplateNotAllocated", + "The relationship template has to be allocated before it can be used to establish a relationship. Send a GET request to the /RelationshipTemplates/{id} endpoint to allocate the template."); + } } diff --git a/Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Migrations/20241011081142_AddPasswordToRelationshipTemplate.Designer.cs b/Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Migrations/20241011081142_AddPasswordToRelationshipTemplate.Designer.cs new file mode 100644 index 0000000000..7bf6101a17 --- /dev/null +++ b/Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Migrations/20241011081142_AddPasswordToRelationshipTemplate.Designer.cs @@ -0,0 +1,268 @@ +// +using System; +using Backbone.Modules.Relationships.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.Relationships.Infrastructure.Database.Postgres.Migrations +{ + [DbContext(typeof(RelationshipsDbContext))] + [Migration("20241011081142_AddPasswordToRelationshipTemplate")] + partial class AddPasswordToRelationshipTemplate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Relationships") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplate", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("Content") + .HasColumnType("bytea"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("CreatedByDevice") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ForIdentity") + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("MaxNumberOfAllocations") + .HasColumnType("integer"); + + b.Property("Password") + .HasMaxLength(200) + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.ToTable("RelationshipTemplates", "Relationships"); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplateAllocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllocatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AllocatedBy") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("AllocatedByDevice") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("RelationshipTemplateId") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.HasKey("Id"); + + b.HasIndex("RelationshipTemplateId", "AllocatedBy"); + + b.ToTable("RelationshipTemplateAllocations", "Relationships"); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.Relationships.Relationship", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationContent") + .HasColumnType("bytea"); + + b.Property("CreationResponseContent") + .HasColumnType("bytea"); + + b.Property("From") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("FromHasDecomposed") + .HasColumnType("boolean"); + + b.Property("RelationshipTemplateId") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("To") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("ToHasDecomposed") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("From"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("From"), "hash"); + + b.HasIndex("RelationshipTemplateId"); + + b.HasIndex("To"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("To"), "hash"); + + b.ToTable("Relationships", "Relationships"); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.Relationships.RelationshipAuditLogEntry", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("character varying(80)") + .IsFixedLength(false); + + b.Property("CreatedByDevice") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.Property("NewStatus") + .HasColumnType("integer"); + + b.Property("OldStatus") + .HasColumnType("integer"); + + b.Property("Reason") + .HasColumnType("integer"); + + b.Property("RelationshipId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("character(20)") + .IsFixedLength(); + + b.HasKey("Id"); + + b.HasIndex("RelationshipId"); + + b.ToTable("RelationshipAuditLog", "Relationships"); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplateAllocation", b => + { + b.HasOne("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplate", null) + .WithMany("Allocations") + .HasForeignKey("RelationshipTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.Relationships.Relationship", b => + { + b.HasOne("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplate", "RelationshipTemplate") + .WithMany("Relationships") + .HasForeignKey("RelationshipTemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("RelationshipTemplate"); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.Relationships.RelationshipAuditLogEntry", b => + { + b.HasOne("Backbone.Modules.Relationships.Domain.Aggregates.Relationships.Relationship", null) + .WithMany("AuditLog") + .HasForeignKey("RelationshipId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplate", b => + { + b.Navigation("Allocations"); + + b.Navigation("Relationships"); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.Relationships.Relationship", b => + { + b.Navigation("AuditLog"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Migrations/20241011081142_AddPasswordToRelationshipTemplate.cs b/Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Migrations/20241011081142_AddPasswordToRelationshipTemplate.cs new file mode 100644 index 0000000000..43b99375e5 --- /dev/null +++ b/Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Migrations/20241011081142_AddPasswordToRelationshipTemplate.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backbone.Modules.Relationships.Infrastructure.Database.Postgres.Migrations +{ + /// + public partial class AddPasswordToRelationshipTemplate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Password", + schema: "Relationships", + table: "RelationshipTemplates", + type: "bytea", + maxLength: 200, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Password", + schema: "Relationships", + table: "RelationshipTemplates"); + } + } +} diff --git a/Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Migrations/RelationshipsDbContextModelSnapshot.cs b/Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Migrations/RelationshipsDbContextModelSnapshot.cs index 5437d1057a..5d497fbe2c 100644 --- a/Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Migrations/RelationshipsDbContextModelSnapshot.cs +++ b/Modules/Relationships/src/Relationships.Infrastructure.Database.Postgres/Migrations/RelationshipsDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Relationships") - .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("ProductVersion", "8.0.10") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -63,6 +63,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("MaxNumberOfAllocations") .HasColumnType("integer"); + b.Property("Password") + .HasMaxLength(200) + .HasColumnType("bytea"); + b.HasKey("Id"); b.ToTable("RelationshipTemplates", "Relationships"); diff --git a/Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Migrations/20241011081556_AddPasswordToRelationshipTemplate.Designer.cs b/Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Migrations/20241011081556_AddPasswordToRelationshipTemplate.Designer.cs new file mode 100644 index 0000000000..0211e59f2f --- /dev/null +++ b/Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Migrations/20241011081556_AddPasswordToRelationshipTemplate.Designer.cs @@ -0,0 +1,266 @@ +// +using System; +using Backbone.Modules.Relationships.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.Relationships.Infrastructure.Database.SqlServer.Migrations +{ + [DbContext(typeof(RelationshipsDbContext))] + [Migration("20241011081556_AddPasswordToRelationshipTemplate")] + partial class AddPasswordToRelationshipTemplate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Relationships") + .HasAnnotation("ProductVersion", "8.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplate", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("Content") + .HasColumnType("varbinary(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("CreatedByDevice") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("ForIdentity") + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("MaxNumberOfAllocations") + .HasColumnType("int"); + + b.Property("Password") + .HasMaxLength(200) + .HasColumnType("varbinary(200)"); + + b.HasKey("Id"); + + b.ToTable("RelationshipTemplates", "Relationships"); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplateAllocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AllocatedAt") + .HasColumnType("datetime2"); + + b.Property("AllocatedBy") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("AllocatedByDevice") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("RelationshipTemplateId") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.HasKey("Id"); + + b.HasIndex("RelationshipTemplateId", "AllocatedBy"); + + b.ToTable("RelationshipTemplateAllocations", "Relationships"); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.Relationships.Relationship", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreationContent") + .HasColumnType("varbinary(max)"); + + b.Property("CreationResponseContent") + .HasColumnType("varbinary(max)"); + + b.Property("From") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("FromHasDecomposed") + .HasColumnType("bit"); + + b.Property("RelationshipTemplateId") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("To") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("ToHasDecomposed") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("From") + .HasAnnotation("Npgsql:IndexMethod", "hash"); + + b.HasIndex("RelationshipTemplateId"); + + b.HasIndex("To") + .HasAnnotation("Npgsql:IndexMethod", "hash"); + + b.ToTable("Relationships", "Relationships"); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.Relationships.RelationshipAuditLogEntry", b => + { + b.Property("Id") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(80) + .IsUnicode(false) + .HasColumnType("varchar(80)") + .IsFixedLength(false); + + b.Property("CreatedByDevice") + .IsRequired() + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.Property("NewStatus") + .HasColumnType("int"); + + b.Property("OldStatus") + .HasColumnType("int"); + + b.Property("Reason") + .HasColumnType("int"); + + b.Property("RelationshipId") + .HasMaxLength(20) + .IsUnicode(false) + .HasColumnType("char(20)") + .IsFixedLength(); + + b.HasKey("Id"); + + b.HasIndex("RelationshipId"); + + b.ToTable("RelationshipAuditLog", "Relationships"); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplateAllocation", b => + { + b.HasOne("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplate", null) + .WithMany("Allocations") + .HasForeignKey("RelationshipTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.Relationships.Relationship", b => + { + b.HasOne("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplate", "RelationshipTemplate") + .WithMany("Relationships") + .HasForeignKey("RelationshipTemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("RelationshipTemplate"); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.Relationships.RelationshipAuditLogEntry", b => + { + b.HasOne("Backbone.Modules.Relationships.Domain.Aggregates.Relationships.Relationship", null) + .WithMany("AuditLog") + .HasForeignKey("RelationshipId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates.RelationshipTemplate", b => + { + b.Navigation("Allocations"); + + b.Navigation("Relationships"); + }); + + modelBuilder.Entity("Backbone.Modules.Relationships.Domain.Aggregates.Relationships.Relationship", b => + { + b.Navigation("AuditLog"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Migrations/20241011081556_AddPasswordToRelationshipTemplate.cs b/Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Migrations/20241011081556_AddPasswordToRelationshipTemplate.cs new file mode 100644 index 0000000000..d52f6824ad --- /dev/null +++ b/Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Migrations/20241011081556_AddPasswordToRelationshipTemplate.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Backbone.Modules.Relationships.Infrastructure.Database.SqlServer.Migrations +{ + /// + public partial class AddPasswordToRelationshipTemplate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Password", + schema: "Relationships", + table: "RelationshipTemplates", + type: "varbinary(200)", + maxLength: 200, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Password", + schema: "Relationships", + table: "RelationshipTemplates"); + } + } +} diff --git a/Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Migrations/RelationshipsDbContextModelSnapshot.cs b/Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Migrations/RelationshipsDbContextModelSnapshot.cs index 45b1c5a2f5..56f1ac8769 100644 --- a/Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Migrations/RelationshipsDbContextModelSnapshot.cs +++ b/Modules/Relationships/src/Relationships.Infrastructure.Database.SqlServer/Migrations/RelationshipsDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Relationships") - .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("ProductVersion", "8.0.10") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -63,6 +63,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("MaxNumberOfAllocations") .HasColumnType("int"); + b.Property("Password") + .HasMaxLength(200) + .HasColumnType("varbinary(200)"); + b.HasKey("Id"); b.ToTable("RelationshipTemplates", "Relationships"); diff --git a/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/EntityTypeConfigurations/RelationshipTemplateEntityTypeConfiguration.cs b/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/EntityTypeConfigurations/RelationshipTemplateEntityTypeConfiguration.cs index 312565fddc..7bdb173c99 100644 --- a/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/EntityTypeConfigurations/RelationshipTemplateEntityTypeConfiguration.cs +++ b/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/EntityTypeConfigurations/RelationshipTemplateEntityTypeConfiguration.cs @@ -25,5 +25,9 @@ public override void Configure(EntityTypeBuilder builder) .WithOne() .HasForeignKey(x => x.RelationshipTemplateId) .OnDelete(DeleteBehavior.Cascade); + + builder + .Property(x => x.Password) + .HasMaxLength(RelationshipTemplate.MAX_PASSWORD_LENGTH); } } diff --git a/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/Repository/RelationshipTemplatesRepository.cs b/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/Repository/RelationshipTemplatesRepository.cs index fca6631c2e..3b730a76f7 100644 --- a/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/Repository/RelationshipTemplatesRepository.cs +++ b/Modules/Relationships/src/Relationships.Infrastructure/Persistence/Database/Repository/RelationshipTemplatesRepository.cs @@ -4,6 +4,7 @@ using Backbone.BuildingBlocks.Application.Pagination; using Backbone.DevelopmentKit.Identity.ValueObjects; using Backbone.Modules.Relationships.Application.Infrastructure.Persistence.Repository; +using Backbone.Modules.Relationships.Application.RelationshipTemplates.Queries.ListRelationshipTemplates; using Backbone.Modules.Relationships.Domain.Aggregates.RelationshipTemplates; using Backbone.Modules.Relationships.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; @@ -47,14 +48,25 @@ public async Task Delete(Expression> filter, Ca return template; } - public async Task> FindTemplatesWithIds(IEnumerable ids, IdentityAddress identityAddress, PaginationFilter paginationFilter, + public async Task> FindTemplatesWithIds(IEnumerable queryItems, IdentityAddress activeIdentity, + PaginationFilter paginationFilter, CancellationToken cancellationToken, bool track = false) { + var queryItemsList = queryItems.ToList(); + + Expression> idAndPasswordFilter = template => false; + + foreach (var inputQuery in queryItemsList) + { + idAndPasswordFilter = idAndPasswordFilter + .Or(RelationshipTemplate.HasId(RelationshipTemplateId.Parse(inputQuery.Id)) + .And(RelationshipTemplate.CanBeCollectedWithPassword(activeIdentity, inputQuery.Password))); + } + var query = (track ? _templates : _readOnlyTemplates) - .AsQueryable() - .NotExpiredFor(identityAddress) - .Where(RelationshipTemplate.CanBeCollectedBy(identityAddress)) - .WithIdIn(ids); + .NotExpiredFor(activeIdentity) + .Where(RelationshipTemplate.CanBeCollectedBy(activeIdentity)) + .Where(idAndPasswordFilter); var templates = await query.OrderAndPaginate(d => d.CreatedAt, paginationFilter, cancellationToken); diff --git a/Sdks/ConsumerApi.Sdk/src/Endpoints/RelationshipTemplates/RelationshipTemplatesEndpoint.cs b/Sdks/ConsumerApi.Sdk/src/Endpoints/RelationshipTemplates/RelationshipTemplatesEndpoint.cs index 96b90f1e01..1ba0415e10 100644 --- a/Sdks/ConsumerApi.Sdk/src/Endpoints/RelationshipTemplates/RelationshipTemplatesEndpoint.cs +++ b/Sdks/ConsumerApi.Sdk/src/Endpoints/RelationshipTemplates/RelationshipTemplatesEndpoint.cs @@ -3,23 +3,19 @@ using Backbone.ConsumerApi.Sdk.Endpoints.RelationshipTemplates.Types; using Backbone.ConsumerApi.Sdk.Endpoints.RelationshipTemplates.Types.Requests; using Backbone.ConsumerApi.Sdk.Endpoints.RelationshipTemplates.Types.Responses; +using Newtonsoft.Json; namespace Backbone.ConsumerApi.Sdk.Endpoints.RelationshipTemplates; public class RelationshipTemplatesEndpoint(EndpointClient client) : ConsumerApiEndpoint(client) { - public async Task> ListTemplates(PaginationFilter? pagination = null) - { - return await _client.Get($"api/{API_VERSION}/RelationshipTemplates", null, pagination); - } - - public async Task> ListTemplates(IEnumerable ids, PaginationFilter? pagination = null) + public async Task> ListTemplates(IEnumerable queryItems, PaginationFilter? pagination = null) { return await _client .Request(HttpMethod.Get, $"api/{API_VERSION}/RelationshipTemplates") .Authenticate() .WithPagination(pagination) - .AddQueryParameter("ids", ids) + .AddQueryParameter("templates", queryItems.ToJson()) .Execute(); } @@ -28,8 +24,21 @@ public async Task> GetTemplate(string id) return await _client.Get($"api/{API_VERSION}/RelationshipTemplates/{id}"); } + public async Task> GetTemplate(string id, byte[] password) + { + return await _client.Get($"api/{API_VERSION}/RelationshipTemplates/{id}?password={Convert.ToBase64String(password)}"); + } + public async Task> CreateTemplate(CreateRelationshipTemplateRequest request) { return await _client.Post($"api/{API_VERSION}/RelationshipTemplates", request); } } + +file static class RelationshipTemplateQueryExtensions +{ + public static string ToJson(this IEnumerable queryItems) + { + return JsonConvert.SerializeObject(queryItems, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Formatting = Formatting.None }); + } +} diff --git a/Sdks/ConsumerApi.Sdk/src/Endpoints/RelationshipTemplates/Types/Requests/CreateRelationshipTemplateRequest.cs b/Sdks/ConsumerApi.Sdk/src/Endpoints/RelationshipTemplates/Types/Requests/CreateRelationshipTemplateRequest.cs index 2a7f6ae147..8e5b66a1ae 100644 --- a/Sdks/ConsumerApi.Sdk/src/Endpoints/RelationshipTemplates/Types/Requests/CreateRelationshipTemplateRequest.cs +++ b/Sdks/ConsumerApi.Sdk/src/Endpoints/RelationshipTemplates/Types/Requests/CreateRelationshipTemplateRequest.cs @@ -5,4 +5,5 @@ public class CreateRelationshipTemplateRequest public int? MaxNumberOfAllocations { get; set; } public required byte[] Content { get; set; } public string? ForIdentity { get; set; } + public byte[]? Password { get; set; } } diff --git a/Sdks/ConsumerApi.Sdk/src/Endpoints/RelationshipTemplates/Types/Requests/RelationshipTemplateQueryItem.cs b/Sdks/ConsumerApi.Sdk/src/Endpoints/RelationshipTemplates/Types/Requests/RelationshipTemplateQueryItem.cs new file mode 100644 index 0000000000..3f4482136c --- /dev/null +++ b/Sdks/ConsumerApi.Sdk/src/Endpoints/RelationshipTemplates/Types/Requests/RelationshipTemplateQueryItem.cs @@ -0,0 +1,7 @@ +namespace Backbone.ConsumerApi.Sdk.Endpoints.RelationshipTemplates.Types.Requests; + +public class RelationshipTemplateQueryItem +{ + public required string Id { get; set; } + public byte[]? Password { get; set; } +}