Re-org into better-defined modules.

This commit is contained in:
Joe Ardent 2024-02-25 18:25:04 -08:00
parent 883dfd67ea
commit 478464ac05
3 changed files with 211 additions and 200 deletions

183
src/handlers.rs Normal file
View file

@ -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<String>,
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<SignupForm>,
) -> Result<impl IntoResponse, CreateUserError> {
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<Path<String>>,
) -> Result<impl IntoResponse, CreateUserError> {
Ok(())
}
pub async fn post_edit_signup(
session: Session,
Form(form): Form<SignupForm>,
) -> Result<impl IntoResponse, CreateUserError> {
Ok(())
}
/// Called from Stripe with the receipt of payment.
pub async fn signup_success(session: Session, receipt: Option<Path<String>>) -> 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<User, CreateUserError> {
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<Option<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
let opt = <Option<String> 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<E: Error>(
opt: &Option<String>,
len_range: Range<usize>,
err: E,
) -> Result<Option<String>, 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<E: Error>(
thing: &str,
len_range: Range<usize>,
err: E,
) -> Result<String, E> {
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())
}
}

View file

@ -1,74 +1,19 @@
use std::{ use std::{
error::Error,
fmt::{Debug, Display}, fmt::{Debug, Display},
net::SocketAddr, net::SocketAddr,
ops::Range,
}; };
use askama::Template; use axum::{routing::get, Router};
use axum::{
extract::{Form, Path},
http::StatusCode,
response::{IntoResponse, Redirect, Response},
routing::get,
Router,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tower_sessions::{MemoryStore, Session, SessionManagerLayer}; use tower_sessions::{MemoryStore, SessionManagerLayer};
use unicode_segmentation::UnicodeSegmentation;
#[macro_use] #[macro_use]
extern crate justerror; extern crate justerror;
const SIGNUP_KEY: &str = "meow"; mod handlers;
use handlers::*;
#[derive(Default, Deserialize, Serialize)] mod templates;
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<SignupForm>,
) -> Result<impl IntoResponse, CreateUserError> {
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<Path<String>>,
) -> Result<impl IntoResponse, CreateUserError> {
Ok(())
}
async fn post_edit_signup(
session: Session,
Form(form): Form<SignupForm>,
) -> Result<impl IntoResponse, CreateUserError> {
Ok(())
}
/// Called from Stripe with the receipt of payment.
async fn signup_success(session: Session, receipt: Option<Path<String>>) -> 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()
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
@ -88,60 +33,6 @@ async fn main() {
.unwrap(); .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<String>,
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<String>,
pub email: Option<String>,
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)] #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct User { pub struct User {
pub username: String, pub username: String,
@ -179,89 +70,3 @@ impl Display for User {
write!(f, "Username: {uname}\nDisplayname: {dname}\nEmail: {email}") write!(f, "Username: {uname}\nDisplayname: {dname}\nEmail: {email}")
} }
} }
async fn verify_user(form: &SignupForm) -> Result<User, CreateUserError> {
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<Option<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
let opt = <Option<String> 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<E: Error>(
opt: &Option<String>,
len_range: Range<usize>,
err: E,
) -> Result<Option<String>, 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<E: Error>(
thing: &str,
len_range: Range<usize>,
err: E,
) -> Result<String, E> {
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())
}
}

23
src/templates.rs Normal file
View file

@ -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<String>,
pub email: Option<String>,
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);