From 89f1ddf4d7b653d9f68f49a79371e6fc17ca4b8b Mon Sep 17 00:00:00 2001 From: Geometrically <18202329+Geometrically@users.noreply.github.com> Date: Mon, 30 Oct 2023 16:59:53 -0700 Subject: [PATCH] Route to view user's orgs (#742) --- ...7148618caa3be7cf33bc0859e51b06eede6e9.json | 22 +++++ ...f285ef8cb2bf8f40f9facafaae3f8c75d587.json} | 4 +- src/database/models/organization_item.rs | 2 +- src/database/models/user_item.rs | 27 +++++- src/routes/v2/organizations.rs | 7 +- src/routes/v2/project_creation.rs | 7 +- src/routes/v2/users.rs | 85 +++++++++++++++++++ 7 files changed, 142 insertions(+), 12 deletions(-) create mode 100644 .sqlx/query-1d356243ac743720af11e6a49d17148618caa3be7cf33bc0859e51b06eede6e9.json rename .sqlx/{query-05047ef3c49f2b90f5d090f69f8e7f626843d9487d5e63a28e8efe28e27cb9ad.json => query-30307fb92fd2d8e1f03f21f8ad76f285ef8cb2bf8f40f9facafaae3f8c75d587.json} (82%) diff --git a/.sqlx/query-1d356243ac743720af11e6a49d17148618caa3be7cf33bc0859e51b06eede6e9.json b/.sqlx/query-1d356243ac743720af11e6a49d17148618caa3be7cf33bc0859e51b06eede6e9.json new file mode 100644 index 00000000..23b9b12a --- /dev/null +++ b/.sqlx/query-1d356243ac743720af11e6a49d17148618caa3be7cf33bc0859e51b06eede6e9.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT o.id FROM organizations o\n INNER JOIN team_members tm ON tm.team_id = o.team_id AND tm.accepted = TRUE\n WHERE tm.user_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "1d356243ac743720af11e6a49d17148618caa3be7cf33bc0859e51b06eede6e9" +} diff --git a/.sqlx/query-05047ef3c49f2b90f5d090f69f8e7f626843d9487d5e63a28e8efe28e27cb9ad.json b/.sqlx/query-30307fb92fd2d8e1f03f21f8ad76f285ef8cb2bf8f40f9facafaae3f8c75d587.json similarity index 82% rename from .sqlx/query-05047ef3c49f2b90f5d090f69f8e7f626843d9487d5e63a28e8efe28e27cb9ad.json rename to .sqlx/query-30307fb92fd2d8e1f03f21f8ad76f285ef8cb2bf8f40f9facafaae3f8c75d587.json index 049047c4..0532f326 100644 --- a/.sqlx/query-05047ef3c49f2b90f5d090f69f8e7f626843d9487d5e63a28e8efe28e27cb9ad.json +++ b/.sqlx/query-30307fb92fd2d8e1f03f21f8ad76f285ef8cb2bf8f40f9facafaae3f8c75d587.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT o.id, o.title, o.team_id, o.description, o.icon_url, o.color\n FROM organizations o\n WHERE o.id = ANY($1) OR o.title = ANY($2)\n GROUP BY o.id;\n ", + "query": "\n SELECT o.id, o.title, o.team_id, o.description, o.icon_url, o.color\n FROM organizations o\n WHERE o.id = ANY($1) OR LOWER(o.title) = ANY($2)\n GROUP BY o.id;\n ", "describe": { "columns": [ { @@ -49,5 +49,5 @@ true ] }, - "hash": "05047ef3c49f2b90f5d090f69f8e7f626843d9487d5e63a28e8efe28e27cb9ad" + "hash": "30307fb92fd2d8e1f03f21f8ad76f285ef8cb2bf8f40f9facafaae3f8c75d587" } diff --git a/src/database/models/organization_item.rs b/src/database/models/organization_item.rs index fe50ccc7..f92622df 100644 --- a/src/database/models/organization_item.rs +++ b/src/database/models/organization_item.rs @@ -162,7 +162,7 @@ impl Organization { " SELECT o.id, o.title, o.team_id, o.description, o.icon_url, o.color FROM organizations o - WHERE o.id = ANY($1) OR o.title = ANY($2) + WHERE o.id = ANY($1) OR LOWER(o.title) = ANY($2) GROUP BY o.id; ", &organization_ids_parsed, diff --git a/src/database/models/user_item.rs b/src/database/models/user_item.rs index 436750ed..5ab27abe 100644 --- a/src/database/models/user_item.rs +++ b/src/database/models/user_item.rs @@ -1,6 +1,6 @@ use super::ids::{ProjectId, UserId}; use super::CollectionId; -use crate::database::models::DatabaseError; +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, RecipientStatus}; @@ -307,6 +307,31 @@ impl User { 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, diff --git a/src/routes/v2/organizations.rs b/src/routes/v2/organizations.rs index d4c8a056..e4dc5f07 100644 --- a/src/routes/v2/organizations.rs +++ b/src/routes/v2/organizations.rs @@ -75,8 +75,7 @@ pub async fn organization_create( let mut transaction = pool.begin().await?; // Try title - let title_organization_id_option: Option = - serde_json::from_str(&format!("\"{}\"", new_organization.title)).ok(); + let title_organization_id_option: Option = parse_base62(&new_organization.title).ok(); let mut organization_strings = vec![]; if let Some(title_organization_id) = title_organization_id_option { organization_strings.push(title_organization_id.to_string()); @@ -93,7 +92,7 @@ pub async fn organization_create( let team = team_item::TeamBuilder { members: vec![team_item::TeamMemberBuilder { user_id: current_user.id.into(), - role: crate::models::teams::OWNER_ROLE.to_owned(), + role: models::teams::OWNER_ROLE.to_owned(), permissions: ProjectPermissions::all(), organization_permissions: Some(OrganizationPermissions::all()), accepted: true, @@ -218,7 +217,7 @@ pub async fn organizations_get( .collect::>(); let teams_data = TeamMember::get_from_team_full_many(&team_ids, &**pool, &redis).await?; - let users = crate::database::models::User::get_many_ids( + let users = database::models::User::get_many_ids( &teams_data.iter().map(|x| x.user_id).collect::>(), &**pool, &redis, diff --git a/src/routes/v2/project_creation.rs b/src/routes/v2/project_creation.rs index 91db4d0c..21c49ed5 100644 --- a/src/routes/v2/project_creation.rs +++ b/src/routes/v2/project_creation.rs @@ -5,6 +5,7 @@ use crate::database::models::{self, image_item, User}; use crate::database::redis::RedisPool; use crate::file_hosting::{FileHost, FileHostingError}; use crate::models::error::ApiError; +use crate::models::ids::base62_impl::parse_base62; use crate::models::ids::ImageId; use crate::models::images::{Image, ImageContext}; use crate::models::pats::Scopes; @@ -417,16 +418,14 @@ async fn project_create_inner( .validate() .map_err(|err| CreateError::InvalidInput(validation_errors_to_string(err, None)))?; - let slug_project_id_option: Option = - serde_json::from_str(&format!("\"{}\"", create_data.slug)).ok(); + let slug_project_id_option: Option = parse_base62(&create_data.slug).ok(); if let Some(slug_project_id) = slug_project_id_option { - let slug_project_id: models::ids::ProjectId = slug_project_id.into(); let results = sqlx::query!( " SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1) ", - slug_project_id as models::ids::ProjectId + slug_project_id as i64 ) .fetch_one(&mut **transaction) .await diff --git a/src/routes/v2/users.rs b/src/routes/v2/users.rs index 0ad8b512..bda8ccc2 100644 --- a/src/routes/v2/users.rs +++ b/src/routes/v2/users.rs @@ -21,6 +21,7 @@ use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use serde_json::json; use sqlx::PgPool; +use std::collections::HashMap; use std::sync::Arc; use tokio::sync::Mutex; use validator::Validate; @@ -32,6 +33,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("user") .service(user_get) + .service(orgs_list) .service(projects_list) .service(collections_list) .service(user_delete) @@ -196,6 +198,89 @@ pub async fn collections_list( } } +#[get("{user_id}/organizatons")] +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, + Some(&[Scopes::PROJECT_READ]), + ) + .await + .map(|x| x.1) + .ok(); + + let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?; + + if let Some(id) = id_option.map(|x| x.id) { + let org_data = User::get_organizations(id, &**pool).await?; + + let organizations_data = + crate::database::models::organization_item::Organization::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::TeamMember::get_from_team_full_many( + &team_ids, &**pool, &redis, + ) + .await?; + let users = User::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 { + 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) + .flat_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 { + Ok(HttpResponse::NotFound().body("")) + } +} + lazy_static! { static ref RE_URL_SAFE: Regex = Regex::new(r"^[a-zA-Z0-9_-]*$").unwrap(); }