Authenticate protected routes

This commit is contained in:
Jai A 2020-09-28 21:05:42 -07:00
parent 05235f8385
commit 3d32c30d2d
18 changed files with 419 additions and 108 deletions

View File

@ -0,0 +1,3 @@
-- Add migration script here
ALTER TABLE users
ADD COLUMN role varchar(50) NOT NULL default 'developer'

View File

@ -0,0 +1,3 @@
-- Add migration script here
ALTER TABLE versions
ADD COLUMN author_id bigint REFERENCES users NOT NULL default 0

118
src/auth/mod.rs Normal file
View File

@ -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<String>,
pub bio: String,
}
pub async fn get_github_user_from_token(
access_token: &str,
) -> Result<GitHubUser, AuthenticationError> {
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<User, AuthenticationError>
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<User, AuthenticationError>
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<User, AuthenticationError>
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<User, AuthenticationError>
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),
}
}

View File

@ -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 {

View File

@ -9,6 +9,7 @@ pub struct User {
pub avatar_url: String,
pub bio: String,
pub created: chrono::DateTime<chrono::Utc>,
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<Option<Self>, sqlx::error::Error>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
pub async fn get<'a, 'b, E>(id: UserId, executor: E) -> Result<Option<Self>, 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<Option<Self>, 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)
}
}
}
}

View File

@ -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<String>,
@ -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<String>,
@ -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<String>,

View File

@ -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<PgPool, sqlx::Error> {
info!("Initializing database connection");

View File

@ -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;

View File

@ -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,

View File

@ -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

View File

@ -16,4 +16,32 @@ pub struct User {
pub avatar_url: String,
pub bio: String,
pub created: chrono::DateTime<chrono::Utc>,
}
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,
}
}
}

View File

@ -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<String>,
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<AuthorizationInit>, client: Data<PgPool>) -> Result<HttpResponse, AuthorizationError> {
pub async fn init(
Query(info): Query<AuthorizationInit>,
client: Data<PgPool>,
) -> Result<HttpResponse, AuthorizationError> {
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<Authorization>, client: Data<PgPool>) -> Result<HttpResponse, AuthorizationError> {
pub async fn auth_callback(
Query(info): Query<Authorization>,
client: Data<PgPool>,
) -> Result<HttpResponse, AuthorizationError> {
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<Authorization>, client: Data<PgPoo
}
sqlx::query!(
"
"
DELETE FROM states
WHERE id = $1
",
state_id as i64
)
.execute(&mut *transaction)
.await?;
state_id as i64
)
.execute(&mut *transaction)
.await?;
let client_id = dotenv::var("GITHUB_CLIENT_ID")?;
let client_secret = dotenv::var("GITHUB_CLIENT_SECRET")?;
@ -163,9 +164,7 @@ pub async fn auth_callback(Query(info): Query<Authorization>, client: Data<PgPoo
client_id, client_secret, info.code
);
let client = reqwest::Client::new();
let token : AccessToken = client
let token: AccessToken = reqwest::Client::new()
.post(&url)
.header(reqwest::header::ACCEPT, "application/json")
.send()
@ -173,22 +172,14 @@ pub async fn auth_callback(Query(info): Query<Authorization>, client: Data<PgPoo
.json()
.await?;
let user : GitHubUser = client
.get("https://api.github.com/user")
.header(reqwest::header::USER_AGENT, "Modrinth")
.header(reqwest::header::AUTHORIZATION, format!("token {}", token.access_token))
.send()
.await?
.json()
.await?;
let user = get_github_user_from_token(&*token.access_token).await?;
let user_result = User::get_from_github_id(UserId(user.id as i64), &mut *transaction).await?;
match user_result{
Some(x) => {
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<Authorization>, client: Data<PgPoo
email: user.email,
avatar_url: user.avatar_url,
bio: user.bio,
created: Utc::now()
}.insert(&mut transaction).await?;
created: Utc::now(),
role: Role::Developer.to_string(),
}
.insert(&mut transaction)
.await?;
}
}
@ -209,7 +203,5 @@ pub async fn auth_callback(Query(info): Query<Authorization>, client: Data<PgPoo
Ok(HttpResponse::TemporaryRedirect()
.header("Location", &*redirect_url)
.json(AuthorizationInit {
url: redirect_url,
}))
.json(AuthorizationInit { url: redirect_url }))
}

View File

@ -9,8 +9,8 @@ mod tags;
mod version_creation;
mod versions;
pub use tags::config as tags_config;
pub use auth::config as auth_config;
pub use tags::config as tags_config;
pub use self::index::index_get;
pub use self::not_found::not_found;
@ -44,12 +44,15 @@ pub fn versions_config(cfg: &mut web::ServiceConfig) {
pub enum ApiError {
#[error("Internal server error")]
DatabaseError(#[from] crate::database::models::DatabaseError),
#[error("Authentication Error")]
AuthenticationError,
}
impl actix_web::ResponseError for ApiError {
fn status_code(&self) -> 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(),
},

View File

@ -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<PgPool>,
file_host: Data<Arc<dyn FileHost + Send + Sync>>,
@ -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<models::version_item::VersionBuilder> = 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<UserId> = (&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(),

View File

@ -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<PgPool>,
) -> Result<HttpResponse, ApiError> {
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

View File

@ -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<PgPool>) -> Result<HttpResponse, ApiE
// TODO: don't fail if category already exists
#[put("category/{name}")]
pub async fn category_create(
req: HttpRequest,
pool: web::Data<PgPool>,
category: web::Path<(String,)>,
) -> Result<HttpResponse, ApiError> {
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<PgPool>,
category: web::Path<(String,)>,
) -> Result<HttpResponse, ApiError> {
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<PgPool>) -> Result<HttpResponse, ApiErr
// TODO: don't fail if loader already exists
#[put("loader/{name}")]
pub async fn loader_create(
req: HttpRequest,
pool: web::Data<PgPool>,
loader: web::Path<(String,)>,
) -> Result<HttpResponse, ApiError> {
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<PgPool>,
loader: web::Path<(String,)>,
) -> Result<HttpResponse, ApiError> {
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<PgPool>) -> Result<HttpResponse,
// remain idempotent
#[put("game_version/{name}")]
pub async fn game_version_create(
req: HttpRequest,
pool: web::Data<PgPool>,
game_version: web::Path<(String,)>,
) -> Result<HttpResponse, ApiError> {
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<PgPool>,
game_version: web::Path<(String,)>,
) -> Result<HttpResponse, ApiError> {
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)?;

View File

@ -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<PgPool>,
@ -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<i64> = 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<PgPool>,
@ -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<InitialFileData> = None;
let mut file_builder: Option<VersionFileBuilder> = 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;

View File

@ -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<PgPool>,
) -> Result<HttpResponse, ApiError> {
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)