Skip to content

Commit

Permalink
Added RetryCustomConfirmationAsync (#3468)
Browse files Browse the repository at this point in the history
  • Loading branch information
papafe authored Nov 3, 2023
1 parent b969719 commit b8dce53
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`:

Expand Down
22 changes: 22 additions & 0 deletions Realm/Realm/Handles/AppHandle.EmailPassword.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.retry_custom_comfirmation(_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();
Expand Down
15 changes: 15 additions & 0 deletions Realm/Realm/Sync/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,21 @@ public Task ResendConfirmationEmailAsync(string email)
return _app.Handle.EmailPassword.ResendConfirmationEmailAsync(email);
}

/// <summary>
/// Rerun the custom confirmation function for the given mail.
/// </summary>
/// <param name="email">The email of the user.</param>
/// <returns>
/// An awaitable <see cref="Task"/> 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.
/// </returns>
public Task RetryCustomConfirmationAsync(string email)
{
Argument.NotNullOrEmpty(email, nameof(email));

return _app.Handle.EmailPassword.RetryCustomConfirmationAsync(email);
}

/// <summary>
/// Sends a password reset email to the specified address.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions Tests/Realm.Tests/Sync/SyncTestHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ public static void RunBaasTestAsync(Func<Task> 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;
Expand Down
36 changes: 36 additions & 0 deletions Tests/Realm.Tests/Sync/UserManagementTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,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<AppException>(() => 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<AppException>(() => 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 = "[email protected]";
var ex2 = await TestHelpers.AssertThrows<AppException>(() => 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()
{
Expand Down
27 changes: 24 additions & 3 deletions Tools/DeployApps/BaasClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,33 @@ 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' };
}
// do not confirm the user
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' };
}
// fail the user confirmation
return { status: 'fail' };
};";

Expand Down
8 changes: 8 additions & 0 deletions wrappers/src/app_cs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,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<App::UsernamePasswordProviderClient>().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, [&]() {
Expand Down

0 comments on commit b8dce53

Please sign in to comment.