From 2d92b08404c15cff5335f752bca6211edb417409 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Fri, 1 Dec 2023 10:02:11 -0800 Subject: [PATCH] V2 removal and _internal rerouting (#770) * deleteed v3 exclusive routes * moved routes around * fixed linkage that movement broke * initial merge errors * fixes --- src/auth/mod.rs | 19 +- src/auth/validate.rs | 4 +- src/database/models/flow_item.rs | 3 +- src/lib.rs | 1 + src/models/v3/users.rs | 3 +- src/queue/session.rs | 2 +- src/routes/{v3 => internal}/admin.rs | 0 src/{auth => routes/internal}/flows.rs | 25 +-- src/routes/internal/mod.rs | 21 ++ src/{auth => routes/internal}/pats.rs | 0 src/{auth => routes/internal}/session.rs | 0 src/routes/mod.rs | 1 + src/routes/v2/analytics_get.rs | 267 ----------------------- src/routes/v2/collections.rs | 191 ---------------- src/routes/v2/images.rs | 59 ----- src/routes/v2/mod.rs | 14 +- src/routes/v2/organizations.rs | 265 ---------------------- src/routes/v2/teams.rs | 11 - src/routes/v2/users.rs | 24 -- src/routes/v3/mod.rs | 7 - tests/common/api_v3/mod.rs | 2 +- tests/common/api_v3/oauth.rs | 8 +- tests/common/api_v3/oauth_clients.rs | 15 +- tests/pats.rs | 32 +-- tests/scopes.rs | 18 +- 25 files changed, 93 insertions(+), 899 deletions(-) rename src/routes/{v3 => internal}/admin.rs (100%) rename src/{auth => routes/internal}/flows.rs (99%) create mode 100644 src/routes/internal/mod.rs rename src/{auth => routes/internal}/pats.rs (100%) rename src/{auth => routes/internal}/session.rs (100%) delete mode 100644 src/routes/v2/analytics_get.rs delete mode 100644 src/routes/v2/collections.rs delete mode 100644 src/routes/v2/images.rs delete mode 100644 src/routes/v2/organizations.rs diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 9c65c914b..4a8c8f6b5 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -1,14 +1,12 @@ pub mod checks; pub mod email; -pub mod flows; pub mod oauth; -pub mod pats; -pub mod session; -mod templates; +pub mod templates; pub mod validate; pub use checks::{ filter_authorized_projects, filter_authorized_versions, is_authorized, is_authorized_version, }; +use serde::{Deserialize, Serialize}; // pub use pat::{generate_pat, PersonalAccessToken}; pub use validate::{check_is_moderator_from_headers, get_user_from_headers}; @@ -98,3 +96,16 @@ impl AuthenticationError { } } } + +#[derive(Serialize, Deserialize, Default, Eq, PartialEq, Clone, Copy, Debug)] +#[serde(rename_all = "lowercase")] +pub enum AuthProvider { + #[default] + GitHub, + Discord, + Microsoft, + GitLab, + Google, + Steam, + PayPal, +} diff --git a/src/auth/validate.rs b/src/auth/validate.rs index 475c59560..4bfd404b0 100644 --- a/src/auth/validate.rs +++ b/src/auth/validate.rs @@ -1,11 +1,11 @@ -use crate::auth::flows::AuthProvider; -use crate::auth::session::get_session_metadata; +use super::AuthProvider; 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::queue::session::AuthQueue; +use crate::routes::internal::session::get_session_metadata; use actix_web::HttpRequest; use chrono::Utc; use reqwest::header::{HeaderValue, AUTHORIZATION}; diff --git a/src/database/models/flow_item.rs b/src/database/models/flow_item.rs index 22d308959..fab7e140d 100644 --- a/src/database/models/flow_item.rs +++ b/src/database/models/flow_item.rs @@ -1,8 +1,9 @@ use super::ids::*; use crate::auth::oauth::uris::OAuthRedirectUris; +use crate::auth::AuthProvider; use crate::database::models::DatabaseError; use crate::database::redis::RedisPool; -use crate::{auth::flows::AuthProvider, models::pats::Scopes}; +use crate::models::pats::Scopes; use chrono::Duration; use rand::distributions::Alphanumeric; use rand::Rng; diff --git a/src/lib.rs b/src/lib.rs index 13eef1679..0f41dca23 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -276,6 +276,7 @@ pub fn app_config(cfg: &mut web::ServiceConfig, labrinth_config: LabrinthConfig) .app_data(labrinth_config.active_sockets.clone()) .configure(routes::v2::config) .configure(routes::v3::config) + .configure(routes::internal::config) .configure(routes::root_config) .default_service(web::get().wrap(default_cors()).to(routes::not_found)); } diff --git a/src/models/v3/users.rs b/src/models/v3/users.rs index 0eefc30af..e810e2618 100644 --- a/src/models/v3/users.rs +++ b/src/models/v3/users.rs @@ -1,6 +1,5 @@ use super::ids::Base62Id; -use crate::auth::flows::AuthProvider; -use crate::bitflags_serde_impl; +use crate::{auth::AuthProvider, bitflags_serde_impl}; use chrono::{DateTime, Utc}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; diff --git a/src/queue/session.rs b/src/queue/session.rs index ee4568a47..e99a75075 100644 --- a/src/queue/session.rs +++ b/src/queue/session.rs @@ -1,8 +1,8 @@ -use crate::auth::session::SessionMetadata; use crate::database::models::pat_item::PersonalAccessToken; use crate::database::models::session_item::Session; use crate::database::models::{DatabaseError, OAuthAccessTokenId, PatId, SessionId, UserId}; use crate::database::redis::RedisPool; +use crate::routes::internal::session::SessionMetadata; use chrono::Utc; use itertools::Itertools; use sqlx::PgPool; diff --git a/src/routes/v3/admin.rs b/src/routes/internal/admin.rs similarity index 100% rename from src/routes/v3/admin.rs rename to src/routes/internal/admin.rs diff --git a/src/auth/flows.rs b/src/routes/internal/flows.rs similarity index 99% rename from src/auth/flows.rs rename to src/routes/internal/flows.rs index 75788bf6f..b97e332ce 100644 --- a/src/auth/flows.rs +++ b/src/routes/internal/flows.rs @@ -1,7 +1,6 @@ use crate::auth::email::send_email; -use crate::auth::session::issue_session; use crate::auth::validate::get_user_record_from_bearer_token; -use crate::auth::{get_user_from_headers, AuthenticationError}; +use crate::auth::{get_user_from_headers, AuthProvider, AuthenticationError}; use crate::database::models::flow_item::Flow; use crate::database::redis::RedisPool; use crate::file_hosting::FileHost; @@ -11,6 +10,7 @@ use crate::models::pats::Scopes; use crate::models::users::{Badges, Role}; use crate::queue::session::AuthQueue; use crate::queue::socket::ActiveSockets; +use crate::routes::internal::session::issue_session; use crate::routes::ApiError; use crate::util::captcha::check_turnstile_captcha; use crate::util::env::parse_strings_from_var; @@ -56,19 +56,6 @@ pub fn config(cfg: &mut ServiceConfig) { ); } -#[derive(Serialize, Deserialize, Default, Eq, PartialEq, Clone, Copy, Debug)] -#[serde(rename_all = "lowercase")] -pub enum AuthProvider { - #[default] - GitHub, - Discord, - Microsoft, - GitLab, - Google, - Steam, - PayPal, -} - #[derive(Debug)] pub struct TempUser { pub id: String, @@ -1146,7 +1133,7 @@ pub async fn auth_callback( client: Data, file_host: Data>, redis: Data, -) -> Result { +) -> Result { let state_string = query .get("state") .ok_or_else(|| AuthenticationError::InvalidCredentials)? @@ -1263,7 +1250,7 @@ pub async fn auth_callback( let _ = ws_conn.close(None).await; - return Ok(super::templates::Success { + return Ok(crate::auth::templates::Success { icon: user.avatar_url.as_deref().unwrap_or("https://cdn-raw.modrinth.com/placeholder.svg"), name: &user.username, }.render()); @@ -1322,7 +1309,7 @@ pub async fn auth_callback( .await.map_err(|_| AuthenticationError::SocketError)?; let _ = ws_conn.close(None).await; - return Ok(super::templates::Success { + return Ok(crate::auth::templates::Success { icon: user.avatar_url.as_deref().unwrap_or("https://cdn-raw.modrinth.com/placeholder.svg"), name: &user.username, }.render()); @@ -2356,7 +2343,7 @@ fn send_email_verify( email: String, flow: String, opener: &str, -) -> Result<(), super::email::MailError> { +) -> Result<(), crate::auth::email::MailError> { send_email( email, "Verify your email", diff --git a/src/routes/internal/mod.rs b/src/routes/internal/mod.rs new file mode 100644 index 000000000..358876c4c --- /dev/null +++ b/src/routes/internal/mod.rs @@ -0,0 +1,21 @@ +mod admin; +pub mod flows; +pub mod pats; +pub mod session; + +use super::v3::oauth_clients; +pub use super::ApiError; +use crate::util::cors::default_cors; + +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service( + actix_web::web::scope("_internal") + .wrap(default_cors()) + .configure(admin::config) + // TODO: write tests that catch these + .configure(oauth_clients::config) + .configure(session::config) + .configure(flows::config) + .configure(pats::config), + ); +} diff --git a/src/auth/pats.rs b/src/routes/internal/pats.rs similarity index 100% rename from src/auth/pats.rs rename to src/routes/internal/pats.rs diff --git a/src/auth/session.rs b/src/routes/internal/session.rs similarity index 100% rename from src/auth/session.rs rename to src/routes/internal/session.rs diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 6e381d510..c68ccfbbb 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -8,6 +8,7 @@ use actix_web::http::StatusCode; use actix_web::{web, HttpResponse}; use futures::FutureExt; +pub mod internal; pub mod v2; pub mod v3; diff --git a/src/routes/v2/analytics_get.rs b/src/routes/v2/analytics_get.rs deleted file mode 100644 index 8dd6532a9..000000000 --- a/src/routes/v2/analytics_get.rs +++ /dev/null @@ -1,267 +0,0 @@ -use super::ApiError; -use crate::database::redis::RedisPool; -use crate::routes::v3; -use crate::{models::ids::VersionId, queue::session::AuthQueue}; -use actix_web::{get, web, HttpRequest, HttpResponse}; -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use sqlx::PgPool; -use std::collections::HashMap; - -pub fn config(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("analytics") - .service(playtimes_get) - .service(views_get) - .service(downloads_get) - .service(revenue_get) - .service(countries_downloads_get) - .service(countries_views_get), - ); -} - -/// The json data to be passed to fetch analytic data -/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out. -/// start_date and end_date are optional, and default to two weeks ago, and the maximum date respectively -/// start_date and end_date are inclusive -/// resolution_minutes is optional. This refers to the window by which we are looking (every day, every minute, etc) and defaults to 1440 (1 day) -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct GetData { - // only one of project_ids or version_ids should be used - // if neither are provided, all projects the user has access to will be used - pub project_ids: Option, - pub version_ids: Option, - - pub start_date: Option>, // defaults to 2 weeks ago - pub end_date: Option>, // defaults to now - - pub resolution_minutes: Option, // defaults to 1 day. Ignored in routes that do not aggregate over a resolution (eg: /countries) -} - -/// Get playtime data for a set of projects or versions -/// Data is returned as a hashmap of project/version ids to a hashmap of days to playtime data -/// eg: -/// { -/// "4N1tEhnO": { -/// "20230824": 23 -/// } -///} -/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out. -#[derive(Serialize, Deserialize, Clone)] -pub struct FetchedPlaytime { - pub time: u64, - pub total_seconds: u64, - pub loader_seconds: HashMap, - pub game_version_seconds: HashMap, - pub parent_seconds: HashMap, -} -#[get("playtime")] -pub async fn playtimes_get( - req: HttpRequest, - clickhouse: web::Data, - data: web::Query, - session_queue: web::Data, - pool: web::Data, - redis: web::Data, -) -> Result { - let data = data.into_inner(); - v3::analytics_get::playtimes_get( - req, - clickhouse, - web::Query(v3::analytics_get::GetData { - project_ids: data.project_ids, - version_ids: data.version_ids, - start_date: data.start_date, - end_date: data.end_date, - resolution_minutes: data.resolution_minutes, - }), - session_queue, - pool, - redis, - ) - .await -} - -/// Get view data for a set of projects or versions -/// Data is returned as a hashmap of project/version ids to a hashmap of days to views -/// eg: -/// { -/// "4N1tEhnO": { -/// "20230824": 1090 -/// } -///} -/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out. -#[get("views")] -pub async fn views_get( - req: HttpRequest, - clickhouse: web::Data, - data: web::Query, - session_queue: web::Data, - pool: web::Data, - redis: web::Data, -) -> Result { - let data = data.into_inner(); - v3::analytics_get::views_get( - req, - clickhouse, - web::Query(v3::analytics_get::GetData { - project_ids: data.project_ids, - version_ids: data.version_ids, - start_date: data.start_date, - end_date: data.end_date, - resolution_minutes: data.resolution_minutes, - }), - session_queue, - pool, - redis, - ) - .await -} - -/// Get download data for a set of projects or versions -/// Data is returned as a hashmap of project/version ids to a hashmap of days to downloads -/// eg: -/// { -/// "4N1tEhnO": { -/// "20230824": 32 -/// } -///} -/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out. -#[get("downloads")] -pub async fn downloads_get( - req: HttpRequest, - clickhouse: web::Data, - data: web::Query, - session_queue: web::Data, - pool: web::Data, - redis: web::Data, -) -> Result { - let data = data.into_inner(); - v3::analytics_get::downloads_get( - req, - clickhouse, - web::Query(v3::analytics_get::GetData { - project_ids: data.project_ids, - version_ids: data.version_ids, - start_date: data.start_date, - end_date: data.end_date, - resolution_minutes: data.resolution_minutes, - }), - session_queue, - pool, - redis, - ) - .await -} - -/// Get payout data for a set of projects -/// Data is returned as a hashmap of project ids to a hashmap of days to amount earned per day -/// eg: -/// { -/// "4N1tEhnO": { -/// "20230824": 0.001 -/// } -///} -/// ONLY project IDs can be used. Unauthorized projects will be filtered out. -#[get("revenue")] -pub async fn revenue_get( - req: HttpRequest, - data: web::Query, - session_queue: web::Data, - pool: web::Data, - redis: web::Data, -) -> Result { - let data = data.into_inner(); - v3::analytics_get::revenue_get( - req, - web::Query(v3::analytics_get::GetData { - project_ids: data.project_ids, - version_ids: None, - start_date: data.start_date, - end_date: data.end_date, - resolution_minutes: data.resolution_minutes, - }), - session_queue, - pool, - redis, - ) - .await -} - -/// Get country data for a set of projects or versions -/// Data is returned as a hashmap of project/version ids to a hashmap of coutnry to downloads. -/// Unknown countries are labeled "". -/// This is usuable to see significant performing countries per project -/// eg: -/// { -/// "4N1tEhnO": { -/// "CAN": 22 -/// } -///} -/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out. -/// For this endpoint, provided dates are a range to aggregate over, not specific days to fetch -#[get("countries/downloads")] -pub async fn countries_downloads_get( - req: HttpRequest, - clickhouse: web::Data, - data: web::Query, - session_queue: web::Data, - pool: web::Data, - redis: web::Data, -) -> Result { - let data = data.into_inner(); - v3::analytics_get::countries_downloads_get( - req, - clickhouse, - web::Query(v3::analytics_get::GetData { - project_ids: data.project_ids, - version_ids: data.version_ids, - start_date: data.start_date, - end_date: data.end_date, - resolution_minutes: data.resolution_minutes, - }), - session_queue, - pool, - redis, - ) - .await -} - -/// Get country data for a set of projects or versions -/// Data is returned as a hashmap of project/version ids to a hashmap of coutnry to views. -/// Unknown countries are labeled "". -/// This is usuable to see significant performing countries per project -/// eg: -/// { -/// "4N1tEhnO": { -/// "CAN": 56165 -/// } -///} -/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out. -/// For this endpoint, provided dates are a range to aggregate over, not specific days to fetch -#[get("countries/views")] -pub async fn countries_views_get( - req: HttpRequest, - clickhouse: web::Data, - data: web::Query, - session_queue: web::Data, - pool: web::Data, - redis: web::Data, -) -> Result { - let data = data.into_inner(); - v3::analytics_get::countries_views_get( - req, - clickhouse, - web::Query(v3::analytics_get::GetData { - project_ids: data.project_ids, - version_ids: data.version_ids, - start_date: data.start_date, - end_date: data.end_date, - resolution_minutes: data.resolution_minutes, - }), - session_queue, - pool, - redis, - ) - .await -} diff --git a/src/routes/v2/collections.rs b/src/routes/v2/collections.rs deleted file mode 100644 index 32412ab51..000000000 --- a/src/routes/v2/collections.rs +++ /dev/null @@ -1,191 +0,0 @@ -use crate::database::redis::RedisPool; -use crate::file_hosting::FileHost; -use crate::models::collections::CollectionStatus; -use crate::queue::session::AuthQueue; -use crate::routes::v3::project_creation::CreateError; -use crate::routes::{v3, ApiError}; -use actix_web::web::Data; -use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; -use serde::{Deserialize, Serialize}; -use sqlx::PgPool; -use std::sync::Arc; -use validator::Validate; - -pub fn config(cfg: &mut web::ServiceConfig) { - cfg.service(collections_get); - cfg.service(collection_create); - cfg.service( - web::scope("collection") - .service(collection_get) - .service(collection_delete) - .service(collection_edit) - .service(collection_icon_edit) - .service(delete_collection_icon), - ); -} - -#[derive(Serialize, Deserialize, Validate, Clone)] -pub struct CollectionCreateData { - #[validate( - length(min = 3, max = 64), - custom(function = "crate::util::validate::validate_name") - )] - /// The title or name of the project. - pub title: String, - #[validate(length(min = 3, max = 255))] - /// A short description of the collection. - pub description: String, - #[validate(length(max = 32))] - #[serde(default = "Vec::new")] - /// A list of initial projects to use with the created collection - pub projects: Vec, -} - -#[post("collection")] -pub async fn collection_create( - req: HttpRequest, - collection_create_data: web::Json, - client: Data, - redis: Data, - session_queue: Data, -) -> Result { - let collection_create_data = collection_create_data.into_inner(); - v3::collections::collection_create( - req, - web::Json(v3::collections::CollectionCreateData { - title: collection_create_data.title, - description: collection_create_data.description, - projects: collection_create_data.projects, - }), - client, - redis, - session_queue, - ) - .await -} - -#[derive(Serialize, Deserialize)] -pub struct CollectionIds { - pub ids: String, -} -#[get("collections")] -pub async fn collections_get( - req: HttpRequest, - web::Query(ids): web::Query, - pool: web::Data, - redis: web::Data, - session_queue: web::Data, -) -> Result { - v3::collections::collections_get( - req, - web::Query(v3::collections::CollectionIds { ids: ids.ids }), - pool, - redis, - session_queue, - ) - .await -} - -#[get("{id}")] -pub async fn collection_get( - req: HttpRequest, - info: web::Path<(String,)>, - pool: web::Data, - redis: web::Data, - session_queue: web::Data, -) -> Result { - v3::collections::collection_get(req, info, pool, redis, session_queue).await -} - -#[derive(Deserialize, Validate)] -pub struct EditCollection { - #[validate( - length(min = 3, max = 64), - custom(function = "crate::util::validate::validate_name") - )] - pub title: Option, - #[validate(length(min = 3, max = 256))] - pub description: Option, - pub status: Option, - #[validate(length(max = 64))] - pub new_projects: Option>, -} - -#[patch("{id}")] -pub async fn collection_edit( - req: HttpRequest, - info: web::Path<(String,)>, - pool: web::Data, - new_collection: web::Json, - redis: web::Data, - session_queue: web::Data, -) -> Result { - let new_collection = new_collection.into_inner(); - v3::collections::collection_edit( - req, - info, - pool, - web::Json(v3::collections::EditCollection { - title: new_collection.title, - description: new_collection.description, - status: new_collection.status, - new_projects: new_collection.new_projects, - }), - redis, - session_queue, - ) - .await -} - -#[derive(Serialize, Deserialize)] -pub struct Extension { - pub ext: String, -} - -#[patch("{id}/icon")] -#[allow(clippy::too_many_arguments)] -pub async fn collection_icon_edit( - web::Query(ext): web::Query, - req: HttpRequest, - info: web::Path<(String,)>, - pool: web::Data, - redis: web::Data, - file_host: web::Data>, - payload: web::Payload, - session_queue: web::Data, -) -> Result { - v3::collections::collection_icon_edit( - web::Query(v3::collections::Extension { ext: ext.ext }), - req, - info, - pool, - redis, - file_host, - payload, - session_queue, - ) - .await -} - -#[delete("{id}/icon")] -pub async fn delete_collection_icon( - req: HttpRequest, - info: web::Path<(String,)>, - pool: web::Data, - redis: web::Data, - file_host: web::Data>, - session_queue: web::Data, -) -> Result { - v3::collections::delete_collection_icon(req, info, pool, redis, file_host, session_queue).await -} - -#[delete("{id}")] -pub async fn collection_delete( - req: HttpRequest, - info: web::Path<(String,)>, - pool: web::Data, - redis: web::Data, - session_queue: web::Data, -) -> Result { - v3::collections::collection_delete(req, info, pool, redis, session_queue).await -} diff --git a/src/routes/v2/images.rs b/src/routes/v2/images.rs deleted file mode 100644 index da6d2aea8..000000000 --- a/src/routes/v2/images.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::sync::Arc; - -use crate::database::redis::RedisPool; -use crate::file_hosting::FileHost; -use crate::models::ids::{ThreadMessageId, VersionId}; -use crate::models::reports::ReportId; -use crate::queue::session::AuthQueue; -use crate::routes::{v3, ApiError}; -use actix_web::{post, web, HttpRequest, HttpResponse}; -use serde::{Deserialize, Serialize}; -use sqlx::PgPool; - -pub fn config(cfg: &mut web::ServiceConfig) { - cfg.service(images_add); -} - -#[derive(Serialize, Deserialize)] -pub struct ImageUpload { - pub ext: String, - - // Context must be an allowed context - // currently: project, version, thread_message, report - pub context: String, - - // Optional context id to associate with - pub project_id: Option, // allow slug or id - pub version_id: Option, - pub thread_message_id: Option, - pub report_id: Option, -} - -#[post("image")] -pub async fn images_add( - req: HttpRequest, - web::Query(data): web::Query, - file_host: web::Data>, - payload: web::Payload, - pool: web::Data, - redis: web::Data, - session_queue: web::Data, -) -> Result { - v3::images::images_add( - req, - web::Query(v3::images::ImageUpload { - ext: data.ext, - context: data.context, - project_id: data.project_id, - version_id: data.version_id, - thread_message_id: data.thread_message_id, - report_id: data.report_id, - }), - file_host, - payload, - pool, - redis, - session_queue, - ) - .await -} diff --git a/src/routes/v2/mod.rs b/src/routes/v2/mod.rs index 9a22fd626..417308e6f 100644 --- a/src/routes/v2/mod.rs +++ b/src/routes/v2/mod.rs @@ -1,10 +1,6 @@ mod admin; -mod analytics_get; -mod collections; -mod images; mod moderation; mod notifications; -mod organizations; pub(crate) mod project_creation; mod projects; mod reports; @@ -25,17 +21,13 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) { actix_web::web::scope("v2") .wrap(default_cors()) .configure(admin::config) - .configure(analytics_get::config) // Todo: separate these- they need to also follow v2-v3 conversion - .configure(crate::auth::session::config) - .configure(crate::auth::flows::config) - .configure(crate::auth::pats::config) + .configure(super::internal::session::config) + .configure(super::internal::flows::config) + .configure(super::internal::pats::config) .configure(moderation::config) .configure(notifications::config) - .configure(organizations::config) .configure(project_creation::config) - .configure(collections::config) - .configure(images::config) .configure(projects::config) .configure(reports::config) .configure(statistics::config) diff --git a/src/routes/v2/organizations.rs b/src/routes/v2/organizations.rs deleted file mode 100644 index 15ea4e6f5..000000000 --- a/src/routes/v2/organizations.rs +++ /dev/null @@ -1,265 +0,0 @@ -use crate::database::redis::RedisPool; -use crate::file_hosting::FileHost; -use crate::models::projects::Project; -use crate::models::v2::projects::LegacyProject; -use crate::queue::session::AuthQueue; -use crate::routes::v3::project_creation::CreateError; -use crate::routes::{v2_reroute, v3, ApiError}; -use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; -use serde::{Deserialize, Serialize}; -use sqlx::PgPool; -use std::sync::Arc; -use validator::Validate; - -pub fn config(cfg: &mut web::ServiceConfig) { - cfg.service(organizations_get).service(organization_create); - cfg.service( - web::scope("organization") - .service(organization_get) - .service(organizations_edit) - .service(organization_delete) - .service(organization_projects_get) - .service(organization_projects_add) - .service(organization_projects_remove) - .service(organization_icon_edit) - .service(delete_organization_icon) - .service(super::teams::team_members_get_organization), - ); -} - -#[derive(Deserialize, Validate)] -pub struct NewOrganization { - #[validate( - length(min = 3, max = 64), - regex = "crate::util::validate::RE_URL_SAFE" - )] - // Title of the organization, also used as slug - pub title: String, - #[validate(length(min = 3, max = 256))] - pub description: String, -} - -#[post("organization")] -pub async fn organization_create( - req: HttpRequest, - new_organization: web::Json, - pool: web::Data, - redis: web::Data, - session_queue: web::Data, -) -> Result { - let new_organization = new_organization.into_inner(); - v3::organizations::organization_create( - req, - web::Json(v3::organizations::NewOrganization { - title: new_organization.title, - description: new_organization.description, - }), - pool.clone(), - redis.clone(), - session_queue, - ) - .await -} - -#[get("{id}")] -pub async fn organization_get( - req: HttpRequest, - info: web::Path<(String,)>, - pool: web::Data, - redis: web::Data, - session_queue: web::Data, -) -> Result { - v3::organizations::organization_get(req, info, pool.clone(), redis.clone(), session_queue).await -} - -#[derive(Deserialize)] -pub struct OrganizationIds { - pub ids: String, -} -#[get("organizations")] -pub async fn organizations_get( - req: HttpRequest, - web::Query(ids): web::Query, - pool: web::Data, - redis: web::Data, - session_queue: web::Data, -) -> Result { - v3::organizations::organizations_get( - req, - web::Query(v3::organizations::OrganizationIds { ids: ids.ids }), - pool, - redis, - session_queue, - ) - .await -} - -#[derive(Serialize, Deserialize, Validate)] -pub struct OrganizationEdit { - #[validate(length(min = 3, max = 256))] - pub description: Option, - #[validate( - length(min = 3, max = 64), - regex = "crate::util::validate::RE_URL_SAFE" - )] - // Title of the organization, also used as slug - pub title: Option, -} - -#[patch("{id}")] -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 new_organization = new_organization.into_inner(); - v3::organizations::organizations_edit( - req, - info, - web::Json(v3::organizations::OrganizationEdit { - description: new_organization.description, - title: new_organization.title, - }), - pool.clone(), - redis.clone(), - session_queue, - ) - .await -} - -#[delete("{id}")] -pub async fn organization_delete( - req: HttpRequest, - info: web::Path<(String,)>, - pool: web::Data, - redis: web::Data, - session_queue: web::Data, -) -> Result { - v3::organizations::organization_delete(req, info, pool.clone(), redis.clone(), session_queue) - .await -} - -#[get("{id}/projects")] -pub async fn organization_projects_get( - req: HttpRequest, - info: web::Path<(String,)>, - pool: web::Data, - redis: web::Data, - session_queue: web::Data, -) -> Result { - let response = v3::organizations::organization_projects_get( - req, - info, - pool.clone(), - redis.clone(), - session_queue, - ) - .await?; - - // Convert v3 projects to v2 - match v2_reroute::extract_ok_json::>(response).await { - Ok(project) => { - let legacy_projects = LegacyProject::from_many(project, &**pool, &redis).await?; - Ok(HttpResponse::Ok().json(legacy_projects)) - } - Err(response) => Ok(response), - } -} - -#[derive(Deserialize)] -pub struct OrganizationProjectAdd { - pub project_id: String, // Also allow title/slug -} -#[post("{id}/projects")] -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 project_info = project_info.into_inner(); - v3::organizations::organization_projects_add( - req, - info, - web::Json(v3::organizations::OrganizationProjectAdd { - project_id: project_info.project_id, - }), - pool.clone(), - redis.clone(), - session_queue, - ) - .await -} - -#[delete("{organization_id}/projects/{project_id}")] -pub async fn organization_projects_remove( - req: HttpRequest, - info: web::Path<(String, String)>, - pool: web::Data, - redis: web::Data, - session_queue: web::Data, -) -> Result { - v3::organizations::organization_projects_remove( - req, - info, - pool.clone(), - redis.clone(), - session_queue, - ) - .await -} - -#[derive(Serialize, Deserialize)] -pub struct Extension { - pub ext: String, -} - -#[patch("{id}/icon")] -#[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>, - payload: web::Payload, - session_queue: web::Data, -) -> Result { - v3::organizations::organization_icon_edit( - web::Query(v3::organizations::Extension { ext: ext.ext }), - req, - info, - pool.clone(), - redis.clone(), - file_host, - payload, - session_queue, - ) - .await -} - -#[delete("{id}/icon")] -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 { - v3::organizations::delete_organization_icon( - req, - info, - pool.clone(), - redis.clone(), - file_host, - session_queue, - ) - .await -} diff --git a/src/routes/v2/teams.rs b/src/routes/v2/teams.rs index fca310d37..74a64a6c5 100644 --- a/src/routes/v2/teams.rs +++ b/src/routes/v2/teams.rs @@ -37,17 +37,6 @@ pub async fn team_members_get_project( v3::teams::team_members_get_project(req, info, pool, redis, session_queue).await } -#[get("{id}/members")] -pub async fn team_members_get_organization( - req: HttpRequest, - info: web::Path<(String,)>, - pool: web::Data, - redis: web::Data, - session_queue: web::Data, -) -> Result { - v3::teams::team_members_get_organization(req, info, pool, redis, session_queue).await -} - // Returns all members of a team, but not necessarily those of a project-team's organization (unlike team_members_get_project) #[get("{id}/members")] pub async fn team_members_get( diff --git a/src/routes/v2/users.rs b/src/routes/v2/users.rs index 7c8c2b6a6..130551874 100644 --- a/src/routes/v2/users.rs +++ b/src/routes/v2/users.rs @@ -20,9 +20,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) .service(user_edit) .service(user_icon_edit) @@ -85,28 +83,6 @@ pub async fn projects_list( } } -#[get("{user_id}/collections")] -pub async fn collections_list( - req: HttpRequest, - info: web::Path<(String,)>, - pool: web::Data, - redis: web::Data, - session_queue: web::Data, -) -> Result { - v3::users::collections_list(req, info, pool, redis, session_queue).await -} - -#[get("{user_id}/organizations")] -pub async fn orgs_list( - req: HttpRequest, - info: web::Path<(String,)>, - pool: web::Data, - redis: web::Data, - session_queue: web::Data, -) -> Result { - v3::users::orgs_list(req, info, pool, redis, session_queue).await -} - lazy_static! { static ref RE_URL_SAFE: Regex = Regex::new(r"^[a-zA-Z0-9_-]*$").unwrap(); } diff --git a/src/routes/v3/mod.rs b/src/routes/v3/mod.rs index 7dd63ccf1..a5165fec2 100644 --- a/src/routes/v3/mod.rs +++ b/src/routes/v3/mod.rs @@ -3,7 +3,6 @@ use crate::util::cors::default_cors; use actix_web::{web, HttpResponse}; use serde_json::json; -pub mod admin; pub mod analytics_get; pub mod collections; pub mod images; @@ -29,13 +28,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("v3") .wrap(default_cors()) - .configure(admin::config) .configure(analytics_get::config) - // TODO: write tests that catch these - .configure(oauth_clients::config) - .configure(crate::auth::session::config) - .configure(crate::auth::flows::config) - .configure(crate::auth::pats::config) .configure(collections::config) .configure(images::config) .configure(moderation::config) diff --git a/tests/common/api_v3/mod.rs b/tests/common/api_v3/mod.rs index 6f1fa9538..ee65744f6 100644 --- a/tests/common/api_v3/mod.rs +++ b/tests/common/api_v3/mod.rs @@ -41,7 +41,7 @@ impl Api for ApiV3 { async fn reset_search_index(&self) -> ServiceResponse { let req = actix_web::test::TestRequest::post() - .uri("/v3/admin/_force_reindex") + .uri("/_internal/admin/_force_reindex") .append_header(( "Modrinth-Admin", dotenvy::var("LABRINTH_ADMIN_KEY").unwrap(), diff --git a/tests/common/api_v3/oauth.rs b/tests/common/api_v3/oauth.rs index 4e6c76692..a1a93add5 100644 --- a/tests/common/api_v3/oauth.rs +++ b/tests/common/api_v3/oauth.rs @@ -55,7 +55,7 @@ impl ApiV3 { pub async fn oauth_accept(&self, flow: &str, pat: &str) -> ServiceResponse { self.call( TestRequest::post() - .uri("/v3/oauth/accept") + .uri("/_internal/oauth/accept") .append_header((AUTHORIZATION, pat)) .set_json(RespondToOAuthClientScopes { flow: flow.to_string(), @@ -68,7 +68,7 @@ impl ApiV3 { pub async fn oauth_reject(&self, flow: &str, pat: &str) -> ServiceResponse { self.call( TestRequest::post() - .uri("/v3/oauth/reject") + .uri("/_internal/oauth/reject") .append_header((AUTHORIZATION, pat)) .set_json(RespondToOAuthClientScopes { flow: flow.to_string(), @@ -87,7 +87,7 @@ impl ApiV3 { ) -> ServiceResponse { self.call( TestRequest::post() - .uri("/v3/oauth/token") + .uri("/_internal/oauth/token") .append_header((AUTHORIZATION, client_secret)) .set_form(TokenRequest { grant_type: "authorization_code".to_string(), @@ -108,7 +108,7 @@ pub fn generate_authorize_uri( state: Option<&str>, ) -> String { format!( - "/v3/oauth/authorize?client_id={}{}{}{}", + "/_internal/oauth/authorize?client_id={}{}{}{}", urlencoding::encode(client_id), optional_query_param("redirect_uri", redirect_uri), optional_query_param("scope", scope), diff --git a/tests/common/api_v3/oauth_clients.rs b/tests/common/api_v3/oauth_clients.rs index b5c3ef56d..dfad4fc28 100644 --- a/tests/common/api_v3/oauth_clients.rs +++ b/tests/common/api_v3/oauth_clients.rs @@ -27,7 +27,7 @@ impl ApiV3 { ) -> ServiceResponse { let max_scopes = max_scopes.bits(); let req = TestRequest::post() - .uri("/v3/oauth/app") + .uri("/_internal/oauth/app") .append_header((AUTHORIZATION, pat)) .set_json(json!({ "name": name, @@ -52,7 +52,7 @@ impl ApiV3 { pub async fn get_oauth_client(&self, client_id: String, pat: &str) -> ServiceResponse { let req = TestRequest::get() - .uri(&format!("/v3/oauth/app/{}", client_id)) + .uri(&format!("/_internal/oauth/app/{}", client_id)) .append_header((AUTHORIZATION, pat)) .to_request(); @@ -66,7 +66,10 @@ impl ApiV3 { pat: &str, ) -> ServiceResponse { let req = TestRequest::patch() - .uri(&format!("/v3/oauth/app/{}", urlencoding::encode(client_id))) + .uri(&format!( + "/_internal/oauth/app/{}", + urlencoding::encode(client_id) + )) .set_json(edit) .append_header((AUTHORIZATION, pat)) .to_request(); @@ -76,7 +79,7 @@ impl ApiV3 { pub async fn delete_oauth_client(&self, client_id: &str, pat: &str) -> ServiceResponse { let req = TestRequest::delete() - .uri(&format!("/v3/oauth/app/{}", client_id)) + .uri(&format!("/_internal/oauth/app/{}", client_id)) .append_header((AUTHORIZATION, pat)) .to_request(); @@ -86,7 +89,7 @@ impl ApiV3 { pub async fn revoke_oauth_authorization(&self, client_id: &str, pat: &str) -> ServiceResponse { let req = TestRequest::delete() .uri(&format!( - "/v3/oauth/authorizations?client_id={}", + "/_internal/oauth/authorizations?client_id={}", urlencoding::encode(client_id) )) .append_header((AUTHORIZATION, pat)) @@ -96,7 +99,7 @@ impl ApiV3 { pub async fn get_user_oauth_authorizations(&self, pat: &str) -> Vec { let req = TestRequest::get() - .uri("/v3/oauth/authorizations") + .uri("/_internal/oauth/authorizations") .append_header((AUTHORIZATION, pat)) .to_request(); let resp = self.call(req).await; diff --git a/tests/pats.rs b/tests/pats.rs index 570a072ff..c32708fe6 100644 --- a/tests/pats.rs +++ b/tests/pats.rs @@ -19,7 +19,7 @@ pub async fn pat_full_test() { with_test_environment_all(None, |test_env| async move { // Create a PAT for a full test let req = test::TestRequest::post() - .uri("/v3/pat") + .uri("/_internal/pat") .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "scopes": Scopes::COLLECTION_CREATE, // Collection create as an easily tested example @@ -43,7 +43,7 @@ pub async fn pat_full_test() { // Get PAT again let req = test::TestRequest::get() .append_header(("Authorization", USER_USER_PAT)) - .uri("/v3/pat") + .uri("/_internal/pat") .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status().as_u16(), 200); @@ -75,7 +75,7 @@ pub async fn pat_full_test() { // Change scopes and test again let req = test::TestRequest::patch() - .uri(&format!("/v3/pat/{}", id)) + .uri(&format!("/_internal/pat/{}", id)) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "scopes": 0, @@ -87,7 +87,7 @@ pub async fn pat_full_test() { // Change scopes back, and set expiry to the past, and test again let req = test::TestRequest::patch() - .uri(&format!("/v3/pat/{}", id)) + .uri(&format!("/_internal/pat/{}", id)) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "scopes": Scopes::COLLECTION_CREATE, @@ -103,7 +103,7 @@ pub async fn pat_full_test() { // Change everything back to normal and test again let req = test::TestRequest::patch() - .uri(&format!("/v3/pat/{}", id)) + .uri(&format!("/_internal/pat/{}", id)) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "expires": Utc::now() + Duration::days(1), // no longer expired! @@ -115,7 +115,7 @@ pub async fn pat_full_test() { // Patching to a bad expiry should fail let req = test::TestRequest::patch() - .uri(&format!("/v3/pat/{}", id)) + .uri(&format!("/_internal/pat/{}", id)) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "expires": Utc::now() - Duration::days(1), // Past @@ -132,7 +132,7 @@ pub async fn pat_full_test() { } let req = test::TestRequest::patch() - .uri(&format!("/v3/pat/{}", id)) + .uri(&format!("/_internal/pat/{}", id)) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "scopes": scope.bits(), @@ -148,7 +148,7 @@ pub async fn pat_full_test() { // Delete PAT let req = test::TestRequest::delete() .append_header(("Authorization", USER_USER_PAT)) - .uri(&format!("/v3/pat/{}", id)) + .uri(&format!("/_internal/pat/{}", id)) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status().as_u16(), 204); @@ -162,7 +162,7 @@ pub async fn bad_pats() { with_test_environment_all(None, |test_env| async move { // Creating a PAT with no name should fail let req = test::TestRequest::post() - .uri("/v3/pat") + .uri("/_internal/pat") .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "scopes": Scopes::COLLECTION_CREATE, // Collection create as an easily tested example @@ -175,7 +175,7 @@ pub async fn bad_pats() { // Name too short or too long should fail for name in ["n", "this_name_is_too_long".repeat(16).as_str()] { let req = test::TestRequest::post() - .uri("/v3/pat") + .uri("/_internal/pat") .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "name": name, @@ -189,7 +189,7 @@ pub async fn bad_pats() { // Creating a PAT with an expiry in the past should fail let req = test::TestRequest::post() - .uri("/v3/pat") + .uri("/_internal/pat") .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "scopes": Scopes::COLLECTION_CREATE, // Collection create as an easily tested example @@ -207,7 +207,7 @@ pub async fn bad_pats() { continue; } let req = test::TestRequest::post() - .uri("/v3/pat") + .uri("/_internal/pat") .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "scopes": scope.bits(), @@ -224,7 +224,7 @@ pub async fn bad_pats() { // Create a 'good' PAT for patching let req = test::TestRequest::post() - .uri("/v3/pat") + .uri("/_internal/pat") .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "scopes": Scopes::COLLECTION_CREATE, @@ -240,7 +240,7 @@ pub async fn bad_pats() { // Patching to a bad name should fail for name in ["n", "this_name_is_too_long".repeat(16).as_str()] { let req = test::TestRequest::post() - .uri("/v3/pat") + .uri("/_internal/pat") .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "name": name, @@ -252,7 +252,7 @@ pub async fn bad_pats() { // Patching to a bad expiry should fail let req = test::TestRequest::patch() - .uri(&format!("/v3/pat/{}", id)) + .uri(&format!("/_internal/pat/{}", id)) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "expires": Utc::now() - Duration::days(1), // Past @@ -269,7 +269,7 @@ pub async fn bad_pats() { } let req = test::TestRequest::patch() - .uri(&format!("/v3/pat/{}", id)) + .uri(&format!("/_internal/pat/{}", id)) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ "scopes": scope.bits(), diff --git a/tests/scopes.rs b/tests/scopes.rs index 880d16f72..391b657b9 100644 --- a/tests/scopes.rs +++ b/tests/scopes.rs @@ -1017,11 +1017,13 @@ pub async fn pat_scopes() { // Pat create let pat_create = Scopes::PAT_CREATE; let req_gen = || { - test::TestRequest::post().uri("/v3/pat").set_json(json!({ - "scopes": 1, - "name": "test_pat_scopes Name", - "expires": Utc::now() + Duration::days(1), - })) + test::TestRequest::post() + .uri("/_internal/pat") + .set_json(json!({ + "scopes": 1, + "name": "test_pat_scopes Name", + "expires": Utc::now() + Duration::days(1), + })) }; let (_, success) = ScopeTest::new(&test_env) .test(req_gen, pat_create) @@ -1033,7 +1035,7 @@ pub async fn pat_scopes() { let pat_write = Scopes::PAT_WRITE; let req_gen = || { test::TestRequest::patch() - .uri(&format!("/v3/pat/{pat_id}")) + .uri(&format!("/_internal/pat/{pat_id}")) .set_json(json!({})) }; ScopeTest::new(&test_env) @@ -1043,7 +1045,7 @@ pub async fn pat_scopes() { // Pat read let pat_read = Scopes::PAT_READ; - let req_gen = || test::TestRequest::get().uri("/v3/pat"); + let req_gen = || test::TestRequest::get().uri("/_internal/pat"); ScopeTest::new(&test_env) .test(req_gen, pat_read) .await @@ -1051,7 +1053,7 @@ pub async fn pat_scopes() { // Pat delete let pat_delete = Scopes::PAT_DELETE; - let req_gen = || test::TestRequest::delete().uri(&format!("/v3/pat/{pat_id}")); + let req_gen = || test::TestRequest::delete().uri(&format!("/_internal/pat/{pat_id}")); ScopeTest::new(&test_env) .test(req_gen, pat_delete) .await