Add API routes to request multiple of an item (#70)

* Change header name

* Add default bio value

* Remove default

* Make name null

* Run prepare

* Add new API Routes for requesting multiple of an item

* Run formatter

* Simplify get mods query

* Run prepare

* Refactor to use one query for most routes, change version create route to have mod_id in data

* More fixes
This commit is contained in:
Geometrically
2020-10-05 14:25:32 -07:00
committed by GitHub
parent 68ee2bdcdc
commit 2719ae5df2
12 changed files with 586 additions and 41 deletions

View File

@@ -18,33 +18,36 @@ pub use self::not_found::not_found;
pub fn mods_config(cfg: &mut web::ServiceConfig) {
cfg.service(mods::mod_search);
cfg.service(mods::mods_get);
cfg.service(mod_creation::mod_create);
cfg.service(
web::scope("mod")
.service(mods::mod_get)
.service(mods::mod_delete)
.service(web::scope("{mod_id}").configure(versions_config)),
.service(web::scope("{mod_id}").service(versions::version_list)),
);
}
pub fn versions_config(cfg: &mut web::ServiceConfig) {
cfg.service(versions::version_list)
.service(version_creation::version_create)
.service(
web::scope("version")
.service(versions::version_get)
.service(versions::version_delete)
.service(version_creation::upload_file_to_version),
);
cfg.service(versions::versions_get);
cfg.service(
web::scope("version")
.service(versions::version_get)
.service(version_creation::version_create)
.service(versions::version_delete)
.service(version_creation::upload_file_to_version),
);
}
pub fn users_config(cfg: &mut web::ServiceConfig) {
cfg.service(users::user_auth_get);
cfg.service(users::users_get);
cfg.service(
web::scope("user")
.service(users::user_get)
.service(users::mods_list)
.service(users::user_delete),
);
}
@@ -53,6 +56,8 @@ pub fn users_config(cfg: &mut web::ServiceConfig) {
pub enum ApiError {
#[error("Internal server error")]
DatabaseError(#[from] crate::database::models::DatabaseError),
#[error("Deserialization error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Authentication Error")]
AuthenticationError,
}
@@ -62,6 +67,7 @@ impl actix_web::ResponseError for ApiError {
match self {
ApiError::DatabaseError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
ApiError::AuthenticationError => actix_web::http::StatusCode::UNAUTHORIZED,
ApiError::JsonError(..) => actix_web::http::StatusCode::BAD_REQUEST,
}
}
@@ -71,6 +77,7 @@ impl actix_web::ResponseError for ApiError {
error: match self {
ApiError::DatabaseError(..) => "database_error",
ApiError::AuthenticationError => "unauthorized",
ApiError::JsonError(..) => "json_error",
},
description: &self.to_string(),
},

View File

@@ -5,6 +5,7 @@ use crate::models;
use crate::models::mods::SearchRequest;
use crate::search::{search_for_mod, SearchError};
use actix_web::{delete, get, web, HttpRequest, HttpResponse};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
#[get("mod")]
@@ -15,6 +16,49 @@ pub async fn mod_search(
Ok(HttpResponse::Ok().json(results))
}
#[derive(Serialize, Deserialize)]
pub struct ModIds {
pub ids: String,
}
// TODO: Make this return the full mod struct
#[get("mods")]
pub async fn mods_get(
web::Query(ids): web::Query<ModIds>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let mod_ids = serde_json::from_str::<Vec<models::ids::ModId>>(&*ids.ids)?
.into_iter()
.map(|x| x.into())
.collect();
let mods_data = database::models::Mod::get_many(mod_ids, &**pool)
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?;
let mods: Vec<models::mods::Mod> = mods_data
.into_iter()
.map(|m| models::mods::Mod {
id: m.id.into(),
team: m.team_id.into(),
title: m.title,
description: m.description,
body_url: m.body_url,
published: m.published,
downloads: m.downloads as u32,
categories: vec![],
versions: vec![],
icon_url: m.icon_url,
issues_url: m.issues_url,
source_url: m.source_url,
wiki_url: m.wiki_url,
})
.collect();
Ok(HttpResponse::Ok().json(mods))
}
#[get("{id}")]
pub async fn mod_get(
info: web::Path<(models::ids::ModId,)>,
@@ -48,7 +92,6 @@ pub async fn mod_get(
Ok(HttpResponse::NotFound().body(""))
}
}
// TODO: The mod remains in meilisearch's index until the index is deleted
#[delete("{id}")]
pub async fn mod_delete(

View File

@@ -1,7 +1,9 @@
use crate::auth::{check_is_moderator_from_headers, get_user_from_headers};
use crate::database::models::User;
use crate::models::users::{Role, UserId};
use crate::routes::ApiError;
use actix_web::{delete, get, web, HttpRequest, HttpResponse};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
#[get("user")]
@@ -22,13 +24,50 @@ pub async fn user_auth_get(
))
}
#[derive(Serialize, Deserialize)]
pub struct UserIds {
pub ids: String,
}
#[get("users")]
pub async fn users_get(
web::Query(ids): web::Query<UserIds>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let user_ids = serde_json::from_str::<Vec<UserId>>(&*ids.ids)?
.into_iter()
.map(|x| x.into())
.collect();
let users_data = User::get_many(user_ids, &**pool)
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?;
let users: Vec<crate::models::users::User> = users_data
.into_iter()
.map(|data| crate::models::users::User {
id: data.id.into(),
github_id: data.github_id.map(|i| i as u64),
username: data.username,
name: data.name,
email: None,
avatar_url: data.avatar_url,
bio: data.bio,
created: data.created,
role: Role::from_string(&*data.role),
})
.collect();
Ok(HttpResponse::Ok().json(users))
}
#[get("{id}")]
pub async fn user_get(
info: web::Path<(UserId,)>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let id = info.0;
let user_data = crate::database::models::User::get(id.into(), &**pool)
let user_data = User::get(id.into(), &**pool)
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?;
@@ -50,6 +89,38 @@ pub async fn user_get(
}
}
#[get("{user_id}/mods")]
pub async fn mods_list(
info: web::Path<(UserId,)>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let id = info.0.into();
let user_exists = sqlx::query!(
"SELECT EXISTS(SELECT 1 FROM users WHERE id = $1)",
id as crate::database::models::UserId,
)
.fetch_one(&**pool)
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?
.exists;
if user_exists.unwrap_or(false) {
let mod_data = User::get_mods(id, &**pool)
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?;
let response = mod_data
.into_iter()
.map(|v| v.into())
.collect::<Vec<crate::models::ids::ModId>>();
Ok(HttpResponse::Ok().json(response))
} else {
Ok(HttpResponse::NotFound().body(""))
}
}
// TODO: Make this actually do stuff
#[delete("{id}")]
pub async fn user_delete(

View File

@@ -15,6 +15,7 @@ use sqlx::postgres::PgPool;
#[derive(Serialize, Deserialize, Clone)]
pub struct InitialVersionData {
pub mod_id: ModId,
pub file_parts: Vec<String>,
pub version_number: String,
pub version_title: String,
@@ -34,7 +35,6 @@ struct InitialFileData {
#[post("version")]
pub async fn version_create(
req: HttpRequest,
url_data: actix_web::web::Path<(ModId,)>,
payload: Multipart,
client: Data<PgPool>,
file_host: Data<std::sync::Arc<dyn FileHost + Send + Sync>>,
@@ -42,15 +42,12 @@ pub async fn version_create(
let mut transaction = client.begin().await?;
let mut uploaded_files = Vec::new();
let mod_id = url_data.into_inner().0.into();
let result = version_create_inner(
req,
payload,
&mut transaction,
&***file_host,
&mut uploaded_files,
mod_id,
)
.await;
@@ -77,7 +74,6 @@ async fn version_create_inner(
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
file_host: &dyn FileHost,
uploaded_files: &mut Vec<UploadedFile>,
mod_id: models::ModId,
) -> Result<HttpResponse, CreateError> {
let cdn_url = dotenv::var("CDN_URL")?;
@@ -104,6 +100,7 @@ async fn version_create_inner(
let version_create_data: InitialVersionData = serde_json::from_slice(&data)?;
initial_version_data = Some(version_create_data);
let version_create_data = initial_version_data.as_ref().unwrap();
let mod_id: models::ModId = version_create_data.mod_id.into();
let results = sqlx::query!(
"SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)",
@@ -154,8 +151,7 @@ async fn version_create_inner(
let version_id: VersionId = models::generate_version_id(transaction).await?.into();
let body_url = format!(
"data/{}/changelogs/{}/body.md",
ModId::from(mod_id),
version_id
version_create_data.mod_id, version_id
);
let uploaded_text = file_host
@@ -180,7 +176,7 @@ async fn version_create_inner(
version_builder = Some(VersionBuilder {
version_id: version_id.into(),
mod_id,
mod_id: version_create_data.mod_id.into(),
author_id: user.id.into(),
name: version_create_data.version_title.clone(),
version_number: version_create_data.version_number.clone(),
@@ -210,7 +206,7 @@ async fn version_create_inner(
uploaded_files,
&cdn_url,
&content_disposition,
ModId::from(mod_id),
version.mod_id.into(),
&version.version_number,
)
.await?;
@@ -272,7 +268,7 @@ async fn version_create_inner(
#[post("{version_id}/file")]
pub async fn upload_file_to_version(
req: HttpRequest,
url_data: actix_web::web::Path<(ModId, VersionId)>,
url_data: actix_web::web::Path<(VersionId,)>,
payload: Multipart,
client: Data<PgPool>,
file_host: Data<std::sync::Arc<dyn FileHost + Send + Sync>>,
@@ -280,9 +276,7 @@ pub async fn upload_file_to_version(
let mut transaction = client.begin().await?;
let mut uploaded_files = Vec::new();
let data = url_data.into_inner();
let mod_id = models::ModId::from(data.0);
let version_id = models::VersionId::from(data.1);
let version_id = models::VersionId::from(url_data.0);
let result = upload_file_to_version_inner(
req,
@@ -291,7 +285,6 @@ pub async fn upload_file_to_version(
&***file_host,
&mut uploaded_files,
version_id,
mod_id,
)
.await;
@@ -319,7 +312,6 @@ async fn upload_file_to_version_inner(
file_host: &dyn FileHost,
uploaded_files: &mut Vec<UploadedFile>,
version_id: models::VersionId,
mod_id: models::ModId,
) -> Result<HttpResponse, CreateError> {
let cdn_url = dotenv::var("CDN_URL")?;
@@ -347,11 +339,6 @@ async fn upload_file_to_version_inner(
));
}
};
if version.mod_id as u64 != mod_id.0 as u64 {
return Err(CreateError::InvalidInput(
"An invalid version id was supplied".to_string(),
));
}
if version.author_id as u64 != user.id.0 {
return Err(CreateError::InvalidInput("Unauthorized".to_string()));

View File

@@ -3,6 +3,7 @@ use crate::auth::check_is_moderator_from_headers;
use crate::database;
use crate::models;
use actix_web::{delete, get, web, HttpRequest, HttpResponse};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
// TODO: this needs filtering, and a better response type
@@ -42,12 +43,56 @@ pub async fn version_list(
}
}
#[get("{version_id}")]
pub async fn version_get(
info: web::Path<(models::ids::ModId, models::ids::VersionId)>,
#[derive(Serialize, Deserialize)]
pub struct VersionIds {
pub ids: String,
}
// TODO: Make this return the versions mod struct
#[get("versions")]
pub async fn versions_get(
web::Query(ids): web::Query<VersionIds>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let id = info.1;
let version_ids = serde_json::from_str::<Vec<models::ids::VersionId>>(&*ids.ids)?
.into_iter()
.map(|x| x.into())
.collect();
let versions_data = database::models::Version::get_many(version_ids, &**pool)
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?;
use models::mods::VersionType;
let versions: Vec<models::mods::Version> = versions_data
.into_iter()
.map(|data| models::mods::Version {
id: data.id.into(),
mod_id: data.mod_id.into(),
author_id: data.author_id.into(),
name: data.name,
version_number: data.version_number,
changelog_url: data.changelog_url,
date_published: data.date_published,
downloads: data.downloads as u32,
version_type: VersionType::Release,
files: vec![],
dependencies: Vec::new(), // TODO: dependencies
game_versions: vec![],
loaders: vec![],
})
.collect();
Ok(HttpResponse::Ok().json(versions))
}
#[get("{version_id}")]
pub async fn version_get(
info: web::Path<(models::ids::VersionId,)>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, ApiError> {
let id = info.0;
let version_data = database::models::Version::get_full(id.into(), &**pool)
.await
.map_err(|e| ApiError::DatabaseError(e.into()))?;
@@ -55,11 +100,6 @@ pub async fn version_get(
if let Some(data) = version_data {
use models::mods::VersionType;
if models::ids::ModId::from(data.mod_id) != info.0 {
// Version doesn't belong to that mod
return Ok(HttpResponse::NotFound().body(""));
}
let response = models::mods::Version {
id: data.id.into(),
mod_id: data.mod_id.into(),