Files
zero2prod/src/routes/unsubscribe.rs
Alphonse Paix 1117d49746
Some checks failed
Rust / Test (push) Has been cancelled
Rust / Rustfmt (push) Has been cancelled
Rust / Clippy (push) Has been cancelled
Rust / Code coverage (push) Has been cancelled
Update telemetry
2025-09-28 03:37:23 +02:00

137 lines
4.1 KiB
Rust

use crate::{
domain::SubscriberEmail,
email_client::EmailClient,
routes::AppError,
startup::AppState,
templates::{
MessageTemplate, NotFoundTemplate, UnsubscribeConfirmTemplate, UnsubscribeTemplate,
},
};
use anyhow::Context;
use askama::Template;
use axum::{
Form,
extract::{Query, State},
response::{Html, IntoResponse, Response},
};
use reqwest::StatusCode;
use sqlx::{Executor, PgPool};
#[derive(serde::Deserialize)]
pub struct UnsubQueryParams {
token: String,
}
pub async fn get_unsubscribe() -> Response {
Html(UnsubscribeTemplate.render().unwrap()).into_response()
}
#[derive(serde::Deserialize)]
pub struct UnsubFormData {
email: String,
}
#[tracing::instrument(
name = "Removing subscriber from database",
skip(connection_pool, email_client, base_url)
)]
pub async fn post_unsubscribe(
State(AppState {
connection_pool,
email_client,
base_url,
}): State<AppState>,
Form(UnsubFormData { email }): Form<UnsubFormData>,
) -> Result<Response, AppError> {
let subscriber_email = SubscriberEmail::parse(email)?;
if let Some(token) = fetch_unsubscribe_token(&connection_pool, &subscriber_email)
.await
.context("Could not fetch unsubscribe token.")?
{
send_unsubscribe_email(&email_client, &subscriber_email, &base_url, &token)
.await
.context("Failed to send a confirmation email.")?;
}
let template = MessageTemplate::Success {
message: "If you are a subscriber, you'll receive a confirmation link shortly.".into(),
};
Ok(Html(template.render().unwrap()).into_response())
}
#[tracing::instrument(name = "Fetching unsubscribe token from database", skip_all)]
async fn fetch_unsubscribe_token(
connection_pool: &PgPool,
subscriber_email: &SubscriberEmail,
) -> Result<Option<String>, sqlx::Error> {
let r = sqlx::query!(
"SELECT unsubscribe_token FROM subscriptions WHERE email = $1",
subscriber_email.as_ref()
)
.fetch_optional(connection_pool)
.await?;
Ok(r.and_then(|r| r.unsubscribe_token))
}
#[tracing::instrument(name = "Send an confirmation email", skip_all)]
pub async fn send_unsubscribe_email(
email_client: &EmailClient,
subscriber_email: &SubscriberEmail,
base_url: &str,
unsubscribe_token: &str,
) -> Result<(), reqwest::Error> {
let confirmation_link = format!(
"{}/unsubscribe/confirm?token={}",
base_url, unsubscribe_token
);
let html_content = format!(
r#"You've requested to unsubscribe from the newsletter. To confirm, please click the link below:<br />
<a href="{}">Confirm unsubscribe</a><br />
If you did not request this, you can safely ignore this email."#,
confirmation_link
);
let text_content = format!(
r#"You've requested to unsubscribe from the newsletter. To confirm, please follow the link below:
{}
If you did not request this, you can safely ignore this email."#,
confirmation_link
);
email_client
.send_email(
subscriber_email,
"I will miss you",
&html_content,
&text_content,
)
.await
}
#[tracing::instrument(name = "Removing user from database", skip(connection_pool))]
pub async fn unsubscribe_confirm(
Query(UnsubQueryParams { token }): Query<UnsubQueryParams>,
State(AppState {
connection_pool, ..
}): State<AppState>,
) -> Result<Response, AppError> {
let query = sqlx::query!(
"DELETE FROM subscriptions WHERE unsubscribe_token = $1",
token
);
let result = connection_pool
.execute(query)
.await
.context("Could not update subscriptions table.")?;
if result.rows_affected() == 0 {
tracing::info!("Unsubscribe token is not tied to any confirmed user");
Ok((
StatusCode::NOT_FOUND,
Html(NotFoundTemplate.render().unwrap()),
)
.into_response())
} else {
tracing::info!("User successfully removed");
Ok(Html(UnsubscribeConfirmTemplate.render().unwrap()).into_response())
}
}