use std::collections::HashMap; use std::sync::Arc; use super::ApiError; use crate::auth::{filter_visible_projects, get_user_from_headers}; use crate::database::models::team_item::DBTeamMember; use crate::database::models::{ DBOrganization, generate_organization_id, team_item, }; use crate::database::redis::RedisPool; use crate::file_hosting::FileHost; use crate::models::ids::OrganizationId; use crate::models::pats::Scopes; use crate::models::teams::{OrganizationPermissions, ProjectPermissions}; use crate::queue::session::AuthQueue; use crate::routes::v3::project_creation::CreateError; use crate::util::img::delete_old_images; use crate::util::routes::read_from_payload; use crate::util::validate::validation_errors_to_string; use crate::{database, models}; use actix_web::{HttpRequest, HttpResponse, web}; use ariadne::ids::UserId; use ariadne::ids::base62_impl::parse_base62; use futures::TryStreamExt; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use validator::Validate; pub fn config(cfg: &mut web::ServiceConfig) { cfg.route("organizations", web::get().to(organizations_get)); cfg.service( web::scope("organization") .route("", web::post().to(organization_create)) .route("{id}/projects", web::get().to(organization_projects_get)) .route("{id}", web::get().to(organization_get)) .route("{id}", web::patch().to(organizations_edit)) .route("{id}", web::delete().to(organization_delete)) .route("{id}/projects", web::post().to(organization_projects_add)) .route( "{id}/projects/{project_id}", web::delete().to(organization_projects_remove), ) .route("{id}/icon", web::patch().to(organization_icon_edit)) .route("{id}/icon", web::delete().to(delete_organization_icon)) .route( "{id}/members", web::get().to(super::teams::team_members_get_organization), ), ); } pub async fn organization_projects_get( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, redis: web::Data, session_queue: web::Data, ) -> Result { let id = info.into_inner().0; let current_user = get_user_from_headers( &req, &**pool, &redis, &session_queue, Some(&[Scopes::ORGANIZATION_READ, Scopes::PROJECT_READ]), ) .await .map(|x| x.1) .ok(); let organization_data = DBOrganization::get(&id, &**pool, &redis).await?; if let Some(organization) = organization_data { let project_ids = sqlx::query!( " SELECT m.id FROM organizations o INNER JOIN mods m ON m.organization_id = o.id WHERE o.id = $1 ", organization.id as database::models::ids::DBOrganizationId ) .fetch(&**pool) .map_ok(|m| database::models::DBProjectId(m.id)) .try_collect::>() .await?; let projects_data = crate::database::models::DBProject::get_many_ids( &project_ids, &**pool, &redis, ) .await?; let projects = filter_visible_projects(projects_data, ¤t_user, &pool, true) .await?; Ok(HttpResponse::Ok().json(projects)) } else { Err(ApiError::NotFound) } } #[derive(Deserialize, Validate)] pub struct NewOrganization { #[validate( length(min = 3, max = 64), regex(path = *crate::util::validate::RE_URL_SAFE) )] pub slug: String, // Title of the organization #[validate(length(min = 3, max = 64))] pub name: String, #[validate(length(min = 3, max = 256))] pub description: String, } pub async fn organization_create( req: HttpRequest, new_organization: web::Json, pool: web::Data, redis: web::Data, session_queue: web::Data, ) -> Result { let current_user = get_user_from_headers( &req, &**pool, &redis, &session_queue, Some(&[Scopes::ORGANIZATION_CREATE]), ) .await? .1; new_organization.validate().map_err(|err| { CreateError::ValidationError(validation_errors_to_string(err, None)) })?; let mut transaction = pool.begin().await?; // Try title let name_organization_id_option: Option = serde_json::from_str(&format!("\"{}\"", new_organization.slug)).ok(); let mut organization_strings = vec![]; if let Some(name_organization_id) = name_organization_id_option { organization_strings.push(name_organization_id.to_string()); } organization_strings.push(new_organization.slug.clone()); let results = DBOrganization::get_many( &organization_strings, &mut *transaction, &redis, ) .await?; if !results.is_empty() { return Err(CreateError::SlugCollision); } let organization_id = generate_organization_id(&mut transaction).await?; // Create organization managerial team let team = team_item::TeamBuilder { members: vec![team_item::TeamMemberBuilder { user_id: current_user.id.into(), role: crate::models::teams::DEFAULT_ROLE.to_owned(), is_owner: true, permissions: ProjectPermissions::all(), organization_permissions: Some(OrganizationPermissions::all()), accepted: true, payouts_split: Decimal::ONE_HUNDRED, ordering: 0, }], }; let team_id = team.insert(&mut transaction).await?; // Create organization let organization = DBOrganization { id: organization_id, slug: new_organization.slug.clone(), name: new_organization.name.clone(), description: new_organization.description.clone(), team_id, icon_url: None, raw_icon_url: None, color: None, }; organization.clone().insert(&mut transaction).await?; transaction.commit().await?; // Only member is the owner, the logged in one let member_data = DBTeamMember::get_from_team_full(team_id, &**pool, &redis) .await? .into_iter() .next(); let members_data = if let Some(member_data) = member_data { vec![crate::models::teams::TeamMember::from_model( member_data, current_user.clone(), false, )] } else { return Err(CreateError::InvalidInput( "Failed to get created team.".to_owned(), // should never happen )); }; let organization = models::organizations::Organization::from(organization, members_data); Ok(HttpResponse::Ok().json(organization)) } pub async fn organization_get( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, redis: web::Data, session_queue: web::Data, ) -> Result { let id = info.into_inner().0; let current_user = get_user_from_headers( &req, &**pool, &redis, &session_queue, Some(&[Scopes::ORGANIZATION_READ]), ) .await .map(|x| x.1) .ok(); let user_id = current_user.as_ref().map(|x| x.id.into()); let organization_data = DBOrganization::get(&id, &**pool, &redis).await?; if let Some(data) = organization_data { let members_data = DBTeamMember::get_from_team_full(data.team_id, &**pool, &redis) .await?; let users = crate::database::models::DBUser::get_many_ids( &members_data.iter().map(|x| x.user_id).collect::>(), &**pool, &redis, ) .await?; let logged_in = current_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 || user_id .map(|y: crate::database::models::DBUserId| { y == x.user_id }) .unwrap_or(false) }) .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 = models::organizations::Organization::from(data, team_members); return Ok(HttpResponse::Ok().json(organization)); } Err(ApiError::NotFound) } #[derive(Deserialize)] pub struct OrganizationIds { pub ids: String, } pub async fn organizations_get( req: HttpRequest, web::Query(ids): web::Query, pool: web::Data, redis: web::Data, session_queue: web::Data, ) -> Result { let ids = serde_json::from_str::>(&ids.ids)?; let organizations_data = DBOrganization::get_many(&ids, &**pool, &redis).await?; let team_ids = organizations_data .iter() .map(|x| x.team_id) .collect::>(); let teams_data = DBTeamMember::get_from_team_full_many(&team_ids, &**pool, &redis) .await?; let users = crate::database::models::DBUser::get_many_ids( &teams_data.iter().map(|x| x.user_id).collect::>(), &**pool, &redis, ) .await?; let current_user = get_user_from_headers( &req, &**pool, &redis, &session_queue, Some(&[Scopes::ORGANIZATION_READ]), ) .await .map(|x| x.1) .ok(); let user_id = current_user.as_ref().map(|x| x.id.into()); 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 = current_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 || user_id .map(|y: crate::database::models::DBUserId| { y == x.user_id }) .unwrap_or(false) }) .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 = models::organizations::Organization::from(data, team_members); organizations.push(organization); } Ok(HttpResponse::Ok().json(organizations)) } #[derive(Serialize, Deserialize, Validate)] pub struct OrganizationEdit { #[validate(length(min = 3, max = 256))] pub description: Option, #[validate( length(min = 3, max = 64), regex(path = *crate::util::validate::RE_URL_SAFE) )] pub slug: Option, #[validate(length(min = 3, max = 64))] pub name: Option, } pub async fn organizations_edit( req: HttpRequest, info: web::Path<(String,)>, new_organization: web::Json, pool: web::Data, redis: web::Data, session_queue: web::Data, ) -> Result { let user = get_user_from_headers( &req, &**pool, &redis, &session_queue, Some(&[Scopes::ORGANIZATION_WRITE]), ) .await? .1; new_organization.validate().map_err(|err| { ApiError::Validation(validation_errors_to_string(err, None)) })?; let string = info.into_inner().0; let result = database::models::DBOrganization::get(&string, &**pool, &redis).await?; if let Some(organization_item) = result { let id = organization_item.id; let team_member = database::models::DBTeamMember::get_from_user_id( organization_item.team_id, user.id.into(), &**pool, ) .await?; let permissions = OrganizationPermissions::get_permissions_by_role( &user.role, &team_member, ); if let Some(perms) = permissions { let mut transaction = pool.begin().await?; if let Some(description) = &new_organization.description { if !perms.contains(OrganizationPermissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the description of this organization!" .to_string(), )); } sqlx::query!( " UPDATE organizations SET description = $1 WHERE (id = $2) ", description, id as database::models::ids::DBOrganizationId, ) .execute(&mut *transaction) .await?; } if let Some(name) = &new_organization.name { if !perms.contains(OrganizationPermissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the name of this organization!" .to_string(), )); } sqlx::query!( " UPDATE organizations SET name = $1 WHERE (id = $2) ", name, id as database::models::ids::DBOrganizationId, ) .execute(&mut *transaction) .await?; } if let Some(slug) = &new_organization.slug { if !perms.contains(OrganizationPermissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the slug of this organization!" .to_string(), )); } let name_organization_id_option: Option = parse_base62(slug).ok(); if let Some(name_organization_id) = name_organization_id_option { let results = sqlx::query!( " SELECT EXISTS(SELECT 1 FROM organizations WHERE id=$1) ", name_organization_id as i64 ) .fetch_one(&mut *transaction) .await?; if results.exists.unwrap_or(true) { return Err(ApiError::InvalidInput( "slug collides with other organization's id!" .to_string(), )); } } // Make sure the new name is different from the old one // We are able to unwrap here because the name is always set if !slug.eq(&organization_item.slug.clone()) { let results = sqlx::query!( " SELECT EXISTS(SELECT 1 FROM organizations WHERE LOWER(slug) = LOWER($1)) ", slug ) .fetch_one(&mut *transaction) .await?; if results.exists.unwrap_or(true) { return Err(ApiError::InvalidInput( "slug collides with other organization's id!" .to_string(), )); } } sqlx::query!( " UPDATE organizations SET slug = $1 WHERE (id = $2) ", Some(slug), id as database::models::ids::DBOrganizationId, ) .execute(&mut *transaction) .await?; } transaction.commit().await?; database::models::DBOrganization::clear_cache( organization_item.id, Some(organization_item.slug), &redis, ) .await?; Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::CustomAuthentication( "You do not have permission to edit this organization!" .to_string(), )) } } else { Err(ApiError::NotFound) } } pub async fn organization_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, Some(&[Scopes::ORGANIZATION_DELETE]), ) .await? .1; let string = info.into_inner().0; let organization = database::models::DBOrganization::get(&string, &**pool, &redis) .await? .ok_or_else(|| { ApiError::InvalidInput( "The specified organization does not exist!".to_string(), ) })?; if !user.role.is_admin() { let team_member = database::models::DBTeamMember::get_from_user_id_organization( organization.id, user.id.into(), false, &**pool, ) .await .map_err(ApiError::Database)? .ok_or_else(|| { ApiError::InvalidInput( "The specified organization does not exist!".to_string(), ) })?; let permissions = OrganizationPermissions::get_permissions_by_role( &user.role, &Some(team_member), ) .unwrap_or_default(); if !permissions.contains(OrganizationPermissions::DELETE_ORGANIZATION) { return Err(ApiError::CustomAuthentication( "You don't have permission to delete this organization!" .to_string(), )); } } let owner_id = sqlx::query!( " SELECT user_id FROM team_members WHERE team_id = $1 AND is_owner = TRUE ", organization.team_id as database::models::ids::DBTeamId ) .fetch_one(&**pool) .await? .user_id; let owner_id = database::models::ids::DBUserId(owner_id); let mut transaction = pool.begin().await?; // Handle projects- every project that is in this organization needs to have its owner changed the organization owner // Now, no project should have an owner if it is in an organization, and also // the owner of an organization should not be a team member in any project let organization_project_teams = sqlx::query!( " SELECT t.id FROM organizations o INNER JOIN mods m ON m.organization_id = o.id INNER JOIN teams t ON t.id = m.team_id WHERE o.id = $1 AND $1 IS NOT NULL ", organization.id as database::models::ids::DBOrganizationId ) .fetch(&mut *transaction) .map_ok(|c| database::models::DBTeamId(c.id)) .try_collect::>() .await?; for organization_project_team in organization_project_teams.iter() { let new_id = crate::database::models::ids::generate_team_member_id( &mut transaction, ) .await?; let member = DBTeamMember { id: new_id, team_id: *organization_project_team, user_id: owner_id, role: "Inherited Owner".to_string(), is_owner: true, permissions: ProjectPermissions::all(), organization_permissions: None, accepted: true, payouts_split: Decimal::ZERO, ordering: 0, }; member.insert(&mut transaction).await?; } // Safely remove the organization let result = database::models::DBOrganization::remove( organization.id, &mut transaction, &redis, ) .await?; transaction.commit().await?; database::models::DBOrganization::clear_cache( organization.id, Some(organization.slug), &redis, ) .await?; for team_id in organization_project_teams { database::models::DBTeamMember::clear_cache(team_id, &redis).await?; } if result.is_some() { Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::NotFound) } } #[derive(Deserialize)] pub struct OrganizationProjectAdd { pub project_id: String, // Also allow name/slug } pub async fn organization_projects_add( req: HttpRequest, info: web::Path<(String,)>, project_info: web::Json, pool: web::Data, redis: web::Data, session_queue: web::Data, ) -> Result { let info = info.into_inner().0; let current_user = get_user_from_headers( &req, &**pool, &redis, &session_queue, Some(&[Scopes::PROJECT_WRITE, Scopes::ORGANIZATION_WRITE]), ) .await? .1; let organization = database::models::DBOrganization::get(&info, &**pool, &redis) .await? .ok_or_else(|| { ApiError::InvalidInput( "The specified organization does not exist!".to_string(), ) })?; let project_item = database::models::DBProject::get( &project_info.project_id, &**pool, &redis, ) .await? .ok_or_else(|| { ApiError::InvalidInput( "The specified project does not exist!".to_string(), ) })?; if project_item.inner.organization_id.is_some() { return Err(ApiError::InvalidInput( "The specified project is already owned by an organization!" .to_string(), )); } let project_team_member = database::models::DBTeamMember::get_from_user_id_project( project_item.inner.id, current_user.id.into(), false, &**pool, ) .await? .ok_or_else(|| { ApiError::InvalidInput( "You are not a member of this project!".to_string(), ) })?; let organization_team_member = database::models::DBTeamMember::get_from_user_id_organization( organization.id, current_user.id.into(), false, &**pool, ) .await? .ok_or_else(|| { ApiError::InvalidInput( "You are not a member of this organization!".to_string(), ) })?; // Require ownership of a project to add it to an organization if !current_user.role.is_admin() && !project_team_member.is_owner { return Err(ApiError::CustomAuthentication( "You need to be an owner of a project to add it to an organization!".to_string(), )); } let permissions = OrganizationPermissions::get_permissions_by_role( ¤t_user.role, &Some(organization_team_member), ) .unwrap_or_default(); if permissions.contains(OrganizationPermissions::ADD_PROJECT) { let mut transaction = pool.begin().await?; sqlx::query!( " UPDATE mods SET organization_id = $1 WHERE (id = $2) ", organization.id as database::models::DBOrganizationId, project_item.inner.id as database::models::ids::DBProjectId ) .execute(&mut *transaction) .await?; // The former owner is no longer an owner (as it is now 'owned' by the organization, 'given' to them) // The former owner is still a member of the project, but not an owner // When later removed from the organization, the project will be owned by whoever is specified as the new owner there let organization_owner_user_id = sqlx::query!( " SELECT u.id FROM team_members INNER JOIN users u ON u.id = team_members.user_id WHERE team_id = $1 AND is_owner = TRUE ", organization.team_id as database::models::ids::DBTeamId ) .fetch_one(&mut *transaction) .await?; let organization_owner_user_id = database::models::ids::DBUserId(organization_owner_user_id.id); sqlx::query!( " DELETE FROM team_members WHERE team_id = $1 AND (is_owner = TRUE OR user_id = $2) ", project_item.inner.team_id as database::models::ids::DBTeamId, organization_owner_user_id as database::models::ids::DBUserId, ) .execute(&mut *transaction) .await?; transaction.commit().await?; database::models::DBUser::clear_project_cache( &[current_user.id.into()], &redis, ) .await?; database::models::DBTeamMember::clear_cache( project_item.inner.team_id, &redis, ) .await?; database::models::DBProject::clear_cache( project_item.inner.id, project_item.inner.slug, None, &redis, ) .await?; } else { return Err(ApiError::CustomAuthentication( "You do not have permission to add projects to this organization!" .to_string(), )); } Ok(HttpResponse::Ok().finish()) } #[derive(Deserialize)] pub struct OrganizationProjectRemoval { // A new owner must be supplied for the project. // That user must be a member of the organization, but not necessarily a member of the project. pub new_owner: UserId, } pub async fn organization_projects_remove( req: HttpRequest, info: web::Path<(String, String)>, pool: web::Data, data: web::Json, redis: web::Data, session_queue: web::Data, ) -> Result { let (organization_id, project_id) = info.into_inner(); let current_user = get_user_from_headers( &req, &**pool, &redis, &session_queue, Some(&[Scopes::PROJECT_WRITE, Scopes::ORGANIZATION_WRITE]), ) .await? .1; let organization = database::models::DBOrganization::get( &organization_id, &**pool, &redis, ) .await? .ok_or_else(|| { ApiError::InvalidInput( "The specified organization does not exist!".to_string(), ) })?; let project_item = database::models::DBProject::get(&project_id, &**pool, &redis) .await? .ok_or_else(|| { ApiError::InvalidInput( "The specified project does not exist!".to_string(), ) })?; if !project_item .inner .organization_id .eq(&Some(organization.id)) { return Err(ApiError::InvalidInput( "The specified project is not owned by this organization!" .to_string(), )); } let organization_team_member = database::models::DBTeamMember::get_from_user_id_organization( organization.id, current_user.id.into(), false, &**pool, ) .await? .ok_or_else(|| { ApiError::InvalidInput( "You are not a member of this organization!".to_string(), ) })?; let permissions = OrganizationPermissions::get_permissions_by_role( ¤t_user.role, &Some(organization_team_member), ) .unwrap_or_default(); if permissions.contains(OrganizationPermissions::REMOVE_PROJECT) { // Now that permissions are confirmed, we confirm the veracity of the new user as an org member database::models::DBTeamMember::get_from_user_id_organization( organization.id, data.new_owner.into(), false, &**pool, ) .await? .ok_or_else(|| { ApiError::InvalidInput( "The specified user is not a member of this organization!" .to_string(), ) })?; // Then, we get the team member of the project and that user (if it exists) // We use the team member get directly let new_owner = database::models::DBTeamMember::get_from_user_id_project( project_item.inner.id, data.new_owner.into(), true, &**pool, ) .await?; let mut transaction = pool.begin().await?; // If the user is not a member of the project, we add them let new_owner = match new_owner { Some(new_owner) => new_owner, None => { let new_id = crate::database::models::ids::generate_team_member_id( &mut transaction, ) .await?; let member = DBTeamMember { id: new_id, team_id: project_item.inner.team_id, user_id: data.new_owner.into(), role: "Inherited Owner".to_string(), is_owner: false, permissions: ProjectPermissions::all(), organization_permissions: None, accepted: true, payouts_split: Decimal::ZERO, ordering: 0, }; member.insert(&mut transaction).await?; member } }; // Set the new owner to fit owner sqlx::query!( " UPDATE team_members SET is_owner = TRUE, accepted = TRUE, permissions = $2, organization_permissions = NULL, role = 'Inherited Owner' WHERE (id = $1) ", new_owner.id as database::models::ids::DBTeamMemberId, ProjectPermissions::all().bits() as i64 ) .execute(&mut *transaction) .await?; sqlx::query!( " UPDATE mods SET organization_id = NULL WHERE (id = $1) ", project_item.inner.id as database::models::ids::DBProjectId ) .execute(&mut *transaction) .await?; transaction.commit().await?; database::models::DBUser::clear_project_cache( &[current_user.id.into()], &redis, ) .await?; database::models::DBTeamMember::clear_cache( project_item.inner.team_id, &redis, ) .await?; database::models::DBProject::clear_cache( project_item.inner.id, project_item.inner.slug, None, &redis, ) .await?; } else { return Err(ApiError::CustomAuthentication( "You do not have permission to add projects to this organization!" .to_string(), )); } Ok(HttpResponse::Ok().finish()) } #[derive(Serialize, Deserialize)] pub struct Extension { pub ext: String, } #[allow(clippy::too_many_arguments)] pub async fn organization_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, Some(&[Scopes::ORGANIZATION_WRITE]), ) .await? .1; let string = info.into_inner().0; let organization_item = database::models::DBOrganization::get(&string, &**pool, &redis) .await? .ok_or_else(|| { ApiError::InvalidInput( "The specified organization does not exist!".to_string(), ) })?; if !user.role.is_mod() { let team_member = database::models::DBTeamMember::get_from_user_id( organization_item.team_id, user.id.into(), &**pool, ) .await .map_err(ApiError::Database)?; let permissions = OrganizationPermissions::get_permissions_by_role( &user.role, &team_member, ) .unwrap_or_default(); if !permissions.contains(OrganizationPermissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthentication( "You don't have permission to edit this organization's icon." .to_string(), )); } } delete_old_images( organization_item.icon_url, organization_item.raw_icon_url, &***file_host, ) .await?; let bytes = read_from_payload( &mut payload, 262144, "Icons must be smaller than 256KiB", ) .await?; let organization_id: OrganizationId = organization_item.id.into(); let upload_result = crate::util::img::upload_image_optimized( &format!("data/{organization_id}"), bytes.freeze(), &ext.ext, Some(96), Some(1.0), &***file_host, ) .await?; let mut transaction = pool.begin().await?; sqlx::query!( " UPDATE organizations SET icon_url = $1, raw_icon_url = $2, color = $3 WHERE (id = $4) ", upload_result.url, upload_result.raw_url, upload_result.color.map(|x| x as i32), organization_item.id as database::models::ids::DBOrganizationId, ) .execute(&mut *transaction) .await?; transaction.commit().await?; database::models::DBOrganization::clear_cache( organization_item.id, Some(organization_item.slug), &redis, ) .await?; Ok(HttpResponse::NoContent().body("")) } pub async fn delete_organization_icon( 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, Some(&[Scopes::ORGANIZATION_WRITE]), ) .await? .1; let string = info.into_inner().0; let organization_item = database::models::DBOrganization::get(&string, &**pool, &redis) .await? .ok_or_else(|| { ApiError::InvalidInput( "The specified organization does not exist!".to_string(), ) })?; if !user.role.is_mod() { let team_member = database::models::DBTeamMember::get_from_user_id( organization_item.team_id, user.id.into(), &**pool, ) .await .map_err(ApiError::Database)?; let permissions = OrganizationPermissions::get_permissions_by_role( &user.role, &team_member, ) .unwrap_or_default(); if !permissions.contains(OrganizationPermissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthentication( "You don't have permission to edit this organization's icon." .to_string(), )); } } delete_old_images( organization_item.icon_url, organization_item.raw_icon_url, &***file_host, ) .await?; let mut transaction = pool.begin().await?; sqlx::query!( " UPDATE organizations SET icon_url = NULL, raw_icon_url = NULL, color = NULL WHERE (id = $1) ", organization_item.id as database::models::ids::DBOrganizationId, ) .execute(&mut *transaction) .await?; transaction.commit().await?; database::models::DBOrganization::clear_cache( organization_item.id, Some(organization_item.slug), &redis, ) .await?; Ok(HttpResponse::NoContent().body("")) }