Authenticate protected routes
This commit is contained in:
parent
05235f8385
commit
3d32c30d2d
3
migrations/20200928195220_add-roles-to-users.sql
Normal file
3
migrations/20200928195220_add-roles-to-users.sql
Normal file
@ -0,0 +1,3 @@
|
||||
-- Add migration script here
|
||||
ALTER TABLE users
|
||||
ADD COLUMN role varchar(50) NOT NULL default 'developer'
|
||||
3
migrations/20200929034101_add-author-to-versions.sql
Normal file
3
migrations/20200929034101_add-author-to-versions.sql
Normal 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
118
src/auth/mod.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>,
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 }))
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
},
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)?;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user