feat: realtime user event (#3241)

* feat: update user profile after receiving realtime user event

* chore: logout if other deivce enable encyrption

* test: fix test

* chore: fix checkbox UI

* chore: fix tauri build

* chore: fix device id

* chore: fix duplicate run appflowy
This commit is contained in:
Nathan.fooo
2023-08-20 14:13:54 +08:00
committed by GitHub
parent c5719be7ae
commit a1647bee78
39 changed files with 814 additions and 865 deletions

View File

@ -154,3 +154,24 @@ pub struct UserStatePB {
#[pb(index = 1)]
pub auth_type: AuthTypePB,
}
#[derive(ProtoBuf, Debug, Default, Clone)]
pub struct AuthStateChangedPB {
#[pb(index = 1)]
pub state: AuthStatePB,
}
#[derive(ProtoBuf_Enum, Debug, Clone)]
pub enum AuthStatePB {
// adding AuthState prefix to avoid conflict with other enums
AuthStateUnknown = 0,
AuthStateSignIn = 1,
AuthStateSignOut = 2,
AuthStateForceSignOut = 3,
}
impl Default for AuthStatePB {
fn default() -> Self {
Self::AuthStateUnknown
}
}

View File

@ -95,8 +95,9 @@ pub async fn get_user_profile_handler(
) -> DataResult<UserProfilePB, FlowyError> {
let manager = upgrade_manager(manager)?;
let uid = manager.get_session()?.user_id;
let user_profile: UserProfilePB = manager.get_user_profile(uid, true).await?.into();
data_result_ok(user_profile)
let user_profile = manager.get_user_profile(uid).await?;
let _ = manager.refresh_user_profile(&user_profile).await;
data_result_ok(user_profile.into())
}
#[tracing::instrument(level = "debug", skip(manager))]
@ -222,7 +223,7 @@ pub async fn check_encrypt_secret_handler(
) -> 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 profile = manager.get_user_profile(uid).await?;
let is_need_secret = match profile.encryption_type {
EncryptionType::NoEncryption => false,

View File

@ -1,7 +1,6 @@
use std::sync::{Arc, Weak};
use collab_folder::core::FolderData;
use serde_json::Value;
use strum_macros::Display;
use flowy_derive::{Flowy_Event, ProtoBuf_Enum};
@ -97,7 +96,6 @@ pub trait UserStatusCallback: Send + Sync + 'static {
fn did_expired(&self, token: &str, user_id: i64) -> Fut<FlowyResult<()>>;
fn open_workspace(&self, user_id: i64, user_workspace: &UserWorkspace) -> Fut<FlowyResult<()>>;
fn did_update_network(&self, _reachable: bool) {}
fn receive_realtime_event(&self, _json: Value) {}
}
/// The user cloud service provider.

View File

@ -14,10 +14,11 @@ use flowy_sqlite::kv::StorePreferences;
use flowy_sqlite::schema::user_table;
use flowy_sqlite::ConnectionPool;
use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods};
use flowy_user_deps::cloud::UserUpdate;
use flowy_user_deps::entities::*;
use lib_infra::box_any::BoxAny;
use crate::entities::{UserProfilePB, UserSettingPB};
use crate::entities::{AuthStateChangedPB, AuthStatePB, UserProfilePB, UserSettingPB};
use crate::event_map::{
DefaultUserStatusCallback, SignUpContext, UserCloudServiceProvider, UserStatusCallback,
};
@ -61,6 +62,7 @@ pub struct UserManager {
pub(crate) user_status_callback: RwLock<Arc<dyn UserStatusCallback>>,
pub(crate) collab_builder: Weak<AppFlowyCollabBuilder>,
resumable_sign_up: Mutex<Option<ResumableSignUp>>,
current_session: parking_lot::RwLock<Option<Session>>,
}
impl UserManager {
@ -69,11 +71,12 @@ impl UserManager {
cloud_services: Arc<dyn UserCloudServiceProvider>,
store_preferences: Arc<StorePreferences>,
collab_builder: Weak<AppFlowyCollabBuilder>,
) -> Self {
) -> Arc<Self> {
let database = UserDB::new(&session_config.root_dir);
let user_status_callback: RwLock<Arc<dyn UserStatusCallback>> =
RwLock::new(Arc::new(DefaultUserStatusCallback));
Self {
let user_manager = Arc::new(Self {
database,
session_config,
cloud_services,
@ -82,7 +85,25 @@ impl UserManager {
user_status_callback,
collab_builder,
resumable_sign_up: Default::default(),
current_session: Default::default(),
});
let weak_user_manager = Arc::downgrade(&user_manager);
if let Ok(user_service) = user_manager.cloud_services.get_user_service() {
if let Some(mut rx) = user_service.subscribe_user_update() {
tokio::spawn(async move {
while let Ok(update) = rx.recv().await {
if let Some(user_manager) = weak_user_manager.upgrade() {
if let Err(err) = user_manager.handler_user_update(update).await {
tracing::error!("handler_user_update failed: {:?}", err);
}
}
}
});
}
}
user_manager
}
pub fn get_store_preferences(&self) -> Weak<StorePreferences> {
@ -121,6 +142,7 @@ impl UserManager {
self
.initialize_user_awareness(&session, UserAwarenessDataSource::Local)
.await;
let cloud_config = get_cloud_config(session.user_id, &self.store_preferences);
if let Err(e) = user_status_callback
.did_init(
@ -191,9 +213,10 @@ impl UserManager {
{
tracing::error!("Failed to call did_sign_in callback: {:?}", e);
}
send_sign_in_notification()
.payload::<UserProfilePB>(user_profile.clone().into())
.send();
send_auth_state_notification(AuthStateChangedPB {
state: AuthStatePB::AuthStateSignIn,
})
.send();
Ok(user_profile)
}
@ -322,6 +345,11 @@ impl UserManager {
self
.save_auth_data(&response, auth_type, &new_session)
.await?;
send_auth_state_notification(AuthStateChangedPB {
state: AuthStatePB::AuthStateSignIn,
})
.send();
Ok(())
}
@ -329,7 +357,7 @@ impl UserManager {
pub async fn sign_out(&self) -> Result<(), FlowyError> {
let session = self.get_session()?;
self.database.close(session.user_id)?;
self.set_current_session(None)?;
self.set_session(None)?;
let server = self.cloud_services.get_user_service()?;
tokio::spawn(async move {
@ -352,27 +380,10 @@ impl UserManager {
&self,
params: UpdateUserProfileParams,
) -> Result<(), FlowyError> {
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!(
user_table,
changeset,
&*self.db_connection(session.user_id)?
);
let session = self.get_session()?;
let new_user_profile = self.get_user_profile(session.user_id, false).await?;
send_notification(
&session.user_id.to_string(),
UserNotification::DidUpdateUserProfile,
)
.payload(UserProfilePB::from(new_user_profile))
.send();
self
.update_user(&auth_type, session.user_id, None, params)
.await?;
save_user_profile_change(session.user_id, self.db_pool(session.user_id)?, changeset)?;
self.update_user(session.user_id, None, params).await?;
Ok(())
}
@ -396,44 +407,38 @@ impl UserManager {
}
/// Fetches the user profile for the given user ID.
///
/// This function retrieves the user profile from the local database. If the `refresh` flag is set to `true`,
/// it also attempts to update the user profile from a cloud service, and then sends a notification about the
/// profile update.
pub async fn get_user_profile(&self, uid: i64, refresh: bool) -> Result<UserProfile, FlowyError> {
let user_id = uid.to_string();
let user = user_table::dsl::user_table
.filter(user_table::id.eq(&user_id))
.first::<UserTable>(&*(self.db_connection(uid)?))?;
pub async fn get_user_profile(&self, uid: i64) -> Result<UserProfile, FlowyError> {
let user: UserProfile = user_table::dsl::user_table
.filter(user_table::id.eq(&uid.to_string()))
.first::<UserTable>(&*(self.db_connection(uid)?))?
.into();
if refresh {
let weak_auth_service = Arc::downgrade(&self.cloud_services.get_user_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 =
user_table::dsl::user_table.filter(user_table::dsl::id.eq(changeset.id.clone()));
let _ = diesel::update(filter).set(changeset).execute(&*conn);
Ok(user)
}
// 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();
}
}
}
});
#[tracing::instrument(level = "info", skip_all)]
pub async fn refresh_user_profile(
&self,
old_user_profile: &UserProfile,
) -> FlowyResult<UserProfile> {
let uid = old_user_profile.uid;
let new_user_profile: UserProfile = self
.cloud_services
.get_user_service()?
.get_user_profile(UserCredentials::from_uid(uid))
.await?
.ok_or_else(|| FlowyError::new(ErrorCode::RecordNotFound, "User not found"))?;
if !is_user_encryption_sign_valid(old_user_profile, &new_user_profile.encryption_type.sign()) {
return Err(FlowyError::new(
ErrorCode::InvalidEncryptSecret,
"Invalid encryption sign",
));
}
Ok(user.into())
let changeset = UserTableChangeset::from_user_profile(new_user_profile.clone());
let _ = save_user_profile_change(uid, self.database.get_pool(uid)?, changeset);
Ok(new_user_profile)
}
pub fn user_dir(&self, uid: i64) -> String {
@ -458,7 +463,6 @@ impl UserManager {
async fn update_user(
&self,
_auth_type: &AuthType,
uid: i64,
token: Option<String>,
params: UpdateUserProfileParams,
@ -490,32 +494,18 @@ impl UserManager {
Ok(())
}
pub(crate) fn set_current_session(&self, session: Option<Session>) -> Result<(), FlowyError> {
tracing::debug!("Set current user: {:?}", session);
match &session {
None => self
.store_preferences
.remove(&self.session_config.session_cache_key),
Some(session) => {
self
.store_preferences
.set_object(&self.session_config.session_cache_key, session.clone())
.map_err(internal_error)?;
},
}
Ok(())
}
pub async fn receive_realtime_event(&self, json: Value) {
self
.user_status_callback
.read()
.await
.receive_realtime_event(json);
if let Ok(user_service) = self.cloud_services.get_user_service() {
user_service.receive_realtime_event(json)
}
}
/// Returns the current user session.
pub fn get_session(&self) -> Result<Session, FlowyError> {
if let Some(session) = (self.current_session.read()).clone() {
return Ok(session);
}
match self
.store_preferences
.get_object::<Session>(&self.session_config.session_cache_key)
@ -524,10 +514,33 @@ impl UserManager {
ErrorCode::RecordNotFound,
"User is not logged in",
)),
Some(session) => Ok(session),
Some(session) => {
self.current_session.write().replace(session.clone());
Ok(session)
},
}
}
pub(crate) fn set_session(&self, session: Option<Session>) -> Result<(), FlowyError> {
tracing::debug!("Set current user: {:?}", session);
match &session {
None => {
self.current_session.write().take();
self
.store_preferences
.remove(&self.session_config.session_cache_key)
},
Some(session) => {
self.current_session.write().replace(session.clone());
self
.store_preferences
.set_object(&self.session_config.session_cache_key, session.clone())
.map_err(internal_error)?;
},
}
Ok(())
}
async fn save_auth_data(
&self,
response: &impl UserAuthResponse,
@ -547,7 +560,7 @@ impl UserManager {
self
.save_user(uid, (user_profile, auth_type.clone()).into())
.await?;
self.set_current_session(Some(session.clone()))?;
self.set_session(Some(session.clone()))?;
Ok(())
}
@ -558,6 +571,27 @@ impl UserManager {
self.cloud_services.set_device_id(&session.device_id);
}
async fn handler_user_update(&self, user_update: UserUpdate) -> FlowyResult<()> {
let session = self.get_session()?;
if session.user_id == user_update.uid {
tracing::debug!("Receive user update: {:?}", user_update);
let user_profile = self.get_user_profile(user_update.uid).await?;
if !is_user_encryption_sign_valid(&user_profile, &user_update.encryption_sign) {
return Ok(());
}
// Save the user profile change
save_user_profile_change(
user_update.uid,
self.db_pool(user_update.uid)?,
UserTableChangeset::from(user_update),
)?;
}
Ok(())
}
async fn migrate_local_user_to_cloud(
&self,
old_user: &MigrationUser,
@ -575,3 +609,33 @@ impl UserManager {
Ok(folder_data)
}
}
fn is_user_encryption_sign_valid(user_profile: &UserProfile, encryption_sign: &str) -> bool {
// If the local user profile's encryption sign is not equal to the user update's encryption sign,
// which means the user enable encryption in another device, we should logout the current user.
let is_valid = user_profile.encryption_type.sign() == encryption_sign;
if !is_valid {
send_auth_state_notification(AuthStateChangedPB {
state: AuthStatePB::AuthStateForceSignOut,
})
.send();
}
is_valid
}
fn save_user_profile_change(
uid: i64,
pool: Arc<ConnectionPool>,
changeset: UserTableChangeset,
) -> FlowyResult<()> {
let conn = pool.get()?;
diesel_update_table!(user_table, changeset, &*conn);
let user: UserProfile = user_table::dsl::user_table
.filter(user_table::id.eq(&uid.to_string()))
.first::<UserTable>(&*conn)?
.into();
send_notification(&uid.to_string(), UserNotification::DidUpdateUserProfile)
.payload(UserProfilePB::from(user))
.send();
Ok(())
}

View File

@ -1,13 +1,15 @@
use flowy_derive::ProtoBuf_Enum;
use flowy_notification::NotificationBuilder;
use crate::entities::AuthStateChangedPB;
const USER_OBSERVABLE_SOURCE: &str = "User";
#[derive(ProtoBuf_Enum, Debug, Default)]
pub(crate) enum UserNotification {
#[default]
Unknown = 0,
DidUserSignIn = 1,
UserAuthStateChanged = 1,
DidUpdateUserProfile = 2,
DidUpdateUserWorkspaces = 3,
DidUpdateCloudConfig = 4,
@ -23,6 +25,11 @@ pub(crate) fn send_notification(id: &str, ty: UserNotification) -> NotificationB
NotificationBuilder::new(id, ty, USER_OBSERVABLE_SOURCE)
}
pub(crate) fn send_sign_in_notification() -> NotificationBuilder {
NotificationBuilder::new("", UserNotification::DidUserSignIn, USER_OBSERVABLE_SOURCE)
pub(crate) fn send_auth_state_notification(payload: AuthStateChangedPB) -> NotificationBuilder {
NotificationBuilder::new(
"auth_state_change_notification",
UserNotification::UserAuthStateChanged,
USER_OBSERVABLE_SOURCE,
)
.payload(payload)
}

View File

@ -17,7 +17,7 @@ impl UserManager {
// 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()?;
let user_profile = self.get_user_profile(session.user_id).await.ok()?;
if user_profile.auth_type == AuthType::Local && !auth_type.is_local() {
Some(MigrationUser {
user_profile,
@ -100,7 +100,7 @@ impl UserManager {
device_id,
user_workspace,
};
self.set_current_session(Some(session))?;
self.set_session(Some(session))?;
Ok(())
}
}

View File

@ -1,6 +1,7 @@
use std::str::FromStr;
use flowy_sqlite::schema::user_table;
use flowy_user_deps::cloud::UserUpdate;
use flowy_user_deps::entities::*;
/// The order of the fields in the struct must be the same as the order of the fields in the table.
@ -102,3 +103,14 @@ impl UserTableChangeset {
}
}
}
impl From<UserUpdate> for UserTableChangeset {
fn from(value: UserUpdate) -> Self {
UserTableChangeset {
id: value.uid.to_string(),
name: Some(value.name),
email: Some(value.email),
..Default::default()
}
}
}