V2 removal and _internal rerouting (#770)

* deleteed v3 exclusive routes

* moved routes around

* fixed linkage that movement broke

* initial merge errors

* fixes
This commit is contained in:
Wyatt Verchere 2023-12-01 10:02:11 -08:00 committed by GitHub
parent 4bbc57b0dc
commit 2d92b08404
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 93 additions and 899 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<PgPool>,
file_host: Data<Arc<dyn FileHost + Send + Sync>>,
redis: Data<RedisPool>,
) -> Result<HttpResponse, super::templates::ErrorPage> {
) -> Result<HttpResponse, crate::auth::templates::ErrorPage> {
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",

View File

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

View File

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

View File

@ -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<String>,
pub version_ids: Option<String>,
pub start_date: Option<DateTime<Utc>>, // defaults to 2 weeks ago
pub end_date: Option<DateTime<Utc>>, // defaults to now
pub resolution_minutes: Option<u32>, // 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<String, u64>,
pub game_version_seconds: HashMap<String, u64>,
pub parent_seconds: HashMap<VersionId, u64>,
}
#[get("playtime")]
pub async fn playtimes_get(
req: HttpRequest,
clickhouse: web::Data<clickhouse::Client>,
data: web::Query<GetData>,
session_queue: web::Data<AuthQueue>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
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<clickhouse::Client>,
data: web::Query<GetData>,
session_queue: web::Data<AuthQueue>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
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<clickhouse::Client>,
data: web::Query<GetData>,
session_queue: web::Data<AuthQueue>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
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<GetData>,
session_queue: web::Data<AuthQueue>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
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<clickhouse::Client>,
data: web::Query<GetData>,
session_queue: web::Data<AuthQueue>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
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<clickhouse::Client>,
data: web::Query<GetData>,
session_queue: web::Data<AuthQueue>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
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
}

View File

@ -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<String>,
}
#[post("collection")]
pub async fn collection_create(
req: HttpRequest,
collection_create_data: web::Json<CollectionCreateData>,
client: Data<PgPool>,
redis: Data<RedisPool>,
session_queue: Data<AuthQueue>,
) -> Result<HttpResponse, CreateError> {
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<CollectionIds>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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<String>,
#[validate(length(min = 3, max = 256))]
pub description: Option<String>,
pub status: Option<CollectionStatus>,
#[validate(length(max = 64))]
pub new_projects: Option<Vec<String>>,
}
#[patch("{id}")]
pub async fn collection_edit(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
new_collection: web::Json<EditCollection>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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<Extension>,
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
payload: web::Payload,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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<PgPool>,
redis: web::Data<RedisPool>,
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::collections::collection_delete(req, info, pool, redis, session_queue).await
}

View File

@ -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<String>, // allow slug or id
pub version_id: Option<VersionId>,
pub thread_message_id: Option<ThreadMessageId>,
pub report_id: Option<ReportId>,
}
#[post("image")]
pub async fn images_add(
req: HttpRequest,
web::Query(data): web::Query<ImageUpload>,
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
payload: web::Payload,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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
}

View File

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

View File

@ -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<NewOrganization>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, CreateError> {
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<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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<OrganizationIds>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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<String>,
#[validate(
length(min = 3, max = 64),
regex = "crate::util::validate::RE_URL_SAFE"
)]
// Title of the organization, also used as slug
pub title: Option<String>,
}
#[patch("{id}")]
pub async fn organizations_edit(
req: HttpRequest,
info: web::Path<(String,)>,
new_organization: web::Json<OrganizationEdit>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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::<Vec<Project>>(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<OrganizationProjectAdd>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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<Extension>,
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
payload: web::Payload,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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<PgPool>,
redis: web::Data<RedisPool>,
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::organizations::delete_organization_icon(
req,
info,
pool.clone(),
redis.clone(),
file_host,
session_queue,
)
.await
}

View File

@ -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<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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(

View File

@ -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<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
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();
}

View File

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

View File

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

View File

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

View File

@ -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<OAuthClientAuthorization> {
let req = TestRequest::get()
.uri("/v3/oauth/authorizations")
.uri("/_internal/oauth/authorizations")
.append_header((AUTHORIZATION, pat))
.to_request();
let resp = self.call(req).await;

View File

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

View File

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