HX-Redirect to handle redirections with htmx
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
pub mod change_password;
|
mod change_password;
|
||||||
pub mod dashboard;
|
mod dashboard;
|
||||||
pub mod newsletters;
|
mod logout;
|
||||||
|
mod newsletters;
|
||||||
|
|
||||||
use crate::{routes::error_chain_fmt, session_state::TypedSession, templates::ErrorTemplate};
|
use crate::{routes::error_chain_fmt, templates::ErrorTemplate};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::{
|
use axum::{
|
||||||
Json,
|
Json,
|
||||||
@@ -10,6 +11,7 @@ use axum::{
|
|||||||
};
|
};
|
||||||
pub use change_password::*;
|
pub use change_password::*;
|
||||||
pub use dashboard::*;
|
pub use dashboard::*;
|
||||||
|
pub use logout::*;
|
||||||
pub use newsletters::*;
|
pub use newsletters::*;
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
|
|
||||||
@@ -69,9 +71,3 @@ impl IntoResponse for AdminError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(name = "Logging out", skip(session))]
|
|
||||||
pub async fn logout(session: TypedSession) -> Result<Response, AdminError> {
|
|
||||||
session.clear().await;
|
|
||||||
Ok(Redirect::to("/login").into_response())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width" />
|
|
||||||
<title>Change password</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<form action="/admin/password" method="post">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="current_password"
|
|
||||||
placeholder="Current password"
|
|
||||||
/>
|
|
||||||
<input type="password" name="new_password" placeholder="New password" />
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="new_password_check"
|
|
||||||
placeholder="Confirm new password"
|
|
||||||
/>
|
|
||||||
<button type="submit">Change password</button>
|
|
||||||
</form>
|
|
||||||
{}
|
|
||||||
<p><a href="/admin/dashboard">Back</a></p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width" />
|
|
||||||
<title>Admin dashboard</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<p>Welcome {}!</p>
|
|
||||||
<p>Available actions:</p>
|
|
||||||
<ol>
|
|
||||||
<li><a href="/admin/password">Change password</a></li>
|
|
||||||
<li><a href="/admin/newsletters">Send a newsletter</a></li>
|
|
||||||
<li>
|
|
||||||
<form name="logoutForm" action="/admin/logout" method="post">
|
|
||||||
<input type="submit" value="Logout" />
|
|
||||||
</form>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width" />
|
|
||||||
<title>Send a newsletter</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<form action="/admin/newsletters" method="post">
|
|
||||||
<input type="text" name="title" placeholder="Subject" />
|
|
||||||
<input type="text" name="html" placeholder="Content (HTML)" />
|
|
||||||
<input type="text" name="text" placeholder="Content (text)" />
|
|
||||||
<input hidden type="text" name="idempotency_key" value="{}" />
|
|
||||||
<button type="submit">Send</button>
|
|
||||||
</form>
|
|
||||||
{}
|
|
||||||
<p><a href="/admin/dashboard">Back</a></p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
14
src/routes/admin/logout.rs
Normal file
14
src/routes/admin/logout.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
use crate::{routes::AdminError, session_state::TypedSession};
|
||||||
|
use axum::{
|
||||||
|
http::HeaderMap,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
|
||||||
|
#[tracing::instrument(name = "Logging out", skip(session))]
|
||||||
|
pub async fn logout(session: TypedSession) -> Result<Response, AdminError> {
|
||||||
|
session.clear().await;
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert("HX-Redirect", "/login".parse().unwrap());
|
||||||
|
Ok((StatusCode::OK, headers).into_response())
|
||||||
|
}
|
||||||
@@ -6,12 +6,12 @@ use crate::{
|
|||||||
templates::ErrorTemplate,
|
templates::ErrorTemplate,
|
||||||
};
|
};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
use axum::http::{HeaderMap, StatusCode};
|
||||||
use axum::{
|
use axum::{
|
||||||
Form, Json,
|
Form, Json,
|
||||||
extract::State,
|
extract::State,
|
||||||
response::{Html, IntoResponse, Redirect, Response},
|
response::{Html, IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use reqwest::StatusCode;
|
|
||||||
use secrecy::SecretString;
|
use secrecy::SecretString;
|
||||||
|
|
||||||
#[derive(thiserror::Error)]
|
#[derive(thiserror::Error)]
|
||||||
@@ -75,7 +75,7 @@ pub async fn post_login(
|
|||||||
connection_pool, ..
|
connection_pool, ..
|
||||||
}): State<AppState>,
|
}): State<AppState>,
|
||||||
Form(form): Form<LoginFormData>,
|
Form(form): Form<LoginFormData>,
|
||||||
) -> Result<Redirect, LoginError> {
|
) -> Result<Response, LoginError> {
|
||||||
let credentials = Credentials {
|
let credentials = Credentials {
|
||||||
username: form.username.clone(),
|
username: form.username.clone(),
|
||||||
password: form.password,
|
password: form.password,
|
||||||
@@ -104,7 +104,10 @@ pub async fn post_login(
|
|||||||
.insert_username(form.username)
|
.insert_username(form.username)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| LoginError::UnexpectedError(e.into()))?;
|
.map_err(|e| LoginError::UnexpectedError(e.into()))?;
|
||||||
Ok(Redirect::to("/admin/dashboard"))
|
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert("HX-Redirect", "/admin/dashboard".parse().unwrap());
|
||||||
|
Ok((StatusCode::OK, headers).into_response())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,14 @@
|
|||||||
<p class="mt-2 text-gray-600">
|
<p class="mt-2 text-gray-600">
|
||||||
Connected as <span class="font-bold">{{ username }}</span>
|
Connected as <span class="font-bold">{{ username }}</span>
|
||||||
</p>
|
</p>
|
||||||
<form action="/admin/logout" method="post" class="inline">
|
<button hx-post="/admin/logout"
|
||||||
<button type="submit"
|
type="submit"
|
||||||
class="flex items-center text-sm text-gray-500 hover:text-red-600 transition-colors cursor-pointer gap-1">
|
class="flex items-center text-sm text-gray-500 hover:text-red-600 transition-colors cursor-pointer gap-1">
|
||||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>Logout</span>
|
<span>Logout</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||||
<div class="bg-white rounded-lg shadow-md p-6 border border-gray-200">
|
<div class="bg-white rounded-lg shadow-md p-6 border border-gray-200">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::helpers::{TestApp, assert_is_redirect_to};
|
use crate::helpers::{TestApp, assert_is_redirect_to};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn an_error_flash_message_is_set_on_failure() {
|
async fn an_error_html_fragment_is_returned_on_failure() {
|
||||||
let app = TestApp::spawn().await;
|
let app = TestApp::spawn().await;
|
||||||
|
|
||||||
let login_body = serde_json::json!({
|
let login_body = serde_json::json!({
|
||||||
@@ -11,11 +11,10 @@ async fn an_error_flash_message_is_set_on_failure() {
|
|||||||
|
|
||||||
let response = app.post_login(&login_body).await;
|
let response = app.post_login(&login_body).await;
|
||||||
|
|
||||||
assert_eq!(response.status().as_u16(), 303);
|
assert_eq!(response.status().as_u16(), 200);
|
||||||
assert_is_redirect_to(&response, "/login");
|
|
||||||
|
|
||||||
let login_page_html = app.get_login_html().await;
|
let response_html = response.text().await.unwrap();
|
||||||
assert!(login_page_html.contains("Authentication failed"));
|
assert!(response_html.contains("Invalid credentials"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -31,5 +30,6 @@ async fn login_redirects_to_admin_dashboard_after_login_success() {
|
|||||||
assert_is_redirect_to(&response, "/admin/dashboard");
|
assert_is_redirect_to(&response, "/admin/dashboard");
|
||||||
|
|
||||||
let html_page = app.get_admin_dashboard_html().await;
|
let html_page = app.get_admin_dashboard_html().await;
|
||||||
assert!(html_page.contains(&format!("Welcome {}", app.test_user.username)));
|
assert!(html_page.contains("Connected as"));
|
||||||
|
assert!(html_page.contains(&app.test_user.username));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user