Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 2bdf10a

Browse files
chore: implemented change_password flow successfully
1 parent b5cb247 commit 2bdf10a

File tree

12 files changed

+160
-8
lines changed

12 files changed

+160
-8
lines changed

‎src/authentication.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use anyhow::Context;
2-
use argon2::{Argon2, PasswordHash, PasswordVerifier};
2+
use argon2::password_hash::SaltString;
3+
use argon2::{Algorithm, Argon2, Params, PasswordHash, PasswordHasher, PasswordVerifier, Version};
34
use secrecy::{ExposeSecret, Secret};
45
use sqlx::PgPool;
56

@@ -88,3 +89,39 @@ fn verify_password_hash(
8889
.context("Invalid password.")
8990
.map_err(AuthError::InvalidCredentials)
9091
}
92+
93+
#[tracing::instrument(name = "Change password", skip(password, pool))]
94+
pub async fn change_password(
95+
user_id: uuid::Uuid,
96+
password: Secret<String>,
97+
pool: &PgPool,
98+
) -> Result<(), anyhow::Error> {
99+
let password_hash = spawn_blocking_with_tracing(move || compute_password_hash(password))
100+
.await?
101+
.context("Failed to hash password")?;
102+
sqlx::query!(
103+
r#"
104+
UPDATE users
105+
SET password_hash = 1ドル
106+
WHERE user_id = 2ドル
107+
"#,
108+
password_hash.expose_secret(),
109+
user_id
110+
)
111+
.execute(pool)
112+
.await
113+
.context("Failed to change user's password in the database.")?;
114+
Ok(())
115+
}
116+
117+
fn compute_password_hash(password: Secret<String>) -> Result<Secret<String>, anyhow::Error> {
118+
let salt = SaltString::generate(&mut rand::thread_rng());
119+
let password_hash = Argon2::new(
120+
Algorithm::Argon2id,
121+
Version::V0x13,
122+
Params::new(15000, 2, 1, None).unwrap(),
123+
)
124+
.hash_password(password.expose_secret().as_bytes(), &salt)?
125+
.to_string();
126+
Ok(Secret::new(password_hash))
127+
}

‎src/routes/admin/dashboard.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ pub async fn admin_dashboard(
3333
<p>Available actions:</p>
3434
<ol>
3535
<li><a href="/admin/password">Change password</a></li>
36+
<li>
37+
<form name="logoutForm" action="/admin/logout" method="post">
38+
<input type="submit" value="Logout">
39+
</form>
40+
</li>
3641
</ol>
3742
</body>
3843
</html>"#,

‎src/routes/admin/logout.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
use crate::session_state::TypedSession;
2+
use crate::utils::{e500, see_other};
3+
use actix_web::HttpResponse;
4+
use actix_web_flash_messages::FlashMessage;
5+
6+
pub async fn log_out(session: TypedSession) -> Result<HttpResponse, actix_web::Error> {
7+
if session.get_user_id().map_err(e500)?.is_none() {
8+
Ok(see_other("/login"))
9+
} else {
10+
session.log_out();
11+
FlashMessage::info("You have successfully logged out.").send();
12+
Ok(see_other("/login"))
13+
}
14+
}

‎src/routes/admin/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
mod dashboard;
2+
mod logout;
23
mod password;
34

45
pub use dashboard::admin_dashboard;
6+
pub use logout::log_out;
57
pub use password::*;

‎src/routes/admin/password/post.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,9 @@ pub async fn change_password(
4949
};
5050
};
5151

52-
todo!()
52+
crate::authentication::change_password(user_id, form.0.new_password, &pool)
53+
.await
54+
.map_err(e500)?;
55+
FlashMessage::error("Your password has been changed.").send();
56+
Ok(see_other("/admin/password"))
5357
}

‎src/routes/login/get.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
use actix_web::{http::header::ContentType, HttpResponse};
2-
use actix_web_flash_messages::{IncomingFlashMessages,Level};
2+
use actix_web_flash_messages::IncomingFlashMessages;
33
use std::fmt::Write;
44

55
pub async fn login_form(flash_messages: IncomingFlashMessages) -> HttpResponse {
66
let mut error_html = String::new();
7-
for m in flash_messages.iter().filter(|m| m.level() == Level::Error) {
7+
for m in flash_messages.iter() {
88
writeln!(error_html, "<p><i>{}</i></p>", m.content()).unwrap();
99
}
1010

‎src/session_state.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ impl TypedSession {
1919
pub fn get_user_id(&self) -> Result<Option<Uuid>, SessionGetError> {
2020
self.0.get(Self::USER_ID_KEY)
2121
}
22+
23+
pub fn log_out(self) {
24+
self.0.purge()
25+
}
2226
}
2327

2428
impl FromRequest for TypedSession {

‎src/startup.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use crate::{
22
configuration::{DatabaseSettings, Settings},
33
email_client::EmailClient,
44
routes::{
5-
admin_dashboard, change_password, change_password_form, confirm, health_check, home, login,
6-
login_form, publish_newsletter, subscribe,
5+
admin_dashboard, change_password, change_password_form, confirm, health_check, home,
6+
log_out, login,login_form, publish_newsletter, subscribe,
77
},
88
};
99
use actix_session::storage::RedisSessionStore;
@@ -109,6 +109,7 @@ async fn run(
109109
.route("/admin/dashboard", web::get().to(admin_dashboard))
110110
.route("/admin/password", web::get().to(change_password_form))
111111
.route("/admin/password", web::post().to(change_password))
112+
.route("/admin/logout", web::post().to(log_out))
112113
.app_data(db_pool.clone())
113114
.app_data(email_client.clone())
114115
.app_data(base_url.clone())

‎tests/api/admin_dashboard.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,35 @@ async fn you_must_be_logged_in_to_access_the_admin_dashboard() {
77
let response = app.get_admin_dashboard().await;
88

99
assert_is_redirected_to("/login", &response);
10-
}
10+
}
11+
12+
#[tokio::test]
13+
async fn logout_clears_session_state() {
14+
// Arrange
15+
let app = spawn_app().await;
16+
17+
// Act - Part 1 - Login
18+
let login_body = serde_json::json!({
19+
"username": &app.test_user.username,
20+
"password": &app.test_user.password
21+
});
22+
23+
let response = app.post_login(&login_body).await;
24+
assert_is_redirected_to("/admin/dashboard", &response);
25+
26+
// Act - Part 2 - Follow the redirect
27+
let html_page = app.get_admin_dashboard_html().await;
28+
assert!(html_page.contains(&format!("Welcome {}", app.test_user.username)));
29+
30+
// Act - Part 3 - Logout
31+
let response = app.post_logout().await;
32+
assert_is_redirected_to("/login", &response);
33+
34+
// Act - Part 4 - Follow the redirect
35+
let html_page = app.get_login_html().await;
36+
assert!(html_page.contains(r#"<p><i>You have successfully logged out.</i></p>"#));
37+
38+
// Act - Part 5 - Attempt to load admin panel
39+
let response = app.get_admin_dashboard().await;
40+
assert_is_redirected_to("/login", &response);
41+
}

‎tests/api/change_password.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,49 @@ async fn current_password_must_be_valid() {
8383
let html_page = app.get_change_password_html().await;
8484
assert!(html_page.contains("<p><i>The current password is incorrect.</i></p>"));
8585
}
86+
87+
#[tokio::test]
88+
async fn changing_password_works() {
89+
// Arrange
90+
let app = spawn_app().await;
91+
let new_password = Uuid::new_v4().to_string();
92+
93+
// Act - Part 1 - Login
94+
let login_body = serde_json::json!({
95+
"username": &app.test_user.username,
96+
"password": &app.test_user.password
97+
});
98+
99+
let response = app.post_login(&login_body).await;
100+
assert_is_redirected_to("/admin/dashboard", &response);
101+
102+
// Act - Part 2 - Change password
103+
let response = app
104+
.post_change_password(&serde_json::json!({
105+
"current_password": &app.test_user.password,
106+
"new_password": &new_password,
107+
"new_password_check": &new_password,
108+
}))
109+
.await;
110+
assert_is_redirected_to("/admin/password", &response);
111+
112+
// Act - Part 3 - Follow the redirect
113+
let html_page = app.get_change_password_html().await;
114+
assert!(html_page.contains("<p><i>Your password has been changed.</i></p>"));
115+
116+
// Act - Part 4 - Logout
117+
let response = app.post_logout().await;
118+
assert_is_redirected_to("/login", &response);
119+
120+
// Act - Part 5 - Follow the redirect
121+
let html_page = app.get_login_html().await;
122+
assert!(html_page.contains("<p><i>You have successfully logged out.</i></p>"));
123+
124+
// Act - Part 6 - Login using the new password
125+
let login_body = serde_json::json!({
126+
"username": &app.test_user.username,
127+
"password": &new_password
128+
});
129+
let response = app.post_login(&login_body).await;
130+
assert_is_redirected_to("/admin/dashboard", &response);
131+
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /