Admin can now write posts
Posts can be displayed on the website. Subscribers are automatically notified by email. This gives the opportunity to track explicitly how many people followed the link provided in the emails sent without being intrusive (no invisible image).
This commit is contained in:
@@ -48,7 +48,7 @@ pub async fn insert_newsletter_issue(
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn enqueue_delivery_tasks(
|
||||
pub async fn enqueue_delivery_tasks(
|
||||
transaction: &mut Transaction<'static, Postgres>,
|
||||
newsletter_issue_id: Uuid,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
@@ -76,9 +76,7 @@ pub async fn publish_newsletter(
|
||||
Extension(AuthenticatedUser { user_id, .. }): Extension<AuthenticatedUser>,
|
||||
Form(form): Form<BodyData>,
|
||||
) -> Result<Response, AdminError> {
|
||||
if let Err(e) = validate_form(&form) {
|
||||
return Err(AdminError::Publish(anyhow::anyhow!(e)));
|
||||
}
|
||||
validate_form(&form).map_err(|e| AdminError::Publish(anyhow::anyhow!(e)))?;
|
||||
|
||||
let idempotency_key: IdempotencyKey = form
|
||||
.idempotency_key
|
||||
@@ -94,11 +92,11 @@ pub async fn publish_newsletter(
|
||||
|
||||
let issue_id = insert_newsletter_issue(&mut transaction, &form.title, &form.text, &form.html)
|
||||
.await
|
||||
.context("Failed to store newsletter issue details")?;
|
||||
.context("Failed to store newsletter issue details.")?;
|
||||
|
||||
enqueue_delivery_tasks(&mut transaction, issue_id)
|
||||
.await
|
||||
.context("Failed to enqueue delivery tasks")?;
|
||||
.context("Failed to enqueue delivery tasks.")?;
|
||||
|
||||
let success_message = format!(
|
||||
r#"The newsletter issue "{}" has been published!"#,
|
||||
|
||||
120
src/routes/admin/posts.rs
Normal file
120
src/routes/admin/posts.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use crate::{
|
||||
authentication::AuthenticatedUser,
|
||||
idempotency::{IdempotencyKey, save_response, try_processing},
|
||||
routes::{AdminError, enqueue_delivery_tasks, insert_newsletter_issue},
|
||||
startup::AppState,
|
||||
templates::SuccessTemplate,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
Extension, Form,
|
||||
extract::State,
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
use chrono::Utc;
|
||||
use sqlx::{Executor, Postgres, Transaction};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct CreatePostForm {
|
||||
title: String,
|
||||
content: String,
|
||||
idempotency_key: String,
|
||||
}
|
||||
|
||||
fn validate_form(form: &CreatePostForm) -> Result<(), anyhow::Error> {
|
||||
if form.title.is_empty() || form.content.is_empty() {
|
||||
anyhow::bail!("Fields cannot be empty.")
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "Creating a post", skip(connection_pool, form))]
|
||||
pub async fn create_post(
|
||||
State(AppState {
|
||||
connection_pool, ..
|
||||
}): State<AppState>,
|
||||
Extension(AuthenticatedUser { user_id, .. }): Extension<AuthenticatedUser>,
|
||||
Form(form): Form<CreatePostForm>,
|
||||
) -> Result<Response, AdminError> {
|
||||
validate_form(&form).map_err(AdminError::Publish)?;
|
||||
|
||||
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 post_id = insert_post(&mut transaction, &form.title, &form.content, &user_id)
|
||||
.await
|
||||
.context("Failed to insert new post in the database.")?;
|
||||
|
||||
let newsletter_uuid = create_newsletter(&mut transaction, &form.title, &form.content, &post_id)
|
||||
.await
|
||||
.context("Failed to create newsletter.")?;
|
||||
|
||||
enqueue_delivery_tasks(&mut transaction, newsletter_uuid)
|
||||
.await
|
||||
.context("Failed to enqueue delivery tasks.")?;
|
||||
|
||||
// Send emails with unique identifiers that contains link to blog post with special param
|
||||
// Get handpoint that returns the post and mark the email as opened
|
||||
|
||||
let template = SuccessTemplate {
|
||||
success_message: "Your new post has been saved. Subscribers will be notified.".into(),
|
||||
};
|
||||
let response = Html(template.render().unwrap()).into_response();
|
||||
save_response(transaction, &idempotency_key, user_id, response)
|
||||
.await
|
||||
.map_err(AdminError::UnexpectedError)
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "Saving new post in the database",
|
||||
skip(transaction, title, content, author)
|
||||
)]
|
||||
pub async fn insert_post(
|
||||
transaction: &mut Transaction<'static, Postgres>,
|
||||
title: &str,
|
||||
content: &str,
|
||||
author: &Uuid,
|
||||
) -> Result<Uuid, AdminError> {
|
||||
let post_id = Uuid::new_v4();
|
||||
let query = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO posts (post_id, author_id, title, content, published_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
"#,
|
||||
post_id,
|
||||
author,
|
||||
title,
|
||||
content,
|
||||
Utc::now()
|
||||
);
|
||||
transaction
|
||||
.execute(query)
|
||||
.await
|
||||
.map_err(|e| AdminError::UnexpectedError(e.into()))?;
|
||||
Ok(post_id)
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "Creating newsletter for new post",
|
||||
skip(transaction, title, content, _post_id)
|
||||
)]
|
||||
pub async fn create_newsletter(
|
||||
transaction: &mut Transaction<'static, Postgres>,
|
||||
title: &str,
|
||||
content: &str,
|
||||
_post_id: &Uuid,
|
||||
) -> Result<Uuid, sqlx::Error> {
|
||||
insert_newsletter_issue(transaction, title, content, content).await
|
||||
}
|
||||
Reference in New Issue
Block a user