Final V2 Changes (#212)

* Redo dependencies, add rejection reasons, make notifications more readable

* Fix errors, add dependency route, finish PR

* Fix clippy errors
This commit is contained in:
Geometrically
2021-06-16 09:05:35 -07:00
committed by GitHub
parent 2a4caa856e
commit d2c2503cfa
39 changed files with 2365 additions and 1303 deletions

View File

@@ -1,9 +1,9 @@
use crate::auth::get_github_user_from_token;
use crate::database::models::{generate_state_id, User};
use crate::models::error::ApiError;
use crate::models::ids::base62_impl::{parse_base62, to_base62};
use crate::models::ids::DecodingError;
use crate::models::users::Role;
use crate::util::auth::get_github_user_from_token;
use actix_web::http::StatusCode;
use actix_web::web::{scope, Data, Query, ServiceConfig};
use actix_web::{get, HttpResponse};
@@ -32,7 +32,7 @@ pub enum AuthorizationError {
#[error("Invalid Authentication credentials")]
InvalidCredentialsError,
#[error("Authentication Error: {0}")]
AuthenticationError(#[from] crate::auth::AuthenticationError),
AuthenticationError(#[from] crate::util::auth::AuthenticationError),
#[error("Error while decoding Base62")]
DecodingError(#[from] DecodingError),
}
@@ -129,78 +129,82 @@ pub async fn auth_callback(
let mut transaction = client.begin().await?;
let state_id = parse_base62(&*info.state)?;
let result = sqlx::query!(
let result_option = sqlx::query!(
"
SELECT url,expires FROM states
WHERE id = $1
",
state_id as i64
)
.fetch_one(&mut *transaction)
.fetch_optional(&mut *transaction)
.await?;
let now = Utc::now();
let duration = result.expires.signed_duration_since(now);
if let Some(result) = result_option {
let now = Utc::now();
let duration = result.expires.signed_duration_since(now);
if duration.num_seconds() < 0 {
return Err(AuthorizationError::InvalidCredentialsError);
}
if duration.num_seconds() < 0 {
return Err(AuthorizationError::InvalidCredentialsError);
}
sqlx::query!(
"
sqlx::query!(
"
DELETE FROM states
WHERE id = $1
",
state_id as i64
)
.execute(&mut *transaction)
.await?;
let client_id = dotenv::var("GITHUB_CLIENT_ID")?;
let client_secret = dotenv::var("GITHUB_CLIENT_SECRET")?;
let url = format!(
"https://github.com/login/oauth/access_token?client_id={}&client_secret={}&code={}",
client_id, client_secret, info.code
);
let token: AccessToken = reqwest::Client::new()
.post(&url)
.header(reqwest::header::ACCEPT, "application/json")
.send()
.await?
.json()
state_id as i64
)
.execute(&mut *transaction)
.await?;
let user = get_github_user_from_token(&*token.access_token).await?;
let client_id = dotenv::var("GITHUB_CLIENT_ID")?;
let client_secret = dotenv::var("GITHUB_CLIENT_SECRET")?;
let user_result = User::get_from_github_id(user.id, &mut *transaction).await?;
match user_result {
Some(x) => info!("{:?}", x.id),
None => {
let user_id = crate::database::models::generate_user_id(&mut transaction).await?;
let url = format!(
"https://github.com/login/oauth/access_token?client_id={}&client_secret={}&code={}",
client_id, client_secret, info.code
);
User {
id: user_id,
github_id: Some(user.id as i64),
username: user.login,
name: user.name,
email: user.email,
avatar_url: Some(user.avatar_url),
bio: user.bio,
created: Utc::now(),
role: Role::Developer.to_string(),
}
.insert(&mut transaction)
let token: AccessToken = reqwest::Client::new()
.post(&url)
.header(reqwest::header::ACCEPT, "application/json")
.send()
.await?
.json()
.await?;
let user = get_github_user_from_token(&*token.access_token).await?;
let user_result = User::get_from_github_id(user.id, &mut *transaction).await?;
match user_result {
Some(x) => info!("{:?}", x.id),
None => {
let user_id = crate::database::models::generate_user_id(&mut transaction).await?;
User {
id: user_id,
github_id: Some(user.id as i64),
username: user.login,
name: user.name,
email: user.email,
avatar_url: Some(user.avatar_url),
bio: user.bio,
created: Utc::now(),
role: Role::Developer.to_string(),
}
.insert(&mut transaction)
.await?;
}
}
transaction.commit().await?;
let redirect_url = format!("{}?code={}", result.url, token.access_token);
Ok(HttpResponse::TemporaryRedirect()
.header("Location", &*redirect_url)
.json(AuthorizationInit { url: redirect_url }))
} else {
Err(AuthorizationError::InvalidCredentialsError)
}
transaction.commit().await?;
let redirect_url = format!("{}?code={}", result.url, token.access_token);
Ok(HttpResponse::TemporaryRedirect()
.header("Location", &*redirect_url)
.json(AuthorizationInit { url: redirect_url }))
}

View File

@@ -1,7 +1,7 @@
use crate::auth::get_user_from_headers;
use crate::database;
use crate::models::projects::ProjectId;
use crate::routes::ApiError;
use crate::util::auth::get_user_from_headers;
use actix_web::{get, web, HttpRequest, HttpResponse};
use sqlx::PgPool;
use yaserde_derive::YaSerialize;

View File

@@ -55,7 +55,8 @@ pub fn projects_config(cfg: &mut web::ServiceConfig) {
.service(projects::project_follow)
.service(projects::project_unfollow)
.service(teams::team_members_get_project)
.service(web::scope("{project_id}").service(versions::version_list)),
.service(web::scope("{project_id}").service(versions::version_list))
.service(projects::dependency_list),
);
}
@@ -119,6 +120,7 @@ pub fn teams_config(cfg: &mut web::ServiceConfig) {
pub fn notifications_config(cfg: &mut web::ServiceConfig) {
cfg.service(notifications::notifications_get);
cfg.service(notifications::notification_delete);
cfg.service(
web::scope("notification")
@@ -152,13 +154,13 @@ pub enum ApiError {
#[error("Deserialization error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Authentication Error: {0}")]
AuthenticationError(#[from] crate::auth::AuthenticationError),
AuthenticationError(#[from] crate::util::auth::AuthenticationError),
#[error("Authentication Error: {0}")]
CustomAuthenticationError(String),
#[error("Invalid Input: {0}")]
InvalidInputError(String),
#[error("Error while validating input: {0}")]
ValidationError(#[from] validator::ValidationErrors),
ValidationError(String),
#[error("Search Error: {0}")]
SearchError(#[from] meilisearch_sdk::errors::Error),
#[error("Indexing Error: {0}")]

View File

@@ -1,7 +1,7 @@
use super::ApiError;
use crate::auth::check_is_moderator_from_headers;
use crate::database;
use crate::models::projects::{Project, ProjectStatus};
use crate::util::auth::check_is_moderator_from_headers;
use actix_web::{get, web, HttpRequest, HttpResponse};
use serde::Deserialize;
use sqlx::PgPool;

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, NotificationAction};
use crate::routes::ApiError;
use crate::util::auth::get_user_from_headers;
use actix_web::{delete, get, web, HttpRequest, HttpResponse};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
@@ -70,6 +70,7 @@ pub fn convert_notification(
Notification {
id: notif.id.into(),
user_id: notif.user_id.into(),
type_: notif.notification_type,
title: notif.title,
text: notif.text,
link: notif.link,
@@ -101,7 +102,12 @@ pub async fn notification_delete(
if let Some(data) = notification_data {
if data.user_id == user.id.into() || user.role.is_mod() {
database::models::notification_item::Notification::remove(id.into(), &**pool).await?;
let mut transaction = pool.begin().await?;
database::models::notification_item::Notification::remove(id.into(), &mut transaction)
.await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {
@@ -113,3 +119,38 @@ pub async fn notification_delete(
Ok(HttpResponse::NotFound().body(""))
}
}
#[delete("notifications")]
pub async fn notifications_delete(
req: HttpRequest,
web::Query(ids): web::Query<NotificationIds>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
let notification_ids = serde_json::from_str::<Vec<NotificationId>>(&*ids.ids)?
.into_iter()
.map(|x| x.into())
.collect();
let mut transaction = pool.begin().await?;
let notifications_data =
database::models::notification_item::Notification::get_many(notification_ids, &**pool)
.await?;
let mut notifications: Vec<database::models::ids::NotificationId> = Vec::new();
for notification in notifications_data {
if notification.user_id == user.id.into() || user.role.is_mod() {
notifications.push(notification.id);
}
}
database::models::notification_item::Notification::remove_many(notifications, &mut transaction)
.await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
}

View File

@@ -1,4 +1,3 @@
use crate::auth::{get_user_from_headers, AuthenticationError};
use crate::database::models;
use crate::file_hosting::{FileHost, FileHostingError};
use crate::models::error::ApiError;
@@ -8,13 +7,13 @@ use crate::models::projects::{
use crate::models::users::UserId;
use crate::routes::version_creation::InitialVersionData;
use crate::search::indexing::{queue::CreationQueue, IndexingError};
use crate::util::auth::{get_user_from_headers, AuthenticationError};
use crate::util::validate::validation_errors_to_string;
use actix_multipart::{Field, Multipart};
use actix_web::http::StatusCode;
use actix_web::web::Data;
use actix_web::{post, HttpRequest, HttpResponse};
use futures::stream::StreamExt;
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
use sqlx::postgres::PgPool;
use std::sync::Arc;
@@ -36,7 +35,7 @@ pub enum CreateError {
#[error("Error while parsing JSON: {0}")]
SerDeError(#[from] serde_json::Error),
#[error("Error while validating input: {0}")]
ValidationError(#[from] validator::ValidationErrors),
ValidationError(String),
#[error("Error while uploading file")]
FileHostingError(#[from] FileHostingError),
#[error("Error while validating uploaded file: {0}")]
@@ -116,10 +115,6 @@ impl actix_web::ResponseError for CreateError {
}
}
lazy_static! {
static ref RE_URL_SAFE: Regex = Regex::new(r"^[a-zA-Z0-9_-]*$").unwrap();
}
fn default_project_type() -> String {
"mod".to_string()
}
@@ -134,7 +129,10 @@ struct ProjectCreateData {
#[serde(default = "default_project_type")]
/// The project type of this mod
pub project_type: String,
#[validate(length(min = 3, max = 64), regex = "RE_URL_SAFE")]
#[validate(
length(min = 3, max = 64),
regex = "crate::util::validate::RE_URL_SAFE"
)]
#[serde(alias = "mod_slug")]
/// The slug of a project, used for vanity URLs
pub slug: String,
@@ -153,6 +151,7 @@ struct ProjectCreateData {
pub server_side: SideType,
#[validate(length(max = 64))]
#[validate]
/// A list of initial versions to upload with the created project
pub initial_versions: Vec<InitialVersionData>,
#[validate(length(max = 3))]
@@ -326,7 +325,9 @@ pub async fn project_create_inner(
}
let create_data: ProjectCreateData = serde_json::from_slice(&data)?;
create_data.validate()?;
create_data
.validate()
.map_err(|err| CreateError::InvalidInput(validation_errors_to_string(err, None)))?;
let slug_project_id_option: Option<ProjectId> =
serde_json::from_str(&*format!("\"{}\"", create_data.slug)).ok();
@@ -498,6 +499,12 @@ pub async fn project_create_inner(
status = ProjectStatus::Draft;
} else {
status = ProjectStatus::Processing;
if project_create_data.initial_versions.is_empty() {
return Err(CreateError::InvalidInput(String::from(
"Project submitted for review with no initial versions",
)));
}
}
let status_id = models::StatusId::get_id(&status, &mut *transaction)
@@ -590,6 +597,7 @@ pub async fn project_create_inner(
published: now,
updated: now,
status: status.clone(),
rejection_data: None,
license: License {
id: project_create_data.license_id.clone(),
name: "".to_string(),
@@ -622,6 +630,12 @@ pub async fn project_create_inner(
)
.await?;
indexing_queue.add(index_project);
if let Ok(webhook_url) = dotenv::var("MODERATION_DISCORD_WEBHOOK") {
crate::util::webhook::send_discord_webhook(response.clone(), webhook_url)
.await
.ok();
}
}
Ok(HttpResponse::Ok().json(response))
@@ -643,7 +657,9 @@ async fn create_initial_version(
)));
}
version_data.validate()?;
version_data
.validate()
.map_err(|err| CreateError::ValidationError(validation_errors_to_string(err, None)))?;
// Randomly generate a new id to be used for the version
let version_id: VersionId = models::generate_version_id(transaction).await?.into();
@@ -684,7 +700,11 @@ async fn create_initial_version(
let dependencies = version_data
.dependencies
.iter()
.map(|x| ((x.version_id).into(), x.dependency_type.to_string()))
.map(|d| models::version_item::DependencyBuilder {
version_id: d.version_id.map(|x| x.into()),
project_id: d.project_id.map(|x| x.into()),
dependency_type: d.dependency_type.to_string(),
})
.collect::<Vec<_>>();
let version = models::version_item::VersionBuilder {

View File

@@ -1,21 +1,23 @@
use crate::auth::get_user_from_headers;
use crate::database;
use crate::database::cache::project_cache::remove_cache_project;
use crate::database::cache::query_project_cache::remove_cache_query_project;
use crate::file_hosting::FileHost;
use crate::models;
use crate::models::projects::{
DonationLink, License, ProjectId, ProjectStatus, SearchRequest, SideType,
DonationLink, License, ProjectId, ProjectStatus, RejectionReason, SearchRequest, SideType,
};
use crate::models::teams::Permissions;
use crate::routes::ApiError;
use crate::search::indexing::queue::CreationQueue;
use crate::search::{search_for_project, SearchConfig, SearchError};
use crate::util::auth::get_user_from_headers;
use crate::util::validate::validation_errors_to_string;
use actix_web::web::Data;
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
use futures::StreamExt;
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use std::collections::HashMap;
use std::sync::Arc;
use validator::Validate;
@@ -91,7 +93,8 @@ pub async fn project_get(
let string = info.into_inner().0;
let project_data =
database::models::Project::get_full_from_slug_or_project_id(string, &**pool).await?;
database::models::Project::get_full_from_slug_or_project_id(string.clone(), &**pool)
.await?;
let user_option = get_user_from_headers(req.headers(), &**pool).await.ok();
@@ -129,6 +132,94 @@ pub async fn project_get(
}
}
struct DependencyInfo {
pub project: Option<models::projects::Project>,
pub version: Option<models::projects::Version>,
}
#[get("dependencies")]
pub async fn dependency_list(
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let string = info.into_inner().0;
let result = database::models::Project::get_from_slug_or_project_id(string, &**pool).await?;
if let Some(project) = result {
let id = project.id;
use futures::stream::TryStreamExt;
let dependencies = sqlx::query!(
"
SELECT d.dependent_id, d.dependency_id, d.mod_dependency_id
FROM versions v
INNER JOIN dependencies d ON d.dependent_id = v.id
WHERE v.mod_id = $1
",
id as database::models::ProjectId
)
.fetch_many(&**pool)
.try_filter_map(|e| async {
Ok(e.right().map(|x| {
(
database::models::VersionId(x.dependent_id),
x.dependency_id.map(database::models::VersionId),
x.mod_dependency_id.map(database::models::ProjectId),
)
}))
})
.try_collect::<Vec<(
database::models::VersionId,
Option<database::models::VersionId>,
Option<database::models::ProjectId>,
)>>()
.await?;
let projects = database::Project::get_many_full(
dependencies.iter().map(|x| x.2).flatten().collect(),
&**pool,
)
.await?;
let versions = database::Version::get_many_full(
dependencies.iter().map(|x| x.1).flatten().collect(),
&**pool,
)
.await?;
let mut response: HashMap<models::projects::VersionId, DependencyInfo> = HashMap::new();
for dependency in dependencies {
response.insert(
dependency.0.into(),
DependencyInfo {
project: if let Some(id) = dependency.2 {
projects
.iter()
.find(|x| x.inner.id == id)
.map(|x| convert_project(x.clone()))
} else {
None
},
version: if let Some(id) = dependency.1 {
versions
.iter()
.find(|x| x.id == id)
.map(|x| super::versions::convert_version(x.clone()))
} else {
None
},
},
);
}
Ok(HttpResponse::NotFound().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
}
}
pub fn convert_project(
data: database::models::project_item::QueryProject,
) -> models::projects::Project {
@@ -146,6 +237,14 @@ pub fn convert_project(
published: m.published,
updated: m.updated,
status: data.status,
rejection_data: if let Some(reason) = m.rejection_reason {
Some(RejectionReason {
reason,
body: m.rejection_body,
})
} else {
None
},
license: License {
id: data.license_id,
name: data.license_name,
@@ -175,10 +274,6 @@ pub fn convert_project(
}
}
lazy_static! {
static ref RE_URL_SAFE: Regex = Regex::new(r"^[a-zA-Z0-9_-]*$").unwrap();
}
/// A project returned from the API
#[derive(Serialize, Deserialize, Validate)]
pub struct EditProject {
@@ -188,7 +283,6 @@ pub struct EditProject {
pub description: Option<String>,
#[validate(length(max = 65536))]
pub body: Option<String>,
pub status: Option<ProjectStatus>,
#[validate(length(max = 3))]
pub categories: Option<Vec<String>>,
#[serde(
@@ -236,8 +330,26 @@ pub struct EditProject {
skip_serializing_if = "Option::is_none",
with = "::serde_with::rust::double_option"
)]
#[validate(length(min = 3, max = 64), regex = "RE_URL_SAFE")]
#[validate(
length(min = 3, max = 64),
regex = "crate::util::validate::RE_URL_SAFE"
)]
pub slug: Option<Option<String>>,
pub status: Option<ProjectStatus>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "::serde_with::rust::double_option"
)]
#[validate(length(max = 2000))]
pub rejection_reason: Option<Option<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "::serde_with::rust::double_option"
)]
#[validate(length(max = 65536))]
pub rejection_body: Option<Option<String>>,
}
#[patch("{id}")]
@@ -251,11 +363,14 @@ pub async fn project_edit(
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
new_project.validate()?;
new_project
.validate()
.map_err(|err| ApiError::ValidationError(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?;
database::models::Project::get_full_from_slug_or_project_id(string.clone(), &**pool)
.await?;
if let Some(project_item) = result {
let id = project_item.inner.id;
@@ -337,6 +452,12 @@ pub async fn project_edit(
));
}
if status == &ProjectStatus::Processing && project_item.versions.is_empty() {
return Err(ApiError::InvalidInputError(String::from(
"Project submitted for review with no initial versions",
)));
}
let status_id = database::models::StatusId::get_id(&status, &mut *transaction)
.await?
.ok_or_else(|| {
@@ -357,6 +478,30 @@ pub async fn project_edit(
.execute(&mut *transaction)
.await?;
if project_item.status == ProjectStatus::Processing {
sqlx::query!(
"
UPDATE mods
SET rejection_reason = NULL
WHERE (id = $1)
",
id as database::models::ids::ProjectId,
)
.execute(&mut *transaction)
.await?;
sqlx::query!(
"
UPDATE mods
SET rejection_body = NULL
WHERE (id = $1)
",
id as database::models::ids::ProjectId,
)
.execute(&mut *transaction)
.await?;
}
if project_item.status.is_searchable() && !status.is_searchable() {
delete_from_index(id.into(), config).await?;
} else if !project_item.status.is_searchable() && status.is_searchable() {
@@ -365,6 +510,15 @@ pub async fn project_edit(
.await?;
indexing_queue.add(index_project);
if let Ok(webhook_url) = dotenv::var("MODERATION_DISCORD_WEBHOOK") {
crate::util::webhook::send_discord_webhook(
convert_project(project_item.clone()),
webhook_url,
)
.await
.ok();
}
}
}
@@ -684,6 +838,48 @@ pub async fn project_edit(
}
}
if let Some(rejection_reason) = &new_project.rejection_reason {
if !user.role.is_mod() {
return Err(ApiError::CustomAuthenticationError(
"You do not have the permissions to edit the rejection reason of this project!"
.to_string(),
));
}
sqlx::query!(
"
UPDATE mods
SET rejection_reason = $1
WHERE (id = $2)
",
rejection_reason.as_deref(),
id as database::models::ids::ProjectId,
)
.execute(&mut *transaction)
.await?;
}
if let Some(rejection_body) = &new_project.rejection_body {
if !user.role.is_mod() {
return Err(ApiError::CustomAuthenticationError(
"You do not have the permissions to edit the rejection body of this project!"
.to_string(),
));
}
sqlx::query!(
"
UPDATE mods
SET rejection_body = $1
WHERE (id = $2)
",
rejection_body.as_deref(),
id as database::models::ids::ProjectId,
)
.execute(&mut *transaction)
.await?;
}
if let Some(body) = &new_project.body {
if !perms.contains(Permissions::EDIT_BODY) {
return Err(ApiError::CustomAuthenticationError(
@@ -705,6 +901,9 @@ pub async fn project_edit(
.await?;
}
remove_cache_project(string.clone()).await;
remove_cache_query_project(string).await;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {
@@ -736,11 +935,12 @@ pub async fn project_icon_edit(
let user = get_user_from_headers(req.headers(), &**pool).await?;
let string = info.into_inner().0;
let project_item = database::models::Project::get_from_slug_or_project_id(string, &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError("The specified project does not exist!".to_string())
})?;
let project_item =
database::models::Project::get_from_slug_or_project_id(string.clone(), &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError("The specified project does not exist!".to_string())
})?;
if !user.role.is_mod() {
let team_member = database::models::TeamMember::get_from_user_id(
@@ -782,12 +982,14 @@ pub async fn project_icon_edit(
)));
}
let hash = sha1::Sha1::from(bytes.clone()).hexdigest();
let project_id: ProjectId = project_item.id.into();
let upload_data = file_host
.upload_file(
content_type,
&format!("data/{}/icon.{}", project_id, ext.ext),
&format!("data/{}/{}.{}", project_id, hash, ext.ext),
bytes.to_vec(),
)
.await?;
@@ -804,6 +1006,9 @@ pub async fn project_icon_edit(
.execute(&**pool)
.await?;
remove_cache_project(string.clone()).await;
remove_cache_query_project(string).await;
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::InvalidInputError(format!(
@@ -823,7 +1028,7 @@ pub async fn project_delete(
let user = get_user_from_headers(req.headers(), &**pool).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_from_slug_or_project_id(string.clone(), &**pool)
.await?
.ok_or_else(|| {
ApiError::InvalidInputError("The specified project does not exist!".to_string())
@@ -851,7 +1056,14 @@ pub async fn project_delete(
}
}
let result = database::models::Project::remove_full(project.id, &**pool).await?;
let mut transaction = pool.begin().await?;
let result = database::models::Project::remove_full(project.id, &mut transaction).await?;
remove_cache_project(string.clone()).await;
remove_cache_query_project(string).await;
transaction.commit().await?;
delete_from_index(project.id.into(), config).await?;
@@ -893,6 +1105,8 @@ pub async fn project_follow(
.unwrap_or(false);
if !following {
let mut transaction = pool.begin().await?;
sqlx::query!(
"
UPDATE mods
@@ -901,7 +1115,7 @@ pub async fn project_follow(
",
project_id as database::models::ids::ProjectId,
)
.execute(&**pool)
.execute(&mut *transaction)
.await?;
sqlx::query!(
@@ -912,9 +1126,11 @@ pub async fn project_follow(
user_id as database::models::ids::UserId,
project_id as database::models::ids::ProjectId
)
.execute(&**pool)
.execute(&mut *transaction)
.await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::InvalidInputError(
@@ -954,6 +1170,8 @@ pub async fn project_unfollow(
.unwrap_or(false);
if following {
let mut transaction = pool.begin().await?;
sqlx::query!(
"
UPDATE mods
@@ -962,7 +1180,7 @@ pub async fn project_unfollow(
",
project_id as database::models::ids::ProjectId,
)
.execute(&**pool)
.execute(&mut *transaction)
.await?;
sqlx::query!(
@@ -973,9 +1191,11 @@ pub async fn project_unfollow(
user_id as database::models::ids::UserId,
project_id as database::models::ids::ProjectId
)
.execute(&**pool)
.execute(&mut *transaction)
.await?;
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::InvalidInputError(

View File

@@ -1,7 +1,7 @@
use crate::auth::{check_is_moderator_from_headers, get_user_from_headers};
use crate::models::ids::{ProjectId, UserId, VersionId};
use crate::models::reports::{ItemType, Report};
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::StreamExt;
use serde::Deserialize;

View File

@@ -1,7 +1,7 @@
use super::ApiError;
use crate::auth::check_is_admin_from_headers;
use crate::database::models;
use crate::database::models::categories::{DonationPlatform, License, ProjectType, ReportType};
use crate::util::auth::check_is_admin_from_headers;
use actix_web::{delete, get, put, web, HttpRequest, HttpResponse};
use models::categories::{Category, GameVersion, Loader};
use sqlx::PgPool;

View File

@@ -1,4 +1,3 @@
use crate::auth::get_user_from_headers;
use crate::database::models::notification_item::{NotificationActionBuilder, NotificationBuilder};
use crate::database::models::team_item::QueryTeamMember;
use crate::database::models::TeamMember;
@@ -6,6 +5,7 @@ use crate::models::ids::ProjectId;
use crate::models::teams::{Permissions, TeamId};
use crate::models::users::UserId;
use crate::routes::ApiError;
use crate::util::auth::get_user_from_headers;
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
@@ -246,6 +246,7 @@ pub async fn add_team_member(
let team: TeamId = team_id.into();
NotificationBuilder {
notification_type: Some("team_invite".to_string()),
title: "You have been invited to join a team!".to_string(),
text: format!(
"Team invite from {} to join the team for project {}",

View File

@@ -1,4 +1,3 @@
use crate::auth::get_user_from_headers;
use crate::database::models::User;
use crate::file_hosting::FileHost;
use crate::models::notifications::Notification;
@@ -6,6 +5,8 @@ use crate::models::projects::{Project, ProjectStatus};
use crate::models::users::{Role, UserId};
use crate::routes::notifications::convert_notification;
use crate::routes::ApiError;
use crate::util::auth::get_user_from_headers;
use crate::util::validate::validation_errors_to_string;
use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse};
use futures::StreamExt;
use lazy_static::lazy_static;
@@ -166,7 +167,9 @@ pub async fn user_edit(
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
new_user.validate()?;
new_user
.validate()
.map_err(|err| ApiError::ValidationError(validation_errors_to_string(err, None)))?;
let id_option =
crate::database::models::User::get_id_from_username_or_id(info.into_inner().0, &**pool)
@@ -396,13 +399,17 @@ pub async fn user_delete(
));
}
let mut transaction = pool.begin().await?;
let result;
if &*removal_type.removal_type == "full" {
result = crate::database::models::User::remove_full(id, &**pool).await?;
result = crate::database::models::User::remove_full(id, &mut transaction).await?;
} else {
result = crate::database::models::User::remove(id, &**pool).await?;
result = crate::database::models::User::remove(id, &mut transaction).await?;
};
transaction.commit().await?;
if result.is_some() {
Ok(HttpResponse::NoContent().body(""))
} else {

View File

@@ -1,8 +1,8 @@
use crate::auth::check_is_moderator_from_headers;
use crate::database;
use crate::models::projects::{Project, ProjectStatus};
use crate::routes::moderation::ResultCount;
use crate::routes::ApiError;
use crate::util::auth::check_is_moderator_from_headers;
use actix_web::web;
use actix_web::{get, HttpRequest, HttpResponse};
use sqlx::PgPool;

View File

@@ -1,4 +1,3 @@
use crate::auth::get_user_from_headers;
use crate::file_hosting::FileHost;
use crate::models::projects::SearchRequest;
use crate::routes::project_creation::{project_create_inner, undo_uploads, CreateError};
@@ -6,6 +5,7 @@ use crate::routes::projects::{convert_project, ProjectIds};
use crate::routes::ApiError;
use crate::search::indexing::queue::CreationQueue;
use crate::search::{search_for_project, SearchConfig, SearchError};
use crate::util::auth::get_user_from_headers;
use crate::{database, models};
use actix_multipart::Multipart;
use actix_web::web;

View File

@@ -1,8 +1,8 @@
use crate::auth::{check_is_moderator_from_headers, get_user_from_headers};
use crate::models::ids::ReportId;
use crate::models::projects::{ProjectId, VersionId};
use crate::models::users::UserId;
use crate::routes::ApiError;
use crate::util::auth::{check_is_moderator_from_headers, get_user_from_headers};
use actix_web::web;
use actix_web::{get, post, HttpRequest, HttpResponse};
use chrono::{DateTime, Utc};

View File

@@ -1,6 +1,6 @@
use crate::auth::check_is_admin_from_headers;
use crate::database::models::categories::{Category, GameVersion, Loader, ProjectType};
use crate::routes::ApiError;
use crate::util::auth::check_is_admin_from_headers;
use actix_web::{get, put, web};
use actix_web::{HttpRequest, HttpResponse};
use sqlx::PgPool;

View File

@@ -1,7 +1,7 @@
use crate::auth::get_user_from_headers;
use crate::models::teams::{Permissions, TeamId};
use crate::models::users::UserId;
use crate::routes::ApiError;
use crate::util::auth::get_user_from_headers;
use actix_web::{get, web, HttpRequest, HttpResponse};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;

View File

@@ -1,8 +1,8 @@
use crate::auth::get_user_from_headers;
use crate::database::models::User;
use crate::models::ids::UserId;
use crate::models::projects::{ProjectId, ProjectStatus};
use crate::routes::ApiError;
use crate::util::auth::get_user_from_headers;
use actix_web::web;
use actix_web::{get, HttpRequest, HttpResponse};
use sqlx::PgPool;

View File

@@ -1,10 +1,10 @@
use crate::auth::get_user_from_headers;
use crate::file_hosting::FileHost;
use crate::models::ids::{ProjectId, UserId, VersionId};
use crate::models::projects::{Dependency, GameVersion, Loader, Version, VersionFile, VersionType};
use crate::models::teams::Permissions;
use crate::routes::versions::{convert_version, VersionIds, VersionListFilters};
use crate::routes::ApiError;
use crate::util::auth::get_user_from_headers;
use crate::{database, models, Pepper};
use actix_web::{delete, get, web, HttpRequest, HttpResponse};
use chrono::{DateTime, Utc};

View File

@@ -1,4 +1,3 @@
use crate::auth::get_user_from_headers;
use crate::database::models;
use crate::database::models::notification_item::NotificationBuilder;
use crate::database::models::version_item::{VersionBuilder, VersionFileBuilder};
@@ -8,28 +7,27 @@ use crate::models::projects::{
};
use crate::models::teams::Permissions;
use crate::routes::project_creation::{CreateError, UploadedFile};
use crate::util::auth::get_user_from_headers;
use crate::util::validate::validation_errors_to_string;
use crate::validate::{validate_file, ValidationResult};
use actix_multipart::{Field, Multipart};
use actix_web::web::Data;
use actix_web::{post, HttpRequest, HttpResponse};
use futures::stream::StreamExt;
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
use sqlx::postgres::PgPool;
use validator::Validate;
lazy_static! {
static ref RE_URL_SAFE: Regex = Regex::new(r"^[a-zA-Z0-9_\-.]*$").unwrap();
}
#[derive(Serialize, Deserialize, Validate, Clone)]
pub struct InitialVersionData {
#[serde(alias = "mod_id")]
pub project_id: Option<ProjectId>,
#[validate(length(min = 1, max = 256))]
pub file_parts: Vec<String>,
#[validate(length(min = 1, max = 64), regex = "RE_URL_SAFE")]
#[validate(
length(min = 1, max = 64),
regex = "crate::util::validate::RE_URL_SAFE"
)]
pub version_number: String,
#[validate(length(min = 3, max = 256))]
pub version_title: String,
@@ -127,7 +125,9 @@ async fn version_create_inner(
));
}
version_create_data.validate()?;
version_create_data.validate().map_err(|err| {
CreateError::ValidationError(validation_errors_to_string(err, None))
})?;
let project_id: models::ProjectId = version_create_data.project_id.unwrap().into();
@@ -234,7 +234,11 @@ async fn version_create_inner(
let dependencies = version_create_data
.dependencies
.iter()
.map(|x| ((x.version_id).into(), x.dependency_type.to_string()))
.map(|d| models::version_item::DependencyBuilder {
version_id: d.version_id.map(|x| x.into()),
project_id: d.project_id.map(|x| x.into()),
dependency_type: d.dependency_type.to_string(),
})
.collect::<Vec<_>>();
version_builder = Some(VersionBuilder {
@@ -332,9 +336,10 @@ async fn version_create_inner(
let version_id: VersionId = builder.version_id.into();
NotificationBuilder {
title: "A project you followed has been updated!".to_string(),
notification_type: Some("project_update".to_string()),
title: format!("**{}** has been updated!", result.title),
text: format!(
"Project {} has been updated to version {}",
"The project, {}, has released a new version: {}",
result.title,
version_data.version_number.clone()
),

View File

@@ -1,9 +1,9 @@
use super::ApiError;
use crate::auth::get_user_from_headers;
use crate::file_hosting::FileHost;
use crate::models;
use crate::models::projects::{GameVersion, Loader};
use crate::models::teams::Permissions;
use crate::util::auth::get_user_from_headers;
use crate::{database, Pepper};
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse};
use serde::{Deserialize, Serialize};
@@ -118,7 +118,19 @@ async fn download_version_inner(
pepper: &web::Data<Pepper>,
) -> Result<(), ApiError> {
let real_ip = req.connection_info();
let ip_option = real_ip.borrow().remote_addr();
let ip_option = if dotenv::var("CLOUDFLARE_INTEGRATION")
.ok()
.map(|i| i.parse().unwrap())
.unwrap_or(false)
{
if let Some(header) = req.headers().get("CF-Connecting-IP") {
header.to_str().ok()
} else {
real_ip.borrow().remote_addr()
}
} else {
real_ip.borrow().remote_addr()
};
if let Some(ip) = ip_option {
let hash = sha1::Sha1::from(format!("{}{}", ip, pepper.pepper)).hexdigest();

View File

@@ -1,12 +1,11 @@
use super::ApiError;
use crate::auth::get_user_from_headers;
use crate::database;
use crate::models;
use crate::models::projects::{Dependency, DependencyType};
use crate::models::teams::Permissions;
use crate::util::auth::get_user_from_headers;
use crate::util::validate::validation_errors_to_string;
use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse};
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use validator::Validate;
@@ -189,8 +188,9 @@ pub fn convert_version(
.dependencies
.into_iter()
.map(|d| Dependency {
version_id: d.0.into(),
dependency_type: DependencyType::from_str(&*d.1),
version_id: d.version_id.map(|x| x.into()),
project_id: d.project_id.map(|x| x.into()),
dependency_type: DependencyType::from_str(&*d.dependency_type),
})
.collect(),
game_versions: data
@@ -206,15 +206,14 @@ pub fn convert_version(
}
}
lazy_static! {
static ref RE_URL_SAFE: Regex = Regex::new(r"^[a-zA-Z0-9_-]*$").unwrap();
}
#[derive(Serialize, Deserialize, Validate)]
pub struct EditVersion {
#[validate(length(min = 3, max = 256))]
pub name: Option<String>,
#[validate(length(min = 1, max = 64), regex = "RE_URL_SAFE")]
#[validate(
length(min = 1, max = 64),
regex = "crate::util::validate::RE_URL_SAFE"
)]
pub version_number: Option<String>,
#[validate(length(max = 65536))]
pub changelog: Option<String>,
@@ -236,7 +235,9 @@ pub async fn version_edit(
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(req.headers(), &**pool).await?;
new_version.validate()?;
new_version
.validate()
.map_err(|err| ApiError::ValidationError(validation_errors_to_string(err, None)))?;
let version_id = info.into_inner().0;
let id = version_id.into();
@@ -332,21 +333,17 @@ pub async fn version_edit(
.execute(&mut *transaction)
.await?;
for dependency in dependencies {
let dependency_id: database::models::ids::VersionId =
dependency.version_id.clone().into();
let builders = dependencies
.iter()
.map(|x| database::models::version_item::DependencyBuilder {
project_id: x.project_id.clone().map(|x| x.into()),
version_id: x.version_id.clone().map(|x| x.into()),
dependency_type: x.dependency_type.to_string(),
})
.collect::<Vec<database::models::version_item::DependencyBuilder>>();
sqlx::query!(
"
INSERT INTO dependencies (dependent_id, dependency_id, dependency_type)
VALUES ($1, $2, $3)
",
id as database::models::ids::VersionId,
dependency_id as database::models::ids::VersionId,
dependency.dependency_type.as_str()
)
.execute(&mut *transaction)
.await?;
for dependency in builders {
dependency.insert(version_item.id, &mut transaction).await?;
}
}
@@ -533,7 +530,11 @@ pub async fn version_delete(
}
}
let result = database::models::Version::remove_full(id.into(), &**pool).await?;
let mut transaction = pool.begin().await?;
let result = database::models::Version::remove_full(id.into(), &mut transaction).await?;
transaction.commit().await?;
if result.is_some() {
Ok(HttpResponse::NoContent().body(""))