diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/build.rs | 10 | ||||
-rw-r--r-- | src/config.rs | 40 | ||||
-rw-r--r-- | src/errors.rs | 35 | ||||
-rw-r--r-- | src/main.rs | 124 |
4 files changed, 209 insertions, 0 deletions
diff --git a/src/build.rs b/src/build.rs new file mode 100644 index 0000000..d7d6882 --- /dev/null +++ b/src/build.rs @@ -0,0 +1,10 @@ +use ructe::{Ructe, RucteError}; + +fn main() -> Result<(), RucteError> { + let mut ructe = Ructe::from_env()?; + let mut statics = ructe.statics()?; + statics.add_files("statics")?; + statics.add_sass_file("scss/style.scss")?; + + ructe.compile_templates("templates") +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..9bacc6a --- /dev/null +++ b/src/config.rs @@ -0,0 +1,40 @@ +use serde::Deserialize; +use tracing::error; + +use crate::errors::ServiceError; +use std::fs; +use std::str; + +#[derive(Deserialize)] +pub struct Config { + pub server: ServerConfig, + pub database: DbConfig, +} + +#[derive(Deserialize)] +pub struct ServerConfig { + pub bind_addr: String, +} + +#[derive(Deserialize, Clone)] +pub struct DbConfig { + pub postgres_url: String, + pub max_connections: u32, +} + +impl Config { + pub fn init(path: String) -> Result<Config, ServiceError> { + if let Ok(c) = fs::read(path) { + if c.len() == 0 { + error!("Config file empty."); + return Err(ServiceError::MissingConfig); + } else { + let config: Config = toml::from_str(str::from_utf8(&c).unwrap())?; + return Ok(config); + } + } else { + error!("Unable to read from config file."); + return Err(ServiceError::MissingConfig); + } + } +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..19e0a8a --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,35 @@ +use axum::body; +use axum::response::{IntoResponse, Response, Html}; +use hyper::StatusCode; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ServiceError { + #[error("No config file")] + MissingConfig, + #[error("Invalid config file: {0}")] + InvalidConfig(#[from] toml::de::Error), + #[error("Not Authorized")] + NotAuthorized, + #[error("Not Found")] + NotFound, + #[error("Axum: {0}")] + Axum(#[from] axum::Error) +} + +pub type StringResult<T = String> = Result<T, ServiceError>; +pub type HtmlResult<T = Html<String>> = Result<T, ServiceError>; +pub type JsonResult<T> = Result<T, ServiceError>; + +impl IntoResponse for ServiceError { + fn into_response(self) -> axum::response::Response { + let body = body::boxed(body::Full::from(self.to_string())); + + let status = match self { + ServiceError::NotFound => StatusCode::NOT_FOUND, + ServiceError::NotAuthorized => StatusCode::UNAUTHORIZED, + _ => StatusCode::INTERNAL_SERVER_ERROR, + }; + Response::builder().status(status).body(body).unwrap() + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c4a298f --- /dev/null +++ b/src/main.rs @@ -0,0 +1,124 @@ +mod config; +mod errors; + +use std::{net::SocketAddr, str::FromStr, sync::Arc, time::Duration}; + +use axum::body; +use axum::extract::Path; +use axum::{error_handling::HandleErrorLayer, routing::get, BoxError, Extension, Router}; +use axum::response::{Html, IntoResponse, Response}; +use errors::{StringResult, HtmlResult}; +use hyper::StatusCode; +use sqlx::{PgPool, postgres::PgPoolOptions}; +use tower::ServiceBuilder; +use tower_http::trace::TraceLayer; +use tracing::{error, info, debug}; +use crate::errors::ServiceError; +use tracing_subscriber::prelude::*; + +pub struct State { + pub config: config::Config, + pub conn: PgPool +} + +#[tokio::main] +async fn main() { + color_eyre::install().unwrap(); + tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::new("debug")) + .with(tracing_subscriber::fmt::layer()) + .init(); + let config = match config::Config::init("/etc/nccd/config.toml".to_owned()) { + Ok(c) => c, + Err(e) => { + error!("Config Error: {:?}", e); + std::process::exit(1); + } + }; + + let bind_addr = config.server.bind_addr.clone(); + + let db_config = config.database.clone(); + + let conn = PgPoolOptions::new() + .max_connections(db_config.max_connections) + .connect(&db_config.postgres_url) + .await.unwrap(); + + let shared_state = Arc::new(State { config, conn }); + + let app = Router::new() + .route("/health", get(health_check)) + .route("/", get(index)) + .route("/static/:name", get(statics)) + .layer( + ServiceBuilder::new() + .layer(HandleErrorLayer::new(|error: BoxError| async move { + if error.is::<tower::timeout::error::Elapsed>() { + Ok(StatusCode::REQUEST_TIMEOUT) + } else { + Err(( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Unhandled internal error: {}", error), + )) + } + })) + .timeout(Duration::from_secs(10)) + .layer(TraceLayer::new_for_http()) + .into_inner(), + ) + .layer(Extension(shared_state)); + + let addr = match SocketAddr::from_str(&bind_addr) { + Ok(a) => a, + Err(e) => { + error!("Invalid bind addr: {:?}", e); + std::process::exit(1); + } + }; + + info!("Listening on {}", addr); + + axum::Server::bind(&addr) + .serve(app.into_make_service()) + .await + .unwrap(); +} + +async fn health_check() -> &'static str { + "OK" +} + +#[axum_macros::debug_handler] +async fn index() -> HtmlResult { + let mut buf = Vec::new(); + crate::templates::index_html(&mut buf).unwrap(); + + match String::from_utf8(buf) { + Ok(s) => Ok(Html(s)), + Err(_) => Err(ServiceError::NotFound), + } +} + +async fn statics(Path(name): Path<String>) -> Result<Response, ServiceError> { + for s in templates::statics::STATICS { + debug!("Name: {}\nContents:\n{:?}\n\n", s.name, s.content); + } + + match templates::statics::StaticFile::get(&name) { + Some(s) => match String::from_utf8(s.content.to_vec()) { + Ok(c) => { + let body = body::boxed(body::Full::from(c)); + + Ok(Response::builder() + .header("Content-Type", "text/css") + .status(StatusCode::OK) + .body(body).unwrap()) + }, + Err(_) => Err(ServiceError::NotFound), + }, + None => Err(ServiceError::NotFound), + } +} + +include!(concat!(env!("OUT_DIR"), "/templates.rs")); |