153 lines
5.1 KiB
Rust
153 lines
5.1 KiB
Rust
use crate::{configuration::Settings, email_client::EmailClient, routes::require_auth, routes::*};
|
|
use anyhow::Context;
|
|
use axum::{
|
|
Router,
|
|
body::Bytes,
|
|
extract::MatchedPath,
|
|
http::Request,
|
|
middleware,
|
|
response::{IntoResponse, Response},
|
|
routing::{delete, get, post},
|
|
};
|
|
use reqwest::{StatusCode, header};
|
|
use sqlx::{PgPool, postgres::PgPoolOptions};
|
|
use std::{sync::Arc, time::Duration};
|
|
use tokio::net::TcpListener;
|
|
use tower_http::{services::ServeDir, trace::TraceLayer};
|
|
use tower_sessions::SessionManagerLayer;
|
|
use tower_sessions_redis_store::{RedisStore, fred::prelude::Pool};
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Clone)]
|
|
pub struct AppState {
|
|
pub connection_pool: PgPool,
|
|
pub email_client: Arc<EmailClient>,
|
|
pub base_url: String,
|
|
}
|
|
|
|
pub struct Application {
|
|
listener: TcpListener,
|
|
router: Router,
|
|
}
|
|
|
|
impl Application {
|
|
pub async fn build(configuration: Settings) -> Result<Self, anyhow::Error> {
|
|
let address = format!(
|
|
"{}:{}",
|
|
configuration.application.host, configuration.application.port
|
|
);
|
|
let connection_pool = PgPoolOptions::new()
|
|
.acquire_timeout(Duration::from_millis(
|
|
configuration.database.timeout_milliseconds,
|
|
))
|
|
.connect_lazy_with(configuration.database.with_db());
|
|
let email_client = EmailClient::build(configuration.email_client).unwrap();
|
|
let redis_store = configuration
|
|
.kv_store
|
|
.session_store()
|
|
.await
|
|
.context("Failed to acquire Redis session store.")?;
|
|
let router = app(
|
|
connection_pool,
|
|
email_client,
|
|
configuration.application.base_url,
|
|
redis_store,
|
|
);
|
|
let listener = TcpListener::bind(address)
|
|
.await
|
|
.context("Could not bind application address.")?;
|
|
Ok(Self { listener, router })
|
|
}
|
|
|
|
pub async fn run_until_stopped(self) -> Result<(), std::io::Error> {
|
|
tracing::debug!("listening on {}", self.local_addr());
|
|
axum::serve(self.listener, self.router).await
|
|
}
|
|
|
|
pub fn local_addr(&self) -> String {
|
|
self.listener.local_addr().unwrap().to_string()
|
|
}
|
|
|
|
pub fn port(&self) -> u16 {
|
|
self.listener.local_addr().unwrap().port()
|
|
}
|
|
}
|
|
|
|
pub fn app(
|
|
connection_pool: PgPool,
|
|
email_client: EmailClient,
|
|
base_url: String,
|
|
redis_store: RedisStore<Pool>,
|
|
) -> Router {
|
|
let app_state = AppState {
|
|
connection_pool,
|
|
email_client: Arc::new(email_client),
|
|
base_url,
|
|
};
|
|
let admin_routes = Router::new()
|
|
.route("/subscribers", get(get_subscribers_page))
|
|
.route("/subscribers/{subscriber_id}", delete(delete_subscriber))
|
|
.route("/posts/{post_id}", delete(delete_post))
|
|
.layer(middleware::from_fn(require_admin));
|
|
let auth_routes = Router::new()
|
|
.route("/dashboard", get(admin_dashboard))
|
|
.route("/password", post(change_password))
|
|
.route("/newsletters", post(publish_newsletter))
|
|
.route("/posts", post(create_post))
|
|
.route("/logout", get(logout))
|
|
.merge(admin_routes)
|
|
.layer(middleware::from_fn(require_auth));
|
|
Router::new()
|
|
.route("/", get(home))
|
|
.route("/login", get(get_login).post(post_login))
|
|
.route("/health_check", get(health_check))
|
|
.route("/subscriptions", post(subscribe))
|
|
.route("/subscriptions/confirm", get(confirm))
|
|
.route("/unsubscribe", get(get_unsubscribe).post(post_unsubscribe))
|
|
.route("/unsubscribe/confirm", get(unsubscribe_confirm))
|
|
.route("/posts", get(list_posts))
|
|
.route("/posts/load_more", get(load_more))
|
|
.route("/posts/{post_id}", get(see_post))
|
|
.route("/favicon.ico", get(favicon))
|
|
.nest("/admin", auth_routes)
|
|
.nest_service("/assets", ServeDir::new("assets"))
|
|
.layer(
|
|
TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {
|
|
let matched_path = request
|
|
.extensions()
|
|
.get::<MatchedPath>()
|
|
.map(MatchedPath::as_str);
|
|
let request_id = Uuid::new_v4().to_string();
|
|
|
|
tracing::info_span!(
|
|
"http_request",
|
|
method = ?request.method(),
|
|
matched_path,
|
|
request_id,
|
|
)
|
|
}),
|
|
)
|
|
.layer(SessionManagerLayer::new(redis_store).with_secure(false))
|
|
.fallback(not_found)
|
|
.with_state(app_state)
|
|
}
|
|
|
|
pub async fn favicon() -> Response {
|
|
match tokio::fs::read("assets/favicon.png").await {
|
|
Ok(content) => {
|
|
let bytes = Bytes::from(content);
|
|
let body = bytes.into();
|
|
Response::builder()
|
|
.status(StatusCode::OK)
|
|
.header(header::CONTENT_TYPE, "image/x-icon")
|
|
.header(header::CACHE_CONTROL, "public, max-age=86400")
|
|
.body(body)
|
|
.unwrap()
|
|
}
|
|
Err(e) => {
|
|
tracing::error!(error = %e, "Error while reading favicon.ico.");
|
|
StatusCode::NOT_FOUND.into_response()
|
|
}
|
|
}
|
|
}
|