Final V2 Changes (#212)

* Redo dependencies, add rejection reasons, make notifications more readable

* Fix errors, add dependency route, finish PR

* Fix clippy errors
This commit is contained in:
Geometrically
2021-06-16 09:05:35 -07:00
committed by GitHub
parent 2a4caa856e
commit d2c2503cfa
39 changed files with 2365 additions and 1303 deletions

118
src/util/auth.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: Option<String>,
pub email: Option<String>,
pub bio: Option<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(github_user.id, executor).await?;
match res {
Some(result) => Ok(User {
id: UserId::from(result.id),
github_id: result.github_id.map(|i| i as u64),
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("Authorization")
.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?;
if user.role.is_mod() {
Ok(user)
} else {
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),
}
}

3
src/util/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod auth;
pub mod validate;
pub mod webhook;

55
src/util/validate.rs Normal file
View File

@@ -0,0 +1,55 @@
use lazy_static::lazy_static;
use regex::Regex;
use validator::{ValidationErrors, ValidationErrorsKind};
lazy_static! {
pub static ref RE_URL_SAFE: Regex = Regex::new(r#"^[a-zA-Z0-9!@$()`.+,_"-]*$"#).unwrap();
}
//TODO: In order to ensure readability, only the first error is printed, this may need to be expanded on in the future!
pub fn validation_errors_to_string(errors: ValidationErrors, adder: Option<String>) -> String {
let mut output = String::new();
let map = errors.into_errors();
let key_option = map.keys().next().copied();
if let Some(field) = key_option {
if let Some(error) = map.get(field) {
return match error {
ValidationErrorsKind::Struct(errors) => {
validation_errors_to_string(*errors.clone(), Some(format!("of item {}", field)))
}
ValidationErrorsKind::List(list) => {
if let Some(errors) = list.get(&0) {
output.push_str(&*validation_errors_to_string(
*errors.clone(),
Some(format!("of list {} with index 0", field)),
));
}
output
}
ValidationErrorsKind::Field(errors) => {
if let Some(error) = errors.get(0) {
if let Some(adder) = adder {
output.push_str(&*format!(
"Field {} {} failed validation with error {}",
field, adder, error.code
));
} else {
output.push_str(&*format!(
"Field {} failed validation with error {}",
field, error.code
));
}
}
output
}
};
}
}
"".to_string()
}

107
src/util/webhook.rs Normal file
View File

@@ -0,0 +1,107 @@
use crate::models::projects::Project;
use chrono::{DateTime, Utc};
use serde::Serialize;
#[derive(Serialize)]
struct DiscordEmbed {
pub title: String,
pub description: String,
pub url: String,
pub timestamp: DateTime<Utc>,
pub color: u32,
pub fields: Vec<DiscordEmbedField>,
pub image: DiscordEmbedImage,
}
#[derive(Serialize)]
struct DiscordEmbedField {
pub name: String,
pub value: String,
pub inline: bool,
}
#[derive(Serialize)]
struct DiscordEmbedImage {
pub url: Option<String>,
}
#[derive(Serialize)]
struct DiscordWebhook {
pub embeds: Vec<DiscordEmbed>,
}
pub async fn send_discord_webhook(
project: Project,
webhook_url: String,
) -> Result<(), reqwest::Error> {
let mut fields = Vec::new();
fields.push(DiscordEmbedField {
name: "id".to_string(),
value: project.id.to_string(),
inline: true,
});
if let Some(slug) = project.slug.clone() {
fields.push(DiscordEmbedField {
name: "slug".to_string(),
value: slug,
inline: true,
});
}
fields.push(DiscordEmbedField {
name: "project_type".to_string(),
value: project.project_type.to_string(),
inline: true,
});
fields.push(DiscordEmbedField {
name: "client_side".to_string(),
value: project.client_side.to_string(),
inline: true,
});
fields.push(DiscordEmbedField {
name: "server_side".to_string(),
value: project.server_side.to_string(),
inline: true,
});
fields.push(DiscordEmbedField {
name: "categories".to_string(),
value: project.categories.join(", "),
inline: true,
});
let embed = DiscordEmbed {
url: format!(
"{}/mod/{}",
dotenv::var("SITE_URL").unwrap_or_default(),
project
.clone()
.slug
.unwrap_or_else(|| project.id.to_string())
),
title: project.title,
description: project.description,
timestamp: project.published,
color: 6137157,
fields,
image: DiscordEmbedImage {
url: project.icon_url,
},
};
let client = reqwest::Client::new();
client
.post(&webhook_url)
.json(&DiscordWebhook {
embeds: vec![embed],
})
.send()
.await?;
Ok(())
}