You've already forked AstralRinth
forked from didirus/AstralRinth
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:
118
src/util/auth.rs
Normal file
118
src/util/auth.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: 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
3
src/util/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod auth;
|
||||
pub mod validate;
|
||||
pub mod webhook;
|
||||
55
src/util/validate.rs
Normal file
55
src/util/validate.rs
Normal 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
107
src/util/webhook.rs
Normal 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user