aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorCara Salter <cara@devcara.com>2022-07-19 12:49:28 -0400
committerCara Salter <cara@devcara.com>2022-07-19 12:49:28 -0400
commitaadc1975d459f0e2e2807dbb3e2a36098476128b (patch)
tree231efcabf21f95e46ae5ffdedadf55bf3ccea66b /src
downloadnccd-aadc1975d459f0e2e2807dbb3e2a36098476128b.tar.gz
nccd-aadc1975d459f0e2e2807dbb3e2a36098476128b.zip
Initial commit
Diffstat (limited to 'src')
-rw-r--r--src/build.rs10
-rw-r--r--src/config.rs40
-rw-r--r--src/errors.rs35
-rw-r--r--src/main.rs124
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"));