use bcrypt::{DEFAULT_COST, hash_with_result, verify}; use crate::constants::{PASSWORD_RESET_TOKEN_LENGTH, PASSWORD_RESET_TOKEN_LIFETIME}; use crate::constants::database_tables_names::{USER_ACCESS_TOKENS_TABLE, USERS_TABLE}; use crate::controllers::user_ws_controller; use crate::data::account_export::AccountExport; use crate::data::api_client::APIClient; use crate::data::error::{ExecError, Res, ResultBoxError}; use crate::data::general_settings::GeneralSettings; use crate::data::lang_settings::LangSettings; use crate::data::new_account::NewAccount; use crate::data::new_data_conservation_policy::NewDataConservationPolicy; use crate::data::new_notifications_settings::NewNotificationsSettings; use crate::data::security_settings::SecuritySettings; use crate::data::user::{AccountImageVisibility, User, UserID, UserPageStatus}; use crate::data::user_token::{PushNotificationToken, UserAccessToken}; use crate::helpers::{comments_helper, conversations_helper, custom_emojies_helper, database, events_helper, forez_presence_helper, friends_helper, groups_helper, likes_helper, notifications_helper, posts_helper, push_notifications_helper, survey_helper, user_helper}; use crate::helpers::database::{DeleteQuery, InsertQuery, QueryInfo, RowResult, UpdateInfo}; use crate::helpers::events_helper::Event; use crate::helpers::likes_helper::LikeType; use crate::utils::crypt_utils::{legacy_crypt_pass, rand_str}; use crate::utils::date_utils::{mysql_date, time}; use crate::utils::user_data_utils::user_data_path; /// Account helper /// /// @author Pierre Hubert /// Create a new account pub fn create(new_account: &NewAccount) -> ResultBoxError { database::InsertQuery::new(USERS_TABLE) .add_str("prenom", &new_account.first_name) .add_str("nom", &new_account.last_name) .add_str("date_creation", &mysql_date()) .add_str("mail", &new_account.email) .add_str("password", &hash_password(&new_account.password)?) .insert_drop_result() } /// Attempt to sign-in user /// /// In this version of the api, we consider that there is only one login token required /// This is why I returns just a simple string, the token created for the user in case of success pub fn login_user(email: &str, password: &str, client: &APIClient) -> ResultBoxError { let user = user_helper::find_user_by_email(email)?; // Validate user password if !validate_password(&user, password)? { return Err(ExecError::boxed_new("The user gave an invalid password!")); } // Create new login tokens let new_token = UserAccessToken { id: 0, user_id: user.id.clone(), client_id: client.id, token: rand_str(150), last_refresh: time(), timeout: client.default_expiration_time, push_notifications_token: PushNotificationToken::UNDEFINED, }; // Save it InsertQuery::new(USER_ACCESS_TOKENS_TABLE) .add_u64("client_id", client.id) .add_user_id("user_id", &new_token.user_id) .add_str("token", &new_token.token) .add_u64("last_refresh", new_token.last_refresh) .add_u64("timeout", new_token.timeout) .add_opt_str("push_notifications_token", new_token.push_notifications_token.to_db().as_ref()) .insert_drop_result()?; Ok(new_token.token) } /// Find a user ID based on login token pub fn find_user_by_login_token(token: &str, client: &APIClient) -> ResultBoxError { QueryInfo::new(USER_ACCESS_TOKENS_TABLE) .cond_u64("client_id", client.id) .cond("token", token) .set_custom_where("last_refresh + timeout > ?") .add_custom_where_argument_u64(time()) .query_row(db_to_user_access_token) } /// Check out whether an email address exists or not pub fn exists_mail(mail: &str) -> ResultBoxError { database::QueryInfo::new(USERS_TABLE) .cond("mail", mail) .exec_count() .map(|r| r > 0) } /// Refresh a user access token pub fn refresh_access_token(token: &UserAccessToken) -> Res { UpdateInfo::new(USER_ACCESS_TOKENS_TABLE) .cond_u64("id", token.id) .set_u64("last_refresh", time()) .exec() } /// Destroy a given user login tokens pub fn destroy_login_tokens(access_tokens: &UserAccessToken) -> Res { // Un-register from independent push notifications service // (continue to destroy token even in case of failure) push_notifications_helper::un_register_from_previous_service(access_tokens)?; DeleteQuery::new(USER_ACCESS_TOKENS_TABLE) .cond_u64("id", access_tokens.id) .exec()?; // Send an event (destroyed_login_tokens) events_helper::propagate_event(&Event::DestroyedLoginToken(access_tokens))?; Ok(()) } /// Clean up old access tokens pub fn clean_up_old_access_tokens() -> Res { let to_delete = QueryInfo::new(USER_ACCESS_TOKENS_TABLE) .set_custom_where("last_refresh + timeout < ?") .add_custom_where_argument_u64(time()) .exec(db_to_user_access_token)?; for token in to_delete { destroy_login_tokens(&token)?; } Ok(()) } /// Get all the login tokens of a user pub fn get_all_login_tokens(id: &UserID) -> Res> { database::QueryInfo::new(USER_ACCESS_TOKENS_TABLE) .cond_user_id("user_id", id) .exec(db_to_user_access_token) } /// Destroy all login tokens of a user pub fn destroy_all_user_tokens(id: &UserID) -> ResultBoxError { user_ws_controller::disconnect_user_from_all_sockets(id)?; for token in get_all_login_tokens(id)? { destroy_login_tokens(&token)?; } Ok(()) } /// Generate a new password reset token pub fn generate_password_reset_token(user_id: &UserID) -> ResultBoxError { let token = rand_str(PASSWORD_RESET_TOKEN_LENGTH); database::UpdateInfo::new(USERS_TABLE) .cond_user_id("ID", user_id) .set_str("password_reset_token", &token) .set_u64("password_reset_token_time_create", time()) .exec()?; Ok(token) } /// Remove password reset token for a given user pub fn destroy_password_reset_token_for_user(user_id: &UserID) -> ResultBoxError { database::UpdateInfo::new(USERS_TABLE) .cond_user_id("ID", user_id) .set_str("password_reset_token", "") .set_u64("password_reset_token_time_create", 0) .exec() } /// Get the ID of a user based on a password reset token pub fn get_user_id_from_password_reset_token(token: &str) -> ResultBoxError { database::QueryInfo::new(USERS_TABLE) .cond("password_reset_token", token) .set_custom_where("password_reset_token_time_create > ?") .add_custom_where_argument_u64(time() - PASSWORD_RESET_TOKEN_LIFETIME) .query_row(|r| r.get_user_id("ID")) } /// Check current user's password pub fn check_user_password(user_id: &UserID, password: &str) -> ResultBoxError { let user = user_helper::find_user_by_id(user_id)?; validate_password(&user, password) } /// Change the password of a user pub fn change_password(user_id: &UserID, new_password: &str) -> ResultBoxError { database::UpdateInfo::new(USERS_TABLE) .cond_user_id("ID", user_id) .set_str("password", &hash_password(new_password)?) .exec() } /// Check out whether a virtual directory is taken by a user or not pub fn check_user_directory_availability(dir: &str, user_id: Option) -> ResultBoxError { let found_user = user_helper::find_user_by_virtual_directory(dir); match (found_user, user_id) { // A user was found, but we did not specify a user (Ok(_), None) => Ok(false), // A user was found, and we specified a user ID, we check if the IDs are the same (Ok(user), Some(id)) => Ok(user.id == id), // No user was found, virtual directory is considered as available (Err(_), _) => Ok(true) } } /// Update the last activity of a user pub fn update_last_activity(user_id: &UserID) -> ResultBoxError { database::UpdateInfo::new(USERS_TABLE) .cond_user_id("ID", user_id) .set_u64("last_activity", time()) .exec() } /// Set new general settings of an account pub fn set_general(settings: &GeneralSettings) -> ResultBoxError { database::UpdateInfo::new(USERS_TABLE) .cond_user_id("ID", &settings.id) .set_str("prenom", &settings.first_name) .set_str("nom", &settings.last_name) .set_legacy_bool("public", settings.page_status != UserPageStatus::PRIVATE) .set_legacy_bool("pageouverte", settings.page_status == UserPageStatus::OPEN) .set_legacy_bool("bloquecommentaire", settings.block_comments) .set_legacy_bool("autoriser_post_amis", settings.allow_posts_from_friends) .set_legacy_bool("autorise_mail", settings.allow_mails) .set_legacy_bool("liste_amis_publique", settings.friends_list_public) .set_legacy_bool("is_email_public", settings.email_public) .set_opt_str("sous_repertoire", settings.virtual_directory.clone()) .set_opt_str("site_web", settings.personal_website.clone()) .set_opt_str("public_note", settings.public_note.clone()) .set_opt_str("location", settings.location.clone()) .exec() } /// Set new language settings pub fn set_language_settings(settings: &LangSettings) -> ResultBoxError { database::UpdateInfo::new(USERS_TABLE) .cond_user_id("ID", &settings.id) .set_str("lang", &settings.lang) .exec() } /// Set new security settings pub fn set_security_settings(settings: &SecuritySettings) -> ResultBoxError { database::UpdateInfo::new(USERS_TABLE) .cond_user_id("ID", &settings.id) .set_opt_str("question1", settings.question1.question()) .set_opt_str("reponse1", settings.question1.answer()) .set_opt_str("question2", settings.question2.question()) .set_opt_str("reponse2", settings.question2.answer()) .exec() } /// Delete user account image pub fn delete_account_image(user_id: &UserID) -> ResultBoxError { let user = user_helper::find_user_by_id(user_id)?; if !user.has_account_image() { return Ok(()); } let path = user_data_path(user.account_image_path.unwrap().as_ref()); if path.exists() { std::fs::remove_file(path)?; } database::UpdateInfo::new(USERS_TABLE) .cond_user_id("ID", user_id) .set_str("account_image_path", "") .exec() } /// Set a new account image pub fn set_account_image(user_id: &UserID, uri: &String) -> ResultBoxError { // First, delete the previous account image delete_account_image(user_id)?; // Update database database::UpdateInfo::new(USERS_TABLE) .cond_user_id("ID", user_id) .set_str("account_image_path", uri) .exec() } /// Set account image visibility level pub fn set_account_image_visibility(user_id: &UserID, level: AccountImageVisibility) -> ResultBoxError { database::UpdateInfo::new(USERS_TABLE) .cond_user_id("ID", user_id) .set_str("account_image_visibility", &level.to_db()) .exec() } /// Set data conservation policy pub fn set_data_conservation_policy(new_policy: NewDataConservationPolicy) -> Res { database::UpdateInfo::new(USERS_TABLE) .cond_user_id("ID", &new_policy.user_id) .set_opt_u64_or_zero("delete_account_after", new_policy.delete_account_after) .set_opt_u64_or_zero("delete_notifications_after", new_policy.delete_notifications_after) .set_opt_u64_or_zero("delete_comments_after", new_policy.delete_comments_after) .set_opt_u64_or_zero("delete_posts_after", new_policy.delete_posts_after) .set_opt_u64_or_zero("delete_conversation_messages_after", new_policy.delete_conversation_messages_after) .set_opt_u64_or_zero("delete_likes_after", new_policy.delete_likes_after) .exec() } /// Set new notifications settings pub fn set_notifications_settings(new_settings: NewNotificationsSettings) -> Res { database::UpdateInfo::new(USERS_TABLE) .cond_user_id("ID", &new_settings.user_id) .set_legacy_bool("allow_notif_sound", new_settings.allow_notifications_sound) .set_legacy_bool("allow_notif_conv", new_settings.allow_conversations) .exec() } /// Set new push notification token pub fn set_push_notification_token(client: &UserAccessToken, new_token: PushNotificationToken) -> Res { // In case of independent push service, remove previous client push_notifications_helper::un_register_from_previous_service(client)?; database::UpdateInfo::new(USER_ACCESS_TOKENS_TABLE) .cond_u64("id", client.id) .set_opt_str("push_notifications_token", new_token.to_db()) .exec() } /// Export an account's data pub fn export(user_id: &UserID) -> ResultBoxError { let mut data = AccountExport { user: user_helper::find_user_by_id(user_id)?, posts: posts_helper::export_all_posts_user(user_id)?, comments: comments_helper::export_all_user(user_id)?, likes: likes_helper::export_all_user(user_id)?, survey_responses: survey_helper::export_all_user_responses(user_id)?, all_conversation_messages: conversations_helper::export_all_user_messages(user_id)?, conversations: conversations_helper::get_list_user(user_id)?, conversation_messages: Default::default(), friends_list: friends_helper::GetFriendsQuery::new(user_id).exec()?, groups: groups_helper::get_list_user(user_id, false)?, }; // Process conversation messages for conv in &data.conversations { data.conversation_messages .insert(conv.id, conversations_helper::get_all_messages(conv.id)?); } Ok(data) } /// Delete a user's account pub fn delete(user_id: &UserID) -> ResultBoxError { // Close all WebSockets of user destroy_all_user_tokens(user_id)?; // Delete all group membership groups_helper::delete_all_user_groups(user_id)?; // Delete all user comments comments_helper::delete_all_user(user_id)?; // Delete all user posts posts_helper::delete_all_user(user_id)?; // Delete all responses of user to surveys survey_helper::delete_all_user_responses(user_id)?; // Delete all the likes created by the user likes_helper::delete_all_user(user_id)?; // Delete all conversation messages conversations_helper::delete_all_user_messages(user_id)?; // Remove the user from all its conversations conversations_helper::delete_all_user_conversations(user_id)?; // Delete all the notifications related with the user notifications_helper::delete_all_related_with_user(user_id)?; // Delete all user friends, including friendship requests friends_helper::delete_all_user(user_id)?; // Delete user account image delete_account_image(user_id)?; // Delete all the likes on the user page likes_helper::delete_all(user_id.id(), LikeType::USER)?; // Delete all custom user emojies custom_emojies_helper::delete_all_user(user_id)?; // Delete all forez presences forez_presence_helper::delete_all_user(user_id)?; // Delete connections to all services destroy_all_user_tokens(user_id)?; // Remove the user from the database database::DeleteQuery::new(USERS_TABLE) .cond_user_id("ID", user_id) .exec()?; Ok(()) } /// Automatically delete the account, if it have been inactive for a too long time pub fn remove_if_inactive_for_too_long_time(user: &User) -> Res { let timeout = user.delete_account_after.unwrap_or(0); if timeout < 1 { return Ok(()); } if user.last_activity < time() - timeout { delete(&user.id)?; } Ok(()) } /// Hash the password to store it inside the database fn hash_password(pass: &str) -> Res { Ok(hash_with_result(pass, DEFAULT_COST)?.to_string()) } /// Validate user password. /// /// If the password is encoded using the legacy method, it is automatically upgraded in case of /// success fn validate_password(user: &User, password: &str) -> Res { // We check if the password use the new storage mechanism if user.password.starts_with("$") { return Ok(verify(password, &user.password)?); } // We need to upgrade the password let crypt_pass = legacy_crypt_pass(password)?; // If the password is not valid if !user.password.eq(&crypt_pass) { return Ok(false); } // Upgrade the password change_password(&user.id, password)?; Ok(true) } fn db_to_user_access_token(res: &RowResult) -> Res { let push_notifications_token = PushNotificationToken::from_db(res.get_optional_str("push_notifications_token")?); Ok(UserAccessToken { id: res.get_u64("id")?, client_id: res.get_u64("client_id")?, user_id: res.get_user_id("user_id")?, token: res.get_str("token")?, last_refresh: res.get_u64("last_refresh")?, timeout: res.get_u64("timeout")?, push_notifications_token, }) }