feat: reload UI (#2999)

* chore: reload folder

* chore: reload folder

* chore: init sync

* chore: update tables

* chore: update database

* chore: load row

* chore: update

* chore: reload row

* test: fit test

* chore: retry

* chore: support batch fetch

* chore: enable sync

* chore: sync switch

* chore: sync switch

* chore: migration user data

* chore: migrate data

* chore: migrate folder

* chore: save user email

* chore: refresh user profile

* chore: fix test

* chore: delete translation files

* test: clippy format
This commit is contained in:
Nathan.fooo
2023-07-14 13:37:13 +08:00
committed by GitHub
parent 5085ea115f
commit f9e7b5ffa4
170 changed files with 3380 additions and 1482 deletions

View File

@ -11,8 +11,11 @@ flowy-sqlite = { path = "../flowy-sqlite", optional = true }
flowy-error = { path = "../flowy-error", features = ["adaptor_database", "adaptor_dispatch"] }
lib-infra = { path = "../../../shared-lib/lib-infra" }
flowy-notification = { path = "../flowy-notification" }
flowy-server-config = { path = "../flowy-server-config" }
lib-dispatch = { path = "../lib-dispatch" }
appflowy-integrate = { version = "0.1.0" }
collab = { version = "0.1.0" }
collab-folder = { version = "0.1.0" }
tracing = { version = "0.1", features = ["log"] }
bytes = "1.4"
@ -32,6 +35,7 @@ tokio = { version = "1.26", features = ["rt"] }
validator = "0.16.0"
unicode-segmentation = "1.10"
fancy-regex = "0.11.0"
uuid = { version = "1.3.3", features = [ "v4"] }
[dev-dependencies]
nanoid = "0.4.0"

View File

@ -23,6 +23,10 @@ pub struct SignInPayloadPB {
#[pb(index = 4)]
pub auth_type: AuthTypePB,
// Only used in local sign in.
#[pb(index = 5, one_of)]
pub uid: Option<i64>,
}
impl TryInto<SignInParams> for SignInPayloadPB {
@ -37,6 +41,7 @@ impl TryInto<SignInParams> for SignInPayloadPB {
password: password.0,
name: self.name,
auth_type: self.auth_type.into(),
uid: self.uid,
})
}
}
@ -78,6 +83,8 @@ pub struct SignInParams {
pub password: String,
pub name: String,
pub auth_type: AuthType,
// Currently, the uid only used in local sign in.
pub uid: Option<i64>,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
@ -121,7 +128,7 @@ pub struct ThirdPartyAuthPB {
pub auth_type: AuthTypePB,
}
#[derive(ProtoBuf_Enum, Debug, Clone)]
#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)]
pub enum AuthTypePB {
Local = 0,
SelfHosted = 1,
@ -143,6 +150,7 @@ pub struct UserProfile {
pub icon_url: String,
pub openai_key: String,
pub workspace_id: String,
pub auth_type: AuthType,
}
#[derive(Serialize, Deserialize, Default, Clone, Debug)]
@ -191,12 +199,6 @@ impl UpdateUserProfileParams {
}
}
#[derive(ProtoBuf, Default)]
pub struct SignOutPB {
#[pb(index = 1)]
pub auth_type: AuthTypePB,
}
#[derive(Debug, ProtoBuf, Default)]
pub struct UserCredentialsPB {
#[pb(index = 1, one_of)]
@ -240,3 +242,9 @@ impl From<UserCredentialsPB> for UserCredentials {
Self::new(value.token, value.uid, value.uuid)
}
}
#[derive(Default, ProtoBuf)]
pub struct UserStatePB {
#[pb(index = 1)]
pub auth_type: AuthTypePB,
}

View File

@ -18,7 +18,7 @@ pub struct UserSettingPB {
pub(crate) user_folder: String,
}
#[derive(ProtoBuf, Default, Debug, PartialEq, Eq, Clone)]
#[derive(ProtoBuf, Default, Eq, PartialEq, Debug, Clone)]
pub struct UserProfilePB {
#[pb(index = 1)]
pub id: i64,
@ -37,6 +37,9 @@ pub struct UserProfilePB {
#[pb(index = 6)]
pub openai_key: String,
#[pb(index = 7)]
pub auth_type: AuthTypePB,
}
impl std::convert::From<UserProfile> for UserProfilePB {
@ -48,6 +51,7 @@ impl std::convert::From<UserProfile> for UserProfilePB {
token: user_profile.token,
icon_url: user_profile.icon_url,
openai_key: user_profile.openai_key,
auth_type: user_profile.auth_type.into(),
}
}
}

View File

@ -1,6 +1,11 @@
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::convert::TryFrom;
use serde::{Deserialize, Serialize};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::FlowyError;
use flowy_server_config::supabase_config::{PostgresConfiguration, SupabaseConfiguration};
#[derive(ProtoBuf, Default, Debug, Clone)]
pub struct UserPreferencesPB {
@ -97,3 +102,82 @@ impl std::default::Default for AppearanceSettingsPB {
}
}
}
#[derive(Default, ProtoBuf)]
pub struct SupabaseConfigPB {
#[pb(index = 1)]
supabase_url: String,
#[pb(index = 2)]
key: String,
#[pb(index = 3)]
jwt_secret: String,
#[pb(index = 4)]
pub postgres_config: PostgresConfigurationPB,
#[pb(index = 5)]
enable_sync: bool,
}
impl TryFrom<SupabaseConfigPB> for SupabaseConfiguration {
type Error = FlowyError;
fn try_from(config: SupabaseConfigPB) -> Result<Self, Self::Error> {
let postgres_config = PostgresConfiguration::try_from(config.postgres_config)?;
Ok(SupabaseConfiguration {
url: config.supabase_url,
key: config.key,
jwt_secret: config.jwt_secret,
enable_sync: config.enable_sync,
postgres_config,
})
}
}
impl From<SupabaseConfiguration> for SupabaseConfigPB {
fn from(value: SupabaseConfiguration) -> Self {
let postgres_config = PostgresConfigurationPB {
url: value.postgres_config.url,
user_name: value.postgres_config.user_name,
password: value.postgres_config.password,
port: value.postgres_config.port as u32,
};
Self {
supabase_url: value.url,
key: value.key,
jwt_secret: value.jwt_secret,
postgres_config,
enable_sync: value.enable_sync,
}
}
}
#[derive(Default, ProtoBuf)]
pub struct PostgresConfigurationPB {
#[pb(index = 1)]
pub url: String,
#[pb(index = 2)]
pub user_name: String,
#[pb(index = 3)]
pub password: String,
#[pb(index = 4)]
pub port: u32,
}
impl TryFrom<PostgresConfigurationPB> for PostgresConfiguration {
type Error = FlowyError;
fn try_from(config: PostgresConfigurationPB) -> Result<Self, Self::Error> {
Ok(Self {
url: config.url,
user_name: config.user_name,
password: config.password,
port: config.port as u16,
})
}
}

View File

@ -1,14 +1,15 @@
use std::convert::TryFrom;
use std::{convert::TryInto, sync::Arc};
use flowy_error::FlowyError;
use flowy_server_config::supabase_config::SupabaseConfiguration;
use flowy_sqlite::kv::KV;
use lib_dispatch::prelude::*;
use lib_infra::box_any::BoxAny;
use crate::entities::*;
use crate::entities::{SignInParams, SignUpParams, UpdateUserProfileParams};
use crate::event_map::UserCredentials;
use crate::services::{AuthType, UserSession};
use crate::services::{get_supabase_config, AuthType, UserSession};
#[tracing::instrument(level = "debug", name = "sign_in", skip(data, session), fields(email = %data.email), err)]
pub async fn sign_in(
@ -17,9 +18,10 @@ pub async fn sign_in(
) -> DataResult<UserProfilePB, FlowyError> {
let params: SignInParams = data.into_inner().try_into()?;
let auth_type = params.auth_type.clone();
session.update_auth_type(&auth_type).await;
let user_profile: UserProfilePB = session
.sign_in(&auth_type, BoxAny::new(params))
.sign_in(BoxAny::new(params), auth_type)
.await?
.into();
data_result_ok(user_profile)
@ -41,11 +43,10 @@ pub async fn sign_up(
) -> DataResult<UserProfilePB, FlowyError> {
let params: SignUpParams = data.into_inner().try_into()?;
let auth_type = params.auth_type.clone();
let user_profile: UserProfilePB = session
.sign_up(&auth_type, BoxAny::new(params))
.await?
.into();
data_result_ok(user_profile)
session.update_auth_type(&auth_type).await;
let user_profile = session.sign_up(auth_type, BoxAny::new(params)).await?;
data_result_ok(user_profile.into())
}
#[tracing::instrument(level = "debug", skip(session))]
@ -56,11 +57,9 @@ pub async fn init_user_handler(session: AFPluginState<Arc<UserSession>>) -> Resu
#[tracing::instrument(level = "debug", skip(session))]
pub async fn check_user_handler(
data: AFPluginData<UserCredentialsPB>,
session: AFPluginState<Arc<UserSession>>,
) -> Result<(), FlowyError> {
let credential = UserCredentials::from(data.into_inner());
session.check_user(credential).await?;
session.check_user().await?;
Ok(())
}
@ -68,17 +67,14 @@ pub async fn check_user_handler(
pub async fn get_user_profile_handler(
session: AFPluginState<Arc<UserSession>>,
) -> DataResult<UserProfilePB, FlowyError> {
let user_profile: UserProfilePB = session.get_user_profile().await?.into();
let uid = session.get_session()?.user_id;
let user_profile: UserProfilePB = session.get_user_profile(uid, true).await?.into();
data_result_ok(user_profile)
}
#[tracing::instrument(level = "debug", skip(data, session))]
pub async fn sign_out(
data: AFPluginData<SignOutPB>,
session: AFPluginState<Arc<UserSession>>,
) -> Result<(), FlowyError> {
let auth_type: AuthType = data.into_inner().auth_type.into();
session.sign_out(&auth_type).await?;
#[tracing::instrument(level = "debug", skip(session))]
pub async fn sign_out(session: AFPluginState<Arc<UserSession>>) -> Result<(), FlowyError> {
session.sign_out().await?;
Ok(())
}
@ -144,9 +140,25 @@ pub async fn third_party_auth_handler(
) -> DataResult<UserProfilePB, FlowyError> {
let params = data.into_inner();
let auth_type: AuthType = params.auth_type.into();
let user_profile: UserProfilePB = session
.sign_up(&auth_type, BoxAny::new(params.map))
.await?
.into();
data_result_ok(user_profile)
session.update_auth_type(&auth_type).await;
let user_profile = session.sign_up(auth_type, BoxAny::new(params.map)).await?;
data_result_ok(user_profile.into())
}
#[tracing::instrument(level = "debug", skip(data, session), err)]
pub async fn set_supabase_config_handler(
data: AFPluginData<SupabaseConfigPB>,
session: AFPluginState<Arc<UserSession>>,
) -> Result<(), FlowyError> {
let config = SupabaseConfiguration::try_from(data.into_inner())?;
session.save_supabase_config(config);
Ok(())
}
#[tracing::instrument(level = "debug", skip_all, err)]
pub async fn get_supabase_config_handler(
_session: AFPluginState<Arc<UserSession>>,
) -> DataResult<SupabaseConfigPB, FlowyError> {
let config = get_supabase_config().unwrap_or_default();
data_result_ok(config.into())
}

View File

@ -1,9 +1,11 @@
use std::sync::Arc;
use collab_folder::core::FolderData;
use strum_macros::Display;
use flowy_derive::{Flowy_Event, ProtoBuf_Enum};
use flowy_error::FlowyResult;
use flowy_server_config::supabase_config::SupabaseConfiguration;
use lib_dispatch::prelude::*;
use lib_infra::box_any::BoxAny;
use lib_infra::future::{to_fut, Fut, FutureResult};
@ -27,36 +29,38 @@ pub fn init(user_session: Arc<UserSession>) -> AFPlugin {
.event(UserEvent::SetAppearanceSetting, set_appearance_setting)
.event(UserEvent::GetAppearanceSetting, get_appearance_setting)
.event(UserEvent::GetUserSetting, get_user_setting)
.event(UserEvent::SetSupabaseConfig, set_supabase_config_handler)
.event(UserEvent::GetSupabaseConfig, get_supabase_config_handler)
.event(UserEvent::ThirdPartyAuth, third_party_auth_handler)
}
pub(crate) struct DefaultUserStatusCallback;
impl UserStatusCallback for DefaultUserStatusCallback {
fn auth_type_did_changed(&self, _auth_type: AuthType) {}
fn did_sign_in(&self, _user_id: i64, _workspace_id: &str) -> Fut<FlowyResult<()>> {
to_fut(async { Ok(()) })
}
fn did_sign_up(&self, _is_new: bool, _user_profile: &UserProfile) -> Fut<FlowyResult<()>> {
to_fut(async { Ok(()) })
}
fn did_expired(&self, _token: &str, _user_id: i64) -> Fut<FlowyResult<()>> {
to_fut(async { Ok(()) })
}
pub struct SignUpContext {
/// Indicate whether the user is new or not.
pub is_new: bool,
/// If the user is sign in as guest, and the is_new is true, then the folder data will be not
/// None.
pub local_folder: Option<FolderData>,
}
pub trait UserStatusCallback: Send + Sync + 'static {
/// When the [AuthType] changed, this method will be called. Currently, the auth type
/// will be changed when the user sign in or sign up.
fn auth_type_did_changed(&self, auth_type: AuthType);
/// This will be called after the application launches if the user is already signed in.
/// If the user is not signed in, this method will not be called
fn did_init(&self, user_id: i64, workspace_id: &str) -> Fut<FlowyResult<()>>;
/// Will be called after the user signed in.
fn did_sign_in(&self, user_id: i64, workspace_id: &str) -> Fut<FlowyResult<()>>;
fn did_sign_up(&self, is_new: bool, user_profile: &UserProfile) -> Fut<FlowyResult<()>>;
/// Will be called after the user signed up.
fn did_sign_up(&self, context: SignUpContext, user_profile: &UserProfile)
-> Fut<FlowyResult<()>>;
fn did_expired(&self, token: &str, user_id: i64) -> Fut<FlowyResult<()>>;
}
/// The user cloud service provider.
/// The provider can be supabase, firebase, aws, or any other cloud service.
pub trait UserCloudServiceProvider: Send + Sync + 'static {
fn update_supabase_config(&self, supabase_config: &SupabaseConfiguration);
fn set_auth_type(&self, auth_type: AuthType);
fn get_auth_service(&self) -> Result<Arc<dyn UserAuthService>, FlowyError>;
}
@ -65,6 +69,10 @@ impl<T> UserCloudServiceProvider for Arc<T>
where
T: UserCloudServiceProvider,
{
fn update_supabase_config(&self, supabase_config: &SupabaseConfiguration) {
(**self).update_supabase_config(supabase_config)
}
fn set_auth_type(&self, auth_type: AuthType) {
(**self).set_auth_type(auth_type)
}
@ -140,6 +148,32 @@ pub trait UserAuthService: Send + Sync {
fn check_user(&self, credential: UserCredentials) -> FutureResult<(), FlowyError>;
}
/// Acts as a placeholder [UserStatusCallback] for the user session, but does not perform any function
pub(crate) struct DefaultUserStatusCallback;
impl UserStatusCallback for DefaultUserStatusCallback {
fn auth_type_did_changed(&self, _auth_type: AuthType) {}
fn did_init(&self, _user_id: i64, _workspace_id: &str) -> Fut<FlowyResult<()>> {
to_fut(async { Ok(()) })
}
fn did_sign_in(&self, _user_id: i64, _workspace_id: &str) -> Fut<FlowyResult<()>> {
to_fut(async { Ok(()) })
}
fn did_sign_up(
&self,
_context: SignUpContext,
_user_profile: &UserProfile,
) -> Fut<FlowyResult<()>> {
to_fut(async { Ok(()) })
}
fn did_expired(&self, _token: &str, _user_id: i64) -> Fut<FlowyResult<()>> {
to_fut(async { Ok(()) })
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
#[event_err = "FlowyError"]
pub enum UserEvent {
@ -154,7 +188,7 @@ pub enum UserEvent {
SignUp = 1,
/// Logging out fo an account
#[event(input = "SignOutPB")]
#[event()]
SignOut = 2,
/// Update the user information
@ -187,4 +221,12 @@ pub enum UserEvent {
#[event(input = "ThirdPartyAuthPB", output = "UserProfilePB")]
ThirdPartyAuth = 10,
/// Set the supabase config. It will be written to the environment variables.
/// Check out the `write_to_env` of [SupabaseConfigPB].
#[event(input = "SupabaseConfigPB")]
SetSupabaseConfig = 13,
#[event(output = "SupabaseConfigPB")]
GetSupabaseConfig = 14,
}

View File

@ -7,82 +7,28 @@ use parking_lot::RwLock;
use flowy_error::{ErrorCode, FlowyError};
use flowy_sqlite::ConnectionPool;
use flowy_sqlite::{schema::user_table, DBConnection, Database};
use flowy_sqlite::{
query_dsl::*,
schema::{user_table, user_table::dsl},
DBConnection, Database, ExpressionMethods,
};
use crate::entities::{SignInResponse, SignUpResponse, UpdateUserProfileParams, UserProfile};
use crate::services::AuthType;
pub struct UserDB {
db_dir: String,
root: String,
}
impl UserDB {
pub fn new(db_dir: &str) -> Self {
Self {
db_dir: db_dir.to_owned(),
root: db_dir.to_owned(),
}
}
fn open_user_db_if_need(&self, user_id: i64) -> Result<Arc<ConnectionPool>, FlowyError> {
if let Some(database) = DB_MAP.read().get(&user_id) {
return Ok(database.get_pool());
}
let mut write_guard = DB_MAP.write();
// The Write guard acquire exclusive access that will guarantee the user db only initialize once.
match write_guard.get(&user_id) {
None => {},
Some(database) => return Ok(database.get_pool()),
}
let mut dir = PathBuf::new();
dir.push(&self.db_dir);
dir.push(user_id.to_string());
let dir = dir.to_str().unwrap().to_owned();
tracing::debug!("open sqlite db {} at path: {}", user_id, dir);
let db = flowy_sqlite::init(&dir).map_err(|e| {
tracing::error!("open user db failed, {:?}", e);
FlowyError::new(ErrorCode::MultipleDBInstance, e)
})?;
let pool = db.get_pool();
write_guard.insert(user_id.to_owned(), db);
drop(write_guard);
Ok(pool)
}
fn open_collab_db_if_need(&self, user_id: i64) -> Result<Arc<RocksCollabDB>, FlowyError> {
if let Some(kv) = COLLAB_DB_MAP.read().get(&user_id) {
return Ok(kv.clone());
}
let mut write_guard = COLLAB_DB_MAP.write();
// The Write guard acquire exclusive access that will guarantee the user db only initialize once.
match write_guard.get(&user_id) {
None => {},
Some(kv) => return Ok(kv.clone()),
}
let mut dir = PathBuf::new();
dir.push(&self.db_dir);
dir.push(user_id.to_string());
dir.push("collab_db");
tracing::trace!("open collab db {} at path: {:?}", user_id, dir);
let db = match RocksCollabDB::open(dir) {
Ok(db) => Ok(db),
Err(err) => {
tracing::error!("open collab db failed, {:?}", err);
Err(FlowyError::new(ErrorCode::MultipleDBInstance, err))
},
}?;
let db = Arc::new(db);
write_guard.insert(user_id.to_owned(), db.clone());
drop(write_guard);
Ok(db)
}
pub(crate) fn close_user_db(&self, user_id: i64) -> Result<(), FlowyError> {
/// Close the database connection for the user.
pub(crate) fn close(&self, user_id: i64) -> Result<(), FlowyError> {
if let Some(mut sqlite_dbs) = DB_MAP.try_write_for(Duration::from_millis(300)) {
sqlite_dbs.remove(&user_id);
}
@ -101,16 +47,81 @@ impl UserDB {
}
pub(crate) fn get_pool(&self, user_id: i64) -> Result<Arc<ConnectionPool>, FlowyError> {
let pool = self.open_user_db_if_need(user_id)?;
let pool = open_user_db(&self.root, user_id)?;
Ok(pool)
}
pub(crate) fn get_collab_db(&self, user_id: i64) -> Result<Arc<RocksCollabDB>, FlowyError> {
let collab_db = self.open_collab_db_if_need(user_id)?;
let collab_db = open_collab_db(&self.root, user_id)?;
Ok(collab_db)
}
}
pub fn open_user_db(root: &str, user_id: i64) -> Result<Arc<ConnectionPool>, FlowyError> {
if let Some(database) = DB_MAP.read().get(&user_id) {
return Ok(database.get_pool());
}
let mut write_guard = DB_MAP.write();
let dir = user_db_path_from_uid(root, user_id);
tracing::debug!("open sqlite db {} at path: {:?}", user_id, dir);
let db = flowy_sqlite::init(&dir)
.map_err(|e| FlowyError::internal().context(format!("open user db failed, {:?}", e)))?;
let pool = db.get_pool();
write_guard.insert(user_id.to_owned(), db);
drop(write_guard);
Ok(pool)
}
pub fn get_user_profile(pool: &Arc<ConnectionPool>, uid: i64) -> Result<UserProfile, FlowyError> {
let uid = uid.to_string();
let conn = pool.get()?;
let user = dsl::user_table
.filter(user_table::id.eq(&uid))
.first::<UserTable>(&*conn)?;
Ok(user.into())
}
pub fn user_db_path_from_uid(root: &str, uid: i64) -> PathBuf {
let mut dir = PathBuf::new();
dir.push(root);
dir.push(uid.to_string());
dir
}
/// Open a collab db for the user. If the db is already opened, return the opened db.
///
pub fn open_collab_db(root: &str, uid: i64) -> Result<Arc<RocksCollabDB>, FlowyError> {
if let Some(collab_db) = COLLAB_DB_MAP.read().get(&uid) {
return Ok(collab_db.clone());
}
let mut write_guard = COLLAB_DB_MAP.write();
let dir = collab_db_path_from_uid(root, uid);
tracing::trace!("open collab db {} at path: {:?}", uid, dir);
let db = match RocksCollabDB::open(dir) {
Ok(db) => Ok(db),
Err(err) => {
tracing::error!("open collab db failed, {:?}", err);
Err(FlowyError::new(ErrorCode::MultipleDBInstance, err))
},
}?;
let db = Arc::new(db);
write_guard.insert(uid.to_owned(), db.clone());
drop(write_guard);
Ok(db)
}
pub fn collab_db_path_from_uid(root: &str, uid: i64) -> PathBuf {
let mut dir = PathBuf::new();
dir.push(root);
dir.push(uid.to_string());
dir.push("collab_db");
dir
}
lazy_static! {
static ref DB_MAP: RwLock<HashMap<i64, Database>> = RwLock::new(HashMap::new());
static ref COLLAB_DB_MAP: RwLock<HashMap<i64, Arc<RocksCollabDB>>> = RwLock::new(HashMap::new());
@ -128,29 +139,19 @@ pub struct UserTable {
pub(crate) openai_key: String,
pub(crate) token: String,
pub(crate) email: String,
pub(crate) auth_type: i32,
}
impl UserTable {
pub fn new(id: String, name: String, email: String, token: String, workspace_id: String) -> Self {
Self {
id,
name,
email,
token,
icon_url: "".to_owned(),
workspace: workspace_id,
openai_key: "".to_owned(),
}
}
pub fn set_workspace(mut self, workspace: String) -> Self {
self.workspace = workspace;
self
}
}
impl From<SignUpResponse> for UserTable {
fn from(resp: SignUpResponse) -> Self {
impl From<(SignUpResponse, AuthType)> for UserTable {
fn from(params: (SignUpResponse, AuthType)) -> Self {
let resp = params.0;
UserTable {
id: resp.user_id.to_string(),
name: resp.name,
@ -159,12 +160,15 @@ impl From<SignUpResponse> for UserTable {
workspace: resp.workspace_id,
icon_url: "".to_string(),
openai_key: "".to_string(),
auth_type: params.1 as i32,
}
}
}
impl From<SignInResponse> for UserTable {
fn from(resp: SignInResponse) -> Self {
impl From<(SignInResponse, AuthType)> for UserTable {
fn from(params: (SignInResponse, AuthType)) -> Self {
let resp = params.0;
let auth_type = params.1;
UserTable {
id: resp.user_id.to_string(),
name: resp.name,
@ -173,6 +177,7 @@ impl From<SignInResponse> for UserTable {
workspace: resp.workspace_id,
icon_url: "".to_string(),
openai_key: "".to_string(),
auth_type: auth_type as i32,
}
}
}
@ -187,6 +192,7 @@ impl From<UserTable> for UserProfile {
icon_url: table.icon_url,
openai_key: table.openai_key,
workspace_id: table.workspace,
auth_type: AuthType::from(table.auth_type),
}
}
}
@ -213,4 +219,15 @@ impl UserTableChangeset {
openai_key: params.openai_key,
}
}
pub fn from_user_profile(user_profile: UserProfile) -> Self {
UserTableChangeset {
id: user_profile.id.to_string(),
workspace: None,
name: Some(user_profile.name),
email: Some(user_profile.email),
icon_url: Some(user_profile.icon_url),
openai_key: Some(user_profile.openai_key),
}
}
}

View File

@ -1,3 +1,5 @@
pub mod database;
mod user_session;
pub use user_session::*;
pub mod database;
mod user_data;
mod user_session;

View File

@ -0,0 +1,91 @@
use std::sync::Arc;
use appflowy_integrate::{RocksCollabDB, YrsDocAction};
use collab::core::collab::MutexCollab;
use collab::core::origin::{CollabClient, CollabOrigin};
use collab::preclude::Collab;
use collab_folder::core::{Folder, FolderData};
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
pub struct UserDataMigration();
impl UserDataMigration {
pub fn migration(
old_uid: i64,
old_collab_db: &Arc<RocksCollabDB>,
old_workspace_id: &str,
new_uid: i64,
new_collab_db: &Arc<RocksCollabDB>,
new_workspace_id: &str,
) -> FlowyResult<Option<FolderData>> {
let mut folder_data = None;
new_collab_db
.with_write_txn(|w_txn| {
let read_txn = old_collab_db.read_txn();
if let Ok(object_ids) = read_txn.get_all_docs() {
// Migration of all objects
for object_id in object_ids {
tracing::debug!("migrate object: {:?}", object_id);
if let Ok(updates) = read_txn.get_all_updates(old_uid, &object_id) {
// If the object is a folder, migrate the folder data
if object_id == old_workspace_id {
let origin = CollabOrigin::Client(CollabClient::new(old_uid, ""));
if let Ok(old_folder_collab) =
Collab::new_with_raw_data(origin, &object_id, updates, vec![])
{
let mutex_collab = Arc::new(MutexCollab::from_collab(old_folder_collab));
let old_folder = Folder::open(mutex_collab, None);
folder_data = migrate_folder(new_workspace_id, old_folder);
}
} else {
let origin = CollabOrigin::Client(CollabClient::new(new_uid, ""));
match Collab::new_with_raw_data(origin, &object_id, updates, vec![]) {
Ok(collab) => {
let txn = collab.transact();
if let Err(err) = w_txn.create_new_doc(new_uid, &object_id, &txn) {
tracing::error!("🔴migrate collab failed: {:?}", err);
}
},
Err(err) => tracing::error!("🔴construct migration collab failed: {:?} ", err),
}
}
}
}
}
Ok(())
})
.map_err(|err| FlowyError::new(ErrorCode::Internal, err))?;
Ok(folder_data)
}
}
fn migrate_folder(new_workspace_id: &str, old_folder: Folder) -> Option<FolderData> {
let mut folder_data = old_folder.get_folder_data()?;
let old_workspace_id = folder_data.current_workspace_id;
folder_data.current_workspace_id = new_workspace_id.to_string();
let mut workspace = folder_data.workspaces.pop()?;
if folder_data.workspaces.len() > 1 {
tracing::error!("🔴migrate folder: more than one workspace");
}
workspace.id = new_workspace_id.to_string();
// Only take one workspace
folder_data.workspaces.clear();
folder_data.workspaces.push(workspace);
// Update the view's parent view id to new workspace id
folder_data.views.iter_mut().for_each(|view| {
if view.parent_view_id == old_workspace_id {
view.parent_view_id = new_workspace_id.to_string();
}
});
Some(folder_data)
}
// fn open_collab_db(uid: i64, root: String) -> FlowyResult<RocksCollabDB> {
// let dir = collab_db_path_from_uid(&root, uid);
// RocksCollabDB::open(dir).map_err(|err| FlowyError::new(ErrorCode::Internal, err))
// }

View File

@ -1,17 +1,22 @@
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;
use appflowy_integrate::RocksCollabDB;
use collab_folder::core::FolderData;
use serde::{Deserialize, Serialize};
use serde_repr::*;
use tokio::sync::RwLock;
use uuid::Uuid;
use flowy_error::internal_error;
use flowy_error::{internal_error, ErrorCode};
use flowy_server_config::supabase_config::SupabaseConfiguration;
use flowy_sqlite::ConnectionPool;
use flowy_sqlite::{
kv::KV,
query_dsl::*,
schema::{user_table, user_table::dsl},
DBConnection, ExpressionMethods, UserDatabaseConnection,
DBConnection, ExpressionMethods,
};
use lib_infra::box_any::BoxAny;
@ -20,15 +25,17 @@ use crate::entities::{
};
use crate::entities::{UserProfilePB, UserSettingPB};
use crate::event_map::{
DefaultUserStatusCallback, UserCloudServiceProvider, UserCredentials, UserStatusCallback,
DefaultUserStatusCallback, SignUpContext, UserCloudServiceProvider, UserCredentials,
UserStatusCallback,
};
use crate::services::user_data::UserDataMigration;
use crate::{
errors::FlowyError,
event_map::UserAuthService,
notification::*,
services::database::{UserDB, UserTable, UserTableChangeset},
};
pub(crate) const SUPABASE_CONFIG_CACHE_KEY: &str = "supabase_config_cache_key";
pub struct UserSessionConfig {
root_dir: String,
@ -73,16 +80,18 @@ impl UserSession {
pub async fn init<C: UserStatusCallback + 'static>(&self, user_status_callback: C) {
if let Ok(session) = self.get_session() {
let _ = user_status_callback
.did_sign_in(session.user_id, &session.workspace_id)
.await;
if let Err(e) = user_status_callback
.did_init(session.user_id, &session.workspace_id)
.await
{
tracing::error!("Failed to call did_sign_in callback: {:?}", e);
}
}
*self.user_status_callback.write().await = Arc::new(user_status_callback);
}
pub fn db_connection(&self) -> Result<DBConnection, FlowyError> {
let user_id = self.get_session()?.user_id;
self.database.get_connection(user_id)
pub fn db_connection(&self, uid: i64) -> Result<DBConnection, FlowyError> {
self.database.get_connection(uid)
}
// The caller will be not 'Sync' before of the return value,
@ -91,29 +100,44 @@ impl UserSession {
//
// let pool = self.db_connection_pool()?;
// let conn: PooledConnection<ConnectionManager> = pool.get()?;
pub fn db_pool(&self) -> Result<Arc<ConnectionPool>, FlowyError> {
let user_id = self.get_session()?.user_id;
self.database.get_pool(user_id)
pub fn db_pool(&self, uid: i64) -> Result<Arc<ConnectionPool>, FlowyError> {
self.database.get_pool(uid)
}
pub fn get_collab_db(&self) -> Result<Arc<RocksCollabDB>, FlowyError> {
let user_id = self.get_session()?.user_id;
self.database.get_collab_db(user_id)
pub fn get_collab_db(&self, uid: i64) -> Result<Arc<RocksCollabDB>, FlowyError> {
self.database.get_collab_db(uid)
}
pub async fn migrate_old_user_data(
&self,
old_uid: i64,
old_workspace_id: &str,
new_uid: i64,
new_workspace_id: &str,
) -> Result<Option<FolderData>, FlowyError> {
let old_collab_db = self.database.get_collab_db(old_uid)?;
let new_collab_db = self.database.get_collab_db(new_uid)?;
let folder_data = UserDataMigration::migration(
old_uid,
&old_collab_db,
old_workspace_id,
new_uid,
&new_collab_db,
new_workspace_id,
)?;
Ok(folder_data)
}
pub fn clear_old_user(&self, old_uid: i64) {
let _ = self.database.close(old_uid);
}
#[tracing::instrument(level = "debug", skip(self, params))]
pub async fn sign_in(
&self,
auth_type: &AuthType,
params: BoxAny,
auth_type: AuthType,
) -> Result<UserProfile, FlowyError> {
self
.user_status_callback
.read()
.await
.auth_type_did_changed(auth_type.clone());
self.cloud_services.set_auth_type(auth_type.clone());
let resp = self
.cloud_services
.get_auth_service()?
@ -121,14 +145,18 @@ impl UserSession {
.await?;
let session: Session = resp.clone().into();
let uid = session.user_id;
self.set_session(Some(session))?;
let user_profile: UserProfile = self.save_user(resp.into()).await?.into();
let _ = self
let user_profile: UserProfile = self.save_user(uid, (resp, auth_type).into()).await?.into();
if let Err(e) = self
.user_status_callback
.read()
.await
.did_sign_in(user_profile.id, &user_profile.workspace_id)
.await;
.await
{
tracing::error!("Failed to call did_sign_in callback: {:?}", e);
}
send_sign_in_notification()
.payload::<UserProfilePB>(user_profile.clone().into())
.send();
@ -136,12 +164,7 @@ impl UserSession {
Ok(user_profile)
}
#[tracing::instrument(level = "debug", skip(self, params))]
pub async fn sign_up(
&self,
auth_type: &AuthType,
params: BoxAny,
) -> Result<UserProfile, FlowyError> {
pub async fn update_auth_type(&self, auth_type: &AuthType) {
self
.user_status_callback
.read()
@ -149,42 +172,86 @@ impl UserSession {
.auth_type_did_changed(auth_type.clone());
self.cloud_services.set_auth_type(auth_type.clone());
let auth_service = self.cloud_services.get_auth_service()?;
let resp = auth_service.sign_up(params).await?;
}
let is_new = resp.is_new;
let session: Session = resp.clone().into();
#[tracing::instrument(level = "debug", skip(self, params))]
pub async fn sign_up(
&self,
auth_type: AuthType,
params: BoxAny,
) -> Result<UserProfile, FlowyError> {
let old_user_profile = {
if let Ok(old_session) = self.get_session() {
self.get_user_profile(old_session.user_id, false).await.ok()
} else {
None
}
};
let auth_service = self.cloud_services.get_auth_service()?;
let response: SignUpResponse = auth_service.sign_up(params).await?;
let mut sign_up_context = SignUpContext {
is_new: response.is_new,
local_folder: None,
};
let session = Session {
user_id: response.user_id,
workspace_id: response.workspace_id.clone(),
};
let uid = session.user_id;
self.set_session(Some(session))?;
let user_table = self.save_user(resp.into()).await?;
let user_profile: UserProfile = user_table.into();
let user_table = self
.save_user(uid, (response, auth_type.clone()).into())
.await?;
let new_user_profile: UserProfile = user_table.into();
// Only migrate the data if the user is login in as a guest and sign up as a new user
if sign_up_context.is_new {
if let Some(old_user_profile) = old_user_profile {
if old_user_profile.auth_type == AuthType::Local && !auth_type.is_local() {
tracing::info!(
"Migrate old user data from {:?} to {:?}",
old_user_profile.id,
new_user_profile.id
);
match self
.migrate_old_user_data(
old_user_profile.id,
&old_user_profile.workspace_id,
new_user_profile.id,
&new_user_profile.workspace_id,
)
.await
{
Ok(folder_data) => sign_up_context.local_folder = folder_data,
Err(e) => tracing::error!("{:?}", e),
}
}
}
}
let _ = self
.user_status_callback
.read()
.await
.did_sign_up(is_new, &user_profile)
.did_sign_up(sign_up_context, &new_user_profile)
.await;
Ok(user_profile)
Ok(new_user_profile)
}
#[tracing::instrument(level = "debug", skip(self))]
pub async fn sign_out(&self, auth_type: &AuthType) -> Result<(), FlowyError> {
pub async fn sign_out(&self) -> Result<(), FlowyError> {
let session = self.get_session()?;
let uid = session.user_id.to_string();
let _ = diesel::delete(dsl::user_table.filter(dsl::id.eq(&uid)))
.execute(&*(self.db_connection()?))?;
self.database.close_user_db(session.user_id)?;
self.database.close(session.user_id)?;
self.set_session(None)?;
let server = self.cloud_services.get_auth_service()?;
let token = session.token;
tokio::spawn(async move {
match server.sign_out(token).await {
match server.sign_out(None).await {
Ok(_) => {},
Err(e) => tracing::error!("Sign out failed: {:?}", e),
}
});
Ok(())
}
@ -196,9 +263,14 @@ impl UserSession {
let auth_type = params.auth_type.clone();
let session = self.get_session()?;
let changeset = UserTableChangeset::new(params.clone());
diesel_update_table!(user_table, changeset, &*self.db_connection()?);
diesel_update_table!(
user_table,
changeset,
&*self.db_connection(session.user_id)?
);
let user_profile = self.get_user_profile().await?;
let session = self.get_session()?;
let user_profile = self.get_user_profile(session.user_id, false).await?;
let profile_pb: UserProfilePB = user_profile.into();
send_notification(
&session.user_id.to_string(),
@ -207,7 +279,7 @@ impl UserSession {
.payload(profile_pb)
.send();
self
.update_user(&auth_type, session.user_id, &session.token, params)
.update_user(&auth_type, session.user_id, None, params)
.await?;
Ok(())
}
@ -216,17 +288,52 @@ impl UserSession {
Ok(())
}
pub async fn check_user(&self, credential: UserCredentials) -> Result<(), FlowyError> {
pub async fn check_user(&self) -> Result<(), FlowyError> {
let user_id = self.get_session()?.user_id;
let credential = UserCredentials::from_uid(user_id);
let auth_service = self.cloud_services.get_auth_service()?;
auth_service.check_user(credential).await
}
pub async fn get_user_profile(&self) -> Result<UserProfile, FlowyError> {
let (user_id, _) = self.get_session()?.into_part();
let user_id = user_id.to_string();
pub async fn check_user_with_uuid(&self, uuid: &Uuid) -> Result<(), FlowyError> {
let credential = UserCredentials::from_uuid(uuid.to_string());
let auth_service = self.cloud_services.get_auth_service()?;
auth_service.check_user(credential).await
}
/// Get the user profile from the database
/// If the refresh is true, it will try to get the user profile from the server
pub async fn get_user_profile(&self, uid: i64, refresh: bool) -> Result<UserProfile, FlowyError> {
let user_id = uid.to_string();
let user = dsl::user_table
.filter(user_table::id.eq(&user_id))
.first::<UserTable>(&*(self.db_connection()?))?;
.first::<UserTable>(&*(self.db_connection(uid)?))?;
if refresh {
let weak_auth_service = Arc::downgrade(&self.cloud_services.get_auth_service()?);
let weak_pool = Arc::downgrade(&self.database.get_pool(uid)?);
tokio::spawn(async move {
if let (Some(auth_service), Some(pool)) = (weak_auth_service.upgrade(), weak_pool.upgrade())
{
if let Ok(Some(user_profile)) = auth_service
.get_user_profile(UserCredentials::from_uid(uid))
.await
{
let changeset = UserTableChangeset::from_user_profile(user_profile.clone());
if let Ok(conn) = pool.get() {
let filter = dsl::user_table.filter(dsl::id.eq(changeset.id.clone()));
let _ = diesel::update(filter).set(changeset).execute(&*conn);
// Send notification to the client
let user_profile_pb: UserProfilePB = user_profile.into();
send_notification(&uid.to_string(), UserNotification::DidUpdateUserProfile)
.payload(user_profile_pb)
.send();
}
}
}
});
}
Ok(user.into())
}
@ -250,21 +357,28 @@ impl UserSession {
Ok(self.get_session()?.user_id)
}
pub fn user_name(&self) -> Result<String, FlowyError> {
Ok(self.get_session()?.name)
pub fn token(&self) -> Result<Option<String>, FlowyError> {
Ok(None)
}
pub fn token(&self) -> Result<Option<String>, FlowyError> {
Ok(self.get_session()?.token)
pub fn save_supabase_config(&self, config: SupabaseConfiguration) {
self.cloud_services.update_supabase_config(&config);
let _ = KV::set_object(SUPABASE_CONFIG_CACHE_KEY, config);
}
}
pub fn get_supabase_config() -> Option<SupabaseConfiguration> {
KV::get_str(SUPABASE_CONFIG_CACHE_KEY)
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_else(|| SupabaseConfiguration::from_env().ok())
}
impl UserSession {
async fn update_user(
&self,
_auth_type: &AuthType,
uid: i64,
token: &Option<String>,
token: Option<String>,
params: UpdateUserProfileParams,
) -> Result<(), FlowyError> {
let server = self.cloud_services.get_auth_service()?;
@ -282,8 +396,8 @@ impl UserSession {
Ok(())
}
async fn save_user(&self, user: UserTable) -> Result<UserTable, FlowyError> {
let conn = self.db_connection()?;
async fn save_user(&self, uid: i64, user: UserTable) -> Result<UserTable, FlowyError> {
let conn = self.db_connection(uid)?;
conn.immediate_transaction(|| {
// delete old user if exists
diesel::delete(dsl::user_table.filter(dsl::id.eq(&user.id))).execute(&*conn)?;
@ -309,77 +423,47 @@ impl UserSession {
Ok(())
}
fn get_session(&self) -> Result<Session, FlowyError> {
/// Returns the current user session.
pub fn get_session(&self) -> Result<Session, FlowyError> {
match KV::get_object::<Session>(&self.session_config.session_cache_key) {
None => Err(FlowyError::unauthorized()),
None => Err(FlowyError::new(
ErrorCode::RecordNotFound,
"User is not logged in".to_string(),
)),
Some(session) => Ok(session),
}
}
}
pub async fn update_user(
_cloud_service: Arc<dyn UserAuthService>,
pool: Arc<ConnectionPool>,
params: UpdateUserProfileParams,
) -> Result<(), FlowyError> {
let changeset = UserTableChangeset::new(params);
let conn = pool.get()?;
diesel_update_table!(user_table, changeset, &*conn);
Ok(())
}
impl UserDatabaseConnection for UserSession {
fn get_connection(&self) -> Result<DBConnection, String> {
self.db_connection().map_err(|e| format!("{:?}", e))
pub fn sign_in_history(&self) -> Vec<UserProfile> {
// match self.db_connection(uid) {
// Ok(conn) => match dsl::user_table.load::<UserTable>(&*conn) {
// Ok(users) => users.into_iter().map(|u| u.into()).collect(),
// Err(_) => vec![],
// },
// Err(e) => {
// tracing::error!("get user sign in history failed: {:?}", e);
// vec![]
// },
// }
vec![]
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
struct Session {
user_id: i64,
workspace_id: String,
#[serde(default)]
name: String,
#[serde(default)]
token: Option<String>,
#[serde(default)]
email: Option<String>,
pub struct Session {
pub user_id: i64,
pub workspace_id: String,
}
impl std::convert::From<SignInResponse> for Session {
fn from(resp: SignInResponse) -> Self {
Session {
user_id: resp.user_id,
token: resp.token,
email: resp.email,
name: resp.name,
workspace_id: resp.workspace_id,
}
}
}
impl std::convert::From<SignUpResponse> for Session {
fn from(resp: SignUpResponse) -> Self {
Session {
user_id: resp.user_id,
token: resp.token,
email: resp.email,
name: resp.name,
workspace_id: resp.workspace_id,
}
}
}
impl Session {
pub fn into_part(self) -> (i64, Option<String>) {
(self.user_id, self.token)
}
}
impl std::convert::From<String> for Session {
fn from(s: String) -> Self {
match serde_json::from_str(&s) {
@ -415,6 +499,12 @@ pub enum AuthType {
Supabase = 2,
}
impl AuthType {
pub fn is_local(&self) -> bool {
matches!(self, AuthType::Local)
}
}
impl Default for AuthType {
fn default() -> Self {
Self::Local
@ -430,3 +520,44 @@ impl From<AuthTypePB> for AuthType {
}
}
}
impl From<AuthType> for AuthTypePB {
fn from(auth_type: AuthType) -> Self {
match auth_type {
AuthType::Supabase => AuthTypePB::Supabase,
AuthType::Local => AuthTypePB::Local,
AuthType::SelfHosted => AuthTypePB::SelfHosted,
}
}
}
impl From<i32> for AuthType {
fn from(value: i32) -> Self {
match value {
0 => AuthType::Local,
1 => AuthType::SelfHosted,
2 => AuthType::Supabase,
_ => AuthType::Local,
}
}
}
pub struct ThirdPartyParams {
pub uuid: Uuid,
pub email: String,
}
pub fn uuid_from_box_any(any: BoxAny) -> Result<ThirdPartyParams, FlowyError> {
let map: HashMap<String, String> = any.unbox_or_error()?;
let uuid = uuid_from_map(&map)?;
let email = map.get("email").cloned().unwrap_or_default();
Ok(ThirdPartyParams { uuid, email })
}
pub fn uuid_from_map(map: &HashMap<String, String>) -> Result<Uuid, FlowyError> {
let uuid = map
.get("uuid")
.ok_or_else(|| FlowyError::new(ErrorCode::MissingAuthField, "Missing uuid field"))?
.as_str();
Uuid::from_str(uuid).map_err(internal_error)
}