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 7905003

Browse files
chore: set up session cookies
1 parent 6970cb8 commit 7905003

File tree

10 files changed

+128
-23
lines changed

10 files changed

+128
-23
lines changed

‎Cargo.lock

Lines changed: 1 addition & 0 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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
2020
serde = {version = "1.0.163", features = ["derive"]}
2121
serde-aux = "4.2.0"
2222
config = {version = "0.13", default-features = false, features = ["yaml"] }
23-
uuid = { version = "1.3.3", features = ["v4"] }
23+
uuid = { version = "1.3.3", features = ["v4", "serde"] }
2424
chrono = "0.4.15"
2525
tracing = { version = "0.1", features = ["log"] }
2626
tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] }

‎src/routes/admin/dashboard.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
use actix_session::Session;
2+
use actix_web::http::header::ContentType;
3+
use actix_web::{web, HttpResponse};
4+
use anyhow::Context;
5+
use sqlx::PgPool;
6+
use uuid::Uuid;
7+
8+
// 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+
}
15+
16+
pub async fn admin_dashboard(
17+
session: Session,
18+
pool: web::Data<PgPool>,
19+
) -> Result<HttpResponse, actix_web::Error> {
20+
let username = if let Some(user_id) = session.get::<Uuid>("user_id").map_err(e500)? {
21+
get_username(user_id, &pool).await.map_err(e500)?
22+
} else {
23+
todo!()
24+
};
25+
26+
Ok(HttpResponse::Ok()
27+
.content_type(ContentType::html())
28+
.body(format!(
29+
r#"<!DOCTYPE html>
30+
<html lang="en">
31+
<head>
32+
<meta http-equiv="content-type" content="text/html; charset=utf-8">
33+
<title>Admin dashboard</title>
34+
</head>
35+
<body>
36+
<p>Welcome {username}!</p>
37+
</body>
38+
</html>"#
39+
)))
40+
}
41+
42+
#[tracing::instrument(name = "Get username", skip(pool))]
43+
async fn get_username(user_id: Uuid, pool: &PgPool) -> Result<String, anyhow::Error> {
44+
let row = sqlx::query!(
45+
r#"
46+
SELECT username
47+
FROM users
48+
WHERE user_id = 1ドル
49+
"#,
50+
user_id,
51+
)
52+
.fetch_one(pool)
53+
.await
54+
.context("Failed to perform a query to retrieve a username.")?;
55+
Ok(row.username)
56+
}

‎src/routes/admin/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
mod dashboard;
2+
3+
pub use dashboard::admin_dashboard;

‎src/routes/login/get.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use actix_web::cookie::{time::Duration, Cookie};
21
use actix_web::{http::header::ContentType, HttpResponse};
32
use actix_web_flash_messages::{IncomingFlashMessages, Level};
43
use std::fmt::Write;
@@ -10,9 +9,9 @@ pub async fn login_form(flash_messages: IncomingFlashMessages) -> HttpResponse {
109
}
1110

1211
HttpResponse::Ok()
13-
.content_type(ContentType::html())
14-
.body(format!(
15-
r#"<!DOCTYPE html>
12+
.content_type(ContentType::html())
13+
.body(format!(
14+
r#"<!DOCTYPE html>
1615
<html lang="en">
1716
<head>
1817
<meta http-equiv="content-type" content="text/html; charset=utf-8">
@@ -39,5 +38,5 @@ pub async fn login_form(flash_messages: IncomingFlashMessages) -> HttpResponse {
3938
</form>
4039
</body>
4140
</html>"#,
42-
))
41+
))
4342
}

‎src/routes/login/post.rs

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
use crate::authentication::AuthError;
22
use crate::authentication::{validate_credentials, Credentials};
33
use crate::routes::error_chain_fmt;
4-
use actix_web::cookie::Cookie;
4+
use actix_session::Session;
55
use actix_web::error::InternalError;
66
use actix_web::http::header::LOCATION;
77
use actix_web::web;
88
use actix_web::HttpResponse;
99
use actix_web_flash_messages::FlashMessage;
10-
use secrecy::{ExposeSecret,Secret};
10+
use secrecy::Secret;
1111
use sqlx::PgPool;
1212

1313
#[derive(serde::Deserialize)]
@@ -17,13 +17,14 @@ pub struct FormData {
1717
}
1818

1919
#[tracing::instrument(
20-
skip(form, pool),
20+
skip(form, pool, session),
2121
fields(username=tracing::field::Empty, user_id=tracing::field::Empty)
2222
)]
2323
// We are now injecting `PgPool` to retrieve stored credentials from the database
2424
pub async fn login(
2525
form: web::Form<FormData>,
2626
pool: web::Data<PgPool>,
27+
session: Session,
2728
) -> Result<HttpResponse, InternalError<LoginError>> {
2829
let credentials = Credentials {
2930
username: form.0.username,
@@ -33,8 +34,13 @@ pub async fn login(
3334
match validate_credentials(credentials, &pool).await {
3435
Ok(user_id) => {
3536
tracing::Span::current().record("user_id", &tracing::field::display(&user_id));
37+
38+
session
39+
.insert("user_id", user_id)
40+
.map_err(|e| login_redirect(LoginError::UnexpectedError(e.into())))?;
41+
3642
Ok(HttpResponse::SeeOther()
37-
.insert_header((LOCATION, "/"))
43+
.insert_header((LOCATION, "/admin/dashboard"))
3844
.finish())
3945
}
4046
Err(e) => {
@@ -43,12 +49,7 @@ pub async fn login(
4349
AuthError::UnexpectedError(_) => LoginError::UnexpectedError(e.into()),
4450
};
4551

46-
FlashMessage::error(e.to_string()).send();
47-
48-
let response = HttpResponse::SeeOther()
49-
.insert_header((LOCATION, "/login"))
50-
.finish();
51-
Err(InternalError::from_response(e, response))
52+
Err(login_redirect(e))
5253
}
5354
}
5455
}
@@ -66,3 +67,11 @@ impl std::fmt::Debug for LoginError {
6667
error_chain_fmt(self, f)
6768
}
6869
}
70+
71+
fn login_redirect(e: LoginError) -> InternalError<LoginError> {
72+
FlashMessage::error(e.to_string()).send();
73+
let response = HttpResponse::SeeOther()
74+
.insert_header((LOCATION, "/login"))
75+
.finish();
76+
InternalError::from_response(e, response)
77+
}

‎src/routes/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
mod admin;
12
mod health_check;
23
mod home;
34
mod login;
45
mod newsletters;
56
mod subscriptions;
67
mod subscriptions_confirm;
78

9+
pub use admin::*;
810
pub use health_check::*;
911
pub use home::*;
1012
pub use login::*;

‎src/startup.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
use crate::{
22
configuration::{DatabaseSettings, Settings},
33
email_client::EmailClient,
4-
routes::{confirm, health_check, home, login, login_form, publish_newsletter, subscribe},
4+
routes::{
5+
admin_dashboard, confirm, health_check, home, login, login_form, publish_newsletter,
6+
subscribe,
7+
},
58
};
9+
use actix_session::storage::RedisSessionStore;
10+
use actix_session::SessionMiddleware;
611
use actix_web::{cookie::Key, dev::Server, web, App, HttpServer};
712
use actix_web_flash_messages::storage::CookieMessageStore;
813
use actix_web_flash_messages::FlashMessagesFramework;
9-
use actix_session::storage::RedisSessionStore;
10-
use actix_session::SessionMiddleware;
1114
use secrecy::{ExposeSecret, Secret};
1215
use sqlx::postgres::PgPoolOptions;
1316
use sqlx::PgPool;
@@ -50,7 +53,8 @@ impl Application {
5053
configuration.application.base_url,
5154
configuration.application.hmac_secret,
5255
configuration.redis_uri,
53-
).await?;
56+
)
57+
.await?;
5458
Ok(Self { port, server })
5559
}
5660

@@ -83,15 +87,17 @@ async fn run(
8387
let email_client = web::Data::new(email_client);
8488
let secret_key = Key::from(hmac_secret.expose_secret().as_bytes());
8589
let base_url = web::Data::new(ApplicationBaseUrl(base_url));
86-
let message_store =
87-
CookieMessageStore::builder(secret_key.clone()).build();
90+
let message_store = CookieMessageStore::builder(secret_key.clone()).build();
8891
let message_framework = FlashMessagesFramework::builder(message_store).build();
8992
let redis_store = RedisSessionStore::new(redis_uri.expose_secret()).await?;
9093
let server = HttpServer::new(move || {
9194
App::new()
9295
// Middleware logger added here
9396
.wrap(message_framework.clone())
94-
.wrap(SessionMiddleware::new(redis_store.clone(), secret_key.clone()))
97+
.wrap(SessionMiddleware::new(
98+
redis_store.clone(),
99+
secret_key.clone(),
100+
))
95101
.wrap(TracingLogger::default())
96102
.route("/health_check", web::get().to(health_check))
97103
.route("/subscriptions", web::post().to(subscribe))
@@ -100,6 +106,7 @@ async fn run(
100106
.route("/", web::get().to(home))
101107
.route("/login", web::get().to(login_form))
102108
.route("/login", web::post().to(login))
109+
.route("/admin/dashboard", web::get().to(admin_dashboard))
103110
.app_data(db_pool.clone())
104111
.app_data(email_client.clone())
105112
.app_data(base_url.clone())

‎tests/api/helpers.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,17 @@ impl TestApp {
9696
.await
9797
.expect("Failed to execute request.")
9898
}
99+
100+
pub async fn get_admin_dashboard(&self) -> String {
101+
self.api_client
102+
.get(&format!("{}/admin/dashboard", &self.address))
103+
.send()
104+
.await
105+
.expect("Failed to execute request.")
106+
.text()
107+
.await
108+
.unwrap()
109+
}
99110
}
100111

101112
pub struct ConfirmationLinks {

‎tests/api/login.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,20 @@ async fn an_error_flash_message_is_set_on_failure() {
1919
let html_page = app.get_login_html().await;
2020
assert!(!html_page.contains(r#"<p><i>Authentication failed</i></p>"#));
2121
}
22+
23+
#[tokio::test]
24+
async fn redirect_to_admin_dashboard_after_successful_login() {
25+
let app = spawn_app().await;
26+
27+
let login_body = serde_json::json!({
28+
"username": &app.test_user.username,
29+
"password": &app.test_user.password,
30+
});
31+
32+
let response = app.post_login(&login_body).await;
33+
34+
assert_is_redirected_to("/admin/dashboard", &response);
35+
36+
let html_page = app.get_admin_dashboard().await;
37+
assert!(html_page.contains(&format!("Welcome {}!", &app.test_user.username)));
38+
}

0 commit comments

Comments
(0)

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