chore: sign in with provider (#3592)

* chore: sign in with provider

* feat: implement oauth flow of appflowy cloud

* chore: rename env

* chore: fix deadlock

* fix: login bugs

* chore: clippyt

* chore: update client api

* chore: fmt
This commit is contained in:
Nathan.fooo
2023-10-07 09:58:44 +08:00
committed by GitHub
parent 92523321f1
commit a59561aee3
45 changed files with 709 additions and 357 deletions

View File

@ -285,6 +285,14 @@
"options": { "options": {
"cwd": "${workspaceFolder}/appflowy_flutter" "cwd": "${workspaceFolder}/appflowy_flutter"
} }
},
{
"label": "AF: Generate AppFlowyEnv",
"type": "shell",
"command": "dart run build_runner build --delete-conflicting-outputs",
"options": {
"cwd": "${workspaceFolder}/appflowy_flutter/packages/appflowy_backend"
}
} }
] ]
} }

View File

@ -5,10 +5,11 @@
# Configuring Cloud Type # 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: # 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 # Local: 0
# AppFlowy Cloud: Set CLOUD_TYPE to 2 # Supabase: 1
# AppFlowy Cloud: 2
CLOUD_TYPE=1 CLOUD_TYPE=0
# Supabase Configuration # Supabase Configuration
# If you're using Supabase (CLOUD_TYPE=1), you need to provide the following configurations: # If you're using Supabase (CLOUD_TYPE=1), you need to provide the following configurations:
@ -18,4 +19,5 @@ SUPABASE_ANON_KEY=replace-with-your-supabase-key
# AppFlowy Cloud Configuration # AppFlowy Cloud Configuration
# If you're using AppFlowy Cloud (CLOUD_TYPE=2), you need to provide the following configurations: # 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_URL=replace-with-your-appflowy-cloud-url
APPFLOWY_CLOUD_BASE_WS_URL=replace-with-your-appflowy-cloud-ws-url APPFLOWY_CLOUD_WS_BASE_URL=replace-with-your-appflowy-cloud-ws-url
APPFLOWY_CLOUD_GOTRUE_URL=replace-with-your-appflowy-cloud-gotrue-url

View File

@ -35,10 +35,17 @@ abstract class Env {
@EnviedField( @EnviedField(
obfuscate: true, obfuscate: true,
varName: 'APPFLOWY_CLOUD_BASE_WS_URL', varName: 'APPFLOWY_CLOUD_WS_BASE_URL',
defaultValue: '', defaultValue: '',
) )
static final String afCloudBaseWSUrl = _Env.afCloudBaseWSUrl; static final String afCloudWSBaseUrl = _Env.afCloudWSBaseUrl;
@EnviedField(
obfuscate: true,
varName: 'APPFLOWY_CLOUD_GOTRUE_URL',
defaultValue: '',
)
static final String afCloudGoTrueUrl = _Env.afCloudGoTrueUrl;
// Supabase Configuration: // Supabase Configuration:
@EnviedField( @EnviedField(
@ -64,6 +71,24 @@ bool get isCloudEnabled {
} }
} }
bool get isSupabaseEnabled {
// Only enable supabase in release and develop mode.
if (integrationMode().isRelease || integrationMode().isDevelop) {
return currentCloudType() == CloudType.supabase;
} else {
return false;
}
}
bool get isAppFlowyCloudEnabled {
// Only enable appflowy cloud in release and develop mode.
if (integrationMode().isRelease || integrationMode().isDevelop) {
return currentCloudType() == CloudType.appflowyCloud;
} else {
return false;
}
}
enum CloudType { enum CloudType {
unknown, unknown,
supabase, supabase,
@ -84,7 +109,7 @@ CloudType currentCloudType() {
} }
if (value == 2) { if (value == 2) {
if (Env.afCloudBaseUrl.isEmpty || Env.afCloudBaseWSUrl.isEmpty) { if (Env.afCloudBaseUrl.isEmpty || Env.afCloudWSBaseUrl.isEmpty) {
Log.error("AppFlowy cloud is not configured"); Log.error("AppFlowy cloud is not configured");
return CloudType.unknown; return CloudType.unknown;
} else { } else {

View File

@ -38,7 +38,8 @@ AppFlowyEnv getAppFlowyEnv() {
final appflowyCloudConfig = AppFlowyCloudConfiguration( final appflowyCloudConfig = AppFlowyCloudConfiguration(
base_url: Env.afCloudBaseUrl, base_url: Env.afCloudBaseUrl,
base_ws_url: Env.afCloudBaseWSUrl, ws_base_url: Env.afCloudWSBaseUrl,
gotrue_url: Env.afCloudGoTrueUrl,
); );
return AppFlowyEnv( return AppFlowyEnv(

View File

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

View File

@ -1,13 +1,23 @@
import 'dart:async'; import 'dart:async';
import 'package:app_links/app_links.dart';
import 'package:appflowy/user/application/auth/backend_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/auth_service.dart';
import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:url_launcher/url_launcher.dart';
import 'auth_error.dart';
import 'device_id.dart';
class AFCloudAuthService implements AuthService { class AFCloudAuthService implements AuthService {
final _appLinks = AppLinks();
StreamSubscription<Uri?>? _deeplinkSubscription;
AFCloudAuthService(); AFCloudAuthService();
final BackendAuthService _backendAuthService = BackendAuthService( final BackendAuthService _backendAuthService = BackendAuthService(
@ -38,8 +48,72 @@ class AFCloudAuthService implements AuthService {
required String platform, required String platform,
Map<String, String> params = const {}, Map<String, String> params = const {},
}) async { }) async {
// final provider = ProviderTypePBExtension.fromPlatform(platform);
throw UnimplementedError();
// Get the oauth url from the backend
final result = await UserEventGetOauthURLWithProvider(
OauthProviderPB.create()..provider = provider,
).send();
return result.fold(
(data) async {
// Open the webview with oauth url
final uri = Uri.parse(data.oauthUrl);
final isSuccess = await launchUrl(
uri,
mode: LaunchMode.externalApplication,
webOnlyWindowName: '_self',
);
final completer = Completer<Either<FlowyError, UserProfilePB>>();
_deeplinkSubscription = _appLinks.uriLinkStream.listen(
(Uri? uri) async {
await _handleUri(uri, completer);
},
onError: (Object err, StackTrace stackTrace) {
Log.error('onDeepLinkError: ${err.toString()}', stackTrace);
_deeplinkSubscription?.cancel();
completer.complete(left(AuthError.deeplinkError));
},
);
if (!isSuccess) {
_deeplinkSubscription?.cancel();
completer.complete(left(AuthError.signInWithOauthError));
}
return completer.future;
},
(r) => left(r),
);
}
Future<void> _handleUri(
Uri? uri,
Completer<Either<FlowyError, UserProfilePB>> completer,
) async {
if (uri != null) {
if (_isAuthCallbackDeeplink(uri)) {
// Sign in with url
final deviceId = await getDeviceId();
final payload = OauthSignInPB(
authType: AuthTypePB.AFCloud,
map: {
AuthServiceMapKeys.signInURL: uri.toString(),
AuthServiceMapKeys.deviceId: deviceId
},
);
final result = await UserEventOauthSignIn(payload)
.send()
.then((value) => value.swap());
_deeplinkSubscription?.cancel();
completer.complete(result);
}
} else {
Log.error('onDeepLinkError: Unexpect empty deep link callback');
_deeplinkSubscription?.cancel();
completer.complete(left(AuthError.emptyDeeplink));
}
} }
@override @override
@ -67,3 +141,22 @@ class AFCloudAuthService implements AuthService {
return UserBackendService.getCurrentUserProfile(); return UserBackendService.getCurrentUserProfile();
} }
} }
extension ProviderTypePBExtension on ProviderTypePB {
static ProviderTypePB fromPlatform(String platform) {
switch (platform) {
case 'github':
return ProviderTypePB.Github;
case 'google':
return ProviderTypePB.Google;
case 'discord':
return ProviderTypePB.Discord;
default:
throw UnimplementedError();
}
}
}
bool _isAuthCallbackDeeplink(Uri uri) {
return (uri.fragment.contains('access_token'));
}

View File

@ -17,4 +17,16 @@ class AuthError {
static final supabaseGetUserError = FlowyError() static final supabaseGetUserError = FlowyError()
..msg = 'unable to get user from supabase -10004' ..msg = 'unable to get user from supabase -10004'
..code = ErrorCode.UserUnauthorized; ..code = ErrorCode.UserUnauthorized;
static final signInWithOauthError = FlowyError()
..msg = 'sign in with oauth error -10003'
..code = ErrorCode.UserUnauthorized;
static final emptyDeeplink = FlowyError()
..msg = 'Unexpected empty deeplink'
..code = ErrorCode.UnexpectedEmpty;
static final deeplinkError = FlowyError()
..msg = 'Deeplink error'
..code = ErrorCode.Internal;
} }

View File

@ -9,6 +9,7 @@ class AuthServiceMapKeys {
static const String uuid = 'uuid'; static const String uuid = 'uuid';
static const String email = 'email'; static const String email = 'email';
static const String deviceId = 'device_id'; static const String deviceId = 'device_id';
static const String signInURL = 'sign_in_url';
} }
/// `AuthService` is an abstract class that defines methods related to user authentication. /// `AuthService` is an abstract class that defines methods related to user authentication.

View File

@ -169,12 +169,12 @@ class SupabaseAuthService implements AuthService {
Future<Either<FlowyError, UserProfilePB>> _setupAuth({ Future<Either<FlowyError, UserProfilePB>> _setupAuth({
required Map<String, String> map, required Map<String, String> map,
}) async { }) async {
final payload = OAuthPB( final payload = OauthSignInPB(
authType: AuthTypePB.Supabase, authType: AuthTypePB.Supabase,
map: map, map: map,
); );
return UserEventOAuth(payload).send().then((value) => value.swap()); return UserEventOauthSignIn(payload).send().then((value) => value.swap());
} }
} }

View File

@ -56,7 +56,7 @@ class MockAuthService implements AuthService {
final uuid = response.user!.id; final uuid = response.user!.id;
final email = response.user!.email!; final email = response.user!.email!;
final payload = OAuthPB( final payload = OauthSignInPB(
authType: AuthTypePB.Supabase, authType: AuthTypePB.Supabase,
map: { map: {
AuthServiceMapKeys.uuid: uuid, AuthServiceMapKeys.uuid: uuid,
@ -65,7 +65,7 @@ class MockAuthService implements AuthService {
}, },
); );
return UserEventOAuth(payload).send().then((value) => value.swap()); return UserEventOauthSignIn(payload).send().then((value) => value.swap());
} on AuthException catch (e) { } on AuthException catch (e) {
Log.error(e); Log.error(e);
return Left(AuthError.supabaseSignInError); return Left(AuthError.supabaseSignInError);

View File

@ -1,10 +1,9 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
// Run `dart run build_runner build` to generate the json serialization If the // Run `dart run build_runner build` to generate the json serialization If the
// file `env_serde.i.dart` is existed, delete it first. // file `env_serde.g.dart` is existed, delete it first.
// //
// the file `env_serde.g.dart` will be generated in the same directory. Rename // the file `env_serde.g.dart` will be generated in the same directory.
// the file to `env_serde.i.dart` because the file is ignored by default.
part 'env_serde.g.dart'; part 'env_serde.g.dart';
@JsonSerializable() @JsonSerializable()
@ -45,11 +44,13 @@ class SupabaseConfiguration {
@JsonSerializable() @JsonSerializable()
class AppFlowyCloudConfiguration { class AppFlowyCloudConfiguration {
final String base_url; final String base_url;
final String base_ws_url; final String ws_base_url;
final String gotrue_url;
AppFlowyCloudConfiguration({ AppFlowyCloudConfiguration({
required this.base_url, required this.base_url,
required this.base_ws_url, required this.ws_base_url,
required this.gotrue_url,
}); });
factory AppFlowyCloudConfiguration.fromJson(Map<String, dynamic> json) => factory AppFlowyCloudConfiguration.fromJson(Map<String, dynamic> json) =>

View File

@ -26,7 +26,7 @@ packages:
source: hosted source: hosted
version: "2.0.7" version: "2.0.7"
app_links: app_links:
dependency: "direct overridden" dependency: "direct main"
description: description:
path: "." path: "."
ref: c64ce17 ref: c64ce17

View File

@ -115,6 +115,7 @@ dependencies:
# TODO: Consider implementing custom package # TODO: Consider implementing custom package
# to gather notification handling for all platforms # to gather notification handling for all platforms
local_notifier: ^0.1.5 local_notifier: ^0.1.5
app_links: ^3.4.1
dev_dependencies: dev_dependencies:
flutter_lints: ^2.0.1 flutter_lints: ^2.0.1

View File

@ -762,7 +762,7 @@ dependencies = [
[[package]] [[package]]
name = "client-api" name = "client-api"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b0c213#b0c213b5c0b941e2013b0126e200f98201a82f54" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=926da91#926da912eab806e5b325e12103d341cea536aa2b"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -775,7 +775,6 @@ dependencies = [
"gotrue-entity", "gotrue-entity",
"lib0", "lib0",
"mime", "mime",
"opener",
"parking_lot", "parking_lot",
"reqwest", "reqwest",
"scraper", "scraper",
@ -853,7 +852,7 @@ dependencies = [
[[package]] [[package]]
name = "collab" name = "collab"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=86c5e8#86c5e8891af93de2d035a57214894e854d39662a" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=82975da#82975da119a0c9d0e8b70585966bb89ed6c97cd3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -872,7 +871,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-database" name = "collab-database"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=86c5e8#86c5e8891af93de2d035a57214894e854d39662a" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=82975da#82975da119a0c9d0e8b70585966bb89ed6c97cd3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -902,7 +901,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-define" name = "collab-define"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=86c5e8#86c5e8891af93de2d035a57214894e854d39662a" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=82975da#82975da119a0c9d0e8b70585966bb89ed6c97cd3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -916,7 +915,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-derive" name = "collab-derive"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=86c5e8#86c5e8891af93de2d035a57214894e854d39662a" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=82975da#82975da119a0c9d0e8b70585966bb89ed6c97cd3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -928,7 +927,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-document" name = "collab-document"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=86c5e8#86c5e8891af93de2d035a57214894e854d39662a" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=82975da#82975da119a0c9d0e8b70585966bb89ed6c97cd3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"collab", "collab",
@ -948,7 +947,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-folder" name = "collab-folder"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=86c5e8#86c5e8891af93de2d035a57214894e854d39662a" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=82975da#82975da119a0c9d0e8b70585966bb89ed6c97cd3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -979,16 +978,18 @@ dependencies = [
"collab-persistence", "collab-persistence",
"collab-plugins", "collab-plugins",
"futures", "futures",
"lib-infra",
"parking_lot", "parking_lot",
"serde", "serde",
"serde_json", "serde_json",
"tokio",
"tracing", "tracing",
] ]
[[package]] [[package]]
name = "collab-persistence" name = "collab-persistence"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=86c5e8#86c5e8891af93de2d035a57214894e854d39662a" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=82975da#82975da119a0c9d0e8b70585966bb89ed6c97cd3"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bincode", "bincode",
@ -1009,7 +1010,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-plugins" name = "collab-plugins"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=86c5e8#86c5e8891af93de2d035a57214894e854d39662a" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=82975da#82975da119a0c9d0e8b70585966bb89ed6c97cd3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1036,7 +1037,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-user" name = "collab-user"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=86c5e8#86c5e8891af93de2d035a57214894e854d39662a" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=82975da#82975da119a0c9d0e8b70585966bb89ed6c97cd3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"collab", "collab",
@ -1289,7 +1290,7 @@ dependencies = [
"cssparser-macros", "cssparser-macros",
"dtoa-short", "dtoa-short",
"itoa 1.0.6", "itoa 1.0.6",
"phf 0.11.2", "phf 0.8.0",
"smallvec", "smallvec",
] ]
@ -1435,13 +1436,15 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]] [[package]]
name = "database-entity" name = "database-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b0c213#b0c213b5c0b941e2013b0126e200f98201a82f54" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=926da91#926da912eab806e5b325e12103d341cea536aa2b"
dependencies = [ dependencies = [
"anyhow",
"chrono", "chrono",
"collab-define", "collab-define",
"serde", "serde",
"serde_json", "serde_json",
"sqlx", "sqlx",
"thiserror",
"uuid", "uuid",
"validator", "validator",
] ]
@ -2772,7 +2775,7 @@ dependencies = [
[[package]] [[package]]
name = "gotrue" name = "gotrue"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b0c213#b0c213b5c0b941e2013b0126e200f98201a82f54" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=926da91#926da912eab806e5b325e12103d341cea536aa2b"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"futures-util", "futures-util",
@ -2782,12 +2785,13 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",
"tracing",
] ]
[[package]] [[package]]
name = "gotrue-entity" name = "gotrue-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b0c213#b0c213b5c0b941e2013b0126e200f98201a82f54" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=926da91#926da912eab806e5b325e12103d341cea536aa2b"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"reqwest", "reqwest",
@ -3220,7 +3224,7 @@ dependencies = [
[[package]] [[package]]
name = "infra" name = "infra"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b0c213#b0c213b5c0b941e2013b0126e200f98201a82f54" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=926da91#926da912eab806e5b325e12103d341cea536aa2b"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"reqwest", "reqwest",
@ -4261,7 +4265,6 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [ dependencies = [
"phf_macros 0.11.2",
"phf_shared 0.11.2", "phf_shared 0.11.2",
] ]
@ -4353,19 +4356,6 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "phf_macros"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
dependencies = [
"phf_generator 0.11.2",
"phf_shared 0.11.2",
"proc-macro2",
"quote",
"syn 2.0.29",
]
[[package]] [[package]]
name = "phf_shared" name = "phf_shared"
version = "0.8.0" version = "0.8.0"
@ -5579,9 +5569,10 @@ dependencies = [
[[package]] [[package]]
name = "shared_entity" name = "shared_entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b0c213#b0c213b5c0b941e2013b0126e200f98201a82f54" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=926da91#926da912eab806e5b325e12103d341cea536aa2b"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"database-entity",
"gotrue-entity", "gotrue-entity",
"opener", "opener",
"reqwest", "reqwest",
@ -6494,7 +6485,9 @@ checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"log", "log",
"native-tls",
"tokio", "tokio",
"tokio-native-tls",
"tungstenite", "tungstenite",
] ]
@ -6711,6 +6704,7 @@ dependencies = [
"http", "http",
"httparse", "httparse",
"log", "log",
"native-tls",
"rand 0.8.5", "rand 0.8.5",
"sha1", "sha1",
"thiserror", "thiserror",

View File

@ -38,7 +38,7 @@ custom-protocol = ["tauri/custom-protocol"]
# Run the script: # Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id # scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "b0c213" } client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "926da91" }
# Please use the following script to update collab. # Please use the following script to update collab.
# Working directory: frontend # Working directory: frontend
# #
@ -48,14 +48,14 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "b0c
# To switch to the local path, run: # To switch to the local path, run:
# scripts/tool/update_collab_source.sh # scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" } collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "82975da" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" } collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "82975da" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" } collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "82975da" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" } collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "82975da" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" } collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "82975da" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" } collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "82975da" }
collab-define = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" } collab-define = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "82975da" }
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" } collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "82975da" }

View File

@ -660,7 +660,7 @@ dependencies = [
[[package]] [[package]]
name = "client-api" name = "client-api"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b0c213#b0c213b5c0b941e2013b0126e200f98201a82f54" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=926da91#926da912eab806e5b325e12103d341cea536aa2b"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -673,7 +673,6 @@ dependencies = [
"gotrue-entity", "gotrue-entity",
"lib0", "lib0",
"mime", "mime",
"opener",
"parking_lot", "parking_lot",
"reqwest", "reqwest",
"scraper", "scraper",
@ -720,7 +719,7 @@ dependencies = [
[[package]] [[package]]
name = "collab" name = "collab"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=86c5e8#86c5e8891af93de2d035a57214894e854d39662a" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=82975da#82975da119a0c9d0e8b70585966bb89ed6c97cd3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -739,7 +738,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-database" name = "collab-database"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=86c5e8#86c5e8891af93de2d035a57214894e854d39662a" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=82975da#82975da119a0c9d0e8b70585966bb89ed6c97cd3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -769,7 +768,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-define" name = "collab-define"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=86c5e8#86c5e8891af93de2d035a57214894e854d39662a" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=82975da#82975da119a0c9d0e8b70585966bb89ed6c97cd3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -783,7 +782,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-derive" name = "collab-derive"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=86c5e8#86c5e8891af93de2d035a57214894e854d39662a" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=82975da#82975da119a0c9d0e8b70585966bb89ed6c97cd3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -795,7 +794,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-document" name = "collab-document"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=86c5e8#86c5e8891af93de2d035a57214894e854d39662a" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=82975da#82975da119a0c9d0e8b70585966bb89ed6c97cd3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"collab", "collab",
@ -815,7 +814,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-folder" name = "collab-folder"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=86c5e8#86c5e8891af93de2d035a57214894e854d39662a" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=82975da#82975da119a0c9d0e8b70585966bb89ed6c97cd3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -846,16 +845,18 @@ dependencies = [
"collab-persistence", "collab-persistence",
"collab-plugins", "collab-plugins",
"futures", "futures",
"lib-infra",
"parking_lot", "parking_lot",
"serde", "serde",
"serde_json", "serde_json",
"tokio",
"tracing", "tracing",
] ]
[[package]] [[package]]
name = "collab-persistence" name = "collab-persistence"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=86c5e8#86c5e8891af93de2d035a57214894e854d39662a" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=82975da#82975da119a0c9d0e8b70585966bb89ed6c97cd3"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bincode", "bincode",
@ -876,7 +877,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-plugins" name = "collab-plugins"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=86c5e8#86c5e8891af93de2d035a57214894e854d39662a" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=82975da#82975da119a0c9d0e8b70585966bb89ed6c97cd3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -903,7 +904,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-user" name = "collab-user"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=86c5e8#86c5e8891af93de2d035a57214894e854d39662a" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=82975da#82975da119a0c9d0e8b70585966bb89ed6c97cd3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"collab", "collab",
@ -1135,7 +1136,7 @@ dependencies = [
"cssparser-macros", "cssparser-macros",
"dtoa-short", "dtoa-short",
"itoa", "itoa",
"phf 0.11.2", "phf 0.8.0",
"smallvec", "smallvec",
] ]
@ -1262,13 +1263,15 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]] [[package]]
name = "database-entity" name = "database-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b0c213#b0c213b5c0b941e2013b0126e200f98201a82f54" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=926da91#926da912eab806e5b325e12103d341cea536aa2b"
dependencies = [ dependencies = [
"anyhow",
"chrono", "chrono",
"collab-define", "collab-define",
"serde", "serde",
"serde_json", "serde_json",
"sqlx", "sqlx",
"thiserror",
"uuid", "uuid",
"validator", "validator",
] ]
@ -2434,7 +2437,7 @@ dependencies = [
[[package]] [[package]]
name = "gotrue" name = "gotrue"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b0c213#b0c213b5c0b941e2013b0126e200f98201a82f54" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=926da91#926da912eab806e5b325e12103d341cea536aa2b"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"futures-util", "futures-util",
@ -2444,12 +2447,13 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",
"tracing",
] ]
[[package]] [[package]]
name = "gotrue-entity" name = "gotrue-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b0c213#b0c213b5c0b941e2013b0126e200f98201a82f54" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=926da91#926da912eab806e5b325e12103d341cea536aa2b"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"reqwest", "reqwest",
@ -2807,7 +2811,7 @@ dependencies = [
[[package]] [[package]]
name = "infra" name = "infra"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b0c213#b0c213b5c0b941e2013b0126e200f98201a82f54" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=926da91#926da912eab806e5b325e12103d341cea536aa2b"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"reqwest", "reqwest",
@ -3570,7 +3574,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [ dependencies = [
"phf_macros 0.8.0", "phf_macros",
"phf_shared 0.8.0", "phf_shared 0.8.0",
"proc-macro-hack", "proc-macro-hack",
] ]
@ -3590,7 +3594,6 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [ dependencies = [
"phf_macros 0.11.2",
"phf_shared 0.11.2", "phf_shared 0.11.2",
] ]
@ -3658,19 +3661,6 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "phf_macros"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
dependencies = [
"phf_generator 0.11.2",
"phf_shared 0.11.2",
"proc-macro2",
"quote",
"syn 2.0.31",
]
[[package]] [[package]]
name = "phf_shared" name = "phf_shared"
version = "0.8.0" version = "0.8.0"
@ -4821,9 +4811,10 @@ dependencies = [
[[package]] [[package]]
name = "shared_entity" name = "shared_entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b0c213#b0c213b5c0b941e2013b0126e200f98201a82f54" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=926da91#926da912eab806e5b325e12103d341cea536aa2b"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"database-entity",
"gotrue-entity", "gotrue-entity",
"opener", "opener",
"reqwest", "reqwest",
@ -5432,7 +5423,9 @@ checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"log", "log",
"native-tls",
"tokio", "tokio",
"tokio-native-tls",
"tungstenite", "tungstenite",
] ]
@ -5660,6 +5653,7 @@ dependencies = [
"http", "http",
"httparse", "httparse",
"log", "log",
"native-tls",
"rand 0.8.5", "rand 0.8.5",
"sha1", "sha1",
"thiserror", "thiserror",

View File

@ -82,7 +82,7 @@ incremental = false
# Run the script: # Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id # scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "b0c213" } client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "926da91" }
# Please use the following script to update collab. # Please use the following script to update collab.
# Working directory: frontend # Working directory: frontend
# #
@ -92,11 +92,11 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "b0c
# To switch to the local path, run: # To switch to the local path, run:
# scripts/tool/update_collab_source.sh # scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" } collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "82975da" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" } collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "82975da" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" } collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "82975da" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" } collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "82975da" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" } collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "82975da" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" } collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "82975da" }
collab-define = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" } collab-define = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "82975da" }
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "86c5e8" } collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "82975da" }

View File

@ -20,6 +20,8 @@ tracing = "0.1"
parking_lot = "0.12.1" parking_lot = "0.12.1"
futures = "0.3" futures = "0.3"
async-trait = "0.1.73" async-trait = "0.1.73"
tokio = {version = "1.26", features = ["sync"]}
lib-infra = { path = "../../../shared-lib/lib-infra" }
[features] [features]
default = [] default = []

View File

@ -11,8 +11,10 @@ use collab_plugins::cloud_storage::network_state::{CollabNetworkReachability, Co
use collab_plugins::local_storage::rocksdb::RocksdbDiskPlugin; use collab_plugins::local_storage::rocksdb::RocksdbDiskPlugin;
use collab_plugins::local_storage::CollabPersistenceConfig; use collab_plugins::local_storage::CollabPersistenceConfig;
use collab_plugins::snapshot::{CollabSnapshotPlugin, SnapshotPersistence}; use collab_plugins::snapshot::{CollabSnapshotPlugin, SnapshotPersistence};
use futures::executor::block_on;
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
use tracing::trace;
use lib_infra::future::{to_fut, Fut};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum CollabSource { pub enum CollabSource {
@ -36,19 +38,17 @@ pub enum CollabPluginContext {
}, },
} }
#[async_trait]
pub trait CollabStorageProvider: Send + Sync + 'static { pub trait CollabStorageProvider: Send + Sync + 'static {
fn storage_source(&self) -> CollabSource; fn storage_source(&self) -> CollabSource;
async fn get_plugins( fn get_plugins(
&self, &self,
context: CollabPluginContext, context: CollabPluginContext,
) -> Vec<Arc<dyn collab::core::collab_plugin::CollabPlugin>>; ) -> Fut<Vec<Arc<dyn collab::core::collab_plugin::CollabPlugin>>>;
fn is_sync_enabled(&self) -> bool; fn is_sync_enabled(&self) -> bool;
} }
#[async_trait]
impl<T> CollabStorageProvider for Arc<T> impl<T> CollabStorageProvider for Arc<T>
where where
T: CollabStorageProvider, T: CollabStorageProvider,
@ -57,8 +57,8 @@ where
(**self).storage_source() (**self).storage_source()
} }
async fn get_plugins(&self, context: CollabPluginContext) -> Vec<Arc<dyn CollabPlugin>> { fn get_plugins(&self, context: CollabPluginContext) -> Fut<Vec<Arc<dyn CollabPlugin>>> {
(**self).get_plugins(context).await (**self).get_plugins(context)
} }
fn is_sync_enabled(&self) -> bool { fn is_sync_enabled(&self) -> bool {
@ -69,7 +69,7 @@ where
pub struct AppFlowyCollabBuilder { pub struct AppFlowyCollabBuilder {
network_reachability: CollabNetworkReachability, network_reachability: CollabNetworkReachability,
workspace_id: RwLock<Option<String>>, workspace_id: RwLock<Option<String>>,
cloud_storage: RwLock<Arc<dyn CollabStorageProvider>>, cloud_storage: tokio::sync::RwLock<Arc<dyn CollabStorageProvider>>,
snapshot_persistence: Mutex<Option<Arc<dyn SnapshotPersistence>>>, snapshot_persistence: Mutex<Option<Arc<dyn SnapshotPersistence>>>,
device_id: Mutex<String>, device_id: Mutex<String>,
} }
@ -79,7 +79,7 @@ impl AppFlowyCollabBuilder {
Self { Self {
network_reachability: CollabNetworkReachability::new(), network_reachability: CollabNetworkReachability::new(),
workspace_id: Default::default(), workspace_id: Default::default(),
cloud_storage: RwLock::new(Arc::new(storage_provider)), cloud_storage: tokio::sync::RwLock::new(Arc::new(storage_provider)),
snapshot_persistence: Default::default(), snapshot_persistence: Default::default(),
device_id: Default::default(), device_id: Default::default(),
} }
@ -141,7 +141,7 @@ impl AppFlowyCollabBuilder {
/// - `raw_data`: The raw data of the collaboration object, defined by the [CollabRawData] type. /// - `raw_data`: The raw data of the collaboration object, defined by the [CollabRawData] type.
/// - `collab_db`: A weak reference to the [RocksCollabDB]. /// - `collab_db`: A weak reference to the [RocksCollabDB].
/// ///
pub fn build( pub async fn build(
&self, &self,
uid: i64, uid: i64,
object_id: &str, object_id: &str,
@ -149,14 +149,16 @@ impl AppFlowyCollabBuilder {
raw_data: CollabRawData, raw_data: CollabRawData,
collab_db: Weak<RocksCollabDB>, collab_db: Weak<RocksCollabDB>,
) -> Result<Arc<MutexCollab>, Error> { ) -> Result<Arc<MutexCollab>, Error> {
self.build_with_config( self
uid, .build_with_config(
object_id, uid,
object_type, object_id,
collab_db, object_type,
raw_data, collab_db,
&CollabPersistenceConfig::default(), raw_data,
) &CollabPersistenceConfig::default(),
)
.await
} }
/// Creates a new collaboration builder with the custom configuration. /// Creates a new collaboration builder with the custom configuration.
@ -173,7 +175,7 @@ impl AppFlowyCollabBuilder {
/// - `raw_data`: The raw data of the collaboration object, defined by the [CollabRawData] type. /// - `raw_data`: The raw data of the collaboration object, defined by the [CollabRawData] type.
/// - `collab_db`: A weak reference to the [RocksCollabDB]. /// - `collab_db`: A weak reference to the [RocksCollabDB].
/// ///
pub fn build_with_config( pub async fn build_with_config(
&self, &self,
uid: i64, uid: i64,
object_id: &str, object_id: &str,
@ -194,21 +196,26 @@ impl AppFlowyCollabBuilder {
.build()?, .build()?,
); );
{ {
let cloud_storage = self.cloud_storage.read(); let cloud_storage_type = self.cloud_storage.read().await.storage_source();
let cloud_storage_type = cloud_storage.storage_source();
let collab_object = self.collab_object(uid, object_id, object_type)?; let collab_object = self.collab_object(uid, object_id, object_type)?;
match cloud_storage_type { match cloud_storage_type {
CollabSource::AFCloud => { CollabSource::AFCloud => {
#[cfg(feature = "appflowy_cloud_integrate")] #[cfg(feature = "appflowy_cloud_integrate")]
{ {
trace!("init appflowy cloud collab plugins");
let local_collab = Arc::downgrade(&collab); let local_collab = Arc::downgrade(&collab);
let plugins = block_on( let plugins = self
cloud_storage.get_plugins(CollabPluginContext::AppFlowyCloud { .cloud_storage
.read()
.await
.get_plugins(CollabPluginContext::AppFlowyCloud {
uid, uid,
collab_object: collab_object.clone(), collab_object: collab_object.clone(),
local_collab, local_collab,
}), })
); .await;
trace!("add appflowy cloud collab plugins: {}", plugins.len());
for plugin in plugins { for plugin in plugins {
collab.lock().add_plugin(plugin); collab.lock().add_plugin(plugin);
} }
@ -217,14 +224,20 @@ impl AppFlowyCollabBuilder {
CollabSource::Supabase => { CollabSource::Supabase => {
#[cfg(feature = "supabase_integrate")] #[cfg(feature = "supabase_integrate")]
{ {
trace!("init supabase collab plugins");
let local_collab = Arc::downgrade(&collab); let local_collab = Arc::downgrade(&collab);
let local_collab_db = collab_db.clone(); let local_collab_db = collab_db.clone();
let plugins = block_on(cloud_storage.get_plugins(CollabPluginContext::Supabase { let plugins = self
uid, .cloud_storage
collab_object: collab_object.clone(), .read()
local_collab, .await
local_collab_db, .get_plugins(CollabPluginContext::Supabase {
})); uid,
collab_object: collab_object.clone(),
local_collab,
local_collab_db,
})
.await;
for plugin in plugins { for plugin in plugins {
collab.lock().add_plugin(plugin); collab.lock().add_plugin(plugin);
} }
@ -248,7 +261,7 @@ impl AppFlowyCollabBuilder {
} }
} }
block_on(collab.async_initialize()); collab.lock().initialize();
Ok(collab) Ok(collab)
} }
} }
@ -261,8 +274,8 @@ impl CollabStorageProvider for DefaultCollabStorageProvider {
CollabSource::Local CollabSource::Local
} }
async fn get_plugins(&self, _context: CollabPluginContext) -> Vec<Arc<dyn CollabPlugin>> { fn get_plugins(&self, _context: CollabPluginContext) -> Fut<Vec<Arc<dyn CollabPlugin>>> {
vec![] to_fut(async move { vec![] })
} }
fn is_sync_enabled(&self) -> bool { fn is_sync_enabled(&self) -> bool {

View File

@ -20,8 +20,7 @@ use flowy_storage::{FileStorageService, StorageObject};
use flowy_user::event_map::UserCloudServiceProvider; use flowy_user::event_map::UserCloudServiceProvider;
use flowy_user_deps::cloud::UserCloudService; use flowy_user_deps::cloud::UserCloudService;
use flowy_user_deps::entities::AuthType; use flowy_user_deps::entities::AuthType;
use lib_infra::async_trait::async_trait; use lib_infra::future::{to_fut, Fut, FutureResult};
use lib_infra::future::FutureResult;
use crate::integrate::server::{ServerProvider, ServerType, SERVER_PROVIDER_TYPE_KEY}; use crate::integrate::server::{ServerProvider, ServerType, SERVER_PROVIDER_TYPE_KEY};
@ -258,48 +257,53 @@ impl DocumentCloudService for ServerProvider {
} }
} }
#[async_trait]
impl CollabStorageProvider for ServerProvider { impl CollabStorageProvider for ServerProvider {
fn storage_source(&self) -> CollabSource { fn storage_source(&self) -> CollabSource {
self.get_server_type().into() self.get_server_type().into()
} }
async fn get_plugins(&self, context: CollabPluginContext) -> Vec<Arc<dyn CollabPlugin>> { fn get_plugins(&self, context: CollabPluginContext) -> Fut<Vec<Arc<dyn CollabPlugin>>> {
let mut plugins: Vec<Arc<dyn CollabPlugin>> = vec![];
match context { match context {
CollabPluginContext::Local => {}, CollabPluginContext::Local => to_fut(async move { vec![] }),
CollabPluginContext::AppFlowyCloud { CollabPluginContext::AppFlowyCloud {
uid: _, uid: _,
collab_object, collab_object,
local_collab, local_collab,
} => { } => {
if let Ok(server) = self.get_server(&ServerType::AFCloud) { if let Ok(server) = self.get_server(&ServerType::AFCloud) {
match server.collab_ws_channel(&collab_object.object_id).await { to_fut(async move {
Ok(Some((channel, ws_connect_state))) => { let mut plugins: Vec<Arc<dyn CollabPlugin>> = vec![];
let origin = CollabOrigin::Client(CollabClient::new( match server.collab_ws_channel(&collab_object.object_id).await {
collab_object.uid, Ok(Some((channel, ws_connect_state))) => {
collab_object.device_id.clone(), let origin = CollabOrigin::Client(CollabClient::new(
)); collab_object.uid,
let sync_object = SyncObject::from(collab_object); collab_object.device_id.clone(),
let (sink, stream) = (channel.sink(), channel.stream()); ));
let sink_config = SinkConfig::new().with_timeout(6); let sync_object = SyncObject::from(collab_object);
let sync_plugin = SyncPlugin::new( let (sink, stream) = (channel.sink(), channel.stream());
origin, let sink_config = SinkConfig::new().with_timeout(6);
sync_object, let sync_plugin = SyncPlugin::new(
local_collab, origin,
sink, sync_object,
sink_config, local_collab,
stream, sink,
Some(channel), sink_config,
ws_connect_state, stream,
); Some(channel),
plugins.push(Arc::new(sync_plugin)); ws_connect_state,
}, );
Ok(None) => { plugins.push(Arc::new(sync_plugin));
tracing::error!("🔴Failed to get collab ws channel: channel is none"); },
}, Ok(None) => {
Err(err) => tracing::error!("🔴Failed to get collab ws channel: {:?}", err), tracing::error!("🔴Failed to get collab ws channel: channel is none");
} },
Err(err) => tracing::error!("🔴Failed to get collab ws channel: {:?}", err),
}
plugins
})
} else {
to_fut(async move { vec![] })
} }
}, },
CollabPluginContext::Supabase { CollabPluginContext::Supabase {
@ -308,6 +312,7 @@ impl CollabStorageProvider for ServerProvider {
local_collab, local_collab,
local_collab_db, local_collab_db,
} => { } => {
let mut plugins: Vec<Arc<dyn CollabPlugin>> = vec![];
if let Some(remote_collab_storage) = self if let Some(remote_collab_storage) = self
.get_server(&ServerType::Supabase) .get_server(&ServerType::Supabase)
.ok() .ok()
@ -322,9 +327,10 @@ impl CollabStorageProvider for ServerProvider {
local_collab_db, local_collab_db,
))); )));
} }
to_fut(async move { plugins })
}, },
} }
plugins
} }
fn is_sync_enabled(&self) -> bool { fn is_sync_enabled(&self) -> bool {

View File

@ -1,5 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use anyhow::Context;
use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::collab_builder::AppFlowyCollabBuilder;
use flowy_database2::DatabaseManager; use flowy_database2::DatabaseManager;
use flowy_document2::manager::DocumentManager; use flowy_document2::manager::DocumentManager;
@ -130,18 +132,22 @@ impl UserStatusCallback for UserStatusCallbackImpl {
}, },
&user_workspace.id, &user_workspace.id,
) )
.await?; .await
.context("FolderManager error")?;
database_manager database_manager
.initialize_with_new_user( .initialize_with_new_user(
user_profile.uid, user_profile.uid,
user_workspace.id.clone(), user_workspace.id.clone(),
user_workspace.database_views_aggregate_id, user_workspace.database_views_aggregate_id,
) )
.await?; .await
.context("DatabaseManager error")?;
document_manager document_manager
.initialize_with_new_user(user_profile.uid, user_workspace.id) .initialize_with_new_user(user_profile.uid, user_workspace.id)
.await?; .await
.context("DocumentManager error")?;
Ok(()) Ok(())
}) })
} }

View File

@ -11,7 +11,9 @@ use collab_database::user::{
}; };
use collab_database::views::{CreateDatabaseParams, CreateViewParams, DatabaseLayout}; use collab_database::views::{CreateDatabaseParams, CreateViewParams, DatabaseLayout};
use collab_define::CollabType; use collab_define::CollabType;
use futures::executor::block_on;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tracing::{instrument, trace};
use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::collab_builder::AppFlowyCollabBuilder;
use collab_integrate::{CollabPersistenceConfig, RocksCollabDB}; use collab_integrate::{CollabPersistenceConfig, RocksCollabDB};
@ -87,7 +89,7 @@ impl DatabaseManager {
// If the workspace database not exist in disk, try to fetch from remote. // If the workspace database not exist in disk, try to fetch from remote.
if !self.is_collab_exist(uid, &collab_db, &database_views_aggregate_id) { if !self.is_collab_exist(uid, &collab_db, &database_views_aggregate_id) {
tracing::trace!("workspace database not exist, try to fetch from remote"); trace!("workspace database not exist, try to fetch from remote");
match self match self
.cloud_service .cloud_service
.get_collab_update(&database_views_aggregate_id, CollabType::WorkspaceDatabase) .get_collab_update(&database_views_aggregate_id, CollabType::WorkspaceDatabase)
@ -106,7 +108,7 @@ impl DatabaseManager {
} }
// Construct the workspace database. // Construct the workspace database.
tracing::trace!("open workspace database: {}", &database_views_aggregate_id); trace!("open workspace database: {}", &database_views_aggregate_id);
let collab = collab_builder.build_collab_with_config( let collab = collab_builder.build_collab_with_config(
uid, uid,
&database_views_aggregate_id, &database_views_aggregate_id,
@ -125,6 +127,7 @@ impl DatabaseManager {
Ok(()) Ok(())
} }
#[instrument(level = "debug", skip_all, err)]
pub async fn initialize_with_new_user( pub async fn initialize_with_new_user(
&self, &self,
user_id: i64, user_id: i64,
@ -170,7 +173,7 @@ impl DatabaseManager {
} }
pub async fn open_database(&self, database_id: &str) -> FlowyResult<Arc<DatabaseEditor>> { pub async fn open_database(&self, database_id: &str) -> FlowyResult<Arc<DatabaseEditor>> {
tracing::trace!("create new editor for database {}", database_id); trace!("create new editor for database {}", database_id);
let mut editors = self.editors.write().await; let mut editors = self.editors.write().await;
let wdb = self.get_workspace_database().await?; let wdb = self.get_workspace_database().await?;
@ -353,7 +356,7 @@ fn subscribe_block_event(workspace_database: &WorkspaceDatabase) {
match event { match event {
BlockEvent::DidFetchRow(row_details) => { BlockEvent::DidFetchRow(row_details) => {
for row_detail in row_details { for row_detail in row_details {
tracing::trace!("Did fetch row: {:?}", row_detail.row.id); trace!("Did fetch row: {:?}", row_detail.row.id);
let row_id = row_detail.row.id.clone(); let row_id = row_detail.row.id.clone();
let pb = DidFetchRowPB::from(row_detail); let pb = DidFetchRowPB::from(row_detail);
send_notification(&row_id, DatabaseNotification::DidFetchRow) send_notification(&row_id, DatabaseNotification::DidFetchRow)
@ -426,16 +429,14 @@ impl DatabaseCollabService for UserDatabaseCollabServiceImpl {
collab_raw_data: CollabRawData, collab_raw_data: CollabRawData,
config: &CollabPersistenceConfig, config: &CollabPersistenceConfig,
) -> Arc<MutexCollab> { ) -> Arc<MutexCollab> {
self block_on(self.collab_builder.build_with_config(
.collab_builder uid,
.build_with_config( object_id,
uid, object_type,
object_id, collab_db,
object_type, collab_raw_data,
collab_db, config,
collab_raw_data, ))
config, .unwrap()
)
.unwrap()
} }
} }

View File

@ -8,6 +8,7 @@ use collab_document::document::Document;
use collab_document::document_data::default_document_data; use collab_document::document_data::default_document_data;
use collab_document::YrsDocAction; use collab_document::YrsDocAction;
use parking_lot::RwLock; use parking_lot::RwLock;
use tracing::instrument;
use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::collab_builder::AppFlowyCollabBuilder;
use collab_integrate::RocksCollabDB; use collab_integrate::RocksCollabDB;
@ -55,6 +56,7 @@ impl DocumentManager {
Ok(()) Ok(())
} }
#[instrument(level = "debug", skip_all, err)]
pub async fn initialize_with_new_user(&self, uid: i64, workspace_id: String) -> FlowyResult<()> { pub async fn initialize_with_new_user(&self, uid: i64, workspace_id: String) -> FlowyResult<()> {
self.initialize(uid, workspace_id).await?; self.initialize(uid, workspace_id).await?;
Ok(()) Ok(())
@ -175,8 +177,22 @@ impl DocumentManager {
let db = self.user.collab_db(uid)?; let db = self.user.collab_db(uid)?;
let collab = self let collab = self
.collab_builder .collab_builder
.build(uid, doc_id, CollabType::Document, updates, db)?; .build(uid, doc_id, CollabType::Document, updates, db)
.await?;
Ok(collab) Ok(collab)
// let doc_id = doc_id.to_string();
// let (tx, rx) = oneshot::channel();
// let collab_builder = self.collab_builder.clone();
// tokio::spawn(async move {
// let collab = collab_builder
// .build(uid, &doc_id, CollabType::Document, updates, db)
// .await
// .unwrap();
// let _ = tx.send(collab);
// });
//
// Ok(rx.await.unwrap())
} }
fn is_doc_exist(&self, doc_id: &str) -> FlowyResult<bool> { fn is_doc_exist(&self, doc_id: &str) -> FlowyResult<bool> {

View File

@ -253,6 +253,9 @@ pub enum ErrorCode {
#[error("Permission denied")] #[error("Permission denied")]
NotEnoughPermissions = 83, NotEnoughPermissions = 83,
#[error("Internal server error")]
InternalServerError = 84,
} }
impl ErrorCode { impl ErrorCode {

View File

@ -99,6 +99,7 @@ impl FlowyError {
ErrorCode::UnexpectedCalendarFieldType ErrorCode::UnexpectedCalendarFieldType
); );
static_flowy_error!(collab_not_sync, ErrorCode::CollabDataNotSync); static_flowy_error!(collab_not_sync, ErrorCode::CollabDataNotSync);
static_flowy_error!(server_error, ErrorCode::InternalServerError);
} }
impl std::convert::From<ErrorCode> for FlowyError { impl std::convert::From<ErrorCode> for FlowyError {

View File

@ -8,12 +8,12 @@ impl From<AppError> for FlowyError {
client_api::error::ErrorCode::Ok => ErrorCode::Internal, client_api::error::ErrorCode::Ok => ErrorCode::Internal,
client_api::error::ErrorCode::Unhandled => ErrorCode::Internal, client_api::error::ErrorCode::Unhandled => ErrorCode::Internal,
client_api::error::ErrorCode::RecordNotFound => ErrorCode::RecordNotFound, client_api::error::ErrorCode::RecordNotFound => ErrorCode::RecordNotFound,
client_api::error::ErrorCode::FileNotFound => ErrorCode::RecordNotFound,
client_api::error::ErrorCode::RecordAlreadyExists => ErrorCode::RecordAlreadyExists, client_api::error::ErrorCode::RecordAlreadyExists => ErrorCode::RecordAlreadyExists,
client_api::error::ErrorCode::InvalidEmail => ErrorCode::EmailFormatInvalid, client_api::error::ErrorCode::InvalidEmail => ErrorCode::EmailFormatInvalid,
client_api::error::ErrorCode::InvalidPassword => ErrorCode::PasswordFormatInvalid, client_api::error::ErrorCode::InvalidPassword => ErrorCode::PasswordFormatInvalid,
client_api::error::ErrorCode::OAuthError => ErrorCode::UserUnauthorized, client_api::error::ErrorCode::OAuthError => ErrorCode::UserUnauthorized,
client_api::error::ErrorCode::MissingPayload => ErrorCode::MissingPayload, client_api::error::ErrorCode::MissingPayload => ErrorCode::MissingPayload,
client_api::error::ErrorCode::StorageError => ErrorCode::Internal,
client_api::error::ErrorCode::OpenError => ErrorCode::Internal, client_api::error::ErrorCode::OpenError => ErrorCode::Internal,
client_api::error::ErrorCode::InvalidUrl => ErrorCode::InvalidURL, client_api::error::ErrorCode::InvalidUrl => ErrorCode::InvalidURL,
client_api::error::ErrorCode::InvalidRequestParams => ErrorCode::InvalidParams, client_api::error::ErrorCode::InvalidRequestParams => ErrorCode::InvalidParams,

View File

@ -12,7 +12,7 @@ use collab_folder::core::{
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
use tokio_stream::wrappers::WatchStream; use tokio_stream::wrappers::WatchStream;
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
use tracing::{event, Level}; use tracing::{event, info, instrument, Level};
use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::collab_builder::AppFlowyCollabBuilder;
use collab_integrate::{CollabPersistenceConfig, RocksCollabDB, YrsDocAction}; use collab_integrate::{CollabPersistenceConfig, RocksCollabDB, YrsDocAction};
@ -159,13 +159,17 @@ impl FolderManager {
} => { } => {
let is_exist = is_exist_in_local_disk(&self.user, &workspace_id).unwrap_or(false); let is_exist = is_exist_in_local_disk(&self.user, &workspace_id).unwrap_or(false);
if is_exist { if is_exist {
let collab = self.collab_for_folder(uid, &workspace_id, collab_db, vec![])?; let collab = self
.collab_for_folder(uid, &workspace_id, collab_db, vec![])
.await?;
Folder::open(collab, Some(folder_notifier)) Folder::open(collab, Some(folder_notifier))
} else if create_if_not_exist { } else if create_if_not_exist {
let folder_data = let folder_data =
DefaultFolderBuilder::build(uid, workspace_id.to_string(), &self.operation_handlers) DefaultFolderBuilder::build(uid, workspace_id.to_string(), &self.operation_handlers)
.await; .await;
let collab = self.collab_for_folder(uid, &workspace_id, collab_db, vec![])?; let collab = self
.collab_for_folder(uid, &workspace_id, collab_db, vec![])
.await?;
Folder::create(collab, Some(folder_notifier), Some(folder_data)) Folder::create(collab, Some(folder_notifier), Some(folder_data))
} else { } else {
return Err(FlowyError::new( return Err(FlowyError::new(
@ -178,11 +182,15 @@ impl FolderManager {
if raw_data.is_empty() { if raw_data.is_empty() {
return Err(workspace_data_not_sync_error(uid, &workspace_id)); return Err(workspace_data_not_sync_error(uid, &workspace_id));
} }
let collab = self.collab_for_folder(uid, &workspace_id, collab_db, raw_data)?; let collab = self
.collab_for_folder(uid, &workspace_id, collab_db, raw_data)
.await?;
Folder::open(collab, Some(folder_notifier)) Folder::open(collab, Some(folder_notifier))
}, },
FolderInitializeDataSource::FolderData(folder_data) => { FolderInitializeDataSource::FolderData(folder_data) => {
let collab = self.collab_for_folder(uid, &workspace_id, collab_db, vec![])?; let collab = self
.collab_for_folder(uid, &workspace_id, collab_db, vec![])
.await?;
Folder::create(collab, Some(folder_notifier), Some(folder_data)) Folder::create(collab, Some(folder_notifier), Some(folder_data))
}, },
}; };
@ -205,21 +213,24 @@ impl FolderManager {
Ok(()) Ok(())
} }
fn collab_for_folder( async fn collab_for_folder(
&self, &self,
uid: i64, uid: i64,
workspace_id: &str, workspace_id: &str,
collab_db: Weak<RocksCollabDB>, collab_db: Weak<RocksCollabDB>,
raw_data: CollabRawData, raw_data: CollabRawData,
) -> Result<Arc<MutexCollab>, FlowyError> { ) -> Result<Arc<MutexCollab>, FlowyError> {
let collab = self.collab_builder.build_with_config( let collab = self
uid, .collab_builder
workspace_id, .build_with_config(
CollabType::Folder, uid,
collab_db, workspace_id,
raw_data, CollabType::Folder,
&CollabPersistenceConfig::new().enable_snapshot(true), collab_db,
)?; raw_data,
&CollabPersistenceConfig::new().enable_snapshot(true),
)
.await?;
Ok(collab) Ok(collab)
} }
@ -236,7 +247,7 @@ impl FolderManager {
.get_folder_updates(workspace_id, user_id) .get_folder_updates(workspace_id, user_id)
.await?; .await?;
tracing::info!( info!(
"Get folder updates via {}, number of updates: {}", "Get folder updates via {}, number of updates: {}",
self.cloud_service.service_name(), self.cloud_service.service_name(),
folder_updates.len() folder_updates.len()
@ -254,6 +265,7 @@ impl FolderManager {
/// Initialize the folder for the new user. /// Initialize the folder for the new user.
/// Using the [DefaultFolderBuilder] to create the default workspace for the new user. /// Using the [DefaultFolderBuilder] to create the default workspace for the new user.
#[instrument(level = "debug", skip_all, err)]
pub async fn initialize_with_new_user( pub async fn initialize_with_new_user(
&self, &self,
user_id: i64, user_id: i64,
@ -263,29 +275,41 @@ impl FolderManager {
workspace_id: &str, workspace_id: &str,
) -> FlowyResult<()> { ) -> FlowyResult<()> {
// Create the default workspace if the user is new // Create the default workspace if the user is new
tracing::info!("initialize_when_sign_up: is_new: {}", is_new); info!("initialize_when_sign_up: is_new: {}", is_new);
if is_new { if is_new {
self.initialize(user_id, workspace_id, data_source).await?; self.initialize(user_id, workspace_id, data_source).await?;
} else { } else {
// The folder updates should not be empty, as the folder data is stored // The folder updates should not be empty, as the folder data is stored
// when the user signs up for the first time. // when the user signs up for the first time.
let folder_updates = self let result = self
.cloud_service .cloud_service
.get_folder_updates(workspace_id, user_id) .get_folder_updates(workspace_id, user_id)
.await?; .await
.map_err(FlowyError::from);
tracing::info!( match result {
"Get folder updates via {}, number of updates: {}", Ok(folder_updates) => {
self.cloud_service.service_name(), info!(
folder_updates.len() "Get folder updates via {}, number of updates: {}",
); self.cloud_service.service_name(),
self folder_updates.len()
.initialize( );
user_id, self
workspace_id, .initialize(
FolderInitializeDataSource::Cloud(folder_updates), user_id,
) workspace_id,
.await?; FolderInitializeDataSource::Cloud(folder_updates),
)
.await?;
},
Err(err) => {
if err.is_record_not_found() {
self.initialize(user_id, workspace_id, data_source).await?;
} else {
return Err(err);
}
},
}
} }
Ok(()) Ok(())
} }

View File

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

View File

@ -5,6 +5,7 @@ use collab_define::CollabType;
use collab_document::document::Document; use collab_document::document::Document;
use flowy_document_deps::cloud::*; use flowy_document_deps::cloud::*;
use flowy_error::FlowyError;
use lib_infra::future::FutureResult; use lib_infra::future::FutureResult;
use crate::af_cloud::AFServer; use crate::af_cloud::AFServer;
@ -23,7 +24,10 @@ where
object_id: document_id.to_string(), object_id: document_id.to_string(),
collab_type: CollabType::Document, collab_type: CollabType::Document,
}; };
let data = try_get_client?.get_collab(params).await?; let data = try_get_client?
.get_collab(params)
.await
.map_err(FlowyError::from)?;
Ok(vec![data]) Ok(vec![data])
}) })
} }
@ -44,7 +48,10 @@ where
object_id: document_id.clone(), object_id: document_id.clone(),
collab_type: CollabType::Document, collab_type: CollabType::Document,
}; };
let updates = vec![try_get_client?.get_collab(params).await?]; let updates = vec![try_get_client?
.get_collab(params)
.await
.map_err(FlowyError::from)?];
let document = Document::from_updates(CollabOrigin::Empty, updates, &document_id, vec![])?; let document = Document::from_updates(CollabOrigin::Empty, updates, &document_id, vec![])?;
Ok(document.get_document_data().ok()) Ok(document.get_document_data().ok())
}) })

View File

@ -3,6 +3,7 @@ use client_api::entity::QueryCollabParams;
use collab::core::origin::CollabOrigin; use collab::core::origin::CollabOrigin;
use collab_define::CollabType; use collab_define::CollabType;
use flowy_error::FlowyError;
use flowy_folder_deps::cloud::{Folder, FolderCloudService, FolderData, FolderSnapshot, Workspace}; use flowy_folder_deps::cloud::{Folder, FolderCloudService, FolderData, FolderSnapshot, Workspace};
use lib_infra::future::FutureResult; use lib_infra::future::FutureResult;
@ -26,7 +27,10 @@ where
object_id: workspace_id.clone(), object_id: workspace_id.clone(),
collab_type: CollabType::Folder, collab_type: CollabType::Folder,
}; };
let updates = vec![try_get_client?.get_collab(params).await?]; let updates = vec![try_get_client?
.get_collab(params)
.await
.map_err(FlowyError::from)?];
let folder = let folder =
Folder::from_collab_raw_data(CollabOrigin::Empty, updates, &workspace_id, vec![])?; Folder::from_collab_raw_data(CollabOrigin::Empty, updates, &workspace_id, vec![])?;
Ok(folder.get_folder_data()) Ok(folder.get_folder_data())
@ -49,8 +53,11 @@ where
object_id: workspace_id, object_id: workspace_id,
collab_type: CollabType::Folder, collab_type: CollabType::Folder,
}; };
let updates = vec![try_get_client?.get_collab(params).await?]; let update = try_get_client?
Ok(updates) .get_collab(params)
.await
.map_err(FlowyError::from)?;
Ok(vec![update])
}) })
} }

View File

@ -3,7 +3,9 @@ use std::sync::Arc;
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use client_api::entity::dto::UserUpdateParams; use client_api::entity::dto::UserUpdateParams;
use client_api::entity::{AFUserProfileView, AFWorkspace, AFWorkspaces, InsertCollabParams}; use client_api::entity::{
AFUserProfileView, AFWorkspace, AFWorkspaces, InsertCollabParams, OAuthProvider,
};
use collab_define::CollabObject; use collab_define::CollabObject;
use flowy_error::{ErrorCode, FlowyError}; use flowy_error::{ErrorCode, FlowyError};
@ -54,7 +56,7 @@ where
FutureResult::new(async move { Ok(try_get_client?.sign_out().await?) }) FutureResult::new(async move { Ok(try_get_client?.sign_out().await?) })
} }
fn generate_sign_in_callback_url(&self, email: &str) -> FutureResult<String, Error> { fn generate_sign_in_url_with_email(&self, email: &str) -> FutureResult<String, Error> {
let email = email.to_string(); let email = email.to_string();
let try_get_client = self.server.try_get_client(); let try_get_client = self.server.try_get_client();
FutureResult::new(async move { FutureResult::new(async move {
@ -62,7 +64,19 @@ where
let admin_email = std::env::var("GOTRUE_ADMIN_EMAIL").unwrap(); let admin_email = std::env::var("GOTRUE_ADMIN_EMAIL").unwrap();
let admin_password = std::env::var("GOTRUE_ADMIN_PASSWORD").unwrap(); let admin_password = std::env::var("GOTRUE_ADMIN_PASSWORD").unwrap();
let url = try_get_client? let url = try_get_client?
.generate_sign_in_callback_url(&admin_email, &admin_password, &email) .generate_sign_in_url_with_email(&admin_email, &admin_password, &email)
.await?;
Ok(url)
})
}
fn generate_oauth_url_with_provider(&self, provider: &str) -> FutureResult<String, Error> {
let provider = OAuthProvider::from(provider);
let try_get_client = self.server.try_get_client();
FutureResult::new(async move {
let provider = provider.ok_or(anyhow!("invalid provider"))?;
let url = try_get_client?
.generate_oauth_url_with_provider(&provider)
.await?; .await?;
Ok(url) Ok(url)
}) })
@ -196,7 +210,6 @@ where
FutureResult::new(async move { FutureResult::new(async move {
let client = try_get_client?; let client = try_get_client?;
let params = InsertCollabParams::new( let params = InsertCollabParams::new(
collab_object.uid,
collab_object.object_id.clone(), collab_object.object_id.clone(),
collab_object.collab_type.clone(), collab_object.collab_type.clone(),
data, data,
@ -219,7 +232,7 @@ pub async fn user_sign_in_with_url(
client: Arc<AFCloudClient>, client: Arc<AFCloudClient>,
params: AFCloudOAuthParams, params: AFCloudOAuthParams,
) -> Result<AuthResponse, FlowyError> { ) -> Result<AuthResponse, FlowyError> {
let is_new_user = client.sign_in_url(&params.sign_in_url).await?; let is_new_user = client.sign_in_with_url(&params.sign_in_url).await?;
let (profile, af_workspaces) = tokio::try_join!(client.profile(), client.workspaces())?; let (profile, af_workspaces) = tokio::try_join!(client.profile(), client.workspaces())?;
let latest_workspace = to_user_workspace( let latest_workspace = to_user_workspace(

View File

@ -7,7 +7,7 @@ use client_api::ws::{
BusinessID, WSClient, WSClientConfig, WSConnectStateReceiver, WebSocketChannel, BusinessID, WSClient, WSClientConfig, WSConnectStateReceiver, WebSocketChannel,
}; };
use client_api::Client; use client_api::Client;
use tokio::sync::RwLock; use tracing::info;
use flowy_database_deps::cloud::DatabaseCloudService; use flowy_database_deps::cloud::DatabaseCloudService;
use flowy_document_deps::cloud::DocumentCloudService; use flowy_document_deps::cloud::DocumentCloudService;
@ -33,7 +33,7 @@ pub struct AFCloudServer {
enable_sync: AtomicBool, enable_sync: AtomicBool,
#[allow(dead_code)] #[allow(dead_code)]
device_id: Arc<parking_lot::RwLock<String>>, device_id: Arc<parking_lot::RwLock<String>>,
ws_client: Arc<RwLock<WSClient>>, ws_client: Arc<WSClient>,
} }
impl AFCloudServer { impl AFCloudServer {
@ -42,13 +42,8 @@ impl AFCloudServer {
enable_sync: bool, enable_sync: bool,
device_id: Arc<parking_lot::RwLock<String>>, device_id: Arc<parking_lot::RwLock<String>>,
) -> Self { ) -> Self {
let http_client = reqwest::Client::new(); let api_client =
let api_client = client_api::Client::from( client_api::Client::new(&config.base_url, &config.ws_base_url, &config.gotrue_url);
http_client,
&config.base_url,
&config.base_ws_url,
&config.gotrue_url,
);
let token_state_rx = api_client.subscribe_token_state(); let token_state_rx = api_client.subscribe_token_state();
let enable_sync = AtomicBool::new(enable_sync); let enable_sync = AtomicBool::new(enable_sync);
@ -57,7 +52,7 @@ impl AFCloudServer {
ping_per_secs: 8, ping_per_secs: 8,
retry_connect_per_pings: 5, retry_connect_per_pings: 5,
}); });
let ws_client = Arc::new(RwLock::new(ws_client)); let ws_client = Arc::new(ws_client);
let api_client = Arc::new(api_client); let api_client = Arc::new(api_client);
spawn_ws_conn(&device_id, token_state_rx, &ws_client, &api_client); spawn_ws_conn(&device_id, token_state_rx, &ws_client, &api_client);
@ -81,7 +76,7 @@ impl AFCloudServer {
impl AppFlowyServer for AFCloudServer { impl AppFlowyServer for AFCloudServer {
fn set_enable_sync(&self, uid: i64, enable: bool) { fn set_enable_sync(&self, uid: i64, enable: bool) {
tracing::info!("{} cloud sync: {}", uid, enable); info!("{} cloud sync: {}", uid, enable);
self.enable_sync.store(enable, Ordering::SeqCst); self.enable_sync.store(enable, Ordering::SeqCst);
} }
fn user_service(&self) -> Arc<dyn UserCloudService> { fn user_service(&self) -> Arc<dyn UserCloudService> {
@ -115,14 +110,8 @@ impl AppFlowyServer for AFCloudServer {
match weak_ws_client.upgrade() { match weak_ws_client.upgrade() {
None => Ok(None), None => Ok(None),
Some(ws_client) => { Some(ws_client) => {
let channel = ws_client let channel = ws_client.subscribe(BusinessID::CollabId, object_id).ok();
.read() let connect_state_recv = ws_client.subscribe_connect_state();
.await
.subscribe(BusinessID::CollabId, object_id)
.await
.ok();
let connect_state_recv = ws_client.read().await.subscribe_connect_state().await;
Ok(channel.map(|c| (c, connect_state_recv))) Ok(channel.map(|c| (c, connect_state_recv)))
}, },
} }
@ -145,7 +134,7 @@ impl AppFlowyServer for AFCloudServer {
fn spawn_ws_conn( fn spawn_ws_conn(
device_id: &Arc<parking_lot::RwLock<String>>, device_id: &Arc<parking_lot::RwLock<String>>,
mut token_state_rx: TokenStateReceiver, mut token_state_rx: TokenStateReceiver,
ws_client: &Arc<RwLock<WSClient>>, ws_client: &Arc<WSClient>,
api_client: &Arc<Client>, api_client: &Arc<Client>,
) { ) {
let weak_device_id = Arc::downgrade(device_id); let weak_device_id = Arc::downgrade(device_id);
@ -154,7 +143,7 @@ fn spawn_ws_conn(
tokio::spawn(async move { tokio::spawn(async move {
if let Some(ws_client) = weak_ws_client.upgrade() { if let Some(ws_client) = weak_ws_client.upgrade() {
let mut state_recv = ws_client.read().await.subscribe_connect_state().await; let mut state_recv = ws_client.subscribe_connect_state();
while let Ok(state) = state_recv.recv().await { while let Ok(state) = state_recv.recv().await {
if !state.is_timeout() { if !state.is_timeout() {
continue; continue;
@ -166,8 +155,8 @@ fn spawn_ws_conn(
{ {
let device_id = device_id.read().clone(); let device_id = device_id.read().clone();
if let Ok(ws_addr) = api_client.ws_url(&device_id) { if let Ok(ws_addr) = api_client.ws_url(&device_id) {
tracing::info!("🟢WebSocket Reconnecting"); info!("🟢WebSocket Reconnecting");
let _ = ws_client.write().await.connect(ws_addr).await; let _ = ws_client.connect(ws_addr).await;
} }
} }
} }
@ -179,7 +168,7 @@ fn spawn_ws_conn(
let weak_api_client = Arc::downgrade(api_client); let weak_api_client = Arc::downgrade(api_client);
tokio::spawn(async move { tokio::spawn(async move {
while let Ok(token_state) = token_state_rx.recv().await { while let Ok(token_state) = token_state_rx.recv().await {
tracing::info!("🟢Token state: {:?}", token_state); info!("🟢Token state: {:?}", token_state);
match token_state { match token_state {
TokenState::Refresh => { TokenState::Refresh => {
if let (Some(api_client), Some(ws_client), Some(device_id)) = ( if let (Some(api_client), Some(ws_client), Some(device_id)) = (
@ -189,14 +178,14 @@ fn spawn_ws_conn(
) { ) {
let device_id = device_id.read().clone(); let device_id = device_id.read().clone();
if let Ok(ws_addr) = api_client.ws_url(&device_id) { if let Ok(ws_addr) = api_client.ws_url(&device_id) {
let _ = ws_client.write().await.connect(ws_addr).await; let _ = ws_client.connect(ws_addr).await;
} }
} }
}, },
TokenState::Invalid => { TokenState::Invalid => {
if let Some(ws_client) = weak_ws_client.upgrade() { if let Some(ws_client) = weak_ws_client.upgrade() {
tracing::info!("🟡WebSocket Disconnecting"); info!("🟡WebSocket Disconnecting");
ws_client.write().await.disconnect().await; ws_client.disconnect().await;
} }
}, },
} }

View File

@ -76,7 +76,7 @@ impl UserCloudService for LocalServerUserAuthServiceImpl {
FutureResult::new(async { Ok(()) }) FutureResult::new(async { Ok(()) })
} }
fn generate_sign_in_callback_url(&self, _email: &str) -> FutureResult<String, Error> { fn generate_sign_in_url_with_email(&self, _email: &str) -> FutureResult<String, Error> {
FutureResult::new(async { FutureResult::new(async {
Err(anyhow::anyhow!( Err(anyhow::anyhow!(
"Can't generate callback url when using offline mode" "Can't generate callback url when using offline mode"
@ -84,6 +84,10 @@ impl UserCloudService for LocalServerUserAuthServiceImpl {
}) })
} }
fn generate_oauth_url_with_provider(&self, _provider: &str) -> FutureResult<String, Error> {
FutureResult::new(async { Err(anyhow::anyhow!("Can't oauth url when using offline mode")) })
}
fn update_user( fn update_user(
&self, &self,
_credential: UserCredentials, _credential: UserCredentials,

View File

@ -165,7 +165,7 @@ where
FutureResult::new(async { Ok(()) }) FutureResult::new(async { Ok(()) })
} }
fn generate_sign_in_callback_url(&self, _email: &str) -> FutureResult<String, Error> { fn generate_sign_in_url_with_email(&self, _email: &str) -> FutureResult<String, Error> {
FutureResult::new(async { FutureResult::new(async {
Err(anyhow::anyhow!( Err(anyhow::anyhow!(
"Can't generate callback url when using supabase" "Can't generate callback url when using supabase"
@ -173,6 +173,14 @@ where
}) })
} }
fn generate_oauth_url_with_provider(&self, _provider: &str) -> FutureResult<String, Error> {
FutureResult::new(async {
Err(anyhow::anyhow!(
"Can't generate oauth url when using supabase"
))
})
}
fn update_user( fn update_user(
&self, &self,
_credential: UserCredentials, _credential: UserCredentials,

View File

@ -23,18 +23,13 @@ pub fn af_cloud_server(config: AFCloudConfiguration) -> Arc<AFCloudServer> {
} }
pub async fn generate_sign_in_url(user_email: &str, config: &AFCloudConfiguration) -> String { pub async fn generate_sign_in_url(user_email: &str, config: &AFCloudConfiguration) -> String {
let http_client = reqwest::Client::new(); let api_client =
let api_client = client_api::Client::from( client_api::Client::new(&config.base_url, &config.ws_base_url, &config.gotrue_url);
http_client,
&config.base_url,
&config.base_ws_url,
&config.gotrue_url,
);
let admin_email = std::env::var("GOTRUE_ADMIN_EMAIL").unwrap(); let admin_email = std::env::var("GOTRUE_ADMIN_EMAIL").unwrap();
let admin_password = std::env::var("GOTRUE_ADMIN_PASSWORD").unwrap(); let admin_password = std::env::var("GOTRUE_ADMIN_PASSWORD").unwrap();
api_client api_client
.generate_sign_in_callback_url(&admin_email, &admin_password, user_email) .generate_sign_in_url_with_email(&admin_email, &admin_password, user_email)
.await .await
.unwrap() .unwrap()
} }

View File

@ -29,7 +29,7 @@ use flowy_notification::entities::SubscribeObject;
use flowy_notification::{register_notification_sender, NotificationSender}; use flowy_notification::{register_notification_sender, NotificationSender};
use flowy_server::supabase::define::{USER_DEVICE_ID, USER_EMAIL, USER_SIGN_IN_URL, USER_UUID}; use flowy_server::supabase::define::{USER_DEVICE_ID, USER_EMAIL, USER_SIGN_IN_URL, USER_UUID};
use flowy_user::entities::{ use flowy_user::entities::{
AuthTypePB, OAuthCallbackRequestPB, OAuthCallbackResponsePB, OAuthPB, UpdateCloudConfigPB, AuthTypePB, OauthSignInPB, SignInUrlPB, SignInUrlPayloadPB, UpdateCloudConfigPB,
UserCloudConfigPB, UserProfilePB, UserCloudConfigPB, UserProfilePB,
}; };
use flowy_user::errors::{FlowyError, FlowyResult}; use flowy_user::errors::{FlowyError, FlowyResult};
@ -169,13 +169,13 @@ impl FlowyCoreTest {
pub async fn supabase_party_sign_up(&self) -> UserProfilePB { pub async fn supabase_party_sign_up(&self) -> UserProfilePB {
let map = third_party_sign_up_param(Uuid::new_v4().to_string()); let map = third_party_sign_up_param(Uuid::new_v4().to_string());
let payload = OAuthPB { let payload = OauthSignInPB {
map, map,
auth_type: AuthTypePB::Supabase, auth_type: AuthTypePB::Supabase,
}; };
EventBuilder::new(self.clone()) EventBuilder::new(self.clone())
.event(OAuth) .event(OauthSignIn)
.payload(payload) .payload(payload)
.async_send() .async_send()
.await .await
@ -198,28 +198,28 @@ impl FlowyCoreTest {
} }
pub async fn af_cloud_sign_in_with_email(&self, email: &str) -> FlowyResult<UserProfilePB> { pub async fn af_cloud_sign_in_with_email(&self, email: &str) -> FlowyResult<UserProfilePB> {
let payload = OAuthCallbackRequestPB { let payload = SignInUrlPayloadPB {
email: email.to_string(), email: email.to_string(),
auth_type: AuthTypePB::AFCloud, auth_type: AuthTypePB::AFCloud,
}; };
let sign_in_url = EventBuilder::new(self.clone()) let sign_in_url = EventBuilder::new(self.clone())
.event(OAuthCallbackURL) .event(GetSignInURL)
.payload(payload) .payload(payload)
.async_send() .async_send()
.await .await
.try_parse::<OAuthCallbackResponsePB>()? .try_parse::<SignInUrlPB>()?
.sign_in_url; .sign_in_url;
let mut map = HashMap::new(); let mut map = HashMap::new();
map.insert(USER_SIGN_IN_URL.to_string(), sign_in_url); map.insert(USER_SIGN_IN_URL.to_string(), sign_in_url);
map.insert(USER_DEVICE_ID.to_string(), uuid::Uuid::new_v4().to_string()); map.insert(USER_DEVICE_ID.to_string(), uuid::Uuid::new_v4().to_string());
let payload = OAuthPB { let payload = OauthSignInPB {
map, map,
auth_type: AuthTypePB::AFCloud, auth_type: AuthTypePB::AFCloud,
}; };
let user_profile = EventBuilder::new(self.clone()) let user_profile = EventBuilder::new(self.clone())
.event(OAuth) .event(OauthSignIn)
.payload(payload) .payload(payload)
.async_send() .async_send()
.await .await
@ -240,13 +240,13 @@ impl FlowyCoreTest {
USER_EMAIL.to_string(), USER_EMAIL.to_string(),
email.unwrap_or_else(|| format!("{}@appflowy.io", nanoid!(10))), email.unwrap_or_else(|| format!("{}@appflowy.io", nanoid!(10))),
); );
let payload = OAuthPB { let payload = OauthSignInPB {
map, map,
auth_type: AuthTypePB::Supabase, auth_type: AuthTypePB::Supabase,
}; };
let user_profile = EventBuilder::new(self.clone()) let user_profile = EventBuilder::new(self.clone())
.event(OAuth) .event(OauthSignIn)
.payload(payload) .payload(payload)
.async_send() .async_send()
.await .await

View File

@ -14,7 +14,7 @@ use flowy_server::supabase::define::{USER_EMAIL, USER_UUID};
use flowy_test::document::document_event::DocumentEventTest; use flowy_test::document::document_event::DocumentEventTest;
use flowy_test::event_builder::EventBuilder; use flowy_test::event_builder::EventBuilder;
use flowy_test::FlowyCoreTest; use flowy_test::FlowyCoreTest;
use flowy_user::entities::{AuthTypePB, OAuthPB, UpdateUserProfilePayloadPB, UserProfilePB}; use flowy_user::entities::{AuthTypePB, OauthSignInPB, UpdateUserProfilePayloadPB, UserProfilePB};
use flowy_user::errors::ErrorCode; use flowy_user::errors::ErrorCode;
use flowy_user::event_map::UserEvent::*; use flowy_user::event_map::UserEvent::*;
@ -30,13 +30,13 @@ async fn third_party_sign_up_test() {
USER_EMAIL.to_string(), USER_EMAIL.to_string(),
format!("{}@appflowy.io", nanoid!(6)), format!("{}@appflowy.io", nanoid!(6)),
); );
let payload = OAuthPB { let payload = OauthSignInPB {
map, map,
auth_type: AuthTypePB::Supabase, auth_type: AuthTypePB::Supabase,
}; };
let response = EventBuilder::new(test.clone()) let response = EventBuilder::new(test.clone())
.event(OAuth) .event(OauthSignIn)
.payload(payload) .payload(payload)
.async_send() .async_send()
.await .await
@ -72,8 +72,8 @@ async fn third_party_sign_up_with_duplicated_uuid() {
map.insert(USER_EMAIL.to_string(), email.clone()); map.insert(USER_EMAIL.to_string(), email.clone());
let response_1 = EventBuilder::new(test.clone()) let response_1 = EventBuilder::new(test.clone())
.event(OAuth) .event(OauthSignIn)
.payload(OAuthPB { .payload(OauthSignInPB {
map: map.clone(), map: map.clone(),
auth_type: AuthTypePB::Supabase, auth_type: AuthTypePB::Supabase,
}) })
@ -83,8 +83,8 @@ async fn third_party_sign_up_with_duplicated_uuid() {
dbg!(&response_1); dbg!(&response_1);
let response_2 = EventBuilder::new(test.clone()) let response_2 = EventBuilder::new(test.clone())
.event(OAuth) .event(OauthSignIn)
.payload(OAuthPB { .payload(OauthSignInPB {
map: map.clone(), map: map.clone(),
auth_type: AuthTypePB::Supabase, auth_type: AuthTypePB::Supabase,
}) })

View File

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

View File

@ -70,8 +70,15 @@ pub trait UserCloudService: Send + Sync + 'static {
/// Sign out an account /// Sign out an account
fn sign_out(&self, token: Option<String>) -> FutureResult<(), Error>; fn sign_out(&self, token: Option<String>) -> FutureResult<(), Error>;
/// Generate a sign in callback url for the user with the given email /// Generate a sign in url for the user with the given email
fn generate_sign_in_callback_url(&self, email: &str) -> FutureResult<String, Error>; fn generate_sign_in_url_with_email(&self, email: &str) -> FutureResult<String, Error>;
/// When the user opens the OAuth URL, it redirects to the corresponding provider's OAuth web page.
/// After the user is authenticated, the browser will open a deep link to the AppFlowy app (iOS, macOS, etc.),
/// which will call [Client::sign_in_with_url] to sign in.
///
/// For example, the OAuth URL on Google looks like `https://appflowy.io/authorize?provider=google`.
fn generate_oauth_url_with_provider(&self, provider: &str) -> FutureResult<String, Error>;
/// Using the user's token to update the user information /// Using the user's token to update the user information
fn update_user( fn update_user(

View File

@ -79,7 +79,7 @@ impl TryInto<SignUpParams> for SignUpPayloadPB {
} }
#[derive(ProtoBuf, Default)] #[derive(ProtoBuf, Default)]
pub struct OAuthPB { pub struct OauthSignInPB {
/// Use this field to store the third party auth information. /// Use this field to store the third party auth information.
/// Different auth type has different fields. /// Different auth type has different fields.
/// Supabase: /// Supabase:
@ -93,7 +93,7 @@ pub struct OAuthPB {
} }
#[derive(ProtoBuf, Default)] #[derive(ProtoBuf, Default)]
pub struct OAuthCallbackRequestPB { pub struct SignInUrlPayloadPB {
#[pb(index = 1)] #[pb(index = 1)]
pub email: String, pub email: String,
@ -102,11 +102,77 @@ pub struct OAuthCallbackRequestPB {
} }
#[derive(ProtoBuf, Default)] #[derive(ProtoBuf, Default)]
pub struct OAuthCallbackResponsePB { pub struct SignInUrlPB {
#[pb(index = 1)] #[pb(index = 1)]
pub sign_in_url: String, pub sign_in_url: String,
} }
#[derive(ProtoBuf, Default)]
pub struct OauthProviderPB {
#[pb(index = 1)]
pub provider: ProviderTypePB,
}
#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone, Default)]
pub enum ProviderTypePB {
Apple = 0,
Azure = 1,
Bitbucket = 2,
Discord = 3,
Facebook = 4,
Figma = 5,
Github = 6,
Gitlab = 7,
#[default]
Google = 8,
Keycloak = 9,
Kakao = 10,
Linkedin = 11,
Notion = 12,
Spotify = 13,
Slack = 14,
Workos = 15,
Twitch = 16,
Twitter = 17,
Email = 18,
Phone = 19,
Zoom = 20,
}
impl ProviderTypePB {
pub fn as_str(&self) -> &str {
match self {
ProviderTypePB::Apple => "apple",
ProviderTypePB::Azure => "azure",
ProviderTypePB::Bitbucket => "bitbucket",
ProviderTypePB::Discord => "discord",
ProviderTypePB::Facebook => "facebook",
ProviderTypePB::Figma => "figma",
ProviderTypePB::Github => "github",
ProviderTypePB::Gitlab => "gitlab",
ProviderTypePB::Google => "google",
ProviderTypePB::Keycloak => "keycloak",
ProviderTypePB::Kakao => "kakao",
ProviderTypePB::Linkedin => "linkedin",
ProviderTypePB::Notion => "notion",
ProviderTypePB::Spotify => "spotify",
ProviderTypePB::Slack => "slack",
ProviderTypePB::Workos => "workos",
ProviderTypePB::Twitch => "twitch",
ProviderTypePB::Twitter => "twitter",
ProviderTypePB::Email => "email",
ProviderTypePB::Phone => "phone",
ProviderTypePB::Zoom => "zoom",
}
}
}
#[derive(ProtoBuf, Default)]
pub struct OauthProviderDataPB {
#[pb(index = 1)]
pub oauth_url: String,
}
#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] #[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)]
pub enum AuthTypePB { pub enum AuthTypePB {
Local = 0, Local = 0,

View File

@ -215,11 +215,9 @@ pub async fn get_user_setting(
data_result_ok(user_setting) data_result_ok(user_setting)
} }
/// Only used for third party auth.
/// Use [UserEvent::SignIn] or [UserEvent::SignUp] If the [AuthType] is Local or SelfHosted
#[tracing::instrument(level = "debug", skip(data, manager), err)] #[tracing::instrument(level = "debug", skip(data, manager), err)]
pub async fn oauth_handler( pub async fn oauth_handler(
data: AFPluginData<OAuthPB>, data: AFPluginData<OauthSignInPB>,
manager: AFPluginState<Weak<UserManager>>, manager: AFPluginState<Weak<UserManager>>,
) -> DataResult<UserProfilePB, FlowyError> { ) -> DataResult<UserProfilePB, FlowyError> {
let manager = upgrade_manager(manager)?; let manager = upgrade_manager(manager)?;
@ -230,20 +228,33 @@ pub async fn oauth_handler(
} }
#[tracing::instrument(level = "debug", skip(data, manager), err)] #[tracing::instrument(level = "debug", skip(data, manager), err)]
pub async fn get_oauth_url_handler( pub async fn get_sign_in_url_handler(
data: AFPluginData<OAuthCallbackRequestPB>, data: AFPluginData<SignInUrlPayloadPB>,
manager: AFPluginState<Weak<UserManager>>, manager: AFPluginState<Weak<UserManager>>,
) -> DataResult<OAuthCallbackResponsePB, FlowyError> { ) -> DataResult<SignInUrlPB, FlowyError> {
let manager = upgrade_manager(manager)?; let manager = upgrade_manager(manager)?;
let params = data.into_inner(); let params = data.into_inner();
let auth_type: AuthType = params.auth_type.into(); let auth_type: AuthType = params.auth_type.into();
let sign_in_url = manager let sign_in_url = manager
.generate_sign_in_callback_url(&auth_type, &params.email) .generate_sign_in_url_with_email(&auth_type, &params.email)
.await?; .await?;
let resp = OAuthCallbackResponsePB { sign_in_url }; let resp = SignInUrlPB { sign_in_url };
data_result_ok(resp) data_result_ok(resp)
} }
#[tracing::instrument(level = "debug", skip_all, err)]
pub async fn sign_in_with_provider_handler(
data: AFPluginData<OauthProviderPB>,
manager: AFPluginState<Weak<UserManager>>,
) -> DataResult<OauthProviderDataPB, FlowyError> {
let manager = upgrade_manager(manager)?;
tracing::debug!("Sign in with provider: {:?}", data.provider.as_str());
let sign_in_url = manager.generate_oauth_url(data.provider.as_str()).await?;
data_result_ok(OauthProviderDataPB {
oauth_url: sign_in_url,
})
}
#[tracing::instrument(level = "debug", skip_all, err)] #[tracing::instrument(level = "debug", skip_all, err)]
pub async fn set_encrypt_secret_handler( pub async fn set_encrypt_secret_handler(
manager: AFPluginState<Weak<UserManager>>, manager: AFPluginState<Weak<UserManager>>,

View File

@ -37,8 +37,12 @@ pub fn init(user_session: Weak<UserManager>) -> AFPlugin {
.event(UserEvent::GetCloudConfig, get_cloud_config_handler) .event(UserEvent::GetCloudConfig, get_cloud_config_handler)
.event(UserEvent::SetEncryptionSecret, set_encrypt_secret_handler) .event(UserEvent::SetEncryptionSecret, set_encrypt_secret_handler)
.event(UserEvent::CheckEncryptionSign, check_encrypt_secret_handler) .event(UserEvent::CheckEncryptionSign, check_encrypt_secret_handler)
.event(UserEvent::OAuth, oauth_handler) .event(UserEvent::OauthSignIn, oauth_handler)
.event(UserEvent::OAuthCallbackURL, get_oauth_url_handler) .event(UserEvent::GetSignInURL, get_sign_in_url_handler)
.event(
UserEvent::GetOauthURLWithProvider,
sign_in_with_provider_handler,
)
.event( .event(
UserEvent::GetAllUserWorkspaces, UserEvent::GetAllUserWorkspaces,
get_all_user_workspace_handler, get_all_user_workspace_handler,
@ -230,13 +234,16 @@ pub enum UserEvent {
#[event(output = "UserSettingPB")] #[event(output = "UserSettingPB")]
GetUserSetting = 9, GetUserSetting = 9,
#[event(input = "OAuthPB", output = "UserProfilePB")] #[event(input = "OauthSignInPB", output = "UserProfilePB")]
OAuth = 10, OauthSignIn = 10,
/// Get the OAuth callback url /// Get the OAuth callback url
/// Only use when the [AuthType] is AFCloud /// Only use when the [AuthType] is AFCloud
#[event(input = "OAuthCallbackRequestPB", output = "OAuthCallbackResponsePB")] #[event(input = "SignInUrlPayloadPB", output = "SignInUrlPB")]
OAuthCallbackURL = 11, GetSignInURL = 11,
#[event(input = "OauthProviderPB", output = "OauthProviderDataPB")]
GetOauthURLWithProvider = 12,
#[event(input = "UpdateCloudConfigPB")] #[event(input = "UpdateCloudConfigPB")]
SetCloudConfig = 13, SetCloudConfig = 13,

View File

@ -4,6 +4,7 @@ use std::sync::{Arc, Weak};
use collab_user::core::MutexUserAwareness; use collab_user::core::MutexUserAwareness;
use serde_json::Value; use serde_json::Value;
use tokio::sync::{Mutex, RwLock}; use tokio::sync::{Mutex, RwLock};
use tracing::{debug, info};
use uuid::Uuid; use uuid::Uuid;
use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::collab_builder::AppFlowyCollabBuilder;
@ -134,7 +135,7 @@ impl UserManager {
{ {
Ok(applied_migrations) => { Ok(applied_migrations) => {
if !applied_migrations.is_empty() { if !applied_migrations.is_empty() {
tracing::info!("Did apply migrations: {:?}", applied_migrations); info!("Did apply migrations: {:?}", applied_migrations);
} }
}, },
Err(e) => tracing::error!("User data migration failed: {:?}", e), Err(e) => tracing::error!("User data migration failed: {:?}", e),
@ -312,16 +313,16 @@ impl UserManager {
UserAwarenessDataSource::Remote UserAwarenessDataSource::Remote
}; };
debug!("Sign up response: {:?}", response);
if response.is_new_user { if response.is_new_user {
if let Some(old_user) = migration_user { if let Some(old_user) = migration_user {
let new_user = MigrationUser { let new_user = MigrationUser {
user_profile: user_profile.clone(), user_profile: user_profile.clone(),
session: new_session.clone(), session: new_session.clone(),
}; };
tracing::info!( info!(
"Migrate old user data from {:?} to {:?}", "Migrate old user data from {:?} to {:?}",
old_user.user_profile.uid, old_user.user_profile.uid, new_user.user_profile.uid
new_user.user_profile.uid
); );
self self
.migrate_local_user_to_cloud(&old_user, &new_user) .migrate_local_user_to_cloud(&old_user, &new_user)
@ -524,7 +525,7 @@ impl UserManager {
} }
pub(crate) fn set_session(&self, session: Option<Session>) -> Result<(), FlowyError> { pub(crate) fn set_session(&self, session: Option<Session>) -> Result<(), FlowyError> {
tracing::debug!("Set current user: {:?}", session); debug!("Set current user: {:?}", session);
match &session { match &session {
None => { None => {
self.current_session.write().take(); self.current_session.write().take();
@ -543,7 +544,7 @@ impl UserManager {
Ok(()) Ok(())
} }
pub(crate) async fn generate_sign_in_callback_url( pub(crate) async fn generate_sign_in_url_with_email(
&self, &self,
auth_type: &AuthType, auth_type: &AuthType,
email: &str, email: &str,
@ -551,7 +552,22 @@ impl UserManager {
self.update_auth_type(auth_type).await; self.update_auth_type(auth_type).await;
let auth_service = self.cloud_services.get_user_service()?; let auth_service = self.cloud_services.get_user_service()?;
let url = auth_service.generate_sign_in_callback_url(email).await?; let url = auth_service
.generate_sign_in_url_with_email(email)
.await
.map_err(|err| FlowyError::server_error().with_context(err))?;
Ok(url)
}
pub(crate) async fn generate_oauth_url(
&self,
oauth_provider: &str,
) -> Result<String, FlowyError> {
self.update_auth_type(&AuthType::AFCloud).await;
let auth_service = self.cloud_services.get_user_service()?;
let url = auth_service
.generate_oauth_url_with_provider(oauth_provider)
.await?;
Ok(url) Ok(url)
} }
@ -588,7 +604,7 @@ impl UserManager {
async fn handler_user_update(&self, user_update: UserUpdate) -> FlowyResult<()> { async fn handler_user_update(&self, user_update: UserUpdate) -> FlowyResult<()> {
let session = self.get_session()?; let session = self.get_session()?;
if session.user_id == user_update.uid { if session.user_id == user_update.uid {
tracing::debug!("Receive user update: {:?}", user_update); debug!("Receive user update: {:?}", user_update);
let user_profile = self.get_user_profile(user_update.uid).await?; let user_profile = self.get_user_profile(user_update.uid).await?;
if !is_user_encryption_sign_valid(&user_profile, &user_update.encryption_sign) { if !is_user_encryption_sign_valid(&user_profile, &user_update.encryption_sign) {

View File

@ -1,9 +1,11 @@
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use anyhow::Context;
use collab::core::collab::{CollabRawData, MutexCollab}; use collab::core::collab::{CollabRawData, MutexCollab};
use collab_define::reminder::Reminder; use collab_define::reminder::Reminder;
use collab_define::CollabType; use collab_define::CollabType;
use collab_user::core::{MutexUserAwareness, UserAwareness}; use collab_user::core::{MutexUserAwareness, UserAwareness};
use tracing::{error, trace};
use collab_integrate::RocksCollabDB; use collab_integrate::RocksCollabDB;
use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_error::{ErrorCode, FlowyError, FlowyResult};
@ -101,12 +103,8 @@ impl UserManager {
source: UserAwarenessDataSource, source: UserAwarenessDataSource,
) { ) {
match self.try_initial_user_awareness(session, source).await { match self.try_initial_user_awareness(session, source).await {
Ok(_) => { Ok(_) => trace!("User awareness initialized"),
tracing::trace!("User awareness initialized"); Err(e) => error!("Failed to initialize user awareness: {:?}", e),
},
Err(e) => {
tracing::error!("Failed to initialize user awareness: {:?}", e);
},
} }
} }
@ -128,11 +126,13 @@ impl UserManager {
session: &Session, session: &Session,
source: UserAwarenessDataSource, source: UserAwarenessDataSource,
) -> FlowyResult<()> { ) -> FlowyResult<()> {
tracing::trace!("Initializing user awareness from {:?}", source); trace!("Initializing user awareness from {:?}", source);
let collab_db = self.get_collab_db(session.user_id)?; let collab_db = self.get_collab_db(session.user_id)?;
let user_awareness = match source { let user_awareness = match source {
UserAwarenessDataSource::Local => { UserAwarenessDataSource::Local => {
let collab = self.collab_for_user_awareness(session, collab_db, vec![])?; let collab = self
.collab_for_user_awareness(session, collab_db, vec![])
.await?;
MutexUserAwareness::new(UserAwareness::create(collab, None)) MutexUserAwareness::new(UserAwareness::create(collab, None))
}, },
UserAwarenessDataSource::Remote => { UserAwarenessDataSource::Remote => {
@ -141,7 +141,10 @@ impl UserManager {
.get_user_service()? .get_user_service()?
.get_user_awareness_updates(session.user_id) .get_user_awareness_updates(session.user_id)
.await?; .await?;
let collab = self.collab_for_user_awareness(session, collab_db, data)?; trace!("Get user awareness collab: {}", data.len());
let collab = self
.collab_for_user_awareness(session, collab_db, data)
.await?;
MutexUserAwareness::new(UserAwareness::create(collab, None)) MutexUserAwareness::new(UserAwareness::create(collab, None))
}, },
}; };
@ -154,7 +157,7 @@ impl UserManager {
/// This function constructs a collaboration instance based on the given session and raw data, /// This function constructs a collaboration instance based on the given session and raw data,
/// using a collaboration builder. This instance is specifically geared towards handling /// using a collaboration builder. This instance is specifically geared towards handling
/// user awareness. /// user awareness.
fn collab_for_user_awareness( async fn collab_for_user_awareness(
&self, &self,
session: &Session, session: &Session,
collab_db: Weak<RocksCollabDB>, collab_db: Weak<RocksCollabDB>,
@ -164,13 +167,16 @@ impl UserManager {
ErrorCode::Internal, ErrorCode::Internal,
"Unexpected error: collab builder is not available", "Unexpected error: collab builder is not available",
))?; ))?;
let collab = collab_builder.build( let collab = collab_builder
session.user_id, .build(
&session.user_id.to_string(), session.user_id,
CollabType::UserAwareness, &session.user_id.to_string(),
raw_data, CollabType::UserAwareness,
collab_db, raw_data,
)?; collab_db,
)
.await
.context("Build collab for user awareness failed")?;
Ok(collab) Ok(collab)
} }

View File

@ -1,17 +1,3 @@
use crate::{
errors::{DispatchError, InternalError},
module::{container::AFPluginStateMap, AFPluginState},
request::{payload::Payload, AFPluginEventRequest, FromAFPluginRequest},
response::{AFPluginEventResponse, AFPluginResponder},
service::{
factory, AFPluginHandler, AFPluginHandlerService, AFPluginServiceFactory, BoxService,
BoxServiceFactory, Service, ServiceRequest, ServiceResponse,
},
};
use futures_core::future::BoxFuture;
use futures_core::ready;
use nanoid::nanoid;
use pin_project::pin_project;
use std::sync::Arc; use std::sync::Arc;
use std::{ use std::{
collections::HashMap, collections::HashMap,
@ -23,6 +9,23 @@ use std::{
task::{Context, Poll}, task::{Context, Poll},
}; };
use futures_core::future::BoxFuture;
use futures_core::ready;
use nanoid::nanoid;
use pin_project::pin_project;
use crate::service::AFPluginHandler;
use crate::{
errors::{DispatchError, InternalError},
module::{container::AFPluginStateMap, AFPluginState},
request::{payload::Payload, AFPluginEventRequest, FromAFPluginRequest},
response::{AFPluginEventResponse, AFPluginResponder},
service::{
factory, AFPluginHandlerService, AFPluginServiceFactory, BoxService, BoxServiceFactory,
Service, ServiceRequest, ServiceResponse,
},
};
pub type AFPluginMap = Arc<HashMap<AFPluginEvent, Arc<AFPlugin>>>; pub type AFPluginMap = Arc<HashMap<AFPluginEvent, Arc<AFPlugin>>>;
pub(crate) fn as_plugin_map(plugins: Vec<AFPlugin>) -> AFPluginMap { pub(crate) fn as_plugin_map(plugins: Vec<AFPlugin>) -> AFPluginMap {
let mut plugin_map = HashMap::new(); let mut plugin_map = HashMap::new();
@ -93,6 +96,7 @@ impl AFPlugin {
self self
} }
#[track_caller]
pub fn event<E, H, T, R>(mut self, event: E, handler: H) -> Self pub fn event<E, H, T, R>(mut self, event: E, handler: H) -> Self
where where
H: AFPluginHandler<T, R>, H: AFPluginHandler<T, R>,