Forgot pass endpoint

This commit is contained in:
eraden 2024-02-09 21:06:04 +01:00
parent 2a5cd7f4a1
commit ae327c1c91

View File

@ -0,0 +1,297 @@
use actix_web::web::Json;
use actix_web::{post, HttpRequest, HttpResponse};
use entities::users::Column;
use jet_contract::event_bus::{EmailMsg, Topic};
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::*;
use super::*;
use crate::extractors::RequireInstanceConfigured;
use crate::models::{JsonError, PasswordResetSecret};
use crate::utils::{extract_req_current_site, pass_reset_token, uidb};
use crate::{db_commit, db_rollback, db_t, DatabaseConnection, EventBusClient};
#[derive(Debug, Deserialize, Serialize)]
struct Input {
email: String,
}
#[post("/forgot-password")]
pub async fn forgot_password(
_: RequireInstanceConfigured,
req: HttpRequest,
input: Json<Input>,
db: Data<DatabaseConnection>,
event_bus: Data<EventBusClient>,
secret: Data<PasswordResetSecret>,
) -> Result<HttpResponse, JsonError> {
let mut t = db_t!(db)?;
match try_forgot_password(req, input, &mut t, event_bus, secret).await {
Ok(r) => {
db_commit!(t)?;
Ok(r)
}
Err(e) => {
db_rollback!(t).ok();
Err(e)
}
}
}
async fn try_forgot_password(
req: HttpRequest,
input: Json<Input>,
t: &mut DatabaseTransaction,
event_bus: Data<EventBusClient>,
secret: Data<PasswordResetSecret>,
) -> Result<HttpResponse, JsonError> {
let email = input.into_inner().email;
if let Err(e) = EmailAllowComment::validate_str(&email) {
warn!("Received invalid email: {email:?} {e}");
return Err(JsonError::new("Please enter a valid email"));
}
let user = Users::find()
.filter(Column::Email.eq(&email))
.one(&mut *t)
.await
.map_err(|e| {
error!("Failed to load user for forgot-password: {e}");
Error::DatabaseError
})?
.ok_or_else(|| JsonError::new("Please check the email"))?;
let current_site = extract_req_current_site(&req).map_err(|e| {
warn!("Request does not contains site information: {e}");
Error::NoHost
})?;
let uidb = uidb::encode(user.id);
let token = pass_reset_token::make_token(&user, &**secret);
if let Err(e) = event_bus
.publish(
Topic::Email,
jet_contract::event_bus::Msg::Email(EmailMsg::ForgotPassword {
current_site,
token,
uidb,
first_name: user.first_name,
}),
rumqttc::QoS::AtLeastOnce,
true,
)
.await
{
error!("Failed to send forgot pass email event: {e}");
Err(
JsonError::new("Something went wrong. Please contact administrator.")
.with_status(StatusCode::INTERNAL_SERVER_ERROR),
)
} else {
Ok(
HttpResponse::Ok()
.json(json!({ "message": "Check your email to reset your password" })),
)
}
}
#[cfg(test)]
mod tests {
use actix_jwt_session::{
Hashing, SessionMiddlewareFactory, JWT_COOKIE_NAME, JWT_HEADER_NAME, REFRESH_COOKIE_NAME,
REFRESH_HEADER_NAME,
};
use actix_web::body::to_bytes;
use actix_web::web::Data;
use actix_web::{test, App};
use jet_contract::deadpool_redis;
use reqwest::{Method, StatusCode};
use sea_orm::Database;
use serde_json::json;
use tracing_test::traced_test;
use uuid::Uuid;
use super::*;
use crate::session;
const OLD_PASS: &str = "qwertyQWERTY12345!@#$%";
macro_rules! create_app {
($app: ident, $session_storage: ident, $db: ident) => {
std::env::set_var("DATABASE_URL", "postgres://postgres@0.0.0.0:5432/jet_test");
let redis = deadpool_redis::Config::from_url("redis://0.0.0.0:6379")
.create_pool(Some(deadpool_redis::Runtime::Tokio1))
.expect("Can't connect to redis");
let $db: sea_orm::prelude::DatabaseConnection =
Database::connect("postgres://postgres@0.0.0.0:5432/jet_test")
.await
.expect("Failed to connect to database");
let ($session_storage, factory) =
SessionMiddlewareFactory::<session::AppClaims>::build_ed_dsa()
.with_redis_pool(redis.clone())
// Check if header "Authorization" exists and contains Bearer with encoded JWT
.with_jwt_header(JWT_HEADER_NAME)
// Check if cookie JWT exists and contains encoded JWT
.with_jwt_cookie(JWT_COOKIE_NAME)
.with_refresh_header(REFRESH_HEADER_NAME)
// Check if cookie JWT exists and contains encoded JWT
.with_refresh_cookie(REFRESH_COOKIE_NAME)
.with_jwt_json(&["access_token"])
.finish();
let $db = Data::new($db.clone());
ensure_instance($db.clone()).await;
let $app = test::init_service(
App::new()
.app_data(Data::new($session_storage.clone()))
.app_data($db.clone())
.app_data(Data::new(redis))
.wrap(actix_web::middleware::NormalizePath::trim())
.wrap(actix_web::middleware::Logger::default())
.wrap(factory)
.service(forgot_password),
)
.await;
};
}
async fn ensure_instance(db: Data<DatabaseConnection>) {
use entities::instances::*;
use entities::prelude::Instances;
use sea_orm::*;
if Instances::find().count(&**db).await.unwrap() > 0 {
return;
}
ActiveModel {
instance_name: Set("Plan Free".into()),
is_setup_done: Set(true),
..Default::default()
}
.save(&**db)
.await
.unwrap();
}
async fn create_user(
db: Data<DatabaseConnection>,
user_name: &str,
pass: &str,
) -> entities::users::Model {
use entities::users::*;
use sea_orm::*;
if let Ok(Some(user)) = Users::find()
.filter(Column::Email.eq(format!("{user_name}@example.com")))
.one(&**db)
.await
{
return user;
}
let pass = Hashing::encrypt(pass).unwrap();
Users::insert(ActiveModel {
password: Set(pass),
email: Set(Some(format!("{user_name}@example.com"))),
display_name: Set(user_name.to_string()),
username: Set(Uuid::new_v4().to_string()),
first_name: Set("".to_string()),
last_name: Set("".to_string()),
last_location: Set("".to_string()),
created_location: Set("".to_string()),
is_password_autoset: Set(false),
token: Set(Uuid::new_v4().to_string()),
billing_address_country: Set("".to_string()),
user_timezone: Set("UTC".to_string()),
last_login_ip: Set("0.0.0.0".to_string()),
last_login_medium: Set("None".to_string()),
last_logout_ip: Set("0.0.0.0".to_string()),
last_login_uagent: Set("test".to_string()),
is_active: Set(true),
avatar: Set("".to_string()),
..Default::default()
})
.exec_with_returning(&**db)
.await
.unwrap()
}
#[traced_test]
#[actix_web::test]
async fn no_user() {
create_app!(app, session, db);
let _user = create_user(db.clone(), "no_user_forgot_pass", OLD_PASS).await;
let req = test::TestRequest::default()
.set_json(Input {
email: "a89hsd9hasd@dsa9hs8.com".to_string(),
})
.uri("/forgot-password")
.method(Method::POST)
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let body = resp.into_body();
let json: serde_json::Value =
serde_json::from_slice(&to_bytes(body).await.unwrap()[..]).unwrap();
let expected = json!({ "error": "Please check the email" });
assert_eq!(json, expected);
}
#[traced_test]
#[actix_web::test]
async fn invalid_email() {
create_app!(app, session, db);
let _user = create_user(db.clone(), "no_user_forgot_pass", OLD_PASS).await;
let req = test::TestRequest::default()
.set_json(Input {
email: format!("no_user_forgot::??_pass@example.11"),
})
.uri("/forgot-password")
.method(Method::POST)
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let body = resp.into_body();
let json: serde_json::Value =
serde_json::from_slice(&to_bytes(body).await.unwrap()[..]).unwrap();
let expected = json!({ "error": "Please enter a valid email" });
assert_eq!(json, expected);
}
#[traced_test]
#[actix_web::test]
async fn valid() {
create_app!(app, session, db);
let user = create_user(db.clone(), "valid_forgot_pass", OLD_PASS).await;
let req = test::TestRequest::default()
.set_json(Input {
email: user.email.unwrap(),
})
.uri("/forgot-password")
.method(Method::POST)
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let body = resp.into_body();
let json: serde_json::Value =
serde_json::from_slice(&to_bytes(body).await.unwrap()[..]).unwrap();
let expected = json!({ "message": "Check your email to reset your password" });
assert_eq!(json, expected);
}
}