diff --git a/README.md b/README.md index 67e7cb3b..79ae1bde 100644 --- a/README.md +++ b/README.md @@ -139,11 +139,11 @@ string emailAddressOrErrorMessage = userResponse.Result.Match( ## ⌛ Progress ### Server & Client -![16 / 288](https://progress-bar.dev/16/?scale=288&suffix=%20/%20288&width=500) +![17 / 288](https://progress-bar.dev/17/?scale=288&suffix=%20/%20288&width=500) ### Server Only ![2 / 195](https://progress-bar.dev/2/?scale=195&suffix=%20/%20195&width=300) ### Client Only -![14 / 93](https://progress-bar.dev/14/?scale=93&suffix=%20/%2093&width=300) +![15 / 93](https://progress-bar.dev/15/?scale=93&suffix=%20/%2093&width=300) ### 🔑 Key | Icon | Definition | @@ -153,7 +153,7 @@ string emailAddressOrErrorMessage = userResponse.Result.Match( | ❌ | There is currently no intention to implement the endpoint for the given SDK type (client or server) | ### Account -![16 / 52](https://progress-bar.dev/16/?scale=52&suffix=%20/%2052&width=120) +![17 / 52](https://progress-bar.dev/17/?scale=52&suffix=%20/%2052&width=120) | Endpoint | Client | Server | |:-:|:-:|:-:| @@ -162,7 +162,7 @@ string emailAddressOrErrorMessage = userResponse.Result.Match( | [Update Email](https://appwrite.io/docs/references/1.5.x/client-rest/account#updateEmail) | ✅ | ❌ | | [List Identities](https://appwrite.io/docs/references/1.5.x/client-rest/account#listIdentities) | ⬛ | ❌ | | [Delete Identity](https://appwrite.io/docs/references/1.5.x/client-rest/account#deleteIdentity) | ⬛ | ❌ | -| [Create JWT](https://appwrite.io/docs/references/1.5.x/client-rest/account#createJWT) | ⬛ | ❌ | +| [Create JWT](https://appwrite.io/docs/references/1.5.x/client-rest/account#createJWT) | ✅ | ❌ | | [List Logs](https://appwrite.io/docs/references/1.5.x/client-rest/account#listLogs) | ⬛ | ❌ | | [Update MFA](https://appwrite.io/docs/references/1.5.x/client-rest/account#updateMFA) | ⬛ | ❌ | | [Add Authenticator](https://appwrite.io/docs/references/1.5.x/client-rest/account#createMfaAuthenticator) | ⬛ | ❌ | diff --git a/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs b/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs index 15f4cb27..8b18bacc 100644 --- a/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/AccountClient.cs @@ -263,4 +263,19 @@ public async Task> CreateEmailVerificationConfirmation(Cre return e.GetExceptionResponse(); } } + + /// + public async Task> CreateJwt() + { + try + { + var result = await _accountApi.CreateJwt(Session); + + 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 bbac581b..1f62609b 100644 --- a/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs @@ -125,4 +125,11 @@ public interface IAccountClient /// The request content /// The token Task> CreateEmailVerificationConfirmation(CreateEmailVerificationConfirmationRequest request); + + /// + /// Use this endpoint to create a JSON Web Token. You can use the resulting JWT to authenticate on behalf of the current user when working with the Appwrite server-side API and SDKs. The JWT secret is valid for 15 minutes from its creation and will be invalid if the user will logout in that time frame. + /// Appwrite Docs + /// + /// The JWT + Task> CreateJwt(); } diff --git a/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs b/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs index d93e4851..4a6f33ee 100644 --- a/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs +++ b/src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs @@ -49,4 +49,7 @@ internal interface IAccountApi : IBaseApi [Put("/account/verification")] Task> CreateEmailVerificationConfirmation(CreateEmailVerificationConfirmationRequest request); + + [Post("/account/jwt")] + Task> CreateJwt([Header("x-appwrite-session")] string? session); } diff --git a/src/PinguApps.Appwrite.Playground/App.cs b/src/PinguApps.Appwrite.Playground/App.cs index f1983854..2a244ebf 100644 --- a/src/PinguApps.Appwrite.Playground/App.cs +++ b/src/PinguApps.Appwrite.Playground/App.cs @@ -26,15 +26,7 @@ public async Task Run(string[] args) Url = "https://localhost:5001/abc123" }; - var response = await _client.Account.CreateEmailVerification(request); - - //var request = new CreateEmailVerificationConfirmationRequest - //{ - // Secret = "secret", - // UserId = "userId" - //}; - - //var response = await _client.Account.CreateEmailVerificationConfirmation(request); + var response = await _client.Account.CreateJwt(); Console.WriteLine(response.Result.Match( account => account.ToString(), diff --git a/src/PinguApps.Appwrite.Shared/Responses/Jwt.cs b/src/PinguApps.Appwrite.Shared/Responses/Jwt.cs new file mode 100644 index 00000000..9a49f2a4 --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Responses/Jwt.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace PinguApps.Appwrite.Shared.Responses; + +/// +/// A JWT response object +/// +/// JWT encoded string +public record Jwt( + [property: JsonPropertyName("jwt")] string Token +); diff --git a/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.CreateJwt.cs b/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.CreateJwt.cs new file mode 100644 index 00000000..20166795 --- /dev/null +++ b/tests/PinguApps.Appwrite.Client.Tests/Clients/Account/AccountClientTests.CreateJwt.cs @@ -0,0 +1,61 @@ +using System.Net; +using PinguApps.Appwrite.Shared.Tests; +using RichardSzalay.MockHttp; + +namespace PinguApps.Appwrite.Client.Tests.Clients.Account; +public partial class AccountClientTests +{ + [Fact] + public async Task CreateJwt_ShouldReturnSuccess_WhenApiCallSucceeds() + { + // Arrange + _mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/jwt") + .ExpectedHeaders(true) + .Respond(Constants.AppJson, Constants.JwtResponse); + + _appwriteClient.SetSession(Constants.Session); + + // Act + var result = await _appwriteClient.Account.CreateJwt(); + + // Assert + Assert.True(result.Success); + } + + [Fact] + public async Task CreateJwt_ShouldHandleException_WhenApiCallFails() + { + // Arrange + _mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/jwt") + .ExpectedHeaders(true) + .Respond(HttpStatusCode.BadRequest, Constants.AppJson, Constants.AppwriteError); + + _appwriteClient.SetSession(Constants.Session); + + // Act + var result = await _appwriteClient.Account.CreateJwt(); + + // Assert + Assert.True(result.IsError); + Assert.True(result.IsAppwriteError); + } + + [Fact] + public async Task CreateJwt_ShouldReturnErrorResponse_WhenExceptionOccurs() + { + // Arrange + _mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/jwt") + .ExpectedHeaders(true) + .Throw(new HttpRequestException("An error occurred")); + + _appwriteClient.SetSession(Constants.Session); + + // Act + var result = await _appwriteClient.Account.CreateJwt(); + + // Assert + Assert.False(result.Success); + Assert.True(result.IsInternalError); + Assert.Equal("An error occurred", result.Result.AsT2.Message); + } +} diff --git a/tests/PinguApps.Appwrite.Shared.Tests/Constants.cs b/tests/PinguApps.Appwrite.Shared.Tests/Constants.cs index aefef2fb..d99a2027 100644 --- a/tests/PinguApps.Appwrite.Shared.Tests/Constants.cs +++ b/tests/PinguApps.Appwrite.Shared.Tests/Constants.cs @@ -126,4 +126,10 @@ public static class Constants "mfaUpdatedAt": "2020-10-15T06:38:00.000+00:00" } """; + + public const string JwtResponse = """ + { + "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + } + """; } diff --git a/tests/PinguApps.Appwrite.Shared.Tests/Responses/JwtTests.cs b/tests/PinguApps.Appwrite.Shared.Tests/Responses/JwtTests.cs new file mode 100644 index 00000000..23698ceb --- /dev/null +++ b/tests/PinguApps.Appwrite.Shared.Tests/Responses/JwtTests.cs @@ -0,0 +1,30 @@ +using System.Text.Json; +using PinguApps.Appwrite.Shared.Responses; + +namespace PinguApps.Appwrite.Shared.Tests.Responses; +public class JwtTests +{ + [Fact] + public void Token_Constructor_AssignsPropertiesCorrectly() + { + // Arrange + var token = "testToken"; + + // Act + var jwt = new Jwt(token); + + // Assert + Assert.Equal(token, jwt.Token); + } + + [Fact] + public void Token_CanBeDeserialized_FromJson() + { + // Act + var jwt = JsonSerializer.Deserialize(Constants.JwtResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + // Assert + Assert.NotNull(jwt); + Assert.Equal("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", jwt.Token); + } +} diff --git a/tests/PinguApps.Appwrite.Shared.Tests/Responses/TokenTests.cs b/tests/PinguApps.Appwrite.Shared.Tests/Responses/TokenTests.cs index 31fb7547..c219039f 100644 --- a/tests/PinguApps.Appwrite.Shared.Tests/Responses/TokenTests.cs +++ b/tests/PinguApps.Appwrite.Shared.Tests/Responses/TokenTests.cs @@ -15,7 +15,6 @@ public void Token_Constructor_AssignsPropertiesCorrectly() var expiresAt = DateTime.UtcNow; var phrase = "phrase"; - // Act var token = new Token(id, createdAt, userId, secret, expiresAt, phrase);