Finish up email verification form

This commit is contained in:
Tyler Hallada 2023-09-29 23:48:37 -04:00
parent c95334a7e2
commit e59c6d596e
6 changed files with 131 additions and 65 deletions

View File

@ -277,3 +277,7 @@ header.feed-header button {
font-size: 16px; font-size: 16px;
grid-column: 2 / 3; grid-column: 2 / 3;
} }
.readable-width {
max-width: 600px;
}

View File

@ -1,11 +1,13 @@
use axum::extract::{Query, State}; use axum::extract::{Query, State};
use axum::response::Response; use axum::response::Response;
use axum::{TypedHeader, Form}; use axum::{Form, TypedHeader};
use axum_login::{extractors::AuthContext, SqlxStore};
use lettre::SmtpTransport; use lettre::SmtpTransport;
use maud::html; use maud::html;
use serde::Deserialize; use serde::Deserialize;
use serde_with::{serde_as, NoneAsEmptyString}; use serde_with::{serde_as, NoneAsEmptyString};
use sqlx::PgPool; use sqlx::PgPool;
use tracing::{info, warn};
use uuid::Uuid; use uuid::Uuid;
use crate::config::Config; use crate::config::Config;
@ -14,69 +16,94 @@ use crate::htmx::HXTarget;
use crate::mailers::email_verification::send_confirmation_email; use crate::mailers::email_verification::send_confirmation_email;
use crate::models::user::User; use crate::models::user::User;
use crate::models::user_email_verification_token::UserEmailVerificationToken; use crate::models::user_email_verification_token::UserEmailVerificationToken;
use crate::partials::confirm_email_form::{confirm_email_form, ConfirmEmailFormProps};
use crate::partials::layout::Layout; use crate::partials::layout::Layout;
use crate::partials::confirm_email_form::confirm_email_form;
use crate::uuid::Base62Uuid; use crate::uuid::Base62Uuid;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct ConfirmEmailQuery { pub struct ConfirmEmailQuery {
pub token_id: Base62Uuid, pub token_id: Option<Base62Uuid>,
} }
pub async fn get( pub async fn get(
State(pool): State<PgPool>, State(pool): State<PgPool>,
auth: AuthContext<Uuid, User, SqlxStore<PgPool, User>>,
hx_target: Option<TypedHeader<HXTarget>>, hx_target: Option<TypedHeader<HXTarget>>,
layout: Layout, layout: Layout,
query: Query<ConfirmEmailQuery>, query: Query<ConfirmEmailQuery>,
) -> Result<Response> { ) -> Result<Response> {
let token = match UserEmailVerificationToken::get(&pool, query.token_id.as_uuid()).await { if let Some(token_id) = query.token_id {
Ok(token) => token, info!(token_id = %token_id.as_uuid(), "get with token_id");
Err(err) => { let token = match UserEmailVerificationToken::get(&pool, token_id.as_uuid()).await {
if let Error::NotFoundUuid(_, _) = err { Ok(token) => token,
return Ok(layout Err(err) => {
.with_subtitle("confirm email") if let Error::NotFoundUuid(_, _) = err {
.targeted(hx_target) warn!(token_id = %token_id.as_uuid(), "token not found in database");
.render(html! { return Ok(layout
div class="center-horizontal" { .with_subtitle("confirm email")
header class="center-text" { .targeted(hx_target)
h2 { "Email verification token not found" } .render(html! {
p { "Enter your email to resend the confirmation email. If you don't have an account yet, create one " a href="/register" { "here" } "." } div class="center-horizontal" {
(confirm_email_form(None)) header class="center-text" {
h2 { "Email verification token not found" }
}
p class="readable-width" { "Enter your email to resend the confirmation email. If you don't have an account yet, create one " a href="/register" { "here" } "." }
(confirm_email_form(ConfirmEmailFormProps::default()))
} }
} }));
}))
}
return Err(err);
}
};
if token.expired() {
Ok(layout
.with_subtitle("confirm email")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
h2 { "Email verification token is expired" }
p { "Click the button below to resend a new confirmation email. The email will be valid for another 24 hours."}
(confirm_email_form(Some(token)))
}
} }
})) return Err(err);
} else { }
User::verify_email(&pool, token.user_id).await?; };
UserEmailVerificationToken::delete(&pool, token.token_id).await?; if token.expired() {
Ok(layout warn!(token_id = %token.token_id, "token expired");
.with_subtitle("confirm email") Ok(layout
.targeted(hx_target) .with_subtitle("confirm email")
.render(html! { .targeted(hx_target)
div class="center-horizontal" { .render(html! {
header class="center-text" { div class="center-horizontal" {
h2 { "Your email is now confirmed!" } header class="center-text" {
p { h2 { "Email verification token is expired" }
}
p class="readable-width" { "Click the button below to resend a new confirmation email. The link in the email will be valid for another 24 hours."}
(confirm_email_form(ConfirmEmailFormProps { token: Some(token), email: None }))
}
}))
} else {
info!(token_id = %token.token_id, "token valid, verifying email");
User::verify_email(&pool, token.user_id).await?;
UserEmailVerificationToken::delete(&pool, token.token_id).await?;
Ok(layout
.with_subtitle("confirm email")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
h2 { "Your email is now confirmed!" }
}
p class="readable-width" {
"Thanks for verifying your email address. " "Thanks for verifying your email address. "
a href="/" { "Return home" } a href="/" { "Return home" }
} }
} }
}))
}
} else {
Ok(layout
.with_subtitle("confirm email")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
h2 { "Confirm your email address" }
}
p class="readable-width" { "An email was sent to your email address upon registration containing a link that will confirm your email address. If you can't find it or it has been more than 24 hours since it was sent, you can resend the email by submitting the form below:"}
(confirm_email_form(
ConfirmEmailFormProps {
token: None,
email: auth.current_user.map(|u| u.email),
}
))
} }
})) }))
} }
@ -100,10 +127,14 @@ pub async fn post(
Form(confirm_email): Form<ConfirmEmail>, Form(confirm_email): Form<ConfirmEmail>,
) -> Result<Response> { ) -> Result<Response> {
if let Some(token_id) = confirm_email.token { if let Some(token_id) = confirm_email.token {
info!(%token_id, "posted with token_id");
let token = UserEmailVerificationToken::get(&pool, token_id).await?; let token = UserEmailVerificationToken::get(&pool, token_id).await?;
let user = User::get(&pool, token.user_id).await?; let user = User::get(&pool, token.user_id).await?;
if !user.email_verified { if !user.email_verified {
info!(user_id = %user.user_id, "user exists, resending confirmation email");
send_confirmation_email(pool, mailer, config, user); send_confirmation_email(pool, mailer, config, user);
} else {
warn!(user_id = %user.user_id, "confirm email submitted for already verified user, skip resend");
} }
return Ok(layout return Ok(layout
.with_subtitle("confirm email") .with_subtitle("confirm email")
@ -112,9 +143,9 @@ pub async fn post(
div class="center-horizontal" { div class="center-horizontal" {
header class="center-text" { header class="center-text" {
h2 { "Resent confirmation email" } h2 { "Resent confirmation email" }
p { }
"Please follow the link sent in the email." p class="readable-width" {
} "Please follow the link sent in the email."
} }
} }
})); }));
@ -122,7 +153,10 @@ pub async fn post(
if let Some(email) = confirm_email.email { if let Some(email) = confirm_email.email {
if let Ok(user) = User::get_by_email(&pool, email).await { if let Ok(user) = User::get_by_email(&pool, email).await {
if !user.email_verified { if !user.email_verified {
info!(user_id = %user.user_id, "user exists, resending confirmation email");
send_confirmation_email(pool, mailer, config, user); send_confirmation_email(pool, mailer, config, user);
} else {
warn!(user_id = %user.user_id, "confirm email submitted for already verified user, skip resend");
} }
} }
return Ok(layout return Ok(layout
@ -132,9 +166,9 @@ pub async fn post(
div class="center-horizontal" { div class="center-horizontal" {
header class="center-text" { header class="center-text" {
h2 { "Resent confirmation email" } h2 { "Resent confirmation email" }
p { }
"If the email you entered matched an existing account, then a confirmation email was sent. Please follow the link sent in the email." p class="readable-width" {
} "If the email you entered matched an existing account, then a confirmation email was sent. Please follow the link sent in the email."
} }
} }
})); }));
@ -146,9 +180,14 @@ pub async fn post(
div class="center-horizontal" { div class="center-horizontal" {
header class="center-text" { header class="center-text" {
h2 { "Email verification token not found" } h2 { "Email verification token not found" }
p { "Enter your email to resend the confirmation email. If you don't have an account yet, create one " a href="/register" { "here" } "." }
(confirm_email_form(None))
} }
p class="readable-width" { "Enter your email to resend the confirmation email." }
p class="readable-width" {
"If you don't have an account yet, create one "
a href="/register" { "here" }
"."
}
(confirm_email_form(ConfirmEmailFormProps::default()))
} }
})) }))
} }

View File

@ -5,6 +5,7 @@ use maud::html;
use serde::Deserialize; use serde::Deserialize;
use serde_with::serde_as; use serde_with::serde_as;
use sqlx::PgPool; use sqlx::PgPool;
use tracing::info;
use crate::auth::verify_password; use crate::auth::verify_password;
use crate::error::{Error, Result}; use crate::error::{Error, Result};
@ -56,6 +57,7 @@ pub async fn post(
Ok(user) => user, Ok(user) => user,
Err(err) => { Err(err) => {
if let Error::NotFoundString(_, _) = err { if let Error::NotFoundString(_, _) = err {
info!(email = login.email, "invalid enail");
return Ok(login_page( return Ok(login_page(
hx_target, hx_target,
layout, layout,
@ -74,6 +76,7 @@ pub async fn post(
.await .await
.is_err() .is_err()
{ {
info!(user_id = %user.user_id, "invalid password");
return Ok(login_page( return Ok(login_page(
hx_target, hx_target,
layout, layout,
@ -84,6 +87,7 @@ pub async fn post(
}, },
)); ));
} }
info!(user_id = %user.user_id, "login successful");
auth.login(&user) auth.login(&user)
.await .await
.map_err(|_| Error::InternalServerError)?; .map_err(|_| Error::InternalServerError)?;

View File

@ -42,11 +42,14 @@ pub fn send_confirmation_email(pool: PgPool, mailer: SmtpTransport, config: Conf
return; return;
} }
}; };
let confirm_link = format!( let mut confirm_link = config
"{}/confirm-email?token_id={}", .public_url
config.public_url, .clone();
Base62Uuid::from(token.token_id) confirm_link.set_path("confirm-email");
); confirm_link.query_pairs_mut()
.append_pair("token_id", &Base62Uuid::from(token.token_id).to_string());
let confirm_link = confirm_link.as_str();
let email = match Message::builder() let email = match Message::builder()
.from(config.email_from.clone()) .from(config.email_from.clone())
.to(mailbox) .to(mailbox)

View File

@ -128,6 +128,7 @@ async fn main() -> Result<()> {
.route("/register", get(handlers::register::get)) .route("/register", get(handlers::register::get))
.route("/register", post(handlers::register::post)) .route("/register", post(handlers::register::post))
.route("/confirm-email", get(handlers::confirm_email::get)) .route("/confirm-email", get(handlers::confirm_email::get))
.route("/confirm-email", post(handlers::confirm_email::post))
.nest_service("/static", ServeDir::new("static")) .nest_service("/static", ServeDir::new("static"))
.with_state(AppState { .with_state(AppState {
pool, pool,

View File

@ -2,15 +2,30 @@ use maud::{html, Markup};
use crate::models::user_email_verification_token::UserEmailVerificationToken; use crate::models::user_email_verification_token::UserEmailVerificationToken;
pub fn confirm_email_form(token: Option<UserEmailVerificationToken>) -> Markup { #[derive(Debug, Clone, Default)]
pub struct ConfirmEmailFormProps {
pub token: Option<UserEmailVerificationToken>,
pub email: Option<String>,
}
pub fn confirm_email_form(props: ConfirmEmailFormProps) -> Markup {
let ConfirmEmailFormProps { token, email } = props;
html! { html! {
form action="/confirm-email" method="post" class="auth-form-grid" { form action="/confirm-email" method="post" class="auth-form-grid" {
@if let Some(token) = token { input
input type="text" name="token" id="token" value=(token.token_id) style="display:none;"; type="text"
} @else { name="token"
label for="email" { "Email" } id="token"
input type="email" name="email" id="email" placeholder="Email" required; value=(token.map(|t| t.token_id.to_string()).unwrap_or_default())
} style="display:none;";
label for="email" { "Email" }
input
type="email"
name="email"
id="email"
placeholder="Email"
value=(email.unwrap_or_default())
required;
button type="submit" { "Resend confirmation email" } button type="submit" { "Resend confirmation email" }
} }
} }