Skip to content

Commit

Permalink
Merge pull request #232 from Xeckt/config-rewrite
Browse files Browse the repository at this point in the history
Config Changes from .env to TOML - Related to issue #217
  • Loading branch information
zleyyij authored Oct 17, 2024
2 parents 6766ba7 + c0f2be6 commit 8cd6e90
Show file tree
Hide file tree
Showing 11 changed files with 264 additions and 123 deletions.
53 changes: 53 additions & 0 deletions backend/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ description = "A backend for the r/TechSupport CMS"
repository = "https://github.com/r-Techsupport/hyde"
readme = "../README.md"
keywords = ["cms", "wiki"]
categories = ["server-backend"]
categories = ["web-programming"]
rust-version = "1.75.0"


[dependencies]
axum = { version = "0.7.7", features = ["http2", "macros"] }
chrono = "0.4.38"
Expand All @@ -28,3 +27,4 @@ tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread", "signal",
tower-http = { version = "0.6.1", features = ["normalize-path", "fs", "cors", "tracing", "trace"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
toml = "0.8.19"
110 changes: 110 additions & 0 deletions backend/src/app_conf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use std::{fs, process};
use std::sync::Arc;
use serde::Deserialize;
use tracing::{info, error};

#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct AppConf {
pub files: Files,
pub discord: Discord,
pub oauth: OAuth,
pub database: Database,
}

#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct Files {
pub asset_path: String,
pub docs_path: String,
pub repo_url: String,
}

#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct Discord {
pub admin_username: String,
}

#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct OAuth {
pub discord: DiscordOAuth,
pub github: GitHubOAuth,
}

#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct DiscordOAuth {
pub client_id: String,
pub secret: String,
pub url: String,
pub token_url: String,
}

#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct GitHubOAuth {
pub client_id: String,
// Uncomment this if needed
// pub secret: String,
}

#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct Database {
pub url: String,
}

// Trait to validate fields in each struct
trait ValidateFields {
fn validate(&self, path: &str) -> Result<(), String>;
}

// Macro to validate all fields for each struct
macro_rules! impl_validate {
($struct_name:ident, $( $field:ident ),* ) => {
impl ValidateFields for $struct_name {
fn validate(&self, path: &str) -> Result<(), String> {
$(
let field_path = format!("{}.{}", path, stringify!($field));
if self.$field.is_empty() {
return Err(format!("Field '{}' is empty", field_path));
}
)*
Ok(())
}
}
};
}

impl_validate!(Files, asset_path, docs_path, repo_url);
impl_validate!(Discord, admin_username);
impl_validate!(DiscordOAuth, client_id, secret, url, token_url);
impl_validate!(GitHubOAuth, client_id);
impl_validate!(Database, url);

impl ValidateFields for OAuth {
fn validate(&self, path: &str) -> Result<(), String> {
self.discord.validate(&format!("{}.discord", path))?;
self.github.validate(&format!("{}.github", path))?;
Ok(())
}
}

impl ValidateFields for AppConf {
fn validate(&self, path: &str) -> Result<(), String> {
self.files.validate(&format!("{}.files", path))?;
self.discord.validate(&format!("{}.discord", path))?;
self.oauth.validate(&format!("{}.oauth", path))?;
self.database.validate(&format!("{}.database", path))?;
Ok(())
}
}
impl AppConf {
pub fn load() -> Arc<Self> {
let file = fs::read_to_string("default.toml").expect("Unable to read config");
let config: Self = toml::from_str(&file).expect("Unable to parse config");
match config.validate("config") {
Ok(_) => info!("Configuration isn't empty"),
Err(e) => {
error!("Validation error: {}", e);
process::exit(1)
},
}
Arc::new(config)
}
}
2 changes: 1 addition & 1 deletion backend/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub struct GroupPermissions {
}

/// A wrapper around the sqlite database, and how consumers should interact with the database in any capacity.
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct Database {
pool: SqlitePool,
}
Expand Down
25 changes: 12 additions & 13 deletions backend/src/gh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use color_eyre::Result;
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::env;
use std::fs::File;
use std::io::Read;
use std::sync::Arc;
Expand Down Expand Up @@ -43,11 +42,11 @@ struct Claims {
}

impl Claims {
pub fn new() -> Result<Self> {
pub fn new(client_id: &str) -> Result<Self> {
let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
let iat = current_time - 60;
let exp = current_time + (60 * 5);
let iss = env::var("GH_CLIENT_ID").wrap_err("Failed to read the `GH_CLIENT_ID` env var")?;
let iss = client_id.to_string();

Ok(Self {
iat,
Expand All @@ -59,7 +58,7 @@ impl Claims {
}

/// A wrapper around the github access token that automatically refreshes if the token has been invalidated
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct GithubAccessToken {
expires_at: Arc<Mutex<SystemTime>>,
token: Arc<Mutex<String>>,
Expand All @@ -76,12 +75,12 @@ impl GithubAccessToken {
}

/// Return the cached token if it's less than one hour old, or fetch a new token from the api, and return that, updating the cache
pub async fn get(&self, req_client: &Client) -> Result<String> {
pub async fn get(&self, req_client: &Client, client_id: &str) -> Result<String> {
let mut token_ref = self.token.lock().await;
// Fetch a new token if more than 59 minutes have passed
// Tokens expire after 1 hour, this is to account for clock drift
if SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() > (60 * 59) {
let api_response = get_access_token(req_client).await?;
let api_response = get_access_token(req_client, client_id).await?;
*token_ref = api_response.0;
let mut expires_ref = self.expires_at.lock().await;
*expires_ref = api_response.1;
Expand All @@ -99,12 +98,12 @@ struct AccessTokenResponse {
/// Request a github installation access token using the provided reqwest client.
/// The installation access token will expire after 1 hour.
/// Returns the new token, and the time of expiration
async fn get_access_token(req_client: &Client) -> Result<(String, SystemTime)> {
let token = gen_jwt_token()?;
async fn get_access_token(req_client: &Client, client_id: &str) -> Result<(String, SystemTime)> {
let token = gen_jwt_token(client_id)?;
let response = req_client
.post(format!(
"https://api.github.com/app/installations/{}/access_tokens",
get_installation_id(req_client).await?
get_installation_id(req_client, client_id).await?
))
.bearer_auth(token)
.header("Accept", "application/vnd.github+json")
Expand All @@ -129,10 +128,10 @@ struct InstallationIdResponse {
/// Fetch the Installation ID. This value is required for most API calls
///
/// <https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation#generating-an-installation-access-token>
async fn get_installation_id(req_client: &Client) -> Result<String> {
async fn get_installation_id(req_client: &Client, client_id: &str) -> Result<String> {
let response = req_client
.get("https://api.github.com/app/installations")
.bearer_auth(gen_jwt_token()?)
.bearer_auth(gen_jwt_token(client_id)?)
.header("User-Agent", "Hyde")
// https://docs.github.com/en/rest/about-the-rest-api/api-versions?apiVersion=2022-11-28
.header("X-GitHub-Api-Version", "2022-11-28")
Expand All @@ -151,14 +150,14 @@ async fn get_installation_id(req_client: &Client) -> Result<String> {
}

/// Generate a new JWT token for use with github api interactions.
fn gen_jwt_token() -> Result<String> {
fn gen_jwt_token(client_id: &str) -> Result<String> {
let mut private_key_file = File::open("hyde-data/key.pem")
.wrap_err("Failed to read private key from `hyde-data/key.pem`")?;
let mut private_key = Vec::new();
private_key_file.read_to_end(&mut private_key)?;
Ok(encode(
&Header::new(Algorithm::RS256),
&Claims::new()?,
&Claims::new(client_id)?,
&EncodingKey::from_rsa_pem(&private_key)?,
)?)
}
Loading

0 comments on commit 8cd6e90

Please sign in to comment.