From cac2961ed1282fe6239a02467018e34f48d0a2d0 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Wed, 3 May 2023 17:09:36 -0700 Subject: [PATCH 01/74] Add GoTrue specific words to dictionary --- gotrue-csharp.sln.DotSettings | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 gotrue-csharp.sln.DotSettings diff --git a/gotrue-csharp.sln.DotSettings b/gotrue-csharp.sln.DotSettings new file mode 100644 index 0000000..9fb10bd --- /dev/null +++ b/gotrue-csharp.sln.DotSettings @@ -0,0 +1,5 @@ + + True + True + True + True \ No newline at end of file From 73e3a3ecb9d9a03ccb87d567bbd5e0fb58e92ce9 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Wed, 3 May 2023 17:10:43 -0700 Subject: [PATCH 02/74] Minor cleanup. Remove unused imports. Add missing docs. Minor tweaks. --- Gotrue/Api.cs | 50 +++++++++++++++++++++----------------------------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/Gotrue/Api.cs b/Gotrue/Api.cs index 3145ec2..aa05b22 100644 --- a/Gotrue/Api.cs +++ b/Gotrue/Api.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Net.Http; using System.Threading.Tasks; using System.Web; @@ -9,11 +7,9 @@ using Supabase.Core; using Supabase.Core.Attributes; using Supabase.Core.Extensions; -using Supabase.Core.Interfaces; using Supabase.Gotrue.Exceptions; using Supabase.Gotrue.Interfaces; using Supabase.Gotrue.Responses; -using static Supabase.Gotrue.Client; using static Supabase.Gotrue.Constants; namespace Supabase.Gotrue @@ -25,17 +21,14 @@ public class Api : IGotrueApi /// /// Function that can be set to return dynamic headers. /// - /// Headers specified in the constructor will ALWAYS take precendece over headers returned by this function. + /// Headers specified in the constructor will ALWAYS take precedence over headers returned by this function. /// public Func>? GetHeaders { get; set; } private Dictionary _headers; protected Dictionary Headers { - get - { - return GetHeaders != null ? GetHeaders().MergeLeft(_headers) : _headers; - } + get => GetHeaders != null ? GetHeaders().MergeLeft(_headers) : _headers; set { _headers = value; @@ -93,7 +86,7 @@ public Api(string url, Dictionary? headers = null) // If account is unconfirmed, Gotrue returned the user object, so fill User data // in from the parsed response. - if (session != null && session.User == null) + if (session is { User: null }) { // Gotrue returns a User object for an unconfirmed account session.User = JsonConvert.DeserializeObject(response.Content!); @@ -139,7 +132,6 @@ public Api(string url, Dictionary? headers = null) public async Task SignInWithOtp(SignInWithPasswordlessEmailOptions options) { var url = string.IsNullOrEmpty(options.EmailRedirectTo) ? $"{Url}/otp" : $"{Url}/otp?redirect_to={options.EmailRedirectTo}"; - string? challenge = null; string? verifier = null; var body = new Dictionary @@ -151,7 +143,7 @@ public async Task SignInWithOtp(SignInWithPasswordlessE if (options.FlowType == OAuthFlowType.PKCE) { - challenge = Helpers.GenerateNonce(); + string challenge = Helpers.GenerateNonce(); verifier = Helpers.GeneratePKCENonceVerifier(challenge); body.Add("code_challenge", challenge); @@ -333,6 +325,7 @@ public Task SendMobileOTP(string phone) /// /// The user's phone number WITH international prefix /// token that user was sent to their mobile phone + /// e.g. SMS or phone change /// public Task VerifyMobileOTP(string phone, string token, MobileOtpType type) { @@ -345,10 +338,11 @@ public Task SendMobileOTP(string phone) } /// - /// Send User supplied Mobile OTP to be verified + /// Send User supplied Email OTP to be verified /// - /// The user's phone number WITH international prefix + /// The user's email address /// token that user was sent to their mobile phone + /// Type of verification, e.g. invite, recovery, etc. /// public Task VerifyEmailOTP(string email, string token, EmailOtpType type) { @@ -374,9 +368,9 @@ public Task ResetPasswordForEmail(string email) /// /// Create a temporary object with all configured headers and adds the Authorization token to be used on request methods /// - /// + /// JWT /// - internal Dictionary CreateAuthedRequestHeaders(string jwt) + private Dictionary CreateAuthedRequestHeaders(string jwt) { var headers = new Dictionary(Headers); @@ -436,7 +430,7 @@ public ProviderAuthState GetUriForProvider(Provider provider, SignInOptions? opt } /// - /// Log in an existing user via a third-party provider. + /// Log in an existing user via code from third-party provider. /// /// Generated verifier (probably from GetUrlForProvider) /// The received Auth Code Callback @@ -460,7 +454,7 @@ public ProviderAuthState GetUriForProvider(Provider provider, SignInOptions? opt /// public Task SignOut(string jwt) { - var data = new Dictionary { }; + var data = new Dictionary(); return Helpers.MakeRequest(HttpMethod.Post, $"{Url}/logout", data, CreateAuthedRequestHeaders(jwt)); } @@ -472,7 +466,7 @@ public Task SignOut(string jwt) /// public Task GetUser(string jwt) { - var data = new Dictionary { }; + var data = new Dictionary(); return Helpers.MakeRequest(HttpMethod.Get, $"{Url}/user", data, CreateAuthedRequestHeaders(jwt)); } @@ -481,11 +475,11 @@ public Task SignOut(string jwt) /// Get User details by Id /// /// A valid JWT. Must be a full-access API key (e.g. service_role key). - /// + /// userID /// public Task GetUserById(string jwt, string userId) { - var data = new Dictionary { }; + var data = new Dictionary(); return Helpers.MakeRequest(HttpMethod.Get, $"{Url}/admin/users/{userId}", data, CreateAuthedRequestHeaders(jwt)); } @@ -518,9 +512,9 @@ public Task SignOut(string jwt) return Helpers.MakeRequest>(HttpMethod.Get, $"{Url}/admin/users", data, CreateAuthedRequestHeaders(jwt)); } - internal Dictionary TransformListUsersParams(string? filter = null, string? sortBy = null, SortOrder sortOrder = SortOrder.Descending, int? page = null, int? perPage = null) + private Dictionary TransformListUsersParams(string? filter = null, string? sortBy = null, SortOrder sortOrder = SortOrder.Descending, int? page = null, int? perPage = null) { - var query = new Dictionary { }; + var query = new Dictionary(); if (filter != null && !string.IsNullOrWhiteSpace(filter)) { @@ -550,9 +544,7 @@ internal Dictionary TransformListUsersParams(string? filter = nu /// Create a user /// /// A valid JWT. Must be a full-access API key (e.g. service_role key). - /// - /// - /// + /// Additional administrative details /// public Task CreateUser(string jwt, AdminUserAttributes? attributes = null) { @@ -568,8 +560,8 @@ internal Dictionary TransformListUsersParams(string? filter = nu /// Update user by Id /// /// A valid JWT. Must be a full-access API key (e.g. service_role key). - /// - /// + /// userID + /// User attributes e.g. email, password, etc. /// public Task UpdateUserById(string jwt, string userId, UserAttributes userData) { @@ -584,7 +576,7 @@ internal Dictionary TransformListUsersParams(string? filter = nu /// public Task DeleteUser(string uid, string jwt) { - var data = new Dictionary { }; + var data = new Dictionary(); return Helpers.MakeRequest(HttpMethod.Delete, $"{Url}/admin/users/{uid}", data, CreateAuthedRequestHeaders(jwt)); } From c1d54e520f5106372c2fce95e6165fff3105a562 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Wed, 3 May 2023 17:11:23 -0700 Subject: [PATCH 03/74] Minor cleanup. Remove unused imports. Add missing docs. Minor tweaks. --- Gotrue/Client.cs | 103 ++++++++++++++++++++++------------------------- 1 file changed, 49 insertions(+), 54 deletions(-) diff --git a/Gotrue/Client.cs b/Gotrue/Client.cs index 92197c1..4d1d51e 100644 --- a/Gotrue/Client.cs +++ b/Gotrue/Client.cs @@ -1,16 +1,12 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Net.Http; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using System.Web; -using Newtonsoft.Json.Linq; -using Supabase.Core.Extensions; -using Supabase.Gotrue.Interfaces; using Supabase.Gotrue.Exceptions; +using Supabase.Gotrue.Interfaces; using static Supabase.Gotrue.Constants; namespace Supabase.Gotrue @@ -37,8 +33,8 @@ public Func>? GetHeaders { _getHeaders = value; - if (api != null) - api.GetHeaders = value; + if (_api != null) + _api.GetHeaders = value; } } private Func>? _getHeaders; @@ -71,6 +67,7 @@ public Func>? GetHeaders /// /// User defined function (via ) to persist the session. /// + // ReSharper disable once IdentifierTypo protected Func> SessionPersistor { get; private set; } /// @@ -91,9 +88,9 @@ public Func>? GetHeaders /// /// Internal timer reference for Refreshing Tokens () /// - private Timer? refreshTimer = null; + private Timer? _refreshTimer; - private IGotrueApi api; + private IGotrueApi _api; /// /// Initializes the Client. @@ -116,7 +113,7 @@ public Client(ClientOptions? options = null) SessionRetriever = options.SessionRetriever; SessionDestroyer = options.SessionDestroyer; - api = new Api(options.Url, options.Headers); + _api = new Api(options.Url, options.Headers); } /// @@ -168,10 +165,10 @@ public Client(ClientOptions? options = null) switch (type) { case SignUpType.Email: - session = await api.SignUpWithEmail(identifier, password, options); + session = await _api.SignUpWithEmail(identifier, password, options); break; case SignUpType.Phone: - session = await api.SignUpWithPhone(identifier, password, options); + session = await _api.SignUpWithPhone(identifier, password, options); break; } @@ -205,7 +202,7 @@ public async Task SignIn(string email, SignInOptions? options = null) try { - await api.SendMagicLinkEmail(email, options); + await _api.SendMagicLinkEmail(email, options); return true; } catch (RequestException ex) @@ -230,7 +227,7 @@ public async Task SignIn(string email, SignInOptions? options = null) try { await DestroySession(); - var result = await api.SignInWithIdToken(provider, idToken, nonce, captchaToken); + var result = await _api.SignInWithIdToken(provider, idToken, nonce, captchaToken); if (result != null) await PersistSession(result); @@ -265,7 +262,7 @@ public async Task SignInWithOtp(SignInWithPasswordlessE try { await DestroySession(); - return await api.SignInWithOtp(options); + return await _api.SignInWithOtp(options); } catch (RequestException ex) { @@ -295,7 +292,7 @@ public async Task SignInWithOtp(SignInWithPasswordlessP try { await DestroySession(); - return await api.SignInWithOtp(options); + return await _api.SignInWithOtp(options); } catch (RequestException ex) { @@ -307,6 +304,7 @@ public async Task SignInWithOtp(SignInWithPasswordlessP /// Sends a Magic email login link to the specified email. /// /// + /// /// public Task SendMagicLink(string email, SignInOptions? options = null) => SignIn(email, options); @@ -345,18 +343,15 @@ public async Task SignInWithOtp(SignInWithPasswordlessP switch (type) { case SignInType.Email: - session = await api.SignInWithEmail(identifierOrToken, password!); + session = await _api.SignInWithEmail(identifierOrToken, password!); break; case SignInType.Phone: - if (string.IsNullOrEmpty(password)) - { - var response = await api.SendMobileOTP(identifierOrToken); + if (string.IsNullOrEmpty(password)) { + await _api.SendMobileOTP(identifierOrToken); return null; } - else - { - session = await api.SignInWithPhone(identifierOrToken, password!); - } + + session = await _api.SignInWithPhone(identifierOrToken, password!); break; case SignInType.RefreshToken: CurrentSession = new Session(); @@ -395,7 +390,7 @@ public async Task SignIn(Provider provider, SignInOptions? op { await DestroySession(); - var providerUri = api.GetUriForProvider(provider, options); + var providerUri = _api.GetUriForProvider(provider, options); return providerUri; } @@ -404,6 +399,7 @@ public async Task SignIn(Provider provider, SignInOptions? op /// /// The user's phone number. /// Token sent to the user's phone. + /// SMS or phone change /// public async Task VerifyOTP(string phone, string token, MobileOtpType type = MobileOtpType.SMS) { @@ -411,7 +407,7 @@ public async Task SignIn(Provider provider, SignInOptions? op { await DestroySession(); - var session = await api.VerifyMobileOTP(phone, token, type); + var session = await _api.VerifyMobileOTP(phone, token, type); if (session?.AccessToken != null) { @@ -433,7 +429,7 @@ public async Task SignIn(Provider provider, SignInOptions? op /// /// /// - /// + /// Defaults to MagicLink /// public async Task VerifyOTP(string email, string token, EmailOtpType type = EmailOtpType.MagicLink) { @@ -441,7 +437,7 @@ public async Task SignIn(Provider provider, SignInOptions? op { await DestroySession(); - var session = await api.VerifyEmailOTP(email, token, type); + var session = await _api.VerifyEmailOTP(email, token, type); if (session?.AccessToken != null) { @@ -467,10 +463,9 @@ public async Task SignOut() if (CurrentSession != null) { if (CurrentSession.AccessToken != null) - await api.SignOut(CurrentSession.AccessToken); + await _api.SignOut(CurrentSession.AccessToken); - if (refreshTimer != null) - refreshTimer.Dispose(); + _refreshTimer?.Dispose(); await DestroySession(); @@ -490,7 +485,7 @@ public async Task SignOut() try { - var result = await api.UpdateUser(CurrentSession.AccessToken!, attributes); + var result = await _api.UpdateUser(CurrentSession.AccessToken!, attributes); CurrentUser = result; @@ -514,7 +509,7 @@ public async Task InviteUserByEmail(string email, string jwt) { try { - var response = await api.InviteUserByEmail(email, jwt); + var response = await _api.InviteUserByEmail(email, jwt); response.ResponseMessage?.EnsureSuccessStatusCode(); return true; } @@ -534,7 +529,7 @@ public async Task DeleteUser(string uid, string jwt) { try { - var result = await api.DeleteUser(uid, jwt); + var result = await _api.DeleteUser(uid, jwt); result.ResponseMessage?.EnsureSuccessStatusCode(); return true; } @@ -558,7 +553,7 @@ public async Task DeleteUser(string uid, string jwt) { try { - return await api.ListUsers(jwt, filter, sortBy, sortOrder, page, perPage); + return await _api.ListUsers(jwt, filter, sortBy, sortOrder, page, perPage); } catch (RequestException ex) { @@ -576,7 +571,7 @@ public async Task DeleteUser(string uid, string jwt) { try { - return await api.GetUserById(jwt, userId); + return await _api.GetUserById(jwt, userId); } catch (RequestException ex) { @@ -593,7 +588,7 @@ public async Task DeleteUser(string uid, string jwt) { try { - return await api.GetUser(jwt); + return await _api.GetUser(jwt); } catch (RequestException ex) { @@ -631,7 +626,7 @@ public async Task DeleteUser(string uid, string jwt) { try { - return await api.CreateUser(jwt, attributes); + return await _api.CreateUser(jwt, attributes); } catch (RequestException ex) { @@ -650,7 +645,7 @@ public async Task DeleteUser(string uid, string jwt) { try { - return await api.UpdateUserById(jwt, userId, userData); + return await _api.UpdateUserById(jwt, userId, userData); } catch (RequestException ex) { @@ -668,7 +663,7 @@ public async Task ResetPasswordForEmail(string email) { try { - var result = await api.ResetPasswordForEmail(email); + var result = await _api.ResetPasswordForEmail(email); result.ResponseMessage?.EnsureSuccessStatusCode(); return true; } @@ -689,7 +684,7 @@ public async Task ResetPasswordForEmail(string email) await RefreshToken(); - var user = await api.GetUser(CurrentSession.AccessToken!); + var user = await _api.GetUser(CurrentSession.AccessToken!); CurrentUser = user; return CurrentSession; @@ -747,7 +742,7 @@ public Session SetAuth(string accessToken) if (string.IsNullOrEmpty(tokenType)) throw new Exception("No token_type detected."); - var user = await api.GetUser(accessToken); + var user = await _api.GetUser(accessToken); var session = new Session { @@ -827,7 +822,7 @@ public Session SetAuth(string accessToken) /// public async Task ExchangeCodeForSession(string codeVerifier, string authCode) { - var result = await api.ExchangeCodeForSession(codeVerifier, authCode); + var result = await _api.ExchangeCodeForSession(codeVerifier, authCode); if (result != null) { @@ -843,7 +838,7 @@ public Session SetAuth(string accessToken) /// Persists a Session in memory and calls (if specified) /// /// - internal async Task PersistSession(Session session) + private async Task PersistSession(Session session) { CurrentSession = session; CurrentUser = session.User; @@ -860,7 +855,7 @@ internal async Task PersistSession(Session session) /// /// Persists a Session in memory and calls (if specified) /// - internal async Task DestroySession() + private async Task DestroySession() { CurrentSession = null; CurrentUser = null; @@ -873,14 +868,14 @@ internal async Task DestroySession() /// Refreshes a Token /// /// - internal async Task RefreshToken(string? refreshToken = null) + private async Task RefreshToken(string? refreshToken = null) { if (CurrentSession == null || string.IsNullOrEmpty(CurrentSession?.RefreshToken) && string.IsNullOrEmpty(refreshToken)) throw new Exception("No current session."); refreshToken ??= CurrentSession!.RefreshToken; - var result = await api.RefreshAccessToken(refreshToken!); + var result = await _api.RefreshAccessToken(refreshToken!); if (result == null || string.IsNullOrEmpty(result.AccessToken)) throw new Exception("Could not refresh token from provided session."); @@ -898,12 +893,12 @@ internal async Task RefreshToken(string? refreshToken = null) InitRefreshTimer(); } - internal void InitRefreshTimer() + private void InitRefreshTimer() { if (CurrentSession == null || CurrentSession.ExpiresIn == default) return; - if (refreshTimer != null) - refreshTimer.Dispose(); + if (_refreshTimer != null) + _refreshTimer.Dispose(); try { @@ -912,7 +907,7 @@ internal void InitRefreshTimer() int timeoutSeconds = Convert.ToInt32((CurrentSession.CreatedAt.AddSeconds(interval) - DateTime.Now).TotalSeconds); TimeSpan timeout = TimeSpan.FromSeconds(timeoutSeconds); - refreshTimer = new Timer(HandleRefreshTimerTick, null, timeout, Timeout.InfiniteTimeSpan); + _refreshTimer = new Timer(HandleRefreshTimerTick, null, timeout, Timeout.InfiniteTimeSpan); } catch { @@ -920,9 +915,9 @@ internal void InitRefreshTimer() } } - internal async void HandleRefreshTimerTick(object _) + private async void HandleRefreshTimerTick(object _) { - refreshTimer?.Dispose(); + _refreshTimer?.Dispose(); try { @@ -933,7 +928,7 @@ internal async void HandleRefreshTimerTick(object _) { // The request failed - potential network error? Debug.WriteLine(ex.Message); - refreshTimer = new Timer(HandleRefreshTimerTick, null, 5000, -1); + _refreshTimer = new Timer(HandleRefreshTimerTick, null, 5000, -1); } catch (Exception ex) { From 25d698c1a4719c625efec82c3f97c4c210718b28 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Wed, 3 May 2023 17:11:48 -0700 Subject: [PATCH 04/74] Remove unneeded declarations --- Gotrue/ClientOptions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gotrue/ClientOptions.cs b/Gotrue/ClientOptions.cs index 2944c6a..fd0c767 100644 --- a/Gotrue/ClientOptions.cs +++ b/Gotrue/ClientOptions.cs @@ -34,7 +34,7 @@ public class ClientOptions /// /// Function called to persist the session (probably on a filesystem or cookie) /// - public Func> SessionPersistor = (TSession session) => Task.FromResult(true); + public Func> SessionPersistor = session => Task.FromResult(true); /// /// Function to retrieve a session (probably from the filesystem or cookie) @@ -52,6 +52,6 @@ public class ClientOptions /// Enables tests to be E2E tests to be run without requiring users to have /// confirmed emails - mirrors the Gotrue server's configuration. /// - public bool AllowUnconfirmedUserSessions { get; set; } = false; + public bool AllowUnconfirmedUserSessions { get; set; } } } From e72c649c9a4288b6ec7f1554b18465af9676c591 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Wed, 3 May 2023 17:11:59 -0700 Subject: [PATCH 05/74] Remove unused imports. --- Gotrue/Constants.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Gotrue/Constants.cs b/Gotrue/Constants.cs index 6c8f58b..f9d6cfb 100644 --- a/Gotrue/Constants.cs +++ b/Gotrue/Constants.cs @@ -1,6 +1,5 @@ -using Supabase.Core.Attributes; -using System; -using System.Collections.Generic; +using System.Collections.Generic; +using Supabase.Core.Attributes; namespace Supabase.Gotrue { From 71dea23f17147f5fbbbf38b6c8f3bb7c2e0f3376 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Wed, 3 May 2023 17:12:15 -0700 Subject: [PATCH 06/74] Simplify imports --- Gotrue/ExceptionHandler.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Gotrue/ExceptionHandler.cs b/Gotrue/ExceptionHandler.cs index 6b0d3cb..cbfdb52 100644 --- a/Gotrue/ExceptionHandler.cs +++ b/Gotrue/ExceptionHandler.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Net; using Supabase.Gotrue.Exceptions; namespace Supabase.Gotrue @@ -13,13 +14,13 @@ internal static Exception Parse(RequestException ex) { switch (ex.Response.StatusCode) { - case System.Net.HttpStatusCode.Unauthorized: + case HttpStatusCode.Unauthorized: Debug.WriteLine(ex.Message); return new UnauthorizedException(ex); - case System.Net.HttpStatusCode.BadRequest: + case HttpStatusCode.BadRequest: Debug.WriteLine(ex.Message); return new BadRequestException(ex); - case System.Net.HttpStatusCode.Forbidden: + case HttpStatusCode.Forbidden: Debug.WriteLine("Forbidden, are sign-ups disabled?"); return new ForbiddenException(ex); } From a04feb90d05fcde59ecf60423c64831e20e7eeee Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Wed, 3 May 2023 17:12:25 -0700 Subject: [PATCH 07/74] Remove unused imports --- Gotrue/Exceptions/BadRequestException.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Gotrue/Exceptions/BadRequestException.cs b/Gotrue/Exceptions/BadRequestException.cs index 29d6b89..59dc229 100644 --- a/Gotrue/Exceptions/BadRequestException.cs +++ b/Gotrue/Exceptions/BadRequestException.cs @@ -1,5 +1,4 @@ -using System; -using System.Net.Http; +using System.Net.Http; namespace Supabase.Gotrue.Exceptions { From 6fb637649b4abb11808708133d9e4b2b9e95510c Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Wed, 3 May 2023 17:12:32 -0700 Subject: [PATCH 08/74] Remove unused imports --- Gotrue/Exceptions/ExistingUserException.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Gotrue/Exceptions/ExistingUserException.cs b/Gotrue/Exceptions/ExistingUserException.cs index 666014d..186a35d 100644 --- a/Gotrue/Exceptions/ExistingUserException.cs +++ b/Gotrue/Exceptions/ExistingUserException.cs @@ -1,5 +1,4 @@ -using System; -using System.Net.Http; +using System.Net.Http; namespace Supabase.Gotrue.Exceptions { From 4191d973a700fdb9561fe645881380af18383485 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Wed, 3 May 2023 17:12:43 -0700 Subject: [PATCH 09/74] Remove unused imports --- Gotrue/Exceptions/ForbiddenException.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Gotrue/Exceptions/ForbiddenException.cs b/Gotrue/Exceptions/ForbiddenException.cs index 8408f6c..a1fac07 100644 --- a/Gotrue/Exceptions/ForbiddenException.cs +++ b/Gotrue/Exceptions/ForbiddenException.cs @@ -1,5 +1,4 @@ -using System; -using System.Net.Http; +using System.Net.Http; namespace Supabase.Gotrue.Exceptions { From 484fd4b224f1b14252ba757ac5385344696c9193 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Wed, 3 May 2023 17:12:51 -0700 Subject: [PATCH 10/74] Remove unused imports --- Gotrue/Exceptions/GotrueException.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Gotrue/Exceptions/GotrueException.cs b/Gotrue/Exceptions/GotrueException.cs index a396e22..20d0a42 100644 --- a/Gotrue/Exceptions/GotrueException.cs +++ b/Gotrue/Exceptions/GotrueException.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace Supabase.Gotrue.Exceptions { From 3abb4e6fd3596f5d6658bd794cd3f432383a1e19 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Wed, 3 May 2023 17:12:58 -0700 Subject: [PATCH 11/74] Remove unused imports --- Gotrue/Exceptions/InvalidEmailOrPasswordException.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Gotrue/Exceptions/InvalidEmailOrPasswordException.cs b/Gotrue/Exceptions/InvalidEmailOrPasswordException.cs index 95da518..6d6e264 100644 --- a/Gotrue/Exceptions/InvalidEmailOrPasswordException.cs +++ b/Gotrue/Exceptions/InvalidEmailOrPasswordException.cs @@ -1,5 +1,4 @@ -using System; -using System.Net.Http; +using System.Net.Http; namespace Supabase.Gotrue.Exceptions { From e54ec16b75f310c4b525211662a675c4cfa2eef4 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Wed, 3 May 2023 17:13:08 -0700 Subject: [PATCH 12/74] Remove unused imports --- Gotrue/Exceptions/InvalidProviderException.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Gotrue/Exceptions/InvalidProviderException.cs b/Gotrue/Exceptions/InvalidProviderException.cs index 959f244..4e29664 100644 --- a/Gotrue/Exceptions/InvalidProviderException.cs +++ b/Gotrue/Exceptions/InvalidProviderException.cs @@ -1,5 +1,4 @@ -using System; -namespace Supabase.Gotrue.Exceptions +namespace Supabase.Gotrue.Exceptions { public class InvalidProviderException : GotrueException { From fa2201976ef82ab5b8ff1017222e5e7149341352 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Wed, 3 May 2023 17:13:16 -0700 Subject: [PATCH 13/74] Remove unused import --- Gotrue/Exceptions/RequestException.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Gotrue/Exceptions/RequestException.cs b/Gotrue/Exceptions/RequestException.cs index b03b01b..27b86c7 100644 --- a/Gotrue/Exceptions/RequestException.cs +++ b/Gotrue/Exceptions/RequestException.cs @@ -1,5 +1,4 @@ -using System; -using System.Net.Http; +using System.Net.Http; using Supabase.Gotrue.Exceptions; using Supabase.Gotrue.Responses; From 07291eac88008b6553e2752188d16b1049d2a8d4 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Wed, 3 May 2023 17:13:23 -0700 Subject: [PATCH 14/74] Remove unused import --- Gotrue/Exceptions/UnauthorizedException.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Gotrue/Exceptions/UnauthorizedException.cs b/Gotrue/Exceptions/UnauthorizedException.cs index fe786e2..0d5cab1 100644 --- a/Gotrue/Exceptions/UnauthorizedException.cs +++ b/Gotrue/Exceptions/UnauthorizedException.cs @@ -1,5 +1,4 @@ -using System; -using System.Net.Http; +using System.Net.Http; namespace Supabase.Gotrue.Exceptions { From 5f6497cc3e653cb35b4e732368ebb760cfc4fcb2 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Wed, 3 May 2023 17:13:58 -0700 Subject: [PATCH 15/74] Removed unused imports. A few of these imports can cause problems (e.g. Linq, compiler services) --- Gotrue/Helpers.cs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/Gotrue/Helpers.cs b/Gotrue/Helpers.cs index 9d63de7..7fa3187 100644 --- a/Gotrue/Helpers.cs +++ b/Gotrue/Helpers.cs @@ -1,17 +1,13 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Net.Http; +using System.Security.Cryptography; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; using Newtonsoft.Json; -using System.Runtime.CompilerServices; using Supabase.Gotrue.Responses; -using System.Threading; -using System.Linq; -using System.Security.Cryptography; -using System.Text.RegularExpressions; namespace Supabase.Gotrue { @@ -101,7 +97,7 @@ internal static Uri AddQueryParams(string url, Dictionary data) /// /// /// - /// + /// /// /// internal static async Task MakeRequest(HttpMethod method, string url, object? data = null, Dictionary? headers = null) where T : class @@ -115,7 +111,7 @@ internal static Uri AddQueryParams(string url, Dictionary data) /// /// /// - /// + /// /// /// internal static async Task MakeRequest(HttpMethod method, string url, object? data = null, Dictionary? headers = null) @@ -164,10 +160,8 @@ internal static async Task MakeRequest(HttpMethod method, string u }; throw new RequestException(response, obj); } - else - { - return new BaseResponse { Content = content, ResponseMessage = response }; - } + + return new BaseResponse { Content = content, ResponseMessage = response }; } } } From 4ed3faa73e669f803102918aedbc86154cbeeab3 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Wed, 3 May 2023 17:14:07 -0700 Subject: [PATCH 16/74] Simplify imports --- Gotrue/Interfaces/IGotrueApi.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gotrue/Interfaces/IGotrueApi.cs b/Gotrue/Interfaces/IGotrueApi.cs index 0a3d7db..9540bc3 100644 --- a/Gotrue/Interfaces/IGotrueApi.cs +++ b/Gotrue/Interfaces/IGotrueApi.cs @@ -1,6 +1,6 @@ -using Supabase.Core.Interfaces; +using System.Threading.Tasks; +using Supabase.Core.Interfaces; using Supabase.Gotrue.Responses; -using System.Threading.Tasks; using static Supabase.Gotrue.Constants; namespace Supabase.Gotrue.Interfaces @@ -14,7 +14,7 @@ public interface IGotrueApi : IGettableHeaders Task GetUser(string jwt); Task GetUserById(string jwt, string userId); Task InviteUserByEmail(string email, string jwt); - Task?> ListUsers(string jwt, string? filter = null, string? sortBy = null, Constants.SortOrder sortOrder = Constants.SortOrder.Descending, int? page = null, int? perPage = null); + Task?> ListUsers(string jwt, string? filter = null, string? sortBy = null, SortOrder sortOrder = SortOrder.Descending, int? page = null, int? perPage = null); Task RefreshAccessToken(string refreshToken); Task ResetPasswordForEmail(string email); Task SendMagicLinkEmail(string email, SignInOptions? options = null); From 5e56caf039f46d5d7cd45a6957897e99828668af Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Wed, 3 May 2023 17:14:15 -0700 Subject: [PATCH 17/74] Simplify imports --- Gotrue/Interfaces/IGotrueClient.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gotrue/Interfaces/IGotrueClient.cs b/Gotrue/Interfaces/IGotrueClient.cs index 909b643..fc687a8 100644 --- a/Gotrue/Interfaces/IGotrueClient.cs +++ b/Gotrue/Interfaces/IGotrueClient.cs @@ -1,6 +1,6 @@ -using Supabase.Core.Interfaces; -using System; +using System; using System.Threading.Tasks; +using Supabase.Core.Interfaces; using static Supabase.Gotrue.Constants; namespace Supabase.Gotrue.Interfaces @@ -20,7 +20,7 @@ public interface IGotrueClient : IGettableHeaders Task GetUser(string jwt); Task GetUserById(string jwt, string userId); Task InviteUserByEmail(string email, string jwt); - Task?> ListUsers(string jwt, string? filter = null, string? sortBy = null, Constants.SortOrder sortOrder = Constants.SortOrder.Descending, int? page = null, int? perPage = null); + Task?> ListUsers(string jwt, string? filter = null, string? sortBy = null, SortOrder sortOrder = SortOrder.Descending, int? page = null, int? perPage = null); Task RefreshSession(); Task ResetPasswordForEmail(string email); Task RetrieveSessionAsync(); From db1e73c0eddea6706d7eb4cea6406919f0fc9de7 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Wed, 3 May 2023 17:14:55 -0700 Subject: [PATCH 18/74] Simplify imports --- Gotrue/Interfaces/IGotrueStatelessClient.cs | 4 ++-- Gotrue/PasswordlessSignInState.cs | 6 +----- Gotrue/Responses/BaseResponse.cs | 3 +-- Gotrue/Responses/ErrorResponse.cs | 5 +---- Gotrue/User.cs | 4 ++-- GotrueTests/StatelessClientTests.cs | 1 - 6 files changed, 7 insertions(+), 16 deletions(-) diff --git a/Gotrue/Interfaces/IGotrueStatelessClient.cs b/Gotrue/Interfaces/IGotrueStatelessClient.cs index c3b5729..af89c5a 100644 --- a/Gotrue/Interfaces/IGotrueStatelessClient.cs +++ b/Gotrue/Interfaces/IGotrueStatelessClient.cs @@ -17,7 +17,7 @@ public interface IGotrueStatelessClient Task GetUser(string jwt, StatelessClientOptions options); Task GetUserById(string jwt, StatelessClientOptions options, string userId); Task InviteUserByEmail(string email, string jwt, StatelessClientOptions options); - Task?> ListUsers(string jwt, StatelessClientOptions options, string? filter = null, string? sortBy = null, Constants.SortOrder sortOrder = Constants.SortOrder.Descending, int? page = null, int? perPage = null); + Task?> ListUsers(string jwt, StatelessClientOptions options, string? filter = null, string? sortBy = null, SortOrder sortOrder = SortOrder.Descending, int? page = null, int? perPage = null); Task RefreshToken(string refreshToken, StatelessClientOptions options); Task ResetPasswordForEmail(string email, StatelessClientOptions options); Task SendMagicLink(string email, StatelessClientOptions options, SignInOptions? signInOptions = null); @@ -26,7 +26,7 @@ public interface IGotrueStatelessClient Task SignIn(string email, StatelessClientOptions options, SignInOptions? signInOptions = null); Task SignIn(string email, string password, StatelessClientOptions options); Task SignOut(string jwt, StatelessClientOptions options); - Task SignUp(Constants.SignUpType type, string identifier, string password, StatelessClientOptions options, SignUpOptions? signUpOptions = null); + Task SignUp(SignUpType type, string identifier, string password, StatelessClientOptions options, SignUpOptions? signUpOptions = null); Task SignUp(string email, string password, StatelessClientOptions options, SignUpOptions? signUpOptions = null); Task Update(string accessToken, UserAttributes attributes, StatelessClientOptions options); Task UpdateUserById(string jwt, StatelessClientOptions options, string userId, AdminUserAttributes userData); diff --git a/Gotrue/PasswordlessSignInState.cs b/Gotrue/PasswordlessSignInState.cs index 98226f4..abe2434 100644 --- a/Gotrue/PasswordlessSignInState.cs +++ b/Gotrue/PasswordlessSignInState.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Supabase.Gotrue +namespace Supabase.Gotrue { /// /// A utility class that represents a successful response from a request to send a user diff --git a/Gotrue/Responses/BaseResponse.cs b/Gotrue/Responses/BaseResponse.cs index ce82851..57f0583 100644 --- a/Gotrue/Responses/BaseResponse.cs +++ b/Gotrue/Responses/BaseResponse.cs @@ -1,5 +1,4 @@ -using System; -using System.Net.Http; +using System.Net.Http; using Newtonsoft.Json; namespace Supabase.Gotrue.Responses diff --git a/Gotrue/Responses/ErrorResponse.cs b/Gotrue/Responses/ErrorResponse.cs index 7f9c5e3..9516ab9 100644 --- a/Gotrue/Responses/ErrorResponse.cs +++ b/Gotrue/Responses/ErrorResponse.cs @@ -1,7 +1,4 @@ -using System; -using Newtonsoft.Json; - -namespace Supabase.Gotrue.Responses +namespace Supabase.Gotrue.Responses { /// /// A representation of Postgrest's API error response. diff --git a/Gotrue/User.cs b/Gotrue/User.cs index f428aa3..79a84e4 100644 --- a/Gotrue/User.cs +++ b/Gotrue/User.cs @@ -1,8 +1,8 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; using Newtonsoft.Json; -#nullable enable namespace Supabase.Gotrue { /// diff --git a/GotrueTests/StatelessClientTests.cs b/GotrueTests/StatelessClientTests.cs index 200f49d..d036bc6 100644 --- a/GotrueTests/StatelessClientTests.cs +++ b/GotrueTests/StatelessClientTests.cs @@ -7,7 +7,6 @@ using Microsoft.IdentityModel.Tokens; using Microsoft.VisualStudio.TestTools.UnitTesting; using Supabase.Gotrue; -using static Supabase.Gotrue.Client; using static Supabase.Gotrue.StatelessClient; using static Supabase.Gotrue.Constants; using Supabase.Gotrue.Exceptions; From 59362e56c5784d0cce9ea05bec43a740a0173ba1 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Wed, 3 May 2023 17:15:02 -0700 Subject: [PATCH 19/74] Fix doc --- Gotrue/SignInOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gotrue/SignInOptions.cs b/Gotrue/SignInOptions.cs index 89822c4..4223345 100644 --- a/Gotrue/SignInOptions.cs +++ b/Gotrue/SignInOptions.cs @@ -3,7 +3,7 @@ namespace Supabase.Gotrue { - // + /// /// Options used for signing in a user. /// public class SignInOptions From 14b990b122ec7b1bba4359f90de98f3d1bb7266c Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Wed, 3 May 2023 17:15:11 -0700 Subject: [PATCH 20/74] Simplify imports, fix doc --- Gotrue/SignInWithPasswordlessEmailOptions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gotrue/SignInWithPasswordlessEmailOptions.cs b/Gotrue/SignInWithPasswordlessEmailOptions.cs index 5c4e905..d8a441c 100644 --- a/Gotrue/SignInWithPasswordlessEmailOptions.cs +++ b/Gotrue/SignInWithPasswordlessEmailOptions.cs @@ -1,10 +1,10 @@ -using Supabase.Core.Attributes; -using System.Collections.Generic; +using System.Collections.Generic; +using Supabase.Core.Attributes; using static Supabase.Gotrue.Constants; namespace Supabase.Gotrue { - // + /// /// Options used for signing in a user with passwordless Options /// public class SignInWithPasswordlessOptions From e26eeb8a8d0ea3ba218cbec7ce122b039462ff1f Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Wed, 3 May 2023 17:15:27 -0700 Subject: [PATCH 21/74] Simplify imports, doc fixes --- Gotrue/StatelessClient.cs | 53 +++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/Gotrue/StatelessClient.cs b/Gotrue/StatelessClient.cs index 7e15caf..2ef1ba1 100644 --- a/Gotrue/StatelessClient.cs +++ b/Gotrue/StatelessClient.cs @@ -1,8 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Net.Http; -using System.Threading; using System.Threading.Tasks; using System.Web; using Supabase.Gotrue.Interfaces; @@ -27,6 +24,7 @@ public class StatelessClient : IGotrueStatelessClient /// /// /// + /// /// Object containing redirectTo and optional user metadata (data) /// public Task SignUp(string email, string password, StatelessClientOptions options, SignUpOptions? signUpOptions = null) => SignUp(SignUpType.Email, email, password, options, signUpOptions); @@ -37,6 +35,7 @@ public class StatelessClient : IGotrueStatelessClient /// Type of signup /// Phone or Email /// + /// /// Object containing redirectTo and optional user metadata (data) /// public async Task SignUp(SignUpType type, string identifier, string password, StatelessClientOptions options, SignUpOptions? signUpOptions = null) @@ -93,6 +92,8 @@ public async Task SignIn(string email, StatelessClientOptions options, Sig /// Sends a Magic email login link to the specified email. /// /// + /// + /// /// public Task SendMagicLink(string email, StatelessClientOptions options, SignInOptions? signInOptions = null) => SignIn(email, options, signInOptions); @@ -101,6 +102,7 @@ public async Task SignIn(string email, StatelessClientOptions options, Sig /// /// /// + /// /// public Task SignIn(string email, string password, StatelessClientOptions options) => SignIn(SignInType.Email, email, password, options); @@ -110,7 +112,7 @@ public async Task SignIn(string email, StatelessClientOptions options, Sig /// Type of Credentials being passed /// An email, phone, or RefreshToken /// Password to account (optional if `RefreshToken`) - /// A space-separated list of scopes granted to the OAuth application. + /// /// public async Task SignIn(SignInType type, string identifierOrToken, string? password = null, StatelessClientOptions? options = null) { @@ -132,10 +134,8 @@ public async Task SignIn(string email, StatelessClientOptions options, Sig var response = await api.SendMobileOTP(identifierOrToken); return null; } - else - { - session = await api.SignInWithPhone(identifierOrToken, password!); - } + + session = await api.SignInWithPhone(identifierOrToken, password!); break; case SignInType.RefreshToken: session = await RefreshToken(identifierOrToken, options); @@ -157,23 +157,24 @@ public async Task SignIn(string email, StatelessClientOptions options, Sig /// /// Retrieves a Url to redirect to for signing in with a . - /// + /// /// This method will need to be combined with when the /// Application receives the Oauth Callback. /// /// /// var client = Supabase.Gotrue.Client.Initialize(options); /// var url = client.SignIn(Provider.Github); - /// + /// /// // Do Redirect User - /// + /// /// // Example code /// Application.HasRecievedOauth += async (uri) => { /// var session = await client.GetSessionFromUri(uri, true); /// } /// /// - /// A space-separated list of scopes granted to the OAuth application. + /// + /// /// public ProviderAuthState SignIn(Provider provider, StatelessClientOptions options, SignInOptions? signInOptions = null) => GetApi(options).GetUriForProvider(provider, signInOptions); @@ -182,8 +183,8 @@ public async Task SignIn(string email, StatelessClientOptions options, Sig /// This will revoke all refresh tokens for the user. /// JWT tokens will still be valid for stateless auth until they expire. /// + /// /// - /// /// public async Task SignOut(string jwt, StatelessClientOptions options) { @@ -204,6 +205,8 @@ public async Task SignOut(string jwt, StatelessClientOptions options) /// /// The user's phone number. /// Token sent to the user's phone. + /// + /// /// public async Task VerifyOTP(string phone, string token, StatelessClientOptions options, MobileOtpType type = MobileOtpType.SMS) { @@ -229,6 +232,7 @@ public async Task SignOut(string jwt, StatelessClientOptions options) /// /// /// + /// /// /// public async Task VerifyOTP(string email, string token, StatelessClientOptions options, EmailOtpType type = EmailOtpType.MagicLink) @@ -254,7 +258,9 @@ public async Task SignOut(string jwt, StatelessClientOptions options) /// /// Updates a User. /// + /// /// + /// /// public async Task Update(string accessToken, UserAttributes attributes, StatelessClientOptions options) { @@ -274,6 +280,7 @@ public async Task SignOut(string jwt, StatelessClientOptions options) /// /// /// this token needs role 'supabase_admin' or 'service_role' + /// /// public async Task InviteUserByEmail(string email, string jwt, StatelessClientOptions options) { @@ -293,6 +300,7 @@ public async Task InviteUserByEmail(string email, string jwt, StatelessCli /// Sends a reset request to an email address. /// /// + /// /// /// public async Task ResetPasswordForEmail(string email, StatelessClientOptions options) @@ -313,8 +321,9 @@ public async Task ResetPasswordForEmail(string email, StatelessClientOptio /// Lists users /// /// A valid JWT. Must be a full-access API key (e.g. service_role key). + /// /// A string for example part of the email - /// Snake case string of the given key, currently only created_at is suppported + /// Snake case string of the given key, currently only created_at is supported /// asc or desc, if null desc is used /// page to show for pagination /// items per page for pagination @@ -335,6 +344,7 @@ public async Task ResetPasswordForEmail(string email, StatelessClientOptio /// Get User details by Id /// /// A valid JWT. Must be a full-access API key (e.g. service_role key). + /// /// /// public async Task GetUserById(string jwt, StatelessClientOptions options, string userId) @@ -353,6 +363,7 @@ public async Task ResetPasswordForEmail(string email, StatelessClientOptio /// Get User details by JWT. Can be used to validate a JWT. /// /// A valid JWT. Must be a JWT that originates from a user. + /// /// public async Task GetUser(string jwt, StatelessClientOptions options) { @@ -370,6 +381,7 @@ public async Task ResetPasswordForEmail(string email, StatelessClientOptio /// Create a user /// /// A valid JWT. Must be a full-access API key (e.g. service_role key). + /// /// /// /// @@ -390,6 +402,7 @@ public async Task ResetPasswordForEmail(string email, StatelessClientOptio /// Create a user /// /// A valid JWT. Must be a full-access API key (e.g. service_role key). + /// /// /// public async Task CreateUser(string jwt, StatelessClientOptions options, AdminUserAttributes attributes) @@ -408,6 +421,7 @@ public async Task ResetPasswordForEmail(string email, StatelessClientOptio /// Update user by Id /// /// A valid JWT. Must be a full-access API key (e.g. service_role key). + /// /// /// /// @@ -428,6 +442,7 @@ public async Task ResetPasswordForEmail(string email, StatelessClientOptio /// /// /// this token needs role 'supabase_admin' or 'service_role' + /// /// public async Task DeleteUser(string uid, string jwt, StatelessClientOptions options) { @@ -447,7 +462,7 @@ public async Task DeleteUser(string uid, string jwt, StatelessClientOption /// Parses a out of a 's Query parameters. /// /// - /// + /// /// public async Task GetSessionFromUrl(Uri uri, StatelessClientOptions options) { @@ -500,19 +515,19 @@ public async Task DeleteUser(string uid, string jwt, StatelessClientOption /// - /// Class represention options available to the . + /// Class representation options available to the . /// public class StatelessClientOptions { /// /// Gotrue Endpoint /// - public string Url { get; set; } = Constants.GOTRUE_URL; + public string Url { get; set; } = GOTRUE_URL; /// /// Headers to be sent with subsequent requests. /// - public Dictionary Headers = new Dictionary(Constants.DEFAULT_HEADERS); + public Dictionary Headers = new Dictionary(DEFAULT_HEADERS); /// /// Very unlikely this flag needs to be changed except in very specific contexts. @@ -520,7 +535,7 @@ public class StatelessClientOptions /// Enables tests to be E2E tests to be run without requiring users to have /// confirmed emails - mirrors the Gotrue server's configuration. /// - public bool AllowUnconfirmedUserSessions { get; set; } = false; + public bool AllowUnconfirmedUserSessions { get; set; } } } From a1e482170572df82a201426868ab846421cfaa48 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Wed, 3 May 2023 17:15:42 -0700 Subject: [PATCH 22/74] Add bit more coverage for nonce tests --- GotrueTests/ClientTests.cs | 193 +++++++++++++++++-------------------- 1 file changed, 90 insertions(+), 103 deletions(-) diff --git a/GotrueTests/ClientTests.cs b/GotrueTests/ClientTests.cs index 020a8eb..ece489a 100644 --- a/GotrueTests/ClientTests.cs +++ b/GotrueTests/ClientTests.cs @@ -8,52 +8,39 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Supabase.Gotrue; using static Supabase.Gotrue.Constants; -using static Supabase.Gotrue.Client; using Supabase.Gotrue.Exceptions; -namespace GotrueTests -{ +namespace GotrueTests { [TestClass] - public class ClientTests - { - private Supabase.Gotrue.Client client; + public class ClientTests { + private Client client; private string password = "I@M@SuperP@ssWord"; private static Random random = new Random(); - private static string RandomString(int length) - { + private static string RandomString(int length) { const string chars = "abcdefghijklmnopqrstuvwxyz0123456789"; - return new string(Enumerable.Repeat(chars, length) - .Select(s => s[random.Next(s.Length)]).ToArray()); + return new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray()); } - private static string GetRandomPhoneNumber() - { + private static string GetRandomPhoneNumber() { const string chars = "123456789"; - var inner = new string(Enumerable.Repeat(chars, 10) - .Select(s => s[random.Next(s.Length)]).ToArray()); + var inner = new string(Enumerable.Repeat(chars, 10).Select(s => s[random.Next(s.Length)]).ToArray()); return $"+1{inner}"; } - private string GenerateServiceRoleToken() - { - var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("37c304f8-51aa-419a-a1af-06154e63707a")); // using GOTRUE_JWT_SECRET + private string GenerateServiceRoleToken() { + var signingKey = + new SymmetricSecurityKey( + Encoding.UTF8.GetBytes("37c304f8-51aa-419a-a1af-06154e63707a")); // using GOTRUE_JWT_SECRET - var tokenDescriptor = new SecurityTokenDescriptor - { + var tokenDescriptor = new SecurityTokenDescriptor { IssuedAt = DateTime.Now, Expires = DateTime.UtcNow.AddDays(7), - SigningCredentials = - new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256Signature), - Claims = new Dictionary() - { - { - "role", "service_role" - } - } + SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256Signature), + Claims = new Dictionary() { { "role", "service_role" } } }; var tokenHandler = new JwtSecurityTokenHandler(); @@ -62,15 +49,13 @@ private string GenerateServiceRoleToken() } [TestInitialize] - public void TestInitializer() - { + public void TestInitializer() { client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true }); } [TestMethod("Client: Signs Up User")] - public async Task ClientSignsUpUser() - { - Session session = null; + public async Task ClientSignsUpUser() { + Session session; var email = $"{RandomString(12)}@supabase.io"; session = await client.SignUp(email, password); @@ -78,20 +63,23 @@ public async Task ClientSignsUpUser() Assert.IsNotNull(session.RefreshToken); Assert.IsInstanceOfType(session.User, typeof(User)); - var phone1 = GetRandomPhoneNumber(); - session = await client.SignUp(SignUpType.Phone, phone1, password, new SignUpOptions { Data = new Dictionary { { "firstName", "Testing" } } }); + session = await client.SignUp(SignUpType.Phone, + phone1, + password, + new SignUpOptions { Data = new Dictionary { { "firstName", "Testing" } } }); Assert.IsNotNull(session.AccessToken); Assert.AreEqual("Testing", session.User.UserMetadata["firstName"]); } [TestMethod("Client: Signs Up the same user twice should throw BadRequestException")] - public async Task ClientSignsUpUserTwiceShouldReturnBadRequest() - { + public async Task ClientSignsUpUserTwiceShouldReturnBadRequest() { var email = $"{RandomString(12)}@supabase.io"; var result1 = await client.SignUp(email, password); + Assert.IsNotNull(result1); + await Assert.ThrowsExceptionAsync(async () => { await client.SignUp(email, password); @@ -99,8 +87,7 @@ await Assert.ThrowsExceptionAsync(async () => } [TestMethod("Client: Triggers Token Refreshed Event")] - public async Task ClientTriggersTokenRefreshedEvent() - { + public async Task ClientTriggersTokenRefreshedEvent() { var tsc = new TaskCompletionSource(); var email = $"{RandomString(12)}@supabase.io"; @@ -108,8 +95,7 @@ public async Task ClientTriggersTokenRefreshedEvent() client.StateChanged += (sender, args) => { - if (args.State == AuthState.TokenRefreshed) - { + if (args.State == AuthState.TokenRefreshed) { tsc.SetResult(client.CurrentSession.AccessToken); } }; @@ -122,8 +108,7 @@ public async Task ClientTriggersTokenRefreshedEvent() } [TestMethod("Client: Signs In User (Email, Phone, Refresh token)")] - public async Task ClientSignsIn() - { + public async Task ClientSignsIn() { Session session = null; string refreshToken = ""; @@ -162,8 +147,7 @@ public async Task ClientSignsIn() } [TestMethod("Client: Sends Magic Login Email")] - public async Task ClientSendsMagicLoginEmail() - { + public async Task ClientSendsMagicLoginEmail() { var user = $"{RandomString(12)}@supabase.io"; await client.SignUp(user, password); @@ -174,8 +158,7 @@ public async Task ClientSendsMagicLoginEmail() } [TestMethod("Client: Sends Magic Login Email (Alias)")] - public async Task ClientSendsMagicLoginEmailAlias() - { + public async Task ClientSendsMagicLoginEmailAlias() { var user = $"{RandomString(12)}@supabase.io"; var user2 = $"{RandomString(12)}@supabase.io"; await client.SignUp(user, password); @@ -183,7 +166,8 @@ public async Task ClientSendsMagicLoginEmailAlias() await client.SignOut(); var result = await client.SendMagicLink(user); - var result2 = await client.SendMagicLink(user2, new SignInOptions { RedirectTo = $"com.{RandomString(12)}.deeplink://login" }); + var result2 = await client.SendMagicLink(user2, + new SignInOptions { RedirectTo = $"com.{RandomString(12)}.deeplink://login" }); Assert.IsTrue(result); Assert.IsTrue(result2); @@ -191,19 +175,17 @@ public async Task ClientSendsMagicLoginEmailAlias() [TestMethod("Client: Returns Auth Url for Provider")] - public async Task ClientReturnsAuthUrlForProvider() - { + public async Task ClientReturnsAuthUrlForProvider() { var result1 = await client.SignIn(Provider.Google); Assert.AreEqual("http://localhost:9999/authorize?provider=google", result1.Uri.ToString()); var result2 = await client.SignIn(Provider.Google, new SignInOptions { Scopes = "special scopes please" }); - Assert.AreEqual("http://localhost:9999/authorize?provider=google&scopes=special+scopes+please", result2.Uri.ToString()); + Assert.AreEqual("http://localhost:9999/authorize?provider=google&scopes=special+scopes+please", + result2.Uri.ToString()); } [TestMethod("Client: Returns Verification Code for Provider")] - public async Task ClientReturnsPKCEVerifier() - { - + public async Task ClientReturnsPKCEVerifier() { var result = await client.SignIn(Provider.Github, new SignInOptions { FlowType = OAuthFlowType.PKCE }); Assert.IsTrue(!string.IsNullOrEmpty(result.PKCEVerifier)); @@ -214,38 +196,26 @@ public async Task ClientReturnsPKCEVerifier() } [TestMethod("Client: Update user")] - public async Task ClientUpdateUser() - { + public async Task ClientUpdateUser() { var email = $"{RandomString(12)}@supabase.io"; var session = await client.SignUp(email, password); - var attributes = new UserAttributes - { - Data = new Dictionary - { - {"hello", "world" } - } - }; + var attributes = new UserAttributes { Data = new Dictionary { { "hello", "world" } } }; var result = await client.Update(attributes); Assert.AreEqual(email, client.CurrentUser.Email); Assert.IsNotNull(client.CurrentUser.UserMetadata); await client.SignOut(); var token = GenerateServiceRoleToken(); - var result2 = await client.UpdateUserById(token, session.User.Id, new AdminUserAttributes - { - UserMetadata = new Dictionary - { - {"hello", "updated" } - } - }); + var result2 = await client.UpdateUserById(token, + session.User.Id, + new AdminUserAttributes { UserMetadata = new Dictionary { { "hello", "updated" } } }); Assert.AreNotEqual(result.UserMetadata["hello"], result2.UserMetadata["hello"]); } [TestMethod("Client: Returns current user")] - public async Task ClientGetUser() - { + public async Task ClientGetUser() { var email = $"{RandomString(12)}@supabase.io"; var newUser = await client.SignUp(email, password); @@ -256,8 +226,7 @@ public async Task ClientGetUser() } [TestMethod("Client: Nulls CurrentUser on SignOut")] - public async Task ClientGetUserAfterLogOut() - { + public async Task ClientGetUserAfterLogOut() { var user = $"{RandomString(12)}@supabase.io"; await client.SignUp(user, password); @@ -267,8 +236,7 @@ public async Task ClientGetUserAfterLogOut() } [TestMethod("Client: Throws Exception on Invalid Username and Password")] - public async Task ClientSignsInUserWrongPassword() - { + public async Task ClientSignsInUserWrongPassword() { var user = $"{RandomString(12)}@supabase.io"; await client.SignUp(user, password); @@ -278,12 +246,10 @@ await Assert.ThrowsExceptionAsync(async () => { var result = await client.SignIn(user, password + "$"); }); - } [TestMethod("Client: Sends Invite Email")] - public async Task ClientSendsInviteEmail() - { + public async Task ClientSendsInviteEmail() { var user = $"{RandomString(12)}@supabase.io"; var service_role_key = GenerateServiceRoleToken(); var result = await client.InviteUserByEmail(user, service_role_key); @@ -291,8 +257,7 @@ public async Task ClientSendsInviteEmail() } [TestMethod("Client: Lists users")] - public async Task ClientListUsers() - { + public async Task ClientListUsers() { var service_role_key = GenerateServiceRoleToken(); var result = await client.ListUsers(service_role_key); @@ -300,8 +265,7 @@ public async Task ClientListUsers() } [TestMethod("Client: Lists users pagination")] - public async Task ClientListUsersPagination() - { + public async Task ClientListUsersPagination() { var service_role_key = GenerateServiceRoleToken(); var page1 = await client.ListUsers(service_role_key, page: 1, perPage: 1); @@ -313,19 +277,19 @@ public async Task ClientListUsersPagination() } [TestMethod("Client: Lists users sort")] - public async Task ClientListUsersSort() - { + public async Task ClientListUsersSort() { var service_role_key = GenerateServiceRoleToken(); - var result1 = await client.ListUsers(service_role_key, sortBy: "created_at", sortOrder: SortOrder.Ascending); - var result2 = await client.ListUsers(service_role_key, sortBy: "created_at", sortOrder: SortOrder.Descending); + var result1 = + await client.ListUsers(service_role_key, sortBy: "created_at", sortOrder: SortOrder.Ascending); + var result2 = + await client.ListUsers(service_role_key, sortBy: "created_at", sortOrder: SortOrder.Descending); Assert.AreNotEqual(result1.Users[0].Id, result2.Users[0].Id); } [TestMethod("Client: Lists users filter")] - public async Task ClientListUsersFilter() - { + public async Task ClientListUsersFilter() { var service_role_key = GenerateServiceRoleToken(); var user = $"{RandomString(12)}@supabase.io"; @@ -340,8 +304,7 @@ public async Task ClientListUsersFilter() } [TestMethod("Client: Get User by Id")] - public async Task ClientGetUserById() - { + public async Task ClientGetUserById() { var service_role_key = GenerateServiceRoleToken(); var result = await client.ListUsers(service_role_key, page: 1, perPage: 1); @@ -353,37 +316,40 @@ public async Task ClientGetUserById() } [TestMethod("Client: Create a user")] - public async Task ClientCreateUser() - { + public async Task ClientCreateUser() { var service_role_key = GenerateServiceRoleToken(); var result = await client.CreateUser(service_role_key, $"{RandomString(12)}@supabase.io", password); Assert.IsNotNull(result); - var attributes = new AdminUserAttributes - { + var attributes = new AdminUserAttributes { UserMetadata = new Dictionary { { "firstName", "123" } }, AppMetadata = new Dictionary { { "roles", new List { "editor", "publisher" } } } }; - var result2 = await client.CreateUser(service_role_key, $"{RandomString(12)}@supabase.io", password, attributes); + var result2 = await client.CreateUser(service_role_key, + $"{RandomString(12)}@supabase.io", + password, + attributes); Assert.AreEqual("123", result2.UserMetadata["firstName"]); - var result3 = await client.CreateUser(service_role_key, new AdminUserAttributes { Email = $"{RandomString(12)}@supabase.io", Password = password }); + var result3 = await client.CreateUser(service_role_key, + new AdminUserAttributes { Email = $"{RandomString(12)}@supabase.io", Password = password }); Assert.IsNotNull(result3); } [TestMethod("Client: Update User by Id")] - public async Task ClientUpdateUserById() - { + public async Task ClientUpdateUserById() { var service_role_key = GenerateServiceRoleToken(); var createdUser = await client.CreateUser(service_role_key, $"{RandomString(12)}@supabase.io", password); Assert.IsNotNull(createdUser); - var updatedUser = await client.UpdateUserById(service_role_key, createdUser.Id, new AdminUserAttributes { Email = $"{RandomString(12)}@supabase.io" }); + var updatedUser = await client.UpdateUserById(service_role_key, + createdUser.Id, + new AdminUserAttributes { Email = $"{RandomString(12)}@supabase.io" }); Assert.IsNotNull(updatedUser); @@ -392,8 +358,7 @@ public async Task ClientUpdateUserById() } [TestMethod("Client: Deletes User")] - public async Task ClientDeletesUser() - { + public async Task ClientDeletesUser() { var user = $"{RandomString(12)}@supabase.io"; await client.SignUp(user, password); var uid = client.CurrentUser.Id; @@ -405,14 +370,36 @@ public async Task ClientDeletesUser() } [TestMethod("Client: Sends Reset Password Email")] - public async Task ClientSendsResetPasswordForEmail() - { + public async Task ClientSendsResetPasswordForEmail() { var email = $"{RandomString(12)}@supabase.io"; await client.SignUp(email, password); var result = await client.ResetPasswordForEmail(email); Assert.IsTrue(result); } + [TestMethod("Nonce generation and verification")] + public async Task NonceGeneration() { + string nonce = Helpers.GenerateNonce(); + Assert.IsNotNull(nonce); + Assert.AreEqual(128, nonce.Length); + + string pkceVerifier = Helpers.GeneratePKCENonceVerifier(nonce); + Assert.IsNotNull(pkceVerifier); + Assert.AreEqual(43, pkceVerifier.Length); + string appleVerifier = Helpers.GenerateSHA256NonceFromRawNonce(nonce); + Assert.IsNotNull(appleVerifier); + Assert.AreEqual(64, appleVerifier.Length); + + const string helloNonce = "hello_world_nonce"; + + string helloPkceVerifier = Helpers.GeneratePKCENonceVerifier(helloNonce); + Assert.IsNotNull(helloPkceVerifier); + Assert.AreEqual("9TMmi4JOlYOQEP2Ha39WXj9pySILGnAfQsz-yXws0yE", helloPkceVerifier); + + string helloAppleVerifier = Helpers.GenerateSHA256NonceFromRawNonce(helloNonce); + Assert.IsNotNull(helloAppleVerifier); + Assert.AreEqual("f533268b824e95839010fd876b7f565e3f69c9220b1a701f42ccfec97c2cd321", helloAppleVerifier); + } } } From 6b002a2e07499d4a6654cd4a3a771f675fc3c65a Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 12:44:13 -0700 Subject: [PATCH 23/74] Import code formatting rules from existing code base --- gotrue-csharp.sln.DotSettings | 43 +++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/gotrue-csharp.sln.DotSettings b/gotrue-csharp.sln.DotSettings index 9fb10bd..0510fb7 100644 --- a/gotrue-csharp.sln.DotSettings +++ b/gotrue-csharp.sln.DotSettings @@ -1,4 +1,47 @@  + AccessorsWithExpressionBody + Required + False + ExpressionBody + internal private protected public file new static abstract virtual sealed async override extern unsafe volatile readonly required + Remove + BaseClass + NEXT_LINE + NEXT_LINE + 0 + 1 + 0 + 0 + 0 + TOGETHER_SAME_LINE + Tab + NEXT_LINE + NEXT_LINE + True + True + NEXT_LINE + NEVER + True + True + ALWAYS + ALWAYS + ALWAYS + NEVER + False + NEVER + False + False + False + False + NEXT_LINE + WRAP_IF_LONG + CHOP_ALWAYS + 225 + CHOP_ALWAYS + CHOP_ALWAYS + UseVar + UseVar + UseVar True True True From c987e7e91e7d2e32b088e2bcae2f2252026dde27 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 13:46:37 -0700 Subject: [PATCH 24/74] Remove debug. Fix floating point warning. --- Gotrue/Client.cs | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/Gotrue/Client.cs b/Gotrue/Client.cs index 4d1d51e..4646915 100644 --- a/Gotrue/Client.cs +++ b/Gotrue/Client.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -20,10 +19,19 @@ namespace Supabase.Gotrue /// public class Client : IGotrueClient { + + private DebugNotification? _debugNotification; + + public void AddDebugListener(Action listener) + { + _debugNotification ??= new DebugNotification(); + _debugNotification.AddDebugListener(listener); + } + /// /// Function that can be set to return dynamic headers. /// - /// Headers specified in the client options will ALWAYS take precendece over headers returned by this function. + /// Headers specified in the client options will ALWAYS take precedence over headers returned by this function. /// public Func>? GetHeaders @@ -346,7 +354,8 @@ public async Task SignInWithOtp(SignInWithPasswordlessP session = await _api.SignInWithEmail(identifierOrToken, password!); break; case SignInType.Phone: - if (string.IsNullOrEmpty(password)) { + if (string.IsNullOrEmpty(password)) + { await _api.SendMobileOTP(identifierOrToken); return null; } @@ -549,7 +558,8 @@ public async Task DeleteUser(string uid, string jwt) /// page to show for pagination /// items per page for pagination /// - public async Task?> ListUsers(string jwt, string? filter = null, string? sortBy = null, SortOrder sortOrder = SortOrder.Descending, int? page = null, int? perPage = null) + public async Task?> ListUsers(string jwt, string? filter = null, string? sortBy = null, SortOrder sortOrder = SortOrder.Descending, int? page = null, + int? perPage = null) { try { @@ -798,7 +808,7 @@ public Session SetAuth(string accessToken) } else if (session == null || session.User == null) { - Debug.WriteLine("Stored Session is missing data."); + _debugNotification?.Log("Stored Session is missing data."); await DestroySession(); return null; } @@ -903,7 +913,7 @@ private void InitRefreshTimer() try { // Interval should be t - (1/5(n)) (i.e. if session time (t) 3600s, attempt refresh at 2880s or 720s (1/5) seconds before expiration) - int interval = (int)Math.Floor((double)(CurrentSession.ExpiresIn * 4 / 5)); + int interval = (int)Math.Floor(CurrentSession.ExpiresIn * 4.0f / 5.0f); int timeoutSeconds = Convert.ToInt32((CurrentSession.CreatedAt.AddSeconds(interval) - DateTime.Now).TotalSeconds); TimeSpan timeout = TimeSpan.FromSeconds(timeoutSeconds); @@ -911,7 +921,7 @@ private void InitRefreshTimer() } catch { - Debug.WriteLine("Unable to parse session timestamp, refresh timer will not work. If persisting, open issue on Github"); + _debugNotification?.Log("Unable to parse session timestamp, refresh timer will not work. If persisting, open issue on Github"); } } @@ -927,12 +937,12 @@ private async void HandleRefreshTimerTick(object _) catch (HttpRequestException ex) { // The request failed - potential network error? - Debug.WriteLine(ex.Message); + _debugNotification?.Log(ex.Message, ex); _refreshTimer = new Timer(HandleRefreshTimerTick, null, 5000, -1); } catch (Exception ex) { - Debug.WriteLine(ex.Message); + _debugNotification?.Log(ex.Message, ex); StateChanged?.Invoke(this, new ClientStateChanged(AuthState.SignedOut)); } } From 2a5302f10744f397344965fe6b83ebbd0b78e63c Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 13:47:32 -0700 Subject: [PATCH 25/74] Remove debug. --- Gotrue/DebugNotification.cs | 23 ++++++++++++++++++++ Gotrue/ExceptionHandler.cs | 42 +++++++++++++++++-------------------- 2 files changed, 42 insertions(+), 23 deletions(-) create mode 100644 Gotrue/DebugNotification.cs diff --git a/Gotrue/DebugNotification.cs b/Gotrue/DebugNotification.cs new file mode 100644 index 0000000..6dc4314 --- /dev/null +++ b/Gotrue/DebugNotification.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace Supabase.Gotrue +{ + public class DebugNotification + { + private readonly List> _debugListeners = new List>(); + + public void AddDebugListener(Action listener) + { + _debugListeners.Add(listener); + } + + public void Log(string message, Exception? e = null) + { + foreach (var l in _debugListeners) + { + l.Invoke(message, e); + } + } + } +} diff --git a/Gotrue/ExceptionHandler.cs b/Gotrue/ExceptionHandler.cs index cbfdb52..7fc1a9c 100644 --- a/Gotrue/ExceptionHandler.cs +++ b/Gotrue/ExceptionHandler.cs @@ -1,30 +1,26 @@ using System; -using System.Diagnostics; using System.Net; using Supabase.Gotrue.Exceptions; namespace Supabase.Gotrue { - /// - /// Internal class for parsing Supabase specific exceptions. - /// - internal static class ExceptionHandler - { - internal static Exception Parse(RequestException ex) - { - switch (ex.Response.StatusCode) - { - case HttpStatusCode.Unauthorized: - Debug.WriteLine(ex.Message); - return new UnauthorizedException(ex); - case HttpStatusCode.BadRequest: - Debug.WriteLine(ex.Message); - return new BadRequestException(ex); - case HttpStatusCode.Forbidden: - Debug.WriteLine("Forbidden, are sign-ups disabled?"); - return new ForbiddenException(ex); - } - return ex; - } - } + /// + /// Internal class for parsing Supabase specific exceptions. + /// + internal static class ExceptionHandler + { + internal static Exception Parse(RequestException ex) + { + switch (ex.Response.StatusCode) + { + case HttpStatusCode.Unauthorized: + return new UnauthorizedException(ex); + case HttpStatusCode.BadRequest: + return new BadRequestException(ex); + case HttpStatusCode.Forbidden: + return new ForbiddenException(ex); + } + return ex; + } + } } From 84b06986ea47ea8508c71f8689197af74d384337 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 13:47:55 -0700 Subject: [PATCH 26/74] Replace debug. Also ran code format on existing rules. --- GotrueTests/ClientTests.cs | 775 ++++++++++++++-------------- GotrueTests/StatelessClientTests.cs | 657 ++++++++++++----------- 2 files changed, 726 insertions(+), 706 deletions(-) diff --git a/GotrueTests/ClientTests.cs b/GotrueTests/ClientTests.cs index ece489a..e688b6b 100644 --- a/GotrueTests/ClientTests.cs +++ b/GotrueTests/ClientTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Text; @@ -10,396 +11,418 @@ using static Supabase.Gotrue.Constants; using Supabase.Gotrue.Exceptions; -namespace GotrueTests { - [TestClass] - public class ClientTests { - private Client client; - - private string password = "I@M@SuperP@ssWord"; - - private static Random random = new Random(); +namespace GotrueTests +{ + [TestClass] + public class ClientTests + { + private Client client; + + private string password = "I@M@SuperP@ssWord"; + + private static Random random = new Random(); + + private static string RandomString(int length) + { + const string chars = "abcdefghijklmnopqrstuvwxyz0123456789"; + return new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray()); + } + + private static string GetRandomPhoneNumber() + { + const string chars = "123456789"; + var inner = new string(Enumerable.Repeat(chars, 10).Select(s => s[random.Next(s.Length)]).ToArray()); + + return $"+1{inner}"; + } + + private string GenerateServiceRoleToken() + { + var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("37c304f8-51aa-419a-a1af-06154e63707a")); // using GOTRUE_JWT_SECRET + + var tokenDescriptor = new SecurityTokenDescriptor + { + IssuedAt = DateTime.Now, + Expires = DateTime.UtcNow.AddDays(7), + SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256Signature), + Claims = new Dictionary() { { "role", "service_role" } } + }; + + var tokenHandler = new JwtSecurityTokenHandler(); + var securityToken = tokenHandler.CreateToken(tokenDescriptor); + return tokenHandler.WriteToken(securityToken); + } + + [TestInitialize] + public void TestInitializer() + { + client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true }); + client.AddDebugListener(LogDebug); + } + private static void LogDebug(string message, Exception e) + { + Debug.WriteLine(message); + if (e != null) + Debug.WriteLine(e); + } + + [TestMethod("Client: Signs Up User")] + public async Task ClientSignsUpUser() + { + Session session; + var email = $"{RandomString(12)}@supabase.io"; + session = await client.SignUp(email, password); + + Assert.IsNotNull(session.AccessToken); + Assert.IsNotNull(session.RefreshToken); + Assert.IsInstanceOfType(session.User, typeof(User)); + + var phone1 = GetRandomPhoneNumber(); + session = await client.SignUp(SignUpType.Phone, phone1, password, new SignUpOptions { Data = new Dictionary { { "firstName", "Testing" } } }); + + Assert.IsNotNull(session.AccessToken); + Assert.AreEqual("Testing", session.User.UserMetadata["firstName"]); + } + + [TestMethod("Client: Signs Up the same user twice should throw BadRequestException")] + public async Task ClientSignsUpUserTwiceShouldReturnBadRequest() + { + var email = $"{RandomString(12)}@supabase.io"; + var result1 = await client.SignUp(email, password); + + Assert.IsNotNull(result1); + + await Assert.ThrowsExceptionAsync(async () => + { + await client.SignUp(email, password); + }); + } + + [TestMethod("Client: Triggers Token Refreshed Event")] + public async Task ClientTriggersTokenRefreshedEvent() + { + var tsc = new TaskCompletionSource(); + + var email = $"{RandomString(12)}@supabase.io"; + var user = await client.SignUp(email, password); + + client.StateChanged += (sender, args) => + { + if (args.State == AuthState.TokenRefreshed) + { + tsc.SetResult(client.CurrentSession.AccessToken); + } + }; + + await client.RefreshSession(); + + var newToken = await tsc.Task; + + Assert.AreNotEqual(user.RefreshToken, client.CurrentSession.RefreshToken); + } + + [TestMethod("Client: Signs In User (Email, Phone, Refresh token)")] + public async Task ClientSignsIn() + { + Session session = null; + string refreshToken = ""; + + // Emails + var email = $"{RandomString(12)}@supabase.io"; + await client.SignUp(email, password); + + await client.SignOut(); + + session = await client.SignIn(email, password); + + Assert.IsNotNull(session.AccessToken); + Assert.IsNotNull(session.RefreshToken); + Assert.IsInstanceOfType(session.User, typeof(User)); + + // Phones + var phone = GetRandomPhoneNumber(); + await client.SignUp(SignUpType.Phone, phone, password); + + await client.SignOut(); + + session = await client.SignIn(SignInType.Phone, phone, password); + + Assert.IsNotNull(session.AccessToken); + Assert.IsNotNull(session.RefreshToken); + Assert.IsInstanceOfType(session.User, typeof(User)); + + // Refresh Token + refreshToken = session.RefreshToken; + + var newSession = await client.SignIn(SignInType.RefreshToken, refreshToken); + + Assert.IsNotNull(newSession.AccessToken); + Assert.IsNotNull(newSession.RefreshToken); + Assert.IsInstanceOfType(newSession.User, typeof(User)); + } - private static string RandomString(int length) { - const string chars = "abcdefghijklmnopqrstuvwxyz0123456789"; - return new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray()); - } + [TestMethod("Client: Sends Magic Login Email")] + public async Task ClientSendsMagicLoginEmail() + { + var user = $"{RandomString(12)}@supabase.io"; + await client.SignUp(user, password); - private static string GetRandomPhoneNumber() { - const string chars = "123456789"; - var inner = new string(Enumerable.Repeat(chars, 10).Select(s => s[random.Next(s.Length)]).ToArray()); + await client.SignOut(); - return $"+1{inner}"; - } + var result = await client.SignIn(user); + Assert.IsTrue(result); + } + + [TestMethod("Client: Sends Magic Login Email (Alias)")] + public async Task ClientSendsMagicLoginEmailAlias() + { + var user = $"{RandomString(12)}@supabase.io"; + var user2 = $"{RandomString(12)}@supabase.io"; + await client.SignUp(user, password); + + await client.SignOut(); + + var result = await client.SendMagicLink(user); + var result2 = await client.SendMagicLink(user2, new SignInOptions { RedirectTo = $"com.{RandomString(12)}.deeplink://login" }); + + Assert.IsTrue(result); + Assert.IsTrue(result2); + } - private string GenerateServiceRoleToken() { - var signingKey = - new SymmetricSecurityKey( - Encoding.UTF8.GetBytes("37c304f8-51aa-419a-a1af-06154e63707a")); // using GOTRUE_JWT_SECRET - var tokenDescriptor = new SecurityTokenDescriptor { - IssuedAt = DateTime.Now, - Expires = DateTime.UtcNow.AddDays(7), - SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256Signature), - Claims = new Dictionary() { { "role", "service_role" } } - }; + [TestMethod("Client: Returns Auth Url for Provider")] + public async Task ClientReturnsAuthUrlForProvider() + { + var result1 = await client.SignIn(Provider.Google); + Assert.AreEqual("http://localhost:9999/authorize?provider=google", result1.Uri.ToString()); + + var result2 = await client.SignIn(Provider.Google, new SignInOptions { Scopes = "special scopes please" }); + Assert.AreEqual("http://localhost:9999/authorize?provider=google&scopes=special+scopes+please", result2.Uri.ToString()); + } + + [TestMethod("Client: Returns Verification Code for Provider")] + public async Task ClientReturnsPKCEVerifier() + { + var result = await client.SignIn(Provider.Github, new SignInOptions { FlowType = OAuthFlowType.PKCE }); + + Assert.IsTrue(!string.IsNullOrEmpty(result.PKCEVerifier)); + Assert.IsTrue(result.Uri.Query.Contains("flow_type=pkce")); + Assert.IsTrue(result.Uri.Query.Contains("code_challenge=")); + Assert.IsTrue(result.Uri.Query.Contains("code_challenge_method=s256")); + Assert.IsTrue(result.Uri.Query.Contains("provider=github")); + } - var tokenHandler = new JwtSecurityTokenHandler(); - var securityToken = tokenHandler.CreateToken(tokenDescriptor); - return tokenHandler.WriteToken(securityToken); - } + [TestMethod("Client: Update user")] + public async Task ClientUpdateUser() + { + var email = $"{RandomString(12)}@supabase.io"; + var session = await client.SignUp(email, password); - [TestInitialize] - public void TestInitializer() { - client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true }); - } + var attributes = new UserAttributes { Data = new Dictionary { { "hello", "world" } } }; + var result = await client.Update(attributes); + Assert.AreEqual(email, client.CurrentUser.Email); + Assert.IsNotNull(client.CurrentUser.UserMetadata); + + await client.SignOut(); + var token = GenerateServiceRoleToken(); + var result2 = await client.UpdateUserById(token, session.User.Id, new AdminUserAttributes { UserMetadata = new Dictionary { { "hello", "updated" } } }); - [TestMethod("Client: Signs Up User")] - public async Task ClientSignsUpUser() { - Session session; - var email = $"{RandomString(12)}@supabase.io"; - session = await client.SignUp(email, password); + Assert.AreNotEqual(result.UserMetadata["hello"], result2.UserMetadata["hello"]); + } + + [TestMethod("Client: Returns current user")] + public async Task ClientGetUser() + { + var email = $"{RandomString(12)}@supabase.io"; + var newUser = await client.SignUp(email, password); + + Assert.AreEqual(email, client.CurrentUser.Email); + + var userByJWT = await client.GetUser(newUser.AccessToken); + Assert.AreEqual(email, userByJWT.Email); + } + + [TestMethod("Client: Nulls CurrentUser on SignOut")] + public async Task ClientGetUserAfterLogOut() + { + var user = $"{RandomString(12)}@supabase.io"; + await client.SignUp(user, password); + + await client.SignOut(); + + Assert.IsNull(client.CurrentUser); + } + + [TestMethod("Client: Throws Exception on Invalid Username and Password")] + public async Task ClientSignsInUserWrongPassword() + { + var user = $"{RandomString(12)}@supabase.io"; + await client.SignUp(user, password); + + await client.SignOut(); + + await Assert.ThrowsExceptionAsync(async () => + { + var result = await client.SignIn(user, password + "$"); + }); + } + + [TestMethod("Client: Sends Invite Email")] + public async Task ClientSendsInviteEmail() + { + var user = $"{RandomString(12)}@supabase.io"; + var service_role_key = GenerateServiceRoleToken(); + var result = await client.InviteUserByEmail(user, service_role_key); + Assert.IsTrue(result); + } + + [TestMethod("Client: Lists users")] + public async Task ClientListUsers() + { + var service_role_key = GenerateServiceRoleToken(); + var result = await client.ListUsers(service_role_key); + + Assert.IsTrue(result.Users.Count > 0); + } + + [TestMethod("Client: Lists users pagination")] + public async Task ClientListUsersPagination() + { + var service_role_key = GenerateServiceRoleToken(); + + var page1 = await client.ListUsers(service_role_key, page: 1, perPage: 1); + var page2 = await client.ListUsers(service_role_key, page: 2, perPage: 1); + + Assert.AreEqual(page1.Users.Count, 1); + Assert.AreEqual(page2.Users.Count, 1); + Assert.AreNotEqual(page1.Users[0].Id, page2.Users[0].Id); + } + + [TestMethod("Client: Lists users sort")] + public async Task ClientListUsersSort() + { + var service_role_key = GenerateServiceRoleToken(); + + var result1 = await client.ListUsers(service_role_key, sortBy: "created_at", sortOrder: SortOrder.Ascending); + var result2 = await client.ListUsers(service_role_key, sortBy: "created_at", sortOrder: SortOrder.Descending); + + Assert.AreNotEqual(result1.Users[0].Id, result2.Users[0].Id); + } + + [TestMethod("Client: Lists users filter")] + public async Task ClientListUsersFilter() + { + var service_role_key = GenerateServiceRoleToken(); + + var user = $"{RandomString(12)}@supabase.io"; + var result = await client.SignUp(user, password); + + var result1 = await client.ListUsers(service_role_key, filter: "@nonexistingrandomemailprovider.com"); + var result2 = await client.ListUsers(service_role_key, filter: "@supabase.io"); + + Assert.AreNotEqual(result2.Users.Count, 0); + Assert.AreEqual(result1.Users.Count, 0); + Assert.AreNotEqual(result1.Users.Count, result2.Users.Count); + } + + [TestMethod("Client: Get User by Id")] + public async Task ClientGetUserById() + { + var service_role_key = GenerateServiceRoleToken(); + var result = await client.ListUsers(service_role_key, page: 1, perPage: 1); + + var userResult = result.Users[0]; + var userByIdResult = await client.GetUserById(service_role_key, userResult.Id); + + Assert.AreEqual(userResult.Id, userByIdResult.Id); + Assert.AreEqual(userResult.Email, userByIdResult.Email); + } + + [TestMethod("Client: Create a user")] + public async Task ClientCreateUser() + { + var service_role_key = GenerateServiceRoleToken(); + var result = await client.CreateUser(service_role_key, $"{RandomString(12)}@supabase.io", password); + + Assert.IsNotNull(result); + + + var attributes = new AdminUserAttributes + { + UserMetadata = new Dictionary { { "firstName", "123" } }, + AppMetadata = new Dictionary { { "roles", new List { "editor", "publisher" } } } + }; + + var result2 = await client.CreateUser(service_role_key, $"{RandomString(12)}@supabase.io", password, attributes); + Assert.AreEqual("123", result2.UserMetadata["firstName"]); + + var result3 = await client.CreateUser(service_role_key, new AdminUserAttributes { Email = $"{RandomString(12)}@supabase.io", Password = password }); + Assert.IsNotNull(result3); + } + + + [TestMethod("Client: Update User by Id")] + public async Task ClientUpdateUserById() + { + var service_role_key = GenerateServiceRoleToken(); + var createdUser = await client.CreateUser(service_role_key, $"{RandomString(12)}@supabase.io", password); + + Assert.IsNotNull(createdUser); + + var updatedUser = await client.UpdateUserById(service_role_key, createdUser.Id, new AdminUserAttributes { Email = $"{RandomString(12)}@supabase.io" }); + + Assert.IsNotNull(updatedUser); + + Assert.AreEqual(createdUser.Id, updatedUser.Id); + Assert.AreNotEqual(createdUser.Email, updatedUser.Email); + } + + [TestMethod("Client: Deletes User")] + public async Task ClientDeletesUser() + { + var user = $"{RandomString(12)}@supabase.io"; + await client.SignUp(user, password); + var uid = client.CurrentUser.Id; - Assert.IsNotNull(session.AccessToken); - Assert.IsNotNull(session.RefreshToken); - Assert.IsInstanceOfType(session.User, typeof(User)); + var service_role_key = GenerateServiceRoleToken(); + var result = await client.DeleteUser(uid, service_role_key); - var phone1 = GetRandomPhoneNumber(); - session = await client.SignUp(SignUpType.Phone, - phone1, - password, - new SignUpOptions { Data = new Dictionary { { "firstName", "Testing" } } }); + Assert.IsTrue(result); + } - Assert.IsNotNull(session.AccessToken); - Assert.AreEqual("Testing", session.User.UserMetadata["firstName"]); - } + [TestMethod("Client: Sends Reset Password Email")] + public async Task ClientSendsResetPasswordForEmail() + { + var email = $"{RandomString(12)}@supabase.io"; + await client.SignUp(email, password); + var result = await client.ResetPasswordForEmail(email); + Assert.IsTrue(result); + } - [TestMethod("Client: Signs Up the same user twice should throw BadRequestException")] - public async Task ClientSignsUpUserTwiceShouldReturnBadRequest() { - var email = $"{RandomString(12)}@supabase.io"; - var result1 = await client.SignUp(email, password); + [TestMethod("Nonce generation and verification")] + public void NonceGeneration() + { + string nonce = Helpers.GenerateNonce(); + Assert.IsNotNull(nonce); + Assert.AreEqual(128, nonce.Length); - Assert.IsNotNull(result1); + string pkceVerifier = Helpers.GeneratePKCENonceVerifier(nonce); + Assert.IsNotNull(pkceVerifier); + Assert.AreEqual(43, pkceVerifier.Length); + + string appleVerifier = Helpers.GenerateSHA256NonceFromRawNonce(nonce); + Assert.IsNotNull(appleVerifier); + Assert.AreEqual(64, appleVerifier.Length); - await Assert.ThrowsExceptionAsync(async () => - { - await client.SignUp(email, password); - }); - } + const string helloNonce = "hello_world_nonce"; - [TestMethod("Client: Triggers Token Refreshed Event")] - public async Task ClientTriggersTokenRefreshedEvent() { - var tsc = new TaskCompletionSource(); + string helloPkceVerifier = Helpers.GeneratePKCENonceVerifier(helloNonce); + Assert.IsNotNull(helloPkceVerifier); + Assert.AreEqual("9TMmi4JOlYOQEP2Ha39WXj9pySILGnAfQsz-yXws0yE", helloPkceVerifier); - var email = $"{RandomString(12)}@supabase.io"; - var user = await client.SignUp(email, password); - - client.StateChanged += (sender, args) => - { - if (args.State == AuthState.TokenRefreshed) { - tsc.SetResult(client.CurrentSession.AccessToken); - } - }; - - await client.RefreshSession(); - - var newToken = await tsc.Task; - - Assert.AreNotEqual(user.RefreshToken, client.CurrentSession.RefreshToken); - } - - [TestMethod("Client: Signs In User (Email, Phone, Refresh token)")] - public async Task ClientSignsIn() { - Session session = null; - string refreshToken = ""; - - // Emails - var email = $"{RandomString(12)}@supabase.io"; - await client.SignUp(email, password); - - await client.SignOut(); - - session = await client.SignIn(email, password); - - Assert.IsNotNull(session.AccessToken); - Assert.IsNotNull(session.RefreshToken); - Assert.IsInstanceOfType(session.User, typeof(User)); - - // Phones - var phone = GetRandomPhoneNumber(); - await client.SignUp(SignUpType.Phone, phone, password); - - await client.SignOut(); - - session = await client.SignIn(SignInType.Phone, phone, password); - - Assert.IsNotNull(session.AccessToken); - Assert.IsNotNull(session.RefreshToken); - Assert.IsInstanceOfType(session.User, typeof(User)); - - // Refresh Token - refreshToken = session.RefreshToken; - - var newSession = await client.SignIn(SignInType.RefreshToken, refreshToken); - - Assert.IsNotNull(newSession.AccessToken); - Assert.IsNotNull(newSession.RefreshToken); - Assert.IsInstanceOfType(newSession.User, typeof(User)); - } - - [TestMethod("Client: Sends Magic Login Email")] - public async Task ClientSendsMagicLoginEmail() { - var user = $"{RandomString(12)}@supabase.io"; - await client.SignUp(user, password); - - await client.SignOut(); - - var result = await client.SignIn(user); - Assert.IsTrue(result); - } - - [TestMethod("Client: Sends Magic Login Email (Alias)")] - public async Task ClientSendsMagicLoginEmailAlias() { - var user = $"{RandomString(12)}@supabase.io"; - var user2 = $"{RandomString(12)}@supabase.io"; - await client.SignUp(user, password); - - await client.SignOut(); - - var result = await client.SendMagicLink(user); - var result2 = await client.SendMagicLink(user2, - new SignInOptions { RedirectTo = $"com.{RandomString(12)}.deeplink://login" }); - - Assert.IsTrue(result); - Assert.IsTrue(result2); - } - - - [TestMethod("Client: Returns Auth Url for Provider")] - public async Task ClientReturnsAuthUrlForProvider() { - var result1 = await client.SignIn(Provider.Google); - Assert.AreEqual("http://localhost:9999/authorize?provider=google", result1.Uri.ToString()); - - var result2 = await client.SignIn(Provider.Google, new SignInOptions { Scopes = "special scopes please" }); - Assert.AreEqual("http://localhost:9999/authorize?provider=google&scopes=special+scopes+please", - result2.Uri.ToString()); - } - - [TestMethod("Client: Returns Verification Code for Provider")] - public async Task ClientReturnsPKCEVerifier() { - var result = await client.SignIn(Provider.Github, new SignInOptions { FlowType = OAuthFlowType.PKCE }); - - Assert.IsTrue(!string.IsNullOrEmpty(result.PKCEVerifier)); - Assert.IsTrue(result.Uri.Query.Contains("flow_type=pkce")); - Assert.IsTrue(result.Uri.Query.Contains("code_challenge=")); - Assert.IsTrue(result.Uri.Query.Contains("code_challenge_method=s256")); - Assert.IsTrue(result.Uri.Query.Contains("provider=github")); - } - - [TestMethod("Client: Update user")] - public async Task ClientUpdateUser() { - var email = $"{RandomString(12)}@supabase.io"; - var session = await client.SignUp(email, password); - - var attributes = new UserAttributes { Data = new Dictionary { { "hello", "world" } } }; - var result = await client.Update(attributes); - Assert.AreEqual(email, client.CurrentUser.Email); - Assert.IsNotNull(client.CurrentUser.UserMetadata); - - await client.SignOut(); - var token = GenerateServiceRoleToken(); - var result2 = await client.UpdateUserById(token, - session.User.Id, - new AdminUserAttributes { UserMetadata = new Dictionary { { "hello", "updated" } } }); - - Assert.AreNotEqual(result.UserMetadata["hello"], result2.UserMetadata["hello"]); - } - - [TestMethod("Client: Returns current user")] - public async Task ClientGetUser() { - var email = $"{RandomString(12)}@supabase.io"; - var newUser = await client.SignUp(email, password); - - Assert.AreEqual(email, client.CurrentUser.Email); - - var userByJWT = await client.GetUser(newUser.AccessToken); - Assert.AreEqual(email, userByJWT.Email); - } - - [TestMethod("Client: Nulls CurrentUser on SignOut")] - public async Task ClientGetUserAfterLogOut() { - var user = $"{RandomString(12)}@supabase.io"; - await client.SignUp(user, password); - - await client.SignOut(); - - Assert.IsNull(client.CurrentUser); - } - - [TestMethod("Client: Throws Exception on Invalid Username and Password")] - public async Task ClientSignsInUserWrongPassword() { - var user = $"{RandomString(12)}@supabase.io"; - await client.SignUp(user, password); - - await client.SignOut(); - - await Assert.ThrowsExceptionAsync(async () => - { - var result = await client.SignIn(user, password + "$"); - }); - } - - [TestMethod("Client: Sends Invite Email")] - public async Task ClientSendsInviteEmail() { - var user = $"{RandomString(12)}@supabase.io"; - var service_role_key = GenerateServiceRoleToken(); - var result = await client.InviteUserByEmail(user, service_role_key); - Assert.IsTrue(result); - } - - [TestMethod("Client: Lists users")] - public async Task ClientListUsers() { - var service_role_key = GenerateServiceRoleToken(); - var result = await client.ListUsers(service_role_key); - - Assert.IsTrue(result.Users.Count > 0); - } - - [TestMethod("Client: Lists users pagination")] - public async Task ClientListUsersPagination() { - var service_role_key = GenerateServiceRoleToken(); - - var page1 = await client.ListUsers(service_role_key, page: 1, perPage: 1); - var page2 = await client.ListUsers(service_role_key, page: 2, perPage: 1); - - Assert.AreEqual(page1.Users.Count, 1); - Assert.AreEqual(page2.Users.Count, 1); - Assert.AreNotEqual(page1.Users[0].Id, page2.Users[0].Id); - } - - [TestMethod("Client: Lists users sort")] - public async Task ClientListUsersSort() { - var service_role_key = GenerateServiceRoleToken(); - - var result1 = - await client.ListUsers(service_role_key, sortBy: "created_at", sortOrder: SortOrder.Ascending); - var result2 = - await client.ListUsers(service_role_key, sortBy: "created_at", sortOrder: SortOrder.Descending); - - Assert.AreNotEqual(result1.Users[0].Id, result2.Users[0].Id); - } - - [TestMethod("Client: Lists users filter")] - public async Task ClientListUsersFilter() { - var service_role_key = GenerateServiceRoleToken(); - - var user = $"{RandomString(12)}@supabase.io"; - var result = await client.SignUp(user, password); - - var result1 = await client.ListUsers(service_role_key, filter: "@nonexistingrandomemailprovider.com"); - var result2 = await client.ListUsers(service_role_key, filter: "@supabase.io"); - - Assert.AreNotEqual(result2.Users.Count, 0); - Assert.AreEqual(result1.Users.Count, 0); - Assert.AreNotEqual(result1.Users.Count, result2.Users.Count); - } - - [TestMethod("Client: Get User by Id")] - public async Task ClientGetUserById() { - var service_role_key = GenerateServiceRoleToken(); - var result = await client.ListUsers(service_role_key, page: 1, perPage: 1); - - var userResult = result.Users[0]; - var userByIdResult = await client.GetUserById(service_role_key, userResult.Id); - - Assert.AreEqual(userResult.Id, userByIdResult.Id); - Assert.AreEqual(userResult.Email, userByIdResult.Email); - } - - [TestMethod("Client: Create a user")] - public async Task ClientCreateUser() { - var service_role_key = GenerateServiceRoleToken(); - var result = await client.CreateUser(service_role_key, $"{RandomString(12)}@supabase.io", password); - - Assert.IsNotNull(result); - - - var attributes = new AdminUserAttributes { - UserMetadata = new Dictionary { { "firstName", "123" } }, - AppMetadata = new Dictionary { { "roles", new List { "editor", "publisher" } } } - }; - - var result2 = await client.CreateUser(service_role_key, - $"{RandomString(12)}@supabase.io", - password, - attributes); - Assert.AreEqual("123", result2.UserMetadata["firstName"]); - - var result3 = await client.CreateUser(service_role_key, - new AdminUserAttributes { Email = $"{RandomString(12)}@supabase.io", Password = password }); - Assert.IsNotNull(result3); - } - - - [TestMethod("Client: Update User by Id")] - public async Task ClientUpdateUserById() { - var service_role_key = GenerateServiceRoleToken(); - var createdUser = await client.CreateUser(service_role_key, $"{RandomString(12)}@supabase.io", password); - - Assert.IsNotNull(createdUser); - - var updatedUser = await client.UpdateUserById(service_role_key, - createdUser.Id, - new AdminUserAttributes { Email = $"{RandomString(12)}@supabase.io" }); - - Assert.IsNotNull(updatedUser); - - Assert.AreEqual(createdUser.Id, updatedUser.Id); - Assert.AreNotEqual(createdUser.Email, updatedUser.Email); - } - - [TestMethod("Client: Deletes User")] - public async Task ClientDeletesUser() { - var user = $"{RandomString(12)}@supabase.io"; - await client.SignUp(user, password); - var uid = client.CurrentUser.Id; - - var service_role_key = GenerateServiceRoleToken(); - var result = await client.DeleteUser(uid, service_role_key); - - Assert.IsTrue(result); - } - - [TestMethod("Client: Sends Reset Password Email")] - public async Task ClientSendsResetPasswordForEmail() { - var email = $"{RandomString(12)}@supabase.io"; - await client.SignUp(email, password); - var result = await client.ResetPasswordForEmail(email); - Assert.IsTrue(result); - } - - [TestMethod("Nonce generation and verification")] - public async Task NonceGeneration() { - string nonce = Helpers.GenerateNonce(); - Assert.IsNotNull(nonce); - Assert.AreEqual(128, nonce.Length); - - string pkceVerifier = Helpers.GeneratePKCENonceVerifier(nonce); - Assert.IsNotNull(pkceVerifier); - Assert.AreEqual(43, pkceVerifier.Length); - - string appleVerifier = Helpers.GenerateSHA256NonceFromRawNonce(nonce); - Assert.IsNotNull(appleVerifier); - Assert.AreEqual(64, appleVerifier.Length); - - const string helloNonce = "hello_world_nonce"; - - string helloPkceVerifier = Helpers.GeneratePKCENonceVerifier(helloNonce); - Assert.IsNotNull(helloPkceVerifier); - Assert.AreEqual("9TMmi4JOlYOQEP2Ha39WXj9pySILGnAfQsz-yXws0yE", helloPkceVerifier); - - string helloAppleVerifier = Helpers.GenerateSHA256NonceFromRawNonce(helloNonce); - Assert.IsNotNull(helloAppleVerifier); - Assert.AreEqual("f533268b824e95839010fd876b7f565e3f69c9220b1a701f42ccfec97c2cd321", helloAppleVerifier); - } - } + string helloAppleVerifier = Helpers.GenerateSHA256NonceFromRawNonce(helloNonce); + Assert.IsNotNull(helloAppleVerifier); + Assert.AreEqual("f533268b824e95839010fd876b7f565e3f69c9220b1a701f42ccfec97c2cd321", helloAppleVerifier); + } + } } diff --git a/GotrueTests/StatelessClientTests.cs b/GotrueTests/StatelessClientTests.cs index d036bc6..4e27366 100644 --- a/GotrueTests/StatelessClientTests.cs +++ b/GotrueTests/StatelessClientTests.cs @@ -13,354 +13,351 @@ namespace GotrueTests { - [TestClass] - public class StatelessClientTests - { - private string password = "I@M@SuperP@ssWord"; - - private static Random random = new Random(); - - private static string RandomString(int length) - { - const string chars = "abcdefghijklmnopqrstuvwxyz0123456789"; - return new string(Enumerable.Repeat(chars, length) - .Select(s => s[random.Next(s.Length)]).ToArray()); - } + [TestClass] + public class StatelessClientTests + { + private string password = "I@M@SuperP@ssWord"; + + private static Random random = new Random(); - private static string GetRandomPhoneNumber() - { - const string chars = "123456789"; - var inner = new string(Enumerable.Repeat(chars, 10) - .Select(s => s[random.Next(s.Length)]).ToArray()); + private static string RandomString(int length) + { + const string chars = "abcdefghijklmnopqrstuvwxyz0123456789"; + return new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray()); + } - return $"+1{inner}"; - } + private static string GetRandomPhoneNumber() + { + const string chars = "123456789"; + var inner = new string(Enumerable.Repeat(chars, 10).Select(s => s[random.Next(s.Length)]).ToArray()); - private string GenerateServiceRoleToken() - { - var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("37c304f8-51aa-419a-a1af-06154e63707a")); // using GOTRUE_JWT_SECRET + return $"+1{inner}"; + } - var tokenDescriptor = new SecurityTokenDescriptor - { - IssuedAt = DateTime.Now, - Expires = DateTime.UtcNow.AddDays(7), - SigningCredentials = - new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256Signature), - Claims = new Dictionary() - { - { - "role", "service_role" - } - } - }; + private string GenerateServiceRoleToken() + { + var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("37c304f8-51aa-419a-a1af-06154e63707a")); // using GOTRUE_JWT_SECRET - var tokenHandler = new JwtSecurityTokenHandler(); - var securityToken = tokenHandler.CreateToken(tokenDescriptor); - return tokenHandler.WriteToken(securityToken); - } + var tokenDescriptor = new SecurityTokenDescriptor + { + IssuedAt = DateTime.Now, + Expires = DateTime.UtcNow.AddDays(7), + SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256Signature), + Claims = new Dictionary() + { + { + "role", "service_role" + } + } + }; - private StatelessClient client; + var tokenHandler = new JwtSecurityTokenHandler(); + var securityToken = tokenHandler.CreateToken(tokenDescriptor); + return tokenHandler.WriteToken(securityToken); + } + private StatelessClient client; - [TestInitialize] - public void TestInitializer() - { - client = new StatelessClient(); - } - StatelessClientOptions options => new StatelessClientOptions() { AllowUnconfirmedUserSessions = true }; + [TestInitialize] + public void TestInitializer() + { + client = new StatelessClient(); + } + StatelessClientOptions options => new StatelessClientOptions() { AllowUnconfirmedUserSessions = true }; - [TestMethod("StatelessClient: Signs Up User")] - public async Task SignsUpUser() - { - Session session = null; - var email = $"{RandomString(12)}@supabase.io"; - session = await client.SignUp(email, password, options); - Assert.IsNotNull(session.AccessToken); - Assert.IsNotNull(session.RefreshToken); - Assert.IsInstanceOfType(session.User, typeof(User)); + [TestMethod("StatelessClient: Signs Up User")] + public async Task SignsUpUser() + { + Session session = null; + var email = $"{RandomString(12)}@supabase.io"; + session = await client.SignUp(email, password, options); + Assert.IsNotNull(session.AccessToken); + Assert.IsNotNull(session.RefreshToken); + Assert.IsInstanceOfType(session.User, typeof(User)); - var phone1 = GetRandomPhoneNumber(); - session = await client.SignUp(SignUpType.Phone, phone1, password, options, new SignUpOptions { Data = new Dictionary { { "firstName", "Testing" } } }); - Assert.IsNotNull(session.AccessToken); - Assert.AreEqual("Testing", session.User.UserMetadata["firstName"]); - } + var phone1 = GetRandomPhoneNumber(); + session = await client.SignUp(SignUpType.Phone, phone1, password, options, new SignUpOptions { Data = new Dictionary { { "firstName", "Testing" } } }); - [TestMethod("StatelessClient: Signs Up the same user twice should throw BadRequest")] - public async Task SignsUpUserTwiceShouldThrowBadRequest() - { - var email = $"{RandomString(12)}@supabase.io"; - var result1 = await client.SignUp(email, password, options); + Assert.IsNotNull(session.AccessToken); + Assert.AreEqual("Testing", session.User.UserMetadata["firstName"]); + } - await Assert.ThrowsExceptionAsync(async () => - { - await client.SignUp(email, password, options); - }); - } + [TestMethod("StatelessClient: Signs Up the same user twice should throw BadRequest")] + public async Task SignsUpUserTwiceShouldThrowBadRequest() + { + var email = $"{RandomString(12)}@supabase.io"; + var result1 = await client.SignUp(email, password, options); - [TestMethod("StatelessClient: Signs In User (Email, Phone, Refresh token)")] - public async Task SignsIn() - { - Session session = null; - string refreshToken = ""; + await Assert.ThrowsExceptionAsync(async () => + { + await client.SignUp(email, password, options); + }); + } - // Emails - var email = $"{RandomString(12)}@supabase.io"; - await client.SignUp(email, password, options); - - - session = await client.SignIn(email, password, options); - - Assert.IsNotNull(session.AccessToken); - Assert.IsNotNull(session.RefreshToken); - Assert.IsInstanceOfType(session.User, typeof(User)); - - // Phones - var phone = GetRandomPhoneNumber(); - await client.SignUp(SignUpType.Phone, phone, password, options); - - - session = await client.SignIn(SignInType.Phone, phone, password, options); - - Assert.IsNotNull(session.AccessToken); - Assert.IsNotNull(session.RefreshToken); - Assert.IsInstanceOfType(session.User, typeof(User)); - - // Refresh Token - refreshToken = session.RefreshToken; - - var newSession = await client.SignIn(SignInType.RefreshToken, refreshToken, options: options); - - Assert.IsNotNull(newSession.AccessToken); - Assert.IsNotNull(newSession.RefreshToken); - Assert.IsInstanceOfType(newSession.User, typeof(User)); - } - - [TestMethod("StatelessClient: Signs Out User (Email)")] - public async Task SignsOut() - { - Session session = null; - - // Emails - var email = $"{RandomString(12)}@supabase.io"; - await client.SignUp(email, password, options); - - - session = await client.SignIn(email, password, options); - - Assert.IsNotNull(session.AccessToken); - Assert.IsInstanceOfType(session.User, typeof(User)); - - var result = await client.SignOut(session.AccessToken, options); - - Assert.IsTrue(result); - } - - [TestMethod("StatelessClient: Sends Magic Login Email")] - public async Task SendsMagicLoginEmail() - { - var user1 = $"{RandomString(12)}@supabase.io"; - await client.SignUp(user1, password, options); - - var result1 = await client.SignIn(user1, options); - Assert.IsTrue(result1); - - var user2 = $"{RandomString(12)}@supabase.io"; - var result2 = await client.SignIn(user2, options, new SignInOptions { RedirectTo = $"com.{RandomString(12)}.deeplink://login" }); - Assert.IsTrue(result2); - } - - [TestMethod("StatelessClient: Sends Magic Login Email (Alias)")] - public async Task SendsMagicLoginEmailAlias() - { - var user1 = $"{RandomString(12)}@supabase.io"; - await client.SignUp(user1, password, options); - - var result1 = await client.SignIn(user1, options); - Assert.IsTrue(result1); - - var user2 = $"{RandomString(12)}@supabase.io"; - var result2 = await client.SignIn(user2, options, new SignInOptions { RedirectTo = $"com.{RandomString(12)}.deeplink://login" }); - Assert.IsTrue(result2); - } - - [TestMethod("StatelessClient: Returns Auth Url for Provider")] - public void ReturnsAuthUrlForProvider() - { - var result1 = client.SignIn(Provider.Google, options); - Assert.AreEqual("http://localhost:9999/authorize?provider=google", result1.Uri.ToString()); - - var result2 = client.SignIn(Provider.Google, options, new SignInOptions { Scopes = "special scopes please" }); - Assert.AreEqual("http://localhost:9999/authorize?provider=google&scopes=special+scopes+please", result2.Uri.ToString()); - } - - [TestMethod("StatelessClient: Update user")] - public async Task UpdateUser() - { - var user = $"{RandomString(12)}@supabase.io"; - var session = await client.SignUp(user, password, options); - - var attributes = new UserAttributes - { - Data = new Dictionary - { - {"hello", "world" } - } - }; - var updateResult = await client.Update(session.AccessToken, attributes, options); - Assert.AreEqual(user, updateResult.Email); - Assert.IsNotNull(updateResult.UserMetadata); - } - - [TestMethod("StatelessClient: Returns current user")] - public async Task GetUser() - { - var user = $"{RandomString(12)}@supabase.io"; - var session = await client.SignUp(user, password, options); - - Assert.AreEqual(user, session.User.Email); - } - - [TestMethod("StatelessClient: Throws Exception on Invalid Username and Password")] - public async Task SignsInUserWrongPassword() - { - var user = $"{RandomString(12)}@supabase.io"; - await client.SignUp(user, password, options); - - await Assert.ThrowsExceptionAsync(async () => - { - var result = await client.SignIn(user, password + "$", options); - }); - } - - [TestMethod("StatelessClient: Sends Invite Email")] - public async Task SendsInviteEmail() - { - var user = $"{RandomString(12)}@supabase.io"; - var service_role_key = GenerateServiceRoleToken(); - var result = await client.InviteUserByEmail(user, service_role_key, options); - Assert.IsTrue(result); - } - - [TestMethod("StatelessClient: Deletes User")] - public async Task DeletesUser() - { - var user = $"{RandomString(12)}@supabase.io"; - var session = await client.SignUp(user, password, options); - var uid = session.User.Id; - - var service_role_key = GenerateServiceRoleToken(); - var result = await client.DeleteUser(uid, service_role_key, options); - - Assert.IsTrue(result); - } - - [TestMethod("StatelessClient: Sends Reset Password Email")] - public async Task ClientSendsResetPasswordForEmail() - { - var email = $"{RandomString(12)}@supabase.io"; - await client.SignUp(email, password, options); - var result = await client.ResetPasswordForEmail(email, options); - Assert.IsTrue(result); - } - - [TestMethod("Client: Lists users")] - public async Task ClientListUsers() - { - var service_role_key = GenerateServiceRoleToken(); - var result = await client.ListUsers(service_role_key, options); - - Assert.IsTrue(result.Users.Count > 0); - } - - [TestMethod("Client: Lists users pagination")] - public async Task ClientListUsersPagination() - { - var service_role_key = GenerateServiceRoleToken(); - - var page1 = await client.ListUsers(service_role_key, options, page: 1, perPage: 1); - var page2 = await client.ListUsers(service_role_key, options, page: 2, perPage: 1); - - Assert.AreEqual(page1.Users.Count, 1); - Assert.AreEqual(page2.Users.Count, 1); - Assert.AreNotEqual(page1.Users[0].Id, page2.Users[0].Id); - } - - [TestMethod("Client: Lists users sort")] - public async Task ClientListUsersSort() - { - var service_role_key = GenerateServiceRoleToken(); - - var result1 = await client.ListUsers(service_role_key, options, sortBy: "created_at", sortOrder: SortOrder.Descending); - var result2 = await client.ListUsers(service_role_key, options, sortBy: "created_at", sortOrder: SortOrder.Ascending); - - Assert.AreNotEqual(result1.Users[0].Id, result2.Users[0].Id); - } - - [TestMethod("Client: Lists users filter")] - public async Task ClientListUsersFilter() - { - var service_role_key = GenerateServiceRoleToken(); - - var result1 = await client.ListUsers(service_role_key, options, filter: "@nonexistingrandomemailprovider.com"); - var result2 = await client.ListUsers(service_role_key, options, filter: "@supabase.io"); - - Assert.AreNotEqual(result2.Users.Count, 0); - Assert.AreEqual(result1.Users.Count, 0); - Assert.AreNotEqual(result1.Users.Count, result2.Users.Count); - } - - [TestMethod("Client: Get User by Id")] - public async Task ClientGetUserById() - { - var service_role_key = GenerateServiceRoleToken(); - var result = await client.ListUsers(service_role_key, options, page: 1, perPage: 1); - - var userResult = result.Users[0]; - var userByIdResult = await client.GetUserById(service_role_key, options, userResult.Id); - - Assert.AreEqual(userResult.Id, userByIdResult.Id); - Assert.AreEqual(userResult.Email, userByIdResult.Email); - } - - [TestMethod("Client: Create a user")] - public async Task ClientCreateUser() - { - var service_role_key = GenerateServiceRoleToken(); - var result = await client.CreateUser(service_role_key, options, $"{RandomString(12)}@supabase.io", password); - - Assert.IsNotNull(result); - - var attributes = new AdminUserAttributes - { - UserMetadata = new Dictionary { { "firstName", "123" } }, - }; - - var result2 = await client.CreateUser(service_role_key, options, $"{RandomString(12)}@supabase.io", password, attributes); - Assert.AreEqual("123", result2.UserMetadata["firstName"]); - - var result3 = await client.CreateUser(service_role_key, options, new AdminUserAttributes { Email = $"{RandomString(12)}@supabase.io", Password = password }); - Assert.IsNotNull(result3); - } - - [TestMethod("Client: Update User by Id")] - public async Task ClientUpdateUserById() - { - var service_role_key = GenerateServiceRoleToken(); - var createdUser = await client.CreateUser(service_role_key, options, $"{RandomString(12)}@supabase.io", password); + [TestMethod("StatelessClient: Signs In User (Email, Phone, Refresh token)")] + public async Task SignsIn() + { + Session session = null; + string refreshToken = ""; - Assert.IsNotNull(createdUser); - - var updatedUser = await client.UpdateUserById(service_role_key, options, createdUser.Id, new AdminUserAttributes { Email = $"{RandomString(12)}@supabase.io" }); - - Assert.IsNotNull(updatedUser); - - Assert.AreEqual(createdUser.Id, updatedUser.Id); - Assert.AreNotEqual(createdUser.Email, updatedUser.Email); - } - } + // Emails + var email = $"{RandomString(12)}@supabase.io"; + await client.SignUp(email, password, options); + + + session = await client.SignIn(email, password, options); + + Assert.IsNotNull(session.AccessToken); + Assert.IsNotNull(session.RefreshToken); + Assert.IsInstanceOfType(session.User, typeof(User)); + + // Phones + var phone = GetRandomPhoneNumber(); + await client.SignUp(SignUpType.Phone, phone, password, options); + + + session = await client.SignIn(SignInType.Phone, phone, password, options); + + Assert.IsNotNull(session.AccessToken); + Assert.IsNotNull(session.RefreshToken); + Assert.IsInstanceOfType(session.User, typeof(User)); + + // Refresh Token + refreshToken = session.RefreshToken; + + var newSession = await client.SignIn(SignInType.RefreshToken, refreshToken, options: options); + + Assert.IsNotNull(newSession.AccessToken); + Assert.IsNotNull(newSession.RefreshToken); + Assert.IsInstanceOfType(newSession.User, typeof(User)); + } + + [TestMethod("StatelessClient: Signs Out User (Email)")] + public async Task SignsOut() + { + Session session = null; + + // Emails + var email = $"{RandomString(12)}@supabase.io"; + await client.SignUp(email, password, options); + + + session = await client.SignIn(email, password, options); + + Assert.IsNotNull(session.AccessToken); + Assert.IsInstanceOfType(session.User, typeof(User)); + + var result = await client.SignOut(session.AccessToken, options); + + Assert.IsTrue(result); + } + + [TestMethod("StatelessClient: Sends Magic Login Email")] + public async Task SendsMagicLoginEmail() + { + var user1 = $"{RandomString(12)}@supabase.io"; + await client.SignUp(user1, password, options); + + var result1 = await client.SignIn(user1, options); + Assert.IsTrue(result1); + + var user2 = $"{RandomString(12)}@supabase.io"; + var result2 = await client.SignIn(user2, options, new SignInOptions { RedirectTo = $"com.{RandomString(12)}.deeplink://login" }); + Assert.IsTrue(result2); + } + + [TestMethod("StatelessClient: Sends Magic Login Email (Alias)")] + public async Task SendsMagicLoginEmailAlias() + { + var user1 = $"{RandomString(12)}@supabase.io"; + await client.SignUp(user1, password, options); + + var result1 = await client.SignIn(user1, options); + Assert.IsTrue(result1); + + var user2 = $"{RandomString(12)}@supabase.io"; + var result2 = await client.SignIn(user2, options, new SignInOptions { RedirectTo = $"com.{RandomString(12)}.deeplink://login" }); + Assert.IsTrue(result2); + } + + [TestMethod("StatelessClient: Returns Auth Url for Provider")] + public void ReturnsAuthUrlForProvider() + { + var result1 = client.SignIn(Provider.Google, options); + Assert.AreEqual("http://localhost:9999/authorize?provider=google", result1.Uri.ToString()); + + var result2 = client.SignIn(Provider.Google, options, new SignInOptions { Scopes = "special scopes please" }); + Assert.AreEqual("http://localhost:9999/authorize?provider=google&scopes=special+scopes+please", result2.Uri.ToString()); + } + + [TestMethod("StatelessClient: Update user")] + public async Task UpdateUser() + { + var user = $"{RandomString(12)}@supabase.io"; + var session = await client.SignUp(user, password, options); + + var attributes = new UserAttributes + { + Data = new Dictionary + { + { "hello", "world" } + } + }; + var updateResult = await client.Update(session.AccessToken, attributes, options); + Assert.AreEqual(user, updateResult.Email); + Assert.IsNotNull(updateResult.UserMetadata); + } + + [TestMethod("StatelessClient: Returns current user")] + public async Task GetUser() + { + var user = $"{RandomString(12)}@supabase.io"; + var session = await client.SignUp(user, password, options); + + Assert.AreEqual(user, session.User.Email); + } + + [TestMethod("StatelessClient: Throws Exception on Invalid Username and Password")] + public async Task SignsInUserWrongPassword() + { + var user = $"{RandomString(12)}@supabase.io"; + await client.SignUp(user, password, options); + + await Assert.ThrowsExceptionAsync(async () => + { + var result = await client.SignIn(user, password + "$", options); + }); + } + + [TestMethod("StatelessClient: Sends Invite Email")] + public async Task SendsInviteEmail() + { + var user = $"{RandomString(12)}@supabase.io"; + var service_role_key = GenerateServiceRoleToken(); + var result = await client.InviteUserByEmail(user, service_role_key, options); + Assert.IsTrue(result); + } + + [TestMethod("StatelessClient: Deletes User")] + public async Task DeletesUser() + { + var user = $"{RandomString(12)}@supabase.io"; + var session = await client.SignUp(user, password, options); + var uid = session.User.Id; + + var service_role_key = GenerateServiceRoleToken(); + var result = await client.DeleteUser(uid, service_role_key, options); + + Assert.IsTrue(result); + } + + [TestMethod("StatelessClient: Sends Reset Password Email")] + public async Task ClientSendsResetPasswordForEmail() + { + var email = $"{RandomString(12)}@supabase.io"; + await client.SignUp(email, password, options); + var result = await client.ResetPasswordForEmail(email, options); + Assert.IsTrue(result); + } + + [TestMethod("Client: Lists users")] + public async Task ClientListUsers() + { + var service_role_key = GenerateServiceRoleToken(); + var result = await client.ListUsers(service_role_key, options); + + Assert.IsTrue(result.Users.Count > 0); + } + + [TestMethod("Client: Lists users pagination")] + public async Task ClientListUsersPagination() + { + var service_role_key = GenerateServiceRoleToken(); + + var page1 = await client.ListUsers(service_role_key, options, page: 1, perPage: 1); + var page2 = await client.ListUsers(service_role_key, options, page: 2, perPage: 1); + + Assert.AreEqual(page1.Users.Count, 1); + Assert.AreEqual(page2.Users.Count, 1); + Assert.AreNotEqual(page1.Users[0].Id, page2.Users[0].Id); + } + + [TestMethod("Client: Lists users sort")] + public async Task ClientListUsersSort() + { + var service_role_key = GenerateServiceRoleToken(); + + var result1 = await client.ListUsers(service_role_key, options, sortBy: "created_at", sortOrder: SortOrder.Descending); + var result2 = await client.ListUsers(service_role_key, options, sortBy: "created_at", sortOrder: SortOrder.Ascending); + + Assert.AreNotEqual(result1.Users[0].Id, result2.Users[0].Id); + } + + [TestMethod("Client: Lists users filter")] + public async Task ClientListUsersFilter() + { + var service_role_key = GenerateServiceRoleToken(); + + var result1 = await client.ListUsers(service_role_key, options, filter: "@nonexistingrandomemailprovider.com"); + var result2 = await client.ListUsers(service_role_key, options, filter: "@supabase.io"); + + Assert.AreNotEqual(result2.Users.Count, 0); + Assert.AreEqual(result1.Users.Count, 0); + Assert.AreNotEqual(result1.Users.Count, result2.Users.Count); + } + + [TestMethod("Client: Get User by Id")] + public async Task ClientGetUserById() + { + var service_role_key = GenerateServiceRoleToken(); + var result = await client.ListUsers(service_role_key, options, page: 1, perPage: 1); + + var userResult = result.Users[0]; + var userByIdResult = await client.GetUserById(service_role_key, options, userResult.Id); + + Assert.AreEqual(userResult.Id, userByIdResult.Id); + Assert.AreEqual(userResult.Email, userByIdResult.Email); + } + + [TestMethod("Client: Create a user")] + public async Task ClientCreateUser() + { + var service_role_key = GenerateServiceRoleToken(); + var result = await client.CreateUser(service_role_key, options, $"{RandomString(12)}@supabase.io", password); + + Assert.IsNotNull(result); + + var attributes = new AdminUserAttributes + { + UserMetadata = new Dictionary { { "firstName", "123" } }, + }; + + var result2 = await client.CreateUser(service_role_key, options, $"{RandomString(12)}@supabase.io", password, attributes); + Assert.AreEqual("123", result2.UserMetadata["firstName"]); + + var result3 = await client.CreateUser(service_role_key, options, new AdminUserAttributes { Email = $"{RandomString(12)}@supabase.io", Password = password }); + Assert.IsNotNull(result3); + } + + [TestMethod("Client: Update User by Id")] + public async Task ClientUpdateUserById() + { + var service_role_key = GenerateServiceRoleToken(); + var createdUser = await client.CreateUser(service_role_key, options, $"{RandomString(12)}@supabase.io", password); + + Assert.IsNotNull(createdUser); + + var updatedUser = await client.UpdateUserById(service_role_key, options, createdUser.Id, new AdminUserAttributes { Email = $"{RandomString(12)}@supabase.io" }); + + Assert.IsNotNull(updatedUser); + + Assert.AreEqual(createdUser.Id, updatedUser.Id); + Assert.AreNotEqual(createdUser.Email, updatedUser.Email); + } + } } From 38563b8ccc63bd2296de5ba9651b025125f8984f Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 17:38:45 -0700 Subject: [PATCH 27/74] Simplify exceptions, persistence, state changes --- Gotrue/Api.cs | 7 +- Gotrue/Client.cs | 480 ++++----- Gotrue/ClientOptions.cs | 6 +- Gotrue/Constants.cs | 1 + Gotrue/ExceptionHandler.cs | 26 - Gotrue/Exceptions/BadRequestException.cs | 16 - Gotrue/Exceptions/ExistingUserException.cs | 15 - Gotrue/Exceptions/ForbiddenException.cs | 15 - Gotrue/Exceptions/GotrueException.cs | 8 + .../InvalidEmailOrPasswordException.cs | 15 - Gotrue/Exceptions/InvalidProviderException.cs | 8 - Gotrue/Exceptions/RequestException.cs | 18 - Gotrue/Exceptions/UnauthorizedException.cs | 16 - Gotrue/Helpers.cs | 46 +- Gotrue/Interfaces/IGotrueClient.cs | 9 +- Gotrue/PersistenceListener.cs | 64 ++ Gotrue/StatelessClient.cs | 959 ++++++++---------- GotrueTests/AnonKeyClientTests.cs | 308 ++++++ GotrueTests/ClientTests.cs | 428 -------- GotrueTests/ServiceRoleTests.cs | 166 +++ GotrueTests/StatelessClientTests.cs | 141 ++- GotrueTests/TestUtils.cs | 57 ++ gotrue-csharp.sln.DotSettings | 2 + 23 files changed, 1310 insertions(+), 1501 deletions(-) delete mode 100644 Gotrue/ExceptionHandler.cs delete mode 100644 Gotrue/Exceptions/BadRequestException.cs delete mode 100644 Gotrue/Exceptions/ExistingUserException.cs delete mode 100644 Gotrue/Exceptions/ForbiddenException.cs delete mode 100644 Gotrue/Exceptions/InvalidEmailOrPasswordException.cs delete mode 100644 Gotrue/Exceptions/InvalidProviderException.cs delete mode 100644 Gotrue/Exceptions/RequestException.cs delete mode 100644 Gotrue/Exceptions/UnauthorizedException.cs create mode 100644 Gotrue/PersistenceListener.cs create mode 100644 GotrueTests/AnonKeyClientTests.cs delete mode 100644 GotrueTests/ClientTests.cs create mode 100644 GotrueTests/ServiceRoleTests.cs create mode 100644 GotrueTests/TestUtils.cs diff --git a/Gotrue/Api.cs b/Gotrue/Api.cs index aa05b22..255357b 100644 --- a/Gotrue/Api.cs +++ b/Gotrue/Api.cs @@ -94,10 +94,7 @@ public Api(string url, Dictionary? headers = null) return session; } - else - { - return null; - } + return null; } /// @@ -209,7 +206,7 @@ public async Task SignInWithOtp(SignInWithPasswordlessP public Task SignInWithIdToken(Provider provider, string idToken, string? nonce = null, string? captchaToken = null) { if (provider != Provider.Google && provider != Provider.Apple) - throw new InvalidProviderException($"Provider must either be: `Provider.Google` or `Provider.Apple`."); + throw new GotrueException($"Provider must be `Provider.Google` or `Provider.Apple` not {provider}"); var body = new Dictionary { diff --git a/Gotrue/Client.cs b/Gotrue/Client.cs index 4646915..53c9918 100644 --- a/Gotrue/Client.cs +++ b/Gotrue/Client.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using System.Web; -using Supabase.Gotrue.Exceptions; using Supabase.Gotrue.Interfaces; using static Supabase.Gotrue.Constants; @@ -17,11 +17,14 @@ namespace Supabase.Gotrue /// var client = new Supabase.Gotrue.Client(options); /// var user = await client.SignIn("user@email.com", "fancyPassword"); /// + [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract")] public class Client : IGotrueClient { private DebugNotification? _debugNotification; + private readonly List.AuthEventHandler> _authEventHandlers = new List.AuthEventHandler>(); + public void AddDebugListener(Action listener) { _debugNotification ??= new DebugNotification(); @@ -45,65 +48,69 @@ public Func>? GetHeaders _api.GetHeaders = value; } } + private Func>? _getHeaders; - /// - /// Event Handler that raises an event when a user signs in, signs out, recovers password, or updates their record. - /// - public event EventHandler? StateChanged; + public void NotifyStateChange(AuthState stateChanged) + { + foreach (var handler in _authEventHandlers) + { + handler.Invoke(this, stateChanged); + } + } /// /// The current User /// public User? CurrentUser { get; private set; } - /// - /// The current Session - /// - public Session? CurrentSession { get; private set; } - - /// - /// Should Client Refresh Token Automatically? (via ) - /// - protected bool AutoRefreshToken { get; private set; } + public void AddStateChangedListener(IGotrueClient.AuthEventHandler authEventHandler) + { + if (_authEventHandlers.Contains(authEventHandler)) + return; - /// - /// Should Client Persist Session? (via ) - /// - protected bool ShouldPersistSession { get; private set; } + _authEventHandlers.Add(authEventHandler); - /// - /// User defined function (via ) to persist the session. - /// - // ReSharper disable once IdentifierTypo - protected Func> SessionPersistor { get; private set; } + } + public void RemoveStateChangedListener(IGotrueClient.AuthEventHandler authEventHandler) + { + if (!_authEventHandlers.Contains(authEventHandler)) + return; - /// - /// User defined function (via ) to retrieve the session. - /// - protected Func> SessionRetriever { get; private set; } + _authEventHandlers.Remove(authEventHandler); + } + public void ClearStateChangedListeners() + { + _authEventHandlers.Clear(); + } /// - /// User defined function (via ) to destroy the session. + /// The current Session /// - protected Func> SessionDestroyer { get; private set; } + public Session? CurrentSession { get; private set; } /// /// The initialized client options. /// - internal ClientOptions Options { get; private set; } + public ClientOptions Options { get; } /// - /// Internal timer reference for Refreshing Tokens () + /// Internal timer reference for Refreshing Tokens ( + /// AutoRefreshToken + /// + /// ) /// private Timer? _refreshTimer; - private IGotrueApi _api; + private readonly IGotrueApi _api; /// /// Initializes the Client. /// - /// Although options are ... optional, you will likely want to at least specify a . + /// Although options are ... optional, you will likely want to at least specify a + /// ClientOptions.Url + /// + /// . /// /// Sessions are no longer automatically retrieved on construction, if you want to set the session, /// @@ -111,15 +118,15 @@ public Func>? GetHeaders /// public Client(ClientOptions? options = null) { - if (options == null) - options = new ClientOptions(); + options ??= new ClientOptions(); Options = options; - AutoRefreshToken = options.AutoRefreshToken; - ShouldPersistSession = options.PersistSession; - SessionPersistor = options.SessionPersistor; - SessionRetriever = options.SessionRetriever; - SessionDestroyer = options.SessionDestroyer; + + if (options.PersistSession) + { + var persistenceListener = new PersistenceListener(options.SessionPersistor, options.SessionDestroyer, options.SessionRetriever); + _authEventHandlers.Add(persistenceListener.EventHandler); + } _api = new Api(options.Url, options.Headers); } @@ -165,36 +172,25 @@ public Client(ClientOptions? options = null) /// public async Task SignUp(SignUpType type, string identifier, string password, SignUpOptions? options = null) { - await DestroySession(); + DestroySession(); - try + var session = type switch { - Session? session = null; - switch (type) - { - case SignUpType.Email: - session = await _api.SignUpWithEmail(identifier, password, options); - break; - case SignUpType.Phone: - session = await _api.SignUpWithPhone(identifier, password, options); - break; - } - - if (session?.User?.ConfirmedAt != null || (session?.User != null && Options.AllowUnconfirmedUserSessions)) - { - await PersistSession(session); + SignUpType.Email => await _api.SignUpWithEmail(identifier, password, options), + SignUpType.Phone => await _api.SignUpWithPhone(identifier, password, options), + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; - StateChanged?.Invoke(this, new ClientStateChanged(AuthState.SignedIn)); + if (session?.User?.ConfirmedAt != null || (session?.User != null && Options.AllowUnconfirmedUserSessions)) + { + await PersistSession(session); - return CurrentSession; - } + NotifyStateChange(AuthState.SignedIn); - return session; - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); + return CurrentSession; } + + return session; } @@ -206,17 +202,9 @@ public Client(ClientOptions? options = null) /// public async Task SignIn(string email, SignInOptions? options = null) { - await DestroySession(); - - try - { - await _api.SendMagicLinkEmail(email, options); - return true; - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } + DestroySession(); + await _api.SendMagicLinkEmail(email, options); + return true; } /// @@ -229,23 +217,18 @@ public async Task SignIn(string email, SignInOptions? options = null) /// Provided from External Library /// Provided from External Library /// - /// + /// + /// InvalidProviderException + /// public async Task SignInWithIdToken(Provider provider, string idToken, string? nonce = null, string? captchaToken = null) { - try - { - await DestroySession(); - var result = await _api.SignInWithIdToken(provider, idToken, nonce, captchaToken); + DestroySession(); + var result = await _api.SignInWithIdToken(provider, idToken, nonce, captchaToken); - if (result != null) - await PersistSession(result); + if (result != null) + await PersistSession(result); - return result; - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } + return result; } /// @@ -267,15 +250,8 @@ public async Task SignIn(string email, SignInOptions? options = null) /// public async Task SignInWithOtp(SignInWithPasswordlessEmailOptions options) { - try - { - await DestroySession(); - return await _api.SignInWithOtp(options); - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } + DestroySession(); + return await _api.SignInWithOtp(options); } /// @@ -297,15 +273,8 @@ public async Task SignInWithOtp(SignInWithPasswordlessE /// public async Task SignInWithOtp(SignInWithPasswordlessPhoneOptions options) { - try - { - await DestroySession(); - return await _api.SignInWithOtp(options); - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } + DestroySession(); + return await _api.SignInWithOtp(options); } /// @@ -343,47 +312,40 @@ public async Task SignInWithOtp(SignInWithPasswordlessP /// public async Task SignIn(SignInType type, string identifierOrToken, string? password = null, string? scopes = null) { - await DestroySession(); + DestroySession(); - try + Session? session = null; + switch (type) { - Session? session = null; - switch (type) - { - case SignInType.Email: - session = await _api.SignInWithEmail(identifierOrToken, password!); - break; - case SignInType.Phone: - if (string.IsNullOrEmpty(password)) - { - await _api.SendMobileOTP(identifierOrToken); - return null; - } - - session = await _api.SignInWithPhone(identifierOrToken, password!); - break; - case SignInType.RefreshToken: - CurrentSession = new Session(); - CurrentSession.RefreshToken = identifierOrToken; - - await RefreshToken(); + case SignInType.Email: + session = await _api.SignInWithEmail(identifierOrToken, password!); + break; + case SignInType.Phone: + if (string.IsNullOrEmpty(password)) + { + await _api.SendMobileOTP(identifierOrToken); + return null; + } - return CurrentSession; - } + session = await _api.SignInWithPhone(identifierOrToken, password!); + break; + case SignInType.RefreshToken: + CurrentSession = new Session(); + CurrentSession.RefreshToken = identifierOrToken; - if (session?.User?.ConfirmedAt != null || (session?.User != null && Options.AllowUnconfirmedUserSessions)) - { - await PersistSession(session); - StateChanged?.Invoke(this, new ClientStateChanged(AuthState.SignedIn)); - return CurrentSession; - } + await RefreshToken(); - return null; + return CurrentSession; } - catch (RequestException ex) + + if (session?.User?.ConfirmedAt != null || (session?.User != null && Options.AllowUnconfirmedUserSessions)) { - throw ExceptionHandler.Parse(ex); + await PersistSession(session); + NotifyStateChange(AuthState.SignedIn); + return CurrentSession; } + + return null; } /// @@ -395,12 +357,12 @@ public async Task SignInWithOtp(SignInWithPasswordlessP /// /// /// - public async Task SignIn(Provider provider, SignInOptions? options = null) + public Task SignIn(Provider provider, SignInOptions? options = null) { - await DestroySession(); + DestroySession(); var providerUri = _api.GetUriForProvider(provider, options); - return providerUri; + return Task.FromResult(providerUri); } /// @@ -412,25 +374,18 @@ public async Task SignIn(Provider provider, SignInOptions? op /// public async Task VerifyOTP(string phone, string token, MobileOtpType type = MobileOtpType.SMS) { - try - { - await DestroySession(); + DestroySession(); - var session = await _api.VerifyMobileOTP(phone, token, type); - - if (session?.AccessToken != null) - { - await PersistSession(session); - StateChanged?.Invoke(this, new ClientStateChanged(AuthState.SignedIn)); - return session; - } + var session = await _api.VerifyMobileOTP(phone, token, type); - return null; - } - catch (RequestException ex) + if (session?.AccessToken != null) { - throw ExceptionHandler.Parse(ex); + await PersistSession(session); + NotifyStateChange(AuthState.SignedIn); + return session; } + + return null; } /// @@ -442,25 +397,18 @@ public async Task SignIn(Provider provider, SignInOptions? op /// public async Task VerifyOTP(string email, string token, EmailOtpType type = EmailOtpType.MagicLink) { - try - { - await DestroySession(); + DestroySession(); - var session = await _api.VerifyEmailOTP(email, token, type); - - if (session?.AccessToken != null) - { - await PersistSession(session); - StateChanged?.Invoke(this, new ClientStateChanged(AuthState.SignedIn)); - return session; - } + var session = await _api.VerifyEmailOTP(email, token, type); - return null; - } - catch (RequestException ex) + if (session?.AccessToken != null) { - throw ExceptionHandler.Parse(ex); + await PersistSession(session); + NotifyStateChange(AuthState.SignedIn); + return session; } + + return null; } /// @@ -476,9 +424,9 @@ public async Task SignOut() _refreshTimer?.Dispose(); - await DestroySession(); + DestroySession(); - StateChanged?.Invoke(this, new ClientStateChanged(AuthState.SignedOut)); + NotifyStateChange(AuthState.SignedOut); } } @@ -492,20 +440,11 @@ public async Task SignOut() if (CurrentSession == null || string.IsNullOrEmpty(CurrentSession.AccessToken)) throw new Exception("Not Logged in."); - try - { - var result = await _api.UpdateUser(CurrentSession.AccessToken!, attributes); - - CurrentUser = result; - - StateChanged?.Invoke(this, new ClientStateChanged(AuthState.UserUpdated)); + var result = await _api.UpdateUser(CurrentSession.AccessToken!, attributes); + CurrentUser = result; + NotifyStateChange(AuthState.UserUpdated); - return result; - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } + return result; } /// @@ -516,16 +455,9 @@ public async Task SignOut() /// public async Task InviteUserByEmail(string email, string jwt) { - try - { - var response = await _api.InviteUserByEmail(email, jwt); - response.ResponseMessage?.EnsureSuccessStatusCode(); - return true; - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } + var response = await _api.InviteUserByEmail(email, jwt); + response.ResponseMessage?.EnsureSuccessStatusCode(); + return true; } /// @@ -536,16 +468,9 @@ public async Task InviteUserByEmail(string email, string jwt) /// public async Task DeleteUser(string uid, string jwt) { - try - { - var result = await _api.DeleteUser(uid, jwt); - result.ResponseMessage?.EnsureSuccessStatusCode(); - return true; - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } + var result = await _api.DeleteUser(uid, jwt); + result.ResponseMessage?.EnsureSuccessStatusCode(); + return true; } /// @@ -553,7 +478,7 @@ public async Task DeleteUser(string uid, string jwt) /// /// A valid JWT. Must be a full-access API key (e.g. service_role key). /// A string for example part of the email - /// Snake case string of the given key, currently only created_at is suppported + /// Snake case string of the given key, currently only created_at is supported /// asc or desc, if null desc is used /// page to show for pagination /// items per page for pagination @@ -561,14 +486,7 @@ public async Task DeleteUser(string uid, string jwt) public async Task?> ListUsers(string jwt, string? filter = null, string? sortBy = null, SortOrder sortOrder = SortOrder.Descending, int? page = null, int? perPage = null) { - try - { - return await _api.ListUsers(jwt, filter, sortBy, sortOrder, page, perPage); - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } + return await _api.ListUsers(jwt, filter, sortBy, sortOrder, page, perPage); } /// @@ -579,14 +497,9 @@ public async Task DeleteUser(string uid, string jwt) /// public async Task GetUserById(string jwt, string userId) { - try - { - return await _api.GetUserById(jwt, userId); - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } + + return await _api.GetUserById(jwt, userId); + } /// @@ -596,14 +509,7 @@ public async Task DeleteUser(string uid, string jwt) /// public async Task GetUser(string jwt) { - try - { - return await _api.GetUser(jwt); - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } + return await _api.GetUser(jwt); } /// @@ -616,16 +522,14 @@ public async Task DeleteUser(string uid, string jwt) /// public Task CreateUser(string jwt, string email, string password, AdminUserAttributes? attributes = null) { - if (attributes == null) - { - attributes = new AdminUserAttributes(); - } + attributes ??= new AdminUserAttributes(); attributes.Email = email; attributes.Password = password; return CreateUser(jwt, attributes); } + /// /// Create a user (as a service_role) /// @@ -634,14 +538,7 @@ public async Task DeleteUser(string uid, string jwt) /// public async Task CreateUser(string jwt, AdminUserAttributes attributes) { - try - { - return await _api.CreateUser(jwt, attributes); - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } + return await _api.CreateUser(jwt, attributes); } /// @@ -653,14 +550,7 @@ public async Task DeleteUser(string uid, string jwt) /// public async Task UpdateUserById(string jwt, string userId, AdminUserAttributes userData) { - try - { - return await _api.UpdateUserById(jwt, userId, userData); - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } + return await _api.UpdateUserById(jwt, userId, userData); } /// @@ -671,16 +561,9 @@ public async Task DeleteUser(string uid, string jwt) /// public async Task ResetPasswordForEmail(string email) { - try - { - var result = await _api.ResetPasswordForEmail(email); - result.ResponseMessage?.EnsureSuccessStatusCode(); - return true; - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } + var result = await _api.ResetPasswordForEmail(email); + result.ResponseMessage?.EnsureSuccessStatusCode(); + return true; } /// @@ -707,13 +590,13 @@ public async Task ResetPasswordForEmail(string email) /// Session. public Session SetAuth(string accessToken) { - if (CurrentSession == null) CurrentSession = new Session(); + CurrentSession ??= new Session(); CurrentSession.AccessToken = accessToken; CurrentSession.TokenType = "bearer"; CurrentSession.User = CurrentUser; - StateChanged?.Invoke(this, new ClientStateChanged(AuthState.TokenRefreshed)); + NotifyStateChange(AuthState.TokenRefreshed); return CurrentSession; } @@ -766,28 +649,29 @@ public Session SetAuth(string accessToken) if (storeSession) { await PersistSession(session); - StateChanged?.Invoke(this, new ClientStateChanged(AuthState.SignedIn)); + NotifyStateChange(AuthState.SignedIn); if (query.Get("type") == "recovery") - StateChanged?.Invoke(this, new ClientStateChanged(AuthState.PasswordRecovery)); + NotifyStateChange(AuthState.PasswordRecovery); } return session; } /// - /// Retrieves the Session by calling - sets internal state and timers. + /// Retrieves the Session by calling + /// SessionRetriever + /// + /// - sets internal state and timers. /// /// public async Task RetrieveSessionAsync() { - if (SessionRetriever == null) return null; - - var session = await SessionRetriever.Invoke(); + var session = CurrentSession; if (session != null && session.ExpiresAt() < DateTime.Now) { - if (AutoRefreshToken && session.RefreshToken != null) + if (Options.AutoRefreshToken && session.RefreshToken != null) { try { @@ -796,20 +680,20 @@ public Session SetAuth(string accessToken) } catch { - await DestroySession(); + DestroySession(); return null; } } else { - await DestroySession(); + DestroySession(); return null; } } else if (session == null || session.User == null) { _debugNotification?.Log("Stored Session is missing data."); - await DestroySession(); + DestroySession(); return null; } else @@ -817,7 +701,7 @@ public Session SetAuth(string accessToken) CurrentSession = session; CurrentUser = session.User; - StateChanged?.Invoke(this, new ClientStateChanged(AuthState.SignedIn)); + NotifyStateChange(AuthState.SignedIn); InitRefreshTimer(); @@ -837,7 +721,7 @@ public Session SetAuth(string accessToken) if (result != null) { await PersistSession(result); - StateChanged?.Invoke(this, new ClientStateChanged(AuthState.SignedIn)); + NotifyStateChange(AuthState.SignedIn); return CurrentSession; } @@ -845,33 +729,34 @@ public Session SetAuth(string accessToken) } /// - /// Persists a Session in memory and calls (if specified) + /// Persists a Session in memory and calls (if specified) + /// ClientOptions.SessionPersistor + /// /// /// - private async Task PersistSession(Session session) + private Task PersistSession(Session session) { CurrentSession = session; CurrentUser = session.User; var expiration = session.ExpiresIn; - if (AutoRefreshToken && expiration != default) + if (Options.AutoRefreshToken && expiration != default) InitRefreshTimer(); - - if (ShouldPersistSession && SessionPersistor != null) - await SessionPersistor.Invoke(session); + return Task.CompletedTask; } /// - /// Persists a Session in memory and calls (if specified) + /// Persists a Session in memory and calls (if specified) + /// ClientOptions.SessionDestroyer + /// /// - private async Task DestroySession() + private void DestroySession() { CurrentSession = null; CurrentUser = null; - if (ShouldPersistSession && SessionDestroyer != null) - await SessionDestroyer.Invoke(); + NotifyStateChange(AuthState.SignedOut); } /// @@ -893,13 +778,9 @@ private async Task RefreshToken(string? refreshToken = null) CurrentSession = result; CurrentUser = result.User; - if (ShouldPersistSession && SessionPersistor != null) - await SessionPersistor.Invoke(result); - - StateChanged?.Invoke(this, new ClientStateChanged(AuthState.TokenRefreshed)); - StateChanged?.Invoke(this, new ClientStateChanged(AuthState.SignedIn)); + NotifyStateChange(AuthState.TokenRefreshed); - if (AutoRefreshToken && CurrentSession.ExpiresIn != default) + if (Options.AutoRefreshToken && CurrentSession.ExpiresIn != default) InitRefreshTimer(); } @@ -943,21 +824,8 @@ private async void HandleRefreshTimerTick(object _) catch (Exception ex) { _debugNotification?.Log(ex.Message, ex); - StateChanged?.Invoke(this, new ClientStateChanged(AuthState.SignedOut)); + NotifyStateChange(AuthState.SignedOut); } } } - - /// - /// Class representing a state change on the . - /// - public class ClientStateChanged : EventArgs - { - public AuthState State { get; private set; } - - public ClientStateChanged(AuthState state) - { - State = state; - } - } } diff --git a/Gotrue/ClientOptions.cs b/Gotrue/ClientOptions.cs index fd0c767..8edcb92 100644 --- a/Gotrue/ClientOptions.cs +++ b/Gotrue/ClientOptions.cs @@ -34,17 +34,17 @@ public class ClientOptions /// /// Function called to persist the session (probably on a filesystem or cookie) /// - public Func> SessionPersistor = session => Task.FromResult(true); + public PersistenceListener.SaveSession? SessionPersistor; /// /// Function to retrieve a session (probably from the filesystem or cookie) /// - public Func> SessionRetriever = () => Task.FromResult(null); + public PersistenceListener.LoadSession? SessionRetriever; /// /// Function to destroy a session. /// - public Func> SessionDestroyer = () => Task.FromResult(true); + public PersistenceListener.DestroySession? SessionDestroyer; /// /// Very unlikely this flag needs to be changed except in very specific contexts. diff --git a/Gotrue/Constants.cs b/Gotrue/Constants.cs index f9d6cfb..7d34079 100644 --- a/Gotrue/Constants.cs +++ b/Gotrue/Constants.cs @@ -93,6 +93,7 @@ public enum Provider /// public enum AuthState { + ClientLaunch, SignedIn, SignedOut, UserUpdated, diff --git a/Gotrue/ExceptionHandler.cs b/Gotrue/ExceptionHandler.cs deleted file mode 100644 index 7fc1a9c..0000000 --- a/Gotrue/ExceptionHandler.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Net; -using Supabase.Gotrue.Exceptions; - -namespace Supabase.Gotrue -{ - /// - /// Internal class for parsing Supabase specific exceptions. - /// - internal static class ExceptionHandler - { - internal static Exception Parse(RequestException ex) - { - switch (ex.Response.StatusCode) - { - case HttpStatusCode.Unauthorized: - return new UnauthorizedException(ex); - case HttpStatusCode.BadRequest: - return new BadRequestException(ex); - case HttpStatusCode.Forbidden: - return new ForbiddenException(ex); - } - return ex; - } - } -} diff --git a/Gotrue/Exceptions/BadRequestException.cs b/Gotrue/Exceptions/BadRequestException.cs deleted file mode 100644 index 59dc229..0000000 --- a/Gotrue/Exceptions/BadRequestException.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Net.Http; - -namespace Supabase.Gotrue.Exceptions -{ - public class BadRequestException : GotrueException - { - public HttpResponseMessage Response { get; private set; } - - public string? Content { get; private set; } - public BadRequestException(RequestException exception): base(exception.Error.Message, exception) - { - Response = exception.Response; - Content = exception.Error.Message; - } - } -} diff --git a/Gotrue/Exceptions/ExistingUserException.cs b/Gotrue/Exceptions/ExistingUserException.cs deleted file mode 100644 index 186a35d..0000000 --- a/Gotrue/Exceptions/ExistingUserException.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Net.Http; - -namespace Supabase.Gotrue.Exceptions -{ - public class ExistingUserException : GotrueException - { - public HttpResponseMessage Response { get; private set; } - public string? Content { get; private set; } - public ExistingUserException(RequestException exception) : base(exception.Error.Message, exception) - { - Response = exception.Response; - Content = exception.Error.Message; - } - } -} diff --git a/Gotrue/Exceptions/ForbiddenException.cs b/Gotrue/Exceptions/ForbiddenException.cs deleted file mode 100644 index a1fac07..0000000 --- a/Gotrue/Exceptions/ForbiddenException.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Net.Http; - -namespace Supabase.Gotrue.Exceptions -{ - public class ForbiddenException : GotrueException - { - public HttpResponseMessage Response { get; private set; } - public string? Content { get; private set; } - public ForbiddenException(RequestException exception) : base(exception.Error.Message, exception) - { - Response = exception.Response; - Content = exception.Error.Message; - } - } -} diff --git a/Gotrue/Exceptions/GotrueException.cs b/Gotrue/Exceptions/GotrueException.cs index 20d0a42..6c392ad 100644 --- a/Gotrue/Exceptions/GotrueException.cs +++ b/Gotrue/Exceptions/GotrueException.cs @@ -1,4 +1,6 @@ using System; +using System.Net.Http; +using Supabase.Gotrue.Responses; namespace Supabase.Gotrue.Exceptions { @@ -6,5 +8,11 @@ public class GotrueException : Exception { public GotrueException(string? message) : base(message) { } public GotrueException(string? message, Exception? innerException) : base(message, innerException) { } + + public HttpResponseMessage? Response { get; internal set; } + + public string? Content { get; internal set; } + + public ErrorResponse? Error { get; private set; } } } diff --git a/Gotrue/Exceptions/InvalidEmailOrPasswordException.cs b/Gotrue/Exceptions/InvalidEmailOrPasswordException.cs deleted file mode 100644 index 6d6e264..0000000 --- a/Gotrue/Exceptions/InvalidEmailOrPasswordException.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Net.Http; - -namespace Supabase.Gotrue.Exceptions -{ - public class InvalidEmailOrPasswordException : GotrueException - { - public HttpResponseMessage Response { get; private set; } - public string? Content { get; private set; } - public InvalidEmailOrPasswordException(RequestException exception) : base(exception.Error.Message, exception) - { - Response = exception.Response; - Content = exception.Error.Message; - } - } -} diff --git a/Gotrue/Exceptions/InvalidProviderException.cs b/Gotrue/Exceptions/InvalidProviderException.cs deleted file mode 100644 index 4e29664..0000000 --- a/Gotrue/Exceptions/InvalidProviderException.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Supabase.Gotrue.Exceptions -{ - public class InvalidProviderException : GotrueException - { - public InvalidProviderException(string message) : base(message) { } - } -} - diff --git a/Gotrue/Exceptions/RequestException.cs b/Gotrue/Exceptions/RequestException.cs deleted file mode 100644 index 27b86c7..0000000 --- a/Gotrue/Exceptions/RequestException.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Net.Http; -using Supabase.Gotrue.Exceptions; -using Supabase.Gotrue.Responses; - -namespace Supabase.Gotrue -{ - public class RequestException : GotrueException - { - public HttpResponseMessage Response { get; private set; } - public ErrorResponse Error { get; private set; } - - public RequestException(HttpResponseMessage response, ErrorResponse error) : base(error.Message) - { - Response = response; - Error = error; - } - } -} diff --git a/Gotrue/Exceptions/UnauthorizedException.cs b/Gotrue/Exceptions/UnauthorizedException.cs deleted file mode 100644 index 0d5cab1..0000000 --- a/Gotrue/Exceptions/UnauthorizedException.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Net.Http; - -namespace Supabase.Gotrue.Exceptions -{ - public class UnauthorizedException : GotrueException - { - public HttpResponseMessage Response { get; private set; } - - public string? Content { get; private set; } - public UnauthorizedException(RequestException exception) : base(exception.Error.Message, exception) - { - Response = exception.Response; - Content = exception.Error.Message; - } - } -} diff --git a/Gotrue/Helpers.cs b/Gotrue/Helpers.cs index 7fa3187..de408f9 100644 --- a/Gotrue/Helpers.cs +++ b/Gotrue/Helpers.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using System.Web; using Newtonsoft.Json; +using Supabase.Gotrue.Exceptions; using Supabase.Gotrue.Responses; namespace Supabase.Gotrue @@ -22,6 +23,7 @@ public static class Helpers /// public static string GenerateNonce() { + // ReSharper disable once StringLiteralTypo const string chars = "abcdefghijklmnopqrstuvwxyz123456789"; var random = new Random(); var nonce = new char[128]; @@ -89,7 +91,7 @@ internal static Uri AddQueryParams(string url, Dictionary data) return builder.Uri; } - private static readonly HttpClient client = new HttpClient(); + private static readonly HttpClient Client = new HttpClient(); /// /// Helper to make a request using the defined parameters to an API Endpoint and coerce into a model. @@ -132,37 +134,33 @@ internal static async Task MakeRequest(HttpMethod method, string u builder.Query = query.ToString(); - using (var requestMessage = new HttpRequestMessage(method, builder.Uri)) + using var requestMessage = new HttpRequestMessage(method, builder.Uri); + if (data != null && method != HttpMethod.Get) { + requestMessage.Content = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json"); + } - if (data != null && method != HttpMethod.Get) + if (headers != null) + { + foreach (var kvp in headers) { - requestMessage.Content = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json"); + requestMessage.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value); } + } - if (headers != null) - { - foreach (var kvp in headers) - { - requestMessage.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value); - } - } + var response = await Client.SendAsync(requestMessage); + var content = await response.Content.ReadAsStringAsync(); - var response = await client.SendAsync(requestMessage); - var content = await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) + { + var e = new GotrueException("Request Failed"); + e.Content = content; + e.Response = response; + throw e; + } - if (!response.IsSuccessStatusCode) - { - var obj = new ErrorResponse - { - Content = content, - Message = content - }; - throw new RequestException(response, obj); - } + return new BaseResponse { Content = content, ResponseMessage = response }; - return new BaseResponse { Content = content, ResponseMessage = response }; - } } } } diff --git a/Gotrue/Interfaces/IGotrueClient.cs b/Gotrue/Interfaces/IGotrueClient.cs index fc687a8..256d8b9 100644 --- a/Gotrue/Interfaces/IGotrueClient.cs +++ b/Gotrue/Interfaces/IGotrueClient.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Supabase.Core.Interfaces; using static Supabase.Gotrue.Constants; @@ -12,7 +13,13 @@ public interface IGotrueClient : IGettableHeaders TSession? CurrentSession { get; } TUser? CurrentUser { get; } - event EventHandler? StateChanged; + delegate void AuthEventHandler(IGotrueClient sender, AuthState stateChanged); + + public void AddStateChangedListener(AuthEventHandler authEventHandler); + public void RemoveStateChangedListener(AuthEventHandler authEventHandler); + public void ClearStateChangedListeners(); + public void NotifyStateChange(AuthState stateChanged); + Task CreateUser(string jwt, AdminUserAttributes attributes); Task CreateUser(string jwt, string email, string password, AdminUserAttributes? attributes = null); Task DeleteUser(string uid, string jwt); diff --git a/Gotrue/PersistenceListener.cs b/Gotrue/PersistenceListener.cs new file mode 100644 index 0000000..05df261 --- /dev/null +++ b/Gotrue/PersistenceListener.cs @@ -0,0 +1,64 @@ +using System; +using System.Threading.Tasks; +using Supabase.Gotrue.Interfaces; + +namespace Supabase.Gotrue +{ + public class PersistenceListener + { + private readonly SaveSession? _save; + private readonly DestroySession? _load; + private readonly LoadSession? _destroy; + + public delegate bool SaveSession(Session session); + + public delegate void DestroySession(); + + public delegate Session LoadSession(); + + public PersistenceListener(SaveSession? s, DestroySession? d, LoadSession? l) + { + _save = s; + _load = d; + _destroy = l; + } + public void EventHandler(IGotrueClient sender, Constants.AuthState stateChanged) + { + switch (stateChanged) + { + case Constants.AuthState.SignedIn: + if (sender == null) + throw new ArgumentException("Tried to save a null session (1)"); + if (sender.CurrentSession == null) + throw new ArgumentException("Tried to save a null session (2)"); + + _save?.Invoke(sender.CurrentSession); + break; + case Constants.AuthState.SignedOut: + _destroy?.Invoke(); + break; + case Constants.AuthState.UserUpdated: + if (sender == null) + throw new ArgumentException("Tried to save a null session (1)"); + if (sender.CurrentSession == null) + throw new ArgumentException("Tried to save a null session (2)"); + + _save?.Invoke(sender.CurrentSession); + break; + case Constants.AuthState.PasswordRecovery: break; + case Constants.AuthState.TokenRefreshed: + if (sender == null) + throw new ArgumentException("Tried to save a null session (1)"); + if (sender.CurrentSession == null) + throw new ArgumentException("Tried to save a null session (2)"); + + _save?.Invoke(sender.CurrentSession); + break; + case Constants.AuthState.ClientLaunch: + _load?.Invoke(); + break; + default: throw new ArgumentOutOfRangeException(nameof(stateChanged), stateChanged, null); + } + } + } +} diff --git a/Gotrue/StatelessClient.cs b/Gotrue/StatelessClient.cs index 2ef1ba1..b2238b8 100644 --- a/Gotrue/StatelessClient.cs +++ b/Gotrue/StatelessClient.cs @@ -7,536 +7,429 @@ namespace Supabase.Gotrue { - /// - /// A Stateless Gotrue Client - /// - /// - /// var options = new StatelessClientOptions { Url = "https://mygotrueurl.com" }; - /// var user = await client.SignIn("user@email.com", "fancyPassword", options); - /// - public class StatelessClient : IGotrueStatelessClient - { - - public IGotrueApi GetApi(StatelessClientOptions options) => new Api(options.Url, options.Headers); - - /// - /// Signs up a user by email address - /// - /// - /// - /// - /// Object containing redirectTo and optional user metadata (data) - /// - public Task SignUp(string email, string password, StatelessClientOptions options, SignUpOptions? signUpOptions = null) => SignUp(SignUpType.Email, email, password, options, signUpOptions); - - /// - /// Signs up a user - /// - /// Type of signup - /// Phone or Email - /// - /// - /// Object containing redirectTo and optional user metadata (data) - /// - public async Task SignUp(SignUpType type, string identifier, string password, StatelessClientOptions options, SignUpOptions? signUpOptions = null) - { - try - { - var api = GetApi(options); - Session? session = null; - switch (type) - { - case SignUpType.Email: - session = await api.SignUpWithEmail(identifier, password, signUpOptions); - break; - case SignUpType.Phone: - session = await api.SignUpWithPhone(identifier, password, signUpOptions); - break; - } - - if (session?.User?.ConfirmedAt != null || (session?.User != null && options.AllowUnconfirmedUserSessions)) - { - return session; - } - - return null; - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } - } - - - /// - /// Sends a Magic email login link to the specified email. - /// - /// - /// - /// - /// - public async Task SignIn(string email, StatelessClientOptions options, SignInOptions? signInOptions = null) - { - try - { - await GetApi(options).SendMagicLinkEmail(email, signInOptions); - return true; - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } - } - - /// - /// Sends a Magic email login link to the specified email. - /// - /// - /// - /// - /// - public Task SendMagicLink(string email, StatelessClientOptions options, SignInOptions? signInOptions = null) => SignIn(email, options, signInOptions); - - /// - /// Signs in a User. - /// - /// - /// - /// - /// - public Task SignIn(string email, string password, StatelessClientOptions options) => SignIn(SignInType.Email, email, password, options); - - /// - /// Log in an existing user, or login via a third-party provider. - /// - /// Type of Credentials being passed - /// An email, phone, or RefreshToken - /// Password to account (optional if `RefreshToken`) - /// - /// - public async Task SignIn(SignInType type, string identifierOrToken, string? password = null, StatelessClientOptions? options = null) - { - try - { - if (options == null) - options = new StatelessClientOptions(); - - var api = GetApi(options); - Session? session = null; - switch (type) - { - case SignInType.Email: - session = await api.SignInWithEmail(identifierOrToken, password!); - break; - case SignInType.Phone: - if (string.IsNullOrEmpty(password)) - { - var response = await api.SendMobileOTP(identifierOrToken); - return null; - } - - session = await api.SignInWithPhone(identifierOrToken, password!); - break; - case SignInType.RefreshToken: - session = await RefreshToken(identifierOrToken, options); - break; - } - - if (session?.User?.ConfirmedAt != null || (session?.User != null && options.AllowUnconfirmedUserSessions)) - { - return session; - } - - return null; - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } - } - - /// - /// Retrieves a Url to redirect to for signing in with a . - /// - /// This method will need to be combined with when the - /// Application receives the Oauth Callback. - /// - /// - /// var client = Supabase.Gotrue.Client.Initialize(options); - /// var url = client.SignIn(Provider.Github); - /// - /// // Do Redirect User - /// - /// // Example code - /// Application.HasRecievedOauth += async (uri) => { - /// var session = await client.GetSessionFromUri(uri, true); - /// } - /// - /// - /// - /// - /// - public ProviderAuthState SignIn(Provider provider, StatelessClientOptions options, SignInOptions? signInOptions = null) => GetApi(options).GetUriForProvider(provider, signInOptions); - - /// - /// Logout a User - /// This will revoke all refresh tokens for the user. - /// JWT tokens will still be valid for stateless auth until they expire. - /// - /// - /// - /// - public async Task SignOut(string jwt, StatelessClientOptions options) - { - try - { - var result = await GetApi(options).SignOut(jwt); - result.ResponseMessage?.EnsureSuccessStatusCode(); - return true; - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } - } - - /// - /// Log in a user given a User supplied OTP received via mobile. - /// - /// The user's phone number. - /// Token sent to the user's phone. - /// - /// - /// - public async Task VerifyOTP(string phone, string token, StatelessClientOptions options, MobileOtpType type = MobileOtpType.SMS) - { - try - { - var session = await GetApi(options).VerifyMobileOTP(phone, token, type); - - if (session?.AccessToken != null) - { - return session; - } - - return null; - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } - } - - /// - /// Log in a user give a user supplied OTP received via email. - /// - /// - /// - /// - /// - /// - public async Task VerifyOTP(string email, string token, StatelessClientOptions options, EmailOtpType type = EmailOtpType.MagicLink) - { - try - { - var session = await GetApi(options).VerifyEmailOTP(email, token, type); - - if (session?.AccessToken != null) - { - return session; - } - - return null; - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } - } - - - /// - /// Updates a User. - /// - /// - /// - /// - /// - public async Task Update(string accessToken, UserAttributes attributes, StatelessClientOptions options) - { - try - { - var result = await GetApi(options).UpdateUser(accessToken, attributes); - return result; - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } - } - - /// - /// Sends an invite email link to the specified email. - /// - /// - /// this token needs role 'supabase_admin' or 'service_role' - /// - /// - public async Task InviteUserByEmail(string email, string jwt, StatelessClientOptions options) - { - try - { - var response = await GetApi(options).InviteUserByEmail(email, jwt); - response.ResponseMessage?.EnsureSuccessStatusCode(); - return true; - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } - } - - /// - /// Sends a reset request to an email address. - /// - /// - /// - /// - /// - public async Task ResetPasswordForEmail(string email, StatelessClientOptions options) - { - try - { - var result = await GetApi(options).ResetPasswordForEmail(email); - result.ResponseMessage?.EnsureSuccessStatusCode(); - return true; - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } - } - - /// - /// Lists users - /// - /// A valid JWT. Must be a full-access API key (e.g. service_role key). - /// - /// A string for example part of the email - /// Snake case string of the given key, currently only created_at is supported - /// asc or desc, if null desc is used - /// page to show for pagination - /// items per page for pagination - /// - public async Task?> ListUsers(string jwt, StatelessClientOptions options, string? filter = null, string? sortBy = null, SortOrder sortOrder = SortOrder.Descending, int? page = null, int? perPage = null) - { - try - { - return await GetApi(options).ListUsers(jwt, filter, sortBy, sortOrder, page, perPage); - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } - } - - /// - /// Get User details by Id - /// - /// A valid JWT. Must be a full-access API key (e.g. service_role key). - /// - /// - /// - public async Task GetUserById(string jwt, StatelessClientOptions options, string userId) - { - try - { - return await GetApi(options).GetUserById(jwt, userId); - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } - } - - /// - /// Get User details by JWT. Can be used to validate a JWT. - /// - /// A valid JWT. Must be a JWT that originates from a user. - /// - /// - public async Task GetUser(string jwt, StatelessClientOptions options) - { - try - { - return await GetApi(options).GetUser(jwt); - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } - } - - /// - /// Create a user - /// - /// A valid JWT. Must be a full-access API key (e.g. service_role key). - /// - /// - /// - /// - /// - public Task CreateUser(string jwt, StatelessClientOptions options, string email, string password, AdminUserAttributes? attributes = null) - { - if (attributes == null) - { - attributes = new AdminUserAttributes(); - } - attributes.Email = email; - attributes.Password = password; - - return CreateUser(jwt, options, attributes); - } - - /// - /// Create a user - /// - /// A valid JWT. Must be a full-access API key (e.g. service_role key). - /// - /// - /// - public async Task CreateUser(string jwt, StatelessClientOptions options, AdminUserAttributes attributes) - { - try - { - return await GetApi(options).CreateUser(jwt, attributes); - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } - } - - /// - /// Update user by Id - /// - /// A valid JWT. Must be a full-access API key (e.g. service_role key). - /// - /// - /// - /// - public async Task UpdateUserById(string jwt, StatelessClientOptions options, string userId, AdminUserAttributes userData) - { - try - { - return await GetApi(options).UpdateUserById(jwt, userId, userData); - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } - } - - /// - /// Deletes a User. - /// - /// - /// this token needs role 'supabase_admin' or 'service_role' - /// - /// - public async Task DeleteUser(string uid, string jwt, StatelessClientOptions options) - { - try - { - var result = await GetApi(options).DeleteUser(uid, jwt); - result.ResponseMessage?.EnsureSuccessStatusCode(); - return true; - } - catch (RequestException ex) - { - throw ExceptionHandler.Parse(ex); - } - } - - /// - /// Parses a out of a 's Query parameters. - /// - /// - /// - /// - public async Task GetSessionFromUrl(Uri uri, StatelessClientOptions options) - { - var query = HttpUtility.ParseQueryString(uri.Query); - - var errorDescription = query.Get("error_description"); - - if (!string.IsNullOrEmpty(errorDescription)) - throw new Exception(errorDescription); - - var accessToken = query.Get("access_token"); - - if (string.IsNullOrEmpty(accessToken)) - throw new Exception("No access_token detected."); - - var expiresIn = query.Get("expires_in"); - - if (string.IsNullOrEmpty(expiresIn)) - throw new Exception("No expires_in detected."); - - var refreshToken = query.Get("refresh_token"); - - if (string.IsNullOrEmpty(refreshToken)) - throw new Exception("No refresh_token detected."); - - var tokenType = query.Get("token_type"); - - if (string.IsNullOrEmpty(tokenType)) - throw new Exception("No token_type detected."); - - var user = await GetApi(options).GetUser(accessToken); - - var session = new Session - { - AccessToken = accessToken, - ExpiresIn = int.Parse(expiresIn), - RefreshToken = refreshToken, - TokenType = tokenType, - User = user - }; - - return session; - } - - /// - /// Refreshes a Token - /// - /// - public async Task RefreshToken(string refreshToken, StatelessClientOptions options) => await GetApi(options).RefreshAccessToken(refreshToken); - - - /// - /// Class representation options available to the . - /// - public class StatelessClientOptions - { - /// - /// Gotrue Endpoint - /// - public string Url { get; set; } = GOTRUE_URL; - - /// - /// Headers to be sent with subsequent requests. - /// - public Dictionary Headers = new Dictionary(DEFAULT_HEADERS); - - /// - /// Very unlikely this flag needs to be changed except in very specific contexts. - /// - /// Enables tests to be E2E tests to be run without requiring users to have - /// confirmed emails - mirrors the Gotrue server's configuration. - /// - public bool AllowUnconfirmedUserSessions { get; set; } - } - } - -} \ No newline at end of file + /// + /// A Stateless Gotrue Client + /// + /// + /// var options = new StatelessClientOptions { Url = "https://mygotrueurl.com" }; + /// var user = await client.SignIn("user@email.com", "fancyPassword", options); + /// + public class StatelessClient : IGotrueStatelessClient + { + + public IGotrueApi GetApi(StatelessClientOptions options) => new Api(options.Url, options.Headers); + + /// + /// Signs up a user by email address + /// + /// + /// + /// + /// Object containing redirectTo and optional user metadata (data) + /// + public Task SignUp(string email, string password, StatelessClientOptions options, SignUpOptions? signUpOptions = null) => SignUp(SignUpType.Email, email, password, options, signUpOptions); + + /// + /// Signs up a user + /// + /// Type of signup + /// Phone or Email + /// + /// + /// Object containing redirectTo and optional user metadata (data) + /// + public async Task SignUp(SignUpType type, string identifier, string password, StatelessClientOptions options, SignUpOptions? signUpOptions = null) + { + var api = GetApi(options); + Session? session = null; + switch (type) + { + case SignUpType.Email: + session = await api.SignUpWithEmail(identifier, password, signUpOptions); + break; + case SignUpType.Phone: + session = await api.SignUpWithPhone(identifier, password, signUpOptions); + break; + } + + if (session?.User?.ConfirmedAt != null || (session?.User != null && options.AllowUnconfirmedUserSessions)) + { + return session; + } + + return null; + } + + + /// + /// Sends a Magic email login link to the specified email. + /// + /// + /// + /// + /// + public async Task SignIn(string email, StatelessClientOptions options, SignInOptions? signInOptions = null) + { + await GetApi(options).SendMagicLinkEmail(email, signInOptions); + return true; + } + + /// + /// Sends a Magic email login link to the specified email. + /// + /// + /// + /// + /// + public Task SendMagicLink(string email, StatelessClientOptions options, SignInOptions? signInOptions = null) => SignIn(email, options, signInOptions); + + /// + /// Signs in a User. + /// + /// + /// + /// + /// + public Task SignIn(string email, string password, StatelessClientOptions options) => SignIn(SignInType.Email, email, password, options); + + /// + /// Log in an existing user, or login via a third-party provider. + /// + /// Type of Credentials being passed + /// An email, phone, or RefreshToken + /// Password to account (optional if `RefreshToken`) + /// + /// + public async Task SignIn(SignInType type, string identifierOrToken, string? password = null, StatelessClientOptions? options = null) + { + + options ??= new StatelessClientOptions(); + + var api = GetApi(options); + Session? session = null; + switch (type) + { + case SignInType.Email: + session = await api.SignInWithEmail(identifierOrToken, password!); + break; + case SignInType.Phone: + if (string.IsNullOrEmpty(password)) + { + await api.SendMobileOTP(identifierOrToken); + return null; + } + + session = await api.SignInWithPhone(identifierOrToken, password!); + break; + case SignInType.RefreshToken: + session = await RefreshToken(identifierOrToken, options); + break; + } + + if (session?.User?.ConfirmedAt != null || (session?.User != null && options.AllowUnconfirmedUserSessions)) + { + return session; + } + + return null; + } + + /// + /// Retrieves a Url to redirect to for signing in with a . + /// + /// This method will need to be combined with when the + /// Application receives the Oauth Callback. + /// + /// + /// var client = Supabase.Gotrue.Client.Initialize(options); + /// var url = client.SignIn(Provider.Github); + /// + /// // Do Redirect User + /// + /// // Example code + /// Application.HasReceivedOauth += async (uri) => { + /// var session = await client.GetSessionFromUri(uri, true); + /// } + /// + /// + /// + /// + /// + public ProviderAuthState SignIn(Provider provider, StatelessClientOptions options, SignInOptions? signInOptions = null) => GetApi(options).GetUriForProvider(provider, signInOptions); + + /// + /// Logout a User + /// This will revoke all refresh tokens for the user. + /// JWT tokens will still be valid for stateless auth until they expire. + /// + /// + /// + /// + public async Task SignOut(string jwt, StatelessClientOptions options) + { + var result = await GetApi(options).SignOut(jwt); + result.ResponseMessage?.EnsureSuccessStatusCode(); + return true; + } + + /// + /// Log in a user given a User supplied OTP received via mobile. + /// + /// The user's phone number. + /// Token sent to the user's phone. + /// + /// + /// + public async Task VerifyOTP(string phone, string token, StatelessClientOptions options, MobileOtpType type = MobileOtpType.SMS) + { + var session = await GetApi(options).VerifyMobileOTP(phone, token, type); + + if (session?.AccessToken != null) + { + return session; + } + + return null; + } + + /// + /// Log in a user give a user supplied OTP received via email. + /// + /// + /// + /// + /// + /// + public async Task VerifyOTP(string email, string token, StatelessClientOptions options, EmailOtpType type = EmailOtpType.MagicLink) + { + var session = await GetApi(options).VerifyEmailOTP(email, token, type); + + if (session?.AccessToken != null) + { + return session; + } + + return null; + } + + + /// + /// Updates a User. + /// + /// + /// + /// + /// + public async Task Update(string accessToken, UserAttributes attributes, StatelessClientOptions options) + { + var result = await GetApi(options).UpdateUser(accessToken, attributes); + return result; + } + + /// + /// Sends an invite email link to the specified email. + /// + /// + /// this token needs role 'supabase_admin' or 'service_role' + /// + /// + public async Task InviteUserByEmail(string email, string jwt, StatelessClientOptions options) + { + var response = await GetApi(options).InviteUserByEmail(email, jwt); + response.ResponseMessage?.EnsureSuccessStatusCode(); + return true; + } + + /// + /// Sends a reset request to an email address. + /// + /// + /// + /// + /// + public async Task ResetPasswordForEmail(string email, StatelessClientOptions options) + { + var result = await GetApi(options).ResetPasswordForEmail(email); + result.ResponseMessage?.EnsureSuccessStatusCode(); + return true; + } + + /// + /// Lists users + /// + /// A valid JWT. Must be a full-access API key (e.g. service_role key). + /// + /// A string for example part of the email + /// Snake case string of the given key, currently only created_at is supported + /// asc or desc, if null desc is used + /// page to show for pagination + /// items per page for pagination + /// + public async Task?> ListUsers(string jwt, StatelessClientOptions options, string? filter = null, string? sortBy = null, SortOrder sortOrder = SortOrder.Descending, + int? page = null, int? perPage = null) + { + return await GetApi(options).ListUsers(jwt, filter, sortBy, sortOrder, page, perPage); + } + + /// + /// Get User details by Id + /// + /// A valid JWT. Must be a full-access API key (e.g. service_role key). + /// + /// + /// + public async Task GetUserById(string jwt, StatelessClientOptions options, string userId) + { + return await GetApi(options).GetUserById(jwt, userId); + } + + /// + /// Get User details by JWT. Can be used to validate a JWT. + /// + /// A valid JWT. Must be a JWT that originates from a user. + /// + /// + public async Task GetUser(string jwt, StatelessClientOptions options) + { + return await GetApi(options).GetUser(jwt); + } + + /// + /// Create a user + /// + /// A valid JWT. Must be a full-access API key (e.g. service_role key). + /// + /// + /// + /// + /// + public Task CreateUser(string jwt, StatelessClientOptions options, string email, string password, AdminUserAttributes? attributes = null) + { + attributes ??= new AdminUserAttributes(); + attributes.Email = email; + attributes.Password = password; + + return CreateUser(jwt, options, attributes); + } + + /// + /// Create a user + /// + /// A valid JWT. Must be a full-access API key (e.g. service_role key). + /// + /// + /// + public async Task CreateUser(string jwt, StatelessClientOptions options, AdminUserAttributes attributes) + { + return await GetApi(options).CreateUser(jwt, attributes); + } + + /// + /// Update user by Id + /// + /// A valid JWT. Must be a full-access API key (e.g. service_role key). + /// + /// + /// + /// + public async Task UpdateUserById(string jwt, StatelessClientOptions options, string userId, AdminUserAttributes userData) + { + return await GetApi(options).UpdateUserById(jwt, userId, userData); + } + + /// + /// Deletes a User. + /// + /// + /// this token needs role 'supabase_admin' or 'service_role' + /// + /// + public async Task DeleteUser(string uid, string jwt, StatelessClientOptions options) + { + var result = await GetApi(options).DeleteUser(uid, jwt); + result.ResponseMessage?.EnsureSuccessStatusCode(); + return true; + } + + /// + /// Parses a out of a 's Query parameters. + /// + /// + /// + /// + public async Task GetSessionFromUrl(Uri uri, StatelessClientOptions options) + { + var query = HttpUtility.ParseQueryString(uri.Query); + + var errorDescription = query.Get("error_description"); + + if (!string.IsNullOrEmpty(errorDescription)) + throw new Exception(errorDescription); + + var accessToken = query.Get("access_token"); + + if (string.IsNullOrEmpty(accessToken)) + throw new Exception("No access_token detected."); + + var expiresIn = query.Get("expires_in"); + + if (string.IsNullOrEmpty(expiresIn)) + throw new Exception("No expires_in detected."); + + var refreshToken = query.Get("refresh_token"); + + if (string.IsNullOrEmpty(refreshToken)) + throw new Exception("No refresh_token detected."); + + var tokenType = query.Get("token_type"); + + if (string.IsNullOrEmpty(tokenType)) + throw new Exception("No token_type detected."); + + var user = await GetApi(options).GetUser(accessToken); + + var session = new Session + { + AccessToken = accessToken, + ExpiresIn = int.Parse(expiresIn), + RefreshToken = refreshToken, + TokenType = tokenType, + User = user + }; + + return session; + } + + /// + /// Refreshes a Token + /// + /// + public async Task RefreshToken(string refreshToken, StatelessClientOptions options) => await GetApi(options).RefreshAccessToken(refreshToken); + + + /// + /// Class representation options available to the . + /// + public class StatelessClientOptions + { + /// + /// Gotrue Endpoint + /// + public string Url { get; set; } = GOTRUE_URL; + + /// + /// Headers to be sent with subsequent requests. + /// + public readonly Dictionary Headers = new Dictionary(DEFAULT_HEADERS); + + /// + /// Very unlikely this flag needs to be changed except in very specific contexts. + /// + /// Enables tests to be E2E tests to be run without requiring users to have + /// confirmed emails - mirrors the Gotrue server's configuration. + /// + public bool AllowUnconfirmedUserSessions { get; set; } + } + } + +} diff --git a/GotrueTests/AnonKeyClientTests.cs b/GotrueTests/AnonKeyClientTests.cs new file mode 100644 index 0000000..2965903 --- /dev/null +++ b/GotrueTests/AnonKeyClientTests.cs @@ -0,0 +1,308 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Supabase.Gotrue; +using Supabase.Gotrue.Exceptions; +using Supabase.Gotrue.Interfaces; +using static GotrueTests.TestUtils; +using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert; +using static Microsoft.VisualStudio.TestTools.UnitTesting.CollectionAssert; +using static Supabase.Gotrue.Constants.AuthState; + +namespace GotrueTests +{ + [SuppressMessage("ReSharper", "PossibleNullReferenceException")] + [TestClass] + public class AnonKeyClientTests + { + + private void AuthStateListener(IGotrueClient sender, Constants.AuthState newState) + { + if (_stateChanges.Contains(newState) && newState != SignedOut) + throw new ArgumentException($"State updated twice {newState}"); + + _stateChanges.Add(newState); + } + + private bool AuthStateIsEmpty() + { + return _stateChanges.Count == 0; + } + + [TestInitialize] + public void TestInitializer() + { + _client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true }); + _client.AddDebugListener(LogDebug); + _client.AddStateChangedListener(AuthStateListener); + } + + private Client _client; + + private readonly List _stateChanges = new List(); + + [TestMethod("Client: Sign Up User")] + public async Task SignUpUserEmail() + { + IsTrue(AuthStateIsEmpty()); + + var email = $"{RandomString(12)}@supabase.io"; + var session = await _client.SignUp(email, PASSWORD); + + Contains(_stateChanges, SignedIn); + + IsNotNull(session.AccessToken); + IsNotNull(session.RefreshToken); + IsNotNull(session.User); + } + + [TestMethod("Client: Sign up Phone")] + public async Task SignUpUserPhone() + { + IsTrue(AuthStateIsEmpty()); + + var phone1 = GetRandomPhoneNumber(); + var session = await _client.SignUp(Constants.SignUpType.Phone, phone1, PASSWORD, new SignUpOptions { Data = new Dictionary { { "firstName", "Testing" } } }); + + Contains(_stateChanges, SignedIn); + + IsNotNull(session.AccessToken); + AreEqual("Testing", session.User.UserMetadata["firstName"]); + } + + [TestMethod("Client: Signs Up the same user twice should throw BadRequestException")] + public async Task SignsUpUserTwiceShouldReturnBadRequest() + { + var email = $"{RandomString(12)}@supabase.io"; + var result1 = await _client.SignUp(email, PASSWORD); + + IsNotNull(result1); + + Contains(_stateChanges, SignedIn); + _stateChanges.Clear(); + + await ThrowsExceptionAsync(async () => + { + // This calls session destroy, logging the user out + await _client.SignUp(email, PASSWORD); + }); + + Contains(_stateChanges, SignedOut); + } + + [TestMethod("Client: Triggers Token Refreshed Event")] + public async Task ClientTriggersTokenRefreshedEvent() + { + var tsc = new TaskCompletionSource(); + + var email = $"{RandomString(12)}@supabase.io"; + + IsTrue(AuthStateIsEmpty()); + + var user = await _client.SignUp(email, PASSWORD); + + Contains(_stateChanges, SignedIn); + + _client.AddStateChangedListener((_, args) => + { + if (args == TokenRefreshed) + { + tsc.SetResult(_client.CurrentSession.AccessToken); + } + }); + + _stateChanges.Clear(); + + await _client.RefreshSession(); + Contains(_stateChanges, TokenRefreshed); + + var newToken = await tsc.Task; + IsNotNull(newToken); + + AreNotEqual(user.RefreshToken, _client.CurrentSession.RefreshToken); + } + + [TestMethod("Client: Signs In User (Email, Phone, Refresh token)")] + public async Task ClientSignsIn() + { + var email = $"{RandomString(12)}@supabase.io"; + await _client.SignUp(email, PASSWORD); + Contains(_stateChanges, SignedIn); + _stateChanges.Clear(); + + await _client.SignOut(); + Contains(_stateChanges, SignedOut); + _stateChanges.Clear(); + + var session = await _client.SignIn(email, PASSWORD); + + IsNotNull(session.AccessToken); + IsNotNull(session.RefreshToken); + IsNotNull(session.User); + + Contains(_stateChanges, SignedIn); + _stateChanges.Clear(); + + // Phones + var phone = GetRandomPhoneNumber(); + await _client.SignUp(Constants.SignUpType.Phone, phone, PASSWORD); + Contains(_stateChanges, SignedIn); + _stateChanges.Clear(); + + await _client.SignOut(); + Contains(_stateChanges, SignedOut); + _stateChanges.Clear(); + + session = await _client.SignIn(Constants.SignInType.Phone, phone, PASSWORD); + Contains(_stateChanges, SignedIn); + _stateChanges.Clear(); + + IsNotNull(session.AccessToken); + IsNotNull(session.RefreshToken); + IsNotNull(session.User); + + // Refresh Token + var refreshToken = session.RefreshToken; + + var newSession = await _client.SignIn(Constants.SignInType.RefreshToken, refreshToken); + Contains(_stateChanges, TokenRefreshed); + DoesNotContain(_stateChanges, SignedIn); + + IsNotNull(newSession.AccessToken); + IsNotNull(newSession.RefreshToken); + IsInstanceOfType(newSession.User, typeof(User)); + } + + [TestMethod("Client: Sends Magic Login Email")] + public async Task ClientSendsMagicLoginEmail() + { + var user = $"{RandomString(12)}@supabase.io"; + await _client.SignUp(user, PASSWORD); + Contains(_stateChanges, SignedIn); + _stateChanges.Clear(); + + await _client.SignOut(); + Contains(_stateChanges, SignedOut); + _stateChanges.Clear(); + + var result = await _client.SignIn(user); + IsTrue(result); + Contains(_stateChanges, SignedOut); + } + + [TestMethod("Client: Sends Magic Login Email (Alias)")] + public async Task ClientSendsMagicLoginEmailAlias() + { + var user = $"{RandomString(12)}@supabase.io"; + var user2 = $"{RandomString(12)}@supabase.io"; + await _client.SignUp(user, PASSWORD); + Contains(_stateChanges, SignedIn); + _stateChanges.Clear(); + + await _client.SignOut(); + Contains(_stateChanges, SignedOut); + + var result = await _client.SendMagicLink(user); + var result2 = await _client.SendMagicLink(user2, new SignInOptions { RedirectTo = $"com.{RandomString(12)}.deeplink://login" }); + + IsTrue(result); + IsTrue(result2); + } + + [TestMethod("Client: Returns Auth Url for Provider")] + public async Task ClientReturnsAuthUrlForProvider() + { + var result1 = await _client.SignIn(Constants.Provider.Google); + AreEqual("http://localhost:9999/authorize?provider=google", result1.Uri.ToString()); + + var result2 = await _client.SignIn(Constants.Provider.Google, new SignInOptions { Scopes = "special scopes please" }); + AreEqual("http://localhost:9999/authorize?provider=google&scopes=special+scopes+please", result2.Uri.ToString()); + } + + [TestMethod("Client: Returns Verification Code for Provider")] + public async Task ClientReturnsPKCEVerifier() + { + var result = await _client.SignIn(Constants.Provider.Github, new SignInOptions { FlowType = Constants.OAuthFlowType.PKCE }); + + IsTrue(!string.IsNullOrEmpty(result.PKCEVerifier)); + IsTrue(result.Uri.Query.Contains("flow_type=pkce")); + IsTrue(result.Uri.Query.Contains("code_challenge=")); + IsTrue(result.Uri.Query.Contains("code_challenge_method=s256")); + IsTrue(result.Uri.Query.Contains("provider=github")); + } + + [TestMethod("Client: Update user")] + public async Task ClientUpdateUser() + { + var email = $"{RandomString(12)}@supabase.io"; + var session = await _client.SignUp(email, PASSWORD); + + var attributes = new UserAttributes { Data = new Dictionary { { "hello", "world" } } }; + var result = await _client.Update(attributes); + AreEqual(email, _client.CurrentUser.Email); + IsNotNull(_client.CurrentUser.UserMetadata); + + await _client.SignOut(); + var token = GenerateServiceRoleToken(); + var result2 = await _client.UpdateUserById(token, session.User.Id ?? throw new InvalidOperationException(), new AdminUserAttributes { UserMetadata = new Dictionary { { "hello", "updated" } } }); + + AreNotEqual(result.UserMetadata["hello"], result2.UserMetadata["hello"]); + } + + [TestMethod("Client: Returns current user")] + public async Task ClientGetUser() + { + var email = $"{RandomString(12)}@supabase.io"; + var newUser = await _client.SignUp(email, PASSWORD); + + AreEqual(email, _client.CurrentUser.Email); + + var userByJWT = await _client.GetUser(newUser.AccessToken ?? throw new InvalidOperationException()); + AreEqual(email, userByJWT.Email); + } + + [TestMethod("Client: Nulls CurrentUser on SignOut")] + public async Task ClientGetUserAfterLogOut() + { + IsTrue(AuthStateIsEmpty()); + var user = $"{RandomString(12)}@supabase.io"; + await _client.SignUp(user, PASSWORD); + Contains(_stateChanges, SignedIn); + + _stateChanges.Clear(); + await _client.SignOut(); + + Contains(_stateChanges, SignedOut); + + IsNull(_client.CurrentUser); + } + + [TestMethod("Client: Throws Exception on Invalid Username and Password")] + public async Task ClientSignsInUserWrongPassword() + { + var user = $"{RandomString(12)}@supabase.io"; + await _client.SignUp(user, PASSWORD); + + await _client.SignOut(); + + await ThrowsExceptionAsync(async () => + { + var result = await _client.SignIn(user, PASSWORD + "$"); + IsNotNull(result); + }); + } + + + [TestMethod("Client: Send Reset Password Email")] + public async Task ClientSendsResetPasswordForEmail() + { + var email = $"{RandomString(12)}@supabase.io"; + await _client.SignUp(email, PASSWORD); + var result = await _client.ResetPasswordForEmail(email); + IsTrue(result); + } + + } +} diff --git a/GotrueTests/ClientTests.cs b/GotrueTests/ClientTests.cs deleted file mode 100644 index e688b6b..0000000 --- a/GotrueTests/ClientTests.cs +++ /dev/null @@ -1,428 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IdentityModel.Tokens.Jwt; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.IdentityModel.Tokens; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Supabase.Gotrue; -using static Supabase.Gotrue.Constants; -using Supabase.Gotrue.Exceptions; - -namespace GotrueTests -{ - [TestClass] - public class ClientTests - { - private Client client; - - private string password = "I@M@SuperP@ssWord"; - - private static Random random = new Random(); - - private static string RandomString(int length) - { - const string chars = "abcdefghijklmnopqrstuvwxyz0123456789"; - return new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray()); - } - - private static string GetRandomPhoneNumber() - { - const string chars = "123456789"; - var inner = new string(Enumerable.Repeat(chars, 10).Select(s => s[random.Next(s.Length)]).ToArray()); - - return $"+1{inner}"; - } - - private string GenerateServiceRoleToken() - { - var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("37c304f8-51aa-419a-a1af-06154e63707a")); // using GOTRUE_JWT_SECRET - - var tokenDescriptor = new SecurityTokenDescriptor - { - IssuedAt = DateTime.Now, - Expires = DateTime.UtcNow.AddDays(7), - SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256Signature), - Claims = new Dictionary() { { "role", "service_role" } } - }; - - var tokenHandler = new JwtSecurityTokenHandler(); - var securityToken = tokenHandler.CreateToken(tokenDescriptor); - return tokenHandler.WriteToken(securityToken); - } - - [TestInitialize] - public void TestInitializer() - { - client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true }); - client.AddDebugListener(LogDebug); - } - private static void LogDebug(string message, Exception e) - { - Debug.WriteLine(message); - if (e != null) - Debug.WriteLine(e); - } - - [TestMethod("Client: Signs Up User")] - public async Task ClientSignsUpUser() - { - Session session; - var email = $"{RandomString(12)}@supabase.io"; - session = await client.SignUp(email, password); - - Assert.IsNotNull(session.AccessToken); - Assert.IsNotNull(session.RefreshToken); - Assert.IsInstanceOfType(session.User, typeof(User)); - - var phone1 = GetRandomPhoneNumber(); - session = await client.SignUp(SignUpType.Phone, phone1, password, new SignUpOptions { Data = new Dictionary { { "firstName", "Testing" } } }); - - Assert.IsNotNull(session.AccessToken); - Assert.AreEqual("Testing", session.User.UserMetadata["firstName"]); - } - - [TestMethod("Client: Signs Up the same user twice should throw BadRequestException")] - public async Task ClientSignsUpUserTwiceShouldReturnBadRequest() - { - var email = $"{RandomString(12)}@supabase.io"; - var result1 = await client.SignUp(email, password); - - Assert.IsNotNull(result1); - - await Assert.ThrowsExceptionAsync(async () => - { - await client.SignUp(email, password); - }); - } - - [TestMethod("Client: Triggers Token Refreshed Event")] - public async Task ClientTriggersTokenRefreshedEvent() - { - var tsc = new TaskCompletionSource(); - - var email = $"{RandomString(12)}@supabase.io"; - var user = await client.SignUp(email, password); - - client.StateChanged += (sender, args) => - { - if (args.State == AuthState.TokenRefreshed) - { - tsc.SetResult(client.CurrentSession.AccessToken); - } - }; - - await client.RefreshSession(); - - var newToken = await tsc.Task; - - Assert.AreNotEqual(user.RefreshToken, client.CurrentSession.RefreshToken); - } - - [TestMethod("Client: Signs In User (Email, Phone, Refresh token)")] - public async Task ClientSignsIn() - { - Session session = null; - string refreshToken = ""; - - // Emails - var email = $"{RandomString(12)}@supabase.io"; - await client.SignUp(email, password); - - await client.SignOut(); - - session = await client.SignIn(email, password); - - Assert.IsNotNull(session.AccessToken); - Assert.IsNotNull(session.RefreshToken); - Assert.IsInstanceOfType(session.User, typeof(User)); - - // Phones - var phone = GetRandomPhoneNumber(); - await client.SignUp(SignUpType.Phone, phone, password); - - await client.SignOut(); - - session = await client.SignIn(SignInType.Phone, phone, password); - - Assert.IsNotNull(session.AccessToken); - Assert.IsNotNull(session.RefreshToken); - Assert.IsInstanceOfType(session.User, typeof(User)); - - // Refresh Token - refreshToken = session.RefreshToken; - - var newSession = await client.SignIn(SignInType.RefreshToken, refreshToken); - - Assert.IsNotNull(newSession.AccessToken); - Assert.IsNotNull(newSession.RefreshToken); - Assert.IsInstanceOfType(newSession.User, typeof(User)); - } - - [TestMethod("Client: Sends Magic Login Email")] - public async Task ClientSendsMagicLoginEmail() - { - var user = $"{RandomString(12)}@supabase.io"; - await client.SignUp(user, password); - - await client.SignOut(); - - var result = await client.SignIn(user); - Assert.IsTrue(result); - } - - [TestMethod("Client: Sends Magic Login Email (Alias)")] - public async Task ClientSendsMagicLoginEmailAlias() - { - var user = $"{RandomString(12)}@supabase.io"; - var user2 = $"{RandomString(12)}@supabase.io"; - await client.SignUp(user, password); - - await client.SignOut(); - - var result = await client.SendMagicLink(user); - var result2 = await client.SendMagicLink(user2, new SignInOptions { RedirectTo = $"com.{RandomString(12)}.deeplink://login" }); - - Assert.IsTrue(result); - Assert.IsTrue(result2); - } - - - [TestMethod("Client: Returns Auth Url for Provider")] - public async Task ClientReturnsAuthUrlForProvider() - { - var result1 = await client.SignIn(Provider.Google); - Assert.AreEqual("http://localhost:9999/authorize?provider=google", result1.Uri.ToString()); - - var result2 = await client.SignIn(Provider.Google, new SignInOptions { Scopes = "special scopes please" }); - Assert.AreEqual("http://localhost:9999/authorize?provider=google&scopes=special+scopes+please", result2.Uri.ToString()); - } - - [TestMethod("Client: Returns Verification Code for Provider")] - public async Task ClientReturnsPKCEVerifier() - { - var result = await client.SignIn(Provider.Github, new SignInOptions { FlowType = OAuthFlowType.PKCE }); - - Assert.IsTrue(!string.IsNullOrEmpty(result.PKCEVerifier)); - Assert.IsTrue(result.Uri.Query.Contains("flow_type=pkce")); - Assert.IsTrue(result.Uri.Query.Contains("code_challenge=")); - Assert.IsTrue(result.Uri.Query.Contains("code_challenge_method=s256")); - Assert.IsTrue(result.Uri.Query.Contains("provider=github")); - } - - [TestMethod("Client: Update user")] - public async Task ClientUpdateUser() - { - var email = $"{RandomString(12)}@supabase.io"; - var session = await client.SignUp(email, password); - - var attributes = new UserAttributes { Data = new Dictionary { { "hello", "world" } } }; - var result = await client.Update(attributes); - Assert.AreEqual(email, client.CurrentUser.Email); - Assert.IsNotNull(client.CurrentUser.UserMetadata); - - await client.SignOut(); - var token = GenerateServiceRoleToken(); - var result2 = await client.UpdateUserById(token, session.User.Id, new AdminUserAttributes { UserMetadata = new Dictionary { { "hello", "updated" } } }); - - Assert.AreNotEqual(result.UserMetadata["hello"], result2.UserMetadata["hello"]); - } - - [TestMethod("Client: Returns current user")] - public async Task ClientGetUser() - { - var email = $"{RandomString(12)}@supabase.io"; - var newUser = await client.SignUp(email, password); - - Assert.AreEqual(email, client.CurrentUser.Email); - - var userByJWT = await client.GetUser(newUser.AccessToken); - Assert.AreEqual(email, userByJWT.Email); - } - - [TestMethod("Client: Nulls CurrentUser on SignOut")] - public async Task ClientGetUserAfterLogOut() - { - var user = $"{RandomString(12)}@supabase.io"; - await client.SignUp(user, password); - - await client.SignOut(); - - Assert.IsNull(client.CurrentUser); - } - - [TestMethod("Client: Throws Exception on Invalid Username and Password")] - public async Task ClientSignsInUserWrongPassword() - { - var user = $"{RandomString(12)}@supabase.io"; - await client.SignUp(user, password); - - await client.SignOut(); - - await Assert.ThrowsExceptionAsync(async () => - { - var result = await client.SignIn(user, password + "$"); - }); - } - - [TestMethod("Client: Sends Invite Email")] - public async Task ClientSendsInviteEmail() - { - var user = $"{RandomString(12)}@supabase.io"; - var service_role_key = GenerateServiceRoleToken(); - var result = await client.InviteUserByEmail(user, service_role_key); - Assert.IsTrue(result); - } - - [TestMethod("Client: Lists users")] - public async Task ClientListUsers() - { - var service_role_key = GenerateServiceRoleToken(); - var result = await client.ListUsers(service_role_key); - - Assert.IsTrue(result.Users.Count > 0); - } - - [TestMethod("Client: Lists users pagination")] - public async Task ClientListUsersPagination() - { - var service_role_key = GenerateServiceRoleToken(); - - var page1 = await client.ListUsers(service_role_key, page: 1, perPage: 1); - var page2 = await client.ListUsers(service_role_key, page: 2, perPage: 1); - - Assert.AreEqual(page1.Users.Count, 1); - Assert.AreEqual(page2.Users.Count, 1); - Assert.AreNotEqual(page1.Users[0].Id, page2.Users[0].Id); - } - - [TestMethod("Client: Lists users sort")] - public async Task ClientListUsersSort() - { - var service_role_key = GenerateServiceRoleToken(); - - var result1 = await client.ListUsers(service_role_key, sortBy: "created_at", sortOrder: SortOrder.Ascending); - var result2 = await client.ListUsers(service_role_key, sortBy: "created_at", sortOrder: SortOrder.Descending); - - Assert.AreNotEqual(result1.Users[0].Id, result2.Users[0].Id); - } - - [TestMethod("Client: Lists users filter")] - public async Task ClientListUsersFilter() - { - var service_role_key = GenerateServiceRoleToken(); - - var user = $"{RandomString(12)}@supabase.io"; - var result = await client.SignUp(user, password); - - var result1 = await client.ListUsers(service_role_key, filter: "@nonexistingrandomemailprovider.com"); - var result2 = await client.ListUsers(service_role_key, filter: "@supabase.io"); - - Assert.AreNotEqual(result2.Users.Count, 0); - Assert.AreEqual(result1.Users.Count, 0); - Assert.AreNotEqual(result1.Users.Count, result2.Users.Count); - } - - [TestMethod("Client: Get User by Id")] - public async Task ClientGetUserById() - { - var service_role_key = GenerateServiceRoleToken(); - var result = await client.ListUsers(service_role_key, page: 1, perPage: 1); - - var userResult = result.Users[0]; - var userByIdResult = await client.GetUserById(service_role_key, userResult.Id); - - Assert.AreEqual(userResult.Id, userByIdResult.Id); - Assert.AreEqual(userResult.Email, userByIdResult.Email); - } - - [TestMethod("Client: Create a user")] - public async Task ClientCreateUser() - { - var service_role_key = GenerateServiceRoleToken(); - var result = await client.CreateUser(service_role_key, $"{RandomString(12)}@supabase.io", password); - - Assert.IsNotNull(result); - - - var attributes = new AdminUserAttributes - { - UserMetadata = new Dictionary { { "firstName", "123" } }, - AppMetadata = new Dictionary { { "roles", new List { "editor", "publisher" } } } - }; - - var result2 = await client.CreateUser(service_role_key, $"{RandomString(12)}@supabase.io", password, attributes); - Assert.AreEqual("123", result2.UserMetadata["firstName"]); - - var result3 = await client.CreateUser(service_role_key, new AdminUserAttributes { Email = $"{RandomString(12)}@supabase.io", Password = password }); - Assert.IsNotNull(result3); - } - - - [TestMethod("Client: Update User by Id")] - public async Task ClientUpdateUserById() - { - var service_role_key = GenerateServiceRoleToken(); - var createdUser = await client.CreateUser(service_role_key, $"{RandomString(12)}@supabase.io", password); - - Assert.IsNotNull(createdUser); - - var updatedUser = await client.UpdateUserById(service_role_key, createdUser.Id, new AdminUserAttributes { Email = $"{RandomString(12)}@supabase.io" }); - - Assert.IsNotNull(updatedUser); - - Assert.AreEqual(createdUser.Id, updatedUser.Id); - Assert.AreNotEqual(createdUser.Email, updatedUser.Email); - } - - [TestMethod("Client: Deletes User")] - public async Task ClientDeletesUser() - { - var user = $"{RandomString(12)}@supabase.io"; - await client.SignUp(user, password); - var uid = client.CurrentUser.Id; - - var service_role_key = GenerateServiceRoleToken(); - var result = await client.DeleteUser(uid, service_role_key); - - Assert.IsTrue(result); - } - - [TestMethod("Client: Sends Reset Password Email")] - public async Task ClientSendsResetPasswordForEmail() - { - var email = $"{RandomString(12)}@supabase.io"; - await client.SignUp(email, password); - var result = await client.ResetPasswordForEmail(email); - Assert.IsTrue(result); - } - - [TestMethod("Nonce generation and verification")] - public void NonceGeneration() - { - string nonce = Helpers.GenerateNonce(); - Assert.IsNotNull(nonce); - Assert.AreEqual(128, nonce.Length); - - string pkceVerifier = Helpers.GeneratePKCENonceVerifier(nonce); - Assert.IsNotNull(pkceVerifier); - Assert.AreEqual(43, pkceVerifier.Length); - - string appleVerifier = Helpers.GenerateSHA256NonceFromRawNonce(nonce); - Assert.IsNotNull(appleVerifier); - Assert.AreEqual(64, appleVerifier.Length); - - const string helloNonce = "hello_world_nonce"; - - string helloPkceVerifier = Helpers.GeneratePKCENonceVerifier(helloNonce); - Assert.IsNotNull(helloPkceVerifier); - Assert.AreEqual("9TMmi4JOlYOQEP2Ha39WXj9pySILGnAfQsz-yXws0yE", helloPkceVerifier); - - string helloAppleVerifier = Helpers.GenerateSHA256NonceFromRawNonce(helloNonce); - Assert.IsNotNull(helloAppleVerifier); - Assert.AreEqual("f533268b824e95839010fd876b7f565e3f69c9220b1a701f42ccfec97c2cd321", helloAppleVerifier); - } - } -} diff --git a/GotrueTests/ServiceRoleTests.cs b/GotrueTests/ServiceRoleTests.cs new file mode 100644 index 0000000..b9ffb75 --- /dev/null +++ b/GotrueTests/ServiceRoleTests.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Supabase.Gotrue; +using static Supabase.Gotrue.Constants; +using static GotrueTests.TestUtils; + +namespace GotrueTests +{ + [TestClass] + [SuppressMessage("ReSharper", "PossibleNullReferenceException")] + public class ServiceRoleTests + { + private Client _client; + + private readonly string _serviceKey = GenerateServiceRoleToken(); + + [TestInitialize] + public void TestInitializer() + { + _client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true }); + _client.AddDebugListener(LogDebug); + } + + [TestMethod("Service Role: Send Invite Email")] + public async Task SendsInviteEmail() + { + var user = $"{RandomString(12)}@supabase.io"; + var result = await _client.InviteUserByEmail(user, _serviceKey); + Assert.IsTrue(result); + } + + [TestMethod("Service Role: List users")] + public async Task ListUsers() + { + var result = await _client.ListUsers(_serviceKey); + Assert.IsTrue(result.Users.Count > 0); + } + + [TestMethod("Service Role: List users by page")] + public async Task ListUsersPagination() + { + var page1 = await _client.ListUsers(_serviceKey, page: 1, perPage: 1); + var page2 = await _client.ListUsers(_serviceKey, page: 2, perPage: 1); + + Assert.AreEqual(page1.Users.Count, 1); + Assert.AreEqual(page2.Users.Count, 1); + Assert.AreNotEqual(page1.Users[0].Id, page2.Users[0].Id); + } + + [TestMethod("Service Role: Lists users sort")] + public async Task ListUsersSort() + { + var serviceRoleKey = GenerateServiceRoleToken(); + + var result1 = await _client.ListUsers(serviceRoleKey, sortBy: "created_at", sortOrder: SortOrder.Ascending); + var result2 = await _client.ListUsers(serviceRoleKey, sortBy: "created_at", sortOrder: SortOrder.Descending); + + Assert.AreNotEqual(result1.Users[0].Id, result2.Users[0].Id); + } + + [TestMethod("Service role: Lists users with filter")] + public async Task ListUsersFilter() + { + var user = $"{RandomString(12)}@supabase.io"; + var result = await _client.SignUp(user, PASSWORD); + Assert.IsNotNull(result); + + // ReSharper disable once StringLiteralTypo + var result1 = await _client.ListUsers(_serviceKey, filter: "@nonexistingrandomemailprovider.com"); + var result2 = await _client.ListUsers(_serviceKey, filter: "@supabase.io"); + + Assert.AreNotEqual(result2.Users.Count, 0); + Assert.AreEqual(result1.Users.Count, 0); + Assert.AreNotEqual(result1.Users.Count, result2.Users.Count); + } + + [TestMethod("Service Role: Get User by Id")] + public async Task GetUserById() + { + var result = await _client.ListUsers(_serviceKey, page: 1, perPage: 1); + + var userResult = result.Users[0]; + var userByIdResult = await _client.GetUserById(_serviceKey, userResult.Id ?? throw new InvalidOperationException()); + + Assert.AreEqual(userResult.Id, userByIdResult.Id); + Assert.AreEqual(userResult.Email, userByIdResult.Email); + } + + [TestMethod("Service Role: Create a user")] + public async Task CreateUser() + { + var result = await _client.CreateUser(_serviceKey, $"{RandomString(12)}@supabase.io", PASSWORD); + + Assert.IsNotNull(result); + + var attributes = new AdminUserAttributes + { + UserMetadata = new Dictionary { { "firstName", "123" } }, + AppMetadata = new Dictionary { { "roles", new List { "editor", "publisher" } } } + }; + + var result2 = await _client.CreateUser(_serviceKey, $"{RandomString(12)}@supabase.io", PASSWORD, attributes); + Assert.AreEqual("123", result2.UserMetadata["firstName"]); + + var result3 = await _client.CreateUser(_serviceKey, new AdminUserAttributes { Email = $"{RandomString(12)}@supabase.io", Password = PASSWORD }); + Assert.IsNotNull(result3); + } + + [TestMethod("Service Role: Update User by Id")] + public async Task UpdateUserById() + { + var createdUser = await _client.CreateUser(_serviceKey, $"{RandomString(12)}@supabase.io", PASSWORD); + + Assert.IsNotNull(createdUser); + + var updatedUser = await _client.UpdateUserById(_serviceKey, createdUser.Id ?? throw new InvalidOperationException(), new AdminUserAttributes { Email = $"{RandomString(12)}@supabase.io" }); + + Assert.IsNotNull(updatedUser); + + Assert.AreEqual(createdUser.Id, updatedUser.Id); + Assert.AreNotEqual(createdUser.Email, updatedUser.Email); + } + + [TestMethod("Service Role: Delete User")] + public async Task DeletesUser() + { + var user = $"{RandomString(12)}@supabase.io"; + await _client.SignUp(user, PASSWORD); + var uid = _client.CurrentUser.Id; + + var result = await _client.DeleteUser(uid ?? throw new InvalidOperationException(), _serviceKey); + + Assert.IsTrue(result); + } + + [TestMethod("Nonce generation and verification")] + public void NonceGeneration() + { + var nonce = Helpers.GenerateNonce(); + Assert.IsNotNull(nonce); + Assert.AreEqual(128, nonce.Length); + + var pkceVerifier = Helpers.GeneratePKCENonceVerifier(nonce); + Assert.IsNotNull(pkceVerifier); + Assert.AreEqual(43, pkceVerifier.Length); + + var appleVerifier = Helpers.GenerateSHA256NonceFromRawNonce(nonce); + Assert.IsNotNull(appleVerifier); + Assert.AreEqual(64, appleVerifier.Length); + + const string helloNonce = "hello_world_nonce"; + + var helloPkceVerifier = Helpers.GeneratePKCENonceVerifier(helloNonce); + Assert.IsNotNull(helloPkceVerifier); + // ReSharper disable once StringLiteralTypo + Assert.AreEqual("9TMmi4JOlYOQEP2Ha39WXj9pySILGnAfQsz-yXws0yE", helloPkceVerifier); + + var helloAppleVerifier = Helpers.GenerateSHA256NonceFromRawNonce(helloNonce); + Assert.IsNotNull(helloAppleVerifier); + Assert.AreEqual("f533268b824e95839010fd876b7f565e3f69c9220b1a701f42ccfec97c2cd321", helloAppleVerifier); + } + } +} diff --git a/GotrueTests/StatelessClientTests.cs b/GotrueTests/StatelessClientTests.cs index 4e27366..54d6ca2 100644 --- a/GotrueTests/StatelessClientTests.cs +++ b/GotrueTests/StatelessClientTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Text; @@ -7,29 +8,31 @@ using Microsoft.IdentityModel.Tokens; using Microsoft.VisualStudio.TestTools.UnitTesting; using Supabase.Gotrue; +using Supabase.Gotrue.Exceptions; using static Supabase.Gotrue.StatelessClient; using static Supabase.Gotrue.Constants; -using Supabase.Gotrue.Exceptions; namespace GotrueTests { [TestClass] + [SuppressMessage("ReSharper", "PossibleNullReferenceException")] + [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute")] public class StatelessClientTests { - private string password = "I@M@SuperP@ssWord"; + private const string PASSWORD = "I@M@SuperP@ssWord"; - private static Random random = new Random(); + private static readonly Random Random = new Random(); private static string RandomString(int length) { const string chars = "abcdefghijklmnopqrstuvwxyz0123456789"; - return new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray()); + return new string(Enumerable.Repeat(chars, length).Select(s => s[Random.Next(s.Length)]).ToArray()); } private static string GetRandomPhoneNumber() { const string chars = "123456789"; - var inner = new string(Enumerable.Repeat(chars, 10).Select(s => s[random.Next(s.Length)]).ToArray()); + var inner = new string(Enumerable.Repeat(chars, 10).Select(s => s[Random.Next(s.Length)]).ToArray()); return $"+1{inner}"; } @@ -56,24 +59,23 @@ private string GenerateServiceRoleToken() return tokenHandler.WriteToken(securityToken); } - private StatelessClient client; + private StatelessClient _client; [TestInitialize] public void TestInitializer() { - client = new StatelessClient(); + _client = new StatelessClient(); } - StatelessClientOptions options => new StatelessClientOptions() { AllowUnconfirmedUserSessions = true }; + private static StatelessClientOptions Options { get => new StatelessClientOptions() { AllowUnconfirmedUserSessions = true }; } [TestMethod("StatelessClient: Signs Up User")] public async Task SignsUpUser() { - Session session = null; var email = $"{RandomString(12)}@supabase.io"; - session = await client.SignUp(email, password, options); + var session = await _client.SignUp(email, PASSWORD, Options); Assert.IsNotNull(session.AccessToken); Assert.IsNotNull(session.RefreshToken); @@ -81,7 +83,7 @@ public async Task SignsUpUser() var phone1 = GetRandomPhoneNumber(); - session = await client.SignUp(SignUpType.Phone, phone1, password, options, new SignUpOptions { Data = new Dictionary { { "firstName", "Testing" } } }); + session = await _client.SignUp(SignUpType.Phone, phone1, PASSWORD, Options, new SignUpOptions { Data = new Dictionary { { "firstName", "Testing" } } }); Assert.IsNotNull(session.AccessToken); Assert.AreEqual("Testing", session.User.UserMetadata["firstName"]); @@ -91,26 +93,23 @@ public async Task SignsUpUser() public async Task SignsUpUserTwiceShouldThrowBadRequest() { var email = $"{RandomString(12)}@supabase.io"; - var result1 = await client.SignUp(email, password, options); + var result1 = await _client.SignUp(email, PASSWORD, Options); + Assert.IsNotNull(result1); - await Assert.ThrowsExceptionAsync(async () => + await Assert.ThrowsExceptionAsync(async () => { - await client.SignUp(email, password, options); + await _client.SignUp(email, PASSWORD, Options); }); } [TestMethod("StatelessClient: Signs In User (Email, Phone, Refresh token)")] public async Task SignsIn() { - Session session = null; - string refreshToken = ""; - // Emails var email = $"{RandomString(12)}@supabase.io"; - await client.SignUp(email, password, options); + await _client.SignUp(email, PASSWORD, Options); - - session = await client.SignIn(email, password, options); + var session = await _client.SignIn(email, PASSWORD, Options); Assert.IsNotNull(session.AccessToken); Assert.IsNotNull(session.RefreshToken); @@ -118,19 +117,19 @@ public async Task SignsIn() // Phones var phone = GetRandomPhoneNumber(); - await client.SignUp(SignUpType.Phone, phone, password, options); + await _client.SignUp(SignUpType.Phone, phone, PASSWORD, Options); - session = await client.SignIn(SignInType.Phone, phone, password, options); + session = await _client.SignIn(SignInType.Phone, phone, PASSWORD, Options); Assert.IsNotNull(session.AccessToken); Assert.IsNotNull(session.RefreshToken); Assert.IsInstanceOfType(session.User, typeof(User)); // Refresh Token - refreshToken = session.RefreshToken; + var refreshToken = session.RefreshToken; - var newSession = await client.SignIn(SignInType.RefreshToken, refreshToken, options: options); + var newSession = await _client.SignIn(SignInType.RefreshToken, refreshToken, options: Options); Assert.IsNotNull(newSession.AccessToken); Assert.IsNotNull(newSession.RefreshToken); @@ -140,19 +139,16 @@ public async Task SignsIn() [TestMethod("StatelessClient: Signs Out User (Email)")] public async Task SignsOut() { - Session session = null; - // Emails var email = $"{RandomString(12)}@supabase.io"; - await client.SignUp(email, password, options); - + await _client.SignUp(email, PASSWORD, Options); - session = await client.SignIn(email, password, options); + var session = await _client.SignIn(email, PASSWORD, Options); Assert.IsNotNull(session.AccessToken); Assert.IsInstanceOfType(session.User, typeof(User)); - var result = await client.SignOut(session.AccessToken, options); + var result = await _client.SignOut(session.AccessToken, Options); Assert.IsTrue(result); } @@ -161,13 +157,13 @@ public async Task SignsOut() public async Task SendsMagicLoginEmail() { var user1 = $"{RandomString(12)}@supabase.io"; - await client.SignUp(user1, password, options); + await _client.SignUp(user1, PASSWORD, Options); - var result1 = await client.SignIn(user1, options); + var result1 = await _client.SignIn(user1, Options); Assert.IsTrue(result1); var user2 = $"{RandomString(12)}@supabase.io"; - var result2 = await client.SignIn(user2, options, new SignInOptions { RedirectTo = $"com.{RandomString(12)}.deeplink://login" }); + var result2 = await _client.SignIn(user2, Options, new SignInOptions { RedirectTo = $"com.{RandomString(12)}.deeplink://login" }); Assert.IsTrue(result2); } @@ -175,23 +171,23 @@ public async Task SendsMagicLoginEmail() public async Task SendsMagicLoginEmailAlias() { var user1 = $"{RandomString(12)}@supabase.io"; - await client.SignUp(user1, password, options); + await _client.SignUp(user1, PASSWORD, Options); - var result1 = await client.SignIn(user1, options); + var result1 = await _client.SignIn(user1, Options); Assert.IsTrue(result1); var user2 = $"{RandomString(12)}@supabase.io"; - var result2 = await client.SignIn(user2, options, new SignInOptions { RedirectTo = $"com.{RandomString(12)}.deeplink://login" }); + var result2 = await _client.SignIn(user2, Options, new SignInOptions { RedirectTo = $"com.{RandomString(12)}.deeplink://login" }); Assert.IsTrue(result2); } [TestMethod("StatelessClient: Returns Auth Url for Provider")] public void ReturnsAuthUrlForProvider() { - var result1 = client.SignIn(Provider.Google, options); + var result1 = _client.SignIn(Provider.Google, Options); Assert.AreEqual("http://localhost:9999/authorize?provider=google", result1.Uri.ToString()); - var result2 = client.SignIn(Provider.Google, options, new SignInOptions { Scopes = "special scopes please" }); + var result2 = _client.SignIn(Provider.Google, Options, new SignInOptions { Scopes = "special scopes please" }); Assert.AreEqual("http://localhost:9999/authorize?provider=google&scopes=special+scopes+please", result2.Uri.ToString()); } @@ -199,7 +195,7 @@ public void ReturnsAuthUrlForProvider() public async Task UpdateUser() { var user = $"{RandomString(12)}@supabase.io"; - var session = await client.SignUp(user, password, options); + var session = await _client.SignUp(user, PASSWORD, Options); var attributes = new UserAttributes { @@ -208,7 +204,7 @@ public async Task UpdateUser() { "hello", "world" } } }; - var updateResult = await client.Update(session.AccessToken, attributes, options); + var updateResult = await _client.Update(session.AccessToken, attributes, Options); Assert.AreEqual(user, updateResult.Email); Assert.IsNotNull(updateResult.UserMetadata); } @@ -217,7 +213,7 @@ public async Task UpdateUser() public async Task GetUser() { var user = $"{RandomString(12)}@supabase.io"; - var session = await client.SignUp(user, password, options); + var session = await _client.SignUp(user, PASSWORD, Options); Assert.AreEqual(user, session.User.Email); } @@ -226,11 +222,11 @@ public async Task GetUser() public async Task SignsInUserWrongPassword() { var user = $"{RandomString(12)}@supabase.io"; - await client.SignUp(user, password, options); + await _client.SignUp(user, PASSWORD, Options); - await Assert.ThrowsExceptionAsync(async () => + await Assert.ThrowsExceptionAsync(async () => { - var result = await client.SignIn(user, password + "$", options); + await _client.SignIn(user, PASSWORD + "$", Options); }); } @@ -238,8 +234,8 @@ await Assert.ThrowsExceptionAsync(async () => public async Task SendsInviteEmail() { var user = $"{RandomString(12)}@supabase.io"; - var service_role_key = GenerateServiceRoleToken(); - var result = await client.InviteUserByEmail(user, service_role_key, options); + var serviceRoleKey = GenerateServiceRoleToken(); + var result = await _client.InviteUserByEmail(user, serviceRoleKey, Options); Assert.IsTrue(result); } @@ -247,11 +243,11 @@ public async Task SendsInviteEmail() public async Task DeletesUser() { var user = $"{RandomString(12)}@supabase.io"; - var session = await client.SignUp(user, password, options); + var session = await _client.SignUp(user, PASSWORD, Options); var uid = session.User.Id; - var service_role_key = GenerateServiceRoleToken(); - var result = await client.DeleteUser(uid, service_role_key, options); + var serviceRoleKey = GenerateServiceRoleToken(); + var result = await _client.DeleteUser(uid, serviceRoleKey, Options); Assert.IsTrue(result); } @@ -260,16 +256,16 @@ public async Task DeletesUser() public async Task ClientSendsResetPasswordForEmail() { var email = $"{RandomString(12)}@supabase.io"; - await client.SignUp(email, password, options); - var result = await client.ResetPasswordForEmail(email, options); + await _client.SignUp(email, PASSWORD, Options); + var result = await _client.ResetPasswordForEmail(email, Options); Assert.IsTrue(result); } [TestMethod("Client: Lists users")] public async Task ClientListUsers() { - var service_role_key = GenerateServiceRoleToken(); - var result = await client.ListUsers(service_role_key, options); + var serviceRoleKey = GenerateServiceRoleToken(); + var result = await _client.ListUsers(serviceRoleKey, Options); Assert.IsTrue(result.Users.Count > 0); } @@ -277,10 +273,10 @@ public async Task ClientListUsers() [TestMethod("Client: Lists users pagination")] public async Task ClientListUsersPagination() { - var service_role_key = GenerateServiceRoleToken(); + var serviceRoleKey = GenerateServiceRoleToken(); - var page1 = await client.ListUsers(service_role_key, options, page: 1, perPage: 1); - var page2 = await client.ListUsers(service_role_key, options, page: 2, perPage: 1); + var page1 = await _client.ListUsers(serviceRoleKey, Options, page: 1, perPage: 1); + var page2 = await _client.ListUsers(serviceRoleKey, Options, page: 2, perPage: 1); Assert.AreEqual(page1.Users.Count, 1); Assert.AreEqual(page2.Users.Count, 1); @@ -290,10 +286,10 @@ public async Task ClientListUsersPagination() [TestMethod("Client: Lists users sort")] public async Task ClientListUsersSort() { - var service_role_key = GenerateServiceRoleToken(); + var serviceRoleKey = GenerateServiceRoleToken(); - var result1 = await client.ListUsers(service_role_key, options, sortBy: "created_at", sortOrder: SortOrder.Descending); - var result2 = await client.ListUsers(service_role_key, options, sortBy: "created_at", sortOrder: SortOrder.Ascending); + var result1 = await _client.ListUsers(serviceRoleKey, Options, sortBy: "created_at", sortOrder: SortOrder.Descending); + var result2 = await _client.ListUsers(serviceRoleKey, Options, sortBy: "created_at", sortOrder: SortOrder.Ascending); Assert.AreNotEqual(result1.Users[0].Id, result2.Users[0].Id); } @@ -301,10 +297,11 @@ public async Task ClientListUsersSort() [TestMethod("Client: Lists users filter")] public async Task ClientListUsersFilter() { - var service_role_key = GenerateServiceRoleToken(); + var serviceRoleKey = GenerateServiceRoleToken(); - var result1 = await client.ListUsers(service_role_key, options, filter: "@nonexistingrandomemailprovider.com"); - var result2 = await client.ListUsers(service_role_key, options, filter: "@supabase.io"); + // https://example.com/ is assigned for use for docs & testing + var result1 = await _client.ListUsers(serviceRoleKey, Options, filter: "@example.com"); + var result2 = await _client.ListUsers(serviceRoleKey, Options, filter: "@supabase.io"); Assert.AreNotEqual(result2.Users.Count, 0); Assert.AreEqual(result1.Users.Count, 0); @@ -314,11 +311,11 @@ public async Task ClientListUsersFilter() [TestMethod("Client: Get User by Id")] public async Task ClientGetUserById() { - var service_role_key = GenerateServiceRoleToken(); - var result = await client.ListUsers(service_role_key, options, page: 1, perPage: 1); + var serviceRoleKey = GenerateServiceRoleToken(); + var result = await _client.ListUsers(serviceRoleKey, Options, page: 1, perPage: 1); var userResult = result.Users[0]; - var userByIdResult = await client.GetUserById(service_role_key, options, userResult.Id); + var userByIdResult = await _client.GetUserById(serviceRoleKey, Options, userResult.Id); Assert.AreEqual(userResult.Id, userByIdResult.Id); Assert.AreEqual(userResult.Email, userByIdResult.Email); @@ -327,8 +324,8 @@ public async Task ClientGetUserById() [TestMethod("Client: Create a user")] public async Task ClientCreateUser() { - var service_role_key = GenerateServiceRoleToken(); - var result = await client.CreateUser(service_role_key, options, $"{RandomString(12)}@supabase.io", password); + var serviceRoleKey = GenerateServiceRoleToken(); + var result = await _client.CreateUser(serviceRoleKey, Options, $"{RandomString(12)}@supabase.io", PASSWORD); Assert.IsNotNull(result); @@ -337,22 +334,22 @@ public async Task ClientCreateUser() UserMetadata = new Dictionary { { "firstName", "123" } }, }; - var result2 = await client.CreateUser(service_role_key, options, $"{RandomString(12)}@supabase.io", password, attributes); + var result2 = await _client.CreateUser(serviceRoleKey, Options, $"{RandomString(12)}@supabase.io", PASSWORD, attributes); Assert.AreEqual("123", result2.UserMetadata["firstName"]); - var result3 = await client.CreateUser(service_role_key, options, new AdminUserAttributes { Email = $"{RandomString(12)}@supabase.io", Password = password }); + var result3 = await _client.CreateUser(serviceRoleKey, Options, new AdminUserAttributes { Email = $"{RandomString(12)}@supabase.io", Password = PASSWORD }); Assert.IsNotNull(result3); } [TestMethod("Client: Update User by Id")] public async Task ClientUpdateUserById() { - var service_role_key = GenerateServiceRoleToken(); - var createdUser = await client.CreateUser(service_role_key, options, $"{RandomString(12)}@supabase.io", password); + var serviceRoleKey = GenerateServiceRoleToken(); + var createdUser = await _client.CreateUser(serviceRoleKey, Options, $"{RandomString(12)}@supabase.io", PASSWORD); Assert.IsNotNull(createdUser); - var updatedUser = await client.UpdateUserById(service_role_key, options, createdUser.Id, new AdminUserAttributes { Email = $"{RandomString(12)}@supabase.io" }); + var updatedUser = await _client.UpdateUserById(serviceRoleKey, Options, createdUser.Id, new AdminUserAttributes { Email = $"{RandomString(12)}@supabase.io" }); Assert.IsNotNull(updatedUser); diff --git a/GotrueTests/TestUtils.cs b/GotrueTests/TestUtils.cs new file mode 100644 index 0000000..a8b2fdd --- /dev/null +++ b/GotrueTests/TestUtils.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Text; +using Microsoft.IdentityModel.Tokens; + +namespace GotrueTests +{ + public static class TestUtils + { + private static readonly Random Random = new Random(); + + public const string PASSWORD = "I@M@SuperP@ssWord"; + + + public static void LogDebug(string message, Exception e) + { + Debug.WriteLine(message); + if (e != null) + Debug.WriteLine(e); + } + + + public static string RandomString(int length) + { + const string chars = "abcdefghijklmnopqrstuvwxyz0123456789"; + return new string(Enumerable.Repeat(chars, length).Select(s => s[Random.Next(s.Length)]).ToArray()); + } + + public static string GetRandomPhoneNumber() + { + const string chars = "123456789"; + var inner = new string(Enumerable.Repeat(chars, 10).Select(s => s[Random.Next(s.Length)]).ToArray()); + return $"+1{inner}"; + } + + public static string GenerateServiceRoleToken() + { + var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("37c304f8-51aa-419a-a1af-06154e63707a")); // using GOTRUE_JWT_SECRET + + var tokenDescriptor = new SecurityTokenDescriptor + { + IssuedAt = DateTime.Now, + Expires = DateTime.UtcNow.AddDays(7), + SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256Signature), + Claims = new Dictionary() { { "role", "service_role" } } + }; + + var tokenHandler = new JwtSecurityTokenHandler(); + var securityToken = tokenHandler.CreateToken(tokenDescriptor); + return tokenHandler.WriteToken(securityToken); + } + + } +} diff --git a/gotrue-csharp.sln.DotSettings b/gotrue-csharp.sln.DotSettings index 0510fb7..5298f15 100644 --- a/gotrue-csharp.sln.DotSettings +++ b/gotrue-csharp.sln.DotSettings @@ -42,7 +42,9 @@ UseVar UseVar UseVar + SHA True True + True True True \ No newline at end of file From 643382cac75193c7c48a6db972086f9371185c81 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 19:29:15 -0700 Subject: [PATCH 28/74] Updates/fixes for persistence and notifications --- Gotrue/Client.cs | 117 ++++++++++++++--------------- Gotrue/Interfaces/IGotrueClient.cs | 8 +- Gotrue/PersistenceListener.cs | 8 +- GotrueTests/AnonKeyClientTests.cs | 75 ++++++++++++++---- GotrueTests/ServiceRoleTests.cs | 26 ++++++- 5 files changed, 146 insertions(+), 88 deletions(-) diff --git a/Gotrue/Client.cs b/Gotrue/Client.cs index 53c9918..2f159b4 100644 --- a/Gotrue/Client.cs +++ b/Gotrue/Client.cs @@ -7,6 +7,7 @@ using System.Web; using Supabase.Gotrue.Interfaces; using static Supabase.Gotrue.Constants; +using static Supabase.Gotrue.Constants.AuthState; namespace Supabase.Gotrue { @@ -36,7 +37,6 @@ public void AddDebugListener(Action listener) /// /// Headers specified in the client options will ALWAYS take precedence over headers returned by this function. /// - public Func>? GetHeaders { get => _getHeaders; @@ -181,12 +181,10 @@ public Client(ClientOptions? options = null) _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) }; - if (session?.User?.ConfirmedAt != null || (session?.User != null && Options.AllowUnconfirmedUserSessions)) + if (session?.User?.ConfirmedAt != null || session?.User != null && Options.AllowUnconfirmedUserSessions) { - await PersistSession(session); - - NotifyStateChange(AuthState.SignedIn); - + UpdateSession(session); + NotifyStateChange(SignedIn); return CurrentSession; } @@ -200,9 +198,8 @@ public Client(ClientOptions? options = null) /// /// /// - public async Task SignIn(string email, SignInOptions? options = null) + public async Task SendMagicLinkEmail(string email, SignInOptions? options = null) { - DestroySession(); await _api.SendMagicLinkEmail(email, options); return true; } @@ -222,11 +219,11 @@ public async Task SignIn(string email, SignInOptions? options = null) /// public async Task SignInWithIdToken(Provider provider, string idToken, string? nonce = null, string? captchaToken = null) { - DestroySession(); + NotifyStateChange(SignedOut); var result = await _api.SignInWithIdToken(provider, idToken, nonce, captchaToken); if (result != null) - await PersistSession(result); + NotifyStateChange(SignedIn); return result; } @@ -250,7 +247,7 @@ public async Task SignIn(string email, SignInOptions? options = null) /// public async Task SignInWithOtp(SignInWithPasswordlessEmailOptions options) { - DestroySession(); + NotifyStateChange(SignedOut); return await _api.SignInWithOtp(options); } @@ -273,7 +270,7 @@ public async Task SignInWithOtp(SignInWithPasswordlessE /// public async Task SignInWithOtp(SignInWithPasswordlessPhoneOptions options) { - DestroySession(); + NotifyStateChange(SignedOut); return await _api.SignInWithOtp(options); } @@ -283,8 +280,7 @@ public async Task SignInWithOtp(SignInWithPasswordlessP /// /// /// - public Task SendMagicLink(string email, SignInOptions? options = null) => SignIn(email, options); - + public Task SendMagicLink(string email, SignInOptions? options = null) => SendMagicLinkEmail(email, options); /// /// Signs in a User. @@ -292,7 +288,7 @@ public async Task SignInWithOtp(SignInWithPasswordlessP /// /// /// - public Task SignIn(string email, string password) => SignIn(SignInType.Email, email, password); + public Task SendMagicLinkEmail(string email, string password) => SendMagicLinkEmail(SignInType.Email, email, password); /// /// Log in an existing user with an email and password or phone and password. @@ -300,7 +296,7 @@ public async Task SignInWithOtp(SignInWithPasswordlessP /// /// /// - public Task SignInWithPassword(string email, string password) => SignIn(email, password); + public Task SignInWithPassword(string email, string password) => SendMagicLinkEmail(email, password); /// /// Log in an existing user, or login via a third-party provider. @@ -310,15 +306,14 @@ public async Task SignInWithOtp(SignInWithPasswordlessP /// Password to account (optional if `RefreshToken`) /// A space-separated list of scopes granted to the OAuth application. /// - public async Task SignIn(SignInType type, string identifierOrToken, string? password = null, string? scopes = null) + public async Task SendMagicLinkEmail(SignInType type, string identifierOrToken, string? password = null, string? scopes = null) { - DestroySession(); - - Session? session = null; + Session? session; switch (type) { case SignInType.Email: session = await _api.SignInWithEmail(identifierOrToken, password!); + UpdateSession(session); break; case SignInType.Phone: if (string.IsNullOrEmpty(password)) @@ -328,20 +323,19 @@ public async Task SignInWithOtp(SignInWithPasswordlessP } session = await _api.SignInWithPhone(identifierOrToken, password!); + UpdateSession(session); break; case SignInType.RefreshToken: CurrentSession = new Session(); CurrentSession.RefreshToken = identifierOrToken; - await RefreshToken(); - return CurrentSession; + default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } if (session?.User?.ConfirmedAt != null || (session?.User != null && Options.AllowUnconfirmedUserSessions)) { - await PersistSession(session); - NotifyStateChange(AuthState.SignedIn); + NotifyStateChange(SignedIn); return CurrentSession; } @@ -357,7 +351,7 @@ public async Task SignInWithOtp(SignInWithPasswordlessP /// /// /// - public Task SignIn(Provider provider, SignInOptions? options = null) + public Task SendMagicLinkEmail(Provider provider, SignInOptions? options = null) { DestroySession(); @@ -380,8 +374,8 @@ public Task SignIn(Provider provider, SignInOptions? options if (session?.AccessToken != null) { - await PersistSession(session); - NotifyStateChange(AuthState.SignedIn); + UpdateSession(session); + NotifyStateChange(SignedIn); return session; } @@ -403,8 +397,8 @@ public Task SignIn(Provider provider, SignInOptions? options if (session?.AccessToken != null) { - await PersistSession(session); - NotifyStateChange(AuthState.SignedIn); + UpdateSession(session); + NotifyStateChange(SignedIn); return session; } @@ -417,17 +411,11 @@ public Task SignIn(Provider provider, SignInOptions? options /// public async Task SignOut() { - if (CurrentSession != null) - { - if (CurrentSession.AccessToken != null) - await _api.SignOut(CurrentSession.AccessToken); - - _refreshTimer?.Dispose(); - - DestroySession(); - - NotifyStateChange(AuthState.SignedOut); - } + if (CurrentSession?.AccessToken != null) + await _api.SignOut(CurrentSession.AccessToken); + _refreshTimer?.Dispose(); + UpdateSession(null); + NotifyStateChange(SignedOut); } /// @@ -442,7 +430,7 @@ public async Task SignOut() var result = await _api.UpdateUser(CurrentSession.AccessToken!, attributes); CurrentUser = result; - NotifyStateChange(AuthState.UserUpdated); + NotifyStateChange(UserUpdated); return result; } @@ -596,7 +584,7 @@ public Session SetAuth(string accessToken) CurrentSession.TokenType = "bearer"; CurrentSession.User = CurrentUser; - NotifyStateChange(AuthState.TokenRefreshed); + NotifyStateChange(TokenRefreshed); return CurrentSession; } @@ -648,11 +636,11 @@ public Session SetAuth(string accessToken) if (storeSession) { - await PersistSession(session); - NotifyStateChange(AuthState.SignedIn); + UpdateSession(session); + NotifyStateChange(SignedIn); if (query.Get("type") == "recovery") - NotifyStateChange(AuthState.PasswordRecovery); + NotifyStateChange(PasswordRecovery); } return session; @@ -701,7 +689,7 @@ public Session SetAuth(string accessToken) CurrentSession = session; CurrentUser = session.User; - NotifyStateChange(AuthState.SignedIn); + NotifyStateChange(SignedIn); InitRefreshTimer(); @@ -720,8 +708,8 @@ public Session SetAuth(string accessToken) if (result != null) { - await PersistSession(result); - NotifyStateChange(AuthState.SignedIn); + UpdateSession(result); + NotifyStateChange(SignedIn); return CurrentSession; } @@ -729,13 +717,21 @@ public Session SetAuth(string accessToken) } /// - /// Persists a Session in memory and calls (if specified) - /// ClientOptions.SessionPersistor - /// + /// Saves the session /// /// - private Task PersistSession(Session session) + private void UpdateSession(Session? session) { + if (session == null) + { + CurrentSession = null; + CurrentUser = null; + NotifyStateChange(SignedOut); + return; + } + + var dirty = CurrentSession != session; + CurrentSession = session; CurrentUser = session.User; @@ -743,20 +739,17 @@ private Task PersistSession(Session session) if (Options.AutoRefreshToken && expiration != default) InitRefreshTimer(); - return Task.CompletedTask; + + if (dirty) + NotifyStateChange(UserUpdated); } /// - /// Persists a Session in memory and calls (if specified) - /// ClientOptions.SessionDestroyer - /// + /// Clears the session /// private void DestroySession() { - CurrentSession = null; - CurrentUser = null; - - NotifyStateChange(AuthState.SignedOut); + UpdateSession(null); } /// @@ -778,7 +771,7 @@ private async Task RefreshToken(string? refreshToken = null) CurrentSession = result; CurrentUser = result.User; - NotifyStateChange(AuthState.TokenRefreshed); + NotifyStateChange(TokenRefreshed); if (Options.AutoRefreshToken && CurrentSession.ExpiresIn != default) InitRefreshTimer(); @@ -824,7 +817,7 @@ private async void HandleRefreshTimerTick(object _) catch (Exception ex) { _debugNotification?.Log(ex.Message, ex); - NotifyStateChange(AuthState.SignedOut); + NotifyStateChange(SignedOut); } } } diff --git a/Gotrue/Interfaces/IGotrueClient.cs b/Gotrue/Interfaces/IGotrueClient.cs index 256d8b9..ee25897 100644 --- a/Gotrue/Interfaces/IGotrueClient.cs +++ b/Gotrue/Interfaces/IGotrueClient.cs @@ -33,13 +33,13 @@ public interface IGotrueClient : IGettableHeaders Task RetrieveSessionAsync(); Task SendMagicLink(string email, SignInOptions? options = null); TSession SetAuth(string accessToken); - Task SignIn(SignInType type, string identifierOrToken, string? password = null, string? scopes = null); - Task SignIn(string email, SignInOptions? options = null); - Task SignIn(string email, string password); + Task SendMagicLinkEmail(SignInType type, string identifierOrToken, string? password = null, string? scopes = null); + Task SendMagicLinkEmail(string email, SignInOptions? options = null); + Task SendMagicLinkEmail(string email, string password); Task SignInWithOtp(SignInWithPasswordlessEmailOptions options); Task SignInWithOtp(SignInWithPasswordlessPhoneOptions options); Task SignInWithPassword(string email, string password); - Task SignIn(Provider provider, SignInOptions? options = null); + Task SendMagicLinkEmail(Provider provider, SignInOptions? options = null); Task SignInWithIdToken(Provider provider, string idToken, string? nonce = null, string? captchaToken = null); Task ExchangeCodeForSession(string codeVerifier, string authCode); Task SignUp(SignUpType type, string identifier, string password, SignUpOptions? options = null); diff --git a/Gotrue/PersistenceListener.cs b/Gotrue/PersistenceListener.cs index 05df261..9d5161f 100644 --- a/Gotrue/PersistenceListener.cs +++ b/Gotrue/PersistenceListener.cs @@ -7,8 +7,8 @@ namespace Supabase.Gotrue public class PersistenceListener { private readonly SaveSession? _save; - private readonly DestroySession? _load; - private readonly LoadSession? _destroy; + private readonly LoadSession? _load; + private readonly DestroySession? _destroy; public delegate bool SaveSession(Session session); @@ -19,8 +19,8 @@ public class PersistenceListener public PersistenceListener(SaveSession? s, DestroySession? d, LoadSession? l) { _save = s; - _load = d; - _destroy = l; + _load = l; + _destroy = d; } public void EventHandler(IGotrueClient sender, Constants.AuthState stateChanged) { diff --git a/GotrueTests/AnonKeyClientTests.cs b/GotrueTests/AnonKeyClientTests.cs index 2965903..727acea 100644 --- a/GotrueTests/AnonKeyClientTests.cs +++ b/GotrueTests/AnonKeyClientTests.cs @@ -34,14 +34,31 @@ private bool AuthStateIsEmpty() [TestInitialize] public void TestInitializer() { - _client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true }); + _client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true, PersistSession = true, SessionPersistor = SaveSession, SessionRetriever = LoadSession, SessionDestroyer = DestroySession }); _client.AddDebugListener(LogDebug); _client.AddStateChangedListener(AuthStateListener); } + private void DestroySession() + { + _savedSession = null; + } + + private Session LoadSession() + { + return _savedSession; + } + + private bool SaveSession(Session session) + { + _savedSession = session; + return true; + } + private Client _client; private readonly List _stateChanges = new List(); + private Session _savedSession; [TestMethod("Client: Sign Up User")] public async Task SignUpUserEmail() @@ -52,6 +69,7 @@ public async Task SignUpUserEmail() var session = await _client.SignUp(email, PASSWORD); Contains(_stateChanges, SignedIn); + AreEqual(_client.CurrentSession, _savedSession); IsNotNull(session.AccessToken); IsNotNull(session.RefreshToken); @@ -67,6 +85,7 @@ public async Task SignUpUserPhone() var session = await _client.SignUp(Constants.SignUpType.Phone, phone1, PASSWORD, new SignUpOptions { Data = new Dictionary { { "firstName", "Testing" } } }); Contains(_stateChanges, SignedIn); + AreEqual(_client.CurrentSession, _savedSession); IsNotNull(session.AccessToken); AreEqual("Testing", session.User.UserMetadata["firstName"]); @@ -81,6 +100,7 @@ public async Task SignsUpUserTwiceShouldReturnBadRequest() IsNotNull(result1); Contains(_stateChanges, SignedIn); + AreEqual(_client.CurrentSession, _savedSession); _stateChanges.Clear(); await ThrowsExceptionAsync(async () => @@ -90,6 +110,7 @@ await ThrowsExceptionAsync(async () => }); Contains(_stateChanges, SignedOut); + AreEqual(null, _savedSession); } [TestMethod("Client: Triggers Token Refreshed Event")] @@ -104,6 +125,7 @@ public async Task ClientTriggersTokenRefreshedEvent() var user = await _client.SignUp(email, PASSWORD); Contains(_stateChanges, SignedIn); + AreEqual(_client.CurrentSession, _savedSession); _client.AddStateChangedListener((_, args) => { @@ -117,10 +139,10 @@ public async Task ClientTriggersTokenRefreshedEvent() await _client.RefreshSession(); Contains(_stateChanges, TokenRefreshed); + AreEqual(_client.CurrentSession, _savedSession); var newToken = await tsc.Task; IsNotNull(newToken); - AreNotEqual(user.RefreshToken, _client.CurrentSession.RefreshToken); } @@ -130,33 +152,40 @@ public async Task ClientSignsIn() var email = $"{RandomString(12)}@supabase.io"; await _client.SignUp(email, PASSWORD); Contains(_stateChanges, SignedIn); + AreEqual(_client.CurrentSession, _savedSession); _stateChanges.Clear(); await _client.SignOut(); Contains(_stateChanges, SignedOut); + AreEqual(_client.CurrentSession, _savedSession); _stateChanges.Clear(); - var session = await _client.SignIn(email, PASSWORD); + var session = await _client.SendMagicLinkEmail(email, PASSWORD); IsNotNull(session.AccessToken); IsNotNull(session.RefreshToken); IsNotNull(session.User); Contains(_stateChanges, SignedIn); + AreEqual(_client.CurrentSession, _savedSession); _stateChanges.Clear(); // Phones var phone = GetRandomPhoneNumber(); await _client.SignUp(Constants.SignUpType.Phone, phone, PASSWORD); Contains(_stateChanges, SignedIn); + AreEqual(_client.CurrentSession, _savedSession); _stateChanges.Clear(); await _client.SignOut(); Contains(_stateChanges, SignedOut); + IsNull(_savedSession); + AreEqual(_client.CurrentSession, _savedSession); _stateChanges.Clear(); - session = await _client.SignIn(Constants.SignInType.Phone, phone, PASSWORD); + session = await _client.SendMagicLinkEmail(Constants.SignInType.Phone, phone, PASSWORD); Contains(_stateChanges, SignedIn); + AreEqual(_client.CurrentSession, _savedSession); _stateChanges.Clear(); IsNotNull(session.AccessToken); @@ -166,7 +195,8 @@ public async Task ClientSignsIn() // Refresh Token var refreshToken = session.RefreshToken; - var newSession = await _client.SignIn(Constants.SignInType.RefreshToken, refreshToken); + var newSession = await _client.SendMagicLinkEmail(Constants.SignInType.RefreshToken, refreshToken); + AreEqual(_client.CurrentSession, _savedSession); Contains(_stateChanges, TokenRefreshed); DoesNotContain(_stateChanges, SignedIn); @@ -181,15 +211,18 @@ public async Task ClientSendsMagicLoginEmail() var user = $"{RandomString(12)}@supabase.io"; await _client.SignUp(user, PASSWORD); Contains(_stateChanges, SignedIn); + AreEqual(_client.CurrentSession, _savedSession); _stateChanges.Clear(); await _client.SignOut(); Contains(_stateChanges, SignedOut); + AreEqual(_client.CurrentSession, _savedSession); _stateChanges.Clear(); - var result = await _client.SignIn(user); + var result = await _client.SendMagicLinkEmail(user); IsTrue(result); - Contains(_stateChanges, SignedOut); + AreEqual(0, _stateChanges.Count); + AreEqual(_client.CurrentSession, _savedSession); } [TestMethod("Client: Sends Magic Login Email (Alias)")] @@ -199,10 +232,13 @@ public async Task ClientSendsMagicLoginEmailAlias() var user2 = $"{RandomString(12)}@supabase.io"; await _client.SignUp(user, PASSWORD); Contains(_stateChanges, SignedIn); + AreEqual(_client.CurrentSession, _savedSession); _stateChanges.Clear(); await _client.SignOut(); Contains(_stateChanges, SignedOut); + IsNull(_savedSession); + AreEqual(_client.CurrentSession, _savedSession); var result = await _client.SendMagicLink(user); var result2 = await _client.SendMagicLink(user2, new SignInOptions { RedirectTo = $"com.{RandomString(12)}.deeplink://login" }); @@ -210,21 +246,24 @@ public async Task ClientSendsMagicLoginEmailAlias() IsTrue(result); IsTrue(result2); } - + [TestMethod("Client: Returns Auth Url for Provider")] public async Task ClientReturnsAuthUrlForProvider() { - var result1 = await _client.SignIn(Constants.Provider.Google); + var result1 = await _client.SendMagicLinkEmail(Constants.Provider.Google); AreEqual("http://localhost:9999/authorize?provider=google", result1.Uri.ToString()); - - var result2 = await _client.SignIn(Constants.Provider.Google, new SignInOptions { Scopes = "special scopes please" }); + + var result2 = await _client.SendMagicLinkEmail(Constants.Provider.Google, new SignInOptions { Scopes = "special scopes please" }); AreEqual("http://localhost:9999/authorize?provider=google&scopes=special+scopes+please", result2.Uri.ToString()); } [TestMethod("Client: Returns Verification Code for Provider")] public async Task ClientReturnsPKCEVerifier() { - var result = await _client.SignIn(Constants.Provider.Github, new SignInOptions { FlowType = Constants.OAuthFlowType.PKCE }); + var result = await _client.SendMagicLinkEmail(Constants.Provider.Github, new SignInOptions { FlowType = Constants.OAuthFlowType.PKCE }); + + Contains(_stateChanges, SignedOut); + IsNull(_savedSession); IsTrue(!string.IsNullOrEmpty(result.PKCEVerifier)); IsTrue(result.Uri.Query.Contains("flow_type=pkce")); @@ -238,17 +277,21 @@ public async Task ClientUpdateUser() { var email = $"{RandomString(12)}@supabase.io"; var session = await _client.SignUp(email, PASSWORD); + IsNotNull(session); + Contains(_stateChanges, SignedIn); + AreEqual(_client.CurrentSession, _savedSession); + _stateChanges.Clear(); var attributes = new UserAttributes { Data = new Dictionary { { "hello", "world" } } }; var result = await _client.Update(attributes); + IsNotNull(result); AreEqual(email, _client.CurrentUser.Email); IsNotNull(_client.CurrentUser.UserMetadata); + Contains(_stateChanges, UserUpdated); + AreEqual(_client.CurrentSession, _savedSession); await _client.SignOut(); - var token = GenerateServiceRoleToken(); - var result2 = await _client.UpdateUserById(token, session.User.Id ?? throw new InvalidOperationException(), new AdminUserAttributes { UserMetadata = new Dictionary { { "hello", "updated" } } }); - AreNotEqual(result.UserMetadata["hello"], result2.UserMetadata["hello"]); } [TestMethod("Client: Returns current user")] @@ -289,7 +332,7 @@ public async Task ClientSignsInUserWrongPassword() await ThrowsExceptionAsync(async () => { - var result = await _client.SignIn(user, PASSWORD + "$"); + var result = await _client.SendMagicLinkEmail(user, PASSWORD + "$"); IsNotNull(result); }); } diff --git a/GotrueTests/ServiceRoleTests.cs b/GotrueTests/ServiceRoleTests.cs index b9ffb75..0573d35 100644 --- a/GotrueTests/ServiceRoleTests.cs +++ b/GotrueTests/ServiceRoleTests.cs @@ -6,6 +6,8 @@ using Supabase.Gotrue; using static Supabase.Gotrue.Constants; using static GotrueTests.TestUtils; +using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert; +using static Microsoft.VisualStudio.TestTools.UnitTesting.CollectionAssert; namespace GotrueTests { @@ -23,7 +25,27 @@ public void TestInitializer() _client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true }); _client.AddDebugListener(LogDebug); } - + + [TestMethod("Service Role: Update User")] + public async Task UpdateUser() + { + var email = $"{RandomString(12)}@supabase.io"; + var session = await _client.SignUp(email, PASSWORD); + IsNotNull(session); + + var attributes = new UserAttributes { Data = new Dictionary { { "hello", "world" } } }; + var result = await _client.Update(attributes); + IsNotNull(result); + AreEqual(email, _client.CurrentUser.Email); + IsNotNull(_client.CurrentUser.UserMetadata); + + await _client.SignOut(); + + var result2 = await _client.UpdateUserById(_serviceKey, session.User.Id!, new AdminUserAttributes { UserMetadata = new Dictionary { { "hello", "updated" } } }); + + AreNotEqual(result.UserMetadata["hello"], result2.UserMetadata["hello"]); + } + [TestMethod("Service Role: Send Invite Email")] public async Task SendsInviteEmail() { @@ -108,7 +130,7 @@ public async Task CreateUser() var result3 = await _client.CreateUser(_serviceKey, new AdminUserAttributes { Email = $"{RandomString(12)}@supabase.io", Password = PASSWORD }); Assert.IsNotNull(result3); } - + [TestMethod("Service Role: Update User by Id")] public async Task UpdateUserById() { From 6fa91f8267b236dc8779de321108402e0c6c6ed4 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 20:36:07 -0700 Subject: [PATCH 29/74] Simplify exceptions --- Gotrue/Exceptions/FailureReason.cs | 34 ++++++++++++++++++++++++++++ Gotrue/Exceptions/GotrueException.cs | 14 +++++++++--- Gotrue/Responses/ErrorResponse.cs | 10 -------- 3 files changed, 45 insertions(+), 13 deletions(-) create mode 100644 Gotrue/Exceptions/FailureReason.cs delete mode 100644 Gotrue/Responses/ErrorResponse.cs diff --git a/Gotrue/Exceptions/FailureReason.cs b/Gotrue/Exceptions/FailureReason.cs new file mode 100644 index 0000000..159fc76 --- /dev/null +++ b/Gotrue/Exceptions/FailureReason.cs @@ -0,0 +1,34 @@ +using static Supabase.Gotrue.Exceptions.FailureHint.Reason; + +namespace Supabase.Gotrue.Exceptions +{ + public static class FailureHint + { + public enum Reason + { + Unknown, + BadPassword, + BadEmailAddress, + MissingInformation, + AlreadyRegistered, + InvalidRefreshToken + } + + public static Reason DetectReason(GotrueException gte) + { + if (gte.Content == null) + return Unknown; + + return gte.StatusCode switch + { + 400 when gte.Content.Contains("User already registered") => AlreadyRegistered, + 400 when gte.Content.Contains("Invalid Refresh Token") => InvalidRefreshToken, + 422 when gte.Content.Contains("Password should be at least") => BadPassword, + 422 when gte.Content.Contains("Unable to validate email address") => BadEmailAddress, + 422 when gte.Content.Contains("provide your email or phone number") => MissingInformation, + _ => Unknown + }; + + } + } +} diff --git a/Gotrue/Exceptions/GotrueException.cs b/Gotrue/Exceptions/GotrueException.cs index 6c392ad..f625163 100644 --- a/Gotrue/Exceptions/GotrueException.cs +++ b/Gotrue/Exceptions/GotrueException.cs @@ -1,18 +1,26 @@ using System; +using System.Diagnostics; using System.Net.Http; using Supabase.Gotrue.Responses; namespace Supabase.Gotrue.Exceptions { + public class GotrueException : Exception { public GotrueException(string? message) : base(message) { } public GotrueException(string? message, Exception? innerException) : base(message, innerException) { } - + public HttpResponseMessage? Response { get; internal set; } public string? Content { get; internal set; } - - public ErrorResponse? Error { get; private set; } + + public int StatusCode { get; set; } + public void AddReason() + { + Reason = FailureHint.DetectReason(this); + Debug.WriteLine(Content); + } + public FailureHint.Reason Reason { get; private set; } } } diff --git a/Gotrue/Responses/ErrorResponse.cs b/Gotrue/Responses/ErrorResponse.cs deleted file mode 100644 index 9516ab9..0000000 --- a/Gotrue/Responses/ErrorResponse.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Supabase.Gotrue.Responses -{ - /// - /// A representation of Postgrest's API error response. - /// - public class ErrorResponse : BaseResponse - { - public string? Message { get; set; } - } -} From 103e6ab887bb55e649d309c4bf049291a5c15cad Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 20:36:48 -0700 Subject: [PATCH 30/74] Add failure test cases. Split anon and service tests --- GotrueTests/AnonKeyClientFailureTests.cs | 156 +++++++++++++++++++++++ GotrueTests/AnonKeyClientTests.cs | 26 +--- GotrueTests/ServiceRoleTests.cs | 2 +- 3 files changed, 159 insertions(+), 25 deletions(-) create mode 100644 GotrueTests/AnonKeyClientFailureTests.cs diff --git a/GotrueTests/AnonKeyClientFailureTests.cs b/GotrueTests/AnonKeyClientFailureTests.cs new file mode 100644 index 0000000..a27f59c --- /dev/null +++ b/GotrueTests/AnonKeyClientFailureTests.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using Supabase.Gotrue; +using Supabase.Gotrue.Exceptions; +using Supabase.Gotrue.Interfaces; +using static GotrueTests.TestUtils; +using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert; +using static Microsoft.VisualStudio.TestTools.UnitTesting.CollectionAssert; +using static Supabase.Gotrue.Constants.AuthState; +using static Supabase.Gotrue.Exceptions.FailureHint.Reason; + +namespace GotrueTests +{ + [SuppressMessage("ReSharper", "PossibleNullReferenceException")] + [TestClass] + public class AnonKeyClientFailureTests + { + + private void AuthStateListener(IGotrueClient sender, Constants.AuthState newState) + { + if (_stateChanges.Contains(newState) && newState != SignedOut) + throw new ArgumentException($"State updated twice {newState}"); + + _stateChanges.Add(newState); + } + + private bool AuthStateIsEmpty() + { + return _stateChanges.Count == 0; + } + + [TestInitialize] + public void TestInitializer() + { + _client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true, PersistSession = true, SessionPersistor = SaveSession, SessionRetriever = LoadSession, SessionDestroyer = DestroySession }); + _client.AddDebugListener(LogDebug); + _client.AddStateChangedListener(AuthStateListener); + } + + private void DestroySession() + { + _savedSession = null; + } + + private Session LoadSession() + { + return _savedSession; + } + + private bool SaveSession(Session session) + { + _savedSession = session; + return true; + } + + private Client _client; + + private readonly List _stateChanges = new List(); + private Session _savedSession; + + [TestMethod("Client: Sign Up With Bad Password")] + public async Task SignUpUserEmailBadPassword() + { + var email = $"{RandomString(12)}@supabase.io"; + var x = await ThrowsExceptionAsync(async () => + { + await _client.SignUp(email, "x"); + }); + AreEqual(BadPassword, x.Reason); + IsNull(_savedSession); + Contains(_stateChanges, SignedOut); + AreEqual(1, _stateChanges.Count); + } + + [TestMethod("Client: Sign Up With Bad Email Address")] + public async Task SignUpUserEmailBadEmailAddress() + { + var x = await ThrowsExceptionAsync(async () => + { + await _client.SignUp("not a real email address", PASSWORD); + }); + AreEqual(BadEmailAddress, x.Reason); + IsNull(_savedSession); + Contains(_stateChanges, SignedOut); + AreEqual(1, _stateChanges.Count); + } + + [TestMethod("Client: Sign up without a phone number")] + public async Task SignUpUserPhone() + { + IsTrue(AuthStateIsEmpty()); + + var phone1 = ""; + var x = await ThrowsExceptionAsync(async () => + { + await _client.SignUp(Constants.SignUpType.Phone, phone1, PASSWORD, new SignUpOptions { Data = new Dictionary { { "firstName", "Testing" } } }); + }); + AreEqual(MissingInformation, x.Reason); + IsNull(_savedSession); + Contains(_stateChanges, SignedOut); + AreEqual(1, _stateChanges.Count); + } + + [TestMethod("Client: Signs Up the same user twice")] + public async Task SignsUpUserTwiceShouldReturnBadRequest() + { + var email = $"{RandomString(12)}@supabase.io"; + var session = await _client.SignUp(email, PASSWORD); + + IsNotNull(session); + + Contains(_stateChanges, SignedIn); + AreEqual(_client.CurrentSession, _savedSession); + _stateChanges.Clear(); + + var x = await ThrowsExceptionAsync(async () => + { + await _client.SignUp(email, PASSWORD); + }); + + AreEqual(AlreadyRegistered, x.Reason); + IsNull(_savedSession); + Contains(_stateChanges, SignedOut); + AreEqual(1, _stateChanges.Count); + } + + [TestMethod("Client: Bogus refresh token")] + public async Task ClientTriggersTokenRefreshedEvent() + { + var email = $"{RandomString(12)}@supabase.io"; + var user = await _client.SignUp(email, PASSWORD); + IsNotNull(user); + + _client.CurrentSession.RefreshToken = "bogus token"; + + var x = await ThrowsExceptionAsync(async () => + { + await _client.RefreshSession(); + }); + AreEqual(x.Reason, InvalidRefreshToken); + } + + [TestMethod("Client: Send Reset Password Email for unknown email")] + public async Task ClientSendsResetPasswordForEmail() + { + var email = $"{RandomString(12)}@supabase.io"; + var result = await _client.ResetPasswordForEmail(email); + IsTrue(result); + } + } +} diff --git a/GotrueTests/AnonKeyClientTests.cs b/GotrueTests/AnonKeyClientTests.cs index 727acea..7c162be 100644 --- a/GotrueTests/AnonKeyClientTests.cs +++ b/GotrueTests/AnonKeyClientTests.cs @@ -34,7 +34,7 @@ private bool AuthStateIsEmpty() [TestInitialize] public void TestInitializer() { - _client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true, PersistSession = true, SessionPersistor = SaveSession, SessionRetriever = LoadSession, SessionDestroyer = DestroySession }); + _client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true, PersistSession = true, SessionPersistor = SaveSession, SessionRetriever = LoadSession, SessionDestroyer = DestroySession }); _client.AddDebugListener(LogDebug); _client.AddStateChangedListener(AuthStateListener); } @@ -91,28 +91,6 @@ public async Task SignUpUserPhone() AreEqual("Testing", session.User.UserMetadata["firstName"]); } - [TestMethod("Client: Signs Up the same user twice should throw BadRequestException")] - public async Task SignsUpUserTwiceShouldReturnBadRequest() - { - var email = $"{RandomString(12)}@supabase.io"; - var result1 = await _client.SignUp(email, PASSWORD); - - IsNotNull(result1); - - Contains(_stateChanges, SignedIn); - AreEqual(_client.CurrentSession, _savedSession); - _stateChanges.Clear(); - - await ThrowsExceptionAsync(async () => - { - // This calls session destroy, logging the user out - await _client.SignUp(email, PASSWORD); - }); - - Contains(_stateChanges, SignedOut); - AreEqual(null, _savedSession); - } - [TestMethod("Client: Triggers Token Refreshed Event")] public async Task ClientTriggersTokenRefreshedEvent() { @@ -202,7 +180,7 @@ public async Task ClientSignsIn() IsNotNull(newSession.AccessToken); IsNotNull(newSession.RefreshToken); - IsInstanceOfType(newSession.User, typeof(User)); + IsNotNull(newSession.User); } [TestMethod("Client: Sends Magic Login Email")] diff --git a/GotrueTests/ServiceRoleTests.cs b/GotrueTests/ServiceRoleTests.cs index 0573d35..2f94ca5 100644 --- a/GotrueTests/ServiceRoleTests.cs +++ b/GotrueTests/ServiceRoleTests.cs @@ -22,7 +22,7 @@ public class ServiceRoleTests [TestInitialize] public void TestInitializer() { - _client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true }); + _client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true }); _client.AddDebugListener(LogDebug); } From 4052ec3f2e258ed8dd2b6cb6838c19b80f52c052 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 20:37:20 -0700 Subject: [PATCH 31/74] Remove unneeded imports --- Gotrue/Interfaces/IGotrueClient.cs | 1 - Gotrue/PersistenceListener.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/Gotrue/Interfaces/IGotrueClient.cs b/Gotrue/Interfaces/IGotrueClient.cs index ee25897..1e31463 100644 --- a/Gotrue/Interfaces/IGotrueClient.cs +++ b/Gotrue/Interfaces/IGotrueClient.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; using Supabase.Core.Interfaces; using static Supabase.Gotrue.Constants; diff --git a/Gotrue/PersistenceListener.cs b/Gotrue/PersistenceListener.cs index 9d5161f..1948bdc 100644 --- a/Gotrue/PersistenceListener.cs +++ b/Gotrue/PersistenceListener.cs @@ -1,5 +1,4 @@ using System; -using System.Threading.Tasks; using Supabase.Gotrue.Interfaces; namespace Supabase.Gotrue From 1cb9d306ecea131b894a18004de7ceb71c1db1f4 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 20:37:38 -0700 Subject: [PATCH 32/74] Add abbreviations to spelling check --- gotrue-csharp.sln.DotSettings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gotrue-csharp.sln.DotSettings b/gotrue-csharp.sln.DotSettings index 5298f15..db9480e 100644 --- a/gotrue-csharp.sln.DotSettings +++ b/gotrue-csharp.sln.DotSettings @@ -43,8 +43,10 @@ UseVar UseVar SHA + SMS True True + True True True True \ No newline at end of file From acdbe2689be3947102eb999497e14cb5b437f1ba Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 20:38:14 -0700 Subject: [PATCH 33/74] Make consistent with rest of project code style --- Gotrue/Api.cs | 28 ++++++++++++------------- Gotrue/Client.cs | 44 ++++++++++++++++++--------------------- Gotrue/ClientOptions.cs | 11 ++++------ Gotrue/Constants.cs | 18 ++++++++-------- Gotrue/Helpers.cs | 14 +++++++------ Gotrue/StatelessClient.cs | 21 ++++++++----------- 6 files changed, 63 insertions(+), 73 deletions(-) diff --git a/Gotrue/Api.cs b/Gotrue/Api.cs index 255357b..f2e1fe5 100644 --- a/Gotrue/Api.cs +++ b/Gotrue/Api.cs @@ -61,8 +61,7 @@ public Api(string url, Dictionary? headers = null) public async Task SignUpWithEmail(string email, string password, SignUpOptions? options = null) { var body = new Dictionary { { "email", email }, { "password", password } }; - - string endpoint = $"{Url}/signup"; + var endpoint = $"{Url}/signup"; if (options != null) { @@ -135,12 +134,12 @@ public async Task SignInWithOtp(SignInWithPasswordlessE { { "email", options.Email }, { "data", options.Data }, - { "create_user", options.ShouldCreateUser }, + { "create_user", options.ShouldCreateUser } }; if (options.FlowType == OAuthFlowType.PKCE) { - string challenge = Helpers.GenerateNonce(); + var challenge = Helpers.GenerateNonce(); verifier = Helpers.GeneratePKCENonceVerifier(challenge); body.Add("code_challenge", challenge); @@ -202,7 +201,9 @@ public async Task SignInWithOtp(SignInWithPasswordlessP /// /// /// - /// + /// + /// InvalidProviderException + /// public Task SignInWithIdToken(Provider provider, string idToken, string? nonce = null, string? captchaToken = null) { if (provider != Provider.Google && provider != Provider.Apple) @@ -211,7 +212,7 @@ public async Task SignInWithOtp(SignInWithPasswordlessP var body = new Dictionary { {"provider", Core.Helpers.GetMappedToAttr(provider).Mapping }, - {"id_token", idToken }, + {"id_token", idToken } }; if (!string.IsNullOrEmpty(nonce)) @@ -234,7 +235,7 @@ public Task SendMagicLinkEmail(string email, SignInOptions? option { var data = new Dictionary { { "email", email } }; - string endpoint = $"{Url}/magiclink"; + var endpoint = $"{Url}/magiclink"; if (options != null) { @@ -301,7 +302,7 @@ public Task InviteUserByEmail(string email, string jwt) { var data = new Dictionary { { "phone", phone }, - { "password", password }, + { "password", password } }; return Helpers.MakeRequest(HttpMethod.Post, $"{Url}/token?grant_type=password", data, Headers); } @@ -403,9 +404,9 @@ public ProviderAuthState GetUriForProvider(Provider provider, SignInOptions? opt result.PKCEVerifier = codeVerifier; } - if (attr is MapToAttribute mappedAttr) + if (attr is MapToAttribute) { - query.Add("provider", mappedAttr.Mapping); + query.Add("provider", attr.Mapping); if (!string.IsNullOrEmpty(options.Scopes)) query.Add("scopes", options.Scopes); @@ -497,7 +498,7 @@ public Task SignOut(string jwt) /// /// A valid JWT. Must be a full-access API key (e.g. service_role key). /// A string for example part of the email - /// Snake case string of the given key, currently only created_at is suppported + /// Snake case string of the given key, currently only created_at is supported /// asc or desc, if null desc is used /// page to show for pagination /// items per page for pagination @@ -545,10 +546,7 @@ private Dictionary TransformListUsersParams(string? filter = nul /// public Task CreateUser(string jwt, AdminUserAttributes? attributes = null) { - if (attributes == null) - { - attributes = new AdminUserAttributes(); - } + attributes ??= new AdminUserAttributes(); return Helpers.MakeRequest(HttpMethod.Post, $"{Url}/admin/users", attributes, CreateAuthedRequestHeaders(jwt)); } diff --git a/Gotrue/Client.cs b/Gotrue/Client.cs index 2f159b4..3065523 100644 --- a/Gotrue/Client.cs +++ b/Gotrue/Client.cs @@ -92,7 +92,7 @@ public void ClearStateChangedListeners() /// /// The initialized client options. /// - public ClientOptions Options { get; } + public ClientOptions Options { get; } /// /// Internal timer reference for Refreshing Tokens ( @@ -116,9 +116,9 @@ public void ClearStateChangedListeners() /// /// /// - public Client(ClientOptions? options = null) + public Client(ClientOptions? options = null) { - options ??= new ClientOptions(); + options ??= new ClientOptions(); Options = options; @@ -143,7 +143,10 @@ public Client(ClientOptions? options = null) /// If signUp() is called for an existing confirmed user: /// - If Confirm email is enabled in your project, an obfuscated/fake user object is returned. /// - If Confirm email is disabled, the error message, User already registered is returned. - /// To fetch the currently logged-in user, refer to . + /// To fetch the currently logged-in user, refer to + /// User + /// + /// . /// /// /// @@ -333,7 +336,7 @@ public async Task SignInWithOtp(SignInWithPasswordlessP default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } - if (session?.User?.ConfirmedAt != null || (session?.User != null && Options.AllowUnconfirmedUserSessions)) + if (session?.User?.ConfirmedAt != null || session?.User != null && Options.AllowUnconfirmedUserSessions) { NotifyStateChange(SignedIn); return CurrentSession; @@ -672,29 +675,23 @@ public Session SetAuth(string accessToken) return null; } } - else - { - DestroySession(); - return null; - } + DestroySession(); + return null; } - else if (session == null || session.User == null) + if (session?.User == null) { _debugNotification?.Log("Stored Session is missing data."); DestroySession(); return null; } - else - { - CurrentSession = session; - CurrentUser = session.User; + CurrentSession = session; + CurrentUser = session.User; - NotifyStateChange(SignedIn); + NotifyStateChange(SignedIn); - InitRefreshTimer(); + InitRefreshTimer(); - return CurrentSession; - } + return CurrentSession; } /// @@ -781,15 +778,14 @@ private void InitRefreshTimer() { if (CurrentSession == null || CurrentSession.ExpiresIn == default) return; - if (_refreshTimer != null) - _refreshTimer.Dispose(); + _refreshTimer?.Dispose(); try { // Interval should be t - (1/5(n)) (i.e. if session time (t) 3600s, attempt refresh at 2880s or 720s (1/5) seconds before expiration) - int interval = (int)Math.Floor(CurrentSession.ExpiresIn * 4.0f / 5.0f); - int timeoutSeconds = Convert.ToInt32((CurrentSession.CreatedAt.AddSeconds(interval) - DateTime.Now).TotalSeconds); - TimeSpan timeout = TimeSpan.FromSeconds(timeoutSeconds); + var interval = (int)Math.Floor(CurrentSession.ExpiresIn * 4.0f / 5.0f); + var timeoutSeconds = Convert.ToInt32((CurrentSession.CreatedAt.AddSeconds(interval) - DateTime.Now).TotalSeconds); + var timeout = TimeSpan.FromSeconds(timeoutSeconds); _refreshTimer = new Timer(HandleRefreshTimerTick, null, timeout, Timeout.InfiniteTimeSpan); } diff --git a/Gotrue/ClientOptions.cs b/Gotrue/ClientOptions.cs index 8edcb92..a1e4b0f 100644 --- a/Gotrue/ClientOptions.cs +++ b/Gotrue/ClientOptions.cs @@ -1,15 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; +using System.Collections.Generic; using static Supabase.Gotrue.Constants; namespace Supabase.Gotrue { /// - /// Class represention options available to the . + /// Class representation options available to the . /// - public class ClientOptions - where TSession : Session + public class ClientOptions { /// /// Gotrue Endpoint @@ -19,7 +16,7 @@ public class ClientOptions /// /// Headers to be sent with subsequent requests. /// - public Dictionary Headers = new Dictionary(DEFAULT_HEADERS); + public readonly Dictionary Headers = new Dictionary(DefaultHeaders); /// /// Should the Client automatically handle refreshing the User's Token? diff --git a/Gotrue/Constants.cs b/Gotrue/Constants.cs index 7d34079..d63a1e2 100644 --- a/Gotrue/Constants.cs +++ b/Gotrue/Constants.cs @@ -5,12 +5,12 @@ namespace Supabase.Gotrue { public static class Constants { - public static string GOTRUE_URL = "http://localhost:9999"; - public static string AUDIENCE = ""; - public static readonly Dictionary DEFAULT_HEADERS = new Dictionary(); - public static int EXPIRY_MARGIN = 60 * 1000; - public static string STORAGE_KEY = "supabase.auth.token"; - public static readonly Dictionary COOKIE_OPTIONS = new Dictionary{ + public const string GOTRUE_URL = "http://localhost:9999"; + public const string AUDIENCE = ""; + public static readonly Dictionary DefaultHeaders = new Dictionary(); + 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", "" }, @@ -86,7 +86,7 @@ public enum Provider Twitter, [MapTo("workos")] WorkOS - }; + } /// /// States that the Auth Client will raise events for. @@ -99,7 +99,7 @@ public enum AuthState UserUpdated, PasswordRecovery, TokenRefreshed - }; + } /// /// Specifies the functionality expected from the `SignIn` method @@ -108,7 +108,7 @@ public enum SignInType { Email, Phone, - RefreshToken, + RefreshToken } /// diff --git a/Gotrue/Helpers.cs b/Gotrue/Helpers.cs index de408f9..6e2cefd 100644 --- a/Gotrue/Helpers.cs +++ b/Gotrue/Helpers.cs @@ -27,7 +27,7 @@ public static string GenerateNonce() const string chars = "abcdefghijklmnopqrstuvwxyz123456789"; var random = new Random(); var nonce = new char[128]; - for (int i = 0; i < nonce.Length; i++) + for (var i = 0; i < nonce.Length; i++) { nonce[i] = chars[random.Next(chars.Length)]; } @@ -61,12 +61,12 @@ public static string GeneratePKCENonceVerifier(string codeVerifier) /// public static string GenerateSHA256NonceFromRawNonce(string rawNonce) { - SHA256Managed sha = new SHA256Managed(); - byte[] utf8RawNonce = Encoding.UTF8.GetBytes(rawNonce); - byte[] hash = sha.ComputeHash(utf8RawNonce); + var sha = new SHA256Managed(); + var utf8RawNonce = Encoding.UTF8.GetBytes(rawNonce); + var hash = sha.ComputeHash(utf8RawNonce); - string result = string.Empty; - foreach (byte t in hash) + var result = string.Empty; + foreach (var t in hash) result += t.ToString("x2"); return result; @@ -156,6 +156,8 @@ internal static async Task MakeRequest(HttpMethod method, string u var e = new GotrueException("Request Failed"); e.Content = content; e.Response = response; + e.StatusCode = (int)response.StatusCode; + e.AddReason(); throw e; } diff --git a/Gotrue/StatelessClient.cs b/Gotrue/StatelessClient.cs index b2238b8..03e9a8c 100644 --- a/Gotrue/StatelessClient.cs +++ b/Gotrue/StatelessClient.cs @@ -41,18 +41,14 @@ public class StatelessClient : IGotrueStatelessClient public async Task SignUp(SignUpType type, string identifier, string password, StatelessClientOptions options, SignUpOptions? signUpOptions = null) { var api = GetApi(options); - Session? session = null; - switch (type) + var session = type switch { - case SignUpType.Email: - session = await api.SignUpWithEmail(identifier, password, signUpOptions); - break; - case SignUpType.Phone: - session = await api.SignUpWithPhone(identifier, password, signUpOptions); - break; - } + SignUpType.Email => await api.SignUpWithEmail(identifier, password, signUpOptions), + SignUpType.Phone => await api.SignUpWithPhone(identifier, password, signUpOptions), + _ => null + }; - if (session?.User?.ConfirmedAt != null || (session?.User != null && options.AllowUnconfirmedUserSessions)) + if (session?.User?.ConfirmedAt != null || session?.User != null && options.AllowUnconfirmedUserSessions) { return session; } @@ -124,9 +120,10 @@ public async Task SignIn(string email, StatelessClientOptions options, Sig case SignInType.RefreshToken: session = await RefreshToken(identifierOrToken, options); break; + default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } - if (session?.User?.ConfirmedAt != null || (session?.User != null && options.AllowUnconfirmedUserSessions)) + if (session?.User?.ConfirmedAt != null || session?.User != null && options.AllowUnconfirmedUserSessions) { return session; } @@ -420,7 +417,7 @@ public class StatelessClientOptions /// /// Headers to be sent with subsequent requests. /// - public readonly Dictionary Headers = new Dictionary(DEFAULT_HEADERS); + public readonly Dictionary Headers = new Dictionary(DefaultHeaders); /// /// Very unlikely this flag needs to be changed except in very specific contexts. From 80c0e08cca97a41a6041b2e39f9e9d6c40cb149b Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 22:00:08 -0700 Subject: [PATCH 34/74] Changed to use Options headers as sole source of truth --- Gotrue/Client.cs | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/Gotrue/Client.cs b/Gotrue/Client.cs index 3065523..8f79570 100644 --- a/Gotrue/Client.cs +++ b/Gotrue/Client.cs @@ -32,25 +32,6 @@ public void AddDebugListener(Action listener) _debugNotification.AddDebugListener(listener); } - /// - /// Function that can be set to return dynamic headers. - /// - /// Headers specified in the client options will ALWAYS take precedence over headers returned by this function. - /// - public Func>? GetHeaders - { - get => _getHeaders; - set - { - _getHeaders = value; - - if (_api != null) - _api.GetHeaders = value; - } - } - - private Func>? _getHeaders; - public void NotifyStateChange(AuthState stateChanged) { foreach (var handler in _authEventHandlers) @@ -688,9 +669,7 @@ public Session SetAuth(string accessToken) CurrentUser = session.User; NotifyStateChange(SignedIn); - InitRefreshTimer(); - return CurrentSession; } @@ -816,5 +795,16 @@ private async void HandleRefreshTimerTick(object _) NotifyStateChange(SignedOut); } } + + public Func>? GetHeaders + { + get => _api.GetHeaders; + set => throw new ArgumentException(); + } + public void LoadSession() + { + if(Options.SessionRetriever != null) + UpdateSession(Options.SessionRetriever.Invoke()); + } } } From 7eb9b5510f83c48eec9ac57e0c09bb6539957602 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 22:00:14 -0700 Subject: [PATCH 35/74] Create ConfigurationFailureTests.cs --- GotrueTests/ConfigurationFailureTests.cs | 45 ++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 GotrueTests/ConfigurationFailureTests.cs diff --git a/GotrueTests/ConfigurationFailureTests.cs b/GotrueTests/ConfigurationFailureTests.cs new file mode 100644 index 0000000..af9437a --- /dev/null +++ b/GotrueTests/ConfigurationFailureTests.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Supabase.Gotrue; +using Supabase.Gotrue.Exceptions; +using static GotrueTests.TestUtils; +using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert; +using static Supabase.Gotrue.Exceptions.FailureHint.Reason; + +namespace GotrueTests +{ + [TestClass] + public class ConfigurationFailureTests + { + [TestMethod("Bad URL message")] + public async Task BadUrlTest() + { + var client = new Client(new ClientOptions { Url = "https://badprojecturl.supabase.co", AllowUnconfirmedUserSessions = true, PersistSession = false }); + client.AddDebugListener(LogDebug); + + var email = $"{RandomString(12)}@supabase.io"; + await ThrowsExceptionAsync(async () => + { + await client.SignUp(email, PASSWORD); + }); + } + + [TestMethod("Bad service key message")] + public async Task BadServiceApiKeyTest() + { + var client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true, PersistSession = false }); + client.AddDebugListener(LogDebug); + + var x = await ThrowsExceptionAsync(async () => + { + await client.ListUsers("garbage key"); + }); + AreEqual(AdminTokenRequired, x.Reason); + } + + + + } +} From da6be92a1d56066efeda506c8d277a28e7e408f7 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 22:00:33 -0700 Subject: [PATCH 36/74] Remove readonly --- Gotrue/ClientOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gotrue/ClientOptions.cs b/Gotrue/ClientOptions.cs index a1e4b0f..b810dc9 100644 --- a/Gotrue/ClientOptions.cs +++ b/Gotrue/ClientOptions.cs @@ -16,7 +16,7 @@ public class ClientOptions /// /// Headers to be sent with subsequent requests. /// - public readonly Dictionary Headers = new Dictionary(DefaultHeaders); + public Dictionary Headers = new Dictionary(); /// /// Should the Client automatically handle refreshing the User's Token? From 58bb4a4917109f5c7b29361b7eb8e4ee08e83985 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 22:00:44 -0700 Subject: [PATCH 37/74] Remove client launch --- Gotrue/Constants.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Gotrue/Constants.cs b/Gotrue/Constants.cs index d63a1e2..3fcb67a 100644 --- a/Gotrue/Constants.cs +++ b/Gotrue/Constants.cs @@ -7,7 +7,6 @@ public static class Constants { public const string GOTRUE_URL = "http://localhost:9999"; public const string AUDIENCE = ""; - public static readonly Dictionary DefaultHeaders = new Dictionary(); public const int EXPIRY_MARGIN = 60 * 1000; public const string STORAGE_KEY = "supabase.auth.token"; public static readonly Dictionary CookieOptions = new Dictionary{ @@ -93,7 +92,6 @@ public enum Provider /// public enum AuthState { - ClientLaunch, SignedIn, SignedOut, UserUpdated, From e34671c816757ff4145ef877297c3b9f560597d6 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 22:01:03 -0700 Subject: [PATCH 38/74] Add bearer token failure --- Gotrue/Exceptions/FailureReason.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Gotrue/Exceptions/FailureReason.cs b/Gotrue/Exceptions/FailureReason.cs index 159fc76..d9bf8ed 100644 --- a/Gotrue/Exceptions/FailureReason.cs +++ b/Gotrue/Exceptions/FailureReason.cs @@ -11,7 +11,8 @@ public enum Reason BadEmailAddress, MissingInformation, AlreadyRegistered, - InvalidRefreshToken + InvalidRefreshToken, + AdminTokenRequired } public static Reason DetectReason(GotrueException gte) @@ -23,6 +24,7 @@ public static Reason DetectReason(GotrueException gte) { 400 when gte.Content.Contains("User already registered") => AlreadyRegistered, 400 when gte.Content.Contains("Invalid Refresh Token") => InvalidRefreshToken, + 401 when gte.Content.Contains("This endpoint requires a Bearer token") => AdminTokenRequired, 422 when gte.Content.Contains("Password should be at least") => BadPassword, 422 when gte.Content.Contains("Unable to validate email address") => BadEmailAddress, 422 when gte.Content.Contains("provide your email or phone number") => MissingInformation, From d0568276c399795b4ed39a5654f36c036c9a408c Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 22:01:46 -0700 Subject: [PATCH 39/74] Comment out debug (used to match server errors) --- Gotrue/Exceptions/GotrueException.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gotrue/Exceptions/GotrueException.cs b/Gotrue/Exceptions/GotrueException.cs index f625163..cb4462b 100644 --- a/Gotrue/Exceptions/GotrueException.cs +++ b/Gotrue/Exceptions/GotrueException.cs @@ -19,7 +19,7 @@ public GotrueException(string? message, Exception? innerException) : base(messag public void AddReason() { Reason = FailureHint.DetectReason(this); - Debug.WriteLine(Content); + //Debug.WriteLine(Content); } public FailureHint.Reason Reason { get; private set; } } From 0c8f5ab5dc553f5747e780a3501fe2d9ab77377e Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 22:01:54 -0700 Subject: [PATCH 40/74] Remove client launch --- Gotrue/PersistenceListener.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Gotrue/PersistenceListener.cs b/Gotrue/PersistenceListener.cs index 1948bdc..9c4c9de 100644 --- a/Gotrue/PersistenceListener.cs +++ b/Gotrue/PersistenceListener.cs @@ -53,9 +53,6 @@ public void EventHandler(IGotrueClient sender, Constants.AuthStat _save?.Invoke(sender.CurrentSession); break; - case Constants.AuthState.ClientLaunch: - _load?.Invoke(); - break; default: throw new ArgumentOutOfRangeException(nameof(stateChanged), stateChanged, null); } } From 665b6c36a6599d757d051f5a0ee5dfb523aff40c Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 22:02:11 -0700 Subject: [PATCH 41/74] Remove unused empty default headers --- Gotrue/StatelessClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gotrue/StatelessClient.cs b/Gotrue/StatelessClient.cs index 03e9a8c..9f73797 100644 --- a/Gotrue/StatelessClient.cs +++ b/Gotrue/StatelessClient.cs @@ -102,7 +102,7 @@ public async Task SignIn(string email, StatelessClientOptions options, Sig options ??= new StatelessClientOptions(); var api = GetApi(options); - Session? session = null; + Session? session; switch (type) { case SignInType.Email: @@ -417,7 +417,7 @@ public class StatelessClientOptions /// /// Headers to be sent with subsequent requests. /// - public readonly Dictionary Headers = new Dictionary(DefaultHeaders); + public readonly Dictionary Headers = new Dictionary(); /// /// Very unlikely this flag needs to be changed except in very specific contexts. From d64f9ed5a6011e33065cb50c583a341eb1b800f7 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 22:02:19 -0700 Subject: [PATCH 42/74] Remove imports --- GotrueTests/AnonKeyClientFailureTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/GotrueTests/AnonKeyClientFailureTests.cs b/GotrueTests/AnonKeyClientFailureTests.cs index a27f59c..6e45f83 100644 --- a/GotrueTests/AnonKeyClientFailureTests.cs +++ b/GotrueTests/AnonKeyClientFailureTests.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json; using Supabase.Gotrue; using Supabase.Gotrue.Exceptions; using Supabase.Gotrue.Interfaces; From a1ad5007dafda0cc949d3b111e1b1393fcaaae04 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 22:02:33 -0700 Subject: [PATCH 43/74] Add load user from persistence test --- GotrueTests/AnonKeyClientTests.cs | 33 +++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/GotrueTests/AnonKeyClientTests.cs b/GotrueTests/AnonKeyClientTests.cs index 7c162be..4a92fad 100644 --- a/GotrueTests/AnonKeyClientTests.cs +++ b/GotrueTests/AnonKeyClientTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -20,8 +21,8 @@ public class AnonKeyClientTests private void AuthStateListener(IGotrueClient sender, Constants.AuthState newState) { - if (_stateChanges.Contains(newState) && newState != SignedOut) - throw new ArgumentException($"State updated twice {newState}"); + if (_stateChanges.Contains(newState)) + Debug.WriteLine($"State updated twice {newState}"); _stateChanges.Add(newState); } @@ -76,6 +77,34 @@ public async Task SignUpUserEmail() IsNotNull(session.User); } + [TestMethod("Client: Load User From Persistence")] + public async Task SaveAndLoadUser() + { + IsTrue(AuthStateIsEmpty()); + + var email = $"{RandomString(12)}@supabase.io"; + var session = await _client.SignUp(email, PASSWORD); + + Contains(_stateChanges, SignedIn); + AreEqual(_client.CurrentSession, _savedSession); + + IsNotNull(session.AccessToken); + IsNotNull(session.RefreshToken); + IsNotNull(session.User); + + var newClient = new Client(new ClientOptions + { + AllowUnconfirmedUserSessions = true, PersistSession = true, + SessionPersistor = SaveSession, SessionRetriever = LoadSession, SessionDestroyer = DestroySession + }); + newClient.AddDebugListener(LogDebug); + newClient.AddStateChangedListener(AuthStateListener); + + // Loads the session from storage + newClient.LoadSession(); + await newClient.RetrieveSessionAsync(); + } + [TestMethod("Client: Sign up Phone")] public async Task SignUpUserPhone() { From 7770aaef2fa15500c8511247b79e5b53e859dac4 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 22:02:48 -0700 Subject: [PATCH 44/74] simplify with static import --- GotrueTests/ServiceRoleTests.cs | 61 ++++++++++++++++----------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/GotrueTests/ServiceRoleTests.cs b/GotrueTests/ServiceRoleTests.cs index 2f94ca5..6a37783 100644 --- a/GotrueTests/ServiceRoleTests.cs +++ b/GotrueTests/ServiceRoleTests.cs @@ -7,7 +7,6 @@ using static Supabase.Gotrue.Constants; using static GotrueTests.TestUtils; using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert; -using static Microsoft.VisualStudio.TestTools.UnitTesting.CollectionAssert; namespace GotrueTests { @@ -51,14 +50,14 @@ public async Task SendsInviteEmail() { var user = $"{RandomString(12)}@supabase.io"; var result = await _client.InviteUserByEmail(user, _serviceKey); - Assert.IsTrue(result); + IsTrue(result); } [TestMethod("Service Role: List users")] public async Task ListUsers() { var result = await _client.ListUsers(_serviceKey); - Assert.IsTrue(result.Users.Count > 0); + IsTrue(result.Users.Count > 0); } [TestMethod("Service Role: List users by page")] @@ -67,9 +66,9 @@ public async Task ListUsersPagination() var page1 = await _client.ListUsers(_serviceKey, page: 1, perPage: 1); var page2 = await _client.ListUsers(_serviceKey, page: 2, perPage: 1); - Assert.AreEqual(page1.Users.Count, 1); - Assert.AreEqual(page2.Users.Count, 1); - Assert.AreNotEqual(page1.Users[0].Id, page2.Users[0].Id); + AreEqual(page1.Users.Count, 1); + AreEqual(page2.Users.Count, 1); + AreNotEqual(page1.Users[0].Id, page2.Users[0].Id); } [TestMethod("Service Role: Lists users sort")] @@ -80,7 +79,7 @@ public async Task ListUsersSort() var result1 = await _client.ListUsers(serviceRoleKey, sortBy: "created_at", sortOrder: SortOrder.Ascending); var result2 = await _client.ListUsers(serviceRoleKey, sortBy: "created_at", sortOrder: SortOrder.Descending); - Assert.AreNotEqual(result1.Users[0].Id, result2.Users[0].Id); + AreNotEqual(result1.Users[0].Id, result2.Users[0].Id); } [TestMethod("Service role: Lists users with filter")] @@ -88,15 +87,15 @@ public async Task ListUsersFilter() { var user = $"{RandomString(12)}@supabase.io"; var result = await _client.SignUp(user, PASSWORD); - Assert.IsNotNull(result); + IsNotNull(result); // ReSharper disable once StringLiteralTypo var result1 = await _client.ListUsers(_serviceKey, filter: "@nonexistingrandomemailprovider.com"); var result2 = await _client.ListUsers(_serviceKey, filter: "@supabase.io"); - Assert.AreNotEqual(result2.Users.Count, 0); - Assert.AreEqual(result1.Users.Count, 0); - Assert.AreNotEqual(result1.Users.Count, result2.Users.Count); + AreNotEqual(result2.Users.Count, 0); + AreEqual(result1.Users.Count, 0); + AreNotEqual(result1.Users.Count, result2.Users.Count); } [TestMethod("Service Role: Get User by Id")] @@ -107,8 +106,8 @@ public async Task GetUserById() var userResult = result.Users[0]; var userByIdResult = await _client.GetUserById(_serviceKey, userResult.Id ?? throw new InvalidOperationException()); - Assert.AreEqual(userResult.Id, userByIdResult.Id); - Assert.AreEqual(userResult.Email, userByIdResult.Email); + AreEqual(userResult.Id, userByIdResult.Id); + AreEqual(userResult.Email, userByIdResult.Email); } [TestMethod("Service Role: Create a user")] @@ -116,7 +115,7 @@ public async Task CreateUser() { var result = await _client.CreateUser(_serviceKey, $"{RandomString(12)}@supabase.io", PASSWORD); - Assert.IsNotNull(result); + IsNotNull(result); var attributes = new AdminUserAttributes { @@ -125,10 +124,10 @@ public async Task CreateUser() }; var result2 = await _client.CreateUser(_serviceKey, $"{RandomString(12)}@supabase.io", PASSWORD, attributes); - Assert.AreEqual("123", result2.UserMetadata["firstName"]); + AreEqual("123", result2.UserMetadata["firstName"]); var result3 = await _client.CreateUser(_serviceKey, new AdminUserAttributes { Email = $"{RandomString(12)}@supabase.io", Password = PASSWORD }); - Assert.IsNotNull(result3); + IsNotNull(result3); } [TestMethod("Service Role: Update User by Id")] @@ -136,14 +135,14 @@ public async Task UpdateUserById() { var createdUser = await _client.CreateUser(_serviceKey, $"{RandomString(12)}@supabase.io", PASSWORD); - Assert.IsNotNull(createdUser); + IsNotNull(createdUser); var updatedUser = await _client.UpdateUserById(_serviceKey, createdUser.Id ?? throw new InvalidOperationException(), new AdminUserAttributes { Email = $"{RandomString(12)}@supabase.io" }); - Assert.IsNotNull(updatedUser); + IsNotNull(updatedUser); - Assert.AreEqual(createdUser.Id, updatedUser.Id); - Assert.AreNotEqual(createdUser.Email, updatedUser.Email); + AreEqual(createdUser.Id, updatedUser.Id); + AreNotEqual(createdUser.Email, updatedUser.Email); } [TestMethod("Service Role: Delete User")] @@ -155,34 +154,34 @@ public async Task DeletesUser() var result = await _client.DeleteUser(uid ?? throw new InvalidOperationException(), _serviceKey); - Assert.IsTrue(result); + IsTrue(result); } [TestMethod("Nonce generation and verification")] public void NonceGeneration() { var nonce = Helpers.GenerateNonce(); - Assert.IsNotNull(nonce); - Assert.AreEqual(128, nonce.Length); + IsNotNull(nonce); + AreEqual(128, nonce.Length); var pkceVerifier = Helpers.GeneratePKCENonceVerifier(nonce); - Assert.IsNotNull(pkceVerifier); - Assert.AreEqual(43, pkceVerifier.Length); + IsNotNull(pkceVerifier); + AreEqual(43, pkceVerifier.Length); var appleVerifier = Helpers.GenerateSHA256NonceFromRawNonce(nonce); - Assert.IsNotNull(appleVerifier); - Assert.AreEqual(64, appleVerifier.Length); + IsNotNull(appleVerifier); + AreEqual(64, appleVerifier.Length); const string helloNonce = "hello_world_nonce"; var helloPkceVerifier = Helpers.GeneratePKCENonceVerifier(helloNonce); - Assert.IsNotNull(helloPkceVerifier); + IsNotNull(helloPkceVerifier); // ReSharper disable once StringLiteralTypo - Assert.AreEqual("9TMmi4JOlYOQEP2Ha39WXj9pySILGnAfQsz-yXws0yE", helloPkceVerifier); + AreEqual("9TMmi4JOlYOQEP2Ha39WXj9pySILGnAfQsz-yXws0yE", helloPkceVerifier); var helloAppleVerifier = Helpers.GenerateSHA256NonceFromRawNonce(helloNonce); - Assert.IsNotNull(helloAppleVerifier); - Assert.AreEqual("f533268b824e95839010fd876b7f565e3f69c9220b1a701f42ccfec97c2cd321", helloAppleVerifier); + IsNotNull(helloAppleVerifier); + AreEqual("f533268b824e95839010fd876b7f565e3f69c9220b1a701f42ccfec97c2cd321", helloAppleVerifier); } } } From 617413f4b4fba5add5ae5b47a76a4929ec789f44 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 22:02:56 -0700 Subject: [PATCH 45/74] format --- GotrueTests/TestUtils.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/GotrueTests/TestUtils.cs b/GotrueTests/TestUtils.cs index a8b2fdd..fc6d48c 100644 --- a/GotrueTests/TestUtils.cs +++ b/GotrueTests/TestUtils.cs @@ -13,8 +13,7 @@ public static class TestUtils private static readonly Random Random = new Random(); public const string PASSWORD = "I@M@SuperP@ssWord"; - - + public static void LogDebug(string message, Exception e) { Debug.WriteLine(message); From 6685b5a9a09f944a1f6afc18300a658d6cea8d2a Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 22:03:03 -0700 Subject: [PATCH 46/74] Add info on changes --- README.md | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 781d1fb..0e444b9 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,26 @@ --- -## BREAKING CHANGES MOVING FROM v3.0 to 3.1 +## BREAKING CHANGES v3.1 → v3.x + +- Exceptions have been simplified to a single GotrueException. A Reason field has been added +to GotrueException to help sort out what happened. This should also be easier to manage as the Gotrue +server API & messages evolve. +- The delegates for save/load/destroy persistence have been simplified to no longer require async. +- Console logging in a few places (most notable the background refresh thread) has been removed +in favor of a notification method. See Client.AddDebugListener() and the test cases for examples. +This will allow you to implement your own logging strategy (write to temp file, console, user visible +err console, etc). +- The client now more reliably emits AuthState changes. +- There is now a single source of truth for headers in the stateful Client - the Options headers. + +Implementation notes: + +- Test cases have been added to help ensure reliability of auth state change notifications + and persistence. +- Persistence is now managed via the same notifications as auth state change + +## BREAKING CHANGES v3.0 → 3.1 - We've implemented the PKCE auth flow. SignIn using a provider now returns an instance of `ProviderAuthState` rather than a `string`. - The provider sign in signature has moved `scopes` into `SignInOptions` @@ -55,7 +74,7 @@ await new StatelessClient().SignUp("new-user@example.com", options); ## Persisting, Retrieving, and Destroying Sessions. -This Gotrue client is written to be agnostic when it comes to session persistance, retrieval, and destruction. `ClientOptions` exposes +This Gotrue client is written to be agnostic when it comes to session persistence, retrieval, and destruction. `ClientOptions` exposes properties that allow these to be specified. In the event these are specified and the `AutoRefreshToken` option is set, as the `Client` Initializes, it will also attempt to @@ -71,18 +90,21 @@ async void Initialize() { var options = new ClientOptions { Url = GOTRUE_URL, - SessionPersistor = SessionPersistor, - SessionRetriever = SessionRetriever, - SessionDestroyer = SessionDestroyer + PersistSession = true, + SessionPersistor = SaveSession, // PeristenceListener public delegate bool SaveSession(Session session); + SessionRetriever = LoadSession, // PeristenceListener public delegate Session LoadSession(); + SessionDestroyer = DestroySession // PeristenceListener delegate void DestroySession(); }; var client = new Client(options); + // Load the session from persistence + newClient.LoadSession(); // Loads the session using SessionRetriever and sets state internally. await client.RetrieveSessionAsync(); } //... -internal Task SessionPersistor(Session session) +internal bool SaveSession(Session session) { try { From e7d8a960f2ade1b92c6d5dd50d0d7f805b175649 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 22:04:25 -0700 Subject: [PATCH 47/74] Remove imports --- Gotrue/Exceptions/GotrueException.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Gotrue/Exceptions/GotrueException.cs b/Gotrue/Exceptions/GotrueException.cs index cb4462b..4b5f956 100644 --- a/Gotrue/Exceptions/GotrueException.cs +++ b/Gotrue/Exceptions/GotrueException.cs @@ -1,7 +1,5 @@ using System; -using System.Diagnostics; using System.Net.Http; -using Supabase.Gotrue.Responses; namespace Supabase.Gotrue.Exceptions { From 9eec8b73a438186f7b8127c5d71e0b1057b9fe09 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 22:11:48 -0700 Subject: [PATCH 48/74] Doc fix --- Gotrue/Api.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Gotrue/Api.cs b/Gotrue/Api.cs index f2e1fe5..8e34bc2 100644 --- a/Gotrue/Api.cs +++ b/Gotrue/Api.cs @@ -20,7 +20,7 @@ public class Api : IGotrueApi /// /// Function that can be set to return dynamic headers. - /// + /// /// Headers specified in the constructor will ALWAYS take precedence over headers returned by this function. /// public Func>? GetHeaders { get; set; } @@ -39,14 +39,13 @@ protected Dictionary Headers } /// - /// Creates a new user using their email address. + /// Creates a new API client /// /// /// public Api(string url, Dictionary? headers = null) { Url = url; - headers ??= new Dictionary(); _headers = headers; } From 048fd821062984ded495a20b3e897a0e0be096f8 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 22:11:54 -0700 Subject: [PATCH 49/74] Add doc --- Gotrue/Exceptions/FailureReason.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Gotrue/Exceptions/FailureReason.cs b/Gotrue/Exceptions/FailureReason.cs index d9bf8ed..3fbcaea 100644 --- a/Gotrue/Exceptions/FailureReason.cs +++ b/Gotrue/Exceptions/FailureReason.cs @@ -2,6 +2,9 @@ namespace Supabase.Gotrue.Exceptions { + /// + /// Maps Supabase server errors to hints based on the status code and the contents of the error message. + /// public static class FailureHint { public enum Reason From aaaea3adae252fde96b733e722f6ce293b079525 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Thu, 4 May 2023 22:12:00 -0700 Subject: [PATCH 50/74] Add doc --- Gotrue/Exceptions/GotrueException.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Gotrue/Exceptions/GotrueException.cs b/Gotrue/Exceptions/GotrueException.cs index 4b5f956..572fd1a 100644 --- a/Gotrue/Exceptions/GotrueException.cs +++ b/Gotrue/Exceptions/GotrueException.cs @@ -4,6 +4,9 @@ namespace Supabase.Gotrue.Exceptions { + /// + /// Errors from Supabase are wrapped by this exception + /// public class GotrueException : Exception { public GotrueException(string? message) : base(message) { } From 370f04bebf3c0c89073ab2da1c5f4706125f6544 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Fri, 5 May 2023 12:05:23 -0700 Subject: [PATCH 51/74] Reformatted, added a few more words to spell checker --- gotrue-csharp.sln.DotSettings | 66 ++++++++++++----------------------- 1 file changed, 22 insertions(+), 44 deletions(-) diff --git a/gotrue-csharp.sln.DotSettings b/gotrue-csharp.sln.DotSettings index 8b0f6a6..872208d 100644 --- a/gotrue-csharp.sln.DotSettings +++ b/gotrue-csharp.sln.DotSettings @@ -1,72 +1,50 @@ - + AccessorsWithExpressionBody Required False ExpressionBody internal private protected public file new static abstract virtual sealed async override extern unsafe volatile readonly required - Remove - BaseClass - NEXT_LINE + Remove + BaseClass + NEXT_LINE NEXT_LINE - 0 - 1 + 0 + 1 0 0 0 - TOGETHER_SAME_LINE + TOGETHER_SAME_LINE Tab NEXT_LINE - NEXT_LINE + NEXT_LINE True True NEXT_LINE - NEVER - True - True + NEVER + True + True ALWAYS - ALWAYS + ALWAYS ALWAYS NEVER - False - NEVER + False + NEVER False False - False - False - NEXT_LINE - WRAP_IF_LONG - CHOP_ALWAYS + False + False + NEXT_LINE + WRAP_IF_LONG + CHOP_ALWAYS 225 - CHOP_ALWAYS - CHOP_ALWAYS + CHOP_ALWAYS + CHOP_ALWAYS UseVar UseVar UseVar SHA SMS + True True True True From 46da91dab134dd7d96a0cd4ef038d8a01b268116 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Fri, 5 May 2023 12:05:55 -0700 Subject: [PATCH 52/74] Add settings endpoint --- Gotrue/Api.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Gotrue/Api.cs b/Gotrue/Api.cs index 541d807..13bad34 100644 --- a/Gotrue/Api.cs +++ b/Gotrue/Api.cs @@ -16,7 +16,7 @@ namespace Supabase.Gotrue { public class Api : IGotrueApi { - protected string Url { get; private set; } + private string Url { get; } /// /// Function that can be set to return dynamic headers. @@ -573,6 +573,11 @@ public Task DeleteUser(string uid, string jwt) return Helpers.MakeRequest(HttpMethod.Delete, $"{Url}/admin/users/{uid}", data, CreateAuthedRequestHeaders(jwt)); } + public Task Settings() + { + return Helpers.MakeRequest(HttpMethod.Get, $"{Url}/settings"); + } + /// /// Generates a new JWT /// From 851646017d3642ed486747df1fefab1e5aa86c38 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Fri, 5 May 2023 12:06:19 -0700 Subject: [PATCH 53/74] Minor method changes. Added some docs. --- Gotrue/Client.cs | 268 +++++++++++++++++++++++++++++------------------ 1 file changed, 168 insertions(+), 100 deletions(-) diff --git a/Gotrue/Client.cs b/Gotrue/Client.cs index 8f79570..b1145f5 100644 --- a/Gotrue/Client.cs +++ b/Gotrue/Client.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -12,27 +11,100 @@ namespace Supabase.Gotrue { /// - /// The Gotrue Instance + /// GoTrue stateful Client. + /// + /// This class is best used as a long-lived singleton object in your application. You can attach listeners + /// to be notified of changes to the user log in state, a persistence system for sessions across application + /// launches, and more. It includes a (optional, on by default) background thread that runs to refresh the + /// user's session token. + /// + /// Check out the test suite for examples of use. /// /// /// var client = new Supabase.Gotrue.Client(options); /// var user = await client.SignIn("user@email.com", "fancyPassword"); /// - [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract")] public class Client : IGotrueClient { + /// + /// The underlying API requests object that sends the requests + /// + private readonly IGotrueApi _api; + + /// + /// Handlers for notifications of state changes. + /// + private readonly List.AuthEventHandler> _authEventHandlers = new List.AuthEventHandler>(); + /// + /// Gets notifications if there is a failure not visible by exceptions (e.g. background thread refresh failure) + /// private DebugNotification? _debugNotification; - private readonly List.AuthEventHandler> _authEventHandlers = new List.AuthEventHandler>(); + /// + /// Internal timer reference for token refresh + /// + /// AutoRefreshToken + /// + /// + private Timer? _refreshTimer; - public void AddDebugListener(Action listener) + /// + /// Initializes the GoTrue stateful client. + /// + /// You will likely want to at least specify a + /// ClientOptions.Url + /// + /// + /// Sessions are not automatically retrieved when this object is created. + /// + /// If you want to load the session from your persistence store, + /// GotrueSessionPersistence + /// . + /// + /// If you want to load/refresh the session, + /// RetrieveSessionAsync + /// . + /// + /// For a typical client application, you'll want to load the session from persistence + /// and then refresh it. If your application is listening for session changes, you'll + /// get two SignIn notifications if the persisted session is valid - one for the + /// session loaded from disk, and a second on a successful session refresh. + /// + /// + /// + /// var client = new Supabase.Gotrue.Client(options); + /// client.LoadSession(); + /// await client.RetrieveSessionAsync(); + /// + /// + /// + public Client(ClientOptions? options = null) { - _debugNotification ??= new DebugNotification(); - _debugNotification.AddDebugListener(listener); + options ??= new ClientOptions(); + + Options = options; + + if (options.SessionPersistence != null) + { + _authEventHandlers.Add(new PersistenceListener(options.SessionPersistence).EventHandler); + } + + _api = new Api(options.Url, options.Headers); } - public void NotifyStateChange(AuthState stateChanged) + /// + /// The initialized client options. + /// + public ClientOptions Options { get; } + + /// + /// Notifies all listeners that the current user auth state has changed. + /// + /// This is mainly used internally to fire notifications - most client applications won't need this. + /// + /// + public void NotifyAuthStateChange(AuthState stateChanged) { foreach (var handler in _authEventHandlers) { @@ -41,10 +113,22 @@ public void NotifyStateChange(AuthState stateChanged) } /// - /// The current User + /// The currently logged in User. This is a local cache of the current session User. + /// To persist modifications to the User you'll want to use other methods. + /// > /// - public User? CurrentUser { get; private set; } + public User? CurrentUser + { + get => CurrentSession?.User; + } + /// + /// Adds a listener to be notified when the user state changes (e.g. the user logs in, logs out, + /// the token is refreshed, etc). + /// + /// + /// + /// public void AddStateChangedListener(IGotrueClient.AuthEventHandler authEventHandler) { if (_authEventHandlers.Contains(authEventHandler)) @@ -53,6 +137,11 @@ public void AddStateChangedListener(IGotrueClient.AuthEventHandle _authEventHandlers.Add(authEventHandler); } + + /// + /// Removes a specified listener from event state changes. + /// + /// public void RemoveStateChangedListener(IGotrueClient.AuthEventHandler authEventHandler) { if (!_authEventHandlers.Contains(authEventHandler)) @@ -60,74 +149,42 @@ public void RemoveStateChangedListener(IGotrueClient.AuthEventHan _authEventHandlers.Remove(authEventHandler); } + + /// + /// Clears all of the listeners from receiving event state changes. + /// + /// WARNING: The persistence handler is installed as a state change listener if provided in options. + /// Clearing the listeners will also delete the persistence handler. + /// public void ClearStateChangedListeners() { _authEventHandlers.Clear(); } /// - /// The current Session + /// The current Session as managed by this client. Does not refresh tokens or have any other side effects. + /// + /// You probably don't want to directly make changes to this object - you'll want to use other methods + /// on this class to make changes. /// public Session? CurrentSession { get; private set; } /// - /// The initialized client options. - /// - public ClientOptions Options { get; } - - /// - /// Internal timer reference for Refreshing Tokens ( - /// AutoRefreshToken - /// - /// ) - /// - private Timer? _refreshTimer; - - private readonly IGotrueApi _api; - - /// - /// Initializes the Client. - /// - /// Although options are ... optional, you will likely want to at least specify a - /// ClientOptions.Url - /// - /// . - /// - /// Sessions are no longer automatically retrieved on construction, if you want to set the session, - /// - /// - /// - public Client(ClientOptions? options = null) - { - options ??= new ClientOptions(); - - Options = options; - - if (options.PersistSession) - { - var persistenceListener = new PersistenceListener(options.SessionPersistor, options.SessionDestroyer, options.SessionRetriever); - _authEventHandlers.Add(persistenceListener.EventHandler); - } - - _api = new Api(options.Url, options.Headers); - } - - /// - /// Signs up a user by email address + /// Signs up a user by email address. /// /// /// By default, the user needs to verify their email address before logging in. To turn this off, disable Confirm email in your project. /// Confirm email determines if users need to confirm their email address after signing up. /// - If Confirm email is enabled, a user is returned but session is null. /// - If Confirm email is disabled, both a user and a session are returned. - /// When the user confirms their email address, they are redirected to the SITE_URL by default. You can modify your SITE_URL or add additional redirect URLs in your project. + /// When the user confirms their email address, they are redirected to the SITE_URL by default. You can modify your SITE_URL or + /// add additional redirect URLs in your project. /// If signUp() is called for an existing confirmed user: /// - If Confirm email is enabled in your project, an obfuscated/fake user object is returned. /// - If Confirm email is disabled, the error message, User already registered is returned. /// To fetch the currently logged-in user, refer to /// User - /// - /// . + /// . /// /// /// @@ -139,7 +196,9 @@ public Client(ClientOptions? options = null) /// Signs up a user /// /// - /// By default, the user needs to verify their email address before logging in. To turn this off, disable Confirm email in your project. + /// Calling this method will log out the current user session (if any). + /// + /// By default, the user needs to verify their email address before logging in. To turn this off, disable confirm email in your project. /// Confirm email determines if users need to confirm their email address after signing up. /// - If Confirm email is enabled, a user is returned but session is null. /// - If Confirm email is disabled, both a user and a session are returned. @@ -168,7 +227,7 @@ public Client(ClientOptions? options = null) if (session?.User?.ConfirmedAt != null || session?.User != null && Options.AllowUnconfirmedUserSessions) { UpdateSession(session); - NotifyStateChange(SignedIn); + NotifyAuthStateChange(SignedIn); return CurrentSession; } @@ -177,12 +236,11 @@ public Client(ClientOptions? options = null) /// - /// Sends a Magic email login link to the specified email. + /// Sends a magic link login email to the specified email. /// /// /// - /// - public async Task SendMagicLinkEmail(string email, SignInOptions? options = null) + public async Task SignIn(string email, SignInOptions? options = null) { await _api.SendMagicLinkEmail(email, options); return true; @@ -197,17 +255,17 @@ public async Task SendMagicLinkEmail(string email, SignInOptions? options /// Provided from External Library /// Provided from External Library /// Provided from External Library - /// + /// Calling this method will eliminate the current session (if any). /// /// InvalidProviderException /// public async Task SignInWithIdToken(Provider provider, string idToken, string? nonce = null, string? captchaToken = null) { - NotifyStateChange(SignedOut); + DestroySession(); var result = await _api.SignInWithIdToken(provider, idToken, nonce, captchaToken); if (result != null) - NotifyStateChange(SignedIn); + NotifyAuthStateChange(SignedIn); return result; } @@ -227,11 +285,12 @@ public async Task SendMagicLinkEmail(string email, SignInOptions? options /// if you are using phone sign in with the 'whatsapp' channel. The whatsapp /// channel is not supported on other providers at this time. /// + /// Calling this method will wipe out the current session (if any) /// /// public async Task SignInWithOtp(SignInWithPasswordlessEmailOptions options) { - NotifyStateChange(SignedOut); + DestroySession(); return await _api.SignInWithOtp(options); } @@ -250,11 +309,12 @@ public async Task SignInWithOtp(SignInWithPasswordlessE /// if you are using phone sign in with the 'whatsapp' channel. The whatsapp /// channel is not supported on other providers at this time. /// + /// Calling this method will wipe out the current session (if any) /// /// public async Task SignInWithOtp(SignInWithPasswordlessPhoneOptions options) { - NotifyStateChange(SignedOut); + DestroySession(); return await _api.SignInWithOtp(options); } @@ -264,7 +324,7 @@ public async Task SignInWithOtp(SignInWithPasswordlessP /// /// /// - public Task SendMagicLink(string email, SignInOptions? options = null) => SendMagicLinkEmail(email, options); + public Task SendMagicLink(string email, SignInOptions? options = null) => SignIn(email, options); /// /// Signs in a User. @@ -272,7 +332,7 @@ public async Task SignInWithOtp(SignInWithPasswordlessP /// /// /// - public Task SendMagicLinkEmail(string email, string password) => SendMagicLinkEmail(SignInType.Email, email, password); + public Task SignIn(string email, string password) => SignIn(SignInType.Email, email, password); /// /// Log in an existing user with an email and password or phone and password. @@ -280,7 +340,7 @@ public async Task SignInWithOtp(SignInWithPasswordlessP /// /// /// - public Task SignInWithPassword(string email, string password) => SendMagicLinkEmail(email, password); + public Task SignInWithPassword(string email, string password) => SignIn(email, password); /// /// Log in an existing user, or login via a third-party provider. @@ -290,7 +350,7 @@ public async Task SignInWithOtp(SignInWithPasswordlessP /// Password to account (optional if `RefreshToken`) /// A space-separated list of scopes granted to the OAuth application. /// - public async Task SendMagicLinkEmail(SignInType type, string identifierOrToken, string? password = null, string? scopes = null) + public async Task SignIn(SignInType type, string identifierOrToken, string? password = null, string? scopes = null) { Session? session; switch (type) @@ -319,7 +379,7 @@ public async Task SignInWithOtp(SignInWithPasswordlessP if (session?.User?.ConfirmedAt != null || session?.User != null && Options.AllowUnconfirmedUserSessions) { - NotifyStateChange(SignedIn); + NotifyAuthStateChange(SignedIn); return CurrentSession; } @@ -335,7 +395,7 @@ public async Task SignInWithOtp(SignInWithPasswordlessP /// /// /// - public Task SendMagicLinkEmail(Provider provider, SignInOptions? options = null) + public Task SignIn(Provider provider, SignInOptions? options = null) { DestroySession(); @@ -359,7 +419,7 @@ public Task SendMagicLinkEmail(Provider provider, SignInOptio if (session?.AccessToken != null) { UpdateSession(session); - NotifyStateChange(SignedIn); + NotifyAuthStateChange(SignedIn); return session; } @@ -382,7 +442,7 @@ public Task SendMagicLinkEmail(Provider provider, SignInOptio if (session?.AccessToken != null) { UpdateSession(session); - NotifyStateChange(SignedIn); + NotifyAuthStateChange(SignedIn); return session; } @@ -399,7 +459,7 @@ public async Task SignOut() await _api.SignOut(CurrentSession.AccessToken); _refreshTimer?.Dispose(); UpdateSession(null); - NotifyStateChange(SignedOut); + NotifyAuthStateChange(SignedOut); } /// @@ -413,8 +473,8 @@ public async Task SignOut() throw new Exception("Not Logged in."); var result = await _api.UpdateUser(CurrentSession.AccessToken!, attributes); - CurrentUser = result; - NotifyStateChange(UserUpdated); + CurrentSession.User = result; + NotifyAuthStateChange(UserUpdated); return result; } @@ -550,7 +610,7 @@ public async Task ResetPasswordForEmail(string email) await RefreshToken(); var user = await _api.GetUser(CurrentSession.AccessToken!); - CurrentUser = user; + CurrentSession.User = user; return CurrentSession; } @@ -568,7 +628,7 @@ public Session SetAuth(string accessToken) CurrentSession.TokenType = "bearer"; CurrentSession.User = CurrentUser; - NotifyStateChange(TokenRefreshed); + NotifyAuthStateChange(TokenRefreshed); return CurrentSession; } @@ -621,10 +681,10 @@ public Session SetAuth(string accessToken) if (storeSession) { UpdateSession(session); - NotifyStateChange(SignedIn); + NotifyAuthStateChange(SignedIn); if (query.Get("type") == "recovery") - NotifyStateChange(PasswordRecovery); + NotifyAuthStateChange(PasswordRecovery); } return session; @@ -666,9 +726,8 @@ public Session SetAuth(string accessToken) return null; } CurrentSession = session; - CurrentUser = session.User; - NotifyStateChange(SignedIn); + NotifyAuthStateChange(SignedIn); InitRefreshTimer(); return CurrentSession; } @@ -685,13 +744,31 @@ public Session SetAuth(string accessToken) if (result != null) { UpdateSession(result); - NotifyStateChange(SignedIn); + NotifyAuthStateChange(SignedIn); return CurrentSession; } return null; } + public Func>? GetHeaders + { + get => _api.GetHeaders; + set => throw new ArgumentException(); + } + + /// + /// Add a listener to get errors that occur outside of a typical Exception flow. + /// In particular, this is used to get errors and messages from the background thread + /// that automatically manages refreshing the user's token. + /// + /// + public void AddDebugListener(Action listener) + { + _debugNotification ??= new DebugNotification(); + _debugNotification.AddDebugListener(listener); + } + /// /// Saves the session /// @@ -701,15 +778,13 @@ private void UpdateSession(Session? session) if (session == null) { CurrentSession = null; - CurrentUser = null; - NotifyStateChange(SignedOut); + NotifyAuthStateChange(SignedOut); return; } var dirty = CurrentSession != session; CurrentSession = session; - CurrentUser = session.User; var expiration = session.ExpiresIn; @@ -717,7 +792,7 @@ private void UpdateSession(Session? session) InitRefreshTimer(); if (dirty) - NotifyStateChange(UserUpdated); + NotifyAuthStateChange(UserUpdated); } /// @@ -745,9 +820,8 @@ private async Task RefreshToken(string? refreshToken = null) throw new Exception("Could not refresh token from provided session."); CurrentSession = result; - CurrentUser = result.User; - NotifyStateChange(TokenRefreshed); + NotifyAuthStateChange(TokenRefreshed); if (Options.AutoRefreshToken && CurrentSession.ExpiresIn != default) InitRefreshTimer(); @@ -792,19 +866,13 @@ private async void HandleRefreshTimerTick(object _) catch (Exception ex) { _debugNotification?.Log(ex.Message, ex); - NotifyStateChange(SignedOut); + NotifyAuthStateChange(SignedOut); } } - - public Func>? GetHeaders - { - get => _api.GetHeaders; - set => throw new ArgumentException(); - } public void LoadSession() { - if(Options.SessionRetriever != null) - UpdateSession(Options.SessionRetriever.Invoke()); + if(Options.SessionPersistence != null) + UpdateSession(Options.SessionPersistence.Load.Invoke()); } } } From 932067408496e20d2b1199c8d2b5894c53441dc5 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Fri, 5 May 2023 12:06:37 -0700 Subject: [PATCH 54/74] Simplified to holder class --- Gotrue/ClientOptions.cs | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/Gotrue/ClientOptions.cs b/Gotrue/ClientOptions.cs index b810dc9..80d360c 100644 --- a/Gotrue/ClientOptions.cs +++ b/Gotrue/ClientOptions.cs @@ -24,24 +24,9 @@ public class ClientOptions public bool AutoRefreshToken { get; set; } = true; /// - /// Should the Client call , , and ? + /// Object called to persist the session (e.g. filesystem or cookie) /// - public bool PersistSession { get; set; } = true; - - /// - /// Function called to persist the session (probably on a filesystem or cookie) - /// - public PersistenceListener.SaveSession? SessionPersistor; - - /// - /// Function to retrieve a session (probably from the filesystem or cookie) - /// - public PersistenceListener.LoadSession? SessionRetriever; - - /// - /// Function to destroy a session. - /// - public PersistenceListener.DestroySession? SessionDestroyer; + public GotrueSessionPersistence? SessionPersistence; /// /// Very unlikely this flag needs to be changed except in very specific contexts. From 6db9ff40c3b1ebe79c23ccb9a117fefa4e9562c8 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Fri, 5 May 2023 12:07:21 -0700 Subject: [PATCH 55/74] Added User hint to clarify user needs to fix --- Gotrue/Exceptions/FailureReason.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Gotrue/Exceptions/FailureReason.cs b/Gotrue/Exceptions/FailureReason.cs index 3fbcaea..8201890 100644 --- a/Gotrue/Exceptions/FailureReason.cs +++ b/Gotrue/Exceptions/FailureReason.cs @@ -10,10 +10,10 @@ public static class FailureHint public enum Reason { Unknown, - BadPassword, - BadEmailAddress, - MissingInformation, - AlreadyRegistered, + UserBadPassword, + UserBadEmailAddress, + UserMissingInformation, + UserAlreadyRegistered, InvalidRefreshToken, AdminTokenRequired } @@ -25,12 +25,12 @@ public static Reason DetectReason(GotrueException gte) return gte.StatusCode switch { - 400 when gte.Content.Contains("User already registered") => AlreadyRegistered, + 400 when gte.Content.Contains("User already registered") => UserAlreadyRegistered, 400 when gte.Content.Contains("Invalid Refresh Token") => InvalidRefreshToken, 401 when gte.Content.Contains("This endpoint requires a Bearer token") => AdminTokenRequired, - 422 when gte.Content.Contains("Password should be at least") => BadPassword, - 422 when gte.Content.Contains("Unable to validate email address") => BadEmailAddress, - 422 when gte.Content.Contains("provide your email or phone number") => MissingInformation, + 422 when gte.Content.Contains("Password should be at least") => UserBadPassword, + 422 when gte.Content.Contains("Unable to validate email address") => UserBadEmailAddress, + 422 when gte.Content.Contains("provide your email or phone number") => UserMissingInformation, _ => Unknown }; From 80f21ba79fc520b895d80af6e8a1f36a5f9080d1 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Fri, 5 May 2023 12:07:53 -0700 Subject: [PATCH 56/74] Minor method renames, changed magic link back to sign in --- Gotrue/Interfaces/IGotrueClient.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Gotrue/Interfaces/IGotrueClient.cs b/Gotrue/Interfaces/IGotrueClient.cs index 1e31463..c276c8a 100644 --- a/Gotrue/Interfaces/IGotrueClient.cs +++ b/Gotrue/Interfaces/IGotrueClient.cs @@ -17,7 +17,7 @@ public interface IGotrueClient : IGettableHeaders public void AddStateChangedListener(AuthEventHandler authEventHandler); public void RemoveStateChangedListener(AuthEventHandler authEventHandler); public void ClearStateChangedListeners(); - public void NotifyStateChange(AuthState stateChanged); + public void NotifyAuthStateChange(AuthState stateChanged); Task CreateUser(string jwt, AdminUserAttributes attributes); Task CreateUser(string jwt, string email, string password, AdminUserAttributes? attributes = null); @@ -32,13 +32,13 @@ public interface IGotrueClient : IGettableHeaders Task RetrieveSessionAsync(); Task SendMagicLink(string email, SignInOptions? options = null); TSession SetAuth(string accessToken); - Task SendMagicLinkEmail(SignInType type, string identifierOrToken, string? password = null, string? scopes = null); - Task SendMagicLinkEmail(string email, SignInOptions? options = null); - Task SendMagicLinkEmail(string email, string password); + Task SignIn(SignInType type, string identifierOrToken, string? password = null, string? scopes = null); + Task SignIn(string email, SignInOptions? options = null); + Task SignIn(string email, string password); Task SignInWithOtp(SignInWithPasswordlessEmailOptions options); Task SignInWithOtp(SignInWithPasswordlessPhoneOptions options); Task SignInWithPassword(string email, string password); - Task SendMagicLinkEmail(Provider provider, SignInOptions? options = null); + Task SignIn(Provider provider, SignInOptions? options = null); Task SignInWithIdToken(Provider provider, string idToken, string? nonce = null, string? captchaToken = null); Task ExchangeCodeForSession(string codeVerifier, string authCode); Task SignUp(SignUpType type, string identifier, string password, SignUpOptions? options = null); From be3c498305a672bbbfc665c07da5341a893e7d7b Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Fri, 5 May 2023 12:08:10 -0700 Subject: [PATCH 57/74] Session wrapper object --- Gotrue/GotrueSessionPersistence.cs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 Gotrue/GotrueSessionPersistence.cs diff --git a/Gotrue/GotrueSessionPersistence.cs b/Gotrue/GotrueSessionPersistence.cs new file mode 100644 index 0000000..f31ec23 --- /dev/null +++ b/Gotrue/GotrueSessionPersistence.cs @@ -0,0 +1,23 @@ +namespace Supabase.Gotrue +{ + public class GotrueSessionPersistence + { + public delegate bool SaveSession(Session session); + + public delegate void DestroySession(); + + public delegate Session LoadSession(); + + public readonly SaveSession Save; + public readonly DestroySession Destroy; + public readonly LoadSession Load; + + public GotrueSessionPersistence(SaveSession save, LoadSession load, DestroySession destroy) + { + Save = save; + Destroy = destroy; + Load = load; + } + } + +} From c820ab456fb6a2a6b1153e6a5194de1d4bc62092 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Fri, 5 May 2023 12:08:18 -0700 Subject: [PATCH 58/74] reformat --- Gotrue/Interfaces/IGotrueApi.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Gotrue/Interfaces/IGotrueApi.cs b/Gotrue/Interfaces/IGotrueApi.cs index 9540bc3..0d3f8a7 100644 --- a/Gotrue/Interfaces/IGotrueApi.cs +++ b/Gotrue/Interfaces/IGotrueApi.cs @@ -34,5 +34,8 @@ public interface IGotrueApi : IGettableHeaders ProviderAuthState GetUriForProvider(Provider provider, SignInOptions? options = null); Task ExchangeCodeForSession(string codeVerifier, string authCode); + + Task Settings(); } + } \ No newline at end of file From 3f46a60318033ae23cff06d33070485efe313a77 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Fri, 5 May 2023 12:08:39 -0700 Subject: [PATCH 59/74] Simplified/tweaked to use persistence object --- Gotrue/PersistenceListener.cs | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/Gotrue/PersistenceListener.cs b/Gotrue/PersistenceListener.cs index 9c4c9de..63be7a2 100644 --- a/Gotrue/PersistenceListener.cs +++ b/Gotrue/PersistenceListener.cs @@ -5,9 +5,7 @@ namespace Supabase.Gotrue { public class PersistenceListener { - private readonly SaveSession? _save; - private readonly LoadSession? _load; - private readonly DestroySession? _destroy; + private readonly GotrueSessionPersistence _persistence; public delegate bool SaveSession(Session session); @@ -15,11 +13,9 @@ public class PersistenceListener public delegate Session LoadSession(); - public PersistenceListener(SaveSession? s, DestroySession? d, LoadSession? l) + public PersistenceListener(GotrueSessionPersistence persistence) { - _save = s; - _load = l; - _destroy = d; + _persistence = persistence; } public void EventHandler(IGotrueClient sender, Constants.AuthState stateChanged) { @@ -31,10 +27,10 @@ public void EventHandler(IGotrueClient sender, Constants.AuthStat if (sender.CurrentSession == null) throw new ArgumentException("Tried to save a null session (2)"); - _save?.Invoke(sender.CurrentSession); + _persistence.Save(sender.CurrentSession); break; case Constants.AuthState.SignedOut: - _destroy?.Invoke(); + _persistence.Destroy.Invoke(); break; case Constants.AuthState.UserUpdated: if (sender == null) @@ -42,16 +38,19 @@ public void EventHandler(IGotrueClient sender, Constants.AuthStat if (sender.CurrentSession == null) throw new ArgumentException("Tried to save a null session (2)"); - _save?.Invoke(sender.CurrentSession); + _persistence.Save.Invoke(sender.CurrentSession); break; case Constants.AuthState.PasswordRecovery: break; case Constants.AuthState.TokenRefreshed: - if (sender == null) - throw new ArgumentException("Tried to save a null session (1)"); if (sender.CurrentSession == null) - throw new ArgumentException("Tried to save a null session (2)"); - - _save?.Invoke(sender.CurrentSession); + { + // If token refresh results in a null session, log out. + EventHandler(sender, Constants.AuthState.SignedOut); + } + else + { + _persistence.Save.Invoke(sender.CurrentSession); + } break; default: throw new ArgumentOutOfRangeException(nameof(stateChanged), stateChanged, null); } From 5722fd49d1ca6e8f55e3da6a2a7c7c9a00e2a01e Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Fri, 5 May 2023 12:08:52 -0700 Subject: [PATCH 60/74] Settings object JSON wrapper --- Gotrue/Settings.cs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 Gotrue/Settings.cs diff --git a/Gotrue/Settings.cs b/Gotrue/Settings.cs new file mode 100644 index 0000000..47a6e29 --- /dev/null +++ b/Gotrue/Settings.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Supabase.Gotrue +{ + public class Settings + { + [JsonProperty("disable_signup")] + public bool? DisableSignup { get; set; } + + [JsonProperty("mailer_autoconfirm")] + public bool? MailerAutoConfirm { get; set; } + + [JsonProperty("phone_autoconfirm")] + public bool? PhoneAutoConfirm { get; set; } + + [JsonProperty("sms_provider")] + public string? SmsProvider { get; set; } + + [JsonProperty("external")] + public Dictionary? Services { get; set; } + + [JsonProperty("external_labels")] + public Dictionary? Labels { get; set; } + } +} From 5f71925cce6d777f3ab48b76cc99f9a5bebaf77b Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Fri, 5 May 2023 12:08:58 -0700 Subject: [PATCH 61/74] Add settings API request --- Gotrue/StatelessClient.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Gotrue/StatelessClient.cs b/Gotrue/StatelessClient.cs index 9f73797..e8b77ee 100644 --- a/Gotrue/StatelessClient.cs +++ b/Gotrue/StatelessClient.cs @@ -16,6 +16,11 @@ namespace Supabase.Gotrue /// public class StatelessClient : IGotrueStatelessClient { + public async Task Settings(StatelessClientOptions options) + { + var api = GetApi(options); + return await api.Settings(); + } public IGotrueApi GetApi(StatelessClientOptions options) => new Api(options.Url, options.Headers); From c66e371b39f0764eb1c96d714ffcbbd698877b36 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Fri, 5 May 2023 12:09:28 -0700 Subject: [PATCH 62/74] Update Reason name, moved test to failure test --- GotrueTests/AnonKeyClientFailureTests.cs | 27 +++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/GotrueTests/AnonKeyClientFailureTests.cs b/GotrueTests/AnonKeyClientFailureTests.cs index 6e45f83..af38566 100644 --- a/GotrueTests/AnonKeyClientFailureTests.cs +++ b/GotrueTests/AnonKeyClientFailureTests.cs @@ -35,7 +35,8 @@ private bool AuthStateIsEmpty() [TestInitialize] public void TestInitializer() { - _client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true, PersistSession = true, SessionPersistor = SaveSession, SessionRetriever = LoadSession, SessionDestroyer = DestroySession }); + var persistence = new GotrueSessionPersistence(SaveSession, LoadSession, DestroySession); + _client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true, SessionPersistence = persistence}); _client.AddDebugListener(LogDebug); _client.AddStateChangedListener(AuthStateListener); } @@ -69,7 +70,7 @@ public async Task SignUpUserEmailBadPassword() { await _client.SignUp(email, "x"); }); - AreEqual(BadPassword, x.Reason); + AreEqual(UserBadPassword, x.Reason); IsNull(_savedSession); Contains(_stateChanges, SignedOut); AreEqual(1, _stateChanges.Count); @@ -82,7 +83,7 @@ public async Task SignUpUserEmailBadEmailAddress() { await _client.SignUp("not a real email address", PASSWORD); }); - AreEqual(BadEmailAddress, x.Reason); + AreEqual(UserBadEmailAddress, x.Reason); IsNull(_savedSession); Contains(_stateChanges, SignedOut); AreEqual(1, _stateChanges.Count); @@ -98,7 +99,7 @@ public async Task SignUpUserPhone() { await _client.SignUp(Constants.SignUpType.Phone, phone1, PASSWORD, new SignUpOptions { Data = new Dictionary { { "firstName", "Testing" } } }); }); - AreEqual(MissingInformation, x.Reason); + AreEqual(UserMissingInformation, x.Reason); IsNull(_savedSession); Contains(_stateChanges, SignedOut); AreEqual(1, _stateChanges.Count); @@ -121,7 +122,7 @@ public async Task SignsUpUserTwiceShouldReturnBadRequest() await _client.SignUp(email, PASSWORD); }); - AreEqual(AlreadyRegistered, x.Reason); + AreEqual(UserAlreadyRegistered, x.Reason); IsNull(_savedSession); Contains(_stateChanges, SignedOut); AreEqual(1, _stateChanges.Count); @@ -150,5 +151,21 @@ public async Task ClientSendsResetPasswordForEmail() var result = await _client.ResetPasswordForEmail(email); IsTrue(result); } + + + [TestMethod("Client: Throws Exception on Invalid Username and Password")] + public async Task ClientSignsInUserWrongPassword() + { + var user = $"{RandomString(12)}@supabase.io"; + await _client.SignUp(user, PASSWORD); + await _client.SignOut(); + + await ThrowsExceptionAsync(async () => + { + var result = await _client.SignIn(user, PASSWORD + "$"); + IsNotNull(result); + }); + } + } } From dd9da1a2d3f45a8e79d142ab38e2a5699732c3d6 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Fri, 5 May 2023 12:09:58 -0700 Subject: [PATCH 63/74] Changes to tests per other changes --- GotrueTests/AnonKeyClientTests.cs | 169 ++++++++++------------- GotrueTests/ConfigurationFailureTests.cs | 7 +- 2 files changed, 78 insertions(+), 98 deletions(-) diff --git a/GotrueTests/AnonKeyClientTests.cs b/GotrueTests/AnonKeyClientTests.cs index 4a92fad..ce181c7 100644 --- a/GotrueTests/AnonKeyClientTests.cs +++ b/GotrueTests/AnonKeyClientTests.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using Supabase.Gotrue; -using Supabase.Gotrue.Exceptions; using Supabase.Gotrue.Interfaces; using static GotrueTests.TestUtils; using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert; @@ -35,7 +34,8 @@ private bool AuthStateIsEmpty() [TestInitialize] public void TestInitializer() { - _client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true, PersistSession = true, SessionPersistor = SaveSession, SessionRetriever = LoadSession, SessionDestroyer = DestroySession }); + var persistence = new GotrueSessionPersistence(SaveSession, LoadSession, DestroySession); + _client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true, SessionPersistence = persistence }); _client.AddDebugListener(LogDebug); _client.AddStateChangedListener(AuthStateListener); } @@ -61,6 +61,25 @@ private bool SaveSession(Session session) private readonly List _stateChanges = new List(); private Session _savedSession; + private void VerifyGoodSession(Session session) + { + Contains(_stateChanges, SignedIn); + AreEqual(_client.CurrentSession, session); + AreEqual(_client.CurrentSession, _savedSession); + AreEqual(_client.CurrentUser, session.User); + IsNotNull(session.AccessToken); + IsNotNull(session.RefreshToken); + IsNotNull(session.User); + } + + private void VerifySignedOut() + { + Contains(_stateChanges, SignedOut); + IsNull(_savedSession); + IsNull(_client.CurrentSession); + IsNull(_client.CurrentUser); + } + [TestMethod("Client: Sign Up User")] public async Task SignUpUserEmail() { @@ -69,12 +88,7 @@ public async Task SignUpUserEmail() var email = $"{RandomString(12)}@supabase.io"; var session = await _client.SignUp(email, PASSWORD); - Contains(_stateChanges, SignedIn); - AreEqual(_client.CurrentSession, _savedSession); - - IsNotNull(session.AccessToken); - IsNotNull(session.RefreshToken); - IsNotNull(session.User); + VerifyGoodSession(session); } [TestMethod("Client: Load User From Persistence")] @@ -85,24 +99,21 @@ public async Task SaveAndLoadUser() var email = $"{RandomString(12)}@supabase.io"; var session = await _client.SignUp(email, PASSWORD); - Contains(_stateChanges, SignedIn); - AreEqual(_client.CurrentSession, _savedSession); - - IsNotNull(session.AccessToken); - IsNotNull(session.RefreshToken); - IsNotNull(session.User); + VerifyGoodSession(session); - var newClient = new Client(new ClientOptions - { - AllowUnconfirmedUserSessions = true, PersistSession = true, - SessionPersistor = SaveSession, SessionRetriever = LoadSession, SessionDestroyer = DestroySession - }); + var persistence = new GotrueSessionPersistence(SaveSession, LoadSession, DestroySession); + var newClient = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true, SessionPersistence = persistence }); newClient.AddDebugListener(LogDebug); newClient.AddStateChangedListener(AuthStateListener); // Loads the session from storage newClient.LoadSession(); - await newClient.RetrieveSessionAsync(); + VerifyGoodSession(newClient.CurrentSession); + + // Refresh the session + var refreshedSession = await newClient.RetrieveSessionAsync(); + + VerifyGoodSession(refreshedSession); } [TestMethod("Client: Sign up Phone")] @@ -113,10 +124,8 @@ public async Task SignUpUserPhone() var phone1 = GetRandomPhoneNumber(); var session = await _client.SignUp(Constants.SignUpType.Phone, phone1, PASSWORD, new SignUpOptions { Data = new Dictionary { { "firstName", "Testing" } } }); - Contains(_stateChanges, SignedIn); - AreEqual(_client.CurrentSession, _savedSession); + VerifyGoodSession(session); - IsNotNull(session.AccessToken); AreEqual("Testing", session.User.UserMetadata["firstName"]); } @@ -129,10 +138,9 @@ public async Task ClientTriggersTokenRefreshedEvent() IsTrue(AuthStateIsEmpty()); - var user = await _client.SignUp(email, PASSWORD); + var session = await _client.SignUp(email, PASSWORD); - Contains(_stateChanges, SignedIn); - AreEqual(_client.CurrentSession, _savedSession); + VerifyGoodSession(session); _client.AddStateChangedListener((_, args) => { @@ -150,59 +158,53 @@ public async Task ClientTriggersTokenRefreshedEvent() var newToken = await tsc.Task; IsNotNull(newToken); - AreNotEqual(user.RefreshToken, _client.CurrentSession.RefreshToken); + AreNotEqual(session.RefreshToken, _client.CurrentSession.RefreshToken); } [TestMethod("Client: Signs In User (Email, Phone, Refresh token)")] public async Task ClientSignsIn() { var email = $"{RandomString(12)}@supabase.io"; - await _client.SignUp(email, PASSWORD); - Contains(_stateChanges, SignedIn); - AreEqual(_client.CurrentSession, _savedSession); + var emailSession = await _client.SignUp(email, PASSWORD); + + VerifyGoodSession(emailSession); _stateChanges.Clear(); await _client.SignOut(); - Contains(_stateChanges, SignedOut); - AreEqual(_client.CurrentSession, _savedSession); + + VerifySignedOut(); + _stateChanges.Clear(); - var session = await _client.SendMagicLinkEmail(email, PASSWORD); + var session2 = await _client.SignIn(email, PASSWORD); - IsNotNull(session.AccessToken); - IsNotNull(session.RefreshToken); - IsNotNull(session.User); + VerifyGoodSession(session2); - Contains(_stateChanges, SignedIn); - AreEqual(_client.CurrentSession, _savedSession); _stateChanges.Clear(); // Phones var phone = GetRandomPhoneNumber(); - await _client.SignUp(Constants.SignUpType.Phone, phone, PASSWORD); - Contains(_stateChanges, SignedIn); - AreEqual(_client.CurrentSession, _savedSession); + var phoneSession = await _client.SignUp(Constants.SignUpType.Phone, phone, PASSWORD); + + VerifyGoodSession(phoneSession); + _stateChanges.Clear(); await _client.SignOut(); - Contains(_stateChanges, SignedOut); - IsNull(_savedSession); - AreEqual(_client.CurrentSession, _savedSession); - _stateChanges.Clear(); - session = await _client.SendMagicLinkEmail(Constants.SignInType.Phone, phone, PASSWORD); - Contains(_stateChanges, SignedIn); - AreEqual(_client.CurrentSession, _savedSession); + VerifySignedOut(); + _stateChanges.Clear(); - IsNotNull(session.AccessToken); - IsNotNull(session.RefreshToken); - IsNotNull(session.User); + emailSession = await _client.SignIn(Constants.SignInType.Phone, phone, PASSWORD); + + VerifyGoodSession(emailSession); + _stateChanges.Clear(); // Refresh Token - var refreshToken = session.RefreshToken; + var refreshToken = emailSession.RefreshToken; - var newSession = await _client.SendMagicLinkEmail(Constants.SignInType.RefreshToken, refreshToken); + var newSession = await _client.SignIn(Constants.SignInType.RefreshToken, refreshToken); AreEqual(_client.CurrentSession, _savedSession); Contains(_stateChanges, TokenRefreshed); DoesNotContain(_stateChanges, SignedIn); @@ -216,17 +218,18 @@ public async Task ClientSignsIn() public async Task ClientSendsMagicLoginEmail() { var user = $"{RandomString(12)}@supabase.io"; - await _client.SignUp(user, PASSWORD); - Contains(_stateChanges, SignedIn); - AreEqual(_client.CurrentSession, _savedSession); + var session = await _client.SignUp(user, PASSWORD); + + VerifyGoodSession(session); _stateChanges.Clear(); await _client.SignOut(); - Contains(_stateChanges, SignedOut); - AreEqual(_client.CurrentSession, _savedSession); + + VerifySignedOut(); + _stateChanges.Clear(); - var result = await _client.SendMagicLinkEmail(user); + var result = await _client.SignIn(user); IsTrue(result); AreEqual(0, _stateChanges.Count); AreEqual(_client.CurrentSession, _savedSession); @@ -237,16 +240,16 @@ public async Task ClientSendsMagicLoginEmailAlias() { var user = $"{RandomString(12)}@supabase.io"; var user2 = $"{RandomString(12)}@supabase.io"; - await _client.SignUp(user, PASSWORD); - Contains(_stateChanges, SignedIn); - AreEqual(_client.CurrentSession, _savedSession); + var session = await _client.SignUp(user, PASSWORD); + + VerifyGoodSession(session); + _stateChanges.Clear(); await _client.SignOut(); - Contains(_stateChanges, SignedOut); - IsNull(_savedSession); - AreEqual(_client.CurrentSession, _savedSession); + VerifySignedOut(); + var result = await _client.SendMagicLink(user); var result2 = await _client.SendMagicLink(user2, new SignInOptions { RedirectTo = $"com.{RandomString(12)}.deeplink://login" }); @@ -257,20 +260,19 @@ public async Task ClientSendsMagicLoginEmailAlias() [TestMethod("Client: Returns Auth Url for Provider")] public async Task ClientReturnsAuthUrlForProvider() { - var result1 = await _client.SendMagicLinkEmail(Constants.Provider.Google); + var result1 = await _client.SignIn(Constants.Provider.Google); AreEqual("http://localhost:9999/authorize?provider=google", result1.Uri.ToString()); - var result2 = await _client.SendMagicLinkEmail(Constants.Provider.Google, new SignInOptions { Scopes = "special scopes please" }); + var result2 = await _client.SignIn(Constants.Provider.Google, new SignInOptions { Scopes = "special scopes please" }); AreEqual("http://localhost:9999/authorize?provider=google&scopes=special+scopes+please", result2.Uri.ToString()); } [TestMethod("Client: Returns Verification Code for Provider")] public async Task ClientReturnsPKCEVerifier() { - var result = await _client.SendMagicLinkEmail(Constants.Provider.Github, new SignInOptions { FlowType = Constants.OAuthFlowType.PKCE }); + var result = await _client.SignIn(Constants.Provider.Github, new SignInOptions { FlowType = Constants.OAuthFlowType.PKCE }); - Contains(_stateChanges, SignedOut); - IsNull(_savedSession); + VerifySignedOut(); IsTrue(!string.IsNullOrEmpty(result.PKCEVerifier)); IsTrue(result.Uri.Query.Contains("flow_type=pkce")); @@ -284,9 +286,9 @@ public async Task ClientUpdateUser() { var email = $"{RandomString(12)}@supabase.io"; var session = await _client.SignUp(email, PASSWORD); - IsNotNull(session); - Contains(_stateChanges, SignedIn); - AreEqual(_client.CurrentSession, _savedSession); + + VerifyGoodSession(session); + _stateChanges.Clear(); var attributes = new UserAttributes { Data = new Dictionary { { "hello", "world" } } }; @@ -324,27 +326,9 @@ public async Task ClientGetUserAfterLogOut() _stateChanges.Clear(); await _client.SignOut(); - Contains(_stateChanges, SignedOut); - - IsNull(_client.CurrentUser); - } - - [TestMethod("Client: Throws Exception on Invalid Username and Password")] - public async Task ClientSignsInUserWrongPassword() - { - var user = $"{RandomString(12)}@supabase.io"; - await _client.SignUp(user, PASSWORD); - - await _client.SignOut(); - - await ThrowsExceptionAsync(async () => - { - var result = await _client.SendMagicLinkEmail(user, PASSWORD + "$"); - IsNotNull(result); - }); + VerifySignedOut(); } - [TestMethod("Client: Send Reset Password Email")] public async Task ClientSendsResetPasswordForEmail() { @@ -353,6 +337,5 @@ public async Task ClientSendsResetPasswordForEmail() var result = await _client.ResetPasswordForEmail(email); IsTrue(result); } - } } diff --git a/GotrueTests/ConfigurationFailureTests.cs b/GotrueTests/ConfigurationFailureTests.cs index af9437a..0f36382 100644 --- a/GotrueTests/ConfigurationFailureTests.cs +++ b/GotrueTests/ConfigurationFailureTests.cs @@ -16,7 +16,7 @@ public class ConfigurationFailureTests [TestMethod("Bad URL message")] public async Task BadUrlTest() { - var client = new Client(new ClientOptions { Url = "https://badprojecturl.supabase.co", AllowUnconfirmedUserSessions = true, PersistSession = false }); + var client = new Client(new ClientOptions { Url = "https://badprojecturl.supabase.co", AllowUnconfirmedUserSessions = true }); client.AddDebugListener(LogDebug); var email = $"{RandomString(12)}@supabase.io"; @@ -29,7 +29,7 @@ await ThrowsExceptionAsync(async () => [TestMethod("Bad service key message")] public async Task BadServiceApiKeyTest() { - var client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true, PersistSession = false }); + var client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true }); client.AddDebugListener(LogDebug); var x = await ThrowsExceptionAsync(async () => @@ -38,8 +38,5 @@ public async Task BadServiceApiKeyTest() }); AreEqual(AdminTokenRequired, x.Reason); } - - - } } From 0a8df1c78f589d3a1897f6f66e2eb8c216905785 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Fri, 5 May 2023 12:10:07 -0700 Subject: [PATCH 64/74] Add settings api test case --- GotrueTests/StatelessClientTests.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/GotrueTests/StatelessClientTests.cs b/GotrueTests/StatelessClientTests.cs index 54d6ca2..2c4df73 100644 --- a/GotrueTests/StatelessClientTests.cs +++ b/GotrueTests/StatelessClientTests.cs @@ -70,6 +70,18 @@ public void TestInitializer() private static StatelessClientOptions Options { get => new StatelessClientOptions() { AllowUnconfirmedUserSessions = true }; } + [TestMethod("StatelessClient: Settings")] + public async Task Settings() + { + var settings = await _client.Settings(Options); + Assert.IsNotNull(settings); + Assert.IsFalse(settings.Services["zoom"]); + Assert.IsTrue(settings.Services["email"]); + Assert.IsFalse(settings.DisableSignup); + Assert.IsTrue(settings.MailerAutoConfirm); + Assert.IsTrue(settings.PhoneAutoConfirm); + Assert.IsNotNull(settings.SmsProvider); + } [TestMethod("StatelessClient: Signs Up User")] public async Task SignsUpUser() From 85f5cb8afb421097e29b8ff5a08b58a5f6e35e28 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Fri, 5 May 2023 12:32:32 -0700 Subject: [PATCH 65/74] Update README to reflect changes --- README.md | 122 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 76 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 0e444b9..02ca414 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,13 @@ ## BREAKING CHANGES v3.1 → v3.x - Exceptions have been simplified to a single GotrueException. A Reason field has been added -to GotrueException to help sort out what happened. This should also be easier to manage as the Gotrue -server API & messages evolve. + to GotrueException to help sort out what happened. This should also be easier to manage as the Gotrue + server API & messages evolve. - The delegates for save/load/destroy persistence have been simplified to no longer require async. -- Console logging in a few places (most notable the background refresh thread) has been removed -in favor of a notification method. See Client.AddDebugListener() and the test cases for examples. -This will allow you to implement your own logging strategy (write to temp file, console, user visible -err console, etc). +- Console logging in a few places (most notable the background refresh thread) has been removed + in favor of a notification method. See Client.AddDebugListener() and the test cases for examples. + This will allow you to implement your own logging strategy (write to temp file, console, user visible + err console, etc). - The client now more reliably emits AuthState changes. - There is now a single source of truth for headers in the stateful Client - the Options headers. @@ -32,10 +32,12 @@ Implementation notes: ## BREAKING CHANGES v3.0 → 3.1 -- We've implemented the PKCE auth flow. SignIn using a provider now returns an instance of `ProviderAuthState` rather than a `string`. +- We've implemented the PKCE auth flow. SignIn using a provider now returns an instance of `ProviderAuthState` rather + than a `string`. - The provider sign in signature has moved `scopes` into `SignInOptions` In Short: + ```c# # What was: var url = await client.SignIn(Provider.Github, "scopes and things"); @@ -49,7 +51,9 @@ var state = await client.SignIn(Provider.Github, new SignInOptions { "scopes and ## Getting Started -To use this library on the Supabase Hosted service but separately from the `supabase-csharp`, you'll need to specify your url and public key like so: +To use this library on the Supabase Hosted service but separately from the `supabase-csharp`, you'll need to specify +your url and public key like so: + ```c# var auth = new Supabase.Gotrue.Client(new ClientOptions { @@ -62,6 +66,7 @@ var auth = new Supabase.Gotrue.Client(new ClientOptions ``` Otherwise, using it this library with a local instance: + ```c# var options = new ClientOptions { Url = "https://example.com/api" }; var client = new Client(options); @@ -74,38 +79,47 @@ await new StatelessClient().SignUp("new-user@example.com", options); ## Persisting, Retrieving, and Destroying Sessions. -This Gotrue client is written to be agnostic when it comes to session persistence, retrieval, and destruction. `ClientOptions` exposes +This Gotrue client is written to be agnostic when it comes to session persistence, retrieval, and +destruction. `ClientOptions` exposes properties that allow these to be specified. -In the event these are specified and the `AutoRefreshToken` option is set, as the `Client` Initializes, it will also attempt to +In the event these are specified and the `AutoRefreshToken` option is set, as the `Client` Initializes, it will also +attempt to retrieve, set, and refresh an existing session. For example, using `Xamarin.Essentials` in `Xamarin.Forms`, this might look like: ```c# +// This is a method you add your application launch/setup +async void Initialize() { -var cacheFileName = ".gotrue.cache"; + // Specify the methods you'd like to use as persistence callbacks + var persistence = new GotrueSessionPersistence(SaveSession, LoadSession, DestroySession); + var client = new Client( + Url = GOTRUE_URL, + new ClientOptions { + AllowUnconfirmedUserSessions = true, + SessionPersistence = persistence }); + + // Specify a debug callback to listen to problems with the background token refresh thread + client.AddDebugListener(LogDebug); + + // Specify a call back to listen to changes in the user state (logged in, out, etc) + client.AddStateChangedListener(AuthStateListener); -async void Initialize() { - var options = new ClientOptions - { - Url = GOTRUE_URL, - PersistSession = true, - SessionPersistor = SaveSession, // PeristenceListener public delegate bool SaveSession(Session session); - SessionRetriever = LoadSession, // PeristenceListener public delegate Session LoadSession(); - SessionDestroyer = DestroySession // PeristenceListener delegate void DestroySession(); - }; - var client = new Client(options); // Load the session from persistence - newClient.LoadSession(); + client.LoadSession(); // Loads the session using SessionRetriever and sets state internally. await client.RetrieveSessionAsync(); } -//... - +// Add callback methods for above +// Here's a quick example of using this to save session data to the user's cache folder +// You'll want to add methods for loading the file and deleting when the user logs out internal bool SaveSession(Session session) { + var cacheFileName = ".gotrue.cache"; + try { var cacheDir = FileSystem.CacheDirectory; @@ -134,11 +148,11 @@ callback, the PKCE flow is preferred: 1) The Callback Url must be set in the Supabase Admin panel 2) The Application should have listener to receive that Callback -3) Generate a sign in request using: `client.SignIn(PROVIDER, options)` and setting the options to use the PKCE `FlowType` +3) Generate a sign in request using: `client.SignIn(PROVIDER, options)` and setting the options to use the + PKCE `FlowType` 4) Store `ProviderAuthState.PKCEVerifier` so that the application callback can use it to verify the returned code 5) In the Callback, use stored `PKCEVerifier` and received `code` to exchange for a session. - ```c# var state = await client.SignIn(Constants.Provider.Github, new SignInOptions { @@ -153,33 +167,48 @@ var session = await client.ExchangeCodeForSession(state.PKCEVerifier, RETRIEVE_C ## Troubleshooting -**I've created a User but while attempting to log in it throws an exception:** +**Q: I've created a User but while attempting to log in it throws an exception:** -Provided the credentials are correct, make sure that the User has also confirmed their email. +A: Provided the credentials are correct, make sure that the User has also confirmed their email. +Adding a handler for email confirmation to a desktop or mobile application can be done, but it +requires setting up URL handlers for each platform, which can be pretty difficult to do if you +aren't really comfortable with configuring these handlers. ( +e.g. [Windows](https://learn.microsoft.com/en-us/windows/win32/search/-search-3x-wds-ph-install-registration), +[Apple](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app), +[Android](https://developer.android.com/training/app-links)) +You may find it easier to create a +simple web application to handle email confirmation - that way a user can just click a link in +their email and get confirmed that way. Your desktop or mobile app should inspect the user object +that comes back and use that to see if the user is confirmed. + +You might find it easiest to do something like create and deploy a +simple [SvelteKit](https://kit.svelte.dev/) or even a very basic +pure [JavaScript](https://github.com/supabase/examples-archive/tree/main/supabase-js-v1/auth/javascript-auth) project +to handle email verification. ## Status - [x] API - - [x] Sign Up with Email - - [x] Sign In with Email - - [x] Send Magic Link Email - - [x] Invite User by Email - - [x] Reset Password for Email - - [x] Signout - - [x] Get Url for Provider - - [x] Get User - - [x] Update User - - [x] Refresh Access Token - - [x] List Users (includes filtering, sorting, pagination) - - [x] Get User by Id - - [x] Create User - - [x] Update User by Id + - [x] Sign Up with Email + - [x] Sign In with Email + - [x] Send Magic Link Email + - [x] Invite User by Email + - [x] Reset Password for Email + - [x] Signout + - [x] Get Url for Provider + - [x] Get User + - [x] Update User + - [x] Refresh Access Token + - [x] List Users (includes filtering, sorting, pagination) + - [x] Get User by Id + - [x] Create User + - [x] Update User by Id - [x] Client - - [x] Get User - - [x] Refresh Session - - [x] Auth State Change Handler - - [x] Provider Sign In (Provides URL) + - [x] Get User + - [x] Refresh Session + - [x] Auth State Change Handler + - [x] Provider Sign In (Provides URL) - [x] Provide Interfaces for Custom Token Persistence Functionality - [x] Documentation - [x] Unit Tests @@ -202,5 +231,6 @@ We are more than happy to have contributions! Please submit a PR. ### Testing To run the tests locally you must have docker and docker-compose installed. Then in the root of the repository run: + - `docker-compose up -d` - `dotnet test` From 3c8d69daa30621b166aebdbadc6a4ef6561aee57 Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Fri, 5 May 2023 12:35:43 -0700 Subject: [PATCH 66/74] Add note about settings api --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 02ca414..c910cbd 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,13 @@ - The client now more reliably emits AuthState changes. - There is now a single source of truth for headers in the stateful Client - the Options headers. +New feature: + +- Added a Settings request to the stateless API only - you can now query the server instance to + determine if it's got the settings you need. This might allow for things like a visual + component in a tool to verify the GoTrue settings are working correctly, or tests that run differently + depending on the server configuration. + Implementation notes: - Test cases have been added to help ensure reliability of auth state change notifications From 1da3f2a61ba82e0e0706c7927894d224884ba17e Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Fri, 5 May 2023 12:39:18 -0700 Subject: [PATCH 67/74] Add internal to set --- Gotrue/Exceptions/GotrueException.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Gotrue/Exceptions/GotrueException.cs b/Gotrue/Exceptions/GotrueException.cs index 572fd1a..aa970bb 100644 --- a/Gotrue/Exceptions/GotrueException.cs +++ b/Gotrue/Exceptions/GotrueException.cs @@ -3,9 +3,8 @@ namespace Supabase.Gotrue.Exceptions { - /// - /// Errors from Supabase are wrapped by this exception + /// Errors from the GoTrue server are wrapped by this exception /// public class GotrueException : Exception { @@ -16,7 +15,7 @@ public GotrueException(string? message, Exception? innerException) : base(messag public string? Content { get; internal set; } - public int StatusCode { get; set; } + public int StatusCode { get; internal set; } public void AddReason() { Reason = FailureHint.DetectReason(this); From 9c8abc2600919d29205aa9de350b8480035e8131 Mon Sep 17 00:00:00 2001 From: Joseph Schultz <9093699+acupofjose@users.noreply.github.com> Date: Sat, 6 May 2023 01:19:07 +0000 Subject: [PATCH 68/74] Cleanup methods that are essentially aliasing `Api` calls. --- Gotrue/Client.cs | 36 +++++++++++++----------------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/Gotrue/Client.cs b/Gotrue/Client.cs index b1145f5..4146a8a 100644 --- a/Gotrue/Client.cs +++ b/Gotrue/Client.cs @@ -515,11 +515,9 @@ public async Task DeleteUser(string uid, string jwt) /// page to show for pagination /// items per page for pagination /// - public async Task?> ListUsers(string jwt, string? filter = null, string? sortBy = null, SortOrder sortOrder = SortOrder.Descending, int? page = null, - int? perPage = null) - { - return await _api.ListUsers(jwt, filter, sortBy, sortOrder, page, perPage); - } + public Task?> ListUsers(string jwt, string? filter = null, string? sortBy = null, SortOrder sortOrder = SortOrder.Descending, int? page = null, + int? perPage = null) => + _api.ListUsers(jwt, filter, sortBy, sortOrder, page, perPage); /// /// Get User details by Id @@ -527,22 +525,16 @@ public async Task DeleteUser(string uid, string jwt) /// A valid JWT. Must be a full-access API key (e.g. service_role key). /// /// - public async Task GetUserById(string jwt, string userId) - { - - return await _api.GetUserById(jwt, userId); - - } + public Task GetUserById(string jwt, string userId) => + _api.GetUserById(jwt, userId); /// /// Get User details by JWT. Can be used to validate a JWT. /// /// A valid JWT. Must be a JWT that originates from a user. /// - public async Task GetUser(string jwt) - { - return await _api.GetUser(jwt); - } + public Task GetUser(string jwt) => + _api.GetUser(jwt); /// /// Create a user (as a service_role) @@ -568,10 +560,8 @@ public async Task DeleteUser(string uid, string jwt) /// A valid JWT. Must be a full-access API key (e.g. service_role key). /// /// - public async Task CreateUser(string jwt, AdminUserAttributes attributes) - { - return await _api.CreateUser(jwt, attributes); - } + public Task CreateUser(string jwt, AdminUserAttributes attributes) => + _api.CreateUser(jwt, attributes); /// /// Update user by Id @@ -580,10 +570,8 @@ public async Task DeleteUser(string uid, string jwt) /// /// /// - public async Task UpdateUserById(string jwt, string userId, AdminUserAttributes userData) - { - return await _api.UpdateUserById(jwt, userId, userData); - } + public Task UpdateUserById(string jwt, string userId, AdminUserAttributes userData) => + _api.UpdateUserById(jwt, userId, userData); /// /// Sends a reset request to an email address. @@ -719,12 +707,14 @@ public Session SetAuth(string accessToken) DestroySession(); return null; } + if (session?.User == null) { _debugNotification?.Log("Stored Session is missing data."); DestroySession(); return null; } + CurrentSession = session; NotifyAuthStateChange(SignedIn); From 3269047104d3cdcfcab26cc60487b5c848e1a79a Mon Sep 17 00:00:00 2001 From: Joseph Schultz <9093699+acupofjose@users.noreply.github.com> Date: Sat, 6 May 2023 01:24:34 +0000 Subject: [PATCH 69/74] Add code highlights --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c910cbd..61b60a3 100644 --- a/README.md +++ b/README.md @@ -13,20 +13,20 @@ ## BREAKING CHANGES v3.1 → v3.x -- Exceptions have been simplified to a single GotrueException. A Reason field has been added - to GotrueException to help sort out what happened. This should also be easier to manage as the Gotrue +- Exceptions have been simplified to a single `GotrueException`. A `Reason` field has been added + to `GotrueException` to clarify what happened. This should also be easier to manage as the Gotrue server API & messages evolve. -- The delegates for save/load/destroy persistence have been simplified to no longer require async. +- The session delegates for `Save`/`Load`/`Destroy` have been simplified to no longer require `async`. - Console logging in a few places (most notable the background refresh thread) has been removed - in favor of a notification method. See Client.AddDebugListener() and the test cases for examples. + in favor of a notification method. See `Client.AddDebugListener()` and the test cases for examples. This will allow you to implement your own logging strategy (write to temp file, console, user visible err console, etc). - The client now more reliably emits AuthState changes. -- There is now a single source of truth for headers in the stateful Client - the Options headers. +- There is now a single source of truth for headers in the stateful Client - the `Options` headers. New feature: -- Added a Settings request to the stateless API only - you can now query the server instance to +- Added a `Settings` request to the stateless API only - you can now query the server instance to determine if it's got the settings you need. This might allow for things like a visual component in a tool to verify the GoTrue settings are working correctly, or tests that run differently depending on the server configuration. From 6a297eeebaba296579a79b91ccc40d6502ed4b7f Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Sat, 6 May 2023 08:21:52 -0700 Subject: [PATCH 70/74] Update gotrue-csharp.sln.DotSettings --- gotrue-csharp.sln.DotSettings | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gotrue-csharp.sln.DotSettings b/gotrue-csharp.sln.DotSettings index 872208d..48ed401 100644 --- a/gotrue-csharp.sln.DotSettings +++ b/gotrue-csharp.sln.DotSettings @@ -44,6 +44,10 @@ UseVar SHA SMS + True + True + True + True True True True From 5d89a166896bd12e36855a0ed647bb3c777fc6ae Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Sat, 6 May 2023 08:59:41 -0700 Subject: [PATCH 71/74] Simplify session persistence with interface --- Gotrue/Client.cs | 25 +++----- Gotrue/ClientOptions.cs | 3 +- Gotrue/GotrueSessionPersistence.cs | 23 ------- .../Interfaces/IGotrueSessionPersistence.cs | 15 +++++ Gotrue/PersistenceListener.cs | 19 ++---- GotrueTests/AnonKeyClientFailureTests.cs | 36 ++++------- GotrueTests/AnonKeyClientTests.cs | 62 ++++++++----------- GotrueTests/TestSessionPersistence.cs | 26 ++++++++ 8 files changed, 97 insertions(+), 112 deletions(-) delete mode 100644 Gotrue/GotrueSessionPersistence.cs create mode 100644 Gotrue/Interfaces/IGotrueSessionPersistence.cs create mode 100644 GotrueTests/TestSessionPersistence.cs diff --git a/Gotrue/Client.cs b/Gotrue/Client.cs index 4146a8a..e21fc0e 100644 --- a/Gotrue/Client.cs +++ b/Gotrue/Client.cs @@ -137,7 +137,7 @@ public void AddStateChangedListener(IGotrueClient.AuthEventHandle _authEventHandlers.Add(authEventHandler); } - + /// /// Removes a specified listener from event state changes. /// @@ -149,7 +149,7 @@ public void RemoveStateChangedListener(IGotrueClient.AuthEventHan _authEventHandlers.Remove(authEventHandler); } - + /// /// Clears all of the listeners from receiving event state changes. /// @@ -516,8 +516,7 @@ public async Task DeleteUser(string uid, string jwt) /// items per page for pagination /// public Task?> ListUsers(string jwt, string? filter = null, string? sortBy = null, SortOrder sortOrder = SortOrder.Descending, int? page = null, - int? perPage = null) => - _api.ListUsers(jwt, filter, sortBy, sortOrder, page, perPage); + int? perPage = null) => _api.ListUsers(jwt, filter, sortBy, sortOrder, page, perPage); /// /// Get User details by Id @@ -525,16 +524,14 @@ public async Task DeleteUser(string uid, string jwt) /// A valid JWT. Must be a full-access API key (e.g. service_role key). /// /// - public Task GetUserById(string jwt, string userId) => - _api.GetUserById(jwt, userId); + public Task GetUserById(string jwt, string userId) => _api.GetUserById(jwt, userId); /// /// Get User details by JWT. Can be used to validate a JWT. /// /// A valid JWT. Must be a JWT that originates from a user. /// - public Task GetUser(string jwt) => - _api.GetUser(jwt); + public Task GetUser(string jwt) => _api.GetUser(jwt); /// /// Create a user (as a service_role) @@ -560,8 +557,7 @@ public async Task DeleteUser(string uid, string jwt) /// A valid JWT. Must be a full-access API key (e.g. service_role key). /// /// - public Task CreateUser(string jwt, AdminUserAttributes attributes) => - _api.CreateUser(jwt, attributes); + public Task CreateUser(string jwt, AdminUserAttributes attributes) => _api.CreateUser(jwt, attributes); /// /// Update user by Id @@ -570,8 +566,7 @@ public async Task DeleteUser(string uid, string jwt) /// /// /// - public Task UpdateUserById(string jwt, string userId, AdminUserAttributes userData) => - _api.UpdateUserById(jwt, userId, userData); + public Task UpdateUserById(string jwt, string userId, AdminUserAttributes userData) => _api.UpdateUserById(jwt, userId, userData); /// /// Sends a reset request to an email address. @@ -707,7 +702,7 @@ public Session SetAuth(string accessToken) DestroySession(); return null; } - + if (session?.User == null) { _debugNotification?.Log("Stored Session is missing data."); @@ -861,8 +856,8 @@ private async void HandleRefreshTimerTick(object _) } public void LoadSession() { - if(Options.SessionPersistence != null) - UpdateSession(Options.SessionPersistence.Load.Invoke()); + if (Options.SessionPersistence != null) + UpdateSession(Options.SessionPersistence.LoadSession()); } } } diff --git a/Gotrue/ClientOptions.cs b/Gotrue/ClientOptions.cs index 80d360c..f2111e3 100644 --- a/Gotrue/ClientOptions.cs +++ b/Gotrue/ClientOptions.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Supabase.Gotrue.Interfaces; using static Supabase.Gotrue.Constants; namespace Supabase.Gotrue @@ -26,7 +27,7 @@ public class ClientOptions /// /// Object called to persist the session (e.g. filesystem or cookie) /// - public GotrueSessionPersistence? SessionPersistence; + public IGotrueSessionPersistence? SessionPersistence; /// /// Very unlikely this flag needs to be changed except in very specific contexts. diff --git a/Gotrue/GotrueSessionPersistence.cs b/Gotrue/GotrueSessionPersistence.cs deleted file mode 100644 index f31ec23..0000000 --- a/Gotrue/GotrueSessionPersistence.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Supabase.Gotrue -{ - public class GotrueSessionPersistence - { - public delegate bool SaveSession(Session session); - - public delegate void DestroySession(); - - public delegate Session LoadSession(); - - public readonly SaveSession Save; - public readonly DestroySession Destroy; - public readonly LoadSession Load; - - public GotrueSessionPersistence(SaveSession save, LoadSession load, DestroySession destroy) - { - Save = save; - Destroy = destroy; - Load = load; - } - } - -} diff --git a/Gotrue/Interfaces/IGotrueSessionPersistence.cs b/Gotrue/Interfaces/IGotrueSessionPersistence.cs new file mode 100644 index 0000000..a3a20aa --- /dev/null +++ b/Gotrue/Interfaces/IGotrueSessionPersistence.cs @@ -0,0 +1,15 @@ +namespace Supabase.Gotrue.Interfaces +{ + /// + /// Interface for session persistence. As a reminder, make sure you handle exceptions and + /// other error conditions in your implementation. + /// + public interface IGotrueSessionPersistence + { + public void SaveSession(Session session); + + public void DestroySession(); + + public Session LoadSession(); + } +} diff --git a/Gotrue/PersistenceListener.cs b/Gotrue/PersistenceListener.cs index 63be7a2..33f7491 100644 --- a/Gotrue/PersistenceListener.cs +++ b/Gotrue/PersistenceListener.cs @@ -5,15 +5,8 @@ namespace Supabase.Gotrue { public class PersistenceListener { - private readonly GotrueSessionPersistence _persistence; - - public delegate bool SaveSession(Session session); - - public delegate void DestroySession(); - - public delegate Session LoadSession(); - - public PersistenceListener(GotrueSessionPersistence persistence) + private readonly IGotrueSessionPersistence _persistence; + public PersistenceListener(IGotrueSessionPersistence persistence) { _persistence = persistence; } @@ -27,10 +20,10 @@ public void EventHandler(IGotrueClient sender, Constants.AuthStat if (sender.CurrentSession == null) throw new ArgumentException("Tried to save a null session (2)"); - _persistence.Save(sender.CurrentSession); + _persistence.SaveSession(sender.CurrentSession); break; case Constants.AuthState.SignedOut: - _persistence.Destroy.Invoke(); + _persistence.DestroySession(); break; case Constants.AuthState.UserUpdated: if (sender == null) @@ -38,7 +31,7 @@ public void EventHandler(IGotrueClient sender, Constants.AuthStat if (sender.CurrentSession == null) throw new ArgumentException("Tried to save a null session (2)"); - _persistence.Save.Invoke(sender.CurrentSession); + _persistence.SaveSession(sender.CurrentSession); break; case Constants.AuthState.PasswordRecovery: break; case Constants.AuthState.TokenRefreshed: @@ -49,7 +42,7 @@ public void EventHandler(IGotrueClient sender, Constants.AuthStat } else { - _persistence.Save.Invoke(sender.CurrentSession); + _persistence.SaveSession(sender.CurrentSession); } break; default: throw new ArgumentOutOfRangeException(nameof(stateChanged), stateChanged, null); diff --git a/GotrueTests/AnonKeyClientFailureTests.cs b/GotrueTests/AnonKeyClientFailureTests.cs index af38566..2367d01 100644 --- a/GotrueTests/AnonKeyClientFailureTests.cs +++ b/GotrueTests/AnonKeyClientFailureTests.cs @@ -18,6 +18,7 @@ namespace GotrueTests [TestClass] public class AnonKeyClientFailureTests { + private TestSessionPersistence _persistence; private void AuthStateListener(IGotrueClient sender, Constants.AuthState newState) { @@ -35,32 +36,16 @@ private bool AuthStateIsEmpty() [TestInitialize] public void TestInitializer() { - var persistence = new GotrueSessionPersistence(SaveSession, LoadSession, DestroySession); - _client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true, SessionPersistence = persistence}); + _persistence = new TestSessionPersistence(); + _client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true, SessionPersistence = _persistence }); _client.AddDebugListener(LogDebug); _client.AddStateChangedListener(AuthStateListener); } - private void DestroySession() - { - _savedSession = null; - } - - private Session LoadSession() - { - return _savedSession; - } - - private bool SaveSession(Session session) - { - _savedSession = session; - return true; - } private Client _client; private readonly List _stateChanges = new List(); - private Session _savedSession; [TestMethod("Client: Sign Up With Bad Password")] public async Task SignUpUserEmailBadPassword() @@ -71,7 +56,7 @@ public async Task SignUpUserEmailBadPassword() await _client.SignUp(email, "x"); }); AreEqual(UserBadPassword, x.Reason); - IsNull(_savedSession); + IsNull(_persistence.SavedSession); Contains(_stateChanges, SignedOut); AreEqual(1, _stateChanges.Count); } @@ -84,7 +69,7 @@ public async Task SignUpUserEmailBadEmailAddress() await _client.SignUp("not a real email address", PASSWORD); }); AreEqual(UserBadEmailAddress, x.Reason); - IsNull(_savedSession); + IsNull(_persistence.SavedSession); Contains(_stateChanges, SignedOut); AreEqual(1, _stateChanges.Count); } @@ -100,7 +85,7 @@ public async Task SignUpUserPhone() await _client.SignUp(Constants.SignUpType.Phone, phone1, PASSWORD, new SignUpOptions { Data = new Dictionary { { "firstName", "Testing" } } }); }); AreEqual(UserMissingInformation, x.Reason); - IsNull(_savedSession); + IsNull(_persistence.SavedSession); Contains(_stateChanges, SignedOut); AreEqual(1, _stateChanges.Count); } @@ -114,7 +99,7 @@ public async Task SignsUpUserTwiceShouldReturnBadRequest() IsNotNull(session); Contains(_stateChanges, SignedIn); - AreEqual(_client.CurrentSession, _savedSession); + AreEqual(_client.CurrentSession, _persistence.SavedSession); _stateChanges.Clear(); var x = await ThrowsExceptionAsync(async () => @@ -123,7 +108,7 @@ public async Task SignsUpUserTwiceShouldReturnBadRequest() }); AreEqual(UserAlreadyRegistered, x.Reason); - IsNull(_savedSession); + IsNull(_persistence.SavedSession); Contains(_stateChanges, SignedOut); AreEqual(1, _stateChanges.Count); } @@ -151,8 +136,8 @@ public async Task ClientSendsResetPasswordForEmail() var result = await _client.ResetPasswordForEmail(email); IsTrue(result); } - - + + [TestMethod("Client: Throws Exception on Invalid Username and Password")] public async Task ClientSignsInUserWrongPassword() { @@ -168,4 +153,5 @@ await ThrowsExceptionAsync(async () => } } + } diff --git a/GotrueTests/AnonKeyClientTests.cs b/GotrueTests/AnonKeyClientTests.cs index ce181c7..b304dd4 100644 --- a/GotrueTests/AnonKeyClientTests.cs +++ b/GotrueTests/AnonKeyClientTests.cs @@ -18,6 +18,8 @@ namespace GotrueTests public class AnonKeyClientTests { + private TestSessionPersistence _persistence; + private void AuthStateListener(IGotrueClient sender, Constants.AuthState newState) { if (_stateChanges.Contains(newState)) @@ -34,38 +36,21 @@ private bool AuthStateIsEmpty() [TestInitialize] public void TestInitializer() { - var persistence = new GotrueSessionPersistence(SaveSession, LoadSession, DestroySession); - _client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true, SessionPersistence = persistence }); + _persistence = new TestSessionPersistence(); + _client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true, SessionPersistence = _persistence }); _client.AddDebugListener(LogDebug); _client.AddStateChangedListener(AuthStateListener); } - private void DestroySession() - { - _savedSession = null; - } - - private Session LoadSession() - { - return _savedSession; - } - - private bool SaveSession(Session session) - { - _savedSession = session; - return true; - } - private Client _client; private readonly List _stateChanges = new List(); - private Session _savedSession; private void VerifyGoodSession(Session session) { Contains(_stateChanges, SignedIn); AreEqual(_client.CurrentSession, session); - AreEqual(_client.CurrentSession, _savedSession); + AreEqual(_client.CurrentSession, _persistence.SavedSession); AreEqual(_client.CurrentUser, session.User); IsNotNull(session.AccessToken); IsNotNull(session.RefreshToken); @@ -75,7 +60,7 @@ private void VerifyGoodSession(Session session) private void VerifySignedOut() { Contains(_stateChanges, SignedOut); - IsNull(_savedSession); + IsNull(_persistence.SavedSession); IsNull(_client.CurrentSession); IsNull(_client.CurrentUser); } @@ -101,13 +86,20 @@ public async Task SaveAndLoadUser() VerifyGoodSession(session); - var persistence = new GotrueSessionPersistence(SaveSession, LoadSession, DestroySession); - var newClient = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true, SessionPersistence = persistence }); + var newPersistence = new TestSessionPersistence(); + newPersistence.SaveSession(session); + var newClient = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true, SessionPersistence = newPersistence }); newClient.AddDebugListener(LogDebug); newClient.AddStateChangedListener(AuthStateListener); // Loads the session from storage newClient.LoadSession(); + + Contains(_stateChanges, SignedIn); + AreEqual(newClient.CurrentSession, newPersistence.SavedSession); + IsNotNull(newClient.CurrentSession.AccessToken); + IsNotNull(newClient.CurrentSession.RefreshToken); + IsNotNull(newClient.CurrentSession.User); VerifyGoodSession(newClient.CurrentSession); // Refresh the session @@ -154,7 +146,7 @@ public async Task ClientTriggersTokenRefreshedEvent() await _client.RefreshSession(); Contains(_stateChanges, TokenRefreshed); - AreEqual(_client.CurrentSession, _savedSession); + AreEqual(_client.CurrentSession, _persistence.SavedSession); var newToken = await tsc.Task; IsNotNull(newToken); @@ -171,9 +163,9 @@ public async Task ClientSignsIn() _stateChanges.Clear(); await _client.SignOut(); - + VerifySignedOut(); - + _stateChanges.Clear(); var session2 = await _client.SignIn(email, PASSWORD); @@ -204,8 +196,8 @@ public async Task ClientSignsIn() // Refresh Token var refreshToken = emailSession.RefreshToken; - var newSession = await _client.SignIn(Constants.SignInType.RefreshToken, refreshToken); - AreEqual(_client.CurrentSession, _savedSession); + var newSession = await _client.SignIn(Constants.SignInType.RefreshToken, refreshToken ?? throw new InvalidOperationException()); + AreEqual(_client.CurrentSession, _persistence.SavedSession); Contains(_stateChanges, TokenRefreshed); DoesNotContain(_stateChanges, SignedIn); @@ -224,15 +216,15 @@ public async Task ClientSendsMagicLoginEmail() _stateChanges.Clear(); await _client.SignOut(); - + VerifySignedOut(); - + _stateChanges.Clear(); var result = await _client.SignIn(user); IsTrue(result); AreEqual(0, _stateChanges.Count); - AreEqual(_client.CurrentSession, _savedSession); + AreEqual(_client.CurrentSession, _persistence.SavedSession); } [TestMethod("Client: Sends Magic Login Email (Alias)")] @@ -243,13 +235,13 @@ public async Task ClientSendsMagicLoginEmailAlias() var session = await _client.SignUp(user, PASSWORD); VerifyGoodSession(session); - + _stateChanges.Clear(); await _client.SignOut(); VerifySignedOut(); - + var result = await _client.SendMagicLink(user); var result2 = await _client.SendMagicLink(user2, new SignInOptions { RedirectTo = $"com.{RandomString(12)}.deeplink://login" }); @@ -288,7 +280,7 @@ public async Task ClientUpdateUser() var session = await _client.SignUp(email, PASSWORD); VerifyGoodSession(session); - + _stateChanges.Clear(); var attributes = new UserAttributes { Data = new Dictionary { { "hello", "world" } } }; @@ -297,7 +289,7 @@ public async Task ClientUpdateUser() AreEqual(email, _client.CurrentUser.Email); IsNotNull(_client.CurrentUser.UserMetadata); Contains(_stateChanges, UserUpdated); - AreEqual(_client.CurrentSession, _savedSession); + AreEqual(_client.CurrentSession, _persistence.SavedSession); await _client.SignOut(); diff --git a/GotrueTests/TestSessionPersistence.cs b/GotrueTests/TestSessionPersistence.cs new file mode 100644 index 0000000..62eaf40 --- /dev/null +++ b/GotrueTests/TestSessionPersistence.cs @@ -0,0 +1,26 @@ +using Supabase.Gotrue; +using Supabase.Gotrue.Interfaces; + +namespace GotrueTests +{ + public class TestSessionPersistence : IGotrueSessionPersistence + { + + public Session SavedSession; + + public void DestroySession() + { + SavedSession = null; + } + + public Session LoadSession() + { + return SavedSession; + } + + public void SaveSession(Session session) + { + SavedSession = session; + } + } +} From 6f7e8990658a2d73ef6d175748060d2d32222d5b Mon Sep 17 00:00:00 2001 From: Will Iverson Date: Sat, 6 May 2023 09:15:07 -0700 Subject: [PATCH 72/74] Move setting persistence to method instead of options --- Gotrue/Client.cs | 30 +++++++++++++++++------- Gotrue/ClientOptions.cs | 5 ---- Gotrue/PersistenceListener.cs | 14 ++++++----- GotrueTests/AnonKeyClientFailureTests.cs | 3 ++- GotrueTests/AnonKeyClientTests.cs | 6 +++-- 5 files changed, 35 insertions(+), 23 deletions(-) diff --git a/Gotrue/Client.cs b/Gotrue/Client.cs index e21fc0e..eea761d 100644 --- a/Gotrue/Client.cs +++ b/Gotrue/Client.cs @@ -49,6 +49,13 @@ public class Client : IGotrueClient /// private Timer? _refreshTimer; + + /// + /// Object called to persist the session (e.g. filesystem or cookie) + /// + private PersistenceListener? _sessionPersistence; + + /// /// Initializes the GoTrue stateful client. /// @@ -82,17 +89,22 @@ public class Client : IGotrueClient public Client(ClientOptions? options = null) { options ??= new ClientOptions(); - Options = options; - - if (options.SessionPersistence != null) - { - _authEventHandlers.Add(new PersistenceListener(options.SessionPersistence).EventHandler); - } - _api = new Api(options.Url, options.Headers); } + /// + /// Set the Session persistence system. Typically an application specific file system location. + /// + /// + public void SetPersistence(IGotrueSessionPersistence persistence) + { + if (_sessionPersistence != null) + _authEventHandlers.Remove(_sessionPersistence.EventHandler); + _sessionPersistence = new PersistenceListener(persistence); + _authEventHandlers.Add(_sessionPersistence.EventHandler); + } + /// /// The initialized client options. /// @@ -856,8 +868,8 @@ private async void HandleRefreshTimerTick(object _) } public void LoadSession() { - if (Options.SessionPersistence != null) - UpdateSession(Options.SessionPersistence.LoadSession()); + if (_sessionPersistence != null) + UpdateSession(_sessionPersistence.Persistence.LoadSession()); } } } diff --git a/Gotrue/ClientOptions.cs b/Gotrue/ClientOptions.cs index f2111e3..2356780 100644 --- a/Gotrue/ClientOptions.cs +++ b/Gotrue/ClientOptions.cs @@ -24,11 +24,6 @@ public class ClientOptions /// public bool AutoRefreshToken { get; set; } = true; - /// - /// Object called to persist the session (e.g. filesystem or cookie) - /// - public IGotrueSessionPersistence? SessionPersistence; - /// /// Very unlikely this flag needs to be changed except in very specific contexts. /// diff --git a/Gotrue/PersistenceListener.cs b/Gotrue/PersistenceListener.cs index 33f7491..ca20746 100644 --- a/Gotrue/PersistenceListener.cs +++ b/Gotrue/PersistenceListener.cs @@ -5,11 +5,13 @@ namespace Supabase.Gotrue { public class PersistenceListener { - private readonly IGotrueSessionPersistence _persistence; public PersistenceListener(IGotrueSessionPersistence persistence) { - _persistence = persistence; + Persistence = persistence; } + + public IGotrueSessionPersistence Persistence { get; } + public void EventHandler(IGotrueClient sender, Constants.AuthState stateChanged) { switch (stateChanged) @@ -20,10 +22,10 @@ public void EventHandler(IGotrueClient sender, Constants.AuthStat if (sender.CurrentSession == null) throw new ArgumentException("Tried to save a null session (2)"); - _persistence.SaveSession(sender.CurrentSession); + Persistence.SaveSession(sender.CurrentSession); break; case Constants.AuthState.SignedOut: - _persistence.DestroySession(); + Persistence.DestroySession(); break; case Constants.AuthState.UserUpdated: if (sender == null) @@ -31,7 +33,7 @@ public void EventHandler(IGotrueClient sender, Constants.AuthStat if (sender.CurrentSession == null) throw new ArgumentException("Tried to save a null session (2)"); - _persistence.SaveSession(sender.CurrentSession); + Persistence.SaveSession(sender.CurrentSession); break; case Constants.AuthState.PasswordRecovery: break; case Constants.AuthState.TokenRefreshed: @@ -42,7 +44,7 @@ public void EventHandler(IGotrueClient sender, Constants.AuthStat } else { - _persistence.SaveSession(sender.CurrentSession); + Persistence.SaveSession(sender.CurrentSession); } break; default: throw new ArgumentOutOfRangeException(nameof(stateChanged), stateChanged, null); diff --git a/GotrueTests/AnonKeyClientFailureTests.cs b/GotrueTests/AnonKeyClientFailureTests.cs index 2367d01..1335072 100644 --- a/GotrueTests/AnonKeyClientFailureTests.cs +++ b/GotrueTests/AnonKeyClientFailureTests.cs @@ -37,7 +37,8 @@ private bool AuthStateIsEmpty() public void TestInitializer() { _persistence = new TestSessionPersistence(); - _client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true, SessionPersistence = _persistence }); + _client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true }); + _client.SetPersistence(_persistence); _client.AddDebugListener(LogDebug); _client.AddStateChangedListener(AuthStateListener); } diff --git a/GotrueTests/AnonKeyClientTests.cs b/GotrueTests/AnonKeyClientTests.cs index b304dd4..e52c610 100644 --- a/GotrueTests/AnonKeyClientTests.cs +++ b/GotrueTests/AnonKeyClientTests.cs @@ -37,7 +37,8 @@ private bool AuthStateIsEmpty() public void TestInitializer() { _persistence = new TestSessionPersistence(); - _client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true, SessionPersistence = _persistence }); + _client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true }); + _client.SetPersistence(_persistence); _client.AddDebugListener(LogDebug); _client.AddStateChangedListener(AuthStateListener); } @@ -88,7 +89,8 @@ public async Task SaveAndLoadUser() var newPersistence = new TestSessionPersistence(); newPersistence.SaveSession(session); - var newClient = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true, SessionPersistence = newPersistence }); + var newClient = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true }); + newClient.SetPersistence(newPersistence); newClient.AddDebugListener(LogDebug); newClient.AddStateChangedListener(AuthStateListener); From 0dcdb51a2070ab479cb683927678207a9a159601 Mon Sep 17 00:00:00 2001 From: Joseph Schultz <9093699+acupofjose@users.noreply.github.com> Date: Sun, 7 May 2023 02:55:45 +0000 Subject: [PATCH 73/74] Extract interface for `PersistenceListener` --- Gotrue/Client.cs | 4 +- Gotrue/Gotrue.csproj | 2 +- .../Interfaces/IGotruePersistenceListener.cs | 12 +++ Gotrue/PersistenceListener.cs | 90 +++++++++---------- 4 files changed, 60 insertions(+), 48 deletions(-) create mode 100644 Gotrue/Interfaces/IGotruePersistenceListener.cs diff --git a/Gotrue/Client.cs b/Gotrue/Client.cs index eea761d..1cc5bc1 100644 --- a/Gotrue/Client.cs +++ b/Gotrue/Client.cs @@ -53,7 +53,7 @@ public class Client : IGotrueClient /// /// Object called to persist the session (e.g. filesystem or cookie) /// - private PersistenceListener? _sessionPersistence; + private IGotruePersistenceListener? _sessionPersistence; /// @@ -97,7 +97,7 @@ public Client(ClientOptions? options = null) /// Set the Session persistence system. Typically an application specific file system location. /// /// - public void SetPersistence(IGotrueSessionPersistence persistence) + public void SetPersistence(IGotrueSessionPersistence persistence) { if (_sessionPersistence != null) _authEventHandlers.Remove(_sessionPersistence.EventHandler); diff --git a/Gotrue/Gotrue.csproj b/Gotrue/Gotrue.csproj index 8cc99da..bfe6a35 100644 --- a/Gotrue/Gotrue.csproj +++ b/Gotrue/Gotrue.csproj @@ -21,7 +21,7 @@ enable - 8.0 + latest CS8600;CS8602;CS8603 diff --git a/Gotrue/Interfaces/IGotruePersistenceListener.cs b/Gotrue/Interfaces/IGotruePersistenceListener.cs new file mode 100644 index 0000000..95faf63 --- /dev/null +++ b/Gotrue/Interfaces/IGotruePersistenceListener.cs @@ -0,0 +1,12 @@ +namespace Supabase.Gotrue.Interfaces +{ + /// + /// Interface for a session persistence auth state handler. + /// + public interface IGotruePersistenceListener where TSession : Session + { + IGotrueSessionPersistence Persistence { get; } + + public void EventHandler(IGotrueClient sender, Constants.AuthState stateChanged); + } +} \ No newline at end of file diff --git a/Gotrue/PersistenceListener.cs b/Gotrue/PersistenceListener.cs index ca20746..6b0bff1 100644 --- a/Gotrue/PersistenceListener.cs +++ b/Gotrue/PersistenceListener.cs @@ -3,52 +3,52 @@ namespace Supabase.Gotrue { - public class PersistenceListener - { - public PersistenceListener(IGotrueSessionPersistence persistence) - { - Persistence = persistence; - } + public class PersistenceListener : IGotruePersistenceListener + { + public PersistenceListener(IGotrueSessionPersistence persistence) + { + Persistence = persistence; + } - public IGotrueSessionPersistence Persistence { get; } - - public void EventHandler(IGotrueClient sender, Constants.AuthState stateChanged) - { - switch (stateChanged) - { - case Constants.AuthState.SignedIn: - if (sender == null) - throw new ArgumentException("Tried to save a null session (1)"); - if (sender.CurrentSession == null) - throw new ArgumentException("Tried to save a null session (2)"); + public IGotrueSessionPersistence Persistence { get; } - Persistence.SaveSession(sender.CurrentSession); - break; - case Constants.AuthState.SignedOut: - Persistence.DestroySession(); - break; - case Constants.AuthState.UserUpdated: - if (sender == null) - throw new ArgumentException("Tried to save a null session (1)"); - if (sender.CurrentSession == null) - throw new ArgumentException("Tried to save a null session (2)"); + public void EventHandler(IGotrueClient sender, Constants.AuthState stateChanged) + { + switch (stateChanged) + { + case Constants.AuthState.SignedIn: + if (sender == null) + throw new ArgumentException("Tried to save a null session (1)"); + if (sender.CurrentSession == null) + throw new ArgumentException("Tried to save a null session (2)"); - Persistence.SaveSession(sender.CurrentSession); - break; - case Constants.AuthState.PasswordRecovery: break; - case Constants.AuthState.TokenRefreshed: - if (sender.CurrentSession == null) - { - // If token refresh results in a null session, log out. - EventHandler(sender, Constants.AuthState.SignedOut); - } - else - { - Persistence.SaveSession(sender.CurrentSession); - } - break; - default: throw new ArgumentOutOfRangeException(nameof(stateChanged), stateChanged, null); - } - } - } + Persistence.SaveSession(sender.CurrentSession); + break; + case Constants.AuthState.SignedOut: + Persistence.DestroySession(); + break; + case Constants.AuthState.UserUpdated: + if (sender == null) + throw new ArgumentException("Tried to save a null session (1)"); + if (sender.CurrentSession == null) + throw new ArgumentException("Tried to save a null session (2)"); + + Persistence.SaveSession(sender.CurrentSession); + break; + case Constants.AuthState.PasswordRecovery: break; + case Constants.AuthState.TokenRefreshed: + if (sender.CurrentSession == null) + { + // If token refresh results in a null session, log out. + EventHandler(sender, Constants.AuthState.SignedOut); + } + else + { + Persistence.SaveSession(sender.CurrentSession); + } + break; + default: throw new ArgumentOutOfRangeException(nameof(stateChanged), stateChanged, null); + } + } + } } From 63470d2dcfc69e479d784a8fdd00fd8e62d5e110 Mon Sep 17 00:00:00 2001 From: Joseph Schultz <9093699+acupofjose@users.noreply.github.com> Date: Sun, 7 May 2023 02:57:16 +0000 Subject: [PATCH 74/74] Update `IGotrueClient` interface for Persistence --- Gotrue/Interfaces/IGotrueClient.cs | 2 ++ .../Interfaces/IGotrueSessionPersistence.cs | 20 +++++++++---------- GotrueTests/TestSessionPersistence.cs | 2 +- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Gotrue/Interfaces/IGotrueClient.cs b/Gotrue/Interfaces/IGotrueClient.cs index c276c8a..a0ac417 100644 --- a/Gotrue/Interfaces/IGotrueClient.cs +++ b/Gotrue/Interfaces/IGotrueClient.cs @@ -14,6 +14,8 @@ public interface IGotrueClient : IGettableHeaders delegate void AuthEventHandler(IGotrueClient sender, AuthState stateChanged); + public void SetPersistence(IGotrueSessionPersistence persistence); + public void AddStateChangedListener(AuthEventHandler authEventHandler); public void RemoveStateChangedListener(AuthEventHandler authEventHandler); public void ClearStateChangedListeners(); diff --git a/Gotrue/Interfaces/IGotrueSessionPersistence.cs b/Gotrue/Interfaces/IGotrueSessionPersistence.cs index a3a20aa..e9126a1 100644 --- a/Gotrue/Interfaces/IGotrueSessionPersistence.cs +++ b/Gotrue/Interfaces/IGotrueSessionPersistence.cs @@ -1,15 +1,15 @@ namespace Supabase.Gotrue.Interfaces { - /// - /// Interface for session persistence. As a reminder, make sure you handle exceptions and - /// other error conditions in your implementation. - /// - public interface IGotrueSessionPersistence - { - public void SaveSession(Session session); + /// + /// Interface for session persistence. As a reminder, make sure you handle exceptions and + /// other error conditions in your implementation. + /// + public interface IGotrueSessionPersistence where TSession : Session + { + public void SaveSession(TSession session); - public void DestroySession(); + public void DestroySession(); - public Session LoadSession(); - } + public TSession LoadSession(); + } } diff --git a/GotrueTests/TestSessionPersistence.cs b/GotrueTests/TestSessionPersistence.cs index 62eaf40..4324751 100644 --- a/GotrueTests/TestSessionPersistence.cs +++ b/GotrueTests/TestSessionPersistence.cs @@ -3,7 +3,7 @@ namespace GotrueTests { - public class TestSessionPersistence : IGotrueSessionPersistence + public class TestSessionPersistence : IGotrueSessionPersistence { public Session SavedSession;