use std::{collections::HashMap, sync::Arc}; use super::{ApiError, oauth_clients::get_user_clients}; use crate::{ auth::{ checks::is_visible_organization, filter_visible_collections, filter_visible_projects, get_user_from_headers, }, database::{models::DBUser, redis::RedisPool}, file_hosting::{FileHost, FileHostPublicity}, models::{ notifications::Notification, pats::Scopes, projects::Project, users::{Badges, Role}, }, queue::session::AuthQueue, util::{ img::delete_old_images, routes::read_limited_from_payload, validate::validation_errors_to_string, }, }; use actix_web::{HttpRequest, HttpResponse, web}; use ariadne::ids::UserId; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use validator::Validate; pub fn config(cfg: &mut web::ServiceConfig) { cfg.route("user", web::get().to(user_auth_get)); cfg.route("users", web::get().to(users_get)); cfg.route("user_email", web::get().to(admin_user_email)); cfg.service( web::scope("user") .route("{user_id}/projects", web::get().to(projects_list)) .route("{id}", web::get().to(user_get)) .route("{user_id}/collections", web::get().to(collections_list)) .route("{user_id}/organizations", web::get().to(orgs_list)) .route("{id}", web::patch().to(user_edit)) .route("{id}/icon", web::patch().to(user_icon_edit)) .route("{id}/icon", web::delete().to(user_icon_delete)) .route("{id}", web::delete().to(user_delete)) .route("{id}/follows", web::get().to(user_follows)) .route("{id}/notifications", web::get().to(user_notifications)) .route("{id}/oauth_apps", web::get().to(get_user_clients)), ); } #[derive(Deserialize)] pub struct UserEmailQuery { pub email: String, } pub async fn admin_user_email( req: HttpRequest, pool: web::Data, redis: web::Data, session_queue: web::Data, email: web::Query, ) -> Result { let user = get_user_from_headers( &req, &**pool, &redis, &session_queue, Scopes::SESSION_ACCESS, ) .await .map(|x| x.1)?; if !user.role.is_admin() { return Err(ApiError::CustomAuthentication( "You do not have permission to get a user from their email!" .to_string(), )); } let user_id = sqlx::query!( " SELECT id FROM users WHERE LOWER(email) = LOWER($1) ", email.email ) .fetch_optional(&**pool) .await? .map(|x| x.id) .ok_or_else(|| { ApiError::InvalidInput( "The email provided is not associated with a user!".to_string(), ) })?; let user = DBUser::get_id( crate::database::models::DBUserId(user_id), &**pool, &redis, ) .await?; if let Some(user) = user { Ok(HttpResponse::Ok().json(user)) } else { Err(ApiError::NotFound) } } pub async fn projects_list( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, redis: web::Data, session_queue: web::Data, ) -> Result { let user = get_user_from_headers( &req, &**pool, &redis, &session_queue, Scopes::PROJECT_READ, ) .await .map(|x| x.1) .ok(); let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis).await?; if let Some(id) = id_option.map(|x| x.id) { let project_data = DBUser::get_projects(id, &**pool, &redis).await?; let projects: Vec<_> = crate::database::DBProject::get_many_ids( &project_data, &**pool, &redis, ) .await?; let projects = filter_visible_projects(projects, &user, &pool, true).await?; Ok(HttpResponse::Ok().json(projects)) } else { Err(ApiError::NotFound) } } pub async fn user_auth_get( req: HttpRequest, pool: web::Data, redis: web::Data, session_queue: web::Data, ) -> Result { let (scopes, mut user) = get_user_from_headers( &req, &**pool, &redis, &session_queue, Scopes::USER_READ, ) .await?; if !scopes.contains(Scopes::USER_READ_EMAIL) { user.email = None; } if !scopes.contains(Scopes::PAYOUTS_READ) { user.payout_data = None; } Ok(HttpResponse::Ok().json(user)) } #[derive(Serialize, Deserialize)] pub struct UserIds { pub ids: String, } pub async fn users_get( web::Query(ids): web::Query, pool: web::Data, redis: web::Data, ) -> Result { let user_ids = serde_json::from_str::>(&ids.ids)?; let users_data = DBUser::get_many(&user_ids, &**pool, &redis).await?; let users: Vec = users_data.into_iter().map(From::from).collect(); Ok(HttpResponse::Ok().json(users)) } pub async fn user_get( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, redis: web::Data, session_queue: web::Data, ) -> Result { let user_data = DBUser::get(&info.into_inner().0, &**pool, &redis).await?; if let Some(data) = user_data { let auth_user = get_user_from_headers( &req, &**pool, &redis, &session_queue, Scopes::SESSION_ACCESS, ) .await .map(|x| x.1) .ok(); let response: crate::models::users::User = if auth_user.is_some_and(|x| x.role.is_admin()) { crate::models::users::User::from_full(data) } else { data.into() }; Ok(HttpResponse::Ok().json(response)) } else { Err(ApiError::NotFound) } } pub async fn collections_list( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, redis: web::Data, session_queue: web::Data, ) -> Result { let user = get_user_from_headers( &req, &**pool, &redis, &session_queue, Scopes::COLLECTION_READ, ) .await .map(|x| x.1) .ok(); let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis).await?; if let Some(id) = id_option.map(|x| x.id) { let collection_data = DBUser::get_collections(id, &**pool).await?; let response: Vec<_> = crate::database::models::DBCollection::get_many( &collection_data, &**pool, &redis, ) .await?; let collections = filter_visible_collections(response, &user, true).await?; Ok(HttpResponse::Ok().json(collections)) } else { Err(ApiError::NotFound) } } pub async fn orgs_list( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, redis: web::Data, session_queue: web::Data, ) -> Result { let user = get_user_from_headers( &req, &**pool, &redis, &session_queue, Scopes::PROJECT_READ, ) .await .map(|x| x.1) .ok(); let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis).await?; if let Some(id) = id_option.map(|x| x.id) { let org_data = DBUser::get_organizations(id, &**pool).await?; let organizations_data = crate::database::models::organization_item::DBOrganization::get_many_ids( &org_data, &**pool, &redis, ) .await?; let team_ids = organizations_data .iter() .map(|x| x.team_id) .collect::>(); let teams_data = crate::database::models::DBTeamMember::get_from_team_full_many( &team_ids, &**pool, &redis, ) .await?; let users = DBUser::get_many_ids( &teams_data.iter().map(|x| x.user_id).collect::>(), &**pool, &redis, ) .await?; let mut organizations = vec![]; let mut team_groups = HashMap::new(); for item in teams_data { team_groups.entry(item.team_id).or_insert(vec![]).push(item); } for data in organizations_data { if !is_visible_organization(&data, &user, &pool, &redis).await? { continue; } let members_data = team_groups.remove(&data.team_id).unwrap_or(vec![]); let logged_in = user .as_ref() .and_then(|user| { members_data .iter() .find(|x| x.user_id == user.id.into() && x.accepted) }) .is_some(); let team_members: Vec<_> = members_data .into_iter() .filter(|x| logged_in || x.accepted || id == x.user_id) .filter_map(|data| { users.iter().find(|x| x.id == data.user_id).map(|user| { crate::models::teams::TeamMember::from( data, user.clone(), !logged_in, ) }) }) .collect(); let organization = crate::models::organizations::Organization::from( data, team_members, ); organizations.push(organization); } Ok(HttpResponse::Ok().json(organizations)) } else { Err(ApiError::NotFound) } } #[derive(Serialize, Deserialize, Validate)] pub struct EditUser { #[validate(length(min = 1, max = 39), regex(path = *crate::util::validate::RE_USERNAME))] pub username: Option, #[serde( default, skip_serializing_if = "Option::is_none", with = "::serde_with::rust::double_option" )] #[validate(length(max = 160))] pub bio: Option>, pub role: Option, pub badges: Option, #[validate(length(max = 160))] pub venmo_handle: Option, pub allow_friend_requests: Option, } pub async fn user_edit( req: HttpRequest, info: web::Path<(String,)>, new_user: web::Json, pool: web::Data, redis: web::Data, session_queue: web::Data, ) -> Result { let (scopes, user) = get_user_from_headers( &req, &**pool, &redis, &session_queue, Scopes::USER_WRITE, ) .await?; new_user.validate().map_err(|err| { ApiError::Validation(validation_errors_to_string(err, None)) })?; let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis).await?; if let Some(actual_user) = id_option { let id = actual_user.id; let user_id: UserId = id.into(); if user.id == user_id || user.role.is_mod() { let mut transaction = pool.begin().await?; if let Some(username) = &new_user.username { let existing_user_id_option = DBUser::get(username, &**pool, &redis).await?; if existing_user_id_option .map(|x| UserId::from(x.id)) .is_none_or(|id| id == user.id) { sqlx::query!( " UPDATE users SET username = $1 WHERE (id = $2) ", username, id as crate::database::models::ids::DBUserId, ) .execute(&mut *transaction) .await?; } else { return Err(ApiError::InvalidInput(format!( "Username {username} is taken!" ))); } } if let Some(bio) = &new_user.bio { sqlx::query!( " UPDATE users SET bio = $1 WHERE (id = $2) ", bio.as_deref(), id as crate::database::models::ids::DBUserId, ) .execute(&mut *transaction) .await?; } if let Some(role) = &new_user.role { if !user.role.is_admin() { return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the role of this user!" .to_string(), )); } let role = role.to_string(); sqlx::query!( " UPDATE users SET role = $1 WHERE (id = $2) ", role, id as crate::database::models::ids::DBUserId, ) .execute(&mut *transaction) .await?; } if let Some(badges) = &new_user.badges { if !user.role.is_admin() { return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the badges of this user!" .to_string(), )); } sqlx::query!( " UPDATE users SET badges = $1 WHERE (id = $2) ", badges.bits() as i64, id as crate::database::models::ids::DBUserId, ) .execute(&mut *transaction) .await?; } if let Some(venmo_handle) = &new_user.venmo_handle { if !scopes.contains(Scopes::PAYOUTS_WRITE) { return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the venmo handle of this user!" .to_string(), )); } sqlx::query!( " UPDATE users SET venmo_handle = $1 WHERE (id = $2) ", venmo_handle, id as crate::database::models::ids::DBUserId, ) .execute(&mut *transaction) .await?; } if let Some(allow_friend_requests) = &user.allow_friend_requests { sqlx::query!( " UPDATE users SET allow_friend_requests = $1 WHERE (id = $2) ", allow_friend_requests, id as crate::database::models::ids::DBUserId, ) .execute(&mut *transaction) .await?; } transaction.commit().await?; DBUser::clear_caches(&[(id, Some(actual_user.username))], &redis) .await?; Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::CustomAuthentication( "You do not have permission to edit this user!".to_string(), )) } } else { Err(ApiError::NotFound) } } #[derive(Serialize, Deserialize)] pub struct Extension { pub ext: String, } #[allow(clippy::too_many_arguments)] pub async fn user_icon_edit( web::Query(ext): web::Query, req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, redis: web::Data, file_host: web::Data>, mut payload: web::Payload, session_queue: web::Data, ) -> Result { let user = get_user_from_headers( &req, &**pool, &redis, &session_queue, Scopes::USER_WRITE, ) .await? .1; let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis).await?; if let Some(actual_user) = id_option { if user.id != actual_user.id.into() && !user.role.is_mod() { return Err(ApiError::CustomAuthentication( "You don't have permission to edit this user's icon." .to_string(), )); } delete_old_images( actual_user.avatar_url, actual_user.raw_avatar_url, FileHostPublicity::Public, &***file_host, ) .await?; let bytes = read_limited_from_payload( &mut payload, 262144, "Icons must be smaller than 256KiB", ) .await?; let user_id: UserId = actual_user.id.into(); let upload_result = crate::util::img::upload_image_optimized( &format!("data/{user_id}"), FileHostPublicity::Public, bytes.freeze(), &ext.ext, Some(96), Some(1.0), &***file_host, ) .await?; sqlx::query!( " UPDATE users SET avatar_url = $1, raw_avatar_url = $2 WHERE (id = $3) ", upload_result.url, upload_result.raw_url, actual_user.id as crate::database::models::ids::DBUserId, ) .execute(&**pool) .await?; DBUser::clear_caches(&[(actual_user.id, None)], &redis).await?; Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::NotFound) } } pub async fn user_icon_delete( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, redis: web::Data, file_host: web::Data>, session_queue: web::Data, ) -> Result { let user = get_user_from_headers( &req, &**pool, &redis, &session_queue, Scopes::USER_WRITE, ) .await? .1; let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis).await?; if let Some(actual_user) = id_option { if user.id != actual_user.id.into() && !user.role.is_mod() { return Err(ApiError::CustomAuthentication( "You don't have permission to edit this user's icon." .to_string(), )); } delete_old_images( actual_user.avatar_url, actual_user.raw_avatar_url, FileHostPublicity::Public, &***file_host, ) .await?; sqlx::query!( " UPDATE users SET avatar_url = NULL, raw_avatar_url = NULL WHERE (id = $1) ", actual_user.id as crate::database::models::ids::DBUserId, ) .execute(&**pool) .await?; DBUser::clear_caches(&[(actual_user.id, None)], &redis).await?; Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::NotFound) } } pub async fn user_delete( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, redis: web::Data, session_queue: web::Data, ) -> Result { let user = get_user_from_headers( &req, &**pool, &redis, &session_queue, Scopes::USER_DELETE, ) .await? .1; let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis).await?; if let Some(id) = id_option.map(|x| x.id) { if !user.role.is_admin() && user.id != id.into() { return Err(ApiError::CustomAuthentication( "You do not have permission to delete this user!".to_string(), )); } let mut transaction = pool.begin().await?; let result = DBUser::remove(id, &mut transaction, &redis).await?; transaction.commit().await?; if result.is_some() { Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::NotFound) } } else { Err(ApiError::NotFound) } } pub async fn user_follows( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, redis: web::Data, session_queue: web::Data, ) -> Result { let user = get_user_from_headers( &req, &**pool, &redis, &session_queue, Scopes::USER_READ, ) .await? .1; let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis).await?; if let Some(id) = id_option.map(|x| x.id) { if !user.role.is_admin() && user.id != id.into() { return Err(ApiError::CustomAuthentication( "You do not have permission to see the projects this user follows!".to_string(), )); } let project_ids = DBUser::get_follows(id, &**pool).await?; let projects: Vec<_> = crate::database::DBProject::get_many_ids( &project_ids, &**pool, &redis, ) .await? .into_iter() .map(Project::from) .collect(); Ok(HttpResponse::Ok().json(projects)) } else { Err(ApiError::NotFound) } } pub async fn user_notifications( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, redis: web::Data, session_queue: web::Data, ) -> Result { let user = get_user_from_headers( &req, &**pool, &redis, &session_queue, Scopes::NOTIFICATION_READ, ) .await? .1; let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis).await?; if let Some(id) = id_option.map(|x| x.id) { if !user.role.is_admin() && user.id != id.into() { return Err(ApiError::CustomAuthentication( "You do not have permission to see the notifications of this user!".to_string(), )); } let mut notifications: Vec = crate::database::models::notification_item::DBNotification::get_many_user_exposed_on_site( id, &**pool, &redis, ) .await? .into_iter() .map(Into::into) .collect(); notifications.sort_by(|a, b| b.created.cmp(&a.created)); Ok(HttpResponse::Ok().json(notifications)) } else { Err(ApiError::NotFound) } }