diff --git a/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeCaptchaToExpectedPayload_GivenOptions.verified.txt b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeCaptchaToExpectedPayload_GivenOptions.verified.txt new file mode 100644 index 00000000..7027963f --- /dev/null +++ b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeCaptchaToExpectedPayload_GivenOptions.verified.txt @@ -0,0 +1 @@ +{"type":"signup","email":"user@supabase.com","gotrue_meta_security":{"captcha_token":"captcha-token"}} \ No newline at end of file diff --git a/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenEmailChangeType.verified.txt b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenEmailChangeType.verified.txt new file mode 100644 index 00000000..f05b5da4 --- /dev/null +++ b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenEmailChangeType.verified.txt @@ -0,0 +1 @@ +{"type":"email_change","email":"user@supabase.com"} \ No newline at end of file diff --git a/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenPhoneChangeType.verified.txt b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenPhoneChangeType.verified.txt new file mode 100644 index 00000000..bfbfa256 --- /dev/null +++ b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenPhoneChangeType.verified.txt @@ -0,0 +1 @@ +{"type":"phone_change","phone":"+5544998989898"} \ No newline at end of file diff --git a/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenSignUpType.verified.txt b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenSignUpType.verified.txt new file mode 100644 index 00000000..16a7bbcd --- /dev/null +++ b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenSignUpType.verified.txt @@ -0,0 +1 @@ +{"type":"signup","email":"user@supabase.com"} \ No newline at end of file diff --git a/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenSmsType.verified.txt b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenSmsType.verified.txt new file mode 100644 index 00000000..6d7aaab2 --- /dev/null +++ b/packages/Gotrue/Gotrue.Tests/Authentication/Data/ResendParametersContractTests.ResendRequest_ShouldSerializeToExpectedPayload_GivenSmsType.verified.txt @@ -0,0 +1 @@ +{"type":"sms","phone":"+5544998989898"} \ No newline at end of file diff --git a/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersContractTests.cs b/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersContractTests.cs new file mode 100644 index 00000000..999bb803 --- /dev/null +++ b/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersContractTests.cs @@ -0,0 +1,89 @@ +using System.Threading.Tasks; +using Gotrue.Tests.Support; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Supabase.Gotrue.Resend; + +namespace Gotrue.Tests.Authentication; + +/// +/// Pins the exact bytes the resend requests put on the wire, including the type enum mappings +/// (signup, sms, phone_change, email_change) and whether the target is sent +/// as email or phone. +/// +[TestClass] +[TestCategory("Contract")] +public class ResendParametersContractTests : RequestApprovalFixture +{ + [TestMethod] + public async Task ResendRequest_ShouldSerializeToExpectedPayload_GivenSignUpType() + { + const string email = "user@supabase.com"; + var resend = new ResendParameters( + type: ResendType.SignUp, + email: email, + phone: null, + options: null + ); + await this.Api.Resend(resend); + + await this.Verify(this.EmittedRequestBody).UseDirectory("Data"); + } + + [TestMethod] + public async Task ResendRequest_ShouldSerializeToExpectedPayload_GivenEmailChangeType() + { + const string email = "user@supabase.com"; + var resend = new ResendParameters( + type: ResendType.EmailChange, + email: email, + phone: null, + options: null + ); + await this.Api.Resend(resend); + + await this.Verify(this.EmittedRequestBody).UseDirectory("Data"); + } + + [TestMethod] + public async Task ResendRequest_ShouldSerializeToExpectedPayload_GivenPhoneChangeType() + { + var resend = new ResendParameters( + type: ResendType.PhoneChange, + email: null, + phone: "+5544998989898", + options: null + ); + await this.Api.Resend(resend); + + await this.Verify(this.EmittedRequestBody).UseDirectory("Data"); + } + + [TestMethod] + public async Task ResendRequest_ShouldSerializeToExpectedPayload_GivenSmsType() + { + var resend = new ResendParameters( + type: ResendType.Sms, + email: null, + phone: "+5544998989898", + options: null + ); + await this.Api.Resend(resend); + + await this.Verify(this.EmittedRequestBody).UseDirectory("Data"); + } + + [TestMethod] + public async Task ResendRequest_ShouldSerializeCaptchaToExpectedPayload_GivenOptions() + { + var resend = new ResendParameters( + type: ResendType.SignUp, + email: "user@supabase.com", + phone: null, + options: new ResendOptions(captchaToken: "captcha-token", emailRedirectTo: null) + ); + + await this.Api.Resend(resend); + + await this.Verify(this.EmittedRequestBody).UseDirectory("Data"); + } +} diff --git a/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersTests.cs b/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersTests.cs new file mode 100644 index 00000000..a92762c3 --- /dev/null +++ b/packages/Gotrue/Gotrue.Tests/Authentication/ResendParametersTests.cs @@ -0,0 +1,143 @@ +using System.Net; +using System.Threading.Tasks; +using Gotrue.Tests.Support; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Supabase.Gotrue.Exceptions; +using Supabase.Gotrue.Resend; +using static Gotrue.Tests.TestUtils; + +namespace Gotrue.Tests.Authentication; + +[TestClass] +[TestCategory("E2E")] +public class ResendParametersTests : AuthClientFixture +{ + [TestMethod] + public async Task Resend_ShouldRequestConfirmationCode_GivenSignUpType() + { + var email = RandomEmail(); + var resend = new ResendParameters( + type: ResendType.SignUp, + email: email, + phone: null, + options: null + ); + var response = await this.Client.Resend(resend); + + Assert.IsNotNull(response); + Assert.AreEqual(HttpStatusCode.OK, response.ResponseMessage?.StatusCode); + } + + [TestMethod] + public async Task Resend_ShouldRequestConfirmationCode_GivenSmsType() + { + var resend = new ResendParameters( + type: ResendType.Sms, + email: null, + phone: "5544989899898", + options: null + ); + var response = await this.Client.Resend(resend); + + Assert.IsNotNull(response); + Assert.AreEqual(HttpStatusCode.OK, response.ResponseMessage?.StatusCode); + } + + [TestMethod] + public async Task Resend_ShouldRequestConfirmationCode_GivenPhoneChangeType() + { + var resend = new ResendParameters( + type: ResendType.PhoneChange, + email: null, + phone: "5544989899898", + options: null + ); + var response = await this.Client.Resend(resend); + + Assert.IsNotNull(response); + Assert.AreEqual(HttpStatusCode.OK, response.ResponseMessage?.StatusCode); + } + + [TestMethod] + public async Task Resend_ShouldRequestConfirmationCode_GivenEmailChangeType() + { + var resend = new ResendParameters( + type: ResendType.EmailChange, + email: RandomEmail(), + phone: null, + options: null + ); + var response = await this.Client.Resend(resend); + + Assert.IsNotNull(response); + Assert.AreEqual(HttpStatusCode.OK, response.ResponseMessage?.StatusCode); + } + + [TestMethod] + public async Task Resend_ShouldThrowErrorWhenParameterAreInvalid_GivenPhoneForTypeEmail() + { + var resend = new ResendParameters( + type: ResendType.EmailChange, + email: null, + phone: "5544989899898", + options: null + ); + + var action = async () => await this.Client.Resend(resend); + + var exception = await Assert.ThrowsAsync(action); + Assert.Contains("Missing email address or phone number", exception.Message); + Assert.Contains("400", exception.Message); + } + + [TestMethod] + public async Task Resend_ShouldThrowErrorWhenParameterAreInvalid_GivenEmailForTypePhone() + { + var resend = new ResendParameters( + type: ResendType.Sms, + email: RandomEmail(), + phone: null, + options: null + ); + + var action = async () => await this.Client.Resend(resend); + + var exception = await Assert.ThrowsAsync(action); + Assert.Contains("Type provided requires a phone number", exception.Message); + Assert.Contains("400", exception.Message); + } + + [TestMethod] + public async Task Resend_ShouldThrowErrorWhenParameterAreInvalid_GivenNothingForTypeEmail() + { + var resend = new ResendParameters( + type: ResendType.EmailChange, + email: null, + phone: null, + options: null + ); + + var action = async () => await this.Client.Resend(resend); + + var exception = await Assert.ThrowsAsync(action); + Assert.Contains("Missing email address or phone number", exception.Message); + Assert.Contains("400", exception.Message); + } + + [TestMethod] + public async Task Resend_ShouldThrowErrorWhenParameterAreInvalid_GivenNothingForTypePhone() + { + var resend = new ResendParameters( + type: ResendType.Sms, + email: null, + phone: null, + options: null + ); + + var action = async () => await this.Client.Resend(resend); + + var exception = await Assert.ThrowsAsync(action); + Assert.Contains("Type provided requires a phone number", exception.Message); + Assert.Contains("400", exception.Message); + } +} diff --git a/packages/Gotrue/Gotrue/Api.cs b/packages/Gotrue/Gotrue/Api.cs index ed3e25f2..e0daa3bc 100644 --- a/packages/Gotrue/Gotrue/Api.cs +++ b/packages/Gotrue/Gotrue/Api.cs @@ -13,6 +13,7 @@ using Supabase.Gotrue.Exceptions; using Supabase.Gotrue.Interfaces; using Supabase.Gotrue.Mfa; +using Supabase.Gotrue.Resend; using Supabase.Gotrue.Responses; using static Supabase.Gotrue.Constants; @@ -889,4 +890,22 @@ public Task GenerateLink(string jwt, GenerateLinkOptions options) return this.MakeRequestAsync(HttpMethod.Post, $"{this.Url}/token?grant_type=refresh_token", data, this.Headers.MergeLeft(headers)); } + + /// + /// Resends a confirmation code to a user's email or phone. + /// + /// + /// BaseResponse + public Task Resend(ResendParameters resendParameters) + { + var body = resendParameters.ToDictionary(); + var url = resendParameters.ApplyRedirectTo(this.Url); + + return this.MakeRequestAsync( + HttpMethod.Post, + url, + body, + this.Headers + ); + } } diff --git a/packages/Gotrue/Gotrue/Client.cs b/packages/Gotrue/Gotrue/Client.cs index 8ee751fa..809ad973 100644 --- a/packages/Gotrue/Gotrue/Client.cs +++ b/packages/Gotrue/Gotrue/Client.cs @@ -14,6 +14,8 @@ using Supabase.Gotrue.Exceptions; using Supabase.Gotrue.Interfaces; using Supabase.Gotrue.Mfa; +using Supabase.Gotrue.Resend; +using Supabase.Gotrue.Responses; using static Supabase.Gotrue.Constants; using static Supabase.Gotrue.Constants.AuthState; using static Supabase.Gotrue.Exceptions.FailureHint.Reason; @@ -983,6 +985,9 @@ private void ApplyLoadedSession(Session? before, Session? loaded) /// public void Shutdown() => this.NotifyAuthStateChange(AuthState.Shutdown); + /// + public Task Resend(ResendParameters resendParameters) => this.api.Resend(resendParameters); + /// public async Task Enroll(MfaEnrollParams mfaEnrollParams) { diff --git a/packages/Gotrue/Gotrue/Constants.cs b/packages/Gotrue/Gotrue/Constants.cs index fac3e04b..5b534b36 100644 --- a/packages/Gotrue/Gotrue/Constants.cs +++ b/packages/Gotrue/Gotrue/Constants.cs @@ -1,167 +1,197 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Supabase.Core.Attributes; #pragma warning disable CS1591 -namespace Supabase.Gotrue +namespace Supabase.Gotrue; + +/// +/// Constants used throughout the Gotrue Client +/// +public static class Constants { - /// - /// Constants used throughout the Gotrue Client - /// - public static class Constants - { - /// - /// URL for the Gotrue server. Defaults to localhost:9999. - /// - public const string GOTRUE_URL = "http://localhost:9999"; - public const string AUDIENCE = ""; - public const int EXPIRY_MARGIN = 60 * 1000; - public const string STORAGE_KEY = "supabase.auth.token"; - public static readonly Dictionary CookieOptions = new Dictionary - { - { "name", "sb:token" }, - { "lifetime", 60 * 60 * 8 }, - { "domain", "" }, - { "path", "/" }, - { "sameSite", "lax" } - }; - - public enum SortOrder - { - [MapTo("asc")] - Ascending, - [MapTo("desc")] - Descending - } - - public enum MobileOtpType - { - [MapTo("sms")] - SMS, - [MapTo("phone_change")] - PhoneChange - } - - public enum EmailOtpType - { - [MapTo("signup")] - Signup, - [MapTo("invite")] - Invite, - [MapTo("magiclink")] - MagicLink, - [MapTo("recovery")] - Recovery, - [MapTo("email_change")] - EmailChange, - [MapTo("email")] - Email - } - - /// - /// Providers available to Supabase - /// Ref: https://supabase.github.io/gotrue-js/modules.html#Provider - /// - public enum Provider - { - [MapTo("anonymous_users")] - AnonymousUsers, - [MapTo("apple")] - Apple, - [MapTo("azure")] - Azure, - [MapTo("bitbucket")] - Bitbucket, - [MapTo("discord")] - Discord, - [MapTo("facebook")] - Facebook, - [MapTo("figma")] - Figma, - [MapTo("fly")] - Fly, - [MapTo("github")] - Github, - [MapTo("gitlab")] - Gitlab, - [MapTo("google")] - Google, - [MapTo("kakao")] - Kakao, - [MapTo("keycloak")] - KeyCloak, - [MapTo("linkedin")] - LinkedIn, - [MapTo("linkedin_oidc")] - LinkedInOIDC, - [MapTo("notion")] - Notion, - [MapTo("slack")] - Slack, - [MapTo("spotify")] - Spotify, - [MapTo("twitch")] - Twitch, - [MapTo("twitter")] - Twitter, - [MapTo("workos")] - WorkOS, - [MapTo("zoom")] - Zoom - } - - /// - /// States that the Auth Client will raise events for. - /// - public enum AuthState - { - SignedIn, - SignedOut, - UserUpdated, - PasswordRecovery, - TokenRefreshed, - Shutdown, - MfaChallengeVerified - } - - /// - /// Specifies the functionality expected from the `SignIn` method - /// - public enum SignInType - { - Email, - Phone, - RefreshToken - } - - /// - /// Represents an OAuth Flow type - /// - public enum OAuthFlowType - { - [MapTo("implicit")] - Implicit, - [MapTo("pkce")] - PKCE - } - - /// - /// Specifies the functionality expected from the `SignUp` method - /// - public enum SignUpType - { - Email, - Phone - } - - public enum SignOutScope - { - [MapTo("global")] - Global, - [MapTo("local")] - Local, - [MapTo("others")] - Others - } + /// + /// URL for the Gotrue server. Defaults to localhost:9999. + /// + public const string GOTRUE_URL = "http://localhost:9999"; + public const string AUDIENCE = ""; + public const int EXPIRY_MARGIN = 60 * 1000; + public const string STORAGE_KEY = "supabase.auth.token"; + public static readonly Dictionary CookieOptions = new Dictionary + { + { "name", "sb:token" }, + { "lifetime", 60 * 60 * 8 }, + { "domain", "" }, + { "path", "/" }, + { "sameSite", "lax" }, + }; + + public enum SortOrder + { + [MapTo("asc")] + Ascending, + + [MapTo("desc")] + Descending, + } + + public enum MobileOtpType + { + [MapTo("sms")] + SMS, + + [MapTo("phone_change")] + PhoneChange, + } + + public enum EmailOtpType + { + [MapTo("signup")] + Signup, + + [MapTo("invite")] + Invite, + + [MapTo("magiclink")] + MagicLink, + + [MapTo("recovery")] + Recovery, + + [MapTo("email_change")] + EmailChange, + + [MapTo("email")] + Email, + } + + /// + /// Providers available to Supabase + /// Ref: https://supabase.github.io/gotrue-js/modules.html#Provider + /// + public enum Provider + { + [MapTo("anonymous_users")] + AnonymousUsers, + + [MapTo("apple")] + Apple, + + [MapTo("azure")] + Azure, + + [MapTo("bitbucket")] + Bitbucket, + + [MapTo("discord")] + Discord, + + [MapTo("facebook")] + Facebook, + + [MapTo("figma")] + Figma, + + [MapTo("fly")] + Fly, + + [MapTo("github")] + Github, + + [MapTo("gitlab")] + Gitlab, + + [MapTo("google")] + Google, + + [MapTo("kakao")] + Kakao, + + [MapTo("keycloak")] + KeyCloak, + + [MapTo("linkedin")] + LinkedIn, + + [MapTo("linkedin_oidc")] + LinkedInOIDC, + + [MapTo("notion")] + Notion, + + [MapTo("slack")] + Slack, + + [MapTo("spotify")] + Spotify, + + [MapTo("twitch")] + Twitch, + + [MapTo("twitter")] + Twitter, + + [MapTo("workos")] + WorkOS, + + [MapTo("zoom")] + Zoom, + } + + /// + /// States that the Auth Client will raise events for. + /// + public enum AuthState + { + SignedIn, + SignedOut, + UserUpdated, + PasswordRecovery, + TokenRefreshed, + Shutdown, + MfaChallengeVerified, + } + + /// + /// Specifies the functionality expected from the `SignIn` method + /// + public enum SignInType + { + Email, + Phone, + RefreshToken, + } + + /// + /// Represents an OAuth Flow type + /// + public enum OAuthFlowType + { + [MapTo("implicit")] + Implicit, + + [MapTo("pkce")] + PKCE, + } + + /// + /// Specifies the functionality expected from the `SignUp` method + /// + public enum SignUpType + { + Email, + Phone, + } + + public enum SignOutScope + { + [MapTo("global")] + Global, + + [MapTo("local")] + Local, + + [MapTo("others")] + Others, } } diff --git a/packages/Gotrue/Gotrue/Interfaces/IGotrueApi.cs b/packages/Gotrue/Gotrue/Interfaces/IGotrueApi.cs index de61f5d0..b3b0fa8b 100644 --- a/packages/Gotrue/Gotrue/Interfaces/IGotrueApi.cs +++ b/packages/Gotrue/Gotrue/Interfaces/IGotrueApi.cs @@ -4,6 +4,7 @@ using Supabase.Core.Interfaces; using Supabase.Gotrue.Claims; using Supabase.Gotrue.Mfa; +using Supabase.Gotrue.Resend; using Supabase.Gotrue.Responses; using static Supabase.Gotrue.Constants; @@ -87,4 +88,11 @@ public interface IGotrueApi : IGettableHeaders /// Identity to be unlinked /// Task UnlinkIdentity(string token, UserIdentity userIdentity); + + /// + /// Resends a confirmation code to a user's email or phone. + /// + /// + /// + Task Resend(ResendParameters resendParameters); } diff --git a/packages/Gotrue/Gotrue/Interfaces/IGotrueClient.cs b/packages/Gotrue/Gotrue/Interfaces/IGotrueClient.cs index 8c10acc1..e749a13f 100644 --- a/packages/Gotrue/Gotrue/Interfaces/IGotrueClient.cs +++ b/packages/Gotrue/Gotrue/Interfaces/IGotrueClient.cs @@ -7,6 +7,8 @@ using Supabase.Gotrue.Claims; using Supabase.Gotrue.Exceptions; using Supabase.Gotrue.Mfa; +using Supabase.Gotrue.Resend; +using Supabase.Gotrue.Responses; using static Supabase.Gotrue.Constants; #endregion @@ -548,6 +550,13 @@ public interface IGotrueClient : IGettableHeaders /// The refresh token to exchange for a new session. Task RefreshToken(string accessToken, string refreshToken); + /// + /// Resends a confirmation code to a user's email or phone. + /// + /// + /// + Task Resend(ResendParameters resendParameters); + #region MFA /// diff --git a/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt b/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt index ed2037b9..362ae531 100644 --- a/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt +++ b/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt @@ -31,6 +31,7 @@ static Supabase.Gotrue.Claims.JwtHeader.operator !=(Supabase.Gotrue.Claims.JwtHe static Supabase.Gotrue.Claims.JwtHeader.operator ==(Supabase.Gotrue.Claims.JwtHeader? left, Supabase.Gotrue.Claims.JwtHeader? right) -> bool Supabase.Gotrue.Api.GetJwksAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Supabase.Gotrue.Api.GetUserAsync(string! jwt, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Supabase.Gotrue.Api.Resend(Supabase.Gotrue.Resend.ResendParameters! resendParameters) -> System.Threading.Tasks.Task! Supabase.Gotrue.Claims.GetClaimsOptions Supabase.Gotrue.Claims.GetClaimsOptions.$() -> Supabase.Gotrue.Claims.GetClaimsOptions! Supabase.Gotrue.Claims.GetClaimsOptions.AllowExpired.get -> bool @@ -110,10 +111,29 @@ Supabase.Gotrue.Claims.JwtHeader.Kid.init -> void Supabase.Gotrue.Claims.JwtHeader.Typ.get -> string? Supabase.Gotrue.Claims.JwtHeader.Typ.init -> void Supabase.Gotrue.Client.GetClaimsAsync(string? jwt = null, Supabase.Gotrue.Claims.GetClaimsOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Supabase.Gotrue.Client.Resend(Supabase.Gotrue.Resend.ResendParameters! resendParameters) -> System.Threading.Tasks.Task! Supabase.Gotrue.Exceptions.FailureHint.Reason.InvalidJwt = 22 -> Supabase.Gotrue.Exceptions.FailureHint.Reason Supabase.Gotrue.Interfaces.IGotrueApi.GetJwksAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Supabase.Gotrue.Interfaces.IGotrueApi.GetUserAsync(string! jwt, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Supabase.Gotrue.Interfaces.IGotrueApi.Resend(Supabase.Gotrue.Resend.ResendParameters! resendParameters) -> System.Threading.Tasks.Task! Supabase.Gotrue.Interfaces.IGotrueClient.GetClaimsAsync(string? jwt = null, Supabase.Gotrue.Claims.GetClaimsOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Supabase.Gotrue.Interfaces.IGotrueClient.Resend(Supabase.Gotrue.Resend.ResendParameters! resendParameters) -> System.Threading.Tasks.Task! +Supabase.Gotrue.Resend.ResendOptions +Supabase.Gotrue.Resend.ResendOptions.CaptchaToken.get -> string? +Supabase.Gotrue.Resend.ResendOptions.EmailRedirectTo.get -> string? +Supabase.Gotrue.Resend.ResendOptions.ResendOptions(string? captchaToken, string? emailRedirectTo) -> void +Supabase.Gotrue.Resend.ResendParameters +Supabase.Gotrue.Resend.ResendParameters.Email.get -> string? +Supabase.Gotrue.Resend.ResendParameters.Options.get -> Supabase.Gotrue.Resend.ResendOptions? +Supabase.Gotrue.Resend.ResendParameters.Phone.get -> string? +Supabase.Gotrue.Resend.ResendParameters.ResendParameters(Supabase.Gotrue.Resend.ResendType type, string? email, string? phone, Supabase.Gotrue.Resend.ResendOptions? options) -> void +Supabase.Gotrue.Resend.ResendParameters.ToDictionary() -> System.Collections.Generic.Dictionary! +Supabase.Gotrue.Resend.ResendParameters.Type.get -> Supabase.Gotrue.Resend.ResendType +Supabase.Gotrue.Resend.ResendType +Supabase.Gotrue.Resend.ResendType.EmailChange = 3 -> Supabase.Gotrue.Resend.ResendType +Supabase.Gotrue.Resend.ResendType.PhoneChange = 2 -> Supabase.Gotrue.Resend.ResendType +Supabase.Gotrue.Resend.ResendType.SignUp = 0 -> Supabase.Gotrue.Resend.ResendType +Supabase.Gotrue.Resend.ResendType.Sms = 1 -> Supabase.Gotrue.Resend.ResendType Supabase.Gotrue.SignUpOptions.CaptchaToken.get -> string? Supabase.Gotrue.SignUpOptions.CaptchaToken.set -> void Supabase.Gotrue.Api.SignInWithEmail(string! email, string! password, Supabase.Gotrue.SignInWithPasswordOptions! options) -> System.Threading.Tasks.Task! diff --git a/packages/Gotrue/Gotrue/Resend/ResendOptions.cs b/packages/Gotrue/Gotrue/Resend/ResendOptions.cs new file mode 100644 index 00000000..b9f21460 --- /dev/null +++ b/packages/Gotrue/Gotrue/Resend/ResendOptions.cs @@ -0,0 +1,28 @@ +namespace Supabase.Gotrue.Resend; + +/// +/// Represents optional parameters used in the Resend API. These parameters provide +/// additional customization for the resend operation, such as including a CAPTCHA +/// token or specifying a redirection URL after email confirmation. +/// +public class ResendOptions +{ + /// + /// Verification token received when the user completes the captcha on the site. + /// + public string? CaptchaToken { get; } + + /// + /// A URL or mobile address to send the user to after they are confirmed. + /// + public string? EmailRedirectTo { get; } + + /// + /// Represents optional parameters for a resend operation in the Resend API. + /// + public ResendOptions(string? captchaToken, string? emailRedirectTo) + { + this.CaptchaToken = captchaToken; + this.EmailRedirectTo = emailRedirectTo; + } +} diff --git a/packages/Gotrue/Gotrue/Resend/ResendParameters.cs b/packages/Gotrue/Gotrue/Resend/ResendParameters.cs new file mode 100644 index 00000000..2ed94893 --- /dev/null +++ b/packages/Gotrue/Gotrue/Resend/ResendParameters.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; + +namespace Supabase.Gotrue.Resend; + +/// +/// Parameters for the Resend API. +/// +public class ResendParameters +{ + /// + /// The email address to resend to. + /// + public string? Email { get; } + + /// + /// The phone number to resend to. + /// + public string? Phone { get; } + + /// + /// The type of resend. + /// + public ResendType Type { get; } + + /// + /// Additional options to customize the behavior of the Resend API. + /// Provides support for features such as CAPTCHA tokens and email redirection. + /// + public ResendOptions? Options { get; } + + /// + /// Represents the parameters required for a resend operation, such as resending a confirmation email + /// or a verification SMS, depending on the specified type. + /// + public ResendParameters(ResendType type, string? email, string? phone, ResendOptions? options) + { + this.Type = type; + this.Email = email; + this.Phone = phone; + this.Options = options; + } + + /// + /// Converts the resend parameters to a dictionary format, including the type, email, phone, + /// and any additional options, for use in API requests. + /// + /// + /// A dictionary containing the key-value pairs representing the resend parameters. + /// + public Dictionary ToDictionary() + { + var body = new Dictionary + { + { "type", Core.Helpers.GetMappedToAttr(this.Type).Mapping }, + }; + + body = this.AddEmailAndTokenField(body); + body = this.AddPhoneField(body); + + return body; + } + + internal string ApplyRedirectTo(string url) + { + var newUrl = $"{url}/resend"; + if (!this.IsEmail()) + return newUrl; + + if (!string.IsNullOrWhiteSpace(this.Options?.EmailRedirectTo)) + newUrl = $"{url}/resend?redirect_to={this.Options.EmailRedirectTo}"; + + return newUrl; + } + + private bool IsEmail() => this.Type is ResendType.EmailChange or ResendType.SignUp; + + private Dictionary AddEmailAndTokenField(Dictionary parameter) + { + if (!this.IsEmail()) + return parameter; + + var body = new Dictionary(parameter); + if (!string.IsNullOrWhiteSpace(this.Email)) + body.Add("email", this.Email); + + if (!string.IsNullOrWhiteSpace(this.Options?.CaptchaToken)) + body.Add( + "gotrue_meta_security", + new Dictionary { { "captcha_token", this.Options.CaptchaToken } } + ); + + return body; + } + + private Dictionary AddPhoneField(Dictionary parameter) + { + if (this.IsEmail()) + return parameter; + + var body = new Dictionary(parameter); + if (!string.IsNullOrWhiteSpace(this.Phone)) + body.Add("phone", this.Phone); + + return body; + } +} diff --git a/packages/Gotrue/Gotrue/Resend/ResendType.cs b/packages/Gotrue/Gotrue/Resend/ResendType.cs new file mode 100644 index 00000000..b57fe572 --- /dev/null +++ b/packages/Gotrue/Gotrue/Resend/ResendType.cs @@ -0,0 +1,25 @@ +using Supabase.Core.Attributes; + +namespace Supabase.Gotrue.Resend; + +/// +/// The type of resend. +/// +public enum ResendType +{ + /// Signup resend. + [MapTo("signup")] + SignUp, + + /// SMS resend. + [MapTo("sms")] + Sms, + + /// Phone change resend. + [MapTo("phone_change")] + PhoneChange, + + /// Email change resend. + [MapTo("email_change")] + EmailChange, +}