manager.rs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. use std::string::ToString;
  2. use std::sync::{Arc, Weak};
  3. use appflowy_integrate::collab_builder::AppFlowyCollabBuilder;
  4. use appflowy_integrate::RocksCollabDB;
  5. use collab_folder::core::FolderData;
  6. use collab_user::core::MutexUserAwareness;
  7. use serde_json::Value;
  8. use tokio::sync::{Mutex, RwLock};
  9. use uuid::Uuid;
  10. use flowy_error::{internal_error, ErrorCode, FlowyResult};
  11. use flowy_sqlite::kv::StorePreferences;
  12. use flowy_sqlite::schema::user_table;
  13. use flowy_sqlite::ConnectionPool;
  14. use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods};
  15. use flowy_user_deps::entities::*;
  16. use lib_infra::box_any::BoxAny;
  17. use crate::entities::{UserProfilePB, UserSettingPB};
  18. use crate::event_map::{
  19. DefaultUserStatusCallback, SignUpContext, UserCloudServiceProvider, UserStatusCallback,
  20. };
  21. use crate::migrations::historical_document::HistoricalEmptyDocumentMigration;
  22. use crate::migrations::local_user_to_cloud::migration_user_to_cloud;
  23. use crate::migrations::migration::UserLocalDataMigration;
  24. use crate::migrations::MigrationUser;
  25. use crate::services::cloud_config::get_cloud_config;
  26. use crate::services::database::UserDB;
  27. use crate::services::entities::{ResumableSignUp, Session};
  28. use crate::services::user_awareness::UserAwarenessDataSource;
  29. use crate::services::user_sql::{UserTable, UserTableChangeset};
  30. use crate::services::user_workspace::save_user_workspaces;
  31. use crate::{errors::FlowyError, notification::*};
  32. pub struct UserSessionConfig {
  33. root_dir: String,
  34. /// Used as the key of `Session` when saving session information to KV.
  35. session_cache_key: String,
  36. }
  37. impl UserSessionConfig {
  38. /// The `root_dir` represents as the root of the user folders. It must be unique for each
  39. /// users.
  40. pub fn new(name: &str, root_dir: &str) -> Self {
  41. let session_cache_key = format!("{}_session_cache", name);
  42. Self {
  43. root_dir: root_dir.to_owned(),
  44. session_cache_key,
  45. }
  46. }
  47. }
  48. pub struct UserManager {
  49. database: UserDB,
  50. session_config: UserSessionConfig,
  51. pub(crate) cloud_services: Arc<dyn UserCloudServiceProvider>,
  52. pub(crate) store_preferences: Arc<StorePreferences>,
  53. pub(crate) user_awareness: Arc<Mutex<Option<MutexUserAwareness>>>,
  54. pub(crate) user_status_callback: RwLock<Arc<dyn UserStatusCallback>>,
  55. pub(crate) collab_builder: Weak<AppFlowyCollabBuilder>,
  56. resumable_sign_up: Mutex<Option<ResumableSignUp>>,
  57. }
  58. impl UserManager {
  59. pub fn new(
  60. session_config: UserSessionConfig,
  61. cloud_services: Arc<dyn UserCloudServiceProvider>,
  62. store_preferences: Arc<StorePreferences>,
  63. collab_builder: Weak<AppFlowyCollabBuilder>,
  64. ) -> Self {
  65. let database = UserDB::new(&session_config.root_dir);
  66. let user_status_callback: RwLock<Arc<dyn UserStatusCallback>> =
  67. RwLock::new(Arc::new(DefaultUserStatusCallback));
  68. Self {
  69. database,
  70. session_config,
  71. cloud_services,
  72. store_preferences,
  73. user_awareness: Arc::new(Default::default()),
  74. user_status_callback,
  75. collab_builder,
  76. resumable_sign_up: Default::default(),
  77. }
  78. }
  79. pub fn get_store_preferences(&self) -> Weak<StorePreferences> {
  80. Arc::downgrade(&self.store_preferences)
  81. }
  82. /// Initializes the user session, including data migrations and user awareness configuration.
  83. ///
  84. /// This asynchronous function starts by retrieving the current session. If the session is successfully obtained,
  85. /// it will attempt a local data migration for the user. After ensuring the user's data is migrated and up-to-date,
  86. /// the function will set up the collaboration configuration and initialize the user's awareness. Upon successful
  87. /// completion, a user status callback is invoked to signify that the initialization process is complete.
  88. pub async fn init<C: UserStatusCallback + 'static>(&self, user_status_callback: C) {
  89. if let Ok(session) = self.get_session() {
  90. // Do the user data migration if needed
  91. match (
  92. self.database.get_collab_db(session.user_id),
  93. self.database.get_pool(session.user_id),
  94. ) {
  95. (Ok(collab_db), Ok(sqlite_pool)) => {
  96. match UserLocalDataMigration::new(session.clone(), collab_db, sqlite_pool)
  97. .run(vec![Box::new(HistoricalEmptyDocumentMigration)])
  98. {
  99. Ok(applied_migrations) => {
  100. if !applied_migrations.is_empty() {
  101. tracing::info!("Did apply migrations: {:?}", applied_migrations);
  102. }
  103. },
  104. Err(e) => tracing::error!("User data migration failed: {:?}", e),
  105. }
  106. },
  107. _ => tracing::error!("Failed to get collab db or sqlite pool"),
  108. }
  109. self.set_collab_config(&session);
  110. // Init the user awareness
  111. self
  112. .initialize_user_awareness(&session, UserAwarenessDataSource::Local)
  113. .await;
  114. let cloud_config = get_cloud_config(session.user_id, &self.store_preferences);
  115. if let Err(e) = user_status_callback
  116. .did_init(
  117. session.user_id,
  118. &cloud_config,
  119. &session.user_workspace,
  120. &session.device_id,
  121. )
  122. .await
  123. {
  124. tracing::error!("Failed to call did_init callback: {:?}", e);
  125. }
  126. }
  127. *self.user_status_callback.write().await = Arc::new(user_status_callback);
  128. }
  129. pub fn db_connection(&self, uid: i64) -> Result<DBConnection, FlowyError> {
  130. self.database.get_connection(uid)
  131. }
  132. pub fn db_pool(&self, uid: i64) -> Result<Arc<ConnectionPool>, FlowyError> {
  133. self.database.get_pool(uid)
  134. }
  135. pub fn get_collab_db(&self, uid: i64) -> Result<Weak<RocksCollabDB>, FlowyError> {
  136. self
  137. .database
  138. .get_collab_db(uid)
  139. .map(|collab_db| Arc::downgrade(&collab_db))
  140. }
  141. /// Performs a user sign-in, initializing user awareness and sending relevant notifications.
  142. ///
  143. /// This asynchronous function interacts with an external user service to authenticate and sign in a user
  144. /// based on provided parameters. Once signed in, it updates the collaboration configuration, logs the user,
  145. /// saves their workspaces, and initializes their user awareness.
  146. ///
  147. /// A sign-in notification is also sent after a successful sign-in.
  148. ///
  149. #[tracing::instrument(level = "debug", skip(self, params))]
  150. pub async fn sign_in(
  151. &self,
  152. params: BoxAny,
  153. auth_type: AuthType,
  154. ) -> Result<UserProfile, FlowyError> {
  155. self.update_auth_type(&auth_type).await;
  156. let response: SignInResponse = self
  157. .cloud_services
  158. .get_user_service()?
  159. .sign_in(params)
  160. .await?;
  161. let session = Session::from(&response);
  162. self.set_collab_config(&session);
  163. let latest_workspace = response.latest_workspace.clone();
  164. let user_profile = UserProfile::from((&response, &auth_type));
  165. self.save_auth_data(&response, &auth_type, &session).await?;
  166. let _ = self
  167. .initialize_user_awareness(&session, UserAwarenessDataSource::Remote)
  168. .await;
  169. if let Err(e) = self
  170. .user_status_callback
  171. .read()
  172. .await
  173. .did_sign_in(user_profile.uid, &latest_workspace, &session.device_id)
  174. .await
  175. {
  176. tracing::error!("Failed to call did_sign_in callback: {:?}", e);
  177. }
  178. send_sign_in_notification()
  179. .payload::<UserProfilePB>(user_profile.clone().into())
  180. .send();
  181. Ok(user_profile)
  182. }
  183. pub(crate) async fn update_auth_type(&self, auth_type: &AuthType) {
  184. self
  185. .user_status_callback
  186. .read()
  187. .await
  188. .auth_type_did_changed(auth_type.clone());
  189. self.cloud_services.set_auth_type(auth_type.clone());
  190. }
  191. /// Manages the user sign-up process, potentially migrating data if necessary.
  192. ///
  193. /// This asynchronous function interacts with an external authentication service to register and sign up a user
  194. /// based on the provided parameters. Following a successful sign-up, it handles configuration updates, logging,
  195. /// and saving workspace information. If a user is signing up with a new profile and previously had guest data,
  196. /// this function may migrate that data over to the new account.
  197. ///
  198. #[tracing::instrument(level = "info", skip(self, params))]
  199. pub async fn sign_up(
  200. &self,
  201. auth_type: AuthType,
  202. params: BoxAny,
  203. ) -> Result<UserProfile, FlowyError> {
  204. self.update_auth_type(&auth_type).await;
  205. let migration_user = self.get_migration_user(&auth_type).await;
  206. let auth_service = self.cloud_services.get_user_service()?;
  207. let response: SignUpResponse = auth_service.sign_up(params).await?;
  208. let user_profile = UserProfile::from((&response, &auth_type));
  209. if user_profile.encryption_type.is_need_encrypt_secret() {
  210. self
  211. .resumable_sign_up
  212. .lock()
  213. .await
  214. .replace(ResumableSignUp {
  215. user_profile: user_profile.clone(),
  216. migration_user,
  217. response,
  218. auth_type,
  219. });
  220. } else {
  221. self
  222. .continue_sign_up(&user_profile, migration_user, response, &auth_type)
  223. .await?;
  224. }
  225. Ok(user_profile)
  226. }
  227. #[tracing::instrument(level = "info", skip(self))]
  228. pub async fn resume_sign_up(&self) -> Result<(), FlowyError> {
  229. let ResumableSignUp {
  230. user_profile,
  231. migration_user,
  232. response,
  233. auth_type,
  234. } = self
  235. .resumable_sign_up
  236. .lock()
  237. .await
  238. .clone()
  239. .ok_or(FlowyError::new(
  240. ErrorCode::Internal,
  241. "No resumable sign up data",
  242. ))?;
  243. self
  244. .continue_sign_up(&user_profile, migration_user, response, &auth_type)
  245. .await?;
  246. Ok(())
  247. }
  248. #[tracing::instrument(level = "info", skip_all, err)]
  249. async fn continue_sign_up(
  250. &self,
  251. user_profile: &UserProfile,
  252. migration_user: Option<MigrationUser>,
  253. response: SignUpResponse,
  254. auth_type: &AuthType,
  255. ) -> FlowyResult<()> {
  256. let new_session = Session::from(&response);
  257. self.set_collab_config(&new_session);
  258. let user_awareness_source = if response.is_new_user {
  259. UserAwarenessDataSource::Local
  260. } else {
  261. UserAwarenessDataSource::Remote
  262. };
  263. let mut sign_up_context = SignUpContext {
  264. is_new: response.is_new_user,
  265. local_folder: None,
  266. };
  267. if response.is_new_user {
  268. if let Some(old_user) = migration_user {
  269. let new_user = MigrationUser {
  270. user_profile: user_profile.clone(),
  271. session: new_session.clone(),
  272. };
  273. tracing::info!(
  274. "Migrate old user data from {:?} to {:?}",
  275. old_user.user_profile.uid,
  276. new_user.user_profile.uid
  277. );
  278. match self.migrate_local_user_to_cloud(&old_user, &new_user).await {
  279. Ok(folder_data) => sign_up_context.local_folder = folder_data,
  280. Err(e) => tracing::error!("{:?}", e),
  281. }
  282. let _ = self.database.close(old_user.session.user_id);
  283. }
  284. }
  285. self
  286. .initialize_user_awareness(&new_session, user_awareness_source)
  287. .await;
  288. self
  289. .user_status_callback
  290. .read()
  291. .await
  292. .did_sign_up(
  293. sign_up_context,
  294. user_profile,
  295. &new_session.user_workspace,
  296. &new_session.device_id,
  297. )
  298. .await?;
  299. self
  300. .save_auth_data(&response, auth_type, &new_session)
  301. .await?;
  302. Ok(())
  303. }
  304. #[tracing::instrument(level = "info", skip(self))]
  305. pub async fn sign_out(&self) -> Result<(), FlowyError> {
  306. let session = self.get_session()?;
  307. self.database.close(session.user_id)?;
  308. self.set_current_session(None)?;
  309. let server = self.cloud_services.get_user_service()?;
  310. tokio::spawn(async move {
  311. match server.sign_out(None).await {
  312. Ok(_) => {},
  313. Err(e) => tracing::error!("Sign out failed: {:?}", e),
  314. }
  315. });
  316. Ok(())
  317. }
  318. /// Updates the user's profile with the given parameters.
  319. ///
  320. /// This function modifies the user's profile based on the provided update parameters. After updating, it
  321. /// sends a notification about the change. It's also responsible for handling interactions with the underlying
  322. /// database and updates user profile.
  323. ///
  324. #[tracing::instrument(level = "debug", skip(self))]
  325. pub async fn update_user_profile(
  326. &self,
  327. params: UpdateUserProfileParams,
  328. ) -> Result<(), FlowyError> {
  329. let old_user_profile = self.get_user_profile(params.uid, false).await?;
  330. let auth_type = old_user_profile.auth_type.clone();
  331. let session = self.get_session()?;
  332. let changeset = UserTableChangeset::new(params.clone());
  333. diesel_update_table!(
  334. user_table,
  335. changeset,
  336. &*self.db_connection(session.user_id)?
  337. );
  338. let session = self.get_session()?;
  339. let new_user_profile = self.get_user_profile(session.user_id, false).await?;
  340. send_notification(
  341. &session.user_id.to_string(),
  342. UserNotification::DidUpdateUserProfile,
  343. )
  344. .payload(UserProfilePB::from(new_user_profile))
  345. .send();
  346. self
  347. .update_user(&auth_type, session.user_id, None, params)
  348. .await?;
  349. Ok(())
  350. }
  351. pub async fn init_user(&self) -> Result<(), FlowyError> {
  352. Ok(())
  353. }
  354. pub async fn check_user(&self) -> Result<(), FlowyError> {
  355. let user_id = self.get_session()?.user_id;
  356. let credential = UserCredentials::from_uid(user_id);
  357. let auth_service = self.cloud_services.get_user_service()?;
  358. auth_service.check_user(credential).await?;
  359. Ok(())
  360. }
  361. pub async fn check_user_with_uuid(&self, uuid: &Uuid) -> Result<(), FlowyError> {
  362. let credential = UserCredentials::from_uuid(uuid.to_string());
  363. let auth_service = self.cloud_services.get_user_service()?;
  364. auth_service.check_user(credential).await?;
  365. Ok(())
  366. }
  367. /// Fetches the user profile for the given user ID.
  368. ///
  369. /// This function retrieves the user profile from the local database. If the `refresh` flag is set to `true`,
  370. /// it also attempts to update the user profile from a cloud service, and then sends a notification about the
  371. /// profile update.
  372. pub async fn get_user_profile(&self, uid: i64, refresh: bool) -> Result<UserProfile, FlowyError> {
  373. let user_id = uid.to_string();
  374. let user = user_table::dsl::user_table
  375. .filter(user_table::id.eq(&user_id))
  376. .first::<UserTable>(&*(self.db_connection(uid)?))?;
  377. if refresh {
  378. let weak_auth_service = Arc::downgrade(&self.cloud_services.get_user_service()?);
  379. let weak_pool = Arc::downgrade(&self.database.get_pool(uid)?);
  380. tokio::spawn(async move {
  381. if let (Some(auth_service), Some(pool)) = (weak_auth_service.upgrade(), weak_pool.upgrade())
  382. {
  383. if let Ok(Some(user_profile)) = auth_service
  384. .get_user_profile(UserCredentials::from_uid(uid))
  385. .await
  386. {
  387. let changeset = UserTableChangeset::from_user_profile(user_profile.clone());
  388. if let Ok(conn) = pool.get() {
  389. let filter =
  390. user_table::dsl::user_table.filter(user_table::dsl::id.eq(changeset.id.clone()));
  391. let _ = diesel::update(filter).set(changeset).execute(&*conn);
  392. // Send notification to the client
  393. let user_profile_pb: UserProfilePB = user_profile.into();
  394. send_notification(&uid.to_string(), UserNotification::DidUpdateUserProfile)
  395. .payload(user_profile_pb)
  396. .send();
  397. }
  398. }
  399. }
  400. });
  401. }
  402. Ok(user.into())
  403. }
  404. pub fn user_dir(&self, uid: i64) -> String {
  405. format!("{}/{}", self.session_config.root_dir, uid)
  406. }
  407. pub fn user_setting(&self) -> Result<UserSettingPB, FlowyError> {
  408. let session = self.get_session()?;
  409. let user_setting = UserSettingPB {
  410. user_folder: self.user_dir(session.user_id),
  411. };
  412. Ok(user_setting)
  413. }
  414. pub fn user_id(&self) -> Result<i64, FlowyError> {
  415. Ok(self.get_session()?.user_id)
  416. }
  417. pub fn token(&self) -> Result<Option<String>, FlowyError> {
  418. Ok(None)
  419. }
  420. async fn update_user(
  421. &self,
  422. _auth_type: &AuthType,
  423. uid: i64,
  424. token: Option<String>,
  425. params: UpdateUserProfileParams,
  426. ) -> Result<(), FlowyError> {
  427. let server = self.cloud_services.get_user_service()?;
  428. let token = token.to_owned();
  429. tokio::spawn(async move {
  430. let credentials = UserCredentials::new(token, Some(uid), None);
  431. server.update_user(credentials, params).await
  432. })
  433. .await
  434. .map_err(internal_error)??;
  435. Ok(())
  436. }
  437. async fn save_user(&self, uid: i64, user: UserTable) -> Result<(), FlowyError> {
  438. let conn = self.db_connection(uid)?;
  439. conn.immediate_transaction(|| {
  440. // delete old user if exists
  441. diesel::delete(user_table::dsl::user_table.filter(user_table::dsl::id.eq(&user.id)))
  442. .execute(&*conn)?;
  443. let _ = diesel::insert_into(user_table::table)
  444. .values(user)
  445. .execute(&*conn)?;
  446. Ok::<(), FlowyError>(())
  447. })?;
  448. Ok(())
  449. }
  450. pub(crate) fn set_current_session(&self, session: Option<Session>) -> Result<(), FlowyError> {
  451. tracing::debug!("Set current user: {:?}", session);
  452. match &session {
  453. None => self
  454. .store_preferences
  455. .remove(&self.session_config.session_cache_key),
  456. Some(session) => {
  457. self
  458. .store_preferences
  459. .set_object(&self.session_config.session_cache_key, session.clone())
  460. .map_err(internal_error)?;
  461. },
  462. }
  463. Ok(())
  464. }
  465. pub async fn receive_realtime_event(&self, json: Value) {
  466. self
  467. .user_status_callback
  468. .read()
  469. .await
  470. .receive_realtime_event(json);
  471. }
  472. /// Returns the current user session.
  473. pub fn get_session(&self) -> Result<Session, FlowyError> {
  474. match self
  475. .store_preferences
  476. .get_object::<Session>(&self.session_config.session_cache_key)
  477. {
  478. None => Err(FlowyError::new(
  479. ErrorCode::RecordNotFound,
  480. "User is not logged in",
  481. )),
  482. Some(session) => Ok(session),
  483. }
  484. }
  485. async fn save_auth_data(
  486. &self,
  487. response: &impl UserAuthResponse,
  488. auth_type: &AuthType,
  489. session: &Session,
  490. ) -> Result<(), FlowyError> {
  491. let user_profile = UserProfile::from((response, auth_type));
  492. let uid = user_profile.uid;
  493. self.add_historical_user(
  494. uid,
  495. response.device_id(),
  496. response.user_name().to_string(),
  497. auth_type,
  498. self.user_dir(uid),
  499. );
  500. save_user_workspaces(uid, self.db_pool(uid)?, response.user_workspaces())?;
  501. self
  502. .save_user(uid, (user_profile, auth_type.clone()).into())
  503. .await?;
  504. self.set_current_session(Some(session.clone()))?;
  505. Ok(())
  506. }
  507. fn set_collab_config(&self, session: &Session) {
  508. let collab_builder = self.collab_builder.upgrade().unwrap();
  509. collab_builder.set_sync_device(session.device_id.clone());
  510. collab_builder.initialize(session.user_workspace.id.clone());
  511. self.cloud_services.set_device_id(&session.device_id);
  512. }
  513. async fn migrate_local_user_to_cloud(
  514. &self,
  515. old_user: &MigrationUser,
  516. new_user: &MigrationUser,
  517. ) -> Result<Option<FolderData>, FlowyError> {
  518. let old_collab_db = self.database.get_collab_db(old_user.session.user_id)?;
  519. let new_collab_db = self.database.get_collab_db(new_user.session.user_id)?;
  520. let folder_data = migration_user_to_cloud(old_user, &old_collab_db, new_user, &new_collab_db)?;
  521. // Save the old user workspace setting.
  522. save_user_workspaces(
  523. old_user.session.user_id,
  524. self.database.get_pool(old_user.session.user_id)?,
  525. &[old_user.session.user_workspace.clone()],
  526. )?;
  527. Ok(folder_data)
  528. }
  529. }