zero-to-production/src/routes/subscriptions.rs

208 lines
6 KiB
Rust
Raw Normal View History

2020-12-29 10:00:11 +00:00
use crate::domain::{NewSubscriber, SubscriberEmail, SubscriberName};
2021-03-08 21:41:34 +00:00
use crate::email_client::EmailClient;
2021-03-11 09:24:57 +00:00
use crate::startup::ApplicationBaseUrl;
2021-05-09 17:36:31 +00:00
use actix_web::http::StatusCode;
use actix_web::{web, HttpResponse, ResponseError};
2021-05-12 08:08:38 +00:00
use anyhow::Context;
use chrono::Utc;
2021-03-11 22:07:17 +00:00
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
2023-09-14 07:43:32 +00:00
use sqlx::{Executor, PgPool, Postgres, Transaction};
2021-09-01 07:22:31 +00:00
use std::convert::{TryFrom, TryInto};
use uuid::Uuid;
#[derive(serde::Deserialize)]
2020-10-18 13:22:22 +00:00
pub struct FormData {
email: String,
name: String,
}
2021-09-01 07:22:31 +00:00
impl TryFrom<FormData> for NewSubscriber {
2020-12-29 10:00:11 +00:00
type Error = String;
2021-09-01 07:22:31 +00:00
fn try_from(value: FormData) -> Result<Self, Self::Error> {
let name = SubscriberName::parse(value.name)?;
let email = SubscriberEmail::parse(value.email)?;
Ok(Self { email, name })
2020-12-29 10:00:11 +00:00
}
}
2021-05-10 08:57:49 +00:00
#[derive(thiserror::Error)]
2021-05-09 17:36:31 +00:00
pub enum SubscribeError {
2021-05-10 08:57:49 +00:00
#[error("{0}")]
2021-05-09 17:36:31 +00:00
ValidationError(String),
2021-05-12 08:08:38 +00:00
#[error(transparent)]
UnexpectedError(#[from] anyhow::Error),
2021-05-09 17:36:31 +00:00
}
impl std::fmt::Debug for SubscribeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
error_chain_fmt(self, f)
}
}
impl ResponseError for SubscribeError {
fn status_code(&self) -> StatusCode {
match self {
SubscribeError::ValidationError(_) => StatusCode::BAD_REQUEST,
2021-05-12 08:08:38 +00:00
SubscribeError::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR,
2021-05-09 17:36:31 +00:00
}
}
}
#[tracing::instrument(
name = "Adding a new subscriber",
2021-03-11 09:24:57 +00:00
skip(form, pool, email_client, base_url),
fields(
subscriber_email = %form.email,
subscriber_name = %form.name
)
)]
pub async fn subscribe(
2020-10-18 13:35:08 +00:00
form: web::Form<FormData>,
pool: web::Data<PgPool>,
2021-03-08 21:41:34 +00:00
email_client: web::Data<EmailClient>,
2021-03-11 09:24:57 +00:00
base_url: web::Data<ApplicationBaseUrl>,
2021-05-09 17:36:31 +00:00
) -> Result<HttpResponse, SubscribeError> {
2021-05-10 08:57:49 +00:00
let new_subscriber = form.0.try_into().map_err(SubscribeError::ValidationError)?;
2021-05-12 08:08:38 +00:00
let mut transaction = pool
.begin()
.await
.context("Failed to acquire a Postgres connection from the pool")?;
2021-03-13 10:08:54 +00:00
let subscriber_id = insert_subscriber(&mut transaction, &new_subscriber)
.await
2021-05-12 08:08:38 +00:00
.context("Failed to insert new subscriber in the database.")?;
2021-03-11 22:07:17 +00:00
let subscription_token = generate_subscription_token();
2021-05-12 08:08:38 +00:00
store_token(&mut transaction, subscriber_id, &subscription_token)
.await
.context("Failed to store the confirmation token for a new subscriber.")?;
2021-04-02 10:51:39 +00:00
transaction
.commit()
.await
2021-05-12 08:08:38 +00:00
.context("Failed to commit SQL transaction to store a new subscriber.")?;
2021-05-01 15:45:19 +00:00
send_confirmation_email(
2021-03-11 22:07:17 +00:00
&email_client,
new_subscriber,
&base_url.0,
&subscription_token,
)
2021-05-12 08:08:38 +00:00
.await
.context("Failed to send a confirmation email.")?;
Ok(HttpResponse::Ok().finish())
}
2021-03-11 22:07:17 +00:00
fn generate_subscription_token() -> String {
let mut rng = thread_rng();
std::iter::repeat_with(|| rng.sample(Alphanumeric))
.map(char::from)
.take(25)
.collect()
}
2021-03-08 22:54:17 +00:00
#[tracing::instrument(
name = "Send a confirmation email to a new subscriber",
2021-03-11 21:40:39 +00:00
skip(email_client, new_subscriber, base_url, subscription_token)
2021-03-08 22:54:17 +00:00
)]
2021-03-08 23:02:39 +00:00
pub async fn send_confirmation_email(
email_client: &EmailClient,
new_subscriber: NewSubscriber,
2021-03-11 09:24:57 +00:00
base_url: &str,
2021-03-11 22:07:17 +00:00
subscription_token: &str,
2021-03-08 23:02:39 +00:00
) -> Result<(), reqwest::Error> {
2021-03-11 22:07:17 +00:00
let confirmation_link = format!(
"{}/subscriptions/confirm?subscription_token={}",
base_url, subscription_token
);
2021-03-08 22:54:17 +00:00
let plain_body = format!(
"Welcome to our newsletter!\nVisit {} to confirm your subscription.",
confirmation_link
);
let html_body = format!(
"Welcome to our newsletter!<br />Click <a href=\"{}\">here</a> to confirm your subscription.",
confirmation_link
);
email_client
2021-08-01 13:57:02 +00:00
.send_email(&new_subscriber.email, "Welcome!", &html_body, &plain_body)
2021-03-08 22:54:17 +00:00
.await
}
#[tracing::instrument(
name = "Saving new subscriber details in the database",
2021-03-13 10:08:54 +00:00
skip(new_subscriber, transaction)
)]
2020-12-09 22:55:00 +00:00
pub async fn insert_subscriber(
2021-03-13 10:08:54 +00:00
transaction: &mut Transaction<'_, Postgres>,
2020-12-09 22:55:00 +00:00
new_subscriber: &NewSubscriber,
2021-03-11 23:11:38 +00:00
) -> Result<Uuid, sqlx::Error> {
let subscriber_id = Uuid::new_v4();
2023-09-14 07:43:32 +00:00
let query = sqlx::query!(
r#"
2021-03-07 18:53:45 +00:00
INSERT INTO subscriptions (id, email, name, subscribed_at, status)
2021-03-09 21:19:16 +00:00
VALUES ($1, $2, $3, $4, 'pending_confirmation')
"#,
2021-03-11 23:11:38 +00:00
subscriber_id,
2020-12-29 10:00:11 +00:00
new_subscriber.email.as_ref(),
2020-12-09 22:55:00 +00:00
new_subscriber.name.as_ref(),
Utc::now()
2023-09-14 07:43:32 +00:00
);
transaction.execute(query).await?;
2021-03-11 23:11:38 +00:00
Ok(subscriber_id)
}
#[tracing::instrument(
name = "Store subscription token in the database",
2021-03-13 10:08:54 +00:00
skip(subscription_token, transaction)
2021-03-11 23:11:38 +00:00
)]
pub async fn store_token(
2021-03-13 10:08:54 +00:00
transaction: &mut Transaction<'_, Postgres>,
2021-03-11 23:11:38 +00:00
subscriber_id: Uuid,
subscription_token: &str,
2021-05-09 17:36:31 +00:00
) -> Result<(), StoreTokenError> {
2023-09-14 07:43:32 +00:00
let query = sqlx::query!(
2021-03-11 23:11:38 +00:00
r#"
INSERT INTO subscription_tokens (subscription_token, subscriber_id)
VALUES ($1, $2)
"#,
subscription_token,
subscriber_id
2023-09-14 07:43:32 +00:00
);
transaction.execute(query).await.map_err(StoreTokenError)?;
Ok(())
}
2021-05-09 17:36:31 +00:00
pub struct StoreTokenError(sqlx::Error);
impl std::error::Error for StoreTokenError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.0)
}
}
impl std::fmt::Debug for StoreTokenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
error_chain_fmt(self, f)
}
}
impl std::fmt::Display for StoreTokenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"A database failure was encountered while trying to store a subscription token."
)
}
}
pub fn error_chain_fmt(
2021-05-09 17:36:31 +00:00
e: &impl std::error::Error,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
writeln!(f, "{}\n", e)?;
let mut current = e.source();
while let Some(cause) = current {
writeln!(f, "Caused by:\n\t{}", cause)?;
current = cause.source();
}
Ok(())
}