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;
grid-column: 2 / 3;
}
.readable-width {
max-width: 600px;
}

View File

@ -1,11 +1,13 @@
use axum::extract::{Query, State};
use axum::response::Response;
use axum::{TypedHeader, Form};
use axum::{Form, TypedHeader};
use axum_login::{extractors::AuthContext, SqlxStore};
use lettre::SmtpTransport;
use maud::html;
use serde::Deserialize;
use serde_with::{serde_as, NoneAsEmptyString};
use sqlx::PgPool;
use tracing::{info, warn};
use uuid::Uuid;
use crate::config::Config;
@ -14,69 +16,94 @@ use crate::htmx::HXTarget;
use crate::mailers::email_verification::send_confirmation_email;
use crate::models::user::User;
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::confirm_email_form::confirm_email_form;
use crate::uuid::Base62Uuid;
#[derive(Deserialize)]
pub struct ConfirmEmailQuery {
pub token_id: Base62Uuid,
pub token_id: Option<Base62Uuid>,
}
pub async fn get(
State(pool): State<PgPool>,
auth: AuthContext<Uuid, User, SqlxStore<PgPool, User>>,
hx_target: Option<TypedHeader<HXTarget>>,
layout: Layout,
query: Query<ConfirmEmailQuery>,
) -> Result<Response> {
let token = match UserEmailVerificationToken::get(&pool, query.token_id.as_uuid()).await {
Ok(token) => token,
Err(err) => {
if let Error::NotFoundUuid(_, _) = err {
return Ok(layout
.with_subtitle("confirm email")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
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))
if let Some(token_id) = query.token_id {
info!(token_id = %token_id.as_uuid(), "get with token_id");
let token = match UserEmailVerificationToken::get(&pool, token_id.as_uuid()).await {
Ok(token) => token,
Err(err) => {
if let Error::NotFoundUuid(_, _) = err {
warn!(token_id = %token_id.as_uuid(), "token not found in database");
return Ok(layout
.with_subtitle("confirm email")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
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)))
}
}));
}
}))
} else {
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 {
return Err(err);
}
};
if token.expired() {
warn!(token_id = %token.token_id, "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 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. "
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>,
) -> Result<Response> {
if let Some(token_id) = confirm_email.token {
info!(%token_id, "posted with token_id");
let token = UserEmailVerificationToken::get(&pool, token_id).await?;
let user = User::get(&pool, token.user_id).await?;
if !user.email_verified {
info!(user_id = %user.user_id, "user exists, resending confirmation email");
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
.with_subtitle("confirm email")
@ -112,9 +143,9 @@ pub async fn post(
div class="center-horizontal" {
header class="center-text" {
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 Ok(user) = User::get_by_email(&pool, email).await {
if !user.email_verified {
info!(user_id = %user.user_id, "user exists, resending confirmation email");
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
@ -132,9 +166,9 @@ pub async fn post(
div class="center-horizontal" {
header class="center-text" {
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" {
header class="center-text" {
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_with::serde_as;
use sqlx::PgPool;
use tracing::info;
use crate::auth::verify_password;
use crate::error::{Error, Result};
@ -56,6 +57,7 @@ pub async fn post(
Ok(user) => user,
Err(err) => {
if let Error::NotFoundString(_, _) = err {
info!(email = login.email, "invalid enail");
return Ok(login_page(
hx_target,
layout,
@ -74,6 +76,7 @@ pub async fn post(
.await
.is_err()
{
info!(user_id = %user.user_id, "invalid password");
return Ok(login_page(
hx_target,
layout,
@ -84,6 +87,7 @@ pub async fn post(
},
));
}
info!(user_id = %user.user_id, "login successful");
auth.login(&user)
.await
.map_err(|_| Error::InternalServerError)?;

View File

@ -42,11 +42,14 @@ pub fn send_confirmation_email(pool: PgPool, mailer: SmtpTransport, config: Conf
return;
}
};
let confirm_link = format!(
"{}/confirm-email?token_id={}",
config.public_url,
Base62Uuid::from(token.token_id)
);
let mut confirm_link = config
.public_url
.clone();
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()
.from(config.email_from.clone())
.to(mailbox)

View File

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

View File

@ -2,15 +2,30 @@ use maud::{html, Markup};
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! {
form action="/confirm-email" method="post" class="auth-form-grid" {
@if let Some(token) = token {
input type="text" name="token" id="token" value=(token.token_id) style="display:none;";
} @else {
label for="email" { "Email" }
input type="email" name="email" id="email" placeholder="Email" required;
}
input
type="text"
name="token"
id="token"
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" }
}
}