From 39c0951ecb655c3a66c0b551fdf092fa6e4126b0 Mon Sep 17 00:00:00 2001 From: Sychic <47618543+Sychic@users.noreply.github.com> Date: Mon, 17 Aug 2026 18:09:50 -0400 Subject: [PATCH] feat(labrinth): check privacy route (#7195) * feat(labrinth): check privacy endpoint * refactor(labrinth): remove duplicated code --- apps/labrinth/src/models/v3/preferences.rs | 10 +++ apps/labrinth/src/routes/internal/mod.rs | 3 + apps/labrinth/src/routes/internal/privacy.rs | 89 ++++++++++++++++++++ apps/labrinth/src/routes/v3/users.rs | 18 +--- 4 files changed, 105 insertions(+), 15 deletions(-) create mode 100644 apps/labrinth/src/routes/internal/privacy.rs diff --git a/apps/labrinth/src/models/v3/preferences.rs b/apps/labrinth/src/models/v3/preferences.rs index 9afbde2ed8..1deedfffc2 100644 --- a/apps/labrinth/src/models/v3/preferences.rs +++ b/apps/labrinth/src/models/v3/preferences.rs @@ -16,6 +16,16 @@ pub struct UserPreferences { pub social: SocialPreferences, } +impl UserPreferences { + pub fn resolve(overrides: Option) -> Self { + let mut preferences = Self::default(); + if let Some(overrides) = overrides { + overrides.apply_to(&mut preferences); + } + preferences + } +} + #[derive( Debug, Serialize, Deserialize, ToSchema, Default, PartialEq, Component, )] diff --git a/apps/labrinth/src/routes/internal/mod.rs b/apps/labrinth/src/routes/internal/mod.rs index 1ad10523b1..d7cfa39d96 100644 --- a/apps/labrinth/src/routes/internal/mod.rs +++ b/apps/labrinth/src/routes/internal/mod.rs @@ -15,6 +15,7 @@ pub mod medal; pub mod moderation; pub mod mural; pub mod pats; +pub mod privacy; pub mod search; pub mod server_ping; pub mod session; @@ -32,6 +33,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { .wrap(default_cors()) .configure(admin::config) .configure(blocked_users::config) + .configure(privacy::config) .configure(session::config) .configure(flows::config) .configure(pats::config) @@ -68,6 +70,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { paths( admin::count_download, blocked_users::block_status, + privacy::invite_privacy_status, admin::force_reindex, admin::force_reindex_project, session::list, diff --git a/apps/labrinth/src/routes/internal/privacy.rs b/apps/labrinth/src/routes/internal/privacy.rs new file mode 100644 index 0000000000..27a2cbd984 --- /dev/null +++ b/apps/labrinth/src/routes/internal/privacy.rs @@ -0,0 +1,89 @@ +use crate::database::PgPool; +use crate::database::models::blocked_user_item::DBBlockedUser; +use crate::database::models::friend_item::DBFriend; +use crate::database::models::user_item::DBUser; +use crate::database::models::user_preferences_item::DBUserPreferences; +use crate::models::v3::preferences::{InvitePrivacy, UserPreferences}; +use crate::routes::ApiError; +use crate::util::error::Context as _; +use crate::util::guards::admin_key_guard; +use actix_web::{get, web}; +use serde::Serialize; +use xredis::RedisPool; + +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service(invite_privacy_status); +} + +#[derive(Serialize, utoipa::ToSchema)] +pub struct InvitePrivacyStatus { + pub allowed: bool, +} + +#[derive(strum::EnumString)] +#[strum(serialize_all = "snake_case")] +enum InviteType { + SharedInstances, + HostingAccess, +} + +/// Check whether `user_id` may send `target_id` an invite of the given type. +#[utoipa::path(tag = "privacy", responses((status = OK, body = InvitePrivacyStatus)))] +#[get( + "/privacy/{invite_type}/{user_id}/{target_id}", + guard = "admin_key_guard" +)] +pub async fn invite_privacy_status( + info: web::Path<(String, String, String)>, + pool: web::Data, + redis: web::Data, +) -> Result, ApiError> { + let (invite_type, user_id, target_id) = info.into_inner(); + + let invite_type = invite_type + .parse::() + .wrap_request_err("invalid invite_type")?; + let (user, target) = futures::try_join!( + DBUser::get(&user_id, &**pool, &redis), + DBUser::get(&target_id, &**pool, &redis), + ) + .wrap_internal_err("resolving user ids")?; + + let user_id = user.wrap_not_found_err("user not found")?.id; + let target_id = target.wrap_not_found_err("target not found")?.id; + + let blocked = DBBlockedUser::is_blocked(target_id, user_id, &**pool) + .await + .wrap_internal_err("checking whether user is blocked")?; + + let allowed = if blocked { + false + } else { + let overrides = DBUserPreferences::get(target_id, &**pool) + .await + .wrap_internal_err("fetching user preferences")?; + let preferences = UserPreferences::resolve(overrides); + + let privacy = match invite_type { + InviteType::SharedInstances => { + preferences.social.shared_instances_privacy + } + InviteType::HostingAccess => { + preferences.social.hosting_access_privacy + } + }; + + match privacy { + InvitePrivacy::Everyone => true, + InvitePrivacy::None => false, + InvitePrivacy::Friends => { + DBFriend::get_friend(user_id, target_id, &**pool) + .await + .wrap_internal_err("checking friendship status")? + .is_some_and(|friend| friend.accepted) + } + } + }; + + Ok(web::Json(InvitePrivacyStatus { allowed })) +} diff --git a/apps/labrinth/src/routes/v3/users.rs b/apps/labrinth/src/routes/v3/users.rs index 840fb9e5fd..3ec7205286 100644 --- a/apps/labrinth/src/routes/v3/users.rs +++ b/apps/labrinth/src/routes/v3/users.rs @@ -409,15 +409,7 @@ pub async fn get_user_preferences( .await .wrap_internal_err("failed to fetch user preferences")?; - let preferences = preference_overrides - .map(|overrides| { - let mut preferences = UserPreferences::default(); - overrides.apply_to(&mut preferences); - preferences - }) - .unwrap_or_default(); - - Ok(web::Json(preferences)) + Ok(web::Json(UserPreferences::resolve(preference_overrides))) } #[utoipa::path( @@ -466,10 +458,7 @@ pub async fn edit_user_preferences( .await .wrap_internal_err("failed to fetch user preferences")?; - let mut preferences = UserPreferences::default(); - if let Some(stored) = stored { - stored.apply_to(&mut preferences); - } + let mut preferences = UserPreferences::resolve(stored); body.into_inner().apply_to(&mut preferences); let overrides = preferences.into_diff_from(&UserPreferences::default()); @@ -481,8 +470,7 @@ pub async fn edit_user_preferences( .await .wrap_internal_err("committing database transaction")?; - let mut preferences = UserPreferences::default(); - overrides.apply_to(&mut preferences); + let preferences = UserPreferences::resolve(Some(overrides)); Ok(web::Json(preferences)) }