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 b5cb247

Browse files
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+
pubasync 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)

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