Forgot pass endpoint
This commit is contained in:
parent
2a5cd7f4a1
commit
ae327c1c91
297
crates/jet-api/src/http/api/authentication/forgot_password.rs
Normal file
297
crates/jet-api/src/http/api/authentication/forgot_password.rs
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user