From 28b6bf8603febe674204c0f75025593bbacc16b6 Mon Sep 17 00:00:00 2001 From: Geometrically <18202329+Geometrically@users.noreply.github.com> Date: Fri, 27 Sep 2024 12:43:17 -0700 Subject: [PATCH] GDPR export route (#969) * GDPR export route * make users able to access --- ...a305e86949fefc8ba11f4b10992fa507d136c.json | 22 +++ ...ba9685fe16e242c0fa4a5eea54f12f9448e6b.json | 14 -- ...6b7b0b30291d595e5ae9f7e0fd38d347c3f74.json | 22 +++ ...125c242cb339db8914afcc3e2c90b0c307053.json | 22 +++ ...e6c3b401fcaa0bcc304b9caf18afc20a3e52b.json | 22 --- ...32c45947cfdc88be25ab04dff16eb289dcbcb.json | 14 ++ ...e54be0f2b753b5550d87d7b5ba3e17a57a202.json | 14 -- ...e70b2a89efe74bde1db9730ac478749dc1b66.json | 14 ++ src/auth/validate.rs | 48 +---- src/database/models/project_item.rs | 23 +-- src/database/models/user_item.rs | 44 ++++- src/database/models/version_item.rs | 3 +- src/models/v3/billing.rs | 17 ++ src/models/v3/threads.rs | 28 +-- src/models/v3/users.rs | 51 +++++ src/routes/internal/billing.rs | 11 +- src/routes/internal/flows.rs | 15 +- src/routes/internal/gdpr.rs | 177 ++++++++++++++++++ src/routes/internal/mod.rs | 4 +- src/routes/v3/users.rs | 15 +- 20 files changed, 417 insertions(+), 163 deletions(-) create mode 100644 .sqlx/query-505543e3e6aa69a9b9d4ee50938a305e86949fefc8ba11f4b10992fa507d136c.json delete mode 100644 .sqlx/query-5a13a79ebb1ab975f88b58e6deaba9685fe16e242c0fa4a5eea54f12f9448e6b.json create mode 100644 .sqlx/query-683e08f3b71aca0d004ebf83a9e6b7b0b30291d595e5ae9f7e0fd38d347c3f74.json create mode 100644 .sqlx/query-8a67a27f45a743f8679ec6021ef125c242cb339db8914afcc3e2c90b0c307053.json delete mode 100644 .sqlx/query-902d0803deb5eca7614f3a68ccae6c3b401fcaa0bcc304b9caf18afc20a3e52b.json create mode 100644 .sqlx/query-b3475fbc7a4d8b353964e6c5f7d32c45947cfdc88be25ab04dff16eb289dcbcb.json delete mode 100644 .sqlx/query-c3391aed338110205a170ba3032e54be0f2b753b5550d87d7b5ba3e17a57a202.json create mode 100644 .sqlx/query-f96967f8d0d7e4c7a9d424e075fe70b2a89efe74bde1db9730ac478749dc1b66.json create mode 100644 src/routes/internal/gdpr.rs diff --git a/.sqlx/query-505543e3e6aa69a9b9d4ee50938a305e86949fefc8ba11f4b10992fa507d136c.json b/.sqlx/query-505543e3e6aa69a9b9d4ee50938a305e86949fefc8ba11f4b10992fa507d136c.json new file mode 100644 index 00000000..a1f5ae56 --- /dev/null +++ b/.sqlx/query-505543e3e6aa69a9b9d4ee50938a305e86949fefc8ba11f4b10992fa507d136c.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT r.id FROM reports r\n WHERE r.user_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "505543e3e6aa69a9b9d4ee50938a305e86949fefc8ba11f4b10992fa507d136c" +} diff --git a/.sqlx/query-5a13a79ebb1ab975f88b58e6deaba9685fe16e242c0fa4a5eea54f12f9448e6b.json b/.sqlx/query-5a13a79ebb1ab975f88b58e6deaba9685fe16e242c0fa4a5eea54f12f9448e6b.json deleted file mode 100644 index 210bd04e..00000000 --- a/.sqlx/query-5a13a79ebb1ab975f88b58e6deaba9685fe16e242c0fa4a5eea54f12f9448e6b.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n DELETE FROM reports\n WHERE version_id = $1\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [] - }, - "hash": "5a13a79ebb1ab975f88b58e6deaba9685fe16e242c0fa4a5eea54f12f9448e6b" -} diff --git a/.sqlx/query-683e08f3b71aca0d004ebf83a9e6b7b0b30291d595e5ae9f7e0fd38d347c3f74.json b/.sqlx/query-683e08f3b71aca0d004ebf83a9e6b7b0b30291d595e5ae9f7e0fd38d347c3f74.json new file mode 100644 index 00000000..9c06e50b --- /dev/null +++ b/.sqlx/query-683e08f3b71aca0d004ebf83a9e6b7b0b30291d595e5ae9f7e0fd38d347c3f74.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id FROM uploaded_images WHERE owner_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "683e08f3b71aca0d004ebf83a9e6b7b0b30291d595e5ae9f7e0fd38d347c3f74" +} diff --git a/.sqlx/query-8a67a27f45a743f8679ec6021ef125c242cb339db8914afcc3e2c90b0c307053.json b/.sqlx/query-8a67a27f45a743f8679ec6021ef125c242cb339db8914afcc3e2c90b0c307053.json new file mode 100644 index 00000000..a149c1cd --- /dev/null +++ b/.sqlx/query-8a67a27f45a743f8679ec6021ef125c242cb339db8914afcc3e2c90b0c307053.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id FROM threads_messages WHERE author_id = $1 AND hide_identity = FALSE\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "8a67a27f45a743f8679ec6021ef125c242cb339db8914afcc3e2c90b0c307053" +} diff --git a/.sqlx/query-902d0803deb5eca7614f3a68ccae6c3b401fcaa0bcc304b9caf18afc20a3e52b.json b/.sqlx/query-902d0803deb5eca7614f3a68ccae6c3b401fcaa0bcc304b9caf18afc20a3e52b.json deleted file mode 100644 index 9af819ae..00000000 --- a/.sqlx/query-902d0803deb5eca7614f3a68ccae6c3b401fcaa0bcc304b9caf18afc20a3e52b.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT t.id\n FROM threads t\n INNER JOIN reports r ON t.report_id = r.id\n WHERE r.mod_id = $1 AND report_id IS NOT NULL \n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false - ] - }, - "hash": "902d0803deb5eca7614f3a68ccae6c3b401fcaa0bcc304b9caf18afc20a3e52b" -} diff --git a/.sqlx/query-b3475fbc7a4d8b353964e6c5f7d32c45947cfdc88be25ab04dff16eb289dcbcb.json b/.sqlx/query-b3475fbc7a4d8b353964e6c5f7d32c45947cfdc88be25ab04dff16eb289dcbcb.json new file mode 100644 index 00000000..2368cf64 --- /dev/null +++ b/.sqlx/query-b3475fbc7a4d8b353964e6c5f7d32c45947cfdc88be25ab04dff16eb289dcbcb.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE reports\n SET version_id = NULL\n WHERE version_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "b3475fbc7a4d8b353964e6c5f7d32c45947cfdc88be25ab04dff16eb289dcbcb" +} diff --git a/.sqlx/query-c3391aed338110205a170ba3032e54be0f2b753b5550d87d7b5ba3e17a57a202.json b/.sqlx/query-c3391aed338110205a170ba3032e54be0f2b753b5550d87d7b5ba3e17a57a202.json deleted file mode 100644 index dea1ce5a..00000000 --- a/.sqlx/query-c3391aed338110205a170ba3032e54be0f2b753b5550d87d7b5ba3e17a57a202.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n DELETE FROM reports\n WHERE mod_id = $1\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [] - }, - "hash": "c3391aed338110205a170ba3032e54be0f2b753b5550d87d7b5ba3e17a57a202" -} diff --git a/.sqlx/query-f96967f8d0d7e4c7a9d424e075fe70b2a89efe74bde1db9730ac478749dc1b66.json b/.sqlx/query-f96967f8d0d7e4c7a9d424e075fe70b2a89efe74bde1db9730ac478749dc1b66.json new file mode 100644 index 00000000..0841edec --- /dev/null +++ b/.sqlx/query-f96967f8d0d7e4c7a9d424e075fe70b2a89efe74bde1db9730ac478749dc1b66.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE reports\n SET mod_id = NULL\n WHERE mod_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "f96967f8d0d7e4c7a9d424e075fe70b2a89efe74bde1db9730ac478749dc1b66" +} diff --git a/src/auth/validate.rs b/src/auth/validate.rs index 2037726e..b70a8703 100644 --- a/src/auth/validate.rs +++ b/src/auth/validate.rs @@ -3,13 +3,12 @@ use crate::auth::AuthenticationError; use crate::database::models::user_item; use crate::database::redis::RedisPool; use crate::models::pats::Scopes; -use crate::models::users::{Role, User, UserId, UserPayoutData}; +use crate::models::users::User; use crate::queue::session::AuthQueue; use crate::routes::internal::session::get_session_metadata; use actix_web::http::header::{HeaderValue, AUTHORIZATION}; use actix_web::HttpRequest; use chrono::Utc; -use rust_decimal::Decimal; pub async fn get_user_from_headers<'a, E>( req: &HttpRequest, @@ -26,51 +25,8 @@ where get_user_record_from_bearer_token(req, None, executor, redis, session_queue) .await? .ok_or_else(|| AuthenticationError::InvalidCredentials)?; - let mut auth_providers = Vec::new(); - if db_user.github_id.is_some() { - auth_providers.push(AuthProvider::GitHub) - } - if db_user.gitlab_id.is_some() { - auth_providers.push(AuthProvider::GitLab) - } - if db_user.discord_id.is_some() { - auth_providers.push(AuthProvider::Discord) - } - if db_user.google_id.is_some() { - auth_providers.push(AuthProvider::Google) - } - if db_user.microsoft_id.is_some() { - auth_providers.push(AuthProvider::Microsoft) - } - if db_user.steam_id.is_some() { - auth_providers.push(AuthProvider::Steam) - } - if db_user.paypal_id.is_some() { - auth_providers.push(AuthProvider::PayPal) - } - let user = User { - id: UserId::from(db_user.id), - username: db_user.username, - email: db_user.email, - email_verified: Some(db_user.email_verified), - avatar_url: db_user.avatar_url, - bio: db_user.bio, - created: db_user.created, - role: Role::from_string(&db_user.role), - badges: db_user.badges, - auth_providers: Some(auth_providers), - has_password: Some(db_user.password.is_some()), - has_totp: Some(db_user.totp_secret.is_some()), - github_id: None, - payout_data: Some(UserPayoutData { - paypal_address: db_user.paypal_email, - paypal_country: db_user.paypal_country, - venmo_handle: db_user.venmo_handle, - balance: Decimal::ZERO, - }), - stripe_customer_id: db_user.stripe_customer_id, - }; + let user = User::from_full(db_user); if let Some(required_scopes) = required_scopes { for scope in required_scopes { diff --git a/src/database/models/project_item.rs b/src/database/models/project_item.rs index 453c22aa..2fb153c7 100644 --- a/src/database/models/project_item.rs +++ b/src/database/models/project_item.rs @@ -355,31 +355,12 @@ impl Project { .execute(&mut **transaction) .await?; - // Notably joins with report id and not thread.mod_id directly, as - // this is set to null for threads that are reports. - let report_threads = sqlx::query!( - " - SELECT t.id - FROM threads t - INNER JOIN reports r ON t.report_id = r.id - WHERE r.mod_id = $1 AND report_id IS NOT NULL - ", - id as ProjectId, - ) - .fetch(&mut **transaction) - .map_ok(|x| ThreadId(x.id)) - .try_collect::>() - .await?; - - for thread_id in report_threads { - models::Thread::remove_full(thread_id, transaction).await?; - } - models::Thread::remove_full(project.thread_id, transaction).await?; sqlx::query!( " - DELETE FROM reports + UPDATE reports + SET mod_id = NULL WHERE mod_id = $1 ", id as ProjectId, diff --git a/src/database/models/user_item.rs b/src/database/models/user_item.rs index 8a655d1a..754bc3e3 100644 --- a/src/database/models/user_item.rs +++ b/src/database/models/user_item.rs @@ -1,5 +1,5 @@ use super::ids::{ProjectId, UserId}; -use super::{CollectionId, ThreadId}; +use super::{CollectionId, ReportId, ThreadId}; use crate::database::models; use crate::database::models::{DatabaseError, OrganizationId}; use crate::database::redis::RedisPool; @@ -323,6 +323,48 @@ impl User { Ok(projects) } + pub async fn get_follows<'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 mf.mod_id FROM mod_follows mf + WHERE mf.follower_id = $1 + ", + user_id as UserId, + ) + .fetch(exec) + .map_ok(|m| ProjectId(m.mod_id)) + .try_collect::>() + .await?; + + Ok(projects) + } + + pub async fn get_reports<'a, E>(user_id: UserId, exec: E) -> Result, sqlx::Error> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + { + use futures::stream::TryStreamExt; + + let reports = sqlx::query!( + " + SELECT r.id FROM reports r + WHERE r.user_id = $1 + ", + user_id as UserId, + ) + .fetch(exec) + .map_ok(|m| ReportId(m.id)) + .try_collect::>() + .await?; + + Ok(reports) + } + pub async fn get_backup_codes<'a, E>( user_id: UserId, exec: E, diff --git a/src/database/models/version_item.rs b/src/database/models/version_item.rs index 5d654ab2..e9902247 100644 --- a/src/database/models/version_item.rs +++ b/src/database/models/version_item.rs @@ -335,7 +335,8 @@ impl Version { sqlx::query!( " - DELETE FROM reports + UPDATE reports + SET version_id = NULL WHERE version_id = $1 ", id as VersionId, diff --git a/src/models/v3/billing.rs b/src/models/v3/billing.rs index c90c6a7f..c78583da 100644 --- a/src/models/v3/billing.rs +++ b/src/models/v3/billing.rs @@ -88,6 +88,23 @@ pub struct UserSubscription { pub last_charge: Option>, } +impl From + for UserSubscription +{ + fn from(x: crate::database::models::user_subscription_item::UserSubscriptionItem) -> Self { + Self { + id: x.id.into(), + user_id: x.user_id.into(), + price_id: x.price_id.into(), + interval: x.interval, + status: x.status, + created: x.created, + expires: x.expires, + last_charge: x.last_charge, + } + } +} + #[derive(Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "kebab-case")] pub enum SubscriptionStatus { diff --git a/src/models/v3/threads.rs b/src/models/v3/threads.rs index 2a7436ab..e36fb147 100644 --- a/src/models/v3/threads.rs +++ b/src/models/v3/threads.rs @@ -113,19 +113,25 @@ impl Thread { true } }) - .map(|x| ThreadMessage { - id: x.id.into(), - author_id: if x.hide_identity && !user.role.is_mod() { - None - } else { - x.author_id.map(|x| x.into()) - }, - body: x.body, - created: x.created, - hide_identity: x.hide_identity, - }) + .map(|x| ThreadMessage::from(x, user)) .collect(), members: users, } } } + +impl ThreadMessage { + pub fn from(data: crate::database::models::ThreadMessage, user: &User) -> Self { + Self { + id: data.id.into(), + author_id: if data.hide_identity && !user.role.is_mod() { + None + } else { + data.author_id.map(|x| x.into()) + }, + body: data.body, + created: data.created, + hide_identity: data.hide_identity, + } + } +} diff --git a/src/models/v3/users.rs b/src/models/v3/users.rs index 790d14fe..a69ee9d9 100644 --- a/src/models/v3/users.rs +++ b/src/models/v3/users.rs @@ -89,6 +89,57 @@ impl From for User { } } +impl User { + pub fn from_full(db_user: DBUser) -> Self { + let mut auth_providers = Vec::new(); + + if db_user.github_id.is_some() { + auth_providers.push(AuthProvider::GitHub) + } + if db_user.gitlab_id.is_some() { + auth_providers.push(AuthProvider::GitLab) + } + if db_user.discord_id.is_some() { + auth_providers.push(AuthProvider::Discord) + } + if db_user.google_id.is_some() { + auth_providers.push(AuthProvider::Google) + } + if db_user.microsoft_id.is_some() { + auth_providers.push(AuthProvider::Microsoft) + } + if db_user.steam_id.is_some() { + auth_providers.push(AuthProvider::Steam) + } + if db_user.paypal_id.is_some() { + auth_providers.push(AuthProvider::PayPal) + } + + Self { + id: UserId::from(db_user.id), + username: db_user.username, + email: db_user.email, + email_verified: Some(db_user.email_verified), + avatar_url: db_user.avatar_url, + bio: db_user.bio, + created: db_user.created, + role: Role::from_string(&db_user.role), + badges: db_user.badges, + auth_providers: Some(auth_providers), + has_password: Some(db_user.password.is_some()), + has_totp: Some(db_user.totp_secret.is_some()), + github_id: None, + payout_data: Some(UserPayoutData { + paypal_address: db_user.paypal_email, + paypal_country: db_user.paypal_country, + venmo_handle: db_user.venmo_handle, + balance: Decimal::ZERO, + }), + stripe_customer_id: db_user.stripe_customer_id, + } + } +} + #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] #[serde(rename_all = "lowercase")] pub enum Role { diff --git a/src/routes/internal/billing.rs b/src/routes/internal/billing.rs index 877a5da8..a63442dc 100644 --- a/src/routes/internal/billing.rs +++ b/src/routes/internal/billing.rs @@ -95,16 +95,7 @@ pub async fn subscriptions( user_subscription_item::UserSubscriptionItem::get_all_user(user.id.into(), &**pool) .await? .into_iter() - .map(|x| UserSubscription { - id: x.id.into(), - user_id: x.user_id.into(), - price_id: x.price_id.into(), - interval: x.interval, - status: x.status, - created: x.created, - expires: x.expires, - last_charge: x.last_charge, - }) + .map(UserSubscription::from) .collect::>(); Ok(HttpResponse::Ok().json(subscriptions)) diff --git a/src/routes/internal/flows.rs b/src/routes/internal/flows.rs index c45fc96a..094e40ff 100644 --- a/src/routes/internal/flows.rs +++ b/src/routes/internal/flows.rs @@ -1641,14 +1641,13 @@ async fn validate_2fa_code( } if input == token { - conn - .set( - TOTP_NAMESPACE, - &format!("{}-{}", token, user_id.0), - "", - Some(60), - ) - .await?; + conn.set( + TOTP_NAMESPACE, + &format!("{}-{}", token, user_id.0), + "", + Some(60), + ) + .await?; Ok(true) } else if allow_backup { diff --git a/src/routes/internal/gdpr.rs b/src/routes/internal/gdpr.rs new file mode 100644 index 00000000..8d7f51da --- /dev/null +++ b/src/routes/internal/gdpr.rs @@ -0,0 +1,177 @@ +use crate::auth::get_user_from_headers; +use crate::database::redis::RedisPool; +use crate::models::pats::Scopes; +use crate::queue::session::AuthQueue; +use crate::routes::ApiError; +use actix_web::{post, web, HttpRequest, HttpResponse}; +use sqlx::PgPool; + +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service(web::scope("gdpr").service(export)); +} + +#[post("/export")] +pub async fn export( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, +) -> Result { + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &*session_queue, + Some(&[Scopes::SESSION_ACCESS]), + ) + .await? + .1; + + let user_id = user.id.into(); + + let collection_ids = crate::database::models::User::get_collections(user_id, &**pool).await?; + let collections = + crate::database::models::Collection::get_many(&collection_ids, &**pool, &redis) + .await? + .into_iter() + .map(|x| crate::models::collections::Collection::from(x)) + .collect::>(); + + let follows = crate::database::models::User::get_follows(user_id, &**pool) + .await? + .into_iter() + .map(|x| crate::models::ids::ProjectId::from(x)) + .collect::>(); + + let projects = crate::database::models::User::get_projects(user_id, &**pool, &redis) + .await? + .into_iter() + .map(|x| crate::models::ids::ProjectId::from(x)) + .collect::>(); + + let org_ids = crate::database::models::User::get_organizations(user_id, &**pool).await?; + let orgs = crate::database::models::organization_item::Organization::get_many_ids( + &org_ids, &**pool, &redis, + ) + .await? + .into_iter() + // TODO: add team members + .map(|x| crate::models::organizations::Organization::from(x, vec![])) + .collect::>(); + + let notifs = crate::database::models::notification_item::Notification::get_many_user( + user_id, &**pool, &redis, + ) + .await? + .into_iter() + .map(|x| crate::models::notifications::Notification::from(x)) + .collect::>(); + + let oauth_clients = + crate::database::models::oauth_client_item::OAuthClient::get_all_user_clients( + user_id, &**pool, + ) + .await? + .into_iter() + .map(|x| crate::models::oauth_clients::OAuthClient::from(x)) + .collect::>(); + + let oauth_authorizations = crate::database::models::oauth_client_authorization_item::OAuthClientAuthorization::get_all_for_user( + user_id, &**pool, + ) + .await? + .into_iter() + .map(|x| crate::models::oauth_clients::OAuthClientAuthorization::from(x)) + .collect::>(); + + let pat_ids = crate::database::models::pat_item::PersonalAccessToken::get_user_pats( + user_id, &**pool, &redis, + ) + .await?; + let pats = crate::database::models::pat_item::PersonalAccessToken::get_many_ids( + &pat_ids, &**pool, &redis, + ) + .await? + .into_iter() + .map(|x| crate::models::pats::PersonalAccessToken::from(x, false)) + .collect::>(); + + let payout_ids = + crate::database::models::payout_item::Payout::get_all_for_user(user_id, &**pool).await?; + + let payouts = crate::database::models::payout_item::Payout::get_many(&payout_ids, &**pool) + .await? + .into_iter() + .map(|x| crate::models::payouts::Payout::from(x)) + .collect::>(); + + let report_ids = + crate::database::models::user_item::User::get_reports(user_id, &**pool).await?; + let reports = crate::database::models::report_item::Report::get_many(&report_ids, &**pool) + .await? + .into_iter() + .map(|x| crate::models::reports::Report::from(x)) + .collect::>(); + + let message_ids = sqlx::query!( + " + SELECT id FROM threads_messages WHERE author_id = $1 AND hide_identity = FALSE + ", + user_id.0 + ) + .fetch_all(pool.as_ref()) + .await? + .into_iter() + .map(|x| crate::database::models::ids::ThreadMessageId(x.id)) + .collect::>(); + + let messages = + crate::database::models::thread_item::ThreadMessage::get_many(&message_ids, &**pool) + .await? + .into_iter() + .map(|x| crate::models::threads::ThreadMessage::from(x, &user)) + .collect::>(); + + let uploaded_images_ids = sqlx::query!( + "SELECT id FROM uploaded_images WHERE owner_id = $1", + user_id.0 + ) + .fetch_all(pool.as_ref()) + .await? + .into_iter() + .map(|x| crate::database::models::ids::ImageId(x.id)) + .collect::>(); + + let uploaded_images = + crate::database::models::image_item::Image::get_many(&uploaded_images_ids, &**pool, &redis) + .await? + .into_iter() + .map(|x| crate::models::images::Image::from(x)) + .collect::>(); + + let subscriptions = + crate::database::models::user_subscription_item::UserSubscriptionItem::get_all_user( + user_id, &**pool, + ) + .await? + .into_iter() + .map(|x| crate::models::billing::UserSubscription::from(x)) + .collect::>(); + + Ok(HttpResponse::Ok().json(serde_json::json!({ + "user": user, + "collections": collections, + "follows": follows, + "projects": projects, + "orgs": orgs, + "notifs": notifs, + "oauth_clients": oauth_clients, + "oauth_authorizations": oauth_authorizations, + "pats": pats, + "payouts": payouts, + "reports": reports, + "messages": messages, + "uploaded_images": uploaded_images, + "subscriptions": subscriptions, + }))) +} diff --git a/src/routes/internal/mod.rs b/src/routes/internal/mod.rs index ddd78077..0472899d 100644 --- a/src/routes/internal/mod.rs +++ b/src/routes/internal/mod.rs @@ -1,6 +1,7 @@ pub(crate) mod admin; pub mod billing; pub mod flows; +pub mod gdpr; pub mod moderation; pub mod pats; pub mod session; @@ -19,6 +20,7 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) { .configure(flows::config) .configure(pats::config) .configure(moderation::config) - .configure(billing::config), + .configure(billing::config) + .configure(gdpr::config), ); } diff --git a/src/routes/v3/users.rs b/src/routes/v3/users.rs index 464b6cfa..6eba46d3 100644 --- a/src/routes/v3/users.rs +++ b/src/routes/v3/users.rs @@ -571,20 +571,7 @@ pub async fn user_follows( )); } - use futures::TryStreamExt; - - let project_ids = sqlx::query!( - " - SELECT mf.mod_id FROM mod_follows mf - WHERE mf.follower_id = $1 - ", - id as crate::database::models::ids::UserId, - ) - .fetch(&**pool) - .map_ok(|m| crate::database::models::ProjectId(m.mod_id)) - .try_collect::>() - .await?; - + let project_ids = User::get_follows(id, &**pool).await?; let projects: Vec<_> = crate::database::Project::get_many_ids(&project_ids, &**pool, &redis) .await?