mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
@ -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"
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
pub mod database;
|
||||
mod user_session;
|
||||
pub use user_session::*;
|
||||
|
||||
pub mod database;
|
||||
mod user_data;
|
||||
mod user_session;
|
||||
|
91
frontend/rust-lib/flowy-user/src/services/user_data.rs
Normal file
91
frontend/rust-lib/flowy-user/src/services/user_data.rs
Normal 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))
|
||||
// }
|
@ -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)
|
||||
}
|
||||
|
Reference in New Issue
Block a user