169 lines
4.6 KiB
Rust
169 lines
4.6 KiB
Rust
use crate::{
|
|
domain::{NewSubscriber, SubscriberEmail, SubscriberName},
|
|
email_client::EmailClient,
|
|
startup::AppState,
|
|
};
|
|
use axum::{Form, extract::State, http::StatusCode, response::IntoResponse};
|
|
use chrono::Utc;
|
|
use rand::{Rng, distr::Alphanumeric};
|
|
use serde::Deserialize;
|
|
use sqlx::{Executor, Postgres, Transaction};
|
|
use uuid::Uuid;
|
|
|
|
fn generate_subscription_token() -> String {
|
|
let mut rng = rand::rng();
|
|
std::iter::repeat_with(|| rng.sample(Alphanumeric))
|
|
.map(char::from)
|
|
.take(25)
|
|
.collect()
|
|
}
|
|
|
|
#[tracing::instrument(
|
|
name = "Adding a new subscriber",
|
|
skip(connection_pool, form, email_client),
|
|
fields(
|
|
subscriber_email = %form.email,
|
|
subscriber_name = %form.name
|
|
)
|
|
)]
|
|
pub async fn subscribe(
|
|
State(AppState {
|
|
connection_pool,
|
|
email_client,
|
|
base_url,
|
|
}): State<AppState>,
|
|
Form(form): Form<FormData>,
|
|
) -> impl IntoResponse {
|
|
let Ok(mut transaction) = connection_pool.begin().await else {
|
|
return StatusCode::INTERNAL_SERVER_ERROR;
|
|
};
|
|
let Ok(new_subscriber) = form.try_into() else {
|
|
return StatusCode::BAD_REQUEST;
|
|
};
|
|
let Ok(subscriber_id) = insert_subscriber(&mut transaction, &new_subscriber).await else {
|
|
return StatusCode::INTERNAL_SERVER_ERROR;
|
|
};
|
|
let subscription_token = generate_subscription_token();
|
|
if store_token(&mut transaction, &subscription_token, &subscriber_id)
|
|
.await
|
|
.is_err()
|
|
{
|
|
return StatusCode::INTERNAL_SERVER_ERROR;
|
|
}
|
|
if send_confirmation_email(
|
|
&email_client,
|
|
&new_subscriber,
|
|
&base_url,
|
|
&subscription_token,
|
|
)
|
|
.await
|
|
.is_err()
|
|
{
|
|
return StatusCode::INTERNAL_SERVER_ERROR;
|
|
}
|
|
if transaction.commit().await.is_err() {
|
|
return StatusCode::INTERNAL_SERVER_ERROR;
|
|
}
|
|
StatusCode::OK
|
|
}
|
|
|
|
#[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<Uuid, sqlx::Error> {
|
|
let subscriber_id = Uuid::new_v4();
|
|
let query = sqlx::query!(
|
|
r#"
|
|
INSERT INTO subscriptions (id, email, name, subscribed_at, status)
|
|
VALUES ($1, $2, $3, $4, 'pending_confirmation')
|
|
"#,
|
|
subscriber_id,
|
|
new_subscriber.email.as_ref(),
|
|
new_subscriber.name.as_ref(),
|
|
Utc::now()
|
|
);
|
|
transaction.execute(query).await.map_err(|e| {
|
|
tracing::error!("Failed to execute query: {:?}", e);
|
|
e
|
|
})?;
|
|
Ok(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.map_err(|e| {
|
|
tracing::error!("Failed to execute query: {:?}", e);
|
|
e
|
|
})?;
|
|
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!<br />\
|
|
Click <a href=\"{}\">here</a> 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)]
|
|
#[allow(dead_code)]
|
|
pub struct FormData {
|
|
name: String,
|
|
email: String,
|
|
}
|
|
|
|
impl TryFrom<FormData> for NewSubscriber {
|
|
type Error = String;
|
|
|
|
fn try_from(value: FormData) -> Result<Self, Self::Error> {
|
|
let name = SubscriberName::parse(value.name)?;
|
|
let email = SubscriberEmail::parse(value.email)?;
|
|
Ok(Self { name, email })
|
|
}
|
|
}
|