Working on emails, updated swagger, added geometry column to airports
This commit is contained in:
161
api/src/account/email_token.rs
Normal file
161
api/src/account/email_token.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
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<Self> {
|
||||
let mut conn = redis_async_connection().await?;
|
||||
let result: RedisResult<Option<String>> = 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use rand::prelude::*;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
|
||||
mod auth;
|
||||
mod email_token;
|
||||
mod routes;
|
||||
mod session;
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
use crate::{
|
||||
account::{SESSION_COOKIE_NAME, Session, verify_hash},
|
||||
error::Error,
|
||||
smtp,
|
||||
users::{LoginRequest, RegisterRequest, User, UserResponse},
|
||||
};
|
||||
use actix_web::{HttpRequest, HttpResponse, ResponseError, get, post, put, web};
|
||||
use serde::Deserialize;
|
||||
use std::fs;
|
||||
use utoipa::ToSchema;
|
||||
use utoipa_actix_web::scope;
|
||||
use utoipa_actix_web::service_config::ServiceConfig;
|
||||
|
||||
use crate::account::email_token::{EmailToken, send_confirm_email, send_password_reset_email};
|
||||
use crate::account::{Auth, csprng};
|
||||
use crate::users::UpdateUser;
|
||||
|
||||
@@ -17,8 +20,8 @@ use crate::users::UpdateUser;
|
||||
content = RegisterRequest, content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "", body = UserResponse),
|
||||
(status = 409, description = ""),
|
||||
(status = 200, description = "Successful Response", body = UserResponse),
|
||||
(status = 409, description = "Conflict"),
|
||||
)
|
||||
)]
|
||||
#[post("/register")]
|
||||
@@ -39,6 +42,17 @@ async fn register(user: web::Json<RegisterRequest>, req: HttpRequest) -> HttpRes
|
||||
email,
|
||||
ip_address
|
||||
);
|
||||
|
||||
// Send confirmation email
|
||||
let token = csprng(128);
|
||||
let email_token = EmailToken::new(email.clone(), token, &ip_address);
|
||||
if let Err(err) = email_token.store(86400).await {
|
||||
return ResponseError::error_response(&err);
|
||||
}
|
||||
if let Err(err) = send_confirm_email(&email, &email_token, &ip_address) {
|
||||
return ResponseError::error_response(&Error::new(500, err.to_string()));
|
||||
};
|
||||
|
||||
HttpResponse::Created().json(user_response)
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -64,7 +78,7 @@ async fn register(user: web::Json<RegisterRequest>, req: HttpRequest) -> HttpRes
|
||||
content = LoginRequest, content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "", body = UserResponse),
|
||||
(status = 200, description = "Successful Response", body = UserResponse),
|
||||
),
|
||||
)]
|
||||
#[post("/login")]
|
||||
@@ -118,9 +132,8 @@ async fn login(request: web::Json<LoginRequest>, req: HttpRequest) -> HttpRespon
|
||||
#[utoipa::path(
|
||||
tag = "Account",
|
||||
responses(
|
||||
(status = 200, description = ""),
|
||||
(status = 401, description = ""),
|
||||
(status = 500, description = ""),
|
||||
(status = 200, description = "Successful Response"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
),
|
||||
security(
|
||||
("session_auth" = [])
|
||||
@@ -168,8 +181,8 @@ async fn logout(req: HttpRequest, auth: Auth) -> HttpResponse {
|
||||
#[utoipa::path(
|
||||
tag = "Account",
|
||||
responses(
|
||||
(status = 200, description = "", body = UserResponse),
|
||||
(status = 401, description = ""),
|
||||
(status = 200, description = "Successful Response", body = UserResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
),
|
||||
security(
|
||||
("session_auth" = [])
|
||||
@@ -229,17 +242,86 @@ async fn get_profile(req: HttpRequest) -> HttpResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
struct TokenRequest {
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Account",
|
||||
request_body(
|
||||
content = TokenRequest, content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Successful Response", body = UserResponse),
|
||||
(status = 404, description = "Not Found"),
|
||||
),
|
||||
)]
|
||||
#[post("/profile/confirm")]
|
||||
async fn confirm_profile(request: web::Json<TokenRequest>, req: HttpRequest) -> HttpResponse {
|
||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
let token = &request.token;
|
||||
|
||||
let email_token = match EmailToken::get(token).await {
|
||||
Ok(password_reset) => {
|
||||
if let Err(err) = EmailToken::delete(&password_reset.token).await {
|
||||
return ResponseError::error_response(&err);
|
||||
};
|
||||
password_reset
|
||||
}
|
||||
Err(_) => {
|
||||
return HttpResponse::NotFound().finish();
|
||||
}
|
||||
};
|
||||
|
||||
match User::select_by_email(&email_token.email).await {
|
||||
Some(user) => {
|
||||
let update_user = UpdateUser {
|
||||
email: None,
|
||||
email_verified: Some(true),
|
||||
password: None,
|
||||
role: None,
|
||||
first_name: None,
|
||||
last_name: None,
|
||||
avatar: None,
|
||||
};
|
||||
|
||||
match update_user.update(&user.id).await {
|
||||
Ok(user) => {
|
||||
let response: UserResponse = user.into();
|
||||
log::info!(
|
||||
"Successful email confirmation attempt [Email: {}] [IP Address: {}]",
|
||||
&email_token.email,
|
||||
ip_address
|
||||
);
|
||||
HttpResponse::Ok().json(response)
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"Invalid email confirmation attempt [Email: {}] [IP Address: {}]: {}",
|
||||
&email_token.email,
|
||||
ip_address,
|
||||
err
|
||||
);
|
||||
ResponseError::error_response(&err)
|
||||
}
|
||||
}
|
||||
}
|
||||
None => HttpResponse::NotFound().finish(),
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Account",
|
||||
responses(
|
||||
(status = 200, description = ""),
|
||||
(status = 401, description = ""),
|
||||
(status = 200, description = "Successful Response"),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
),
|
||||
security(
|
||||
("session_auth" = [])
|
||||
)
|
||||
)]
|
||||
#[get("/session")]
|
||||
#[post("/session")]
|
||||
async fn session_refresh(req: HttpRequest) -> HttpResponse {
|
||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
// Verify a session cookie exists
|
||||
@@ -282,14 +364,19 @@ async fn session_refresh(req: HttpRequest) -> HttpResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
struct PasswordRequest {
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Account",
|
||||
request_body(
|
||||
content = String, content_type = "application/json"
|
||||
content = PasswordRequest, content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "", body = UserResponse),
|
||||
(status = 401, description = ""),
|
||||
(status = 200, description = "Successful Response", body = UserResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
),
|
||||
security(
|
||||
("session_auth" = [])
|
||||
@@ -297,7 +384,7 @@ async fn session_refresh(req: HttpRequest) -> HttpResponse {
|
||||
)]
|
||||
#[put("/password")]
|
||||
async fn change_password(
|
||||
password: web::Json<String>,
|
||||
request: web::Json<PasswordRequest>,
|
||||
req: HttpRequest,
|
||||
auth: Auth,
|
||||
) -> HttpResponse {
|
||||
@@ -311,7 +398,7 @@ async fn change_password(
|
||||
let update_user = UpdateUser {
|
||||
email: None,
|
||||
email_verified: None,
|
||||
password: Some(password.into_inner()),
|
||||
password: Some(request.password.clone()),
|
||||
role: None,
|
||||
first_name: None,
|
||||
last_name: None,
|
||||
@@ -340,35 +427,72 @@ async fn change_password(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
struct EmailRequest {
|
||||
email: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Account",
|
||||
responses(
|
||||
(status = 200, description = ""),
|
||||
(status = 401, description = ""),
|
||||
request_body(
|
||||
content = EmailRequest, content_type = "application/json"
|
||||
),
|
||||
security(
|
||||
("session_auth" = [])
|
||||
responses(
|
||||
(status = 200, description = "Successful Response"),
|
||||
)
|
||||
)]
|
||||
#[post("/password/reset")]
|
||||
async fn reset_password(req: HttpRequest, auth: Auth) -> HttpResponse {
|
||||
async fn reset_password(request: web::Json<EmailRequest>, req: HttpRequest) -> HttpResponse {
|
||||
let email = &request.email;
|
||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
let id = auth.user.id;
|
||||
let email = auth.user.email;
|
||||
let token = csprng(128);
|
||||
|
||||
match smtp::send_password_reset(&email, &token) {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
// Silently return if the user does not exist
|
||||
if let None = User::select_by_email(&email).await {
|
||||
return HttpResponse::Ok().finish();
|
||||
};
|
||||
|
||||
let email_token = EmailToken::new(email.clone(), token, &ip_address);
|
||||
if let Err(err) = email_token.store(86400).await {
|
||||
return ResponseError::error_response(&err);
|
||||
}
|
||||
|
||||
if let Err(err) = send_password_reset_email(email, &email_token, &ip_address) {
|
||||
return ResponseError::error_response(&Error::new(500, err.to_string()));
|
||||
};
|
||||
HttpResponse::Ok().finish()
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Account",
|
||||
request_body(
|
||||
content = TokenRequest, content_type = "application/json"
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Successful Response"),
|
||||
(status = 404, description = "Not Found"),
|
||||
)
|
||||
)]
|
||||
#[post("/password/validate")]
|
||||
async fn validate_reset_password(
|
||||
request: web::Json<TokenRequest>,
|
||||
req: HttpRequest,
|
||||
) -> HttpResponse {
|
||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
let token = &request.token;
|
||||
|
||||
let email_token = match EmailToken::get(token).await {
|
||||
Ok(password_reset) => {
|
||||
if let Err(err) = EmailToken::delete(&password_reset.token).await {
|
||||
return ResponseError::error_response(&err);
|
||||
};
|
||||
password_reset
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"Invalid password reset attempt [ID: {}] [IP Address: {}]: {}",
|
||||
&id,
|
||||
ip_address,
|
||||
err
|
||||
);
|
||||
ResponseError::error_response(&err)
|
||||
return HttpResponse::NotFound().json(err);
|
||||
}
|
||||
};
|
||||
|
||||
HttpResponse::Ok().finish()
|
||||
}
|
||||
|
||||
@@ -379,8 +503,10 @@ pub fn init_routes(config: &mut ServiceConfig) {
|
||||
.service(login)
|
||||
.service(logout)
|
||||
.service(get_profile)
|
||||
.service(confirm_profile)
|
||||
.service(session_refresh)
|
||||
.service(change_password)
|
||||
.service(reset_password),
|
||||
.service(reset_password)
|
||||
.service(validate_reset_password),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ use std::str::FromStr;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
const TABLE_NAME: &str = "airports";
|
||||
const DEFAULT_COLUMNS: &str = "icao, iata, local, name, category, iso_country, \
|
||||
iso_region, municipality, elevation_ft, longitude, latitude, has_tower, has_beacon,\
|
||||
public, metar_observation_time";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct Airport {
|
||||
@@ -209,10 +212,13 @@ impl Airport {
|
||||
let pool = db::pool();
|
||||
|
||||
let airport_fut = async {
|
||||
sqlx::query_as(&format!("SELECT * FROM {} WHERE icao = $1", TABLE_NAME))
|
||||
.bind(icao)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
sqlx::query_as(&format!(
|
||||
"SELECT {} FROM {} WHERE icao = $1",
|
||||
DEFAULT_COLUMNS, TABLE_NAME
|
||||
))
|
||||
.bind(icao)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
};
|
||||
|
||||
let metar_fut = async {
|
||||
@@ -283,8 +289,8 @@ impl Airport {
|
||||
pub async fn select_all(client: &Client, query: &AirportQuery) -> ApiResult<Vec<Self>> {
|
||||
let pool = db::pool();
|
||||
|
||||
let mut builder = QueryBuilder::<Postgres>::new("SELECT * FROM ");
|
||||
builder.push(TABLE_NAME);
|
||||
let mut builder =
|
||||
QueryBuilder::<Postgres>::new(format!("SELECT {} FROM {}", DEFAULT_COLUMNS, TABLE_NAME));
|
||||
|
||||
let mut has_where = false;
|
||||
Self::push_condition_array(&mut builder, &mut has_where, "icao", &query.icaos);
|
||||
@@ -445,15 +451,17 @@ impl Airport {
|
||||
r#"
|
||||
INSERT INTO {} (
|
||||
icao, iata, local, name, category, iso_country, iso_region, municipality,
|
||||
elevation_ft, longitude, latitude, has_tower, has_beacon, public
|
||||
elevation_ft, longitude, latitude, geometry, has_tower, has_beacon, public
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7,
|
||||
$8, $9, $10, $11, $12, $13, $14
|
||||
$1, $2, $3, $4, $5, $6, $7, $8,
|
||||
$9, $10, $11,
|
||||
ST_SetSRID(ST_MakePoint($10, $11), 4326),
|
||||
$12, $13, $14
|
||||
)
|
||||
RETURNING *
|
||||
RETURNING {}
|
||||
"#,
|
||||
TABLE_NAME,
|
||||
TABLE_NAME, DEFAULT_COLUMNS
|
||||
))
|
||||
.bind(self.icao.to_string())
|
||||
.bind(&self.iata)
|
||||
@@ -497,7 +505,7 @@ impl Airport {
|
||||
let mut query_builder: QueryBuilder<Postgres> = QueryBuilder::new(
|
||||
"INSERT INTO airports (icao, iata, local, name, category, \
|
||||
iso_country, iso_region, municipality, elevation_ft, \
|
||||
longitude, latitude, has_tower, has_beacon, public) ",
|
||||
longitude, latitude, geometry, has_tower, has_beacon, public) ",
|
||||
);
|
||||
query_builder.push_values(chunk, |mut b, row| {
|
||||
b.push_bind(&row.icao)
|
||||
@@ -511,6 +519,11 @@ impl Airport {
|
||||
.push_bind(row.elevation_ft)
|
||||
.push_bind(row.longitude)
|
||||
.push_bind(row.latitude)
|
||||
.push_unseparated(", ST_SetSRID(ST_MakePoint(")
|
||||
.push_bind_unseparated(row.longitude)
|
||||
.push_unseparated(", ")
|
||||
.push_bind_unseparated(row.latitude)
|
||||
.push_unseparated("), 4326)")
|
||||
.push_bind(row.has_tower)
|
||||
.push_bind(row.has_beacon)
|
||||
.push_bind(row.public);
|
||||
@@ -641,15 +654,15 @@ impl Airport {
|
||||
let bounds = Bounds::parse(bounds_string)?;
|
||||
builder
|
||||
.push("(")
|
||||
.push("latitude BETWEEN ")
|
||||
.push_bind(bounds.south_west_lat)
|
||||
.push(" AND ")
|
||||
.push_bind(bounds.north_east_lat)
|
||||
.push(" AND ")
|
||||
.push("longitude BETWEEN ")
|
||||
.push("geometry && ST_MakeEnvelope(")
|
||||
.push_bind(bounds.south_west_lon)
|
||||
.push(" AND ")
|
||||
.push(", ")
|
||||
.push_bind(bounds.south_west_lat)
|
||||
.push(", ")
|
||||
.push_bind(bounds.north_east_lon)
|
||||
.push(", ")
|
||||
.push_bind(bounds.north_east_lat)
|
||||
.push(", 4326)")
|
||||
.push(")");
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::time::Duration;
|
||||
use utoipa::openapi::security::{ApiKey, ApiKeyValue, SecurityScheme};
|
||||
use utoipa::openapi::{Contact, SecurityRequirement};
|
||||
use utoipa_actix_web::{AppExt, scope};
|
||||
use utoipa_swagger_ui::SwaggerUi;
|
||||
use utoipa_swagger_ui::{Config, SwaggerUi};
|
||||
use uuid::Uuid;
|
||||
|
||||
mod account;
|
||||
@@ -111,21 +111,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
)
|
||||
.split_for_parts();
|
||||
|
||||
let contact_name = env::var("API_CONTACT_NAME").unwrap();
|
||||
let contact_url = env::var("EXTERNAL_URL").unwrap();
|
||||
let contact_email = env::var("API_CONTACT_EMAIL").unwrap();
|
||||
let version = env::var("CARGO_PKG_VERSION").unwrap();
|
||||
|
||||
api.info.title = "Aviation Data".to_string();
|
||||
api.info.description = None;
|
||||
api.info.terms_of_service = None;
|
||||
api.info.contact = Some(
|
||||
Contact::builder()
|
||||
.name(Some(contact_name))
|
||||
.url(Some(format!("{}/support", contact_url)))
|
||||
.email(Some(contact_email))
|
||||
.build(),
|
||||
);
|
||||
api.info.contact = None;
|
||||
api.info.license = None;
|
||||
api.info.version = version;
|
||||
|
||||
|
||||
@@ -6,8 +6,9 @@ use lettre::message::{Mailbox, MultiPart, SinglePart};
|
||||
use lettre::transport::smtp::authentication::Credentials;
|
||||
use lettre::{Address, Message, SmtpTransport, Transport};
|
||||
use serde::Serialize;
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
use std::sync::OnceLock;
|
||||
use std::{env, fs};
|
||||
|
||||
static MAILER: OnceLock<SmtpTransport> = OnceLock::new();
|
||||
static FROM_ADDRESS: OnceLock<Mailbox> = OnceLock::new();
|
||||
@@ -34,46 +35,10 @@ fn from_address() -> &'static Mailbox {
|
||||
})
|
||||
}
|
||||
|
||||
fn registry() -> &'static Handlebars<'static> {
|
||||
pub fn registry() -> &'static Handlebars<'static> {
|
||||
REGISTRY.get_or_init(|| Handlebars::new())
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PasswordResetCtx {
|
||||
logo_url: String,
|
||||
link: String,
|
||||
domain: String,
|
||||
year: i32,
|
||||
}
|
||||
|
||||
pub fn send_password_reset(to: &str, token: &str) -> ApiResult<()> {
|
||||
let base_url = env::var("EXTERNAL_URL")?.trim_end_matches('/').to_string();
|
||||
let link = format!("{base_url}/profile/reset?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\
|
||||
\tAviation Data",
|
||||
link = link
|
||||
);
|
||||
|
||||
let ctx = PasswordResetCtx {
|
||||
logo_url: format!("{}/logo.svg", base_url),
|
||||
link: link.clone(),
|
||||
domain: base_url,
|
||||
year: Utc::now().year(),
|
||||
};
|
||||
|
||||
let template_html = include_str!("../.././templates/password_reset.html");
|
||||
let html = registry().render_template(template_html, &ctx).unwrap();
|
||||
|
||||
send_email(to, subject, plain, html)
|
||||
}
|
||||
|
||||
pub fn send_email(to: &str, subject: &str, header: String, html: String) -> ApiResult<()> {
|
||||
let to_address = to.parse::<Address>()?;
|
||||
let to_mailbox = Mailbox::new(None, to_address);
|
||||
|
||||
Reference in New Issue
Block a user