use crate::Result; use axum::extract::State; use axum::routing::post; use axum::Form; use axum::Router; use axum::{ http::StatusCode, response::{IntoResponse, Response}, }; use chrono::Utc; use serde::Deserialize; use sqlx::PgPool; use unicode_segmentation::UnicodeSegmentation; use uuid::Uuid; #[derive(Debug, Deserialize)] struct FormData { email: String, name: String, } /// Returns `true` if the input satisfies all our validation constraints /// on subscriber names, `false` otherwise. pub fn is_valid_name(s: &str) -> bool { // `.trim()` returns a view over the input `s` without trailing // whitespace-like characters. // `.is_empty` checks if the view contains any character. let is_empty_or_whitespace = s.trim().is_empty(); // A grapheme is defined by the Unicode standard as a "user-perceived" // character: `å` is a single grapheme, but it is composed of two characters // (`a` and `̊`). // // `graphemes` returns an iterator over the graphemes in the input `s`. // `true` specifies that we want to use the extended grapheme definition set, // the recommended one. let is_too_long = s.graphemes(true).count() > 256; // Iterate over all characters in the input `s` to check if any of them matches // one of the characters in the forbidden array. let forbidden_characters = ['/', '(', ')', '"', '<', '>', '\\', '{', '}']; let contains_forbidden_characters = s.chars().any(|g| forbidden_characters.contains(&g)); // Return `false` if any of our conditions have been violated !(is_empty_or_whitespace || is_too_long || contains_forbidden_characters) } #[tracing::instrument( name = "Adding a new subscriber", skip(form, pool), fields( request_id = %Uuid::new_v4(), subscriber_email = %form.email, subscriber_name = %form.name ) )] pub async fn subscribe(State(pool): State, Form(form): Form) -> Response { if !is_valid_name(&form.name) { tracing::error!("Failed to add subscriber to the database"); return (StatusCode::BAD_REQUEST, "Invalid name").into_response(); } match insert_subscriber(&pool, &form).await { Ok(_) => { tracing::info!("Subscriber added to the database"); return (StatusCode::OK,).into_response(); } Err(_) => { tracing::error!("Failed to add subscriber to the database"); return (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong").into_response(); } } } #[tracing::instrument( name = "Saving new subscriber details in the database", skip(form, pool) )] pub async fn insert_subscriber(pool: &PgPool, form: &FormData) -> Result<()> { let _ = sqlx::query!( r#" INSERT INTO subscriptions (id, email, name, subscribed_at) VALUES ($1, $2, $3, $4) "#, Uuid::new_v4(), form.email, form.name, Utc::now() ) // We use `get_ref` to get an immutable reference to the `PgConnection` // wrapped by `web::Data`. .execute(pool) .await .map_err(|e| { tracing::error!("Failed to execute query: {:?}", e); e }); Ok(()) } pub fn routes_subscriptions(pool: PgPool) -> Router { Router::new() .route("/subscriptions", post(subscribe)) .with_state(pool) }