瀏覽代碼

feat: integrate client-api (#3430)

* chore: update client-api rev

* chore: update collab rev id

* feat: add sign_in_request and import shared entity

* feat: added to userworkspace from af_workspace

* chore: add script to update the client-api rev id

* chore: update client-api rev

* feat: add workspaces api

* feat: added check user

* chore: config

* chore: update client_api version

* chore: ws connect

* chore: ws connect

* chore: update crate versions

* chore: rename event

* chore: update client-appi

* chore: set appflowy cloud env

* chore: add env template

* chore: update env name

* docs: update docs

* fix: check_user

* feat: impl sign_in_with_url

* feat: add file storage placeholders

* chore: update client-api

* chore: disable test

* feat: impl workspace add and remove

* chore: sign up test

* feat: select cover image on upload (#3488)

* fix: close popover after item selection in settings view (#3362)

* fix: close popover after item selection in settings view

* fix: add missing await before closing popover

* fix: find popover container by context instead of passing controllers around

* fix: add requested changes

* feat: close text direction settings popups after selection

* fix: clean up

* fix: restore theme value dropdown as StatefulWidget

* feat: openai and stabilityai integration (#3439)

* chore: create trait

* test: add tests

* chore: remove log

* chore: disable log

* chore: checklist ux flow redesign (#3418)

* chore: ux flow redesign

* chore: remove unused imports

* fix: allow creation of tasks of the same name

* chore: apply code suggestions from Mathias

* fix: add padding below field title text field (#3440)

* Fixed Issue no #3426

* Reversed the pubspec.lock mistaken update

* FIXED PADDING

* Fixed Padding issue on calender field edit popup

* chore: rename package name (#3501)

* fix: right icon size sam as left one (#3494)

* feat: enable removing user icon (#3487)

* feat: enable removing user icon

* fix: generate to true

* fix: review comments

* fix: more review comments

* fix: integration test and final changes

* fix: made cursor grab and background color when hovering on Appearance Options Buttons (#3498)

* chore: calendar UI polish (#3484)

* chore: update calendar theming

* feat: add event popup editor

* chore: new event button redesign and add card shadows

* chore: unscheduled events button

* chore: event title text field

* fix: focus node double dispose

* chore: show popover when create new event

* test: integrate some tests for integration testing purposes

* fix: some fixes and more integration tests

* chore: add more space between font item and font menu

* feat: add reset font button in toolbar

* feat: only show text direction toolbar item when RTL is enabled

* fix:  unable to change RTL of heading block

* test: add integration test for ltr/rtl mode

* chore: update inlang project settings (#3441)

* feat: using script to update the collab source. (#3508)

* chore: add script

* chore: update script

* chore: update bytes version

* chore: submit lock file

* chore: update test

* chore: update test

* chore: bump version

* chore: update

* ci: fix

* ci: fix

* chore: update commit id

* chore: update commit id

* chore: update commit id

* fix: is cloud enable

---------

Co-authored-by: Fu Zi Xiang <[email protected]>
Co-authored-by: Mathias Mogensen <[email protected]>
Co-authored-by: Vincenzo De Petris <[email protected]>
Co-authored-by: Richard Shiue <[email protected]>
Co-authored-by: Aryan More <[email protected]>
Co-authored-by: Lucas.Xu <[email protected]>
Co-authored-by: Lakhan Baheti <[email protected]>
Co-authored-by: Nitin-Poojary <[email protected]>
Co-authored-by: Jannes Blobel <[email protected]>
Nathan.fooo 1 年之前
父節點
當前提交
7f44b181bd
共有 98 個文件被更改,包括 2353 次插入755 次删除
  1. 2 2
      frontend/appflowy_flutter/.gitignore
  2. 21 0
      frontend/appflowy_flutter/dev.env
  3. 1 1
      frontend/appflowy_flutter/integration_test/runner.dart
  4. 57 2
      frontend/appflowy_flutter/lib/env/env.dart
  5. 21 10
      frontend/appflowy_flutter/lib/startup/deps_resolver.dart
  6. 6 0
      frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart
  7. 1 1
      frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart
  8. 69 0
      frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart
  9. 0 9
      frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart
  10. 5 5
      frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart
  11. 12 21
      frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart
  12. 8 16
      frontend/appflowy_flutter/lib/user/application/auth/supabase_mock_auth_service.dart
  13. 1 1
      frontend/appflowy_flutter/lib/user/application/prelude.dart
  14. 2 2
      frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart
  15. 1 1
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart
  16. 2 2
      frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart
  17. 19 1
      frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.dart
  18. 0 33
      frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.i.dart
  19. 0 1
      frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart
  20. 434 63
      frontend/appflowy_tauri/src-tauri/Cargo.lock
  21. 23 24
      frontend/appflowy_tauri/src-tauri/Cargo.toml
  22. 560 37
      frontend/rust-lib/Cargo.lock
  23. 14 12
      frontend/rust-lib/Cargo.toml
  24. 11 1
      frontend/rust-lib/collab-integrate/src/collab_builder.rs
  25. 3 0
      frontend/rust-lib/dart-ffi/src/env_serde.rs
  26. 2 0
      frontend/rust-lib/flowy-core/Cargo.toml
  27. 2 2
      frontend/rust-lib/flowy-core/src/integrate/log.rs
  28. 8 16
      frontend/rust-lib/flowy-core/src/integrate/server.rs
  29. 19 7
      frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs
  30. 1 1
      frontend/rust-lib/flowy-core/src/lib.rs
  31. 1 1
      frontend/rust-lib/flowy-database-deps/src/cloud.rs
  32. 2 2
      frontend/rust-lib/flowy-database2/Cargo.toml
  33. 2 3
      frontend/rust-lib/flowy-document2/src/document.rs
  34. 3 0
      frontend/rust-lib/flowy-error/src/code.rs
  35. 3 0
      frontend/rust-lib/flowy-error/src/impl_from/cloud.rs
  36. 1 1
      frontend/rust-lib/flowy-folder2/Cargo.toml
  37. 40 0
      frontend/rust-lib/flowy-server-config/src/af_cloud_config.rs
  38. 1 0
      frontend/rust-lib/flowy-server-config/src/lib.rs
  39. 4 3
      frontend/rust-lib/flowy-server/Cargo.toml
  40. 0 79
      frontend/rust-lib/flowy-server/src/af_cloud/configuration.rs
  41. 0 5
      frontend/rust-lib/flowy-server/src/af_cloud/configuration/base.yaml
  42. 0 3
      frontend/rust-lib/flowy-server/src/af_cloud/configuration/local.yaml
  43. 0 2
      frontend/rust-lib/flowy-server/src/af_cloud/configuration/production.yaml
  44. 22 3
      frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs
  45. 27 4
      frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs
  46. 43 0
      frontend/rust-lib/flowy-server/src/af_cloud/impls/file_storage.rs
  47. 29 10
      frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs
  48. 2 0
      frontend/rust-lib/flowy-server/src/af_cloud/impls/mod.rs
  49. 213 56
      frontend/rust-lib/flowy-server/src/af_cloud/impls/user.rs
  50. 0 1
      frontend/rust-lib/flowy-server/src/af_cloud/mod.rs
  51. 55 22
      frontend/rust-lib/flowy-server/src/af_cloud/server.rs
  52. 1 1
      frontend/rust-lib/flowy-server/src/local_server/impls/database.rs
  53. 13 4
      frontend/rust-lib/flowy-server/src/local_server/impls/user.rs
  54. 2 2
      frontend/rust-lib/flowy-server/src/server.rs
  55. 2 2
      frontend/rust-lib/flowy-server/src/supabase/api/database.rs
  56. 28 6
      frontend/rust-lib/flowy-server/src/supabase/api/user.rs
  57. 1 0
      frontend/rust-lib/flowy-server/src/supabase/define.rs
  58. 2 0
      frontend/rust-lib/flowy-server/tests/af_cloud_test/mod.rs
  59. 21 0
      frontend/rust-lib/flowy-server/tests/af_cloud_test/user_test.rs
  60. 57 0
      frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs
  61. 1 0
      frontend/rust-lib/flowy-server/tests/main.rs
  62. 2 2
      frontend/rust-lib/flowy-server/tests/supabase_test/database_test.rs
  63. 4 4
      frontend/rust-lib/flowy-server/tests/supabase_test/folder_test.rs
  64. 6 6
      frontend/rust-lib/flowy-server/tests/supabase_test/user_test.rs
  65. 7 7
      frontend/rust-lib/flowy-server/tests/supabase_test/util.rs
  66. 7 7
      frontend/rust-lib/flowy-test/Cargo.toml
  67. 101 10
      frontend/rust-lib/flowy-test/src/lib.rs
  68. 1 1
      frontend/rust-lib/flowy-test/tests/database/mod.rs
  69. 2 8
      frontend/rust-lib/flowy-test/tests/database/supabase_test/helper.rs
  70. 34 0
      frontend/rust-lib/flowy-test/tests/document/af_cloud_test/edit_test.rs
  71. 2 0
      frontend/rust-lib/flowy-test/tests/document/af_cloud_test/mod.rs
  72. 37 0
      frontend/rust-lib/flowy-test/tests/document/af_cloud_test/util.rs
  73. 2 1
      frontend/rust-lib/flowy-test/tests/document/mod.rs
  74. 61 0
      frontend/rust-lib/flowy-test/tests/document/supabase_test/edit_test.rs
  75. 4 56
      frontend/rust-lib/flowy-test/tests/document/supabase_test/helper.rs
  76. 1 1
      frontend/rust-lib/flowy-test/tests/document/supabase_test/mod.rs
  77. 0 86
      frontend/rust-lib/flowy-test/tests/document/supabase_test/test.rs
  78. 1 1
      frontend/rust-lib/flowy-test/tests/folder/mod.rs
  79. 1 1
      frontend/rust-lib/flowy-test/tests/folder/supabase_test/helper.rs
  80. 1 0
      frontend/rust-lib/flowy-test/tests/user/af_cloud_test/mod.rs
  81. 13 0
      frontend/rust-lib/flowy-test/tests/user/af_cloud_test/test.rs
  82. 2 1
      frontend/rust-lib/flowy-test/tests/user/mod.rs
  83. 16 27
      frontend/rust-lib/flowy-test/tests/user/supabase_test/auth_test.rs
  84. 3 3
      frontend/rust-lib/flowy-test/tests/user/supabase_test/workspace_test.rs
  85. 34 0
      frontend/rust-lib/flowy-test/tests/util.rs
  86. 1 1
      frontend/rust-lib/flowy-user-deps/Cargo.toml
  87. 6 16
      frontend/rust-lib/flowy-user-deps/src/cloud.rs
  88. 11 6
      frontend/rust-lib/flowy-user-deps/src/entities.rs
  89. 1 1
      frontend/rust-lib/flowy-user/Cargo.toml
  90. 17 2
      frontend/rust-lib/flowy-user/src/entities/auth.rs
  91. 17 2
      frontend/rust-lib/flowy-user/src/event_handler.rs
  92. 9 3
      frontend/rust-lib/flowy-user/src/event_map.rs
  93. 15 3
      frontend/rust-lib/flowy-user/src/manager.rs
  94. 4 4
      frontend/rust-lib/flowy-user/src/services/entities.rs
  95. 30 0
      frontend/scripts/tool/update_client_api_rev.sh
  96. 7 2
      frontend/scripts/tool/update_collab_rev.sh
  97. 9 9
      shared-lib/Cargo.lock
  98. 3 3
      shared-lib/lib-infra/Cargo.toml

+ 2 - 2
frontend/appflowy_flutter/.gitignore

@@ -69,8 +69,8 @@ windows/flutter/dart_ffi/
 **/.sandbox
 **/.vscode/
 
-*.env
-*.env.*
+.env
+.env.*
 
 coverage/
 

+ 21 - 0
frontend/appflowy_flutter/dev.env

@@ -0,0 +1,21 @@
+# Initial Setup
+# 1. Copy the dev.env file to .env:
+# cp dev.env .env 
+# 2. Alternatively, you can generate the .env file using the "Generate Env File" task in VSCode.
+
+# Configuring Cloud Type
+# This configuration file is used to specify the cloud type and the necessary configurations for each cloud type. The available options are:
+# Supabase: Set CLOUD_TYPE to 1
+# AppFlowy Cloud: Set CLOUD_TYPE to 2
+
+CLOUD_TYPE=1
+
+# Supabase Configuration
+# If you're using Supabase (CLOUD_TYPE=1), you need to provide the following configurations:
+SUPABASE_URL=replace-with-your-supabase-url
+SUPABASE_ANON_KEY=replace-with-your-supabase-key
+
+# AppFlowy Cloud Configuration
+# If you're using AppFlowy Cloud (CLOUD_TYPE=2), you need to provide the following configurations:
+APPFLOWY_CLOUD_BASE_URL=replace-with-your-appflowy-cloud-url
+APPFLOWY_CLOUD_BASE_WS_URL=replace-with-your-appflowy-cloud-ws-url

+ 1 - 1
frontend/appflowy_flutter/integration_test/runner.dart

@@ -73,7 +73,7 @@ void main() {
   user_icon_test.main();
   user_language_test.main();
 
-  if (isSupabaseEnabled) {
+  if (isCloudEnabled) {
     auth_test_runner.main();
   }
 

+ 57 - 2
frontend/appflowy_flutter/lib/env/env.dart

@@ -1,5 +1,6 @@
 // lib/env/env.dart
 import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy_backend/log.dart';
 import 'package:envied/envied.dart';
 
 part 'env.g.dart';
@@ -17,6 +18,29 @@ part 'env.g.dart';
 ///
 @Envied(path: '.env')
 abstract class Env {
+  @EnviedField(
+    obfuscate: true,
+    varName: 'CLOUD_TYPE',
+    defaultValue: '0',
+  )
+  static final int cloudType = _Env.cloudType;
+
+  /// AppFlowy Cloud Configuration
+  @EnviedField(
+    obfuscate: true,
+    varName: 'APPFLOWY_CLOUD_BASE_URL',
+    defaultValue: '',
+  )
+  static final String afCloudBaseUrl = _Env.afCloudBaseUrl;
+
+  @EnviedField(
+    obfuscate: true,
+    varName: 'APPFLOWY_CLOUD_BASE_WS_URL',
+    defaultValue: '',
+  )
+  static final String afCloudBaseWSUrl = _Env.afCloudBaseWSUrl;
+
+  // Supabase Configuration:
   @EnviedField(
     obfuscate: true,
     varName: 'SUPABASE_URL',
@@ -31,11 +55,42 @@ abstract class Env {
   static final String supabaseAnonKey = _Env.supabaseAnonKey;
 }
 
-bool get isSupabaseEnabled {
+bool get isCloudEnabled {
   // Only enable supabase in release and develop mode.
   if (integrationMode().isRelease || integrationMode().isDevelop) {
-    return Env.supabaseUrl.isNotEmpty && Env.supabaseAnonKey.isNotEmpty;
+    return currentCloudType().isEnabled;
   } else {
     return false;
   }
 }
+
+enum CloudType {
+  unknown,
+  supabase,
+  appflowyCloud;
+
+  bool get isEnabled => this != CloudType.unknown;
+}
+
+CloudType currentCloudType() {
+  final value = Env.cloudType;
+  if (value == 1) {
+    if (Env.supabaseUrl.isEmpty || Env.supabaseAnonKey.isEmpty) {
+      Log.error("Supabase is not configured");
+      return CloudType.unknown;
+    } else {
+      return CloudType.supabase;
+    }
+  }
+
+  if (value == 2) {
+    if (Env.afCloudBaseUrl.isEmpty || Env.afCloudBaseWSUrl.isEmpty) {
+      Log.error("AppFlowy cloud is not configured");
+      return CloudType.unknown;
+    } else {
+      return CloudType.appflowyCloud;
+    }
+  }
+
+  return CloudType.unknown;
+}

+ 21 - 10
frontend/appflowy_flutter/lib/startup/deps_resolver.dart

@@ -11,8 +11,9 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_p
 import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
 import 'package:appflowy/plugins/trash/application/prelude.dart';
 import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy/user/application/auth/af_cloud_auth_service.dart';
 import 'package:appflowy/user/application/auth/auth_service.dart';
-import 'package:appflowy/user/application/auth/mock_auth_service.dart';
+import 'package:appflowy/user/application/auth/supabase_mock_auth_service.dart';
 import 'package:appflowy/user/application/auth/supabase_auth_service.dart';
 import 'package:appflowy/user/application/prelude.dart';
 import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
@@ -29,7 +30,7 @@ import 'package:appflowy/workspace/application/view/prelude.dart';
 import 'package:appflowy/workspace/application/workspace/prelude.dart';
 import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
 import 'package:flowy_infra/file_picker/file_picker_impl.dart';
 import 'package:flowy_infra/file_picker/file_picker_service.dart';
 import 'package:fluttertoast/fluttertoast.dart';
@@ -90,14 +91,24 @@ void _resolveCommonService(
 }
 
 void _resolveUserDeps(GetIt getIt, IntegrationMode mode) {
-  if (isSupabaseEnabled) {
-    if (mode.isIntegrationTest) {
-      getIt.registerFactory<AuthService>(() => MockAuthService());
-    } else {
-      getIt.registerFactory<AuthService>(() => SupabaseAuthService());
-    }
-  } else {
-    getIt.registerFactory<AuthService>(() => AppFlowyAuthService());
+  switch (currentCloudType()) {
+    case CloudType.unknown:
+      getIt.registerFactory<AuthService>(
+        () => BackendAuthService(
+          AuthTypePB.Local,
+        ),
+      );
+      break;
+    case CloudType.supabase:
+      if (mode.isIntegrationTest) {
+        getIt.registerFactory<AuthService>(() => MockAuthService());
+      } else {
+        getIt.registerFactory<AuthService>(() => SupabaseAuthService());
+      }
+      break;
+    case CloudType.appflowyCloud:
+      getIt.registerFactory<AuthService>(() => AFCloudAuthService());
+      break;
   }
 
   getIt.registerFactory<AuthRouter>(() => AuthRouter());

+ 6 - 0
frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart

@@ -36,8 +36,14 @@ AppFlowyEnv getAppFlowyEnv() {
     anon_key: Env.supabaseAnonKey,
   );
 
+  final appflowyCloudConfig = AppFlowyCloudConfiguration(
+    base_url: Env.afCloudBaseUrl,
+    base_ws_url: Env.afCloudBaseWSUrl,
+  );
+
   return AppFlowyEnv(
     supabase_config: supabaseConfig,
+    appflowy_cloud_config: appflowyCloudConfig,
   );
 }
 

+ 1 - 1
frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart

@@ -28,7 +28,7 @@ SupbaseRealtimeService? realtimeService;
 class InitSupabaseTask extends LaunchTask {
   @override
   Future<void> initialize(LaunchContext context) async {
-    if (!isSupabaseEnabled) {
+    if (!isCloudEnabled) {
       return;
     }
 

+ 69 - 0
frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart

@@ -0,0 +1,69 @@
+import 'dart:async';
+
+import 'package:appflowy/user/application/auth/backend_auth_service.dart';
+import 'package:appflowy/user/application/auth/auth_service.dart';
+import 'package:appflowy/user/application/user_service.dart';
+import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
+import 'package:dartz/dartz.dart';
+
+class AFCloudAuthService implements AuthService {
+  AFCloudAuthService();
+
+  final BackendAuthService _backendAuthService = BackendAuthService(
+    AuthTypePB.AFCloud,
+  );
+
+  @override
+  Future<Either<FlowyError, UserProfilePB>> signUp({
+    required String name,
+    required String email,
+    required String password,
+    Map<String, String> params = const {},
+  }) async {
+    throw UnimplementedError();
+  }
+
+  @override
+  Future<Either<FlowyError, UserProfilePB>> signIn({
+    required String email,
+    required String password,
+    Map<String, String> params = const {},
+  }) async {
+    throw UnimplementedError();
+  }
+
+  @override
+  Future<Either<FlowyError, UserProfilePB>> signUpWithOAuth({
+    required String platform,
+    Map<String, String> params = const {},
+  }) async {
+    //
+    throw UnimplementedError();
+  }
+
+  @override
+  Future<void> signOut() async {
+    await _backendAuthService.signOut();
+  }
+
+  @override
+  Future<Either<FlowyError, UserProfilePB>> signUpAsGuest({
+    Map<String, String> params = const {},
+  }) async {
+    return _backendAuthService.signUpAsGuest();
+  }
+
+  @override
+  Future<Either<FlowyError, UserProfilePB>> signInWithMagicLink({
+    required String email,
+    Map<String, String> params = const {},
+  }) async {
+    throw UnimplementedError();
+  }
+
+  @override
+  Future<Either<FlowyError, UserProfilePB>> getUser() async {
+    return UserBackendService.getCurrentUserProfile();
+  }
+}

+ 0 - 9
frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart

@@ -1,5 +1,4 @@
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-user/auth.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
 import 'package:dartz/dartz.dart';
 
@@ -21,7 +20,6 @@ abstract class AuthService {
   ///
   /// - `email`: The email address of the user.
   /// - `password`: The password of the user.
-  /// - `authType`: The type of authentication (optional).
   /// - `params`: Additional parameters for authentication (optional).
   ///
   /// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError].
@@ -29,7 +27,6 @@ abstract class AuthService {
   Future<Either<FlowyError, UserProfilePB>> signIn({
     required String email,
     required String password,
-    AuthTypePB authType,
     Map<String, String> params,
   });
 
@@ -38,7 +35,6 @@ abstract class AuthService {
   /// - `name`: The name of the user.
   /// - `email`: The email address of the user.
   /// - `password`: The password of the user.
-  /// - `authType`: The type of authentication (optional).
   /// - `params`: Additional parameters for registration (optional).
   ///
   /// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError].
@@ -46,31 +42,26 @@ abstract class AuthService {
     required String name,
     required String email,
     required String password,
-    AuthTypePB authType,
     Map<String, String> params,
   });
 
   /// Registers a new user with an OAuth platform.
   ///
   /// - `platform`: The OAuth platform name.
-  /// - `authType`: The type of authentication (optional).
   /// - `params`: Additional parameters for OAuth registration (optional).
   ///
   /// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError].
   Future<Either<FlowyError, UserProfilePB>> signUpWithOAuth({
     required String platform,
-    AuthTypePB authType,
     Map<String, String> params,
   });
 
   /// Registers a user as a guest.
   ///
-  /// - `authType`: The type of authentication (optional).
   /// - `params`: Additional parameters for guest registration (optional).
   ///
   /// Returns a default [UserProfilePB].
   Future<Either<FlowyError, UserProfilePB>> signUpAsGuest({
-    AuthTypePB authType,
     Map<String, String> params,
   });
 

+ 5 - 5
frontend/appflowy_flutter/lib/user/application/auth/appflowy_auth_service.dart → frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart

@@ -13,12 +13,15 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
 import '../../../generated/locale_keys.g.dart';
 import 'device_id.dart';
 
-class AppFlowyAuthService implements AuthService {
+class BackendAuthService implements AuthService {
+  final AuthTypePB authType;
+
+  BackendAuthService(this.authType);
+
   @override
   Future<Either<FlowyError, UserProfilePB>> signIn({
     required String email,
     required String password,
-    AuthTypePB authType = AuthTypePB.Local,
     Map<String, String> params = const {},
   }) async {
     final request = SignInPayloadPB.create()
@@ -35,7 +38,6 @@ class AppFlowyAuthService implements AuthService {
     required String name,
     required String email,
     required String password,
-    AuthTypePB authType = AuthTypePB.Local,
     Map<String, String> params = const {},
   }) async {
     final request = SignUpPayloadPB.create()
@@ -52,7 +54,6 @@ class AppFlowyAuthService implements AuthService {
 
   @override
   Future<void> signOut({
-    AuthTypePB authType = AuthTypePB.Local,
     Map<String, String> params = const {},
   }) async {
     await UserEventSignOut().send();
@@ -61,7 +62,6 @@ class AppFlowyAuthService implements AuthService {
 
   @override
   Future<Either<FlowyError, UserProfilePB>> signUpAsGuest({
-    AuthTypePB authType = AuthTypePB.Local,
     Map<String, String> params = const {},
   }) {
     const password = "Guest!@123456";

+ 12 - 21
frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart

@@ -1,7 +1,7 @@
 import 'dart:async';
 
 import 'package:appflowy/startup/tasks/prelude.dart';
-import 'package:appflowy/user/application/auth/appflowy_auth_service.dart';
+import 'package:appflowy/user/application/auth/backend_auth_service.dart';
 import 'package:appflowy/user/application/auth/auth_service.dart';
 import 'package:appflowy/user/application/auth/device_id.dart';
 import 'package:appflowy/user/application/user_service.dart';
@@ -20,14 +20,15 @@ class SupabaseAuthService implements AuthService {
   SupabaseClient get _client => Supabase.instance.client;
   GoTrueClient get _auth => _client.auth;
 
-  final AppFlowyAuthService _appFlowyAuthService = AppFlowyAuthService();
+  final BackendAuthService _backendAuthService = BackendAuthService(
+    AuthTypePB.Supabase,
+  );
 
   @override
   Future<Either<FlowyError, UserProfilePB>> signUp({
     required String name,
     required String email,
     required String password,
-    AuthTypePB authType = AuthTypePB.Supabase,
     Map<String, String> params = const {},
   }) async {
     // fetch the uuid from supabase.
@@ -41,11 +42,10 @@ class SupabaseAuthService implements AuthService {
     }
     // assign the uuid to our backend service.
     //  and will transfer this logic to backend later.
-    return _appFlowyAuthService.signUp(
+    return _backendAuthService.signUp(
       name: name,
       email: email,
       password: password,
-      authType: authType,
       params: {
         AuthServiceMapKeys.uuid: uuid,
       },
@@ -56,7 +56,6 @@ class SupabaseAuthService implements AuthService {
   Future<Either<FlowyError, UserProfilePB>> signIn({
     required String email,
     required String password,
-    AuthTypePB authType = AuthTypePB.Supabase,
     Map<String, String> params = const {},
   }) async {
     try {
@@ -68,10 +67,9 @@ class SupabaseAuthService implements AuthService {
       if (uuid == null) {
         return Left(AuthError.supabaseSignInError);
       }
-      return _appFlowyAuthService.signIn(
+      return _backendAuthService.signIn(
         email: email,
         password: password,
-        authType: authType,
         params: {
           AuthServiceMapKeys.uuid: uuid,
         },
@@ -85,7 +83,6 @@ class SupabaseAuthService implements AuthService {
   @override
   Future<Either<FlowyError, UserProfilePB>> signUpWithOAuth({
     required String platform,
-    AuthTypePB authType = AuthTypePB.Supabase,
     Map<String, String> params = const {},
   }) async {
     // Before signing in, sign out any existing users. Otherwise, the callback will be triggered even if the user doesn't click the 'Sign In' button on the website
@@ -118,23 +115,18 @@ class SupabaseAuthService implements AuthService {
   }
 
   @override
-  Future<void> signOut({
-    AuthTypePB authType = AuthTypePB.Supabase,
-  }) async {
+  Future<void> signOut() async {
     await _auth.signOut();
-    await _appFlowyAuthService.signOut(
-      authType: authType,
-    );
+    await _backendAuthService.signOut();
   }
 
   @override
   Future<Either<FlowyError, UserProfilePB>> signUpAsGuest({
-    AuthTypePB authType = AuthTypePB.Supabase,
     Map<String, String> params = const {},
   }) async {
     // supabase don't support guest login.
     // so, just forward to our backend.
-    return _appFlowyAuthService.signUpAsGuest();
+    return _backendAuthService.signUpAsGuest();
   }
 
   @override
@@ -177,13 +169,12 @@ class SupabaseAuthService implements AuthService {
   Future<Either<FlowyError, UserProfilePB>> _setupAuth({
     required Map<String, String> map,
   }) async {
-    final payload = ThirdPartyAuthPB(
+    final payload = OAuthPB(
       authType: AuthTypePB.Supabase,
       map: map,
     );
-    return UserEventThirdPartyAuth(payload)
-        .send()
-        .then((value) => value.swap());
+
+    return UserEventOAuth(payload).send().then((value) => value.swap());
   }
 }
 

+ 8 - 16
frontend/appflowy_flutter/lib/user/application/auth/mock_auth_service.dart → frontend/appflowy_flutter/lib/user/application/auth/supabase_mock_auth_service.dart

@@ -1,6 +1,6 @@
 import 'dart:async';
 
-import 'package:appflowy/user/application/auth/appflowy_auth_service.dart';
+import 'package:appflowy/user/application/auth/backend_auth_service.dart';
 import 'package:appflowy/user/application/auth/auth_service.dart';
 import 'package:appflowy/user/application/user_service.dart';
 import 'package:appflowy_backend/dispatch/dispatch.dart';
@@ -20,14 +20,14 @@ class MockAuthService implements AuthService {
   SupabaseClient get _client => Supabase.instance.client;
   GoTrueClient get _auth => _client.auth;
 
-  final AppFlowyAuthService _appFlowyAuthService = AppFlowyAuthService();
+  final BackendAuthService _appFlowyAuthService =
+      BackendAuthService(AuthTypePB.Supabase);
 
   @override
   Future<Either<FlowyError, UserProfilePB>> signUp({
     required String name,
     required String email,
     required String password,
-    AuthTypePB authType = AuthTypePB.Supabase,
     Map<String, String> params = const {},
   }) async {
     throw UnimplementedError();
@@ -37,7 +37,6 @@ class MockAuthService implements AuthService {
   Future<Either<FlowyError, UserProfilePB>> signIn({
     required String email,
     required String password,
-    AuthTypePB authType = AuthTypePB.Supabase,
     Map<String, String> params = const {},
   }) async {
     throw UnimplementedError();
@@ -46,7 +45,6 @@ class MockAuthService implements AuthService {
   @override
   Future<Either<FlowyError, UserProfilePB>> signUpWithOAuth({
     required String platform,
-    AuthTypePB authType = AuthTypePB.Supabase,
     Map<String, String> params = const {},
   }) async {
     try {
@@ -58,7 +56,7 @@ class MockAuthService implements AuthService {
       final uuid = response.user!.id;
       final email = response.user!.email!;
 
-      final payload = ThirdPartyAuthPB(
+      final payload = OAuthPB(
         authType: AuthTypePB.Supabase,
         map: {
           AuthServiceMapKeys.uuid: uuid,
@@ -66,9 +64,8 @@ class MockAuthService implements AuthService {
           AuthServiceMapKeys.deviceId: 'MockDeviceId'
         },
       );
-      return UserEventThirdPartyAuth(payload)
-          .send()
-          .then((value) => value.swap());
+
+      return UserEventOAuth(payload).send().then((value) => value.swap());
     } on AuthException catch (e) {
       Log.error(e);
       return Left(AuthError.supabaseSignInError);
@@ -76,18 +73,13 @@ class MockAuthService implements AuthService {
   }
 
   @override
-  Future<void> signOut({
-    AuthTypePB authType = AuthTypePB.Supabase,
-  }) async {
+  Future<void> signOut() async {
     await _auth.signOut();
-    await _appFlowyAuthService.signOut(
-      authType: authType,
-    );
+    await _appFlowyAuthService.signOut();
   }
 
   @override
   Future<Either<FlowyError, UserProfilePB>> signUpAsGuest({
-    AuthTypePB authType = AuthTypePB.Supabase,
     Map<String, String> params = const {},
   }) async {
     // supabase don't support guest login.

+ 1 - 1
frontend/appflowy_flutter/lib/user/application/prelude.dart

@@ -1,4 +1,4 @@
-export 'auth/appflowy_auth_service.dart';
+export 'auth/backend_auth_service.dart';
 export './sign_in_bloc.dart';
 export './sign_up_bloc.dart';
 export './splash_bloc.dart';

+ 2 - 2
frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart

@@ -105,10 +105,10 @@ class SplashScreen extends StatelessWidget {
 
   void _handleUnauthenticated(BuildContext context, Unauthenticated result) {
     Log.trace(
-      '_handleUnauthenticated -> Supabase is enabled: $isSupabaseEnabled',
+      '_handleUnauthenticated -> cloud is enabled: $isCloudEnabled',
     );
     // replace Splash screen as root page
-    if (isSupabaseEnabled) {
+    if (isCloudEnabled) {
       context.go(SignInScreen.routeName);
     } else {
       // if the env is not configured, we will skip to the 'skip login screen'.

+ 1 - 1
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart

@@ -60,7 +60,7 @@ class SettingsMenu extends StatelessWidget {
         ),
 
         // Only show supabase setting if supabase is enabled and the current auth type is not local
-        if (isSupabaseEnabled &&
+        if (isCloudEnabled &&
             context.read<SettingsDialogBloc>().state.userProfile.authType !=
                 AuthTypePB.Local)
           SettingsMenuElement(

+ 2 - 2
frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart

@@ -54,7 +54,7 @@ class SettingsUserView extends StatelessWidget {
             mainAxisSize: MainAxisSize.min,
             children: [
               _buildUserIconSetting(context),
-              if (isSupabaseEnabled) ...[
+              if (isCloudEnabled) ...[
                 const VSpace(12),
                 UserEmailInput(user.email)
               ],
@@ -188,7 +188,7 @@ class SettingsUserView extends StatelessWidget {
     BuildContext context,
     SettingsUserState state,
   ) {
-    if (!isSupabaseEnabled) {
+    if (!isCloudEnabled) {
       return const SizedBox.shrink();
     }
 

+ 19 - 1
frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.dart

@@ -5,14 +5,16 @@ import 'package:json_annotation/json_annotation.dart';
 //
 // the file `env_serde.g.dart` will be generated in the same directory. Rename
 // the file to `env_serde.i.dart` because the file is ignored by default.
-part 'env_serde.i.dart';
+part 'env_serde.g.dart';
 
 @JsonSerializable()
 class AppFlowyEnv {
   final SupabaseConfiguration supabase_config;
+  final AppFlowyCloudConfiguration appflowy_cloud_config;
 
   AppFlowyEnv({
     required this.supabase_config,
+    required this.appflowy_cloud_config,
   });
 
   factory AppFlowyEnv.fromJson(Map<String, dynamic> json) =>
@@ -39,3 +41,19 @@ class SupabaseConfiguration {
 
   Map<String, dynamic> toJson() => _$SupabaseConfigurationToJson(this);
 }
+
+@JsonSerializable()
+class AppFlowyCloudConfiguration {
+  final String base_url;
+  final String base_ws_url;
+
+  AppFlowyCloudConfiguration({
+    required this.base_url,
+    required this.base_ws_url,
+  });
+
+  factory AppFlowyCloudConfiguration.fromJson(Map<String, dynamic> json) =>
+      _$AppFlowyCloudConfigurationFromJson(json);
+
+  Map<String, dynamic> toJson() => _$AppFlowyCloudConfigurationToJson(this);
+}

+ 0 - 33
frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.i.dart

@@ -1,33 +0,0 @@
-// GENERATED CODE - DO NOT MODIFY BY HAND
-
-part of 'env_serde.dart';
-
-// **************************************************************************
-// JsonSerializableGenerator
-// **************************************************************************
-
-AppFlowyEnv _$AppFlowyEnvFromJson(Map<String, dynamic> json) => AppFlowyEnv(
-      supabase_config: SupabaseConfiguration.fromJson(
-          json['supabase_config'] as Map<String, dynamic>),
-    );
-
-Map<String, dynamic> _$AppFlowyEnvToJson(AppFlowyEnv instance) =>
-    <String, dynamic>{
-      'supabase_config': instance.supabase_config,
-    };
-
-SupabaseConfiguration _$SupabaseConfigurationFromJson(
-        Map<String, dynamic> json) =>
-    SupabaseConfiguration(
-      enable_sync: json['enable_sync'] as bool? ?? true,
-      url: json['url'] as String,
-      anon_key: json['anon_key'] as String,
-    );
-
-Map<String, dynamic> _$SupabaseConfigurationToJson(
-        SupabaseConfiguration instance) =>
-    <String, dynamic>{
-      'enable_sync': instance.enable_sync,
-      'url': instance.url,
-      'anon_key': instance.anon_key,
-    };

+ 0 - 1
frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart

@@ -88,7 +88,6 @@ class FlowyColorScheme {
   final Color calendarWeekendBGColor;
   //grid bottom count color
   final Color gridRowCountColor;
-
   const FlowyColorScheme({
     required this.surface,
     required this.hover,

File diff suppressed because it is too large
+ 434 - 63
frontend/appflowy_tauri/src-tauri/Cargo.lock


+ 23 - 24
frontend/appflowy_tauri/src-tauri/Cargo.toml

@@ -18,18 +18,11 @@ serde_json = "1.0"
 serde = { version = "1.0", features = ["derive"] }
 tauri = { version = "1.2", features = ["fs-all", "shell-open"] }
 tauri-utils = "1.2"
-bytes = { version = "1.4" }
+bytes = { version = "1.5" }
 tracing = { version = "0.1", features = ["log"] }
-lib-dispatch = { path = "../../rust-lib/lib-dispatch", features = [
-    "use_serde",
-] }
-flowy-core = { path = "../../rust-lib/flowy-core", features = [
-    "rev-sqlite",
-    "ts",
-] }
-flowy-notification = { path = "../../rust-lib/flowy-notification", features = [
-    "ts",
-] }
+lib-dispatch = { path = "../../rust-lib/lib-dispatch", features = ["use_serde"] }
+flowy-core = { path = "../../rust-lib/flowy-core", features = ["rev-sqlite", "ts"] }
+flowy-notification = { path = "../../rust-lib/flowy-notification", features = ["ts"] }
 
 [features]
 # by default Tauri runs in production mode
@@ -40,24 +33,30 @@ default = ["custom-protocol"]
 custom-protocol = ["tauri/custom-protocol"]
 
 [patch.crates-io]
-client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "8f8f6a" }
-
-# ⚠️⚠️⚠️
+# Please using the following command to update the revision id
+# Current directory: frontend
+# Run the script:
+# scripts/tool/update_client_api_rev.sh  new_rev_id
+# ⚠️⚠️⚠️️
+client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "b0c213" }
 # Please use the following script to update collab.
 # Working directory: frontend
 #
 # To update the commit ID, run:
-# scripts/tool/update_collab_rev.sh e37ee7
+# scripts/tool/update_collab_rev.sh new_rev_id
 #
 # To switch to the local path, run:
 # scripts/tool/update_collab_source.sh
 # ⚠️⚠️⚠️️
-collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e37ee7" }
-collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e37ee7" }
-collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e37ee7" }
-collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e37ee7" }
-collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e37ee7" }
-collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e37ee7" }
-collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e37ee7" }
-collab-define = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e37ee7" }
-collab-sync-protocol = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e37ee7" }
+collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" }
+collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" }
+collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" }
+collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" }
+collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" }
+collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" }
+collab-define = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" }
+collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" }
+
+
+
+

File diff suppressed because it is too large
+ 560 - 37
frontend/rust-lib/Cargo.lock


+ 14 - 12
frontend/rust-lib/Cargo.toml

@@ -77,9 +77,12 @@ lto = false
 incremental = false
 
 [patch.crates-io]
-client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "8f8f6a" }
-
-# ⚠️⚠️⚠️
+# Please using the following command to update the revision id
+# Current directory: frontend
+# Run the script:
+# scripts/tool/update_client_api_rev.sh  new_rev_id
+# ⚠️⚠️⚠️️
+client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "b0c213" }
 # Please use the following script to update collab.
 # Working directory: frontend
 #
@@ -89,12 +92,11 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "8f8
 # To switch to the local path, run:
 # scripts/tool/update_collab_source.sh
 # ⚠️⚠️⚠️️
-collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e37ee7" }
-collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e37ee7" }
-collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e37ee7" }
-collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e37ee7" }
-collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e37ee7" }
-collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e37ee7" }
-collab-define = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e37ee7" }
-collab-sync-protocol = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e37ee7" }
-collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e37ee7" }
+collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" }
+collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" }
+collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" }
+collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" }
+collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" }
+collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" }
+collab-define = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" }
+collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" }

+ 11 - 1
frontend/rust-lib/collab-integrate/src/collab_builder.rs

@@ -201,7 +201,17 @@ impl AppFlowyCollabBuilder {
         CollabSource::AFCloud => {
           #[cfg(feature = "appflowy_cloud_integrate")]
           {
-            //
+            let local_collab = Arc::downgrade(&collab);
+            let plugins = block_on(
+              cloud_storage.get_plugins(CollabPluginContext::AppFlowyCloud {
+                uid,
+                collab_object: collab_object.clone(),
+                local_collab,
+              }),
+            );
+            for plugin in plugins {
+              collab.lock().add_plugin(plugin);
+            }
           }
         },
         CollabSource::Supabase => {

+ 3 - 0
frontend/rust-lib/dart-ffi/src/env_serde.rs

@@ -1,10 +1,12 @@
 use serde::Deserialize;
 
+use flowy_server_config::af_cloud_config::AFCloudConfiguration;
 use flowy_server_config::supabase_config::SupabaseConfiguration;
 
 #[derive(Deserialize, Debug)]
 pub struct AppFlowyEnv {
   supabase_config: SupabaseConfiguration,
+  appflowy_cloud_config: AFCloudConfiguration,
 }
 
 impl AppFlowyEnv {
@@ -13,6 +15,7 @@ impl AppFlowyEnv {
   pub fn parser(env_str: &str) {
     if let Ok(env) = serde_json::from_str::<AppFlowyEnv>(env_str) {
       env.supabase_config.write_env();
+      env.appflowy_cloud_config.write_env();
     }
   }
 }

+ 2 - 0
frontend/rust-lib/flowy-core/Cargo.toml

@@ -31,11 +31,13 @@ collab = { version = "0.1.0" }
 diesel = { version = "1.4.8", features = ["sqlite"] }
 uuid = { version = "1.3.3", features = ["v4"] }
 flowy-storage = { workspace = true }
+client-api = { version = "0.1.0", features = ["collab-sync"] }
 
 tracing = { version = "0.1", features = ["log"] }
 futures-core = { version = "0.3", default-features = false }
 bytes = "1.5"
 tokio = { version = "1.26", features = ["full"] }
+tokio-stream = {version = "0.1.14", features = ["sync"]}
 console-subscriber = { version = "0.1.8", optional = true }
 parking_lot = "0.12.1"
 anyhow = "1.0.75"

+ 2 - 2
frontend/rust-lib/flowy-core/src/integrate/log.rs

@@ -25,7 +25,7 @@ pub(crate) fn create_log_filter(level: String, with_crates: Vec<String>) -> Stri
   filters.push(format!("collab_persistence={}", level));
   filters.push(format!("collab_database={}", level));
   filters.push(format!("collab_plugins={}", level));
-  filters.push(format!("appflowy_integrate={}", level));
+  filters.push(format!("collab_integrate={}", level));
   filters.push(format!("collab={}", level));
   filters.push(format!("flowy_user={}", level));
   filters.push(format!("flowy_document2={}", level));
@@ -37,7 +37,7 @@ pub(crate) fn create_log_filter(level: String, with_crates: Vec<String>) -> Stri
 
   filters.push(format!("dart_ffi={}", "info"));
   filters.push(format!("flowy_sqlite={}", "info"));
-  filters.push(format!("flowy_net={}", level));
+  filters.push(format!("client_api={}", level));
   #[cfg(feature = "profiling")]
   filters.push(format!("tokio={}", level));
 

+ 8 - 16
frontend/rust-lib/flowy-core/src/integrate/server.rs

@@ -6,12 +6,12 @@ use parking_lot::RwLock;
 use serde_repr::*;
 
 use collab_integrate::YrsDocAction;
-use flowy_error::{ErrorCode, FlowyError, FlowyResult};
-use flowy_server::af_cloud::configuration::appflowy_cloud_server_configuration;
+use flowy_error::{FlowyError, FlowyResult};
 use flowy_server::af_cloud::AFCloudServer;
 use flowy_server::local_server::{LocalServer, LocalServerDB};
 use flowy_server::supabase::SupabaseServer;
 use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl};
+use flowy_server_config::af_cloud_config::AFCloudConfiguration;
 use flowy_server_config::supabase_config::SupabaseConfiguration;
 use flowy_sqlite::kv::StorePreferences;
 use flowy_user::services::database::{
@@ -30,10 +30,10 @@ pub enum ServerType {
   /// Local server provider.
   /// Offline mode, no user authentication and the data is stored locally.
   Local = 0,
-  /// Self-hosted server provider.
+  /// AppFlowy Cloud server provider.
   /// The [AppFlowy-Server](https://github.com/AppFlowy-IO/AppFlowy-Cloud) is still a work in
   /// progress.
-  AppFlowyCloud = 1,
+  AFCloud = 1,
   /// Supabase server provider.
   /// It uses supabase postgresql database to store data and user authentication.
   Supabase = 2,
@@ -43,7 +43,7 @@ impl Display for ServerType {
   fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
     match self {
       ServerType::Local => write!(f, "Local"),
-      ServerType::AppFlowyCloud => write!(f, "AppFlowyCloud"),
+      ServerType::AFCloud => write!(f, "AppFlowyCloud"),
       ServerType::Supabase => write!(f, "Supabase"),
     }
   }
@@ -111,16 +111,8 @@ impl ServerProvider {
         let server = Arc::new(LocalServer::new(local_db));
         Ok::<Arc<dyn AppFlowyServer>, FlowyError>(server)
       },
-      ServerType::AppFlowyCloud => {
-        let config = appflowy_cloud_server_configuration().map_err(|e| {
-          FlowyError::new(
-            ErrorCode::InvalidAuthConfig,
-            format!(
-              "Missing self host config: {:?}. Error: {:?}",
-              server_type, e
-            ),
-          )
-        })?;
+      ServerType::AFCloud => {
+        let config = AFCloudConfiguration::from_env()?;
         tracing::trace!("🔑AppFlowy cloud config: {:?}", config);
         let server = Arc::new(AFCloudServer::new(
           config,
@@ -163,7 +155,7 @@ impl From<AuthType> for ServerType {
   fn from(auth_provider: AuthType) -> Self {
     match auth_provider {
       AuthType::Local => ServerType::Local,
-      AuthType::SelfHosted => ServerType::AppFlowyCloud,
+      AuthType::AFCloud => ServerType::AFCloud,
       AuthType::Supabase => ServerType::Supabase,
     }
   }

+ 19 - 7
frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs

@@ -2,10 +2,10 @@ use std::sync::Arc;
 
 use anyhow::Error;
 use bytes::Bytes;
+use client_api::collab_sync::{SinkConfig, SyncObject, SyncPlugin};
 use collab::core::origin::{CollabClient, CollabOrigin};
 use collab::preclude::CollabPlugin;
 use collab_define::CollabType;
-use collab_plugins::sync_plugin::{SyncObject, SyncPlugin};
 
 use collab_integrate::collab_builder::{CollabPluginContext, CollabSource, CollabStorageProvider};
 use collab_integrate::postgres::SupabaseDBPlugin;
@@ -177,14 +177,14 @@ impl DatabaseCloudService for ServerProvider {
   fn get_collab_update(
     &self,
     object_id: &str,
-    object_ty: CollabType,
+    collab_type: CollabType,
   ) -> FutureResult<CollabObjectUpdate, Error> {
     let server = self.get_server(&self.get_server_type());
     let database_id = object_id.to_string();
     FutureResult::new(async move {
       server?
         .database_service()
-        .get_collab_update(&database_id, object_ty)
+        .get_collab_update(&database_id, collab_type)
         .await
     })
   }
@@ -273,19 +273,31 @@ impl CollabStorageProvider for ServerProvider {
         collab_object,
         local_collab,
       } => {
-        if let Ok(server) = self.get_server(&ServerType::AppFlowyCloud) {
+        if let Ok(server) = self.get_server(&ServerType::AFCloud) {
           match server.collab_ws_channel(&collab_object.object_id).await {
-            Ok(Some(channel)) => {
+            Ok(Some((channel, ws_connect_state))) => {
               let origin = CollabOrigin::Client(CollabClient::new(
                 collab_object.uid,
                 collab_object.device_id.clone(),
               ));
               let sync_object = SyncObject::from(collab_object);
               let (sink, stream) = (channel.sink(), channel.stream());
-              let sync_plugin = SyncPlugin::new(origin, sync_object, local_collab, sink, stream);
+              let sink_config = SinkConfig::new().with_timeout(6);
+              let sync_plugin = SyncPlugin::new(
+                origin,
+                sync_object,
+                local_collab,
+                sink,
+                sink_config,
+                stream,
+                Some(channel),
+                ws_connect_state,
+              );
               plugins.push(Arc::new(sync_plugin));
             },
-            Ok(None) => {},
+            Ok(None) => {
+              tracing::error!("🔴Failed to get collab ws channel: channel is none");
+            },
             Err(err) => tracing::error!("🔴Failed to get collab ws channel: {:?}", err),
           }
         }

+ 1 - 1
frontend/rust-lib/flowy-core/src/lib.rs

@@ -232,7 +232,7 @@ impl From<ServerType> for CollabSource {
   fn from(server_type: ServerType) -> Self {
     match server_type {
       ServerType::Local => CollabSource::Local,
-      ServerType::AppFlowyCloud => CollabSource::Local,
+      ServerType::AFCloud => CollabSource::AFCloud,
       ServerType::Supabase => CollabSource::Supabase,
     }
   }

+ 1 - 1
frontend/rust-lib/flowy-database-deps/src/cloud.rs

@@ -15,7 +15,7 @@ pub trait DatabaseCloudService: Send + Sync {
   fn get_collab_update(
     &self,
     object_id: &str,
-    object_ty: CollabType,
+    collab_type: CollabType,
   ) -> FutureResult<CollabObjectUpdate, Error>;
 
   fn batch_get_collab_updates(

+ 2 - 2
frontend/rust-lib/flowy-database2/Cargo.toml

@@ -26,7 +26,7 @@ serde = { version = "1.0", features = ["derive"] }
 serde_json = {version = "1.0"}
 serde_repr = "0.1"
 lib-infra = { path = "../../../shared-lib/lib-infra" }
-chrono = { version = "0.4.27", default-features = false, features = ["clock"] }
+chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
 rust_decimal = "1.28.1"
 rusty-money = {version = "0.4.1", features = ["iso"]}
 lazy_static = "1.4.0"
@@ -39,7 +39,7 @@ anyhow = "1.0"
 async-stream = "0.3.4"
 rayon = "1.6.1"
 nanoid = "0.4.0"
-async-trait = "0.1"
+async-trait = "0.1.73"
 chrono-tz = "0.8.2"
 csv = "1.1.6"
 

+ 2 - 3
frontend/rust-lib/flowy-document2/src/document.rs

@@ -7,7 +7,6 @@ use collab::core::collab::MutexCollab;
 use collab_document::{blocks::DocumentData, document::Document};
 use futures::StreamExt;
 use parking_lot::Mutex;
-use tokio_stream::wrappers::WatchStream;
 
 use flowy_error::FlowyResult;
 
@@ -61,7 +60,7 @@ fn subscribe_document_changed(doc_id: &str, document: &MutexDocument) {
 
 fn subscribe_document_snapshot_state(collab: &Arc<MutexCollab>) {
   let document_id = collab.lock().object_id.clone();
-  let mut snapshot_state = WatchStream::new(collab.lock().subscribe_snapshot_state());
+  let mut snapshot_state = collab.lock().subscribe_snapshot_state();
   tokio::spawn(async move {
     while let Some(snapshot_state) = snapshot_state.next().await {
       if let Some(new_snapshot_id) = snapshot_state.snapshot_id() {
@@ -79,7 +78,7 @@ fn subscribe_document_snapshot_state(collab: &Arc<MutexCollab>) {
 
 fn subscribe_document_sync_state(collab: &Arc<MutexCollab>) {
   let document_id = collab.lock().object_id.clone();
-  let mut sync_state_stream = WatchStream::new(collab.lock().subscribe_sync_state());
+  let mut sync_state_stream = collab.lock().subscribe_sync_state();
   tokio::spawn(async move {
     while let Some(sync_state) = sync_state_stream.next().await {
       send_notification(

+ 3 - 0
frontend/rust-lib/flowy-error/src/code.rs

@@ -250,6 +250,9 @@ pub enum ErrorCode {
 
   #[error("Missing payload")]
   MissingPayload = 82,
+
+  #[error("Permission denied")]
+  NotEnoughPermissions = 83,
 }
 
 impl ErrorCode {

+ 3 - 0
frontend/rust-lib/flowy-error/src/impl_from/cloud.rs

@@ -20,6 +20,9 @@ impl From<AppError> for FlowyError {
       client_api::error::ErrorCode::UrlMissingParameter => ErrorCode::InvalidParams,
       client_api::error::ErrorCode::InvalidOAuthProvider => ErrorCode::InvalidAuthConfig,
       client_api::error::ErrorCode::NotLoggedIn => ErrorCode::UserUnauthorized,
+      client_api::error::ErrorCode::NotEnoughPermissions => ErrorCode::NotEnoughPermissions,
+      client_api::error::ErrorCode::UserNameIsEmpty => ErrorCode::UserNameIsEmpty,
+      _ => ErrorCode::Internal,
     };
 
     FlowyError::new(code, error.message)

+ 1 - 1
frontend/rust-lib/flowy-folder2/Cargo.toml

@@ -24,7 +24,7 @@ lib-infra = { path = "../../../shared-lib/lib-infra" }
 tokio = { version = "1.26", features = ["full"] }
 nanoid = "0.4.0"
 lazy_static = "1.4.0"
-chrono = { version = "0.4.27", default-features = false, features = ["clock"] }
+chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
 strum_macros = "0.21"
 protobuf = {version = "2.28.0"}
 uuid = { version = "1.3.3", features = ["v4"] }

+ 40 - 0
frontend/rust-lib/flowy-server-config/src/af_cloud_config.rs

@@ -0,0 +1,40 @@
+use serde::{Deserialize, Serialize};
+
+use flowy_error::{ErrorCode, FlowyError};
+
+pub const AF_CLOUD_BASE_URL: &str = "AF_CLOUD_BASE_URL";
+pub const AF_CLOUD_WS_BASE_URL: &str = "AF_CLOUD_WS_BASE_URL";
+pub const AF_CLOUD_GOTRUE_URL: &str = "AF_GOTRUE_URL";
+
+#[derive(Debug, Serialize, Deserialize, Clone, Default)]
+pub struct AFCloudConfiguration {
+  pub base_url: String,
+  pub base_ws_url: String,
+  pub gotrue_url: String,
+}
+
+impl AFCloudConfiguration {
+  pub fn from_env() -> Result<Self, FlowyError> {
+    let base_url = std::env::var(AF_CLOUD_BASE_URL)
+      .map_err(|_| FlowyError::new(ErrorCode::InvalidAuthConfig, "Missing AF_CLOUD_BASE_URL"))?;
+
+    let base_ws_url = std::env::var(AF_CLOUD_WS_BASE_URL)
+      .map_err(|_| FlowyError::new(ErrorCode::InvalidAuthConfig, "Missing AF_CLOUD_WS_BASE_URL"))?;
+
+    let gotrue_url = std::env::var(AF_CLOUD_GOTRUE_URL)
+      .map_err(|_| FlowyError::new(ErrorCode::InvalidAuthConfig, "Missing AF_CLOUD_GOTRUE_URL"))?;
+
+    Ok(Self {
+      base_url,
+      base_ws_url,
+      gotrue_url,
+    })
+  }
+
+  /// Write the configuration to the environment variables.
+  pub fn write_env(&self) {
+    std::env::set_var(AF_CLOUD_BASE_URL, &self.base_url);
+    std::env::set_var(AF_CLOUD_WS_BASE_URL, &self.base_ws_url);
+    std::env::set_var(AF_CLOUD_GOTRUE_URL, &self.gotrue_url);
+  }
+}

+ 1 - 0
frontend/rust-lib/flowy-server-config/src/lib.rs

@@ -1 +1,2 @@
+pub mod af_cloud_config;
 pub mod supabase_config;

+ 4 - 3
frontend/rust-lib/flowy-server/Cargo.toml

@@ -19,11 +19,11 @@ thiserror = "1.0"
 tokio = { version = "1.26", features = ["sync"]}
 parking_lot = "0.12"
 lazy_static = "1.4.0"
-bytes = { version = "1.0.1", features = ["serde"] }
+bytes = { version = "1.5", features = ["serde"] }
 tokio-retry = "0.3"
 anyhow = "1.0"
 uuid = { version = "1.3.3", features = ["v4"] }
-chrono = { version = "0.4.27", default-features = false, features = ["clock"] }
+chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
 collab = { version = "0.1.0" }
 collab-plugins = { version = "0.1.0", features = ["sync_plugin"] }
 collab-document = { version = "0.1.0" }
@@ -42,7 +42,7 @@ flowy-storage = { workspace = true }
 mime_guess = "2.0"
 url = "2.4"
 tokio-util = "0.7"
-client-api = { version = "0.1.0" }
+client-api = { version = "0.1.0", features = ["collab-sync"] }
 
 [dev-dependencies]
 uuid = { version = "1.3.3", features = ["v4"] }
@@ -51,3 +51,4 @@ dotenv = "0.15.0"
 yrs = "0.16.5"
 assert-json-diff = "2.0.2"
 serde_json = "1.0.104"
+client-api = { version = "0.1.0" }

+ 0 - 79
frontend/rust-lib/flowy-server/src/af_cloud/configuration.rs

@@ -1,79 +0,0 @@
-use std::convert::{TryFrom, TryInto};
-
-use config::FileFormat;
-use serde_aux::field_attributes::deserialize_number_from_string;
-
-pub const HEADER_TOKEN: &str = "token";
-
-#[derive(serde::Deserialize, Clone, Debug)]
-pub struct AFCloudConfiguration {
-  #[serde(deserialize_with = "deserialize_number_from_string")]
-  pub port: u16,
-  pub host: String,
-  pub http_scheme: String,
-  pub ws_scheme: String,
-}
-
-pub fn appflowy_cloud_server_configuration() -> Result<AFCloudConfiguration, config::ConfigError> {
-  let mut settings = config::Config::default();
-  let base = include_str!("./configuration/base.yaml");
-  settings.merge(config::File::from_str(base, FileFormat::Yaml).required(true))?;
-
-  let environment: Environment = std::env::var("APP_ENVIRONMENT")
-    .unwrap_or_else(|_| "local".to_owned())
-    .try_into()
-    .expect("Failed to parse APP_ENVIRONMENT.");
-
-  let custom = match environment {
-    Environment::Local => include_str!("./configuration/local.yaml"),
-    Environment::Production => include_str!("./configuration/production.yaml"),
-  };
-
-  settings.merge(config::File::from_str(custom, FileFormat::Yaml).required(true))?;
-  settings.try_into()
-}
-
-impl AFCloudConfiguration {
-  pub fn reset_host_with_port(&mut self, host: &str, port: u16) {
-    self.host = host.to_owned();
-    self.port = port;
-  }
-
-  pub fn base_url(&self) -> String {
-    format!("{}://{}:{}", self.http_scheme, self.host, self.port)
-  }
-
-  pub fn ws_addr(&self) -> String {
-    format!("{}://{}:{}/ws", self.ws_scheme, self.host, self.port)
-  }
-}
-
-pub enum Environment {
-  Local,
-  Production,
-}
-
-impl Environment {
-  #[allow(dead_code)]
-  pub fn as_str(&self) -> &'static str {
-    match self {
-      Environment::Local => "local",
-      Environment::Production => "production",
-    }
-  }
-}
-
-impl TryFrom<String> for Environment {
-  type Error = String;
-
-  fn try_from(s: String) -> Result<Self, Self::Error> {
-    match s.to_lowercase().as_str() {
-      "local" => Ok(Self::Local),
-      "production" => Ok(Self::Production),
-      other => Err(format!(
-        "{} is not a supported environment. Use either `local` or `production`.",
-        other
-      )),
-    }
-  }
-}

+ 0 - 5
frontend/rust-lib/flowy-server/src/af_cloud/configuration/base.yaml

@@ -1,5 +0,0 @@
-port: 8000
-host: 0.0.0.0
-http_scheme: http
-ws_scheme: ws
-

+ 0 - 3
frontend/rust-lib/flowy-server/src/af_cloud/configuration/local.yaml

@@ -1,3 +0,0 @@
-host: 127.0.0.1
-http_scheme: http
-ws_scheme: ws

+ 0 - 2
frontend/rust-lib/flowy-server/src/af_cloud/configuration/production.yaml

@@ -1,2 +0,0 @@
-host: 0.0.0.0
-

+ 22 - 3
frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs

@@ -1,4 +1,6 @@
 use anyhow::Error;
+use client_api::entity::QueryCollabParams;
+use client_api::error::ErrorCode::RecordNotFound;
 use collab_define::CollabType;
 
 use flowy_database_deps::cloud::{
@@ -16,10 +18,27 @@ where
 {
   fn get_collab_update(
     &self,
-    _object_id: &str,
-    _object_ty: CollabType,
+    object_id: &str,
+    collab_type: CollabType,
   ) -> FutureResult<CollabObjectUpdate, Error> {
-    FutureResult::new(async move { Ok(vec![]) })
+    let object_id = object_id.to_string();
+    let try_get_client = self.0.try_get_client();
+    FutureResult::new(async move {
+      let params = QueryCollabParams {
+        object_id,
+        collab_type,
+      };
+      match try_get_client?.get_collab(params).await {
+        Ok(data) => Ok(vec![data]),
+        Err(err) => {
+          if err.code == RecordNotFound {
+            Ok(vec![])
+          } else {
+            Err(Error::new(err))
+          }
+        },
+      }
+    })
   }
 
   fn batch_get_collab_updates(

+ 27 - 4
frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs

@@ -1,4 +1,8 @@
 use anyhow::Error;
+use client_api::entity::QueryCollabParams;
+use collab::core::origin::CollabOrigin;
+use collab_define::CollabType;
+use collab_document::document::Document;
 
 use flowy_document_deps::cloud::*;
 use lib_infra::future::FutureResult;
@@ -11,8 +15,17 @@ impl<T> DocumentCloudService for AFCloudDocumentCloudServiceImpl<T>
 where
   T: AFServer,
 {
-  fn get_document_updates(&self, _document_id: &str) -> FutureResult<Vec<Vec<u8>>, Error> {
-    FutureResult::new(async move { Ok(vec![]) })
+  fn get_document_updates(&self, document_id: &str) -> FutureResult<Vec<Vec<u8>>, Error> {
+    let try_get_client = self.0.try_get_client();
+    let document_id = document_id.to_string();
+    FutureResult::new(async move {
+      let params = QueryCollabParams {
+        object_id: document_id.to_string(),
+        collab_type: CollabType::Document,
+      };
+      let data = try_get_client?.get_collab(params).await?;
+      Ok(vec![data])
+    })
   }
 
   fn get_document_snapshots(
@@ -23,7 +36,17 @@ where
     FutureResult::new(async move { Ok(vec![]) })
   }
 
-  fn get_document_data(&self, _document_id: &str) -> FutureResult<Option<DocumentData>, Error> {
-    FutureResult::new(async move { Ok(None) })
+  fn get_document_data(&self, document_id: &str) -> FutureResult<Option<DocumentData>, Error> {
+    let try_get_client = self.0.try_get_client();
+    let document_id = document_id.to_string();
+    FutureResult::new(async move {
+      let params = QueryCollabParams {
+        object_id: document_id.clone(),
+        collab_type: CollabType::Document,
+      };
+      let updates = vec![try_get_client?.get_collab(params).await?];
+      let document = Document::from_updates(CollabOrigin::Empty, updates, &document_id, vec![])?;
+      Ok(document.get_document_data().ok())
+    })
   }
 }

+ 43 - 0
frontend/rust-lib/flowy-server/src/af_cloud/impls/file_storage.rs

@@ -0,0 +1,43 @@
+use bytes::Bytes;
+use flowy_error::FlowyError;
+use flowy_storage::{FileStorageService, StorageObject};
+use lib_infra::future::FutureResult;
+
+use crate::af_cloud::AFServer;
+
+pub struct AFCloudFileStorageServiceImpl<T> {
+  #[allow(dead_code)]
+  client: T,
+}
+
+impl<T> AFCloudFileStorageServiceImpl<T> {
+  pub fn new(client: T) -> Self {
+    Self { client }
+  }
+}
+
+impl<T> FileStorageService for AFCloudFileStorageServiceImpl<T>
+where
+  T: AFServer,
+{
+  fn create_object(&self, _object: StorageObject) -> FutureResult<String, FlowyError> {
+    FutureResult::new(async move {
+      // TODO
+      Ok("".to_owned())
+    })
+  }
+
+  fn delete_object_by_url(&self, _object_url: String) -> FutureResult<(), FlowyError> {
+    FutureResult::new(async move {
+      // TODO
+      Ok(())
+    })
+  }
+
+  fn get_object_by_url(&self, _object_url: String) -> FutureResult<Bytes, FlowyError> {
+    FutureResult::new(async move {
+      // TODO
+      Ok(Bytes::new())
+    })
+  }
+}

+ 29 - 10
frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs

@@ -1,6 +1,9 @@
 use anyhow::Error;
+use client_api::entity::QueryCollabParams;
+use collab::core::origin::CollabOrigin;
+use collab_define::CollabType;
 
-use flowy_folder_deps::cloud::{FolderCloudService, FolderData, FolderSnapshot, Workspace};
+use flowy_folder_deps::cloud::{Folder, FolderCloudService, FolderData, FolderSnapshot, Workspace};
 use lib_infra::future::FutureResult;
 
 use crate::af_cloud::AFServer;
@@ -15,8 +18,19 @@ where
     FutureResult::new(async move { todo!() })
   }
 
-  fn get_folder_data(&self, _workspace_id: &str) -> FutureResult<Option<FolderData>, Error> {
-    FutureResult::new(async move { Ok(None) })
+  fn get_folder_data(&self, workspace_id: &str) -> FutureResult<Option<FolderData>, Error> {
+    let workspace_id = workspace_id.to_string();
+    let try_get_client = self.0.try_get_client();
+    FutureResult::new(async move {
+      let params = QueryCollabParams {
+        object_id: workspace_id.clone(),
+        collab_type: CollabType::Folder,
+      };
+      let updates = vec![try_get_client?.get_collab(params).await?];
+      let folder =
+        Folder::from_collab_raw_data(CollabOrigin::Empty, updates, &workspace_id, vec![])?;
+      Ok(folder.get_folder_data())
+    })
   }
 
   fn get_folder_snapshots(
@@ -27,15 +41,20 @@ where
     FutureResult::new(async move { Ok(vec![]) })
   }
 
-  fn get_folder_updates(
-    &self,
-    _workspace_id: &str,
-    _uid: i64,
-  ) -> FutureResult<Vec<Vec<u8>>, Error> {
-    FutureResult::new(async move { Ok(vec![]) })
+  fn get_folder_updates(&self, workspace_id: &str, _uid: i64) -> FutureResult<Vec<Vec<u8>>, Error> {
+    let workspace_id = workspace_id.to_string();
+    let try_get_client = self.0.try_get_client();
+    FutureResult::new(async move {
+      let params = QueryCollabParams {
+        object_id: workspace_id,
+        collab_type: CollabType::Folder,
+      };
+      let updates = vec![try_get_client?.get_collab(params).await?];
+      Ok(updates)
+    })
   }
 
   fn service_name(&self) -> String {
-    "SelfHosted".to_string()
+    "AppFlowy Cloud".to_string()
   }
 }

+ 2 - 0
frontend/rust-lib/flowy-server/src/af_cloud/impls/mod.rs

@@ -1,9 +1,11 @@
 pub(crate) use database::*;
 pub(crate) use document::*;
+pub(crate) use file_storage::*;
 pub(crate) use folder::*;
 pub(crate) use user::*;
 
 mod database;
 mod document;
+mod file_storage;
 mod folder;
 mod user;

+ 213 - 56
frontend/rust-lib/flowy-server/src/af_cloud/impls/user.rs

@@ -1,15 +1,19 @@
+use std::collections::HashMap;
 use std::sync::Arc;
 
-use anyhow::Error;
+use anyhow::{anyhow, Error};
+use client_api::entity::dto::UserUpdateParams;
+use client_api::entity::{AFUserProfileView, AFWorkspace, AFWorkspaces, InsertCollabParams};
 use collab_define::CollabObject;
 
-use flowy_error::FlowyError;
+use flowy_error::{ErrorCode, FlowyError};
 use flowy_user_deps::cloud::UserCloudService;
 use flowy_user_deps::entities::*;
 use lib_infra::box_any::BoxAny;
 use lib_infra::future::FutureResult;
 
 use crate::af_cloud::{AFCloudClient, AFServer};
+use crate::supabase::define::{USER_DEVICE_ID, USER_SIGN_IN_URL};
 
 pub(crate) struct AFCloudUserAuthServiceImpl<T> {
   server: T,
@@ -25,67 +29,151 @@ impl<T> UserCloudService for AFCloudUserAuthServiceImpl<T>
 where
   T: AFServer,
 {
-  fn sign_up(&self, params: BoxAny) -> FutureResult<SignUpResponse, Error> {
+  fn sign_up(&self, params: BoxAny) -> FutureResult<AuthResponse, Error> {
     let try_get_client = self.server.try_get_client();
     FutureResult::new(async move {
-      let params = params.unbox_or_error::<SignUpParams>()?;
+      let params = oauth_params_from_box_any(params)?;
       let resp = user_sign_up_request(try_get_client?, params).await?;
       Ok(resp)
     })
   }
 
-  fn sign_in(&self, _params: BoxAny) -> FutureResult<SignInResponse, Error> {
-    todo!()
+  // Zack: Not sure if this is needed anymore since sign_up handles both cases
+  fn sign_in(&self, params: BoxAny) -> FutureResult<AuthResponse, Error> {
+    let try_get_client = self.server.try_get_client();
+    FutureResult::new(async move {
+      let client = try_get_client?;
+      let params = oauth_params_from_box_any(params)?;
+      let resp = user_sign_in_with_url(client, params).await?;
+      Ok(resp)
+    })
   }
 
   fn sign_out(&self, _token: Option<String>) -> FutureResult<(), Error> {
-    todo!()
+    let try_get_client = self.server.try_get_client();
+    FutureResult::new(async move { Ok(try_get_client?.sign_out().await?) })
+  }
+
+  fn generate_sign_in_callback_url(&self, email: &str) -> FutureResult<String, Error> {
+    let email = email.to_string();
+    let try_get_client = self.server.try_get_client();
+    FutureResult::new(async move {
+      // TODO(nathan): replace the admin_email and admin_password with encryption key
+      let admin_email = std::env::var("GOTRUE_ADMIN_EMAIL").unwrap();
+      let admin_password = std::env::var("GOTRUE_ADMIN_PASSWORD").unwrap();
+      let url = try_get_client?
+        .generate_sign_in_callback_url(&admin_email, &admin_password, &email)
+        .await?;
+      Ok(url)
+    })
   }
 
   fn update_user(
     &self,
     _credential: UserCredentials,
-    _params: UpdateUserProfileParams,
+    params: UpdateUserProfileParams,
   ) -> FutureResult<(), Error> {
-    todo!()
+    let try_get_client = self.server.try_get_client();
+    FutureResult::new(async move {
+      let client = try_get_client?;
+      client
+        .update(UserUpdateParams {
+          name: params.name,
+          email: params.email,
+          password: params.password,
+        })
+        .await?;
+      Ok(())
+    })
   }
 
   fn get_user_profile(
     &self,
     _credential: UserCredentials,
   ) -> FutureResult<Option<UserProfile>, Error> {
-    todo!()
+    let try_get_client = self.server.try_get_client();
+    FutureResult::new(async move {
+      let client = try_get_client?;
+      let profile = client.profile().await?;
+      let encryption_type = encryption_type_from_profile(&profile);
+      Ok(Some(UserProfile {
+        email: profile.email.unwrap_or("".to_string()),
+        name: profile.name.unwrap_or("".to_string()),
+        token: token_from_client(client).await.unwrap_or("".to_string()),
+        icon_url: "".to_owned(),
+        openai_key: "".to_owned(),
+        workspace_id: match profile.latest_workspace_id {
+          Some(w) => w.to_string(),
+          None => "".to_string(),
+        },
+        auth_type: AuthType::AFCloud,
+        encryption_type,
+        uid: profile.uid.ok_or(anyhow!("no uid found"))?,
+      }))
+    })
   }
 
-  fn get_user_workspaces(
-    &self,
-    _uid: i64,
-  ) -> FutureResult<std::vec::Vec<flowy_user_deps::entities::UserWorkspace>, Error> {
-    // TODO(nathan): implement the RESTful API for this
-    todo!()
+  fn get_user_workspaces(&self, _uid: i64) -> FutureResult<Vec<UserWorkspace>, Error> {
+    let try_get_client = self.server.try_get_client();
+    FutureResult::new(async move {
+      let workspaces = try_get_client?.workspaces().await?;
+      Ok(to_user_workspaces(workspaces)?)
+    })
   }
 
-  fn check_user(&self, _credential: UserCredentials) -> FutureResult<(), Error> {
-    // TODO(nathan): implement the RESTful API for this
-    FutureResult::new(async { Ok(()) })
+  fn check_user(&self, credential: UserCredentials) -> FutureResult<(), Error> {
+    let try_get_client = self.server.try_get_client();
+    FutureResult::new(async move {
+      // from params
+      let token = credential.token.ok_or(anyhow!("expecting token"))?;
+      let uuid = credential.uuid.ok_or(anyhow!("expecting uuid"))?;
+      let uid = credential.uid.ok_or(anyhow!("expecting uid"))?;
+
+      // from cloud
+      let client = try_get_client?;
+      let profile = client.profile().await?;
+      let client_token = client.access_token()?;
+
+      // compare and check
+      if uuid != profile.uuid.ok_or(anyhow!("expecting uuid"))?.to_string() {
+        return Err(anyhow!("uuid mismatch"));
+      }
+      if uid != profile.uid.ok_or(anyhow!("expecting uid"))? {
+        return Err(anyhow!("uid mismatch"));
+      }
+      if token != client_token {
+        return Err(anyhow!("token mismatch"));
+      }
+      Ok(())
+    })
   }
 
   fn add_workspace_member(
     &self,
-    _user_email: String,
-    _workspace_id: String,
+    user_email: String,
+    workspace_id: String,
   ) -> FutureResult<(), Error> {
-    // TODO(nathan): implement the RESTful API for this
-    FutureResult::new(async { Ok(()) })
+    let try_get_client = self.server.try_get_client();
+    FutureResult::new(async move {
+      try_get_client?
+        .add_workspace_members(workspace_id.parse()?, vec![user_email])
+        .await?;
+      Ok(())
+    })
   }
 
   fn remove_workspace_member(
     &self,
-    _user_email: String,
-    _workspace_id: String,
+    user_email: String,
+    workspace_id: String,
   ) -> FutureResult<(), Error> {
-    // TODO(nathan): implement the RESTful API for this
-    FutureResult::new(async { Ok(()) })
+    let try_get_client = self.server.try_get_client();
+    FutureResult::new(async move {
+      try_get_client?
+        .remove_workspace_members(workspace_id.parse()?, vec![user_email])
+        .await?;
+      Ok(())
+    })
   }
 
   fn get_user_awareness_updates(&self, _uid: i64) -> FutureResult<Vec<Vec<u8>>, Error> {
@@ -100,39 +188,108 @@ where
 
   fn create_collab_object(
     &self,
-    _collab_object: &CollabObject,
-    _data: Vec<u8>,
+    collab_object: &CollabObject,
+    data: Vec<u8>,
   ) -> FutureResult<(), Error> {
-    // TODO(nathan): implement the RESTful API for this
-    FutureResult::new(async { Ok(()) })
+    let try_get_client = self.server.try_get_client();
+    let collab_object = collab_object.clone();
+    FutureResult::new(async move {
+      let client = try_get_client?;
+      let params = InsertCollabParams::new(
+        collab_object.uid,
+        collab_object.object_id.clone(),
+        collab_object.collab_type.clone(),
+        data,
+        collab_object.workspace_id.clone(),
+      );
+      client.create_collab(params).await?;
+      Ok(())
+    })
   }
 }
 
 pub async fn user_sign_up_request(
   client: Arc<AFCloudClient>,
-  params: SignUpParams,
-) -> Result<SignUpResponse, FlowyError> {
-  client
-    .read()
-    .await
-    .sign_up(&params.email, &params.password)
-    .await?;
-  todo!()
-  // tracing::info!("User signed up: {:?}", user);
-  // match user.confirmed_at {
-  //   Some(_) => {
-  //       // User is already confirmed, help her/him to sign in
-  //       let token = client.sign_in_password(&params.email, &params.password).await?;
-  //
-  //       // TODO:
-  //       // Query workspace list
-  //       // Query user profile
-  //
-  //       todo!()
-  //   },
-  //   None => Err(FlowyError::new(
-  //     ErrorCode::AwaitingEmailConfirmation,
-  //     "Awaiting email confirmation".to_string(),
-  //   )),
-  // }
+  params: AFCloudOAuthParams,
+) -> Result<AuthResponse, FlowyError> {
+  user_sign_in_with_url(client, params).await
+}
+
+pub async fn user_sign_in_with_url(
+  client: Arc<AFCloudClient>,
+  params: AFCloudOAuthParams,
+) -> Result<AuthResponse, FlowyError> {
+  let is_new_user = client.sign_in_url(&params.sign_in_url).await?;
+  let (profile, af_workspaces) = tokio::try_join!(client.profile(), client.workspaces())?;
+
+  let latest_workspace = to_user_workspace(
+    af_workspaces
+      .get_latest(&profile)
+      .or(af_workspaces.first().cloned())
+      .ok_or(anyhow!("no workspace found"))?,
+  )?;
+
+  let user_workspaces = to_user_workspaces(af_workspaces)?;
+  let encryption_type = encryption_type_from_profile(&profile);
+
+  Ok(AuthResponse {
+    user_id: profile.uid.ok_or(anyhow!("no uid found"))?,
+    name: profile.name.ok_or(anyhow!("no name found"))?,
+    latest_workspace,
+    user_workspaces,
+    email: profile.email,
+    token: token_from_client(client.clone()).await,
+    device_id: params.device_id,
+    encryption_type,
+    is_new_user,
+  })
+}
+
+async fn token_from_client(client: Arc<AFCloudClient>) -> Option<String> {
+  client.access_token().ok()
+}
+
+fn encryption_type_from_profile(profile: &AFUserProfileView) -> EncryptionType {
+  match &profile.encryption_sign {
+    Some(e) => EncryptionType::SelfEncryption(e.to_string()),
+    None => EncryptionType::NoEncryption,
+  }
+}
+
+fn to_user_workspace(af_workspace: AFWorkspace) -> Result<UserWorkspace, FlowyError> {
+  Ok(UserWorkspace {
+    id: af_workspace.workspace_id.to_string(),
+    name: af_workspace
+      .workspace_name
+      .ok_or(anyhow!("no workspace_name found"))?,
+    created_at: af_workspace
+      .created_at
+      .ok_or(anyhow!("no created_at found"))?,
+    database_views_aggregate_id: af_workspace
+      .database_storage_id
+      .ok_or(anyhow!("no database_views_aggregate_id found"))?
+      .to_string(),
+  })
+}
+
+fn to_user_workspaces(af_workspaces: AFWorkspaces) -> Result<Vec<UserWorkspace>, FlowyError> {
+  let mut result = Vec::with_capacity(af_workspaces.len());
+  for item in af_workspaces.0.into_iter() {
+    let user_workspace = to_user_workspace(item)?;
+    result.push(user_workspace);
+  }
+  Ok(result)
+}
+
+fn oauth_params_from_box_any(any: BoxAny) -> Result<AFCloudOAuthParams, Error> {
+  let map: HashMap<String, String> = any.unbox_or_error()?;
+  let sign_in_url = map
+    .get(USER_SIGN_IN_URL)
+    .ok_or_else(|| FlowyError::new(ErrorCode::MissingAuthField, "Missing token field"))?
+    .as_str();
+  let device_id = map.get(USER_DEVICE_ID).cloned().unwrap_or_default();
+  Ok(AFCloudOAuthParams {
+    sign_in_url: sign_in_url.to_string(),
+    device_id,
+  })
 }

+ 0 - 1
frontend/rust-lib/flowy-server/src/af_cloud/mod.rs

@@ -1,5 +1,4 @@
 pub use server::*;
 
-pub mod configuration;
 pub mod impls;
 mod server;

+ 55 - 22
frontend/rust-lib/flowy-server/src/af_cloud/server.rs

@@ -3,7 +3,9 @@ use std::sync::Arc;
 
 use anyhow::Error;
 use client_api::notify::{TokenState, TokenStateReceiver};
-use client_api::ws::{BusinessID, WSClient, WSClientConfig, WebSocketChannel};
+use client_api::ws::{
+  BusinessID, WSClient, WSClientConfig, WSConnectStateReceiver, WebSocketChannel,
+};
 use client_api::Client;
 use tokio::sync::RwLock;
 
@@ -11,18 +13,18 @@ use flowy_database_deps::cloud::DatabaseCloudService;
 use flowy_document_deps::cloud::DocumentCloudService;
 use flowy_error::{ErrorCode, FlowyError};
 use flowy_folder_deps::cloud::FolderCloudService;
+use flowy_server_config::af_cloud_config::AFCloudConfiguration;
 use flowy_storage::FileStorageService;
 use flowy_user_deps::cloud::UserCloudService;
 use lib_infra::future::FutureResult;
 
-use crate::af_cloud::configuration::AFCloudConfiguration;
 use crate::af_cloud::impls::{
-  AFCloudDatabaseCloudServiceImpl, AFCloudDocumentCloudServiceImpl, AFCloudFolderCloudServiceImpl,
-  AFCloudUserAuthServiceImpl,
+  AFCloudDatabaseCloudServiceImpl, AFCloudDocumentCloudServiceImpl, AFCloudFileStorageServiceImpl,
+  AFCloudFolderCloudServiceImpl, AFCloudUserAuthServiceImpl,
 };
 use crate::AppFlowyServer;
 
-pub(crate) type AFCloudClient = RwLock<client_api::Client>;
+pub(crate) type AFCloudClient = client_api::Client;
 
 pub struct AFCloudServer {
   #[allow(dead_code)]
@@ -41,17 +43,22 @@ impl AFCloudServer {
     device_id: Arc<parking_lot::RwLock<String>>,
   ) -> Self {
     let http_client = reqwest::Client::new();
-    let api_client = client_api::Client::from(http_client, &config.base_url(), &config.ws_addr());
+    let api_client = client_api::Client::from(
+      http_client,
+      &config.base_url,
+      &config.base_ws_url,
+      &config.gotrue_url,
+    );
     let token_state_rx = api_client.subscribe_token_state();
     let enable_sync = AtomicBool::new(enable_sync);
 
     let ws_client = WSClient::new(WSClientConfig {
       buffer_capacity: 100,
-      ping_per_secs: 2,
+      ping_per_secs: 8,
       retry_connect_per_pings: 5,
     });
     let ws_client = Arc::new(RwLock::new(ws_client));
-    let api_client = Arc::new(RwLock::new(api_client));
+    let api_client = Arc::new(api_client);
 
     spawn_ws_conn(&device_id, token_state_rx, &ws_client, &api_client);
     Self {
@@ -100,24 +107,24 @@ impl AppFlowyServer for AFCloudServer {
   fn collab_ws_channel(
     &self,
     object_id: &str,
-  ) -> FutureResult<Option<Arc<WebSocketChannel>>, anyhow::Error> {
+  ) -> FutureResult<Option<(Arc<WebSocketChannel>, WSConnectStateReceiver)>, anyhow::Error> {
     if self.enable_sync.load(Ordering::SeqCst) {
       let object_id = object_id.to_string();
       let weak_ws_client = Arc::downgrade(&self.ws_client);
       FutureResult::new(async move {
         match weak_ws_client.upgrade() {
-          None => {
-            tracing::warn!("🟡Collab WS client is dropped");
-            Ok(None)
-          },
-          Some(ws_client) => Ok(
-            ws_client
+          None => Ok(None),
+          Some(ws_client) => {
+            let channel = ws_client
               .read()
               .await
               .subscribe(BusinessID::CollabId, object_id)
               .await
-              .ok(),
-          ),
+              .ok();
+            let connect_state_recv = ws_client.read().await.subscribe_connect_state().await;
+
+            Ok(channel.map(|c| (c, connect_state_recv)))
+          },
         }
       })
     } else {
@@ -126,7 +133,8 @@ impl AppFlowyServer for AFCloudServer {
   }
 
   fn file_storage(&self) -> Option<Arc<dyn FileStorageService>> {
-    None
+    let client = AFServerImpl(self.get_client());
+    Some(Arc::new(AFCloudFileStorageServiceImpl::new(client)))
   }
 }
 
@@ -138,8 +146,34 @@ fn spawn_ws_conn(
   device_id: &Arc<parking_lot::RwLock<String>>,
   mut token_state_rx: TokenStateReceiver,
   ws_client: &Arc<RwLock<WSClient>>,
-  api_client: &Arc<RwLock<Client>>,
+  api_client: &Arc<Client>,
 ) {
+  let weak_device_id = Arc::downgrade(device_id);
+  let weak_ws_client = Arc::downgrade(ws_client);
+  let weak_api_client = Arc::downgrade(api_client);
+
+  tokio::spawn(async move {
+    if let Some(ws_client) = weak_ws_client.upgrade() {
+      let mut state_recv = ws_client.read().await.subscribe_connect_state().await;
+      while let Ok(state) = state_recv.recv().await {
+        if !state.is_timeout() {
+          continue;
+        }
+
+        // Try to reconnect if the connection is timed out.
+        if let (Some(api_client), Some(device_id)) =
+          (weak_api_client.upgrade(), weak_device_id.upgrade())
+        {
+          let device_id = device_id.read().clone();
+          if let Ok(ws_addr) = api_client.ws_url(&device_id) {
+            tracing::info!("🟢WebSocket Reconnecting");
+            let _ = ws_client.write().await.connect(ws_addr).await;
+          }
+        }
+      }
+    }
+  });
+
   let weak_device_id = Arc::downgrade(device_id);
   let weak_ws_client = Arc::downgrade(ws_client);
   let weak_api_client = Arc::downgrade(api_client);
@@ -154,15 +188,14 @@ fn spawn_ws_conn(
             weak_device_id.upgrade(),
           ) {
             let device_id = device_id.read().clone();
-            if let Ok(ws_addr) = api_client.read().await.ws_url(&device_id) {
-              tracing::info!("🟢Connecting to websocket");
+            if let Ok(ws_addr) = api_client.ws_url(&device_id) {
               let _ = ws_client.write().await.connect(ws_addr).await;
             }
           }
         },
         TokenState::Invalid => {
           if let Some(ws_client) = weak_ws_client.upgrade() {
-            tracing::info!("🟡Disconnecting from websocket");
+            tracing::info!("🟡WebSocket Disconnecting");
             ws_client.write().await.disconnect().await;
           }
         },

+ 1 - 1
frontend/rust-lib/flowy-server/src/local_server/impls/database.rs

@@ -12,7 +12,7 @@ impl DatabaseCloudService for LocalServerDatabaseCloudServiceImpl {
   fn get_collab_update(
     &self,
     _object_id: &str,
-    _object_ty: CollabType,
+    _collab_type: CollabType,
   ) -> FutureResult<CollabObjectUpdate, Error> {
     FutureResult::new(async move { Ok(vec![]) })
   }

+ 13 - 4
frontend/rust-lib/flowy-server/src/local_server/impls/user.rs

@@ -24,7 +24,7 @@ pub(crate) struct LocalServerUserAuthServiceImpl {
 }
 
 impl UserCloudService for LocalServerUserAuthServiceImpl {
-  fn sign_up(&self, params: BoxAny) -> FutureResult<SignUpResponse, Error> {
+  fn sign_up(&self, params: BoxAny) -> FutureResult<AuthResponse, Error> {
     FutureResult::new(async move {
       let params = params.unbox_or_error::<SignUpParams>()?;
       let uid = ID_GEN.lock().next_id();
@@ -35,7 +35,7 @@ impl UserCloudService for LocalServerUserAuthServiceImpl {
       } else {
         params.name.clone()
       };
-      Ok(SignUpResponse {
+      Ok(AuthResponse {
         user_id: uid,
         name: user_name,
         latest_workspace: user_workspace.clone(),
@@ -49,7 +49,7 @@ impl UserCloudService for LocalServerUserAuthServiceImpl {
     })
   }
 
-  fn sign_in(&self, params: BoxAny) -> FutureResult<SignInResponse, Error> {
+  fn sign_in(&self, params: BoxAny) -> FutureResult<AuthResponse, Error> {
     let db = self.db.clone();
     FutureResult::new(async move {
       let params: SignInParams = params.unbox_or_error::<SignInParams>()?;
@@ -58,11 +58,12 @@ impl UserCloudService for LocalServerUserAuthServiceImpl {
       let user_workspace = db
         .get_user_workspace(uid)?
         .unwrap_or_else(make_user_workspace);
-      Ok(SignInResponse {
+      Ok(AuthResponse {
         user_id: uid,
         name: params.name,
         latest_workspace: user_workspace.clone(),
         user_workspaces: vec![user_workspace],
+        is_new_user: false,
         email: Some(params.email),
         token: None,
         device_id: params.device_id,
@@ -75,6 +76,14 @@ impl UserCloudService for LocalServerUserAuthServiceImpl {
     FutureResult::new(async { Ok(()) })
   }
 
+  fn generate_sign_in_callback_url(&self, _email: &str) -> FutureResult<String, Error> {
+    FutureResult::new(async {
+      Err(anyhow::anyhow!(
+        "Can't generate callback url when using offline mode"
+      ))
+    })
+  }
+
   fn update_user(
     &self,
     _credential: UserCredentials,

+ 2 - 2
frontend/rust-lib/flowy-server/src/server.rs

@@ -1,6 +1,6 @@
 use std::sync::Arc;
 
-use client_api::ws::WebSocketChannel;
+use client_api::ws::{WSConnectStateReceiver, WebSocketChannel};
 use collab_define::CollabObject;
 use collab_plugins::cloud_storage::RemoteCollabStorage;
 use parking_lot::RwLock;
@@ -94,7 +94,7 @@ pub trait AppFlowyServer: Send + Sync + 'static {
   fn collab_ws_channel(
     &self,
     _object_id: &str,
-  ) -> FutureResult<Option<Arc<WebSocketChannel>>, anyhow::Error> {
+  ) -> FutureResult<Option<(Arc<WebSocketChannel>, WSConnectStateReceiver)>, anyhow::Error> {
     FutureResult::new(async { Ok(None) })
   }
 

+ 2 - 2
frontend/rust-lib/flowy-server/src/supabase/api/database.rs

@@ -29,7 +29,7 @@ where
   fn get_collab_update(
     &self,
     object_id: &str,
-    object_ty: CollabType,
+    collab_type: CollabType,
   ) -> FutureResult<CollabObjectUpdate, Error> {
     let try_get_postgrest = self.server.try_get_weak_postgrest();
     let object_id = object_id.to_string();
@@ -38,7 +38,7 @@ where
       tx.send(
         async move {
           let postgrest = try_get_postgrest?;
-          let updates = FetchObjectUpdateAction::new(object_id.to_string(), object_ty, postgrest)
+          let updates = FetchObjectUpdateAction::new(object_id.to_string(), collab_type, postgrest)
             .run_with_fix_interval(5, 10)
             .await?;
           Ok(updates)

+ 28 - 6
frontend/rust-lib/flowy-server/src/supabase/api/user.rs

@@ -1,3 +1,4 @@
+use std::collections::HashMap;
 use std::future::Future;
 use std::iter::Take;
 use std::pin::Pin;
@@ -63,11 +64,11 @@ impl<T> UserCloudService for SupabaseUserServiceImpl<T>
 where
   T: SupabaseServerService,
 {
-  fn sign_up(&self, params: BoxAny) -> FutureResult<SignUpResponse, Error> {
+  fn sign_up(&self, params: BoxAny) -> FutureResult<AuthResponse, Error> {
     let try_get_postgrest = self.server.try_get_postgrest();
     FutureResult::new(async move {
       let postgrest = try_get_postgrest?;
-      let params = third_party_params_from_box_any(params)?;
+      let params = oauth_params_from_box_any(params)?;
       let is_new_user = postgrest
         .from(USER_TABLE)
         .select("uid")
@@ -117,7 +118,7 @@ where
         user_profile.name
       };
 
-      Ok(SignUpResponse {
+      Ok(AuthResponse {
         user_id: user_profile.uid,
         name: user_name,
         latest_workspace: latest_workspace.unwrap(),
@@ -131,11 +132,11 @@ where
     })
   }
 
-  fn sign_in(&self, params: BoxAny) -> FutureResult<SignInResponse, Error> {
+  fn sign_in(&self, params: BoxAny) -> FutureResult<AuthResponse, Error> {
     let try_get_postgrest = self.server.try_get_postgrest();
     FutureResult::new(async move {
       let postgrest = try_get_postgrest?;
-      let params = third_party_params_from_box_any(params)?;
+      let params = oauth_params_from_box_any(params)?;
       let uuid = params.uuid;
       let response = get_user_profile(postgrest.clone(), GetUserProfileParams::Uuid(uuid))
         .await?
@@ -146,11 +147,12 @@ where
         .find(|user_workspace| user_workspace.id == response.latest_workspace_id)
         .cloned();
 
-      Ok(SignInResponse {
+      Ok(AuthResponse {
         user_id: response.uid,
         name: DEFAULT_USER_NAME(),
         latest_workspace: latest_workspace.unwrap(),
         user_workspaces,
+        is_new_user: false,
         email: None,
         token: None,
         device_id: params.device_id,
@@ -163,6 +165,14 @@ where
     FutureResult::new(async { Ok(()) })
   }
 
+  fn generate_sign_in_callback_url(&self, _email: &str) -> FutureResult<String, Error> {
+    FutureResult::new(async {
+      Err(anyhow::anyhow!(
+        "Can't generate callback url when using supabase"
+      ))
+    })
+  }
+
   fn update_user(
     &self,
     _credential: UserCredentials,
@@ -624,3 +634,15 @@ fn empty_workspace_update(collab_object: &CollabObject) -> Vec<u8> {
   folder.set_current_workspace(&workspace_id);
   collab.encode_as_update_v1().0
 }
+
+fn oauth_params_from_box_any(any: BoxAny) -> Result<SupabaseOAuthParams, Error> {
+  let map: HashMap<String, String> = any.unbox_or_error()?;
+  let uuid = uuid_from_map(&map)?;
+  let email = map.get("email").cloned().unwrap_or_default();
+  let device_id = map.get("device_id").cloned().unwrap_or_default();
+  Ok(SupabaseOAuthParams {
+    uuid,
+    email,
+    device_id,
+  })
+}

+ 1 - 0
frontend/rust-lib/flowy-server/src/supabase/define.rs

@@ -11,6 +11,7 @@ pub const AF_COLLAB_SNAPSHOT_CREATED_AT_COLUMN: &str = "created_at";
 pub const AF_COLLAB_SNAPSHOT_TABLE: &str = "af_collab_snapshot";
 
 pub const USER_UUID: &str = "uuid";
+pub const USER_SIGN_IN_URL: &str = "sign_in_url";
 pub const USER_UID: &str = "uid";
 pub const OWNER_USER_UID: &str = "owner_uid";
 pub const USER_EMAIL: &str = "email";

+ 2 - 0
frontend/rust-lib/flowy-server/tests/af_cloud_test/mod.rs

@@ -0,0 +1,2 @@
+mod user_test;
+mod util;

+ 21 - 0
frontend/rust-lib/flowy-server/tests/af_cloud_test/user_test.rs

@@ -0,0 +1,21 @@
+use flowy_server::AppFlowyServer;
+use flowy_user_deps::entities::AuthResponse;
+use lib_infra::box_any::BoxAny;
+
+use crate::af_cloud_test::util::{
+  af_cloud_server, af_cloud_sign_up_param, generate_test_email, get_af_cloud_config,
+};
+
+#[tokio::test]
+async fn sign_up_test() {
+  if let Some(config) = get_af_cloud_config() {
+    let server = af_cloud_server(config.clone());
+    let user_service = server.user_service();
+    let email = generate_test_email();
+    let params = af_cloud_sign_up_param(&email, &config).await;
+    let resp: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
+    assert_eq!(resp.email.unwrap(), email);
+    assert!(resp.is_new_user);
+    assert_eq!(resp.user_workspaces.len(), 1);
+  }
+}

+ 57 - 0
frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs

@@ -0,0 +1,57 @@
+use std::collections::HashMap;
+use std::sync::Arc;
+
+use parking_lot::RwLock;
+use uuid::Uuid;
+
+use flowy_server::af_cloud::AFCloudServer;
+use flowy_server::supabase::define::{USER_DEVICE_ID, USER_SIGN_IN_URL};
+use flowy_server_config::af_cloud_config::AFCloudConfiguration;
+
+use crate::setup_log;
+
+pub fn get_af_cloud_config() -> Option<AFCloudConfiguration> {
+  dotenv::from_filename("./.env.ci").ok()?;
+  setup_log();
+  AFCloudConfiguration::from_env().ok()
+}
+
+pub fn af_cloud_server(config: AFCloudConfiguration) -> Arc<AFCloudServer> {
+  let fake_device_id = uuid::Uuid::new_v4().to_string();
+  let device_id = Arc::new(RwLock::new(fake_device_id));
+  Arc::new(AFCloudServer::new(config, true, device_id))
+}
+
+pub async fn generate_sign_in_url(user_email: &str, config: &AFCloudConfiguration) -> String {
+  let http_client = reqwest::Client::new();
+  let api_client = client_api::Client::from(
+    http_client,
+    &config.base_url,
+    &config.base_ws_url,
+    &config.gotrue_url,
+  );
+
+  let admin_email = std::env::var("GOTRUE_ADMIN_EMAIL").unwrap();
+  let admin_password = std::env::var("GOTRUE_ADMIN_PASSWORD").unwrap();
+  api_client
+    .generate_sign_in_callback_url(&admin_email, &admin_password, user_email)
+    .await
+    .unwrap()
+}
+
+pub async fn af_cloud_sign_up_param(
+  email: &str,
+  config: &AFCloudConfiguration,
+) -> HashMap<String, String> {
+  let mut params = HashMap::new();
+  params.insert(
+    USER_SIGN_IN_URL.to_string(),
+    generate_sign_in_url(email, config).await,
+  );
+  params.insert(USER_DEVICE_ID.to_string(), Uuid::new_v4().to_string());
+  params
+}
+
+pub fn generate_test_email() -> String {
+  format!("{}@test.com", Uuid::new_v4())
+}

+ 1 - 0
frontend/rust-lib/flowy-server/tests/main.rs

@@ -4,6 +4,7 @@ use tracing_subscriber::fmt::Subscriber;
 use tracing_subscriber::util::SubscriberInitExt;
 use tracing_subscriber::EnvFilter;
 
+mod af_cloud_test;
 mod supabase_test;
 
 pub fn setup_log() {

+ 2 - 2
frontend/rust-lib/flowy-server/tests/supabase_test/database_test.rs

@@ -1,7 +1,7 @@
 use collab_define::{CollabObject, CollabType};
 use uuid::Uuid;
 
-use flowy_user_deps::entities::SignUpResponse;
+use flowy_user_deps::entities::AuthResponse;
 use lib_infra::box_any::BoxAny;
 
 use crate::supabase_test::util::{
@@ -18,7 +18,7 @@ async fn supabase_create_database_test() {
   let user_service = user_auth_service();
   let uuid = Uuid::new_v4().to_string();
   let params = third_party_sign_up_param(uuid);
-  let user: SignUpResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
+  let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
 
   let collab_service = collab_service();
   let database_service = database_service();

+ 4 - 4
frontend/rust-lib/flowy-server/tests/supabase_test/folder_test.rs

@@ -6,7 +6,7 @@ use yrs::types::ToJson;
 use yrs::updates::decoder::Decode;
 use yrs::{merge_updates_v1, Array, Doc, Map, MapPrelim, ReadTxn, StateVector, Transact, Update};
 
-use flowy_user_deps::entities::SignUpResponse;
+use flowy_user_deps::entities::AuthResponse;
 use lib_infra::box_any::BoxAny;
 
 use crate::supabase_test::util::{
@@ -37,7 +37,7 @@ async fn supabase_get_folder_test() {
   let collab_service = collab_service();
   let uuid = Uuid::new_v4().to_string();
   let params = third_party_sign_up_param(uuid);
-  let user: SignUpResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
+  let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
 
   let collab_object = CollabObject::new(
     user.user_id,
@@ -111,7 +111,7 @@ async fn supabase_duplicate_updates_test() {
   let collab_service = collab_service();
   let uuid = Uuid::new_v4().to_string();
   let params = third_party_sign_up_param(uuid);
-  let user: SignUpResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
+  let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
 
   let collab_object = CollabObject::new(
     user.user_id,
@@ -218,7 +218,7 @@ async fn supabase_diff_state_vector_test() {
   let collab_service = collab_service();
   let uuid = Uuid::new_v4().to_string();
   let params = third_party_sign_up_param(uuid);
-  let user: SignUpResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
+  let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
 
   let collab_object = CollabObject::new(
     user.user_id,

+ 6 - 6
frontend/rust-lib/flowy-server/tests/supabase_test/user_test.rs

@@ -17,7 +17,7 @@ async fn supabase_user_sign_up_test() {
   let user_service = user_auth_service();
   let uuid = Uuid::new_v4().to_string();
   let params = third_party_sign_up_param(uuid);
-  let user: SignUpResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
+  let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
   assert!(!user.latest_workspace.id.is_empty());
   assert!(!user.user_workspaces.is_empty());
   assert!(!user.latest_workspace.database_views_aggregate_id.is_empty());
@@ -31,11 +31,11 @@ async fn supabase_user_sign_up_with_existing_uuid_test() {
   let user_service = user_auth_service();
   let uuid = Uuid::new_v4().to_string();
   let params = third_party_sign_up_param(uuid);
-  let _user: SignUpResponse = user_service
+  let _user: AuthResponse = user_service
     .sign_up(BoxAny::new(params.clone()))
     .await
     .unwrap();
-  let user: SignUpResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
+  let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
   assert!(!user.latest_workspace.id.is_empty());
   assert!(!user.latest_workspace.database_views_aggregate_id.is_empty());
   assert!(!user.user_workspaces.is_empty());
@@ -49,7 +49,7 @@ async fn supabase_update_user_profile_test() {
   let user_service = user_auth_service();
   let uuid = Uuid::new_v4().to_string();
   let params = third_party_sign_up_param(uuid);
-  let user: SignUpResponse = user_service
+  let user: AuthResponse = user_service
     .sign_up(BoxAny::new(params.clone()))
     .await
     .unwrap();
@@ -87,7 +87,7 @@ async fn supabase_get_user_profile_test() {
   let user_service = user_auth_service();
   let uuid = Uuid::new_v4().to_string();
   let params = third_party_sign_up_param(uuid);
-  let user: SignUpResponse = user_service
+  let user: AuthResponse = user_service
     .sign_up(BoxAny::new(params.clone()))
     .await
     .unwrap();
@@ -123,7 +123,7 @@ async fn user_encryption_sign_test() {
   let user_service = user_auth_service();
   let uuid = Uuid::new_v4().to_string();
   let params = third_party_sign_up_param(uuid);
-  let user: SignUpResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
+  let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
 
   // generate encryption sign
   let secret = generate_encryption_secret();

+ 7 - 7
frontend/rust-lib/flowy-server/tests/supabase_test/util.rs

@@ -37,7 +37,7 @@ pub fn get_supabase_dev_config() -> Option<SupabaseConfiguration> {
 }
 
 pub fn collab_service() -> Arc<dyn RemoteCollabStorage> {
-  let (server, encryption_impl) = appflowy_server(None);
+  let (server, encryption_impl) = supabase_server_service(None);
   Arc::new(SupabaseCollabStorageImpl::new(
     server,
     None,
@@ -46,17 +46,17 @@ pub fn collab_service() -> Arc<dyn RemoteCollabStorage> {
 }
 
 pub fn database_service() -> Arc<dyn DatabaseCloudService> {
-  let (server, _encryption_impl) = appflowy_server(None);
+  let (server, _encryption_impl) = supabase_server_service(None);
   Arc::new(SupabaseDatabaseServiceImpl::new(server))
 }
 
 pub fn user_auth_service() -> Arc<dyn UserCloudService> {
-  let (server, _encryption_impl) = appflowy_server(None);
+  let (server, _encryption_impl) = supabase_server_service(None);
   Arc::new(SupabaseUserServiceImpl::new(server, vec![], None))
 }
 
 pub fn folder_service() -> Arc<dyn FolderCloudService> {
-  let (server, _encryption_impl) = appflowy_server(None);
+  let (server, _encryption_impl) = supabase_server_service(None);
   Arc::new(SupabaseFolderServiceImpl::new(server))
 }
 
@@ -77,7 +77,7 @@ pub fn file_storage_service() -> Arc<dyn FileStorageService> {
 pub fn encryption_folder_service(
   secret: Option<String>,
 ) -> (Arc<dyn FolderCloudService>, Arc<dyn AppFlowyEncryption>) {
-  let (server, encryption_impl) = appflowy_server(secret);
+  let (server, encryption_impl) = supabase_server_service(secret);
   let service = Arc::new(SupabaseFolderServiceImpl::new(server));
   (service, encryption_impl)
 }
@@ -86,7 +86,7 @@ pub fn encryption_folder_service(
 pub fn encryption_collab_service(
   secret: Option<String>,
 ) -> (Arc<dyn RemoteCollabStorage>, Arc<dyn AppFlowyEncryption>) {
-  let (server, encryption_impl) = appflowy_server(secret);
+  let (server, encryption_impl) = supabase_server_service(secret);
   let service = Arc::new(SupabaseCollabStorageImpl::new(
     server,
     None,
@@ -120,7 +120,7 @@ pub async fn print_encryption_folder_snapshot(folder_id: &str, encryption_secret
   println!("{}", serde_json::to_string_pretty(&json).unwrap());
 }
 
-pub fn appflowy_server(
+pub fn supabase_server_service(
   encryption_secret: Option<String>,
 ) -> (SupabaseServerServiceImpl, Arc<dyn AppFlowyEncryption>) {
   let config = SupabaseConfiguration::from_env().unwrap();

+ 7 - 7
frontend/rust-lib/flowy-test/Cargo.toml

@@ -35,22 +35,22 @@ nanoid = "0.4.0"
 tracing = { version = "0.1.27" }
 parking_lot = "0.12.1"
 uuid = { version = "1.3.3", features = ["serde", "v4"] }
-
-[dev-dependencies]
-dotenv = "0.15.0"
-tempdir = "0.3.7"
-uuid = { version = "1.3.3", features = ["v4"] }
 collab = { version = "0.1.0" }
 collab-document = { version = "0.1.0" }
 collab-folder = { version = "0.1.0" }
 collab-database = { version = "0.1.0" }
 collab-plugins = { version = "0.1.0" }
 collab-define = { version = "0.1.0" }
+
+[dev-dependencies]
+dotenv = "0.15.0"
+tempdir = "0.3.7"
+uuid = { version = "1.3.3", features = ["v4"] }
 assert-json-diff = "2.0.2"
 tokio-postgres = { version = "0.7.8" }
 zip = "0.6.6"
 
 [features]
-default = ["cloud_test"]
+default = ["supabase_cloud_test"]
 dart = ["flowy-core/dart"]
-cloud_test = []
+supabase_cloud_test = []

+ 101 - 10
frontend/rust-lib/flowy-test/src/lib.rs

@@ -5,6 +5,12 @@ use std::path::PathBuf;
 use std::sync::Arc;
 
 use bytes::Bytes;
+use collab::core::collab::MutexCollab;
+use collab::core::origin::CollabOrigin;
+use collab::preclude::updates::decoder::Decode;
+use collab::preclude::{merge_updates_v1, Update};
+use collab_document::blocks::DocumentData;
+use collab_document::document::Document;
 use nanoid::nanoid;
 use parking_lot::RwLock;
 use protobuf::ProtobufError;
@@ -21,14 +27,15 @@ use flowy_folder2::entities::*;
 use flowy_folder2::event_map::FolderEvent;
 use flowy_notification::entities::SubscribeObject;
 use flowy_notification::{register_notification_sender, NotificationSender};
-use flowy_server::supabase::define::{USER_DEVICE_ID, USER_EMAIL, USER_UUID};
+use flowy_server::supabase::define::{USER_DEVICE_ID, USER_EMAIL, USER_SIGN_IN_URL, USER_UUID};
 use flowy_user::entities::{
-  AuthTypePB, ThirdPartyAuthPB, UpdateCloudConfigPB, UserCloudConfigPB, UserProfilePB,
+  AuthTypePB, OAuthCallbackRequestPB, OAuthCallbackResponsePB, OAuthPB, UpdateCloudConfigPB,
+  UserCloudConfigPB, UserProfilePB,
 };
 use flowy_user::errors::{FlowyError, FlowyResult};
 use flowy_user::event_map::UserEvent::*;
 
-use crate::document::document_event::OpenDocumentData;
+use crate::document::document_event::{DocumentEventTest, OpenDocumentData};
 use crate::event_builder::EventBuilder;
 use crate::user_event::{async_sign_up, SignUpContext};
 
@@ -59,10 +66,52 @@ impl FlowyCoreTest {
     Self::default()
   }
 
+  pub async fn insert_document_text(&self, document_id: &str, text: &str, index: usize) {
+    let document_event = DocumentEventTest::new_with_core(self.clone());
+    document_event
+      .insert_index(document_id, text, index, None)
+      .await;
+  }
+
+  pub async fn get_document_data(&self, view_id: &str) -> DocumentData {
+    let pb = EventBuilder::new(self.clone())
+      .event(DocumentEvent::GetDocumentData)
+      .payload(OpenDocumentPayloadPB {
+        document_id: view_id.to_string(),
+      })
+      .async_send()
+      .await
+      .parse::<DocumentDataPB>();
+
+    DocumentData::from(pb)
+  }
+
+  pub async fn get_document_update(&self, document_id: &str) -> Vec<u8> {
+    let cloud_service = self.document_manager.get_cloud_service().clone();
+    let remote_updates = cloud_service
+      .get_document_updates(document_id)
+      .await
+      .unwrap();
+
+    if remote_updates.is_empty() {
+      return vec![];
+    }
+
+    let updates = remote_updates
+      .iter()
+      .map(|update| update.as_ref())
+      .collect::<Vec<&[u8]>>();
+
+    merge_updates_v1(&updates).unwrap()
+  }
+
   pub fn new_with_user_data_path(path: PathBuf, name: String) -> Self {
     let config = AppFlowyCoreConfig::new(path.to_str().unwrap(), name).log_filter(
-      "debug",
-      vec!["flowy_test".to_string(), "lib_dispatch".to_string()],
+      "trace",
+      vec![
+        "flowy_test".to_string(),
+        // "lib_dispatch".to_string()
+      ],
     );
 
     let inner = std::thread::spawn(|| AppFlowyCore::new(config))
@@ -120,13 +169,13 @@ impl FlowyCoreTest {
 
   pub async fn supabase_party_sign_up(&self) -> UserProfilePB {
     let map = third_party_sign_up_param(Uuid::new_v4().to_string());
-    let payload = ThirdPartyAuthPB {
+    let payload = OAuthPB {
       map,
       auth_type: AuthTypePB::Supabase,
     };
 
     EventBuilder::new(self.clone())
-      .event(ThirdPartyAuth)
+      .event(OAuth)
       .payload(payload)
       .async_send()
       .await
@@ -148,7 +197,38 @@ impl FlowyCoreTest {
     self.sign_up_as_guest().await.user_profile
   }
 
-  pub async fn third_party_sign_up_with_uuid(
+  pub async fn af_cloud_sign_in_with_email(&self, email: &str) -> FlowyResult<UserProfilePB> {
+    let payload = OAuthCallbackRequestPB {
+      email: email.to_string(),
+      auth_type: AuthTypePB::AFCloud,
+    };
+    let sign_in_url = EventBuilder::new(self.clone())
+      .event(OAuthCallbackURL)
+      .payload(payload)
+      .async_send()
+      .await
+      .try_parse::<OAuthCallbackResponsePB>()?
+      .sign_in_url;
+
+    let mut map = HashMap::new();
+    map.insert(USER_SIGN_IN_URL.to_string(), sign_in_url);
+    map.insert(USER_DEVICE_ID.to_string(), uuid::Uuid::new_v4().to_string());
+    let payload = OAuthPB {
+      map,
+      auth_type: AuthTypePB::AFCloud,
+    };
+
+    let user_profile = EventBuilder::new(self.clone())
+      .event(OAuth)
+      .payload(payload)
+      .async_send()
+      .await
+      .try_parse::<UserProfilePB>()?;
+
+    Ok(user_profile)
+  }
+
+  pub async fn supabase_sign_up_with_uuid(
     &self,
     uuid: &str,
     email: Option<String>,
@@ -160,13 +240,13 @@ impl FlowyCoreTest {
       USER_EMAIL.to_string(),
       email.unwrap_or_else(|| format!("{}@appflowy.io", nanoid!(10))),
     );
-    let payload = ThirdPartyAuthPB {
+    let payload = OAuthPB {
       map,
       auth_type: AuthTypePB::Supabase,
     };
 
     let user_profile = EventBuilder::new(self.clone())
-      .event(ThirdPartyAuth)
+      .event(OAuth)
       .payload(payload)
       .async_send()
       .await
@@ -879,3 +959,14 @@ pub fn third_party_sign_up_param(uuid: String) -> HashMap<String, String> {
   params.insert(USER_DEVICE_ID.to_string(), Uuid::new_v4().to_string());
   params
 }
+
+pub fn assert_document_data_equal(collab_update: &[u8], doc_id: &str, expected: DocumentData) {
+  let collab = MutexCollab::new(CollabOrigin::Server, doc_id, vec![]);
+  collab.lock().with_origin_transact_mut(|txn| {
+    let update = Update::decode_v1(collab_update).unwrap();
+    txn.apply_update(update);
+  });
+  let document = Document::open(Arc::new(collab)).unwrap();
+  let actual = document.get_document_data().unwrap();
+  assert_eq!(actual, expected);
+}

+ 1 - 1
frontend/rust-lib/flowy-test/tests/database/mod.rs

@@ -1,4 +1,4 @@
 mod local_test;
 
-#[cfg(feature = "cloud_test")]
+#[cfg(feature = "supabase_cloud_test")]
 mod supabase_test;

+ 2 - 8
frontend/rust-lib/flowy-test/tests/database/supabase_test/helper.rs

@@ -23,20 +23,14 @@ impl FlowySupabaseDatabaseTest {
   #[allow(dead_code)]
   pub async fn new_with_user(uuid: String) -> Option<Self> {
     let inner = FlowySupabaseTest::new()?;
-    inner
-      .third_party_sign_up_with_uuid(&uuid, None)
-      .await
-      .unwrap();
+    inner.supabase_sign_up_with_uuid(&uuid, None).await.unwrap();
     Some(Self { uuid, inner })
   }
 
   pub async fn new_with_new_user() -> Option<Self> {
     let inner = FlowySupabaseTest::new()?;
     let uuid = uuid::Uuid::new_v4().to_string();
-    let _ = inner
-      .third_party_sign_up_with_uuid(&uuid, None)
-      .await
-      .unwrap();
+    let _ = inner.supabase_sign_up_with_uuid(&uuid, None).await.unwrap();
     Some(Self { uuid, inner })
   }
 

+ 34 - 0
frontend/rust-lib/flowy-test/tests/document/af_cloud_test/edit_test.rs

@@ -0,0 +1,34 @@
+use std::time::Duration;
+
+use flowy_document2::entities::DocumentSyncStatePB;
+use flowy_test::assert_document_data_equal;
+
+use crate::document::af_cloud_test::util::AFCloudDocumentTest;
+use crate::util::receive_with_timeout;
+
+#[tokio::test]
+async fn af_cloud_edit_document_test() {
+  if let Some(test) = AFCloudDocumentTest::new().await {
+    let document_id = test.create_document().await;
+    let cloned_test = test.clone();
+    let cloned_document_id = document_id.clone();
+    tokio::spawn(async move {
+      cloned_test
+        .insert_document_text(&cloned_document_id, "hello world", 0)
+        .await;
+    });
+
+    // wait all update are send to the remote
+    let mut rx = test
+      .notification_sender
+      .subscribe_with_condition::<DocumentSyncStatePB, _>(&document_id, |pb| pb.is_finish);
+    receive_with_timeout(&mut rx, Duration::from_secs(15))
+      .await
+      .unwrap();
+
+    let document_data = test.get_document_data(&document_id).await;
+    let update = test.get_document_update(&document_id).await;
+    assert!(!update.is_empty());
+    assert_document_data_equal(&update, &document_id, document_data);
+  }
+}

+ 2 - 0
frontend/rust-lib/flowy-test/tests/document/af_cloud_test/mod.rs

@@ -0,0 +1,2 @@
+mod edit_test;
+mod util;

+ 37 - 0
frontend/rust-lib/flowy-test/tests/document/af_cloud_test/util.rs

@@ -0,0 +1,37 @@
+use std::ops::Deref;
+
+use crate::util::{generate_test_email, AFCloudTest};
+
+pub struct AFCloudDocumentTest {
+  inner: AFCloudTest,
+}
+
+impl AFCloudDocumentTest {
+  pub async fn new() -> Option<Self> {
+    let inner = AFCloudTest::new()?;
+    let email = generate_test_email();
+    let _ = inner.af_cloud_sign_in_with_email(&email).await.unwrap();
+    Some(Self { inner })
+  }
+
+  pub async fn create_document(&self) -> String {
+    let current_workspace = self.inner.get_current_workspace().await;
+    let view = self
+      .inner
+      .create_document(
+        &current_workspace.workspace.id,
+        "my document".to_string(),
+        vec![],
+      )
+      .await;
+    view.id
+  }
+}
+
+impl Deref for AFCloudDocumentTest {
+  type Target = AFCloudTest;
+
+  fn deref(&self) -> &Self::Target {
+    &self.inner
+  }
+}

+ 2 - 1
frontend/rust-lib/flowy-test/tests/document/mod.rs

@@ -1,4 +1,5 @@
 mod local_test;
 
-#[cfg(feature = "cloud_test")]
+mod af_cloud_test;
+#[cfg(feature = "supabase_cloud_test")]
 mod supabase_test;

+ 61 - 0
frontend/rust-lib/flowy-test/tests/document/supabase_test/edit_test.rs

@@ -0,0 +1,61 @@
+use std::time::Duration;
+
+use flowy_document2::entities::DocumentSyncStatePB;
+use flowy_test::assert_document_data_equal;
+
+use crate::document::supabase_test::helper::FlowySupabaseDocumentTest;
+use crate::util::receive_with_timeout;
+
+#[tokio::test]
+async fn supabase_document_edit_sync_test() {
+  if let Some(test) = FlowySupabaseDocumentTest::new().await {
+    let view = test.create_document().await;
+    let document_id = view.id.clone();
+
+    let cloned_test = test.clone();
+    let cloned_document_id = document_id.clone();
+    tokio::spawn(async move {
+      cloned_test
+        .insert_document_text(&cloned_document_id, "hello world", 0)
+        .await;
+    });
+
+    // wait all update are send to the remote
+    let mut rx = test
+      .notification_sender
+      .subscribe_with_condition::<DocumentSyncStatePB, _>(&document_id, |pb| pb.is_finish);
+    receive_with_timeout(&mut rx, Duration::from_secs(30))
+      .await
+      .unwrap();
+
+    let document_data = test.get_document_data(&document_id).await;
+    let update = test.get_document_update(&document_id).await;
+    assert_document_data_equal(&update, &document_id, document_data);
+  }
+}
+
+#[tokio::test]
+async fn supabase_document_edit_sync_test2() {
+  if let Some(test) = FlowySupabaseDocumentTest::new().await {
+    let view = test.create_document().await;
+    let document_id = view.id.clone();
+
+    for i in 0..10 {
+      test
+        .insert_document_text(&document_id, "hello world", i)
+        .await;
+    }
+
+    // wait all update are send to the remote
+    let mut rx = test
+      .notification_sender
+      .subscribe_with_condition::<DocumentSyncStatePB, _>(&document_id, |pb| pb.is_finish);
+    receive_with_timeout(&mut rx, Duration::from_secs(30))
+      .await
+      .unwrap();
+
+    let document_data = test.get_document_data(&document_id).await;
+    let update = test.get_document_update(&document_id).await;
+    assert_document_data_equal(&update, &document_id, document_data);
+  }
+}

+ 4 - 56
frontend/rust-lib/flowy-test/tests/document/supabase_test/helper.rs

@@ -1,17 +1,7 @@
 use std::ops::Deref;
-use std::sync::Arc;
 
-use collab::core::collab::MutexCollab;
-use collab::core::origin::CollabOrigin;
-use collab::preclude::updates::decoder::Decode;
-use collab::preclude::{merge_updates_v1, Update};
-use collab_document::blocks::DocumentData;
-use collab_document::document::Document;
-
-use flowy_document2::entities::{
-  DocumentDataPB, OpenDocumentPayloadPB, RepeatedDocumentSnapshotPB,
-};
-use flowy_document2::event_map::DocumentEvent::{GetDocumentData, GetDocumentSnapshots};
+use flowy_document2::entities::{OpenDocumentPayloadPB, RepeatedDocumentSnapshotPB};
+use flowy_document2::event_map::DocumentEvent::GetDocumentSnapshots;
 use flowy_folder2::entities::ViewPB;
 use flowy_test::event_builder::EventBuilder;
 
@@ -25,7 +15,7 @@ impl FlowySupabaseDocumentTest {
   pub async fn new() -> Option<Self> {
     let inner = FlowySupabaseTest::new()?;
     let uuid = uuid::Uuid::new_v4().to_string();
-    let _ = inner.third_party_sign_up_with_uuid(&uuid, None).await;
+    let _ = inner.supabase_sign_up_with_uuid(&uuid, None).await;
     Some(Self { inner })
   }
 
@@ -41,6 +31,7 @@ impl FlowySupabaseDocumentTest {
       .await
   }
 
+  #[allow(dead_code)]
   pub async fn get_document_snapshots(&self, view_id: &str) -> RepeatedDocumentSnapshotPB {
     EventBuilder::new(self.inner.deref().clone())
       .event(GetDocumentSnapshots)
@@ -51,38 +42,6 @@ impl FlowySupabaseDocumentTest {
       .await
       .parse::<RepeatedDocumentSnapshotPB>()
   }
-
-  pub async fn get_document_data(&self, view_id: &str) -> DocumentData {
-    let pb = EventBuilder::new(self.inner.deref().clone())
-      .event(GetDocumentData)
-      .payload(OpenDocumentPayloadPB {
-        document_id: view_id.to_string(),
-      })
-      .async_send()
-      .await
-      .parse::<DocumentDataPB>();
-
-    DocumentData::from(pb)
-  }
-
-  pub async fn get_collab_update(&self, document_id: &str) -> Vec<u8> {
-    let cloud_service = self.document_manager.get_cloud_service().clone();
-    let remote_updates = cloud_service
-      .get_document_updates(document_id)
-      .await
-      .unwrap();
-
-    if remote_updates.is_empty() {
-      return vec![];
-    }
-
-    let updates = remote_updates
-      .iter()
-      .map(|update| update.as_ref())
-      .collect::<Vec<&[u8]>>();
-
-    merge_updates_v1(&updates).unwrap()
-  }
 }
 
 impl Deref for FlowySupabaseDocumentTest {
@@ -92,14 +51,3 @@ impl Deref for FlowySupabaseDocumentTest {
     &self.inner
   }
 }
-
-pub fn assert_document_data_equal(collab_update: &[u8], doc_id: &str, expected: DocumentData) {
-  let collab = MutexCollab::new(CollabOrigin::Server, doc_id, vec![]);
-  collab.lock().with_origin_transact_mut(|txn| {
-    let update = Update::decode_v1(collab_update).unwrap();
-    txn.apply_update(update);
-  });
-  let document = Document::open(Arc::new(collab)).unwrap();
-  let actual = document.get_document_data().unwrap();
-  assert_eq!(actual, expected);
-}

+ 1 - 1
frontend/rust-lib/flowy-test/tests/document/supabase_test/mod.rs

@@ -1,3 +1,3 @@
+mod edit_test;
 mod file_test;
 mod helper;
-mod test;

+ 0 - 86
frontend/rust-lib/flowy-test/tests/document/supabase_test/test.rs

@@ -1,86 +0,0 @@
-use std::ops::Deref;
-use std::time::Duration;
-
-use flowy_document2::entities::{DocumentSnapshotStatePB, DocumentSyncStatePB};
-use flowy_document2::notification::DocumentNotification::DidUpdateDocumentSnapshotState;
-use flowy_test::document::document_event::DocumentEventTest;
-
-use crate::document::supabase_test::helper::{
-  assert_document_data_equal, FlowySupabaseDocumentTest,
-};
-use crate::util::receive_with_timeout;
-
-#[tokio::test]
-async fn supabase_initial_document_snapshot_test() {
-  if let Some(test) = FlowySupabaseDocumentTest::new().await {
-    let view = test.create_document().await;
-
-    let mut rx = test
-      .notification_sender
-      .subscribe::<DocumentSnapshotStatePB>(&view.id, DidUpdateDocumentSnapshotState);
-
-    receive_with_timeout(&mut rx, Duration::from_secs(30))
-      .await
-      .unwrap();
-
-    let snapshots = test.get_document_snapshots(&view.id).await;
-    assert_eq!(snapshots.items.len(), 1);
-
-    let document_data = test.get_document_data(&view.id).await;
-    assert_document_data_equal(&snapshots.items[0].data, &view.id, document_data);
-  }
-}
-
-#[tokio::test]
-async fn supabase_document_edit_sync_test() {
-  if let Some(test) = FlowySupabaseDocumentTest::new().await {
-    let view = test.create_document().await;
-    let document_id = view.id.clone();
-
-    let core = test.deref().deref().clone();
-    let document_event = DocumentEventTest::new_with_core(core);
-    document_event
-      .insert_index(&document_id, "hello world", 0, None)
-      .await;
-
-    // wait all update are send to the remote
-    let mut rx = test
-      .notification_sender
-      .subscribe_with_condition::<DocumentSyncStatePB, _>(&document_id, |pb| pb.is_finish);
-    receive_with_timeout(&mut rx, Duration::from_secs(30))
-      .await
-      .unwrap();
-
-    let document_data = test.get_document_data(&document_id).await;
-    let update = test.get_collab_update(&document_id).await;
-    assert_document_data_equal(&update, &document_id, document_data);
-  }
-}
-
-#[tokio::test]
-async fn supabase_document_edit_sync_test2() {
-  if let Some(test) = FlowySupabaseDocumentTest::new().await {
-    let view = test.create_document().await;
-    let document_id = view.id.clone();
-    let core = test.deref().deref().clone();
-    let document_event = DocumentEventTest::new_with_core(core);
-
-    for i in 0..10 {
-      document_event
-        .insert_index(&document_id, "hello world", i, None)
-        .await;
-    }
-
-    // wait all update are send to the remote
-    let mut rx = test
-      .notification_sender
-      .subscribe_with_condition::<DocumentSyncStatePB, _>(&document_id, |pb| pb.is_finish);
-    receive_with_timeout(&mut rx, Duration::from_secs(30))
-      .await
-      .unwrap();
-
-    let document_data = test.get_document_data(&document_id).await;
-    let update = test.get_collab_update(&document_id).await;
-    assert_document_data_equal(&update, &document_id, document_data);
-  }
-}

+ 1 - 1
frontend/rust-lib/flowy-test/tests/folder/mod.rs

@@ -1,4 +1,4 @@
 mod local_test;
 
-#[cfg(feature = "cloud_test")]
+#[cfg(feature = "supabase_cloud_test")]
 mod supabase_test;

+ 1 - 1
frontend/rust-lib/flowy-test/tests/folder/supabase_test/helper.rs

@@ -21,7 +21,7 @@ impl FlowySupabaseFolderTest {
   pub async fn new() -> Option<Self> {
     let inner = FlowySupabaseTest::new()?;
     let uuid = uuid::Uuid::new_v4().to_string();
-    let _ = inner.third_party_sign_up_with_uuid(&uuid, None).await;
+    let _ = inner.supabase_sign_up_with_uuid(&uuid, None).await;
     Some(Self { inner })
   }
 

+ 1 - 0
frontend/rust-lib/flowy-test/tests/user/af_cloud_test/mod.rs

@@ -0,0 +1 @@
+mod test;

+ 13 - 0
frontend/rust-lib/flowy-test/tests/user/af_cloud_test/test.rs

@@ -0,0 +1,13 @@
+use flowy_test::FlowyCoreTest;
+
+use crate::util::{generate_test_email, get_af_cloud_config};
+
+#[tokio::test]
+async fn af_cloud_sign_up_test() {
+  if get_af_cloud_config().is_some() {
+    let test = FlowyCoreTest::new();
+    let email = generate_test_email();
+    let user = test.af_cloud_sign_in_with_email(&email).await.unwrap();
+    assert_eq!(user.email, email);
+  }
+}

+ 2 - 1
frontend/rust-lib/flowy-test/tests/user/mod.rs

@@ -1,5 +1,6 @@
 mod local_test;
 mod migration_test;
 
-#[cfg(feature = "cloud_test")]
+mod af_cloud_test;
+#[cfg(feature = "supabase_cloud_test")]
 mod supabase_test;

+ 16 - 27
frontend/rust-lib/flowy-test/tests/user/supabase_test/auth_test.rs

@@ -14,9 +14,7 @@ use flowy_server::supabase::define::{USER_EMAIL, USER_UUID};
 use flowy_test::document::document_event::DocumentEventTest;
 use flowy_test::event_builder::EventBuilder;
 use flowy_test::FlowyCoreTest;
-use flowy_user::entities::{
-  AuthTypePB, ThirdPartyAuthPB, UpdateUserProfilePayloadPB, UserProfilePB,
-};
+use flowy_user::entities::{AuthTypePB, OAuthPB, UpdateUserProfilePayloadPB, UserProfilePB};
 use flowy_user::errors::ErrorCode;
 use flowy_user::event_map::UserEvent::*;
 
@@ -32,13 +30,13 @@ async fn third_party_sign_up_test() {
       USER_EMAIL.to_string(),
       format!("{}@appflowy.io", nanoid!(6)),
     );
-    let payload = ThirdPartyAuthPB {
+    let payload = OAuthPB {
       map,
       auth_type: AuthTypePB::Supabase,
     };
 
     let response = EventBuilder::new(test.clone())
-      .event(ThirdPartyAuth)
+      .event(OAuth)
       .payload(payload)
       .async_send()
       .await
@@ -74,8 +72,8 @@ async fn third_party_sign_up_with_duplicated_uuid() {
     map.insert(USER_EMAIL.to_string(), email.clone());
 
     let response_1 = EventBuilder::new(test.clone())
-      .event(ThirdPartyAuth)
-      .payload(ThirdPartyAuthPB {
+      .event(OAuth)
+      .payload(OAuthPB {
         map: map.clone(),
         auth_type: AuthTypePB::Supabase,
       })
@@ -85,8 +83,8 @@ async fn third_party_sign_up_with_duplicated_uuid() {
     dbg!(&response_1);
 
     let response_2 = EventBuilder::new(test.clone())
-      .event(ThirdPartyAuth)
-      .payload(ThirdPartyAuthPB {
+      .event(OAuth)
+      .payload(OAuthPB {
         map: map.clone(),
         auth_type: AuthTypePB::Supabase,
       })
@@ -103,11 +101,11 @@ async fn third_party_sign_up_with_duplicated_email() {
     let test = FlowyCoreTest::new();
     let email = format!("{}@appflowy.io", nanoid!(6));
     test
-      .third_party_sign_up_with_uuid(&uuid::Uuid::new_v4().to_string(), Some(email.clone()))
+      .supabase_sign_up_with_uuid(&uuid::Uuid::new_v4().to_string(), Some(email.clone()))
       .await
       .unwrap();
     let error = test
-      .third_party_sign_up_with_uuid(&uuid::Uuid::new_v4().to_string(), Some(email.clone()))
+      .supabase_sign_up_with_uuid(&uuid::Uuid::new_v4().to_string(), Some(email.clone()))
       .await
       .err()
       .unwrap();
@@ -127,10 +125,7 @@ async fn sign_up_as_guest_and_then_update_to_new_cloud_user_test() {
     let old_workspace = test.folder_manager.get_current_workspace().await.unwrap();
 
     let uuid = uuid::Uuid::new_v4().to_string();
-    test
-      .third_party_sign_up_with_uuid(&uuid, None)
-      .await
-      .unwrap();
+    test.supabase_sign_up_with_uuid(&uuid, None).await.unwrap();
     let new_views = test
       .folder_manager
       .get_current_workspace_views()
@@ -159,7 +154,7 @@ async fn sign_up_as_guest_and_then_update_to_existing_cloud_user_test() {
     let email = format!("{}@appflowy.io", nanoid!(6));
     // The workspace of the guest will be migrated to the new user with given uuid
     let _user_profile = test
-      .third_party_sign_up_with_uuid(&uuid, Some(email.clone()))
+      .supabase_sign_up_with_uuid(&uuid, Some(email.clone()))
       .await
       .unwrap();
     let old_cloud_workspace = test.folder_manager.get_current_workspace().await.unwrap();
@@ -185,7 +180,7 @@ async fn sign_up_as_guest_and_then_update_to_existing_cloud_user_test() {
     // upload to cloud user with given uuid. This time the workspace of the guest will not be merged
     // because the cloud user already has a workspace
     test
-      .third_party_sign_up_with_uuid(&uuid, Some(email))
+      .supabase_sign_up_with_uuid(&uuid, Some(email))
       .await
       .unwrap();
     let new_cloud_workspace = test.folder_manager.get_current_workspace().await.unwrap();
@@ -214,10 +209,7 @@ async fn check_not_exist_user_test() {
 async fn get_user_profile_test() {
   if let Some(test) = FlowySupabaseTest::new() {
     let uuid = uuid::Uuid::new_v4().to_string();
-    test
-      .third_party_sign_up_with_uuid(&uuid, None)
-      .await
-      .unwrap();
+    test.supabase_sign_up_with_uuid(&uuid, None).await.unwrap();
 
     let result = test.get_user_profile().await;
     assert!(result.is_ok());
@@ -228,10 +220,7 @@ async fn get_user_profile_test() {
 async fn update_user_profile_test() {
   if let Some(test) = FlowySupabaseTest::new() {
     let uuid = uuid::Uuid::new_v4().to_string();
-    let profile = test
-      .third_party_sign_up_with_uuid(&uuid, None)
-      .await
-      .unwrap();
+    let profile = test.supabase_sign_up_with_uuid(&uuid, None).await.unwrap();
     test
       .update_user_profile(UpdateUserProfilePayloadPB::new(profile.id).name("lucas"))
       .await;
@@ -246,11 +235,11 @@ async fn update_user_profile_with_existing_email_test() {
   if let Some(test) = FlowySupabaseTest::new() {
     let email = format!("{}@appflowy.io", nanoid!(6));
     let _ = test
-      .third_party_sign_up_with_uuid(&uuid::Uuid::new_v4().to_string(), Some(email.clone()))
+      .supabase_sign_up_with_uuid(&uuid::Uuid::new_v4().to_string(), Some(email.clone()))
       .await;
 
     let profile = test
-      .third_party_sign_up_with_uuid(
+      .supabase_sign_up_with_uuid(
         &uuid::Uuid::new_v4().to_string(),
         Some(format!("{}@appflowy.io", nanoid!(6))),
       )

+ 3 - 3
frontend/rust-lib/flowy-test/tests/user/supabase_test/workspace_test.rs

@@ -4,7 +4,7 @@ use flowy_folder2::entities::WorkspaceSettingPB;
 use flowy_folder2::event_map::FolderEvent::GetCurrentWorkspace;
 use flowy_server::supabase::define::{USER_EMAIL, USER_UUID};
 use flowy_test::{event_builder::EventBuilder, FlowyCoreTest};
-use flowy_user::entities::{AuthTypePB, ThirdPartyAuthPB, UserProfilePB};
+use flowy_user::entities::{AuthTypePB, OAuthPB, UserProfilePB};
 use flowy_user::event_map::UserEvent::*;
 
 use crate::util::*;
@@ -19,13 +19,13 @@ async fn initial_workspace_test() {
       USER_EMAIL.to_string(),
       format!("{}@gmail.com", uuid::Uuid::new_v4()),
     );
-    let payload = ThirdPartyAuthPB {
+    let payload = OAuthPB {
       map,
       auth_type: AuthTypePB::Supabase,
     };
 
     let _ = EventBuilder::new(test.clone())
-      .event(ThirdPartyAuth)
+      .event(OAuth)
       .payload(payload)
       .async_send()
       .await

+ 34 - 0
frontend/rust-lib/flowy-test/tests/util.rs

@@ -11,12 +11,14 @@ use collab_plugins::cloud_storage::RemoteCollabStorage;
 use nanoid::nanoid;
 use tokio::sync::mpsc::Receiver;
 use tokio::time::timeout;
+use uuid::Uuid;
 use zip::ZipArchive;
 
 use flowy_database_deps::cloud::DatabaseCloudService;
 use flowy_folder_deps::cloud::{FolderCloudService, FolderSnapshot};
 use flowy_server::supabase::api::*;
 use flowy_server::{AppFlowyEncryption, EncryptionImpl};
+use flowy_server_config::af_cloud_config::AFCloudConfiguration;
 use flowy_server_config::supabase_config::SupabaseConfiguration;
 use flowy_test::event_builder::EventBuilder;
 use flowy_test::Cleaner;
@@ -211,3 +213,35 @@ pub fn unzip_history_user_db(root: &str, folder_name: &str) -> std::io::Result<(
     PathBuf::from(path),
   ))
 }
+
+pub struct AFCloudTest {
+  inner: FlowyCoreTest,
+}
+
+impl AFCloudTest {
+  pub fn new() -> Option<Self> {
+    let _ = get_af_cloud_config()?;
+    let test = FlowyCoreTest::new();
+    test.set_auth_type(AuthTypePB::AFCloud);
+    test.server_provider.set_auth_type(AuthType::AFCloud);
+
+    Some(Self { inner: test })
+  }
+}
+
+impl Deref for AFCloudTest {
+  type Target = FlowyCoreTest;
+
+  fn deref(&self) -> &Self::Target {
+    &self.inner
+  }
+}
+
+pub fn generate_test_email() -> String {
+  format!("{}@test.com", Uuid::new_v4())
+}
+
+pub fn get_af_cloud_config() -> Option<AFCloudConfiguration> {
+  dotenv::from_filename("./.env.ci").ok()?;
+  AFCloudConfiguration::from_env().ok()
+}

+ 1 - 1
frontend/rust-lib/flowy-user-deps/Cargo.toml

@@ -13,6 +13,6 @@ serde = { version = "1.0", features = ["derive"] }
 collab-define = { version = "0.1.0" }
 serde_json = { version = "1.0"}
 serde_repr = "0.1"
-chrono = { version = "0.4.27", default-features = false, features = ["clock", "serde"] }
+chrono = { version = "0.4.31", default-features = false, features = ["clock", "serde"] }
 anyhow = "1.0.71"
 tokio = { version = "1.26", features = ["sync"] }

+ 6 - 16
frontend/rust-lib/flowy-user-deps/src/cloud.rs

@@ -13,8 +13,7 @@ use lib_infra::box_any::BoxAny;
 use lib_infra::future::FutureResult;
 
 use crate::entities::{
-  SignInResponse, SignUpResponse, ThirdPartyParams, UpdateUserProfileParams, UserCredentials,
-  UserProfile, UserWorkspace,
+  AuthResponse, UpdateUserProfileParams, UserCredentials, UserProfile, UserWorkspace,
 };
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -62,15 +61,18 @@ pub trait UserCloudService: Send + Sync + 'static {
   /// Sign up a new account.
   /// The type of the params is defined the this trait's implementation.
   /// Use the `unbox_or_error` of the [BoxAny] to get the params.
-  fn sign_up(&self, params: BoxAny) -> FutureResult<SignUpResponse, Error>;
+  fn sign_up(&self, params: BoxAny) -> FutureResult<AuthResponse, Error>;
 
   /// Sign in an account
   /// The type of the params is defined the this trait's implementation.
-  fn sign_in(&self, params: BoxAny) -> FutureResult<SignInResponse, Error>;
+  fn sign_in(&self, params: BoxAny) -> FutureResult<AuthResponse, Error>;
 
   /// Sign out an account
   fn sign_out(&self, token: Option<String>) -> FutureResult<(), Error>;
 
+  /// Generate a sign in callback url for the user with the given email
+  fn generate_sign_in_callback_url(&self, email: &str) -> FutureResult<String, Error>;
+
   /// Using the user's token to update the user information
   fn update_user(
     &self,
@@ -129,18 +131,6 @@ pub struct UserUpdate {
   pub encryption_sign: String,
 }
 
-pub fn third_party_params_from_box_any(any: BoxAny) -> Result<ThirdPartyParams, Error> {
-  let map: HashMap<String, String> = any.unbox_or_error()?;
-  let uuid = uuid_from_map(&map)?;
-  let email = map.get("email").cloned().unwrap_or_default();
-  let device_id = map.get("device_id").cloned().unwrap_or_default();
-  Ok(ThirdPartyParams {
-    uuid,
-    email,
-    device_id,
-  })
-}
-
 pub fn uuid_from_map(map: &HashMap<String, String>) -> Result<Uuid, Error> {
   let uuid = map
     .get("uuid")

+ 11 - 6
frontend/rust-lib/flowy-user-deps/src/entities.rs

@@ -81,7 +81,7 @@ pub struct SignUpParams {
 }
 
 #[derive(Serialize, Deserialize, Debug, Clone)]
-pub struct SignUpResponse {
+pub struct AuthResponse {
   pub user_id: i64,
   pub name: String,
   pub latest_workspace: UserWorkspace,
@@ -93,7 +93,7 @@ pub struct SignUpResponse {
   pub encryption_type: EncryptionType,
 }
 
-impl UserAuthResponse for SignUpResponse {
+impl UserAuthResponse for AuthResponse {
   fn user_id(&self) -> i64 {
     self.user_id
   }
@@ -129,7 +129,7 @@ impl UserAuthResponse for SignUpResponse {
 
 #[derive(Clone, Debug)]
 pub struct UserCredentials {
-  /// Currently, the token is only used when the [AuthType] is SelfHosted
+  /// Currently, the token is only used when the [AuthType] is AFCloud
   pub token: Option<String>,
 
   /// The user id
@@ -326,7 +326,7 @@ pub enum AuthType {
   Local = 0,
   /// Currently not supported. It will be supported in the future when the
   /// [AppFlowy-Server](https://github.com/AppFlowy-IO/AppFlowy-Server) ready.
-  SelfHosted = 1,
+  AFCloud = 1,
   /// It uses Supabase as the backend.
   Supabase = 2,
 }
@@ -347,14 +347,19 @@ impl From<i32> for AuthType {
   fn from(value: i32) -> Self {
     match value {
       0 => AuthType::Local,
-      1 => AuthType::SelfHosted,
+      1 => AuthType::AFCloud,
       2 => AuthType::Supabase,
       _ => AuthType::Local,
     }
   }
 }
-pub struct ThirdPartyParams {
+pub struct SupabaseOAuthParams {
   pub uuid: Uuid,
   pub email: String,
   pub device_id: String,
 }
+
+pub struct AFCloudOAuthParams {
+  pub sign_in_url: String,
+  pub device_id: String,
+}

+ 1 - 1
frontend/rust-lib/flowy-user/Cargo.toml

@@ -44,7 +44,7 @@ validator = "0.16.0"
 unicode-segmentation = "1.10"
 fancy-regex = "0.11.0"
 uuid = { version = "1.3.3", features = [ "v4"] }
-chrono = { version = "0.4.27", default-features = false, features = ["clock"] }
+chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
 base64 = "^0.21"
 
 [dev-dependencies]

+ 17 - 2
frontend/rust-lib/flowy-user/src/entities/auth.rs

@@ -79,7 +79,7 @@ impl TryInto<SignUpParams> for SignUpPayloadPB {
 }
 
 #[derive(ProtoBuf, Default)]
-pub struct ThirdPartyAuthPB {
+pub struct OAuthPB {
   /// Use this field to store the third party auth information.
   /// Different auth type has different fields.
   /// Supabase:
@@ -92,10 +92,25 @@ pub struct ThirdPartyAuthPB {
   pub auth_type: AuthTypePB,
 }
 
+#[derive(ProtoBuf, Default)]
+pub struct OAuthCallbackRequestPB {
+  #[pb(index = 1)]
+  pub email: String,
+
+  #[pb(index = 2)]
+  pub auth_type: AuthTypePB,
+}
+
+#[derive(ProtoBuf, Default)]
+pub struct OAuthCallbackResponsePB {
+  #[pb(index = 1)]
+  pub sign_in_url: String,
+}
+
 #[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)]
 pub enum AuthTypePB {
   Local = 0,
-  SelfHosted = 1,
+  AFCloud = 1,
   Supabase = 2,
 }
 

+ 17 - 2
frontend/rust-lib/flowy-user/src/event_handler.rs

@@ -218,8 +218,8 @@ pub async fn get_user_setting(
 /// Only used for third party auth.
 /// Use [UserEvent::SignIn] or [UserEvent::SignUp] If the [AuthType] is Local or SelfHosted
 #[tracing::instrument(level = "debug", skip(data, manager), err)]
-pub async fn third_party_auth_handler(
-  data: AFPluginData<ThirdPartyAuthPB>,
+pub async fn oauth_handler(
+  data: AFPluginData<OAuthPB>,
   manager: AFPluginState<Weak<UserManager>>,
 ) -> DataResult<UserProfilePB, FlowyError> {
   let manager = upgrade_manager(manager)?;
@@ -229,6 +229,21 @@ pub async fn third_party_auth_handler(
   data_result_ok(user_profile.into())
 }
 
+#[tracing::instrument(level = "debug", skip(data, manager), err)]
+pub async fn get_oauth_url_handler(
+  data: AFPluginData<OAuthCallbackRequestPB>,
+  manager: AFPluginState<Weak<UserManager>>,
+) -> DataResult<OAuthCallbackResponsePB, FlowyError> {
+  let manager = upgrade_manager(manager)?;
+  let params = data.into_inner();
+  let auth_type: AuthType = params.auth_type.into();
+  let sign_in_url = manager
+    .generate_sign_in_callback_url(&auth_type, &params.email)
+    .await?;
+  let resp = OAuthCallbackResponsePB { sign_in_url };
+  data_result_ok(resp)
+}
+
 #[tracing::instrument(level = "debug", skip_all, err)]
 pub async fn set_encrypt_secret_handler(
   manager: AFPluginState<Weak<UserManager>>,

+ 9 - 3
frontend/rust-lib/flowy-user/src/event_map.rs

@@ -37,7 +37,8 @@ pub fn init(user_session: Weak<UserManager>) -> AFPlugin {
     .event(UserEvent::GetCloudConfig, get_cloud_config_handler)
     .event(UserEvent::SetEncryptionSecret, set_encrypt_secret_handler)
     .event(UserEvent::CheckEncryptionSign, check_encrypt_secret_handler)
-    .event(UserEvent::ThirdPartyAuth, third_party_auth_handler)
+    .event(UserEvent::OAuth, oauth_handler)
+    .event(UserEvent::OAuthCallbackURL, get_oauth_url_handler)
     .event(
       UserEvent::GetAllUserWorkspaces,
       get_all_user_workspace_handler,
@@ -229,8 +230,13 @@ pub enum UserEvent {
   #[event(output = "UserSettingPB")]
   GetUserSetting = 9,
 
-  #[event(input = "ThirdPartyAuthPB", output = "UserProfilePB")]
-  ThirdPartyAuth = 10,
+  #[event(input = "OAuthPB", output = "UserProfilePB")]
+  OAuth = 10,
+
+  /// Get the OAuth callback url
+  /// Only use when the [AuthType] is AFCloud
+  #[event(input = "OAuthCallbackRequestPB", output = "OAuthCallbackResponsePB")]
+  OAuthCallbackURL = 11,
 
   #[event(input = "UpdateCloudConfigPB")]
   SetCloudConfig = 13,

+ 15 - 3
frontend/rust-lib/flowy-user/src/manager.rs

@@ -195,7 +195,7 @@ impl UserManager {
     auth_type: AuthType,
   ) -> Result<UserProfile, FlowyError> {
     self.update_auth_type(&auth_type).await;
-    let response: SignInResponse = self
+    let response: AuthResponse = self
       .cloud_services
       .get_user_service()?
       .sign_in(params)
@@ -252,7 +252,7 @@ impl UserManager {
 
     let migration_user = self.get_migration_user(&auth_type).await;
     let auth_service = self.cloud_services.get_user_service()?;
-    let response: SignUpResponse = auth_service.sign_up(params).await?;
+    let response: AuthResponse = auth_service.sign_up(params).await?;
     let user_profile = UserProfile::from((&response, &auth_type));
     if user_profile.encryption_type.is_need_encrypt_secret() {
       self
@@ -300,7 +300,7 @@ impl UserManager {
     &self,
     user_profile: &UserProfile,
     migration_user: Option<MigrationUser>,
-    response: SignUpResponse,
+    response: AuthResponse,
     auth_type: &AuthType,
   ) -> FlowyResult<()> {
     let new_session = Session::from(&response);
@@ -543,6 +543,18 @@ impl UserManager {
     Ok(())
   }
 
+  pub(crate) async fn generate_sign_in_callback_url(
+    &self,
+    auth_type: &AuthType,
+    email: &str,
+  ) -> Result<String, FlowyError> {
+    self.update_auth_type(auth_type).await;
+
+    let auth_service = self.cloud_services.get_user_service()?;
+    let url = auth_service.generate_sign_in_callback_url(email).await?;
+    Ok(url)
+  }
+
   async fn save_auth_data(
     &self,
     response: &impl UserAuthResponse,

+ 4 - 4
frontend/rust-lib/flowy-user/src/services/entities.rs

@@ -7,8 +7,8 @@ use serde::de::{Deserializer, MapAccess, Visitor};
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
 
+use flowy_user_deps::entities::{AuthResponse, UserProfile, UserWorkspace};
 use flowy_user_deps::entities::{AuthType, UserAuthResponse};
-use flowy_user_deps::entities::{SignUpResponse, UserProfile, UserWorkspace};
 
 use crate::entities::AuthTypePB;
 use crate::migrations::MigrationUser;
@@ -162,7 +162,7 @@ impl From<AuthTypePB> for AuthType {
     match pb {
       AuthTypePB::Supabase => AuthType::Supabase,
       AuthTypePB::Local => AuthType::Local,
-      AuthTypePB::SelfHosted => AuthType::SelfHosted,
+      AuthTypePB::AFCloud => AuthType::AFCloud,
     }
   }
 }
@@ -172,7 +172,7 @@ impl From<AuthType> for AuthTypePB {
     match auth_type {
       AuthType::Supabase => AuthTypePB::Supabase,
       AuthType::Local => AuthTypePB::Local,
-      AuthType::SelfHosted => AuthTypePB::SelfHosted,
+      AuthType::AFCloud => AuthTypePB::AFCloud,
     }
   }
 }
@@ -206,7 +206,7 @@ const DEFAULT_AUTH_TYPE: fn() -> AuthType = || AuthType::Local;
 #[derive(Clone)]
 pub(crate) struct ResumableSignUp {
   pub user_profile: UserProfile,
-  pub response: SignUpResponse,
+  pub response: AuthResponse,
   pub auth_type: AuthType,
   pub migration_user: Option<MigrationUser>,
 }

+ 30 - 0
frontend/scripts/tool/update_client_api_rev.sh

@@ -0,0 +1,30 @@
+#!/bin/bash
+
+# Ensure a new revision ID is provided
+if [ "$#" -ne 1 ]; then
+    echo "Usage: $0 <new_revision_id>"
+    exit 1
+fi
+
+NEW_REV="$1"
+echo "New revision: $NEW_REV"
+directories=("rust-lib" "appflowy_tauri/src-tauri")
+
+for dir in "${directories[@]}"; do
+    echo "Updating $dir"
+
+    cd "$dir"
+    sed -i.bak "/^client-api[[:alnum:]-]*[[:space:]]*=/s/rev = \"[a-fA-F0-9]\{6,40\}\"/rev = \"$NEW_REV\"/g" Cargo.toml
+
+    # Detect changed crates
+    client_api_crates=($(grep -E '^client-api[a-zA-Z0-9_-]* =' Cargo.toml | awk -F'=' '{print $1}' | tr -d ' '))
+
+    # Update only the changed crates in Cargo.lock
+    for crate in "${client_api_crates[@]}"; do
+        echo "Updating $crate"
+        cargo update -p $crate
+    done
+
+    cd ..
+done
+

+ 7 - 2
frontend/scripts/tool/update_collab_rev.sh

@@ -20,11 +20,16 @@ for dir in "${directories[@]}"; do
     collab_crates=($(grep -E '^collab[a-zA-Z0-9_-]* =' Cargo.toml | awk -F'=' '{print $1}' | tr -d ' '))
 
     # Update only the changed crates in Cargo.lock
+
+    crates_to_update=""
     for crate in "${collab_crates[@]}"; do
-        echo "Updating $crate"
-        cargo update -p $crate
+        crates_to_update="$crates_to_update -p $crate"
     done
 
+    # Update all the specified crates at once
+    echo "Updating crates: $crates_to_update"
+    cargo update $crates_to_update
+
     cd ..
 done
 

+ 9 - 9
shared-lib/Cargo.lock

@@ -115,9 +115,9 @@ checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
 
 [[package]]
 name = "bytes"
-version = "1.4.0"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
+checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
 
 [[package]]
 name = "cc"
@@ -949,22 +949,22 @@ dependencies = [
 
 [[package]]
 name = "pin-project"
-version = "1.0.12"
+version = "1.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc"
+checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422"
 dependencies = [
  "pin-project-internal",
 ]
 
 [[package]]
 name = "pin-project-internal"
-version = "1.0.12"
+version = "1.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
+checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 1.0.109",
+ "syn 2.0.16",
 ]
 
 [[package]]
@@ -1011,9 +1011,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.57"
+version = "1.0.67"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4ec6d5fe0b140acb27c9a0444118cf55bfbb4e0b259739429abb4521dd67c16"
+checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
 dependencies = [
  "unicode-ident",
 ]

+ 3 - 3
shared-lib/lib-infra/Cargo.toml

@@ -6,9 +6,9 @@ edition = "2018"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-chrono = { version = "0.4.27", default-features = false, features = ["clock"] }
-bytes = { version = "1.4" }
-pin-project = "1.0.12"
+chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
+bytes = { version = "1.5" }
+pin-project = "1.1.3"
 futures-core = { version = "0.3" }
 tokio = { version = "1.26", features = ["time", "rt"] }
 rand = "0.8.5"

Some files were not shown because too many files changed in this diff