Skip to content

Commit b5cb247

Browse files
committed
chore: tidying up the codebase
1 parent adae7ad commit b5cb247

File tree

5 files changed

+147
-25
lines changed

5 files changed

+147
-25
lines changed

src/routes/admin/dashboard.rs

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11
use crate::session_state::TypedSession;
2+
use crate::utils::e500;
23
use actix_web::http::header::ContentType;
34
use actix_web::{web, HttpResponse};
45
use anyhow::Context;
56
use sqlx::PgPool;
67
use uuid::Uuid;
7-
88
// Return an opaque 500 while preserving the error's root cause for logging.
9-
fn e500<T>(e: T) -> actix_web::Error
10-
where
11-
T: std::fmt::Debug + std::fmt::Display + 'static,
12-
{
13-
actix_web::error::ErrorInternalServerError(e)
14-
}
159

1610
pub async fn admin_dashboard(
1711
session: TypedSession,
@@ -29,20 +23,24 @@ pub async fn admin_dashboard(
2923
.content_type(ContentType::html())
3024
.body(format!(
3125
r#"<!DOCTYPE html>
32-
<html lang="en">
33-
<head>
34-
<meta http-equiv="content-type" content="text/html; charset=utf-8">
35-
<title>Admin dashboard</title>
36-
</head>
37-
<body>
38-
<p>Welcome {username}!</p>
39-
</body>
40-
</html>"#
26+
<html lang="en">
27+
<head>
28+
<meta http-equiv="content-type" content="text/html; charset=utf-8">
29+
<title>Admin dashboard</title>
30+
</head>
31+
<body>
32+
<p>Welcome {username}!</p>
33+
<p>Available actions:</p>
34+
<ol>
35+
<li><a href="/admin/password">Change password</a></li>
36+
</ol>
37+
</body>
38+
</html>"#,
4139
)))
4240
}
4341

4442
#[tracing::instrument(name = "Get username", skip(pool))]
45-
async fn get_username(user_id: Uuid, pool: &PgPool) -> Result<String, anyhow::Error> {
43+
pub async fn get_username(user_id: Uuid, pool: &PgPool) -> Result<String, anyhow::Error> {
4644
let row = sqlx::query!(
4745
r#"
4846
SELECT username

src/routes/admin/password/get.rs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,34 @@
1-
use actix_web::http::header::ContentType;
2-
use actix_web::HttpResponse;
31
use crate::session_state::TypedSession;
42
use crate::utils::{e500, see_other};
3+
use actix_web::http::header::ContentType;
4+
use actix_web::HttpResponse;
5+
use actix_web_flash_messages::IncomingFlashMessages;
6+
use std::fmt::Write;
7+
8+
pub async fn change_password_form(
9+
session: TypedSession,
10+
flash_message: IncomingFlashMessages,
11+
) -> Result<HttpResponse, actix_web::Error> {
12+
if session.get_user_id().map_err(e500)?.is_none() {
13+
return Ok(see_other("/login"));
14+
};
15+
16+
let mut msg_html = String::new();
17+
for m in flash_message.iter() {
18+
writeln!(msg_html, "<p><i>{}</i></p>", m.content()).unwrap();
19+
}
520

6-
pub async fn change_password_form() -> Result<HttpResponse, actix_web::Error> {
7-
Ok(HttpResponse::Ok().content_type(ContentType::html()).body(
8-
r#"<!DOCTYPE html>
21+
Ok(HttpResponse::Ok()
22+
.content_type(ContentType::html())
23+
.body(format!(
24+
r#"<!DOCTYPE html>
925
<html lang="en">
1026
<head>
1127
<meta http-equiv="content-type" content="text/html; charset=utf-8">
1228
<title>Change Password</title>
1329
</head>
1430
<body>
31+
{msg_html}
1532
<form action="/admin/password" method="post">
1633
<label>Current password
1734
<input
@@ -42,5 +59,5 @@ pub async fn change_password_form() -> Result<HttpResponse, actix_web::Error> {
4259
<p><a href="/admin/dashboard">&lt;- Back</a></p>
4360
</body>
4461
</html>"#,
45-
))
62+
)))
4663
}

src/routes/admin/password/post.rs

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,53 @@
1+
use crate::authentication::{validate_credentials, AuthError, Credentials};
2+
use crate::routes::admin::dashboard::get_username;
3+
use crate::session_state::TypedSession;
4+
use crate::utils::{e500, see_other};
15
use actix_web::{web, HttpResponse};
2-
use secrecy::Secret;
6+
use actix_web_flash_messages::FlashMessage;
7+
use secrecy::{ExposeSecret, Secret};
8+
use sqlx::PgPool;
9+
310
#[derive(serde::Deserialize)]
411
pub struct FormData {
512
current_password: Secret<String>,
613
new_password: Secret<String>,
714
new_password_check: Secret<String>,
815
}
9-
pub async fn change_password(form: web::Form<FormData>) -> Result<HttpResponse, actix_web::Error> {
16+
pub async fn change_password(
17+
form: web::Form<FormData>,
18+
session: TypedSession,
19+
pool: web::Data<PgPool>,
20+
) -> Result<HttpResponse, actix_web::Error> {
21+
let user_id = session.get_user_id().map_err(e500)?;
22+
if user_id.is_none() {
23+
return Ok(see_other("/login"));
24+
};
25+
26+
let user_id = user_id.unwrap();
27+
28+
if form.new_password.expose_secret() != form.new_password_check.expose_secret() {
29+
FlashMessage::error(
30+
"You entered two different new passwords - the field values must match.",
31+
)
32+
.send();
33+
return Ok(see_other("/admin/password"));
34+
};
35+
36+
let username = get_username(user_id, &pool).await.map_err(e500)?;
37+
38+
let credentials = Credentials {
39+
username,
40+
password: form.0.current_password,
41+
};
42+
if let Err(e) = validate_credentials(credentials, &pool).await {
43+
return match e {
44+
AuthError::InvalidCredentials(_) => {
45+
FlashMessage::error("The current password is incorrect.").send();
46+
Ok(see_other("/admin/password"))
47+
}
48+
AuthError::UnexpectedError(_) => Err(e500(e)),
49+
};
50+
};
51+
1052
todo!()
1153
}

tests/api/change_password.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,59 @@ async fn you_must_be_logged_in_to_change_your_password() {
2727
// Assert
2828
assert_is_redirected_to("/login", &response);
2929
}
30+
31+
#[tokio::test]
32+
async fn new_password_fields_must_match() {
33+
// Arrange
34+
let app = spawn_app().await;
35+
let new_password = Uuid::new_v4().to_string();
36+
let another_new_password = Uuid::new_v4().to_string();
37+
// Act - Part 1 - Login
38+
app.post_login(&serde_json::json!({
39+
"username": &app.test_user.username,
40+
"password": &app.test_user.password
41+
}))
42+
.await;
43+
// Act - Part 2 - Try to change password
44+
let response = app
45+
.post_change_password(&serde_json::json!({
46+
"current_password": &app.test_user.password,
47+
"new_password": &new_password,
48+
"new_password_check": &another_new_password,
49+
}))
50+
.await;
51+
assert_is_redirected_to("/admin/password", &response);
52+
// Act - Part 3 - Follow the redirect
53+
let html_page = app.get_change_password_html().await;
54+
assert!(html_page.contains(
55+
"<p><i>You entered two different new passwords - \
56+
the field values must match.</i></p>"
57+
));
58+
}
59+
60+
#[tokio::test]
61+
async fn current_password_must_be_valid() {
62+
// Arrange
63+
let app = spawn_app().await;
64+
let new_password = Uuid::new_v4().to_string();
65+
let wrong_password = Uuid::new_v4().to_string();
66+
// Act - Part 1 - Login
67+
app.post_login(&serde_json::json!({
68+
"username": &app.test_user.username,
69+
"password": &app.test_user.password
70+
}))
71+
.await;
72+
// Act - Part 2 - Try to change password
73+
let response = app
74+
.post_change_password(&serde_json::json!({
75+
"current_password": &wrong_password,
76+
"new_password": &new_password,
77+
"new_password_check": &new_password,
78+
}))
79+
.await;
80+
// Assert
81+
assert_is_redirected_to("/admin/password", &response);
82+
// Act - Part 3 - Follow the redirect
83+
let html_page = app.get_change_password_html().await;
84+
assert!(html_page.contains("<p><i>The current password is incorrect.</i></p>"));
85+
}

tests/api/helpers.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,15 @@ impl TestApp {
120120
.await
121121
.expect("Failed to execute request.")
122122
}
123+
124+
pub async fn get_change_password_html(&self) -> String {
125+
self.get_change_password()
126+
.await
127+
.text()
128+
.await
129+
.expect("Failed to get response text.")
130+
}
131+
123132
pub async fn post_change_password<Body>(&self, body: &Body) -> reqwest::Response
124133
where
125134
Body: serde::Serialize,

0 commit comments

Comments
 (0)