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, pub base_url: String, } pub struct Application { listener: TcpListener, router: Router, } impl Application { pub async fn build(configuration: Settings) -> Result { 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, ) -> 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( "/posts/{post_id}/comments", post(post_comment).get(get_comments), ) .route("/users/{username}", get(user_profile)) .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::() .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() } } }