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 08d1c03

Browse files
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

‎src/authentication/middleware.rs

Lines changed: 10 additions & 3 deletions
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");

‎src/routes/admin/mod.rs

Lines changed: 2 additions & 0 deletions
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::*;

‎src/routes/admin/newsletters/get.rs

Lines changed: 55 additions & 0 deletions
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+
}

‎src/routes/admin/newsletters/mod.rs

Lines changed: 5 additions & 0 deletions
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::*;

‎src/routes/admin/newsletters/post.rs

Lines changed: 88 additions & 0 deletions
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+
}

‎src/routes/admin/password/post.rs

Lines changed: 5 additions & 18 deletions
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();

‎src/routes/mod.rs

Lines changed: 0 additions & 2 deletions
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)

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