From c422cfbdb6445c6314ae3f3fb90c878be7d6c52c Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 2 Nov 2023 13:02:15 +0100 Subject: [PATCH 1/3] Added methods --- .../Realm/Handles/AppHandle.EmailPassword.cs | 22 +++++++++++++++++++ Realm/Realm/Sync/App.cs | 15 +++++++++++++ wrappers/src/app_cs.cpp | 8 +++++++ 3 files changed, 45 insertions(+) diff --git a/Realm/Realm/Handles/AppHandle.EmailPassword.cs b/Realm/Realm/Handles/AppHandle.EmailPassword.cs index 9917f77e86..a6b5c53b62 100644 --- a/Realm/Realm/Handles/AppHandle.EmailPassword.cs +++ b/Realm/Realm/Handles/AppHandle.EmailPassword.cs @@ -43,6 +43,11 @@ public static extern void resend_confirmation_email(AppHandle app, [MarshalAs(UnmanagedType.LPWStr)] string email, IntPtr email_len, IntPtr tcs_ptr, out NativeException ex); + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_app_email_retry_custom_confirmation", CallingConvention = CallingConvention.Cdecl)] + public static extern void retry_custom_comfirmation(AppHandle app, + [MarshalAs(UnmanagedType.LPWStr)] string email, IntPtr email_len, + IntPtr tcs_ptr, out NativeException ex); + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_app_email_send_reset_password_email", CallingConvention = CallingConvention.Cdecl)] public static extern void send_reset_password_email(AppHandle app, [MarshalAs(UnmanagedType.LPWStr)] string email, IntPtr email_len, @@ -125,6 +130,23 @@ public async Task ResendConfirmationEmailAsync(string email) } } + public async Task RetryCustomConfirmationAsync(string email) + { + var tcs = new TaskCompletionSource(); + var tcsHandle = GCHandle.Alloc(tcs); + + try + { + EmailNativeMethods.resend_confirmation_email(_appHandle, email, (IntPtr)email.Length, GCHandle.ToIntPtr(tcsHandle), out var ex); + ex.ThrowIfNecessary(); + await tcs.Task; + } + finally + { + tcsHandle.Free(); + } + } + public async Task SendResetPasswordEmailAsync(string username) { var tcs = new TaskCompletionSource(); diff --git a/Realm/Realm/Sync/App.cs b/Realm/Realm/Sync/App.cs index 53abd96088..0fc08e1bc0 100644 --- a/Realm/Realm/Sync/App.cs +++ b/Realm/Realm/Sync/App.cs @@ -408,6 +408,21 @@ public Task ResendConfirmationEmailAsync(string email) return _app.Handle.EmailPassword.ResendConfirmationEmailAsync(email); } + /// + /// Rerun the custom confirmation function for the given mail. + /// + /// The email of the user. + /// //TODO Finish the docs + /// An awaitable representing the asynchronous request to the server that the custom confirmation function is run again. Successful + /// completion indicates that the user has been confirmed on the server. + /// + public Task RetryCustomConfirmationAsync(string email) + { + Argument.NotNullOrEmpty(email, nameof(email)); + + return _app.Handle.EmailPassword.ResendConfirmationEmailAsync(email); + } + /// /// Sends a password reset email to the specified address. /// diff --git a/wrappers/src/app_cs.cpp b/wrappers/src/app_cs.cpp index 8539af5a49..a0e0abeb18 100644 --- a/wrappers/src/app_cs.cpp +++ b/wrappers/src/app_cs.cpp @@ -368,6 +368,14 @@ extern "C" { }); } + REALM_EXPORT void shared_app_email_retry_custom_confirmation(SharedApp& app, uint16_t* email_buf, size_t email_len, void* tcs_ptr, NativeException::Marshallable& ex) + { + handle_errors(ex, [&]() { + Utf16StringAccessor email(email_buf, email_len); + app->provider_client().retry_custom_confirmation(email, get_callback_handler(tcs_ptr)); + }); + } + REALM_EXPORT void shared_app_email_send_reset_password_email(SharedApp& app, uint16_t* email_buf, size_t email_len, void* tcs_ptr, NativeException::Marshallable& ex) { handle_errors(ex, [&]() { From 57688ea8dc993a19e9c01f5ccb86bba3e972f95d Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Fri, 3 Nov 2023 10:56:20 +0100 Subject: [PATCH 2/3] Final fixes --- CHANGELOG.md | 1 + .../Realm/Handles/AppHandle.EmailPassword.cs | 2 +- Realm/Realm/Sync/App.cs | 4 +-- Tests/Realm.Tests/Sync/SyncTestHelpers.cs | 2 ++ Tests/Realm.Tests/Sync/UserManagementTests.cs | 36 +++++++++++++++++++ Tools/DeployApps/BaasClient.cs | 28 +++++++++++++-- 6 files changed, 67 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbe52a8e2a..0d88858870 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## vNext (TBD) ### Enhancements +* Added the `App.EmailPasswordAuth.RetryCustomConfirmationAsync` method to be able to run again the confirmation function on the server for a given email. (Issue [#3463](https://github.com/realm/realm-dotnet/issues/3463)) * Added `User.Changed` event that can be used to notify subscribers that something about the user changed - typically this would be the user state or the access token. (Issue [#3429](https://github.com/realm/realm-dotnet/issues/3429)) * Added support for customizing the ignore attribute applied on certain generated properties of Realm models. The configuration option is called `realm.custom_ignore_attribute` and can be set in a global configuration file (more information about global configuration files can be found in the [.NET documentation](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/configuration-files)). The Realm generator will treat this as an opaque string, that will be appended to the `IgnoreDataMember` and `XmlIgnore` attributes already applied on these members. The attributes must be fully qualified unless the namespace they reside in is added to a global usings file. For example, this is how you would add `JsonIgnore` from `System.Text.Json`: diff --git a/Realm/Realm/Handles/AppHandle.EmailPassword.cs b/Realm/Realm/Handles/AppHandle.EmailPassword.cs index a6b5c53b62..cd23cbf375 100644 --- a/Realm/Realm/Handles/AppHandle.EmailPassword.cs +++ b/Realm/Realm/Handles/AppHandle.EmailPassword.cs @@ -137,7 +137,7 @@ public async Task RetryCustomConfirmationAsync(string email) try { - EmailNativeMethods.resend_confirmation_email(_appHandle, email, (IntPtr)email.Length, GCHandle.ToIntPtr(tcsHandle), out var ex); + EmailNativeMethods.retry_custom_comfirmation(_appHandle, email, (IntPtr)email.Length, GCHandle.ToIntPtr(tcsHandle), out var ex); ex.ThrowIfNecessary(); await tcs.Task; } diff --git a/Realm/Realm/Sync/App.cs b/Realm/Realm/Sync/App.cs index 0fc08e1bc0..febfbb525b 100644 --- a/Realm/Realm/Sync/App.cs +++ b/Realm/Realm/Sync/App.cs @@ -412,7 +412,7 @@ public Task ResendConfirmationEmailAsync(string email) /// Rerun the custom confirmation function for the given mail. /// /// The email of the user. - /// //TODO Finish the docs + /// /// An awaitable representing the asynchronous request to the server that the custom confirmation function is run again. Successful /// completion indicates that the user has been confirmed on the server. /// @@ -420,7 +420,7 @@ public Task RetryCustomConfirmationAsync(string email) { Argument.NotNullOrEmpty(email, nameof(email)); - return _app.Handle.EmailPassword.ResendConfirmationEmailAsync(email); + return _app.Handle.EmailPassword.RetryCustomConfirmationAsync(email); } /// diff --git a/Tests/Realm.Tests/Sync/SyncTestHelpers.cs b/Tests/Realm.Tests/Sync/SyncTestHelpers.cs index 43b2890311..fae2c57766 100644 --- a/Tests/Realm.Tests/Sync/SyncTestHelpers.cs +++ b/Tests/Realm.Tests/Sync/SyncTestHelpers.cs @@ -92,6 +92,8 @@ public static void RunBaasTestAsync(Func testFunc, int timeout = 30000) public static string GetVerifiedUsername() => $"realm_tests_do_autoverify-{Guid.NewGuid()}"; + public static string GetUnconfirmedUsername() => $"realm_tests_do_not_confirm-{Guid.NewGuid()}@g.it"; + public static async Task TriggerClientResetOnServer(SyncConfigurationBase config) { var userId = config.User.Id; diff --git a/Tests/Realm.Tests/Sync/UserManagementTests.cs b/Tests/Realm.Tests/Sync/UserManagementTests.cs index 401e9d3a67..9ca3b86ee0 100644 --- a/Tests/Realm.Tests/Sync/UserManagementTests.cs +++ b/Tests/Realm.Tests/Sync/UserManagementTests.cs @@ -400,6 +400,42 @@ public void User_LinkCredentials_WhenInUse_Throws() }); } + [Test] + public void User_RetryCustomConfirmationAsync_WorksInAllScenarios() + { + SyncTestHelpers.RunBaasTestAsync(async () => + { + // Standard case + var unconfirmedMail = SyncTestHelpers.GetUnconfirmedUsername(); + var credentials = Credentials.EmailPassword(unconfirmedMail, SyncTestHelpers.DefaultPassword); + + // The first time the confirmation function is called we return "pending", so the user needs to be confirmed. + // At the same time we save the user email in a collection. + await DefaultApp.EmailPasswordAuth.RegisterUserAsync(unconfirmedMail, SyncTestHelpers.DefaultPassword).Timeout(10_000, detail: "Failed to register user"); + + var ex3 = await TestHelpers.AssertThrows(() => DefaultApp.LogInAsync(credentials)); + Assert.That(ex3.Message, Does.Contain("confirmation required")); + + // The second time we call the confirmation function we find the email we saved in the collection and return "success", so the user + // gets confirmed and can log in. + await DefaultApp.EmailPasswordAuth.RetryCustomConfirmationAsync(unconfirmedMail); + var user = await DefaultApp.LogInAsync(credentials); + Assert.That(user.State, Is.EqualTo(UserState.LoggedIn)); + + // Logged in user case + var loggedInUser = await GetUserAsync(); + var ex = await TestHelpers.AssertThrows(() => DefaultApp.EmailPasswordAuth.RetryCustomConfirmationAsync(loggedInUser.Profile.Email!)); + Assert.That(ex.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); + Assert.That(ex.Message, Does.Contain("already confirmed")); + + // Unknown user case + var invalidEmail = "test@gmail.com"; + var ex2 = await TestHelpers.AssertThrows(() => DefaultApp.EmailPasswordAuth.RetryCustomConfirmationAsync(invalidEmail)); + Assert.That(ex2.StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); + Assert.That(ex2.Message, Does.Contain("user not found")); + }); + } + [Test] public void User_JWT_LogsInAndReadsDataFromToken() { diff --git a/Tools/DeployApps/BaasClient.cs b/Tools/DeployApps/BaasClient.cs index fa3ece4eee..c8696c8ab2 100644 --- a/Tools/DeployApps/BaasClient.cs +++ b/Tools/DeployApps/BaasClient.cs @@ -88,12 +88,34 @@ public class FunctionReturn } private const string ConfirmFuncSource = - @"exports = ({ token, tokenId, username }) => { + @"exports = async function ({ token, tokenId, username }) { // process the confirm token, tokenId and username if (username.includes(""realm_tests_do_autoverify"")) { - return { status: 'success' } + return { status: 'success' }; + } + + if (username.includes(""realm_tests_do_not_confirm"")) { + const mongodb = context.services.get('BackingDB'); + let collection = mongodb.db('test_db').collection('not_confirmed'); + let result = await collection.findOne({'email': username}); + + if(result === null) + { + let newVal = { + 'email': username, + 'token': token, + 'tokenId': tokenId, + } + + await collection.insertOne(newVal); + + return { status: 'pending' }; + } + + return { status: 'success' }; } - // do not confirm the user + + // fail the user confirmation return { status: 'fail' }; };"; From 23f9b621164decece1526d73f6d1f3567a675004 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Fri, 3 Nov 2023 10:59:36 +0100 Subject: [PATCH 3/3] Fixed spacing --- Tools/DeployApps/BaasClient.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Tools/DeployApps/BaasClient.cs b/Tools/DeployApps/BaasClient.cs index c8696c8ab2..dc2998369c 100644 --- a/Tools/DeployApps/BaasClient.cs +++ b/Tools/DeployApps/BaasClient.cs @@ -108,7 +108,6 @@ public class FunctionReturn } await collection.insertOne(newVal); - return { status: 'pending' }; }