feat: add input validation

This commit is contained in:
Sandro Eiler 2024-02-12 10:55:23 +01:00
parent d7d37341ba
commit 419be581b3
10 changed files with 271 additions and 122 deletions

View file

@ -1,4 +1,5 @@
use crate::Result;
use crate::domain::SubscriberEmail;
use crate::domain::{NewSubscriber, SubscriberName};
use axum::extract::State;
use axum::routing::post;
use axum::Form;
@ -10,7 +11,6 @@ use axum::{
use chrono::Utc;
use serde::Deserialize;
use sqlx::PgPool;
use unicode_segmentation::UnicodeSegmentation;
use uuid::Uuid;
#[derive(Debug, Deserialize)]
@ -19,32 +19,6 @@ struct FormData {
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),
@ -55,17 +29,24 @@ pub fn is_valid_name(s: &str) -> bool {
)
)]
pub async fn subscribe(State(pool): State<PgPool>, Form(form): Form<FormData>) -> 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 {
let name = match SubscriberName::parse(form.name) {
Ok(name) => name,
Err(_) => {
return (StatusCode::BAD_REQUEST, "Invalid name").into_response();
}
};
let email = match SubscriberEmail::parse(form.email) {
Ok(email) => email,
Err(_) => {
return (StatusCode::BAD_REQUEST, "Invalid email address").into_response();
}
};
let new_subscriber = NewSubscriber { email, name };
match insert_subscriber(&pool, &new_subscriber).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();
}
}
@ -73,17 +54,20 @@ pub async fn subscribe(State(pool): State<PgPool>, Form(form): Form<FormData>) -
#[tracing::instrument(
name = "Saving new subscriber details in the database",
skip(form, pool)
skip(new_subscriber, pool)
)]
pub async fn insert_subscriber(pool: &PgPool, form: &FormData) -> Result<()> {
pub async fn insert_subscriber(
pool: &PgPool,
new_subscriber: &NewSubscriber,
) -> Result<(), sqlx::Error> {
let _ = sqlx::query!(
r#"
INSERT INTO subscriptions (id, email, name, subscribed_at)
VALUES ($1, $2, $3, $4)
"#,
Uuid::new_v4(),
form.email,
form.name,
new_subscriber.email.as_ref(),
new_subscriber.name.as_ref(),
Utc::now()
)
// We use `get_ref` to get an immutable reference to the `PgConnection`