Skip to content

Commit

Permalink
Adding Magic Links (#116)
Browse files Browse the repository at this point in the history
Added Magic links to the SDK

Co-authored-by: Oleksii Holub <[email protected]>
Co-authored-by: Jonas Hendrickx <[email protected]>
  • Loading branch information
3 people authored Apr 4, 2024
1 parent 1ba094c commit 79b4b6e
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 3 deletions.
1 change: 1 addition & 0 deletions src/Passwordless/Helpers/PasswordlessSerializerContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ namespace Passwordless.Helpers;
[JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(GetEventLogRequest))]
[JsonSerializable(typeof(GetEventLogResponse))]
[JsonSerializable(typeof(SendMagicLinkApiRequest))]
internal partial class PasswordlessSerializerContext : JsonSerializerContext;
8 changes: 8 additions & 0 deletions src/Passwordless/IPasswordlessClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,12 @@ Task DeleteCredentialAsync(
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<GetEventLogResponse> GetEventLogAsync(GetEventLogRequest request, CancellationToken cancellationToken = default);

/// <summary>
/// Sends an email containing a magic link template allowing users to login.
/// </summary>
/// <param name="request"><see cref="SendMagicLinkRequest"/></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task SendMagicLinkAsync(SendMagicLinkRequest request, CancellationToken cancellationToken = default);
}
2 changes: 0 additions & 2 deletions src/Passwordless/Models/GetEventLogRequest.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using System.ComponentModel.DataAnnotations;

namespace Passwordless.Models;

/// <summary>
Expand Down
23 changes: 23 additions & 0 deletions src/Passwordless/Models/SendMagicLinkRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;
using Passwordless.Helpers.Extensions;

namespace Passwordless.Models;

/// <summary>
/// Request for sending an email with a link that contains a 1-time-use token to be used for validating signin.
/// </summary>
/// <param name="EmailAddress">Valid email address that will be the recipient of the magic link email</param>
/// <param name="UrlTemplate">Url template that needs to contain the token template, <token>. The token template will be replaced with a valid signin token to be sent to the verify sign in token endpoint (https://v4.passwsordless.dev/signin/verify).</param>
/// <param name="UserId">Identifier for the user the email is intended for.</param>
/// <param name="TimeToLive">Length of time the magic link will be active. Default value will be 15 minutes.</param>
public record SendMagicLinkRequest(string EmailAddress, string UrlTemplate, string UserId, TimeSpan? TimeToLive)
{
internal SendMagicLinkApiRequest ToRequest() =>
new(
this.EmailAddress,
this.UrlTemplate,
this.UserId,
this.TimeToLive?.TotalSeconds.Pipe(Convert.ToInt32));
};

internal record SendMagicLinkApiRequest(string EmailAddress, string UrlTemplate, string UserId, int? TimeToLive);
12 changes: 12 additions & 0 deletions src/Passwordless/PasswordlessClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,18 @@ public async Task<GetEventLogResponse> GetEventLogAsync(GetEventLogRequest reque
PasswordlessSerializerContext.Default.GetEventLogResponse,
cancellationToken))!;

/// <inheritdoc />
public async Task SendMagicLinkAsync(SendMagicLinkRequest request, CancellationToken cancellationToken = default)
{
using var response = await _http.PostAsJsonAsync(
"magic-links/send",
request.ToRequest(),
PasswordlessSerializerContext.Default.SendMagicLinkApiRequest,
cancellationToken);

response.EnsureSuccessStatusCode();
}

/// <inheritdoc />
public async Task<UsersCount> GetUsersCountAsync(CancellationToken cancellationToken = default) =>
(await _http.GetFromJsonAsync(
Expand Down
3 changes: 2 additions & 1 deletion tests/Passwordless.Tests.Infra/TestApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ public async Task<PasswordlessApplication> CreateAppAsync()
{
"adminEmail": "[email protected]",
"eventLoggingIsEnabled": true,
"eventLoggingRetentionPeriod": 7
"eventLoggingRetentionPeriod": 7,
"magicLinkEmailMonthlyQuota" : 100
}
""",
Encoding.UTF8,
Expand Down
41 changes: 41 additions & 0 deletions tests/Passwordless.Tests/MagicLinksTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Passwordless.Models;
using Passwordless.Tests.Infra;
using Xunit;
using Xunit.Abstractions;

namespace Passwordless.Tests;

public class MagicLinksTests(TestApiFixture api, ITestOutputHelper testOutput) : ApiTestBase(api, testOutput)
{
[Fact]
public async Task I_can_send_a_magic_link_with_a_specified_time_to_live()
{
// Arrange
var passwordless = await Api.CreateClientAsync();
var request = new SendMagicLinkRequest("[email protected]", "https://www.example.com?token=$TOKEN", "user", new TimeSpan(0, 15, 0));

// Act
var action = async () => await passwordless.SendMagicLinkAsync(request, CancellationToken.None);

// Assert
await action.Should().NotThrowAsync();
}

[Fact]
public async Task I_can_send_a_magic_link_without_a_time_to_live()
{
// Arrange
var passwordless = await Api.CreateClientAsync();
var request = new SendMagicLinkRequest("[email protected]", "https://www.example.com?token=$TOKEN", "user", null);

// Act
var action = async () => await passwordless.SendMagicLinkAsync(request, CancellationToken.None);

// Assert
await action.Should().NotThrowAsync();
}
}

0 comments on commit 79b4b6e

Please sign in to comment.