use crate::{ domain::{NewSubscriber, SubscriberEmail}, email_client::EmailClient, routes::{AppError, generate_token}, startup::AppState, templates::MessageTemplate, }; use anyhow::Context; use askama::Template; use axum::{ Form, extract::State, response::{Html, IntoResponse, Response}, }; use chrono::Utc; use serde::Deserialize; use sqlx::{Executor, Postgres, Transaction}; use uuid::Uuid; #[tracing::instrument( name = "Adding a new subscriber", skip(connection_pool, email_client, base_url, form), fields( subscriber_email = %form.email, ) )] pub async fn subscribe( State(AppState { connection_pool, email_client, base_url, .. }): State, Form(form): Form, ) -> Result { let new_subscriber = form .try_into() .context("Failed to parse subscription form data.") .map_err(AppError::FormError)?; let mut transaction = connection_pool .begin() .await .context("Failed to acquire a Postgres connection from the pool.")?; if let Some(subscriber_id) = insert_subscriber(&mut transaction, &new_subscriber) .await .context("Failed to insert new subscriber in the database.") .map_err(AppError::unexpected_message)? { let subscription_token = generate_token(); store_token(&mut transaction, &subscription_token, &subscriber_id) .await .context("Failed to store the confirmation token for a new subscriber.") .map_err(AppError::unexpected_message)?; send_confirmation_email( &email_client, &new_subscriber, &base_url, &subscription_token, ) .await .context("Failed to send a confirmation email.")?; transaction .commit() .await .context("Failed to commit the database transaction to store a new subscriber.")?; } let template = MessageTemplate::Success { message: "You'll receive a confirmation email shortly.".to_string(), }; Ok(Html(template.render().unwrap()).into_response()) } #[tracing::instrument( name = "Saving new subscriber details in the database", skip(transaction, new_subscriber) )] pub async fn insert_subscriber( transaction: &mut Transaction<'_, Postgres>, new_subscriber: &NewSubscriber, ) -> Result, sqlx::Error> { let query = sqlx::query!( "SELECT id FROM subscriptions WHERE email = $1", new_subscriber.email.as_ref() ); let existing = transaction.fetch_optional(query).await?; if existing.is_some() { return Ok(None); } let subscriber_id = Uuid::new_v4(); let query = sqlx::query!( r#" INSERT INTO subscriptions (id, email, subscribed_at, status) VALUES ($1, $2, $3, 'pending_confirmation') "#, subscriber_id, new_subscriber.email.as_ref(), Utc::now() ); transaction.execute(query).await?; Ok(Some(subscriber_id)) } #[tracing::instrument( name = "Store subscription token in the database", skip(transaction, subscription_token) )] async fn store_token( transaction: &mut Transaction<'_, Postgres>, subscription_token: &str, subscriber_id: &Uuid, ) -> Result<(), sqlx::Error> { let query = sqlx::query!( r#" INSERT INTO subscription_tokens (subscription_token, subscriber_id) VALUES ($1, $2) "#, subscription_token, subscriber_id, ); transaction.execute(query).await?; Ok(()) } #[tracing::instrument( name = "Send a confirmation email to a new subscriber", skip(email_client, new_subscriber, base_url, subscription_token) )] pub async fn send_confirmation_email( email_client: &EmailClient, new_subscriber: &NewSubscriber, base_url: &str, subscription_token: &str, ) -> Result<(), reqwest::Error> { let confirmation_link = format!( "{}/subscriptions/confirm?subscription_token={}", base_url, subscription_token ); let html_content = format!( "Welcome to our newsletter!
\ Click here to confirm your subscription.", confirmation_link ); let text_content = format!( "Welcome to our newsletter!\nVisit {} to confirm your subscription.", confirmation_link ); email_client .send_email( &new_subscriber.email, "Welcome!", &html_content, &text_content, ) .await } #[derive(Debug, Deserialize)] pub struct SubscriptionFormData { email: String, } impl TryFrom for NewSubscriber { type Error = anyhow::Error; fn try_from(value: SubscriptionFormData) -> Result { let email = SubscriberEmail::parse(value.email)?; Ok(Self { email }) } }