diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb98b8f41..35b8e4fd2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -305,7 +305,7 @@ jobs: build-windows: # TODO: build on windows-latest - runs-on: windows-2019 + runs-on: windows-2022 name: Build Windows steps: - name: Checkout diff --git a/CHANGELOG.md b/CHANGELOG.md index 46fb7abdc..0664aa89f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,9 +26,14 @@ x.x.x Release notes (yyyy-MM-dd) * Support EmailPassword calling custom reset password functions. ([#482](https://github.com/realm/realm-dart/pull/482)) * Support EmailPassword retry custom user confirmation functions. ([#484](https://github.com/realm/realm-dart/pull/484)) * Expose currentUser property on App. ([473](https://github.com/realm/realm-dart/pull/473)) -* Support logout user. ([#476](https://github.com/realm/realm-dart/pull/476)) +* Support app logout user. ([#476](https://github.com/realm/realm-dart/pull/476)) * Support remove user. ([#492](https://github.com/realm/realm-dart/pull/492)) * Support switch current user. ([#493](https://github.com/realm/realm-dart/pull/493)) +* Support user custom data and refresh. ([#525](https://github.com/realm/realm-dart/pull/525)) +* Support linking user credentials. ([#525](https://github.com/realm/realm-dart/pull/525)) +* Support user state. ([#525](https://github.com/realm/realm-dart/pull/525)) +* Support getting user identity and all identities. ([#525](https://github.com/realm/realm-dart/pull/525)) +* Support user logout. ([#525](https://github.com/realm/realm-dart/pull/525)) ### Fixed * Fixed an issue that would result in the wrong transaction being rolled back if you start a write transaction inside a write transaction. ([#442](https://github.com/realm/realm-dart/issues/442)) diff --git a/README.md b/README.md index 1f03fce34..ba589e852 100644 --- a/README.md +++ b/README.md @@ -395,7 +395,7 @@ BAAS_API_KEY= BAAS_PRIVATE_API_KEY= BAAS_PROJECT_ID= ``` -10) Now you can run `dart test` and it should include the integration tests (`testWithBaaS`). +10) Now you can run `dart test` and it should include the integration tests. If you are a MongoDB employee, you can instead choose to run the tests against [cloud-dev](cloud-dev.mongodb.com). The procedure is the same, except you need to use your qa credentials instead. diff --git a/lib/src/app.dart b/lib/src/app.dart index e66360265..3a6f1d1b5 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -123,7 +123,7 @@ class App { /// Logs in a user with the given credentials. Future logIn(Credentials credentials) async { var userHandle = await realmCore.logIn(this, credentials); - return UserInternal.create(userHandle); + return UserInternal.create(this, userHandle); } /// Gets the currently logged in [User]. If none exists, `null` is returned. @@ -132,16 +132,16 @@ class App { if (userHandle == null) { return null; } - return UserInternal.create(userHandle); + return UserInternal.create(this, userHandle); } /// Gets all currently logged in users. Iterable get users { - return realmCore.getUsers(this).map((handle) => UserInternal.create(handle)); + return realmCore.getUsers(this).map((handle) => UserInternal.create(this, handle)); } - /// Removes the user's local credentials and attempts to invalidate their refresh token from the server. - /// + /// Removes the user's local credentials. This will also close any associated Sessions. + /// /// If [user] is null logs out [currentUser] if it exists. Future logout(User? user) async { user ??= currentUser; diff --git a/lib/src/native/realm_core.dart b/lib/src/native/realm_core.dart index e774e8d82..0f889d7f7 100644 --- a/lib/src/native/realm_core.dart +++ b/lib/src/native/realm_core.dart @@ -861,7 +861,7 @@ class _RealmCore { return AppHandle._(realmAppPtr); } - static void _logInCallback(Pointer userdata, Pointer user, Pointer error) { + static void _app_user_completion_callback(Pointer userdata, Pointer user, Pointer error) { final Completer? completer = userdata.toObject(isPersistent: true); if (completer == null) { return; @@ -875,7 +875,7 @@ class _RealmCore { var userClone = _realmLib.realm_clone(user.cast()); if (userClone == nullptr) { - completer.completeError(RealmException("Error while cloning login data")); + completer.completeError(RealmException("Error while cloning user object.")); return; } @@ -888,7 +888,7 @@ class _RealmCore { () => _realmLib.realm_app_log_in_with_credentials( app.handle._pointer, credentials.handle._pointer, - Pointer.fromFunction(_logInCallback), + Pointer.fromFunction(_app_user_completion_callback), completer.toPersistentHandle(), _deletePersistentHandleFuncPtr, ), @@ -897,7 +897,7 @@ class _RealmCore { } static void void_completion_callback(Pointer userdata, Pointer error) { - final Completer? completer = userdata.toObject(); + final Completer? completer = userdata.toObject(isPersistent: true); if (completer == null) { return; } @@ -1038,7 +1038,7 @@ class _RealmCore { completer.complete(); } - Future logOut(App application, User user) async { + Future logOut(App application, User user) { final completer = Completer(); _realmLib.invokeGetBool( () => _realmLib.realm_app_log_out( @@ -1079,7 +1079,7 @@ class _RealmCore { }); } - Future removeUser(App app, User user) async { + Future removeUser(App app, User user) { final completer = Completer(); _realmLib.invokeGetBool( () => _realmLib.realm_app_remove_user( @@ -1092,7 +1092,7 @@ class _RealmCore { "Remove user failed"); return completer.future; } - + void switchUser(App application, User user) { return using((arena) { _realmLib.invokeGetBool( @@ -1104,6 +1104,82 @@ class _RealmCore { "Switch user failed"); }); } + + String userGetCustomData(User user) { + final customDataPtr = _realmLib.invokeGetPointer(() => _realmLib.realm_user_get_custom_data(user.handle._pointer)); + try { + final customData = customDataPtr.cast().toDartString(); + return customData; + } finally { + _realmLib.realm_free(customDataPtr.cast()); + } + } + + Future userRefreshCustomData(App app, User user) { + final completer = Completer(); + _realmLib.invokeGetBool( + () => _realmLib.realm_app_refresh_custom_data( + app.handle._pointer, + user.handle._pointer, + Pointer.fromFunction(void_completion_callback), + completer.toPersistentHandle(), + _deletePersistentHandleFuncPtr, + ), + "Refresh custom data failed"); + return completer.future; + } + + Future userLinkCredentials(App app, User user, Credentials credentials) { + final completer = Completer(); + _realmLib.invokeGetBool( + () => _realmLib.realm_app_link_user( + app.handle._pointer, + user.handle._pointer, + credentials.handle._pointer, + Pointer.fromFunction(_app_user_completion_callback), + completer.toPersistentHandle(), + _deletePersistentHandleFuncPtr, + ), + "Link credentials failed"); + return completer.future; + } + + UserState userGetState(User user) { + final nativeUserState = _realmLib.realm_user_get_state(user.handle._pointer); + return UserState.values.fromIndex(nativeUserState); + } + + String userGetId(User user) { + final idPtr = _realmLib.invokeGetPointer(() => _realmLib.realm_user_get_identity(user.handle._pointer), "Error while getting user id"); + final userId = idPtr.cast().toDartString(); + return userId; + } + + List userGetIdentities(User user) { + return using((arena) { + //TODO: This approach is prone to race conditions. Fix this once Core changes how count is retrieved. + final idsCount = arena(); + _realmLib.invokeGetBool( + () => _realmLib.realm_user_get_all_identities(user.handle._pointer, nullptr, 0, idsCount), "Error while getting user identities count"); + + final idsPtr = arena(idsCount.value); + _realmLib.invokeGetBool( + () => _realmLib.realm_user_get_all_identities(user.handle._pointer, idsPtr, idsCount.value, idsCount), "Error while getting user identities"); + + final userIdentities = []; + for (var i = 0; i < idsCount.value; i++) { + final idPtr = idsPtr.elementAt(i); + userIdentities.add(UserIdentityInternal.create(idPtr.ref.id.cast().toDartString(), AuthProviderType.values.fromIndex(idPtr.ref.provider_type))); + } + + return userIdentities; + }); + } + + Future userLogOut(User user) { + _realmLib.invokeGetBool(() => _realmLib.realm_user_log_out(user.handle._pointer), "Logout failed"); + return Future.value(); + } } class LastError { @@ -1405,6 +1481,26 @@ extension on Object { } } +extension on List { + AuthProviderType fromIndex(int index) { + if (!AuthProviderType.values.any((value) => value.index == index)) { + throw RealmError("Unknown AuthProviderType $index"); + } + + return AuthProviderType.values[index]; + } +} + +extension on List { + UserState fromIndex(int index) { + if (!UserState.values.any((value) => value.index == index)) { + throw RealmError("Unknown user state $index"); + } + + return UserState.values[index]; + } +} + // TODO: Once enhanced-enums land in 2.17, replace with: /* enum _CustomErrorCode { diff --git a/lib/src/user.dart b/lib/src/user.dart index a015833b8..5e262c0fe 100644 --- a/lib/src/user.dart +++ b/lib/src/user.dart @@ -16,7 +16,12 @@ // //////////////////////////////////////////////////////////////////////////////// +import 'dart:convert'; + import 'native/realm_core.dart'; +import 'app.dart'; +import 'credentials.dart'; +import 'realm_class.dart'; /// This class represents a user in a MongoDB Realm app. /// A user can log in to the server and, if access is granted, it is possible to synchronize the local Realm to MongoDB. @@ -27,12 +32,85 @@ import 'native/realm_core.dart'; class User { final UserHandle _handle; - User._(this._handle); + /// The [App] with which the user is associated with. + final App app; + + User._(this.app, this._handle); + + /// The custom user data associated with this user. + dynamic get customData { + final data = realmCore.userGetCustomData(this); + return jsonDecode(data); + } + + /// Refreshes the custom data for a this [User]. + Future refreshCustomData() async { + await realmCore.userRefreshCustomData(app, this); + return customData; + } + + /// Links this [User] with a new [User] identity represented by the given credentials. + /// + /// Linking a user with more credentials, mean the user can login either of these credentials. It also makes it possible to "upgrade" an anonymous user + /// by linking it with e.g. Email/Password credentials. + /// Note: It is not possible to link two existing users of MongoDB Realm. The provided credentials must not have been used by another user. + Future linkCredentials(Credentials credentials) async { + final userHandle = await realmCore.userLinkCredentials(app, this, credentials); + return UserInternal.create(app, userHandle); + } + + /// The current state of this [User]. + UserState get state { + return realmCore.userGetState(this); + } + + /// Get this [User]'s id on MongoDB Realm + String get id { + return realmCore.userGetId(this); + } + + /// Gets a collection of all identities associated with this [User] + List get identities { + return realmCore.userGetIdentities(this); + } + + /// Removes the user's local credentials. This will also close any associated Sessions. + Future logout() async { + return await realmCore.userLogOut(this); + } +} + +/// The current state of a [User]. +enum UserState { + /// The user is logged in, and any Realms associated with it are synchronizing with MongoDB Realm. + loogedIn, + + /// The user is logged out. Call LogInAsync(Credentials) with valid credentials to log the user back in. + loggedOut, + + /// The user has been logged out and their local data has been removed. + removed, +} + +/// The user identity associated with a [User] +class UserIdentity { + /// The unique identifier for this [UserIdentity] + final String id; + + /// The authentication provider defining this identity + final AuthProviderType provider; + + const UserIdentity._(this.id, this.provider); +} + +/// @nodoc +extension UserIdentityInternal on UserIdentity { + static UserIdentity create(String identity, AuthProviderType provider) => UserIdentity._(identity, provider); } /// @nodoc extension UserInternal on User { UserHandle get handle => _handle; - static User create(UserHandle handle) => User._(handle); + static User create(App app, UserHandle handle) => User._(app, handle); } diff --git a/scripts/build.bat b/scripts/build.bat index c67e2e7f2..01ea88e48 100644 --- a/scripts/build.bat +++ b/scripts/build.bat @@ -14,7 +14,7 @@ pushd %PROJECT_ROOT%\build-windows SET EXIT_CODE=0 cmake ^ - -G "Visual Studio 16 2019" ^ + -G "Visual Studio 17 2022" ^ -A x64 ^ -DCMAKE_TOOLCHAIN_FILE="%PROJECT_ROOT%/src/realm-core/tools/vcpkg/ports/scripts/buildsystems/vcpkg.cmake" ^ -DVCPKG_MANIFEST_DIR="%PROJECT_ROOT%/src/realm-core/tools/vcpkg" ^