forgot password: send emails
This commit is contained in:
parent
751a21485f
commit
feb9f16bc9
10 changed files with 676 additions and 18 deletions
|
|
@ -27,3 +27,4 @@ jwt = { path = "../jwt" }
|
|||
apps = { path = "../apps" }
|
||||
authorization_codes = { path = "../authorization_codes" }
|
||||
refresh_tokens = { path = "../refresh_tokens" }
|
||||
email = { path = "../email" }
|
||||
|
|
@ -1,2 +1,8 @@
|
|||
[default.databases.ezidam]
|
||||
url = "../../database/ezidam.sqlite"
|
||||
|
||||
[default.email]
|
||||
from = "ezidam <ezidam@mail.local>"
|
||||
transport = "unencrypted"
|
||||
host = "localhost"
|
||||
port = 1025
|
||||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
))
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue