1
0

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
This commit is contained in:
triphora
2023-03-16 14:56:04 -04:00
committed by GitHub
parent 0271337f8e
commit 3c2f144795
28 changed files with 264 additions and 911 deletions

View File

@@ -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())?

View File

@@ -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<String>,
}
#[derive(Default, Debug, Clone, YaSerialize)]
#[yaserde(rename = "project", namespace = "http://maven.apache.org/POM/4.0.0")]
pub struct MavenPom {

View File

@@ -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",

View File

@@ -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,

View File

@@ -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),
);
}

View File

@@ -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<String>,
pub author: String,
pub title: String,
pub description: String,
pub categories: Vec<String>,
pub versions: Vec<String>,
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<ResultSearchMod>,
pub offset: usize,
pub limit: usize,
pub total_hits: usize,
}
#[get("mod")]
pub async fn mod_search(
web::Query(info): web::Query<SearchRequest>,
config: web::Data<SearchConfig>,
) -> Result<HttpResponse, SearchError> {
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<PgPool>,
) -> Result<HttpResponse, ApiError> {
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<ProjectIds>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let project_ids: Vec<database::models::ids::ProjectId> =
serde_json::from_str::<Vec<models::ids::ProjectId>>(&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))
}

View File

@@ -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<PgPool>,
) -> Result<HttpResponse, ApiError> {
let results = Category::list(&**pool)
.await?
.into_iter()
.filter(|x| &*x.project_type == "mod")
.map(|x| x.category)
.collect::<Vec<String>>();
Ok(HttpResponse::Ok().json(results))
}
#[get("loader")]
pub async fn loader_list(
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let results = Loader::list(&**pool)
.await?
.into_iter()
.filter(|x| x.supported_project_types.contains(&"mod".to_string()))
.map(|x| x.loader)
.collect::<Vec<_>>();
Ok(HttpResponse::Ok().json(results))
}
#[derive(serde::Deserialize)]
pub struct GameVersionQueryData {
#[serde(rename = "type")]
type_: Option<String>,
major: Option<bool>,
}
#[get("game_version")]
pub async fn game_version_list(
pool: web::Data<PgPool>,
query: web::Query<GameVersionQueryData>,
) -> Result<HttpResponse, ApiError> {
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::<Vec<String>>();
Ok(HttpResponse::Ok().json(results))
} else {
let results = GameVersion::list(&**pool)
.await?
.into_iter()
.map(|x| x.version)
.collect::<Vec<String>>();
Ok(HttpResponse::Ok().json(results))
}
}

View File

@@ -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<Permissions>,
/// 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<PgPool>,
) -> Result<HttpResponse, ApiError> {
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<TeamMember> = 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<TeamMember> = 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))
}

View File

@@ -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<PgPool>,
) -> Result<HttpResponse, ApiError> {
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::<Vec<ProjectId>>();
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<PgPool>,
) -> Result<HttpResponse, ApiError> {
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<ProjectId> = 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::<Vec<ProjectId>>()
.await?;
Ok(HttpResponse::Ok().json(projects))
} else {
Ok(HttpResponse::NotFound().body(""))
}
}

View File

@@ -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<String>,
pub date_published: DateTime<Utc>,
pub downloads: u32,
pub version_type: VersionType,
pub files: Vec<VersionFile>,
pub dependencies: Vec<Dependency>,
pub game_versions: Vec<GameVersion>,
pub loaders: Vec<Loader>,
}
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<VersionListFilters>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
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::<Vec<_>>();
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<VersionIds>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let version_ids = serde_json::from_str::<Vec<VersionId>>(&ids.ids)?
.into_iter()
.map(|x| x.into())
.collect::<Vec<database::models::VersionId>>();
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<PgPool>,
) -> Result<HttpResponse, ApiError> {
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(""))
}
}

View File

@@ -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,

View File

@@ -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,

38
src/routes/v2/mod.rs Normal file
View File

@@ -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),
);
}

View File

@@ -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")]

View File

@@ -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,

View File

@@ -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")]

View File

@@ -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<SearchRequest>,

View File

@@ -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,

View File

@@ -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<PgPool>,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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")]

View File

@@ -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<String>,

13
src/routes/v3/mod.rs Normal file
View File

@@ -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<HttpResponse, ApiError> {
Ok(HttpResponse::Ok().json(json!({
"hello": "world",
})))
}

View File

@@ -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;