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 40b733d

Browse files
chore: added user authentication
1 parent 7dd5289 commit 40b733d

File tree

6 files changed

+93
-10
lines changed

6 files changed

+93
-10
lines changed

‎Cargo.lock‎

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

‎Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ reqwest = { version = "0.11.18", default-features = false, features = ["json", "
3333
rand = { version = "0.8", features = ["std_rng"] }
3434
thiserror = "1"
3535
anyhow = "1"
36+
base64 = "0.13.0"
3637

3738
[dev-dependencies]
3839
once_cell = "1"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-- Add migration script here
2+
CREATE TABLE users(
3+
user_id uuid PRIMARY KEY,
4+
username TEXT NOT NULL UNIQUE,
5+
password TEXT NOT NULL
6+
);

‎src/routes/newsletters.rs‎

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use crate::{domain::SubscriberEmail, email_client::EmailClient, routes::error_chain_fmt};
2-
use actix_web::http::StatusCode;
3-
use actix_web::{web, HttpResponse, ResponseError};
2+
use actix_web::http::header::{HeaderMap, HeaderValue};
3+
use actix_web::http::{header, StatusCode};
4+
use actix_web::{web, HttpRequest, HttpResponse, ResponseError};
45
use anyhow::Context;
6+
use secrecy::{ExposeSecret, Secret};
57
use sqlx::PgPool;
68

79
#[derive(serde::Deserialize)]
@@ -39,14 +41,26 @@ pub struct Content {
3941

4042
#[derive(thiserror::Error)]
4143
pub enum PublishError {
44+
#[error("Authentication failed")]
45+
AuthError(#[source] anyhow::Error),
4246
#[error(transparent)]
4347
UnexpectedError(#[from] anyhow::Error),
4448
}
4549

4650
impl ResponseError for PublishError {
47-
fn status_code(&self) -> StatusCode {
51+
fn error_response(&self) -> HttpResponse {
4852
match self {
49-
PublishError::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR,
53+
PublishError::UnexpectedError(_) => {
54+
HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR)
55+
}
56+
PublishError::AuthError(_) => {
57+
let mut response = HttpResponse::new(StatusCode::UNAUTHORIZED);
58+
let header_value = HeaderValue::from_str(r#"Basic realm="publish""#).unwrap();
59+
response
60+
.headers_mut()
61+
.insert(header::WWW_AUTHENTICATE, header_value);
62+
response
63+
}
5064
}
5165
}
5266
}
@@ -61,7 +75,9 @@ pub async fn publish_newsletter(
6175
body: web::Json<BodyData>,
6276
pool: web::Data<PgPool>,
6377
email_client: web::Data<EmailClient>,
78+
request: HttpRequest,
6479
) -> Result<HttpResponse, PublishError> {
80+
let credentials = basic_authentication(request.headers()).map_err(PublishError::AuthError)?;
6581
let subscribers = get_confirmed_subscribers(&pool).await?;
6682
for subscriber in subscribers {
6783
match subscriber {
@@ -86,3 +102,38 @@ pub async fn publish_newsletter(
86102
}
87103
Ok(HttpResponse::Ok().finish())
88104
}
105+
106+
struct Credentials {
107+
username: String,
108+
password: Secret<String>,
109+
}
110+
111+
fn basic_authentication(headers: &HeaderMap) -> Result<Credentials, anyhow::Error> {
112+
let header_value = headers
113+
.get("Authorization")
114+
.context("Missing authorization header")?
115+
.to_str()
116+
.context("The authorization header value is invalid UTF-8")?;
117+
118+
let base64_encoded_segment = header_value
119+
.strip_prefix("Basic ")
120+
.context("The Authorization scheme was not 'Basic'. ")?;
121+
let decoded_bytes = base64::decode_config(base64_encoded_segment, base64::STANDARD)
122+
.context("Failed to base64 decode 'Basic' credentials.")?;
123+
let decoded_credentials =
124+
String::from_utf8(decoded_bytes).context("The decoded credentials are not valid UTF-8.")?;
125+
126+
let mut credentials = decoded_credentials.splitn(2, ':');
127+
let username = credentials
128+
.next()
129+
.ok_or_else(|| anyhow::anyhow!("The username is missing."))?
130+
.to_owned();
131+
let password = credentials
132+
.next()
133+
.ok_or_else(|| anyhow::anyhow!("The password is missing. Username: {}", username))?;
134+
135+
Ok(Credentials {
136+
username,
137+
password: Secret::new(password.to_owned()),
138+
})
139+
}

‎tests/api/helpers.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ impl TestApp {
6868
pub async fn post_newsletters(&self, body: serde_json::Value) -> reqwest::Response {
6969
reqwest::Client::new()
7070
.post(&format!("{}/newsletters", &self.address))
71+
.basic_auth(Uuid::new_v4().to_string(), Some(Uuid::new_v4().to_string()))
7172
.json(&body)
7273
.send()
7374
.await

‎tests/api/newsletter.rs‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,26 @@ async fn create_confirmed_subscriber(app: &TestApp) {
124124
.error_for_status()
125125
.unwrap();
126126
}
127+
128+
#[tokio::test]
129+
async fn requests_missing_authorization_are_rejected() {
130+
let app = spawn_app().await;
131+
let response = reqwest::Client::new()
132+
.post(&format!("{}/newsletters", &app.address))
133+
.json(&serde_json::json!({
134+
"title": "Newsletter title",
135+
"content": {
136+
"text": "Newsletter content",
137+
"html": "<p>Newsletter content</p>",
138+
}
139+
}))
140+
.send()
141+
.await
142+
.expect("Failed to execute request.");
143+
144+
assert_eq!(response.status().as_u16(), 401);
145+
assert_eq!(
146+
r#"Basic realm="publish""#,
147+
response.headers()["WWW-Authenticate"]
148+
);
149+
}

0 commit comments

Comments
(0)

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