Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Token regeneration password reset #13

Merged
merged 2 commits into from May 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
93 changes: 93 additions & 0 deletions backend/src/routes/users/generate_new_token.rs
@@ -0,0 +1,93 @@
use sqlx::Row;

#[derive(serde::Deserialize, Debug)]
pub struct UserEmail {
email: String,
}

#[derive(serde::Serialize, serde::Deserialize)]
struct SimpleUser {
id: uuid::Uuid,
email: String,
first_name: String,
last_name: String,
is_active: bool,
is_staff: bool,
is_superuser: bool,
thumbnail: Option<String>,
date_joined: chrono::DateTime<chrono::Utc>,
}

#[tracing::instrument(name = "Regenerate token for a user", skip(pool, redis_pool))]
#[actix_web::post("/regenerate-token/")]
pub async fn regenerate_token(
pool: actix_web::web::Data<sqlx::postgres::PgPool>,
user_email: actix_web::web::Json<UserEmail>,
redis_pool: actix_web::web::Data<deadpool_redis::Pool>,
) -> actix_web::HttpResponse {
match get_user_who_is_not_active(&pool, &user_email.email).await {
Ok(visible_user_detail) => {
let mut redis_con = redis_pool
.get()
.await
.map_err(|e| {
tracing::event!(target: "backend", tracing::Level::ERROR, "{}", e);
actix_web::HttpResponse::InternalServerError().json(
crate::types::ErrorResponse {
error: "We cannot activate your account at the moment".to_string(),
},
)
})
.expect("Redis connection cannot be gotten.");
crate::utils::send_multipart_email(
"RustAuth - Let's get you verified".to_string(),
visible_user_detail.id,
visible_user_detail.email,
visible_user_detail.first_name,
visible_user_detail.last_name,
"verification_email.html",
&mut redis_con,
)
.await
.unwrap();
actix_web::HttpResponse::Ok().json(crate::types::SuccessResponse {
message: "Account activation link has been sent to your email address. Kindly take action before its expiration".to_string(),
})
}
Err(e) => {
tracing::event!(target: "sqlx",tracing::Level::ERROR, "User not found:{:#?}", e);
actix_web::HttpResponse::NotFound().json(crate::types::ErrorResponse {
error: "A user with this e-mail address does not exist. If you registered with this email, ensure you haven't activated it yet. You can check by logging in".to_string(),
})
}
}
}

#[tracing::instrument(name = "Getting a user from DB who isn't active yet.", skip(pool, email),fields(user_email = %email))]
async fn get_user_who_is_not_active(
pool: &sqlx::postgres::PgPool,
email: &String,
) -> Result<SimpleUser, sqlx::Error> {
match sqlx::query("SELECT id, email, first_name, last_name, password, is_active, is_staff, is_superuser, date_joined, thumbnail FROM users WHERE email = $1 AND is_active=false")
.bind(email)
.map(|row: sqlx::postgres::PgRow| SimpleUser {
id: row.get("id"),
email: row.get("email"),
first_name: row.get("first_name"),
last_name: row.get("last_name"),
is_active: row.get("is_active"),
is_staff: row.get("is_staff"),
is_superuser: row.get("is_superuser"),
thumbnail: row.get("thumbnail"),
date_joined: row.get("date_joined"),
})
.fetch_one(pool)
.await
{
Ok(user) => Ok(user),
Err(e) => {
tracing::event!(target: "sqlx",tracing::Level::ERROR, "User not found in DB: {:#?}", e);
Err(e)
}
}
}
7 changes: 6 additions & 1 deletion backend/src/routes/users/mod.rs
@@ -1,7 +1,9 @@
mod confirm_registration;
mod current_user;
mod generate_new_token;
mod login;
mod logout;
mod password_change;
mod register;
mod update_user;

Expand All @@ -13,6 +15,9 @@ pub fn auth_routes_config(cfg: &mut actix_web::web::ServiceConfig) {
.service(login::login_user)
.service(current_user::get_current_user)
.service(update_user::update_users_details)
.service(logout::log_out),
.service(generate_new_token::regenerate_token)
.service(logout::log_out)
// Password operations
.configure(password_change::password_routes_config),
);
}
94 changes: 94 additions & 0 deletions backend/src/routes/users/password_change/change.rs
@@ -0,0 +1,94 @@
#[derive(serde::Deserialize)]
pub struct NewPassword {
token: String,
password: String,
}

#[tracing::instrument(
name = "Changing user's password",
skip(pool, new_password, redis_pool)
)]
#[actix_web::post("/change-user-password/")]
pub async fn change_user_password(
pool: actix_web::web::Data<sqlx::postgres::PgPool>,
new_password: actix_web::web::Json<NewPassword>,
redis_pool: actix_web::web::Data<deadpool_redis::Pool>,
) -> actix_web::HttpResponse {
let settings = crate::settings::get_settings().expect("Failed to read settings.");

let mut redis_con = redis_pool
.get()
.await
.map_err(|e| {
tracing::event!(target: "backend", tracing::Level::ERROR, "{}", e);

actix_web::HttpResponse::SeeOther()
.insert_header((
actix_web::http::header::LOCATION,
format!(
"{}/auth/error?reason=We cannot activate your account at the moment",
settings.frontend_url
),
))
.finish()
})
.expect("Redis connection cannot be gotten.");

let confirmation_token = match crate::utils::verify_confirmation_token_pasetor(
new_password.0.token,
&mut redis_con,
Some(true),
)
.await
{
Ok(token) => token,
Err(e) => {
tracing::event!(target: "backend",tracing::Level::ERROR, "{:#?}", e);
return actix_web::HttpResponse::BadRequest().json(crate::types::ErrorResponse {
error: "It appears that your password request token has expired or previously used"
.to_string(),
});
}
};

let new_user_password = crate::utils::hash(new_password.0.password.as_bytes()).await;

match update_user_password_in_db(&pool, &new_user_password, confirmation_token.user_id).await {
Ok(_) => {
tracing::event!(target: "backend",tracing::Level::INFO, "User password updated successfully");
actix_web::HttpResponse::Ok().json(crate::types::SuccessResponse {
message: "Your password has been changed successfully. Kindly login with the new password".to_string(),
})
}
Err(e) => {
tracing::event!(target: "backend",tracing::Level::ERROR, "Failed to change user password: {:#?}", e);
actix_web::HttpResponse::BadRequest().json(crate::types::ErrorResponse {
error: "Sorry, we could not change your password this time. Please try again."
.to_string(),
})
}
}
}

#[tracing::instrument(name = "Updating user password in the DB.", skip(pool, new_password))]
async fn update_user_password_in_db(
pool: &sqlx::postgres::PgPool,
new_password: &String,
user_id: uuid::Uuid,
) -> Result<bool, sqlx::Error> {
match sqlx::query("UPDATE users SET password = $1 WHERE id = $2")
.bind(new_password)
.bind(user_id)
.execute(pool)
.await
{
Ok(r) => {
tracing::event!(target: "sqlx", tracing::Level::INFO, "User password has been updated successfully in the DB: {:?}", r);
Ok(true)
}
Err(e) => {
tracing::event!(target: "sqlx",tracing::Level::ERROR, "Failed to update user password in the DB: {:#?}", e);
Err(e)
}
}
}
82 changes: 82 additions & 0 deletions backend/src/routes/users/password_change/confirm_change_request.rs
@@ -0,0 +1,82 @@
#[derive(serde::Deserialize)]
pub struct Parameters {
token: String,
}

#[tracing::instrument(name = "Confirming change password token", skip(params, redis_pool))]
#[actix_web::get("/confirm/change-password/")]
pub async fn confirm_change_password_token(
params: actix_web::web::Query<Parameters>,
redis_pool: actix_web::web::Data<deadpool_redis::Pool>,
) -> actix_web::HttpResponse {
let settings = crate::settings::get_settings().expect("Failed to read settings.");

let mut redis_con = redis_pool
.get()
.await
.map_err(|e| {
tracing::event!(target: "backend", tracing::Level::ERROR, "{}", e);

actix_web::HttpResponse::SeeOther()
.insert_header((
actix_web::http::header::LOCATION,
format!(
"{}/auth/error?reason=Something unexpected happened. Kindly try again",
settings.frontend_url
),
))
.finish()
})
.expect("Redis connection cannot be gotten.");

let confirmation_token = match crate::utils::verify_confirmation_token_pasetor(
params.token.clone(),
&mut redis_con,
None,
)
.await
{
Ok(token) => token,
Err(e) => {
tracing::event!(target: "backend",tracing::Level::ERROR, "{:#?}", e);

return actix_web::HttpResponse::SeeOther()
.insert_header((
actix_web::http::header::LOCATION,
format!("{}/auth/error?reason=It appears that your password request token has expired or previously used", settings.frontend_url, ),
))
.finish();
}
};

let issued_token = match crate::utils::issue_confirmation_token_pasetors(
confirmation_token.user_id,
&mut redis_con,
Some(true),
)
.await
{
Ok(t) => t,
Err(e) => {
tracing::event!(target: "backend", tracing::Level::ERROR, "{}", e);
return actix_web::HttpResponse::SeeOther()
.insert_header((
actix_web::http::header::LOCATION,
format!("{}/auth/error?reason={}", settings.frontend_url, e),
))
.json(crate::types::ErrorResponse {
error: format!("{}", e),
});
}
};

actix_web::HttpResponse::SeeOther()
.insert_header((
actix_web::http::header::LOCATION,
format!(
"{}/auth/password/change-password?token={}",
settings.frontend_url, issued_token
),
))
.finish()
}
12 changes: 12 additions & 0 deletions backend/src/routes/users/password_change/mod.rs
@@ -0,0 +1,12 @@
mod change;
mod confirm_change_request;
mod request_change;

pub fn password_routes_config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(
actix_web::web::scope("/password-change")
.service(request_change::request_password_change)
.service(confirm_change_request::confirm_change_password_token)
.service(change::change_user_password),
);
}
52 changes: 52 additions & 0 deletions backend/src/routes/users/password_change/request_change.rs
@@ -0,0 +1,52 @@
#[derive(serde::Deserialize, Debug)]
pub struct UserEmail {
email: String,
}

#[tracing::instrument(name = "Requesting a password change", skip(pool, redis_pool))]
#[actix_web::post("/request-password-change/")]
pub async fn request_password_change(
pool: actix_web::web::Data<sqlx::postgres::PgPool>,
user_email: actix_web::web::Json<UserEmail>,
redis_pool: actix_web::web::Data<deadpool_redis::Pool>,
) -> actix_web::HttpResponse {
let settings = crate::settings::get_settings().expect("Failed to read settings.");
match crate::utils::get_active_user_from_db(Some(&pool), None, None, Some(&user_email.0.email))
.await
{
Ok(visible_user_detail) => {
let mut redis_con = redis_pool
.get()
.await
.map_err(|e| {
tracing::event!(target: "backend", tracing::Level::ERROR, "{}", e);
actix_web::HttpResponse::InternalServerError().json(
crate::types::ErrorResponse {
error: "Something happened. Please try again".to_string(),
},
)
})
.expect("Redis connection cannot be gotten.");
crate::utils::send_multipart_email(
"RustAuth - Password Reset Instructions".to_string(),
visible_user_detail.id,
visible_user_detail.email,
visible_user_detail.first_name,
visible_user_detail.last_name,
"password_reset_email.html",
&mut redis_con,
)
.await
.unwrap();
actix_web::HttpResponse::Ok().json(crate::types::SuccessResponse {
message: "Password reset instructions have been sent to your email address. Kindly take action before its expiration".to_string(),
})
}
Err(e) => {
tracing::event!(target: "sqlx",tracing::Level::ERROR, "User not found:{:#?}", e);
actix_web::HttpResponse::NotFound().json(crate::types::ErrorResponse {
error: format!("An active user with this e-mail address does not exist. If you registered with this email, ensure you have activated your account. You can check by logging in. If you have not activated it, visit {}/auth/regenerate-token to regenerate the token that will allow you activate your account.", settings.frontend_url),
})
}
}
}
2 changes: 1 addition & 1 deletion backend/src/utils/emails.rs
Expand Up @@ -137,7 +137,7 @@ pub async fn send_multipart_email(
let confirmation_link = {
if template_name == "password_reset_email.html" {
format!(
"{}/users/password/confirm/change_password?token={}",
"{}/users/password-change/confirm/change-password/?token={}",
web_address, issued_token,
)
} else {
Expand Down