Use HTML swap to display success and error messages
This commit is contained in:
@@ -137,7 +137,7 @@ mod tests {
|
||||
Mock::given(header_exists("Authorization"))
|
||||
.and(header("Content-Type", "application/json"))
|
||||
.and(header("X-Requested-With", "XMLHttpRequest"))
|
||||
.and(path("v1/email"))
|
||||
.and(path("email"))
|
||||
.and(method("POST"))
|
||||
.and(SendEmailBodyMatcher)
|
||||
.respond_with(ResponseTemplate::new(200))
|
||||
|
||||
@@ -8,3 +8,4 @@ pub mod routes;
|
||||
pub mod session_state;
|
||||
pub mod startup;
|
||||
pub mod telemetry;
|
||||
pub mod templates;
|
||||
|
||||
@@ -2,12 +2,12 @@ pub mod change_password;
|
||||
pub mod dashboard;
|
||||
pub mod newsletters;
|
||||
|
||||
use crate::{routes::error_chain_fmt, session_state::TypedSession};
|
||||
use crate::{routes::error_chain_fmt, session_state::TypedSession, templates::ErrorTemplate};
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
Json,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
};
|
||||
use axum_messages::Messages;
|
||||
pub use change_password::*;
|
||||
pub use dashboard::*;
|
||||
pub use newsletters::*;
|
||||
@@ -20,7 +20,7 @@ pub enum AdminError {
|
||||
#[error("Trying to access admin dashboard without authentication.")]
|
||||
NotAuthenticated,
|
||||
#[error("Updating password failed.")]
|
||||
ChangePassword,
|
||||
ChangePassword(String),
|
||||
#[error("Could not publish newsletter.")]
|
||||
Publish(#[source] anyhow::Error),
|
||||
#[error("The idempotency key was invalid.")]
|
||||
@@ -51,8 +51,18 @@ impl IntoResponse for AdminError {
|
||||
)
|
||||
.into_response(),
|
||||
AdminError::NotAuthenticated => Redirect::to("/login").into_response(),
|
||||
AdminError::ChangePassword => Redirect::to("/admin/password").into_response(),
|
||||
AdminError::Publish(_) => Redirect::to("/admin/newsletters").into_response(),
|
||||
AdminError::ChangePassword(e) => {
|
||||
let template = ErrorTemplate {
|
||||
error_message: e.to_owned(),
|
||||
};
|
||||
Html(template.render().unwrap()).into_response()
|
||||
}
|
||||
AdminError::Publish(e) => {
|
||||
let template = ErrorTemplate {
|
||||
error_message: e.to_string(),
|
||||
};
|
||||
Html(template.render().unwrap()).into_response()
|
||||
}
|
||||
AdminError::Idempotency(e) => {
|
||||
(StatusCode::BAD_REQUEST, Json(ErrorResponse { message: e })).into_response()
|
||||
}
|
||||
@@ -60,9 +70,8 @@ impl IntoResponse for AdminError {
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "Logging out", skip(messages, session))]
|
||||
pub async fn logout(messages: Messages, session: TypedSession) -> Result<Response, AdminError> {
|
||||
#[tracing::instrument(name = "Logging out", skip(session))]
|
||||
pub async fn logout(session: TypedSession) -> Result<Response, AdminError> {
|
||||
session.clear().await;
|
||||
messages.success("You have successfully logged out.");
|
||||
Ok(Redirect::to("/login").into_response())
|
||||
}
|
||||
|
||||
@@ -2,13 +2,14 @@ use crate::{
|
||||
authentication::{self, AuthenticatedUser, Credentials, validate_credentials},
|
||||
routes::AdminError,
|
||||
startup::AppState,
|
||||
templates::SuccessTemplate,
|
||||
};
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
Extension, Form,
|
||||
extract::State,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
use axum_messages::Messages;
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
@@ -23,7 +24,6 @@ pub async fn change_password(
|
||||
State(AppState {
|
||||
connection_pool, ..
|
||||
}): State<AppState>,
|
||||
messages: Messages,
|
||||
Form(form): Form<PasswordFormData>,
|
||||
) -> Result<Response, AdminError> {
|
||||
let credentials = Credentials {
|
||||
@@ -31,23 +31,26 @@ pub async fn change_password(
|
||||
password: form.current_password,
|
||||
};
|
||||
if form.new_password.expose_secret() != form.new_password_check.expose_secret() {
|
||||
messages.error("You entered two different passwords - the field values must match.");
|
||||
Err(AdminError::ChangePassword)
|
||||
Err(AdminError::ChangePassword(
|
||||
"You entered two different passwords - the field values must match.".to_string(),
|
||||
))
|
||||
} else if validate_credentials(credentials, &connection_pool)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
messages.error("The current password is incorrect.");
|
||||
Err(AdminError::ChangePassword)
|
||||
Err(AdminError::ChangePassword(
|
||||
"The current password is incorrect.".to_string(),
|
||||
))
|
||||
} else if let Err(e) = verify_password(form.new_password.expose_secret()) {
|
||||
messages.error(e);
|
||||
Err(AdminError::ChangePassword)
|
||||
Err(AdminError::ChangePassword(e))
|
||||
} else {
|
||||
authentication::change_password(user_id, form.new_password, &connection_pool)
|
||||
.await
|
||||
.map_err(|_| AdminError::ChangePassword)?;
|
||||
messages.success("Your password has been changed.");
|
||||
Ok(Redirect::to("/admin/password").into_response())
|
||||
.map_err(|e| AdminError::ChangePassword(e.to_string()))?;
|
||||
let template = SuccessTemplate {
|
||||
success_message: "Your password has been changed.".to_string(),
|
||||
};
|
||||
Ok(Html(template.render().unwrap()).into_response())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,15 @@ use crate::{
|
||||
idempotency::{IdempotencyKey, save_response, try_processing},
|
||||
routes::AdminError,
|
||||
startup::AppState,
|
||||
templates::SuccessTemplate,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
Extension, Form,
|
||||
extract::State,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
use axum_messages::Messages;
|
||||
use sqlx::{Executor, Postgres, Transaction};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -67,20 +68,15 @@ async fn enqueue_delivery_tasks(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "Publishing a newsletter",
|
||||
skip(connection_pool, form, messages)
|
||||
)]
|
||||
#[tracing::instrument(name = "Publishing a newsletter", skip(connection_pool, form))]
|
||||
pub async fn publish_newsletter(
|
||||
State(AppState {
|
||||
connection_pool, ..
|
||||
}): State<AppState>,
|
||||
Extension(AuthenticatedUser { user_id, .. }): Extension<AuthenticatedUser>,
|
||||
messages: Messages,
|
||||
Form(form): Form<BodyData>,
|
||||
) -> Result<Response, AdminError> {
|
||||
if let Err(e) = validate_form(&form) {
|
||||
messages.error(e);
|
||||
return Err(AdminError::Publish(anyhow::anyhow!(e)));
|
||||
}
|
||||
|
||||
@@ -89,17 +85,9 @@ pub async fn publish_newsletter(
|
||||
.try_into()
|
||||
.map_err(AdminError::Idempotency)?;
|
||||
|
||||
let success_message = || {
|
||||
messages.success(format!(
|
||||
"The newsletter issue '{}' has been published!",
|
||||
form.title
|
||||
))
|
||||
};
|
||||
|
||||
let mut transaction = match try_processing(&connection_pool, &idempotency_key, user_id).await? {
|
||||
crate::idempotency::NextAction::StartProcessing(t) => t,
|
||||
crate::idempotency::NextAction::ReturnSavedResponse(response) => {
|
||||
success_message();
|
||||
return Ok(response);
|
||||
}
|
||||
};
|
||||
@@ -112,8 +100,12 @@ pub async fn publish_newsletter(
|
||||
.await
|
||||
.context("Failed to enqueue delivery tasks")?;
|
||||
|
||||
let response = Redirect::to("/admin/newsletters").into_response();
|
||||
success_message();
|
||||
let success_message = format!(
|
||||
r#"The newsletter issue "{}" has been published!"#,
|
||||
form.title
|
||||
);
|
||||
let template = SuccessTemplate { success_message };
|
||||
let response = Html(template.render().unwrap()).into_response();
|
||||
save_response(transaction, &idempotency_key, user_id, response)
|
||||
.await
|
||||
.map_err(AdminError::UnexpectedError)
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
use askama::Template;
|
||||
use axum::response::Html;
|
||||
use axum_messages::Messages;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "../templates/home.html")]
|
||||
struct HomeTemplate {
|
||||
message: String,
|
||||
}
|
||||
struct HomeTemplate;
|
||||
|
||||
pub async fn home(messages: Messages) -> Html<String> {
|
||||
let template = HomeTemplate {
|
||||
message: messages
|
||||
.last()
|
||||
.map(|msg| msg.to_string())
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
Html(template.render().unwrap())
|
||||
pub async fn home() -> Html<String> {
|
||||
Html(HomeTemplate.render().unwrap())
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::{
|
||||
routes::error_chain_fmt,
|
||||
session_state::TypedSession,
|
||||
startup::AppState,
|
||||
templates::ErrorTemplate,
|
||||
};
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
@@ -10,7 +11,6 @@ use axum::{
|
||||
extract::State,
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
};
|
||||
use axum_messages::Messages;
|
||||
use reqwest::StatusCode;
|
||||
use secrecy::SecretString;
|
||||
|
||||
@@ -45,16 +45,19 @@ impl IntoResponse for LoginError {
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
LoginError::AuthError(_) => Redirect::to("/login").into_response(),
|
||||
LoginError::AuthError(e) => {
|
||||
let template = ErrorTemplate {
|
||||
error_message: e.to_string(),
|
||||
};
|
||||
Html(template.render().unwrap()).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "../templates/login.html")]
|
||||
struct LoginTemplate {
|
||||
error: String,
|
||||
}
|
||||
struct LoginTemplate;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct LoginFormData {
|
||||
@@ -62,19 +65,12 @@ pub struct LoginFormData {
|
||||
password: SecretString,
|
||||
}
|
||||
|
||||
pub async fn get_login(messages: Messages) -> Html<String> {
|
||||
let template = LoginTemplate {
|
||||
error: messages
|
||||
.last()
|
||||
.map(|msg| msg.to_string())
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
Html(template.render().unwrap())
|
||||
pub async fn get_login() -> Html<String> {
|
||||
Html(LoginTemplate.render().unwrap())
|
||||
}
|
||||
|
||||
pub async fn post_login(
|
||||
session: TypedSession,
|
||||
messages: Messages,
|
||||
State(AppState {
|
||||
connection_pool, ..
|
||||
}): State<AppState>,
|
||||
@@ -89,11 +85,7 @@ pub async fn post_login(
|
||||
Err(e) => {
|
||||
let e = match e {
|
||||
AuthError::UnexpectedError(_) => LoginError::UnexpectedError(e.into()),
|
||||
AuthError::InvalidCredentials(_) => {
|
||||
let e = LoginError::AuthError(e.into());
|
||||
messages.error(e.to_string());
|
||||
e
|
||||
}
|
||||
AuthError::InvalidCredentials(_) => LoginError::AuthError(e.into()),
|
||||
AuthError::NotAuthenticated => unreachable!(),
|
||||
};
|
||||
Err(e)
|
||||
|
||||
@@ -2,15 +2,16 @@ use crate::{
|
||||
domain::{NewSubscriber, SubscriberEmail},
|
||||
email_client::EmailClient,
|
||||
startup::AppState,
|
||||
templates::{ErrorTemplate, SuccessTemplate},
|
||||
};
|
||||
use anyhow::Context;
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
Form, Json,
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
use axum_messages::Messages;
|
||||
use chrono::Utc;
|
||||
use rand::{Rng, distr::Alphanumeric};
|
||||
use serde::Deserialize;
|
||||
@@ -72,20 +73,22 @@ impl IntoResponse for SubscribeError {
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
SubscribeError::ValidationError(_) => Redirect::to("/").into_response(),
|
||||
SubscribeError::ValidationError(e) => {
|
||||
let template = ErrorTemplate { error_message: e };
|
||||
Html(template.render().unwrap()).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "Adding a new subscriber",
|
||||
skip(messages, connection_pool, email_client, base_url, form),
|
||||
skip(connection_pool, email_client, base_url, form),
|
||||
fields(
|
||||
subscriber_email = %form.email,
|
||||
)
|
||||
)]
|
||||
pub async fn subscribe(
|
||||
messages: Messages,
|
||||
State(AppState {
|
||||
connection_pool,
|
||||
email_client,
|
||||
@@ -97,7 +100,6 @@ pub async fn subscribe(
|
||||
let new_subscriber = match form.try_into() {
|
||||
Ok(new_sub) => new_sub,
|
||||
Err(e) => {
|
||||
messages.error(&e);
|
||||
return Err(SubscribeError::ValidationError(e));
|
||||
}
|
||||
};
|
||||
@@ -124,8 +126,10 @@ pub async fn subscribe(
|
||||
.commit()
|
||||
.await
|
||||
.context("Failed to commit the database transaction to store a new subscriber.")?;
|
||||
messages.success("A confirmation email has been sent.");
|
||||
Ok(Redirect::to("/").into_response())
|
||||
let template = SuccessTemplate {
|
||||
success_message: "A confirmation email has been sent.".to_string(),
|
||||
};
|
||||
Ok(Html(template.render().unwrap()).into_response())
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
|
||||
@@ -8,7 +8,6 @@ use axum::{
|
||||
middleware,
|
||||
routing::{get, post},
|
||||
};
|
||||
use axum_messages::MessagesManagerLayer;
|
||||
use axum_server::tls_rustls::RustlsConfig;
|
||||
use secrecy::ExposeSecret;
|
||||
use sqlx::{PgPool, postgres::PgPoolOptions};
|
||||
@@ -147,7 +146,6 @@ pub fn app(
|
||||
)
|
||||
}),
|
||||
)
|
||||
.layer(MessagesManagerLayer)
|
||||
.layer(SessionManagerLayer::new(redis_store).with_secure(false))
|
||||
.with_state(app_state)
|
||||
}
|
||||
|
||||
13
src/templates.rs
Normal file
13
src/templates.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use askama::Template;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "../templates/success.html")]
|
||||
pub struct SuccessTemplate {
|
||||
pub success_message: String,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "../templates/error.html")]
|
||||
pub struct ErrorTemplate {
|
||||
pub error_message: String,
|
||||
}
|
||||
Reference in New Issue
Block a user