Files
aviation/api/src/account/email_token.rs
2025-09-19 19:33:53 -04:00

150 lines
4.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Self> {
let result: Option<String> = 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 didnt 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(())
}