Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consumer Api: Backup Devices #953

Merged
merged 25 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
189f3be
feat: Add IsBackupDevice to Device class, DTOs, requests, responses a…
MH321Productions Nov 22, 2024
642b3bc
feat: Add method to ensure that only one backup device exists per ide…
MH321Productions Nov 22, 2024
21d2240
feat: Add backup device parameter to sdk methods and integration tests
MH321Productions Nov 22, 2024
0be532a
Merge branch 'main' into backup-devices
MH321Productions Nov 22, 2024
62d3449
Merge branch 'main' into backup-devices
MH321Productions Nov 25, 2024
ae41048
feat: Add command, handler and push notification for a used backup de…
MH321Productions Nov 25, 2024
3dc73d8
feat: Add translations for backup device push notification
MH321Productions Nov 25, 2024
8aaa94e
feat: Add tests for backup device usage
MH321Productions Nov 28, 2024
bf7b59b
Merge branch 'main' into backup-devices
MH321Productions Nov 28, 2024
3631335
Merge branch 'main' into backup-devices
mergify[bot] Nov 29, 2024
da01c20
Merge branch 'main' into backup-devices
mergify[bot] Nov 29, 2024
5ea1bab
refactor: use eventing to set IsBackupDevice to false
tnotheis Nov 29, 2024
946e7fb
chore: add dot at the end of push notification text body
tnotheis Nov 29, 2024
e2b16aa
test: improve tests
tnotheis Nov 29, 2024
7958a6a
chore: add missing file
tnotheis Nov 29, 2024
23aa3d2
refactor: simplify RegisterDeviceResponse
tnotheis Nov 29, 2024
0293a07
refactor: move default value to controller method
tnotheis Nov 29, 2024
37d2a9d
chore: add explaining comment
tnotheis Nov 29, 2024
a67f3a8
refactor: use special method for creating a backup device via SDK, in…
tnotheis Nov 29, 2024
54b7108
feat: don't use default value for RegisterDeviceRequest in SDK
tnotheis Nov 29, 2024
590b672
refactor: inline method
tnotheis Nov 29, 2024
d041867
Merge branch 'main' into backup-devices
mergify[bot] Dec 2, 2024
8ec6cea
Merge branch 'main' into backup-devices
mergify[bot] Dec 2, 2024
44588b5
Merge branch 'main' into backup-devices
mergify[bot] Dec 2, 2024
c092c87
fix: Only check for existing backup devices when trying to create one
MH321Productions Dec 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,6 @@ Generated_Code/
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,18 @@ User creates a Device
When i sends a POST request to the /Devices endpoint with a valid signature on c
Then the response status code is 201 (Created)
And the response contains a Device

Scenario: Registering a backup Device
Given Identity i
And a Challenge c created by i
When i sends a POST request to the /Devices endpoint with a valid signature on c as a backup Device
Then the response status code is 201 (Created)
And the response contains a Device
And the created Device is a backup Device

Scenario: Registering a second backup Device is not possible
Given an Identity i with a Device d1 and a backup Device d2
And a Challenge c created by i
When i sends a POST request to the /Devices endpoint with a valid signature on c as a backup Device
Then the response status code is 400 (Bad Request)
And the response content contains an error with the error code "error.platform.validation.device.onlyOneBackupDeviceCanExist"
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Backbone.ConsumerApi.Sdk;
using Backbone.BuildingBlocks.SDK.Endpoints.Common.Types;
using Backbone.ConsumerApi.Sdk;
using Backbone.ConsumerApi.Sdk.Authentication;
using Backbone.ConsumerApi.Sdk.Endpoints.Devices.Types.Requests;
using Backbone.ConsumerApi.Sdk.Endpoints.Devices.Types.Responses;
using Backbone.ConsumerApi.Tests.Integration.Configuration;
using Backbone.ConsumerApi.Tests.Integration.Contexts;
using Backbone.ConsumerApi.Tests.Integration.Helpers;
Expand All @@ -24,6 +26,8 @@ internal class DevicesStepDefinitions
private readonly ResponseContext _responseContext;
private readonly ClientPool _clientPool;

private ApiResponse<RegisterDeviceResponse>? _registerDeviceResponse;

public DevicesStepDefinitions(ChallengesContext challengesContext, ResponseContext responseContext, HttpClientFactory factory,
IOptions<HttpConfiguration> httpConfiguration, ClientPool clientPool)
{
Expand Down Expand Up @@ -51,8 +55,17 @@ public async Task GivenAnIdentityWithADeviceAndAnUnonboardedDevice(string identi
{
var client = await Client.CreateForNewIdentity(_httpClient, _clientCredentials, DEVICE_PASSWORD);
_clientPool.Add(client).ForIdentity(identityName).AndDevice(onboardedDeviceName);
var clientForUnOnboardedDevice = await client.OnboardNewDevice("Passw0rd");
_clientPool.Add(clientForUnOnboardedDevice).ForIdentity(identityName).AndDevice(unonboardedDeviceName);
var clientForBackupDevice = await client.OnboardNewDevice("Passw0rd");
_clientPool.Add(clientForBackupDevice).ForIdentity(identityName).AndDevice(unonboardedDeviceName);
}

[Given($"an Identity {RegexFor.SINGLE_THING} with a Device {RegexFor.SINGLE_THING} and a backup Device {RegexFor.SINGLE_THING}")]
public async Task GivenAnIdentityWithADeviceAndABackupDevice(string identityName, string onboardedDeviceName, string backupDeviceName)
{
var client = await Client.CreateForNewIdentity(_httpClient, _clientCredentials, DEVICE_PASSWORD);
_clientPool.Add(client).ForIdentity(identityName).AndDevice(onboardedDeviceName);
var clientForBackupDevice = await client.OnboardNewBackupDevice("Passw0rd");
_clientPool.Add(clientForBackupDevice).ForIdentity(identityName).AndDevice(backupDeviceName);
}

[Given($"an Identity {RegexFor.SINGLE_THING} with Devices {RegexFor.LIST_OF_THINGS}")]
Expand Down Expand Up @@ -82,10 +95,25 @@ public async Task WhenIdentitySendsAPostRequestToTheDevicesEndpointWithASignedCh
var identity = _clientPool.FirstForIdentityName(identityName);
var signedChallenge = CreateSignedChallenge(identity, _challengesContext.Challenges[challengeName]);

_responseContext.WhenResponse = await identity.Devices.RegisterDevice(new RegisterDeviceRequest
_responseContext.WhenResponse = _registerDeviceResponse = await identity.Devices.RegisterDevice(new RegisterDeviceRequest
{
DevicePassword = DEVICE_PASSWORD,
SignedChallenge = signedChallenge,
IsBackupDevice = false
});
}

[When($"{RegexFor.SINGLE_THING} sends a POST request to the /Devices endpoint with a valid signature on {RegexFor.SINGLE_THING} as a backup Device")]
public async Task WhenIdentitySendsAPostRequestToTheDevicesEndpointWithASignedChallengeAsABackupDevice(string identityName, string challengeName)
{
var identity = _clientPool.FirstForIdentityName(identityName);
var signedChallenge = CreateSignedChallenge(identity, _challengesContext.Challenges[challengeName]);

_responseContext.WhenResponse = _registerDeviceResponse = await identity.Devices.RegisterDevice(new RegisterDeviceRequest
{
DevicePassword = DEVICE_PASSWORD,
SignedChallenge = signedChallenge
SignedChallenge = signedChallenge,
IsBackupDevice = true
});
}

Expand Down Expand Up @@ -174,5 +202,11 @@ public async Task ThenTheBackboneHasPersistedAsTheNewCommunicationLanguageOfDevi
response.Result!.First().CommunicationLanguage.Should().Be(_communicationLanguage);
}

[Then("the created Device is a backup Device")]
public void ThenTheCreatedDeviceIsABackupDevice()
{
_registerDeviceResponse!.Result!.IsBackupDevice.Should().BeTrue();
}

#endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ public CustomSigninManager(
IAuthenticationSchemeProvider schemes,
IUserConfirmation<ApplicationUser> confirmation) : base(userManager, contextAccessor, claimsFactory,
optionsAccessor, logger, schemes, confirmation)
{ }
{
}

public override async Task<SignInResult> CheckPasswordSignInAsync(ApplicationUser user, string password,
bool lockoutOnFailure)
Expand All @@ -35,7 +36,7 @@ public override async Task<SignInResult> CheckPasswordSignInAsync(ApplicationUse

private async Task UpdateLastLoginDate(ApplicationUser user)
{
user.LoginOccurred();
user.Device.LoginOccurred();
await UserManager.UpdateAsync(user);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ namespace Backbone.BuildingBlocks.API.AspNetCoreIdentityCustomizations;

public class CustomUserStore : UserStore<ApplicationUser>
{
public CustomUserStore(DevicesDbContext context, IdentityErrorDescriber? describer = null) : base(context, describer) { }
public CustomUserStore(DevicesDbContext context, IdentityErrorDescriber? describer = null) : base(context, describer)
{
}

public override async Task<ApplicationUser?> FindByIdAsync(string userId, CancellationToken cancellationToken = default)
{
Expand All @@ -34,4 +36,11 @@ public CustomUserStore(DevicesDbContext context, IdentityErrorDescriber? describ

return user;
}

public override Task<IdentityResult> UpdateAsync(ApplicationUser user, CancellationToken cancellationToken = new CancellationToken())
{
Context.Attach(user.Device);
Context.Update(user.Device);
return base.UpdateAsync(user, cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,11 @@ public static ApplicationError ClientReachedIdentitiesLimit()
return new ApplicationError("error.platform.validation.device.clientReachedIdentitiesLimit",
"The client's Identity limit has been reached. A new Identity cannot be created with this client.");
}

public static ApplicationError BackupDeviceAlreadyExists()
{
return new ApplicationError("error.platform.validation.device.onlyOneBackupDeviceCanExist",
"Only one backup device can be created per identity.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Backbone.Modules.Devices.Domain.Entities.Identities;
using MediatR;
using Microsoft.Extensions.Logging;
using ApplicationException = Backbone.BuildingBlocks.Application.Abstractions.Exceptions.ApplicationException;

namespace Backbone.Modules.Devices.Application.Devices.Commands.RegisterDevice;

Expand All @@ -31,18 +32,21 @@ public async Task<RegisterDeviceResponse> Handle(RegisterDeviceCommand command,
{
var identity = await _identitiesRepository.FindByAddress(_userContext.GetAddress(), cancellationToken, track: true) ?? throw new NotFoundException(nameof(Identity));

if (command.IsBackupDevice && await _identitiesRepository.HasBackupDevice(identity.Address, cancellationToken))
throw new ApplicationException(ApplicationErrors.Devices.BackupDeviceAlreadyExists());

await _challengeValidator.Validate(command.SignedChallenge, PublicKey.FromBytes(identity.PublicKey));
_logger.LogTrace("Successfully validated challenge.");

var communicationLanguageResult = CommunicationLanguage.Create(command.CommunicationLanguage);

var newDevice = identity.AddDevice(communicationLanguageResult.Value, _userContext.GetDeviceId());
var newDevice = identity.AddDevice(communicationLanguageResult.Value, _userContext.GetDeviceId(), command.IsBackupDevice);

await _identitiesRepository.UpdateWithNewDevice(identity, command.DevicePassword);

_logger.CreatedDevice();

return new RegisterDeviceResponse(newDevice.User);
return new RegisterDeviceResponse(newDevice);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ public class RegisterDeviceCommand : IRequest<RegisterDeviceResponse>
public required string DevicePassword { get; set; }
public required string CommunicationLanguage { get; set; }
public required SignedChallengeDTO SignedChallenge { get; set; }
public required bool IsBackupDevice { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ namespace Backbone.Modules.Devices.Application.Devices.Commands.RegisterDevice;

public class RegisterDeviceResponse
{
public RegisterDeviceResponse(ApplicationUser user)
public RegisterDeviceResponse(Device device)
{
Id = user.DeviceId;
Username = user.UserName!;
CreatedByDevice = user.Device.CreatedByDevice;
CreatedAt = user.Device.CreatedAt;
Id = device.Id.Value;
Username = device.User.UserName!;
CreatedByDevice = device.CreatedByDevice.Value;
CreatedAt = device.CreatedAt;
IsBackupDevice = device.IsBackupDevice;
}

public string Id { get; set; }
public string Username { get; set; }
public DateTime CreatedAt { get; set; }
public string CreatedByDevice { get; set; }
public bool IsBackupDevice { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public DeviceDTO(Device device)
CreatedByDevice = device.CreatedByDevice;
LastLogin = new LastLoginInformation { Time = device.User.LastLoginAt };
CommunicationLanguage = device.CommunicationLanguage;
IsBackupDevice = device.IsBackupDevice;
}

public string Id { get; set; }
Expand All @@ -20,6 +21,7 @@ public DeviceDTO(Device device)
public string CreatedByDevice { get; set; }
public LastLoginInformation LastLogin { get; set; }
public string CommunicationLanguage { get; set; }
public bool IsBackupDevice { get; set; }
}

public class LastLoginInformation
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus;
using Backbone.BuildingBlocks.Application.PushNotifications;
using Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.Device;
using Backbone.Modules.Devices.Domain.DomainEvents.Outgoing;

namespace Backbone.Modules.Devices.Application.DomainEvents.Incoming.BackupDeviceUsed;

public class BackupDeviceUsedDomainEventHandler : IDomainEventHandler<BackupDeviceUsedDomainEvent>
{
private readonly IPushNotificationSender _pushNotificationSender;

public BackupDeviceUsedDomainEventHandler(IPushNotificationSender pushNotificationSender)
{
_pushNotificationSender = pushNotificationSender;
}

public async Task Handle(BackupDeviceUsedDomainEvent @event)
{
await _pushNotificationSender.SendNotification(new BackupDeviceUsedPushNotification(), SendPushNotificationFilter.AllDevicesOf(@event.IdentityAddress), CancellationToken.None);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus;
using Backbone.Modules.Devices.Application.DomainEvents.Incoming.AnnouncementCreated;
using Backbone.Modules.Devices.Application.DomainEvents.Incoming.BackupDeviceUsed;
using Backbone.Modules.Devices.Application.DomainEvents.Incoming.DatawalletModificationCreated;
using Backbone.Modules.Devices.Application.DomainEvents.Incoming.ExternalEventCreated;
using Backbone.Modules.Devices.Application.DomainEvents.Incoming.IdentityDeletionProcessStarted;
Expand All @@ -15,6 +16,7 @@ public static class IEventBusExtensions
public static void AddDevicesDomainEventSubscriptions(this IEventBus eventBus)
{
eventBus.SubscribeToAnnouncementsEvents();
eventBus.SubscribeToDevicesEvents();
eventBus.SubscribeToSynchronizationEvents();
}

Expand All @@ -23,6 +25,11 @@ private static void SubscribeToAnnouncementsEvents(this IEventBus eventBus)
eventBus.Subscribe<AnnouncementCreatedDomainEvent, AnnouncementCreatedDomainEventHandler>();
}

private static void SubscribeToDevicesEvents(this IEventBus eventBus)
{
eventBus.Subscribe<BackupDeviceUsedDomainEvent, BackupDeviceUsedDomainEventHandler>();
}

private static void SubscribeToSynchronizationEvents(this IEventBus eventBus)
{
eventBus.Subscribe<DatawalletModifiedDomainEvent, DatawalletModifiedDomainEventHandler>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public interface IIdentitiesRepository
Task<IEnumerable<Device>> GetDevicesByIds(IEnumerable<DeviceId> deviceIds, CancellationToken cancellationToken, bool track = false);
Task Update(Device device, CancellationToken cancellationToken);
Task<T[]> FindDevices<T>(Expression<Func<Device, bool>> filter, Expression<Func<Device, T>> selector, CancellationToken cancellationToken, bool track = false);
Task<bool> HasBackupDevice(IdentityAddress identity, CancellationToken cancellationToken);

#endregion

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Backbone.BuildingBlocks.Application.PushNotifications;

namespace Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.Device;

public class BackupDeviceUsedPushNotification : IPushNotification;
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ public async Task<IActionResult> RegisterDevice(RegisterDeviceRequest request, C
{
CommunicationLanguage = request.CommunicationLanguage ?? CommunicationLanguage.DEFAULT_LANGUAGE.Value,
SignedChallenge = request.SignedChallenge,
DevicePassword = request.DevicePassword
DevicePassword = request.DevicePassword,
IsBackupDevice = request.IsBackupDevice ?? false
};

var response = await _mediator.Send(command, cancellationToken);
Expand Down Expand Up @@ -110,4 +111,5 @@ public class RegisterDeviceRequest
public required string DevicePassword { get; set; }
public string? CommunicationLanguage { get; set; }
public required SignedChallengeDTO SignedChallenge { get; set; }
public bool? IsBackupDevice { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Backbone.BuildingBlocks.Domain.Events;
using Backbone.DevelopmentKit.Identity.ValueObjects;

namespace Backbone.Modules.Devices.Domain.DomainEvents.Outgoing;

public class BackupDeviceUsedDomainEvent : DomainEvent
{
public BackupDeviceUsedDomainEvent(IdentityAddress identityAddress)
{
IdentityAddress = identityAddress;
}

public IdentityAddress IdentityAddress { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public Device Device

public bool HasLoggedIn => LastLoginAt.HasValue;

public void LoginOccurred()
internal void LoginOccurred()
{
LastLoginAt = SystemTime.UtcNow;
}
Expand Down
Loading