forgot password: send emails

This commit is contained in:
Philippe Loctaux 2023-04-22 01:27:24 +02:00
parent 751a21485f
commit feb9f16bc9
10 changed files with 676 additions and 18 deletions

View file

@ -27,3 +27,4 @@ jwt = { path = "../jwt" }
apps = { path = "../apps" }
authorization_codes = { path = "../authorization_codes" }
refresh_tokens = { path = "../refresh_tokens" }
email = { path = "../email" }

View file

@ -1,2 +1,8 @@
[default.databases.ezidam]
url = "../../database/ezidam.sqlite"
[default.email]
from = "ezidam <ezidam@mail.local>"
transport = "unencrypted"
host = "localhost"
port = 1025

View file

@ -1,9 +1,9 @@
use ezidam::rocket_setup;
use rocket::{main, Error};
use rocket::main;
// see for using rocket with main function https://github.com/intellij-rust/intellij-rust/issues/5975#issuecomment-920620289
#[main]
async fn main() -> Result<(), Error> {
async fn main() -> Result<(), String> {
// Custom config
// - rocket defaults
// - from `Rocket.toml`
@ -11,17 +11,33 @@ async fn main() -> Result<(), Error> {
// - from code below
let config = rocket::Config::figment().merge(("ip_header", "x-forwarded-for"));
// Get email config
let email_config = match config.extract_inner::<email::Config>("email") {
Ok(email_config) => match email_config.status() {
Ok(status) => {
println!("{status}");
email_config
}
Err(e) => return Err(format!("Invalid email configuration: {e}")),
},
Err(e) => return Err(format!("No email configuration was found: {e}")),
};
println!("Emails will be sent from \"{}\"", email_config.from);
// Rocket with custom config
let rocket_builder = rocket::custom(config);
// Setup server
let rocket_builder = rocket_setup(rocket_builder);
// Attach email config
let rocket_builder = rocket_builder.manage(email_config);
// Launch server
let _ = rocket_builder
.launch()
.await
.expect("Failed to launch server");
let _ = rocket_builder.launch().await.map_err(|e| e.to_string())?;
// After shutdown is done
println!("Shutdown is complete. Exiting.");
Ok(())
}

View file

@ -1,8 +1,12 @@
use crate::routes::prelude::*;
use email_address::EmailAddress;
use hash::PaperKey;
use rocket::serde::Serialize;
use rocket::State;
use rocket::{get, post};
use settings::Settings;
use std::str::FromStr;
use url::Url;
use users::{password_reset::PasswordResetToken, User};
#[get("/forgot-password")]
@ -19,12 +23,26 @@ pub struct ForgotPasswordEmailForm<'r> {
pub email: &'r str,
}
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
#[derive(Clone)]
pub struct ResetPasswordEmail {
pub title: String,
pub ezidam_version: &'static str,
pub logo_url: String,
pub token_duration: i64,
pub reset_password_url: String,
pub user_email: String,
pub user_timezone: String,
}
const SUCCESS_MESSAGE: &str = "An email is on the way with instructions to reset your password.";
#[post("/forgot-password/email", data = "<form>")]
pub async fn forgot_password_email_form(
mut db: Connection<Database>,
form: Form<ForgotPasswordEmailForm<'_>>,
email_config: &State<email::Config>,
) -> Result<Flash<Redirect>> {
if form.email.is_empty() {
return Ok(Flash::new(
@ -48,6 +66,24 @@ pub async fn forgot_password_email_form(
let mut transaction = db.begin().await?;
// Get settings
let settings = Settings::get(&mut transaction).await?;
// Get server url
let url = settings
.url()
.ok_or_else(|| Error::not_found("Server url"))?;
let url = match Url::parse(url) {
Ok(url) => url,
Err(_) => {
return Ok(Flash::new(
Redirect::to(uri!(forgot_password_page)),
FlashKind::Danger,
"Failed to parse server url",
));
}
};
// Get user
let user = match User::get_by_email(&mut transaction, &email).await? {
Some(user) => user,
@ -61,7 +97,8 @@ pub async fn forgot_password_email_form(
};
// Generate reset token
let token = task::spawn_blocking(|| PasswordResetToken::generate(15)).await?;
let token_duration = 15;
let token = task::spawn_blocking(move || PasswordResetToken::generate(token_duration)).await?;
// Save in database
user.set_password_reset_token(&mut transaction, Some(&token))
@ -69,12 +106,95 @@ pub async fn forgot_password_email_form(
transaction.commit().await?;
// TODO: send email here
// Construct url to reset password
let token = RocketResetPasswordToken(token);
let uri = uri!(super::reset_password_page(token)).to_string();
let reset_url = match url.join(&uri) {
Ok(url) => url,
Err(_) => {
return Ok(Flash::new(
Redirect::to(uri!(forgot_password_page)),
FlashKind::Danger,
"Failed to construct url to reset password ",
));
}
};
// Url to logo
let logo_url = match url.join("/logo") {
Ok(url) => url,
Err(_) => {
return Ok(Flash::new(
Redirect::to(uri!(forgot_password_page)),
FlashKind::Danger,
"Failed to construct url to business logo",
));
}
};
let email_title = "Reset password - ezidam";
let content = ResetPasswordEmail {
title: email_title.into(),
ezidam_version: env!("CARGO_PKG_VERSION"),
logo_url: logo_url.to_string(),
token_duration,
reset_password_url: reset_url.to_string(),
user_email: email.to_string(),
user_timezone: user.timezone().into(),
};
// Render email template
let mjml = match email::render_template("password-reset", &content) {
Ok(mjml) => mjml,
Err(e) => {
return Ok(Flash::new(
Redirect::to(uri!(forgot_password_page)),
FlashKind::Danger,
format!("Failed to create email: {e}"),
));
}
};
// Create html email
let html = match email::render_email(&mjml) {
Ok(html) => html,
Err(e) => {
return Ok(Flash::new(
Redirect::to(uri!(forgot_password_page)),
FlashKind::Danger,
format!("Failed to render email: {e}"),
));
}
};
let user_for_email = match user.name() {
Some(name) => {
format!("{name} <{email}>")
}
None => email.to_string(),
};
// Send email
let (flash_kind, flash_message) =
match email::send_email(email_config, &user_for_email, email_title, html).await {
Ok(okay) => {
if okay.is_positive() {
(FlashKind::Success, SUCCESS_MESSAGE.to_string())
} else {
(
FlashKind::Warning,
"Email should be on it's way, but it might not arrive".into(),
)
}
}
Err(e) => (FlashKind::Danger, e.to_string()),
};
Ok(Flash::new(
Redirect::to(uri!(forgot_password_page)),
FlashKind::Success,
SUCCESS_MESSAGE,
flash_kind,
flash_message,
))
}