Initial Auth Impl + More Caching (#647)

* Port redis to staging

* redis cache on staging

* add back legacy auth callback

* Begin work on new auth flows

* Finish all auth flows

* Finish base session authentication

* run prep + fix clippy

* make compilation work
This commit is contained in:
Geometrically
2023-07-07 12:20:16 -07:00
committed by GitHub
parent b0057b130e
commit 239214ef92
53 changed files with 6250 additions and 6359 deletions

View File

@@ -1,31 +0,0 @@
use crate::health::status::test_database;
use crate::health::SEARCH_READY;
use actix_web::web::Data;
use actix_web::{get, HttpResponse};
use serde_json::json;
use sqlx::PgPool;
use std::sync::atomic::Ordering;
#[get("/health")]
pub async fn health_get(client: Data<PgPool>) -> HttpResponse {
// Check database connection:
let result = test_database(client).await;
if result.is_err() {
let data = json!({
"ready": false,
"reason": "Database connection error"
});
return HttpResponse::InternalServerError().json(data);
}
if !SEARCH_READY.load(Ordering::Acquire) {
let data = json!({
"ready": false,
"reason": "Indexing is not finished"
});
return HttpResponse::InternalServerError().json(data);
}
HttpResponse::Ok().json(json!({
"ready": true,
"reason": "Everything is OK"
}))
}

View File

@@ -1,9 +1,9 @@
use crate::auth::{get_user_from_headers, is_authorized_version};
use crate::database::models::project_item::QueryProject;
use crate::database::models::version_item::{QueryFile, QueryVersion};
use crate::models::projects::{ProjectId, VersionId};
use crate::routes::ApiError;
use crate::util::auth::{get_user_from_headers, is_authorized_version};
use crate::{database, util::auth::is_authorized};
use crate::{auth::is_authorized, database};
use actix_web::{get, route, web, HttpRequest, HttpResponse};
use sqlx::PgPool;
use std::collections::HashSet;
@@ -66,10 +66,10 @@ pub async fn maven_metadata(
req: HttpRequest,
params: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let project_id = params.into_inner().0;
let project_data =
database::models::Project::get_from_slug_or_project_id(&project_id, &**pool).await?;
let project_data = database::models::Project::get(&project_id, &**pool, &redis).await?;
let data = if let Some(data) = project_data {
data
@@ -77,9 +77,11 @@ pub async fn maven_metadata(
return Ok(HttpResponse::NotFound().body(""));
};
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
let user_option = get_user_from_headers(req.headers(), &**pool, &redis)
.await
.ok();
if !is_authorized(&data, &user_option, &pool).await? {
if !is_authorized(&data.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
}
@@ -90,7 +92,7 @@ pub async fn maven_metadata(
WHERE mod_id = $1 AND status = ANY($2)
ORDER BY date_published ASC
",
data.id as database::models::ids::ProjectId,
data.inner.id as database::models::ids::ProjectId,
&*crate::models::projects::VersionStatus::iterator()
.filter(|x| x.is_listed())
.map(|x| x.to_string())
@@ -118,7 +120,7 @@ pub async fn maven_metadata(
new_versions.push(value);
}
let project_id: ProjectId = data.id.into();
let project_id: ProjectId = data.inner.id.into();
let respdata = Metadata {
group_id: "maven.modrinth".to_string(),
@@ -132,7 +134,7 @@ pub async fn maven_metadata(
versions: Versions {
versions: new_versions,
},
last_updated: data.updated.format("%Y%m%d%H%M%S").to_string(),
last_updated: data.inner.updated.format("%Y%m%d%H%M%S").to_string(),
},
};
@@ -185,10 +187,10 @@ pub async fn version_file(
req: HttpRequest,
params: web::Path<(String, String, String)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let (project_id, vnum, file) = params.into_inner();
let project_data =
database::models::Project::get_full_from_slug_or_project_id(&project_id, &**pool).await?;
let project_data = database::models::Project::get(&project_id, &**pool, &redis).await?;
let project = if let Some(data) = project_data {
data
@@ -196,7 +198,9 @@ pub async fn version_file(
return Ok(HttpResponse::NotFound().body(""));
};
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
let user_option = get_user_from_headers(req.headers(), &**pool, &redis)
.await
.ok();
if !is_authorized(&project.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
@@ -221,7 +225,7 @@ pub async fn version_file(
};
let version = if let Some(version) =
database::models::Version::get_full(database::models::ids::VersionId(vid.id), &**pool)
database::models::Version::get(database::models::ids::VersionId(vid.id), &**pool, &redis)
.await?
{
version
@@ -266,10 +270,10 @@ pub async fn version_file_sha1(
req: HttpRequest,
params: web::Path<(String, String, String)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let (project_id, vnum, file) = params.into_inner();
let project_data =
database::models::Project::get_full_from_slug_or_project_id(&project_id, &**pool).await?;
let project_data = database::models::Project::get(&project_id, &**pool, &redis).await?;
let project = if let Some(data) = project_data {
data
@@ -277,7 +281,9 @@ pub async fn version_file_sha1(
return Ok(HttpResponse::NotFound().body(""));
};
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
let user_option = get_user_from_headers(req.headers(), &**pool, &redis)
.await
.ok();
if !is_authorized(&project.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
@@ -302,7 +308,7 @@ pub async fn version_file_sha1(
};
let version = if let Some(version) =
database::models::Version::get_full(database::models::ids::VersionId(vid.id), &**pool)
database::models::Version::get(database::models::ids::VersionId(vid.id), &**pool, &redis)
.await?
{
version
@@ -321,10 +327,10 @@ pub async fn version_file_sha512(
req: HttpRequest,
params: web::Path<(String, String, String)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let (project_id, vnum, file) = params.into_inner();
let project_data =
database::models::Project::get_full_from_slug_or_project_id(&project_id, &**pool).await?;
let project_data = database::models::Project::get(&project_id, &**pool, &redis).await?;
let project = if let Some(data) = project_data {
data
@@ -332,7 +338,9 @@ pub async fn version_file_sha512(
return Ok(HttpResponse::NotFound().body(""));
};
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
let user_option = get_user_from_headers(req.headers(), &**pool, &redis)
.await
.ok();
if !is_authorized(&project.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
@@ -357,7 +365,7 @@ pub async fn version_file_sha512(
};
let version = if let Some(version) =
database::models::Version::get_full(database::models::ids::VersionId(vid.id), &**pool)
database::models::Version::get(database::models::ids::VersionId(vid.id), &**pool, &redis)
.await?
{
version

View File

@@ -6,7 +6,6 @@ use futures::FutureExt;
pub mod v2;
pub mod v3;
mod health;
mod index;
mod maven;
mod not_found;
@@ -16,7 +15,6 @@ pub use self::not_found::not_found;
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(
@@ -47,7 +45,7 @@ pub enum ApiError {
#[error("Deserialization error: {0}")]
Json(#[from] serde_json::Error),
#[error("Authentication Error: {0}")]
Authentication(#[from] crate::util::auth::AuthenticationError),
Authentication(#[from] crate::auth::AuthenticationError),
#[error("Authentication Error: {0}")]
CustomAuthentication(String),
#[error("Invalid Input: {0}")]
@@ -60,8 +58,6 @@ pub enum ApiError {
Indexing(#[from] crate::search::indexing::IndexingError),
#[error("Ariadne Error: {0}")]
Analytics(String),
#[error("Crypto Error: {0}")]
Crypto(String),
#[error("Payments Error: {0}")]
Payments(String),
#[error("Discord Error: {0}")]
@@ -88,7 +84,6 @@ impl actix_web::ResponseError for ApiError {
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,
@@ -112,7 +107,6 @@ impl actix_web::ResponseError for ApiError {
ApiError::InvalidInput(..) => "invalid_input",
ApiError::Validation(..) => "invalid_input",
ApiError::Analytics(..) => "analytics_error",
ApiError::Crypto(..) => "crypto_error",
ApiError::Payments(..) => "payments_error",
ApiError::DiscordError(..) => "discord_error",
ApiError::Decoding(..) => "decoding_error",

View File

@@ -4,9 +4,9 @@ use actix_web::{get, web, HttpRequest, HttpResponse};
use serde::Serialize;
use sqlx::PgPool;
use crate::auth::{filter_authorized_versions, get_user_from_headers, is_authorized};
use crate::database;
use crate::models::projects::VersionType;
use crate::util::auth::{filter_authorized_versions, get_user_from_headers, is_authorized};
use super::ApiError;
@@ -19,36 +19,36 @@ pub async fn forge_updates(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
const ERROR: &str = "The specified project does not exist!";
let (id,) = info.into_inner();
let project = database::models::Project::get_from_slug_or_project_id(&id, &**pool)
let project = database::models::Project::get(&id, &**pool, &redis)
.await?
.ok_or_else(|| ApiError::InvalidInput(ERROR.to_string()))?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
let user_option = get_user_from_headers(req.headers(), &**pool, &redis)
.await
.ok();
if !is_authorized(&project, &user_option, &pool).await? {
if !is_authorized(&project.inner, &user_option, &pool).await? {
return Err(ApiError::InvalidInput(ERROR.to_string()));
}
let version_ids = database::models::Version::get_project_versions(
project.id,
None,
Some(vec!["forge".to_string()]),
None,
None,
None,
&**pool,
let versions = database::models::Version::get_many(&project.versions, &**pool, &redis).await?;
let mut versions = filter_authorized_versions(
versions
.into_iter()
.filter(|x| x.loaders.iter().any(|y| *y == "forge"))
.collect(),
&user_option,
&pool,
)
.await?;
let versions = database::models::Version::get_many_full(&version_ids, &**pool).await?;
let mut versions = filter_authorized_versions(versions, &user_option, &pool).await?;
versions.sort_by(|a, b| b.date_published.cmp(&a.date_published));
#[derive(Serialize)]

View File

@@ -1,12 +1,10 @@
use crate::database::models::user_item;
use crate::database::models::{User, UserId};
use crate::models::ids::ProjectId;
use crate::models::projects::MonetizationStatus;
use crate::models::users::User;
use crate::routes::ApiError;
use crate::util::auth::{link_or_insert_new_user, MinosNewUser};
use crate::util::guards::admin_key_guard;
use crate::DownloadQueue;
use actix_web::{get, patch, post, web, HttpResponse};
use actix_web::{patch, post, web, HttpResponse};
use chrono::{DateTime, SecondsFormat, Utc};
use rust_decimal::Decimal;
use serde::Deserialize;
@@ -19,110 +17,10 @@ pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("admin")
.service(count_download)
.service(add_minos_user)
.service(edit_github_id)
.service(edit_email)
.service(get_legacy_account)
.service(process_payout),
);
}
// Adds a Minos user to the database
// This is an internal endpoint, and should not be used by applications, only by the Minos backend
#[post("_minos-user-callback", guard = "admin_key_guard")]
pub async fn add_minos_user(
minos_user: web::Json<MinosNewUser>, // getting directly from Kratos rather than Minos, so unparse
client: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let minos_new_user = minos_user.into_inner();
let mut transaction = client.begin().await?;
link_or_insert_new_user(&mut transaction, minos_new_user).await?;
transaction.commit().await?;
Ok(HttpResponse::Ok().finish())
}
// Add or update a user's GitHub ID by their kratos id
// OIDC ids should be kept in Minos, but Github is duplicated in Labrinth for legacy support
// This should not be directly useable by applications, only by the Minos backend
// user id is passed in path, github id is passed in body
#[derive(Deserialize)]
pub struct EditGithubId {
github_id: Option<String>,
}
#[post("_edit_github_id/{kratos_id}", guard = "admin_key_guard")]
pub async fn edit_github_id(
pool: web::Data<PgPool>,
kratos_id: web::Path<String>,
github_id: web::Json<EditGithubId>,
) -> Result<HttpResponse, ApiError> {
let github_id = github_id.into_inner().github_id;
// Parse error if github inner id not a number
let github_id = github_id
.as_ref()
.map(|x| x.parse::<i64>())
.transpose()
.map_err(|_| ApiError::InvalidInput("Github id must be a number".to_string()))?;
let mut transaction = pool.begin().await?;
sqlx::query!(
"
UPDATE users
SET github_id = $1
WHERE kratos_id = $2
",
github_id,
kratos_id.into_inner()
)
.execute(&mut transaction)
.await?;
transaction.commit().await?;
Ok(HttpResponse::Ok().finish())
}
// Update a user's email ID by their kratos id
// email ids should be kept in Minos, but email is duplicated in Labrinth for legacy support (and to avoid Minos calls)
// This should not be directly useable by applications, only by the Minos backend
// user id is passed in path, email is passed in body
#[derive(Deserialize)]
pub struct EditEmail {
email: String,
}
#[post("_edit_email/{kratos_id}", guard = "admin_key_guard")]
pub async fn edit_email(
pool: web::Data<PgPool>,
kratos_id: web::Path<String>,
email: web::Json<EditEmail>,
) -> Result<HttpResponse, ApiError> {
let email = email.into_inner().email;
let mut transaction = pool.begin().await?;
sqlx::query!(
"
UPDATE users
SET email = $1
WHERE kratos_id = $2
",
email,
kratos_id.into_inner()
)
.execute(&mut transaction)
.await?;
transaction.commit().await?;
Ok(HttpResponse::Ok().finish())
}
#[get("_legacy_account/{github_id}", guard = "admin_key_guard")]
pub async fn get_legacy_account(
pool: web::Data<PgPool>,
github_id: web::Path<i32>,
) -> Result<HttpResponse, ApiError> {
let github_id = github_id.into_inner();
let user = user_item::User::get_from_github_id(github_id as u64, &**pool).await?;
let user: Option<User> = user.map(|u| u.into());
Ok(HttpResponse::Ok().json(user))
}
#[derive(Deserialize)]
pub struct DownloadBody {
pub url: String,
@@ -214,6 +112,7 @@ pub struct PayoutData {
#[post("/_process_payout", guard = "admin_key_guard")]
pub async fn process_payout(
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
data: web::Json<PayoutData>,
) -> Result<HttpResponse, ApiError> {
let start: DateTime<Utc> = DateTime::from_utc(
@@ -409,6 +308,8 @@ pub async fn process_payout(
let sum_splits: Decimal = project.team_members.iter().map(|x| x.1).sum();
let sum_tm_splits: Decimal = project.split_team_members.iter().map(|x| x.1).sum();
let mut clear_cache_users = Vec::new();
if sum_splits > Decimal::ZERO {
for (user_id, split) in project.team_members {
let payout: Decimal = data.amount
@@ -445,6 +346,7 @@ pub async fn process_payout(
)
.execute(&mut *transaction)
.await?;
clear_cache_users.push(user_id);
}
}
}
@@ -481,9 +383,19 @@ pub async fn process_payout(
)
.execute(&mut *transaction)
.await?;
clear_cache_users.push(user_id);
}
}
}
User::clear_caches(
&clear_cache_users
.into_iter()
.map(|x| (UserId(x), None))
.collect::<Vec<_>>(),
&redis,
)
.await?;
}
}

View File

@@ -1,214 +0,0 @@
/*!
This auth module is how we allow for authentication within the Modrinth sphere.
It uses a self-hosted Ory Kratos instance on the backend, powered by our Minos backend.
Applications interacting with the authenticated API (a very small portion - notifications, private projects, editing/creating projects
and versions) should include the Ory authentication cookie in their requests. This cookie is set by the Ory Kratos instance and Minos provides function to access these.
In addition, you can use a logged-in-account to generate a PAT.
This token can be passed in as a Bearer token in the Authorization header, as an alternative to a cookie.
This is useful for applications that don't have a frontend, or for applications that need to access the authenticated API on behalf of a user.
Just as a summary: Don't implement this flow in your application!
*/
use crate::database::models::{self, generate_state_id};
use crate::models::error::ApiError;
use crate::models::ids::base62_impl::{parse_base62, to_base62};
use crate::models::ids::DecodingError;
use crate::parse_strings_from_var;
use crate::util::auth::{get_minos_user_from_cookies, AuthenticationError};
use actix_web::http::StatusCode;
use actix_web::web::{scope, Data, Query, ServiceConfig};
use actix_web::{get, HttpRequest, HttpResponse};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use sqlx::postgres::PgPool;
use thiserror::Error;
pub fn config(cfg: &mut ServiceConfig) {
cfg.service(scope("auth").service(auth_callback).service(init));
}
#[derive(Error, Debug)]
pub enum AuthorizationError {
#[error("Environment Error")]
Env(#[from] dotenvy::Error),
#[error("An unknown database error occured: {0}")]
SqlxDatabase(#[from] sqlx::Error),
#[error("Database Error: {0}")]
Database(#[from] crate::database::models::DatabaseError),
#[error("Error while parsing JSON: {0}")]
SerDe(#[from] serde_json::Error),
#[error("Error with communicating to Minos")]
Minos(#[from] reqwest::Error),
#[error("Invalid Authentication credentials")]
InvalidCredentials,
#[error("Authentication Error: {0}")]
Authentication(#[from] crate::util::auth::AuthenticationError),
#[error("Error while decoding Base62")]
Decoding(#[from] DecodingError),
#[error("Invalid callback URL specified")]
Url,
#[error("User exists in Minos but not in Labrinth")]
DatabaseMismatch,
}
impl actix_web::ResponseError for AuthorizationError {
fn status_code(&self) -> StatusCode {
match self {
AuthorizationError::Env(..) => StatusCode::INTERNAL_SERVER_ERROR,
AuthorizationError::SqlxDatabase(..) => StatusCode::INTERNAL_SERVER_ERROR,
AuthorizationError::Database(..) => StatusCode::INTERNAL_SERVER_ERROR,
AuthorizationError::SerDe(..) => StatusCode::BAD_REQUEST,
AuthorizationError::Minos(..) => StatusCode::INTERNAL_SERVER_ERROR,
AuthorizationError::InvalidCredentials => StatusCode::UNAUTHORIZED,
AuthorizationError::Decoding(..) => StatusCode::BAD_REQUEST,
AuthorizationError::Authentication(..) => StatusCode::UNAUTHORIZED,
AuthorizationError::Url => StatusCode::BAD_REQUEST,
AuthorizationError::DatabaseMismatch => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code()).json(ApiError {
error: match self {
AuthorizationError::Env(..) => "environment_error",
AuthorizationError::SqlxDatabase(..) => "database_error",
AuthorizationError::Database(..) => "database_error",
AuthorizationError::SerDe(..) => "invalid_input",
AuthorizationError::Minos(..) => "network_error",
AuthorizationError::InvalidCredentials => "invalid_credentials",
AuthorizationError::Decoding(..) => "decoding_error",
AuthorizationError::Authentication(..) => "authentication_error",
AuthorizationError::Url => "url_error",
AuthorizationError::DatabaseMismatch => "database_mismatch",
},
description: &self.to_string(),
})
}
}
#[derive(Serialize, Deserialize)]
pub struct AuthorizationInit {
pub url: String,
}
#[derive(Serialize, Deserialize)]
pub struct StateResponse {
pub state: String,
}
// Init link takes us to Minos API and calls back to callback endpoint with a code and state
//http://<URL>:8000/api/v1/auth/init?url=https%3A%2F%2Fmodrinth.com%2Fmods
#[get("init")]
pub async fn init(
Query(info): Query<AuthorizationInit>, // callback url
client: Data<PgPool>,
) -> Result<HttpResponse, AuthorizationError> {
let url = url::Url::parse(&info.url).map_err(|_| AuthorizationError::Url)?;
let allowed_callback_urls = parse_strings_from_var("ALLOWED_CALLBACK_URLS").unwrap_or_default();
let domain = url.host_str().ok_or(AuthorizationError::Url)?; // TODO: change back to .domain() (host_str is so we can use 127.0.0.1)
if !allowed_callback_urls.iter().any(|x| domain.ends_with(x)) && domain != "modrinth.com" {
return Err(AuthorizationError::Url);
}
let mut transaction = client.begin().await?;
let state = generate_state_id(&mut transaction).await?;
sqlx::query!(
"
INSERT INTO states (id, url)
VALUES ($1, $2)
",
state.0,
info.url
)
.execute(&mut *transaction)
.await?;
transaction.commit().await?;
let kratos_url = dotenvy::var("KRATOS_URL")?;
let labrinth_url = dotenvy::var("SELF_ADDR")?;
let url = format!(
// Callback URL of initialization is /callback below.
"{kratos_url}/self-service/login/browser?return_to={labrinth_url}/v2/auth/callback?state={}",
to_base62(state.0 as u64)
);
Ok(HttpResponse::TemporaryRedirect()
.append_header(("Location", &*url))
.json(AuthorizationInit { url }))
}
#[get("callback")]
pub async fn auth_callback(
req: HttpRequest,
Query(state): Query<StateResponse>,
client: Data<PgPool>,
) -> Result<HttpResponse, AuthorizationError> {
let mut transaction = client.begin().await?;
let state_id: u64 = parse_base62(&state.state)?;
let result_option = sqlx::query!(
"
SELECT url, expires FROM states
WHERE id = $1
",
state_id as i64
)
.fetch_optional(&mut *transaction)
.await?;
// Extract cookie header from request
let cookie_header = req.headers().get("Cookie");
if let Some(result) = result_option {
if let Some(cookie_header) = cookie_header {
// Extract cookie header to get authenticated user from Minos
let duration: chrono::Duration = result.expires - Utc::now();
if duration.num_seconds() < 0 {
return Err(AuthorizationError::InvalidCredentials);
}
sqlx::query!(
"
DELETE FROM states
WHERE id = $1
",
state_id as i64
)
.execute(&mut *transaction)
.await?;
// Attempt to create a minos user from the cookie header- if this fails, the user is invalid
let minos_user = get_minos_user_from_cookies(
cookie_header
.to_str()
.map_err(|_| AuthenticationError::InvalidCredentials)?,
)
.await?;
let user_result =
models::User::get_from_minos_kratos_id(minos_user.id.clone(), &mut transaction)
.await?;
// Cookies exist, but user does not exist in database, meaning they are invalid
if user_result.is_none() {
return Err(AuthorizationError::DatabaseMismatch);
}
transaction.commit().await?;
// Cookie is attached now, so redirect to the original URL
// Do not re-append cookie header, as it is not needed,
// because all redirects are to various modrinth.com subdomains
Ok(HttpResponse::TemporaryRedirect()
.append_header(("Location", &*result.url))
.json(AuthorizationInit { url: result.url }))
} else {
Err(AuthorizationError::InvalidCredentials)
}
} else {
Err(AuthorizationError::InvalidCredentials)
}
}

View File

@@ -1,325 +0,0 @@
use crate::models::users::UserId;
use crate::routes::ApiError;
use crate::util::auth::get_user_from_headers;
use actix_web::{post, web, HttpRequest, HttpResponse};
use chrono::{DateTime, Duration, NaiveDateTime, Utc};
use hmac::{Hmac, Mac, NewMac};
use itertools::Itertools;
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,
}
#[post("/_stripe-init-checkout")]
pub async fn init_checkout(
req: HttpRequest,
pool: web::Data<PgPool>,
data: web::Json<CheckoutData>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let client = reqwest::Client::new();
#[derive(Deserialize)]
struct Session {
url: Option<String>,
}
let session = client
.post("https://api.stripe.com/v1/checkout/sessions")
.header(
"Authorization",
format!("Bearer {}", dotenvy::var("STRIPE_TOKEN")?),
)
.form(&[
("mode", "subscription"),
("line_items[0][price]", &*data.price_id),
("line_items[0][quantity]", "1"),
("success_url", "https://modrinth.com/welcome-to-midas"),
("cancel_url", "https://modrinth.com/midas"),
("metadata[user_id]", &user.id.to_string()),
])
.send()
.await
.map_err(|_| ApiError::Payments("Error while creating checkout session!".to_string()))?
.json::<Session>()
.await
.map_err(|_| {
ApiError::Payments("Error while deserializing checkout response!".to_string())
})?;
Ok(HttpResponse::Ok().json(json!(
{
"url": session.url
}
)))
}
#[post("/_stripe-init-portal")]
pub async fn init_customer_portal(
req: HttpRequest,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let customer_id = sqlx::query!(
"
SELECT u.stripe_customer_id
FROM users u
WHERE u.id = $1
",
user.id.0 as i64,
)
.fetch_optional(&**pool)
.await?
.and_then(|x| x.stripe_customer_id)
.ok_or_else(|| ApiError::InvalidInput("User is not linked to stripe account!".to_string()))?;
let client = reqwest::Client::new();
#[derive(Deserialize)]
struct Session {
url: Option<String>,
}
let session = client
.post("https://api.stripe.com/v1/billing_portal/sessions")
.header(
"Authorization",
format!("Bearer {}", dotenvy::var("STRIPE_TOKEN")?),
)
.form(&[
("customer", &*customer_id),
("return_url", "https://modrinth.com/settings/billing"),
])
.send()
.await
.map_err(|_| ApiError::Payments("Error while creating billing session!".to_string()))?
.json::<Session>()
.await
.map_err(|_| {
ApiError::Payments("Error while deserializing billing response!".to_string())
})?;
Ok(HttpResponse::Ok().json(json!(
{
"url": session.url
}
)))
}
#[post("/_stripe-webook")]
pub async fn handle_stripe_webhook(
body: String,
req: HttpRequest,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
if let Some(signature_raw) = req
.headers()
.get("Stripe-Signature")
.and_then(|x| x.to_str().ok())
{
let mut timestamp = None;
let mut signature = None;
for val in signature_raw.split(',') {
let key_val = val.split('=').collect_vec();
if key_val.len() == 2 {
if key_val[0] == "v1" {
signature = hex::decode(key_val[1]).ok()
} else if key_val[0] == "t" {
timestamp = key_val[1].parse::<i64>().ok()
}
}
}
if let Some(timestamp) = timestamp {
if let Some(signature) = signature {
type HmacSha256 = Hmac<sha2::Sha256>;
let mut key =
HmacSha256::new_from_slice(dotenvy::var("STRIPE_WEBHOOK_SECRET")?.as_bytes())
.map_err(|_| {
ApiError::Crypto(
"Unable to initialize HMAC instance due to invalid key length!"
.to_string(),
)
})?;
key.update(format!("{timestamp}.{body}").as_bytes());
key.verify(&signature).map_err(|_| {
ApiError::Crypto("Unable to verify webhook signature!".to_string())
})?;
if timestamp < (Utc::now() - Duration::minutes(5)).timestamp()
|| timestamp > (Utc::now() + Duration::minutes(5)).timestamp()
{
return Err(ApiError::Crypto("Webhook signature expired!".to_string()));
}
} else {
return Err(ApiError::Crypto("Missing signature!".to_string()));
}
} else {
return Err(ApiError::Crypto("Missing timestamp!".to_string()));
}
} else {
return Err(ApiError::Crypto("Missing signature header!".to_string()));
}
#[derive(Deserialize)]
struct StripeWebhookBody {
#[serde(rename = "type")]
type_: String,
data: StripeWebhookObject,
}
#[derive(Deserialize)]
struct StripeWebhookObject {
object: Value,
}
let webhook: StripeWebhookBody = serde_json::from_str(&body)?;
#[derive(Deserialize)]
struct CheckoutSession {
customer: String,
metadata: SessionMetadata,
}
#[derive(Deserialize)]
struct SessionMetadata {
user_id: UserId,
}
#[derive(Deserialize)]
struct Invoice {
customer: String,
// paid: bool,
lines: InvoiceLineItems,
}
#[derive(Deserialize)]
struct InvoiceLineItems {
pub data: Vec<InvoiceLineItem>,
}
#[derive(Deserialize)]
struct InvoiceLineItem {
period: Period,
}
#[derive(Deserialize)]
struct Period {
// start: i64,
end: i64,
}
#[derive(Deserialize)]
struct Subscription {
customer: String,
}
let mut transaction = pool.begin().await?;
// TODO: Currently hardcoded to midas-only. When we add more stuff should include price IDs
match &*webhook.type_ {
"checkout.session.completed" => {
let session: CheckoutSession = serde_json::from_value(webhook.data.object)?;
sqlx::query!(
"
UPDATE users
SET stripe_customer_id = $1
WHERE (id = $2)
",
session.customer,
session.metadata.user_id.0 as i64,
)
.execute(&mut *transaction)
.await?;
}
"invoice.paid" => {
let invoice: Invoice = serde_json::from_value(webhook.data.object)?;
if let Some(item) = invoice.lines.data.first() {
let expires: DateTime<Utc> = DateTime::from_utc(
NaiveDateTime::from_timestamp_opt(item.period.end, 0).unwrap_or_default(),
Utc,
) + Duration::days(1);
sqlx::query!(
"
UPDATE users
SET midas_expires = $1, is_overdue = FALSE
WHERE (stripe_customer_id = $2)
",
expires,
invoice.customer,
)
.execute(&mut *transaction)
.await?;
}
}
"invoice.payment_failed" => {
let invoice: Invoice = serde_json::from_value(webhook.data.object)?;
let customer_id = sqlx::query!(
"
SELECT u.id
FROM users u
WHERE u.stripe_customer_id = $1
",
invoice.customer,
)
.fetch_optional(&**pool)
.await?
.map(|x| x.id);
if let Some(user_id) = customer_id {
sqlx::query!(
"
UPDATE users
SET is_overdue = TRUE
WHERE (id = $1)
",
user_id,
)
.execute(&mut *transaction)
.await?;
}
}
"customer.subscription.deleted" => {
let session: Subscription = serde_json::from_value(webhook.data.object)?;
sqlx::query!(
"
UPDATE users
SET stripe_customer_id = NULL, midas_expires = NULL, is_overdue = NULL
WHERE (stripe_customer_id = $1)
",
session.customer,
)
.execute(&mut *transaction)
.await?;
}
_ => {}
};
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
}

View File

@@ -1,6 +1,4 @@
mod admin;
mod auth;
mod midas;
mod moderation;
mod notifications;
mod pats;
@@ -22,20 +20,26 @@ 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(crate::auth::config)
.configure(moderation::config)
.configure(notifications::config)
.configure(pats::config)
.configure(project_creation::config)
// SHOULD CACHE
.configure(projects::config)
.configure(reports::config)
// should cache in future
.configure(statistics::config)
// should cache in future
.configure(tags::config)
// should cache
.configure(teams::config)
.configure(threads::config)
// should cache
.configure(users::config)
// should cache in future
.configure(version_file::config)
// SHOULD CACHE
.configure(versions::config),
);
}

View File

@@ -1,7 +1,7 @@
use super::ApiError;
use crate::auth::check_is_moderator_from_headers;
use crate::database;
use crate::models::projects::ProjectStatus;
use crate::util::auth::check_is_moderator_from_headers;
use actix_web::{get, web, HttpRequest, HttpResponse};
use serde::Deserialize;
use sqlx::PgPool;
@@ -24,9 +24,10 @@ fn default_count() -> i16 {
pub async fn get_projects(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
count: web::Query<ResultCount>,
) -> Result<HttpResponse, ApiError> {
check_is_moderator_from_headers(req.headers(), &**pool).await?;
check_is_moderator_from_headers(req.headers(), &**pool, &redis).await?;
use futures::stream::TryStreamExt;
@@ -45,7 +46,7 @@ pub async fn get_projects(
.try_collect::<Vec<database::models::ProjectId>>()
.await?;
let projects: Vec<_> = database::Project::get_many_full(&project_ids, &**pool)
let projects: Vec<_> = database::Project::get_many_ids(&project_ids, &**pool, &redis)
.await?
.into_iter()
.map(crate::models::projects::Project::from)

View File

@@ -1,8 +1,8 @@
use crate::auth::get_user_from_headers;
use crate::database;
use crate::models::ids::NotificationId;
use crate::models::notifications::Notification;
use crate::routes::ApiError;
use crate::util::auth::get_user_from_headers;
use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
@@ -30,8 +30,9 @@ pub async fn notifications_get(
req: HttpRequest,
web::Query(ids): web::Query<NotificationIds>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
use database::models::notification_item::Notification as DBNotification;
use database::models::NotificationId as DBNotificationId;
@@ -60,8 +61,9 @@ pub async fn notification_get(
req: HttpRequest,
info: web::Path<(NotificationId,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let id = info.into_inner().0;
@@ -84,8 +86,9 @@ pub async fn notification_read(
req: HttpRequest,
info: web::Path<(NotificationId,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let id = info.into_inner().0;
@@ -117,8 +120,9 @@ pub async fn notification_delete(
req: HttpRequest,
info: web::Path<(NotificationId,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let id = info.into_inner().0;
@@ -150,8 +154,9 @@ pub async fn notifications_read(
req: HttpRequest,
web::Query(ids): web::Query<NotificationIds>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let notification_ids = serde_json::from_str::<Vec<NotificationId>>(&ids.ids)?
.into_iter()
@@ -185,8 +190,9 @@ pub async fn notifications_delete(
req: HttpRequest,
web::Query(ids): web::Query<NotificationIds>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let notification_ids = serde_json::from_str::<Vec<NotificationId>>(&ids.ids)?
.into_iter()

View File

@@ -9,10 +9,10 @@ use crate::database;
use crate::database::models::generate_pat_id;
use crate::models::ids::base62_impl::{parse_base62, to_base62};
use crate::auth::get_user_from_headers;
use crate::auth::{generate_pat, PersonalAccessToken};
use crate::models::users::UserId;
use crate::routes::ApiError;
use crate::util::auth::get_user_from_headers;
use crate::util::pat::{generate_pat, PersonalAccessToken};
use actix_web::web::{self, Data, Query};
use actix_web::{delete, get, patch, post, HttpRequest, HttpResponse};
@@ -46,8 +46,13 @@ pub struct ModifyPersonalAccessToken {
// Get all personal access tokens for the given user. Minos/Kratos cookie must be attached for it to work.
// Does not return the actual access token, only the ID + metadata.
#[get("pat")]
pub async fn get_pats(req: HttpRequest, pool: Data<PgPool>) -> Result<HttpResponse, ApiError> {
let user: crate::models::users::User = get_user_from_headers(req.headers(), &**pool).await?;
pub async fn get_pats(
req: HttpRequest,
pool: Data<PgPool>,
redis: Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user: crate::models::users::User =
get_user_from_headers(req.headers(), &**pool, &redis).await?;
let db_user_id: database::models::UserId = database::models::UserId::from(user.id);
let pats = sqlx::query!(
@@ -84,8 +89,10 @@ pub async fn create_pat(
req: HttpRequest,
Query(info): Query<CreatePersonalAccessToken>, // callback url
pool: Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user: crate::models::users::User = get_user_from_headers(req.headers(), &**pool).await?;
let user: crate::models::users::User =
get_user_from_headers(req.headers(), &**pool, &redis).await?;
let db_user_id: database::models::UserId = database::models::UserId::from(user.id);
let mut transaction: sqlx::Transaction<sqlx::Postgres> = pool.begin().await?;
@@ -135,8 +142,10 @@ pub async fn edit_pat(
id: web::Path<String>,
Query(info): Query<ModifyPersonalAccessToken>, // callback url
pool: Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user: crate::models::users::User = get_user_from_headers(req.headers(), &**pool).await?;
let user: crate::models::users::User =
get_user_from_headers(req.headers(), &**pool, &redis).await?;
let pat_id = database::models::PatId(parse_base62(&id)? as i64);
let db_user_id: database::models::UserId = database::models::UserId::from(user.id);
@@ -198,8 +207,10 @@ pub async fn delete_pat(
req: HttpRequest,
id: web::Path<String>,
pool: Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user: crate::models::users::User = get_user_from_headers(req.headers(), &**pool).await?;
let user: crate::models::users::User =
get_user_from_headers(req.headers(), &**pool, &redis).await?;
let pat_id = database::models::PatId(parse_base62(&id)? as i64);
let db_user_id: database::models::UserId = database::models::UserId::from(user.id);

View File

@@ -1,4 +1,5 @@
use super::version_creation::InitialVersionData;
use crate::auth::{get_user_from_headers, AuthenticationError};
use crate::database::models;
use crate::database::models::thread_item::ThreadBuilder;
use crate::file_hosting::{FileHost, FileHostingError};
@@ -10,7 +11,6 @@ use crate::models::projects::{
use crate::models::threads::ThreadType;
use crate::models::users::UserId;
use crate::search::indexing::IndexingError;
use crate::util::auth::{get_user_from_headers_transaction, AuthenticationError};
use crate::util::routes::read_from_field;
use crate::util::validate::validation_errors_to_string;
use actix_multipart::{Field, Multipart};
@@ -270,6 +270,7 @@ pub async fn project_create(
req: HttpRequest,
mut payload: Multipart,
client: Data<PgPool>,
redis: Data<deadpool_redis::Pool>,
file_host: Data<Arc<dyn FileHost + Send + Sync>>,
) -> Result<HttpResponse, CreateError> {
let mut transaction = client.begin().await?;
@@ -282,6 +283,7 @@ pub async fn project_create(
&***file_host,
&mut uploaded_files,
&client,
&redis,
)
.await;
@@ -336,12 +338,13 @@ async fn project_create_inner(
file_host: &dyn FileHost,
uploaded_files: &mut Vec<UploadedFile>,
pool: &PgPool,
redis: &deadpool_redis::Pool,
) -> Result<HttpResponse, CreateError> {
// The base URL for files uploaded to backblaze
let cdn_url = dotenvy::var("CDN_URL")?;
// The currently logged in user
let current_user = get_user_from_headers_transaction(req.headers(), &mut *transaction).await?;
let current_user = get_user_from_headers(req.headers(), pool, redis).await?;
let project_id: ProjectId = models::generate_project_id(transaction).await?.into();

View File

@@ -1,3 +1,4 @@
use crate::auth::{filter_authorized_projects, get_user_from_headers, is_authorized};
use crate::database;
use crate::database::models::notification_item::NotificationBuilder;
use crate::database::models::thread_item::ThreadMessageBuilder;
@@ -12,7 +13,6 @@ use crate::models::teams::Permissions;
use crate::models::threads::MessageBody;
use crate::routes::ApiError;
use crate::search::{search_for_project, SearchConfig, SearchError};
use crate::util::auth::{filter_authorized_projects, get_user_from_headers, is_authorized};
use crate::util::routes::read_from_payload;
use crate::util::validate::validation_errors_to_string;
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
@@ -74,6 +74,7 @@ pub struct RandomProjects {
pub async fn random_projects_get(
web::Query(count): web::Query<RandomProjects>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
count
.validate()
@@ -94,7 +95,7 @@ pub async fn random_projects_get(
.try_collect::<Vec<_>>()
.await?;
let projects_data = database::models::Project::get_many_full(&project_ids, &**pool)
let projects_data = database::models::Project::get_many_ids(&project_ids, &**pool, &redis)
.await?
.into_iter()
.map(Project::from)
@@ -113,16 +114,14 @@ pub async fn projects_get(
req: HttpRequest,
web::Query(ids): web::Query<ProjectIds>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let project_ids: Vec<database::models::ids::ProjectId> =
serde_json::from_str::<Vec<ProjectId>>(&ids.ids)?
.into_iter()
.map(|x| x.into())
.collect();
let ids = serde_json::from_str::<Vec<&str>>(&ids.ids)?;
let projects_data = database::models::Project::get_many(&ids, &**pool, &redis).await?;
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 user_option = get_user_from_headers(req.headers(), &**pool, &redis)
.await
.ok();
let projects = filter_authorized_projects(projects_data, &user_option, &pool).await?;
@@ -134,13 +133,15 @@ pub async fn project_get(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> 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 project_data = database::models::Project::get(&string, &**pool, &redis).await?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
let user_option = get_user_from_headers(req.headers(), &**pool, &redis)
.await
.ok();
if let Some(data) = project_data {
if is_authorized(&data.inner, &user_option, &pool).await? {
@@ -155,52 +156,15 @@ pub async fn project_get(
pub async fn project_get_check(
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let slug = info.into_inner().0;
let id_option = parse_base62(&slug).ok();
let project_data = database::models::Project::get(&slug, &**pool, &redis).await?;
let id = if let Some(id) = id_option {
let id = sqlx::query!(
"
SELECT id FROM mods
WHERE id = $1
",
id as i64
)
.fetch_optional(&**pool)
.await?;
if id.is_none() {
sqlx::query!(
"
SELECT id FROM mods
WHERE slug = LOWER($1)
",
&slug
)
.fetch_optional(&**pool)
.await?
.map(|x| x.id)
} else {
id.map(|x| x.id)
}
} else {
sqlx::query!(
"
SELECT id FROM mods
WHERE slug = LOWER($1)
",
&slug
)
.fetch_optional(&**pool)
.await?
.map(|x| x.id)
};
if let Some(id) = id {
if let Some(project) = project_data {
Ok(HttpResponse::Ok().json(json! ({
"id": models::ids::ProjectId(id as u64)
"id": models::ids::ProjectId::from(project.inner.id)
})))
} else {
Ok(HttpResponse::NotFound().body(""))
@@ -218,52 +182,23 @@ pub async fn dependency_list(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0;
let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool).await?;
let result = database::models::Project::get(&string, &**pool, &redis).await?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
let user_option = get_user_from_headers(req.headers(), &**pool, &redis)
.await
.ok();
if let Some(project) = result {
if !is_authorized(&project, &user_option, &pool).await? {
if !is_authorized(&project.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
}
let id = project.id;
use futures::stream::TryStreamExt;
let dependencies = sqlx::query!(
"
SELECT d.dependency_id, COALESCE(vd.mod_id, 0) mod_id, d.mod_dependency_id
FROM versions v
INNER JOIN dependencies d ON d.dependent_id = v.id
LEFT JOIN versions vd ON d.dependency_id = vd.id
WHERE v.mod_id = $1
",
id as database::models::ProjectId
)
.fetch_many(&**pool)
.try_filter_map(|e| async {
Ok(e.right().map(|x| {
(
x.dependency_id.map(database::models::VersionId),
if x.mod_id == Some(0) {
None
} else {
x.mod_id.map(database::models::ProjectId)
},
x.mod_dependency_id.map(database::models::ProjectId),
)
}))
})
.try_collect::<Vec<(
Option<database::models::VersionId>,
Option<database::models::ProjectId>,
Option<database::models::ProjectId>,
)>>()
.await?;
let dependencies =
database::Project::get_dependencies(project.inner.id, &**pool, &redis).await?;
let project_ids = dependencies
.iter()
@@ -285,8 +220,8 @@ pub async fn dependency_list(
.filter_map(|x| x.0)
.collect::<Vec<database::models::VersionId>>();
let (projects_result, versions_result) = futures::future::try_join(
database::Project::get_many_full(&project_ids, &**pool),
database::Version::get_many_full(&dep_version_ids, &**pool),
database::Project::get_many_ids(&project_ids, &**pool, &redis),
database::Version::get_many(&dep_version_ids, &**pool, &redis),
)
.await?;
@@ -417,16 +352,16 @@ pub async fn project_edit(
pool: web::Data<PgPool>,
config: web::Data<SearchConfig>,
new_project: web::Json<EditProject>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
new_project
.validate()
.map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
let string = info.into_inner().0;
let result =
database::models::Project::get_full_from_slug_or_project_id(&string, &**pool).await?;
let result = database::models::Project::get(&string, &**pool, &redis).await?;
if let Some(project_item) = result {
let id = project_item.inner.id;
@@ -889,7 +824,7 @@ pub async fn project_edit(
// Make sure the new slug is different from the old one
// We are able to unwrap here because the slug is always set
if !slug.eq(&project_item.inner.slug.unwrap_or_default()) {
if !slug.eq(&project_item.inner.slug.clone().unwrap_or_default()) {
let results = sqlx::query!(
"
SELECT EXISTS(SELECT 1 FROM mods WHERE slug = LOWER($1))
@@ -1151,6 +1086,14 @@ pub async fn project_edit(
.await?;
}
database::models::Project::clear_cache(
project_item.inner.id,
project_item.inner.slug,
None,
&redis,
)
.await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {
@@ -1232,8 +1175,9 @@ pub async fn projects_edit(
web::Query(ids): web::Query<ProjectIds>,
pool: web::Data<PgPool>,
bulk_edit_project: web::Json<BulkEditProject>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
bulk_edit_project
.validate()
@@ -1245,7 +1189,8 @@ pub async fn projects_edit(
.map(|x| x.into())
.collect();
let projects_data = database::models::Project::get_many_full(&project_ids, &**pool).await?;
let projects_data =
database::models::Project::get_many_ids(&project_ids, &**pool, &redis).await?;
if let Some(id) = project_ids
.iter()
@@ -1262,7 +1207,7 @@ pub async fn projects_edit(
.map(|x| x.inner.team_id)
.collect::<Vec<database::models::TeamId>>();
let team_members =
database::models::TeamMember::get_from_team_full_many(&team_ids, &**pool).await?;
database::models::TeamMember::get_from_team_full_many(&team_ids, &**pool, &redis).await?;
let categories = database::models::categories::Category::list(&**pool).await?;
let donation_platforms = database::models::categories::DonationPlatform::list(&**pool).await?;
@@ -1538,6 +1483,9 @@ pub async fn projects_edit(
.execute(&mut *transaction)
.await?;
}
database::models::Project::clear_cache(project.inner.id, project.inner.slug, None, &redis)
.await?;
}
transaction.commit().await?;
@@ -1556,9 +1504,10 @@ pub async fn project_schedule(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
scheduling_data: web::Json<SchedulingData>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
if scheduling_data.time < Utc::now() {
return Err(ApiError::InvalidInput(
@@ -1573,11 +1522,11 @@ pub async fn project_schedule(
}
let string = info.into_inner().0;
let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool).await?;
let result = database::models::Project::get(&string, &**pool, &redis).await?;
if let Some(project_item) = result {
let team_member = database::models::TeamMember::get_from_user_id(
project_item.team_id,
project_item.inner.team_id,
user.id.into(),
&**pool,
)
@@ -1601,11 +1550,19 @@ pub async fn project_schedule(
",
ProjectStatus::Scheduled.as_str(),
scheduling_data.time,
project_item.id as database::models::ids::ProjectId,
project_item.inner.id as database::models::ids::ProjectId,
)
.execute(&**pool)
.await?;
database::models::Project::clear_cache(
project_item.inner.id,
project_item.inner.slug,
None,
&redis,
)
.await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
@@ -1623,15 +1580,16 @@ pub async fn project_icon_edit(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
mut payload: web::Payload,
) -> Result<HttpResponse, ApiError> {
if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
let cdn_url = dotenvy::var("CDN_URL")?;
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let string = info.into_inner().0;
let project_item = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
let project_item = database::models::Project::get(&string, &**pool, &redis)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The specified project does not exist!".to_string())
@@ -1639,7 +1597,7 @@ pub async fn project_icon_edit(
if !user.role.is_mod() {
let team_member = database::models::TeamMember::get_from_user_id(
project_item.team_id,
project_item.inner.team_id,
user.id.into(),
&**pool,
)
@@ -1656,7 +1614,7 @@ pub async fn project_icon_edit(
}
}
if let Some(icon) = project_item.icon_url {
if let Some(icon) = project_item.inner.icon_url {
let name = icon.split(&format!("{cdn_url}/")).nth(1);
if let Some(icon_path) = name {
@@ -1670,7 +1628,7 @@ pub async fn project_icon_edit(
let color = crate::util::img::get_color_from_img(&bytes)?;
let hash = sha1::Sha1::from(&bytes).hexdigest();
let project_id: ProjectId = project_item.id.into();
let project_id: ProjectId = project_item.inner.id.into();
let upload_data = file_host
.upload_file(
content_type,
@@ -1689,11 +1647,19 @@ pub async fn project_icon_edit(
",
format!("{}/{}", cdn_url, upload_data.file_name),
color.map(|x| x as i32),
project_item.id as database::models::ids::ProjectId,
project_item.inner.id as database::models::ids::ProjectId,
)
.execute(&mut *transaction)
.await?;
database::models::Project::clear_cache(
project_item.inner.id,
project_item.inner.slug,
None,
&redis,
)
.await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
@@ -1710,12 +1676,13 @@ pub async fn delete_project_icon(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let string = info.into_inner().0;
let project_item = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
let project_item = database::models::Project::get(&string, &**pool, &redis)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The specified project does not exist!".to_string())
@@ -1723,7 +1690,7 @@ pub async fn delete_project_icon(
if !user.role.is_mod() {
let team_member = database::models::TeamMember::get_from_user_id(
project_item.team_id,
project_item.inner.team_id,
user.id.into(),
&**pool,
)
@@ -1741,7 +1708,7 @@ pub async fn delete_project_icon(
}
let cdn_url = dotenvy::var("CDN_URL")?;
if let Some(icon) = project_item.icon_url {
if let Some(icon) = project_item.inner.icon_url {
let name = icon.split(&format!("{cdn_url}/")).nth(1);
if let Some(icon_path) = name {
@@ -1757,11 +1724,19 @@ pub async fn delete_project_icon(
SET icon_url = NULL, color = NULL
WHERE (id = $1)
",
project_item.id as database::models::ids::ProjectId,
project_item.inner.id as database::models::ids::ProjectId,
)
.execute(&mut *transaction)
.await?;
database::models::Project::clear_cache(
project_item.inner.id,
project_item.inner.slug,
None,
&redis,
)
.await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
@@ -1778,12 +1753,14 @@ pub struct GalleryCreateQuery {
}
#[post("{id}/gallery")]
#[allow(clippy::too_many_arguments)]
pub async fn add_gallery_item(
web::Query(ext): web::Query<Extension>,
req: HttpRequest,
web::Query(item): web::Query<GalleryCreateQuery>,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
mut payload: web::Payload,
) -> Result<HttpResponse, ApiError> {
@@ -1792,15 +1769,14 @@ pub async fn add_gallery_item(
.map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
let cdn_url = dotenvy::var("CDN_URL")?;
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let string = info.into_inner().0;
let project_item =
database::models::Project::get_full_from_slug_or_project_id(&string, &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The specified project does not exist!".to_string())
})?;
let project_item = database::models::Project::get(&string, &**pool, &redis)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The specified project does not exist!".to_string())
})?;
if project_item.gallery_items.len() > 64 {
return Err(ApiError::CustomAuthentication(
@@ -1880,6 +1856,14 @@ pub async fn add_gallery_item(
.insert(project_item.inner.id, &mut transaction)
.await?;
database::models::Project::clear_cache(
project_item.inner.id,
project_item.inner.slug,
None,
&redis,
)
.await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
@@ -1919,14 +1903,15 @@ pub async fn edit_gallery_item(
web::Query(item): web::Query<GalleryEditQuery>,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let string = info.into_inner().0;
item.validate()
.map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
let project_item = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
let project_item = database::models::Project::get(&string, &**pool, &redis)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The specified project does not exist!".to_string())
@@ -1934,7 +1919,7 @@ pub async fn edit_gallery_item(
if !user.role.is_mod() {
let team_member = database::models::TeamMember::get_from_user_id(
project_item.team_id,
project_item.inner.team_id,
user.id.into(),
&**pool,
)
@@ -1979,7 +1964,7 @@ pub async fn edit_gallery_item(
SET featured = $2
WHERE mod_id = $1
",
project_item.id as database::models::ids::ProjectId,
project_item.inner.id as database::models::ids::ProjectId,
false,
)
.execute(&mut *transaction)
@@ -2038,6 +2023,14 @@ pub async fn edit_gallery_item(
.await?;
}
database::models::Project::clear_cache(
project_item.inner.id,
project_item.inner.slug,
None,
&redis,
)
.await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
@@ -2054,12 +2047,13 @@ pub async fn delete_gallery_item(
web::Query(item): web::Query<GalleryDeleteQuery>,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let string = info.into_inner().0;
let project_item = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
let project_item = database::models::Project::get(&string, &**pool, &redis)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The specified project does not exist!".to_string())
@@ -2067,7 +2061,7 @@ pub async fn delete_gallery_item(
if !user.role.is_mod() {
let team_member = database::models::TeamMember::get_from_user_id(
project_item.team_id,
project_item.inner.team_id,
user.id.into(),
&**pool,
)
@@ -2121,6 +2115,14 @@ pub async fn delete_gallery_item(
.execute(&mut *transaction)
.await?;
database::models::Project::clear_cache(
project_item.inner.id,
project_item.inner.slug,
None,
&redis,
)
.await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
@@ -2131,12 +2133,13 @@ pub async fn project_delete(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
config: web::Data<SearchConfig>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let string = info.into_inner().0;
let project = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
let project = database::models::Project::get(&string, &**pool, &redis)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The specified project does not exist!".to_string())
@@ -2144,7 +2147,7 @@ pub async fn project_delete(
if !user.role.is_admin() {
let team_member = database::models::TeamMember::get_from_user_id_project(
project.id,
project.inner.id,
user.id.into(),
&**pool,
)
@@ -2166,11 +2169,12 @@ pub async fn project_delete(
let mut transaction = pool.begin().await?;
let result = database::models::Project::remove_full(project.id, &mut transaction).await?;
let result =
database::models::Project::remove(project.inner.id, &mut transaction, &redis).await?;
transaction.commit().await?;
delete_from_index(project.id.into(), config).await?;
delete_from_index(project.inner.id.into(), config).await?;
if result.is_some() {
Ok(HttpResponse::NoContent().body(""))
@@ -2184,20 +2188,21 @@ pub async fn project_follow(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let string = info.into_inner().0;
let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
let result = database::models::Project::get(&string, &**pool, &redis)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The specified project does not exist!".to_string())
})?;
let user_id: database::models::ids::UserId = user.id.into();
let project_id: database::models::ids::ProjectId = result.id;
let project_id: database::models::ids::ProjectId = result.inner.id;
if !is_authorized(&result, &Some(user), &pool).await? {
if !is_authorized(&result.inner, &Some(user), &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
}
@@ -2253,18 +2258,19 @@ pub async fn project_unfollow(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let string = info.into_inner().0;
let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool)
let result = database::models::Project::get(&string, &**pool, &redis)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The specified project does not exist!".to_string())
})?;
let user_id: database::models::ids::UserId = user.id.into();
let project_id = result.id;
let project_id = result.inner.id;
let following = sqlx::query!(
"

View File

@@ -1,11 +1,9 @@
use crate::auth::{check_is_moderator_from_headers, get_user_from_headers};
use crate::database::models::thread_item::{ThreadBuilder, ThreadMessageBuilder};
use crate::models::ids::{base62_impl::parse_base62, ProjectId, UserId, VersionId};
use crate::models::reports::{ItemType, Report};
use crate::models::threads::{MessageBody, ThreadType};
use crate::routes::ApiError;
use crate::util::auth::{
check_is_moderator_from_headers, get_user_from_headers, get_user_from_headers_transaction,
};
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
use chrono::Utc;
use futures::StreamExt;
@@ -35,10 +33,11 @@ pub async fn report_create(
req: HttpRequest,
pool: web::Data<PgPool>,
mut body: web::Payload,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let mut transaction = pool.begin().await?;
let current_user = get_user_from_headers_transaction(req.headers(), &mut transaction).await?;
let current_user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let mut bytes = web::BytesMut::new();
while let Some(item) = body.next().await {
@@ -179,9 +178,10 @@ fn default_all() -> bool {
pub async fn reports(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
count: web::Query<ReportsRequestOptions>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
use futures::stream::TryStreamExt;
@@ -225,10 +225,10 @@ pub async fn reports(
let query_reports =
crate::database::models::report_item::Report::get_many(&report_ids, &**pool).await?;
let mut reports = Vec::new();
let mut reports: Vec<Report> = Vec::new();
for x in query_reports {
reports.push(to_report(x));
reports.push(x.into());
}
Ok(HttpResponse::Ok().json(reports))
@@ -244,6 +244,7 @@ pub async fn reports_get(
req: HttpRequest,
web::Query(ids): web::Query<ReportIds>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let report_ids: Vec<crate::database::models::ids::ReportId> =
serde_json::from_str::<Vec<crate::models::ids::ReportId>>(&ids.ids)?
@@ -254,12 +255,12 @@ pub async fn reports_get(
let reports_data =
crate::database::models::report_item::Report::get_many(&report_ids, &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let all_reports = reports_data
.into_iter()
.filter(|x| user.role.is_mod() || x.reporter == user.id.into())
.map(to_report)
.map(|x| x.into())
.collect::<Vec<Report>>();
Ok(HttpResponse::Ok().json(all_reports))
@@ -269,9 +270,10 @@ pub async fn reports_get(
pub async fn report_get(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
info: web::Path<(crate::models::reports::ReportId,)>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let id = info.into_inner().0.into();
let report = crate::database::models::report_item::Report::get(id, &**pool).await?;
@@ -281,7 +283,8 @@ pub async fn report_get(
return Ok(HttpResponse::NotFound().body(""));
}
Ok(HttpResponse::Ok().json(to_report(report)))
let report: Report = report.into();
Ok(HttpResponse::Ok().json(report))
} else {
Ok(HttpResponse::NotFound().body(""))
}
@@ -298,10 +301,11 @@ pub struct EditReport {
pub async fn report_edit(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
info: web::Path<(crate::models::reports::ReportId,)>,
edit_report: web::Json<EditReport>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let id = info.into_inner().0.into();
let report = crate::database::models::report_item::Report::get(id, &**pool).await?;
@@ -374,8 +378,9 @@ pub async fn report_delete(
req: HttpRequest,
pool: web::Data<PgPool>,
info: web::Path<(crate::models::reports::ReportId,)>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
check_is_moderator_from_headers(req.headers(), &**pool).await?;
check_is_moderator_from_headers(req.headers(), &**pool, &redis).await?;
let mut transaction = pool.begin().await?;
let result = crate::database::models::report_item::Report::remove_full(
@@ -391,31 +396,3 @@ pub async fn report_delete(
Ok(HttpResponse::NotFound().body(""))
}
}
fn to_report(x: crate::database::models::report_item::QueryReport) -> Report {
let mut item_id = "".to_string();
let mut item_type = ItemType::Unknown;
if let Some(project_id) = x.project_id {
item_id = ProjectId::from(project_id).to_string();
item_type = ItemType::Project;
} else if let Some(version_id) = x.version_id {
item_id = VersionId::from(version_id).to_string();
item_type = ItemType::Version;
} else if let Some(user_id) = x.user_id {
item_id = UserId::from(user_id).to_string();
item_type = ItemType::User;
}
Report {
id: x.id.into(),
report_type: x.report_type,
item_id,
item_type,
reporter: x.reporter.into(),
body: x.body,
created: x.created,
closed: x.closed,
thread_id: x.thread_id.map(|x| x.into()),
}
}

View File

@@ -1,3 +1,4 @@
use crate::auth::{get_user_from_headers, is_authorized};
use crate::database::models::notification_item::NotificationBuilder;
use crate::database::models::TeamMember;
use crate::models::ids::ProjectId;
@@ -5,7 +6,6 @@ use crate::models::notifications::NotificationBody;
use crate::models::teams::{Permissions, TeamId};
use crate::models::users::UserId;
use crate::routes::ApiError;
use crate::util::auth::{get_user_from_headers, is_authorized};
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
@@ -30,25 +30,27 @@ pub async fn team_members_get_project(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0;
let project_data =
crate::database::models::Project::get_from_slug_or_project_id(&string, &**pool).await?;
let project_data = crate::database::models::Project::get(&string, &**pool, &redis).await?;
if let Some(project) = project_data {
let current_user = get_user_from_headers(req.headers(), &**pool).await.ok();
let current_user = get_user_from_headers(req.headers(), &**pool, &redis)
.await
.ok();
if !is_authorized(&project, &current_user, &pool).await? {
let members_data =
TeamMember::get_from_team_full(project.inner.team_id, &**pool, &redis).await?;
if !is_authorized(&project.inner, &current_user, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
}
let members_data = TeamMember::get_from_team_full(project.team_id, &**pool).await?;
if let Some(user) = &current_user {
let team_member =
TeamMember::get_from_user_id(project.team_id, user.id.into(), &**pool)
.await
.map_err(ApiError::Database)?;
let team_member = members_data
.iter()
.find(|x| x.user.id == user.id.into() && x.accepted);
if team_member.is_some() {
let team_members: Vec<_> = members_data
@@ -83,16 +85,19 @@ pub async fn team_members_get(
req: HttpRequest,
info: web::Path<(TeamId,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let id = info.into_inner().0;
let members_data = TeamMember::get_from_team_full(id.into(), &**pool).await?;
let members_data = TeamMember::get_from_team_full(id.into(), &**pool, &redis).await?;
let current_user = get_user_from_headers(req.headers(), &**pool).await.ok();
let current_user = get_user_from_headers(req.headers(), &**pool, &redis)
.await
.ok();
if let Some(user) = &current_user {
let team_member = TeamMember::get_from_user_id(id.into(), user.id.into(), &**pool)
.await
.map_err(ApiError::Database)?;
let team_member = members_data
.iter()
.find(|x| x.user.id == user.id.into() && x.accepted);
if team_member.is_some() {
let team_members: Vec<_> = members_data
@@ -129,6 +134,7 @@ pub async fn teams_get(
req: HttpRequest,
web::Query(ids): web::Query<TeamIds>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
use itertools::Itertools;
@@ -137,34 +143,39 @@ pub async fn teams_get(
.map(|x| x.into())
.collect::<Vec<crate::database::models::ids::TeamId>>();
let teams_data = TeamMember::get_from_team_full_many(&team_ids, &**pool).await?;
let teams_data = TeamMember::get_from_team_full_many(&team_ids, &**pool, &redis).await?;
let current_user = get_user_from_headers(req.headers(), &**pool).await.ok();
let accepted = if let Some(user) = current_user {
TeamMember::get_from_user_id_many(&team_ids, user.id.into(), &**pool)
.await?
.into_iter()
.map(|m| m.team_id.0)
.collect()
} else {
std::collections::HashSet::new()
};
let current_user = get_user_from_headers(req.headers(), &**pool, &redis)
.await
.ok();
let teams_groups = teams_data.into_iter().group_by(|data| data.team_id.0);
let mut teams: Vec<Vec<crate::models::teams::TeamMember>> = vec![];
for (id, member_data) in &teams_groups {
if accepted.contains(&id) {
let team_members =
member_data.map(|data| crate::models::teams::TeamMember::from(data, false));
for (_, member_data) in &teams_groups {
let members = member_data.collect::<Vec<_>>();
let team_member = if let Some(user) = &current_user {
members
.iter()
.find(|x| x.user.id == user.id.into() && x.accepted)
} else {
None
};
if team_member.is_some() {
let team_members = members
.into_iter()
.map(|data| crate::models::teams::TeamMember::from(data, false));
teams.push(team_members.collect());
continue;
}
let team_members = member_data
let team_members = members
.into_iter()
.filter(|x| x.accepted)
.map(|data| crate::models::teams::TeamMember::from(data, true));
@@ -179,9 +190,10 @@ pub async fn join_team(
req: HttpRequest,
info: web::Path<(TeamId,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let team_id = info.into_inner().0.into();
let current_user = get_user_from_headers(req.headers(), &**pool).await?;
let current_user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let member =
TeamMember::get_from_user_id_pending(team_id, current_user.id.into(), &**pool).await?;
@@ -207,6 +219,8 @@ pub async fn join_team(
)
.await?;
TeamMember::clear_cache(team_id, &redis).await?;
transaction.commit().await?;
} else {
return Err(ApiError::InvalidInput(
@@ -244,12 +258,13 @@ pub async fn add_team_member(
info: web::Path<(TeamId,)>,
pool: web::Data<PgPool>,
new_member: web::Json<NewTeamMember>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let team_id = info.into_inner().0.into();
let mut transaction = pool.begin().await?;
let current_user = get_user_from_headers(req.headers(), &**pool).await?;
let current_user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let member = TeamMember::get_from_user_id(team_id, current_user.id.into(), &**pool)
.await?
.ok_or_else(|| {
@@ -281,12 +296,8 @@ pub async fn add_team_member(
));
}
let request = crate::database::models::team_item::TeamMember::get_from_user_id_pending(
team_id,
new_member.user_id.into(),
&**pool,
)
.await?;
let request =
TeamMember::get_from_user_id_pending(team_id, new_member.user_id.into(), &**pool).await?;
if let Some(req) = request {
if req.accepted {
@@ -300,7 +311,7 @@ pub async fn add_team_member(
}
}
crate::database::models::User::get(member.user_id, &**pool)
crate::database::models::User::get_id(member.user_id, &**pool, &redis)
.await?
.ok_or_else(|| ApiError::InvalidInput("An invalid User ID specified".to_string()))?;
@@ -340,6 +351,8 @@ pub async fn add_team_member(
.insert(new_member.user_id.into(), &mut transaction)
.await?;
TeamMember::clear_cache(team_id, &redis).await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
@@ -359,12 +372,13 @@ pub async fn edit_team_member(
info: web::Path<(TeamId, UserId)>,
pool: web::Data<PgPool>,
edit_member: web::Json<EditTeamMember>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let ids = info.into_inner();
let id = ids.0.into();
let user_id = ids.1.into();
let current_user = get_user_from_headers(req.headers(), &**pool).await?;
let current_user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool)
.await?
.ok_or_else(|| {
@@ -430,6 +444,8 @@ pub async fn edit_team_member(
)
.await?;
TeamMember::clear_cache(id, &redis).await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
@@ -446,10 +462,11 @@ pub async fn transfer_ownership(
info: web::Path<(TeamId,)>,
pool: web::Data<PgPool>,
new_owner: web::Json<TransferOwnership>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let id = info.into_inner().0;
let current_user = get_user_from_headers(req.headers(), &**pool).await?;
let current_user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
if !current_user.role.is_admin() {
let member = TeamMember::get_from_user_id(id.into(), current_user.id.into(), &**pool)
@@ -505,6 +522,8 @@ pub async fn transfer_ownership(
)
.await?;
TeamMember::clear_cache(id.into(), &redis).await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
@@ -515,12 +534,13 @@ pub async fn remove_team_member(
req: HttpRequest,
info: web::Path<(TeamId, UserId)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let ids = info.into_inner();
let id = ids.0.into();
let user_id = ids.1.into();
let current_user = get_user_from_headers(req.headers(), &**pool).await?;
let current_user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool)
.await?
.ok_or_else(|| {
@@ -566,6 +586,8 @@ pub async fn remove_team_member(
));
}
TeamMember::clear_cache(id, &redis).await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {

View File

@@ -1,13 +1,13 @@
use crate::auth::{check_is_moderator_from_headers, get_user_from_headers};
use crate::database;
use crate::database::models::notification_item::NotificationBuilder;
use crate::database::models::thread_item::ThreadMessageBuilder;
use crate::models::ids::ThreadMessageId;
use crate::models::notifications::NotificationBody;
use crate::models::projects::ProjectStatus;
use crate::models::threads::{MessageBody, Thread, ThreadId, ThreadMessage, ThreadType};
use crate::models::threads::{MessageBody, Thread, ThreadId, ThreadType};
use crate::models::users::User;
use crate::routes::ApiError;
use crate::util::auth::{check_is_moderator_from_headers, get_user_from_headers};
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse};
use futures::TryStreamExt;
use serde::Deserialize;
@@ -68,6 +68,7 @@ pub async fn filter_authorized_threads(
threads: Vec<database::models::Thread>,
user: &User,
pool: &web::Data<PgPool>,
redis: &deadpool_redis::Pool,
) -> Result<Vec<Thread>, ApiError> {
let user_id: database::models::UserId = user.id.into();
@@ -171,7 +172,7 @@ pub async fn filter_authorized_threads(
.collect::<Vec<database::models::UserId>>(),
);
let users: Vec<User> = database::models::User::get_many(&user_ids, &***pool)
let users: Vec<User> = database::models::User::get_many_ids(&user_ids, &***pool, redis)
.await?
.into_iter()
.map(From::from)
@@ -190,7 +191,7 @@ pub async fn filter_authorized_threads(
.collect::<Vec<_>>(),
);
final_threads.push(convert_thread(
final_threads.push(Thread::from(
thread,
users
.iter()
@@ -204,56 +205,18 @@ pub async fn filter_authorized_threads(
Ok(final_threads)
}
fn convert_thread(data: database::models::Thread, users: Vec<User>, user: &User) -> Thread {
let thread_type = data.type_;
Thread {
id: data.id.into(),
type_: thread_type,
messages: data
.messages
.into_iter()
.filter(|x| {
if let MessageBody::Text { private, .. } = x.body {
!private || user.role.is_mod()
} else {
true
}
})
.map(|x| ThreadMessage {
id: x.id.into(),
author_id: if users
.iter()
.find(|y| x.author_id == Some(y.id.into()))
.map(|x| x.role.is_mod() && !user.role.is_mod())
.unwrap_or(false)
{
None
} else {
x.author_id.map(|x| x.into())
},
body: x.body,
created: x.created,
})
.collect(),
members: users
.into_iter()
.filter(|x| !x.role.is_mod() || user.role.is_mod())
.collect(),
}
}
#[get("{id}")]
pub async fn thread_get(
req: HttpRequest,
info: web::Path<(ThreadId,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0.into();
let thread_data = database::models::Thread::get(string, &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
if let Some(mut data) = thread_data {
if is_authorized_thread(&data, &user, &pool).await? {
@@ -267,13 +230,13 @@ pub async fn thread_get(
.collect::<Vec<_>>(),
);
let users: Vec<User> = database::models::User::get_many(authors, &**pool)
let users: Vec<User> = database::models::User::get_many_ids(authors, &**pool, &redis)
.await?
.into_iter()
.map(From::from)
.collect();
return Ok(HttpResponse::Ok().json(convert_thread(data, users, &user)));
return Ok(HttpResponse::Ok().json(Thread::from(data, users, &user)));
}
}
Ok(HttpResponse::NotFound().body(""))
@@ -289,8 +252,9 @@ pub async fn threads_get(
req: HttpRequest,
web::Query(ids): web::Query<ThreadIds>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let thread_ids: Vec<database::models::ids::ThreadId> =
serde_json::from_str::<Vec<ThreadId>>(&ids.ids)?
@@ -300,7 +264,7 @@ pub async fn threads_get(
let threads_data = database::models::Thread::get_many(&thread_ids, &**pool).await?;
let threads = filter_authorized_threads(threads_data, &user, &pool).await?;
let threads = filter_authorized_threads(threads_data, &user, &pool, &redis).await?;
Ok(HttpResponse::Ok().json(threads))
}
@@ -316,8 +280,9 @@ pub async fn thread_send_message(
info: web::Path<(ThreadId,)>,
pool: web::Data<PgPool>,
new_message: web::Json<NewThreadMessage>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let string: database::models::ThreadId = info.into_inner().0.into();
@@ -392,6 +357,7 @@ pub async fn thread_send_message(
let members = database::models::TeamMember::get_from_team_full(
database::models::TeamId(record.team_id),
&**pool,
&redis,
)
.await?;
@@ -475,8 +441,9 @@ pub async fn thread_send_message(
pub async fn moderation_inbox(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user = check_is_moderator_from_headers(req.headers(), &**pool).await?;
let user = check_is_moderator_from_headers(req.headers(), &**pool, &redis).await?;
let ids = sqlx::query!(
"
@@ -491,7 +458,7 @@ pub async fn moderation_inbox(
.await?;
let threads_data = database::models::Thread::get_many(&ids, &**pool).await?;
let threads = filter_authorized_threads(threads_data, &user, &pool).await?;
let threads = filter_authorized_threads(threads_data, &user, &pool, &redis).await?;
Ok(HttpResponse::Ok().json(threads))
}
@@ -501,8 +468,9 @@ pub async fn thread_read(
req: HttpRequest,
info: web::Path<(ThreadId,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
check_is_moderator_from_headers(req.headers(), &**pool).await?;
check_is_moderator_from_headers(req.headers(), &**pool, &redis).await?;
let id = info.into_inner().0;
let mut transaction = pool.begin().await?;
@@ -528,8 +496,9 @@ pub async fn message_delete(
req: HttpRequest,
info: web::Path<(ThreadMessageId,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let result = database::models::ThreadMessage::get(info.into_inner().0.into(), &**pool).await?;

View File

@@ -1,3 +1,4 @@
use crate::auth::get_user_from_headers;
use crate::database::models::User;
use crate::file_hosting::FileHost;
use crate::models::notifications::Notification;
@@ -5,7 +6,6 @@ use crate::models::projects::Project;
use crate::models::users::{Badges, RecipientType, RecipientWallet, Role, UserId};
use crate::queue::payouts::{PayoutAmount, PayoutItem, PayoutsQueue};
use crate::routes::ApiError;
use crate::util::auth::get_user_from_headers;
use crate::util::routes::read_from_payload;
use crate::util::validate::validation_errors_to_string;
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
@@ -43,8 +43,9 @@ pub fn config(cfg: &mut web::ServiceConfig) {
pub async fn user_auth_get(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
Ok(HttpResponse::Ok().json(get_user_from_headers(req.headers(), &**pool).await?))
Ok(HttpResponse::Ok().json(get_user_from_headers(req.headers(), &**pool, &redis).await?))
}
#[derive(Serialize)]
@@ -57,8 +58,9 @@ pub struct UserData {
pub async fn user_data_get(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let data = sqlx::query!(
"
@@ -93,13 +95,11 @@ pub struct UserIds {
pub async fn users_get(
web::Query(ids): web::Query<UserIds>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user_ids = serde_json::from_str::<Vec<UserId>>(&ids.ids)?
.into_iter()
.map(|x| x.into())
.collect::<Vec<crate::database::models::UserId>>();
let user_ids = serde_json::from_str::<Vec<String>>(&ids.ids)?;
let users_data = User::get_many(&user_ids, &**pool).await?;
let users_data = User::get_many(&user_ids, &**pool, &redis).await?;
let users: Vec<crate::models::users::User> = users_data.into_iter().map(From::from).collect();
@@ -110,21 +110,9 @@ pub async fn users_get(
pub async fn user_get(
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0;
let id_option: Option<UserId> = serde_json::from_str(&format!("\"{string}\"")).ok();
let mut user_data;
if let Some(id) = id_option {
user_data = User::get(id.into(), &**pool).await?;
if user_data.is_none() {
user_data = User::get_from_username(string, &**pool).await?;
}
} else {
user_data = User::get_from_username(string, &**pool).await?;
}
let user_data = User::get(&info.into_inner().0, &**pool, &redis).await?;
if let Some(data) = user_data {
let response: crate::models::users::User = data.into();
@@ -139,12 +127,15 @@ pub async fn projects_list(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await.ok();
let user = get_user_from_headers(req.headers(), &**pool, &redis)
.await
.ok();
let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?;
if let Some(id) = id_option {
if let Some(id) = id_option.map(|x| x.id) {
let user_id: UserId = id.into();
let can_view_private = user
@@ -153,12 +144,13 @@ pub async fn projects_list(
let project_data = User::get_projects(id, &**pool).await?;
let response: Vec<_> = crate::database::Project::get_many_full(&project_data, &**pool)
.await?
.into_iter()
.filter(|x| can_view_private || x.inner.status.is_searchable())
.map(Project::from)
.collect();
let response: Vec<_> =
crate::database::Project::get_many_ids(&project_data, &**pool, &redis)
.await?
.into_iter()
.filter(|x| can_view_private || x.inner.status.is_searchable())
.map(Project::from)
.collect();
Ok(HttpResponse::Ok().json(response))
} else {
@@ -211,29 +203,30 @@ pub struct EditPayoutData {
pub async fn user_edit(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
new_user: web::Json<EditUser>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
new_user
.validate()
.map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?;
if let Some(id) = id_option {
if let Some(actual_user) = id_option {
let id = actual_user.id;
let user_id: UserId = id.into();
if user.id == user_id || user.role.is_mod() {
let mut transaction = pool.begin().await?;
if let Some(username) = &new_user.username {
let existing_user_id_option =
User::get_id_from_username_or_id(username, &**pool).await?;
let existing_user_id_option = User::get(username, &**pool, &redis).await?;
if existing_user_id_option
.map(UserId::from)
.map(|x| UserId::from(x.id))
.map(|id| id == user.id)
.unwrap_or(true)
{
@@ -394,6 +387,7 @@ pub async fn user_edit(
}
}
User::clear_caches(&[(id, Some(actual_user.username))], &redis).await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {
@@ -417,34 +411,24 @@ pub async fn user_icon_edit(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
mut payload: web::Payload,
) -> Result<HttpResponse, ApiError> {
if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
let cdn_url = dotenvy::var("CDN_URL")?;
let user = get_user_from_headers(req.headers(), &**pool).await?;
let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?;
if let Some(id) = id_option {
if user.id != id.into() && !user.role.is_mod() {
if let Some(actual_user) = id_option {
if user.id != actual_user.id.into() && !user.role.is_mod() {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this user's icon.".to_string(),
));
}
let mut icon_url = user.avatar_url;
let user_id: UserId = id.into();
if user.id != user_id {
let new_user = User::get(id, &**pool).await?;
if let Some(new) = new_user {
icon_url = new.avatar_url;
} else {
return Ok(HttpResponse::NotFound().body(""));
}
}
let icon_url = actual_user.avatar_url;
let user_id: UserId = actual_user.id.into();
if let Some(icon) = icon_url {
let name = icon.split(&format!("{cdn_url}/")).nth(1);
@@ -473,10 +457,12 @@ pub async fn user_icon_edit(
WHERE (id = $2)
",
format!("{}/{}", cdn_url, upload_data.file_name),
id as crate::database::models::ids::UserId,
actual_user.id as crate::database::models::ids::UserId,
)
.execute(&**pool)
.await?;
User::clear_caches(&[(actual_user.id, None)], &redis).await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
@@ -505,11 +491,12 @@ pub async fn user_delete(
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
removal_type: web::Query<RemovalType>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?;
if let Some(id) = id_option {
if let Some(id) = id_option.map(|x| x.id) {
if !user.role.is_admin() && user.id != id.into() {
return Err(ApiError::CustomAuthentication(
"You do not have permission to delete this user!".to_string(),
@@ -518,11 +505,13 @@ pub async fn user_delete(
let mut transaction = pool.begin().await?;
let result = if &*removal_type.removal_type == "full" {
User::remove_full(id, &mut transaction).await?
} else {
User::remove(id, &mut transaction).await?
};
let result = User::remove(
id,
removal_type.removal_type == "full",
&mut transaction,
&redis,
)
.await?;
transaction.commit().await?;
@@ -541,11 +530,12 @@ pub async fn user_follows(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?;
if let Some(id) = id_option {
if let Some(id) = id_option.map(|x| x.id) {
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(),
@@ -569,11 +559,12 @@ pub async fn user_follows(
.try_collect::<Vec<crate::database::models::ProjectId>>()
.await?;
let projects: Vec<_> = crate::database::Project::get_many_full(&project_ids, &**pool)
.await?
.into_iter()
.map(Project::from)
.collect();
let projects: Vec<_> =
crate::database::Project::get_many_ids(&project_ids, &**pool, &redis)
.await?
.into_iter()
.map(Project::from)
.collect();
Ok(HttpResponse::Ok().json(projects))
} else {
@@ -586,11 +577,12 @@ pub async fn user_notifications(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?;
if let Some(id) = id_option {
if let Some(id) = id_option.map(|x| x.id) {
if !user.role.is_admin() && user.id != id.into() {
return Err(ApiError::CustomAuthentication(
"You do not have permission to see the notifications of this user!".to_string(),
@@ -624,11 +616,12 @@ pub async fn user_payouts(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?;
if let Some(id) = id_option {
if let Some(id) = id_option.map(|x| x.id) {
if !user.role.is_admin() && user.id != id.into() {
return Err(ApiError::CustomAuthentication(
"You do not have permission to see the payouts of this user!".to_string(),
@@ -699,13 +692,14 @@ pub async fn user_payouts_request(
pool: web::Data<PgPool>,
data: web::Json<PayoutData>,
payouts_queue: web::Data<Arc<Mutex<PayoutsQueue>>>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let mut payouts_queue = payouts_queue.lock().await;
let user = get_user_from_headers(req.headers(), &**pool).await?;
let id_option = User::get_id_from_username_or_id(&info.into_inner().0, &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?;
if let Some(id) = id_option {
if let Some(id) = id_option.map(|x| x.id) {
if !user.role.is_admin() && user.id != id.into() {
return Err(ApiError::CustomAuthentication(
"You do not have permission to request payouts of this user!".to_string(),
@@ -761,6 +755,7 @@ pub async fn user_payouts_request(
)
.execute(&mut *transaction)
.await?;
User::clear_caches(&[(id, None)], &redis).await?;
transaction.commit().await?;

View File

@@ -1,4 +1,5 @@
use super::project_creation::{CreateError, UploadedFile};
use crate::auth::get_user_from_headers;
use crate::database::models;
use crate::database::models::notification_item::NotificationBuilder;
use crate::database::models::version_item::{
@@ -12,7 +13,6 @@ use crate::models::projects::{
VersionId, VersionStatus, VersionType,
};
use crate::models::teams::Permissions;
use crate::util::auth::get_user_from_headers_transaction;
use crate::util::routes::read_from_field;
use crate::util::validate::validation_errors_to_string;
use crate::validate::{validate_file, ValidationResult};
@@ -82,6 +82,7 @@ pub async fn version_create(
req: HttpRequest,
mut payload: Multipart,
client: Data<PgPool>,
redis: Data<deadpool_redis::Pool>,
file_host: Data<Arc<dyn FileHost + Send + Sync>>,
) -> Result<HttpResponse, CreateError> {
let mut transaction = client.begin().await?;
@@ -91,8 +92,10 @@ pub async fn version_create(
req,
&mut payload,
&mut transaction,
&redis,
&***file_host,
&mut uploaded_files,
&client,
)
.await;
@@ -116,8 +119,10 @@ async fn version_create_inner(
req: HttpRequest,
payload: &mut Multipart,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
redis: &deadpool_redis::Pool,
file_host: &dyn FileHost,
uploaded_files: &mut Vec<UploadedFile>,
pool: &PgPool,
) -> Result<HttpResponse, CreateError> {
let cdn_url = dotenvy::var("CDN_URL")?;
@@ -127,7 +132,7 @@ async fn version_create_inner(
let all_game_versions = models::categories::GameVersion::list(&mut *transaction).await?;
let all_loaders = models::categories::Loader::list(&mut *transaction).await?;
let user = get_user_from_headers_transaction(req.headers(), &mut *transaction).await?;
let user = get_user_from_headers(req.headers(), pool, redis).await?;
let mut error = None;
while let Some(item) = payload.next().await {
@@ -417,8 +422,7 @@ async fn version_create_inner(
let project_id = builder.project_id;
builder.insert(transaction).await?;
models::Project::update_game_versions(project_id, &mut *transaction).await?;
models::Project::update_loaders(project_id, &mut *transaction).await?;
models::Project::clear_cache(project_id, None, Some(true), redis).await?;
Ok(HttpResponse::Ok().json(response))
}
@@ -430,6 +434,7 @@ pub async fn upload_file_to_version(
url_data: web::Path<(VersionId,)>,
mut payload: Multipart,
client: Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
file_host: Data<Arc<dyn FileHost + Send + Sync>>,
) -> Result<HttpResponse, CreateError> {
let mut transaction = client.begin().await?;
@@ -442,6 +447,7 @@ pub async fn upload_file_to_version(
&mut payload,
client,
&mut transaction,
redis,
&***file_host,
&mut uploaded_files,
version_id,
@@ -470,6 +476,7 @@ async fn upload_file_to_version_inner(
payload: &mut Multipart,
client: Data<PgPool>,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
redis: Data<deadpool_redis::Pool>,
file_host: &dyn FileHost,
uploaded_files: &mut Vec<UploadedFile>,
version_id: models::VersionId,
@@ -479,9 +486,9 @@ async fn upload_file_to_version_inner(
let mut initial_file_data: Option<InitialFileData> = None;
let mut file_builders: Vec<VersionFileBuilder> = Vec::new();
let user = get_user_from_headers_transaction(req.headers(), &mut *transaction).await?;
let user = get_user_from_headers(req.headers(), &**client, &redis).await?;
let result = models::Version::get_full(version_id, &**client).await?;
let result = models::Version::get(version_id, &**client, &redis).await?;
let version = match result {
Some(v) => v,
@@ -493,8 +500,8 @@ async fn upload_file_to_version_inner(
};
if !user.role.is_admin() {
let team_member = models::TeamMember::get_from_user_id_version(
version_id,
let team_member = models::TeamMember::get_from_user_id_project(
version.inner.project_id,
user.id.into(),
&mut *transaction,
)

View File

@@ -1,13 +1,13 @@
use super::ApiError;
use crate::database::models::{version_item::QueryVersion, DatabaseError};
use crate::auth::{
filter_authorized_projects, filter_authorized_versions, get_user_from_headers,
is_authorized_version,
};
use crate::models::ids::VersionId;
use crate::models::projects::{GameVersion, Loader, Project, Version};
use crate::models::projects::VersionType;
use crate::models::teams::Permissions;
use crate::util::auth::get_user_from_headers;
use crate::util::routes::ok_or_not_found;
use crate::{database, models};
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse};
use futures::TryStreamExt;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
@@ -25,7 +25,6 @@ pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("version_files")
.service(get_versions_from_hashes)
.service(download_files)
.service(update_files),
);
}
@@ -34,8 +33,6 @@ pub fn config(cfg: &mut web::ServiceConfig) {
pub struct HashQuery {
#[serde(default = "default_algorithm")]
pub algorithm: String,
#[serde(default = "default_multiple")]
pub multiple: bool,
pub version_id: Option<VersionId>,
}
@@ -43,59 +40,40 @@ fn default_algorithm() -> String {
"sha1".into()
}
fn default_multiple() -> bool {
false
}
// under /api/v1/version_file/{hash}
#[get("{version_id}")]
pub async fn get_version_from_hash(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
hash_query: web::Query<HashQuery>,
) -> Result<HttpResponse, ApiError> {
let hash = info.into_inner().0.to_lowercase();
let user_option = get_user_from_headers(req.headers(), &**pool, &redis)
.await
.ok();
let result = sqlx::query!(
"
SELECT f.version_id version_id
FROM hashes h
INNER JOIN files f ON h.file_id = f.id
INNER JOIN versions v on f.version_id = v.id AND v.status != ALL($1)
INNER JOIN mods m on v.mod_id = m.id
WHERE h.algorithm = $3 AND h.hash = $2 AND m.status != ALL($4)
ORDER BY v.date_published ASC
",
&*crate::models::projects::VersionStatus::iterator()
.filter(|x| x.is_hidden())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
hash.as_bytes(),
hash_query.algorithm,
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_hidden())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
let hash = info.into_inner().0.to_lowercase();
let file = database::models::Version::get_file_from_hash(
hash_query.algorithm.clone(),
hash,
hash_query.version_id.map(|x| x.into()),
&**pool,
&redis,
)
.fetch_all(&**pool)
.await?;
let version_ids = result
.iter()
.map(|x| database::models::VersionId(x.version_id))
.collect::<Vec<_>>();
let versions_data = database::models::Version::get_many_full(&version_ids, &**pool).await?;
if let Some(file) = file {
let version = database::models::Version::get(file.version_id, &**pool, &redis).await?;
if let Some(first) = versions_data.first() {
if hash_query.multiple {
Ok(HttpResponse::Ok().json(
versions_data
.into_iter()
.map(models::projects::Version::from)
.collect::<Vec<_>>(),
))
if let Some(version) = version {
if !is_authorized_version(&version.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
}
Ok(HttpResponse::Ok().json(models::projects::Version::from(version)))
} else {
Ok(HttpResponse::Ok().json(models::projects::Version::from(first.clone())))
Ok(HttpResponse::NotFound().body(""))
}
} else {
Ok(HttpResponse::NotFound().body(""))
@@ -110,42 +88,40 @@ pub struct DownloadRedirect {
// under /api/v1/version_file/{hash}/download
#[get("{version_id}/download")]
pub async fn download_version(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
hash_query: web::Query<HashQuery>,
) -> Result<HttpResponse, ApiError> {
let hash = info.into_inner().0.to_lowercase();
let mut transaction = pool.begin().await?;
let user_option = get_user_from_headers(req.headers(), &**pool, &redis)
.await
.ok();
let result = sqlx::query!(
"
SELECT f.url url, f.id id, f.version_id version_id, v.mod_id project_id FROM hashes h
INNER JOIN files f ON h.file_id = f.id
INNER JOIN versions v ON v.id = f.version_id AND v.status != ALL($1)
INNER JOIN mods m on v.mod_id = m.id
WHERE h.algorithm = $3 AND h.hash = $2 AND m.status != ALL($4)
ORDER BY v.date_published ASC
",
&*crate::models::projects::VersionStatus::iterator()
.filter(|x| x.is_hidden())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
hash.as_bytes(),
hash_query.algorithm,
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_hidden())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
let hash = info.into_inner().0.to_lowercase();
let file = database::models::Version::get_file_from_hash(
hash_query.algorithm.clone(),
hash,
hash_query.version_id.map(|x| x.into()),
&**pool,
&redis,
)
.fetch_optional(&mut *transaction)
.await?;
if let Some(id) = result {
transaction.commit().await?;
if let Some(file) = file {
let version = database::models::Version::get(file.version_id, &**pool, &redis).await?;
Ok(HttpResponse::TemporaryRedirect()
.append_header(("Location", &*id.url))
.json(DownloadRedirect { url: id.url }))
if let Some(version) = version {
if !is_authorized_version(&version.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
}
Ok(HttpResponse::TemporaryRedirect()
.append_header(("Location", &*file.url))
.json(DownloadRedirect { url: file.url }))
} else {
Ok(HttpResponse::NotFound().body(""))
}
} else {
Ok(HttpResponse::NotFound().body(""))
}
@@ -157,33 +133,26 @@ pub async fn delete_file(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
hash_query: web::Query<HashQuery>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let hash = info.into_inner().0.to_lowercase();
let result = sqlx::query!(
"
SELECT f.id id, f.version_id version_id, f.filename filename, v.version_number version_number, v.mod_id project_id FROM hashes h
INNER JOIN files f ON h.file_id = f.id
INNER JOIN versions v ON v.id = f.version_id
WHERE h.algorithm = $2 AND h.hash = $1
ORDER BY v.date_published ASC
",
hash.as_bytes(),
hash_query.algorithm
let file = database::models::Version::get_file_from_hash(
hash_query.algorithm.clone(),
hash,
hash_query.version_id.map(|x| x.into()),
&**pool,
&redis,
)
.fetch_all(&**pool)
.await?;
.await?;
if let Some(row) = result.iter().find_or_first(|x| {
hash_query.version_id.is_none()
|| Some(x.version_id) == hash_query.version_id.map(|x| x.0 as i64)
}) {
if let Some(row) = file {
if !user.role.is_admin() {
let team_member = database::models::TeamMember::get_from_user_id_version(
database::models::ids::VersionId(row.version_id),
row.version_id,
user.id.into(),
&**pool,
)
@@ -205,24 +174,15 @@ pub async fn delete_file(
}
}
use futures::stream::TryStreamExt;
let version = database::models::Version::get(row.version_id, &**pool, &redis).await?;
if let Some(version) = version {
if version.files.len() < 2 {
return Err(ApiError::InvalidInput(
"Versions must have at least one file uploaded to them".to_string(),
));
}
let files = sqlx::query!(
"
SELECT f.id id FROM files f
WHERE f.version_id = $1
",
row.version_id
)
.fetch_many(&**pool)
.try_filter_map(|e| async { Ok(e.right().map(|_| ())) })
.try_collect::<Vec<()>>()
.await?;
if files.len() < 2 {
return Err(ApiError::InvalidInput(
"Versions must have at least one file uploaded to them".to_string(),
));
database::models::Version::clear_cache(&version, &redis).await?;
}
let mut transaction = pool.begin().await?;
@@ -232,7 +192,7 @@ pub async fn delete_file(
DELETE FROM hashes
WHERE file_id = $1
",
row.id
row.id.0
)
.execute(&mut *transaction)
.await?;
@@ -242,7 +202,7 @@ pub async fn delete_file(
DELETE FROM files
WHERE files.id = $1
",
row.id,
row.id.0,
)
.execute(&mut *transaction)
.await?;
@@ -257,82 +217,72 @@ pub async fn delete_file(
#[derive(Deserialize)]
pub struct UpdateData {
pub loaders: Vec<Loader>,
pub game_versions: Vec<GameVersion>,
pub loaders: Option<Vec<String>>,
pub game_versions: Option<Vec<String>>,
pub version_types: Option<Vec<VersionType>>,
}
#[post("{version_id}/update")]
pub async fn get_update_from_hash(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
hash_query: web::Query<HashQuery>,
update_data: web::Json<UpdateData>,
) -> Result<HttpResponse, ApiError> {
let user_option = get_user_from_headers(req.headers(), &**pool, &redis)
.await
.ok();
let hash = info.into_inner().0.to_lowercase();
// get version_id from hash
// get mod_id from hash
// get latest version satisfying conditions - if not found
let result = sqlx::query!(
"
SELECT v.mod_id project_id FROM hashes h
INNER JOIN files f ON h.file_id = f.id
INNER JOIN versions v ON v.id = f.version_id AND v.status != ALL($1)
INNER JOIN mods m on v.mod_id = m.id
WHERE h.algorithm = $3 AND h.hash = $2 AND m.status != ALL($4)
ORDER BY v.date_published ASC
",
&*crate::models::projects::VersionStatus::iterator()
.filter(|x| x.is_hidden())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
hash.as_bytes(),
hash_query.algorithm,
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_hidden())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
if let Some(file) = database::models::Version::get_file_from_hash(
hash_query.algorithm.clone(),
hash,
hash_query.version_id.map(|x| x.into()),
&**pool,
&redis,
)
.fetch_optional(&**pool)
.await?;
if let Some(id) = result {
let version_ids = database::models::Version::get_project_versions(
database::models::ProjectId(id.project_id),
Some(
update_data
.game_versions
.clone()
.await?
{
if let Some(project) =
database::models::Project::get_id(file.project_id, &**pool, &redis).await?
{
let mut versions =
database::models::Version::get_many(&project.versions, &**pool, &redis)
.await?
.into_iter()
.map(|x| x.0)
.collect(),
),
Some(
update_data
.loaders
.clone()
.into_iter()
.map(|x| x.0)
.collect(),
),
None,
None,
None,
&**pool,
)
.await?;
.filter(|x| {
let mut bool = true;
if let Some(version_id) = version_ids.first() {
let version_data = database::models::Version::get_full(*version_id, &**pool).await?;
if let Some(version_types) = &update_data.version_types {
bool &= version_types
.iter()
.any(|y| y.as_str() == x.inner.version_type);
}
if let Some(loaders) = &update_data.loaders {
bool &= x.loaders.iter().any(|y| loaders.contains(y));
}
if let Some(game_versions) = &update_data.game_versions {
bool &= x.game_versions.iter().any(|y| game_versions.contains(y));
}
ok_or_not_found::<QueryVersion, Version>(version_data)
} else {
Ok(HttpResponse::NotFound().body(""))
bool
})
.sorted_by(|a, b| b.inner.date_published.cmp(&a.inner.date_published))
.collect::<Vec<_>>();
if let Some(first) = versions.pop() {
if !is_authorized_version(&first.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
}
return Ok(HttpResponse::Ok().json(models::projects::Version::from(first)));
}
}
} else {
Ok(HttpResponse::NotFound().body(""))
}
Ok(HttpResponse::NotFound().body(""))
}
// Requests above with multiple versions below
@@ -345,274 +295,164 @@ pub struct FileHashes {
// under /api/v2/version_files
#[post("")]
pub async fn get_versions_from_hashes(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
file_data: web::Json<FileHashes>,
) -> Result<HttpResponse, ApiError> {
let hashes_parsed: Vec<Vec<u8>> = file_data
.hashes
.iter()
.map(|x| x.to_lowercase().as_bytes().to_vec())
.collect();
let user_option = get_user_from_headers(req.headers(), &**pool, &redis)
.await
.ok();
let result = sqlx::query!(
"
SELECT h.hash hash, h.algorithm algorithm, f.version_id version_id FROM hashes h
INNER JOIN files f ON h.file_id = f.id
INNER JOIN versions v ON v.id = f.version_id AND v.status != ALL($1)
INNER JOIN mods m on v.mod_id = m.id
WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ALL($4)
",
&*crate::models::projects::VersionStatus::iterator()
.filter(|x| x.is_hidden())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
hashes_parsed.as_slice(),
file_data.algorithm,
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_hidden())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
)
.fetch_all(&**pool)
.await?;
let version_ids = result
.iter()
.map(|x| database::models::VersionId(x.version_id))
.collect::<Vec<_>>();
let versions_data = database::models::Version::get_many_full(&version_ids, &**pool).await?;
let response: Result<HashMap<String, Version>, ApiError> = result
.into_iter()
.filter_map(|row| {
versions_data
.clone()
.into_iter()
.find(|x| x.inner.id.0 == row.version_id)
.map(|v| {
if let Ok(parsed_hash) = String::from_utf8(row.hash) {
Ok((parsed_hash, crate::models::projects::Version::from(v)))
} else {
Err(ApiError::Database(DatabaseError::Other(format!(
"Could not parse hash for version {}",
row.version_id
))))
}
})
})
.collect();
Ok(HttpResponse::Ok().json(response?))
}
#[post("project")]
pub async fn get_projects_from_hashes(
pool: web::Data<PgPool>,
file_data: web::Json<FileHashes>,
) -> Result<HttpResponse, ApiError> {
let hashes_parsed: Vec<Vec<u8>> = file_data
.hashes
.iter()
.map(|x| x.to_lowercase().as_bytes().to_vec())
.collect();
let result = sqlx::query!(
"
SELECT h.hash hash, h.algorithm algorithm, m.id project_id FROM hashes h
INNER JOIN files f ON h.file_id = f.id
INNER JOIN versions v ON v.id = f.version_id AND v.status != ALL($1)
INNER JOIN mods m on v.mod_id = m.id
WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ALL($4)
",
&*crate::models::projects::VersionStatus::iterator()
.filter(|x| x.is_hidden())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
hashes_parsed.as_slice(),
file_data.algorithm,
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_hidden())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
)
.fetch_all(&**pool)
.await?;
let project_ids = result
.iter()
.map(|x| database::models::ProjectId(x.project_id))
.collect::<Vec<_>>();
let versions_data = database::models::Project::get_many_full(&project_ids, &**pool).await?;
let response: Result<HashMap<String, Project>, ApiError> = result
.into_iter()
.filter_map(|row| {
versions_data
.clone()
.into_iter()
.find(|x| x.inner.id.0 == row.project_id)
.map(|v| {
if let Ok(parsed_hash) = String::from_utf8(row.hash) {
Ok((parsed_hash, crate::models::projects::Project::from(v)))
} else {
Err(ApiError::Database(DatabaseError::Other(format!(
"Could not parse hash for version {}",
row.project_id
))))
}
})
})
.collect();
Ok(HttpResponse::Ok().json(response?))
}
#[post("download")]
pub async fn download_files(
pool: web::Data<PgPool>,
file_data: web::Json<FileHashes>,
) -> Result<HttpResponse, ApiError> {
let hashes_parsed: Vec<Vec<u8>> = file_data
.hashes
.iter()
.map(|x| x.to_lowercase().as_bytes().to_vec())
.collect();
let mut transaction = pool.begin().await?;
let result = sqlx::query!(
"
SELECT f.url url, h.hash hash, h.algorithm algorithm, f.version_id version_id, v.mod_id project_id FROM hashes h
INNER JOIN files f ON h.file_id = f.id
INNER JOIN versions v ON v.id = f.version_id AND v.status != ALL($1)
INNER JOIN mods m on v.mod_id = m.id
WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ALL($4)
",
&*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
hashes_parsed.as_slice(),
file_data.algorithm,
&*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
)
.fetch_all(&mut *transaction)
.await?;
let response = result
.into_iter()
.map(|row| {
if let Ok(parsed_hash) = String::from_utf8(row.hash) {
Ok((parsed_hash, row.url))
} else {
Err(ApiError::Database(DatabaseError::Other(format!(
"Could not parse hash for version {}",
row.version_id
))))
}
})
.collect::<Result<HashMap<String, String>, ApiError>>();
Ok(HttpResponse::Ok().json(response?))
}
#[derive(Deserialize)]
pub struct ManyUpdateData {
pub algorithm: String,
pub hashes: Vec<String>,
pub loaders: Vec<Loader>,
pub game_versions: Vec<GameVersion>,
}
#[post("update")]
pub async fn update_files(
pool: web::Data<PgPool>,
update_data: web::Json<ManyUpdateData>,
) -> Result<HttpResponse, ApiError> {
let hashes_parsed: Vec<Vec<u8>> = update_data
.hashes
.iter()
.map(|x| x.to_lowercase().as_bytes().to_vec())
.collect();
let mut transaction = pool.begin().await?;
let result = sqlx::query!(
"
SELECT h.hash, v.mod_id FROM hashes h
INNER JOIN files f ON h.file_id = f.id
INNER JOIN versions v ON v.id = f.version_id AND v.status != ALL($1)
INNER JOIN mods m on v.mod_id = m.id
WHERE h.algorithm = $3 AND h.hash = ANY($2::bytea[]) AND m.status != ALL($4)
",
&*crate::models::projects::VersionStatus::iterator()
.filter(|x| x.is_hidden())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
hashes_parsed.as_slice(),
update_data.algorithm,
&*crate::models::projects::ProjectStatus::iterator()
.filter(|x| x.is_hidden())
.map(|x| x.to_string())
.collect::<Vec<String>>(),
)
.fetch_many(&mut *transaction)
.try_filter_map(|e| async {
Ok(e.right()
.map(|m| (m.hash, database::models::ids::ProjectId(m.mod_id))))
})
.try_collect::<Vec<_>>()
.await?;
let mut version_ids: HashMap<database::models::VersionId, Vec<u8>> = HashMap::new();
let updated_versions = database::models::Version::get_projects_versions(
result
.iter()
.map(|x| x.1)
.collect::<Vec<database::models::ProjectId>>()
.clone(),
Some(
update_data
.game_versions
.clone()
.iter()
.map(|x| x.0.clone())
.collect(),
),
Some(
update_data
.loaders
.clone()
.iter()
.map(|x| x.0.clone())
.collect(),
),
None,
None,
None,
let files = database::models::Version::get_files_from_hash(
file_data.algorithm.clone(),
&file_data.hashes,
&**pool,
&redis,
)
.await?;
for (hash, id) in result {
if let Some(latest_version) = updated_versions.get(&id).and_then(|x| x.last()) {
version_ids.insert(*latest_version, hash);
}
}
let query_version_ids = version_ids.keys().copied().collect::<Vec<_>>();
let versions = database::models::Version::get_many_full(&query_version_ids, &**pool).await?;
let version_ids = files.iter().map(|x| x.version_id).collect::<Vec<_>>();
let versions_data = filter_authorized_versions(
database::models::Version::get_many(&version_ids, &**pool, &redis).await?,
&user_option,
&pool,
)
.await?;
let mut response = HashMap::new();
for version in versions {
let hash = version_ids.get(&version.inner.id);
if let Some(hash) = hash {
if let Ok(parsed_hash) = String::from_utf8(hash.clone()) {
response.insert(parsed_hash, models::projects::Version::from(version));
} else {
let version_id: VersionId = version.inner.id.into();
return Err(ApiError::Database(DatabaseError::Other(format!(
"Could not parse hash for version {version_id}"
))));
for version in versions_data {
for file in files.iter().filter(|x| x.version_id == version.id.into()) {
if let Some(hash) = file.hashes.get(&file_data.algorithm) {
response.insert(hash.clone(), version.clone());
}
}
}
Ok(HttpResponse::Ok().json(response))
}
#[post("project")]
pub async fn get_projects_from_hashes(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
file_data: web::Json<FileHashes>,
) -> Result<HttpResponse, ApiError> {
let user_option = get_user_from_headers(req.headers(), &**pool, &redis)
.await
.ok();
let files = database::models::Version::get_files_from_hash(
file_data.algorithm.clone(),
&file_data.hashes,
&**pool,
&redis,
)
.await?;
let project_ids = files.iter().map(|x| x.project_id).collect::<Vec<_>>();
let projects_data = filter_authorized_projects(
database::models::Project::get_many_ids(&project_ids, &**pool, &redis).await?,
&user_option,
&pool,
)
.await?;
let mut response = HashMap::new();
for project in projects_data {
for file in files.iter().filter(|x| x.project_id == project.id.into()) {
if let Some(hash) = file.hashes.get(&file_data.algorithm) {
response.insert(hash.clone(), project.clone());
}
}
}
Ok(HttpResponse::Ok().json(response))
}
#[derive(Deserialize)]
pub struct ManyUpdateData {
pub algorithm: String,
pub hashes: Vec<String>,
pub loaders: Option<Vec<String>>,
pub game_versions: Option<Vec<String>>,
pub version_types: Option<Vec<VersionType>>,
}
#[post("update")]
pub async fn update_files(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
update_data: web::Json<ManyUpdateData>,
) -> Result<HttpResponse, ApiError> {
let user_option = get_user_from_headers(req.headers(), &**pool, &redis)
.await
.ok();
let files = database::models::Version::get_files_from_hash(
update_data.algorithm.clone(),
&update_data.hashes,
&**pool,
&redis,
)
.await?;
let projects = database::models::Project::get_many_ids(
&files.iter().map(|x| x.project_id).collect::<Vec<_>>(),
&**pool,
&redis,
)
.await?;
let all_versions = database::models::Version::get_many(
&projects
.iter()
.flat_map(|x| x.versions.clone())
.collect::<Vec<_>>(),
&**pool,
&redis,
)
.await?;
let mut response = HashMap::new();
for project in projects {
for file in files.iter().filter(|x| x.project_id == project.inner.id) {
let version = all_versions
.iter()
.filter(|x| {
let mut bool = true;
if let Some(version_types) = &update_data.version_types {
bool &= version_types
.iter()
.any(|y| y.as_str() == x.inner.version_type);
}
if let Some(loaders) = &update_data.loaders {
bool &= x.loaders.iter().any(|y| loaders.contains(y));
}
if let Some(game_versions) = &update_data.game_versions {
bool &= x.game_versions.iter().any(|y| game_versions.contains(y));
}
bool
})
.sorted_by(|a, b| b.inner.date_published.cmp(&a.inner.date_published))
.next();
if let Some(version) = version {
if is_authorized_version(&version.inner, &user_option, &pool).await? {
if let Some(hash) = file.hashes.get(&update_data.algorithm) {
response.insert(
hash.clone(),
models::projects::Version::from(version.clone()),
);
}
}
}
}
}

View File

@@ -1,11 +1,11 @@
use super::ApiError;
use crate::auth::{
filter_authorized_versions, get_user_from_headers, is_authorized, is_authorized_version,
};
use crate::database;
use crate::models;
use crate::models::projects::{Dependency, FileType, VersionStatus, VersionType};
use crate::models::teams::Permissions;
use crate::util::auth::{
filter_authorized_versions, get_user_from_headers, is_authorized, is_authorized_version,
};
use crate::util::validate::validation_errors_to_string;
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
use chrono::{DateTime, Utc};
@@ -33,8 +33,8 @@ pub struct VersionListFilters {
pub loaders: Option<String>,
pub featured: Option<bool>,
pub version_type: Option<VersionType>,
pub limit: Option<u32>,
pub offset: Option<u32>,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
#[get("version")]
@@ -43,38 +43,50 @@ pub async fn version_list(
info: web::Path<(String,)>,
web::Query(filters): web::Query<VersionListFilters>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0;
let result = database::models::Project::get_from_slug_or_project_id(&string, &**pool).await?;
let result = database::models::Project::get(&string, &**pool, &redis).await?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
let user_option = get_user_from_headers(req.headers(), &**pool, &redis)
.await
.ok();
if let Some(project) = result {
if !is_authorized(&project, &user_option, &pool).await? {
if !is_authorized(&project.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
}
let id = project.id;
let version_filters = filters
.game_versions
.as_ref()
.map(|x| serde_json::from_str::<Vec<String>>(x).unwrap_or_default());
let loader_filters = filters
.loaders
.as_ref()
.map(|x| serde_json::from_str::<Vec<String>>(x).unwrap_or_default());
let mut versions = database::models::Version::get_many(&project.versions, &**pool, &redis)
.await?
.into_iter()
.skip(filters.offset.unwrap_or(0))
.take(filters.limit.unwrap_or(usize::MAX))
.filter(|x| {
let mut bool = true;
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?;
if let Some(version_type) = filters.version_type {
bool &= &*x.inner.version_type == version_type.as_str();
}
if let Some(loaders) = &loader_filters {
bool &= x.loaders.iter().any(|y| loaders.contains(y));
}
if let Some(game_versions) = &version_filters {
bool &= x.game_versions.iter().any(|y| game_versions.contains(y));
}
let mut versions = database::models::Version::get_many_full(&version_ids, &**pool).await?;
bool
})
.collect::<Vec<_>>();
let mut response = versions
.iter()
@@ -139,12 +151,15 @@ pub async fn version_project_get(
req: HttpRequest,
info: web::Path<(String, String)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let id = info.into_inner();
let version_data =
database::models::Version::get_full_from_id_slug(&id.0, &id.1, &**pool).await?;
database::models::Version::get_full_from_id_slug(&id.0, &id.1, &**pool, &redis).await?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
let user_option = get_user_from_headers(req.headers(), &**pool, &redis)
.await
.ok();
if let Some(data) = version_data {
if is_authorized_version(&data.inner, &user_option, &pool).await? {
@@ -165,14 +180,17 @@ pub async fn versions_get(
req: HttpRequest,
web::Query(ids): web::Query<VersionIds>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let version_ids = serde_json::from_str::<Vec<models::ids::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 versions_data = database::models::Version::get_many(&version_ids, &**pool, &redis).await?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
let user_option = get_user_from_headers(req.headers(), &**pool, &redis)
.await
.ok();
let versions = filter_authorized_versions(versions_data, &user_option, &pool).await?;
@@ -184,11 +202,14 @@ pub async fn version_get(
req: HttpRequest,
info: web::Path<(models::ids::VersionId,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let id = info.into_inner().0;
let version_data = database::models::Version::get_full(id.into(), &**pool).await?;
let version_data = database::models::Version::get(id.into(), &**pool, &redis).await?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
let user_option = get_user_from_headers(req.headers(), &**pool, &redis)
.await
.ok();
if let Some(data) = version_data {
if is_authorized_version(&data.inner, &user_option, &pool).await? {
@@ -240,9 +261,10 @@ pub async fn version_edit(
req: HttpRequest,
info: web::Path<(models::ids::VersionId,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
new_version: web::Json<EditVersion>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
new_version
.validate()
@@ -251,14 +273,15 @@ pub async fn version_edit(
let version_id = info.into_inner().0;
let id = version_id.into();
let result = database::models::Version::get_full(id, &**pool).await?;
let result = database::models::Version::get(id, &**pool, &redis).await?;
if let Some(version_item) = result {
let project_item =
database::models::Project::get_full(version_item.inner.project_id, &**pool).await?;
database::models::Project::get_id(version_item.inner.project_id, &**pool, &redis)
.await?;
let team_member = database::models::TeamMember::get_from_user_id_version(
version_item.inner.id,
let team_member = database::models::TeamMember::get_from_user_id_project(
version_item.inner.project_id,
user.id.into(),
&**pool,
)
@@ -390,12 +413,6 @@ pub async fn version_edit(
.execute(&mut *transaction)
.await?;
}
database::models::Project::update_game_versions(
version_item.inner.project_id,
&mut transaction,
)
.await?;
}
if let Some(loaders) = &new_version.loaders {
@@ -429,12 +446,6 @@ pub async fn version_edit(
.execute(&mut *transaction)
.await?;
}
database::models::Project::update_loaders(
version_item.inner.project_id,
&mut transaction,
)
.await?;
}
if let Some(featured) = &new_version.featured {
@@ -595,6 +606,14 @@ pub async fn version_edit(
}
}
database::models::Version::clear_cache(&version_item, &redis).await?;
database::models::Project::clear_cache(
version_item.inner.project_id,
None,
Some(true),
&redis,
)
.await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {
@@ -618,9 +637,10 @@ pub async fn version_schedule(
req: HttpRequest,
info: web::Path<(models::ids::VersionId,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
scheduling_data: web::Json<SchedulingData>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
if scheduling_data.time < Utc::now() {
return Err(ApiError::InvalidInput(
@@ -635,11 +655,11 @@ pub async fn version_schedule(
}
let string = info.into_inner().0;
let result = database::models::Version::get_full(string.into(), &**pool).await?;
let result = database::models::Version::get(string.into(), &**pool, &redis).await?;
if let Some(version_item) = result {
let team_member = database::models::TeamMember::get_from_user_id_version(
version_item.inner.id,
let team_member = database::models::TeamMember::get_from_user_id_project(
version_item.inner.project_id,
user.id.into(),
&**pool,
)
@@ -655,6 +675,7 @@ pub async fn version_schedule(
));
}
let mut transaction = pool.begin().await?;
sqlx::query!(
"
UPDATE versions
@@ -665,9 +686,12 @@ pub async fn version_schedule(
scheduling_data.time,
version_item.inner.id as database::models::ids::VersionId,
)
.execute(&**pool)
.execute(&mut *transaction)
.await?;
database::models::Version::clear_cache(&version_item, &redis).await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
@@ -679,13 +703,20 @@ pub async fn version_delete(
req: HttpRequest,
info: web::Path<(models::ids::VersionId,)>,
pool: web::Data<PgPool>,
redis: web::Data<deadpool_redis::Pool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let user = get_user_from_headers(req.headers(), &**pool, &redis).await?;
let id = info.into_inner().0;
let version = database::models::Version::get(id.into(), &**pool, &redis)
.await?
.ok_or_else(|| {
ApiError::InvalidInput("The specified version does not exist!".to_string())
})?;
if !user.role.is_admin() {
let team_member = database::models::TeamMember::get_from_user_id_version(
id.into(),
let team_member = database::models::TeamMember::get_from_user_id_project(
version.inner.project_id,
user.id.into(),
&**pool,
)
@@ -709,7 +740,11 @@ pub async fn version_delete(
let mut transaction = pool.begin().await?;
let result = database::models::Version::remove_full(id.into(), &mut transaction).await?;
let result =
database::models::Version::remove_full(version.inner.id, &redis, &mut transaction).await?;
database::models::Project::clear_cache(version.inner.project_id, None, Some(true), &redis)
.await?;
transaction.commit().await?;