use crate::account::hash; use crate::db::redis_async_connection; use crate::error::{ApiResult, Error}; use crate::smtp; use chrono::{Datelike, Utc}; use redis::{AsyncCommands, RedisResult}; use serde::{Deserialize, Serialize}; use std::path::Path; use std::{env, fs}; #[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, ttl_secs: i64) -> ApiResult<()> { let mut conn = redis_async_connection().await?; 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(); let result: RedisResult<()> = conn.set_ex(key, &value, ttl as u64).await; match result { Ok(_) => Ok(()), Err(err) => Err(err.into()), } } pub async fn get(token: &str) -> ApiResult { let mut conn = redis_async_connection().await?; let result: RedisResult> = conn.get(token).await; match result { Ok(Some(value)) => Ok(serde_json::from_str(&value)?), Ok(None) => Err(Error::new(404, format!("Missing email token {}", token))), Err(err) => Err(err.into()), } } pub async fn delete(token: &str) -> ApiResult<()> { let mut conn = redis_async_connection().await?; let result: RedisResult<()> = conn.del(token).await; match result { Ok(_) => Ok(()), Err(err) => Err(err.into()), } } } #[derive(Serialize)] pub struct SimpleEmailCtx { pub logo_url: String, pub link: String, pub domain: String, pub year: i32, } pub 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) { Ok(_) => Ok(()), Err(err) => { log::error!( "Invalid password reset attempt [Email: {}] [IP Address: {}]: {}", email, ip_address, err ); Err(err.into()) } } } pub fn send_confirm_email( email: &str, email_token: &EmailToken, ip_address: &str, ) -> ApiResult<()> { 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(); match smtp::send_email(&email, subject, plain, html) { Ok(_) => Ok(()), Err(err) => { log::error!( "Invalid email confirmation attempt [Email: {}] [IP Address: {}]: {}", email, ip_address, err ); Err(err.into()) } } }