use crate::account::{csprng, hash}; use crate::error::{ApiResult, Error}; use crate::smtp; use chrono::{Datelike, Utc}; use serde::{Deserialize, Serialize}; use std::path::Path; use std::{env, fs}; use redis::aio::ConnectionManager; use crate::state::AppState; #[derive(Debug, Serialize, Deserialize)] pub struct EmailToken { pub email: String, pub token: String, pub ip_address: String, } impl EmailToken { pub fn new(email: String, token: String, ip_address: &str) -> Self { Self { email, token, ip_address: hash(&ip_address).unwrap(), } } pub async fn store(&self, state: &AppState, ttl_secs: i64) -> ApiResult<()> { let key = self.token.clone(); let value = serde_json::to_string(self)?; let now = Utc::now(); let expires_at = now + chrono::Duration::seconds(ttl_secs); let ttl = expires_at.timestamp() - now.timestamp(); state.set_ex(&key, &value, ttl as u64).await } pub async fn get(state: &AppState, token: &str) -> ApiResult { let result: Option = state.get(token).await?; match result { Some(value) => Ok(serde_json::from_str(&value)?), None => Err(Error::new(404, format!("Missing email token {}", token))), } } pub async fn delete(state: &AppState, token: &str) -> ApiResult<()> { state.del(token).await } } #[derive(Serialize)] pub struct SimpleEmailCtx { pub logo_url: String, pub link: String, pub domain: String, pub year: i32, } pub async fn send_password_reset_email( email: &str, email_token: &EmailToken, ip_address: &str, ) -> ApiResult<()> { let base_url = env::var("EXTERNAL_URL")?; let link = format!("{base_url}/profile/reset?token={}", email_token.token); let subject = "Reset your password"; let plain = format!( "Hello,\n\n\ We received a password reset request. Click the link below:\n\n\ {link}\n\n\ This link expires in 24 hours. If you didn't request this, please ignore.\n\n\ Cheers,\n\ The Aviation Data Team", link = link ); let ctx = SimpleEmailCtx { logo_url: format!("{}/logo.svg", base_url), link: link.clone(), domain: base_url, year: Utc::now().year(), }; let template_dir = env::var("TEMPLATE_DIR")?; let tpl_path = Path::new(&template_dir).join("password_reset.html"); let template_html = fs::read_to_string(&tpl_path)?; let html = smtp::registry() .render_template(&template_html, &ctx) .unwrap(); match smtp::send_email(&email, subject, plain, html).await { Ok(_) => Ok(()), Err(err) => { log::error!( "Invalid password reset attempt [Email: {}] [IP Address: {}]: {}", email, ip_address, err ); Err(err.into()) } } } pub async fn send_confirm_email(state: &AppState, email: &str, ip_address: &str) -> ApiResult<()> { let token = csprng(128); let email_token = EmailToken::new(email.to_string(), token, &ip_address); email_token.store(state, 86400).await?; let base_url = env::var("EXTERNAL_URL")?; let link = format!("{base_url}/profile/confirm?token={}", email_token.token); let subject = "Confirm your email address"; let plain = format!( "Hello,\n\n\ Thanks for registering! Click the link below to confirm your email address:\n\n\ {link}\n\n\ If you didn’t sign up for an Aviation Data account, please ignore this.\n\n\ Cheers,\n\ The Aviation Data Team", link = link ); let ctx = SimpleEmailCtx { logo_url: format!("{}/logo.svg", base_url), link: link.clone(), domain: base_url, year: Utc::now().year(), }; let template_dir = env::var("TEMPLATE_DIR")?; let tpl_path = Path::new(&template_dir).join("confirm_email.html"); let template_html = fs::read_to_string(&tpl_path)?; let html = smtp::registry() .render_template(&template_html, &ctx) .unwrap(); if let Err(err) = smtp::send_email(&email, subject, plain, html).await { log::error!( "Invalid email confirmation attempt [Email: {}] [IP Address: {}]: {}", email, ip_address, err ); let _ = EmailToken::delete(state, &email_token.token); return Err(err); } Ok(()) }