feat: anon user save (#4185)

* feat: anon user save

* chore: add missing files

* chore: remove error
This commit is contained in:
Nathan.fooo 2023-12-21 14:13:21 +08:00 committed by GitHub
parent 0730a0caf4
commit 1401ba5960
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 284 additions and 401 deletions

View File

@ -63,14 +63,14 @@ void main() {
);
await tester.tapGoogleLoginInButton();
tester.expectToSeeHomePage();
await tester.pumpAndSettle();
await tester.pumpAndSettle(const Duration(seconds: 2));
// The document will be synced from the server
await tester.openPage(
pageName,
);
await tester.pumpAndSettle();
await tester.pumpAndSettle(const Duration(seconds: 2));
expect(find.text('hello world', findRichText: true), findsOneWidget);
});
});

View File

@ -0,0 +1,64 @@
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'anon_user_bloc.freezed.dart';
class AnonUserBloc extends Bloc<AnonUserEvent, AnonUserState> {
AnonUserBloc() : super(AnonUserState.initial()) {
on<AnonUserEvent>((event, emit) async {
await event.when(
initial: () async {
await _loadHistoricalUsers();
},
didLoadAnonUsers: (List<UserProfilePB> anonUsers) {
emit(state.copyWith(anonUsers: anonUsers));
},
openAnonUser: (anonUser) async {
await UserBackendService.openAnonUser();
emit(state.copyWith(openedAnonUser: anonUser));
},
);
});
}
Future<void> _loadHistoricalUsers() async {
final result = await UserBackendService.getAnonUser();
result.fold(
(anonUser) {
add(AnonUserEvent.didLoadAnonUsers([anonUser]));
},
(error) {
if (error.code != ErrorCode.RecordNotFound) {
Log.error(error);
}
},
);
}
}
@freezed
class AnonUserEvent with _$AnonUserEvent {
const factory AnonUserEvent.initial() = _Initial;
const factory AnonUserEvent.didLoadAnonUsers(
List<UserProfilePB> historicalUsers,
) = _DidLoadHistoricalUsers;
const factory AnonUserEvent.openAnonUser(UserProfilePB anonUser) =
_OpenHistoricalUser;
}
@freezed
class AnonUserState with _$AnonUserState {
const factory AnonUserState({
required List<UserProfilePB> anonUsers,
required UserProfilePB? openedAnonUser,
}) = _AnonUserState;
factory AnonUserState.initial() => const AnonUserState(
anonUsers: [],
openedAnonUser: null,
);
}

View File

@ -1,64 +0,0 @@
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/auth.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'historical_user_bloc.freezed.dart';
class HistoricalUserBloc
extends Bloc<HistoricalUserEvent, HistoricalUserState> {
HistoricalUserBloc() : super(HistoricalUserState.initial()) {
on<HistoricalUserEvent>((event, emit) async {
await event.when(
initial: () async {
await _loadHistoricalUsers();
},
didLoadHistoricalUsers: (List<HistoricalUserPB> historicalUsers) {
emit(state.copyWith(historicalUsers: historicalUsers));
},
openHistoricalUser: (HistoricalUserPB historicalUser) async {
await UserBackendService.openHistoricalUser(historicalUser);
emit(state.copyWith(openedHistoricalUser: historicalUser));
},
);
});
}
Future<void> _loadHistoricalUsers() async {
final result = await UserBackendService.loadHistoricalUsers();
result.fold(
(historicalUsers) {
historicalUsers
.retainWhere((element) => element.authType == AuthTypePB.Local);
add(HistoricalUserEvent.didLoadHistoricalUsers(historicalUsers));
},
(error) => Log.error(error),
);
}
}
@freezed
class HistoricalUserEvent with _$HistoricalUserEvent {
const factory HistoricalUserEvent.initial() = _Initial;
const factory HistoricalUserEvent.didLoadHistoricalUsers(
List<HistoricalUserPB> historicalUsers,
) = _DidLoadHistoricalUsers;
const factory HistoricalUserEvent.openHistoricalUser(
HistoricalUserPB historicalUser,
) = _OpenHistoricalUser;
}
@freezed
class HistoricalUserState with _$HistoricalUserState {
const factory HistoricalUserState({
required List<HistoricalUserPB> historicalUsers,
required HistoricalUserPB? openedHistoricalUser,
}) = _HistoricalUserState;
factory HistoricalUserState.initial() => const HistoricalUserState(
historicalUsers: [],
openedHistoricalUser: null,
);
}

View File

@ -71,22 +71,12 @@ class UserBackendService {
return UserEventInitUser().send();
}
static Future<Either<List<HistoricalUserPB>, FlowyError>>
loadHistoricalUsers() async {
return UserEventGetHistoricalUsers().send().then(
(result) {
return result.fold(
(historicalUsers) => left(historicalUsers.items),
(error) => right(error),
);
},
);
static Future<Either<UserProfilePB, FlowyError>> getAnonUser() async {
return UserEventGetAnonUser().send();
}
static Future<Either<Unit, FlowyError>> openHistoricalUser(
HistoricalUserPB user,
) async {
return UserEventOpenHistoricalUser(user).send();
static Future<Either<Unit, FlowyError>> openAnonUser() async {
return UserEventOpenAnonUser().send();
}
Future<Either<List<WorkspacePB>, FlowyError>> getWorkspaces() {

View File

@ -1,26 +1,26 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/application/historical_user_bloc.dart';
import 'package:appflowy/user/application/anon_user_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class HistoricalUserList extends StatelessWidget {
class AnonUserList extends StatelessWidget {
final VoidCallback didOpenUser;
const HistoricalUserList({required this.didOpenUser, super.key});
const AnonUserList({required this.didOpenUser, super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => HistoricalUserBloc()
create: (context) => AnonUserBloc()
..add(
const HistoricalUserEvent.initial(),
const AnonUserEvent.initial(),
),
child: BlocBuilder<HistoricalUserBloc, HistoricalUserState>(
child: BlocBuilder<AnonUserBloc, AnonUserState>(
builder: (context, state) {
if (state.historicalUsers.isEmpty) {
if (state.anonUsers.isEmpty) {
return const SizedBox.shrink();
} else {
return Column(
@ -38,15 +38,15 @@ class HistoricalUserList extends StatelessWidget {
Expanded(
child: ListView.builder(
itemBuilder: (context, index) {
final user = state.historicalUsers[index];
return HistoricalUserItem(
key: ValueKey(user.userId),
final user = state.anonUsers[index];
return AnonUserItem(
key: ValueKey(user.id),
user: user,
isSelected: false,
didOpenUser: didOpenUser,
);
},
itemCount: state.historicalUsers.length,
itemCount: state.anonUsers.length,
),
),
],
@ -58,11 +58,11 @@ class HistoricalUserList extends StatelessWidget {
}
}
class HistoricalUserItem extends StatelessWidget {
class AnonUserItem extends StatelessWidget {
final VoidCallback didOpenUser;
final bool isSelected;
final HistoricalUserPB user;
const HistoricalUserItem({
final UserProfilePB user;
const AnonUserItem({
required this.user,
required this.isSelected,
required this.didOpenUser,
@ -73,11 +73,7 @@ class HistoricalUserItem extends StatelessWidget {
Widget build(BuildContext context) {
final icon = isSelected ? const FlowySvg(FlowySvgs.check_s) : null;
final isDisabled = isSelected || user.authType != AuthTypePB.Local;
final outputFormat = DateFormat('MM/dd/yyyy hh:mm a');
final date =
DateTime.fromMillisecondsSinceEpoch(user.lastTime.toInt() * 1000);
final lastTime = outputFormat.format(date);
final desc = "${user.userName}\t ${user.authType}\t$lastTime";
final desc = "${user.name}\t ${user.authType}\t";
final child = SizedBox(
height: 30,
child: FlowyButton(
@ -88,9 +84,7 @@ class HistoricalUserItem extends StatelessWidget {
),
rightIcon: icon,
onTap: () {
context
.read<HistoricalUserBloc>()
.add(HistoricalUserEvent.openHistoricalUser(user));
context.read<AnonUserBloc>().add(AnonUserEvent.openAnonUser(user));
didOpenUser();
},
),

View File

@ -1,4 +1,4 @@
export 'screens/screens.dart';
export 'widgets/widgets.dart';
export 'historical_user.dart';
export 'anon_user.dart';
export 'router.dart';

View File

@ -1,6 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/historical_user_bloc.dart';
import 'package:appflowy/user/application/anon_user_bloc.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
@ -22,31 +22,31 @@ class SignInAnonymousButton extends StatelessWidget {
return BlocBuilder<SignInBloc, SignInState>(
builder: (context, signInState) {
return BlocProvider(
create: (context) => HistoricalUserBloc()
create: (context) => AnonUserBloc()
..add(
const HistoricalUserEvent.initial(),
const AnonUserEvent.initial(),
),
child: BlocListener<HistoricalUserBloc, HistoricalUserState>(
listenWhen: (previous, current) =>
previous.openedHistoricalUser != current.openedHistoricalUser,
child: BlocListener<AnonUserBloc, AnonUserState>(
listener: (context, state) async {
await runAppFlowy();
if (state.openedAnonUser != null) {
await runAppFlowy();
}
},
child: BlocBuilder<HistoricalUserBloc, HistoricalUserState>(
child: BlocBuilder<AnonUserBloc, AnonUserState>(
builder: (context, state) {
final text = state.historicalUsers.isEmpty
final text = state.anonUsers.isEmpty
? LocaleKeys.signIn_loginStartWithAnonymous.tr()
: LocaleKeys.signIn_continueAnonymousUser.tr();
final onTap = state.historicalUsers.isEmpty
final onTap = state.anonUsers.isEmpty
? () {
context
.read<SignInBloc>()
.add(const SignInEvent.signedInAsGuest());
}
: () {
final bloc = context.read<HistoricalUserBloc>();
final user = bloc.state.historicalUsers.first;
bloc.add(HistoricalUserEvent.openHistoricalUser(user));
final bloc = context.read<AnonUserBloc>();
final user = bloc.state.anonUsers.first;
bloc.add(AnonUserEvent.openAnonUser(user));
};
// SignInAnonymousButton in mobile
if (isMobile) {

View File

@ -4,7 +4,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/entry_point.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/historical_user_bloc.dart';
import 'package:appflowy/user/application/anon_user_bloc.dart';
import 'package:appflowy/user/presentation/router.dart';
import 'package:appflowy/user/presentation/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
@ -267,19 +267,19 @@ class GoButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => HistoricalUserBloc()
create: (context) => AnonUserBloc()
..add(
const HistoricalUserEvent.initial(),
const AnonUserEvent.initial(),
),
child: BlocListener<HistoricalUserBloc, HistoricalUserState>(
listenWhen: (previous, current) =>
previous.openedHistoricalUser != current.openedHistoricalUser,
child: BlocListener<AnonUserBloc, AnonUserState>(
listener: (context, state) async {
await runAppFlowy();
if (state.openedAnonUser != null) {
await runAppFlowy();
}
},
child: BlocBuilder<HistoricalUserBloc, HistoricalUserState>(
child: BlocBuilder<AnonUserBloc, AnonUserState>(
builder: (context, state) {
final text = state.historicalUsers.isEmpty
final text = state.anonUsers.isEmpty
? LocaleKeys.letsGoButtonText.tr()
: LocaleKeys.signIn_continueAnonymousUser.tr();
@ -320,11 +320,11 @@ class GoButton extends StatelessWidget {
text: textWidget,
radius: Corners.s6Border,
onTap: () {
if (state.historicalUsers.isNotEmpty) {
final bloc = context.read<HistoricalUserBloc>();
final historicalUser = state.historicalUsers.first;
if (state.anonUsers.isNotEmpty) {
final bloc = context.read<AnonUserBloc>();
final historicalUser = state.anonUsers.first;
bloc.add(
HistoricalUserEvent.openHistoricalUser(historicalUser),
AnonUserEvent.openAnonUser(historicalUser),
);
} else {
onPressed();

View File

@ -70,12 +70,6 @@ class SettingsUserViewBloc extends Bloc<SettingsUserEvent, SettingsUserState> {
);
});
},
didLoadHistoricalUsers: (List<HistoricalUserPB> historicalUsers) {
emit(state.copyWith(historicalUsers: historicalUsers));
},
openHistoricalUser: (HistoricalUserPB historicalUser) async {
await UserBackendService.openHistoricalUser(historicalUser);
},
updateUserEmail: (String email) {
_userService.updateUserProfile(email: email).then((result) {
result.fold(
@ -135,26 +129,18 @@ class SettingsUserEvent with _$SettingsUserEvent {
const factory SettingsUserEvent.didReceiveUserProfile(
UserProfilePB newUserProfile,
) = _DidReceiveUserProfile;
const factory SettingsUserEvent.didLoadHistoricalUsers(
List<HistoricalUserPB> historicalUsers,
) = _DidLoadHistoricalUsers;
const factory SettingsUserEvent.openHistoricalUser(
HistoricalUserPB historicalUser,
) = _OpenHistoricalUser;
}
@freezed
class SettingsUserState with _$SettingsUserState {
const factory SettingsUserState({
required UserProfilePB userProfile,
required List<HistoricalUserPB> historicalUsers,
required Either<Unit, String> successOrFailure,
}) = _SettingsUserState;
factory SettingsUserState.initial(UserProfilePB userProfile) =>
SettingsUserState(
userProfile: userProfile,
historicalUsers: const [],
successOrFailure: left(unit),
);
}

View File

@ -77,7 +77,6 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
final view_ = result.fold((l) => l, (r) => null);
e.result.fold(
(view) async {
Log.debug('viewDidUpdate: $view');
// ignore child view changes because it only contains one level
// children data.
if (_isSameViewIgnoreChildren(view, state.view)) {

View File

@ -1006,10 +1006,10 @@ packages:
dependency: transitive
description:
name: keyboard_height_plugin
sha256: "9fd5cd9e3e80d8f530aaa26ad17b4d18d34a63956cf0d530920a54c228200510"
sha256: bbb32804bf93601249c17c33125cd2e654f5ef650fc6acf1b031d69b478b35ce
url: "https://pub.dev"
source: hosted
version: "0.0.4"
version: "0.0.5"
leak_tracker:
dependency: "direct main"
description:
@ -1174,10 +1174,10 @@ packages:
dependency: transitive
description:
name: numerus
sha256: ada1b55e4a505b8e4578218e57be1ce4b7968bb0e51d824082ac0c74ef18a10d
sha256: "49cd96fe774dd1f574fc9117ed67e8a2b06a612f723e87ef3119456a7729d837"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.2.0"
octo_image:
dependency: transitive
description:

View File

@ -24,7 +24,7 @@ tokio = { workspace = true, features = ["sync"]}
lib-infra = { path = "../../../shared-lib/lib-infra" }
[features]
default = []
default = ["rocksdb_plugin", "snapshot_plugin"]
supabase_integrate = ["collab-plugins/postgres_storage_plugin", "rocksdb_plugin"]
appflowy_cloud_integrate = ["rocksdb_plugin"]
snapshot_plugin = ["collab-plugins/snapshot_plugin"]

View File

@ -13,7 +13,7 @@ use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl};
use flowy_server_config::af_cloud_config::AFCloudConfiguration;
use flowy_server_config::supabase_config::SupabaseConfiguration;
use flowy_sqlite::kv::StorePreferences;
use flowy_user::services::database::{get_user_profile, get_user_workspace, open_user_db};
use flowy_user::services::db::{get_user_profile, get_user_workspace, open_user_db};
use flowy_user_deps::entities::*;
use crate::AppFlowyCoreConfig;

View File

@ -69,7 +69,7 @@ impl UserStatusCallback for UserStatusCallbackImpl {
.initialize(
user_id,
user_workspace.id.clone(),
user_workspace.database_views_aggregate_id,
user_workspace.database_storage_id,
)
.await?;
document_manager
@ -107,7 +107,7 @@ impl UserStatusCallback for UserStatusCallbackImpl {
.initialize(
user_id,
user_workspace.id.clone(),
user_workspace.database_views_aggregate_id,
user_workspace.database_storage_id,
)
.await?;
document_manager
@ -170,7 +170,7 @@ impl UserStatusCallback for UserStatusCallbackImpl {
.initialize_with_new_user(
user_profile.uid,
user_workspace.id.clone(),
user_workspace.database_views_aggregate_id,
user_workspace.database_storage_id,
)
.await
.context("DatabaseManager error")?;
@ -208,7 +208,7 @@ impl UserStatusCallback for UserStatusCallbackImpl {
.initialize(
user_id,
user_workspace.id.clone(),
user_workspace.database_views_aggregate_id,
user_workspace.database_storage_id,
)
.await?;
document_manager

View File

@ -10,7 +10,6 @@ use collab_document::YrsDocAction;
use collab_entity::CollabType;
use lru::LruCache;
use parking_lot::Mutex;
use tokio_stream::StreamExt;
use tracing::{event, instrument};
use collab_integrate::collab_builder::AppFlowyCollabBuilder;

View File

@ -270,6 +270,7 @@ pub async fn user_sign_in_with_url(
Ok(AuthResponse {
user_id: user_profile.uid,
user_uuid: user_profile.uuid,
name: user_profile.name.unwrap_or_default(),
latest_workspace,
user_workspaces,
@ -287,7 +288,7 @@ fn to_user_workspace(af_workspace: AFWorkspace) -> UserWorkspace {
id: af_workspace.workspace_id.to_string(),
name: af_workspace.workspace_name,
created_at: af_workspace.created_at,
database_views_aggregate_id: af_workspace.database_storage_id.to_string(),
database_storage_id: af_workspace.database_storage_id.to_string(),
}
}

View File

@ -4,6 +4,7 @@ use anyhow::Error;
use collab_entity::CollabObject;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use uuid::Uuid;
use flowy_error::FlowyError;
use flowy_user_deps::cloud::UserCloudService;
@ -39,6 +40,7 @@ impl UserCloudService for LocalServerUserAuthServiceImpl {
};
Ok(AuthResponse {
user_id: uid,
user_uuid: Uuid::new_v4(),
name: user_name,
latest_workspace: user_workspace.clone(),
user_workspaces: vec![user_workspace],
@ -63,6 +65,7 @@ impl UserCloudService for LocalServerUserAuthServiceImpl {
.unwrap_or_else(make_user_workspace);
Ok(AuthResponse {
user_id: uid,
user_uuid: Uuid::new_v4(),
name: params.name,
latest_workspace: user_workspace.clone(),
user_workspaces: vec![user_workspace],
@ -148,6 +151,6 @@ fn make_user_workspace() -> UserWorkspace {
id: uuid::Uuid::new_v4().to_string(),
name: "My Workspace".to_string(),
created_at: Default::default(),
database_views_aggregate_id: uuid::Uuid::new_v4().to_string(),
database_storage_id: uuid::Uuid::new_v4().to_string(),
}
}

View File

@ -97,11 +97,13 @@ where
}
// Query the user profile and workspaces
tracing::debug!("user uuid: {}", params.uuid,);
let user_profile =
get_user_profile(postgrest.clone(), GetUserProfileParams::Uuid(params.uuid))
.await?
.unwrap();
tracing::debug!("user uuid: {}", params.uuid);
let user_profile = get_user_profile(
postgrest.clone(),
GetUserProfileParams::Uuid(params.uuid.clone()),
)
.await?
.unwrap();
let user_workspaces = get_user_workspaces(postgrest.clone(), user_profile.uid).await?;
let latest_workspace = user_workspaces
.iter()
@ -116,6 +118,7 @@ where
Ok(AuthResponse {
user_id: user_profile.uid,
user_uuid: params.uuid,
name: user_name,
latest_workspace: latest_workspace.unwrap(),
user_workspaces,
@ -134,7 +137,7 @@ where
FutureResult::new(async move {
let postgrest = try_get_postgrest?;
let params = oauth_params_from_box_any(params)?;
let uuid = params.uuid;
let uuid = params.uuid.clone();
let response = get_user_profile(postgrest.clone(), GetUserProfileParams::Uuid(uuid))
.await?
.unwrap();
@ -146,6 +149,7 @@ where
Ok(AuthResponse {
user_id: response.uid,
user_uuid: params.uuid,
name: DEFAULT_USER_NAME(),
latest_workspace: latest_workspace.unwrap(),
user_workspaces,

View File

@ -21,7 +21,7 @@ async fn supabase_user_sign_up_test() {
let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
assert!(!user.latest_workspace.id.is_empty());
assert!(!user.user_workspaces.is_empty());
assert!(!user.latest_workspace.database_views_aggregate_id.is_empty());
assert!(!user.latest_workspace.database_storage_id.is_empty());
}
#[tokio::test]
@ -38,7 +38,7 @@ async fn supabase_user_sign_up_with_existing_uuid_test() {
.unwrap();
let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
assert!(!user.latest_workspace.id.is_empty());
assert!(!user.latest_workspace.database_views_aggregate_id.is_empty());
assert!(!user.latest_workspace.database_storage_id.is_empty());
assert!(!user.user_workspaces.is_empty());
}

View File

@ -13,6 +13,7 @@ pub const USER_METADATA_UPDATE_AT: &str = "updated_at";
pub trait UserAuthResponse {
fn user_id(&self) -> i64;
fn user_uuid(&self) -> &Uuid;
fn user_name(&self) -> &str;
fn latest_workspace(&self) -> &UserWorkspace;
fn user_workspaces(&self) -> &[UserWorkspace];
@ -43,6 +44,7 @@ pub struct SignUpParams {
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AuthResponse {
pub user_id: i64,
pub user_uuid: Uuid,
pub name: String,
pub latest_workspace: UserWorkspace,
pub user_workspaces: Vec<UserWorkspace>,
@ -59,6 +61,10 @@ impl UserAuthResponse for AuthResponse {
self.user_id
}
fn user_uuid(&self) -> &Uuid {
&self.user_uuid
}
fn user_name(&self) -> &str {
&self.name
}
@ -132,8 +138,7 @@ pub struct UserWorkspace {
pub name: String,
pub created_at: DateTime<Utc>,
/// The database storage id is used indexing all the database views in current workspace.
#[serde(rename = "database_storage_id")]
pub database_views_aggregate_id: String,
pub database_storage_id: String,
}
impl UserWorkspace {
@ -142,7 +147,7 @@ impl UserWorkspace {
id: workspace_id.to_string(),
name: "".to_string(),
created_at: Utc::now(),
database_views_aggregate_id: uuid::Uuid::new_v4().to_string(),
database_storage_id: Uuid::new_v4().to_string(),
}
}
}

View File

@ -50,7 +50,7 @@ pub fn migration_anon_user_on_sign_up(
// Migration of all objects except the folder and database_with_views
object_ids.retain(|id| {
id != &old_user.session.user_workspace.id
&& id != &old_user.session.user_workspace.database_views_aggregate_id
&& id != &old_user.session.user_workspace.database_storage_id
});
tracing::info!("migrate collab objects: {:?}", object_ids.len());
@ -138,20 +138,20 @@ where
{
let database_with_views_collab = Collab::new(
old_user.session.user_id,
&old_user.session.user_workspace.database_views_aggregate_id,
&old_user.session.user_workspace.database_storage_id,
"phantom",
vec![],
);
database_with_views_collab.with_origin_transact_mut(|txn| {
old_collab_r_txn.load_doc_with_txn(
old_user.session.user_id,
&old_user.session.user_workspace.database_views_aggregate_id,
&old_user.session.user_workspace.database_storage_id,
txn,
)
})?;
let new_uid = new_user.session.user_id;
let new_object_id = &new_user.session.user_workspace.database_views_aggregate_id;
let new_object_id = &new_user.session.user_workspace.database_storage_id;
let array = DatabaseWithViewsArray::from_collab(&database_with_views_collab);
for database_view in array.get_all_databases() {

View File

@ -43,7 +43,7 @@ pub async fn sync_af_user_data_to_cloud(
uid,
&workspace_id,
device_id,
&new_user.session.user_workspace.database_views_aggregate_id,
&new_user.session.user_workspace.database_storage_id,
collab_db,
user_service.clone(),
)

View File

@ -43,7 +43,7 @@ pub async fn sync_supabase_user_data_to_cloud(
uid,
&workspace_id,
device_id,
&new_user.session.user_workspace.database_views_aggregate_id,
&new_user.session.user_workspace.database_storage_id,
collab_db,
user_service.clone(),
)

View File

@ -9,7 +9,6 @@ use crate::entities::parser::{UserEmail, UserIcon, UserName, UserOpenaiKey, User
use crate::entities::required_not_empty_str;
use crate::entities::AuthTypePB;
use crate::errors::ErrorCode;
use crate::services::entities::HistoricalUser;
use super::parser::UserStabilityAIKey;
@ -239,53 +238,6 @@ impl From<UserWorkspace> for UserWorkspacePB {
}
}
#[derive(ProtoBuf, Default, Clone)]
pub struct RepeatedHistoricalUserPB {
#[pb(index = 1)]
pub items: Vec<HistoricalUserPB>,
}
#[derive(ProtoBuf, Default, Clone)]
pub struct HistoricalUserPB {
#[pb(index = 1)]
pub user_id: i64,
#[pb(index = 2)]
pub user_name: String,
#[pb(index = 3)]
pub last_time: i64,
#[pb(index = 4)]
pub auth_type: AuthTypePB,
#[pb(index = 5)]
pub device_id: String,
}
impl From<Vec<HistoricalUser>> for RepeatedHistoricalUserPB {
fn from(historical_users: Vec<HistoricalUser>) -> Self {
Self {
items: historical_users
.into_iter()
.map(HistoricalUserPB::from)
.collect(),
}
}
}
impl From<HistoricalUser> for HistoricalUserPB {
fn from(historical_user: HistoricalUser) -> Self {
Self {
user_id: historical_user.user_id,
user_name: historical_user.user_name,
last_time: historical_user.sign_in_timestamp,
auth_type: historical_user.auth_type.into(),
device_id: historical_user.device_id,
}
}
}
#[derive(ProtoBuf, Default, Clone)]
pub struct ResetWorkspacePB {
#[pb(index = 1)]

View File

@ -465,25 +465,20 @@ pub async fn update_network_state_handler(
}
#[tracing::instrument(level = "debug", skip_all, err)]
pub async fn get_historical_users_handler(
pub async fn get_anon_user_handler(
manager: AFPluginState<Weak<UserManager>>,
) -> DataResult<RepeatedHistoricalUserPB, FlowyError> {
) -> DataResult<UserProfilePB, FlowyError> {
let manager = upgrade_manager(manager)?;
let users = RepeatedHistoricalUserPB::from(manager.get_historical_users());
data_result_ok(users)
let user_profile = manager.get_anon_user().await?;
data_result_ok(user_profile)
}
#[tracing::instrument(level = "debug", skip_all, err)]
pub async fn open_historical_users_handler(
user: AFPluginData<HistoricalUserPB>,
pub async fn open_anon_user_handler(
manager: AFPluginState<Weak<UserManager>>,
) -> Result<(), FlowyError> {
let user = user.into_inner();
let manager = upgrade_manager(manager)?;
let auth_type = Authenticator::from(user.auth_type);
manager
.open_historical_user(user.user_id, auth_type)
.await?;
manager.open_anon_user().await?;
Ok(())
}

View File

@ -44,8 +44,8 @@ pub fn init(user_session: Weak<UserManager>) -> AFPlugin {
.event(UserEvent::GetAllWorkspace, get_all_workspace_handler)
.event(UserEvent::OpenWorkspace, open_workspace_handler)
.event(UserEvent::UpdateNetworkState, update_network_state_handler)
.event(UserEvent::GetHistoricalUsers, get_historical_users_handler)
.event(UserEvent::OpenHistoricalUser, open_historical_users_handler)
.event(UserEvent::OpenAnonUser, open_anon_user_handler)
.event(UserEvent::GetAnonUser, get_anon_user_handler)
.event(UserEvent::PushRealtimeEvent, push_realtime_event_handler)
.event(UserEvent::CreateReminder, create_reminder_event_handler)
.event(UserEvent::GetAllReminders, get_all_reminder_event_handler)
@ -138,11 +138,11 @@ pub enum UserEvent {
#[event(input = "NetworkStatePB")]
UpdateNetworkState = 24,
#[event(output = "RepeatedHistoricalUserPB")]
GetHistoricalUsers = 25,
#[event(output = "UserProfilePB")]
GetAnonUser = 25,
#[event(input = "HistoricalUserPB")]
OpenHistoricalUser = 26,
#[event()]
OpenAnonUser = 26,
/// Push a realtime event to the user. Currently, the realtime event
/// is only used when the auth type is: [Authenticator::Supabase].

View File

@ -33,11 +33,12 @@ use crate::entities::{AuthStateChangedPB, AuthStatePB, UserProfilePB, UserSettin
use crate::event_map::{DefaultUserStatusCallback, UserCloudServiceProvider, UserStatusCallback};
use crate::migrations::document_empty_content::HistoricalEmptyDocumentMigration;
use crate::migrations::migration::{UserDataMigration, UserLocalDataMigration};
use crate::migrations::session_migration::migrate_session_with_user_uuid;
use crate::migrations::workspace_and_favorite_v1::FavoriteV1AndWorkspaceArrayMigration;
use crate::migrations::MigrationUser;
use crate::services::cloud_config::get_cloud_config;
use crate::services::collab_interact::{CollabInteract, DefaultCollabInteract};
use crate::services::database::{UserDB, UserDBPath};
use crate::services::db::{UserDB, UserDBPath};
use crate::services::entities::{ResumableSignUp, Session};
use crate::services::user_awareness::UserAwarenessDataSource;
use crate::services::user_sql::{UserTable, UserTableChangeset};
@ -55,7 +56,7 @@ pub struct UserConfig {
application_path: String,
pub device_id: String,
/// Used as the key of `Session` when saving session information to KV.
session_cache_key: String,
pub(crate) session_cache_key: String,
}
impl UserConfig {
@ -103,6 +104,14 @@ impl UserManager {
let database = Arc::new(UserDB::new(user_paths.clone()));
let user_status_callback: RwLock<Arc<dyn UserStatusCallback>> =
RwLock::new(Arc::new(DefaultUserStatusCallback));
let current_session = Arc::new(parking_lot::RwLock::new(None));
let current_authenticator = current_authenticator();
migrate_session_with_user_uuid(
&current_authenticator,
&user_config,
&current_session,
&store_preferences,
);
let refresh_user_profile_since = AtomicI64::new(0);
let user_manager = Arc::new(Self {
@ -116,7 +125,7 @@ impl UserManager {
collab_builder,
collab_interact: RwLock::new(Arc::new(DefaultCollabInteract)),
resumable_sign_up: Default::default(),
current_session: Default::default(),
current_session,
refresh_user_profile_since,
});
@ -163,11 +172,7 @@ impl UserManager {
let user = self.get_user_profile_from_disk(session.user_id).await?;
// Get the current authenticator from the environment variable
let current_authenticator = match AuthenticatorType::from_env() {
AuthenticatorType::Local => Authenticator::Local,
AuthenticatorType::Supabase => Authenticator::Supabase,
AuthenticatorType::AppFlowyCloud => Authenticator::AppFlowyCloud,
};
let current_authenticator = current_authenticator();
// If the current authenticator is different from the authenticator in the session and it's
// not a local authenticator, we need to sign out the user.
@ -586,14 +591,6 @@ impl UserManager {
"User is unauthorized, sign out the user"
);
self.add_historical_user(
uid,
&self.user_config.device_id,
old_user_profile.name.clone(),
&old_user_profile.authenticator,
self.user_dir(uid),
);
self.sign_out().await?;
send_auth_state_notification(AuthStateChangedPB {
state: AuthStatePB::InvalidAuth,
@ -748,15 +745,11 @@ impl UserManager {
) -> Result<(), FlowyError> {
let user_profile = UserProfile::from((response, authenticator));
let uid = user_profile.uid;
event!(tracing::Level::DEBUG, "Save new history user: {:?}", uid);
self.add_historical_user(
uid,
&self.user_config.device_id,
response.user_name().to_string(),
authenticator,
self.user_dir(uid),
);
event!(tracing::Level::DEBUG, "Save new history user workspace");
if authenticator.is_local() {
event!(tracing::Level::DEBUG, "Save new anon user: {:?}", uid);
self.set_anon_user(session.clone());
}
save_user_workspaces(uid, self.db_pool(uid)?, response.user_workspaces())?;
event!(tracing::Level::INFO, "Save new user profile to disk");
self
@ -839,6 +832,14 @@ impl UserManager {
}
}
fn current_authenticator() -> Authenticator {
match AuthenticatorType::from_env() {
AuthenticatorType::Local => Authenticator::Local,
AuthenticatorType::Supabase => Authenticator::Supabase,
AuthenticatorType::AppFlowyCloud => Authenticator::AppFlowyCloud,
}
}
fn validate_encryption_sign(user_profile: &UserProfile, encryption_sign: &str) -> bool {
// If the local user profile's encryption sign is not equal to the user update's encryption sign,
// which means the user enable encryption in another device, we should logout the current user.

View File

@ -1,9 +0,0 @@
use flowy_user_deps::entities::UserProfile;
use crate::services::entities::Session;
#[derive(Clone, Debug)]
pub struct MigrationUser {
pub user_profile: UserProfile,
pub session: Session,
}

View File

@ -1,7 +1,14 @@
pub use define::*;
use crate::services::entities::Session;
use flowy_user_deps::entities::UserProfile;
mod define;
pub mod document_empty_content;
pub mod migration;
pub mod session_migration;
mod util;
pub mod workspace_and_favorite_v1;
#[derive(Clone, Debug)]
pub struct MigrationUser {
pub user_profile: UserProfile,
pub session: Session,
}

View File

@ -0,0 +1,29 @@
use crate::manager::UserConfig;
use crate::services::entities::Session;
use flowy_sqlite::kv::StorePreferences;
use flowy_user_deps::entities::Authenticator;
use serde_json::{json, Value};
use std::sync::Arc;
use uuid::Uuid;
pub fn migrate_session_with_user_uuid(
authenticator: &Authenticator,
user_config: &UserConfig,
session: &Arc<parking_lot::RwLock<Option<Session>>>,
store_preferences: &Arc<StorePreferences>,
) {
if matches!(authenticator, Authenticator::Local) {
if let Some(mut value) = store_preferences.get_object::<Value>(&user_config.session_cache_key) {
if value.get("user_uuid").is_none() {
value.as_object_mut().map(|map| {
map.insert("user_uuid".to_string(), json!(Uuid::new_v4()));
});
}
if let Ok(new_session) = serde_json::from_value::<Session>(value) {
*session.write() = Some(new_session.clone());
let _ = store_preferences.set_object(&user_config.session_cache_key, &new_session);
}
}
}
}

View File

@ -6,7 +6,7 @@ use collab::preclude::Collab;
use collab_integrate::{PersistenceError, YrsDocAction};
use flowy_error::FlowyResult;
pub fn load_collab<'a, R>(
pub(crate) fn load_collab<'a, R>(
uid: i64,
collab_r_txn: &R,
object_id: &str,

View File

@ -20,7 +20,7 @@ use lib_dispatch::prelude::af_spawn;
use lib_infra::file_util::{unzip_and_replace, zip_folder};
use crate::services::user_sql::UserTable;
use crate::services::user_workspace_sql::UserWorkspaceTable;
use crate::services::workspace_sql::UserWorkspaceTable;
pub trait UserDBPath: Send + Sync + 'static {
fn user_db_path(&self, uid: i64) -> PathBuf;

View File

@ -6,6 +6,7 @@ use chrono::prelude::*;
use serde::de::{Deserializer, MapAccess, Visitor};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
use flowy_user_deps::entities::{AuthResponse, UserProfile, UserWorkspace};
use flowy_user_deps::entities::{Authenticator, UserAuthResponse};
@ -16,6 +17,7 @@ use crate::migrations::MigrationUser;
#[derive(Debug, Clone, Serialize)]
pub struct Session {
pub user_id: i64,
pub user_uuid: Uuid,
pub user_workspace: UserWorkspace,
}
@ -32,6 +34,7 @@ impl<'de> Visitor<'de> for SessionVisitor {
M: MapAccess<'de>,
{
let mut user_id = None;
let mut user_uuid = None;
// For historical reasons, the session used to contain a workspace_id field.
// This field is no longer used, and is replaced by user_workspace.
let mut workspace_id = None;
@ -42,6 +45,9 @@ impl<'de> Visitor<'de> for SessionVisitor {
"user_id" => {
user_id = Some(map.next_value()?);
},
"user_uuid" => {
user_uuid = Some(map.next_value()?);
},
"workspace_id" => {
workspace_id = Some(map.next_value()?);
},
@ -54,6 +60,7 @@ impl<'de> Visitor<'de> for SessionVisitor {
}
}
let user_id = user_id.ok_or(serde::de::Error::missing_field("user_id"))?;
let user_uuid = user_uuid.ok_or(serde::de::Error::missing_field("user_uuid"))?;
if user_workspace.is_none() {
if let Some(workspace_id) = workspace_id {
user_workspace = Some(UserWorkspace {
@ -61,13 +68,14 @@ impl<'de> Visitor<'de> for SessionVisitor {
name: "My Workspace".to_string(),
created_at: Utc::now(),
// For historical reasons, the database_storage_id is constructed by the user_id.
database_views_aggregate_id: STANDARD.encode(format!("{}:user:database", user_id)),
database_storage_id: STANDARD.encode(format!("{}:user:database", user_id)),
})
}
}
let session = Session {
user_id,
user_uuid,
user_workspace: user_workspace.ok_or(serde::de::Error::missing_field("user_workspace"))?,
};
@ -91,6 +99,7 @@ where
fn from(value: &T) -> Self {
Self {
user_id: value.user_id(),
user_uuid: value.user_uuid().clone(),
user_workspace: value.latest_workspace().clone(),
}
}
@ -98,55 +107,10 @@ where
impl std::convert::From<Session> for String {
fn from(session: Session) -> Self {
match serde_json::to_string(&session) {
Ok(s) => s,
Err(e) => {
tracing::error!("Serialize session to string failed: {:?}", e);
"".to_string()
},
}
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[derive(serde::Serialize)]
struct OldSession {
user_id: i64,
workspace_id: String,
name: String,
}
#[test]
fn deserialize_user_workspace_from_workspace_id() {
// For historical reasons, the session used to contain a workspace_id field.
let old = OldSession {
user_id: 223238635422486528,
workspace_id: "f58f5492-ee0a-4a9f-8cf1-dacb459a55f6".to_string(),
name: "Me".to_string(),
};
let s = serde_json::to_string(&old).unwrap();
let new = serde_json::from_str::<Session>(&s).unwrap();
assert_eq!(old.user_id, new.user_id);
assert_eq!(old.workspace_id, new.user_workspace.id);
let json = json!({
"user_id": 2232386,
"workspace_id": "f58f5492-ee0a-4a9f-8cf1-dacb459a55f6",
"name": "Me",
"token": null,
"email": "0085bfda-85fa-4611-bfbe-25d5a1229f44@appflowy.io"
});
let new = serde_json::from_value::<Session>(json).unwrap();
assert_eq!(new.user_id, 2232386);
assert_eq!(
new.user_workspace.id,
"f58f5492-ee0a-4a9f-8cf1-dacb459a55f6"
);
serde_json::to_string(&session).unwrap_or_else(|e| {
tracing::error!("Serialize session to string failed: {:?}", e);
"".to_string()
})
}
}

View File

@ -1,18 +1,14 @@
use diesel::RunQueryDsl;
use tracing::instrument;
use flowy_error::FlowyResult;
use flowy_sqlite::schema::user_workspace_table;
use flowy_sqlite::{query_dsl::*, ExpressionMethods};
use flowy_user_deps::entities::{Authenticator, UserWorkspace};
use lib_infra::util::timestamp;
use crate::entities::UserProfilePB;
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use flowy_user_deps::entities::Authenticator;
use crate::manager::UserManager;
use crate::migrations::MigrationUser;
use crate::services::entities::{HistoricalUser, HistoricalUsers, Session};
use crate::services::user_workspace_sql::UserWorkspaceTable;
use crate::services::entities::Session;
const HISTORICAL_USER: &str = "af_historical_users";
const ANON_USER: &str = "anon_user";
impl UserManager {
#[instrument(skip_all)]
pub async fn get_migration_user(&self, auth_type: &Authenticator) -> Option<MigrationUser> {
@ -32,54 +28,23 @@ impl UserManager {
None
}
}
/// Logs a user's details for historical tracking.
///
/// This function adds a user's details to a local historical tracking system, useful for
/// keeping track of past sign-ins or any other historical activities.
///
/// # Parameters
/// - `uid`: The user ID.
/// - `device_id`: The ID of the device the user is using.
/// - `user_name`: The name of the user.
/// - `auth_type`: The type of authentication used.
/// - `storage_path`: Path where user data is stored.
///
pub fn add_historical_user(
&self,
uid: i64,
device_id: &str,
user_name: String,
authenticator: &Authenticator,
storage_path: String,
) {
let mut logger_users = self
.store_preferences
.get_object::<HistoricalUsers>(HISTORICAL_USER)
.unwrap_or_default();
logger_users.add_user(HistoricalUser {
user_id: uid,
user_name,
auth_type: authenticator.clone(),
sign_in_timestamp: timestamp(),
storage_path,
device_id: device_id.to_string(),
});
let _ = self
.store_preferences
.set_object(HISTORICAL_USER, logger_users);
pub fn set_anon_user(&self, session: Session) {
let _ = self.store_preferences.set_object(ANON_USER, session);
}
/// Fetches a list of historical users, sorted by their sign-in timestamp.
///
/// This function retrieves a list of users who have previously been logged for historical tracking.
pub fn get_historical_users(&self) -> Vec<HistoricalUser> {
let mut users = self
pub async fn get_anon_user(&self) -> FlowyResult<UserProfilePB> {
let anon_session = self
.store_preferences
.get_object::<HistoricalUsers>(HISTORICAL_USER)
.unwrap_or_default()
.users;
users.sort_by(|a, b| b.sign_in_timestamp.cmp(&a.sign_in_timestamp));
users
.get_object::<Session>(ANON_USER)
.ok_or(FlowyError::new(
ErrorCode::RecordNotFound,
"Anon user not found",
))?;
let profile = self
.get_user_profile_from_disk(anon_session.user_id)
.await?;
Ok(UserProfilePB::from(profile))
}
/// Opens a historical user's session based on their user ID, device ID, and authentication type.
@ -87,19 +52,15 @@ impl UserManager {
/// This function facilitates the re-opening of a user's session from historical tracking.
/// It retrieves the user's workspace and establishes a new session for the user.
///
pub async fn open_historical_user(&self, uid: i64, auth_type: Authenticator) -> FlowyResult<()> {
debug_assert!(auth_type.is_local());
self.update_authenticator(&auth_type).await;
let conn = self.db_connection(uid)?;
let row = user_workspace_table::dsl::user_workspace_table
.filter(user_workspace_table::uid.eq(uid))
.first::<UserWorkspaceTable>(&*conn)?;
let user_workspace = UserWorkspace::from(row);
let session = Session {
user_id: uid,
user_workspace,
};
self.set_session(Some(session))?;
pub async fn open_anon_user(&self) -> FlowyResult<()> {
let anon_session = self
.store_preferences
.get_object::<Session>(ANON_USER)
.ok_or(FlowyError::new(
ErrorCode::RecordNotFound,
"Anon user not found",
))?;
self.set_session(Some(anon_session))?;
Ok(())
}
}

View File

@ -1,10 +1,10 @@
pub mod cloud_config;
pub mod collab_interact;
pub mod database;
pub mod db;
pub mod entities;
pub(crate) mod historical_user;
pub(crate) mod user_awareness;
pub(crate) mod user_encryption;
pub(crate) mod user_sql;
pub(crate) mod user_workspace;
pub(crate) mod user_workspace_sql;
pub(crate) mod workspace_sql;

View File

@ -6,6 +6,7 @@ use collab_entity::reminder::Reminder;
use collab_entity::CollabType;
use collab_user::core::{MutexUserAwareness, UserAwareness};
use tracing::{error, trace};
use uuid::Uuid;
use collab_integrate::RocksCollabDB;
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
@ -167,10 +168,11 @@ impl UserManager {
ErrorCode::Internal,
"Unexpected error: collab builder is not available",
))?;
let user_awareness_id = Uuid::new_v5(&session.user_uuid, b"user_awareness");
let collab = collab_builder
.build(
session.user_id,
&session.user_id.to_string(),
&user_awareness_id.to_string(),
CollabType::UserAwareness,
raw_data,
collab_db,

View File

@ -13,7 +13,7 @@ use lib_dispatch::prelude::af_spawn;
use crate::entities::{RepeatedUserWorkspacePB, ResetWorkspacePB};
use crate::manager::UserManager;
use crate::notification::{send_notification, UserNotification};
use crate::services::user_workspace_sql::UserWorkspaceTable;
use crate::services::workspace_sql::UserWorkspaceTable;
impl UserManager {
#[instrument(skip(self), err)]

View File

@ -23,7 +23,7 @@ impl TryFrom<(i64, &UserWorkspace)> for UserWorkspaceTable {
if value.1.id.is_empty() {
return Err(FlowyError::invalid_data().with_context("The id is empty"));
}
if value.1.database_views_aggregate_id.is_empty() {
if value.1.database_storage_id.is_empty() {
return Err(FlowyError::invalid_data().with_context("The database storage id is empty"));
}
@ -32,7 +32,7 @@ impl TryFrom<(i64, &UserWorkspace)> for UserWorkspaceTable {
name: value.1.name.clone(),
uid: value.0,
created_at: value.1.created_at.timestamp(),
database_storage_id: value.1.database_views_aggregate_id.clone(),
database_storage_id: value.1.database_storage_id.clone(),
})
}
}
@ -46,7 +46,7 @@ impl From<UserWorkspaceTable> for UserWorkspace {
.timestamp_opt(value.created_at, 0)
.single()
.unwrap_or_default(),
database_views_aggregate_id: value.database_storage_id,
database_storage_id: value.database_storage_id,
}
}
}