From c3bc254924866ce090f06d8a9670f8922d1622a2 Mon Sep 17 00:00:00 2001 From: Mo8it Date: Mon, 5 Dec 2022 22:28:43 +0100 Subject: [PATCH] Port to Axum --- .gitignore | 5 +- Cargo.toml | 23 ++++----- README.adoc | 91 ---------------------------------- README.md | 89 ++++++++++++++++++++++++++++++++++ src/config.rs | 36 ++++++++++++-- src/db.rs | 4 +- src/errors.rs | 25 ++++++++++ src/guards.rs | 115 ------------------------------------------- src/logging.rs | 35 +++++-------- src/mailer.rs | 48 ++++++++++++++++++ src/main.rs | 51 +++++++------------ src/routes.rs | 124 ++++++++++++++++++++++++++++++----------------- src/states.rs | 30 ++++++++++-- src/templates.rs | 31 ++++++++++++ 14 files changed, 375 insertions(+), 332 deletions(-) delete mode 100644 README.adoc create mode 100644 README.md create mode 100644 src/errors.rs delete mode 100644 src/guards.rs create mode 100644 src/mailer.rs create mode 100644 src/templates.rs diff --git a/.gitignore b/.gitignore index 9561499..2ce198f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ -*.json -*.log /Cargo.lock /db/ +*.json +*.log /scripts/ /target/ +*.yaml diff --git a/Cargo.toml b/Cargo.toml index 1b0b801..3c90993 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "git-webhook-client" -version = "0.2.0" +version = "0.3.0" authors = ["Mo Bitar "] edition = "2021" -readme = "README.adoc" +readme = "README.md" repository = "https://codeberg.org/Mo8it/git-webhook-client" license-file = "LICENSE.txt" @@ -11,22 +11,19 @@ license-file = "LICENSE.txt" anyhow = "1.0" askama = { git = "https://github.com/djc/askama.git" } askama_axum = { git = "https://github.com/djc/askama.git", package = "askama_axum" } -axum = { version = "0.5", default-features = false, features = [ - "http1", - "query", - "form", -] } -axum-extra = { version = "0.3", features = ["spa"] } -chrono = { version = "0.4", default-features = false } +axum = { version = "0.6", default-features = false, features = ["http1", "tokio", "macros"] } +axum-extra = { version = "0.4", features = ["spa"] } +bytes = "1.3" +chrono = { version = "0.4", default-features = false, features = ["clock"] } +diesel = { version = "2.0", features = ["r2d2", "sqlite", "returning_clauses_for_sqlite_3_35", "without-deprecated"] } hex = "0.4" hmac = "0.12" -lettre = "0.10" +lettre = { version = "0.10", default-features = false, features = ["smtp-transport", "hostname", "rustls-tls", "pool", "builder"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +serde_yaml = "0.9" sha2 = "0.10" -tokio = { version = "1.21", features = ["full"] } -tower = { version = "0.4", features = ["limit", "buffer"] } -tower-http = { version = "0.3", features = ["trace"] } +tokio = { version = "1.22", features = ["full"] } tracing = "0.1" tracing-appender = "0.2" tracing-subscriber = "0.3" diff --git a/README.adoc b/README.adoc deleted file mode 100644 index 21fe914..0000000 --- a/README.adoc +++ /dev/null @@ -1,91 +0,0 @@ -= Git Webhook Client - -Git webhook client that runs commands after a webhook event and shows their output. - -Currently, only Gitea is supported. If you want support for Gitlab or Github, then please open an issue. - -== Features - -* Verify the webhook event with a secret. -* Run a configured command to a specific repository on a webhook event. -* Save the output of the command. -* Show an output by visiting the url of the client. -* Supported configuration for multiple repositories. -* Written in Rust :D - -== Getting started - -=== Requirements - -* `cargo` to compile the source code. -* Development package for SQLite (`sqlite-devel` on Fedora) - -=== Configuration - -The program looks for the configuration file configured with the environment variable `GWC_CONFIG_FILE` that contains the following: - -. `secret`: The secret of the webhook. -. `base_url`: The base_url of the webhook client. -. `hooks`: List of webhooks. -.. `repo_url`: Repository url. -.. `current_dir`: The directory to run the command in. -.. `command`: The command without any arguments. -.. `args`: List of arguments separated by a comma. - -==== Example configuration file: - -[source, json] ----- -{ - "secret": "CHANGE_ME!", - "base_url": "https://webhook.mo8it.xyz", - "hooks": [ - { - "repo_url": "https://codeberg.org/Mo8it/git-webhook-client", - "current_dir": ".", - "command": "ls", - "args": ["-l", "-a", "test_directory"] - } - ] -} ----- - -==== First setup - -* Clone the repository. -* Create the configuration file. -* Run the following to initialize the database: -. -[source, bash] ----- -cargo install diesel_cli --no-default-features --features sqlite -DATABASE_URL=PATH/TO/DATABASE/DIRECTORY/db.sqlite diesel_cli migration run -cargo build --release ----- - -==== Run - -After running `cargo build --release`, the binary can be found in the directory `target/release/git-webhook-client`. To run it, you have to specify the environment variable `DATABASE_URL`: - -[source, bash] ----- -DATABASE_URL=PATH/TO/DATABASE/DIRECTORY/db.sqlite target/release/git-webhook-client ----- - -==== Setup on the git server - -Setup the webhook for the configured repositories on the git server. Don't forget to enter the same secret that you did specify in the configuration file. - -==== Show output - -After an event, the client responds with a URL that shows the log. The id in that URL is important and specific to this event. - -If you want to see the last log, just visit the `base_url` from the configuration. - -To see a specific log with an id, visit the URL: `base_url/?id=THE_ID_OF_AN_EVENT`. - -You can specify a negative ID to see the last events. `id=-1` corresponds to the last log, `id=-2` corresponds to the log before it and so on. - -== Note - -This is my first Rust project and I am still learning. If you have any suggestions, just open an issue! diff --git a/README.md b/README.md new file mode 100644 index 0000000..322b2d3 --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# Git Webhook Client + +Git webhook client that runs commands after a webhook event and shows their output. + +Currently, only Gitea is supported. If you want support for Gitlab or Github, then please open an issue. + +## Features + +- Verify the webhook event with a secret. +- Run a configured command to a specific repository on a webhook event. +- Save the output of the command. +- Show an output by visiting the url of the client. +- Supported configuration for multiple repositories. +- Written in Rust :D + +## Getting started + +### Requirements + +- `cargo` to compile the source code. +- Development package for SQLite (`sqlite-devel` on Fedora) + +### Configuration + +The program looks for the configuration file configured with the environment variable `GWC_CONFIG_FILE` that contains the following: + + + +1. `secret`: The secret of the webhook. +1. `base_url`: The base_url of the webhook client. +1. `hooks`: List of webhooks. +1. `repo_url`: Repository url. +1. `current_dir`: The directory to run the command in. +1. `command`: The command without any arguments. +1. `args`: List of arguments separated by a comma. + +#### Example configuration file: + + + +```yaml +secret: CHANGE_ME! +base_url: https://webhook.mo8it.com + +hooks: + repo_url: https://codeberg.org/Mo8it/git-webhook-client + current_dir: . + command: ls + args: ["-l", "-a", "test_directory"] +``` + +#### First setup + + + +- Clone the repository. +- Create the configuration file. +- Run the following to initialize the database: + ```bash + cargo install diesel_cli --no-default-features --features sqlite + DATABASE_URL=PATH/TO/DATABASE/DIRECTORY/db.sqlite diesel_cli migration run + cargo build --release + ``` + +#### Run + +After running `cargo build --release`, the binary can be found in the directory `target/release/git-webhook-client`. To run it, you have to specify the environment variable `DATABASE_URL`: + +```bash +DATABASE_URL=PATH/TO/DATABASE/DIRECTORY/db.sqlite target/release/git-webhook-client +``` + +#### Setup on the git server + +Setup the webhook for the configured repositories on the git server. Don't forget to enter the same secret that you did specify in the configuration file. + +#### Show output + +After an event, the client responds with a URL that shows the log. The id in that URL is important and specific to this event. + +If you want to see the last log, just visit the `base_url` from the configuration. + +To see a specific log with an id, visit the URL: `base_url/?id=THE_ID_OF_AN_EVENT`. + +You can specify a negative ID to see the last events. `id=-1` corresponds to the last log, `id=-2` corresponds to the log before it and so on. + +## Note + +This is my first Rust project and I am still learning. If you have any suggestions, just open an issue! diff --git a/src/config.rs b/src/config.rs index 0a08a48..10697e2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,6 +4,32 @@ use std::env; use std::fs::File; use std::io::BufReader; +#[derive(Deserialize)] +pub struct SocketAddress { + pub address: [u8; 4], + pub port: u16, +} + +#[derive(Deserialize)] +pub struct EmailServer { + pub server_name: String, + pub email: String, + pub password: String, +} + +#[derive(Deserialize)] +pub struct Address { + pub name: String, + pub user: String, + pub domain: String, +} + +#[derive(Deserialize)] +pub struct Logging { + pub directory: String, + pub filename: String, +} + #[derive(Deserialize)] pub struct Hook { pub repo_url: String, @@ -16,7 +42,11 @@ pub struct Hook { pub struct Config { pub secret: String, pub base_url: String, - pub log_file: String, + pub socket_address: SocketAddress, + pub email_server: EmailServer, + pub email_from: Address, + pub email_to: Address, + pub logging: Logging, pub hooks: Vec, } @@ -29,8 +59,8 @@ impl Config { let config_file = File::open(&config_path) .with_context(|| format!("Can not open the config file at the path {config_path}"))?; let config_reader = BufReader::new(config_file); - let config: Self = serde_json::from_reader(config_reader) - .context("Can not parse the config file as JSON!")?; + let config: Self = serde_yaml::from_reader(config_reader) + .context("Can not parse the YAML config file!")?; Ok(config) } diff --git a/src/db.rs b/src/db.rs index 55aa6f7..fb665ca 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,9 +1,9 @@ use anyhow::{Context, Result}; -use chrono::Local; +use chrono::offset::Local; use diesel::prelude::*; use diesel::r2d2::{ConnectionManager, Pool, PooledConnection}; -use log::error; use std::env; +use tracing::error; use crate::config::Hook; use crate::models::{HookLog, NewHookLog}; diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..83e56ee --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,25 @@ +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use tracing::error; + +pub struct AppError(anyhow::Error); + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + error!("{:?}", self.0); + + StatusCode::BAD_REQUEST.into_response() + } +} + +impl From for AppError { + fn from(err: anyhow::Error) -> Self { + Self(err) + } +} + +impl From<&str> for AppError { + fn from(s: &str) -> Self { + Self(anyhow::Error::msg(s.to_string())) + } +} diff --git a/src/guards.rs b/src/guards.rs deleted file mode 100644 index 09ade9c..0000000 --- a/src/guards.rs +++ /dev/null @@ -1,115 +0,0 @@ -use hmac::{Hmac, Mac}; -use rocket::data::{Data, FromData, Limits, Outcome}; -use rocket::http::Status; -use rocket::request::{self, Request}; -use serde_json::Value; -use sha2::Sha256; - -use crate::states; - -pub struct Repo<'r> { - pub clone_url: &'r str, -} - -#[rocket::async_trait] -impl<'r> FromData<'r> for Repo<'r> { - type Error = String; - - async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self> { - let payload = match data.open(Limits::JSON).into_bytes().await { - Ok(payload) if payload.is_complete() => payload.into_inner(), - Ok(_) => { - return Outcome::Failure((Status::PayloadTooLarge, "Payload too large".to_string())) - } - Err(e) => return Outcome::Failure((Status::InternalServerError, e.to_string())), - }; - - let mut received_signatures = req.headers().get("X-GITEA-SIGNATURE"); - let received_signature = match received_signatures.next() { - Some(signature) => match hex::decode(signature) { - Ok(signature) => signature, - Err(_) => { - return Outcome::Failure(( - Status::BadRequest, - "Can not hex decode the received signature!".to_string(), - )) - } - }, - None => { - return Outcome::Failure((Status::BadRequest, "Missing signature!".to_string())) - } - }; - - if received_signatures.next().is_some() { - return Outcome::Failure(( - Status::BadRequest, - "Received more than one signature!".to_string(), - )); - } - - let config_state = match req.rocket().state::() { - Some(state) => state, - None => { - return Outcome::Failure(( - Status::BadRequest, - "Can not get the config state!".to_string(), - )) - } - }; - - if !is_valid_signature(&config_state.secret, &received_signature, &payload) { - return Outcome::Failure((Status::BadRequest, "Invalid signature!".to_string())); - } - - let json: Value = match serde_json::from_slice(&payload) { - Ok(json) => json, - Err(_) => { - return Outcome::Failure(( - Status::BadRequest, - "Can not parse payload into JSON!".to_string(), - )) - } - }; - let repo = match json.get("repository") { - Some(repo) => repo, - None => { - return Outcome::Failure(( - Status::BadRequest, - "Can not get the repository value from the payload!".to_string(), - )) - } - }; - let clone_url = match repo.get("clone_url") { - Some(url) => url, - None => { - return Outcome::Failure(( - Status::BadRequest, - "Can not get value clone_url from repository in the payload!".to_string(), - )) - } - }; - let clone_url = match clone_url.as_str() { - Some(url) => url.to_string(), - None => { - return Outcome::Failure(( - Status::BadRequest, - "The value of clone_url from repository in the payload is not a string!" - .to_string(), - )) - } - }; - - let clone_url = request::local_cache!(req, clone_url); - - Outcome::Success(Repo { clone_url }) - } -} - -fn is_valid_signature(secret: &[u8], received_signature: &[u8], payload: &[u8]) -> bool { - let mut mac = - Hmac::::new_from_slice(secret).expect("Can not generate a mac from the secret!"); - mac.update(payload); - let expected_signature = mac.finalize().into_bytes(); - - received_signature[..] == expected_signature[..] -} diff --git a/src/logging.rs b/src/logging.rs index 74d08d3..2015161 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -1,30 +1,17 @@ -use anyhow::{Context, Result}; -use simplelog::{ColorChoice, LevelFilter, TermLogger, TerminalMode, WriteLogger}; -use std::fs::OpenOptions; +use tracing_appender::non_blocking::WorkerGuard; +use tracing_subscriber::filter::LevelFilter; use crate::config; -pub fn init_logger(config: &config::Config) -> Result<()> { - let logger = if cfg!(debug_assertions) { - TermLogger::init( - LevelFilter::Debug, - simplelog::Config::default(), - TerminalMode::Mixed, - ColorChoice::Auto, - ) - } else { - WriteLogger::init( - LevelFilter::Info, - simplelog::Config::default(), - OpenOptions::new() - .create(true) - .append(true) - .open(&config.log_file) - .with_context(|| format!("Could not open the log file {}", &config.log_file))?, - ) - }; +pub fn init_logger(logging_config: &config::Logging) -> WorkerGuard { + let file_appender = + tracing_appender::rolling::never(&logging_config.directory, &logging_config.filename); + let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); - logger.context("Could not initialize the logger!")?; + tracing_subscriber::fmt() + .with_max_level(LevelFilter::INFO) + .with_writer(non_blocking) + .init(); - Ok(()) + guard } diff --git a/src/mailer.rs b/src/mailer.rs new file mode 100644 index 0000000..55e689e --- /dev/null +++ b/src/mailer.rs @@ -0,0 +1,48 @@ +use anyhow::{Context, Result}; +use lettre::address::Address; +use lettre::message::{Mailbox, MessageBuilder}; +use lettre::transport::smtp::authentication::Credentials; +use lettre::{Message, SmtpTransport}; +use std::mem; + +use crate::config; + +pub struct Mailer { + mailer: SmtpTransport, + message_builder: MessageBuilder, +} + +impl Mailer { + pub fn new(config: &mut config::Config) -> Result { + let creds = Credentials::new( + mem::take(&mut config.email_server.email), + mem::take(&mut config.email_server.password), + ); + + let mailer = SmtpTransport::relay(&config.email_server.server_name) + .context("Failed to connect to the email server!")? + .credentials(creds) + .build(); + + let message_builder = Message::builder() + .from(Mailbox::new( + Some(mem::take(&mut config.email_from.name)), + Address::new(&config.email_from.user, &config.email_from.domain) + .context("Failed to create the From email address!")?, + )) + .to(Mailbox::new( + Some(mem::take(&mut config.email_to.name)), + Address::new(&config.email_to.user, &config.email_to.domain) + .context("Failed to create the To email address!")?, + )); + + Ok(Self { + mailer, + message_builder, + }) + } + + pub fn send(&self) -> Result<()> { + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 83c4f33..bc69316 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,36 +1,25 @@ mod config; mod db; -mod guards; +mod errors; mod logging; +mod mailer; mod models; mod routes; mod schema; mod states; +mod templates; use anyhow::Result; -use axum::extract::Extension; use axum::routing::{get, post}; -use axum::{error_handling::HandleErrorLayer, http::StatusCode, BoxError}; use axum::{Router, Server}; use axum_extra::routing::SpaRouter; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::process; -use std::sync::Arc; -use tower_http::trace::{DefaultOnResponse, TraceLayer}; -use tracing::Level; -use tracing_appender::non_blocking::WorkerGuard; +use tracing::info; -fn init() -> Result> { - let rocket = rocket::build() - .mount("/", rocket::routes![routes::index]) - .mount("/api", rocket::routes![routes::trigger]) - .manage(states::DB::new()?) -} - -async fn init() -> Result { +async fn init() -> Result<()> { let mut config = config::Config::new()?; - let path_prefix = config.path_prefix.clone(); - let mailer = Arc::new(mailer::Mailer::new(&mut config)?); + let mailer = mailer::Mailer::new(&mut config)?; let address = config.socket_address.address; let socket_address = SocketAddr::new( @@ -40,37 +29,33 @@ async fn init() -> Result { config.socket_address.port, ); - let tracing_worker_gurad = logging::init_logger(&config.logging); + let _tracing_gurad = logging::init_logger(&config.logging); - let config = Arc::new(config); + let app_state = states::AppState::new(config, mailer)?; - let spa = SpaRouter::new(&format!("{}/static", &path_prefix), "static"); - - let api_routes = Router::new() - .route("/submit", post(routes::trigger)); + let api_routes = Router::new().route("/trigger", post(routes::trigger)); let routes = Router::new() .route("/", get(routes::index)) -.nest("/api", api_routes); + .route("/:id", get(routes::index_id)) + .nest("/api", api_routes) + .with_state(app_state); - let app = Router::new() - .merge(spa) - .merge(routes) - .layer(TraceLayer::new_for_http().on_response(DefaultOnResponse::new().level(Level::INFO))) - .layer(Extension(config)) - .layer(Extension(mailer)); + let spa = SpaRouter::new("/static", "static"); + let app = Router::new().merge(routes).merge(spa); + + info!("Starting server"); Server::bind(&socket_address) .serve(app.into_make_service()) .await .unwrap(); - Ok(tracing_worker_gurad) + Ok(()) } - #[tokio::main] async fn main() { - let _tracing_worker_gurad = init().await.unwrap_or_else(|e| { + init().await.unwrap_or_else(|e| { eprintln!("{e:?}"); process::exit(1); }); diff --git a/src/routes.rs b/src/routes.rs index 6dbe598..daa170f 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1,59 +1,92 @@ -use rocket::response::status::BadRequest; -use rocket::{get, post, State}; -use rocket_dyn_templates::Template; +use anyhow::Context; +use askama_axum::IntoResponse; +use axum::extract::{Path, State}; +use axum::http::header::HeaderMap; +use axum::response::Response; +use bytes::Bytes; +use hmac::{Hmac, Mac}; +use serde_json::Value; +use sha2::Sha256; use std::process::Command; +use std::sync::Arc; use std::thread; +use tracing::info; -use crate::db; -use crate::guards; -use crate::states; +use crate::{db, errors, states, templates}; -fn bad_req(err: E) -> BadRequest -where - E: std::fmt::Display, -{ - BadRequest(Some(err.to_string())) +pub async fn index(State(db_state): State>) -> Result { + index_id(State(db_state), Path(-1)).await } -#[get("/?")] -pub fn index( - db_state: &State, - id: Option, -) -> Result> { - let id = id.unwrap_or(-1); - +pub async fn index_id( + State(db_state): State>, + Path(id): Path, +) -> Result { if id == 0 { - return Err(bad_req("id=0 not allowed!")); + return Err("id=0 not allowed!".into()); } - let hook_log = match db::get_hook_log(&db_state.pool, id) { - Ok(hl) => hl, - Err(e) => return Err(bad_req(e)), - }; + let hook_log = db::get_hook_log(&db_state.pool, id)?; - Ok(Template::render("hook_log", hook_log)) + info!("Viewed hook log with id: {}", hook_log.id); + + let template = templates::HookLog::from(hook_log); + + Ok(template.into_response()) } -#[post("/trigger", format = "json", data = "")] -pub fn trigger( - repo: guards::Repo, - db_state: &State, - config_state: &State, -) -> Result> { - let hook = match config_state.get_hook(repo.clone_url) { - Some(hook) => hook, - None => { - return Err(bad_req(format!( - "No matching repository with url {} in the configuration file.", - repo.clone_url - ))) - } - }; +async fn is_valid_signature(secret: &[u8], received_signature: &[u8], body: &[u8]) -> bool { + let mut mac = + Hmac::::new_from_slice(secret).expect("Can not generate a mac from the secret!"); + mac.update(body); + let expected_signature = mac.finalize().into_bytes(); - let hook_log_id = match db::add_hook_log(&db_state.pool, hook) { - Ok(hook_log) => hook_log.id, - Err(e) => return Err(bad_req(e)), - }; + received_signature[..] == expected_signature[..] +} + +pub async fn trigger( + State(db_state): State>, + State(config_state): State>, + headers: HeaderMap, + body: Bytes, +) -> Result { + info!("Trigger called"); + + let mut received_signatures = headers.get_all("X-GITEA-SIGNATURE").iter(); + + let received_signature = received_signatures.next().context("Missing signature!")?; + + let received_signature = + hex::decode(received_signature).context("Can not hex decode the received signature!")?; + + if received_signatures.next().is_some() { + return Err("Received more than one signature!".into()); + } + + if !is_valid_signature(&config_state.secret, &received_signature, &body).await { + return Err("Invalid signature!".into()); + } + + let json: Value = + serde_json::from_slice(&body).context("Can not parse the request body into JSON!")?; + + let repo = json + .get("repository") + .context("Can not get the repository value from the request body!")?; + + let clone_url = repo + .get("clone_url") + .context("Can not get value clone_url from repository in the request body!")?; + + let clone_url = clone_url + .as_str() + .context("The value of clone_url from repository in the request body is not a string!")?; + + let hook = config_state.get_hook(clone_url).with_context(|| { + format!("No matching repository with url {clone_url} in the configuration file.") + })?; + + let hook_log_id = db::add_hook_log(&db_state.pool, hook)?.id; { // Spawn and detach a thread that runs the command and fills the output in the log. @@ -64,8 +97,11 @@ pub fn trigger( let args = hook.args.clone(); let current_dir = hook.current_dir.clone(); let db_pool = db_state.pool.clone(); + let clone_url = clone_url.to_string(); thread::spawn(move || { + info!("Running webhook for Repo: {clone_url}"); + let stdout: Vec; let stderr: Vec; let status_code: Option; @@ -93,5 +129,5 @@ pub fn trigger( }); } - Ok(format!("{}/?id={}", config_state.base_url, hook_log_id)) + Ok(format!("{}/?id={}", config_state.base_url, hook_log_id).into_response()) } diff --git a/src/states.rs b/src/states.rs index fba152e..1427e5a 100644 --- a/src/states.rs +++ b/src/states.rs @@ -1,7 +1,8 @@ -use crate::config; -use crate::db; - use anyhow::Result; +use axum::extract::FromRef; +use std::sync::Arc; + +use crate::{config, db, mailer}; pub struct DB { pub pool: db::DBPool, @@ -21,16 +22,35 @@ pub struct Config { pub hooks: Vec, } -impl Config { - pub fn new(config: config::Config) -> Self { +impl From for Config { + fn from(config: config::Config) -> Self { Self { secret: config.secret.as_bytes().to_owned(), base_url: config.base_url, hooks: config.hooks, } } +} +impl Config { pub fn get_hook(&self, clone_url: &str) -> Option<&config::Hook> { self.hooks.iter().find(|&hook| hook.repo_url == clone_url) } } + +#[derive(Clone, FromRef)] +pub struct AppState { + pub config: Arc, + pub mailer: Arc, + pub db: Arc, +} + +impl AppState { + pub fn new(config: config::Config, mailer: mailer::Mailer) -> Result { + Ok(Self { + config: Arc::new(Config::from(config)), + mailer: Arc::new(mailer), + db: Arc::new(DB::new()?), + }) + } +} diff --git a/src/templates.rs b/src/templates.rs new file mode 100644 index 0000000..1ac77e1 --- /dev/null +++ b/src/templates.rs @@ -0,0 +1,31 @@ +use askama::Template; + +use crate::models; + +#[derive(Template)] +#[template(path = "hook_log.txt")] +pub struct HookLog { + pub id: i32, + pub datetime: String, + pub repo_url: String, + pub command_with_args: String, + pub current_dir: String, + pub stdout: String, + pub stderr: String, + pub status_code: i32, +} + +impl From for HookLog { + fn from(hook_log: models::HookLog) -> Self { + Self { + id: hook_log.id, + datetime: hook_log.datetime, + repo_url: hook_log.repo_url, + command_with_args: hook_log.command_with_args, + current_dir: hook_log.current_dir, + stdout: hook_log.stdout.unwrap_or_default(), + stderr: hook_log.stderr.unwrap_or_default(), + status_code: hook_log.status_code.unwrap_or_default(), + } + } +}