Basic unsubscribe endpoint
This commit is contained in:
@@ -5,6 +5,7 @@ mod login;
|
||||
mod posts;
|
||||
mod subscriptions;
|
||||
mod subscriptions_confirm;
|
||||
mod unsubscribe;
|
||||
|
||||
pub use admin::*;
|
||||
use askama::Template;
|
||||
@@ -16,15 +17,38 @@ pub use health_check::*;
|
||||
pub use home::*;
|
||||
pub use login::*;
|
||||
pub use posts::*;
|
||||
use rand::{Rng, distr::Alphanumeric};
|
||||
use reqwest::StatusCode;
|
||||
pub use subscriptions::*;
|
||||
pub use subscriptions_confirm::*;
|
||||
pub use unsubscribe::*;
|
||||
|
||||
use crate::{
|
||||
authentication::AuthError,
|
||||
templates::{InternalErrorTemplate, MessageTemplate, NotFoundTemplate},
|
||||
};
|
||||
|
||||
pub fn generate_token() -> String {
|
||||
let mut rng = rand::rng();
|
||||
std::iter::repeat_with(|| rng.sample(Alphanumeric))
|
||||
.map(char::from)
|
||||
.take(25)
|
||||
.collect()
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error)]
|
||||
pub enum AppError {
|
||||
#[error("An unexpected error was encountered.")]
|
||||
|
||||
@@ -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.")?;
|
||||
|
||||
@@ -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
36
src/routes/unsubscribe.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
@@ -127,6 +127,7 @@ pub fn app(
|
||||
.route("/health_check", get(health_check))
|
||||
.route("/subscriptions", post(subscribe))
|
||||
.route("/subscriptions/confirm", get(confirm))
|
||||
.route("/unsubscribe", get(unsubscribe))
|
||||
.route("/posts", get(list_posts))
|
||||
.route("/posts/{post_id}", get(see_post))
|
||||
.nest("/admin", admin_routes)
|
||||
|
||||
@@ -50,6 +50,10 @@ pub struct ConfirmTemplate;
|
||||
#[template(path = "../templates/404.html")]
|
||||
pub struct NotFoundTemplate;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "../templates/unsubscribe.html")]
|
||||
pub struct UnsubscribeTemplate;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "../templates/email/new_post.html")]
|
||||
pub struct NewPostEmailTemplate<'a> {
|
||||
|
||||
Reference in New Issue
Block a user