Files
zero2prod/src/routes/admin/newsletters.rs
Alphonse Paix de44564ba0
All checks were successful
Rust / Test (push) Successful in 5m34s
Rust / Rustfmt (push) Successful in 22s
Rust / Clippy (push) Successful in 1m13s
Rust / Code coverage (push) Successful in 3m33s
Templates and TLS requests
Refactored HTML templates and added TLS back to issue HTTP requests
2025-09-29 02:39:53 +02:00

147 lines
4.4 KiB
Rust

use crate::{
authentication::AuthenticatedUser,
idempotency::{IdempotencyKey, save_response, try_processing},
routes::{AdminError, AppError},
startup::AppState,
templates::{EmailTemplate, MessageTemplate, StandaloneEmailTemplate},
};
use anyhow::Context;
use askama::Template;
use axum::{
Extension, Form,
extract::State,
response::{Html, IntoResponse, Response},
};
use sqlx::{Executor, Postgres, Transaction};
use std::fmt::Display;
use uuid::Uuid;
#[derive(serde::Deserialize)]
pub struct BodyData {
title: String,
html: String,
text: String,
idempotency_key: String,
}
#[tracing::instrument(name = "Creating newsletter isue", skip_all, fields(issue_id = tracing::field::Empty))]
pub async fn insert_newsletter_issue(
transaction: &mut Transaction<'static, Postgres>,
title: &str,
email_template: &dyn EmailTemplate,
) -> Result<Uuid, sqlx::Error> {
let newsletter_issue_id = Uuid::new_v4();
tracing::Span::current().record("issue_id", newsletter_issue_id.to_string());
let query = sqlx::query!(
r#"
INSERT INTO newsletter_issues (
newsletter_issue_id, title, text_content, html_content, published_at
)
VALUES ($1, $2, $3, $4, now())
"#,
newsletter_issue_id,
title,
email_template.text(),
email_template.html(),
);
transaction.execute(query).await?;
Ok(newsletter_issue_id)
}
#[derive(Debug)]
pub enum EmailType {
NewPost,
Newsletter,
}
impl Display for EmailType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EmailType::NewPost => write!(f, "new_post"),
EmailType::Newsletter => write!(f, "newsletter"),
}
}
}
#[tracing::instrument(name = "Adding new task to queue", skip(transaction))]
pub async fn enqueue_delivery_tasks(
transaction: &mut Transaction<'static, Postgres>,
newsletter_issue_id: Uuid,
kind: EmailType,
) -> Result<(), sqlx::Error> {
let query = sqlx::query!(
r#"
INSERT INTO issue_delivery_queue (
newsletter_issue_id,
subscriber_email,
unsubscribe_token,
kind
)
SELECT $1, email, unsubscribe_token, $2
FROM subscriptions
WHERE status = 'confirmed'
"#,
newsletter_issue_id,
kind.to_string()
);
transaction.execute(query).await?;
Ok(())
}
#[tracing::instrument(name = "Publishing a newsletter", skip_all, fields(title = %form.title))]
pub async fn publish_newsletter(
State(AppState {
connection_pool,
base_url,
..
}): State<AppState>,
Extension(AuthenticatedUser { user_id, .. }): Extension<AuthenticatedUser>,
Form(form): Form<BodyData>,
) -> Result<Response, AppError> {
validate_form(&form).map_err(|e| AdminError::Publish(anyhow::anyhow!(e)))?;
let idempotency_key: IdempotencyKey = form
.idempotency_key
.try_into()
.map_err(AdminError::Idempotency)?;
let mut transaction = match try_processing(&connection_pool, &idempotency_key, user_id).await? {
crate::idempotency::NextAction::StartProcessing(t) => t,
crate::idempotency::NextAction::ReturnSavedResponse(response) => {
return Ok(response);
}
};
let email_template = StandaloneEmailTemplate {
base_url: &base_url,
text_content: &form.text,
html_content: &form.html,
};
let issue_id = insert_newsletter_issue(&mut transaction, &form.title, &email_template)
.await
.context("Failed to store newsletter issue details.")?;
enqueue_delivery_tasks(&mut transaction, issue_id, EmailType::Newsletter)
.await
.context("Failed to enqueue delivery tasks.")?;
let message = String::from("Your email has been queued for delivery.");
let template = MessageTemplate::success(message);
let response = Html(template.render().unwrap()).into_response();
let response = save_response(transaction, &idempotency_key, user_id, response)
.await
.map_err(AdminError::UnexpectedError)?;
Ok(response)
}
fn validate_form(form: &BodyData) -> Result<(), anyhow::Error> {
if form.title.is_empty() {
anyhow::bail!("The title was empty.");
}
if form.html.is_empty() || form.text.is_empty() {
anyhow::bail!("The content was empty.");
}
Ok(())
}