From 3c2f144795883f0fc2857ffaec4ab0a39d918d82 Mon Sep 17 00:00:00 2001 From: triphora Date: Thu, 16 Mar 2023 14:56:04 -0400 Subject: [PATCH] Perses finale (#558) * Move v2 routes to v2 module * Remove v1 routes and make it run * Make config declaration consistent, add v3 module * Readd API v1 msgs * Fix imports --- src/main.rs | 9 +- src/routes/maven.rs | 10 + src/routes/mod.rs | 282 ++++-------------------- src/routes/updates.rs | 4 + src/routes/v1/mod.rs | 95 -------- src/routes/v1/mods.rs | 144 ------------ src/routes/v1/tags.rs | 64 ------ src/routes/v1/teams.rs | 78 ------- src/routes/v1/users.rs | 87 -------- src/routes/v1/versions.rs | 195 ---------------- src/routes/{ => v2}/admin.rs | 8 + src/routes/{ => v2}/auth.rs | 0 src/routes/{ => v2}/midas.rs | 9 + src/routes/v2/mod.rs | 38 ++++ src/routes/{ => v2}/moderation.rs | 9 + src/routes/{ => v2}/notifications.rs | 11 + src/routes/{ => v2}/project_creation.rs | 6 +- src/routes/{ => v2}/projects.rs | 24 ++ src/routes/{ => v2}/reports.rs | 6 + src/routes/{ => v2}/statistics.rs | 4 + src/routes/{ => v2}/tags.rs | 0 src/routes/{ => v2}/teams.rs | 16 ++ src/routes/{ => v2}/users.rs | 18 ++ src/routes/{ => v2}/version_creation.rs | 8 +- src/routes/{ => v2}/version_file.rs | 17 ++ src/routes/{ => v2}/versions.rs | 18 ++ src/routes/v3/mod.rs | 13 ++ src/util/routes.rs | 2 +- 28 files changed, 264 insertions(+), 911 deletions(-) delete mode 100644 src/routes/v1/mod.rs delete mode 100644 src/routes/v1/mods.rs delete mode 100644 src/routes/v1/tags.rs delete mode 100644 src/routes/v1/teams.rs delete mode 100644 src/routes/v1/users.rs delete mode 100644 src/routes/v1/versions.rs rename src/routes/{ => v2}/admin.rs (98%) rename src/routes/{ => v2}/auth.rs (100%) rename src/routes/{ => v2}/midas.rs (97%) create mode 100644 src/routes/v2/mod.rs rename src/routes/{ => v2}/moderation.rs (91%) rename src/routes/{ => v2}/notifications.rs (94%) rename src/routes/{ => v2}/project_creation.rs (99%) rename src/routes/{ => v2}/projects.rs (98%) rename src/routes/{ => v2}/reports.rs (98%) rename src/routes/{ => v2}/statistics.rs (97%) rename src/routes/{ => v2}/tags.rs (100%) rename src/routes/{ => v2}/teams.rs (97%) rename src/routes/{ => v2}/users.rs (98%) rename src/routes/{ => v2}/version_creation.rs (99%) rename src/routes/{ => v2}/version_file.rs (97%) rename src/routes/{ => v2}/versions.rs (98%) create mode 100644 src/routes/v3/mod.rs diff --git a/src/main.rs b/src/main.rs index 8d6ed1713..256ea23d6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -366,12 +366,9 @@ async fn main() -> std::io::Result<()> { .app_data(web::Data::new(payouts_queue.clone())) .app_data(web::Data::new(ip_salt.clone())) .wrap(sentry_actix::Sentry::new()) - .configure(routes::v1_config) - .configure(routes::v2_config) - .service(routes::index_get) - .service(routes::health_get) - .service(web::scope("maven").configure(routes::maven_config)) - .service(web::scope("updates").configure(routes::updates)) + .configure(routes::root_config) + .configure(routes::v2::config) + .configure(routes::v3::config) .default_service(web::get().to(routes::not_found)) }) .bind(dotenvy::var("BIND_ADDR").unwrap())? diff --git a/src/routes/maven.rs b/src/routes/maven.rs index 574550e08..a21cef0a0 100644 --- a/src/routes/maven.rs +++ b/src/routes/maven.rs @@ -9,6 +9,13 @@ use sqlx::PgPool; use std::collections::HashSet; use yaserde_derive::YaSerialize; +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service(maven_metadata); + cfg.service(version_file_sha512); + cfg.service(version_file_sha1); + cfg.service(version_file); +} + #[derive(Default, Debug, Clone, YaSerialize)] #[yaserde(root = "metadata", rename = "metadata")] pub struct Metadata { @@ -18,6 +25,7 @@ pub struct Metadata { artifact_id: String, versioning: Versioning, } + #[derive(Default, Debug, Clone, YaSerialize)] #[yaserde(rename = "versioning")] pub struct Versioning { @@ -27,12 +35,14 @@ pub struct Versioning { #[yaserde(rename = "lastUpdated")] last_updated: String, } + #[derive(Default, Debug, Clone, YaSerialize)] #[yaserde(rename = "versions")] pub struct Versions { #[yaserde(rename = "version")] versions: Vec, } + #[derive(Default, Debug, Clone, YaSerialize)] #[yaserde(rename = "project", namespace = "http://maven.apache.org/POM/4.0.0")] pub struct MavenPom { diff --git a/src/routes/mod.rs b/src/routes/mod.rs index d74a5355f..193ecea36 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,200 +1,34 @@ -mod v1; -pub use v1::v1_config; +use crate::file_hosting::FileHostingError; +use actix_web::http::StatusCode; +use actix_web::{web, HttpResponse}; +use futures::FutureExt; + +pub mod v2; +pub mod v3; -mod admin; -mod auth; mod health; mod index; mod maven; -mod midas; -mod moderation; mod not_found; -mod notifications; -pub(crate) mod project_creation; -mod projects; -mod reports; -mod statistics; -mod tags; -mod teams; mod updates; -mod users; -mod version_creation; -mod version_file; -mod versions; -pub use auth::config as auth_config; -pub use tags::config as tags_config; - -pub use self::health::health_get; -pub use self::index::index_get; pub use self::not_found::not_found; -use crate::file_hosting::FileHostingError; -use actix_web::web; -use image::ImageError; -pub fn v2_config(cfg: &mut web::ServiceConfig) { +pub fn root_config(cfg: &mut web::ServiceConfig) { + cfg.service(index::index_get); + cfg.service(health::health_get); + cfg.service(web::scope("maven").configure(maven::config)); + cfg.service(web::scope("updates").configure(updates::config)); cfg.service( - web::scope("v2") - .configure(auth_config) - .configure(tags_config) - .configure(projects_config) - .configure(versions_config) - .configure(teams_config) - .configure(users_config) - .configure(moderation_config) - .configure(reports_config) - .configure(notifications_config) - .configure(statistics_config) - .configure(admin_config) - .configure(midas_config), - ); -} - -pub fn projects_config(cfg: &mut web::ServiceConfig) { - cfg.service(projects::project_search); - cfg.service(projects::projects_get); - cfg.service(projects::projects_edit); - cfg.service(projects::random_projects_get); - cfg.service(project_creation::project_create); - - cfg.service( - web::scope("project") - .service(projects::project_get) - .service(projects::project_get_check) - .service(projects::project_delete) - .service(projects::project_edit) - .service(projects::project_icon_edit) - .service(projects::delete_project_icon) - .service(projects::add_gallery_item) - .service(projects::edit_gallery_item) - .service(projects::delete_gallery_item) - .service(projects::project_follow) - .service(projects::project_unfollow) - .service(projects::project_schedule) - .service(teams::team_members_get_project) - .service( - web::scope("{project_id}") - .service(versions::version_list) - .service(projects::dependency_list) - .service(versions::version_project_get), - ), - ); -} - -pub fn maven_config(cfg: &mut web::ServiceConfig) { - cfg.service(maven::maven_metadata); - cfg.service(maven::version_file_sha512); - cfg.service(maven::version_file_sha1); - cfg.service(maven::version_file); -} - -pub fn updates(cfg: &mut web::ServiceConfig) { - cfg.service(updates::forge_updates); -} - -pub fn versions_config(cfg: &mut web::ServiceConfig) { - cfg.service(versions::versions_get); - cfg.service(version_creation::version_create); - cfg.service( - web::scope("version") - .service(versions::version_get) - .service(versions::version_delete) - .service(version_creation::upload_file_to_version) - .service(versions::version_edit) - .service(versions::version_schedule), - ); - cfg.service( - web::scope("version_file") - .service(version_file::delete_file) - .service(version_file::get_version_from_hash) - .service(version_file::download_version) - .service(version_file::get_update_from_hash), - ); - - cfg.service( - web::scope("version_files") - .service(version_file::get_versions_from_hashes) - .service(version_file::download_files) - .service(version_file::update_files), - ); -} - -pub fn users_config(cfg: &mut web::ServiceConfig) { - cfg.service(users::user_auth_get); - - cfg.service(users::users_get); - cfg.service( - web::scope("user") - .service(users::user_get) - .service(users::projects_list) - .service(users::user_delete) - .service(users::user_edit) - .service(users::user_icon_edit) - .service(users::user_notifications) - .service(users::user_follows) - .service(users::user_payouts) - .service(users::user_payouts_request), - ); -} - -pub fn teams_config(cfg: &mut web::ServiceConfig) { - cfg.service(teams::teams_get); - - cfg.service( - web::scope("team") - .service(teams::team_members_get) - .service(teams::edit_team_member) - .service(teams::transfer_ownership) - .service(teams::add_team_member) - .service(teams::join_team) - .service(teams::remove_team_member), - ); -} - -pub fn notifications_config(cfg: &mut web::ServiceConfig) { - cfg.service(notifications::notifications_get); - cfg.service(notifications::notifications_delete); - - cfg.service( - web::scope("notification") - .service(notifications::notification_get) - .service(notifications::notification_delete), - ); -} - -pub fn moderation_config(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("moderation") - .service(moderation::get_projects) - .service(moderation::ban_user) - .service(moderation::unban_user), - ); -} - -pub fn reports_config(cfg: &mut web::ServiceConfig) { - cfg.service(reports::reports); - cfg.service(reports::report_create); - cfg.service(reports::delete_report); -} - -pub fn statistics_config(cfg: &mut web::ServiceConfig) { - cfg.service(statistics::get_stats); -} - -pub fn admin_config(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("admin") - .service(admin::count_download) - .service(admin::process_payout), - ); -} - -pub fn midas_config(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("midas") - .service(midas::init_checkout) - .service(midas::init_customer_portal) - .service(midas::handle_stripe_webhook), + web::scope("api/v1").wrap_fn(|req, _srv| { + async { + Ok(req.into_response( + HttpResponse::Gone() + .content_type("application/json") + .body(r#"{"error":"api_deprecated","description":"You are using an application that uses an outdated version of Modrinth's API. Please either update it or switch to another application. For developers: https://docs.modrinth.com/docs/migrations/v1-to-v2/"}"#) + )) + }.boxed_local() + }) ); } @@ -235,65 +69,35 @@ pub enum ApiError { #[error("Error while decoding Base62: {0}")] Decoding(#[from] crate::models::ids::DecodingError), #[error("Image Parsing Error: {0}")] - ImageError(#[from] ImageError), + ImageError(#[from] image::ImageError), } impl actix_web::ResponseError for ApiError { - fn status_code(&self) -> actix_web::http::StatusCode { + fn status_code(&self) -> StatusCode { match self { - ApiError::Env(..) => { - actix_web::http::StatusCode::INTERNAL_SERVER_ERROR - } - ApiError::Database(..) => { - actix_web::http::StatusCode::INTERNAL_SERVER_ERROR - } - ApiError::SqlxDatabase(..) => { - actix_web::http::StatusCode::INTERNAL_SERVER_ERROR - } - ApiError::Authentication(..) => { - actix_web::http::StatusCode::UNAUTHORIZED - } - ApiError::CustomAuthentication(..) => { - actix_web::http::StatusCode::UNAUTHORIZED - } - ApiError::Xml(..) => { - actix_web::http::StatusCode::INTERNAL_SERVER_ERROR - } - ApiError::Json(..) => actix_web::http::StatusCode::BAD_REQUEST, - ApiError::Search(..) => { - actix_web::http::StatusCode::INTERNAL_SERVER_ERROR - } - ApiError::Indexing(..) => { - actix_web::http::StatusCode::INTERNAL_SERVER_ERROR - } - ApiError::FileHosting(..) => { - actix_web::http::StatusCode::INTERNAL_SERVER_ERROR - } - ApiError::InvalidInput(..) => { - actix_web::http::StatusCode::BAD_REQUEST - } - ApiError::Validation(..) => { - actix_web::http::StatusCode::BAD_REQUEST - } - ApiError::Analytics(..) => { - actix_web::http::StatusCode::FAILED_DEPENDENCY - } - ApiError::Crypto(..) => actix_web::http::StatusCode::FORBIDDEN, - ApiError::Payments(..) => { - actix_web::http::StatusCode::FAILED_DEPENDENCY - } - ApiError::DiscordError(..) => { - actix_web::http::StatusCode::FAILED_DEPENDENCY - } - ApiError::Decoding(..) => actix_web::http::StatusCode::BAD_REQUEST, - ApiError::ImageError(..) => { - actix_web::http::StatusCode::BAD_REQUEST - } + ApiError::Env(..) => StatusCode::INTERNAL_SERVER_ERROR, + ApiError::Database(..) => StatusCode::INTERNAL_SERVER_ERROR, + ApiError::SqlxDatabase(..) => StatusCode::INTERNAL_SERVER_ERROR, + ApiError::Authentication(..) => StatusCode::UNAUTHORIZED, + ApiError::CustomAuthentication(..) => StatusCode::UNAUTHORIZED, + ApiError::Xml(..) => StatusCode::INTERNAL_SERVER_ERROR, + ApiError::Json(..) => StatusCode::BAD_REQUEST, + ApiError::Search(..) => StatusCode::INTERNAL_SERVER_ERROR, + ApiError::Indexing(..) => StatusCode::INTERNAL_SERVER_ERROR, + ApiError::FileHosting(..) => StatusCode::INTERNAL_SERVER_ERROR, + ApiError::InvalidInput(..) => StatusCode::BAD_REQUEST, + ApiError::Validation(..) => StatusCode::BAD_REQUEST, + ApiError::Analytics(..) => StatusCode::FAILED_DEPENDENCY, + ApiError::Crypto(..) => StatusCode::FORBIDDEN, + ApiError::Payments(..) => StatusCode::FAILED_DEPENDENCY, + ApiError::DiscordError(..) => StatusCode::FAILED_DEPENDENCY, + ApiError::Decoding(..) => StatusCode::BAD_REQUEST, + ApiError::ImageError(..) => StatusCode::BAD_REQUEST, } } - fn error_response(&self) -> actix_web::HttpResponse { - actix_web::HttpResponse::build(self.status_code()).json( + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).json( crate::models::error::ApiError { error: match self { ApiError::Env(..) => "environment_error", diff --git a/src/routes/updates.rs b/src/routes/updates.rs index 585dc5bb1..b0ad0a0ea 100644 --- a/src/routes/updates.rs +++ b/src/routes/updates.rs @@ -12,6 +12,10 @@ use crate::util::auth::{ use super::ApiError; +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service(forge_updates); +} + #[get("{id}/forge_updates.json")] pub async fn forge_updates( req: HttpRequest, diff --git a/src/routes/v1/mod.rs b/src/routes/v1/mod.rs deleted file mode 100644 index e9e50c615..000000000 --- a/src/routes/v1/mod.rs +++ /dev/null @@ -1,95 +0,0 @@ -use actix_web::{dev::Service, http::Method, web, HttpResponse}; -use chrono::{Timelike, Utc}; -use futures::FutureExt; - -mod mods; -mod tags; -mod teams; -mod users; -mod versions; - -pub fn v1_config(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("api/v1") - .wrap_fn(|req, srv| { - let time = Utc::now(); - - if req.method() == Method::GET && time.hour12().1 < 6 && time.minute() % 10 < 5 { - srv.call(req).boxed_local() - } else { - async { - Ok( - req.into_response( - HttpResponse::Gone() - .content_type("application/json") - .body(r#"{"error":"api_deprecated","description":"You are using an application that uses an outdated version of Modrinth's API. Please either update it or switch to another application. For developers: https://docs.modrinth.com/docs/migrations/v1-to-v2/"}"#) - ) - ) - }.boxed_local() - } - }) - .configure(tags_config) - .configure(mods_config) - .configure(versions_config) - .configure(teams_config) - .configure(users_config) - .configure(notifications_config), - ); -} - -pub fn tags_config(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("tag") - .service(tags::category_list) - .service(tags::loader_list) - .service(tags::game_version_list) - .service(super::tags::license_list) - .service(super::tags::report_type_list), - ); -} - -pub fn mods_config(cfg: &mut web::ServiceConfig) { - cfg.service(mods::mod_search); - cfg.service(mods::mods_get); - - cfg.service( - web::scope("mod") - .service(mods::mod_get) - .service(web::scope("{mod_id}").service(versions::version_list)), - ); -} - -pub fn versions_config(cfg: &mut web::ServiceConfig) { - cfg.service(versions::versions_get); - cfg.service(web::scope("version").service(versions::version_get)); - cfg.service( - web::scope("version_file") - .service(super::version_file::get_version_from_hash), - ); -} - -pub fn users_config(cfg: &mut web::ServiceConfig) { - cfg.service(super::users::user_auth_get); - - cfg.service(super::users::users_get); - cfg.service( - web::scope("user") - .service(super::users::user_get) - .service(users::mods_list) - .service(super::users::user_notifications) - .service(users::user_follows), - ); -} - -pub fn teams_config(cfg: &mut web::ServiceConfig) { - cfg.service(web::scope("team").service(teams::team_members_get)); -} - -pub fn notifications_config(cfg: &mut web::ServiceConfig) { - cfg.service(super::notifications::notifications_get); - - cfg.service( - web::scope("notification") - .service(super::notifications::notification_get), - ); -} diff --git a/src/routes/v1/mods.rs b/src/routes/v1/mods.rs deleted file mode 100644 index d4f49b552..000000000 --- a/src/routes/v1/mods.rs +++ /dev/null @@ -1,144 +0,0 @@ -use crate::models::projects::SearchRequest; -use crate::routes::projects::ProjectIds; -use crate::routes::ApiError; -use crate::search::{search_for_project, SearchConfig, SearchError}; -use crate::util::auth::{get_user_from_headers, is_authorized}; -use crate::{database, models}; -use actix_web::{get, web, HttpRequest, HttpResponse}; -use serde::{Deserialize, Serialize}; -use sqlx::PgPool; - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct ResultSearchMod { - pub mod_id: String, - pub slug: Option, - pub author: String, - pub title: String, - pub description: String, - pub categories: Vec, - pub versions: Vec, - pub downloads: i32, - pub follows: i32, - pub page_url: String, - pub icon_url: String, - pub author_url: String, - pub date_created: String, - pub date_modified: String, - pub latest_version: String, - pub license: String, - pub client_side: String, - pub server_side: String, - pub host: String, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct SearchResults { - pub hits: Vec, - pub offset: usize, - pub limit: usize, - pub total_hits: usize, -} - -#[get("mod")] -pub async fn mod_search( - web::Query(info): web::Query, - config: web::Data, -) -> Result { - let results = search_for_project(&info, &config).await?; - Ok(HttpResponse::Ok().json(SearchResults { - hits: results - .hits - .into_iter() - .map(|x| ResultSearchMod { - mod_id: format!("local-{}", x.project_id), - slug: x.slug, - author: x.author.clone(), - title: format!("[STOP USING API v1] {}", x.title), - description: format!("[STOP USING API v1] {}", x.description), - categories: x.categories, - versions: x.versions, - downloads: x.downloads, - follows: x.follows, - page_url: format!("https://modrinth.com/mod/{}", x.project_id), - icon_url: x.icon_url, - author_url: format!("https://modrinth.com/user/{}", x.author), - date_created: x.date_created, - date_modified: x.date_modified, - latest_version: x.latest_version, - license: x.license, - client_side: x.client_side, - server_side: x.server_side, - host: "modrinth".to_string(), - }) - .collect(), - offset: results.offset, - limit: results.limit, - total_hits: results.total_hits, - })) -} - -#[get("{id}")] -pub async fn mod_get( - req: HttpRequest, - info: web::Path<(String,)>, - pool: web::Data, -) -> Result { - let string = info.into_inner().0; - - let project_data = - database::models::Project::get_full_from_slug_or_project_id( - &string, &**pool, - ) - .await?; - - let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); - - if let Some(mut data) = project_data { - if is_authorized(&data.inner, &user_option, &pool).await? { - data.inner.title = - format!("[STOP USING API v1] {}", data.inner.title); - data.inner.description = - format!("[STOP USING API v1] {}", data.inner.description); - data.inner.body = - format!("# STOP USING API v1 - whatever application you're using right now is likely deprecated or abandoned\n{}", data.inner.body); - return Ok( - HttpResponse::Ok().json(models::projects::Project::from(data)) - ); - } - } - Ok(HttpResponse::NotFound().body("")) -} - -#[get("mods")] -pub async fn mods_get( - req: HttpRequest, - ids: web::Query, - pool: web::Data, -) -> Result { - let project_ids: Vec = - serde_json::from_str::>(&ids.ids)? - .into_iter() - .map(|x| x.into()) - .collect(); - - let projects_data = - database::models::Project::get_many_full(&project_ids, &**pool).await?; - - let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); - - let mut projects = Vec::with_capacity(projects_data.len()); - - // can't use `map` and `collect` here since `is_authorized` must be async - for mut proj in projects_data { - if is_authorized(&proj.inner, &user_option, &pool).await? { - proj.inner.title = - format!("[STOP USING API v1] {}", proj.inner.title); - proj.inner.description = - format!("[STOP USING API v1] {}", proj.inner.description); - proj.inner.body = - format!("# STOP USING API v1 - whatever application you're using right now is likely deprecated or abandoned\n{}", proj.inner.body); - projects.push(crate::models::projects::Project::from(proj)) - } - } - Ok(HttpResponse::Ok().json(projects)) -} diff --git a/src/routes/v1/tags.rs b/src/routes/v1/tags.rs deleted file mode 100644 index 18abb65dd..000000000 --- a/src/routes/v1/tags.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::database::models::categories::{Category, GameVersion, Loader}; -use crate::routes::ApiError; -use actix_web::{get, web, HttpResponse}; -use sqlx::PgPool; - -#[get("category")] -pub async fn category_list( - pool: web::Data, -) -> Result { - let results = Category::list(&**pool) - .await? - .into_iter() - .filter(|x| &*x.project_type == "mod") - .map(|x| x.category) - .collect::>(); - Ok(HttpResponse::Ok().json(results)) -} - -#[get("loader")] -pub async fn loader_list( - pool: web::Data, -) -> Result { - let results = Loader::list(&**pool) - .await? - .into_iter() - .filter(|x| x.supported_project_types.contains(&"mod".to_string())) - .map(|x| x.loader) - .collect::>(); - - Ok(HttpResponse::Ok().json(results)) -} - -#[derive(serde::Deserialize)] -pub struct GameVersionQueryData { - #[serde(rename = "type")] - type_: Option, - major: Option, -} - -#[get("game_version")] -pub async fn game_version_list( - pool: web::Data, - query: web::Query, -) -> Result { - if query.type_.is_some() || query.major.is_some() { - let results = GameVersion::list_filter( - query.type_.as_deref(), - query.major, - &**pool, - ) - .await? - .into_iter() - .map(|x| x.version) - .collect::>(); - Ok(HttpResponse::Ok().json(results)) - } else { - let results = GameVersion::list(&**pool) - .await? - .into_iter() - .map(|x| x.version) - .collect::>(); - Ok(HttpResponse::Ok().json(results)) - } -} diff --git a/src/routes/v1/teams.rs b/src/routes/v1/teams.rs deleted file mode 100644 index 24489a1dc..000000000 --- a/src/routes/v1/teams.rs +++ /dev/null @@ -1,78 +0,0 @@ -use crate::models::teams::{Permissions, TeamId}; -use crate::models::users::UserId; -use crate::routes::ApiError; -use crate::util::auth::get_user_from_headers; -use actix_web::{get, web, HttpRequest, HttpResponse}; -use serde::{Deserialize, Serialize}; -use sqlx::PgPool; - -/// A member of a team -#[derive(Serialize, Deserialize, Clone)] -pub struct TeamMember { - /// The ID of the team this team member is a member of - pub team_id: TeamId, - /// The ID of the user associated with the member - pub user_id: UserId, - /// The role of the user in the team - pub role: String, - /// A bitset containing the user's permissions in this team - pub permissions: Option, - /// Whether the user has joined the team or is just invited to it - pub accepted: bool, -} - -#[get("{id}/members")] -pub async fn team_members_get( - req: HttpRequest, - info: web::Path<(TeamId,)>, - pool: web::Data, -) -> Result { - let id = info.into_inner().0; - let members_data = - crate::database::models::TeamMember::get_from_team(id.into(), &**pool) - .await?; - - let current_user = get_user_from_headers(req.headers(), &**pool).await.ok(); - - if let Some(user) = current_user { - let team_member = - crate::database::models::TeamMember::get_from_user_id( - id.into(), - user.id.into(), - &**pool, - ) - .await - .map_err(ApiError::Database)?; - - if team_member.is_some() { - let team_members: Vec = members_data - .into_iter() - .map(|data| TeamMember { - team_id: id, - user_id: data.user_id.into(), - role: data.role, - permissions: Some(data.permissions), - accepted: data.accepted, - }) - .collect(); - - return Ok(HttpResponse::Ok().json(team_members)); - } - } - - let mut team_members: Vec = Vec::new(); - - for team_member in members_data { - if team_member.accepted { - team_members.push(TeamMember { - team_id: id, - user_id: team_member.user_id.into(), - role: team_member.role, - permissions: None, - accepted: team_member.accepted, - }) - } - } - - Ok(HttpResponse::Ok().json(team_members)) -} diff --git a/src/routes/v1/users.rs b/src/routes/v1/users.rs deleted file mode 100644 index a3389d2aa..000000000 --- a/src/routes/v1/users.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::database::models::User; -use crate::models::ids::UserId; -use crate::models::projects::ProjectId; -use crate::routes::ApiError; -use crate::util::auth::get_user_from_headers; -use actix_web::web; -use actix_web::{get, HttpRequest, HttpResponse}; -use sqlx::PgPool; - -#[get("{user_id}/mods")] -pub async fn mods_list( - req: HttpRequest, - info: web::Path<(String,)>, - pool: web::Data, -) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await.ok(); - - let id_option = crate::database::models::User::get_id_from_username_or_id( - &info.into_inner().0, - &**pool, - ) - .await?; - - if let Some(id) = id_option { - let user_id: UserId = id.into(); - - let can_view_private = user - .map(|y| y.role.is_mod() || y.id == user_id) - .unwrap_or(false); - - let project_data = User::get_projects(id, &**pool).await?; - - let response: Vec<_> = - crate::database::Project::get_many(&project_data, &**pool) - .await? - .into_iter() - .filter(|x| can_view_private || x.status.is_approved()) - .map(|x| x.id.into()) - .collect::>(); - - Ok(HttpResponse::Ok().json(response)) - } else { - Ok(HttpResponse::NotFound().body("")) - } -} - -#[get("{id}/follows")] -pub async fn user_follows( - req: HttpRequest, - info: web::Path<(String,)>, - pool: web::Data, -) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; - let id_option = crate::database::models::User::get_id_from_username_or_id( - &info.into_inner().0, - &**pool, - ) - .await?; - - if let Some(id) = id_option { - if !user.role.is_admin() && user.id != id.into() { - return Err(ApiError::CustomAuthentication( - "You do not have permission to see the projects this user follows!".to_string(), - )); - } - - use futures::TryStreamExt; - - let projects: Vec = sqlx::query!( - " - SELECT mf.mod_id FROM mod_follows mf - WHERE mf.follower_id = $1 - ", - id as crate::database::models::ids::UserId, - ) - .fetch_many(&**pool) - .try_filter_map(|e| async { - Ok(e.right().map(|m| ProjectId(m.mod_id as u64))) - }) - .try_collect::>() - .await?; - - Ok(HttpResponse::Ok().json(projects)) - } else { - Ok(HttpResponse::NotFound().body("")) - } -} diff --git a/src/routes/v1/versions.rs b/src/routes/v1/versions.rs deleted file mode 100644 index 7d2a62e55..000000000 --- a/src/routes/v1/versions.rs +++ /dev/null @@ -1,195 +0,0 @@ -use crate::database; -use crate::models::ids::{ProjectId, UserId, VersionId}; -use crate::models::projects::{ - Dependency, GameVersion, Loader, Version, VersionFile, VersionType, -}; -use crate::routes::versions::{VersionIds, VersionListFilters}; -use crate::routes::ApiError; -use actix_web::{get, web, HttpResponse}; -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use sqlx::PgPool; - -/// A specific version of a mod -#[derive(Serialize, Deserialize)] -pub struct LegacyVersion { - pub id: VersionId, - pub mod_id: ProjectId, - pub author_id: UserId, - pub featured: bool, - pub name: String, - pub version_number: String, - pub changelog: String, - pub changelog_url: Option, - pub date_published: DateTime, - pub downloads: u32, - pub version_type: VersionType, - pub files: Vec, - pub dependencies: Vec, - pub game_versions: Vec, - pub loaders: Vec, -} - -fn convert_to_legacy(version: Version) -> LegacyVersion { - LegacyVersion { - id: version.id, - mod_id: version.project_id, - author_id: version.author_id, - featured: version.featured, - name: format!("[STOP USING API v1] {}", version.name), - version_number: version.version_number, - changelog: format!("# STOP USING API v1 - whatever application you're using right now is likely deprecated or abandoned\n{}", version.changelog), - changelog_url: None, - date_published: version.date_published, - downloads: version.downloads, - version_type: version.version_type, - files: version.files, - dependencies: version.dependencies, - game_versions: version.game_versions, - loaders: version.loaders, - } -} - -#[get("version")] -pub async fn version_list( - info: web::Path<(String,)>, - web::Query(filters): web::Query, - pool: web::Data, -) -> Result { - let string = info.into_inner().0; - - let result = database::models::Project::get_from_slug_or_project_id( - &string, &**pool, - ) - .await?; - - if let Some(project) = result { - let id = project.id; - - let version_ids = database::models::Version::get_project_versions( - id, - filters - .game_versions - .as_ref() - .map(|x| serde_json::from_str(x).unwrap_or_default()), - filters - .loaders - .as_ref() - .map(|x| serde_json::from_str(x).unwrap_or_default()), - filters.version_type, - filters.limit, - filters.offset, - &**pool, - ) - .await?; - - let mut versions = - database::models::Version::get_many_full(&version_ids, &**pool) - .await?; - - let mut response = versions - .iter() - .cloned() - .filter(|version| { - filters - .featured - .map(|featured| featured == version.inner.featured) - .unwrap_or(true) - }) - .map(Version::from) - .map(convert_to_legacy) - .collect::>(); - - versions.sort_by(|a, b| { - b.inner.date_published.cmp(&a.inner.date_published) - }); - - // Attempt to populate versions with "auto featured" versions - if response.is_empty() - && !versions.is_empty() - && filters.featured.unwrap_or(false) - { - let loaders = - database::models::categories::Loader::list(&**pool).await?; - let game_versions = - database::models::categories::GameVersion::list_filter( - None, - Some(true), - &**pool, - ) - .await?; - - let mut joined_filters = Vec::new(); - for game_version in &game_versions { - for loader in &loaders { - joined_filters.push((game_version, loader)) - } - } - - joined_filters.into_iter().for_each(|filter| { - versions - .iter() - .find(|version| { - version.game_versions.contains(&filter.0.version) - && version.loaders.contains(&filter.1.loader) - }) - .map(|version| { - response.push(convert_to_legacy(Version::from( - version.clone(), - ))) - }) - .unwrap_or(()); - }); - - if response.is_empty() { - versions.into_iter().for_each(|version| { - response.push(convert_to_legacy(Version::from(version))) - }); - } - } - - response.sort_by(|a, b| b.date_published.cmp(&a.date_published)); - response.dedup_by(|a, b| a.id == b.id); - - Ok(HttpResponse::Ok().json(response)) - } else { - Ok(HttpResponse::NotFound().body("")) - } -} - -#[get("versions")] -pub async fn versions_get( - ids: web::Query, - pool: web::Data, -) -> Result { - let version_ids = serde_json::from_str::>(&ids.ids)? - .into_iter() - .map(|x| x.into()) - .collect::>(); - let versions_data = - database::models::Version::get_many_full(&version_ids, &**pool).await?; - - let mut versions = Vec::new(); - - for version_data in versions_data { - versions.push(convert_to_legacy(Version::from(version_data))); - } - - Ok(HttpResponse::Ok().json(versions)) -} - -#[get("{version_id}")] -pub async fn version_get( - info: web::Path<(VersionId,)>, - pool: web::Data, -) -> Result { - let id = info.into_inner().0; - let version_data = - database::models::Version::get_full(id.into(), &**pool).await?; - - if let Some(data) = version_data { - Ok(HttpResponse::Ok().json(convert_to_legacy(Version::from(data)))) - } else { - Ok(HttpResponse::NotFound().body("")) - } -} diff --git a/src/routes/admin.rs b/src/routes/v2/admin.rs similarity index 98% rename from src/routes/admin.rs rename to src/routes/v2/admin.rs index 2b54b4ac8..d7a201f15 100644 --- a/src/routes/admin.rs +++ b/src/routes/v2/admin.rs @@ -11,6 +11,14 @@ use sqlx::PgPool; use std::collections::HashMap; use std::sync::Arc; +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("admin") + .service(count_download) + .service(process_payout), + ); +} + #[derive(Deserialize)] pub struct DownloadBody { pub url: String, diff --git a/src/routes/auth.rs b/src/routes/v2/auth.rs similarity index 100% rename from src/routes/auth.rs rename to src/routes/v2/auth.rs diff --git a/src/routes/midas.rs b/src/routes/v2/midas.rs similarity index 97% rename from src/routes/midas.rs rename to src/routes/v2/midas.rs index 5f5c7a62f..e191c0cca 100644 --- a/src/routes/midas.rs +++ b/src/routes/v2/midas.rs @@ -9,6 +9,15 @@ use serde::Deserialize; use serde_json::{json, Value}; use sqlx::PgPool; +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("midas") + .service(init_checkout) + .service(init_customer_portal) + .service(handle_stripe_webhook), + ); +} + #[derive(Deserialize)] pub struct CheckoutData { pub price_id: String, diff --git a/src/routes/v2/mod.rs b/src/routes/v2/mod.rs new file mode 100644 index 000000000..97f8b2d54 --- /dev/null +++ b/src/routes/v2/mod.rs @@ -0,0 +1,38 @@ +mod admin; +mod auth; +mod midas; +mod moderation; +mod notifications; +pub(crate) mod project_creation; +mod projects; +mod reports; +mod statistics; +mod tags; +mod teams; +mod users; +mod version_creation; +mod version_file; +mod versions; + +pub use super::ApiError; + +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service( + actix_web::web::scope("v2") + .configure(admin::config) + .configure(auth::config) + .configure(midas::config) + .configure(moderation::config) + .configure(notifications::config) + .configure(project_creation::config) + .configure(projects::config) + .configure(reports::config) + .configure(statistics::config) + .configure(tags::config) + .configure(teams::config) + .configure(users::config) + .configure(version_creation::config) + .configure(version_file::config) + .configure(versions::config), + ); +} diff --git a/src/routes/moderation.rs b/src/routes/v2/moderation.rs similarity index 91% rename from src/routes/moderation.rs rename to src/routes/v2/moderation.rs index 09f2588c0..90458b2d7 100644 --- a/src/routes/moderation.rs +++ b/src/routes/v2/moderation.rs @@ -6,6 +6,15 @@ use actix_web::{delete, get, web, HttpRequest, HttpResponse}; use serde::Deserialize; use sqlx::PgPool; +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("moderation") + .service(get_projects) + .service(ban_user) + .service(unban_user), + ); +} + #[derive(Deserialize)] pub struct ResultCount { #[serde(default = "default_count")] diff --git a/src/routes/notifications.rs b/src/routes/v2/notifications.rs similarity index 94% rename from src/routes/notifications.rs rename to src/routes/v2/notifications.rs index f79406080..a4c14cd62 100644 --- a/src/routes/notifications.rs +++ b/src/routes/v2/notifications.rs @@ -7,6 +7,17 @@ use actix_web::{delete, get, web, HttpRequest, HttpResponse}; use serde::{Deserialize, Serialize}; use sqlx::PgPool; +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service(notifications_get); + cfg.service(notifications_delete); + + cfg.service( + web::scope("notification") + .service(notification_get) + .service(notification_delete), + ); +} + #[derive(Serialize, Deserialize)] pub struct NotificationIds { pub ids: String, diff --git a/src/routes/project_creation.rs b/src/routes/v2/project_creation.rs similarity index 99% rename from src/routes/project_creation.rs rename to src/routes/v2/project_creation.rs index 73c0f1e4f..eeeb276c8 100644 --- a/src/routes/project_creation.rs +++ b/src/routes/v2/project_creation.rs @@ -1,3 +1,4 @@ +use super::version_creation::InitialVersionData; use crate::database::models; use crate::file_hosting::{FileHost, FileHostingError}; use crate::models::error::ApiError; @@ -6,7 +7,6 @@ use crate::models::projects::{ VersionStatus, }; use crate::models::users::UserId; -use crate::routes::version_creation::InitialVersionData; use crate::search::indexing::IndexingError; use crate::util::auth::{get_user_from_headers, AuthenticationError}; use crate::util::routes::read_from_field; @@ -25,6 +25,10 @@ use std::sync::Arc; use thiserror::Error; use validator::Validate; +pub fn config(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(project_create); +} + #[derive(Error, Debug)] pub enum CreateError { #[error("Environment Error")] diff --git a/src/routes/projects.rs b/src/routes/v2/projects.rs similarity index 98% rename from src/routes/projects.rs rename to src/routes/v2/projects.rs index df3238ac5..65c541c80 100644 --- a/src/routes/projects.rs +++ b/src/routes/v2/projects.rs @@ -24,6 +24,30 @@ use sqlx::PgPool; use std::sync::Arc; use validator::Validate; +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service(project_search); + cfg.service(projects_get); + cfg.service(projects_edit); + cfg.service(random_projects_get); + + cfg.service( + web::scope("project") + .service(project_get) + .service(project_get_check) + .service(project_delete) + .service(project_edit) + .service(project_icon_edit) + .service(delete_project_icon) + .service(add_gallery_item) + .service(edit_gallery_item) + .service(delete_gallery_item) + .service(project_follow) + .service(project_unfollow) + .service(project_schedule) + .service(web::scope("{project_id}").service(dependency_list)), + ); +} + #[get("search")] pub async fn project_search( web::Query(info): web::Query, diff --git a/src/routes/reports.rs b/src/routes/v2/reports.rs similarity index 98% rename from src/routes/reports.rs rename to src/routes/v2/reports.rs index c866dc487..ec05860a6 100644 --- a/src/routes/reports.rs +++ b/src/routes/v2/reports.rs @@ -12,6 +12,12 @@ use futures::StreamExt; use serde::Deserialize; use sqlx::PgPool; +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service(reports); + cfg.service(report_create); + cfg.service(delete_report); +} + #[derive(Deserialize)] pub struct CreateReport { pub report_type: String, diff --git a/src/routes/statistics.rs b/src/routes/v2/statistics.rs similarity index 97% rename from src/routes/statistics.rs rename to src/routes/v2/statistics.rs index 2533c85ff..58e068a71 100644 --- a/src/routes/statistics.rs +++ b/src/routes/v2/statistics.rs @@ -3,6 +3,10 @@ use actix_web::{get, web, HttpResponse}; use serde_json::json; use sqlx::PgPool; +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service(get_stats); +} + #[get("statistics")] pub async fn get_stats( pool: web::Data, diff --git a/src/routes/tags.rs b/src/routes/v2/tags.rs similarity index 100% rename from src/routes/tags.rs rename to src/routes/v2/tags.rs diff --git a/src/routes/teams.rs b/src/routes/v2/teams.rs similarity index 97% rename from src/routes/teams.rs rename to src/routes/v2/teams.rs index 15fffceb1..0c18e5f58 100644 --- a/src/routes/teams.rs +++ b/src/routes/v2/teams.rs @@ -12,6 +12,22 @@ use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use sqlx::PgPool; +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service(teams_get); + + cfg.service( + web::scope("team") + .service(team_members_get) + .service(edit_team_member) + .service(transfer_ownership) + .service(add_team_member) + .service(join_team) + .service(remove_team_member), + ); + + cfg.service(web::scope("project").service(team_members_get_project)); +} + #[get("{id}/members")] pub async fn team_members_get_project( req: HttpRequest, diff --git a/src/routes/users.rs b/src/routes/v2/users.rs similarity index 98% rename from src/routes/users.rs rename to src/routes/v2/users.rs index e7072250d..c91a30fff 100644 --- a/src/routes/users.rs +++ b/src/routes/v2/users.rs @@ -22,6 +22,24 @@ use std::sync::Arc; use tokio::sync::Mutex; use validator::Validate; +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service(user_auth_get); + cfg.service(users_get); + + cfg.service( + web::scope("user") + .service(user_get) + .service(projects_list) + .service(user_delete) + .service(user_edit) + .service(user_icon_edit) + .service(user_notifications) + .service(user_follows) + .service(user_payouts) + .service(user_payouts_request), + ); +} + #[get("user")] pub async fn user_auth_get( req: HttpRequest, diff --git a/src/routes/version_creation.rs b/src/routes/v2/version_creation.rs similarity index 99% rename from src/routes/version_creation.rs rename to src/routes/v2/version_creation.rs index f2f68bc73..e720b432e 100644 --- a/src/routes/version_creation.rs +++ b/src/routes/v2/version_creation.rs @@ -1,3 +1,4 @@ +use super::project_creation::{CreateError, UploadedFile}; use crate::database::models; use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::version_item::{ @@ -10,7 +11,6 @@ use crate::models::projects::{ Version, VersionFile, VersionId, VersionStatus, VersionType, }; use crate::models::teams::Permissions; -use crate::routes::project_creation::{CreateError, UploadedFile}; use crate::util::auth::get_user_from_headers; use crate::util::routes::read_from_field; use crate::util::validate::validation_errors_to_string; @@ -26,6 +26,12 @@ use std::collections::HashMap; use std::sync::Arc; use validator::Validate; +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service(version_create); + + cfg.service(web::scope("version").service(upload_file_to_version)); +} + fn default_requested_status() -> VersionStatus { VersionStatus::Listed } diff --git a/src/routes/version_file.rs b/src/routes/v2/version_file.rs similarity index 97% rename from src/routes/version_file.rs rename to src/routes/v2/version_file.rs index 96e94e902..254dfb72f 100644 --- a/src/routes/version_file.rs +++ b/src/routes/v2/version_file.rs @@ -13,6 +13,23 @@ use serde::{Deserialize, Serialize}; use sqlx::PgPool; use std::collections::HashMap; +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("version_file") + .service(delete_file) + .service(get_version_from_hash) + .service(download_version) + .service(get_update_from_hash), + ); + + cfg.service( + web::scope("version_files") + .service(get_versions_from_hashes) + .service(download_files) + .service(update_files), + ); +} + #[derive(Deserialize)] pub struct HashQuery { #[serde(default = "default_algorithm")] diff --git a/src/routes/versions.rs b/src/routes/v2/versions.rs similarity index 98% rename from src/routes/versions.rs rename to src/routes/v2/versions.rs index 9d0b32f5d..ef65a2522 100644 --- a/src/routes/versions.rs +++ b/src/routes/v2/versions.rs @@ -16,6 +16,24 @@ use serde::{Deserialize, Serialize}; use sqlx::PgPool; use validator::Validate; +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service(versions_get); + + cfg.service( + web::scope("version") + .service(version_get) + .service(version_delete) + .service(version_edit) + .service(version_schedule), + ); + + cfg.service( + web::scope("project/{project_id}") + .service(version_list) + .service(version_project_get), + ); +} + #[derive(Serialize, Deserialize, Clone)] pub struct VersionListFilters { pub game_versions: Option, diff --git a/src/routes/v3/mod.rs b/src/routes/v3/mod.rs new file mode 100644 index 000000000..cc7ca6d82 --- /dev/null +++ b/src/routes/v3/mod.rs @@ -0,0 +1,13 @@ +pub use super::ApiError; +use actix_web::{web, HttpResponse}; +use serde_json::json; + +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service(web::scope("v3").route("", web::get().to(hello_world))); +} + +pub async fn hello_world() -> Result { + Ok(HttpResponse::Ok().json(json!({ + "hello": "world", + }))) +} diff --git a/src/util/routes.rs b/src/util/routes.rs index 85c997ba2..8be27f523 100644 --- a/src/util/routes.rs +++ b/src/util/routes.rs @@ -1,4 +1,4 @@ -use crate::routes::project_creation::CreateError; +use crate::routes::v2::project_creation::CreateError; use crate::routes::ApiError; use actix_multipart::Field; use actix_web::web::Payload;