feat: encrypt collab update (#3215)

* feat: implement encrypt and decrypt

* feat: encrypt and decrypt

* feat: update user profile with encrypt

* chore: store encryption sign

* fix: login in setting menu

* chore: show encryption account name

* chore: fix test

* ci: fix warnings

* test: enable supabase test

* chore: fix test and rename column

* fix: update user profile after set the secret

* fix: encryption with wrong secret

* fix: don't save user data if the return value of did_sign_up is err

* chore: encrypt snapshot data

* chore: refactor snapshots interface

* ci: add tests

* chore: update collab rev
This commit is contained in:
Nathan.fooo
2023-08-17 23:46:39 +08:00
committed by GitHub
parent 103f56922f
commit 649b0a135a
103 changed files with 2825 additions and 905 deletions

View File

@ -8,6 +8,7 @@ edition = "2018"
[dependencies]
flowy-derive = { path = "../../../shared-lib/flowy-derive" }
flowy-sqlite = { path = "../flowy-sqlite", optional = true }
flowy-encrypt = { path = "../flowy-encrypt" }
flowy-error = { path = "../flowy-error", features = ["impl_from_sqlite", "impl_from_dispatch_error"] }
lib-infra = { path = "../../../shared-lib/lib-infra" }
flowy-notification = { path = "../flowy-notification" }

View File

@ -1,6 +1,6 @@
use std::convert::TryInto;
use flowy_derive::ProtoBuf;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_user_deps::entities::*;
use crate::entities::parser::{UserEmail, UserIcon, UserName, UserOpenaiKey, UserPassword};
@ -42,18 +42,42 @@ pub struct UserProfilePB {
#[pb(index = 7)]
pub auth_type: AuthTypePB,
#[pb(index = 8)]
pub encryption_sign: String,
#[pb(index = 9)]
pub encryption_type: EncryptionTypePB,
}
#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)]
pub enum EncryptionTypePB {
NoEncryption = 0,
Symmetric = 1,
}
impl Default for EncryptionTypePB {
fn default() -> Self {
Self::NoEncryption
}
}
impl std::convert::From<UserProfile> for UserProfilePB {
fn from(user_profile: UserProfile) -> Self {
let (encryption_sign, encryption_ty) = match user_profile.encryption_type {
EncryptionType::NoEncryption => ("".to_string(), EncryptionTypePB::NoEncryption),
EncryptionType::SelfEncryption(sign) => (sign, EncryptionTypePB::Symmetric),
};
Self {
id: user_profile.id,
id: user_profile.uid,
email: user_profile.email,
name: user_profile.name,
token: user_profile.token,
icon_url: user_profile.icon_url,
openai_key: user_profile.openai_key,
auth_type: user_profile.auth_type.into(),
encryption_sign,
encryption_type: encryption_ty,
}
}
}
@ -77,9 +101,6 @@ pub struct UpdateUserProfilePayloadPB {
#[pb(index = 6, one_of)]
pub openai_key: Option<String>,
#[pb(index = 7)]
pub auth_type: AuthTypePB,
}
impl UpdateUserProfilePayloadPB {
@ -146,13 +167,13 @@ impl TryInto<UpdateUserProfileParams> for UpdateUserProfilePayloadPB {
};
Ok(UpdateUserProfileParams {
id: self.id,
auth_type: self.auth_type.into(),
uid: self.id,
name,
email,
password,
icon_url,
openai_key,
encryption_sign: None,
})
}
}

View File

@ -1,11 +1,11 @@
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::SupabaseConfiguration;
use flowy_user_deps::cloud::UserCloudConfig;
use crate::entities::EncryptionTypePB;
#[derive(ProtoBuf, Default, Debug, Clone)]
pub struct UserPreferencesPB {
@ -104,40 +104,53 @@ impl std::default::Default for AppearanceSettingsPB {
}
#[derive(Default, ProtoBuf)]
pub struct SupabaseConfigPB {
pub struct UserCloudConfigPB {
#[pb(index = 1)]
supabase_url: String,
enable_sync: bool,
#[pb(index = 2)]
key: String,
enable_encrypt: bool,
#[pb(index = 3)]
jwt_secret: String,
pub encrypt_secret: String,
}
#[derive(Default, ProtoBuf)]
pub struct UpdateCloudConfigPB {
#[pb(index = 1, one_of)]
pub enable_sync: Option<bool>,
#[pb(index = 2, one_of)]
pub enable_encrypt: Option<bool>,
}
#[derive(Default, ProtoBuf)]
pub struct UserSecretPB {
#[pb(index = 1)]
pub user_id: i64,
#[pb(index = 2)]
pub encryption_secret: String,
#[pb(index = 3)]
pub encryption_type: EncryptionTypePB,
#[pb(index = 4)]
enable_sync: bool,
pub encryption_sign: String,
}
impl TryFrom<SupabaseConfigPB> for SupabaseConfiguration {
type Error = FlowyError;
fn try_from(config: SupabaseConfigPB) -> Result<Self, Self::Error> {
Ok(SupabaseConfiguration {
url: config.supabase_url,
anon_key: config.key,
jwt_secret: config.jwt_secret,
enable_sync: config.enable_sync,
})
}
#[derive(Default, ProtoBuf)]
pub struct UserEncryptionSecretCheckPB {
#[pb(index = 1)]
pub is_need_secret: bool,
}
impl From<SupabaseConfiguration> for SupabaseConfigPB {
fn from(value: SupabaseConfiguration) -> Self {
impl From<UserCloudConfig> for UserCloudConfigPB {
fn from(value: UserCloudConfig) -> Self {
Self {
supabase_url: value.url,
key: value.anon_key,
jwt_secret: value.jwt_secret,
enable_sync: value.enable_sync,
enable_encrypt: value.enable_encrypt,
encrypt_secret: value.encrypt_secret,
}
}
}

View File

@ -1,18 +1,18 @@
use std::convert::TryFrom;
use std::sync::Weak;
use std::{convert::TryInto, sync::Arc};
use serde_json::Value;
use flowy_error::{FlowyError, FlowyResult};
use flowy_server_config::supabase_config::SupabaseConfiguration;
use flowy_sqlite::kv::StorePreferences;
use flowy_user_deps::entities::*;
use lib_dispatch::prelude::*;
use lib_infra::box_any::BoxAny;
use crate::entities::*;
use crate::manager::{get_supabase_config, UserManager};
use crate::manager::UserManager;
use crate::notification::{send_notification, UserNotification};
use crate::services::cloud_config::{generate_cloud_config, get_cloud_config, save_cloud_config};
fn upgrade_manager(manager: AFPluginState<Weak<UserManager>>) -> FlowyResult<Arc<UserManager>> {
let manager = manager
@ -38,7 +38,6 @@ pub async fn sign_in(
let manager = upgrade_manager(manager)?;
let params: SignInParams = data.into_inner().try_into()?;
let auth_type = params.auth_type.clone();
manager.update_auth_type(&auth_type).await;
let user_profile: UserProfilePB = manager
.sign_in(BoxAny::new(params), auth_type)
@ -64,7 +63,6 @@ pub async fn sign_up(
let manager = upgrade_manager(manager)?;
let params: SignUpParams = data.into_inner().try_into()?;
let auth_type = params.auth_type.clone();
manager.update_auth_type(&auth_type).await;
let user_profile = manager.sign_up(auth_type, BoxAny::new(params)).await?;
data_result_ok(user_profile.into())
@ -175,28 +173,134 @@ pub async fn third_party_auth_handler(
let manager = upgrade_manager(manager)?;
let params = data.into_inner();
let auth_type: AuthType = params.auth_type.into();
manager.update_auth_type(&auth_type).await;
let user_profile = manager.sign_up(auth_type, BoxAny::new(params.map)).await?;
data_result_ok(user_profile.into())
}
#[tracing::instrument(level = "debug", skip(data, manager), err)]
pub async fn set_supabase_config_handler(
data: AFPluginData<SupabaseConfigPB>,
#[tracing::instrument(level = "debug", skip_all, err)]
pub async fn set_encrypt_secret_handler(
manager: AFPluginState<Weak<UserManager>>,
data: AFPluginData<UserSecretPB>,
store_preferences: AFPluginState<Weak<StorePreferences>>,
) -> Result<(), FlowyError> {
let manager = upgrade_manager(manager)?;
let config = SupabaseConfiguration::try_from(data.into_inner())?;
manager.save_supabase_config(config);
let store_preferences = upgrade_store_preferences(store_preferences)?;
let data = data.into_inner();
let mut config = get_cloud_config(&store_preferences).unwrap_or_else(|| {
tracing::trace!("Generate default cloud config");
generate_cloud_config(&store_preferences)
});
match data.encryption_type {
EncryptionTypePB::NoEncryption => {
tracing::error!("Encryption type is NoEncryption, but set encrypt secret");
},
EncryptionTypePB::Symmetric => {
manager.check_encryption_sign_with_secret(
data.user_id,
&data.encryption_sign,
&data.encryption_secret,
)?;
config.encrypt_secret = data.encryption_secret;
config.enable_encrypt = true;
manager
.set_encrypt_secret(
data.user_id,
config.encrypt_secret.clone(),
EncryptionType::SelfEncryption(data.encryption_sign),
)
.await?;
},
}
save_cloud_config(data.user_id, &store_preferences, config)?;
manager.resume_sign_up().await?;
Ok(())
}
#[tracing::instrument(level = "debug", skip_all, err)]
pub async fn get_supabase_config_handler(
pub async fn check_encrypt_secret_handler(
manager: AFPluginState<Weak<UserManager>>,
) -> DataResult<UserEncryptionSecretCheckPB, FlowyError> {
let manager = upgrade_manager(manager)?;
let uid = manager.get_session()?.user_id;
let profile = manager.get_user_profile(uid, false).await?;
let is_need_secret = match profile.encryption_type {
EncryptionType::NoEncryption => false,
EncryptionType::SelfEncryption(sign) => {
if sign.is_empty() {
false
} else {
manager.check_encryption_sign(uid, &sign).is_err()
}
},
};
data_result_ok(UserEncryptionSecretCheckPB { is_need_secret })
}
#[tracing::instrument(level = "debug", skip_all, err)]
pub async fn set_cloud_config_handler(
manager: AFPluginState<Weak<UserManager>>,
data: AFPluginData<UpdateCloudConfigPB>,
store_preferences: AFPluginState<Weak<StorePreferences>>,
) -> DataResult<SupabaseConfigPB, FlowyError> {
) -> Result<(), FlowyError> {
let manager = upgrade_manager(manager)?;
let session = manager.get_session()?;
let store_preferences = upgrade_store_preferences(store_preferences)?;
let config = get_supabase_config(&store_preferences).unwrap_or_default();
let update = data.into_inner();
let mut config = get_cloud_config(&store_preferences)
.ok_or(FlowyError::internal().context("Can't find any cloud config"))?;
if let Some(enable_sync) = update.enable_sync {
manager.cloud_services.set_enable_sync(enable_sync);
config.enable_sync = enable_sync;
}
if let Some(enable_encrypt) = update.enable_encrypt {
config.enable_encrypt = enable_encrypt;
if enable_encrypt {
// The encryption secret is generated when the user first enables encryption and will be
// used to validate the encryption secret is correct when the user logs in.
let encryption_sign =
manager.generate_encryption_sign(session.user_id, &config.encrypt_secret)?;
let encryption_type = EncryptionType::SelfEncryption(encryption_sign);
manager
.set_encrypt_secret(
session.user_id,
config.encrypt_secret.clone(),
encryption_type.clone(),
)
.await?;
let params =
UpdateUserProfileParams::new(session.user_id).with_encryption_type(encryption_type);
manager.update_user_profile(params).await?;
}
}
let config_pb = UserCloudConfigPB::from(config.clone());
save_cloud_config(session.user_id, &store_preferences, config)?;
send_notification(
&session.user_id.to_string(),
UserNotification::DidUpdateCloudConfig,
)
.payload(config_pb)
.send();
Ok(())
}
#[tracing::instrument(level = "debug", skip_all, err)]
pub async fn get_cloud_config_handler(
store_preferences: AFPluginState<Weak<StorePreferences>>,
) -> DataResult<UserCloudConfigPB, FlowyError> {
let store_preferences = upgrade_store_preferences(store_preferences)?;
// Generate the default config if the config is not exist
let config = get_cloud_config(&store_preferences)
.unwrap_or_else(|| generate_cloud_config(&store_preferences));
data_result_ok(config.into())
}
@ -279,7 +383,9 @@ pub async fn open_historical_users_handler(
let user = user.into_inner();
let manager = upgrade_manager(manager)?;
let auth_type = AuthType::from(user.auth_type);
manager.open_historical_user(user.user_id, user.device_id, auth_type)?;
manager
.open_historical_user(user.user_id, user.device_id, auth_type)
.await?;
Ok(())
}

View File

@ -6,7 +6,6 @@ use strum_macros::Display;
use flowy_derive::{Flowy_Event, ProtoBuf_Enum};
use flowy_error::FlowyResult;
use flowy_server_config::supabase_config::SupabaseConfiguration;
use flowy_user_deps::cloud::UserService;
use flowy_user_deps::entities::*;
use lib_dispatch::prelude::*;
@ -35,8 +34,10 @@ pub fn init(user_session: Weak<UserManager>) -> 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::SetCloudConfig, set_cloud_config_handler)
.event(UserEvent::GetCloudConfig, get_cloud_config_handler)
.event(UserEvent::SetEncryptionSecret, set_encrypt_secret_handler)
.event(UserEvent::CheckEncryptionSign, check_encrypt_secret_handler)
.event(UserEvent::ThirdPartyAuth, third_party_auth_handler)
.event(
UserEvent::GetAllUserWorkspaces,
@ -101,7 +102,8 @@ pub trait UserStatusCallback: Send + Sync + 'static {
/// The user cloud service provider.
/// The provider can be supabase, firebase, aws, or any other cloud service.
pub trait UserCloudServiceProvider: Send + Sync + 'static {
fn set_supabase_config(&self, supabase_config: &SupabaseConfiguration);
fn set_enable_sync(&self, enable_sync: bool);
fn set_encrypt_secret(&self, secret: String);
fn set_auth_type(&self, auth_type: AuthType);
fn set_device_id(&self, device_id: &str);
fn get_user_service(&self) -> Result<Arc<dyn UserService>, FlowyError>;
@ -112,8 +114,12 @@ impl<T> UserCloudServiceProvider for Arc<T>
where
T: UserCloudServiceProvider,
{
fn set_supabase_config(&self, supabase_config: &SupabaseConfiguration) {
(**self).set_supabase_config(supabase_config)
fn set_enable_sync(&self, enable_sync: bool) {
(**self).set_enable_sync(enable_sync)
}
fn set_encrypt_secret(&self, secret: String) {
(**self).set_encrypt_secret(secret)
}
fn set_auth_type(&self, auth_type: AuthType) {
@ -221,13 +227,17 @@ 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(input = "UpdateCloudConfigPB")]
SetCloudConfig = 13,
#[event(output = "SupabaseConfigPB")]
GetSupabaseConfig = 14,
#[event(output = "UserCloudConfigPB")]
GetCloudConfig = 14,
#[event(input = "UserSecretPB")]
SetEncryptionSecret = 15,
#[event(output = "UserEncryptionSecretCheckPB")]
CheckEncryptionSign = 16,
/// Return the all the workspaces of the user
#[event()]

View File

@ -9,8 +9,7 @@ use serde_json::Value;
use tokio::sync::{Mutex, RwLock};
use uuid::Uuid;
use flowy_error::{internal_error, ErrorCode};
use flowy_server_config::supabase_config::SupabaseConfiguration;
use flowy_error::{internal_error, ErrorCode, FlowyResult};
use flowy_sqlite::kv::StorePreferences;
use flowy_sqlite::schema::user_table;
use flowy_sqlite::ConnectionPool;
@ -25,16 +24,15 @@ use crate::event_map::{
use crate::migrations::historical_document::HistoricalEmptyDocumentMigration;
use crate::migrations::local_user_to_cloud::migration_user_to_cloud;
use crate::migrations::migration::UserLocalDataMigration;
use crate::migrations::UserMigrationContext;
use crate::migrations::MigrationUser;
use crate::services::cloud_config::remove_cloud_config;
use crate::services::database::UserDB;
use crate::services::entities::Session;
use crate::services::entities::{ResumableSignUp, Session};
use crate::services::user_awareness::UserAwarenessDataSource;
use crate::services::user_sql::{UserTable, UserTableChangeset};
use crate::services::user_workspace::save_user_workspaces;
use crate::{errors::FlowyError, notification::*};
const SUPABASE_CONFIG_CACHE_KEY: &str = "af_supabase_config";
pub struct UserSessionConfig {
root_dir: String,
@ -62,6 +60,7 @@ pub struct UserManager {
pub(crate) user_awareness: Arc<Mutex<Option<MutexUserAwareness>>>,
pub(crate) user_status_callback: RwLock<Arc<dyn UserStatusCallback>>,
pub(crate) collab_builder: Weak<AppFlowyCollabBuilder>,
resumable_sign_up: Mutex<Option<ResumableSignUp>>,
}
impl UserManager {
@ -82,6 +81,7 @@ impl UserManager {
user_awareness: Arc::new(Default::default()),
user_status_callback,
collab_builder,
resumable_sign_up: Default::default(),
}
}
@ -160,29 +160,18 @@ impl UserManager {
params: BoxAny,
auth_type: AuthType,
) -> Result<UserProfile, FlowyError> {
self.update_auth_type(&auth_type).await;
let response: SignInResponse = self
.cloud_services
.get_user_service()?
.sign_in(params)
.await?;
let session: Session = response.clone().into();
let uid = session.user_id;
let device_id = session.device_id.clone();
let session = Session::from(&response);
self.set_collab_config(&session);
self.set_current_session(Some(session.clone()))?;
self.log_historical_user(
uid,
&response.device_id,
response.name.clone(),
&auth_type,
self.user_dir(uid),
);
let user_workspace = response.latest_workspace.clone();
save_user_workspaces(uid, self.db_pool(uid)?, &response.user_workspaces)?;
let user_profile: UserProfile = self
.save_user(uid, (response, auth_type).into())
.await?
.into();
let latest_workspace = response.latest_workspace.clone();
let user_profile = UserProfile::from((&response, &auth_type));
self.save_auth_data(&response, &auth_type, &session).await?;
let _ = self
.initialize_user_awareness(&session, UserAwarenessDataSource::Remote)
.await;
@ -191,25 +180,23 @@ impl UserManager {
.user_status_callback
.read()
.await
.did_sign_in(user_profile.id, &user_workspace, &device_id)
.did_sign_in(user_profile.uid, &latest_workspace, &session.device_id)
.await
{
tracing::error!("Failed to call did_sign_in callback: {:?}", e);
}
send_sign_in_notification()
.payload::<UserProfilePB>(user_profile.clone().into())
.send();
Ok(user_profile)
}
pub async fn update_auth_type(&self, auth_type: &AuthType) {
pub(crate) async fn update_auth_type(&self, auth_type: &AuthType) {
self
.user_status_callback
.read()
.await
.auth_type_did_changed(auth_type.clone());
self.cloud_services.set_auth_type(auth_type.clone());
}
@ -220,94 +207,117 @@ impl UserManager {
/// and saving workspace information. If a user is signing up with a new profile and previously had guest data,
/// this function may migrate that data over to the new account.
///
#[tracing::instrument(level = "debug", skip(self, params))]
#[tracing::instrument(level = "info", skip(self, params))]
pub async fn sign_up(
&self,
auth_type: AuthType,
params: BoxAny,
) -> Result<UserProfile, FlowyError> {
let old_user = {
if let Ok(old_session) = self.get_session() {
self
.get_user_profile(old_session.user_id, false)
.await
.ok()
.map(|user_profile| UserMigrationContext {
user_profile,
session: old_session,
})
} else {
None
}
};
remove_cloud_config(&self.store_preferences);
self.update_auth_type(&auth_type).await;
let migration_user = self.get_migration_user(&auth_type).await;
let auth_service = self.cloud_services.get_user_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 user_profile = UserProfile::from((&response, &auth_type));
if user_profile.encryption_type.is_need_encrypt_secret() {
self
.resumable_sign_up
.lock()
.await
.replace(ResumableSignUp {
user_profile: user_profile.clone(),
migration_user,
response,
auth_type,
});
} else {
self
.continue_sign_up(&user_profile, migration_user, response, &auth_type)
.await?;
}
Ok(user_profile)
}
#[tracing::instrument(level = "info", skip(self))]
pub async fn resume_sign_up(&self) -> Result<(), FlowyError> {
let ResumableSignUp {
user_profile,
migration_user,
response,
auth_type,
} = self
.resumable_sign_up
.lock()
.await
.clone()
.ok_or(FlowyError::new(
ErrorCode::Internal,
"No resumable sign up data",
))?;
self
.continue_sign_up(&user_profile, migration_user, response, &auth_type)
.await?;
Ok(())
}
#[tracing::instrument(level = "info", skip_all, err)]
async fn continue_sign_up(
&self,
user_profile: &UserProfile,
migration_user: Option<MigrationUser>,
response: SignUpResponse,
auth_type: &AuthType,
) -> FlowyResult<()> {
let new_session = Session::from(&response);
self.set_current_session(Some(new_session.clone()))?;
self.set_collab_config(&new_session);
let uid = response.user_id;
self.log_historical_user(
uid,
&response.device_id,
response.name.clone(),
&auth_type,
self.user_dir(uid),
);
save_user_workspaces(uid, self.db_pool(uid)?, &response.user_workspaces)?;
let new_user_profile: UserProfile = self
.save_user(uid, (response, auth_type.clone()).into())
.await?
.into();
let user_awareness_source = if sign_up_context.is_new {
let user_awareness_source = if response.is_new_user {
UserAwarenessDataSource::Local
} else {
UserAwarenessDataSource::Remote
};
// Only migrate the data if the user is login in as a guest and sign up as a new user if the current
// auth type is not [AuthType::Local].
if sign_up_context.is_new {
if let Some(old_user) = old_user {
if old_user.user_profile.auth_type == AuthType::Local && !auth_type.is_local() {
let new_user = UserMigrationContext {
user_profile: new_user_profile.clone(),
session: new_session.clone(),
};
tracing::info!(
"Migrate old user data from {:?} to {:?}",
old_user.user_profile.id,
new_user.user_profile.id
);
match self.migrate_local_user_to_cloud(&old_user, &new_user).await {
Ok(folder_data) => sign_up_context.local_folder = folder_data,
Err(e) => tracing::error!("{:?}", e),
}
// close the old user db
let _ = self.database.close(old_user.session.user_id);
let mut sign_up_context = SignUpContext {
is_new: response.is_new_user,
local_folder: None,
};
if response.is_new_user {
if let Some(old_user) = migration_user {
let new_user = MigrationUser {
user_profile: user_profile.clone(),
session: new_session.clone(),
};
tracing::info!(
"Migrate old user data from {:?} to {:?}",
old_user.user_profile.uid,
new_user.user_profile.uid
);
match self.migrate_local_user_to_cloud(&old_user, &new_user).await {
Ok(folder_data) => sign_up_context.local_folder = folder_data,
Err(e) => tracing::error!("{:?}", e),
}
let _ = self.database.close(old_user.session.user_id);
}
}
self
.initialize_user_awareness(&new_session, user_awareness_source)
.await;
let _ = self
self
.user_status_callback
.read()
.await
.did_sign_up(
sign_up_context,
&new_user_profile,
user_profile,
&new_session.user_workspace,
&new_session.device_id,
)
.await;
Ok(new_user_profile)
.await?;
self
.save_auth_data(&response, auth_type, &new_session)
.await?;
Ok(())
}
#[tracing::instrument(level = "info", skip(self))]
@ -315,6 +325,7 @@ impl UserManager {
let session = self.get_session()?;
self.database.close(session.user_id)?;
self.set_current_session(None)?;
remove_cloud_config(&self.store_preferences);
let server = self.cloud_services.get_user_service()?;
tokio::spawn(async move {
@ -337,7 +348,8 @@ impl UserManager {
&self,
params: UpdateUserProfileParams,
) -> Result<(), FlowyError> {
let auth_type = params.auth_type.clone();
let old_user_profile = self.get_user_profile(params.uid, false).await?;
let auth_type = old_user_profile.auth_type.clone();
let session = self.get_session()?;
let changeset = UserTableChangeset::new(params.clone());
diesel_update_table!(
@ -347,13 +359,12 @@ impl UserManager {
);
let session = self.get_session()?;
let user_profile = self.get_user_profile(session.user_id, false).await?;
let profile_pb: UserProfilePB = user_profile.into();
let new_user_profile = self.get_user_profile(session.user_id, false).await?;
send_notification(
&session.user_id.to_string(),
UserNotification::DidUpdateUserProfile,
)
.payload(profile_pb)
.payload(UserProfilePB::from(new_user_profile))
.send();
self
.update_user(&auth_type, session.user_id, None, params)
@ -441,13 +452,6 @@ impl UserManager {
Ok(None)
}
pub fn save_supabase_config(&self, config: SupabaseConfiguration) {
self.cloud_services.set_supabase_config(&config);
let _ = self
.store_preferences
.set_object(SUPABASE_CONFIG_CACHE_KEY, config);
}
async fn update_user(
&self,
_auth_type: &AuthType,
@ -466,7 +470,7 @@ impl UserManager {
Ok(())
}
async fn save_user(&self, uid: i64, user: UserTable) -> Result<UserTable, FlowyError> {
async fn save_user(&self, uid: i64, user: UserTable) -> Result<(), FlowyError> {
let conn = self.db_connection(uid)?;
conn.immediate_transaction(|| {
// delete old user if exists
@ -474,12 +478,12 @@ impl UserManager {
.execute(&*conn)?;
let _ = diesel::insert_into(user_table::table)
.values(user.clone())
.values(user)
.execute(&*conn)?;
Ok::<(), FlowyError>(())
})?;
Ok(user)
Ok(())
}
pub(crate) fn set_current_session(&self, session: Option<Session>) -> Result<(), FlowyError> {
@ -520,6 +524,29 @@ impl UserManager {
}
}
async fn save_auth_data(
&self,
response: &impl UserAuthResponse,
auth_type: &AuthType,
session: &Session,
) -> Result<(), FlowyError> {
let user_profile = UserProfile::from((response, auth_type));
let uid = user_profile.uid;
self.add_historical_user(
uid,
response.device_id(),
response.user_name().to_string(),
auth_type,
self.user_dir(uid),
);
save_user_workspaces(uid, self.db_pool(uid)?, response.user_workspaces())?;
self
.save_user(uid, (user_profile, auth_type.clone()).into())
.await?;
self.set_current_session(Some(session.clone()))?;
Ok(())
}
fn set_collab_config(&self, session: &Session) {
let collab_builder = self.collab_builder.upgrade().unwrap();
collab_builder.set_sync_device(session.device_id.clone());
@ -529,21 +556,18 @@ impl UserManager {
async fn migrate_local_user_to_cloud(
&self,
old_user: &UserMigrationContext,
new_user: &UserMigrationContext,
old_user: &MigrationUser,
new_user: &MigrationUser,
) -> Result<Option<FolderData>, FlowyError> {
let old_collab_db = self.database.get_collab_db(old_user.session.user_id)?;
let new_collab_db = self.database.get_collab_db(new_user.session.user_id)?;
let folder_data = migration_user_to_cloud(old_user, &old_collab_db, new_user, &new_collab_db)?;
// Save the old user workspace setting.
save_user_workspaces(
old_user.session.user_id,
self.database.get_pool(old_user.session.user_id)?,
&[old_user.session.user_workspace.clone()],
)?;
Ok(folder_data)
}
}
pub fn get_supabase_config(
store_preference: &Arc<StorePreferences>,
) -> Option<SupabaseConfiguration> {
store_preference
.get_str(SUPABASE_CONFIG_CACHE_KEY)
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_else(|| SupabaseConfiguration::from_env().ok())
}

View File

@ -2,7 +2,8 @@ use flowy_user_deps::entities::UserProfile;
use crate::services::entities::Session;
pub struct UserMigrationContext {
#[derive(Clone)]
pub struct MigrationUser {
pub user_profile: UserProfile,
pub session: Session,
}

View File

@ -8,14 +8,14 @@ use collab_folder::core::{Folder, FolderData};
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use crate::migrations::UserMigrationContext;
use crate::migrations::MigrationUser;
/// Migration the collab objects of the old user to new user. Currently, it only happens when
/// the user is a local user and try to use AppFlowy cloud service.
pub fn migration_user_to_cloud(
old_user: &UserMigrationContext,
old_user: &MigrationUser,
old_collab_db: &Arc<RocksCollabDB>,
new_user: &UserMigrationContext,
new_user: &MigrationUser,
new_collab_db: &Arc<RocksCollabDB>,
) -> FlowyResult<Option<FolderData>> {
let mut folder_data = None;

View File

@ -10,6 +10,7 @@ pub(crate) enum UserNotification {
DidUserSignIn = 1,
DidUpdateUserProfile = 2,
DidUpdateUserWorkspaces = 3,
DidUpdateCloudConfig = 4,
}
impl std::convert::From<UserNotification> for i32 {

View File

@ -0,0 +1,48 @@
use std::sync::Arc;
use flowy_encrypt::generate_encrypt_secret;
use flowy_error::FlowyResult;
use flowy_sqlite::kv::StorePreferences;
use flowy_user_deps::cloud::UserCloudConfig;
const CLOUD_CONFIG_KEY: &str = "af_user_cloud_config";
pub fn generate_cloud_config(store_preference: &Arc<StorePreferences>) -> UserCloudConfig {
let config = UserCloudConfig::new(generate_encrypt_secret());
let key = cache_key_for_cloud_config();
store_preference.set_object(&key, config.clone()).unwrap();
config
}
pub fn remove_cloud_config(store_preference: &Arc<StorePreferences>) {
let key = cache_key_for_cloud_config();
store_preference.remove(&key);
}
pub fn save_cloud_config(
uid: i64,
store_preference: &Arc<StorePreferences>,
config: UserCloudConfig,
) -> FlowyResult<()> {
let encrypt_secret = config.encrypt_secret.clone();
let key = cache_key_for_cloud_config();
store_preference.set_object(&key, config)?;
store_preference.set_object(&format!("{}-encrypt-secret", uid), encrypt_secret)?;
Ok(())
}
fn cache_key_for_cloud_config() -> String {
CLOUD_CONFIG_KEY.to_string()
}
pub fn get_cloud_config(store_preference: &Arc<StorePreferences>) -> Option<UserCloudConfig> {
let key = cache_key_for_cloud_config();
store_preference.get_object::<UserCloudConfig>(&key)
}
pub fn get_encrypt_secret(store_preference: &Arc<StorePreferences>) -> Option<String> {
let key = cache_key_for_cloud_config();
store_preference
.get_object::<UserCloudConfig>(&key)
.map(|config| config.encrypt_secret)
}

View File

@ -7,10 +7,11 @@ use serde::de::{Deserializer, MapAccess, Visitor};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use flowy_user_deps::entities::AuthType;
use flowy_user_deps::entities::{SignInResponse, SignUpResponse, UserWorkspace};
use flowy_user_deps::entities::{AuthType, UserAuthResponse};
use flowy_user_deps::entities::{SignUpResponse, UserProfile, UserWorkspace};
use crate::entities::AuthTypePB;
use crate::migrations::MigrationUser;
#[derive(Debug, Clone, Serialize)]
pub struct Session {
@ -89,12 +90,15 @@ impl<'de> Deserialize<'de> for Session {
}
}
impl std::convert::From<SignInResponse> for Session {
fn from(resp: SignInResponse) -> Self {
Session {
user_id: resp.user_id,
device_id: resp.device_id,
user_workspace: resp.latest_workspace,
impl<T> From<&T> for Session
where
T: UserAuthResponse,
{
fn from(value: &T) -> Self {
Self {
user_id: value.user_id(),
device_id: value.device_id().to_string(),
user_workspace: value.latest_workspace().clone(),
}
}
}
@ -111,16 +115,6 @@ impl std::convert::From<Session> for String {
}
}
impl From<&SignUpResponse> for Session {
fn from(value: &SignUpResponse) -> Self {
Session {
user_id: value.user_id,
device_id: value.device_id.clone(),
user_workspace: value.latest_workspace.clone(),
}
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
@ -208,3 +202,11 @@ pub struct HistoricalUser {
pub device_id: String,
}
const DEFAULT_AUTH_TYPE: fn() -> AuthType = || AuthType::Local;
#[derive(Clone)]
pub(crate) struct ResumableSignUp {
pub user_profile: UserProfile,
pub response: SignUpResponse,
pub auth_type: AuthType,
pub migration_user: Option<MigrationUser>,
}

View File

@ -7,11 +7,26 @@ use flowy_user_deps::entities::{AuthType, UserWorkspace};
use lib_infra::util::timestamp;
use crate::manager::UserManager;
use crate::migrations::MigrationUser;
use crate::services::entities::{HistoricalUser, HistoricalUsers, Session};
use crate::services::user_workspace_sql::UserWorkspaceTable;
const HISTORICAL_USER: &str = "af_historical_users";
impl UserManager {
pub async fn get_migration_user(&self, auth_type: &AuthType) -> Option<MigrationUser> {
// Only migrate the data if the user is login in as a guest and sign up as a new user if the current
// auth type is not [AuthType::Local].
let session = self.get_session().ok()?;
let user_profile = self.get_user_profile(session.user_id, false).await.ok()?;
if user_profile.auth_type == AuthType::Local && !auth_type.is_local() {
Some(MigrationUser {
user_profile,
session,
})
} else {
None
}
}
/// Logs a user's details for historical tracking.
///
/// This function adds a user's details to a local historical tracking system, useful for
@ -24,7 +39,7 @@ impl UserManager {
/// - `auth_type`: The type of authentication used.
/// - `storage_path`: Path where user data is stored.
///
pub fn log_historical_user(
pub fn add_historical_user(
&self,
uid: i64,
device_id: &str,
@ -67,12 +82,14 @@ 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 fn open_historical_user(
pub async fn open_historical_user(
&self,
uid: i64,
device_id: String,
auth_type: AuthType,
) -> FlowyResult<()> {
debug_assert!(auth_type.is_local());
self.update_auth_type(&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))
@ -83,8 +100,6 @@ impl UserManager {
device_id,
user_workspace,
};
debug_assert!(auth_type.is_local());
self.cloud_services.set_auth_type(auth_type);
self.set_current_session(Some(session))?;
Ok(())
}

View File

@ -1,7 +1,9 @@
pub mod cloud_config;
pub mod database;
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;

View File

@ -0,0 +1,62 @@
use flowy_encrypt::{decrypt_string, encrypt_string};
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use flowy_user_deps::entities::{EncryptionType, UpdateUserProfileParams, UserCredentials};
use crate::manager::UserManager;
use crate::services::cloud_config::get_encrypt_secret;
impl UserManager {
pub async fn set_encrypt_secret(
&self,
uid: i64,
secret: String,
encryption_type: EncryptionType,
) -> FlowyResult<()> {
let params = UpdateUserProfileParams::new(uid).with_encryption_type(encryption_type);
self
.cloud_services
.get_user_service()?
.update_user(UserCredentials::from_uid(uid), params.clone())
.await?;
self.cloud_services.set_encrypt_secret(secret);
Ok(())
}
pub fn generate_encryption_sign(&self, uid: i64, encrypt_secret: &str) -> FlowyResult<String> {
let encrypt_sign = encrypt_string(uid.to_string(), encrypt_secret)?;
Ok(encrypt_sign)
}
pub fn check_encryption_sign(&self, uid: i64, encrypt_sign: &str) -> FlowyResult<()> {
let store_preference = self
.get_store_preferences()
.upgrade()
.ok_or(FlowyError::new(
ErrorCode::Internal,
"Failed to get store preference",
))?;
let encrypt_secret = get_encrypt_secret(&store_preference).ok_or(FlowyError::new(
ErrorCode::Internal,
"Encrypt secret is not set",
))?;
self.check_encryption_sign_with_secret(uid, encrypt_sign, &encrypt_secret)
}
pub fn check_encryption_sign_with_secret(
&self,
uid: i64,
encrypt_sign: &str,
encryption_secret: &str,
) -> FlowyResult<()> {
let decrypt_str = decrypt_string(encrypt_sign, encryption_secret)
.map_err(|_| FlowyError::new(ErrorCode::InvalidEncryptSecret, "Invalid decryption secret"))?;
if uid.to_string() == decrypt_str {
Ok(())
} else {
Err(ErrorCode::InvalidEncryptSecret.into())
}
}
}

View File

@ -1,3 +1,5 @@
use std::str::FromStr;
use flowy_sqlite::schema::user_table;
use flowy_user_deps::entities::*;
@ -14,6 +16,7 @@ pub struct UserTable {
pub(crate) token: String,
pub(crate) email: String,
pub(crate) auth_type: i32,
pub(crate) encryption_type: String,
}
impl UserTable {
@ -23,35 +26,20 @@ impl UserTable {
}
}
impl From<(SignUpResponse, AuthType)> for UserTable {
fn from(params: (SignUpResponse, AuthType)) -> Self {
let resp = params.0;
impl From<(UserProfile, AuthType)> for UserTable {
fn from(value: (UserProfile, AuthType)) -> Self {
let (user_profile, auth_type) = value;
let encryption_type = serde_json::to_string(&user_profile.encryption_type).unwrap_or_default();
UserTable {
id: resp.user_id.to_string(),
name: resp.name,
token: resp.token.unwrap_or_default(),
email: resp.email.unwrap_or_default(),
workspace: resp.latest_workspace.id,
icon_url: "".to_string(),
openai_key: "".to_string(),
auth_type: params.1 as i32,
}
}
}
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,
token: resp.token.unwrap_or_default(),
email: resp.email.unwrap_or_default(),
workspace: resp.latest_workspace.id,
icon_url: "".to_string(),
openai_key: "".to_string(),
id: user_profile.uid.to_string(),
name: user_profile.name,
workspace: user_profile.workspace_id,
icon_url: user_profile.icon_url,
openai_key: user_profile.openai_key,
token: user_profile.token,
email: user_profile.email,
auth_type: auth_type as i32,
encryption_type,
}
}
}
@ -59,7 +47,7 @@ impl From<(SignInResponse, AuthType)> for UserTable {
impl From<UserTable> for UserProfile {
fn from(table: UserTable) -> Self {
UserProfile {
id: table.id.parse::<i64>().unwrap_or(0),
uid: table.id.parse::<i64>().unwrap_or(0),
email: table.email,
name: table.name,
token: table.token,
@ -67,6 +55,7 @@ impl From<UserTable> for UserProfile {
openai_key: table.openai_key,
workspace_id: table.workspace,
auth_type: AuthType::from(table.auth_type),
encryption_type: EncryptionType::from_str(&table.encryption_type).unwrap_or_default(),
}
}
}
@ -80,28 +69,36 @@ pub struct UserTableChangeset {
pub email: Option<String>,
pub icon_url: Option<String>,
pub openai_key: Option<String>,
pub encryption_type: Option<String>,
}
impl UserTableChangeset {
pub fn new(params: UpdateUserProfileParams) -> Self {
let encryption_type = params.encryption_sign.map(|sign| {
let ty = EncryptionType::from_sign(&sign);
serde_json::to_string(&ty).unwrap_or_default()
});
UserTableChangeset {
id: params.id.to_string(),
id: params.uid.to_string(),
workspace: None,
name: params.name,
email: params.email,
icon_url: params.icon_url,
openai_key: params.openai_key,
encryption_type,
}
}
pub fn from_user_profile(user_profile: UserProfile) -> Self {
let encryption_type = serde_json::to_string(&user_profile.encryption_type).unwrap_or_default();
UserTableChangeset {
id: user_profile.id.to_string(),
id: user_profile.uid.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),
encryption_type: Some(encryption_type),
}
}
}