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