From c33d1de2e64c45da937393b304e0a29ed3d36e4b Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Sat, 10 Aug 2024 17:23:00 +0100 Subject: [PATCH 1/4] Implemented Create Mfa Recovery codes --- .../Clients/AccountClient.cs | 15 +++++++++++++++ .../Clients/IAccountClient.cs | 7 +++++++ .../Internals/IAccountApi.cs | 3 +++ src/PinguApps.Appwrite.Playground/App.cs | 2 +- .../Responses/MfaRecoveryCodes.cs | 12 ++++++++++++ 5 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 src/PinguApps.Appwrite.Shared/Responses/MfaRecoveryCodes.cs diff --git a/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs b/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs index 470d93d6..af5f6c84 100644 --- a/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs @@ -420,4 +420,19 @@ public async Task> ListFactors() return e.GetExceptionResponse(); } } + + /// + public async Task> CreateMfaRecoveryCodes() + { + try + { + var result = await _accountApi.CreateMfaRecoveryCodes(GetCurrentSessionOrThrow()); + + return result.GetApiResponse(); + } + catch (Exception e) + { + return e.GetExceptionResponse(); + } + } } diff --git a/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs b/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs index 4d6ef0f0..721b7f3d 100644 --- a/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs @@ -197,4 +197,11 @@ public interface IAccountClient /// /// The Mfa Factors Task> ListFactors(); + + /// + /// Generate recovery codes as backup for MFA flow. It's recommended to generate and show then immediately after user successfully adds their authenticator. Recovery codes can be used as a MFA verification type in + /// Appwrite Docs + /// + /// The Mfa Recovery Codes + Task> CreateMfaRecoveryCodes(); } diff --git a/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs b/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs index 7a54c72e..db93820f 100644 --- a/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs +++ b/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs @@ -77,4 +77,7 @@ internal interface IAccountApi : IBaseApi [Get("/account/mfa/factors")] Task> ListFactors([Header("x-appwrite-session")] string session); + + [Post("/account/mfa/recovery-codes")] + Task> CreateMfaRecoveryCodes([Header("x-appwrite-session")] string session); } diff --git a/src/PinguApps.Appwrite.Playground/App.cs b/src/PinguApps.Appwrite.Playground/App.cs index 499f8f05..dba39cd6 100644 --- a/src/PinguApps.Appwrite.Playground/App.cs +++ b/src/PinguApps.Appwrite.Playground/App.cs @@ -20,7 +20,7 @@ public async Task Run(string[] args) { _client.SetSession(_session); - var response = await _client.Account.ListFactors(); + var response = await _client.Account.CreateMfaRecoveryCodes(); Console.WriteLine(response.Result.Match( account => account.ToString(), diff --git a/src/PinguApps.Appwrite.Shared/Responses/MfaRecoveryCodes.cs b/src/PinguApps.Appwrite.Shared/Responses/MfaRecoveryCodes.cs new file mode 100644 index 00000000..2e2b4417 --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Responses/MfaRecoveryCodes.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace PinguApps.Appwrite.Shared.Responses; + +/// +/// An Appwrite Mfa Recovery Codes object +/// +/// Recovery codes +public record MfaRecoveryCodes( + [property: JsonPropertyName("recoveryCodes")] IReadOnlyList RecoveryCodes +); From f38aec58325966545e33acac72aa91ef626fae0f Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Sat, 10 Aug 2024 22:42:17 +0100 Subject: [PATCH 2/4] added shared tests --- .../Responses/MfaRecoveryCodesTests.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/PinguApps.Appwrite.Shared.Tests/Responses/MfaRecoveryCodesTests.cs diff --git a/tests/PinguApps.Appwrite.Shared.Tests/Responses/MfaRecoveryCodesTests.cs b/tests/PinguApps.Appwrite.Shared.Tests/Responses/MfaRecoveryCodesTests.cs new file mode 100644 index 00000000..5d3d99c0 --- /dev/null +++ b/tests/PinguApps.Appwrite.Shared.Tests/Responses/MfaRecoveryCodesTests.cs @@ -0,0 +1,34 @@ +using System.Text.Json; +using PinguApps.Appwrite.Shared.Responses; + +namespace PinguApps.Appwrite.Shared.Tests.Responses; +public class MfaRecoveryCodesTests +{ + [Fact] + public void Constructor_AssignsPropertiesCorrectly() + { + // Arrange + var code1 = "a3kf0-s0cl2"; + var code2 = "s0co1-as98s"; + + // Act + var mfaFactors = new MfaRecoveryCodes([code1, code2]); + + // Assert + Assert.Collection(mfaFactors.RecoveryCodes, x => Assert.Equal(code1, x), x => Assert.Equal(code2, x)); + } + + [Fact] + public void CanBeDeserialized_FromJson() + { + // Act + var mfaFactors = JsonSerializer.Deserialize(Constants.MfaFactorsResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + // Assert + Assert.NotNull(mfaFactors); + Assert.True(mfaFactors.Totp); + Assert.True(mfaFactors.Phone); + Assert.True(mfaFactors.Email); + Assert.True(mfaFactors.RecoveryCode); + } +} From 946fe8553562837be468d1d5532d6ca8ed1d4d9c Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Sat, 10 Aug 2024 22:45:46 +0100 Subject: [PATCH 3/4] Created client tests --- ...countClientTests.CreateMfaRecoveryCodes.cs | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.CreateMfaRecoveryCodes.cs diff --git a/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.CreateMfaRecoveryCodes.cs b/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.CreateMfaRecoveryCodes.cs new file mode 100644 index 00000000..2be9b3c3 --- /dev/null +++ b/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.CreateMfaRecoveryCodes.cs @@ -0,0 +1,74 @@ +using System.Net; +using PinguApps.Appwrite.Client.Clients; +using PinguApps.Appwrite.Shared.Tests; +using RichardSzalay.MockHttp; + +namespace PinguApps.Appwrite.Client.Tests.Clients.Account; +public partial class AccountClientTests +{ + [Fact] + public async Task CreateMfaRecoveryCodes_ShouldReturnSuccess_WhenApiCallSucceeds() + { + // Arrange + _mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/mfa/recovery-codes") + .ExpectedHeaders(true) + .Respond(Constants.AppJson, Constants.JwtResponse); + + _appwriteClient.SetSession(Constants.Session); + + // Act + var result = await _appwriteClient.Account.CreateMfaRecoveryCodes(); + + // Assert + Assert.True(result.Success); + } + + [Fact] + public async Task CreateMfaRecoveryCodes_ShouldReturnError_WhenSessionIsNull() + { + // Act + var result = await _appwriteClient.Account.CreateMfaRecoveryCodes(); + + // Assert + Assert.True(result.IsError); + Assert.True(result.IsInternalError); + Assert.Equal(ISessionAware.SessionExceptionMessage, result.Result.AsT2.Message); + } + + [Fact] + public async Task CreateMfaRecoveryCodes_ShouldHandleException_WhenApiCallFails() + { + // Arrange + _mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/mfa/recovery-codes") + .ExpectedHeaders(true) + .Respond(HttpStatusCode.BadRequest, Constants.AppJson, Constants.AppwriteError); + + _appwriteClient.SetSession(Constants.Session); + + // Act + var result = await _appwriteClient.Account.CreateMfaRecoveryCodes(); + + // Assert + Assert.True(result.IsError); + Assert.True(result.IsAppwriteError); + } + + [Fact] + public async Task CreateMfaRecoveryCodes_ShouldReturnErrorResponse_WhenExceptionOccurs() + { + // Arrange + _mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/mfa/recovery-codes") + .ExpectedHeaders(true) + .Throw(new HttpRequestException("An error occurred")); + + _appwriteClient.SetSession(Constants.Session); + + // Act + var result = await _appwriteClient.Account.CreateMfaRecoveryCodes(); + + // Assert + Assert.False(result.Success); + Assert.True(result.IsInternalError); + Assert.Equal("An error occurred", result.Result.AsT2.Message); + } +} From 9fef88cf6f196f944df0e03aeba852166be4afd3 Mon Sep 17 00:00:00 2001 From: Matthew Parker Date: Sat, 10 Aug 2024 22:46:57 +0100 Subject: [PATCH 4/4] Update README.md --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 2e6a52d9..bc8e43d1 100644 --- a/README.md +++ b/README.md @@ -138,14 +138,14 @@ string emailAddressOrErrorMessage = userResponse.Result.Match( ``` ## ⌛ Progress - -![Server & Client - 25 / 288](https://img.shields.io/badge/Server_&_Client-25%20%2F%20288-red?style=for-the-badge) + +![Server & Client - 26 / 288](https://img.shields.io/badge/Server_&_Client-26%20%2F%20288-red?style=for-the-badge) ![Server - 2 / 195](https://img.shields.io/badge/Server-2%20%2F%20195-red?style=for-the-badge) - -![Client - 23 / 93](https://img.shields.io/badge/Client-23%20%2F%2093-red?style=for-the-badge) + +![Client - 24 / 93](https://img.shields.io/badge/Client-24%20%2F%2093-red?style=for-the-badge) ### 🔑 Key | Icon | Definition | @@ -155,8 +155,8 @@ string emailAddressOrErrorMessage = userResponse.Result.Match( | ❌ | There is currently no intention to implement the endpoint for the given SDK type (client or server) | ### Account - -![Account - 25 / 52](https://img.shields.io/badge/Account-25%20%2F%2052-yellow?style=for-the-badge) + +![Account - 26 / 52](https://img.shields.io/badge/Account-26%20%2F%2052-yellow?style=for-the-badge) | Endpoint | Client | Server | |:-:|:-:|:-:| @@ -175,7 +175,7 @@ string emailAddressOrErrorMessage = userResponse.Result.Match( | [Create MFA Challenge (confirmation)](https://appwrite.io/docs/references/1.5.x/client-rest/account#updateMfaChallenge) | ✅ | ❌ | | [List Factors](https://appwrite.io/docs/references/1.5.x/client-rest/account#listMfaFactors) | ✅ | ❌ | | [Get MFA Recovery Codes](https://appwrite.io/docs/references/1.5.x/client-rest/account#getMfaRecoveryCodes) | ⬛ | ❌ | -| [Create MFA Recovery Codes](https://appwrite.io/docs/references/1.5.x/client-rest/account#createMfaRecoveryCodes) | ⬛ | ❌ | +| [Create MFA Recovery Codes](https://appwrite.io/docs/references/1.5.x/client-rest/account#createMfaRecoveryCodes) | ✅ | ❌ | | [Regenerate MFA Recovery Codes](https://appwrite.io/docs/references/1.5.x/client-rest/account#updateMfaRecoveryCodes) | ⬛ | ❌ | | [Update Name](https://appwrite.io/docs/references/1.5.x/client-rest/account#updateName) | ✅ | ❌ | | [Update Password](https://appwrite.io/docs/references/1.5.x/client-rest/account#updatePassword) | ✅ | ❌ |