diff --git a/frontend/appflowy_flutter/.gitignore b/frontend/appflowy_flutter/.gitignore index 82c35e91cd..a0886f8dbc 100644 --- a/frontend/appflowy_flutter/.gitignore +++ b/frontend/appflowy_flutter/.gitignore @@ -68,3 +68,5 @@ windows/flutter/dart_ffi/ **/**/Brewfile.lock.json **/.sandbox **/.vscode/ + +*.env diff --git a/frontend/appflowy_flutter/assets/images/common/open_folder.svg b/frontend/appflowy_flutter/assets/images/common/open_folder.svg new file mode 100644 index 0000000000..cd81df9271 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/common/open_folder.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_flutter/assets/images/common/recover.svg b/frontend/appflowy_flutter/assets/images/common/recover.svg new file mode 100644 index 0000000000..38d77b51de --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/common/recover.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_flutter/assets/images/editor/duplicate.svg b/frontend/appflowy_flutter/assets/images/editor/duplicate.svg new file mode 100644 index 0000000000..40b5ed5a95 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/editor/duplicate.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_flutter/assets/images/login/discord-mark.svg b/frontend/appflowy_flutter/assets/images/login/discord-mark.svg new file mode 100644 index 0000000000..640e06af95 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/login/discord-mark.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_flutter/assets/images/login/github-mark.svg b/frontend/appflowy_flutter/assets/images/login/github-mark.svg new file mode 100644 index 0000000000..d5e6491854 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/login/github-mark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/images/login/google-mark.svg b/frontend/appflowy_flutter/assets/images/login/google-mark.svg new file mode 100644 index 0000000000..5a65dae32d --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/login/google-mark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/translations/en.json b/frontend/appflowy_flutter/assets/translations/en.json index a43c32e849..0c2b62ea5d 100644 --- a/frontend/appflowy_flutter/assets/translations/en.json +++ b/frontend/appflowy_flutter/assets/translations/en.json @@ -21,6 +21,7 @@ "signIn": { "loginTitle": "Login to @:appName", "loginButtonText": "Login", + "loginAsGuestButtonText": "Get Started", "buttonText": "Sign In", "forgotPassword": "Forgot Password?", "emailHint": "Email", @@ -184,7 +185,8 @@ "theme": "Theme" }, "files": { - "defaultLocation": "Where your data is stored now", + "copy": "Copy", + "defaultLocation": "Read files and data storage location", "doubleTapToCopy": "Double tap to copy the path", "restoreLocation": "Restore to AppFlowy default path", "customizeLocation": "Open another folder", @@ -203,7 +205,11 @@ "create": "Create", "folderPath": "Path to store your folder", "locationCannotBeEmpty": "Path cannot be empty", - "pathCopiedSnackbar": "File storage path copied to clipboard!" + "pathCopiedSnackbar": "File storage path copied to clipboard!", + "changeLocationTooltips": "Change the files read the data directory", + "change": "Change", + "openLocationTooltips": "Open the files read the data directory", + "recoverLocationTooltips": "Recover the files read the data directory" }, "user": { "name": "Name", diff --git a/frontend/appflowy_flutter/integration_test/util/base.dart b/frontend/appflowy_flutter/integration_test/util/base.dart index 3525fc5871..acc4b64e7a 100644 --- a/frontend/appflowy_flutter/integration_test/util/base.dart +++ b/frontend/appflowy_flutter/integration_test/util/base.dart @@ -1,8 +1,8 @@ import 'dart:io'; +import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/main.dart' as app; import 'package:appflowy/startup/tasks/prelude.dart'; -import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -21,7 +21,7 @@ class TestFolder { static Future setTestLocation(String? name) async { final location = await testLocation(name); SharedPreferences.setMockInitialValues({ - kSettingsLocationDefaultLocation: location.path, + KVKeys.pathLocation: location.path, }); return; } @@ -36,7 +36,7 @@ class TestFolder { /// Get current using location. static Future currentLocation() async { final prefs = await SharedPreferences.getInstance(); - return prefs.getString(kSettingsLocationDefaultLocation)!; + return prefs.getString(KVKeys.pathLocation)!; } /// Get default location under development environment. diff --git a/frontend/appflowy_flutter/integration_test/util/data.dart b/frontend/appflowy_flutter/integration_test/util/data.dart index 0dd0961ee2..6725f2958b 100644 --- a/frontend/appflowy_flutter/integration_test/util/data.dart +++ b/frontend/appflowy_flutter/integration_test/util/data.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart'; +import 'package:appflowy/core/config/kv_keys.dart'; import 'package:archive/archive_io.dart'; import 'package:flutter/services.dart'; import 'package:path/path.dart' as p; @@ -51,8 +51,7 @@ class TestWorkspaceService { Future setUpAll() async { SharedPreferences.setMockInitialValues( { - kSettingsLocationDefaultLocation: - await workspace.root.then((value) => value.path), + KVKeys.pathLocation: await workspace.root.then((value) => value.path), }, ); } diff --git a/frontend/appflowy_flutter/lib/core/config/config.dart b/frontend/appflowy_flutter/lib/core/config/config.dart index ad06ecb860..19c771c793 100644 --- a/frontend/appflowy_flutter/lib/core/config/config.dart +++ b/frontend/appflowy_flutter/lib/core/config/config.dart @@ -4,13 +4,15 @@ import 'package:appflowy_backend/protobuf/flowy-config/entities.pb.dart'; class Config { static Future setSupabaseConfig({ required String url, + required String anonKey, required String key, required String secret, }) async { await ConfigEventSetSupabaseConfig( SupabaseConfigPB.create() ..supabaseUrl = url - ..supabaseKey = key + ..key = key + ..anonKey = anonKey ..jwtSecret = secret, ).send(); } diff --git a/frontend/appflowy_flutter/lib/core/config/kv.dart b/frontend/appflowy_flutter/lib/core/config/kv.dart index 692c691b57..4c0901cb8c 100644 --- a/frontend/appflowy_flutter/lib/core/config/kv.dart +++ b/frontend/appflowy_flutter/lib/core/config/kv.dart @@ -2,11 +2,61 @@ import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-config/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:dartz/dartz.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +abstract class KeyValueStorage { + Future set(String key, String value); + Future> get(String key); + Future remove(String key); + Future clear(); +} + +class DartKeyValue implements KeyValueStorage { + SharedPreferences? _sharedPreferences; + SharedPreferences get sharedPreferences => _sharedPreferences!; + + @override + Future> get(String key) async { + await _initSharedPreferencesIfNeeded(); + + final value = sharedPreferences.getString(key); + if (value != null) { + return Right(value); + } + return Left(FlowyError()); + } + + @override + Future remove(String key) async { + await _initSharedPreferencesIfNeeded(); + + await sharedPreferences.remove(key); + } + + @override + Future set(String key, String value) async { + await _initSharedPreferencesIfNeeded(); + + await sharedPreferences.setString(key, value); + } + + @override + Future clear() async { + await _initSharedPreferencesIfNeeded(); + + await sharedPreferences.clear(); + } + + Future _initSharedPreferencesIfNeeded() async { + _sharedPreferences ??= await SharedPreferences.getInstance(); + } +} /// Key-value store /// The data is stored in the local storage of the device. -class KeyValue { - static Future set(String key, String value) async { +class RustKeyValue implements KeyValueStorage { + @override + Future set(String key, String value) async { await ConfigEventSetKeyValue( KeyValuePB.create() ..key = key @@ -14,20 +64,23 @@ class KeyValue { ).send(); } - static Future> get(String key) { - return ConfigEventGetKeyValue( - KeyPB.create()..key = key, - ).send().then( - (result) => result.fold( - (pb) => left(pb.value), - (error) => right(error), - ), - ); + @override + Future> get(String key) async { + final payload = KeyPB.create()..key = key; + final response = await ConfigEventGetKeyValue(payload).send(); + return response.swap().map((r) => r.value); } - static Future remove(String key) async { + @override + Future remove(String key) async { await ConfigEventRemoveKeyValue( KeyPB.create()..key = key, ).send(); } + + @override + Future clear() { + // TODO: implement clear + throw UnimplementedError(); + } } diff --git a/frontend/appflowy_flutter/lib/core/config/kv_keys.dart b/frontend/appflowy_flutter/lib/core/config/kv_keys.dart new file mode 100644 index 0000000000..192a0e6960 --- /dev/null +++ b/frontend/appflowy_flutter/lib/core/config/kv_keys.dart @@ -0,0 +1,15 @@ +class KVKeys { + const KVKeys._(); + + static const String prefix = 'io.appflowy.appflowy_flutter'; + + /// The key for the path location of the local data for the whole app. + static const String pathLocation = '$prefix.path_location'; + + /// The key for the last time login type. + /// + /// The value is one of the following: + /// - local + /// - supabase + static const String loginType = '$prefix.login_type'; +} diff --git a/frontend/appflowy_flutter/lib/env/env.dart b/frontend/appflowy_flutter/lib/env/env.dart new file mode 100644 index 0000000000..311df8a3ce --- /dev/null +++ b/frontend/appflowy_flutter/lib/env/env.dart @@ -0,0 +1,38 @@ +// lib/env/env.dart +import 'package:envied/envied.dart'; + +part 'env.g.dart'; + +@Envied(path: '.env') +abstract class Env { + @EnviedField( + obfuscate: true, + varName: 'SUPABASE_URL', + defaultValue: '', + ) + static final supabaseUrl = _Env.supabaseUrl; + @EnviedField( + obfuscate: true, + varName: 'SUPABASE_ANON_KEY', + defaultValue: '', + ) + static final supabaseAnonKey = _Env.supabaseAnonKey; + @EnviedField( + obfuscate: true, + varName: 'SUPABASE_KEY', + defaultValue: '', + ) + static final supabaseKey = _Env.supabaseKey; + @EnviedField( + obfuscate: true, + varName: 'SUPABASE_JWT_SECRET', + defaultValue: '', + ) + static final supabaseJwtSecret = _Env.supabaseJwtSecret; +} + +bool get isSupabaseEnable => + Env.supabaseUrl.isNotEmpty && + Env.supabaseAnonKey.isNotEmpty && + Env.supabaseKey.isNotEmpty && + Env.supabaseJwtSecret.isNotEmpty; diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart b/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart index 4fe2db1b07..2f7c0d78ce 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart @@ -65,7 +65,6 @@ extension on InsertOperation { final parentId = node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? ''; final prevId = previousNode?.id ?? - node.previous?.id ?? editorState.getNodeAtPath(path.previous)?.id ?? ''; assert(parentId.isNotEmpty && prevId.isNotEmpty); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart index eb6e861919..2da5eccc0d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart @@ -37,7 +37,7 @@ class ColorOptionAction extends PopoverActionCell { @override Widget? leftIcon(Color iconColor) { return svgWidget( - 'editor/delete', // todo: add color icon + 'editor/color_formatter', color: iconColor, ); } @@ -100,10 +100,10 @@ class ColorOptionAction extends PopoverActionCell { } class OptionActionWrapper extends ActionCell { - final OptionAction inner; - OptionActionWrapper(this.inner); + final OptionAction inner; + @override Widget? leftIcon(Color iconColor) { var name = ''; @@ -125,15 +125,13 @@ class OptionActionWrapper extends ActionCell { name = 'editor/move_down'; break; case OptionAction.color: - name = 'editor/color'; - break; + throw UnimplementedError(); case OptionAction.divider: throw UnimplementedError(); } if (name.isEmpty) { return null; } - name = 'editor/delete'; return svgWidget( name, color: iconColor, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_image_picker_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_image_picker_bloc.dart index 3c375ad7ee..43993d3f47 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_image_picker_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/cover_image_picker_bloc.dart @@ -130,7 +130,7 @@ class CoverImagePickerBloc } Future _coverPath() async { - final directory = await getIt().fetchLocation(); + final directory = await getIt().getPath(); return Directory(p.join(directory, 'covers')) .create(recursive: true) .then((value) => value.path); diff --git a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart index d060aabab1..5186646f53 100644 --- a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart +++ b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/network_monitor.dart'; import 'package:appflowy/plugins/database_view/application/field/field_action_sheet_bloc.dart'; import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; @@ -5,6 +6,8 @@ import 'package:appflowy/plugins/database_view/application/field/field_service.d import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart'; import 'package:appflowy/plugins/database_view/grid/application/grid_header_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/user/application/auth/supabase_auth_service.dart'; import 'package:appflowy/user/application/user_listener.dart'; import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy/util/file_picker/file_picker_impl.dart'; @@ -45,7 +48,10 @@ class DependencyResolver { } void _resolveCommonService(GetIt getIt) async { + // getIt.registerFactory(() => RustKeyValue()); + getIt.registerFactory(() => DartKeyValue()); getIt.registerFactory(() => FilePicker()); + getIt.registerFactory(() => LocalFileStorage()); getIt.registerFactoryAsync( () async { @@ -66,11 +72,17 @@ void _resolveCommonService(GetIt getIt) async { } void _resolveUserDeps(GetIt getIt) { - getIt.registerFactory(() => AuthService()); + // getIt.registerFactory(() => AppFlowyAuthService()); + getIt.registerFactory(() => SupabaseAuthService()); + getIt.registerFactory(() => AuthRouter()); - getIt.registerFactory(() => SignInBloc(getIt())); - getIt.registerFactory(() => SignUpBloc(getIt())); + getIt.registerFactory( + () => SignInBloc(getIt()), + ); + getIt.registerFactory( + () => SignUpBloc(getIt()), + ); getIt.registerFactory(() => SplashRoute()); getIt.registerFactory(() => EditPanelBloc()); @@ -131,11 +143,6 @@ void _resolveFolderDeps(GetIt getIt) { (user, _) => SettingsDialogBloc(user), ); - // Location - getIt.registerFactory( - () => SettingsLocationCubit(), - ); - //User getIt.registerFactoryParam( (user, _) => SettingsUserViewBloc(user), diff --git a/frontend/appflowy_flutter/lib/startup/startup.dart b/frontend/appflowy_flutter/lib/startup/startup.dart index 0fa50120e8..aa7cd9d4a5 100644 --- a/frontend/appflowy_flutter/lib/startup/startup.dart +++ b/frontend/appflowy_flutter/lib/startup/startup.dart @@ -1,33 +1,17 @@ import 'dart:io'; +import 'package:appflowy/env/env.dart'; +import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart'; import 'package:appflowy_backend/appflowy_backend.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; -import '../workspace/application/settings/settings_location_cubit.dart'; import 'deps_resolver.dart'; import 'launch_configuration.dart'; import 'plugin/plugin.dart'; import 'tasks/prelude.dart'; -// [[diagram: flowy startup flow]] -// ┌──────────┐ -// │ FlowyApp │ -// └──────────┘ -// │ impl -// ▼ -// ┌────────┐ 1.run ┌──────────┐ -// │ System │───┬───▶│EntryPoint│ -// └────────┘ │ └──────────┘ ┌─────────────────┐ -// │ ┌──▶ │ RustSDKInitTask │ -// │ ┌───────────┐ │ └─────────────────┘ -// └──▶ │AppLauncher│───┤ -// 2.launch └───────────┘ │ ┌─────────────┐ ┌──────────────────┐ ┌───────────────┐ -// └───▶│AppWidgetTask│────────▶│ApplicationWidget │─────▶│ SplashScreen │ -// └─────────────┘ └──────────────────┘ └───────────────┘ -// -// 3.build MaterialApp final getIt = GetIt.instance; abstract class EntryPoint { @@ -48,10 +32,12 @@ class FlowyRunner { final env = integrationEnv(); initGetIt(getIt, env, f, config); - final directory = getIt() - .fetchLocation() + final directory = await getIt() + .getPath() .then((value) => Directory(value)); + // final directory = await appFlowyDocumentDirectory(); + // add task final launcher = getIt(); launcher.addTasks( @@ -71,6 +57,12 @@ class FlowyRunner { // ignore in test mode if (!env.isTest()) ...[ const HotKeyTask(), + InitSupabaseTask( + url: Env.supabaseUrl, + anonKey: Env.supabaseAnonKey, + key: Env.supabaseKey, + jwtSecret: Env.supabaseJwtSecret, + ), const InitAppWidgetTask(), const InitPlatformServiceTask() ], diff --git a/frontend/appflowy_flutter/lib/startup/tasks/prelude.dart b/frontend/appflowy_flutter/lib/startup/tasks/prelude.dart index 208712b81a..d7d7993af7 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/prelude.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/prelude.dart @@ -6,3 +6,4 @@ export 'hot_key.dart'; export 'platform_error_catcher.dart'; export 'windows.dart'; export 'localization.dart'; +export 'supabase_task.dart'; diff --git a/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart b/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart index 135012c298..7e389c491c 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart @@ -12,30 +12,23 @@ class InitRustSDKTask extends LaunchTask { }); // Customize the RustSDK initialization path - final Future? directory; + final Directory? directory; @override LaunchTaskType get type => LaunchTaskType.dataProcessing; @override Future initialize(LaunchContext context) async { - // use the custom directory - if (directory != null) { - return directory!.then((directory) async { - await context.getIt().init(directory); - }); - } else { - return appFlowyDocumentDirectory().then((directory) async { - await context.getIt().init(directory); - }); - } + final dir = directory ?? await appFlowyDocumentDirectory(); + await context.getIt().init(dir); } } Future appFlowyDocumentDirectory() async { switch (integrationEnv()) { case IntegrationMode.develop: - Directory documentsDir = await getApplicationDocumentsDirectory(); + Directory documentsDir = await getApplicationDocumentsDirectory() + ..create(); return Directory(path.join(documentsDir.path, 'data_dev')).create(); case IntegrationMode.release: Directory documentsDir = await getApplicationDocumentsDirectory(); diff --git a/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart new file mode 100644 index 0000000000..e7369cd331 --- /dev/null +++ b/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart @@ -0,0 +1,46 @@ +import 'package:appflowy/core/config/config.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +import '../startup.dart'; + +bool isSupabaseEnable = false; +bool isSupabaseInitialized = false; + +class InitSupabaseTask extends LaunchTask { + const InitSupabaseTask({ + required this.url, + required this.anonKey, + required this.key, + required this.jwtSecret, + }); + + final String url; + final String anonKey; + final String key; + final String jwtSecret; + + @override + Future initialize(LaunchContext context) async { + if (url.isEmpty || anonKey.isEmpty || jwtSecret.isEmpty || key.isEmpty) { + isSupabaseEnable = false; + Log.info('Supabase config is empty, skip init supabase.'); + return; + } + if (isSupabaseInitialized) { + return; + } + await Supabase.initialize( + url: url, + anonKey: anonKey, + ); + await Config.setSupabaseConfig( + url: url, + key: key, + secret: jwtSecret, + anonKey: anonKey, + ); + isSupabaseEnable = true; + isSupabaseInitialized = true; + } +} diff --git a/frontend/appflowy_flutter/lib/startup/tasks/windows.dart b/frontend/appflowy_flutter/lib/startup/tasks/windows.dart index 4fabfd962b..fcb80ecfe0 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/windows.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/windows.dart @@ -7,7 +7,7 @@ import 'package:window_manager/window_manager.dart'; class InitAppWindowTask extends LaunchTask { const InitAppWindowTask({ - this.minimumSize = const Size(600, 400), + this.minimumSize = const Size(800, 600), this.title = 'AppFlowy', }); diff --git a/frontend/appflowy_flutter/lib/user/application/auth/appflowy_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/appflowy_auth_service.dart new file mode 100644 index 0000000000..302d31a75c --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/application/auth/appflowy_auth_service.dart @@ -0,0 +1,87 @@ +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/user/application/user_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/auth.pb.dart'; +import 'package:dartz/dartz.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/uuid.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' + show SignInPayloadPB, SignUpPayloadPB, UserProfilePB; + +import '../../../generated/locale_keys.g.dart'; + +class AppFlowyAuthService implements AuthService { + @override + Future> signIn({ + required String email, + required String password, + AuthTypePB authType = AuthTypePB.Local, + Map map = const {}, + }) async { + final request = SignInPayloadPB.create() + ..email = email + ..password = password + ..authType = authType; + final response = UserEventSignIn(request).send(); + return response.then((value) => value.swap()); + } + + @override + Future> signUp({ + required String name, + required String email, + required String password, + AuthTypePB authType = AuthTypePB.Local, + Map map = const {}, + }) async { + final request = SignUpPayloadPB.create() + ..name = name + ..email = email + ..password = password + ..authType = authType; + final response = await UserEventSignUp(request).send().then( + (value) => value.swap(), + ); + return response; + } + + @override + Future signOut({ + AuthTypePB authType = AuthTypePB.Local, + Map map = const {}, + }) async { + final payload = SignOutPB()..authType = authType; + await UserEventSignOut(payload).send(); + return; + } + + @override + Future> signUpAsGuest({ + AuthTypePB authType = AuthTypePB.Local, + Map map = const {}, + }) { + const password = "AppFlowy123@"; + final uid = uuid(); + final userEmail = "$uid@appflowy.io"; + return signUp( + name: LocaleKeys.defaultUsername.tr(), + password: password, + email: userEmail, + ); + } + + @override + Future> signUpWithOAuth({ + required String platform, + AuthTypePB authType = AuthTypePB.Local, + Map map = const {}, + }) { + throw UnimplementedError(); + } + + @override + Future> getUser() async { + return UserBackendService.getCurrentUserProfile(); + } +} diff --git a/frontend/appflowy_flutter/lib/user/application/auth/auth_error.dart b/frontend/appflowy_flutter/lib/user/application/auth/auth_error.dart new file mode 100644 index 0000000000..f9629e1547 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/application/auth/auth_error.dart @@ -0,0 +1,19 @@ +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; + +class AuthError { + static final supabaseSignInError = FlowyError() + ..msg = 'supabase sign in error' + ..code = -10001; + + static final supabaseSignUpError = FlowyError() + ..msg = 'supabase sign up error' + ..code = -10002; + + static final supabaseSignInWithOauthError = FlowyError() + ..msg = 'supabase sign in with oauth error' + ..code = -10003; + + static final supabaseGetUserError = FlowyError() + ..msg = 'supabase sign in with oauth error' + ..code = -10003; +} diff --git a/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart new file mode 100644 index 0000000000..70c7fd73f9 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart @@ -0,0 +1,51 @@ +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'; + +class AuthServiceMapKeys { + const AuthServiceMapKeys._(); + + // for supabase auth use only. + static const String uuid = 'uuid'; +} + +abstract class AuthService { + /// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError]. + Future> signIn({ + required String email, + required String password, + AuthTypePB authType, + Map map, + }); + + /// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError]. + Future> signUp({ + required String name, + required String email, + required String password, + AuthTypePB authType, + Map map, + }); + + /// + Future> signUpWithOAuth({ + required String platform, + AuthTypePB authType, + Map map, + }); + + /// Returns a default [UserProfilePB] + Future> signUpAsGuest({ + AuthTypePB authType, + Map map, + }); + + /// + Future signOut({ + AuthTypePB authType, + }); + + /// Returns [UserProfilePB] if the user has sign in, otherwise returns null. + Future> getUser(); +} diff --git a/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart new file mode 100644 index 0000000000..6a28a44981 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart @@ -0,0 +1,221 @@ +import 'dart:async'; + +import 'package:appflowy/core/config/kv.dart'; +import 'package:appflowy/core/config/kv_keys.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/startup/tasks/prelude.dart'; +import 'package:appflowy/user/application/auth/appflowy_auth_service.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.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'; +import 'package:supabase_flutter/supabase_flutter.dart'; +import 'auth_error.dart'; + +class SupabaseAuthService implements AuthService { + SupabaseAuthService(); + + SupabaseClient get _client => Supabase.instance.client; + GoTrueClient get _auth => _client.auth; + + final AppFlowyAuthService _appFlowyAuthService = AppFlowyAuthService(); + + @override + Future> signUp({ + required String name, + required String email, + required String password, + AuthTypePB authType = AuthTypePB.Supabase, + Map map = const {}, + }) async { + if (!isSupabaseEnable) { + return _appFlowyAuthService.signUp( + name: name, + email: email, + password: password, + ); + } + + // fetch the uuid from supabase. + final response = await _auth.signUp( + email: email, + password: password, + ); + final uuid = response.user?.id; + if (uuid == null) { + return left(AuthError.supabaseSignUpError); + } + // assign the uuid to our backend service. + // and will transfer this logic to backend later. + return _appFlowyAuthService.signUp( + name: name, + email: email, + password: password, + authType: authType, + map: { + AuthServiceMapKeys.uuid: uuid, + }, + ); + } + + @override + Future> signIn({ + required String email, + required String password, + AuthTypePB authType = AuthTypePB.Supabase, + Map map = const {}, + }) async { + if (!isSupabaseEnable) { + return _appFlowyAuthService.signIn( + email: email, + password: password, + ); + } + + try { + final response = await _auth.signInWithPassword( + email: email, + password: password, + ); + final uuid = response.user?.id; + if (uuid == null) { + return Left(AuthError.supabaseSignInError); + } + return _appFlowyAuthService.signIn( + email: email, + password: password, + authType: authType, + map: { + AuthServiceMapKeys.uuid: uuid, + }, + ); + } on AuthException catch (e) { + Log.error(e); + return Left(AuthError.supabaseSignInError); + } + } + + @override + Future> signUpWithOAuth({ + required String platform, + AuthTypePB authType = AuthTypePB.Supabase, + Map map = const {}, + }) async { + if (!isSupabaseEnable) { + return _appFlowyAuthService.signUpWithOAuth( + platform: platform, + ); + } + final provider = platform.toProvider(); + final completer = Completer>(); + late final StreamSubscription subscription; + subscription = _auth.onAuthStateChange.listen((event) async { + if (event.event != AuthChangeEvent.signedIn) { + completer.complete(left(AuthError.supabaseSignInWithOauthError)); + } else { + final user = await getSupabaseUser(); + final Either response = await user.fold( + (l) => left(l), + (r) async => await setupAuth(map: {AuthServiceMapKeys.uuid: r.id}), + ); + completer.complete(response); + } + subscription.cancel(); + }); + final Map query = {}; + if (provider == Provider.google) { + query['access_type'] = 'offline'; + query['prompt'] = 'consent'; + } + final response = await _auth.signInWithOAuth( + provider, + queryParams: query, + redirectTo: + 'io.appflowy.appflowy-flutter://login-callback', // can't use underscore here. + ); + if (!response) { + completer.complete(left(AuthError.supabaseSignInWithOauthError)); + } + return completer.future; + } + + @override + Future signOut({ + AuthTypePB authType = AuthTypePB.Supabase, + }) async { + if (!isSupabaseEnable) { + return _appFlowyAuthService.signOut(); + } + await _auth.signOut(); + await _appFlowyAuthService.signOut( + authType: authType, + ); + } + + @override + Future> signUpAsGuest({ + AuthTypePB authType = AuthTypePB.Supabase, + Map map = const {}, + }) async { + // supabase don't support guest login. + // so, just forward to our backend. + return _appFlowyAuthService.signUpAsGuest(); + } + + @override + Future> getUser() async { + final loginType = await getIt() + .get(KVKeys.loginType) + .then((value) => value.toOption().toNullable()); + if (!isSupabaseEnable || (loginType != null && loginType != 'supabase')) { + return _appFlowyAuthService.getUser(); + } + final user = await getSupabaseUser(); + return user.map((r) => r.toUserProfile()); + } + + Future> getSupabaseUser() async { + final user = _auth.currentUser; + if (user == null) { + return left(AuthError.supabaseGetUserError); + } + return Right(user); + } + + Future> setupAuth({ + required Map map, + }) async { + final payload = ThirdPartyAuthPB( + authType: AuthTypePB.Supabase, + map: map, + ); + return UserEventThirdPartyAuth(payload) + .send() + .then((value) => value.swap()); + } +} + +extension on User { + UserProfilePB toUserProfile() { + return UserProfilePB() + ..email = email ?? '' + ..token = this.id; + } +} + +extension on String { + Provider toProvider() { + switch (this) { + case 'github': + return Provider.github; + case 'google': + return Provider.google; + case 'discord': + return Provider.discord; + default: + throw UnimplementedError(); + } + } +} diff --git a/frontend/appflowy_flutter/lib/user/application/auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth_service.dart deleted file mode 100644 index c6e684d611..0000000000 --- a/frontend/appflowy_flutter/lib/user/application/auth_service.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:dartz/dartz.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/uuid.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' - show SignInPayloadPB, SignUpPayloadPB, UserProfilePB; - -import '../../generated/locale_keys.g.dart'; - -class AuthService { - Future> signIn({ - required String? email, - required String? password, - }) { - // - final request = SignInPayloadPB.create() - ..email = email ?? '' - ..password = password ?? ''; - - return UserEventSignIn(request).send(); - } - - Future> signUp({ - required String? name, - required String? password, - required String? email, - }) { - final request = SignUpPayloadPB.create() - ..email = email ?? '' - ..name = name ?? '' - ..password = password ?? ''; - - return UserEventSignUp(request).send(); - - // return UserEventSignUp(request).send().then((result) { - // return result.fold((userProfile) async { - // return await FolderEventCreateDefaultWorkspace().send().then((result) { - // return result.fold((workspaceIdentifier) { - // return left(Tuple2(userProfile, workspaceIdentifier.workspaceId)); - // }, (error) { - // throw UnimplementedError; - // }); - // }); - // }, (error) => right(error)); - // }); - } - - Future> signOut() { - return UserEventSignOut().send(); - } - - Future> autoSignUp() { - const password = "AppFlowy123@"; - final uid = uuid(); - final userEmail = "$uid@appflowy.io"; - return signUp( - name: LocaleKeys.defaultUsername.tr(), - password: password, - email: userEmail, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/application/prelude.dart b/frontend/appflowy_flutter/lib/user/application/prelude.dart index 74b644808e..ddacb654d3 100644 --- a/frontend/appflowy_flutter/lib/user/application/prelude.dart +++ b/frontend/appflowy_flutter/lib/user/application/prelude.dart @@ -1,4 +1,4 @@ -export './auth_service.dart'; +export 'auth/appflowy_auth_service.dart'; export './sign_in_bloc.dart'; export './sign_up_bloc.dart'; export './splash_bloc.dart'; diff --git a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart index 69b0fe7a62..a95530a5c0 100644 --- a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart @@ -1,4 +1,4 @@ -import 'package:appflowy/user/application/auth_service.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; @@ -20,6 +20,16 @@ class SignInBloc extends Bloc { emit, ); }, + signedInWithOAuth: (value) async => + await _performActionOnSignInWithOAuth( + state, + emit, + value.platform, + ), + signedInAsGuest: (value) async => await _performActionOnSignInAsGuest( + state, + emit, + ), emailChanged: (EmailChanged value) async { emit( state.copyWith( @@ -45,6 +55,26 @@ class SignInBloc extends Bloc { Future _performActionOnSignIn( SignInState state, Emitter emit, + ) async { + final result = await authService.signIn( + email: state.email ?? '', + password: state.password ?? '', + ); + emit( + result.fold( + (error) => stateFromCode(error), + (userProfile) => state.copyWith( + isSubmitting: false, + successOrFail: some(left(userProfile)), + ), + ), + ); + } + + Future _performActionOnSignInWithOAuth( + SignInState state, + Emitter emit, + String platform, ) async { emit( state.copyWith( @@ -55,17 +85,41 @@ class SignInBloc extends Bloc { ), ); - final result = await authService.signIn( - email: state.email, - password: state.password, + final result = await authService.signUpWithOAuth( + platform: platform, ); emit( result.fold( + (error) => stateFromCode(error), (userProfile) => state.copyWith( isSubmitting: false, successOrFail: some(left(userProfile)), ), + ), + ); + } + + Future _performActionOnSignInAsGuest( + SignInState state, + Emitter emit, + ) async { + emit( + state.copyWith( + isSubmitting: true, + emailError: none(), + passwordError: none(), + successOrFail: none(), + ), + ); + + final result = await authService.signUpAsGuest(); + emit( + result.fold( (error) => stateFromCode(error), + (userProfile) => state.copyWith( + isSubmitting: false, + successOrFail: some(left(userProfile)), + ), ), ); } @@ -97,6 +151,9 @@ class SignInBloc extends Bloc { class SignInEvent with _$SignInEvent { const factory SignInEvent.signedInWithUserEmailAndPassword() = SignedInWithUserEmailAndPassword; + const factory SignInEvent.signedInWithOAuth(String platform) = + SignedInWithOAuth; + const factory SignInEvent.signedInAsGuest() = SignedInAsGuest; const factory SignInEvent.emailChanged(String email) = EmailChanged; const factory SignInEvent.passwordChanged(String password) = PasswordChanged; } diff --git a/frontend/appflowy_flutter/lib/user/application/sign_up_bloc.dart b/frontend/appflowy_flutter/lib/user/application/sign_up_bloc.dart index bb3e99f51c..ca884cdecc 100644 --- a/frontend/appflowy_flutter/lib/user/application/sign_up_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/sign_up_bloc.dart @@ -1,4 +1,4 @@ -import 'package:appflowy/user/application/auth_service.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:dartz/dartz.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart'; @@ -100,12 +100,13 @@ class SignUpBloc extends Bloc { ); final result = await authService.signUp( - name: state.email, - password: state.password, - email: state.email, + name: state.email ?? '', + password: state.password ?? '', + email: state.email ?? '', ); emit( result.fold( + (error) => stateFromCode(error), (profile) => state.copyWith( isSubmitting: false, successOrFail: some(left(profile)), @@ -113,7 +114,6 @@ class SignUpBloc extends Bloc { passwordError: none(), repeatPasswordError: none(), ), - (error) => stateFromCode(error), ), ); } diff --git a/frontend/appflowy_flutter/lib/user/application/splash_bloc.dart b/frontend/appflowy_flutter/lib/user/application/splash_bloc.dart index 56448ad538..f603e1cf53 100644 --- a/frontend/appflowy_flutter/lib/user/application/splash_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/splash_bloc.dart @@ -1,5 +1,6 @@ +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/domain/auth_state.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -10,16 +11,11 @@ class SplashBloc extends Bloc { on((event, emit) async { await event.map( getUser: (val) async { - final result = await UserEventCheckUser().send(); - final authState = result.fold( - (userProfile) { - return AuthState.authenticated(userProfile); - }, - (error) { - return AuthState.unauthenticated(error); - }, + final response = await getIt().getUser(); + final authState = response.fold( + (error) => AuthState.unauthenticated(error), + (user) => AuthState.authenticated(user), ); - emit(state.copyWith(auth: authState)); }, ); diff --git a/frontend/appflowy_flutter/lib/user/application/user_listener.dart b/frontend/appflowy_flutter/lib/user/application/user_listener.dart index 91185b8f45..a06e398bc5 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_listener.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_listener.dart @@ -40,7 +40,7 @@ class UserListener { } _userParser = UserNotificationParser( - id: _userProfile.token, + id: _userProfile.id.toString(), callback: _userNotificationCallback, ); _subscription = RustStreamReceiver.listen((observable) { diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index eacb938de1..60ec1e26f8 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -1,10 +1,10 @@ import 'dart:async'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:fixnum/fixnum.dart'; class UserBackendService { @@ -58,8 +58,9 @@ class UserBackendService { throw UnimplementedError(); } - Future> signOut() { - return UserEventSignOut().send(); + Future> signOut(AuthTypePB authType) { + final payload = SignOutPB()..authType = authType; + return UserEventSignOut(payload).send(); } Future> initUser() async { diff --git a/frontend/appflowy_flutter/lib/user/presentation/folder/folder_widget.dart b/frontend/appflowy_flutter/lib/user/presentation/folder/folder_widget.dart index 4033d59e13..20eb65fbcd 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/folder/folder_widget.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/folder/folder_widget.dart @@ -61,9 +61,9 @@ class _FolderWidgetState extends State { } Future _openFolder() async { - final directory = await getIt().getDirectoryPath(); - if (directory != null) { - await getIt().setLocation(directory); + final path = await getIt().getDirectoryPath(); + if (path != null) { + await getIt().setPath(path); await widget.createFolderCallback(); } } @@ -188,7 +188,7 @@ class CreateFolderWidgetState extends State { LocaleKeys.settings_files_locationCannotBeEmpty.tr(), ); } else { - await getIt().setLocation(_path); + await getIt().setPath(_path); await widget.onPressedCreate(); } }, diff --git a/frontend/appflowy_flutter/lib/user/presentation/router.dart b/frontend/appflowy_flutter/lib/user/presentation/router.dart index b03f3ef042..d0845d41d7 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/router.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/router.dart @@ -1,5 +1,5 @@ import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/auth_service.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/presentation/sign_in_screen.dart'; import 'package:appflowy/user/presentation/sign_up_screen.dart'; import 'package:appflowy/user/presentation/skip_log_in_screen.dart'; @@ -28,7 +28,7 @@ class AuthRouter { ); } - void pushHomeScreen( + void pushHomeScreenWithWorkSpace( BuildContext context, UserProfilePB profile, WorkspaceSettingPB workspaceSetting, @@ -45,6 +45,21 @@ class AuthRouter { ), ); } + + Future pushHomeScreen( + BuildContext context, + UserProfilePB userProfile, + ) async { + final result = await FolderEventReadCurrentWorkspace().send(); + result.fold( + (workspaceSettingPB) => pushHomeScreenWithWorkSpace( + context, + userProfile, + workspaceSettingPB, + ), + (r) => pushWelcomeScreen(context, userProfile), + ); + } } class SplashRoute { diff --git a/frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart index 44b339a31e..fef663f104 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart @@ -1,13 +1,15 @@ +import 'package:appflowy/core/config/kv.dart'; +import 'package:appflowy/core/config/kv_keys.dart'; +import 'package:appflowy/core/frameless_window.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/user/presentation/widgets/background.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/rounded_button.dart'; import 'package:flowy_infra_ui/widget/rounded_input_field.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' @@ -19,21 +21,29 @@ import 'package:flowy_infra/image.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; class SignInScreen extends StatelessWidget { + const SignInScreen({ + super.key, + required this.router, + }); + final AuthRouter router; - const SignInScreen({Key? key, required this.router}) : super(key: key); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => getIt(), - child: BlocListener( + child: BlocConsumer( listener: (context, state) { state.successOrFail.fold( () => null, (result) => _handleSuccessOrFail(result, context), ); }, - child: Scaffold( + builder: (_, __) => Scaffold( + appBar: const PreferredSize( + preferredSize: Size(double.infinity, 60), + child: MoveWindowDetector(), + ), body: SignInForm(router: router), ), ), @@ -45,18 +55,19 @@ class SignInScreen extends StatelessWidget { BuildContext context, ) { result.fold( - (user) => router.pushWelcomeScreen(context, user), + (user) => router.pushHomeScreen(context, user), (error) => showSnapBar(context, error.msg), ); } } class SignInForm extends StatelessWidget { - final AuthRouter router; const SignInForm({ - Key? key, + super.key, required this.router, - }) : super(key: key); + }); + + final AuthRouter router; @override Widget build(BuildContext context) { @@ -64,22 +75,42 @@ class SignInForm extends StatelessWidget { alignment: Alignment.center, child: AuthFormContainer( children: [ + // Email. FlowyLogoTitle( title: LocaleKeys.signIn_loginTitle.tr(), logoSize: const Size(60, 60), ), const VSpace(30), - const EmailTextField(), - const PasswordTextField(), - ForgetPasswordButton(router: router), - const VSpace(30), - const LoginButton(), + // Email and password. don't support yet. + /* + ...[ + const EmailTextField(), + const VSpace(5), + const PasswordTextField(), + const VSpace(20), + const LoginButton(), + const VSpace(10), + + const VSpace(10), + SignUpPrompt(router: router), + ], + */ + + const SignInAsGuestButton(), + + // third-party sign in. + const VSpace(20), + const OrContinueWith(), const VSpace(10), - SignUpPrompt(router: router), + const ThirdPartySignInButtons(), + const VSpace(20), + + // loading status if (context.read().state.isSubmitting) ...[ const SizedBox(height: 8), const LinearProgressIndicator(value: null), - ] + const VSpace(20), + ], ], ), ); @@ -113,6 +144,7 @@ class SignUpPrompt extends StatelessWidget { style: TextStyle(color: Theme.of(context).colorScheme.primary), ), ), + ForgetPasswordButton(router: router), ], ); } @@ -136,6 +168,25 @@ class LoginButton extends StatelessWidget { } } +class SignInAsGuestButton extends StatelessWidget { + const SignInAsGuestButton({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return RoundedTextButton( + title: LocaleKeys.signIn_loginAsGuestButtonText.tr(), + height: 48, + borderRadius: Corners.s6Border, + onPressed: () { + getIt().set(KVKeys.loginType, 'local'); + context.read().add(const SignInEvent.signedInAsGuest()); + }, + ); + } +} + class ForgetPasswordButton extends StatelessWidget { const ForgetPasswordButton({ Key? key, @@ -150,7 +201,9 @@ class ForgetPasswordButton extends StatelessWidget { style: TextButton.styleFrom( textStyle: Theme.of(context).textTheme.bodyMedium, ), - onPressed: () => router.pushForgetPasswordScreen(context), + onPressed: () { + throw UnimplementedError(); + }, child: Text( LocaleKeys.signIn_forgotPassword.tr(), style: TextStyle(color: Theme.of(context).colorScheme.primary), @@ -214,3 +267,98 @@ class EmailTextField extends StatelessWidget { ); } } + +class OrContinueWith extends StatelessWidget { + const OrContinueWith({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: const [ + Flexible( + child: Divider( + color: Colors.white, + height: 10, + ), + ), + FlowyText.regular(' Or continue with '), + Flexible( + child: Divider( + color: Colors.white, + height: 10, + ), + ), + ], + ); + } +} + +class ThirdPartySignInButton extends StatelessWidget { + const ThirdPartySignInButton({ + Key? key, + required this.icon, + required this.onPressed, + }) : super(key: key); + + final String icon; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return FlowyIconButton( + height: 48, + width: 48, + iconPadding: const EdgeInsets.all(8.0), + radius: Corners.s10Border, + onPressed: onPressed, + icon: svgWidget( + icon, + ), + ); + } +} + +class ThirdPartySignInButtons extends StatelessWidget { + const ThirdPartySignInButtons({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ThirdPartySignInButton( + icon: 'login/google-mark', + onPressed: () { + getIt().set(KVKeys.loginType, 'supabase'); + context + .read() + .add(const SignInEvent.signedInWithOAuth('google')); + }, + ), + const SizedBox(width: 20), + ThirdPartySignInButton( + icon: 'login/github-mark', + onPressed: () { + getIt().set(KVKeys.loginType, 'supabase'); + context + .read() + .add(const SignInEvent.signedInWithOAuth('github')); + }, + ), + const SizedBox(width: 20), + ThirdPartySignInButton( + icon: 'login/discord-mark', + onPressed: () { + getIt().set(KVKeys.loginType, 'supabase'); + context + .read() + .add(const SignInEvent.signedInWithOAuth('discord')); + }, + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/sign_up_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/sign_up_screen.dart index 7b8b796fd1..95f9c2fef8 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/sign_up_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/sign_up_screen.dart @@ -18,8 +18,12 @@ import 'package:flowy_infra/image.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; class SignUpScreen extends StatelessWidget { + const SignUpScreen({ + super.key, + required this.router, + }); + final AuthRouter router; - const SignUpScreen({Key? key, required this.router}) : super(key: key); @override Widget build(BuildContext context) { @@ -65,7 +69,9 @@ class SignUpForm extends StatelessWidget { ), const VSpace(30), const EmailTextField(), + const VSpace(5), const PasswordTextField(), + const VSpace(5), const RepeatPasswordTextField(), const VSpace(30), const SignUpButton(), diff --git a/frontend/appflowy_flutter/lib/user/presentation/skip_log_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/skip_log_in_screen.dart index fb9662bc8c..8f69763603 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/skip_log_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/skip_log_in_screen.dart @@ -1,5 +1,6 @@ import 'package:appflowy/core/frameless_window.dart'; import 'package:appflowy/startup/entry_point.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:dartz/dartz.dart' as dartz; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; @@ -15,7 +16,6 @@ import 'package:url_launcher/url_launcher.dart'; import '../../generated/locale_keys.g.dart'; import '../../startup/launch_configuration.dart'; import '../../startup/startup.dart'; -import '../application/auth_service.dart'; import 'folder/folder_widget.dart'; import 'router.dart'; import 'widgets/background.dart'; @@ -120,16 +120,16 @@ class _SkipLogInScreenState extends State { } Future _autoRegister(BuildContext context) async { - final result = await widget.authService.autoSignUp(); + final result = await widget.authService.signUpAsGuest(); result.fold( + (error) { + Log.error(error); + }, (user) { FolderEventReadCurrentWorkspace().send().then((result) { _openCurrentWorkspace(context, user, result); }); }, - (error) { - Log.error(error); - }, ); } @@ -140,7 +140,8 @@ class _SkipLogInScreenState extends State { ) { workspacesOrError.fold( (workspaceSetting) { - widget.router.pushHomeScreen(context, user, workspaceSetting); + widget.router + .pushHomeScreenWithWorkSpace(context, user, workspaceSetting); }, (error) { Log.error(error); diff --git a/frontend/appflowy_flutter/lib/user/presentation/splash_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/splash_screen.dart index e2c60447ce..aefcada89d 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/splash_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/splash_screen.dart @@ -1,10 +1,11 @@ +import 'package:appflowy/startup/tasks/supabase_task.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../startup/startup.dart'; -import '../application/auth_service.dart'; import '../application/splash_bloc.dart'; import '../domain/auth_state.dart'; import 'router.dart'; @@ -64,33 +65,40 @@ class SplashScreen extends StatelessWidget { ); } - void _handleAuthenticated(BuildContext context, Authenticated result) { - final userProfile = result.userProfile; - FolderEventReadCurrentWorkspace().send().then( - (result) { - return result.fold( - (workspaceSetting) { - getIt() - .pushHomeScreen(context, userProfile, workspaceSetting); - }, - (error) async { - Log.error(error); - getIt().pushWelcomeScreen(context, userProfile); - }, + Future _handleAuthenticated( + BuildContext context, + Authenticated authenticated, + ) async { + final userProfile = authenticated.userProfile; + final result = await FolderEventReadCurrentWorkspace().send(); + result.fold( + (workspaceSetting) { + getIt().pushHomeScreen( + context, + userProfile, + workspaceSetting, ); }, + (error) async { + Log.error(error); + getIt().pushWelcomeScreen(context, userProfile); + }, ); } void _handleUnauthenticated(BuildContext context, Unauthenticated result) { - // getIt().pushSignInScreen(context); - getIt().pushSkipLoginScreen(context); + // if the env is not configured, we will skip to the 'skip login screen'. + if (isSupabaseEnable) { + getIt().pushSignInScreen(context); + } else { + getIt().pushSkipLoginScreen(context); + } } Future _registerIfNeeded() async { final result = await UserEventCheckUser().send(); if (!result.isLeft()) { - await getIt().autoSignUp(); + await getIt().signUpAsGuest(); } } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_location_cubit.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_location_cubit.dart index 44b32dfe5d..344ffc2887 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_location_cubit.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_location_cubit.dart @@ -1,71 +1,83 @@ import 'dart:io'; -import 'package:bloc/bloc.dart'; -import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:appflowy/core/config/kv.dart'; +import 'package:appflowy/core/config/kv_keys.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import '../../../startup/tasks/prelude.dart'; -@visibleForTesting -const String kSettingsLocationDefaultLocation = - 'kSettingsLocationDefaultLocation'; +part 'settings_location_cubit.freezed.dart'; -class SettingsLocation { - SettingsLocation({ - String? path, - }) : _path = path; +@freezed +class SettingsLocationState with _$SettingsLocationState { + const factory SettingsLocationState.initial() = _Initial; + const factory SettingsLocationState.didReceivedPath(String path) = + _DidReceivedPath; +} - String? _path; - - set path(String? path) { - _path = path; +class SettingsLocationCubit extends Cubit { + SettingsLocationCubit() : super(const SettingsLocationState.initial()) { + _init(); } - String? get path { + Future setPath(String path) async { + await getIt().setPath(path); + emit(SettingsLocationState.didReceivedPath(path)); + } + + Future _init() async { + final path = await getIt().getPath(); + emit(SettingsLocationState.didReceivedPath(path)); + } +} + +class LocalFileStorage { + LocalFileStorage(); + String? _cachePath; + + Future setPath(String path) async { + if (kIsWeb || Platform.isAndroid || Platform.isIOS) { + Log.info('LocalFileStorage is not supported on this platform.'); + return; + } + if (Platform.isMacOS) { // remove the prefix `/Volumes/*` - return _path?.replaceFirst(RegExp(r'^/Volumes/[^/]+'), ''); + path = path.replaceFirst(RegExp(r'^/Volumes/[^/]+'), ''); } else if (Platform.isWindows) { - return _path?.replaceAll("/", "\\"); + path = path.replaceAll('/', '\\'); } - return _path; + + await getIt().set(KVKeys.pathLocation, path); + // clear the cache path, and not set the cache path to the new path because the set path may be invalid + _cachePath = null; } - SettingsLocation copyWith({String? path}) { - return SettingsLocation( - path: path ?? this.path, + Future getPath() async { + if (_cachePath != null) { + return _cachePath!; + } + + final response = await getIt().get(KVKeys.pathLocation); + final String path = await response.fold( + (error) async { + // return the default path if the path is not set + final directory = await appFlowyDocumentDirectory(); + return directory.path; + }, + (path) => path, ); - } -} + _cachePath = path; -class SettingsLocationCubit extends Cubit { - SettingsLocationCubit() : super(SettingsLocation(path: null)); - - /// Returns a path that used to store user data - Future fetchLocation() async { - final prefs = await SharedPreferences.getInstance(); - - /// Use the [appFlowyDocumentDirectory] instead if there is no user - /// preference location - final path = prefs.getString(kSettingsLocationDefaultLocation) ?? - (await appFlowyDocumentDirectory()).path; - - emit(state.copyWith(path: path)); - return Future.value(path); - } - - /// Saves the user preference local data store location - Future setLocation(String? path) async { - path = path ?? (await appFlowyDocumentDirectory()).path; - - assert(path.isNotEmpty); - if (path.isEmpty) { - path = (await appFlowyDocumentDirectory()).path; + // if the path is not exists means the path is invalid, so we should clear the kv store + if (!Directory(path).existsSync()) { + await getIt().clear(); } - final prefs = await SharedPreferences.getInstance(); - prefs.setString(kSettingsLocationDefaultLocation, path); - await Directory(path).create(recursive: true); - emit(state.copyWith(path: path)); + return path; } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart index 5ed0a07f13..8a7e9af1f7 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart @@ -1,154 +1,258 @@ +import 'dart:io'; + import 'package:appflowy/startup/entry_point.dart'; import 'package:appflowy/util/file_picker/file_picker_service.dart'; import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/services.dart'; - +import 'package:styled_widget/styled_widget.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../../../../generated/locale_keys.g.dart'; import '../../../../startup/launch_configuration.dart'; import '../../../../startup/startup.dart'; import '../../../../startup/tasks/prelude.dart'; -class SettingsFileLocationCustomzier extends StatefulWidget { - const SettingsFileLocationCustomzier({ +class SettingsFileLocationCustomizer extends StatefulWidget { + const SettingsFileLocationCustomizer({ super.key, - required this.cubit, }); - final SettingsLocationCubit cubit; - @override - State createState() => - SettingsFileLocationCustomzierState(); + State createState() => + SettingsFileLocationCustomizerState(); } @visibleForTesting -class SettingsFileLocationCustomzierState - extends State { +class SettingsFileLocationCustomizerState + extends State { @override Widget build(BuildContext context) { - return BlocProvider.value( - value: widget.cubit, - child: BlocBuilder( + return BlocProvider( + create: (_) => SettingsLocationCubit(), + child: BlocBuilder( builder: (context, state) { - return ListTile( - title: FlowyText.medium( - LocaleKeys.settings_files_defaultLocation.tr(), - overflow: TextOverflow.ellipsis, + return state.when( + initial: () => const Center( + child: CircularProgressIndicator(), ), - subtitle: Tooltip( - message: LocaleKeys.settings_files_doubleTapToCopy.tr(), - child: GestureDetector( - onDoubleTap: () { - Clipboard.setData( - ClipboardData( - text: state.path, + didReceivedPath: (path) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + // display file paths. + Flexible( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText.medium( + LocaleKeys.settings_files_defaultLocation.tr(), + fontSize: 13, + overflow: TextOverflow.visible, + ).padding(horizontal: 5), + const VSpace(5), + _CopyableText( + usingPath: path, + ), + ], ), - ).then((_) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: FlowyText( - LocaleKeys.settings_files_pathCopiedSnackbar.tr(), - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ); - } - }); - }, - child: FlowyText.regular( - state.path ?? '', - overflow: TextOverflow.ellipsis, - ), - ), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Tooltip( - message: LocaleKeys.settings_files_restoreLocation.tr(), - child: FlowyIconButton( - height: 40, - width: 40, - icon: const Icon(Icons.restore_outlined), - hoverColor: - Theme.of(context).colorScheme.secondaryContainer, - onPressed: () async { - final result = await appFlowyDocumentDirectory(); - await _setCustomLocation(result.path); - await FlowyRunner.run( - FlowyApp(), - config: const LaunchConfiguration( - autoRegistrationSupported: true, - ), - ); - if (mounted) { - Navigator.of(context).pop(); - } - }, ), - ), - const SizedBox( - width: 5, - ), - Tooltip( - message: LocaleKeys.settings_files_customizeLocation.tr(), - child: FlowyIconButton( - height: 40, - width: 40, - icon: const Icon(Icons.folder_open_outlined), - hoverColor: - Theme.of(context).colorScheme.secondaryContainer, - onPressed: () async { - final result = - await getIt().getDirectoryPath(); - if (result != null) { - await _setCustomLocation(result); - await reloadApp(); - } - }, + + // display the icons + Flexible( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + _ChangeStoragePathButton( + usingPath: path, + ), + const HSpace(10), + _OpenStorageButton( + usingPath: path, + ), + _RecoverDefaultStorageButton( + usingPath: path, + ), + ], + ), ), - ) - ], - ), + ], + ); + }, ); }, ), ); } +} - Future _setCustomLocation(String? path) async { - // Using default location if path equals null. - final location = path ?? (await appFlowyDocumentDirectory()).path; - if (mounted) { - widget.cubit.setLocation(location); - } +class _CopyableText extends StatelessWidget { + const _CopyableText({ + required this.usingPath, + }); - // The location could not save into the KV db, because the db initialize is later than the rust sdk initialize. - /* - final prefs = await SharedPreferences.getInstance(); - if (mounted) { - context - .read() - .setKeyValue(AppearanceKeys.defaultLocation, location); - } - */ - } + final String usingPath; - Future reloadApp() async { - await FlowyRunner.run( - FlowyApp(), - config: const LaunchConfiguration( - autoRegistrationSupported: true, - ), + @override + Widget build(BuildContext context) { + return FlowyHover( + builder: (_, onHover) { + return GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: usingPath)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: FlowyText( + LocaleKeys.settings_files_pathCopiedSnackbar.tr(), + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ); + }, + child: Container( + height: 20, + padding: const EdgeInsets.symmetric(horizontal: 5), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: FlowyText.regular( + usingPath, + fontSize: 12, + overflow: TextOverflow.ellipsis, + ), + ), + if (onHover) + FlowyText.regular( + LocaleKeys.settings_files_copy.tr(), + fontSize: 12, + color: Theme.of(context).colorScheme.primary, + ) + ], + ), + ), + ); + }, + ); + } +} + +class _ChangeStoragePathButton extends StatefulWidget { + const _ChangeStoragePathButton({ + required this.usingPath, + }); + + final String usingPath; + + @override + State<_ChangeStoragePathButton> createState() => + _ChangeStoragePathButtonState(); +} + +class _ChangeStoragePathButtonState extends State<_ChangeStoragePathButton> { + @override + Widget build(BuildContext context) { + return Tooltip( + message: LocaleKeys.settings_files_changeLocationTooltips.tr(), + child: SecondaryTextButton( + LocaleKeys.settings_files_change.tr(), + mode: SecondaryTextButtonMode.small, + onPressed: () async { + // pick the new directory and reload app + final path = await getIt().getDirectoryPath(); + if (path == null || !mounted || widget.usingPath == path) { + return; + } + await context.read().setPath(path); + await FlowyRunner.run( + FlowyApp(), + config: const LaunchConfiguration( + autoRegistrationSupported: true, + ), + ); + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ); + } +} + +class _OpenStorageButton extends StatelessWidget { + const _OpenStorageButton({ + required this.usingPath, + }); + + final String usingPath; + + @override + Widget build(BuildContext context) { + return FlowyIconButton( + hoverColor: Theme.of(context).colorScheme.secondaryContainer, + tooltipText: LocaleKeys.settings_files_openLocationTooltips.tr(), + icon: svgWidget( + 'common/open_folder', + color: Theme.of(context).iconTheme.color, + ), + onPressed: () async { + final uri = Directory(usingPath).uri; + if (await canLaunchUrl(uri)) { + launchUrl(uri); + } + }, + ); + } +} + +class _RecoverDefaultStorageButton extends StatefulWidget { + const _RecoverDefaultStorageButton({ + required this.usingPath, + }); + + final String usingPath; + + @override + State<_RecoverDefaultStorageButton> createState() => + _RecoverDefaultStorageButtonState(); +} + +class _RecoverDefaultStorageButtonState + extends State<_RecoverDefaultStorageButton> { + @override + Widget build(BuildContext context) { + return FlowyIconButton( + hoverColor: Theme.of(context).colorScheme.secondaryContainer, + tooltipText: LocaleKeys.settings_files_recoverLocationTooltips.tr(), + icon: svgWidget( + 'common/recover', + color: Theme.of(context).iconTheme.color, + ), + onPressed: () async { + // reset to the default directory and reload app + final directory = await appFlowyDocumentDirectory(); + final path = directory.path; + if (!mounted || widget.usingPath == path) { + return; + } + await context.read().setPath(path); + await FlowyRunner.run( + FlowyApp(), + config: const LaunchConfiguration( + autoRegistrationSupported: true, + ), + ); + if (mounted) { + Navigator.of(context).pop(); + } + }, ); - if (mounted) { - Navigator.of(context).pop(); - } - return; } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart index c90793fbfd..507efbe37b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_system_view.dart @@ -1,8 +1,6 @@ import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart'; import 'package:flutter/material.dart'; -import '../../../application/settings/settings_location_cubit.dart'; - class SettingsFileSystemView extends StatefulWidget { const SettingsFileSystemView({ super.key, @@ -13,16 +11,15 @@ class SettingsFileSystemView extends StatefulWidget { } class _SettingsFileSystemViewState extends State { - final _locationCubit = SettingsLocationCubit()..fetchLocation(); - @override Widget build(BuildContext context) { + // return Column( + // children: [], + // ); return ListView.separated( itemBuilder: (context, index) { if (index == 0) { - return SettingsFileLocationCustomzier( - cubit: _locationCubit, - ); + return const SettingsFileLocationCustomizer(); } else if (index == 1) { // return _buildExportDatabaseButton(); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index 2e3c8b0622..5b62a726bc 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -227,7 +227,7 @@ class OkCancelButton extends StatelessWidget { SecondaryTextButton( cancelTitle ?? LocaleKeys.button_Cancel.tr(), onPressed: onCancelPressed, - bigMode: true, + mode: SecondaryTextButtonMode.big, ), HSpace(Insets.m), if (onOkPressed != null) diff --git a/frontend/appflowy_flutter/macos/Runner/Info.plist b/frontend/appflowy_flutter/macos/Runner/Info.plist index 4628404f6e..6c91d53468 100644 --- a/frontend/appflowy_flutter/macos/Runner/Info.plist +++ b/frontend/appflowy_flutter/macos/Runner/Info.plist @@ -40,5 +40,16 @@ MainMenu NSPrincipalClass NSApplication + CFBundleURLTypes + + + CFBundleURLName + + CFBundleURLSchemes + + io.appflowy.appflowy-flutter + + + diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/pubspec.yaml b/frontend/appflowy_flutter/packages/appflowy_popover/pubspec.yaml index 80e808f890..7f83c6aee8 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/appflowy_popover/pubspec.yaml @@ -4,8 +4,8 @@ version: 0.0.1 homepage: environment: - sdk: ">=2.17.0 <3.0.0" - flutter: ">=1.17.0" + sdk: ">=2.19.0 <3.0.0" + flutter: ">=3.7.0" dependencies: flutter: diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart index 4dceb85710..972a330ef2 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart @@ -65,9 +65,12 @@ class FlowyText extends StatelessWidget { @override Widget build(BuildContext context) { + final text = overflow == TextOverflow.ellipsis + ? title.replaceAll('', '\u200B') + : title; if (selectable) { return SelectableText( - title, + text, maxLines: maxLines, textAlign: textAlign, style: Theme.of(context).textTheme.bodyMedium!.copyWith( @@ -79,7 +82,7 @@ class FlowyText extends StatelessWidget { ); } else { return Text( - title, + text, maxLines: maxLines, textAlign: textAlign, overflow: overflow ?? TextOverflow.clip, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/secondary_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/secondary_button.dart index eae3c58a2a..d45e6affbd 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/secondary_button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/secondary_button.dart @@ -4,19 +4,50 @@ import 'package:flowy_infra/size.dart'; import 'base_styled_button.dart'; +enum SecondaryTextButtonMode { + normal, + big, + small; + + Size get size { + switch (this) { + case SecondaryTextButtonMode.normal: + return const Size(80, 38); + case SecondaryTextButtonMode.big: + return const Size(100, 40); + case SecondaryTextButtonMode.small: + return const Size(100, 30); + } + } + + BorderRadius get borderRadius { + switch (this) { + case SecondaryTextButtonMode.normal: + return Corners.s8Border; + case SecondaryTextButtonMode.big: + return Corners.s12Border; + case SecondaryTextButtonMode.small: + return Corners.s6Border; + } + } +} + class SecondaryTextButton extends StatelessWidget { + const SecondaryTextButton( + this.label, { + super.key, + this.onPressed, + this.mode = SecondaryTextButtonMode.normal, + }); + final String label; final VoidCallback? onPressed; - final bool bigMode; - - const SecondaryTextButton(this.label, - {Key? key, this.onPressed, this.bigMode = false}) - : super(key: key); + final SecondaryTextButtonMode mode; @override Widget build(BuildContext context) { return SecondaryButton( - bigMode: bigMode, + mode: mode, onPressed: onPressed, child: FlowyText.regular( label, @@ -27,23 +58,27 @@ class SecondaryTextButton extends StatelessWidget { } class SecondaryButton extends StatelessWidget { + const SecondaryButton({ + super.key, + required this.child, + this.onPressed, + this.mode = SecondaryTextButtonMode.normal, + }); + final Widget child; final VoidCallback? onPressed; - final bool bigMode; - - const SecondaryButton( - {Key? key, required this.child, this.onPressed, this.bigMode = false}) - : super(key: key); + final SecondaryTextButtonMode mode; @override Widget build(BuildContext context) { + final size = mode.size; return BaseStyledButton( - minWidth: bigMode ? 100 : 80, - minHeight: bigMode ? 40 : 38, + minWidth: size.width, + minHeight: size.height, contentPadding: EdgeInsets.zero, bgColor: Theme.of(context).colorScheme.surface, outlineColor: Theme.of(context).colorScheme.primary, - borderRadius: bigMode ? Corners.s12Border : Corners.s8Border, + borderRadius: mode.borderRadius, onPressed: onPressed, child: child, ); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml index 59d0c48b7e..03547676a8 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml @@ -5,8 +5,8 @@ homepage: publish_to: "none" environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" + sdk: ">=2.19.0 <3.0.0" + flutter: ">=3.7.0" dependencies: flutter: diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 99cecdcfac..065c49f0ac 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8" + sha256: "405666cd3cf0ee0a48d21ec67e65406aad2c726d9fa58840d3375e7bdcd32a07" url: "https://pub.dev" source: hosted - version: "47.0.0" + version: "60.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80" + sha256: "1952250bd005bacb895a01bf1b4dc00e3ba1c526cf47dca54dfe24979c65f5b3" url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "5.12.0" animations: dependency: transitive description: @@ -149,10 +149,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "93f05c041932674be039b0a2323d6cf57e5f2bbf884a3c0382f9e53fc45ebace" + sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.3" build_runner_core: dependency: transitive description: @@ -393,6 +393,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.2" + envied: + dependency: "direct main" + description: + name: envied + sha256: "60d3f5606c7b35bc6ef493e650d916b34351d8af2e58b7ac45881ba59dfcf039" + url: "https://pub.dev" + source: hosted + version: "0.3.0+3" + envied_generator: + dependency: "direct dev" + description: + name: envied_generator + sha256: dfdbe5dc52863e54c036a4c4042afbdf1bd528cb4c1e638ecba26228ba72e9e5 + url: "https://pub.dev" + source: hosted + version: "0.3.0+3" equatable: dependency: "direct main" description: @@ -579,10 +595,10 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: "4f4a162323c86ffc1245765cfe138872b8f069deb42f7dbb36115fa27f31469b" + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "3.2.0" fuchsia_remote_debug_protocol: dependency: transitive description: flutter @@ -717,10 +733,10 @@ packages: dependency: transitive description: name: intl_utils - sha256: "856baa08d4735ee3476901827d52671c1a2b6f9ccb8b848d73bc5b8dd28b21e5" + sha256: db392393fbf891e3eb32f6beb1928b00cdb33e3c54597fd5f5dc5c43e5ba601c url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.8.2" io: dependency: transitive description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index b0320c7953..0f2afa8481 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -108,6 +108,7 @@ dependencies: flutter_svg: ^2.0.5 nanoid: ^1.0.0 supabase_flutter: ^1.9.1 + envied: ^0.3.0+3 dev_dependencies: flutter_lints: ^2.0.1 @@ -116,10 +117,11 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter - build_runner: ^2.2.0 + build_runner: ^2.3.3 freezed: ^2.1.0+1 bloc_test: ^9.0.2 json_serializable: ^6.5.4 + envied_generator: ^0.3.0+3 # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is @@ -174,6 +176,7 @@ flutter: - assets/images/emoji/ - assets/images/grid/field/ - assets/images/common/ + - assets/images/login/ - assets/images/grid/setting/ - assets/translations/ diff --git a/frontend/appflowy_flutter/test/util.dart b/frontend/appflowy_flutter/test/util.dart index 72606d5796..4278ea2a1b 100644 --- a/frontend/appflowy_flutter/test/util.dart +++ b/frontend/appflowy_flutter/test/util.dart @@ -1,6 +1,6 @@ import 'package:appflowy/startup/launch_configuration.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/auth_service.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy/workspace/application/workspace/workspace_service.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; @@ -55,11 +55,11 @@ class AppFlowyUnitTest { email: userEmail, ); return result.fold( + (error) {}, (user) { userProfile = user; userService = UserBackendService(userId: userProfile.id); }, - (error) {}, ); } diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index ed921f33f2..d0055f78ff 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -423,10 +423,10 @@ dependencies = [ "http", "http-body", "hyper", - "hyper-rustls", + "hyper-rustls 0.23.2", "lazy_static", "pin-project-lite", - "rustls", + "rustls 0.20.8", "tokio", "tower", "tracing", @@ -1791,6 +1791,7 @@ dependencies = [ "flowy-error", "flowy-folder2", "flowy-net", + "flowy-server", "flowy-sqlite", "flowy-task", "flowy-user", @@ -1932,6 +1933,7 @@ dependencies = [ "tokio", "tracing", "unicode-segmentation", + "uuid", ] [[package]] @@ -1939,33 +1941,14 @@ name = "flowy-net" version = "0.1.0" dependencies = [ "anyhow", - "async-stream", "bytes", - "config", - "dashmap", "flowy-codegen", "flowy-derive", - "flowy-document2", "flowy-error", - "flowy-folder2", - "flowy-user", - "futures-util", - "hyper", - "lazy_static", "lib-dispatch", - "lib-infra", - "lib-ws", - "nanoid", - "parking_lot 0.12.1", "protobuf", - "reqwest", - "serde", - "serde-aux", - "serde_json", - "strum", "strum_macros", "thiserror", - "tokio", "tracing", ] @@ -1983,6 +1966,34 @@ dependencies = [ "tracing", ] +[[package]] +name = "flowy-server" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "config", + "flowy-config", + "flowy-error", + "flowy-user", + "futures-util", + "hyper", + "lazy_static", + "lib-infra", + "nanoid", + "parking_lot 0.12.1", + "postgrest", + "reqwest", + "serde", + "serde-aux", + "serde_json", + "thiserror", + "tokio", + "tokio-retry", + "tracing", + "uuid", +] + [[package]] name = "flowy-sqlite" version = "0.1.0" @@ -2036,6 +2047,7 @@ dependencies = [ "protobuf", "serde", "serde_json", + "serde_repr", "strum", "strum_macros", "tokio", @@ -2706,10 +2718,23 @@ dependencies = [ "http", "hyper", "log", - "rustls", + "rustls 0.20.8", "rustls-native-certs", "tokio", - "tokio-rustls", + "tokio-rustls 0.23.4", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0646026eb1b3eea4cd9ba47912ea5ce9cc07713d105b1a14698f4e6433d348b7" +dependencies = [ + "http", + "hyper", + "rustls 0.21.1", + "tokio", + "tokio-rustls 0.24.0", ] [[package]] @@ -3021,6 +3046,7 @@ dependencies = [ name = "lib-infra" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", "bytes", "chrono", @@ -3994,6 +4020,15 @@ dependencies = [ "miniz_oxide 0.7.1", ] +[[package]] +name = "postgrest" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e66400cb23a379592bc8c8bdc9adda652eef4a969b74ab78454a8e8c11330c2b" +dependencies = [ + "reqwest", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -4419,6 +4454,7 @@ dependencies = [ "http", "http-body", "hyper", + "hyper-rustls 0.24.0", "hyper-tls", "ipnet", "js-sys", @@ -4428,16 +4464,20 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls 0.21.1", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "tokio", "tokio-native-tls", + "tokio-rustls 0.24.0", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", "winreg 0.10.1", ] @@ -4587,6 +4627,18 @@ dependencies = [ "webpki", ] +[[package]] +name = "rustls" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c911ba11bc8433e811ce56fde130ccf32f5127cab0e0194e9c68c5a5b671791e" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + [[package]] name = "rustls-native-certs" version = "0.6.2" @@ -4608,6 +4660,16 @@ dependencies = [ "base64 0.21.0", ] +[[package]] +name = "rustls-webpki" +version = "0.100.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.12" @@ -4754,9 +4816,9 @@ dependencies = [ [[package]] name = "serde-aux" -version = "1.1.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "905f2fc9f3d1574e8b5923a58118240021f01d4e239673937ffb9f42707a4f22" +checksum = "c3dfe1b7eb6f9dcf011bd6fad169cdeaae75eda0d61b1a99a3f015b41b0cae39" dependencies = [ "chrono", "serde", @@ -5641,11 +5703,21 @@ version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" dependencies = [ - "rustls", + "rustls 0.20.8", "tokio", "webpki", ] +[[package]] +name = "tokio-rustls" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0d409377ff5b1e3ca6437aa86c1eb7d40c134bfec254e44c830defa92669db5" +dependencies = [ + "rustls 0.21.1", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.14" @@ -6329,6 +6401,15 @@ dependencies = [ "untrusted", ] +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + [[package]] name = "webview2-com" version = "0.19.1" diff --git a/frontend/appflowy_tauri/src-tauri/src/init.rs b/frontend/appflowy_tauri/src-tauri/src/init.rs index fbffd072c5..3043d0c785 100644 --- a/frontend/appflowy_tauri/src-tauri/src/init.rs +++ b/frontend/appflowy_tauri/src-tauri/src/init.rs @@ -1,5 +1,4 @@ -use flowy_core::{ AppFlowyCore, AppFlowyCoreConfig, DEFAULT_NAME}; -use flowy_net::http_server::self_host::configuration::get_client_server_configuration; +use flowy_core::{AppFlowyCore, AppFlowyCoreConfig, DEFAULT_NAME}; pub fn init_flowy_core() -> AppFlowyCore { let config_json = include_str!("../tauri.conf.json"); @@ -12,12 +11,7 @@ pub fn init_flowy_core() -> AppFlowyCore { data_path.push("data"); std::env::set_var("RUST_LOG", "trace"); - let server_config = get_client_server_configuration().unwrap(); - let config = AppFlowyCoreConfig::new( - data_path.to_str().unwrap(), - DEFAULT_NAME.to_string(), - server_config, - ) - .log_filter("trace", vec!["appflowy_tauri".to_string()]); + let config = AppFlowyCoreConfig::new(data_path.to_str().unwrap(), DEFAULT_NAME.to_string()) + .log_filter("trace", vec!["appflowy_tauri".to_string()]); AppFlowyCore::new(config) } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts index 7c9e50d3fc..2159dc761c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts @@ -1,5 +1,7 @@ import { nanoid } from '@reduxjs/toolkit'; import { + AuthTypePB, + SignOutPB, UserEventCheckUser, UserEventGetUserProfile, UserEventSignIn, @@ -8,13 +10,13 @@ import { UserEventUpdateUserProfile, } from '@/services/backend/events/flowy-user'; import { + CreateWorkspacePayloadPB, SignInPayloadPB, SignUpPayloadPB, UpdateUserProfilePayloadPB, WorkspaceIdPB, - CreateWorkspacePayloadPB, - WorkspaceSettingPB, WorkspacePB, + WorkspaceSettingPB, } from '@/services/backend'; import { FolderEventCreateWorkspace, @@ -81,7 +83,8 @@ export class UserBackendService { }; signOut = () => { - return UserEventSignOut(); + const payload = SignOutPB.fromObject({ auth_type: AuthTypePB.Local }); + return UserEventSignOut(payload); }; } @@ -97,7 +100,8 @@ export class AuthBackendService { }; signOut = () => { - return UserEventSignOut(); + const payload = SignOutPB.fromObject({ auth_type: AuthTypePB.Local }); + return UserEventSignOut(payload); }; autoSignUp = () => { diff --git a/frontend/rust-lib/.gitignore b/frontend/rust-lib/.gitignore index 740f8d77a7..5e19a3e66b 100644 --- a/frontend/rust-lib/.gitignore +++ b/frontend/rust-lib/.gitignore @@ -13,4 +13,5 @@ bin/ **/src/protobuf **/resources/proto .idea/ -AppFlowy-Collab/ \ No newline at end of file +AppFlowy-Collab/ +.env \ No newline at end of file diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index bf8f64793c..591497edb9 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -1262,6 +1262,7 @@ dependencies = [ "flowy-derive", "flowy-net", "flowy-notification", + "flowy-server", "lazy_static", "lib-dispatch", "log", @@ -1376,6 +1377,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "dyn-clone" version = "1.0.11" @@ -1563,6 +1570,7 @@ dependencies = [ "flowy-error", "flowy-folder2", "flowy-net", + "flowy-server", "flowy-sqlite", "flowy-task", "flowy-user", @@ -1709,6 +1717,7 @@ dependencies = [ "tokio", "tracing", "unicode-segmentation", + "uuid", ] [[package]] @@ -1716,33 +1725,14 @@ name = "flowy-net" version = "0.1.0" dependencies = [ "anyhow", - "async-stream", "bytes", - "config", - "dashmap", "flowy-codegen", "flowy-derive", - "flowy-document2", "flowy-error", - "flowy-folder2", - "flowy-user", - "futures-util", - "hyper", - "lazy_static", "lib-dispatch", - "lib-infra", - "lib-ws", - "nanoid", - "parking_lot 0.12.1", "protobuf", - "reqwest", - "serde", - "serde-aux", - "serde_json", - "strum", "strum_macros", "thiserror", - "tokio", "tracing", ] @@ -1760,6 +1750,35 @@ dependencies = [ "tracing", ] +[[package]] +name = "flowy-server" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "config", + "dotenv", + "flowy-config", + "flowy-error", + "flowy-user", + "futures-util", + "hyper", + "lazy_static", + "lib-infra", + "nanoid", + "parking_lot 0.12.1", + "postgrest", + "reqwest", + "serde", + "serde-aux", + "serde_json", + "thiserror", + "tokio", + "tokio-retry", + "tracing", + "uuid", +] + [[package]] name = "flowy-sqlite" version = "0.1.0" @@ -1804,6 +1823,7 @@ dependencies = [ "flowy-core", "flowy-folder2", "flowy-net", + "flowy-server", "flowy-user", "futures", "futures-util", @@ -1853,6 +1873,7 @@ dependencies = [ "rand_core 0.6.4", "serde", "serde_json", + "serde_repr", "strum", "strum_macros", "tokio", @@ -2488,6 +2509,7 @@ dependencies = [ name = "lib-infra" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", "bytes", "chrono", @@ -3237,6 +3259,15 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +[[package]] +name = "postgrest" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e66400cb23a379592bc8c8bdc9adda652eef4a969b74ab78454a8e8c11330c2b" +dependencies = [ + "reqwest", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -3727,6 +3758,7 @@ dependencies = [ "http", "http-body", "hyper", + "hyper-rustls", "hyper-tls", "ipnet", "js-sys", @@ -3736,16 +3768,20 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "tokio", "tokio-native-tls", + "tokio-rustls", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", "winreg", ] @@ -4030,9 +4066,9 @@ dependencies = [ [[package]] name = "serde-aux" -version = "1.1.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "905f2fc9f3d1574e8b5923a58118240021f01d4e239673937ffb9f42707a4f22" +checksum = "c3dfe1b7eb6f9dcf011bd6fad169cdeaae75eda0d61b1a99a3f015b41b0cae39" dependencies = [ "chrono", "serde", @@ -4970,6 +5006,15 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "uuid" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" +dependencies = [ + "getrandom 0.2.9", +] + [[package]] name = "validator" version = "0.16.0" @@ -5133,6 +5178,15 @@ dependencies = [ "untrusted", ] +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + [[package]] name = "which" version = "4.4.0" diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index f9232200d6..d613ae3bea 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -14,6 +14,7 @@ members = [ "flowy-error", "flowy-database2", "flowy-task", + "flowy-server", "flowy-config", ] diff --git a/frontend/rust-lib/dart-ffi/Cargo.toml b/frontend/rust-lib/dart-ffi/Cargo.toml index 5c2d0b71e9..0b0380d9e6 100644 --- a/frontend/rust-lib/dart-ffi/Cargo.toml +++ b/frontend/rust-lib/dart-ffi/Cargo.toml @@ -31,6 +31,7 @@ flowy-core = { path = "../flowy-core" } flowy-notification = { path = "../flowy-notification" } flowy-net = { path = "../flowy-net" } flowy-derive = { path = "../../../shared-lib/flowy-derive" } +flowy-server = { path = "../flowy-server" } [features] default = ["dart", "rev-sqlite"] diff --git a/frontend/rust-lib/dart-ffi/src/lib.rs b/frontend/rust-lib/dart-ffi/src/lib.rs index e86227aa1e..4b18368f90 100644 --- a/frontend/rust-lib/dart-ffi/src/lib.rs +++ b/frontend/rust-lib/dart-ffi/src/lib.rs @@ -6,7 +6,6 @@ use lazy_static::lazy_static; use parking_lot::RwLock; use flowy_core::*; -use flowy_net::http_server::self_host::configuration::get_client_server_configuration; use flowy_notification::register_notification_sender; use lib_dispatch::prelude::ToBytes; use lib_dispatch::prelude::*; @@ -32,10 +31,9 @@ pub extern "C" fn init_sdk(path: *mut c_char) -> i64 { let c_str: &CStr = unsafe { CStr::from_ptr(path) }; let path: &str = c_str.to_str().unwrap(); - let server_config = get_client_server_configuration().unwrap(); let log_crates = vec!["flowy-ffi".to_string()]; - let config = AppFlowyCoreConfig::new(path, DEFAULT_NAME.to_string(), server_config) - .log_filter("info", log_crates); + let config = + AppFlowyCoreConfig::new(path, DEFAULT_NAME.to_string()).log_filter("info", log_crates); *APPFLOWY_CORE.write() = Some(AppFlowyCore::new(config)); 0 diff --git a/frontend/rust-lib/flowy-config/src/entities.rs b/frontend/rust-lib/flowy-config/src/entities.rs index d3b1bad2e1..625e45f7d0 100644 --- a/frontend/rust-lib/flowy-config/src/entities.rs +++ b/frontend/rust-lib/flowy-config/src/entities.rs @@ -15,18 +15,35 @@ pub struct KeyPB { pub key: String, } +pub const SUPABASE_URL: &str = "SUPABASE_URL"; +pub const SUPABASE_ANON_KEY: &str = "SUPABASE_ANON_KEY"; +pub const SUPABASE_KEY: &str = "SUPABASE_KEY"; +pub const SUPABASE_JWT_SECRET: &str = "SUPABASE_JWT_SECRET"; + #[derive(Default, ProtoBuf)] pub struct SupabaseConfigPB { #[pb(index = 1)] supabase_url: String, #[pb(index = 2)] - supabase_key: String, + anon_key: String, #[pb(index = 3)] + key: String, + + #[pb(index = 4)] jwt_secret: String, } +impl SupabaseConfigPB { + pub(crate) fn write_to_env(self) { + std::env::set_var(SUPABASE_URL, self.supabase_url); + std::env::set_var(SUPABASE_ANON_KEY, self.anon_key); + std::env::set_var(SUPABASE_KEY, self.key); + std::env::set_var(SUPABASE_JWT_SECRET, self.jwt_secret); + } +} + #[derive(Default, ProtoBuf)] pub struct AppFlowyCollabConfigPB { #[pb(index = 1, one_of)] diff --git a/frontend/rust-lib/flowy-config/src/event_handler.rs b/frontend/rust-lib/flowy-config/src/event_handler.rs index 9bd3b1eda4..9915e83526 100644 --- a/frontend/rust-lib/flowy-config/src/event_handler.rs +++ b/frontend/rust-lib/flowy-config/src/event_handler.rs @@ -35,6 +35,7 @@ pub(crate) async fn remove_key_value_handler(data: AFPluginData) -> Flowy pub(crate) async fn set_supabase_config_handler( data: AFPluginData, ) -> FlowyResult<()> { - let _config = data.into_inner(); + let config = data.into_inner(); + config.write_to_env(); Ok(()) } diff --git a/frontend/rust-lib/flowy-config/src/event_map.rs b/frontend/rust-lib/flowy-config/src/event_map.rs index 1a4068acdb..e9c5392547 100644 --- a/frontend/rust-lib/flowy-config/src/event_map.rs +++ b/frontend/rust-lib/flowy-config/src/event_map.rs @@ -26,6 +26,8 @@ pub enum ConfigEvent { #[event(input = "KeyPB")] RemoveKeyValue = 2, + /// Set the supabase config. It will be written to the environment variables. + /// Check out the `write_to_env` of [SupabaseConfigPB]. #[event(input = "SupabaseConfigPB")] SetSupabaseConfig = 3, } diff --git a/frontend/rust-lib/flowy-config/src/lib.rs b/frontend/rust-lib/flowy-config/src/lib.rs index 0c11fb9c5a..e08a6c9ce6 100644 --- a/frontend/rust-lib/flowy-config/src/lib.rs +++ b/frontend/rust-lib/flowy-config/src/lib.rs @@ -1,4 +1,4 @@ -mod entities; +pub mod entities; mod event_handler; pub mod event_map; mod protobuf; diff --git a/frontend/rust-lib/flowy-core/Cargo.toml b/frontend/rust-lib/flowy-core/Cargo.toml index bee70ed1a5..05597d4c2a 100644 --- a/frontend/rust-lib/flowy-core/Cargo.toml +++ b/frontend/rust-lib/flowy-core/Cargo.toml @@ -19,6 +19,7 @@ flowy-document2 = { path = "../flowy-document2" } #flowy-revision = { path = "../flowy-revision" } flowy-error = { path = "../flowy-error" } flowy-task = { path = "../flowy-task" } +flowy-server = { path = "../flowy-server" } flowy-config = { path = "../flowy-config" } appflowy-integrate = { version = "0.1.0" } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs index db8bc84e45..d388be3b1e 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs @@ -31,7 +31,7 @@ impl DatabaseUser2 for DatabaseUserImpl { .map_err(|e| FlowyError::internal().context(e)) } - fn token(&self) -> Result { + fn token(&self) -> Result, FlowyError> { self .0 .token() diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/document2_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/document2_deps.rs index 3bbc269443..5ad0a81dae 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/document2_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/document2_deps.rs @@ -29,7 +29,7 @@ impl DocumentUser for DocumentUserImpl { .map_err(|e| FlowyError::internal().context(e)) } - fn token(&self) -> Result { + fn token(&self) -> Result, FlowyError> { self .0 .token() diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs index 4f74aed4c8..07cd1d0fc0 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs @@ -63,7 +63,7 @@ impl FolderUser for FolderUserImpl { .map_err(|e| FlowyError::internal().context(e)) } - fn token(&self) -> Result { + fn token(&self) -> Result, FlowyError> { self .0 .token() diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs index 772b015ca9..cf2a7381d1 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs @@ -117,7 +117,7 @@ impl WorkspaceUser for WorkspaceUserImpl { .map_err(|e| FlowyError::internal().context(e)) } - fn token(&self) -> Result { + fn token(&self) -> Result, FlowyError> { self .0 .token() diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/mod.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/mod.rs index 06fd4a9cef..7264d60864 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/mod.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/mod.rs @@ -1,11 +1,9 @@ pub use database_deps::*; pub use document2_deps::*; pub use folder2_deps::*; -pub use user_deps::*; mod document2_deps; mod folder2_deps; -mod user_deps; mod util; mod database_deps; diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs deleted file mode 100644 index 66b3922070..0000000000 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs +++ /dev/null @@ -1,19 +0,0 @@ -use std::sync::Arc; - -use flowy_net::http_server::self_host::configuration::ClientServerConfiguration; -use flowy_net::http_server::self_host::user::UserHttpCloudService; -use flowy_net::local_server::LocalServer; -use flowy_user::event_map::UserCloudService; - -pub struct UserDepsResolver(); -impl UserDepsResolver { - pub fn resolve( - local_server: &Option>, - server_config: &ClientServerConfiguration, - ) -> Arc { - match local_server.clone() { - None => Arc::new(UserHttpCloudService::new(server_config)), - Some(local_server) => local_server, - } - } -} diff --git a/frontend/rust-lib/flowy-core/src/integrate/mod.rs b/frontend/rust-lib/flowy-core/src/integrate/mod.rs new file mode 100644 index 0000000000..da8d32a924 --- /dev/null +++ b/frontend/rust-lib/flowy-core/src/integrate/mod.rs @@ -0,0 +1 @@ +pub(crate) mod server; diff --git a/frontend/rust-lib/flowy-core/src/integrate/server.rs b/frontend/rust-lib/flowy-core/src/integrate/server.rs new file mode 100644 index 0000000000..eb4a5bb0f9 --- /dev/null +++ b/frontend/rust-lib/flowy-core/src/integrate/server.rs @@ -0,0 +1,68 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use parking_lot::RwLock; + +use flowy_error::{ErrorCode, FlowyError}; +use flowy_server::local_server::LocalServer; +use flowy_server::self_host::configuration::self_host_server_configuration; +use flowy_server::self_host::SelfHostServer; +use flowy_server::supabase::{SupabaseConfiguration, SupabaseServer}; +use flowy_server::AppFlowyServer; +use flowy_user::event_map::{UserAuthService, UserCloudServiceProvider}; +use flowy_user::services::AuthType; + +/// The [AppFlowyServerProvider] provides list of [AppFlowyServer] base on the [AuthType]. Using +/// the auth type, the [AppFlowyServerProvider] will create a new [AppFlowyServer] if it doesn't +/// exist. +/// Each server implements the [AppFlowyServer] trait, which provides the [UserAuthService], etc. +#[derive(Default)] +pub struct AppFlowyServerProvider { + providers: RwLock>>, +} + +impl AppFlowyServerProvider { + pub fn new() -> Self { + Self::default() + } +} + +impl UserCloudServiceProvider for AppFlowyServerProvider { + /// Returns the [UserAuthService] base on the current [AuthType]. + /// Creates a new [AppFlowyServer] if it doesn't exist. + fn get_auth_service(&self, auth_type: &AuthType) -> Result, FlowyError> { + if let Some(provider) = self.providers.read().get(auth_type) { + return Ok(provider.user_service()); + } + + let server = server_from_auth_type(auth_type)?; + let user_service = server.user_service(); + self.providers.write().insert(auth_type.clone(), server); + Ok(user_service) + } +} + +fn server_from_auth_type(auth_type: &AuthType) -> Result, FlowyError> { + match auth_type { + AuthType::Local => { + let server = Arc::new(LocalServer::new()); + Ok(server) + }, + AuthType::SelfHosted => { + let config = self_host_server_configuration().map_err(|e| { + FlowyError::new( + ErrorCode::InvalidAuthConfig, + format!("Missing self host config: {:?}. Error: {:?}", auth_type, e), + ) + })?; + let server = Arc::new(SelfHostServer::new(config)); + Ok(server) + }, + AuthType::Supabase => { + // init the SupabaseServerConfiguration from the environment variables. + let config = SupabaseConfiguration::from_env()?; + let server = Arc::new(SupabaseServer::new(config)); + Ok(server) + }, + } +} diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 0b865c82b2..c5b5b81a64 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(unused_doc_comments)] + use std::str::FromStr; use std::time::Duration; use std::{ @@ -16,12 +18,10 @@ use flowy_database2::DatabaseManager2; use flowy_document2::manager::DocumentManager as DocumentManager2; use flowy_error::FlowyResult; use flowy_folder2::manager::Folder2Manager; -use flowy_net::http_server::self_host::configuration::ClientServerConfiguration; -use flowy_net::local_server::LocalServer; use flowy_sqlite::kv::KV; use flowy_task::{TaskDispatcher, TaskRunner}; use flowy_user::entities::UserProfile; -use flowy_user::event_map::UserStatusCallback; +use flowy_user::event_map::{UserCloudServiceProvider, UserStatusCallback}; use flowy_user::services::{UserSession, UserSessionConfig}; use lib_dispatch::prelude::*; use lib_dispatch::runtime::tokio_default_runtime; @@ -30,8 +30,10 @@ use module::make_plugins; pub use module::*; use crate::deps_resolve::*; +use crate::integrate::server::AppFlowyServerProvider; mod deps_resolve; +mod integrate; pub mod module; static INIT_LOG: AtomicBool = AtomicBool::new(false); @@ -47,25 +49,22 @@ pub struct AppFlowyCoreConfig { /// Panics if the `root` path is not existing storage_path: String, log_filter: String, - server_config: ClientServerConfiguration, } impl fmt::Debug for AppFlowyCoreConfig { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("AppFlowyCoreConfig") .field("storage_path", &self.storage_path) - .field("server-config", &self.server_config) .finish() } } impl AppFlowyCoreConfig { - pub fn new(root: &str, name: String, server_config: ClientServerConfiguration) -> Self { + pub fn new(root: &str, name: String) -> Self { AppFlowyCoreConfig { name, storage_path: root.to_owned(), log_filter: create_log_filter("info".to_owned(), vec![]), - server_config, } } @@ -117,22 +116,33 @@ pub struct AppFlowyCore { pub user_session: Arc, pub document_manager2: Arc, pub folder_manager: Arc, - // pub database_manager: Arc, pub database_manager: Arc, pub event_dispatcher: Arc, - pub local_server: Option>, + pub server_provider: Arc, pub task_dispatcher: Arc>, } impl AppFlowyCore { pub fn new(config: AppFlowyCoreConfig) -> Self { + /// The profiling can be used to tracing the performance of the application. + /// Check out the [Link](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/profiling) + /// for more information. #[cfg(feature = "profiling")] console_subscriber::init(); + // Init the logger before anything else init_log(&config); + + // Init the key value database init_kv(&config.storage_path); + + // The collab config is used to build the [Collab] instance that used in document, + // database, folder, etc. let collab_config = get_collab_config(); inject_aws_env(collab_config.aws_config()); + + /// The shared collab builder is used to build the [Collab] instance. The plugins will be loaded + /// on demand based on the [AppFlowyCollabConfig]. let collab_builder = Arc::new(AppFlowyCollabBuilder::new(collab_config)); tracing::debug!("🔥 {:?}", config); @@ -141,11 +151,10 @@ impl AppFlowyCore { let task_dispatcher = Arc::new(RwLock::new(task_scheduler)); runtime.spawn(TaskRunner::run(task_dispatcher.clone())); - let local_server = mk_local_server(&config.server_config); - let (user_session, folder_manager, local_server, database_manager, document_manager2) = runtime - .block_on(async { - let user_session = mk_user_session(&config, &local_server, &config.server_config); - + let server_provider = Arc::new(AppFlowyServerProvider::new()); + let (user_session, folder_manager, server_provider, database_manager, document_manager2) = + runtime.block_on(async { + let user_session = mk_user_session(&config, server_provider.clone()); let database_manager2 = Database2DepsResolver::resolve( user_session.clone(), task_dispatcher.clone(), @@ -170,7 +179,7 @@ impl AppFlowyCore { ( user_session, folder_manager, - local_server, + server_provider, database_manager2, document_manager2, ) @@ -181,9 +190,11 @@ impl AppFlowyCore { database_manager: database_manager.clone(), config: config.clone(), }; + let user_status_callback = UserStatusCallbackImpl { listener: Arc::new(user_status_listener), }; + let cloned_user_session = user_session.clone(); runtime.block_on(async move { cloned_user_session.clone().init(user_status_callback).await; @@ -205,7 +216,7 @@ impl AppFlowyCore { folder_manager, database_manager, event_dispatcher, - local_server, + server_provider, task_dispatcher, } } @@ -215,18 +226,6 @@ impl AppFlowyCore { } } -fn mk_local_server(server_config: &ClientServerConfiguration) -> Option> { - // let ws_addr = server_config.ws_addr(); - if cfg!(feature = "http_sync") { - // let ws_conn = Arc::new(FlowyWebSocketConnect::new(ws_addr)); - None - } else { - let context = flowy_net::local_server::build_server(server_config); - // let ws_conn = Arc::new(FlowyWebSocketConnect::from_local(ws_addr, local_ws)); - Some(Arc::new(context.local_server)) - } -} - fn init_kv(root: &str) { match KV::init(root) { Ok(_) => {}, @@ -263,12 +262,10 @@ fn init_log(config: &AppFlowyCoreConfig) { fn mk_user_session( config: &AppFlowyCoreConfig, - local_server: &Option>, - server_config: &ClientServerConfiguration, + user_cloud_service_provider: Arc, ) -> Arc { let user_config = UserSessionConfig::new(&config.name, &config.storage_path); - let cloud_service = UserDepsResolver::resolve(local_server, server_config); - Arc::new(UserSession::new(user_config, cloud_service)) + Arc::new(UserSession::new(user_config, user_cloud_service_provider)) } struct UserStatusListener { @@ -279,20 +276,23 @@ struct UserStatusListener { } impl UserStatusListener { - async fn did_sign_in(&self, token: &str, user_id: i64) -> FlowyResult<()> { - self.folder_manager.initialize(user_id).await?; - self.database_manager.initialize(user_id, token).await?; - // self - // .ws_conn - // .start(token.to_owned(), user_id.to_owned()) - // .await?; + async fn did_sign_in(&self, user_id: i64, workspace_id: &str) -> FlowyResult<()> { + self + .folder_manager + .initialize(user_id, workspace_id) + .await?; + self.database_manager.initialize(user_id).await?; Ok(()) } async fn did_sign_up(&self, user_profile: &UserProfile) -> FlowyResult<()> { self .folder_manager - .initialize_with_new_user(user_profile.id, &user_profile.token) + .initialize_with_new_user( + user_profile.id, + &user_profile.token, + &user_profile.workspace_id, + ) .await?; self @@ -314,11 +314,11 @@ struct UserStatusCallbackImpl { } impl UserStatusCallback for UserStatusCallbackImpl { - fn did_sign_in(&self, token: &str, user_id: i64) -> Fut> { + fn did_sign_in(&self, user_id: i64, workspace_id: &str) -> Fut> { let listener = self.listener.clone(); - let token = token.to_owned(); let user_id = user_id.to_owned(); - to_fut(async move { listener.did_sign_in(&token, user_id).await }) + let workspace_id = workspace_id.to_owned(); + to_fut(async move { listener.did_sign_in(user_id, &workspace_id).await }) } fn did_sign_up(&self, user_profile: &UserProfile) -> Fut> { @@ -333,9 +333,4 @@ impl UserStatusCallback for UserStatusCallbackImpl { let user_id = user_id.to_owned(); to_fut(async move { listener.did_expired(&token, user_id).await }) } - - fn will_migrated(&self, _token: &str, _old_user_id: &str, _user_id: i64) -> Fut> { - // Read the folder data - todo!() - } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs index 989736867c..ab9ca7bd9b 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs @@ -1,3 +1,12 @@ +use std::convert::TryInto; +use std::sync::Arc; + +use bytes::Bytes; +use collab_database::fields::Field; + +use flowy_derive::ProtoBuf; +use flowy_error::ErrorCode; + use crate::entities::parser::NotEmptyStr; use crate::entities::{ CheckboxFilterPB, ChecklistFilterPB, DateFilterContentPB, DateFilterPB, FieldType, @@ -5,12 +14,6 @@ use crate::entities::{ }; use crate::services::field::SelectOptionIds; use crate::services::filter::{Filter, FilterType}; -use bytes::Bytes; -use collab_database::fields::Field; -use flowy_derive::ProtoBuf; -use flowy_error::ErrorCode; -use std::convert::TryInto; -use std::sync::Arc; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct FilterPB { @@ -95,7 +98,7 @@ impl TryInto for DeleteFilterPayloadPB { .0; let filter_id = NotEmptyStr::parse(self.filter_id) - .map_err(|_| ErrorCode::UnexpectedEmptyString)? + .map_err(|_| ErrorCode::UnexpectedEmpty)? .0; let filter_type = FilterType { diff --git a/frontend/rust-lib/flowy-database2/src/entities/sort_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/sort_entities.rs index e4479a6874..ceea1fa61b 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/sort_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/sort_entities.rs @@ -178,7 +178,7 @@ impl TryInto for DeleteSortPayloadPB { .0; let sort_id = NotEmptyStr::parse(self.sort_id) - .map_err(|_| ErrorCode::UnexpectedEmptyString)? + .map_err(|_| ErrorCode::UnexpectedEmpty)? .0; let sort_type = SortType { diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 3b59434c50..97ad7c132c 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -19,7 +19,7 @@ use crate::services::database::{DatabaseEditor, MutexDatabase}; pub trait DatabaseUser2: Send + Sync { fn user_id(&self) -> Result; - fn token(&self) -> Result; + fn token(&self) -> Result, FlowyError>; fn collab_db(&self) -> Result, FlowyError>; } @@ -46,7 +46,7 @@ impl DatabaseManager2 { } } - pub async fn initialize(&self, user_id: i64, _token: &str) -> FlowyResult<()> { + pub async fn initialize(&self, user_id: i64) -> FlowyResult<()> { let db = self.user.collab_db()?; *self.user_database.lock() = Some(InnerUserDatabase::new( user_id, @@ -58,8 +58,8 @@ impl DatabaseManager2 { Ok(()) } - pub async fn initialize_with_new_user(&self, user_id: i64, token: &str) -> FlowyResult<()> { - self.initialize(user_id, token).await?; + pub async fn initialize_with_new_user(&self, user_id: i64, _token: &str) -> FlowyResult<()> { + self.initialize(user_id).await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs index a1d86891d8..bdc7974b5b 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs @@ -1,4 +1,3 @@ -use std::any::Any; use std::cmp::Ordering; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; @@ -7,6 +6,7 @@ use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{Cell, RowId}; use flowy_error::FlowyResult; +use lib_infra::box_any::BoxAny; use crate::entities::FieldType; use crate::services::cell::{ @@ -486,41 +486,7 @@ fn get_type_option_transform_handler( } } -pub struct BoxCellData(Box); - -impl BoxCellData { - fn new(value: T) -> Self - where - T: Send + Sync + 'static, - { - Self(Box::new(value)) - } - - fn unbox_or_default(self) -> T - where - T: Default + 'static, - { - match self.0.downcast::() { - Ok(value) => *value, - Err(_) => T::default(), - } - } - - pub(crate) fn unbox_or_none(self) -> Option - where - T: Default + 'static, - { - match self.0.downcast::() { - Ok(value) => Some(*value), - Err(_) => None, - } - } - - #[allow(dead_code)] - fn downcast_ref(&self) -> Option<&T> { - self.0.downcast_ref() - } -} +pub type BoxCellData = BoxAny; pub struct RowSingleCellData { pub row_id: RowId, diff --git a/frontend/rust-lib/flowy-document2/src/manager.rs b/frontend/rust-lib/flowy-document2/src/manager.rs index 0ca89467d3..1024c7610d 100644 --- a/frontend/rust-lib/flowy-document2/src/manager.rs +++ b/frontend/rust-lib/flowy-document2/src/manager.rs @@ -15,7 +15,7 @@ use crate::{ pub trait DocumentUser: Send + Sync { fn user_id(&self) -> Result; - fn token(&self) -> Result; // unused now. + fn token(&self) -> Result, FlowyError>; // unused now. fn collab_db(&self) -> Result, FlowyError>; } diff --git a/frontend/rust-lib/flowy-document2/tests/document/util.rs b/frontend/rust-lib/flowy-document2/tests/document/util.rs index f42277c0f3..3d59b50af5 100644 --- a/frontend/rust-lib/flowy-document2/tests/document/util.rs +++ b/frontend/rust-lib/flowy-document2/tests/document/util.rs @@ -24,8 +24,8 @@ impl DocumentUser for FakeUser { Ok(1) } - fn token(&self) -> Result { - Ok("1".to_string()) + fn token(&self) -> Result, flowy_error::FlowyError> { + Ok(None) } fn collab_db(&self) -> Result, flowy_error::FlowyError> { diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index 4b7abde17a..d238b0d810 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -149,9 +149,6 @@ pub enum ErrorCode { #[error("Invalid date time format")] InvalidDateTimeFormat = 47, - #[error("The input string is empty or contains invalid characters")] - UnexpectedEmptyString = 48, - #[error("Invalid data")] InvalidData = 49, @@ -185,14 +182,23 @@ pub enum ErrorCode { #[error("Http request error")] HttpError = 59, - #[error("Payload should not be empty")] - UnexpectedEmptyPayload = 60, + #[error("The content should not be empty")] + UnexpectedEmpty = 60, #[error("Only the date type can be used in calendar")] UnexpectedCalendarFieldType = 61, #[error("Document Data Invalid")] DocumentDataInvalid = 62, + + #[error("Unsupported auth type")] + UnsupportedAuthType = 63, + + #[error("Invalid auth configuration")] + InvalidAuthConfig = 64, + + #[error("Missing auth field")] + MissingAuthField = 65, } impl ErrorCode { diff --git a/frontend/rust-lib/flowy-error/src/errors.rs b/frontend/rust-lib/flowy-error/src/errors.rs index 46f078e2a8..de8ac4e36a 100644 --- a/frontend/rust-lib/flowy-error/src/errors.rs +++ b/frontend/rust-lib/flowy-error/src/errors.rs @@ -1,9 +1,12 @@ -use crate::code::ErrorCode; -use anyhow::Result; -use flowy_derive::ProtoBuf; use std::fmt::Debug; + +use anyhow::Result; use thiserror::Error; +use flowy_derive::ProtoBuf; + +use crate::code::ErrorCode; + pub type FlowyResult = anyhow::Result; #[derive(Debug, Default, Clone, ProtoBuf, Error)] @@ -26,10 +29,10 @@ macro_rules! static_flowy_error { } impl FlowyError { - pub fn new(code: ErrorCode, msg: &str) -> Self { + pub fn new(code: ErrorCode, msg: T) -> Self { Self { code: code.value() as i32, - msg: msg.to_owned(), + msg: msg.to_string(), } } pub fn context(mut self, error: T) -> Self { @@ -80,7 +83,7 @@ impl FlowyError { static_flowy_error!(out_of_bounds, ErrorCode::OutOfBounds); static_flowy_error!(serde, ErrorCode::Serde); static_flowy_error!(field_record_not_found, ErrorCode::FieldRecordNotFound); - static_flowy_error!(payload_none, ErrorCode::UnexpectedEmptyPayload); + static_flowy_error!(payload_none, ErrorCode::UnexpectedEmpty); static_flowy_error!(http, ErrorCode::HttpError); static_flowy_error!( unexpect_calendar_field_type, @@ -115,3 +118,9 @@ impl std::convert::From for FlowyError { FlowyError::internal().context(e) } } + +impl From for FlowyError { + fn from(e: anyhow::Error) -> Self { + FlowyError::internal().context(e) + } +} diff --git a/frontend/rust-lib/flowy-folder2/Cargo.toml b/frontend/rust-lib/flowy-folder2/Cargo.toml index 4ec7c71791..5c010052a0 100644 --- a/frontend/rust-lib/flowy-folder2/Cargo.toml +++ b/frontend/rust-lib/flowy-folder2/Cargo.toml @@ -26,6 +26,7 @@ chrono = { version = "0.4.24"} strum = "0.21" strum_macros = "0.21" protobuf = {version = "2.28.0"} +uuid = { version = "1.3.3", features = ["v4"] } #flowy-document = { path = "../flowy-document" } [dev-dependencies] diff --git a/frontend/rust-lib/flowy-folder2/src/manager.rs b/frontend/rust-lib/flowy-folder2/src/manager.rs index c3a4a01e64..253168d2af 100644 --- a/frontend/rust-lib/flowy-folder2/src/manager.rs +++ b/frontend/rust-lib/flowy-folder2/src/manager.rs @@ -4,7 +4,6 @@ use std::sync::Arc; use appflowy_integrate::collab_builder::AppFlowyCollabBuilder; use appflowy_integrate::RocksCollabDB; - use collab_folder::core::{ Folder as InnerFolder, FolderContext, TrashChange, TrashChangeReceiver, TrashInfo, TrashRecord, View, ViewChange, ViewChangeReceiver, ViewLayout, Workspace, @@ -30,7 +29,7 @@ use crate::view_ext::{ pub trait FolderUser: Send + Sync { fn user_id(&self) -> Result; - fn token(&self) -> Result; + fn token(&self) -> Result, FlowyError>; fn collab_db(&self) -> Result, FlowyError>; } @@ -92,32 +91,37 @@ impl Folder2Manager { /// Called immediately after the application launched fi the user already sign in/sign up. #[tracing::instrument(level = "trace", skip(self), err)] - pub async fn initialize(&self, user_id: i64) -> FlowyResult<()> { - if let Ok(uid) = self.user.user_id() { - let folder_id = FolderId::new(uid); - - if let Ok(kv_db) = self.user.collab_db() { - let collab = self.collab_builder.build(uid, folder_id.as_ref(), kv_db); - let (view_tx, view_rx) = tokio::sync::broadcast::channel(100); - let (trash_tx, trash_rx) = tokio::sync::broadcast::channel(100); - let folder_context = FolderContext { - view_change_tx: Some(view_tx), - trash_change_tx: Some(trash_tx), - }; - *self.folder.lock() = Some(InnerFolder::get_or_create(collab, folder_context)); - listen_on_trash_change(trash_rx, self.folder.clone()); - listen_on_view_change(view_rx, self.folder.clone()); - } + pub async fn initialize(&self, uid: i64, workspace_id: &str) -> FlowyResult<()> { + if let Ok(collab_db) = self.user.collab_db() { + let collab = self.collab_builder.build(uid, workspace_id, collab_db); + let (view_tx, view_rx) = tokio::sync::broadcast::channel(100); + let (trash_tx, trash_rx) = tokio::sync::broadcast::channel(100); + let folder_context = FolderContext { + view_change_tx: Some(view_tx), + trash_change_tx: Some(trash_tx), + }; + *self.folder.lock() = Some(InnerFolder::get_or_create(collab, folder_context)); + listen_on_trash_change(trash_rx, self.folder.clone()); + listen_on_view_change(view_rx, self.folder.clone()); } Ok(()) } /// Called after the user sign up / sign in - pub async fn initialize_with_new_user(&self, user_id: i64, token: &str) -> FlowyResult<()> { - self.initialize(user_id).await?; - let (folder_data, workspace_pb) = - DefaultFolderBuilder::build(self.user.user_id()?, &self.view_processors).await; + pub async fn initialize_with_new_user( + &self, + user_id: i64, + token: &str, + workspace_id: &str, + ) -> FlowyResult<()> { + self.initialize(user_id, workspace_id).await?; + let (folder_data, workspace_pb) = DefaultFolderBuilder::build( + self.user.user_id()?, + workspace_id.to_string(), + &self.view_processors, + ) + .await; self.with_folder((), |folder| { folder.create_with_data(folder_data); }); @@ -555,7 +559,7 @@ fn notify_parent_view_did_change>( for parent_view_id in parent_view_ids { let parent_view_id = parent_view_id.as_ref(); - // if the view's bid is equal to workspace id. Then it will fetch the current + // if the view's parent id equal to workspace id. Then it will fetch the current // workspace views. Because the the workspace is not a view stored in the views map. if parent_view_id == workspace_id { let repeated_view: RepeatedViewPB = get_workspace_view_pbs(&workspace_id, folder).into(); @@ -585,19 +589,6 @@ fn folder_not_init_error() -> FlowyError { FlowyError::internal().context("Folder not initialized") } -#[derive(Clone)] -pub struct FolderId(String); -impl FolderId { - pub fn new(uid: i64) -> Self { - Self(format!("{}:folder", uid)) - } -} - -impl AsRef for FolderId { - fn as_ref(&self) -> &str { - &self.0 - } -} #[derive(Clone, Default)] pub struct Folder(Arc>>); diff --git a/frontend/rust-lib/flowy-folder2/src/user_default.rs b/frontend/rust-lib/flowy-folder2/src/user_default.rs index 9e1d014907..8681d68297 100644 --- a/frontend/rust-lib/flowy-folder2/src/user_default.rs +++ b/frontend/rust-lib/flowy-folder2/src/user_default.rs @@ -11,10 +11,10 @@ pub struct DefaultFolderBuilder(); impl DefaultFolderBuilder { pub async fn build( uid: i64, + workspace_id: String, view_processors: &ViewDataProcessorMap, ) -> (FolderData, WorkspacePB) { let time = Utc::now().timestamp(); - let workspace_id = gen_workspace_id(); let view_id = gen_view_id(); let child_view_id = gen_view_id(); diff --git a/frontend/rust-lib/flowy-folder2/src/view_ext.rs b/frontend/rust-lib/flowy-folder2/src/view_ext.rs index c8de262cf3..337f4211aa 100644 --- a/frontend/rust-lib/flowy-folder2/src/view_ext.rs +++ b/frontend/rust-lib/flowy-folder2/src/view_ext.rs @@ -4,7 +4,7 @@ use collab_folder::core::{View, ViewLayout}; use flowy_error::FlowyError; use lib_infra::future::FutureResult; use lib_infra::util::timestamp; -use nanoid::nanoid; + use std::collections::HashMap; use std::sync::Arc; @@ -68,5 +68,5 @@ pub fn view_from_create_view_params(params: CreateViewParams, layout: ViewLayout } } pub fn gen_view_id() -> String { - format!("v:{}", nanoid!(10)) + uuid::Uuid::new_v4().to_string() } diff --git a/frontend/rust-lib/flowy-net/Cargo.toml b/frontend/rust-lib/flowy-net/Cargo.toml index 59aad0e8ea..23e52ec7bb 100644 --- a/frontend/rust-lib/flowy-net/Cargo.toml +++ b/frontend/rust-lib/flowy-net/Cargo.toml @@ -9,44 +9,22 @@ edition = "2018" lib-dispatch = { path = "../lib-dispatch" } flowy-error = { path = "../flowy-error", features = ["adaptor_reqwest", "adaptor_server_error"] } flowy-derive = { path = "../../../shared-lib/flowy-derive" } -flowy-folder2 = { path = "../flowy-folder2" } -flowy-document2 = { path = "../flowy-document2" } -flowy-user = { path = "../flowy-user" } -#flowy-document = { path = "../flowy-document" } -lazy_static = "1.4.0" -lib-infra = { path = "../../../shared-lib/lib-infra" } protobuf = {version = "2.28.0"} -lib-ws = { path = "../../../shared-lib/lib-ws" } -bytes = { version = "1.4" } anyhow = "1.0" -tokio = { version = "1.26", features = ["sync"]} -parking_lot = "0.12.1" -strum = "0.21" -strum_macros = "0.21" -tracing = { version = "0.1", features = ["log"] } -dashmap = "5" -async-stream = "0.3.4" -futures-util = "0.3.26" -reqwest = "0.11.14" -hyper = "0.14" -config = { version = "0.10.1", default-features = false, features = ["yaml"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -serde-aux = "1.1.0" -nanoid = "0.4.0" thiserror = "1.0" +bytes = { version = "1.4" } +strum_macros = "0.21" +tracing = { version = "0.1"} [features] http_server = [] dart = [ "flowy-codegen/dart", - "flowy-user/dart", "flowy-error/dart", ] ts = [ "flowy-codegen/ts", - "flowy-user/ts", "flowy-error/ts", ] diff --git a/frontend/rust-lib/flowy-net/src/http_server/mod.rs b/frontend/rust-lib/flowy-net/src/http_server/mod.rs deleted file mode 100644 index d5e9de07ec..0000000000 --- a/frontend/rust-lib/flowy-net/src/http_server/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod self_host; diff --git a/frontend/rust-lib/flowy-net/src/http_server/self_host/mod.rs b/frontend/rust-lib/flowy-net/src/http_server/self_host/mod.rs deleted file mode 100644 index 2cd9f5266c..0000000000 --- a/frontend/rust-lib/flowy-net/src/http_server/self_host/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod configuration; -pub mod user; diff --git a/frontend/rust-lib/flowy-net/src/http_server/self_host/user.rs b/frontend/rust-lib/flowy-net/src/http_server/self_host/user.rs deleted file mode 100644 index a8151d1871..0000000000 --- a/frontend/rust-lib/flowy-net/src/http_server/self_host/user.rs +++ /dev/null @@ -1,134 +0,0 @@ -use flowy_error::FlowyError; -use flowy_user::entities::{ - SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, - UserProfilePB, -}; -use flowy_user::event_map::UserCloudService; -use lib_infra::future::FutureResult; - -use crate::http_server::self_host::configuration::{ClientServerConfiguration, HEADER_TOKEN}; -use crate::request::HttpRequestBuilder; - -pub struct UserHttpCloudService { - config: ClientServerConfiguration, -} -impl UserHttpCloudService { - pub fn new(config: &ClientServerConfiguration) -> Self { - Self { - config: config.clone(), - } - } -} - -impl UserCloudService for UserHttpCloudService { - fn sign_up(&self, params: SignUpParams) -> FutureResult { - let url = self.config.sign_up_url(); - FutureResult::new(async move { - let resp = user_sign_up_request(params, &url).await?; - Ok(resp) - }) - } - - fn sign_in(&self, params: SignInParams) -> FutureResult { - let url = self.config.sign_in_url(); - FutureResult::new(async move { - let resp = user_sign_in_request(params, &url).await?; - Ok(resp) - }) - } - - fn sign_out(&self, token: &str) -> FutureResult<(), FlowyError> { - let token = token.to_owned(); - let url = self.config.sign_out_url(); - FutureResult::new(async move { - let _ = user_sign_out_request(&token, &url).await; - Ok(()) - }) - } - - fn update_user( - &self, - token: &str, - params: UpdateUserProfileParams, - ) -> FutureResult<(), FlowyError> { - let token = token.to_owned(); - let url = self.config.user_profile_url(); - FutureResult::new(async move { - update_user_profile_request(&token, params, &url).await?; - Ok(()) - }) - } - - fn get_user(&self, token: &str) -> FutureResult { - let token = token.to_owned(); - let url = self.config.user_profile_url(); - FutureResult::new(async move { - let profile = get_user_profile_request(&token, &url).await?; - Ok(profile) - }) - } - - fn ws_addr(&self) -> String { - self.config.ws_addr() - } -} - -pub async fn user_sign_up_request( - params: SignUpParams, - url: &str, -) -> Result { - let response = request_builder() - .post(url) - .json(params)? - .json_response() - .await?; - Ok(response) -} - -pub async fn user_sign_in_request( - params: SignInParams, - url: &str, -) -> Result { - let response = request_builder() - .post(url) - .json(params)? - .json_response() - .await?; - Ok(response) -} - -pub async fn user_sign_out_request(token: &str, url: &str) -> Result<(), FlowyError> { - request_builder() - .delete(url) - .header(HEADER_TOKEN, token) - .send() - .await?; - Ok(()) -} - -pub async fn get_user_profile_request(token: &str, url: &str) -> Result { - let user_profile = request_builder() - .get(url) - .header(HEADER_TOKEN, token) - .response() - .await?; - Ok(user_profile) -} - -pub async fn update_user_profile_request( - token: &str, - params: UpdateUserProfileParams, - url: &str, -) -> Result<(), FlowyError> { - request_builder() - .patch(url) - .header(HEADER_TOKEN, token) - .json(params)? - .send() - .await?; - Ok(()) -} - -fn request_builder() -> HttpRequestBuilder { - HttpRequestBuilder::new() -} diff --git a/frontend/rust-lib/flowy-net/src/lib.rs b/frontend/rust-lib/flowy-net/src/lib.rs index d0f148701f..9e3de4ba43 100644 --- a/frontend/rust-lib/flowy-net/src/lib.rs +++ b/frontend/rust-lib/flowy-net/src/lib.rs @@ -1,8 +1,4 @@ pub mod entities; pub mod event_map; mod handlers; -pub mod http_server; -pub mod local_server; pub mod protobuf; -mod request; -mod response; diff --git a/frontend/rust-lib/flowy-net/src/local_server/context.rs b/frontend/rust-lib/flowy-net/src/local_server/context.rs deleted file mode 100644 index e564f37561..0000000000 --- a/frontend/rust-lib/flowy-net/src/local_server/context.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::http_server::self_host::configuration::ClientServerConfiguration; -use crate::local_server::LocalServer; - -pub struct LocalServerContext { - pub local_server: LocalServer, -} - -pub fn build_server(_config: &ClientServerConfiguration) -> LocalServerContext { - let local_server = LocalServer::new(); - LocalServerContext { local_server } -} diff --git a/frontend/rust-lib/flowy-net/src/local_server/mod.rs b/frontend/rust-lib/flowy-net/src/local_server/mod.rs deleted file mode 100644 index ef14ac204a..0000000000 --- a/frontend/rust-lib/flowy-net/src/local_server/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub use context::*; -pub use server::*; - -mod context; -mod server; diff --git a/frontend/rust-lib/flowy-net/src/local_server/server.rs b/frontend/rust-lib/flowy-net/src/local_server/server.rs deleted file mode 100644 index 528155a5cf..0000000000 --- a/frontend/rust-lib/flowy-net/src/local_server/server.rs +++ /dev/null @@ -1,80 +0,0 @@ -use lazy_static::lazy_static; -use parking_lot::{Mutex, RwLock}; -use tokio::sync::mpsc; - -use flowy_error::FlowyError; -use flowy_user::entities::{ - SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, - UserProfilePB, -}; -use flowy_user::event_map::UserCloudService; -use flowy_user::uid::UserIDGenerator; -use lib_infra::future::FutureResult; - -lazy_static! { - static ref ID_GEN: Mutex = Mutex::new(UserIDGenerator::new(1)); -} - -#[derive(Default)] -pub struct LocalServer { - stop_tx: RwLock>>, -} - -impl LocalServer { - pub fn new() -> Self { - Self::default() - } - - pub async fn stop(&self) { - let sender = self.stop_tx.read().clone(); - if let Some(stop_tx) = sender { - let _ = stop_tx.send(()).await; - } - } -} - -impl UserCloudService for LocalServer { - fn sign_up(&self, params: SignUpParams) -> FutureResult { - let uid = ID_GEN.lock().next_id(); - FutureResult::new(async move { - Ok(SignUpResponse { - user_id: uid, - name: params.name, - email: params.email, - token: "".to_string(), - }) - }) - } - - fn sign_in(&self, params: SignInParams) -> FutureResult { - let uid = ID_GEN.lock().next_id(); - FutureResult::new(async move { - Ok(SignInResponse { - user_id: uid, - name: params.name, - email: params.email, - token: "".to_string(), - }) - }) - } - - fn sign_out(&self, _token: &str) -> FutureResult<(), FlowyError> { - FutureResult::new(async { Ok(()) }) - } - - fn update_user( - &self, - _token: &str, - _params: UpdateUserProfileParams, - ) -> FutureResult<(), FlowyError> { - FutureResult::new(async { Ok(()) }) - } - - fn get_user(&self, _token: &str) -> FutureResult { - FutureResult::new(async { Ok(UserProfilePB::default()) }) - } - - fn ws_addr(&self) -> String { - "ws://localhost:8000/ws/".to_owned() - } -} diff --git a/frontend/rust-lib/flowy-server/Cargo.toml b/frontend/rust-lib/flowy-server/Cargo.toml new file mode 100644 index 0000000000..86de966116 --- /dev/null +++ b/frontend/rust-lib/flowy-server/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "flowy-server" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tracing = { version = "0.1" } +futures-util = "0.3.26" +reqwest = "0.11.14" +hyper = "0.14" +config = { version = "0.10.1", default-features = false, features = ["yaml"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde-aux = "4.2.0" +nanoid = "0.4.0" +thiserror = "1.0" +tokio = { version = "1.26", features = ["sync"]} +parking_lot = "0.12" +lazy_static = "1.4.0" +bytes = "1.0.1" +postgrest = "1.0" +tokio-retry = "0.3" +anyhow = "1.0" +uuid = { version = "1.3.3", features = ["v4"] } + +lib-infra = { path = "../../../shared-lib/lib-infra" } +flowy-user = { path = "../flowy-user" } +flowy-error = { path = "../flowy-error" } +flowy-config = { path = "../flowy-config" } + +[dev-dependencies] +uuid = { version = "1.3.3", features = ["v4"] } +dotenv = "0.15.0" diff --git a/frontend/rust-lib/flowy-server/src/lib.rs b/frontend/rust-lib/flowy-server/src/lib.rs new file mode 100644 index 0000000000..a6df90122c --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/lib.rs @@ -0,0 +1,13 @@ +use std::sync::Arc; + +use flowy_user::event_map::UserAuthService; + +pub mod local_server; +mod request; +mod response; +pub mod self_host; +pub mod supabase; + +pub trait AppFlowyServer: Send + Sync + 'static { + fn user_service(&self) -> Arc; +} diff --git a/frontend/rust-lib/flowy-server/src/local_server/mod.rs b/frontend/rust-lib/flowy-server/src/local_server/mod.rs new file mode 100644 index 0000000000..27e033264b --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/local_server/mod.rs @@ -0,0 +1,5 @@ +pub use server::*; + +mod server; +pub(crate) mod uid; +mod user; diff --git a/frontend/rust-lib/flowy-server/src/local_server/server.rs b/frontend/rust-lib/flowy-server/src/local_server/server.rs new file mode 100644 index 0000000000..1074dae092 --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/local_server/server.rs @@ -0,0 +1,34 @@ +use std::sync::Arc; + +use parking_lot::RwLock; +use tokio::sync::mpsc; + +use flowy_user::event_map::UserAuthService; + +use crate::local_server::user::LocalServerUserAuthServiceImpl; +use crate::AppFlowyServer; + +#[derive(Default)] +pub struct LocalServer { + stop_tx: RwLock>>, +} + +impl LocalServer { + pub fn new() -> Self { + // let _config = self_host_server_configuration().unwrap(); + Self::default() + } + + pub async fn stop(&self) { + let sender = self.stop_tx.read().clone(); + if let Some(stop_tx) = sender { + let _ = stop_tx.send(()).await; + } + } +} + +impl AppFlowyServer for LocalServer { + fn user_service(&self) -> Arc { + Arc::new(LocalServerUserAuthServiceImpl()) + } +} diff --git a/frontend/rust-lib/flowy-user/src/uid.rs b/frontend/rust-lib/flowy-server/src/local_server/uid.rs similarity index 100% rename from frontend/rust-lib/flowy-user/src/uid.rs rename to frontend/rust-lib/flowy-server/src/local_server/uid.rs diff --git a/frontend/rust-lib/flowy-server/src/local_server/user.rs b/frontend/rust-lib/flowy-server/src/local_server/user.rs new file mode 100644 index 0000000000..95077bbcc5 --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/local_server/user.rs @@ -0,0 +1,71 @@ +use lazy_static::lazy_static; +use parking_lot::Mutex; + +use flowy_error::FlowyError; +use flowy_user::entities::{ + SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, UserProfile, +}; +use flowy_user::event_map::UserAuthService; +use lib_infra::box_any::BoxAny; +use lib_infra::future::FutureResult; + +use crate::local_server::uid::UserIDGenerator; + +lazy_static! { + static ref ID_GEN: Mutex = Mutex::new(UserIDGenerator::new(1)); +} + +pub(crate) struct LocalServerUserAuthServiceImpl(); + +impl UserAuthService for LocalServerUserAuthServiceImpl { + fn sign_up(&self, params: BoxAny) -> FutureResult { + FutureResult::new(async move { + let params = params.unbox_or_error::()?; + let uid = ID_GEN.lock().next_id(); + let workspace_id = uuid::Uuid::new_v4().to_string(); + Ok(SignUpResponse { + user_id: uid, + name: params.name, + workspace_id, + email: Some(params.email), + token: None, + }) + }) + } + + fn sign_in(&self, params: BoxAny) -> FutureResult { + FutureResult::new(async move { + let uid = ID_GEN.lock().next_id(); + let params = params.unbox_or_error::()?; + let workspace_id = uuid::Uuid::new_v4().to_string(); + Ok(SignInResponse { + user_id: uid, + name: params.name, + workspace_id, + email: Some(params.email), + token: None, + }) + }) + } + + fn sign_out(&self, _token: Option) -> FutureResult<(), FlowyError> { + FutureResult::new(async { Ok(()) }) + } + + fn update_user( + &self, + _uid: i64, + _token: &Option, + _params: UpdateUserProfileParams, + ) -> FutureResult<(), FlowyError> { + FutureResult::new(async { Ok(()) }) + } + + fn get_user_profile( + &self, + _token: Option, + _uid: i64, + ) -> FutureResult, FlowyError> { + FutureResult::new(async { Ok(None) }) + } +} diff --git a/frontend/rust-lib/flowy-net/src/request.rs b/frontend/rust-lib/flowy-server/src/request.rs similarity index 67% rename from frontend/rust-lib/flowy-net/src/request.rs rename to frontend/rust-lib/flowy-server/src/request.rs index 8facdf9d65..30a05d187f 100644 --- a/frontend/rust-lib/flowy-net/src/request.rs +++ b/frontend/rust-lib/flowy-server/src/request.rs @@ -1,19 +1,14 @@ -use std::{ - convert::{TryFrom, TryInto}, - sync::Arc, - time::Duration, -}; +use std::{sync::Arc, time::Duration}; use bytes::Bytes; use hyper::http; -use protobuf::ProtobufError; use reqwest::{header::HeaderMap, Client, Method, Response}; use tokio::sync::oneshot; -use flowy_error::FlowyError; +use flowy_error::{internal_error, FlowyError}; -use crate::http_server::self_host::configuration::HEADER_TOKEN; use crate::response::HttpResponse; +use crate::self_host::configuration::HEADER_TOKEN; pub trait ResponseMiddleware { fn receive_response(&self, token: &Option, response: &HttpResponse); @@ -84,20 +79,11 @@ impl HttpRequestBuilder { self } - #[allow(dead_code)] - pub fn protobuf(self, body: T) -> Result - where - T: TryInto, - { - let body: Bytes = body.try_into()?; - self.bytes(body) - } - pub fn json(self, body: T) -> Result where T: serde::Serialize, { - let bytes = Bytes::from(serde_json::to_vec(&body)?); + let bytes = Bytes::from(serde_json::to_vec(&body).map_err(internal_error)?); self.bytes(bytes) } @@ -112,60 +98,13 @@ impl HttpRequestBuilder { } pub async fn response(self) -> Result - where - T: TryFrom, - { - let builder = self.inner_send().await?; - match builder.response { - None => Err(unexpected_empty_payload(&builder.url)), - Some(data) => Ok(T::try_from(data)?), - } - } - - pub async fn json_response(self) -> Result where T: serde::de::DeserializeOwned, { let builder = self.inner_send().await?; match builder.response { None => Err(unexpected_empty_payload(&builder.url)), - Some(data) => Ok(serde_json::from_slice(&data)?), - } - } - - #[allow(dead_code)] - pub async fn option_protobuf_response(self) -> Result, FlowyError> - where - T: TryFrom, - { - let result = self.inner_send().await; - match result { - Ok(builder) => match builder.response { - None => Err(unexpected_empty_payload(&builder.url)), - Some(data) => Ok(Some(T::try_from(data)?)), - }, - Err(error) => match error.is_record_not_found() { - true => Ok(None), - false => Err(error), - }, - } - } - - #[allow(dead_code)] - pub async fn option_json_response(self) -> Result, FlowyError> - where - T: serde::de::DeserializeOwned + 'static, - { - let result = self.inner_send().await; - match result { - Ok(builder) => match builder.response { - None => Err(unexpected_empty_payload(&builder.url)), - Some(data) => Ok(Some(serde_json::from_slice(&data)?)), - }, - Err(error) => match error.is_record_not_found() { - true => Ok(None), - false => Err(error), - }, + Some(data) => serde_json::from_slice(&data).map_err(internal_error), } } @@ -198,10 +137,7 @@ impl HttpRequestBuilder { let _ = tx.send(response); }); - let response = rx.await.map_err(|e| { - let mag = format!("Receive http response channel error: {}", e); - FlowyError::internal().context(mag) - })??; + let response = rx.await.map_err(internal_error)?.map_err(internal_error)?; tracing::trace!("Http Response: {:?}", response); let flowy_response = flowy_response_from(response).await?; let token = self.token(); @@ -224,16 +160,16 @@ fn unexpected_empty_payload(url: &str) -> FlowyError { } async fn flowy_response_from(original: Response) -> Result { - let bytes = original.bytes().await?; - let response: HttpResponse = serde_json::from_slice(&bytes)?; + let bytes = original.bytes().await.map_err(internal_error)?; + let response: HttpResponse = serde_json::from_slice(&bytes).map_err(internal_error)?; Ok(response) } #[allow(dead_code)] async fn get_response_data(original: Response) -> Result { if original.status() == http::StatusCode::OK { - let bytes = original.bytes().await?; - let response: HttpResponse = serde_json::from_slice(&bytes)?; + let bytes = original.bytes().await.map_err(internal_error)?; + let response: HttpResponse = serde_json::from_slice(&bytes).map_err(internal_error)?; match response.error { None => Ok(response.data), Some(error) => Err(FlowyError::new(error.code, &error.msg)), diff --git a/frontend/rust-lib/flowy-net/src/response.rs b/frontend/rust-lib/flowy-server/src/response.rs similarity index 100% rename from frontend/rust-lib/flowy-net/src/response.rs rename to frontend/rust-lib/flowy-server/src/response.rs diff --git a/frontend/rust-lib/flowy-net/src/http_server/self_host/configuration.rs b/frontend/rust-lib/flowy-server/src/self_host/configuration.rs similarity index 94% rename from frontend/rust-lib/flowy-net/src/http_server/self_host/configuration.rs rename to frontend/rust-lib/flowy-server/src/self_host/configuration.rs index 54909825fd..5d2b89a8ab 100644 --- a/frontend/rust-lib/flowy-net/src/http_server/self_host/configuration.rs +++ b/frontend/rust-lib/flowy-server/src/self_host/configuration.rs @@ -6,7 +6,7 @@ use serde_aux::field_attributes::deserialize_number_from_string; pub const HEADER_TOKEN: &str = "token"; #[derive(serde::Deserialize, Clone, Debug)] -pub struct ClientServerConfiguration { +pub struct SelfHostedConfiguration { #[serde(deserialize_with = "deserialize_number_from_string")] pub port: u16, pub host: String, @@ -14,7 +14,7 @@ pub struct ClientServerConfiguration { pub ws_scheme: String, } -pub fn get_client_server_configuration() -> Result { +pub fn self_host_server_configuration() -> Result { let mut settings = config::Config::default(); let base = include_str!("./configuration/base.yaml"); settings.merge(config::File::from_str(base, FileFormat::Yaml).required(true))?; @@ -33,7 +33,7 @@ pub fn get_client_server_configuration() -> Result Self { + Self { config } + } +} + +impl AppFlowyServer for SelfHostServer { + fn user_service(&self) -> Arc { + Arc::new(SelfHostedUserAuthServiceImpl::new(self.config.clone())) + } +} diff --git a/frontend/rust-lib/flowy-server/src/self_host/user.rs b/frontend/rust-lib/flowy-server/src/self_host/user.rs new file mode 100644 index 0000000000..74148403b4 --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/self_host/user.rs @@ -0,0 +1,156 @@ +use flowy_error::{ErrorCode, FlowyError}; +use flowy_user::entities::{ + SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, UserProfile, +}; +use flowy_user::event_map::UserAuthService; +use lib_infra::box_any::BoxAny; +use lib_infra::future::FutureResult; + +use crate::request::HttpRequestBuilder; +use crate::self_host::configuration::{SelfHostedConfiguration, HEADER_TOKEN}; + +pub(crate) struct SelfHostedUserAuthServiceImpl { + config: SelfHostedConfiguration, +} + +impl SelfHostedUserAuthServiceImpl { + pub(crate) fn new(config: SelfHostedConfiguration) -> Self { + Self { config } + } +} + +impl UserAuthService for SelfHostedUserAuthServiceImpl { + fn sign_up(&self, params: BoxAny) -> FutureResult { + let url = self.config.sign_up_url(); + FutureResult::new(async move { + let params = params.unbox_or_error::()?; + let resp = user_sign_up_request(params, &url).await?; + Ok(resp) + }) + } + + fn sign_in(&self, params: BoxAny) -> FutureResult { + let url = self.config.sign_in_url(); + FutureResult::new(async move { + let params = params.unbox_or_error::()?; + let resp = user_sign_in_request(params, &url).await?; + Ok(resp) + }) + } + + fn sign_out(&self, token: Option) -> FutureResult<(), FlowyError> { + match token { + None => FutureResult::new(async { + Err(FlowyError::new( + ErrorCode::InvalidData, + "Token should not be empty", + )) + }), + Some(token) => { + let token = token; + let url = self.config.sign_out_url(); + FutureResult::new(async move { + let _ = user_sign_out_request(&token, &url).await; + Ok(()) + }) + }, + } + } + + fn update_user( + &self, + _uid: i64, + token: &Option, + params: UpdateUserProfileParams, + ) -> FutureResult<(), FlowyError> { + match token { + None => FutureResult::new(async { + Err(FlowyError::new( + ErrorCode::InvalidData, + "Token should not be empty", + )) + }), + Some(token) => { + let token = token.to_owned(); + let url = self.config.user_profile_url(); + FutureResult::new(async move { + update_user_profile_request(&token, params, &url).await?; + Ok(()) + }) + }, + } + } + + fn get_user_profile( + &self, + token: Option, + _uid: i64, + ) -> FutureResult, FlowyError> { + let token = token; + let url = self.config.user_profile_url(); + FutureResult::new(async move { + match token { + None => Err(FlowyError::new( + ErrorCode::UnexpectedEmpty, + "Token should not be empty", + )), + Some(token) => { + let profile = get_user_profile_request(&token, &url).await?; + Ok(Some(profile)) + }, + } + }) + } +} + +pub async fn user_sign_up_request( + params: SignUpParams, + url: &str, +) -> Result { + let response = request_builder().post(url).json(params)?.response().await?; + Ok(response) +} + +pub async fn user_sign_in_request( + params: SignInParams, + url: &str, +) -> Result { + let response = request_builder().post(url).json(params)?.response().await?; + Ok(response) +} + +pub async fn user_sign_out_request(token: &str, url: &str) -> Result<(), FlowyError> { + request_builder() + .delete(url) + .header(HEADER_TOKEN, token) + .send() + .await?; + Ok(()) +} + +pub async fn get_user_profile_request(token: &str, url: &str) -> Result { + let user_profile = request_builder() + .get(url) + .header(HEADER_TOKEN, token) + .response() + .await?; + Ok(user_profile) +} + +pub async fn update_user_profile_request( + token: &str, + params: UpdateUserProfileParams, + url: &str, +) -> Result<(), FlowyError> { + request_builder() + .patch(url) + .header(HEADER_TOKEN, token) + .json(params)? + .send() + .await?; + Ok(()) +} + +fn request_builder() -> HttpRequestBuilder { + HttpRequestBuilder::new() +} diff --git a/frontend/rust-lib/flowy-server/src/supabase/mod.rs b/frontend/rust-lib/flowy-server/src/supabase/mod.rs new file mode 100644 index 0000000000..6870d3f721 --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/supabase/mod.rs @@ -0,0 +1,7 @@ +pub use server::*; + +mod request; +mod response; +mod retry; +mod server; +pub mod user; diff --git a/frontend/rust-lib/flowy-server/src/supabase/request.rs b/frontend/rust-lib/flowy-server/src/supabase/request.rs new file mode 100644 index 0000000000..5ff3a1747c --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/supabase/request.rs @@ -0,0 +1,212 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use postgrest::Postgrest; +use serde_json::json; + +use flowy_error::{ErrorCode, FlowyError}; +use flowy_user::entities::UpdateUserProfileParams; +use lib_infra::box_any::BoxAny; + +use crate::supabase::response::{ + InsertResponse, PostgrestError, UserProfile, UserProfileList, UserWorkspace, UserWorkspaceList, +}; +use crate::supabase::user::{USER_PROFILE_TABLE, USER_TABLE, USER_WORKSPACE_TABLE}; + +const USER_ID: &str = "uid"; +const USER_UUID: &str = "uuid"; + +pub(crate) async fn create_user_with_uuid( + postgrest: Arc, + uuid: String, +) -> Result { + let insert = format!("{{\"{}\": \"{}\"}}", USER_UUID, &uuid); + + // Create a new user with uuid. + let resp = postgrest + .from(USER_TABLE) + .insert(insert) + .execute() + .await + .map_err(|e| FlowyError::new(ErrorCode::HttpError, e))?; + + // Check if the request is successful. + // If the request is successful, get the user id from the response. Otherwise, try to get the + // user id with uuid if the error is unique violation, + let is_success = resp.status().is_success(); + let content = resp + .text() + .await + .map_err(|e| FlowyError::new(ErrorCode::UnexpectedEmpty, e))?; + + if is_success { + let record = serde_json::from_str::(&content) + .map_err(|e| FlowyError::serde().context(e))? + .first_or_error()?; + + match get_user_workspace_with_uid(postgrest, record.uid).await { + Ok(Some(user)) => Ok(user), + _ => Err(FlowyError::new( + ErrorCode::Internal, + "Failed to get user workspace", + )), + } + } else { + let err = serde_json::from_str::(&content) + .map_err(|e| FlowyError::serde().context(e))?; + + // If there is a unique violation, try to get the user id with uuid. At this point, the user + // should exist. + if err.is_unique_violation() { + match get_user_workspace_with_uuid(postgrest, uuid).await { + Ok(Some(user)) => Ok(user), + _ => Err(FlowyError::new( + ErrorCode::Internal, + "Failed to get user workspace", + )), + } + } else { + Err(FlowyError::new(ErrorCode::Internal, err)) + } + } +} + +#[allow(dead_code)] +pub(crate) async fn get_user_id_with_uuid( + postgrest: Arc, + uuid: String, +) -> Result, FlowyError> { + let resp = postgrest + .from(USER_TABLE) + .eq(USER_UUID, uuid) + .select("*") + .execute() + .await + .map_err(|e| FlowyError::new(ErrorCode::HttpError, e))?; + + let is_success = resp.status().is_success(); + if !is_success { + return Err(FlowyError::new( + ErrorCode::Internal, + "Failed to get user id with uuid", + )); + } + + let content = resp + .text() + .await + .map_err(|e| FlowyError::new(ErrorCode::UnexpectedEmpty, e))?; + let resp = serde_json::from_str::(&content).unwrap(); + if resp.0.is_empty() { + Ok(None) + } else { + Ok(Some(resp.0[0].uid)) + } +} + +pub(crate) fn uuid_from_box_any(any: BoxAny) -> Result { + let map: HashMap = any.unbox_or_error()?; + let uuid = map + .get(USER_UUID) + .ok_or_else(|| FlowyError::new(ErrorCode::MissingAuthField, "Missing uuid field"))?; + Ok(uuid.to_string()) +} + +#[allow(dead_code)] +pub(crate) async fn get_user_profile( + postgrest: Arc, + uid: i64, +) -> Result, FlowyError> { + let resp = postgrest + .from(USER_PROFILE_TABLE) + .eq(USER_ID, uid.to_string()) + .select("*") + .execute() + .await + .map_err(|e| FlowyError::new(ErrorCode::HttpError, e))?; + + let content = resp + .text() + .await + .map_err(|e| FlowyError::new(ErrorCode::UnexpectedEmpty, e))?; + let resp = serde_json::from_str::(&content) + .map_err(|_e| FlowyError::new(ErrorCode::Serde, "Deserialize UserProfileList failed"))?; + Ok(resp.0.first().cloned()) +} + +pub(crate) async fn get_user_workspace_with_uuid( + postgrest: Arc, + uuid: String, +) -> Result, FlowyError> { + let resp = postgrest + .from(USER_WORKSPACE_TABLE) + .eq(USER_UUID, uuid) + .select("*") + .execute() + .await + .map_err(|e| FlowyError::new(ErrorCode::HttpError, e))?; + + let content = resp + .text() + .await + .map_err(|e| FlowyError::new(ErrorCode::UnexpectedEmpty, e))?; + let resp = serde_json::from_str::(&content) + .map_err(|_e| FlowyError::new(ErrorCode::Serde, "Deserialize UserWorkspaceList failed"))?; + Ok(resp.0.first().cloned()) +} + +pub(crate) async fn get_user_workspace_with_uid( + postgrest: Arc, + uid: i64, +) -> Result, FlowyError> { + let resp = postgrest + .from(USER_WORKSPACE_TABLE) + .eq(USER_ID, uid.to_string()) + .select("*") + .execute() + .await + .map_err(|e| FlowyError::new(ErrorCode::HttpError, e))?; + + let content = resp + .text() + .await + .map_err(|e| FlowyError::new(ErrorCode::UnexpectedEmpty, e))?; + let resp = serde_json::from_str::(&content) + .map_err(|_e| FlowyError::new(ErrorCode::Serde, "Deserialize UserWorkspaceList failed"))?; + Ok(resp.0.first().cloned()) +} + +#[allow(dead_code)] +pub(crate) async fn update_user_profile( + postgrest: Arc, + params: UpdateUserProfileParams, +) -> Result, FlowyError> { + if params.is_empty() { + return Err(FlowyError::new( + ErrorCode::UnexpectedEmpty, + "Empty update params", + )); + } + + let mut update = serde_json::Map::new(); + if let Some(name) = params.name { + update.insert("name".to_string(), json!(name)); + } + let update_str = serde_json::to_string(&update).unwrap(); + let resp = postgrest + .from(USER_PROFILE_TABLE) + .eq(USER_ID, params.id.to_string()) + .update(update_str) + .execute() + .await + .map_err(|e| FlowyError::new(ErrorCode::HttpError, e))?; + + let content = resp + .text() + .await + .map_err(|e| FlowyError::new(ErrorCode::UnexpectedEmpty, e))?; + + let resp = serde_json::from_str::(&content) + .map_err(|_e| FlowyError::new(ErrorCode::Serde, "Deserialize UserProfileList failed"))?; + Ok(resp.0.first().cloned()) +} diff --git a/frontend/rust-lib/flowy-server/src/supabase/response.rs b/frontend/rust-lib/flowy-server/src/supabase/response.rs new file mode 100644 index 0000000000..15cb821345 --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/supabase/response.rs @@ -0,0 +1,90 @@ +use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::Value; +use thiserror::Error; + +use flowy_error::{ErrorCode, FlowyError}; + +#[derive(Debug, Error, Serialize, Deserialize)] +#[error( + "PostgrestException(message: {message}, code: {code:?}, details: {details:?}, hint: {hint:?})" +)] +pub struct PostgrestError { + message: String, + code: String, + details: Value, + hint: Option, +} + +impl PostgrestError { + /// Error code 23505 is a PostgreSQL error code. It signifies a "unique_violation", which occurs + /// when a certain unique constraint has been violated. + pub fn is_unique_violation(&self) -> bool { + self.code == "23505" + } +} + +#[allow(dead_code)] +#[derive(Debug, Deserialize)] +pub struct PostgrestResponse { + data: Option, + status: i32, + count: Option, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct InsertResponse(pub Vec); + +impl InsertResponse { + pub(crate) fn first_or_error(&self) -> Result { + if self.0.is_empty() { + Err(FlowyError::new( + ErrorCode::UnexpectedEmpty, + "Insert response contains no records", + )) + } else { + Ok(self.0[0].clone()) + } + } +} + +#[derive(Debug, Deserialize, Clone)] +pub(crate) struct InsertRecord { + pub(crate) uid: i64, + #[allow(dead_code)] + pub(crate) uuid: String, +} + +#[allow(dead_code)] +#[derive(Debug, Deserialize, Clone)] +pub(crate) struct UserProfile { + pub uid: i64, + #[serde(deserialize_with = "deserialize_null_or_default")] + pub name: String, + #[serde(deserialize_with = "deserialize_null_or_default")] + pub email: String, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct UserProfileList(pub Vec); + +#[derive(Debug, Deserialize, Clone)] +pub(crate) struct UserWorkspace { + pub uid: i64, + #[serde(deserialize_with = "deserialize_null_or_default")] + pub name: String, + pub workspace_id: String, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct UserWorkspaceList(pub Vec); + +/// Handles the case where the value is null. If the value is null, return the default value of the +/// type. Otherwise, deserialize the value. +fn deserialize_null_or_default<'de, D, T>(deserializer: D) -> Result +where + T: Default + Deserialize<'de>, + D: Deserializer<'de>, +{ + let opt = Option::deserialize(deserializer)?; + Ok(opt.unwrap_or_default()) +} diff --git a/frontend/rust-lib/flowy-server/src/supabase/retry.rs b/frontend/rust-lib/flowy-server/src/supabase/retry.rs new file mode 100644 index 0000000000..25a0598862 --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/supabase/retry.rs @@ -0,0 +1,11 @@ +// pub(crate) struct SupabaseRetryAction {} +// +// impl Action for SupabaseRetryAction { +// type Future = (); +// type Item = (); +// type Error = (); +// +// fn run(&mut self) -> Self::Future { +// todo!() +// } +// } diff --git a/frontend/rust-lib/flowy-server/src/supabase/server.rs b/frontend/rust-lib/flowy-server/src/supabase/server.rs new file mode 100644 index 0000000000..e4238ddd9a --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/supabase/server.rs @@ -0,0 +1,56 @@ +use std::sync::Arc; + +use postgrest::Postgrest; + +use flowy_config::entities::{SUPABASE_JWT_SECRET, SUPABASE_KEY, SUPABASE_URL}; +use flowy_error::{ErrorCode, FlowyError}; +use flowy_user::event_map::UserAuthService; + +use crate::supabase::user::PostgrestUserAuthServiceImpl; +use crate::AppFlowyServer; + +#[derive(Debug)] +pub struct SupabaseConfiguration { + /// The url of the supabase server. + pub url: String, + /// The key of the supabase server. + pub key: String, + /// The secret used to sign the JWT tokens. + pub jwt_secret: String, +} + +impl SupabaseConfiguration { + pub fn from_env() -> Result { + Ok(Self { + url: std::env::var(SUPABASE_URL) + .map_err(|_| FlowyError::new(ErrorCode::InvalidAuthConfig, "Missing SUPABASE_URL"))?, + key: std::env::var(SUPABASE_KEY) + .map_err(|_| FlowyError::new(ErrorCode::InvalidAuthConfig, "Missing SUPABASE_KEY"))?, + jwt_secret: std::env::var(SUPABASE_JWT_SECRET).map_err(|_| { + FlowyError::new(ErrorCode::InvalidAuthConfig, "Missing SUPABASE_JWT_SECRET") + })?, + }) + } +} + +pub struct SupabaseServer { + pub postgres: Arc, +} + +impl SupabaseServer { + pub fn new(config: SupabaseConfiguration) -> Self { + let url = format!("{}/rest/v1/", config.url); + let auth = format!("Bearer {}", config.key); + let postgrest = Postgrest::new(url) + .insert_header("apikey", config.key) + .insert_header("Authorization", auth); + let postgres = Arc::new(postgrest); + Self { postgres } + } +} + +impl AppFlowyServer for SupabaseServer { + fn user_service(&self) -> Arc { + Arc::new(PostgrestUserAuthServiceImpl::new(self.postgres.clone())) + } +} diff --git a/frontend/rust-lib/flowy-server/src/supabase/user.rs b/frontend/rust-lib/flowy-server/src/supabase/user.rs new file mode 100644 index 0000000000..23c1b07018 --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/supabase/user.rs @@ -0,0 +1,167 @@ +use std::sync::Arc; + +use postgrest::Postgrest; + +use flowy_error::FlowyError; +use flowy_user::entities::{SignInResponse, SignUpResponse, UpdateUserProfileParams, UserProfile}; +use flowy_user::event_map::UserAuthService; +use lib_infra::box_any::BoxAny; +use lib_infra::future::FutureResult; + +use crate::supabase::request::*; + +pub(crate) const USER_TABLE: &str = "af_user"; +pub(crate) const USER_PROFILE_TABLE: &str = "af_user_profile"; +pub(crate) const USER_WORKSPACE_TABLE: &str = "af_user_workspace_view"; +pub(crate) struct PostgrestUserAuthServiceImpl { + postgrest: Arc, +} + +impl PostgrestUserAuthServiceImpl { + pub(crate) fn new(postgrest: Arc) -> Self { + Self { postgrest } + } +} + +impl UserAuthService for PostgrestUserAuthServiceImpl { + fn sign_up(&self, params: BoxAny) -> FutureResult { + let postgrest = self.postgrest.clone(); + FutureResult::new(async move { + let uuid = uuid_from_box_any(params)?; + let user = create_user_with_uuid(postgrest, uuid).await?; + Ok(SignUpResponse { + user_id: user.uid, + workspace_id: user.workspace_id, + ..Default::default() + }) + }) + } + + fn sign_in(&self, params: BoxAny) -> FutureResult { + let postgrest = self.postgrest.clone(); + FutureResult::new(async move { + let uuid = uuid_from_box_any(params)?; + match get_user_workspace_with_uuid(postgrest, uuid).await? { + None => Err(FlowyError::user_not_exist()), + Some(user) => Ok(SignInResponse { + user_id: user.uid, + workspace_id: user.workspace_id, + ..Default::default() + }), + } + }) + } + + fn sign_out(&self, _token: Option) -> FutureResult<(), FlowyError> { + FutureResult::new(async { Ok(()) }) + } + + fn update_user( + &self, + _uid: i64, + _token: &Option, + params: UpdateUserProfileParams, + ) -> FutureResult<(), FlowyError> { + let postgrest = self.postgrest.clone(); + FutureResult::new(async move { + let _ = update_user_profile(postgrest, params).await?; + Ok(()) + }) + } + + fn get_user_profile( + &self, + _token: Option, + uid: i64, + ) -> FutureResult, FlowyError> { + let postgrest = self.postgrest.clone(); + FutureResult::new(async move { + let profile = get_user_workspace_with_uid(postgrest, uid) + .await? + .map(|user_workspace| UserProfile { + id: user_workspace.uid, + email: "".to_string(), + name: user_workspace.name, + token: "".to_string(), + icon_url: "".to_string(), + openai_key: "".to_string(), + workspace_id: user_workspace.workspace_id, + }); + Ok(profile) + }) + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use dotenv::dotenv; + + use flowy_user::entities::UpdateUserProfileParams; + + use crate::supabase::request::{get_user_profile, get_user_workspace_with_uid}; + use crate::supabase::user::{create_user_with_uuid, get_user_id_with_uuid, update_user_profile}; + use crate::supabase::{SupabaseConfiguration, SupabaseServer}; + + #[tokio::test] + async fn read_user_table_test() { + dotenv().ok(); + if let Ok(config) = SupabaseConfiguration::from_env() { + let server = Arc::new(SupabaseServer::new(config)); + let uid = get_user_id_with_uuid( + server.postgres.clone(), + "c8c674fc-506f-403c-b052-209e09817f6e".to_string(), + ) + .await + .unwrap(); + println!("uid: {:?}", uid); + } + } + + #[tokio::test] + async fn insert_user_table_test() { + dotenv().ok(); + if let Ok(config) = SupabaseConfiguration::from_env() { + let server = Arc::new(SupabaseServer::new(config)); + let uuid = uuid::Uuid::new_v4(); + // let uuid = "c8c674fc-506f-403c-b052-209e09817f6e"; + let uid = create_user_with_uuid(server.postgres.clone(), uuid.to_string()).await; + println!("uid: {:?}", uid); + } + } + + #[tokio::test] + async fn create_and_then_update_user_profile_test() { + dotenv().ok(); + if let Ok(config) = SupabaseConfiguration::from_env() { + let server = Arc::new(SupabaseServer::new(config)); + let uuid = uuid::Uuid::new_v4(); + let uid = create_user_with_uuid(server.postgres.clone(), uuid.to_string()) + .await + .unwrap() + .uid; + let params = UpdateUserProfileParams { + id: uid, + name: Some("nathan".to_string()), + ..Default::default() + }; + let result = update_user_profile(server.postgres.clone(), params) + .await + .unwrap(); + println!("result: {:?}", result); + + let result = get_user_profile(server.postgres.clone(), uid) + .await + .unwrap() + .unwrap(); + assert_eq!(result.name, "nathan".to_string()); + + let result = get_user_workspace_with_uid(server.postgres.clone(), uid) + .await + .unwrap() + .unwrap(); + assert!(!result.workspace_id.is_empty()); + } + } +} diff --git a/frontend/rust-lib/flowy-test/Cargo.toml b/frontend/rust-lib/flowy-test/Cargo.toml index 00a3e4a146..f9407dcb3a 100644 --- a/frontend/rust-lib/flowy-test/Cargo.toml +++ b/frontend/rust-lib/flowy-test/Cargo.toml @@ -14,6 +14,7 @@ flowy-folder2 = { path = "../flowy-folder2", features = ["test_helper"] } lib-dispatch = { path = "../lib-dispatch" } lib-ot = { path = "../../../shared-lib/lib-ot" } lib-infra = { path = "../../../shared-lib/lib-infra" } +flowy-server = { path = "../flowy-server" } serde = { version = "1.0", features = ["derive"] } serde_json = {version = "1.0"} diff --git a/frontend/rust-lib/flowy-test/src/helper.rs b/frontend/rust-lib/flowy-test/src/helper.rs index 3024af529b..4a4566738b 100644 --- a/frontend/rust-lib/flowy-test/src/helper.rs +++ b/frontend/rust-lib/flowy-test/src/helper.rs @@ -1,8 +1,10 @@ -use crate::prelude::*; +use std::sync::Arc; + use flowy_folder2::entities::{ CreateViewPayloadPB, CreateWorkspacePayloadPB, ViewLayoutPB, ViewPB, WorkspaceIdPB, WorkspacePB, }; use flowy_folder2::event_map::FolderEvent::{CreateView, CreateWorkspace, OpenWorkspace}; +use flowy_user::entities::AuthTypePB; use flowy_user::{ entities::{SignInPayloadPB, SignUpPayloadPB, UserProfilePB}, errors::FlowyError, @@ -10,7 +12,7 @@ use flowy_user::{ }; use lib_dispatch::prelude::{AFPluginDispatcher, AFPluginRequest, ToBytes}; -use std::sync::Arc; +use crate::prelude::*; pub struct ViewTest { pub sdk: FlowySDKTest, @@ -142,6 +144,7 @@ pub fn sign_up(dispatch: Arc) -> SignUpContext { email: random_email(), name: "app flowy".to_string(), password: password.clone(), + auth_type: AuthTypePB::Local, } .into_bytes() .unwrap(); @@ -165,6 +168,7 @@ pub async fn async_sign_up(dispatch: Arc) -> SignUpContext { email, name: "app flowy".to_string(), password: password.clone(), + auth_type: AuthTypePB::Local, } .into_bytes() .unwrap(); @@ -194,6 +198,7 @@ fn sign_in(dispatch: Arc) -> UserProfilePB { email: login_email(), password: login_password(), name: "rust".to_owned(), + auth_type: AuthTypePB::Local, } .into_bytes() .unwrap(); diff --git a/frontend/rust-lib/flowy-test/src/lib.rs b/frontend/rust-lib/flowy-test/src/lib.rs index 28c1a4a16e..b023aa434e 100644 --- a/frontend/rust-lib/flowy-test/src/lib.rs +++ b/frontend/rust-lib/flowy-test/src/lib.rs @@ -2,7 +2,6 @@ use nanoid::nanoid; use std::env::temp_dir; use flowy_core::{AppFlowyCore, AppFlowyCoreConfig}; -use flowy_net::http_server::self_host::configuration::get_client_server_configuration; use flowy_user::entities::UserProfilePB; use crate::helper::*; @@ -37,9 +36,8 @@ impl std::default::Default for FlowySDKTest { impl FlowySDKTest { pub fn new() -> Self { - let server_config = get_client_server_configuration().unwrap(); - let config = AppFlowyCoreConfig::new(temp_dir().to_str().unwrap(), nanoid!(6), server_config) - .log_filter("info", vec![]); + let config = + AppFlowyCoreConfig::new(temp_dir().to_str().unwrap(), nanoid!(6)).log_filter("info", vec![]); let sdk = std::thread::spawn(|| AppFlowyCore::new(config)) .join() .unwrap(); diff --git a/frontend/rust-lib/flowy-user/Cargo.toml b/frontend/rust-lib/flowy-user/Cargo.toml index cbeef480cf..1a2dee82e8 100644 --- a/frontend/rust-lib/flowy-user/Cargo.toml +++ b/frontend/rust-lib/flowy-user/Cargo.toml @@ -18,6 +18,7 @@ tracing = { version = "0.1", features = ["log"] } bytes = "1.4" serde = { version = "1.0", features = ["derive"] } serde_json = {version = "1.0"} +serde_repr = "0.1" log = "0.4.17" protobuf = {version = "2.28.0"} lazy_static = "1.4.0" diff --git a/frontend/rust-lib/flowy-user/src/entities/auth.rs b/frontend/rust-lib/flowy-user/src/entities/auth.rs index fd1f3b7dcb..d663d10dcc 100644 --- a/frontend/rust-lib/flowy-user/src/entities/auth.rs +++ b/frontend/rust-lib/flowy-user/src/entities/auth.rs @@ -1,11 +1,13 @@ +use std::collections::HashMap; use std::convert::TryInto; use serde::{Deserialize, Serialize}; -use flowy_derive::ProtoBuf; +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use crate::entities::parser::*; use crate::errors::ErrorCode; +use crate::services::AuthType; #[derive(ProtoBuf, Default)] pub struct SignInPayloadPB { @@ -17,6 +19,9 @@ pub struct SignInPayloadPB { #[pb(index = 3)] pub name: String, + + #[pb(index = 4)] + pub auth_type: AuthTypePB, } impl TryInto for SignInPayloadPB { @@ -30,6 +35,7 @@ impl TryInto for SignInPayloadPB { email: email.0, password: password.0, name: self.name, + auth_type: self.auth_type.into(), }) } } @@ -44,6 +50,9 @@ pub struct SignUpPayloadPB { #[pb(index = 3)] pub password: String, + + #[pb(index = 4)] + pub auth_type: AuthTypePB, } impl TryInto for SignUpPayloadPB { type Error = ErrorCode; @@ -57,6 +66,7 @@ impl TryInto for SignUpPayloadPB { email: email.0, name: name.0, password: password.0, + auth_type: self.auth_type.into(), }) } } @@ -66,14 +76,16 @@ pub struct SignInParams { pub email: String, pub password: String, pub name: String, + pub auth_type: AuthType, } #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct SignInResponse { pub user_id: i64, pub name: String, - pub email: String, - pub token: String, + pub workspace_id: String, + pub email: Option, + pub token: Option, } #[derive(Serialize, Deserialize, Default, Debug)] @@ -81,14 +93,43 @@ pub struct SignUpParams { pub email: String, pub name: String, pub password: String, + pub auth_type: AuthType, } #[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct SignUpResponse { pub user_id: i64, pub name: String, - pub email: String, - pub token: String, + pub workspace_id: String, + pub email: Option, + pub token: Option, +} + +#[derive(ProtoBuf, Default)] +pub struct ThirdPartyAuthPB { + /// Use this field to store the third party auth information. + /// Different auth type has different fields. + /// Supabase: + /// - map: { "uuid": "xxx" } + /// + #[pb(index = 1)] + pub map: HashMap, + + #[pb(index = 2)] + pub auth_type: AuthTypePB, +} + +#[derive(ProtoBuf_Enum, Debug, Clone)] +pub enum AuthTypePB { + Local = 0, + SelfHosted = 1, + Supabase = 2, +} + +impl Default for AuthTypePB { + fn default() -> Self { + Self::Local + } } #[derive(Serialize, Deserialize, Default, Debug, Clone)] @@ -99,11 +140,13 @@ pub struct UserProfile { pub token: String, pub icon_url: String, pub openai_key: String, + pub workspace_id: String, } #[derive(Serialize, Deserialize, Default, Clone, Debug)] pub struct UpdateUserProfileParams { pub id: i64, + pub auth_type: AuthType, pub name: Option, pub email: Option, pub password: Option, @@ -112,17 +155,6 @@ pub struct UpdateUserProfileParams { } impl UpdateUserProfileParams { - pub fn new(id: i64) -> Self { - Self { - id, - name: None, - email: None, - password: None, - icon_url: None, - openai_key: None, - } - } - pub fn name(mut self, name: &str) -> Self { self.name = Some(name.to_owned()); self @@ -147,4 +179,18 @@ impl UpdateUserProfileParams { self.openai_key = Some(openai_key.to_owned()); self } + + pub fn is_empty(&self) -> bool { + self.name.is_none() + && self.email.is_none() + && self.password.is_none() + && self.icon_url.is_none() + && self.openai_key.is_none() + } +} + +#[derive(ProtoBuf, Default)] +pub struct SignOutPB { + #[pb(index = 1)] + pub auth_type: AuthTypePB, } diff --git a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs index a535d2b026..acec2f0d8d 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -3,7 +3,7 @@ use std::convert::TryInto; use flowy_derive::ProtoBuf; use crate::entities::parser::{UserEmail, UserIcon, UserName, UserOpenaiKey, UserPassword}; -use crate::entities::{UpdateUserProfileParams, UserProfile}; +use crate::entities::{AuthTypePB, UpdateUserProfileParams, UserProfile}; use crate::errors::ErrorCode; #[derive(Default, ProtoBuf)] @@ -71,6 +71,9 @@ pub struct UpdateUserProfilePayloadPB { #[pb(index = 6, one_of)] pub openai_key: Option, + + #[pb(index = 7)] + pub auth_type: AuthTypePB, } impl UpdateUserProfilePayloadPB { @@ -138,6 +141,7 @@ impl TryInto for UpdateUserProfilePayloadPB { Ok(UpdateUserProfileParams { id: self.id, + auth_type: self.auth_type.into(), name, email, password, diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 6f1eb6ade2..02cb5f407b 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -3,19 +3,23 @@ use std::{convert::TryInto, sync::Arc}; use flowy_error::FlowyError; use flowy_sqlite::kv::KV; use lib_dispatch::prelude::*; +use lib_infra::box_any::BoxAny; use crate::entities::*; use crate::entities::{SignInParams, SignUpParams, UpdateUserProfileParams}; -use crate::services::UserSession; +use crate::services::{AuthType, UserSession}; -// tracing instrument 👉🏻 https://docs.rs/tracing/0.1.26/tracing/attr.instrument.html #[tracing::instrument(level = "debug", name = "sign_in", skip(data, session), fields(email = %data.email), err)] pub async fn sign_in( data: AFPluginData, session: AFPluginState>, ) -> DataResult { let params: SignInParams = data.into_inner().try_into()?; - let user_profile: UserProfilePB = session.sign_in(params).await?.into(); + let auth_type = params.auth_type.clone(); + let user_profile: UserProfilePB = session + .sign_in(&auth_type, BoxAny::new(params)) + .await? + .into(); data_result_ok(user_profile) } @@ -34,8 +38,11 @@ pub async fn sign_up( session: AFPluginState>, ) -> DataResult { let params: SignUpParams = data.into_inner().try_into()?; - let user_profile: UserProfilePB = session.sign_up(params).await?.into(); - + let auth_type = params.auth_type.clone(); + let user_profile: UserProfilePB = session + .sign_up(&auth_type, BoxAny::new(params)) + .await? + .into(); data_result_ok(user_profile) } @@ -61,9 +68,13 @@ pub async fn get_user_profile_handler( data_result_ok(user_profile) } -#[tracing::instrument(level = "debug", name = "sign_out", skip(session))] -pub async fn sign_out(session: AFPluginState>) -> Result<(), FlowyError> { - session.sign_out().await?; +#[tracing::instrument(level = "debug", skip(data, session))] +pub async fn sign_out( + data: AFPluginData, + session: AFPluginState>, +) -> Result<(), FlowyError> { + let auth_type: AuthType = data.into_inner().auth_type.into(); + session.sign_out(&auth_type).await?; Ok(()) } @@ -88,8 +99,7 @@ pub async fn set_appearance_setting( setting.theme = APPEARANCE_DEFAULT_THEME.to_string(); } - let s = serde_json::to_string(&setting)?; - KV::set_str(APPEARANCE_SETTING_CACHE_KEY, s); + KV::set_object(APPEARANCE_SETTING_CACHE_KEY, setting)?; Ok(()) } @@ -120,3 +130,19 @@ pub async fn get_user_setting( let user_setting = session.user_setting()?; data_result_ok(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, session), err)] +pub async fn third_party_auth_handler( + data: AFPluginData, + session: AFPluginState>, +) -> DataResult { + let params = data.into_inner(); + let auth_type: AuthType = params.auth_type.into(); + let user_profile: UserProfilePB = session + .sign_up(&auth_type, BoxAny::new(params.map)) + .await? + .into(); + data_result_ok(user_profile) +} diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 7862fc015e..b0342c9285 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -4,14 +4,14 @@ use strum_macros::Display; use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; use flowy_error::FlowyResult; + use lib_dispatch::prelude::*; +use lib_infra::box_any::BoxAny; use lib_infra::future::{Fut, FutureResult}; -use crate::entities::UserProfilePB; -use crate::entities::{ - SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, UserProfile, -}; +use crate::entities::{SignInResponse, SignUpResponse, UpdateUserProfileParams, UserProfile}; use crate::event_handler::*; +use crate::services::AuthType; use crate::{errors::FlowyError, services::UserSession}; pub fn init(user_session: Arc) -> AFPlugin { @@ -28,41 +28,76 @@ pub fn init(user_session: Arc) -> AFPlugin { .event(UserEvent::SetAppearanceSetting, set_appearance_setting) .event(UserEvent::GetAppearanceSetting, get_appearance_setting) .event(UserEvent::GetUserSetting, get_user_setting) + .event(UserEvent::ThirdPartyAuth, third_party_auth_handler) } pub trait UserStatusCallback: Send + Sync + 'static { - fn did_sign_in(&self, token: &str, user_id: i64) -> Fut>; + fn did_sign_in(&self, user_id: i64, workspace_id: &str) -> Fut>; fn did_sign_up(&self, user_profile: &UserProfile) -> Fut>; fn did_expired(&self, token: &str, user_id: i64) -> Fut>; - fn will_migrated(&self, token: &str, old_user_id: &str, user_id: i64) -> Fut>; } -pub trait UserCloudService: Send + Sync { - fn sign_up(&self, params: SignUpParams) -> FutureResult; - fn sign_in(&self, params: SignInParams) -> FutureResult; - fn sign_out(&self, token: &str) -> FutureResult<(), FlowyError>; +/// The user cloud service provider. +/// The provider can be supabase, firebase, aws, or any other cloud service. +pub trait UserCloudServiceProvider: Send + Sync + 'static { + fn get_auth_service(&self, auth_type: &AuthType) -> Result, FlowyError>; +} + +impl UserCloudServiceProvider for Arc +where + T: UserCloudServiceProvider, +{ + fn get_auth_service(&self, auth_type: &AuthType) -> Result, FlowyError> { + (**self).get_auth_service(auth_type) + } +} + +/// Provide the generic interface for the user cloud service +/// The user cloud service is responsible for the user authentication and user profile management +pub trait UserAuthService: Send + Sync { + /// 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; + + /// Sign in an account + /// The type of the params is defined the this trait's implementation. + fn sign_in(&self, params: BoxAny) -> FutureResult; + + /// Sign out an account + fn sign_out(&self, token: Option) -> FutureResult<(), FlowyError>; + + /// Using the user's token to update the user information fn update_user( &self, - token: &str, + uid: i64, + token: &Option, params: UpdateUserProfileParams, ) -> FutureResult<(), FlowyError>; - fn get_user(&self, token: &str) -> FutureResult; - fn ws_addr(&self) -> String; + + /// Get the user information using the user's token + fn get_user_profile( + &self, + token: Option, + uid: i64, + ) -> FutureResult, FlowyError>; } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] #[event_err = "FlowyError"] pub enum UserEvent { + /// Only use when the [AuthType] is Local or SelfHosted /// Logging into an account using a register email and password #[event(input = "SignInPayloadPB", output = "UserProfilePB")] SignIn = 0, + /// Only use when the [AuthType] is Local or SelfHosted /// Creating a new account #[event(input = "SignUpPayloadPB", output = "UserProfilePB")] SignUp = 1, /// Logging out fo an account - #[event(passthrough)] + #[event(input = "SignOutPB")] SignOut = 2, /// Update the user information @@ -92,4 +127,7 @@ pub enum UserEvent { /// Get the settings of the user, such as the user storage folder #[event(output = "UserSettingPB")] GetUserSetting = 9, + + #[event(input = "ThirdPartyAuthPB", output = "UserProfilePB")] + ThirdPartyAuth = 10, } diff --git a/frontend/rust-lib/flowy-user/src/lib.rs b/frontend/rust-lib/flowy-user/src/lib.rs index e53ce14814..48aae0f063 100644 --- a/frontend/rust-lib/flowy-user/src/lib.rs +++ b/frontend/rust-lib/flowy-user/src/lib.rs @@ -1,14 +1,12 @@ +#[macro_use] +extern crate flowy_sqlite; + pub mod entities; mod event_handler; pub mod event_map; mod notification; pub mod protobuf; pub mod services; -pub mod uid; -// mod sql_tables; - -#[macro_use] -extern crate flowy_sqlite; pub mod errors { pub use flowy_error::*; diff --git a/frontend/rust-lib/flowy-user/src/services/database.rs b/frontend/rust-lib/flowy-user/src/services/database.rs index af0569e5ac..98bc6fde47 100644 --- a/frontend/rust-lib/flowy-user/src/services/database.rs +++ b/frontend/rust-lib/flowy-user/src/services/database.rs @@ -112,20 +112,20 @@ pub struct UserTable { pub(crate) name: String, pub(crate) token: String, pub(crate) email: String, - pub(crate) workspace: String, // deprecated + pub(crate) workspace: String, pub(crate) icon_url: String, pub(crate) openai_key: String, } impl UserTable { - pub fn new(id: String, name: String, email: String, token: String) -> Self { + pub fn new(id: String, name: String, email: String, token: String, workspace_id: String) -> Self { Self { id, name, email, token, icon_url: "".to_owned(), - workspace: "".to_owned(), + workspace: workspace_id, openai_key: "".to_owned(), } } @@ -138,13 +138,29 @@ impl UserTable { impl From for UserTable { fn from(resp: SignUpResponse) -> Self { - UserTable::new(resp.user_id.to_string(), resp.name, resp.email, resp.token) + UserTable { + id: resp.user_id.to_string(), + name: resp.name, + token: resp.token.unwrap_or_default(), + email: resp.email.unwrap_or_default(), + workspace: resp.workspace_id, + icon_url: "".to_string(), + openai_key: "".to_string(), + } } } impl From for UserTable { fn from(resp: SignInResponse) -> Self { - UserTable::new(resp.user_id.to_string(), resp.name, resp.email, resp.token) + UserTable { + id: resp.user_id.to_string(), + name: resp.name, + token: resp.token.unwrap_or_default(), + email: resp.email.unwrap_or_default(), + workspace: resp.workspace_id, + icon_url: "".to_string(), + openai_key: "".to_string(), + } } } @@ -157,6 +173,7 @@ impl From for UserProfile { token: table.token, icon_url: table.icon_url, openai_key: table.openai_key, + workspace_id: table.workspace, } } } diff --git a/frontend/rust-lib/flowy-user/src/services/user_session.rs b/frontend/rust-lib/flowy-user/src/services/user_session.rs index 007ba2aa49..fdd2f8c23a 100644 --- a/frontend/rust-lib/flowy-user/src/services/user_session.rs +++ b/frontend/rust-lib/flowy-user/src/services/user_session.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use appflowy_integrate::RocksCollabDB; use serde::{Deserialize, Serialize}; +use serde_repr::*; use tokio::sync::RwLock; use flowy_error::internal_error; @@ -12,15 +13,16 @@ use flowy_sqlite::{ schema::{user_table, user_table::dsl}, DBConnection, ExpressionMethods, UserDatabaseConnection, }; +use lib_infra::box_any::BoxAny; use crate::entities::{ - SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, UserProfile, + AuthTypePB, SignInResponse, SignUpResponse, UpdateUserProfileParams, UserProfile, }; use crate::entities::{UserProfilePB, UserSettingPB}; -use crate::event_map::UserStatusCallback; +use crate::event_map::{UserCloudServiceProvider, UserStatusCallback}; use crate::{ errors::FlowyError, - event_map::UserCloudService, + event_map::UserAuthService, notification::*, services::database::{UserDB, UserTable, UserTableChangeset}, }; @@ -47,18 +49,21 @@ impl UserSessionConfig { pub struct UserSession { database: UserDB, session_config: UserSessionConfig, - cloud_service: Arc, + cloud_services: Arc, user_status_callback: RwLock>>, } impl UserSession { - pub fn new(session_config: UserSessionConfig, cloud_service: Arc) -> Self { + pub fn new( + session_config: UserSessionConfig, + cloud_services: Arc, + ) -> Self { let db = UserDB::new(&session_config.root_dir); let user_status_callback = RwLock::new(None); Self { database: db, session_config, - cloud_service, + cloud_services, user_status_callback, } } @@ -66,7 +71,7 @@ impl UserSession { pub async fn init(&self, user_status_callback: C) { if let Ok(session) = self.get_session() { let _ = user_status_callback - .did_sign_in(&session.token, session.user_id) + .did_sign_in(session.user_id, &session.workspace_id) .await; } *self.user_status_callback.write().await = Some(Arc::new(user_status_callback)); @@ -93,77 +98,79 @@ impl UserSession { self.database.get_kv_db(user_id) } - #[tracing::instrument(level = "debug", skip(self))] - pub async fn sign_in(&self, params: SignInParams) -> Result { - if self.is_user_login(¶ms.email) { - match self.get_user_profile().await { - Ok(profile) => { - send_sign_in_notification() - .payload::(profile.clone().into()) - .send(); - Ok(profile) - }, - Err(err) => Err(err), - } - } else { - let resp = self.cloud_service.sign_in(params).await?; - let session: Session = resp.clone().into(); - self.set_session(Some(session))?; - let user_profile: UserProfile = self.save_user(resp.into()).await?.into(); - let _ = self - .user_status_callback - .read() - .await - .as_ref() - .unwrap() - .did_sign_in(&user_profile.token, user_profile.id) - .await; - send_sign_in_notification() - .payload::(user_profile.clone().into()) - .send(); - Ok(user_profile) - } - } + #[tracing::instrument(level = "debug", skip(self, params))] + pub async fn sign_in( + &self, + auth_type: &AuthType, + params: BoxAny, + ) -> Result { + let resp = self + .cloud_services + .get_auth_service(auth_type)? + .sign_in(params) + .await?; - #[tracing::instrument(level = "debug", skip(self))] - pub async fn sign_up(&self, params: SignUpParams) -> Result { - if self.is_user_login(¶ms.email) { - self.get_user_profile().await - } else { - let resp = self.cloud_service.sign_up(params).await?; - let session: Session = resp.clone().into(); - self.set_session(Some(session))?; - let user_table = self.save_user(resp.into()).await?; - let user_profile: UserProfile = user_table.into(); - let _ = self - .user_status_callback - .read() - .await - .as_ref() - .unwrap() - .did_sign_up(&user_profile) - .await; - Ok(user_profile) - } - } - - #[tracing::instrument(level = "debug", skip(self))] - pub async fn sign_out(&self) -> Result<(), FlowyError> { - let session = self.get_session()?; - let uid = session.user_id.to_string(); - let _ = diesel::delete(dsl::user_table.filter(dsl::id.eq(&uid))) - .execute(&*(self.db_connection()?))?; - self.database.close_user_db(session.user_id)?; - self.set_session(None)?; + let session: Session = resp.clone().into(); + self.set_session(Some(session))?; + let user_profile: UserProfile = self.save_user(resp.into()).await?.into(); let _ = self .user_status_callback .read() .await .as_ref() .unwrap() - .did_expired(&session.token, session.user_id) + .did_sign_in(user_profile.id, &user_profile.workspace_id) .await; - self.sign_out_on_server(&session.token).await?; + send_sign_in_notification() + .payload::(user_profile.clone().into()) + .send(); + + Ok(user_profile) + } + + #[tracing::instrument(level = "debug", skip(self, params))] + pub async fn sign_up( + &self, + auth_type: &AuthType, + params: BoxAny, + ) -> Result { + let resp = self + .cloud_services + .get_auth_service(auth_type)? + .sign_up(params) + .await?; + + let session: Session = resp.clone().into(); + self.set_session(Some(session))?; + let user_table = self.save_user(resp.into()).await?; + let user_profile: UserProfile = user_table.into(); + let _ = self + .user_status_callback + .read() + .await + .as_ref() + .unwrap() + .did_sign_up(&user_profile) + .await; + Ok(user_profile) + } + + #[tracing::instrument(level = "debug", skip(self))] + pub async fn sign_out(&self, auth_type: &AuthType) -> Result<(), FlowyError> { + let session = self.get_session()?; + let uid = session.user_id.to_string(); + let _ = diesel::delete(dsl::user_table.filter(dsl::id.eq(&uid))) + .execute(&*(self.db_connection()?))?; + self.database.close_user_db(session.user_id)?; + self.set_session(None)?; + let server = self.cloud_services.get_auth_service(auth_type)?; + let token = session.token; + let _ = tokio::spawn(async move { + match server.sign_out(token).await { + Ok(_) => {}, + Err(e) => tracing::error!("Sign out failed: {:?}", e), + } + }); Ok(()) } @@ -173,16 +180,22 @@ impl UserSession { &self, params: UpdateUserProfileParams, ) -> Result<(), FlowyError> { + let auth_type = params.auth_type.clone(); let session = self.get_session()?; let changeset = UserTableChangeset::new(params.clone()); diesel_update_table!(user_table, changeset, &*self.db_connection()?); let user_profile = self.get_user_profile().await?; let profile_pb: UserProfilePB = user_profile.into(); - send_notification(&session.token, UserNotification::DidUpdateUserProfile) - .payload(profile_pb) - .send(); - self.update_user_on_server(&session.token, params).await?; + send_notification( + &session.user_id.to_string(), + UserNotification::DidUpdateUserProfile, + ) + .payload(profile_pb) + .send(); + self + .update_user(&auth_type, session.user_id, &session.token, params) + .await?; Ok(()) } @@ -191,24 +204,21 @@ impl UserSession { } pub async fn check_user(&self) -> Result { - let (user_id, token) = self.get_session()?.into_part(); + let (user_id, _token) = self.get_session()?.into_part(); let user_id = user_id.to_string(); let user = dsl::user_table .filter(user_table::id.eq(&user_id)) .first::(&*(self.db_connection()?))?; - - self.read_user_profile_on_server(&token)?; Ok(user.into()) } pub async fn get_user_profile(&self) -> Result { - let (user_id, token) = self.get_session()?.into_part(); + let (user_id, _) = self.get_session()?.into_part(); let user_id = user_id.to_string(); let user = dsl::user_table .filter(user_table::id.eq(&user_id)) .first::(&*(self.db_connection()?))?; - self.read_user_profile_on_server(&token)?; Ok(user.into()) } @@ -235,25 +245,23 @@ impl UserSession { Ok(self.get_session()?.name) } - pub fn token(&self) -> Result { + pub fn token(&self) -> Result, FlowyError> { Ok(self.get_session()?.token) } } impl UserSession { - fn read_user_profile_on_server(&self, _token: &str) -> Result<(), FlowyError> { - Ok(()) - } - - async fn update_user_on_server( + async fn update_user( &self, - token: &str, + auth_type: &AuthType, + uid: i64, + token: &Option, params: UpdateUserProfileParams, ) -> Result<(), FlowyError> { - let server = self.cloud_service.clone(); + let server = self.cloud_services.get_auth_service(auth_type)?; let token = token.to_owned(); let _ = tokio::spawn(async move { - match server.update_user(&token, params).await { + match server.update_user(uid, &token, params).await { Ok(_) => {}, Err(e) => { // TODO: retry? @@ -265,19 +273,6 @@ impl UserSession { Ok(()) } - async fn sign_out_on_server(&self, token: &str) -> Result<(), FlowyError> { - let server = self.cloud_service.clone(); - let token = token.to_owned(); - let _ = tokio::spawn(async move { - match server.sign_out(&token).await { - Ok(_) => {}, - Err(e) => tracing::error!("Sign out failed: {:?}", e), - } - }) - .await; - Ok(()) - } - async fn save_user(&self, user: UserTable) -> Result { let conn = self.db_connection()?; let _ = diesel::insert_into(user_table::table) @@ -304,17 +299,10 @@ impl UserSession { Some(session) => Ok(session), } } - - fn is_user_login(&self, email: &str) -> bool { - match self.get_session() { - Ok(session) => session.email == email, - Err(_) => false, - } - } } pub async fn update_user( - _cloud_service: Arc, + _cloud_service: Arc, pool: Arc, params: UpdateUserProfileParams, ) -> Result<(), FlowyError> { @@ -333,10 +321,17 @@ impl UserDatabaseConnection for UserSession { #[derive(Debug, Clone, Default, Serialize, Deserialize)] struct Session { user_id: i64, - token: String, - email: String, + + workspace_id: String, + #[serde(default)] name: String, + + #[serde(default)] + token: Option, + + #[serde(default)] + email: Option, } impl std::convert::From for Session { @@ -346,6 +341,7 @@ impl std::convert::From for Session { token: resp.token, email: resp.email, name: resp.name, + workspace_id: resp.workspace_id, } } } @@ -357,12 +353,13 @@ impl std::convert::From for Session { token: resp.token, email: resp.email, name: resp.name, + workspace_id: resp.workspace_id, } } } impl Session { - pub fn into_part(self) -> (i64, String) { + pub fn into_part(self) -> (i64, Option) { (self.user_id, self.token) } } @@ -390,11 +387,30 @@ impl std::convert::From for String { } } -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -struct OldSession { - user_id: String, - token: String, - email: String, - #[serde(default)] - name: String, +#[derive(Debug, Clone, Hash, Serialize_repr, Deserialize_repr, Eq, PartialEq)] +#[repr(u8)] +pub enum AuthType { + /// It's a local server, we do fake sign in default. + 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, + /// It uses Supabase as the backend. + Supabase = 2, +} + +impl Default for AuthType { + fn default() -> Self { + Self::Local + } +} + +impl From for AuthType { + fn from(pb: AuthTypePB) -> Self { + match pb { + AuthTypePB::Supabase => AuthType::Supabase, + AuthTypePB::Local => AuthType::Local, + AuthTypePB::SelfHosted => AuthType::SelfHosted, + } + } } diff --git a/frontend/rust-lib/flowy-user/tests/event/auth_test.rs b/frontend/rust-lib/flowy-user/tests/event/auth_test.rs index 7fd5e0bfa8..408baa064a 100644 --- a/frontend/rust-lib/flowy-user/tests/event/auth_test.rs +++ b/frontend/rust-lib/flowy-user/tests/event/auth_test.rs @@ -1,8 +1,9 @@ -use crate::helper::*; use flowy_test::{event_builder::UserModuleEventBuilder, FlowySDKTest}; -use flowy_user::entities::{SignInPayloadPB, SignUpPayloadPB, UserProfilePB}; +use flowy_user::entities::{AuthTypePB, SignInPayloadPB, SignUpPayloadPB, UserProfilePB}; use flowy_user::{errors::ErrorCode, event_map::UserEvent::*}; +use crate::helper::*; + #[tokio::test] async fn sign_up_with_invalid_email() { for email in invalid_email_test_case() { @@ -11,6 +12,7 @@ async fn sign_up_with_invalid_email() { email: email.to_string(), name: valid_name(), password: login_password(), + auth_type: AuthTypePB::Local, }; assert_eq!( @@ -33,6 +35,7 @@ async fn sign_up_with_invalid_password() { email: random_email(), name: valid_name(), password, + auth_type: AuthTypePB::Local, }; UserModuleEventBuilder::new(sdk) @@ -56,6 +59,7 @@ async fn sign_in_success() { email: sign_up_context.user_profile.email.clone(), password: sign_up_context.password.clone(), name: "".to_string(), + auth_type: AuthTypePB::Local, }; let response = UserModuleEventBuilder::new(test.clone()) @@ -75,6 +79,7 @@ async fn sign_in_with_invalid_email() { email: email.to_string(), password: login_password(), name: "".to_string(), + auth_type: AuthTypePB::Local, }; assert_eq!( @@ -99,6 +104,7 @@ async fn sign_in_with_invalid_password() { email: random_email(), password, name: "".to_string(), + auth_type: AuthTypePB::Local, }; UserModuleEventBuilder::new(sdk) diff --git a/shared-lib/Cargo.lock b/shared-lib/Cargo.lock index e743c3327f..7f0316748a 100644 --- a/shared-lib/Cargo.lock +++ b/shared-lib/Cargo.lock @@ -29,6 +29,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + [[package]] name = "async-trait" version = "0.1.64" @@ -820,6 +826,7 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" name = "lib-infra" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", "bytes", "chrono", diff --git a/shared-lib/lib-infra/Cargo.toml b/shared-lib/lib-infra/Cargo.toml index d4d3196064..7ee1c8559f 100644 --- a/shared-lib/lib-infra/Cargo.toml +++ b/shared-lib/lib-infra/Cargo.toml @@ -14,3 +14,4 @@ tokio = { version = "1.26", features = ["time", "rt"] } rand = "0.8.5" async-trait = "0.1.64" md5 = "0.7.0" +anyhow = "1.0" diff --git a/shared-lib/lib-infra/src/box_any.rs b/shared-lib/lib-infra/src/box_any.rs new file mode 100644 index 0000000000..25562fda43 --- /dev/null +++ b/shared-lib/lib-infra/src/box_any.rs @@ -0,0 +1,53 @@ +use std::any::Any; + +use anyhow::Result; + +pub struct BoxAny(Box); + +impl BoxAny { + pub fn new(value: T) -> Self + where + T: Send + Sync + 'static, + { + Self(Box::new(value)) + } + + pub fn unbox_or_default(self) -> T + where + T: Default + 'static, + { + match self.0.downcast::() { + Ok(value) => *value, + Err(_) => T::default(), + } + } + + pub fn unbox_or_error(self) -> Result + where + T: Default + 'static, + { + match self.0.downcast::() { + Ok(value) => Ok(*value), + Err(e) => Err(anyhow::anyhow!( + "downcast error to {} failed: {:?}", + std::any::type_name::(), + e + )), + } + } + + pub fn unbox_or_none(self) -> Option + where + T: Default + 'static, + { + match self.0.downcast::() { + Ok(value) => Some(*value), + Err(_) => None, + } + } + + #[allow(dead_code)] + pub fn downcast_ref(&self) -> Option<&T> { + self.0.downcast_ref() + } +} diff --git a/shared-lib/lib-infra/src/lib.rs b/shared-lib/lib-infra/src/lib.rs index 5c0f9819cf..f2cf594209 100644 --- a/shared-lib/lib-infra/src/lib.rs +++ b/shared-lib/lib-infra/src/lib.rs @@ -1,6 +1,7 @@ +pub use async_trait; + +pub mod box_any; pub mod future; pub mod ref_map; pub mod retry; pub mod util; - -pub use async_trait;