Basic email verification done

Still need to add forms to confirmation page to allow resending and add
rate limiting.
This commit is contained in:
2023-09-28 23:53:46 -04:00
parent f938a6b46b
commit cdc8eb9b02
13 changed files with 277 additions and 27 deletions

View File

@@ -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