Basic unsubscribe endpoint

This commit is contained in:
Alphonse Paix
2025-09-21 17:49:31 +02:00
parent 0725b87bf2
commit 829f3e4e4f
17 changed files with 292 additions and 43 deletions

View File

@@ -1,7 +1,7 @@
use crate::{
domain::{NewSubscriber, SubscriberEmail},
email_client::EmailClient,
routes::AppError,
routes::{AppError, generate_token},
startup::AppState,
templates::MessageTemplate,
};
@@ -13,35 +13,10 @@ use axum::{
response::{Html, IntoResponse, Response},
};
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()
}
pub fn error_chain_fmt(
e: &impl std::error::Error,
f: &mut std::fmt::Formatter,
) -> std::fmt::Result {
writeln!(f, "{}", e)?;
let mut current = e.source();
while let Some(cause) = current {
write!(f, "Caused by:\n\t{}", cause)?;
current = cause.source();
if current.is_some() {
writeln!(f)?;
}
}
Ok(())
}
#[tracing::instrument(
name = "Adding a new subscriber",
skip(connection_pool, email_client, base_url, form),
@@ -69,7 +44,7 @@ pub async fn subscribe(
let subscriber_id = insert_subscriber(&mut transaction, &new_subscriber)
.await
.context("Failed to insert new subscriber in the database.")?;
let subscription_token = generate_subscription_token();
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.")?;

View File

@@ -1,4 +1,4 @@
use crate::{startup::AppState, templates::ConfirmTemplate};
use crate::{routes::generate_token, startup::AppState, templates::ConfirmTemplate};
use askama::Template;
use axum::{
extract::{Query, State},
@@ -44,7 +44,8 @@ async fn confirm_subscriber(
subscriber_id: &Uuid,
) -> Result<(), sqlx::Error> {
sqlx::query!(
"UPDATE subscriptions SET status = 'confirmed' WHERE id = $1",
"UPDATE subscriptions SET status = 'confirmed', unsubscribe_token = $1 WHERE id = $2",
generate_token(),
subscriber_id
)
.execute(connection_pool)

36
src/routes/unsubscribe.rs Normal file
View File

@@ -0,0 +1,36 @@
use crate::{routes::AppError, startup::AppState, templates::UnsubscribeTemplate};
use anyhow::Context;
use askama::Template;
use axum::{
extract::{Query, State},
response::{Html, IntoResponse, Response},
};
use reqwest::StatusCode;
use sqlx::Executor;
#[derive(serde::Deserialize)]
pub struct UnsubQueryParams {
token: String,
}
pub async fn unsubscribe(
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 {
Ok(StatusCode::NOT_FOUND.into_response())
} else {
Ok(Html(UnsubscribeTemplate.render().unwrap()).into_response())
}
}