diff --git a/migrations/20200928195220_add-roles-to-users.sql b/migrations/20200928195220_add-roles-to-users.sql new file mode 100644 index 000000000..bfbf6aa47 --- /dev/null +++ b/migrations/20200928195220_add-roles-to-users.sql @@ -0,0 +1,3 @@ +-- Add migration script here +ALTER TABLE users +ADD COLUMN role varchar(50) NOT NULL default 'developer' \ No newline at end of file diff --git a/migrations/20200929034101_add-author-to-versions.sql b/migrations/20200929034101_add-author-to-versions.sql new file mode 100644 index 000000000..ff2561156 --- /dev/null +++ b/migrations/20200929034101_add-author-to-versions.sql @@ -0,0 +1,3 @@ +-- Add migration script here +ALTER TABLE versions +ADD COLUMN author_id bigint REFERENCES users NOT NULL default 0 \ No newline at end of file diff --git a/src/auth/mod.rs b/src/auth/mod.rs new file mode 100644 index 000000000..83d3936b9 --- /dev/null +++ b/src/auth/mod.rs @@ -0,0 +1,118 @@ +use crate::database::models; +use crate::models::users::{Role, User, UserId}; +use actix_web::http::HeaderMap; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum AuthenticationError { + #[error("An unknown database error occurred")] + SqlxDatabaseError(#[from] sqlx::Error), + #[error("Database Error: {0}")] + DatabaseError(#[from] crate::database::models::DatabaseError), + #[error("Error while parsing JSON: {0}")] + SerDeError(#[from] serde_json::Error), + #[error("Error while communicating to GitHub OAuth2: {0}")] + GithubError(#[from] reqwest::Error), + #[error("Invalid Authentication Credentials")] + InvalidCredentialsError, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct GitHubUser { + pub login: String, + pub id: u64, + pub avatar_url: String, + pub name: String, + pub email: Option, + pub bio: String, +} + +pub async fn get_github_user_from_token( + access_token: &str, +) -> Result { + Ok(reqwest::Client::new() + .get("https://api.github.com/user") + .header(reqwest::header::USER_AGENT, "Modrinth") + .header( + reqwest::header::AUTHORIZATION, + format!("token {}", access_token), + ) + .send() + .await? + .json() + .await?) +} + +pub async fn get_user_from_token<'a, 'b, E>( + access_token: &str, + executor: E, +) -> Result +where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, +{ + let github_user = get_github_user_from_token(access_token).await?; + + let res = + models::User::get_from_github_id(models::UserId(github_user.id as i64), executor).await?; + + match res { + Some(result) => Ok(User { + id: UserId::from(result.id), + github_id: UserId::from(result.github_id), + username: result.username, + name: result.name, + email: result.email, + avatar_url: result.avatar_url, + bio: result.bio, + created: result.created, + role: Role::from_string(&*result.role), + }), + None => Err(AuthenticationError::InvalidCredentialsError), + } +} +pub async fn get_user_from_headers<'a, 'b, E>( + headers: &HeaderMap, + executor: E, +) -> Result +where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, +{ + let token = headers + .get("Authentication") + .ok_or(AuthenticationError::InvalidCredentialsError)? + .to_str() + .map_err(|_| AuthenticationError::InvalidCredentialsError)?; + + Ok(get_user_from_token(token, executor).await?) +} + +pub async fn check_is_moderator_from_headers<'a, 'b, E>( + headers: &HeaderMap, + executor: E, +) -> Result +where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, +{ + let user = get_user_from_headers(headers, executor).await?; + + match user.role { + Role::Moderator | Role::Admin => Ok(user), + _ => Err(AuthenticationError::InvalidCredentialsError), + } +} + +pub async fn check_is_admin_from_headers<'a, 'b, E>( + headers: &HeaderMap, + executor: E, +) -> Result +where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, +{ + let user = get_user_from_headers(headers, executor).await?; + + match user.role { + Role::Admin => Ok(user), + _ => Err(AuthenticationError::InvalidCredentialsError), + } +} diff --git a/src/database/models/mod.rs b/src/database/models/mod.rs index 086c3b456..75f32ee4a 100644 --- a/src/database/models/mod.rs +++ b/src/database/models/mod.rs @@ -7,17 +7,17 @@ pub mod categories; pub mod ids; pub mod mod_item; pub mod team_item; -pub mod version_item; pub mod user_item; +pub mod version_item; pub use ids::*; pub use mod_item::Mod; pub use team_item::Team; pub use team_item::TeamMember; +pub use user_item::User; pub use version_item::FileHash; pub use version_item::Version; pub use version_item::VersionFile; -pub use user_item::User; #[derive(Error, Debug)] pub enum DatabaseError { diff --git a/src/database/models/user_item.rs b/src/database/models/user_item.rs index c676be85a..42a0ea5a3 100644 --- a/src/database/models/user_item.rs +++ b/src/database/models/user_item.rs @@ -9,6 +9,7 @@ pub struct User { pub avatar_url: String, pub bio: String, pub created: chrono::DateTime, + pub role: String, } impl User { @@ -36,30 +37,27 @@ impl User { &self.bio, self.created, ) - .execute(&mut *transaction) - .await?; + .execute(&mut *transaction) + .await?; Ok(()) } - pub async fn get<'a, 'b, E>( - id: UserId, - executor: E, - ) -> Result, sqlx::error::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, + pub async fn get<'a, 'b, E>(id: UserId, executor: E) -> Result, sqlx::error::Error> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let result = sqlx::query!( " SELECT u.github_id, u.name, u.email, u.avatar_url, u.username, u.bio, - u.created + u.created, u.role FROM users u WHERE u.id = $1 ", id as UserId, ) - .fetch_optional(executor) - .await?; + .fetch_optional(executor) + .await?; if let Some(row) = result { Ok(Some(User { @@ -71,6 +69,7 @@ impl User { username: row.username, bio: row.bio, created: row.created, + role: row.role, })) } else { Ok(None) @@ -81,21 +80,21 @@ impl User { github_id: UserId, executor: E, ) -> Result, sqlx::error::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres>, + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let result = sqlx::query!( " SELECT u.id, u.name, u.email, u.avatar_url, u.username, u.bio, - u.created + u.created, u.role FROM users u WHERE u.github_id = $1 ", github_id as UserId, ) - .fetch_optional(executor) - .await?; + .fetch_optional(executor) + .await?; if let Some(row) = result { Ok(Some(User { @@ -107,9 +106,10 @@ impl User { username: row.username, bio: row.bio, created: row.created, + role: row.role, })) } else { Ok(None) } } -} \ No newline at end of file +} diff --git a/src/database/models/version_item.rs b/src/database/models/version_item.rs index 770a96b23..8cb191c89 100644 --- a/src/database/models/version_item.rs +++ b/src/database/models/version_item.rs @@ -5,6 +5,7 @@ use super::DatabaseError; pub struct VersionBuilder { pub version_id: VersionId, pub mod_id: ModId, + pub author_id: UserId, pub name: String, pub version_number: String, pub changelog_url: Option, @@ -73,6 +74,7 @@ impl VersionBuilder { let version = Version { id: self.version_id, mod_id: self.mod_id, + author_id: self.author_id, name: self.name, version_number: self.version_number, changelog_url: self.changelog_url, @@ -133,6 +135,7 @@ impl VersionBuilder { pub struct Version { pub id: VersionId, pub mod_id: ModId, + pub author_id: UserId, pub name: String, pub version_number: String, pub changelog_url: Option, @@ -149,18 +152,19 @@ impl Version { sqlx::query!( " INSERT INTO versions ( - id, mod_id, name, version_number, + id, mod_id, author_id, name, version_number, changelog_url, date_published, downloads, release_channel ) VALUES ( - $1, $2, $3, $4, - $5, $6, - $7, $8 + $1, $2, $3, $4, $5, + $6, $7, + $8, $9 ) ", self.id as VersionId, self.mod_id as ModId, + self.author_id as UserId, &self.name, &self.version_number, self.changelog_url.as_ref(), @@ -339,7 +343,7 @@ impl Version { { let result = sqlx::query!( " - SELECT v.mod_id, v.name, v.version_number, + SELECT v.mod_id, v.author_id, v.name, v.version_number, v.changelog_url, v.date_published, v.downloads, v.release_channel FROM versions v @@ -354,6 +358,7 @@ impl Version { Ok(Some(Version { id, mod_id: ModId(row.mod_id), + author_id: UserId(row.author_id), name: row.name, version_number: row.version_number, changelog_url: row.changelog_url, @@ -375,7 +380,7 @@ impl Version { { let result = sqlx::query!( " - SELECT v.mod_id, v.name, v.version_number, + SELECT v.mod_id, v.author_id, v.name, v.version_number, v.changelog_url, v.date_published, v.downloads, release_channels.channel FROM versions v @@ -455,6 +460,7 @@ impl Version { Ok(Some(QueryVersion { id, mod_id: ModId(row.mod_id), + author_id: UserId(row.author_id), name: row.name, version_number: row.version_number, changelog_url: row.changelog_url, @@ -493,6 +499,7 @@ pub struct FileHash { pub struct QueryVersion { pub id: VersionId, pub mod_id: ModId, + pub author_id: UserId, pub name: String, pub version_number: String, pub changelog_url: Option, diff --git a/src/database/postgres_database.rs b/src/database/postgres_database.rs index eaa6c3136..1409222b8 100644 --- a/src/database/postgres_database.rs +++ b/src/database/postgres_database.rs @@ -4,7 +4,7 @@ use sqlx::postgres::{PgPool, PgPoolOptions}; use sqlx::{Connection, PgConnection, Postgres}; use std::path::Path; -const MIGRATION_FOLDER: &'static str = "migrations"; +const MIGRATION_FOLDER: &str = "migrations"; pub async fn connect() -> Result { info!("Initializing database connection"); diff --git a/src/main.rs b/src/main.rs index 64e5d0183..2172a3d0c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ use search::indexing::index_mods; use search::indexing::IndexingSettings; use std::sync::Arc; +mod auth; mod database; mod file_hosting; mod models; diff --git a/src/models/mods.rs b/src/models/mods.rs index 272a18bea..3b93a353d 100644 --- a/src/models/mods.rs +++ b/src/models/mods.rs @@ -1,5 +1,6 @@ use super::ids::Base62Id; use super::teams::TeamId; +use super::users::UserId; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -54,6 +55,8 @@ pub struct Version { pub id: VersionId, /// The ID of the mod this version is for. pub mod_id: ModId, + /// The ID of the author who published this version + pub author_id: UserId, /// The name of this version pub name: String, diff --git a/src/models/teams.rs b/src/models/teams.rs index bc7ddc9da..725ce5a1a 100644 --- a/src/models/teams.rs +++ b/src/models/teams.rs @@ -1,6 +1,6 @@ use super::ids::Base62Id; -use serde::{Deserialize, Serialize}; use crate::models::users::UserId; +use serde::{Deserialize, Serialize}; //TODO Implement Item for teams /// The ID of a team diff --git a/src/models/users.rs b/src/models/users.rs index 8337d6d7a..6e70234c7 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -16,4 +16,32 @@ pub struct User { pub avatar_url: String, pub bio: String, pub created: chrono::DateTime, -} \ No newline at end of file + pub role: Role, +} + +#[derive(Serialize, Deserialize)] +pub enum Role { + Developer, + Moderator, + Admin, +} + +impl ToString for Role { + fn to_string(&self) -> String { + match self { + Role::Developer => String::from("developer"), + Role::Moderator => String::from("moderator"), + Role::Admin => String::from("admin"), + } + } +} + +impl Role { + pub fn from_string(string: &str) -> Role { + match string { + "admin" => Role::Admin, + "moderator" => Role::Moderator, + _ => Role::Developer, + } + } +} diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 4acea01bd..727ee2b36 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -1,22 +1,20 @@ -use crate::models::error::ApiError; -use log::{info}; -use actix_web::web::{Query, ServiceConfig, scope, Data}; -use actix_web::{get, HttpResponse}; -use actix_web::http::StatusCode; -use serde::{Deserialize, Serialize}; -use thiserror::Error; +use crate::auth::get_github_user_from_token; use crate::database::models::{generate_state_id, User, UserId}; -use sqlx::postgres::PgPool; -use crate::models::ids::base62_impl::{to_base62, parse_base62}; +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 actix_web::http::StatusCode; +use actix_web::web::{scope, Data, Query, ServiceConfig}; +use actix_web::{get, HttpResponse}; use chrono::Utc; -use crate::models::ids::{DecodingError}; +use log::info; +use serde::{Deserialize, Serialize}; +use sqlx::postgres::PgPool; +use thiserror::Error; pub fn config(cfg: &mut ServiceConfig) { - cfg.service( - scope("/auth/") - .service(auth_callback) - .service(init) - ); + cfg.service(scope("/auth/").service(auth_callback).service(init)); } #[derive(Error, Debug)] @@ -33,6 +31,8 @@ pub enum AuthorizationError { GithubError(#[from] reqwest::Error), #[error("Invalid Authentication credentials")] InvalidCredentialsError, + #[error("Authentication Error: {0}")] + AuthenticationError(#[from] crate::auth::AuthenticationError), #[error("Error while decoding Base62")] DecodingError(#[from] DecodingError), } @@ -46,6 +46,7 @@ impl actix_web::ResponseError for AuthorizationError { AuthorizationError::GithubError(..) => StatusCode::FAILED_DEPENDENCY, AuthorizationError::InvalidCredentialsError => StatusCode::UNAUTHORIZED, AuthorizationError::DecodingError(..) => StatusCode::BAD_REQUEST, + AuthorizationError::AuthenticationError(..) => StatusCode::UNAUTHORIZED, } } @@ -59,6 +60,7 @@ impl actix_web::ResponseError for AuthorizationError { AuthorizationError::GithubError(..) => "github_error", AuthorizationError::InvalidCredentialsError => "invalid_credentials", AuthorizationError::DecodingError(..) => "decoding_error", + AuthorizationError::AuthenticationError(..) => "authentication_error", }, description: &self.to_string(), }) @@ -83,60 +85,59 @@ pub struct AccessToken { pub token_type: String, } -#[derive(Serialize, Deserialize, Debug)] -pub struct GitHubUser { - pub login: String, - pub id: u64, - pub avatar_url: String, - pub name: String, - pub email: Option, - pub bio: String, -} - //http://localhost:8000/api/v1/auth/init?url=https%3A%2F%2Fmodrinth.com%2Fmods #[get("init")] -pub async fn init(Query(info): Query, client: Data) -> Result { +pub async fn init( + Query(info): Query, + client: Data, +) -> Result { let mut transaction = client.begin().await?; let state = generate_state_id(&mut transaction).await?; sqlx::query!( - " + " INSERT INTO states (id, url) VALUES ($1, $2) ", - state.0, - info.url - ) - .execute(&mut *transaction) - .await?; + state.0, + info.url + ) + .execute(&mut *transaction) + .await?; transaction.commit().await?; let client_id = dotenv::var("GITHUB_CLIENT_ID")?; - let url = format!("https://github.com/login/oauth/authorize?client_id={}&state={}&scope={}", client_id, to_base62(state.0 as u64), "%20repo%20read%3Aorg%20read%3Auser%20user%3Aemail"); + let url = format!( + "https://github.com/login/oauth/authorize?client_id={}&state={}&scope={}", + client_id, + to_base62(state.0 as u64), + "%20repo%20read%3Aorg%20read%3Auser%20user%3Aemail" + ); Ok(HttpResponse::TemporaryRedirect() .header("Location", &*url) - .json(AuthorizationInit { - url, - })) + .json(AuthorizationInit { url })) } #[get("callback")] -pub async fn auth_callback(Query(info): Query, client: Data) -> Result { +pub async fn auth_callback( + Query(info): Query, + client: Data, +) -> Result { let mut transaction = client.begin().await?; let state_id = parse_base62(&*info.state)?; let result = sqlx::query!( - " + " SELECT url,expires FROM states WHERE id = $1 ", - state_id as i64 - ) - .fetch_one(&mut *transaction) - .await?; + state_id as i64 + ) + .fetch_one(&mut *transaction) + .await?; let now = Utc::now(); let duration = result.expires.signed_duration_since(now); @@ -146,14 +147,14 @@ pub async fn auth_callback(Query(info): Query, client: Data, client: Data, client: Data { - info!("{:?}", x.id) - } + match user_result { + Some(x) => info!("{:?}", x.id), None => { - let user_id = crate::database::models::generate_user_id(&mut transaction).await?.into(); + let user_id = crate::database::models::generate_user_id(&mut transaction) + .await?; User { id: user_id, @@ -198,8 +189,11 @@ pub async fn auth_callback(Query(info): Query, client: Data, client: Data actix_web::http::StatusCode { match self { ApiError::DatabaseError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, + ApiError::AuthenticationError => actix_web::http::StatusCode::UNAUTHORIZED, } } @@ -58,6 +61,7 @@ impl actix_web::ResponseError for ApiError { crate::models::error::ApiError { error: match self { ApiError::DatabaseError(..) => "database_error", + ApiError::AuthenticationError => "unauthorized", }, description: &self.to_string(), }, diff --git a/src/routes/mod_creation.rs b/src/routes/mod_creation.rs index 0ffa4362d..16b70a39c 100644 --- a/src/routes/mod_creation.rs +++ b/src/routes/mod_creation.rs @@ -1,14 +1,16 @@ +use crate::auth::{get_user_from_headers, AuthenticationError}; use crate::database::models; use crate::file_hosting::{FileHost, FileHostingError}; use crate::models::error::ApiError; use crate::models::mods::{ModId, VersionId, VersionType}; use crate::models::teams::TeamMember; +use crate::models::users::UserId; use crate::routes::version_creation::InitialVersionData; use crate::search::indexing::queue::CreationQueue; use actix_multipart::{Field, Multipart}; use actix_web::http::StatusCode; use actix_web::web::Data; -use actix_web::{post, HttpResponse}; +use actix_web::{post, HttpRequest, HttpResponse}; use futures::stream::StreamExt; use serde::{Deserialize, Serialize}; use sqlx::postgres::PgPool; @@ -42,6 +44,8 @@ pub enum CreateError { InvalidLoader(String), #[error("Invalid category: {0}")] InvalidCategory(String), + #[error("Authentication Error: {0}")] + Unauthorized(#[from] AuthenticationError), } impl actix_web::ResponseError for CreateError { @@ -59,6 +63,7 @@ impl actix_web::ResponseError for CreateError { CreateError::InvalidGameVersion(..) => StatusCode::BAD_REQUEST, CreateError::InvalidLoader(..) => StatusCode::BAD_REQUEST, CreateError::InvalidCategory(..) => StatusCode::BAD_REQUEST, + CreateError::Unauthorized(..) => StatusCode::UNAUTHORIZED, } } @@ -77,6 +82,7 @@ impl actix_web::ResponseError for CreateError { CreateError::InvalidGameVersion(..) => "invalid_input", CreateError::InvalidLoader(..) => "invalid_input", CreateError::InvalidCategory(..) => "invalid_input", + CreateError::Unauthorized(..) => "unauthorized", }, description: &self.to_string(), }) @@ -126,6 +132,7 @@ pub async fn undo_uploads( #[post("mod")] pub async fn mod_create( + req: HttpRequest, payload: Multipart, client: Data, file_host: Data>, @@ -135,6 +142,7 @@ pub async fn mod_create( let mut uploaded_files = Vec::new(); let result = mod_create_inner( + req, payload, &mut transaction, &***file_host, @@ -161,6 +169,7 @@ pub async fn mod_create( } async fn mod_create_inner( + req: HttpRequest, mut payload: Multipart, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, file_host: &dyn FileHost, @@ -170,6 +179,7 @@ async fn mod_create_inner( let cdn_url = dotenv::var("CDN_URL")?; let mod_id = models::generate_mod_id(transaction).await?.into(); + let user = get_user_from_headers(req.headers(), &mut *transaction).await?; let mut created_versions: Vec = vec![]; @@ -287,6 +297,7 @@ async fn mod_create_inner( let version = models::version_item::VersionBuilder { version_id: version_id.into(), mod_id: mod_id.into(), + author_id: user.id.into(), name: version_data.version_title.clone(), version_number: version_data.version_number.clone(), changelog_url: Some(format!("{}/{}", cdn_url, body_url)), @@ -356,6 +367,16 @@ async fn mod_create_inner( ))); }; + let ids: Vec = (&create_data.team_members) + .iter() + .map(|m| m.user_id) + .collect(); + if !ids.contains(&user.id) { + return Err(CreateError::InvalidInput(String::from( + "Team members must include yourself!", + ))); + } + let mut categories = Vec::with_capacity(create_data.categories.len()); for category in &create_data.categories { let id = models::categories::Category::get_id(&category, &mut *transaction) @@ -430,9 +451,9 @@ async fn mod_create_inner( versions: versions_list, page_url: mod_builder.body_url.clone(), icon_url: mod_builder.icon_url.clone().unwrap(), - // TODO: Author/team info, latest version info - author: String::new(), - author_url: String::new(), + author: user.username, + author_url: format!("https://modrinth.com/user/{}", user.id), + // TODO: latest version info latest_version: String::new(), downloads: 0, date_created: formatted.clone(), diff --git a/src/routes/mods.rs b/src/routes/mods.rs index 346bfdca6..f0b20b72a 100644 --- a/src/routes/mods.rs +++ b/src/routes/mods.rs @@ -1,9 +1,10 @@ use super::ApiError; +use crate::auth::check_is_moderator_from_headers; use crate::database; use crate::models; use crate::models::mods::SearchRequest; use crate::search::{search_for_mod, SearchError}; -use actix_web::{delete, get, web, HttpResponse}; +use actix_web::{delete, get, web, HttpRequest, HttpResponse}; use sqlx::PgPool; #[get("mod")] @@ -48,13 +49,23 @@ pub async fn mod_get( } } -// TODO: This really needs auth // TODO: The mod remains in meilisearch's index until the index is deleted #[delete("{id}")] pub async fn mod_delete( + req: HttpRequest, info: web::Path<(models::ids::ModId,)>, pool: web::Data, ) -> Result { + check_is_moderator_from_headers( + req.headers(), + &mut *pool + .acquire() + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?, + ) + .await + .map_err(|_| ApiError::AuthenticationError)?; + let id = info.0; let result = database::models::Mod::remove_full(id.into(), &**pool) .await diff --git a/src/routes/tags.rs b/src/routes/tags.rs index 06e2d0827..3aa668b5d 100644 --- a/src/routes/tags.rs +++ b/src/routes/tags.rs @@ -1,6 +1,7 @@ use super::ApiError; +use crate::auth::check_is_admin_from_headers; use crate::database::models; -use actix_web::{delete, get, put, web, HttpResponse}; +use actix_web::{delete, get, put, web, HttpResponse, HttpRequest}; use models::categories::{Category, GameVersion, Loader}; use sqlx::PgPool; @@ -32,9 +33,20 @@ pub async fn category_list(pool: web::Data) -> Result, category: web::Path<(String,)>, ) -> Result { + check_is_admin_from_headers( + req.headers(), + &mut *pool + .acquire() + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?, + ) + .await + .map_err(|_| ApiError::AuthenticationError)?; + let name = category.into_inner().0; let _id = Category::builder().name(&name)?.insert(&**pool).await?; @@ -44,9 +56,20 @@ pub async fn category_create( #[delete("category/{name}")] pub async fn category_delete( + req: HttpRequest, pool: web::Data, category: web::Path<(String,)>, ) -> Result { + check_is_admin_from_headers( + req.headers(), + &mut *pool + .acquire() + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?, + ) + .await + .map_err(|_| ApiError::AuthenticationError)?; + let name = category.into_inner().0; let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?; @@ -75,9 +98,20 @@ pub async fn loader_list(pool: web::Data) -> Result, loader: web::Path<(String,)>, ) -> Result { + check_is_admin_from_headers( + req.headers(), + &mut *pool + .acquire() + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?, + ) + .await + .map_err(|_| ApiError::AuthenticationError)?; + let name = loader.into_inner().0; let _id = Loader::builder().name(&name)?.insert(&**pool).await?; @@ -87,9 +121,20 @@ pub async fn loader_create( #[delete("loader/{name}")] pub async fn loader_delete( + req: HttpRequest, pool: web::Data, loader: web::Path<(String,)>, ) -> Result { + check_is_admin_from_headers( + req.headers(), + &mut *pool + .acquire() + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?, + ) + .await + .map_err(|_| ApiError::AuthenticationError)?; + let name = loader.into_inner().0; let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?; @@ -117,9 +162,20 @@ pub async fn game_version_list(pool: web::Data) -> Result, game_version: web::Path<(String,)>, ) -> Result { + check_is_admin_from_headers( + req.headers(), + &mut *pool + .acquire() + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?, + ) + .await + .map_err(|_| ApiError::AuthenticationError)?; + let name = game_version.into_inner().0; let _id = GameVersion::builder() @@ -132,9 +188,20 @@ pub async fn game_version_create( #[delete("game_version/{name}")] pub async fn game_version_delete( + req: HttpRequest, pool: web::Data, game_version: web::Path<(String,)>, ) -> Result { + check_is_admin_from_headers( + req.headers(), + &mut *pool + .acquire() + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?, + ) + .await + .map_err(|_| ApiError::AuthenticationError)?; + let name = game_version.into_inner().0; let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?; diff --git a/src/routes/version_creation.rs b/src/routes/version_creation.rs index 38470eed8..1482ec005 100644 --- a/src/routes/version_creation.rs +++ b/src/routes/version_creation.rs @@ -1,3 +1,4 @@ +use crate::auth::get_user_from_headers; use crate::database::models; use crate::database::models::version_item::{VersionBuilder, VersionFileBuilder}; use crate::file_hosting::FileHost; @@ -7,7 +8,7 @@ use crate::models::mods::{ use crate::routes::mod_creation::{CreateError, UploadedFile}; use actix_multipart::{Field, Multipart}; use actix_web::web::Data; -use actix_web::{post, HttpResponse}; +use actix_web::{post, HttpRequest, HttpResponse}; use futures::stream::StreamExt; use serde::{Deserialize, Serialize}; use sqlx::postgres::PgPool; @@ -32,6 +33,7 @@ struct InitialFileData { // under `/api/v1/mod/{mod_id}` #[post("version")] pub async fn version_create( + req: HttpRequest, url_data: actix_web::web::Path<(ModId,)>, payload: Multipart, client: Data, @@ -43,6 +45,7 @@ pub async fn version_create( let mod_id = url_data.into_inner().0.into(); let result = version_create_inner( + req, payload, &mut transaction, &***file_host, @@ -69,6 +72,7 @@ pub async fn version_create( } async fn version_create_inner( + req: HttpRequest, mut payload: Multipart, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, file_host: &dyn FileHost, @@ -80,6 +84,8 @@ async fn version_create_inner( let mut initial_version_data = None; let mut version_builder = None; + let user = get_user_from_headers(req.headers(), &mut *transaction).await?; + while let Some(item) = payload.next().await { let mut field: Field = item.map_err(CreateError::MultipartError)?; let content_disposition = field.content_disposition().ok_or_else(|| { @@ -126,6 +132,28 @@ async fn version_create_inner( )); } + let team_id = sqlx::query!( + "SELECT team_id FROM mods WHERE id=$1", + mod_id as models::ModId, + ) + .fetch_one(&mut *transaction) + .await?.team_id; + + let member_ids_rows = sqlx::query!( + "SELECT user_id FROM team_members WHERE team_id=$1", + team_id, + ) + .fetch_all(&mut *transaction) + .await?; + + let member_ids : Vec = member_ids_rows.iter() + .map(|m| m.user_id) + .collect(); + + if !member_ids.contains(&(user.id.0 as i64)) { + return Err(CreateError::InvalidInput("Unauthorized".to_string())) + } + let version_id: VersionId = models::generate_version_id(transaction).await?.into(); let body_url = format!( "data/{}/changelogs/{}/body.md", @@ -156,6 +184,7 @@ async fn version_create_inner( version_builder = Some(VersionBuilder { version_id: version_id.into(), mod_id, + author_id: user.id.into(), name: version_create_data.version_title.clone(), version_number: version_create_data.version_number.clone(), changelog_url: Some(format!("{}/{}", cdn_url, body_url)), @@ -239,6 +268,7 @@ async fn version_create_inner( let response = Version { id: version_builder_safe.version_id.into(), mod_id: version_builder_safe.mod_id.into(), + author_id: user.id, name: version_builder_safe.name.clone(), version_number: version_builder_safe.version_number.clone(), changelog_url: version_builder_safe.changelog_url.clone(), @@ -282,6 +312,7 @@ async fn version_create_inner( // under /api/v1/mod/{mod_id}/version/{version_id} #[post("file")] pub async fn upload_file_to_version( + req: HttpRequest, url_data: actix_web::web::Path<(ModId, VersionId)>, payload: Multipart, client: Data, @@ -295,6 +326,7 @@ pub async fn upload_file_to_version( let version_id = models::VersionId::from(data.1); let result = upload_file_to_version_inner( + req, payload, &mut transaction, &***file_host, @@ -322,6 +354,7 @@ pub async fn upload_file_to_version( } async fn upload_file_to_version_inner( + req: HttpRequest, mut payload: Multipart, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, file_host: &dyn FileHost, @@ -334,9 +367,11 @@ async fn upload_file_to_version_inner( let mut initial_file_data: Option = None; let mut file_builder: Option = None; + let user = get_user_from_headers(req.headers(), &mut *transaction).await?; + let result = sqlx::query!( " - SELECT mod_id, version_number + SELECT mod_id, version_number, author_id FROM versions WHERE id = $1 ", @@ -359,6 +394,12 @@ async fn upload_file_to_version_inner( )); } + if version.author_id as u64 != user.id.0 { + return Err(CreateError::InvalidInput( + "Unauthorized".to_string(), + )); + } + let mod_id = ModId(version.mod_id as u64); let version_number = version.version_number; diff --git a/src/routes/versions.rs b/src/routes/versions.rs index a04d4a67a..f76b8aa95 100644 --- a/src/routes/versions.rs +++ b/src/routes/versions.rs @@ -1,7 +1,8 @@ use super::ApiError; +use crate::auth::check_is_moderator_from_headers; use crate::database; use crate::models; -use actix_web::{delete, get, web, HttpResponse}; +use actix_web::{delete, get, web, HttpRequest, HttpResponse}; use sqlx::PgPool; // TODO: this needs filtering, and a better response type @@ -62,6 +63,7 @@ pub async fn version_get( let response = 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, @@ -111,12 +113,22 @@ pub async fn version_get( } } -// TODO: This really needs auth #[delete("{version_id}")] pub async fn version_delete( + req: HttpRequest, info: web::Path<(models::ids::ModId, models::ids::VersionId)>, pool: web::Data, ) -> Result { + check_is_moderator_from_headers( + req.headers(), + &mut *pool + .acquire() + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?, + ) + .await + .map_err(|_| ApiError::AuthenticationError)?; + // TODO: check if the mod exists and matches the version id let id = info.1; let result = database::models::Version::remove_full(id.into(), &**pool)