diff --git a/crates/jet-api/src/http/api/authentication/forgot_password.rs b/crates/jet-api/src/http/api/authentication/forgot_password.rs
new file mode 100644
index 0000000..b5253b9
--- /dev/null
+++ b/crates/jet-api/src/http/api/authentication/forgot_password.rs
@@ -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,
+ db: Data,
+ event_bus: Data,
+ secret: Data,
+) -> Result {
+ 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,
+ t: &mut DatabaseTransaction,
+ event_bus: Data,
+ secret: Data,
+) -> Result {
+ 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::::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) {
+ 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,
+ 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);
+ }
+}