Confirmation page and minor improvements to homepage and form messages

Basic redirect with flash messages for success and error messages
This commit is contained in:
Alphonse Paix
2025-09-16 16:47:28 +02:00
parent 612a221907
commit 8e1d68d948
12 changed files with 102 additions and 75 deletions

View File

@@ -2,7 +2,6 @@ mod admin;
mod health_check; mod health_check;
mod home; mod home;
mod login; mod login;
mod register;
mod subscriptions; mod subscriptions;
mod subscriptions_confirm; mod subscriptions_confirm;
@@ -10,6 +9,5 @@ pub use admin::*;
pub use health_check::*; pub use health_check::*;
pub use home::*; pub use home::*;
pub use login::*; pub use login::*;
pub use register::*;
pub use subscriptions::*; pub use subscriptions::*;
pub use subscriptions_confirm::*; pub use subscriptions_confirm::*;

View File

@@ -1,11 +1,19 @@
use askama::Template; use askama::Template;
use axum::response::Html; use axum::response::Html;
use axum_messages::Messages;
#[derive(Template)] #[derive(Template)]
#[template(path = "../templates/home.html")] #[template(path = "../templates/home.html")]
struct HomeTemplate; struct HomeTemplate {
message: String,
}
pub async fn home() -> Html<String> { pub async fn home(messages: Messages) -> Html<String> {
let template = HomeTemplate; let template = HomeTemplate {
message: messages
.last()
.map(|msg| msg.to_string())
.unwrap_or_default(),
};
Html(template.render().unwrap()) Html(template.render().unwrap())
} }

View File

@@ -13,7 +13,6 @@ use axum::{
use axum_messages::Messages; use axum_messages::Messages;
use reqwest::StatusCode; use reqwest::StatusCode;
use secrecy::SecretString; use secrecy::SecretString;
use std::fmt::Write;
#[derive(thiserror::Error)] #[derive(thiserror::Error)]
pub enum LoginError { pub enum LoginError {
@@ -54,7 +53,7 @@ impl IntoResponse for LoginError {
#[derive(Template)] #[derive(Template)]
#[template(path = "../templates/login.html")] #[template(path = "../templates/login.html")]
struct LoginTemplate { struct LoginTemplate {
error_html: String, error: String,
} }
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
@@ -64,11 +63,12 @@ pub struct LoginFormData {
} }
pub async fn get_login(messages: Messages) -> Html<String> { pub async fn get_login(messages: Messages) -> Html<String> {
let mut error_html = String::new(); let template = LoginTemplate {
for message in messages { error: messages
writeln!(error_html, "<p><i>{}</i></p>", message).unwrap(); .last()
} .map(|msg| msg.to_string())
let template = LoginTemplate { error_html }; .unwrap_or_default(),
};
Html(template.render().unwrap()) Html(template.render().unwrap())
} }

View File

@@ -1,11 +0,0 @@
use axum::response::{Html, IntoResponse, Response};
use axum_messages::Messages;
use std::fmt::Write;
pub async fn register(messages: Messages) -> Response {
let mut error_html = String::new();
for message in messages {
writeln!(error_html, "<p><i>{}</i></p>", message).unwrap();
}
Html(format!(include_str!("register/register.html"), error_html)).into_response()
}

View File

@@ -1,11 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Account confirmed</title>
</head>
<body>
<p>Your account has been confirmed. Welcome!</p>
</body>
</html>

View File

@@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Register</title>
</head>
<body>
<form action="/subscriptions" method="post">
<input type="text" name="name" placeholder="Name" />
<input type="text" name="email" placeholder="Email address" />
<input
type="text"
name="email_check"
placeholder="Confirm email address"
/>
<button type="Register">Register</button>
</form>
{}
<p><a href="/">Back</a></p>
</body>
</html>

View File

@@ -72,7 +72,7 @@ impl IntoResponse for SubscribeError {
}), }),
) )
.into_response(), .into_response(),
SubscribeError::ValidationError(_) => Redirect::to("/register").into_response(), SubscribeError::ValidationError(_) => Redirect::to("/").into_response(),
} }
} }
} }
@@ -125,7 +125,7 @@ pub async fn subscribe(
.await .await
.context("Failed to commit the database transaction to store a new subscriber.")?; .context("Failed to commit the database transaction to store a new subscriber.")?;
messages.success("A confirmation email has been sent."); messages.success("A confirmation email has been sent.");
Ok(Redirect::to("/register").into_response()) Ok(Redirect::to("/").into_response())
} }
#[tracing::instrument( #[tracing::instrument(

View File

@@ -1,4 +1,5 @@
use crate::startup::AppState; use crate::startup::AppState;
use askama::Template;
use axum::{ use axum::{
extract::{Query, State}, extract::{Query, State},
http::StatusCode, http::StatusCode,
@@ -8,6 +9,10 @@ use serde::Deserialize;
use sqlx::PgPool; use sqlx::PgPool;
use uuid::Uuid; use uuid::Uuid;
#[derive(Template)]
#[template(path = "../templates/confirm.html")]
struct ConfirmTemplate;
#[tracing::instrument(name = "Confirming new subscriber", skip(params))] #[tracing::instrument(name = "Confirming new subscriber", skip(params))]
pub async fn confirm( pub async fn confirm(
State(AppState { State(AppState {
@@ -27,7 +32,7 @@ pub async fn confirm(
{ {
StatusCode::INTERNAL_SERVER_ERROR.into_response() StatusCode::INTERNAL_SERVER_ERROR.into_response()
} else { } else {
Html(include_str!("register/confirm.html")).into_response() Html(ConfirmTemplate.render().unwrap()).into_response()
} }
} else { } else {
StatusCode::UNAUTHORIZED.into_response() StatusCode::UNAUTHORIZED.into_response()

View File

@@ -127,7 +127,6 @@ pub fn app(
.layer(middleware::from_fn(require_auth)); .layer(middleware::from_fn(require_auth));
Router::new() Router::new()
.route("/", get(home)) .route("/", get(home))
.route("/register", get(register))
.route("/login", get(get_login).post(post_login)) .route("/login", get(get_login).post(post_login))
.route("/health_check", get(health_check)) .route("/health_check", get(health_check))
.route("/subscriptions", post(subscribe)) .route("/subscriptions", post(subscribe))

67
templates/confirm.html Normal file
View File

@@ -0,0 +1,67 @@
{% extends "base.html" %} {% block title %}zero2prod{% endblock %} {% block
content %}
<div class="min-h-[60vh] flex items-center justify-center">
<div class="max-w-md w-full space-y-8">
<div class="text-center">
<div
class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-green-100 mb-6"
>
<svg
class="h-8 w-8 text-green-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
></path>
</svg>
</div>
<h1 class="text-3xl font-bold text-gray-900 mb-4">
Subscription confirmed!
</h1>
<p class="text-lg text-gray-600 mb-8">
Your email has been confirmed. You're all set to receive our newsletter
updates.
</p>
</div>
<div class="bg-white rounded-lg shadow-md p-6 border border-gray-200">
<div class="bg-green-50 border border-green-200 rounded-md p-4 mb-6">
<div class="flex">
<svg
class="h-5 w-5 text-green-400 mt-0.5"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd"
/>
</svg>
<div class="ml-3">
<p class="text-sm font-medium text-green-800">
Welcome to our newsletter! You'll receive quality content about
Rust backend development straight to your inbox.
</p>
</div>
</div>
</div>
<div class="text-center">
<a
href="/"
class="text-sm text-blue-600 hover:text-blue-500 transition-colors"
>
← Back to homepage
</a>
</div>
</div>
</div>
{% endblock %}
</div>

View File

@@ -7,7 +7,7 @@ block content %}
<div class="max-w-3xl"> <div class="max-w-3xl">
<h1 class="text-4xl font-bold mb-4">zero2prod</h1> <h1 class="text-4xl font-bold mb-4">zero2prod</h1>
<p class="text-xl text-blue-100 mb-6"> <p class="text-xl text-blue-100 mb-6">
Welcome to my newsletter! Stay updated on my latest projects and Welcome to our newsletter! Stay updated on our latest projects and
thoughts. Unsubscribe at any time. thoughts. Unsubscribe at any time.
</p> </p>
<div class="flex flex-col sm:flex-row gap-4"> <div class="flex flex-col sm:flex-row gap-4">
@@ -28,7 +28,6 @@ block content %}
</div> </div>
</div> </div>
<!-- Feature Cards -->
<div class="grid md:grid-cols-3 gap-6 mb-8"> <div class="grid md:grid-cols-3 gap-6 mb-8">
<div class="bg-white rounded-lg shadow-md p-6 border border-gray-200"> <div class="bg-white rounded-lg shadow-md p-6 border border-gray-200">
<div <div
@@ -113,15 +112,10 @@ block content %}
<div class="max-w-2xl mx-auto text-center"> <div class="max-w-2xl mx-auto text-center">
<h2 class="text-2xl font-bold text-gray-900 mb-4">Stay updated</h2> <h2 class="text-2xl font-bold text-gray-900 mb-4">Stay updated</h2>
<p class="text-gray-600 mb-6"> <p class="text-gray-600 mb-6">
Subscribe to my newsletter to get the latest updates. Subscribe to our newsletter to get the latest updates.
</p> </p>
<form <form action="/subscriptions" method="post" class="max-w-md mx-auto">
hx-post="/subscriptions"
hx-target="#subscription-result"
hx-swap="innerHTML"
class="max-w-md mx-auto"
>
<div class="flex flex-col sm:flex-row gap-3"> <div class="flex flex-col sm:flex-row gap-3">
<input <input
type="email" type="email"
@@ -139,7 +133,7 @@ block content %}
</div> </div>
</form> </form>
<div id="subscription-result" class="mt-4"></div> <div class="mt-4">{{ message }}</div>
</div> </div>
</div> </div>
@@ -159,12 +153,12 @@ block content %}
</div> </div>
</div> </div>
<div> <div>
<div class="text-2xl font-bold text-orange-600">0</div> <div class="text-2xl font-bold text-green-600">0</div>
<div class="text-sm text-gray-600">email opened</div> <div class="text-sm text-gray-600">email opened</div>
</div> </div>
<div> <div>
<div class="text-2xl font-bold text-purple-600">~1ms</div> <div class="text-2xl font-bold text-purple-600">3</div>
<div class="text-sm text-gray-600">response time</div> <div class="text-sm text-gray-600">issues delivered</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -5,7 +5,7 @@ block content %}
<div class="text-center"> <div class="text-center">
<h2 class="text-3xl font-bold text-gray-900">Login</h2> <h2 class="text-3xl font-bold text-gray-900">Login</h2>
<p class="mt-2 text-sm text-gray-600"> <p class="mt-2 text-sm text-gray-600">
Sign in to access the admin dashboard Sign in to access the admin dashboard.
</p> </p>
</div> </div>
@@ -53,7 +53,7 @@ block content %}
</div> </div>
</form> </form>
<div class="mt-4">{{ error_html }}</div> <div class="mt-4 text-center">{{ error }}</div>
</div> </div>
<div class="text-center"> <div class="text-center">