feat: integrate client-api (#3430)

* chore: update client-api rev

* chore: update collab rev id

* feat: add sign_in_request and import shared entity

* feat: added to userworkspace from af_workspace

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

* chore: update client-api rev

* feat: add workspaces api

* feat: added check user

* chore: config

* chore: update client_api version

* chore: ws connect

* chore: ws connect

* chore: update crate versions

* chore: rename event

* chore: update client-appi

* chore: set appflowy cloud env

* chore: add env template

* chore: update env name

* docs: update docs

* fix: check_user

* feat: impl sign_in_with_url

* feat: add file storage placeholders

* chore: update client-api

* chore: disable test

* feat: impl workspace add and remove

* chore: sign up test

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

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

* fix: close popover after item selection in settings view

* fix: add missing await before closing popover

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

* fix: add requested changes

* feat: close text direction settings popups after selection

* fix: clean up

* fix: restore theme value dropdown as StatefulWidget

* feat: openai and stabilityai integration (#3439)

* chore: create trait

* test: add tests

* chore: remove log

* chore: disable log

* chore: checklist ux flow redesign (#3418)

* chore: ux flow redesign

* chore: remove unused imports

* fix: allow creation of tasks of the same name

* chore: apply code suggestions from Mathias

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

* Fixed Issue no #3426

* Reversed the pubspec.lock mistaken update

* FIXED PADDING

* Fixed Padding issue on calender field edit popup

* chore: rename package name (#3501)

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

* feat: enable removing user icon (#3487)

* feat: enable removing user icon

* fix: generate to true

* fix: review comments

* fix: more review comments

* fix: integration test and final changes

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

* chore: calendar UI polish (#3484)

* chore: update calendar theming

* feat: add event popup editor

* chore: new event button redesign and add card shadows

* chore: unscheduled events button

* chore: event title text field

* fix: focus node double dispose

* chore: show popover when create new event

* test: integrate some tests for integration testing purposes

* fix: some fixes and more integration tests

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

* feat: add reset font button in toolbar

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

* fix:  unable to change RTL of heading block

* test: add integration test for ltr/rtl mode

* chore: update inlang project settings (#3441)

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

* chore: add script

* chore: update script

* chore: update bytes version

* chore: submit lock file

* chore: update test

* chore: update test

* chore: bump version

* chore: update

* ci: fix

* ci: fix

* chore: update commit id

* chore: update commit id

* chore: update commit id

* fix: is cloud enable

---------

Co-authored-by: Fu Zi Xiang <speed2exe@live.com.sg>
Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
Co-authored-by: Vincenzo De Petris <37916223+vincendep@users.noreply.github.com>
Co-authored-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com>
Co-authored-by: Aryan More <61151896+aryan-more@users.noreply.github.com>
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
Co-authored-by: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com>
Co-authored-by: Nitin-Poojary <70025277+Nitin-Poojary@users.noreply.github.com>
Co-authored-by: Jannes Blobel <72493222+jannesblobel@users.noreply.github.com>
This commit is contained in:
Nathan.fooo 2023-10-02 17:22:22 +08:00 committed by GitHub
parent 01c3fec5aa
commit 7f44b181bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
98 changed files with 2495 additions and 845 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

18
shared-lib/Cargo.lock generated
View File

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

View File

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