use crate::domain::SubscriberEmail; use crate::domain::{NewSubscriber, SubscriberName}; use crate::email_client::EmailClient; use crate::startup::AppState; use axum::extract::State; use axum::routing::post; use axum::Form; use axum::Router; use axum::{ http::StatusCode, response::{IntoResponse, Response}, }; use chrono::Utc; use serde::Deserialize; use sqlx::PgPool; use uuid::Uuid; #[derive(Debug, Deserialize)] struct FormData { email: String, name: String, } impl TryFrom for NewSubscriber { type Error = String; fn try_from(value: FormData) -> Result { let name = SubscriberName::parse(value.name)?; let email = SubscriberEmail::parse(value.email)?; Ok(Self { email, name }) } } #[tracing::instrument( name = "Send a confirmation email to a new subscriber", skip(email_client, new_subscriber, base_url) )] pub async fn send_confirmation_email( email_client: &EmailClient, new_subscriber: NewSubscriber, base_url: &str, ) -> Result<(), reqwest::Error> { let confirmation_link = format!( "{}/subscriptions/confirm?subscription_token=mytoken", base_url ); let plain_body = format!( "Welcome to our newsletter! Visit {} to confirm your subscription.", confirmation_link ); let html_body = format!( "Welcome to our newsletter!
\ Click here to confirm your subscription.", confirmation_link ); email_client .send_email(new_subscriber.email, "Welcome!", &html_body, &plain_body) .await } // TODO: remove request_id? #[tracing::instrument( name = "Adding a new subscriber", skip(form, db_pool, email_client, base_url), fields( request_id = %Uuid::new_v4(), subscriber_email = %form.email, subscriber_name = %form.name ) )] pub async fn subscribe( State(AppState { db_pool, email_client, base_url, }): State, Form(form): Form, ) -> Response { let new_subscriber = match form.try_into() { Ok(form) => form, Err(_) => { return (StatusCode::BAD_REQUEST, "Invalid subscription.").into_response(); } }; if insert_subscriber(&db_pool, &new_subscriber).await.is_err() { return (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong.").into_response(); } if send_confirmation_email(&email_client, new_subscriber, &base_url.0) .await .is_err() { return (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong.").into_response(); } return (StatusCode::OK,).into_response(); } #[tracing::instrument( name = "Saving new subscriber details in the database", skip(new_subscriber, db_pool) )] pub async fn insert_subscriber( db_pool: &PgPool, new_subscriber: &NewSubscriber, ) -> Result<(), sqlx::Error> { let _ = sqlx::query!( r#" INSERT INTO subscriptions (id, email, name, subscribed_at, status) VALUES ($1, $2, $3, $4, 'pending_confirmation') "#, Uuid::new_v4(), new_subscriber.email.as_ref(), new_subscriber.name.as_ref(), Utc::now() ) .execute(db_pool) .await .map_err(|e| { tracing::error!("Failed to execute query: {:?}", e); e }); Ok(()) } pub fn routes_subscriptions() -> Router { Router::new().route("/subscriptions", post(subscribe)) }