You've already forked AstralRinth
forked from didirus/AstralRinth
Optimize user-generated images for reduced bandwidth (#961)
* Optimize user-generated images for reduced bandwidth * run prepare * Finish compression
This commit is contained in:
@@ -14,7 +14,8 @@ use crate::routes::internal::session::issue_session;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::captcha::check_turnstile_captcha;
|
||||
use crate::util::env::parse_strings_from_var;
|
||||
use crate::util::ext::{get_image_content_type, get_image_ext};
|
||||
use crate::util::ext::get_image_ext;
|
||||
use crate::util::img::upload_image_optimized;
|
||||
use crate::util::validate::{validation_errors_to_string, RE_URL_SAFE};
|
||||
use actix_web::web::{scope, Data, Payload, Query, ServiceConfig};
|
||||
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
|
||||
@@ -112,9 +113,7 @@ impl TempUser {
|
||||
}
|
||||
}
|
||||
|
||||
let avatar_url = if let Some(avatar_url) = self.avatar_url {
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
|
||||
let (avatar_url, raw_avatar_url) = if let Some(avatar_url) = self.avatar_url {
|
||||
let res = reqwest::get(&avatar_url).await?;
|
||||
let headers = res.headers().clone();
|
||||
|
||||
@@ -122,36 +121,34 @@ impl TempUser {
|
||||
.get(reqwest::header::CONTENT_TYPE)
|
||||
.and_then(|ct| ct.to_str().ok())
|
||||
{
|
||||
get_image_ext(content_type).map(|ext| (ext, content_type))
|
||||
} else if let Some(ext) = avatar_url.rsplit('.').next() {
|
||||
get_image_content_type(ext).map(|content_type| (ext, content_type))
|
||||
get_image_ext(content_type)
|
||||
} else {
|
||||
None
|
||||
avatar_url.rsplit('.').next()
|
||||
};
|
||||
|
||||
if let Some((ext, content_type)) = img_data {
|
||||
if let Some(ext) = img_data {
|
||||
let bytes = res.bytes().await?;
|
||||
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
||||
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!(
|
||||
"user/{}/{}.{}",
|
||||
crate::models::users::UserId::from(user_id),
|
||||
hash,
|
||||
ext
|
||||
),
|
||||
bytes,
|
||||
)
|
||||
.await?;
|
||||
let upload_result = upload_image_optimized(
|
||||
&format!("user/{}", crate::models::users::UserId::from(user_id)),
|
||||
bytes,
|
||||
ext,
|
||||
Some(96),
|
||||
Some(1.0),
|
||||
&**file_host,
|
||||
)
|
||||
.await;
|
||||
|
||||
Some(format!("{}/{}", cdn_url, upload_data.file_name))
|
||||
if let Ok(upload_result) = upload_result {
|
||||
(Some(upload_result.url), Some(upload_result.raw_url))
|
||||
} else {
|
||||
(None, None)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
(None, None)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
(None, None)
|
||||
};
|
||||
|
||||
if let Some(username) = username {
|
||||
@@ -223,6 +220,7 @@ impl TempUser {
|
||||
email: self.email,
|
||||
email_verified: true,
|
||||
avatar_url,
|
||||
raw_avatar_url,
|
||||
bio: self.bio,
|
||||
created: Utc::now(),
|
||||
role: Role::Developer.to_string(),
|
||||
@@ -1518,6 +1516,7 @@ pub async fn create_account_with_password(
|
||||
email: Some(new_account.email.clone()),
|
||||
email_verified: false,
|
||||
avatar_url: None,
|
||||
raw_avatar_url: None,
|
||||
bio: None,
|
||||
created: Utc::now(),
|
||||
role: Role::Developer.to_string(),
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::models::pats::Scopes;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::v3::project_creation::CreateError;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::img::delete_old_images;
|
||||
use crate::util::routes::read_from_payload;
|
||||
use crate::util::validate::validation_errors_to_string;
|
||||
use crate::{database, models};
|
||||
@@ -371,78 +372,69 @@ pub async fn collection_icon_edit(
|
||||
mut payload: web::Payload,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> 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,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::COLLECTION_WRITE]),
|
||||
)
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::COLLECTION_WRITE]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
let string = info.into_inner().0;
|
||||
let id = database::models::CollectionId(parse_base62(&string)? as i64);
|
||||
let collection_item = database::models::Collection::get(id, &**pool, &redis)
|
||||
.await?
|
||||
.1;
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified collection does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
let string = info.into_inner().0;
|
||||
let id = database::models::CollectionId(parse_base62(&string)? as i64);
|
||||
let collection_item = database::models::Collection::get(id, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified collection does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
if !can_modify_collection(&collection_item, &user) {
|
||||
return Ok(HttpResponse::Unauthorized().body(""));
|
||||
}
|
||||
|
||||
if let Some(icon) = collection_item.icon_url {
|
||||
let name = icon.split(&format!("{cdn_url}/")).nth(1);
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
|
||||
let color = crate::util::img::get_color_from_img(&bytes)?;
|
||||
|
||||
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
||||
let collection_id: CollectionId = collection_item.id.into();
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("data/{}/{}.{}", collection_id, hash, ext.ext),
|
||||
bytes.freeze(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE collections
|
||||
SET icon_url = $1, color = $2
|
||||
WHERE (id = $3)
|
||||
",
|
||||
format!("{}/{}", cdn_url, upload_data.file_name),
|
||||
color.map(|x| x as i32),
|
||||
collection_item.id as database::models::ids::CollectionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
database::models::Collection::clear_cache(collection_item.id, &redis).await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::InvalidInput(format!(
|
||||
"Invalid format for collection icon: {}",
|
||||
ext.ext
|
||||
)))
|
||||
if !can_modify_collection(&collection_item, &user) {
|
||||
return Ok(HttpResponse::Unauthorized().body(""));
|
||||
}
|
||||
|
||||
delete_old_images(
|
||||
collection_item.icon_url,
|
||||
collection_item.raw_icon_url,
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
|
||||
let collection_id: CollectionId = collection_item.id.into();
|
||||
let upload_result = crate::util::img::upload_image_optimized(
|
||||
&format!("data/{}", collection_id),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(96),
|
||||
Some(1.0),
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE collections
|
||||
SET icon_url = $1, raw_icon_url = $2, color = $3
|
||||
WHERE (id = $4)
|
||||
",
|
||||
upload_result.url,
|
||||
upload_result.raw_url,
|
||||
upload_result.color.map(|x| x as i32),
|
||||
collection_item.id as database::models::ids::CollectionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
database::models::Collection::clear_cache(collection_item.id, &redis).await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
pub async fn delete_collection_icon(
|
||||
@@ -474,21 +466,18 @@ pub async fn delete_collection_icon(
|
||||
return Ok(HttpResponse::Unauthorized().body(""));
|
||||
}
|
||||
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
if let Some(icon) = collection_item.icon_url {
|
||||
let name = icon.split(&format!("{cdn_url}/")).nth(1);
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
delete_old_images(
|
||||
collection_item.icon_url,
|
||||
collection_item.raw_icon_url,
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE collections
|
||||
SET icon_url = NULL, color = NULL
|
||||
SET icon_url = NULL, raw_icon_url = NULL, color = NULL
|
||||
WHERE (id = $1)
|
||||
",
|
||||
collection_item.id as database::models::ids::CollectionId,
|
||||
|
||||
+169
-185
@@ -1,5 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::threads::is_authorized_thread;
|
||||
use crate::auth::checks::{is_team_member_project, is_team_member_version};
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::database;
|
||||
@@ -11,13 +12,12 @@ use crate::models::images::{Image, ImageContext};
|
||||
use crate::models::reports::ReportId;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::img::upload_image_optimized;
|
||||
use crate::util::routes::read_from_payload;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use super::threads::is_authorized_thread;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.route("image", web::post().to(images_add));
|
||||
}
|
||||
@@ -46,198 +46,182 @@ pub async fn images_add(
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if let Some(content_type) = crate::util::ext::get_image_content_type(&data.ext) {
|
||||
let mut context = ImageContext::from_str(&data.context, None);
|
||||
let mut context = ImageContext::from_str(&data.context, None);
|
||||
|
||||
let scopes = vec![context.relevant_scope()];
|
||||
let scopes = vec![context.relevant_scope()];
|
||||
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
let user = get_user_from_headers(&req, &**pool, &redis, &session_queue, Some(&scopes))
|
||||
.await?
|
||||
.1;
|
||||
let user = get_user_from_headers(&req, &**pool, &redis, &session_queue, Some(&scopes))
|
||||
.await?
|
||||
.1;
|
||||
|
||||
// Attempt to associated a supplied id with the context
|
||||
// If the context cannot be found, or the user is not authorized to upload images for the context, return an error
|
||||
match &mut context {
|
||||
ImageContext::Project { project_id } => {
|
||||
if let Some(id) = data.project_id {
|
||||
let project = project_item::Project::get(&id, &**pool, &redis).await?;
|
||||
if let Some(project) = project {
|
||||
if is_team_member_project(&project.inner, &Some(user.clone()), &pool)
|
||||
.await?
|
||||
{
|
||||
*project_id = Some(project.inner.id.into());
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You are not authorized to upload images for this project"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"The project could not be found.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
ImageContext::Version { version_id } => {
|
||||
if let Some(id) = data.version_id {
|
||||
let version = version_item::Version::get(id.into(), &**pool, &redis).await?;
|
||||
if let Some(version) = version {
|
||||
if is_team_member_version(
|
||||
&version.inner,
|
||||
&Some(user.clone()),
|
||||
&pool,
|
||||
&redis,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
*version_id = Some(version.inner.id.into());
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You are not authorized to upload images for this version"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"The version could not be found.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
ImageContext::ThreadMessage { thread_message_id } => {
|
||||
if let Some(id) = data.thread_message_id {
|
||||
let thread_message = thread_item::ThreadMessage::get(id.into(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The thread message could not found.".to_string(),
|
||||
)
|
||||
})?;
|
||||
let thread = thread_item::Thread::get(thread_message.thread_id, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The thread associated with the thread message could not be found"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
if is_authorized_thread(&thread, &user, &pool).await? {
|
||||
*thread_message_id = Some(thread_message.id.into());
|
||||
// Attempt to associated a supplied id with the context
|
||||
// If the context cannot be found, or the user is not authorized to upload images for the context, return an error
|
||||
match &mut context {
|
||||
ImageContext::Project { project_id } => {
|
||||
if let Some(id) = data.project_id {
|
||||
let project = project_item::Project::get(&id, &**pool, &redis).await?;
|
||||
if let Some(project) = project {
|
||||
if is_team_member_project(&project.inner, &Some(user.clone()), &pool).await? {
|
||||
*project_id = Some(project.inner.id.into());
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You are not authorized to upload images for this thread message"
|
||||
.to_string(),
|
||||
"You are not authorized to upload images for this project".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"The project could not be found.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
ImageContext::Report { report_id } => {
|
||||
if let Some(id) = data.report_id {
|
||||
let report = report_item::Report::get(id.into(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The report could not be found.".to_string())
|
||||
})?;
|
||||
let thread = thread_item::Thread::get(report.thread_id, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The thread associated with the report could not be found."
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
if is_authorized_thread(&thread, &user, &pool).await? {
|
||||
*report_id = Some(report.id.into());
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You are not authorized to upload images for this report".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
ImageContext::Unknown => {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Context must be one of: project, version, thread_message, report".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Upload the image to the file host
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 1_048_576, "Icons must be smaller than 1MiB").await?;
|
||||
|
||||
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("data/cached_images/{}.{}", hash, data.ext),
|
||||
bytes.freeze(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let db_image: database::models::Image = database::models::Image {
|
||||
id: database::models::generate_image_id(&mut transaction).await?,
|
||||
url: format!("{}/{}", cdn_url, upload_data.file_name),
|
||||
size: upload_data.content_length as u64,
|
||||
created: chrono::Utc::now(),
|
||||
owner_id: database::models::UserId::from(user.id),
|
||||
context: context.context_as_str().to_string(),
|
||||
project_id: if let ImageContext::Project {
|
||||
project_id: Some(id),
|
||||
} = context
|
||||
{
|
||||
Some(database::models::ProjectId::from(id))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
version_id: if let ImageContext::Version {
|
||||
version_id: Some(id),
|
||||
} = context
|
||||
{
|
||||
Some(database::models::VersionId::from(id))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
thread_message_id: if let ImageContext::ThreadMessage {
|
||||
thread_message_id: Some(id),
|
||||
} = context
|
||||
{
|
||||
Some(database::models::ThreadMessageId::from(id))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
report_id: if let ImageContext::Report {
|
||||
report_id: Some(id),
|
||||
} = context
|
||||
{
|
||||
Some(database::models::ReportId::from(id))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
};
|
||||
|
||||
// Insert
|
||||
db_image.insert(&mut transaction).await?;
|
||||
|
||||
let image = Image {
|
||||
id: db_image.id.into(),
|
||||
url: db_image.url,
|
||||
size: db_image.size,
|
||||
created: db_image.created,
|
||||
owner_id: db_image.owner_id.into(),
|
||||
context,
|
||||
};
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(image))
|
||||
} else {
|
||||
Err(ApiError::InvalidInput(
|
||||
"The specified file is not an image!".to_string(),
|
||||
))
|
||||
ImageContext::Version { version_id } => {
|
||||
if let Some(id) = data.version_id {
|
||||
let version = version_item::Version::get(id.into(), &**pool, &redis).await?;
|
||||
if let Some(version) = version {
|
||||
if is_team_member_version(&version.inner, &Some(user.clone()), &pool, &redis)
|
||||
.await?
|
||||
{
|
||||
*version_id = Some(version.inner.id.into());
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You are not authorized to upload images for this version".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"The version could not be found.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
ImageContext::ThreadMessage { thread_message_id } => {
|
||||
if let Some(id) = data.thread_message_id {
|
||||
let thread_message = thread_item::ThreadMessage::get(id.into(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The thread message could not found.".to_string())
|
||||
})?;
|
||||
let thread = thread_item::Thread::get(thread_message.thread_id, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The thread associated with the thread message could not be found"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
if is_authorized_thread(&thread, &user, &pool).await? {
|
||||
*thread_message_id = Some(thread_message.id.into());
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You are not authorized to upload images for this thread message"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
ImageContext::Report { report_id } => {
|
||||
if let Some(id) = data.report_id {
|
||||
let report = report_item::Report::get(id.into(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The report could not be found.".to_string())
|
||||
})?;
|
||||
let thread = thread_item::Thread::get(report.thread_id, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The thread associated with the report could not be found.".to_string(),
|
||||
)
|
||||
})?;
|
||||
if is_authorized_thread(&thread, &user, &pool).await? {
|
||||
*report_id = Some(report.id.into());
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You are not authorized to upload images for this report".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
ImageContext::Unknown => {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Context must be one of: project, version, thread_message, report".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Upload the image to the file host
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 1_048_576, "Icons must be smaller than 1MiB").await?;
|
||||
|
||||
let content_length = bytes.len();
|
||||
let upload_result = upload_image_optimized(
|
||||
"data/cached_images",
|
||||
bytes.freeze(),
|
||||
&data.ext,
|
||||
None,
|
||||
None,
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let db_image: database::models::Image = database::models::Image {
|
||||
id: database::models::generate_image_id(&mut transaction).await?,
|
||||
url: upload_result.url,
|
||||
raw_url: upload_result.raw_url,
|
||||
size: content_length as u64,
|
||||
created: chrono::Utc::now(),
|
||||
owner_id: database::models::UserId::from(user.id),
|
||||
context: context.context_as_str().to_string(),
|
||||
project_id: if let ImageContext::Project {
|
||||
project_id: Some(id),
|
||||
} = context
|
||||
{
|
||||
Some(crate::database::models::ProjectId::from(id))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
version_id: if let ImageContext::Version {
|
||||
version_id: Some(id),
|
||||
} = context
|
||||
{
|
||||
Some(database::models::VersionId::from(id))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
thread_message_id: if let ImageContext::ThreadMessage {
|
||||
thread_message_id: Some(id),
|
||||
} = context
|
||||
{
|
||||
Some(database::models::ThreadMessageId::from(id))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
report_id: if let ImageContext::Report {
|
||||
report_id: Some(id),
|
||||
} = context
|
||||
{
|
||||
Some(database::models::ReportId::from(id))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
};
|
||||
|
||||
// Insert
|
||||
db_image.insert(&mut transaction).await?;
|
||||
|
||||
let image = Image {
|
||||
id: db_image.id.into(),
|
||||
url: db_image.url,
|
||||
size: db_image.size,
|
||||
created: db_image.created,
|
||||
owner_id: db_image.owner_id.into(),
|
||||
context,
|
||||
};
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(image))
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ use crate::{
|
||||
|
||||
use crate::database::models::oauth_client_item::OAuthClient as DBOAuthClient;
|
||||
use crate::models::ids::OAuthClientId as ApiOAuthClientId;
|
||||
use crate::util::img::{delete_old_images, upload_image_optimized};
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
@@ -135,12 +136,6 @@ pub struct NewOAuthApp {
|
||||
)]
|
||||
pub name: String,
|
||||
|
||||
#[validate(
|
||||
custom(function = "crate::util::validate::validate_url"),
|
||||
length(max = 255)
|
||||
)]
|
||||
pub icon_url: Option<String>,
|
||||
|
||||
#[validate(custom(function = "crate::util::validate::validate_no_restricted_scopes"))]
|
||||
pub max_scopes: Scopes,
|
||||
|
||||
@@ -190,7 +185,8 @@ pub async fn oauth_client_create<'a>(
|
||||
|
||||
let client = OAuthClient {
|
||||
id: client_id,
|
||||
icon_url: new_oauth_app.icon_url.clone(),
|
||||
icon_url: None,
|
||||
raw_icon_url: None,
|
||||
max_scopes: new_oauth_app.max_scopes,
|
||||
name: new_oauth_app.name.clone(),
|
||||
redirect_uris,
|
||||
@@ -349,63 +345,56 @@ pub async fn oauth_client_icon_edit(
|
||||
mut payload: web::Payload,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> 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,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
)
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
let client = OAuthClient::get((*client_id).into(), &**pool)
|
||||
.await?
|
||||
.1;
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified client does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
let client = OAuthClient::get((*client_id).into(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified client does not exist!".to_string())
|
||||
})?;
|
||||
client.validate_authorized(Some(&user))?;
|
||||
|
||||
client.validate_authorized(Some(&user))?;
|
||||
delete_old_images(
|
||||
client.icon_url.clone(),
|
||||
client.raw_icon_url.clone(),
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(ref icon) = client.icon_url {
|
||||
let name = icon.split(&format!("{cdn_url}/")).nth(1);
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
let upload_result = upload_image_optimized(
|
||||
&format!("data/{}", client_id),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(96),
|
||||
Some(1.0),
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("data/{}/{}.{}", client_id, hash, ext.ext),
|
||||
bytes.freeze(),
|
||||
)
|
||||
.await?;
|
||||
let mut editable_client = client.clone();
|
||||
editable_client.icon_url = Some(upload_result.url);
|
||||
editable_client.raw_icon_url = Some(upload_result.raw_url);
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
editable_client
|
||||
.update_editable_fields(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
let mut editable_client = client.clone();
|
||||
editable_client.icon_url = Some(format!("{}/{}", cdn_url, upload_data.file_name));
|
||||
transaction.commit().await?;
|
||||
|
||||
editable_client
|
||||
.update_editable_fields(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::InvalidInput(format!(
|
||||
"Invalid format for project icon: {}",
|
||||
ext.ext
|
||||
)))
|
||||
}
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
#[delete("app/{id}/icon")]
|
||||
@@ -417,7 +406,6 @@ pub async fn oauth_client_icon_delete(
|
||||
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
@@ -435,18 +423,18 @@ pub async fn oauth_client_icon_delete(
|
||||
})?;
|
||||
client.validate_authorized(Some(&user))?;
|
||||
|
||||
if let Some(ref icon) = client.icon_url {
|
||||
let name = icon.split(&format!("{cdn_url}/")).nth(1);
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
delete_old_images(
|
||||
client.icon_url.clone(),
|
||||
client.raw_icon_url.clone(),
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let mut editable_client = client.clone();
|
||||
editable_client.icon_url = None;
|
||||
editable_client.raw_icon_url = None;
|
||||
|
||||
editable_client
|
||||
.update_editable_fields(&mut *transaction)
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::models::pats::Scopes;
|
||||
use crate::models::teams::{OrganizationPermissions, ProjectPermissions};
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::v3::project_creation::CreateError;
|
||||
use crate::util::img::delete_old_images;
|
||||
use crate::util::routes::read_from_payload;
|
||||
use crate::util::validate::validation_errors_to_string;
|
||||
use crate::{database, models};
|
||||
@@ -164,6 +165,7 @@ pub async fn organization_create(
|
||||
description: new_organization.description.clone(),
|
||||
team_id,
|
||||
icon_url: None,
|
||||
raw_icon_url: None,
|
||||
color: None,
|
||||
};
|
||||
organization.clone().insert(&mut transaction).await?;
|
||||
@@ -926,98 +928,89 @@ pub async fn organization_icon_edit(
|
||||
mut payload: web::Payload,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> 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,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::ORGANIZATION_WRITE]),
|
||||
)
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::ORGANIZATION_WRITE]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
let string = info.into_inner().0;
|
||||
|
||||
let organization_item = database::models::Organization::get(&string, &**pool, &redis)
|
||||
.await?
|
||||
.1;
|
||||
let string = info.into_inner().0;
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified organization does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
let organization_item = database::models::Organization::get(&string, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified organization does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
if !user.role.is_mod() {
|
||||
let team_member = database::models::TeamMember::get_from_user_id(
|
||||
organization_item.team_id,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await
|
||||
.map_err(ApiError::Database)?;
|
||||
|
||||
let permissions =
|
||||
OrganizationPermissions::get_permissions_by_role(&user.role, &team_member)
|
||||
.unwrap_or_default();
|
||||
|
||||
if !permissions.contains(OrganizationPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this organization's icon.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(icon) = organization_item.icon_url {
|
||||
let name = icon.split(&format!("{cdn_url}/")).nth(1);
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
|
||||
let color = crate::util::img::get_color_from_img(&bytes)?;
|
||||
|
||||
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
||||
let organization_id: OrganizationId = organization_item.id.into();
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("data/{}/{}.{}", organization_id, hash, ext.ext),
|
||||
bytes.freeze(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE organizations
|
||||
SET icon_url = $1, color = $2
|
||||
WHERE (id = $3)
|
||||
",
|
||||
format!("{}/{}", cdn_url, upload_data.file_name),
|
||||
color.map(|x| x as i32),
|
||||
organization_item.id as database::models::ids::OrganizationId,
|
||||
if !user.role.is_mod() {
|
||||
let team_member = database::models::TeamMember::get_from_user_id(
|
||||
organization_item.team_id,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(ApiError::Database)?;
|
||||
|
||||
transaction.commit().await?;
|
||||
database::models::Organization::clear_cache(
|
||||
organization_item.id,
|
||||
Some(organization_item.slug),
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
let permissions =
|
||||
OrganizationPermissions::get_permissions_by_role(&user.role, &team_member)
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::InvalidInput(format!(
|
||||
"Invalid format for project icon: {}",
|
||||
ext.ext
|
||||
)))
|
||||
if !permissions.contains(OrganizationPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this organization's icon.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
delete_old_images(
|
||||
organization_item.icon_url,
|
||||
organization_item.raw_icon_url,
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
|
||||
let organization_id: OrganizationId = organization_item.id.into();
|
||||
let upload_result = crate::util::img::upload_image_optimized(
|
||||
&format!("data/{}", organization_id),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(96),
|
||||
Some(1.0),
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE organizations
|
||||
SET icon_url = $1, raw_icon_url = $2, color = $3
|
||||
WHERE (id = $4)
|
||||
",
|
||||
upload_result.url,
|
||||
upload_result.raw_url,
|
||||
upload_result.color.map(|x| x as i32),
|
||||
organization_item.id as database::models::ids::OrganizationId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
database::models::Organization::clear_cache(
|
||||
organization_item.id,
|
||||
Some(organization_item.slug),
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
pub async fn delete_organization_icon(
|
||||
@@ -1065,21 +1058,19 @@ pub async fn delete_organization_icon(
|
||||
}
|
||||
}
|
||||
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
if let Some(icon) = organization_item.icon_url {
|
||||
let name = icon.split(&format!("{cdn_url}/")).nth(1);
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
delete_old_images(
|
||||
organization_item.icon_url,
|
||||
organization_item.raw_icon_url,
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE organizations
|
||||
SET icon_url = NULL, color = NULL
|
||||
SET icon_url = NULL, raw_icon_url = NULL, color = NULL
|
||||
WHERE (id = $1)
|
||||
",
|
||||
organization_item.id as database::models::ids::OrganizationId,
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::database::models::{self, image_item, User};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::file_hosting::{FileHost, FileHostingError};
|
||||
use crate::models::error::ApiError;
|
||||
use crate::models::ids::base62_impl::to_base62;
|
||||
use crate::models::ids::{ImageId, OrganizationId};
|
||||
use crate::models::images::{Image, ImageContext};
|
||||
use crate::models::pats::Scopes;
|
||||
@@ -17,6 +18,7 @@ use crate::models::threads::ThreadType;
|
||||
use crate::models::users::UserId;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::search::indexing::IndexingError;
|
||||
use crate::util::img::upload_image_optimized;
|
||||
use crate::util::routes::read_from_field;
|
||||
use crate::util::validate::validation_errors_to_string;
|
||||
use actix_multipart::{Field, Multipart};
|
||||
@@ -481,7 +483,6 @@ async fn project_create_inner(
|
||||
file_extension,
|
||||
file_host,
|
||||
field,
|
||||
&cdn_url,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
@@ -496,33 +497,40 @@ async fn project_create_inner(
|
||||
if let Some(item) = gallery_items.iter().find(|x| x.item == name) {
|
||||
let data = read_from_field(
|
||||
&mut field,
|
||||
5 * (1 << 20),
|
||||
"Gallery image exceeds the maximum of 5MiB.",
|
||||
2 * (1 << 20),
|
||||
"Gallery image exceeds the maximum of 2MiB.",
|
||||
)
|
||||
.await?;
|
||||
let hash = sha1::Sha1::from(&data).hexdigest();
|
||||
|
||||
let (_, file_extension) =
|
||||
super::version_creation::get_name_ext(&content_disposition)?;
|
||||
let content_type = crate::util::ext::get_image_content_type(file_extension)
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidIconFormat(file_extension.to_string())
|
||||
})?;
|
||||
let url = format!("data/{project_id}/images/{hash}.{file_extension}");
|
||||
let upload_data = file_host
|
||||
.upload_file(content_type, &url, data.freeze())
|
||||
.await?;
|
||||
|
||||
let url = format!("data/{project_id}/images");
|
||||
let upload_result = upload_image_optimized(
|
||||
&url,
|
||||
data.freeze(),
|
||||
file_extension,
|
||||
Some(350),
|
||||
Some(1.0),
|
||||
file_host,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| CreateError::InvalidIconFormat(e.to_string()))?;
|
||||
|
||||
uploaded_files.push(UploadedFile {
|
||||
file_id: upload_data.file_id,
|
||||
file_name: upload_data.file_name,
|
||||
file_id: upload_result.raw_url_path.clone(),
|
||||
file_name: upload_result.raw_url_path,
|
||||
});
|
||||
gallery_urls.push(crate::models::projects::GalleryItem {
|
||||
url: format!("{cdn_url}/{url}"),
|
||||
url: upload_result.url,
|
||||
raw_url: upload_result.raw_url,
|
||||
featured: item.featured,
|
||||
name: item.name.clone(),
|
||||
description: item.description.clone(),
|
||||
created: Utc::now(),
|
||||
ordering: item.ordering,
|
||||
});
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -715,6 +723,7 @@ async fn project_create_inner(
|
||||
summary: project_create_data.summary,
|
||||
description: project_create_data.description,
|
||||
icon_url: icon_data.clone().map(|x| x.0),
|
||||
raw_icon_url: icon_data.clone().map(|x| x.1),
|
||||
|
||||
license_url: project_create_data.license_url,
|
||||
categories,
|
||||
@@ -729,6 +738,7 @@ async fn project_create_inner(
|
||||
.iter()
|
||||
.map(|x| models::project_item::GalleryItem {
|
||||
image_url: x.url.clone(),
|
||||
raw_image_url: x.raw_url.clone(),
|
||||
featured: x.featured,
|
||||
name: x.name.clone(),
|
||||
description: x.description.clone(),
|
||||
@@ -736,7 +746,7 @@ async fn project_create_inner(
|
||||
ordering: x.ordering,
|
||||
})
|
||||
.collect(),
|
||||
color: icon_data.and_then(|x| x.1),
|
||||
color: icon_data.and_then(|x| x.2),
|
||||
monetization_status: MonetizationStatus::Monetized,
|
||||
};
|
||||
let project_builder = project_builder_actual.clone();
|
||||
@@ -943,29 +953,32 @@ async fn process_icon_upload(
|
||||
file_extension: &str,
|
||||
file_host: &dyn FileHost,
|
||||
mut field: Field,
|
||||
cdn_url: &str,
|
||||
) -> Result<(String, Option<u32>), CreateError> {
|
||||
if let Some(content_type) = crate::util::ext::get_image_content_type(file_extension) {
|
||||
let data = read_from_field(&mut field, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
) -> Result<(String, String, Option<u32>), CreateError> {
|
||||
let data = read_from_field(&mut field, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
let upload_result = crate::util::img::upload_image_optimized(
|
||||
&format!("data/{}", to_base62(id)),
|
||||
data.freeze(),
|
||||
file_extension,
|
||||
Some(96),
|
||||
Some(1.0),
|
||||
file_host,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| CreateError::InvalidIconFormat(e.to_string()))?;
|
||||
|
||||
let color = crate::util::img::get_color_from_img(&data)?;
|
||||
uploaded_files.push(UploadedFile {
|
||||
file_id: upload_result.raw_url_path.clone(),
|
||||
file_name: upload_result.raw_url_path,
|
||||
});
|
||||
|
||||
let hash = sha1::Sha1::from(&data).hexdigest();
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("data/{id}/{hash}.{file_extension}"),
|
||||
data.freeze(),
|
||||
)
|
||||
.await?;
|
||||
uploaded_files.push(UploadedFile {
|
||||
file_id: upload_result.url_path.clone(),
|
||||
file_name: upload_result.url_path,
|
||||
});
|
||||
|
||||
uploaded_files.push(UploadedFile {
|
||||
file_id: upload_data.file_id,
|
||||
file_name: upload_data.file_name.clone(),
|
||||
});
|
||||
|
||||
Ok((format!("{}/{}", cdn_url, upload_data.file_name), color))
|
||||
} else {
|
||||
Err(CreateError::InvalidIconFormat(file_extension.to_string()))
|
||||
}
|
||||
Ok((
|
||||
upload_result.url,
|
||||
upload_result.raw_url,
|
||||
upload_result.color,
|
||||
))
|
||||
}
|
||||
|
||||
+199
-225
@@ -26,6 +26,7 @@ use crate::routes::ApiError;
|
||||
use crate::search::indexing::remove_documents;
|
||||
use crate::search::{search_for_project, SearchConfig, SearchError};
|
||||
use crate::util::img;
|
||||
use crate::util::img::{delete_old_images, upload_image_optimized};
|
||||
use crate::util::routes::read_from_payload;
|
||||
use crate::util::validate::validation_errors_to_string;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
@@ -1317,109 +1318,95 @@ pub async fn project_icon_edit(
|
||||
mut payload: web::Payload,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> 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,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_WRITE]),
|
||||
)
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_WRITE]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
let string = info.into_inner().0;
|
||||
|
||||
let project_item = db_models::Project::get(&string, &**pool, &redis)
|
||||
.await?
|
||||
.1;
|
||||
let string = info.into_inner().0;
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified project does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
let project_item = db_models::Project::get(&string, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified project does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
if !user.role.is_mod() {
|
||||
let (team_member, organization_team_member) =
|
||||
db_models::TeamMember::get_for_project_permissions(
|
||||
&project_item.inner,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Hide the project
|
||||
if team_member.is_none() && organization_team_member.is_none() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"The specified project does not exist!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let permissions = ProjectPermissions::get_permissions_by_role(
|
||||
&user.role,
|
||||
&team_member,
|
||||
&organization_team_member,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
if !permissions.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this project's icon.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(icon) = project_item.inner.icon_url {
|
||||
let name = icon.split(&format!("{cdn_url}/")).nth(1);
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
|
||||
let color = crate::util::img::get_color_from_img(&bytes)?;
|
||||
|
||||
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
||||
let project_id: ProjectId = project_item.inner.id.into();
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("data/{}/{}.{}", project_id, hash, ext.ext),
|
||||
bytes.freeze(),
|
||||
if !user.role.is_mod() {
|
||||
let (team_member, organization_team_member) =
|
||||
db_models::TeamMember::get_for_project_permissions(
|
||||
&project_item.inner,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
// Hide the project
|
||||
if team_member.is_none() && organization_team_member.is_none() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"The specified project does not exist!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET icon_url = $1, color = $2
|
||||
WHERE (id = $3)
|
||||
",
|
||||
format!("{}/{}", cdn_url, upload_data.file_name),
|
||||
color.map(|x| x as i32),
|
||||
project_item.inner.id as db_ids::ProjectId,
|
||||
let permissions = ProjectPermissions::get_permissions_by_role(
|
||||
&user.role,
|
||||
&team_member,
|
||||
&organization_team_member,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
.unwrap_or_default();
|
||||
|
||||
transaction.commit().await?;
|
||||
db_models::Project::clear_cache(
|
||||
project_item.inner.id,
|
||||
project_item.inner.slug,
|
||||
None,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::InvalidInput(format!(
|
||||
"Invalid format for project icon: {}",
|
||||
ext.ext
|
||||
)))
|
||||
if !permissions.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this project's icon.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
delete_old_images(
|
||||
project_item.inner.icon_url,
|
||||
project_item.inner.raw_icon_url,
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
|
||||
let project_id: ProjectId = project_item.inner.id.into();
|
||||
let upload_result = upload_image_optimized(
|
||||
&format!("data/{}", project_id),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(96),
|
||||
Some(1.0),
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET icon_url = $1, raw_icon_url = $2, color = $3
|
||||
WHERE (id = $4)
|
||||
",
|
||||
upload_result.url,
|
||||
upload_result.raw_url,
|
||||
upload_result.color.map(|x| x as i32),
|
||||
project_item.inner.id as db_ids::ProjectId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
db_models::Project::clear_cache(project_item.inner.id, project_item.inner.slug, None, &redis)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
pub async fn delete_project_icon(
|
||||
@@ -1476,21 +1463,19 @@ pub async fn delete_project_icon(
|
||||
}
|
||||
}
|
||||
|
||||
let cdn_url = dotenvy::var("CDN_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 {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
delete_old_images(
|
||||
project_item.inner.icon_url,
|
||||
project_item.inner.raw_icon_url,
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET icon_url = NULL, color = NULL
|
||||
SET icon_url = NULL, raw_icon_url = NULL, color = NULL
|
||||
WHERE (id = $1)
|
||||
",
|
||||
project_item.inner.id as db_ids::ProjectId,
|
||||
@@ -1527,132 +1512,122 @@ pub async fn add_gallery_item(
|
||||
mut payload: web::Payload,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
|
||||
item.validate()
|
||||
.map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
|
||||
item.validate()
|
||||
.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,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_WRITE]),
|
||||
)
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_WRITE]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
let string = info.into_inner().0;
|
||||
|
||||
let project_item = db_models::Project::get(&string, &**pool, &redis)
|
||||
.await?
|
||||
.1;
|
||||
let string = info.into_inner().0;
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified project does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
let project_item = db_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(
|
||||
"You have reached the maximum of gallery images to upload.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if project_item.gallery_items.len() > 64 {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You have reached the maximum of gallery images to upload.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if !user.role.is_admin() {
|
||||
let (team_member, organization_team_member) =
|
||||
db_models::TeamMember::get_for_project_permissions(
|
||||
&project_item.inner,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Hide the project
|
||||
if team_member.is_none() && organization_team_member.is_none() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"The specified project does not exist!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let permissions = ProjectPermissions::get_permissions_by_role(
|
||||
&user.role,
|
||||
&team_member,
|
||||
&organization_team_member,
|
||||
if !user.role.is_admin() {
|
||||
let (team_member, organization_team_member) =
|
||||
db_models::TeamMember::get_for_project_permissions(
|
||||
&project_item.inner,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
if !permissions.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this project's gallery.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let bytes = read_from_payload(
|
||||
&mut payload,
|
||||
5 * (1 << 20),
|
||||
"Gallery image exceeds the maximum of 5MiB.",
|
||||
)
|
||||
.await?;
|
||||
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
||||
|
||||
let id: ProjectId = project_item.inner.id.into();
|
||||
let url = format!("data/{}/images/{}.{}", id, hash, &*ext.ext);
|
||||
|
||||
let file_url = format!("{cdn_url}/{url}");
|
||||
if project_item
|
||||
.gallery_items
|
||||
.iter()
|
||||
.any(|x| x.image_url == file_url)
|
||||
{
|
||||
return Err(ApiError::InvalidInput(
|
||||
"You may not upload duplicate gallery images!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
file_host
|
||||
.upload_file(content_type, &url, bytes.freeze())
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
// Hide the project
|
||||
if team_member.is_none() && organization_team_member.is_none() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"The specified project does not exist!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if item.featured {
|
||||
sqlx::query!(
|
||||
"
|
||||
let permissions = ProjectPermissions::get_permissions_by_role(
|
||||
&user.role,
|
||||
&team_member,
|
||||
&organization_team_member,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
if !permissions.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this project's gallery.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let bytes = read_from_payload(
|
||||
&mut payload,
|
||||
2 * (1 << 20),
|
||||
"Gallery image exceeds the maximum of 2MiB.",
|
||||
)
|
||||
.await?;
|
||||
|
||||
let id: ProjectId = project_item.inner.id.into();
|
||||
let upload_result = upload_image_optimized(
|
||||
&format!("data/{}/images", id),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(350),
|
||||
Some(1.0),
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if project_item
|
||||
.gallery_items
|
||||
.iter()
|
||||
.any(|x| x.image_url == upload_result.url)
|
||||
{
|
||||
return Err(ApiError::InvalidInput(
|
||||
"You may not upload duplicate gallery images!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
if item.featured {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods_gallery
|
||||
SET featured = $2
|
||||
WHERE mod_id = $1
|
||||
",
|
||||
project_item.inner.id as db_ids::ProjectId,
|
||||
false,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let gallery_item = vec![db_models::project_item::GalleryItem {
|
||||
image_url: file_url,
|
||||
featured: item.featured,
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
created: Utc::now(),
|
||||
ordering: item.ordering.unwrap_or(0),
|
||||
}];
|
||||
GalleryItem::insert_many(gallery_item, project_item.inner.id, &mut transaction).await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
db_models::Project::clear_cache(
|
||||
project_item.inner.id,
|
||||
project_item.inner.slug,
|
||||
None,
|
||||
&redis,
|
||||
project_item.inner.id as db_ids::ProjectId,
|
||||
false,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let gallery_item = vec![db_models::project_item::GalleryItem {
|
||||
image_url: upload_result.url,
|
||||
raw_image_url: upload_result.raw_url,
|
||||
featured: item.featured,
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
created: Utc::now(),
|
||||
ordering: item.ordering.unwrap_or(0),
|
||||
}];
|
||||
GalleryItem::insert_many(gallery_item, project_item.inner.id, &mut transaction).await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
db_models::Project::clear_cache(project_item.inner.id, project_item.inner.slug, None, &redis)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::InvalidInput(format!(
|
||||
"Invalid format for gallery image: {}",
|
||||
ext.ext
|
||||
)))
|
||||
}
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Validate)]
|
||||
@@ -1891,9 +1866,9 @@ pub async fn delete_gallery_item(
|
||||
}
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let id = sqlx::query!(
|
||||
let item = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM mods_gallery
|
||||
SELECT id, image_url, raw_image_url FROM mods_gallery
|
||||
WHERE image_url = $1
|
||||
",
|
||||
item.url
|
||||
@@ -1905,15 +1880,14 @@ pub async fn delete_gallery_item(
|
||||
"Gallery item at URL {} is not part of the project's gallery.",
|
||||
item.url
|
||||
))
|
||||
})?
|
||||
.id;
|
||||
})?;
|
||||
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
let name = item.url.split(&format!("{cdn_url}/")).nth(1);
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
delete_old_images(
|
||||
Some(item.image_url),
|
||||
Some(item.raw_image_url),
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
@@ -1922,7 +1896,7 @@ pub async fn delete_gallery_item(
|
||||
DELETE FROM mods_gallery
|
||||
WHERE id = $1
|
||||
",
|
||||
id
|
||||
item.id
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
+55
-64
@@ -7,6 +7,8 @@ use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
use validator::Validate;
|
||||
|
||||
use super::{oauth_clients::get_user_clients, ApiError};
|
||||
use crate::util::img::delete_old_images;
|
||||
use crate::{
|
||||
auth::{filter_visible_projects, get_user_from_headers},
|
||||
database::{models::User, redis::RedisPool},
|
||||
@@ -23,8 +25,6 @@ use crate::{
|
||||
util::{routes::read_from_payload, validate::validation_errors_to_string},
|
||||
};
|
||||
|
||||
use super::{oauth_clients::get_user_clients, ApiError};
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.route("user", web::get().to(user_auth_get));
|
||||
cfg.route("users", web::get().to(users_get));
|
||||
@@ -446,71 +446,62 @@ pub async fn user_icon_edit(
|
||||
mut payload: web::Payload,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> 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,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_WRITE]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?;
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_WRITE]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?;
|
||||
|
||||
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 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);
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 2097152, "Icons must be smaller than 2MiB").await?;
|
||||
|
||||
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("user/{}/{}.{}", user_id, hash, ext.ext),
|
||||
bytes.freeze(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET avatar_url = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
format!("{}/{}", cdn_url, upload_data.file_name),
|
||||
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 {
|
||||
Err(ApiError::NotFound)
|
||||
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(),
|
||||
));
|
||||
}
|
||||
|
||||
delete_old_images(
|
||||
actual_user.avatar_url,
|
||||
actual_user.raw_avatar_url,
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
|
||||
let user_id: UserId = actual_user.id.into();
|
||||
let upload_result = crate::util::img::upload_image_optimized(
|
||||
&format!("data/{}", user_id),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(96),
|
||||
Some(1.0),
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET avatar_url = $1, raw_avatar_url = $2
|
||||
WHERE (id = $3)
|
||||
",
|
||||
upload_result.url,
|
||||
upload_result.raw_url,
|
||||
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 {
|
||||
Err(ApiError::InvalidInput(format!(
|
||||
"Invalid format for user icon: {}",
|
||||
ext.ext
|
||||
)))
|
||||
Err(ApiError::NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user