Email verification form submit

This commit is contained in:
Tyler Hallada 2023-09-29 20:56:43 -04:00
parent cdc8eb9b02
commit c95334a7e2
8 changed files with 187 additions and 80 deletions

View File

@ -249,8 +249,9 @@ header.feed-header button {
display: grid; display: grid;
grid-template-columns: fit-content(100%) minmax(100px, 400px); grid-template-columns: fit-content(100%) minmax(100px, 400px);
grid-gap: 16px; grid-gap: 16px;
margin: 16px; margin: 16px auto;
margin-bottom: 32px; margin-bottom: 32px;
width: fit-content;
} }
.auth-form-grid label { .auth-form-grid label {

View File

@ -1,15 +1,21 @@
use axum::extract::{Query, State}; use axum::extract::{Query, State};
use axum::response::Response; use axum::response::Response;
use axum::TypedHeader; use axum::{TypedHeader, Form};
use lettre::SmtpTransport;
use maud::html; use maud::html;
use serde::Deserialize; use serde::Deserialize;
use serde_with::{serde_as, NoneAsEmptyString};
use sqlx::PgPool; use sqlx::PgPool;
use uuid::Uuid;
use crate::config::Config;
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use crate::htmx::HXTarget; use crate::htmx::HXTarget;
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::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)]
@ -34,7 +40,8 @@ pub async fn get(
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 { "Form with email input and button to resend goes here"} 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))
} }
} }
})) }))
@ -50,7 +57,8 @@ pub async fn get(
div class="center-horizontal" { div class="center-horizontal" {
header class="center-text" { header class="center-text" {
h2 { "Email verification token is expired" } h2 { "Email verification token is expired" }
p { "Form with button to resend goes here"} 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)))
} }
} }
})) }))
@ -73,3 +81,74 @@ pub async fn get(
})) }))
} }
} }
#[serde_as]
#[derive(Deserialize)]
pub struct ConfirmEmail {
#[serde_as(as = "NoneAsEmptyString")]
token: Option<Uuid>,
#[serde_as(as = "NoneAsEmptyString")]
email: Option<String>,
}
pub async fn post(
State(pool): State<PgPool>,
State(mailer): State<SmtpTransport>,
State(config): State<Config>,
hx_target: Option<TypedHeader<HXTarget>>,
layout: Layout,
Form(confirm_email): Form<ConfirmEmail>,
) -> Result<Response> {
if let Some(token_id) = confirm_email.token {
let token = UserEmailVerificationToken::get(&pool, token_id).await?;
let user = User::get(&pool, token.user_id).await?;
if !user.email_verified {
send_confirmation_email(pool, mailer, config, user);
}
return Ok(layout
.with_subtitle("confirm email")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
h2 { "Resent confirmation email" }
p {
"Please follow the link sent in the email."
}
}
}
}));
}
if let Some(email) = confirm_email.email {
if let Ok(user) = User::get_by_email(&pool, email).await {
if !user.email_verified {
send_confirmation_email(pool, mailer, config, user);
}
}
return Ok(layout
.with_subtitle("confirm email")
.targeted(hx_target)
.render(html! {
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."
}
}
}
}));
}
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))
}
}
}))
}

View File

@ -1,30 +1,19 @@
use std::time::Duration;
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
use axum::TypedHeader; use axum::TypedHeader;
use axum::{extract::State, Form}; use axum::{extract::State, Form};
use chrono::Utc; use lettre::SmtpTransport;
use lettre::message::{Mailbox, Message, MultiPart};
use lettre::{SmtpTransport, Transport};
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::error;
use crate::config::Config; use crate::config::Config;
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use crate::htmx::{HXRedirect, HXTarget}; use crate::htmx::{HXRedirect, HXTarget};
use crate::mailers::email_verification::send_confirmation_email;
use crate::models::user::{AuthContext, CreateUser, User}; use crate::models::user::{AuthContext, CreateUser, User};
use crate::models::user_email_verification_token::{
CreateUserEmailVerificationToken, UserEmailVerificationToken,
};
use crate::partials::layout::Layout; use crate::partials::layout::Layout;
use crate::partials::register_form::{register_form, RegisterFormProps}; use crate::partials::register_form::{register_form, RegisterFormProps};
use crate::uuid::Base62Uuid;
// TODO: put in config
const USER_EMAIL_VERIFICATION_TOKEN_EXPIRATION: Duration = Duration::from_secs(24 * 60 * 60);
#[serde_as] #[serde_as]
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -50,68 +39,6 @@ pub async fn get(hx_target: Option<TypedHeader<HXTarget>>, layout: Layout) -> Re
})) }))
} }
pub fn send_confirmation_email(pool: PgPool, mailer: SmtpTransport, config: Config, user: User) {
tokio::spawn(async move {
let user_email_address = match user.email.parse() {
Ok(address) => address,
Err(err) => {
error!("failed to parse email address: {}", err);
return;
}
};
let mailbox = Mailbox::new(user.name.clone(), user_email_address);
let token = match UserEmailVerificationToken::create(
&pool,
CreateUserEmailVerificationToken {
user_id: user.user_id,
expires_at: Utc::now() + USER_EMAIL_VERIFICATION_TOKEN_EXPIRATION,
},
)
.await
{
Ok(token) => token,
Err(err) => {
error!("failed to create user email verification token: {}", err);
return;
}
};
let confirm_link = format!(
"{}/confirm-email?token_id={}",
config.public_url,
Base62Uuid::from(token.token_id)
);
let email = match Message::builder()
.from(config.email_from.clone())
.to(mailbox)
.subject("Welcome to crawlnicle, please confirm your email address")
.multipart(MultiPart::alternative_plain_html(
format!("Welcome to crawlnicle!\n\nPlease confirm your email address\n\nClick here to confirm your email address: {}", confirm_link),
html! {
h1 { "Welcome to crawlnicle!" }
h2 { "Please confirm your email address" }
p {
a href=(confirm_link) { "Click here to confirm your email address" }
}
}.into_string(),
))
{
Ok(email) => email,
Err(err) => {
error!("failed to create email: {}", err);
return;
}
};
// TODO: notify the user that email has been sent somehow
match mailer.send(&email) {
Ok(_) => (),
Err(err) => {
error!("failed to send email: {}", err);
}
}
});
}
pub async fn post( pub async fn post(
State(pool): State<PgPool>, State(pool): State<PgPool>,
State(mailer): State<SmtpTransport>, State(mailer): State<SmtpTransport>,

View File

@ -8,6 +8,7 @@ pub mod handlers;
pub mod headers; pub mod headers;
pub mod htmx; pub mod htmx;
pub mod log; pub mod log;
pub mod mailers;
pub mod models; pub mod models;
pub mod partials; pub mod partials;
pub mod state; pub mod state;

View File

@ -0,0 +1,80 @@
use std::time::Duration;
use chrono::Utc;
use lettre::message::{Mailbox, Message, MultiPart};
use lettre::{SmtpTransport, Transport};
use maud::html;
use sqlx::PgPool;
use tracing::error;
use crate::config::Config;
use crate::models::user::User;
use crate::models::user_email_verification_token::{
CreateUserEmailVerificationToken, UserEmailVerificationToken,
};
use crate::uuid::Base62Uuid;
// TODO: put in config
const USER_EMAIL_VERIFICATION_TOKEN_EXPIRATION: Duration = Duration::from_secs(24 * 60 * 60);
pub fn send_confirmation_email(pool: PgPool, mailer: SmtpTransport, config: Config, user: User) {
tokio::spawn(async move {
let user_email_address = match user.email.parse() {
Ok(address) => address,
Err(err) => {
error!("failed to parse email address: {}", err);
return;
}
};
let mailbox = Mailbox::new(user.name.clone(), user_email_address);
let token = match UserEmailVerificationToken::create(
&pool,
CreateUserEmailVerificationToken {
user_id: user.user_id,
expires_at: Utc::now() + USER_EMAIL_VERIFICATION_TOKEN_EXPIRATION,
},
)
.await
{
Ok(token) => token,
Err(err) => {
error!("failed to create user email verification token: {}", err);
return;
}
};
let confirm_link = format!(
"{}/confirm-email?token_id={}",
config.public_url,
Base62Uuid::from(token.token_id)
);
let email = match Message::builder()
.from(config.email_from.clone())
.to(mailbox)
.subject("Welcome to crawlnicle, please confirm your email address")
.multipart(MultiPart::alternative_plain_html(
format!("Welcome to crawlnicle!\n\nPlease confirm your email address\n\nClick here to confirm your email address: {}", confirm_link),
html! {
h1 { "Welcome to crawlnicle!" }
h2 { "Please confirm your email address" }
p {
a href=(confirm_link) { "Click here to confirm your email address" }
}
}.into_string(),
))
{
Ok(email) => email,
Err(err) => {
error!("failed to create email: {}", err);
return;
}
};
// TODO: notify the user that email has been sent somehow
match mailer.send(&email) {
Ok(_) => (),
Err(err) => {
error!("failed to send email: {}", err);
}
}
});
}

1
src/mailers/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod email_verification;

View File

@ -0,0 +1,17 @@
use maud::{html, Markup};
use crate::models::user_email_verification_token::UserEmailVerificationToken;
pub fn confirm_email_form(token: Option<UserEmailVerificationToken>) -> Markup {
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;
}
button type="submit" { "Resend confirmation email" }
}
}
}

View File

@ -1,4 +1,5 @@
pub mod add_feed_form; pub mod add_feed_form;
pub mod confirm_email_form;
pub mod entry_link; pub mod entry_link;
pub mod entry_list; pub mod entry_list;
pub mod feed_link; pub mod feed_link;