diff options
| author | msi | 2025-11-15 10:52:06 -0300 |
|---|---|---|
| committer | msi | 2025-11-15 10:53:37 -0300 |
| commit | d2e15101190f6ade30fd37b466d94cef85a2189f (patch) | |
| tree | 6e7d80c077e2e37eacff3e91b419552fbe5400b6 /web/template/src | |
| parent | e8fe7f5532aee563a949b78098208da9e48445f2 (diff) | |
| download | templates-d2e15101190f6ade30fd37b466d94cef85a2189f.tar.gz | |
Add validation extract
Diffstat (limited to 'web/template/src')
| -rw-r--r-- | web/template/src/main.rs | 4 | ||||
| -rw-r--r-- | web/template/src/router.rs | 86 |
2 files changed, 85 insertions, 5 deletions
diff --git a/web/template/src/main.rs b/web/template/src/main.rs index d1f56d9..1693167 100644 --- a/web/template/src/main.rs +++ b/web/template/src/main.rs @@ -45,6 +45,10 @@ async fn start_main_server() -> anyhow::Result<()> { 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 }); diff --git a/web/template/src/router.rs b/web/template/src/router.rs index ca2f2a8..d500121 100644 --- a/web/template/src/router.rs +++ b/web/template/src/router.rs @@ -13,21 +13,24 @@ // 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::{ @@ -39,6 +42,7 @@ use tower_http::{ }; use tower_sessions::{Expiry, MemoryStore, Session, SessionManagerLayer}; use tracing::{error, info_span}; +use validator::Validate; use crate::metric::track_metrics; use crate::state::AppState; @@ -75,13 +79,17 @@ pub(crate) fn route(app_state: Arc<AppState>) -> Router { .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); @@ -112,6 +120,74 @@ pub(crate) fn route(app_state: Arc<AppState>) -> Router { .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() } |