mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: anon user save (#4185)
* feat: anon user save * chore: add missing files * chore: remove error
This commit is contained in:
parent
0730a0caf4
commit
1401ba5960
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
@ -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() {
|
||||
|
@ -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();
|
||||
},
|
||||
),
|
@ -1,4 +1,4 @@
|
||||
export 'screens/screens.dart';
|
||||
export 'widgets/widgets.dart';
|
||||
export 'historical_user.dart';
|
||||
export 'anon_user.dart';
|
||||
export 'router.dart';
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
@ -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)) {
|
||||
|
@ -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:
|
||||
|
@ -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"]
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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(),
|
||||
)
|
||||
|
@ -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(),
|
||||
)
|
||||
|
@ -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)]
|
||||
|
@ -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(())
|
||||
}
|
||||
|
||||
|
@ -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].
|
||||
|
@ -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(
|
||||
¤t_authenticator,
|
||||
&user_config,
|
||||
¤t_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.
|
||||
|
@ -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,
|
||||
}
|
@ -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,
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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;
|
@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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)]
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user