mirror of
https://gitlab.com/comunic/comunicapiv3
synced 2024-11-26 15:29:21 +00:00
471 lines
17 KiB
Rust
471 lines
17 KiB
Rust
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<String> {
|
|
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<UserAccessToken> {
|
|
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<bool> {
|
|
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<Vec<UserAccessToken>> {
|
|
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<String> {
|
|
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<UserID> {
|
|
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<bool> {
|
|
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<UserID>) -> ResultBoxError<bool> {
|
|
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<AccountExport> {
|
|
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<String> {
|
|
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<bool> {
|
|
|
|
// 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<UserAccessToken> {
|
|
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,
|
|
})
|
|
} |