Email verification form submit
This commit is contained in:
parent
cdc8eb9b02
commit
c95334a7e2
@ -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 {
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
@ -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>,
|
||||||
|
@ -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;
|
||||||
|
80
src/mailers/email_verification.rs
Normal file
80
src/mailers/email_verification.rs
Normal 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
1
src/mailers/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod email_verification;
|
17
src/partials/confirm_email_form.rs
Normal file
17
src/partials/confirm_email_form.rs
Normal 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" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user