Skip to content

Commit 08d1c03

Browse files
committed
Feat: removed Basic authentication from newsletter endpoint and replaced it with session authentication
1 parent 479ee3c commit 08d1c03

File tree

9 files changed

+175
-192
lines changed

9 files changed

+175
-192
lines changed

Diff for: src/authentication/middleware.rs

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
use crate::session_state::TypedSession;
2+
use crate::utils::{e500, see_other};
23
use actix_web::body::MessageBody;
34
use actix_web::dev::{ServiceRequest, ServiceResponse};
4-
use actix_web::FromRequest;
5-
use actix_web::HttpMessage;
5+
use actix_web::error::InternalError;
6+
use actix_web::{FromRequest, HttpMessage};
67
use actix_web_lab::middleware::Next;
78
use std::ops::Deref;
89
use uuid::Uuid;
910

1011
#[derive(Copy, Clone, Debug)]
1112
pub struct UserId(Uuid);
13+
1214
impl std::fmt::Display for UserId {
1315
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1416
self.0.fmt(f)
1517
}
1618
}
19+
1720
impl Deref for UserId {
1821
type Target = Uuid;
22+
1923
fn deref(&self) -> &Self::Target {
2024
&self.0
2125
}
@@ -31,7 +35,10 @@ pub async fn reject_anonymous_users(
3135
}?;
3236

3337
match session.get_user_id().map_err(e500)? {
34-
Some(_) => next.call(req).await,
38+
Some(user_id) => {
39+
req.extensions_mut().insert(UserId(user_id));
40+
next.call(req).await
41+
}
3542
None => {
3643
let response = see_other("/login");
3744
let e = anyhow::anyhow!("The user has not logged in");

Diff for: src/routes/admin/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
mod dashboard;
22
mod logout;
33
mod password;
4+
mod newsletters;
45

56
pub use dashboard::admin_dashboard;
67
pub use logout::log_out;
78
pub use password::*;
9+
pub use newsletters::*;

Diff for: src/routes/admin/newsletters/get.rs

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
use actix_web::http::header::ContentType;
2+
use actix_web::HttpResponse;
3+
use actix_web_flash_messages::IncomingFlashMessages;
4+
use std::fmt::Write;
5+
6+
pub async fn publish_newsletter_form(flash_messages: IncomingFlashMessages) -> Result<HttpResponse, actix_web::Error> {
7+
let mut incoming_flash = String::new();
8+
for message in flash_messages.iter() {
9+
writeln!(incoming_flash, "<p><i>{}</i></p>", message.content()).unwrap();
10+
}
11+
Ok(HttpResponse::Ok()
12+
.content_type(ContentType::html())
13+
.body(format!(
14+
r#"<!DOCTYPE html>
15+
<html lang="en">
16+
<head>
17+
<meta http-equiv="content-type" content="text/html; charset=utf-8">
18+
<title>Publish Newsletter Issue</title>
19+
</head>
20+
<body>
21+
{incoming_flash}
22+
<form action="/admin/newsletters" method="post">
23+
<label>Title:<br>
24+
<input
25+
type="text"
26+
placeholder="Enter the issue title"
27+
name="title"
28+
>
29+
</label>
30+
<br>
31+
<label>Plain text content:<br>
32+
<textarea
33+
placeholder="Enter the content in plain text"
34+
name="text_content"
35+
rows="20"
36+
cols="50"
37+
></textarea>
38+
</label>
39+
<br>
40+
<label>HTML content:<br>
41+
<textarea
42+
placeholder="Enter the content in HTML format"
43+
name="html_content"
44+
rows="20"
45+
cols="50"
46+
></textarea>
47+
</label>
48+
<br>
49+
<button type="submit">Publish</button>
50+
</form>
51+
<p><a href="/admin/dashboard">&lt;- Back</a></p>
52+
</body>
53+
</html>"#,
54+
)))
55+
}

Diff for: src/routes/admin/newsletters/mod.rs

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
mod get;
2+
mod post;
3+
4+
pub use get::*;
5+
pub use post::*;

Diff for: src/routes/admin/newsletters/post.rs

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
use crate::authentication::UserId;
2+
use crate::domain::SubscriberEmail;
3+
use crate::email_client::EmailClient;
4+
use crate::utils::{e500, see_other};
5+
use actix_web::web::ReqData;
6+
use actix_web::{web, HttpResponse};
7+
use actix_web_flash_messages::FlashMessage;
8+
use anyhow::Context;
9+
use sqlx::PgPool;
10+
11+
#[derive(serde::Deserialize)]
12+
pub struct BodyData {
13+
title: String,
14+
content: Content,
15+
}
16+
17+
#[derive(serde::Deserialize)]
18+
pub struct Content {
19+
html: String,
20+
text: String,
21+
}
22+
23+
#[tracing::instrument(
24+
name = "Publish a newsletter issue",
25+
skip(body, pool, email_client, user_id),
26+
fields(user_id = %*user_id)
27+
)]
28+
pub async fn publish_newsletter(
29+
body: web::Form<BodyData>,
30+
pool: web::Data<PgPool>,
31+
email_client: web::Data<EmailClient>,
32+
user_id: ReqData<UserId>,
33+
) -> Result<HttpResponse, actix_web::Error> {
34+
let subscribers = get_confirmed_subscribers(&pool).await.map_err(e500)?;
35+
for subscriber in subscribers {
36+
match subscriber {
37+
Ok(subscriber) => {
38+
email_client
39+
.send_email(
40+
&subscriber.email,
41+
&body.title,
42+
&body.content.html,
43+
&body.content.text,
44+
)
45+
.await
46+
.with_context(|| {
47+
format!("Failed to send newsletter issue to {}", subscriber.email)
48+
}).map_err(e500)?;
49+
}
50+
Err(error) => {
51+
tracing::warn!(
52+
error.cause_chain = ?error,
53+
error.message = %error,
54+
"Skipping a confirmed subscriber. \
55+
Their stored contact details are invalid",
56+
);
57+
}
58+
}
59+
}
60+
FlashMessage::info("The newsletter issue has been published!").send();
61+
Ok(see_other("/admin/newsletters"))
62+
}
63+
64+
struct ConfirmedSubscriber {
65+
email: SubscriberEmail,
66+
}
67+
68+
#[tracing::instrument(name = "Get confirmed subscribers", skip(pool))]
69+
async fn get_confirmed_subscribers(
70+
pool: &PgPool,
71+
) -> Result<Vec<Result<ConfirmedSubscriber, anyhow::Error>>, anyhow::Error> {
72+
let confirmed_subscribers = sqlx::query!(
73+
r#"
74+
SELECT email
75+
FROM subscriptions
76+
WHERE status = 'confirmed'
77+
"#,
78+
)
79+
.fetch_all(pool)
80+
.await?
81+
.into_iter()
82+
.map(|r| match SubscriberEmail::parse(r.email) {
83+
Ok(email) => Ok(ConfirmedSubscriber { email }),
84+
Err(error) => Err(anyhow::anyhow!(error)),
85+
})
86+
.collect();
87+
Ok(confirmed_subscribers)
88+
}

Diff for: src/routes/admin/password/post.rs

+5-18
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1+
use crate::authentication::UserId;
12
use crate::authentication::{validate_credentials, AuthError, Credentials};
23
use crate::routes::admin::dashboard::get_username;
3-
use crate::session_state::TypedSession;
44
use crate::utils::{e500, see_other};
5-
use actix_web::error::InternalError;
65
use actix_web::{web, HttpResponse};
76
use actix_web_flash_messages::FlashMessage;
87
use secrecy::{ExposeSecret, Secret};
98
use sqlx::PgPool;
10-
use uuid::Uuid;
119

1210
#[derive(serde::Deserialize)]
1311
pub struct FormData {
@@ -16,23 +14,12 @@ pub struct FormData {
1614
new_password_check: Secret<String>,
1715
}
1816

19-
async fn reject_anonymous_users(session: TypedSession) -> Result<Uuid, actix_web::Error> {
20-
match session.get_user_id().map_err(e500)? {
21-
Some(user_id) => Ok(user_id),
22-
None => {
23-
let response = see_other("/login");
24-
let e = anyhow::anyhow!("The user has not logged in");
25-
Err(InternalError::from_response(e, response).into())
26-
}
27-
}
28-
}
29-
3017
pub async fn change_password(
3118
form: web::Form<FormData>,
32-
session: TypedSession,
3319
pool: web::Data<PgPool>,
20+
user_id: web::ReqData<UserId>,
3421
) -> Result<HttpResponse, actix_web::Error> {
35-
let user_id = reject_anonymous_users(session).await?;
22+
let user_id = user_id.into_inner();
3623

3724
if form.new_password.expose_secret() != form.new_password_check.expose_secret() {
3825
FlashMessage::error(
@@ -42,7 +29,7 @@ pub async fn change_password(
4229
return Ok(see_other("/admin/password"));
4330
};
4431

45-
let username = get_username(user_id, &pool).await.map_err(e500)?;
32+
let username = get_username(*user_id, &pool).await.map_err(e500)?;
4633

4734
let credentials = Credentials {
4835
username,
@@ -58,7 +45,7 @@ pub async fn change_password(
5845
};
5946
};
6047

61-
crate::authentication::change_password(user_id, form.0.new_password, &pool)
48+
crate::authentication::change_password(*user_id, form.0.new_password, &pool)
6249
.await
6350
.map_err(e500)?;
6451
FlashMessage::error("Your password has been changed.").send();

Diff for: src/routes/mod.rs

-2
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@ mod admin;
22
mod health_check;
33
mod home;
44
mod login;
5-
mod newsletters;
65
mod subscriptions;
76
mod subscriptions_confirm;
87

98
pub use admin::*;
109
pub use health_check::*;
1110
pub use home::*;
1211
pub use login::*;
13-
pub use newsletters::*;
1412
pub use subscriptions::*;
1513
pub use subscriptions_confirm::*;

0 commit comments

Comments
 (0)