Skip to content

Commit

Permalink
Merge pull request #71 from PinguApps/45-create-magic-url
Browse files Browse the repository at this point in the history
Implemented create magic url
  • Loading branch information
pingu2k4 authored Jul 13, 2024
2 parents fff9ed9 + a791860 commit 2575002
Show file tree
Hide file tree
Showing 16 changed files with 459 additions and 19 deletions.
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

0 comments on commit 2575002

Please sign in to comment.