commit - e8fe7f5532aee563a949b78098208da9e48445f2
commit + d2e15101190f6ade30fd37b466d94cef85a2189f
blob - 9b0bebcc211a37fb3c319e992a6b8b8919de1f93
blob + e9d1ba5ff092fc6401f81e38f4cb452e7869f2fe
--- web/README.md
+++ web/README.md
* [x] Messages (like flask)
* [x] Sessions
* [x] CSRF
+* [x] Validation
* [ ] 404
blob - d266dac95ef710f4e07d61d0565c34bc52ca0566
blob + b7d3f32466ba2542cdc0e80e42a5e7062a020d9d
--- web/template/Cargo.toml
+++ web/template/Cargo.toml
metrics-exporter-prometheus = { version = "=0.17.2", default-features = false }
minijinja = "=2.12.0"
serde = { version = "=1.0.228", features = ["derive"] }
+thiserror = "2.0.17"
time = "=0.3.44"
tokio = { version = "=1.48.0", features = ["macros", "rt-multi-thread", "signal"] }
tower-http = { version = "=0.6.6", features = ["timeout", "trace", "fs", "request-id"] }
tower-sessions = "=0.14.0"
tracing = "=0.1.41"
tracing-subscriber = { version = "=0.3.20", features = ["env-filter"] }
+validator = { version = "=0.20.0", features = ["derive"] }
blob - d1f56d93282c97bddded2b3c6399390911d2364c
blob + 1693167744b435b06ce0ff681c4a17473b2e6c40
--- web/template/src/main.rs
+++ web/template/src/main.rs
env.add_template("content", include_str!("../templates/content.jinja"))?;
env.add_template("about", include_str!("../templates/about.jinja"))?;
env.add_template("csrf", include_str!("../templates/csrf.jinja"))?;
+ env.add_template(
+ "validation",
+ include_str!("../templates/validation.jinja"),
+ )?;
let app_state = Arc::new(state::AppState { env });
blob - ca2f2a800fa47ac104c61617c1a9d127a84700b4
blob + d500121b1749957b78c80b02e8cb69c0029480cd
--- web/template/src/router.rs
+++ web/template/src/router.rs
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
+
use std::sync::Arc;
use axum::{
- Form, Router,
- extract::State,
- http::{HeaderName, Request, StatusCode},
+ Router,
+ extract::{Form, FromRequest, Request, State, rejection::FormRejection},
+ http::{self, HeaderName, StatusCode},
middleware,
- response::{Html, IntoResponse, Redirect},
+ response::{Html, IntoResponse, Redirect, Response},
routing::get,
};
use axum_client_ip::{ClientIp, ClientIpSource};
use axum_csrf::{CsrfConfig, CsrfLayer, CsrfToken, Key};
use axum_messages::{Messages, MessagesManagerLayer};
use minijinja::context;
+use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
+use thiserror::Error;
use time::Duration;
use tower_http::{
request_id::{
};
use tower_sessions::{Expiry, MemoryStore, Session, SessionManagerLayer};
use tracing::{error, info_span};
+use validator::Validate;
use crate::metric::track_metrics;
use crate::state::AppState;
.route("/read-messages", get(read_messages_handler))
.route("/csrf", get(csrf_root).post(csrf_check_key))
.route("/ip", get(ip_handler))
+ .route(
+ "/validation",
+ get(get_validation_handler).post(post_validation_handler),
+ )
.layer(MessagesManagerLayer)
// TODO(msi): from config folder asssets
.nest_service("/assets", ServeDir::new("assets"))
.layer((
SetRequestIdLayer::new(x_request_id.clone(), MakeRequestUuid),
TraceLayer::new_for_http().make_span_with(
- |request: &Request<_>| {
+ |request: &http::Request<_>| {
// Log the request id as generated.
let request_id = request.headers().get(REQUEST_ID_HEADER);
.with_state(app_state)
}
+#[derive(Debug, Deserialize, Validate)]
+pub struct NameInput {
+ #[validate(length(min = 2, message = "Can not be empty"))]
+ pub name: String,
+}
+
+async fn get_validation_handler(
+ State(state): State<Arc<AppState>>,
+) -> Result<Html<String>, ServerError> {
+ let template = state.env.get_template("validation").unwrap();
+
+ let rendered = template.render(context! {}).unwrap();
+
+ Ok(Html(rendered))
+}
+
+async fn post_validation_handler(
+ ValidatedForm(input): ValidatedForm<NameInput>,
+) -> Html<String> {
+ Html(format!("<h1>Hello, {}!</h1>", input.name))
+}
+
+#[derive(Debug, Clone, Copy, Default)]
+pub struct ValidatedForm<T>(pub T);
+
+impl<T, S> FromRequest<S> for ValidatedForm<T>
+where
+ T: DeserializeOwned + Validate,
+ S: Send + Sync,
+ Form<T>: FromRequest<S, Rejection = FormRejection>,
+{
+ type Rejection = ServerError;
+
+ async fn from_request(
+ req: Request,
+ state: &S,
+ ) -> Result<Self, Self::Rejection> {
+ let Form(value) = Form::<T>::from_request(req, state).await?;
+ value.validate()?;
+ Ok(ValidatedForm(value))
+ }
+}
+
+#[derive(Debug, Error)]
+pub enum ServerError {
+ #[error(transparent)]
+ ValidationError(#[from] validator::ValidationErrors),
+
+ #[error(transparent)]
+ AxumFormRejection(#[from] FormRejection),
+}
+
+impl IntoResponse for ServerError {
+ fn into_response(self) -> Response {
+ match self {
+ ServerError::ValidationError(_) => {
+ let message = format!("Input validation error: [{self}]")
+ .replace('\n', ", ");
+ (StatusCode::BAD_REQUEST, message)
+ }
+ ServerError::AxumFormRejection(_) => {
+ (StatusCode::BAD_REQUEST, self.to_string())
+ }
+ }
+ .into_response()
+ }
+}
+
async fn ip_handler(ClientIp(ip): ClientIp) -> String {
ip.to_string()
}
blob - 6abc2fda11620fbecb3d72b3b37cc2b3dac02d2d
blob + ee0649398dbcf65170b3ec8c1a23ab209f7d4cc0
--- web/template/templates/layout.jinja
+++ web/template/templates/layout.jinja
<li><a href="/read-messages">Read Messages</a></li>
<li><a href="/csrf">Csrf</a></li>
<li><a href="/ip">Ip</a></li>
+ <li><a href="/validation">Validation</a></li>
</ul>
</nav>
<h1><h1>Hello, World web =]</h1>
blob - /dev/null
blob + e121f990b9365500592811c80c9fb26acd4dbbe4 (mode 644)
--- /dev/null
+++ web/template/templates/validation.jinja
+{% extends "layout" %}
+{% block title %}{{ super() }} | {{ title }} {% endblock %}
+{% block body %}
+<h1>{{ title }}</h1>
+<p>{{ about_text }}</p>
+ <form method="post" action="/validation">
+ <input type="text" name="name" value=""/>
+ <input id="button" type="submit" value="Submit" tabindex="4" />
+ </form>
+{% endblock %}