Skip to content

Commit

Permalink
Decouple recovery key consent handling from the login response model
Browse files Browse the repository at this point in the history
  • Loading branch information
Jack-Edwards committed Jan 14, 2025
1 parent 1900bc5 commit bf2806d
Show file tree
Hide file tree
Showing 22 changed files with 1,051 additions and 93 deletions.
24 changes: 20 additions & 4 deletions Crypter.API/Controllers/ConsentController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,14 @@
* Contact the current copyright holder to discuss commercial license options.
*/

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Crypter.API.Controllers.Base;
using Crypter.Common.Contracts.Features.UserConsents;
using Crypter.Core.Features.UserConsent.Commands;
using Crypter.Core.Features.UserConsent.Queries;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
Expand All @@ -45,14 +50,25 @@ public ConsentController(ISender sender)
_sender = sender;
}

[HttpPost("recovery-key-risk")]
[HttpGet]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Dictionary<UserConsentType, DateTimeOffset?>))]
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(void))]
public async Task<IActionResult> GetUserConsentsAsync(CancellationToken cancellationToken)
{
GetUserConsentsQuery query = new GetUserConsentsQuery(UserId);
Dictionary<UserConsentType, DateTimeOffset?> result = await _sender.Send(query, cancellationToken);
return Ok(result);
}

[HttpPost]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(void))]
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(void))]
public async Task<IActionResult> ConsentToRecoveryKeyRisksAsync()
public async Task<IActionResult> ConsentAsync(UserConsentRequest request)
{
SaveAcknowledgementOfRecoveryKeyRisksCommand request = new SaveAcknowledgementOfRecoveryKeyRisksCommand(UserId);
await _sender.Send(request);
SaveUserConsentCommand command = new SaveUserConsentCommand(UserId, request.ConsentType);
await _sender.Send(command, CancellationToken.None);
return Ok();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,16 @@ public async Task<Either<ErrorResponse, TResponse>> PostEitherAsync<TRequest, TR
return await DeserializeResponseAsync<TResponse>(response);
}

public async Task<Maybe<Unit>> PostMaybeUnitResponseAsync<TRequest>(string uri, TRequest body)
where TRequest : class
{
Func<HttpRequestMessage> requestFactory = MakeRequestMessageFactory(HttpMethod.Post, uri, body);
using HttpResponseMessage response = await SendWithAuthenticationAsync(requestFactory, false);
return response.IsSuccessStatusCode
? Unit.Default
: Maybe<Unit>.None;
}

public async Task<Maybe<Unit>> PostMaybeUnitResponseAsync(string uri)
{
Func<HttpRequestMessage> requestFactory = MakeRequestMessageFactory(HttpMethod.Post, uri);
Expand Down
10 changes: 10 additions & 0 deletions Crypter.Common.Client/HttpClients/CrypterHttpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ public async Task<Either<ErrorResponse, TResponse>> PostEitherAsync<TRequest, TR
return await SendRequestEitherResponseAsync<TResponse>(request);
}

public async Task<Maybe<Unit>> PostMaybeUnitResponseAsync<TRequest>(string uri, TRequest body)
where TRequest : class
{
using HttpRequestMessage request = MakeRequestMessage(HttpMethod.Post, uri, body);
using HttpResponseMessage response = await _httpClient.SendAsync(request);
return response.IsSuccessStatusCode
? Unit.Default
: Maybe<Unit>.None;
}

public async Task<Maybe<Unit>> PostMaybeUnitResponseAsync(string uri)
{
using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uri);
Expand Down
18 changes: 14 additions & 4 deletions Crypter.Common.Client/HttpClients/Requests/UserConsentRequests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Crypter File Transfer
* Copyright (C) 2025 Crypter File Transfer
*
* This file is part of the Crypter file transfer project.
*
Expand All @@ -24,9 +24,12 @@
* Contact the current copyright holder to discuss commercial license options.
*/

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Crypter.Common.Client.Interfaces.HttpClients;
using Crypter.Common.Client.Interfaces.Requests;
using Crypter.Common.Contracts.Features.UserConsents;
using EasyMonads;

namespace Crypter.Common.Client.HttpClients.Requests;
Expand All @@ -40,9 +43,16 @@ public UserConsentRequests(ICrypterAuthenticatedHttpClient crypterAuthenticatedH
_crypterAuthenticatedHttpClient = crypterAuthenticatedHttpClient;
}

public Task<Maybe<Unit>> ConsentToRecoveryKeyRisksAsync()
public Task<Maybe<Dictionary<UserConsentType, DateTimeOffset?>>> GetUserConsentsAsync()
{
const string url = "api/user/consent/recovery-key-risk";
return _crypterAuthenticatedHttpClient.PostMaybeUnitResponseAsync(url);
const string url = "api/user/consent";
return _crypterAuthenticatedHttpClient.GetMaybeAsync<Dictionary<UserConsentType, DateTimeOffset?>>(url);
}

public Task<Maybe<Unit>> ConsentAsync(UserConsentType consentType)
{
const string url = "api/user/consent";
UserConsentRequest request = new UserConsentRequest(consentType);
return _crypterAuthenticatedHttpClient.PostMaybeUnitResponseAsync(url, request);
}
}
20 changes: 7 additions & 13 deletions Crypter.Common.Client/HttpClients/Requests/UserSettingRequests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Crypter File Transfer
* Copyright (C) 2025 Crypter File Transfer
*
* This file is part of the Crypter file transfer project.
*
Expand Down Expand Up @@ -41,8 +41,7 @@ public class UserSettingRequests : IUserSettingRequests
private readonly ICrypterHttpClient _crypterHttpClient;
private readonly ICrypterAuthenticatedHttpClient _crypterAuthenticatedHttpClient;

public UserSettingRequests(ICrypterHttpClient crypterHttpClient,
ICrypterAuthenticatedHttpClient crypterAuthenticatedHttpClient)
public UserSettingRequests(ICrypterHttpClient crypterHttpClient, ICrypterAuthenticatedHttpClient crypterAuthenticatedHttpClient)
{
_crypterHttpClient = crypterHttpClient;
_crypterAuthenticatedHttpClient = crypterAuthenticatedHttpClient;
Expand All @@ -54,8 +53,7 @@ public Task<Maybe<ProfileSettings>> GetProfileSettingsAsync()
return _crypterAuthenticatedHttpClient.GetMaybeAsync<ProfileSettings>(url);
}

public Task<Either<SetProfileSettingsError, ProfileSettings>> SetProfileSettingsAsync(
ProfileSettings newProfileSettings)
public Task<Either<SetProfileSettingsError, ProfileSettings>> SetProfileSettingsAsync(ProfileSettings newProfileSettings)
{
const string url = "api/user/setting/profile";
return _crypterAuthenticatedHttpClient.PutEitherAsync<ProfileSettings, ProfileSettings>(url, newProfileSettings)
Expand All @@ -68,8 +66,7 @@ public Task<Maybe<ContactInfoSettings>> GetContactInfoSettingsAsync()
return _crypterAuthenticatedHttpClient.GetMaybeAsync<ContactInfoSettings>(url);
}

public Task<Either<UpdateContactInfoSettingsError, ContactInfoSettings>> UpdateContactInfoSettingsAsync(
UpdateContactInfoSettingsRequest newContactInfoSettings)
public Task<Either<UpdateContactInfoSettingsError, ContactInfoSettings>> UpdateContactInfoSettingsAsync(UpdateContactInfoSettingsRequest newContactInfoSettings)
{
const string url = "api/user/setting/contact";
return _crypterAuthenticatedHttpClient
Expand All @@ -83,8 +80,7 @@ public Task<Maybe<NotificationSettings>> GetNotificationSettingsAsync()
return _crypterAuthenticatedHttpClient.GetMaybeAsync<NotificationSettings>(url);
}

public Task<Either<UpdateNotificationSettingsError, NotificationSettings>> UpdateNotificationSettingsAsync(
NotificationSettings newNotificationSettings)
public Task<Either<UpdateNotificationSettingsError, NotificationSettings>> UpdateNotificationSettingsAsync(NotificationSettings newNotificationSettings)
{
const string url = "api/user/setting/notification";
return _crypterAuthenticatedHttpClient
Expand All @@ -98,16 +94,14 @@ public Task<Maybe<PrivacySettings>> GetPrivacySettingsAsync()
return _crypterAuthenticatedHttpClient.GetMaybeAsync<PrivacySettings>(url);
}

public Task<Either<SetPrivacySettingsError, PrivacySettings>> SetPrivacySettingsAsync(
PrivacySettings newPrivacySettings)
public Task<Either<SetPrivacySettingsError, PrivacySettings>> SetPrivacySettingsAsync(PrivacySettings newPrivacySettings)
{
const string url = "api/user/setting/privacy";
return _crypterAuthenticatedHttpClient.PutEitherAsync<PrivacySettings, PrivacySettings>(url, newPrivacySettings)
.ExtractErrorCode<SetPrivacySettingsError, PrivacySettings>();
}

public Task<Either<VerifyEmailAddressError, Unit>> VerifyUserEmailAddressAsync(
VerifyEmailAddressRequest verificationInfo)
public Task<Either<VerifyEmailAddressError, Unit>> VerifyUserEmailAddressAsync(VerifyEmailAddressRequest verificationInfo)
{
const string url = "api/user/setting/contact/verify";
return _crypterHttpClient.PostEitherUnitResponseAsync(url, verificationInfo)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,12 @@ Task<Either<ErrorResponse, TResponse>> GetEitherAsync<TResponse>(string uri)
Task<Either<ErrorResponse, TResponse>> PostEitherAsync<TRequest, TResponse>(string uri, TRequest body)
where TRequest : class
where TResponse : class;

Task<Maybe<Unit>> PostMaybeUnitResponseAsync(string uri);

Task<Maybe<Unit>> PostMaybeUnitResponseAsync<TRequest>(string uri, TRequest body)
where TRequest : class;

Task<Either<ErrorResponse, Unit>> PostEitherUnitResponseAsync(string uri);

Task<Either<ErrorResponse, Unit>> PostEitherUnitResponseAsync<TRequest>(string uri, TRequest body)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Crypter File Transfer
* Copyright (C) 2025 Crypter File Transfer
*
* This file is part of the Crypter file transfer project.
*
Expand All @@ -24,12 +24,16 @@
* Contact the current copyright holder to discuss commercial license options.
*/

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Crypter.Common.Contracts.Features.UserConsents;
using EasyMonads;

namespace Crypter.Common.Client.Interfaces.Requests;

public interface IUserConsentRequests
{
Task<Maybe<Unit>> ConsentToRecoveryKeyRisksAsync();
Task<Maybe<Dictionary<UserConsentType, DateTimeOffset?>>> GetUserConsentsAsync();
Task<Maybe<Unit>> ConsentAsync(UserConsentType consentType);
}
14 changes: 11 additions & 3 deletions Crypter.Common.Client/Services/UserSessionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
using Crypter.Common.Client.Interfaces.Services;
using Crypter.Common.Client.Models;
using Crypter.Common.Contracts.Features.UserAuthentication;
using Crypter.Common.Contracts.Features.UserConsents;
using Crypter.Common.Enums;
using Crypter.Common.Primitives;
using EasyMonads;
Expand Down Expand Up @@ -144,9 +145,16 @@ public Task<Either<LoginError, Unit>> LoginAsync(Username username, Password pas
from unit0 in Either<LoginError, Unit>.FromRightAsync(StoreSessionInfo(loginResponse, rememberUser))
select loginResponse;

Either<LoginError, LoginResponse> loginResult = await loginTask;
loginResult.DoRight(x => HandleUserLoggedInEvent(username, password, versionedPassword, rememberUser, x.ShowRecoveryKey));
return loginResult.Map(_ => Unit.Default);
return await loginTask
.DoRightAsync(async x =>
{
bool showRecoveryKeyModal = await _crypterApiClient.UserConsent.GetUserConsentsAsync()
.MatchAsync(
none: () => false,
some: y => y.TryGetValue(UserConsentType.RecoveryKeyRisks, out DateTimeOffset? value) && !value.HasValue);
HandleUserLoggedInEvent(username, password, versionedPassword, rememberUser, showRecoveryKeyModal);
})
.BindAsync<LoginError, LoginResponse, Unit>(_ => Unit.Default);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,12 @@ public class LoginResponse
public string Username { get; init; }
public string AuthenticationToken { get; init; }
public string RefreshToken { get; init; }
public bool ShowRecoveryKey { get; init; }

[JsonConstructor]
public LoginResponse(string username, string authenticationToken, string refreshToken, bool showRecoveryKey)
public LoginResponse(string username, string authenticationToken, string refreshToken)
{
Username = username;
AuthenticationToken = authenticationToken;
RefreshToken = refreshToken;
ShowRecoveryKey = showRecoveryKey;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (C) 2025 Crypter File Transfer
*
* This file is part of the Crypter file transfer project.
*
* Crypter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Crypter source code is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the aforementioned license
* by purchasing a commercial license. Buying such a license is mandatory
* as soon as you develop commercial activities involving the Crypter source
* code without disclosing the source code of your own applications.
*
* Contact the current copyright holder to discuss commercial license options.
*/

using System.Text.Json.Serialization;

namespace Crypter.Common.Contracts.Features.UserConsents;

public sealed record UserConsentRequest
{
public UserConsentType ConsentType { get; init; }

[JsonConstructor]
public UserConsentRequest(UserConsentType consentType)
{
ConsentType = consentType;
}
}
35 changes: 35 additions & 0 deletions Crypter.Common/Contracts/Features/UserConsents/UserConsentType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (C) 2025 Crypter File Transfer
*
* This file is part of the Crypter file transfer project.
*
* Crypter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Crypter source code is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* You can be released from the requirements of the aforementioned license
* by purchasing a commercial license. Buying such a license is mandatory
* as soon as you develop commercial activities involving the Crypter source
* code without disclosing the source code of your own applications.
*
* Contact the current copyright holder to discuss commercial license options.
*/

namespace Crypter.Common.Contracts.Features.UserConsents;

public enum UserConsentType
{

TermsOfService,
PrivacyPolicy,
RecoveryKeyRisks
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Crypter File Transfer
* Copyright (C) 2025 Crypter File Transfer
*
* This file is part of the Crypter file transfer project.
*
Expand Down Expand Up @@ -29,6 +29,7 @@
using System.Threading;
using System.Threading.Tasks;
using Crypter.Common.Contracts.Features.AccountRecovery.SubmitRecovery;
using Crypter.Common.Contracts.Features.UserConsents;
using Crypter.Common.Infrastructure;
using Crypter.Common.Primitives;
using Crypter.Core.Features.AccountRecovery.Events;
Expand Down Expand Up @@ -163,8 +164,8 @@ await result.DoRightAsync(async recoveryResult =>

UserConsentEntity? latestRecoveryConsent = await _dataContext.UserConsents
.Where(x => x.Owner == user.Id)
.Where(x => x.ConsentType == ConsentType.RecoveryKeyRisks)
.OrderBy(x => x.Created)
.Where(x => x.ConsentType == UserConsentType.RecoveryKeyRisks)
.OrderBy(x => x.Activated)
.FirstOrDefaultAsync();

if (latestRecoveryConsent is not null)
Expand Down
Loading

0 comments on commit bf2806d

Please sign in to comment.