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

Implemented create magic url #71

Merged
merged 9 commits into from
Jul 13, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,11 @@ string emailAddressOrErrorMessage = userResponse.Result.Match(

## ⌛ Progress
### Server & Client
![9 / 298](https://progress-bar.dev/9/?scale=298&suffix=%20/%20298&width=500)
![11 / 298](https://progress-bar.dev/11/?scale=298&suffix=%20/%20298&width=500)
### Server Only
![1 / 195](https://progress-bar.dev/1/?scale=195&suffix=%20/%20195&width=300)
![2 / 195](https://progress-bar.dev/2/?scale=195&suffix=%20/%20195&width=300)
### Client Only
![8 / 93](https://progress-bar.dev/8/?scale=93&suffix=%20/%2093&width=300)
![9 / 93](https://progress-bar.dev/9/?scale=93&suffix=%20/%2093&width=300)

### 🔑 Key
| Icon | Definition |
Expand All @@ -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
![9 / 52](https://progress-bar.dev/9/?scale=52&suffix=%20/%2052&width=120)
![11 / 52](https://progress-bar.dev/11/?scale=52&suffix=%20/%2052&width=120)

| Endpoint | Client | Server |
|:-:|:-:|:-:|
Expand Down Expand Up @@ -196,7 +196,7 @@ string emailAddressOrErrorMessage = userResponse.Result.Match(
| [Create Push Target](https://appwrite.io/docs/references/1.5.x/client-rest/account#createPushTarget) | ⬛ | ❌ |
| [Update Push Target](https://appwrite.io/docs/references/1.5.x/client-rest/account#updatePushTarget) | ⬛ | ❌ |
| [Delete Push Target](https://appwrite.io/docs/references/1.5.x/client-rest/account#deletePushTarget) | ⬛ | ❌ |
| [Create Email Token (OTP)](https://appwrite.io/docs/references/1.5.x/client-rest/account#createEmailToken) | | |
| [Create Email Token (OTP)](https://appwrite.io/docs/references/1.5.x/client-rest/account#createEmailToken) | | |
| [Create Magic URL Token](https://appwrite.io/docs/references/1.5.x/client-rest/account#createMagicURLToken) | ⬛ | ⬛ |
| [Create OAuth2 Token](https://appwrite.io/docs/references/1.5.x/client-rest/account#createOAuth2Token) | ⬛ | ⬛ |
| [Create Phone Token](https://appwrite.io/docs/references/1.5.x/client-rest/account#createPhoneToken) | ⬛ | ⬛ |
Expand Down
17 changes: 17 additions & 0 deletions src/PinguApps.Appwrite.Client/Clients/AccountClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,21 @@ public async Task<AppwriteResult<User>> UpdatePreferences(UpdatePreferencesReque
return e.GetExceptionResponse<User>();
}
}

/// <inheritdoc/>
public async Task<AppwriteResult<Token>> CreateEmailToken(CreateEmailTokenRequest request)
{
try
{
request.Validate(true);

var result = await _accountApi.CreateEmailToken(request);

return result.GetApiResponse();
}
catch (Exception e)
{
return e.GetExceptionResponse<Token>();
}
}
}
9 changes: 9 additions & 0 deletions src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,13 @@ public interface IAccountClient
/// <param name="preferences">The request content</param>
/// <returns>The user</returns>
Task<AppwriteResult<User>> UpdatePreferences(UpdatePreferencesRequest request);

/// <summary>
/// <para>Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the Create Session endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.</para>
/// <para>A user is limited to 10 active sessions at a time by default. <see href="https://appwrite.io/docs/products/auth/security#limits">Learn more about session limits.</see></para>
/// <para><see href="https://appwrite.io/docs/references/1.5.x/client-rest/account#createEmailToken">Appwrite Docs</see></para>
/// </summary>
/// <param name="request">The request content</param>
/// <returns>The token</returns>
Task<AppwriteResult<Token>> CreateEmailToken(CreateEmailTokenRequest request);
}
3 changes: 3 additions & 0 deletions src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,7 @@ internal interface IAccountApi : IBaseApi

[Patch("/account/prefs")]
Task<IApiResponse<User>> UpdatePreferences([Header("x-appwrite-session")] string? session, UpdatePreferencesRequest request);

[Post("/account/tokens/email")]
Task<IApiResponse<Token>> CreateEmailToken(CreateEmailTokenRequest request);
}
20 changes: 6 additions & 14 deletions src/PinguApps.Appwrite.Playground/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,16 @@ public App(IAppwriteClient client, IAppwriteServer server, IConfiguration config

public async Task Run(string[] args)
{
_client.SetSession(_session);
//_client.SetSession(_session);

var request = new UpdatePhoneRequest
var request = new CreateEmailTokenRequest
{
Password = "sword",
Phone = "14155552671"
Email = "[email protected]",
UserId = "664aac1a00113f82e620",
Phrase = true
};

var f = request.IsValid();

var result = await _client.Account.UpdatePreferences(new UpdatePreferencesRequest
{
Preferences = new Dictionary<string, string>
{
{ "key1", "val1" },
{ "key2", "val2" }
}
});
var result = await _server.Account.CreateEmailToken(request);

result.Result.Switch(
account => Console.WriteLine(string.Join(',', account)),
Expand Down
3 changes: 3 additions & 0 deletions src/PinguApps.Appwrite.Server/Internals/IAccountApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ internal interface IAccountApi : IBaseApi
{
[Post("/account")]
Task<IApiResponse<User>> CreateAccount(CreateAccountRequest request);

[Post("/account/tokens/email")]
Task<IApiResponse<Token>> CreateEmailToken(CreateEmailTokenRequest request);
}
18 changes: 18 additions & 0 deletions src/PinguApps.Appwrite.Server/Servers/AccountServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public AccountServer(IServiceProvider services)
_accountApi = services.GetRequiredService<IAccountApi>();
}

/// <inheritdoc/>
public async Task<AppwriteResult<User>> Create(CreateAccountRequest request)
{
try
Expand All @@ -32,4 +33,21 @@ public async Task<AppwriteResult<User>> Create(CreateAccountRequest request)
return e.GetExceptionResponse<User>();
}
}

/// <inheritdoc/>
public async Task<AppwriteResult<Token>> CreateEmailToken(CreateEmailTokenRequest request)
{
try
{
request.Validate(true);

var result = await _accountApi.CreateEmailToken(request);

return result.GetApiResponse();
}
catch (Exception e)
{
return e.GetExceptionResponse<Token>();
}
}
}
9 changes: 9 additions & 0 deletions src/PinguApps.Appwrite.Server/Servers/IAccountServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,13 @@ public interface IAccountServer
/// <param name="request">The request content</param>
/// <returns>The created user</returns>
Task<AppwriteResult<User>> Create(CreateAccountRequest request);

/// <summary>
/// <para>Sends the user an email with a secret key for creating a session. If the provided user ID has not be registered, a new user will be created. Use the returned user ID and secret and submit a request to the Create Session endpoint to complete the login process. The secret sent to the user's email is valid for 15 minutes.</para>
/// <para>A user is limited to 10 active sessions at a time by default. <see href="https://appwrite.io/docs/products/auth/security#limits">Learn more about session limits.</see></para>
/// <para><see href="https://appwrite.io/docs/references/1.5.x/server-rest/account#createEmailToken">Appwrite Docs</see></para>
/// </summary>
/// <param name="request">The request content</param>
/// <returns>The token</returns>
Task<AppwriteResult<Token>> CreateEmailToken(CreateEmailTokenRequest request);
}
29 changes: 29 additions & 0 deletions src/PinguApps.Appwrite.Shared/Requests/CreateEmailTokenRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Text.Json.Serialization;
using PinguApps.Appwrite.Shared.Requests.Validators;
using PinguApps.Appwrite.Shared.Utils;

namespace PinguApps.Appwrite.Shared.Requests;

/// <summary>
/// The request for creating an email token
/// </summary>
public class CreateEmailTokenRequest : BaseRequest<CreateEmailTokenRequest, CreateEmailTokenRequestValidator>
{
/// <summary>
/// User ID. Choose a custom ID or generate a random ID with <see cref="IdUtils.GenerateUniqueId(int)"/>. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars
/// </summary>
[JsonPropertyName("userId")]
public string UserId { get; set; } = IdUtils.GenerateUniqueId();

/// <summary>
/// User email
/// </summary>
[JsonPropertyName("email")]
public string Email { get; set; } = string.Empty;

/// <summary>
/// Toggle for security phrase. If enabled, email will be send with a randomly generated phrase and the phrase will also be included in the response. Confirming phrases match increases the security of your authentication flow.
/// </summary>
[JsonPropertyName("phrase")]
public bool Phrase { get; set; } = false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using FluentValidation;

namespace PinguApps.Appwrite.Shared.Requests.Validators;
public class CreateEmailTokenRequestValidator : AbstractValidator<CreateEmailTokenRequest>
{
public CreateEmailTokenRequestValidator()
{
RuleFor(x => x.UserId).NotEmpty().Matches("^[a-zA-Z0-9][a-zA-Z0-9._-]{0,35}$");
RuleFor(x => x.Email).NotEmpty().EmailAddress();
}
}
22 changes: 22 additions & 0 deletions src/PinguApps.Appwrite.Shared/Responses/Token.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using System.Text.Json.Serialization;

namespace PinguApps.Appwrite.Shared.Responses;

/// <summary>
/// An Appwrite Token object
/// </summary>
/// <param name="Id">Token ID</param>
/// <param name="CreatedAt">Token creation date in ISO 8601 format</param>
/// <param name="UserId">User ID</param>
/// <param name="Secret">Token secret key. This will return an empty string unless the response is returned using an API key or as part of a webhook payload</param>
/// <param name="ExpiresAt">Token expiration date in ISO 8601 format</param>
/// <param name="Phrase">Security phrase of a token. Empty if security phrase was not requested when creating a token. It includes randomly generated phrase which is also sent in the external resource such as email</param>
public record Token(
[property: JsonPropertyName("$id")] string Id,
[property: JsonPropertyName("$createdAt")] DateTime CreatedAt,
[property: JsonPropertyName("userId")] string UserId,
[property: JsonPropertyName("secret")] string Secret,
[property: JsonPropertyName("expire")] DateTime ExpiresAt,
[property: JsonPropertyName("phrase")] string Phrase
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System.Net;
using PinguApps.Appwrite.Shared.Requests;
using PinguApps.Appwrite.Shared.Tests;
using RichardSzalay.MockHttp;

namespace PinguApps.Appwrite.Client.Tests.Clients.Account;
public partial class AccountClientTests
{
[Fact]
public async Task CreateEmailToken_ShouldReturnSuccess_WhenApiCallSucceeds()
{
// Arrange
var request = new CreateEmailTokenRequest()
{
UserId = "123456",
Email = "[email protected]"
};

_mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/tokens/email")
.ExpectedHeaders()
.WithJsonContent(request)
.Respond(Constants.AppJson, Constants.TokenResponse);

// Act
var result = await _appwriteClient.Account.CreateEmailToken(request);

// Assert
Assert.True(result.Success);
}

[Fact]
public async Task CreateEmailToken_ShouldHandleException_WhenApiCallFails()
{
// Arrange
var request = new CreateEmailTokenRequest()
{
UserId = "123456",
Email = "[email protected]"
};

_mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/tokens/email")
.ExpectedHeaders()
.WithJsonContent(request)
.Respond(HttpStatusCode.BadRequest, Constants.AppJson, Constants.AppwriteError);

// Act
var result = await _appwriteClient.Account.CreateEmailToken(request);

// Assert
Assert.True(result.IsError);
Assert.True(result.IsAppwriteError);
}

[Fact]
public async Task CreateEmailToken_ShouldReturnErrorResponse_WhenExceptionOccurs()
{
// Arrange
var request = new CreateEmailTokenRequest()
{
UserId = "123456",
Email = "[email protected]"
};

_mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/tokens/email")
.ExpectedHeaders()
.WithJsonContent(request)
.Throw(new HttpRequestException("An error occurred"));

// Act
var result = await _appwriteClient.Account.CreateEmailToken(request);

// Assert
Assert.False(result.Success);
Assert.True(result.IsInternalError);
Assert.Equal("An error occurred", result.Result.AsT2.Message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System.Net;
using PinguApps.Appwrite.Shared.Requests;
using PinguApps.Appwrite.Shared.Tests;
using RichardSzalay.MockHttp;

namespace PinguApps.Appwrite.Server.Tests.Servers.Account;
public partial class AccountServerTests
{
[Fact]
public async Task CreateEmailToken_ShouldReturnSuccess_WhenApiCallSucceeds()
{
// Arrange
var request = new CreateEmailTokenRequest()
{
UserId = "123456",
Email = "[email protected]"
};

_mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/tokens/email")
.ExpectedHeaders()
.WithJsonContent(request)
.Respond(Constants.AppJson, Constants.TokenResponse);

// Act
var result = await _appwriteServer.Account.CreateEmailToken(request);

// Assert
Assert.True(result.Success);
}

[Fact]
public async Task CreateEmailToken_ShouldHandleException_WhenApiCallFails()
{
// Arrange
var request = new CreateEmailTokenRequest()
{
UserId = "123456",
Email = "[email protected]"
};

_mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/tokens/email")
.ExpectedHeaders()
.WithJsonContent(request)
.Respond(HttpStatusCode.BadRequest, Constants.AppJson, Constants.AppwriteError);

// Act
var result = await _appwriteServer.Account.CreateEmailToken(request);

// Assert
Assert.True(result.IsError);
Assert.True(result.IsAppwriteError);
}

[Fact]
public async Task CreateEmailToken_ShouldReturnErrorResponse_WhenExceptionOccurs()
{
// Arrange
var request = new CreateEmailTokenRequest()
{
UserId = "123456",
Email = "[email protected]"
};

_mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/tokens/email")
.ExpectedHeaders()
.WithJsonContent(request)
.Throw(new HttpRequestException("An error occurred"));

// Act
var result = await _appwriteServer.Account.CreateEmailToken(request);

// Assert
Assert.False(result.Success);
Assert.True(result.IsInternalError);
Assert.Equal("An error occurred", result.Result.AsT2.Message);
}
}
11 changes: 11 additions & 0 deletions tests/PinguApps.Appwrite.Shared.Tests/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,15 @@ public static class Constants
"def": "456"
}
""";

public const string TokenResponse = """
{
"$id": "bb8ea5c16897e",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"userId": "5e5ea5c168bb8",
"secret": "secret",
"expire": "2020-10-15T06:38:00.000+00:00",
"phrase": "Golden Fox"
}
""";
}
Loading