use super::ids::{ProjectId, UserId}; use super::CollectionId; use crate::database::models::{DatabaseError, OrganizationId}; use crate::database::redis::RedisPool; use crate::models::ids::base62_impl::{parse_base62, to_base62}; use crate::models::users::Badges; use chrono::{DateTime, Utc}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; const USERS_NAMESPACE: &str = "users"; const USER_USERNAMES_NAMESPACE: &str = "users_usernames"; const USERS_PROJECTS_NAMESPACE: &str = "users_projects"; #[derive(Deserialize, Serialize, Clone, Debug)] pub struct User { pub id: UserId, pub github_id: Option, pub discord_id: Option, pub gitlab_id: Option, pub google_id: Option, pub steam_id: Option, pub microsoft_id: Option, pub password: Option, pub paypal_id: Option, pub paypal_country: Option, pub paypal_email: Option, pub venmo_handle: Option, pub totp_secret: Option, pub username: String, pub name: Option, pub email: Option, pub email_verified: bool, pub avatar_url: Option, pub bio: Option, pub created: DateTime, pub role: String, pub badges: Badges, pub balance: Decimal, } impl User { pub async fn insert( &self, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result<(), sqlx::error::Error> { sqlx::query!( " INSERT INTO users ( id, username, name, email, avatar_url, bio, created, github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id, email_verified, password, paypal_id, paypal_country, paypal_email, venmo_handle ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19 ) ", self.id as UserId, &self.username, self.name.as_ref(), self.email.as_ref(), self.avatar_url.as_ref(), self.bio.as_ref(), self.created, self.github_id, self.discord_id, self.gitlab_id, self.google_id, self.steam_id, self.microsoft_id, self.email_verified, self.password, self.paypal_id, self.paypal_country, self.paypal_email, self.venmo_handle ) .execute(&mut **transaction) .await?; Ok(()) } pub async fn get<'a, 'b, E>( string: &str, executor: E, redis: &RedisPool, ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { User::get_many(&[string], executor, redis) .await .map(|x| x.into_iter().next()) } pub async fn get_id<'a, 'b, E>( id: UserId, executor: E, redis: &RedisPool, ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { User::get_many(&[crate::models::ids::UserId::from(id)], executor, redis) .await .map(|x| x.into_iter().next()) } pub async fn get_many_ids<'a, E>( user_ids: &[UserId], exec: E, redis: &RedisPool, ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let ids = user_ids .iter() .map(|x| crate::models::ids::UserId::from(*x)) .collect::>(); User::get_many(&ids, exec, redis).await } pub async fn get_many<'a, E, T: ToString>( users_strings: &[T], exec: E, redis: &RedisPool, ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { use futures::TryStreamExt; let mut redis = redis.connect().await?; if users_strings.is_empty() { return Ok(Vec::new()); } let mut found_users = Vec::new(); let mut remaining_strings = users_strings .iter() .map(|x| x.to_string()) .collect::>(); let mut user_ids = users_strings .iter() .flat_map(|x| parse_base62(&x.to_string()).map(|x| x as i64)) .collect::>(); user_ids.append( &mut redis .multi_get::( USER_USERNAMES_NAMESPACE, users_strings.iter().map(|x| x.to_string().to_lowercase()), ) .await? .into_iter() .flatten() .collect(), ); if !user_ids.is_empty() { let users = redis .multi_get::(USERS_NAMESPACE, user_ids.iter().map(|x| x.to_string())) .await?; for user in users { if let Some(user) = user.and_then(|x| serde_json::from_str::(&x).ok()) { remaining_strings.retain(|x| { &to_base62(user.id.0 as u64) != x && user.username.to_lowercase() != x.to_lowercase() }); found_users.push(user); continue; } } } if !remaining_strings.is_empty() { let user_ids_parsed: Vec = remaining_strings .iter() .flat_map(|x| parse_base62(&x.to_string()).ok()) .map(|x| x as i64) .collect(); let db_users: Vec = sqlx::query!( " SELECT id, name, email, avatar_url, username, bio, created, role, badges, balance, github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id, email_verified, password, totp_secret, paypal_id, paypal_country, paypal_email, venmo_handle FROM users WHERE id = ANY($1) OR LOWER(username) = ANY($2) ", &user_ids_parsed, &remaining_strings .into_iter() .map(|x| x.to_string().to_lowercase()) .collect::>(), ) .fetch_many(exec) .try_filter_map(|e| async { Ok(e.right().map(|u| User { id: UserId(u.id), github_id: u.github_id, discord_id: u.discord_id, gitlab_id: u.gitlab_id, google_id: u.google_id, steam_id: u.steam_id, microsoft_id: u.microsoft_id, name: u.name, email: u.email, email_verified: u.email_verified, avatar_url: u.avatar_url, username: u.username, bio: u.bio, created: u.created, role: u.role, badges: Badges::from_bits(u.badges as u64).unwrap_or_default(), balance: u.balance, password: u.password, paypal_id: u.paypal_id, paypal_country: u.paypal_country, paypal_email: u.paypal_email, venmo_handle: u.venmo_handle, totp_secret: u.totp_secret, })) }) .try_collect::>() .await?; for user in db_users { redis .set_serialized_to_json(USERS_NAMESPACE, user.id.0, &user, None) .await?; redis .set( USER_USERNAMES_NAMESPACE, &user.username.to_lowercase(), &user.id.0.to_string(), None, ) .await?; found_users.push(user); } } Ok(found_users) } pub async fn get_email<'a, E>(email: &str, exec: E) -> Result, sqlx::Error> where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { let user_pass = sqlx::query!( " SELECT id FROM users WHERE email = $1 ", email ) .fetch_optional(exec) .await?; Ok(user_pass.map(|x| UserId(x.id))) } pub async fn get_projects<'a, E>( user_id: UserId, exec: E, redis: &RedisPool, ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { use futures::stream::TryStreamExt; let mut redis = redis.connect().await?; let cached_projects = redis .get_deserialized_from_json::>( USERS_PROJECTS_NAMESPACE, &user_id.0.to_string(), ) .await?; if let Some(projects) = cached_projects { return Ok(projects); } let db_projects = sqlx::query!( " SELECT m.id FROM mods m INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.accepted = TRUE WHERE tm.user_id = $1 ORDER BY m.downloads DESC ", user_id as UserId, ) .fetch_many(exec) .try_filter_map(|e| async { Ok(e.right().map(|m| ProjectId(m.id))) }) .try_collect::>() .await?; redis .set_serialized_to_json(USERS_PROJECTS_NAMESPACE, user_id.0, &db_projects, None) .await?; Ok(db_projects) } pub async fn get_organizations<'a, E>( user_id: UserId, exec: E, ) -> Result, sqlx::Error> where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { use futures::stream::TryStreamExt; let orgs = sqlx::query!( " SELECT o.id FROM organizations o INNER JOIN team_members tm ON tm.team_id = o.team_id AND tm.accepted = TRUE WHERE tm.user_id = $1 ", user_id as UserId, ) .fetch_many(exec) .try_filter_map(|e| async { Ok(e.right().map(|m| OrganizationId(m.id))) }) .try_collect::>() .await?; Ok(orgs) } pub async fn get_collections<'a, E>( user_id: UserId, exec: E, ) -> Result, sqlx::Error> where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { use futures::stream::TryStreamExt; let projects = sqlx::query!( " SELECT c.id FROM collections c WHERE c.user_id = $1 ", user_id as UserId, ) .fetch_many(exec) .try_filter_map(|e| async { Ok(e.right().map(|m| CollectionId(m.id))) }) .try_collect::>() .await?; Ok(projects) } pub async fn get_backup_codes<'a, E>( user_id: UserId, exec: E, ) -> Result, sqlx::Error> where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { use futures::stream::TryStreamExt; let codes = sqlx::query!( " SELECT code FROM user_backup_codes WHERE user_id = $1 ", user_id as UserId, ) .fetch_many(exec) .try_filter_map(|e| async { Ok(e.right().map(|m| to_base62(m.code as u64))) }) .try_collect::>() .await?; Ok(codes) } pub async fn clear_caches( user_ids: &[(UserId, Option)], redis: &RedisPool, ) -> Result<(), DatabaseError> { let mut redis = redis.connect().await?; redis .delete_many(user_ids.iter().flat_map(|(id, username)| { [ (USERS_NAMESPACE, Some(id.0.to_string())), ( USER_USERNAMES_NAMESPACE, username.clone().map(|i| i.to_lowercase()), ), ] })) .await?; Ok(()) } pub async fn clear_project_cache( user_ids: &[UserId], redis: &RedisPool, ) -> Result<(), DatabaseError> { let mut redis = redis.connect().await?; redis .delete_many( user_ids .iter() .map(|id| (USERS_PROJECTS_NAMESPACE, Some(id.0.to_string()))), ) .await?; Ok(()) } pub async fn remove( id: UserId, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, redis: &RedisPool, ) -> Result, DatabaseError> { let user = Self::get_id(id, &mut **transaction, redis).await?; if let Some(delete_user) = user { User::clear_caches(&[(id, Some(delete_user.username))], redis).await?; let deleted_user: UserId = crate::models::users::DELETED_USER.into(); sqlx::query!( " UPDATE team_members SET user_id = $1 WHERE (user_id = $2 AND is_owner = TRUE) ", deleted_user as UserId, id as UserId, ) .execute(&mut **transaction) .await?; sqlx::query!( " UPDATE versions SET author_id = $1 WHERE (author_id = $2) ", deleted_user as UserId, id as UserId, ) .execute(&mut **transaction) .await?; use futures::TryStreamExt; let notifications: Vec = sqlx::query!( " SELECT n.id FROM notifications n WHERE n.user_id = $1 ", id as UserId, ) .fetch_many(&mut **transaction) .try_filter_map(|e| async { Ok(e.right().map(|m| m.id)) }) .try_collect::>() .await?; sqlx::query!( " DELETE FROM notifications WHERE user_id = $1 ", id as UserId, ) .execute(&mut **transaction) .await?; sqlx::query!( " DELETE FROM notifications_actions WHERE notification_id = ANY($1) ", ¬ifications ) .execute(&mut **transaction) .await?; sqlx::query!( " DELETE FROM reports WHERE user_id = $1 OR reporter = $1 ", id as UserId, ) .execute(&mut **transaction) .await?; sqlx::query!( " DELETE FROM mod_follows WHERE follower_id = $1 ", id as UserId, ) .execute(&mut **transaction) .await?; sqlx::query!( " DELETE FROM team_members WHERE user_id = $1 ", id as UserId, ) .execute(&mut **transaction) .await?; sqlx::query!( " DELETE FROM payouts_values WHERE user_id = $1 ", id as UserId, ) .execute(&mut **transaction) .await?; sqlx::query!( " DELETE FROM payouts WHERE user_id = $1 ", id as UserId, ) .execute(&mut **transaction) .await?; sqlx::query!( r#" UPDATE threads_messages SET body = '{"type": "deleted"}', author_id = $2 WHERE author_id = $1 "#, id as UserId, deleted_user as UserId, ) .execute(&mut **transaction) .await?; sqlx::query!( " DELETE FROM threads_members WHERE user_id = $1 ", id as UserId, ) .execute(&mut **transaction) .await?; sqlx::query!( " DELETE FROM sessions WHERE user_id = $1 ", id as UserId, ) .execute(&mut **transaction) .await?; sqlx::query!( " DELETE FROM pats WHERE user_id = $1 ", id as UserId, ) .execute(&mut **transaction) .await?; sqlx::query!( " DELETE FROM user_backup_codes WHERE user_id = $1 ", id as UserId, ) .execute(&mut **transaction) .await?; sqlx::query!( " DELETE FROM users WHERE id = $1 ", id as UserId, ) .execute(&mut **transaction) .await?; Ok(Some(())) } else { Ok(None) } } }