2024-02-10 21:33:01 +01:00
|
|
|
use crate::Result;
|
2024-01-28 22:22:29 +01:00
|
|
|
use axum::extract::State;
|
2024-01-01 21:02:31 +01:00
|
|
|
use axum::routing::post;
|
2024-01-28 22:22:29 +01:00
|
|
|
use axum::Form;
|
2023-12-30 22:21:57 +01:00
|
|
|
use axum::Router;
|
2024-02-10 21:33:01 +01:00
|
|
|
use axum::{
|
|
|
|
|
http::StatusCode,
|
|
|
|
|
response::{IntoResponse, Response},
|
|
|
|
|
};
|
2024-01-28 22:22:29 +01:00
|
|
|
use chrono::Utc;
|
2024-01-02 14:39:30 +01:00
|
|
|
use serde::Deserialize;
|
2024-01-28 22:22:29 +01:00
|
|
|
use sqlx::PgPool;
|
2024-02-10 21:33:01 +01:00
|
|
|
use unicode_segmentation::UnicodeSegmentation;
|
2024-01-28 22:22:29 +01:00
|
|
|
use uuid::Uuid;
|
2024-01-02 14:39:30 +01:00
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
struct FormData {
|
|
|
|
|
email: String,
|
|
|
|
|
name: String,
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-10 21:33:01 +01:00
|
|
|
/// 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)
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-04 13:48:31 +01:00
|
|
|
#[tracing::instrument(
|
|
|
|
|
name = "Adding a new subscriber",
|
|
|
|
|
skip(form, pool),
|
|
|
|
|
fields(
|
|
|
|
|
request_id = %Uuid::new_v4(),
|
|
|
|
|
subscriber_email = %form.email,
|
|
|
|
|
subscriber_name = %form.name
|
|
|
|
|
)
|
|
|
|
|
)]
|
2024-02-10 21:33:01 +01:00
|
|
|
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();
|
|
|
|
|
}
|
2024-02-04 13:48:31 +01:00
|
|
|
match insert_subscriber(&pool, &form).await {
|
|
|
|
|
Ok(_) => {
|
|
|
|
|
tracing::info!("Subscriber added to the database");
|
2024-02-10 21:33:01 +01:00
|
|
|
return (StatusCode::OK,).into_response();
|
2024-02-04 13:48:31 +01:00
|
|
|
}
|
|
|
|
|
Err(_) => {
|
|
|
|
|
tracing::error!("Failed to add subscriber to the database");
|
2024-02-10 21:33:01 +01:00
|
|
|
return (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong").into_response();
|
2024-02-04 13:48:31 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tracing::instrument(
|
|
|
|
|
name = "Saving new subscriber details in the database",
|
|
|
|
|
skip(form, pool)
|
|
|
|
|
)]
|
2024-02-10 21:33:01 +01:00
|
|
|
pub async fn insert_subscriber(pool: &PgPool, form: &FormData) -> Result<()> {
|
|
|
|
|
let _ = sqlx::query!(
|
2024-01-28 22:22:29 +01:00
|
|
|
r#"
|
2024-02-04 13:48:31 +01:00
|
|
|
INSERT INTO subscriptions (id, email, name, subscribed_at)
|
|
|
|
|
VALUES ($1, $2, $3, $4)
|
|
|
|
|
"#,
|
2024-01-28 22:22:29 +01:00
|
|
|
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`.
|
2024-02-04 13:48:31 +01:00
|
|
|
.execute(pool)
|
2024-01-30 21:43:32 +01:00
|
|
|
.await
|
2024-02-04 13:48:31 +01:00
|
|
|
.map_err(|e| {
|
|
|
|
|
tracing::error!("Failed to execute query: {:?}", e);
|
|
|
|
|
e
|
2024-02-10 21:33:01 +01:00
|
|
|
});
|
2024-02-04 13:48:31 +01:00
|
|
|
Ok(())
|
2024-01-02 14:39:30 +01:00
|
|
|
}
|
2023-12-30 22:21:57 +01:00
|
|
|
|
2024-01-28 22:22:29 +01:00
|
|
|
pub fn routes_subscriptions(pool: PgPool) -> Router {
|
|
|
|
|
Router::new()
|
|
|
|
|
.route("/subscriptions", post(subscribe))
|
|
|
|
|
.with_state(pool)
|
2023-12-30 22:21:57 +01:00
|
|
|
}
|