diff --git a/src/handlers.rs b/src/handlers.rs new file mode 100644 index 0000000..3f757cd --- /dev/null +++ b/src/handlers.rs @@ -0,0 +1,183 @@ +use std::{error::Error, fmt::Debug, ops::Range}; + +use axum::{ + extract::{Form, Path}, + http::StatusCode, + response::{IntoResponse, Redirect, Response}, +}; +use serde::Deserialize; +use tower_sessions::Session; +use unicode_segmentation::UnicodeSegmentation; + +use crate::{templates::*, User}; + +const SIGNUP_KEY: &str = "meow"; + +#[Error(desc = "Could not create user.")] +#[non_exhaustive] +pub struct CreateUserError(#[from] CreateUserErrorKind); + +impl IntoResponse for CreateUserError { + fn into_response(self) -> Response { + (StatusCode::FORBIDDEN, format!("{:?}", self.0)).into_response() + } +} + +#[Error] +#[non_exhaustive] +pub enum CreateUserErrorKind { + AlreadyExists, + #[error(desc = "Usernames must be between 1 and 20 characters long")] + BadUsername, + PasswordMismatch, + #[error(desc = "Password must have at least 4 and at most 50 characters")] + BadPassword, + #[error(desc = "Display name must be less than 100 characters long")] + BadDisplayname, + BadEmail, + BadPayment, +} + +#[derive(Debug, Default, Deserialize, PartialEq, Eq)] +pub struct SignupForm { + pub username: String, + #[serde(default, deserialize_with = "empty_string_as_none")] + pub displayname: Option, + pub email: String, + pub password: String, + pub pw_verify: String, +} + +/// Displays the signup form. +pub async fn get_signup() -> impl IntoResponse { + SignupPage { + ..Default::default() + } +} + +/// Receives the form with the user signup fields filled out. +pub async fn post_signup( + session: Session, + Form(form): Form, +) -> Result { + let user = verify_user(&form).await?; + session.insert(SIGNUP_KEY, user).await.unwrap(); + + Ok(Redirect::to( + "https://buy.stripe.com/test_eVa6rrb7ygjNbwk000", + )) +} + +pub async fn get_edit_signup( + session: Session, + receipt: Option>, +) -> Result { + Ok(()) +} + +pub async fn post_edit_signup( + session: Session, + Form(form): Form, +) -> Result { + Ok(()) +} + +/// Called from Stripe with the receipt of payment. +pub async fn signup_success(session: Session, receipt: Option>) -> impl IntoResponse { + let user: User = session.get(SIGNUP_KEY).await.unwrap().unwrap_or_default(); + if user == User::default() { + return SignupErrorPage("who you?".to_string()).into_response(); + } + + // TODO: check Stripe for the receipt, verify it's legit + SignupSuccessPage(user).into_response() +} + +//-************************************************************************ +// helpers +//-************************************************************************ +async fn verify_user(form: &SignupForm) -> Result { + let username = form.username.trim(); + let password = form.password.trim(); + let verify = form.pw_verify.trim(); + + let name_len = username.graphemes(true).size_hint().1.unwrap(); + // we are not ascii exclusivists around here + if !(1..=20).contains(&name_len) { + return Err(CreateUserErrorKind::BadUsername.into()); + } + + if password != verify { + return Err(CreateUserErrorKind::PasswordMismatch.into()); + } + let pwlen = password.graphemes(true).size_hint().1.unwrap_or(0); + if !(4..=50).contains(&pwlen) { + return Err(CreateUserErrorKind::BadPassword.into()); + } + + // clean up the optionals + let displayname = validate_optional_length( + &form.displayname, + 0..100, + CreateUserErrorKind::BadDisplayname, + )?; + + let email = validate_length(&form.email, 5..30, CreateUserErrorKind::BadEmail)?; + + let user = User { + username: username.to_string(), + displayname, + email, + password: password.to_string(), + pw_verify: verify.to_string(), + }; + + Ok(user) +} + +pub(crate) fn empty_string_as_none<'de, D, T>(de: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, + T: std::str::FromStr, + T::Err: std::fmt::Display, +{ + let opt = as serde::Deserialize>::deserialize(de)?; + match opt.as_deref() { + None | Some("") => Ok(None), + Some(s) => std::str::FromStr::from_str(s) + .map_err(serde::de::Error::custom) + .map(Some), + } +} + +pub(crate) fn validate_optional_length( + opt: &Option, + len_range: Range, + err: E, +) -> Result, E> { + if let Some(opt) = opt { + let opt = opt.trim(); + let len = opt.graphemes(true).size_hint().1.unwrap(); + if !len_range.contains(&len) { + Err(err) + } else { + Ok(Some(opt.to_string())) + } + } else { + Ok(None) + } +} + +pub(crate) fn validate_length( + thing: &str, + len_range: Range, + err: E, +) -> Result { + let thing = thing.trim(); + let len = thing.graphemes(true).size_hint().1.unwrap(); + if !len_range.contains(&len) { + Err(err) + } else { + Ok(thing.to_string()) + } +} diff --git a/src/main.rs b/src/main.rs index b78866b..bfabdab 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,74 +1,19 @@ use std::{ - error::Error, fmt::{Debug, Display}, net::SocketAddr, - ops::Range, }; -use askama::Template; -use axum::{ - extract::{Form, Path}, - http::StatusCode, - response::{IntoResponse, Redirect, Response}, - routing::get, - Router, -}; +use axum::{routing::get, Router}; use serde::{Deserialize, Serialize}; -use tower_sessions::{MemoryStore, Session, SessionManagerLayer}; -use unicode_segmentation::UnicodeSegmentation; +use tower_sessions::{MemoryStore, SessionManagerLayer}; #[macro_use] extern crate justerror; -const SIGNUP_KEY: &str = "meow"; +mod handlers; +use handlers::*; -#[derive(Default, Deserialize, Serialize)] -struct Counter(usize); - -/// Displays the signup form. -async fn get_signup() -> impl IntoResponse { - SignupPage { - ..Default::default() - } -} - -/// Receives the form with the user signup fields filled out. -async fn post_signup( - session: Session, - Form(form): Form, -) -> Result { - let user = verify_user(&form).await?; - session.insert(SIGNUP_KEY, user).await.unwrap(); - - Ok(Redirect::to( - "https://buy.stripe.com/test_eVa6rrb7ygjNbwk000", - )) -} - -async fn get_edit_signup( - session: Session, - receipt: Option>, -) -> Result { - Ok(()) -} - -async fn post_edit_signup( - session: Session, - Form(form): Form, -) -> Result { - Ok(()) -} - -/// Called from Stripe with the receipt of payment. -async fn signup_success(session: Session, receipt: Option>) -> impl IntoResponse { - let user: User = session.get(SIGNUP_KEY).await.unwrap().unwrap_or_default(); - if user == User::default() { - return SignupErrorPage("who you?".to_string()).into_response(); - } - - // TODO: check Stripe for the receipt, verify it's legit - SignupSuccessPage(user).into_response() -} +mod templates; #[tokio::main] async fn main() { @@ -88,60 +33,6 @@ async fn main() { .unwrap(); } -#[Error(desc = "Could not create user.")] -#[non_exhaustive] -pub struct CreateUserError(#[from] CreateUserErrorKind); - -impl IntoResponse for CreateUserError { - fn into_response(self) -> Response { - (StatusCode::FORBIDDEN, format!("{:?}", self.0)).into_response() - } -} - -#[Error] -#[non_exhaustive] -pub enum CreateUserErrorKind { - AlreadyExists, - #[error(desc = "Usernames must be between 1 and 20 characters long")] - BadUsername, - PasswordMismatch, - #[error(desc = "Password must have at least 4 and at most 50 characters")] - BadPassword, - #[error(desc = "Display name must be less than 100 characters long")] - BadDisplayname, - BadEmail, - BadPayment, -} - -#[derive(Debug, Default, Deserialize, PartialEq, Eq)] -pub struct SignupForm { - pub username: String, - #[serde(default, deserialize_with = "empty_string_as_none")] - pub displayname: Option, - pub email: String, - pub password: String, - pub pw_verify: String, -} - -#[derive(Debug, Default, Template, Deserialize, Serialize, PartialEq, Eq)] -#[template(path = "signup.html")] -pub struct SignupPage { - pub username: String, - pub displayname: Option, - pub email: Option, - pub password: String, - pub pw_verify: String, - pub receipt: String, -} - -#[derive(Debug, Clone, Template, Default, Deserialize, Serialize, PartialEq, Eq)] -#[template(path = "signup_success.html")] -pub struct SignupSuccessPage(pub User); - -#[derive(Debug, Clone, Template, Default, Deserialize, Serialize, PartialEq, Eq)] -#[template(path = "signup_error.html")] -pub struct SignupErrorPage(pub String); - #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct User { pub username: String, @@ -179,89 +70,3 @@ impl Display for User { write!(f, "Username: {uname}\nDisplayname: {dname}\nEmail: {email}") } } - -async fn verify_user(form: &SignupForm) -> Result { - let username = form.username.trim(); - let password = form.password.trim(); - let verify = form.pw_verify.trim(); - - let name_len = username.graphemes(true).size_hint().1.unwrap(); - // we are not ascii exclusivists around here - if !(1..=20).contains(&name_len) { - return Err(CreateUserErrorKind::BadUsername.into()); - } - - if password != verify { - return Err(CreateUserErrorKind::PasswordMismatch.into()); - } - let pwlen = password.graphemes(true).size_hint().1.unwrap_or(0); - if !(4..=50).contains(&pwlen) { - return Err(CreateUserErrorKind::BadPassword.into()); - } - - // clean up the optionals - let displayname = validate_optional_length( - &form.displayname, - 0..100, - CreateUserErrorKind::BadDisplayname, - )?; - - let email = validate_length(&form.email, 5..30, CreateUserErrorKind::BadEmail)?; - - let user = User { - username: username.to_string(), - displayname, - email, - password: password.to_string(), - pw_verify: verify.to_string(), - }; - - Ok(user) -} - -pub(crate) fn empty_string_as_none<'de, D, T>(de: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, - T: std::str::FromStr, - T::Err: std::fmt::Display, -{ - let opt = as serde::Deserialize>::deserialize(de)?; - match opt.as_deref() { - None | Some("") => Ok(None), - Some(s) => std::str::FromStr::from_str(s) - .map_err(serde::de::Error::custom) - .map(Some), - } -} - -pub(crate) fn validate_optional_length( - opt: &Option, - len_range: Range, - err: E, -) -> Result, E> { - if let Some(opt) = opt { - let opt = opt.trim(); - let len = opt.graphemes(true).size_hint().1.unwrap(); - if !len_range.contains(&len) { - Err(err) - } else { - Ok(Some(opt.to_string())) - } - } else { - Ok(None) - } -} - -pub(crate) fn validate_length( - thing: &str, - len_range: Range, - err: E, -) -> Result { - let thing = thing.trim(); - let len = thing.graphemes(true).size_hint().1.unwrap(); - if !len_range.contains(&len) { - Err(err) - } else { - Ok(thing.to_string()) - } -} diff --git a/src/templates.rs b/src/templates.rs new file mode 100644 index 0000000..f77dad0 --- /dev/null +++ b/src/templates.rs @@ -0,0 +1,23 @@ +use askama::Template; +use serde::{Deserialize, Serialize}; + +use crate::User; + +#[derive(Debug, Default, Template, Deserialize, Serialize, PartialEq, Eq)] +#[template(path = "signup.html")] +pub struct SignupPage { + pub username: String, + pub displayname: Option, + pub email: Option, + pub password: String, + pub pw_verify: String, + pub receipt: String, +} + +#[derive(Debug, Clone, Template, Default, Deserialize, Serialize, PartialEq, Eq)] +#[template(path = "signup_success.html")] +pub struct SignupSuccessPage(pub User); + +#[derive(Debug, Clone, Template, Default, Deserialize, Serialize, PartialEq, Eq)] +#[template(path = "signup_error.html")] +pub struct SignupErrorPage(pub String);