Basic email verification done
Still need to add forms to confirmation page to allow resending and add rate limiting.
This commit is contained in:
@@ -1,19 +1,30 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::TypedHeader;
|
||||
use axum::{extract::State, Form};
|
||||
use lettre::message::header::ContentType;
|
||||
use lettre::message::{Mailbox, Message};
|
||||
use chrono::Utc;
|
||||
use lettre::message::{Mailbox, Message, MultiPart};
|
||||
use lettre::{SmtpTransport, Transport};
|
||||
use maud::html;
|
||||
use serde::Deserialize;
|
||||
use serde_with::{serde_as, NoneAsEmptyString};
|
||||
use sqlx::PgPool;
|
||||
use tracing::error;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::htmx::{HXRedirect, HXTarget};
|
||||
use crate::models::user::{AuthContext, CreateUser, User};
|
||||
use crate::models::user_email_verification_token::{
|
||||
CreateUserEmailVerificationToken, UserEmailVerificationToken,
|
||||
};
|
||||
use crate::partials::layout::Layout;
|
||||
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]
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -39,9 +50,72 @@ 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(
|
||||
State(pool): State<PgPool>,
|
||||
State(mailer): State<SmtpTransport>,
|
||||
State(config): State<Config>,
|
||||
mut auth: AuthContext,
|
||||
Form(register): Form<Register>,
|
||||
) -> Result<Response> {
|
||||
@@ -69,8 +143,6 @@ pub async fn post(
|
||||
Err(err) => {
|
||||
if let Error::InvalidEntity(validation_errors) = err {
|
||||
let field_errors = validation_errors.field_errors();
|
||||
dbg!(&validation_errors);
|
||||
dbg!(&field_errors);
|
||||
return Ok(register_form(RegisterFormProps {
|
||||
email: Some(register.email),
|
||||
name: register.name,
|
||||
@@ -116,26 +188,7 @@ pub async fn post(
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: don't 500 error on email send failure, render form with error message instead
|
||||
let mailbox = Mailbox::new(
|
||||
user.name.clone(),
|
||||
user.email.parse().map_err(|_| Error::InternalServerError)?,
|
||||
);
|
||||
let email = Message::builder()
|
||||
// TODO: make from address configurable and store in config already parsed
|
||||
.from("crawlnicle <accounts@mail.crawlnicle.com>".parse().unwrap())
|
||||
.to(mailbox)
|
||||
.subject("Welcome to crawlnicle, please confirm your email address")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
// TODO: fill in email body, use maud to create HTML body
|
||||
.body(String::from("TODO"))
|
||||
.map_err(|_| Error::InternalServerError)?;
|
||||
|
||||
// TODO: do email sending in a background async task
|
||||
// TODO: notify the user that email has been sent somehow
|
||||
mailer
|
||||
.send(&email)
|
||||
.map_err(|_| Error::InternalServerError)?;
|
||||
send_confirmation_email(pool, mailer, config, user.clone());
|
||||
|
||||
auth.login(&user)
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user