Skip to content

Commit 97f774c

Browse files
committed
chore: tidying up codebase
1 parent 40b733d commit 97f774c

File tree

5 files changed

+183
-15
lines changed

5 files changed

+183
-15
lines changed

Cargo.lock

Lines changed: 98 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ rand = { version = "0.8", features = ["std_rng"] }
3434
thiserror = "1"
3535
anyhow = "1"
3636
base64 = "0.13.0"
37+
sha3 = "0.9"
38+
argon2 = { version = "0.3", features = ["std"] }
3739

3840
[dev-dependencies]
3941
once_cell = "1"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- Add migration script here
2+
ALTER TABLE users RENAME password TO password_hash;

src/routes/newsletters.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use actix_web::http::{header, StatusCode};
44
use actix_web::{web, HttpRequest, HttpResponse, ResponseError};
55
use anyhow::Context;
66
use secrecy::{ExposeSecret, Secret};
7+
use sha3::Digest;
78
use sqlx::PgPool;
89

910
#[derive(serde::Deserialize)]
@@ -71,13 +72,22 @@ impl std::fmt::Debug for PublishError {
7172
}
7273
}
7374

75+
#[tracing::instrument(
76+
name = "Publish a newsletter issue",
77+
skip(body, pool, email_client, request),
78+
fields(
79+
username = tracing::field::Empty,
80+
user_id = tracing::field::Empty,
81+
)
82+
)]
7483
pub async fn publish_newsletter(
7584
body: web::Json<BodyData>,
7685
pool: web::Data<PgPool>,
7786
email_client: web::Data<EmailClient>,
7887
request: HttpRequest,
7988
) -> Result<HttpResponse, PublishError> {
8089
let credentials = basic_authentication(request.headers()).map_err(PublishError::AuthError)?;
90+
let user_id = validate_credentials(credentials, &pool);
8191
let subscribers = get_confirmed_subscribers(&pool).await?;
8292
for subscriber in subscribers {
8393
match subscriber {
@@ -137,3 +147,26 @@ fn basic_authentication(headers: &HeaderMap) -> Result<Credentials, anyhow::Erro
137147
password: Secret::new(password.to_owned()),
138148
})
139149
}
150+
151+
async fn validate_credentials(
152+
credentials: Credentials,
153+
pool: &PgPool,
154+
) -> Result<uuid::Uuid, PublishError> {
155+
let password_hash = sha3::Sha3_256::digest(credentials.password.expose_secret().as_bytes());
156+
let password_hash = format!("{:x}", password_hash);
157+
158+
let user_id: Option<_> = sqlx::query!(
159+
r#"SELECT user_id FROM users WHERE username = $1 AND password_hash = $2"#,
160+
credentials.username,
161+
password_hash,
162+
)
163+
.fetch_optional(pool)
164+
.await
165+
.context("Failed to perform the query to validate auth credentials")
166+
.map_err(PublishError::UnexpectedError)?;
167+
168+
user_id
169+
.map(|row| row.user_id)
170+
.ok_or_else(|| anyhow::anyhow!("Invalid username or password"))
171+
.map_err(PublishError::AuthError)
172+
}

tests/api/helpers.rs

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use robust_rust::{
44
startup::{get_connection_pool, Application},
55
telemetry::{get_subscriber, init_subscriber},
66
};
7+
use sha3::Digest;
78
use sqlx::{Connection, Executor, PgConnection, PgPool};
89
use uuid::Uuid;
910
use wiremock::MockServer;
@@ -27,11 +28,7 @@ pub struct TestApp {
2728
pub db_pool: PgPool,
2829
pub mock_server: MockServer,
2930
pub port: u16,
30-
}
31-
32-
pub struct ConfirmationLinks {
33-
pub html: reqwest::Url,
34-
pub plain_text: reqwest::Url,
31+
test_user: TestUser,
3532
}
3633

3734
impl TestApp {
@@ -68,14 +65,53 @@ impl TestApp {
6865
pub async fn post_newsletters(&self, body: serde_json::Value) -> reqwest::Response {
6966
reqwest::Client::new()
7067
.post(&format!("{}/newsletters", &self.address))
71-
.basic_auth(Uuid::new_v4().to_string(), Some(Uuid::new_v4().to_string()))
68+
.basic_auth(&self.test_user.username, Some(&self.test_user.password))
7269
.json(&body)
7370
.send()
7471
.await
7572
.expect("Failed to execute request.")
7673
}
7774
}
7875

76+
pub struct ConfirmationLinks {
77+
pub html: reqwest::Url,
78+
pub plain_text: reqwest::Url,
79+
}
80+
81+
pub struct TestUser {
82+
pub user_id: Uuid,
83+
pub username: String,
84+
pub password: String,
85+
}
86+
87+
impl TestUser {
88+
pub fn generate() -> Self {
89+
Self {
90+
user_id: Uuid::new_v4(),
91+
username: Uuid::new_v4().to_string(),
92+
password: Uuid::new_v4().to_string(),
93+
}
94+
}
95+
96+
async fn store(&self, pool: &PgPool) {
97+
let password_harsh = sha3::Sha3_256::digest(self.password.as_bytes());
98+
let password_hash = format!("{:x}", password_harsh);
99+
100+
sqlx::query!(
101+
r#"
102+
INSERT INTO users (user_id, username, password_hash)
103+
VALUES ($1, $2, $3)
104+
"#,
105+
self.user_id,
106+
self.username,
107+
password_hash
108+
)
109+
.execute(pool)
110+
.await
111+
.expect("Failed to store test user.");
112+
}
113+
}
114+
79115
#[allow(clippy::let_underscore_future)]
80116
pub async fn spawn_app() -> TestApp {
81117
Lazy::force(&TRACING);
@@ -105,12 +141,16 @@ pub async fn spawn_app() -> TestApp {
105141

106142
let _ = tokio::spawn(application.run_until_stopped());
107143

108-
TestApp {
144+
let test_app = TestApp {
109145
address,
110146
db_pool: get_connection_pool(&configuration.database),
111147
mock_server,
112148
port: application_port,
113-
}
149+
test_user: TestUser::generate(),
150+
};
151+
152+
test_app.test_user.store(&test_app.db_pool).await;
153+
test_app
114154
}
115155

116156
async fn configure_database(config: &DatabaseSettings) -> PgPool {

0 commit comments

Comments
 (0)