prepare for when a username gets snatched between initial signup and having paid

This commit is contained in:
Joe Ardent 2024-02-25 17:58:28 -08:00
parent d9d7e7d9c7
commit 883dfd67ea
3 changed files with 83 additions and 46 deletions

View file

@ -27,7 +27,9 @@ struct Counter(usize);
/// Displays the signup form. /// Displays the signup form.
async fn get_signup() -> impl IntoResponse { async fn get_signup() -> impl IntoResponse {
SignupPage::default() SignupPage {
..Default::default()
}
} }
/// Receives the form with the user signup fields filled out. /// Receives the form with the user signup fields filled out.
@ -35,41 +37,7 @@ async fn post_signup(
session: Session, session: Session,
Form(form): Form<SignupForm>, Form(form): Form<SignupForm>,
) -> Result<impl IntoResponse, CreateUserError> { ) -> Result<impl IntoResponse, CreateUserError> {
let username = form.username.trim(); let user = verify_user(&form).await?;
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_optional_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(),
};
session.insert(SIGNUP_KEY, user).await.unwrap(); session.insert(SIGNUP_KEY, user).await.unwrap();
Ok(Redirect::to( Ok(Redirect::to(
@ -77,14 +45,29 @@ async fn post_signup(
)) ))
} }
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. /// Called from Stripe with the receipt of payment.
async fn signup_success(session: Session, receipt: Option<Path<String>>) -> impl IntoResponse { async fn signup_success(session: Session, receipt: Option<Path<String>>) -> impl IntoResponse {
let user: User = session.get(SIGNUP_KEY).await.unwrap().unwrap_or_default(); let user: User = session.get(SIGNUP_KEY).await.unwrap().unwrap_or_default();
if user != User::default() { if user == User::default() {
SignupSuccessPage(user).into_response() return SignupErrorPage("who you?".to_string()).into_response();
} else {
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]
@ -135,8 +118,7 @@ pub struct SignupForm {
pub username: String, pub username: String,
#[serde(default, deserialize_with = "empty_string_as_none")] #[serde(default, deserialize_with = "empty_string_as_none")]
pub displayname: Option<String>, pub displayname: Option<String>,
#[serde(default, deserialize_with = "empty_string_as_none")] pub email: String,
pub email: Option<String>,
pub password: String, pub password: String,
pub pw_verify: String, pub pw_verify: String,
} }
@ -149,6 +131,7 @@ pub struct SignupPage {
pub email: Option<String>, pub email: Option<String>,
pub password: String, pub password: String,
pub pw_verify: String, pub pw_verify: String,
pub receipt: String,
} }
#[derive(Debug, Clone, Template, Default, Deserialize, Serialize, PartialEq, Eq)] #[derive(Debug, Clone, Template, Default, Deserialize, Serialize, PartialEq, Eq)]
@ -163,7 +146,7 @@ pub struct SignupErrorPage(pub String);
pub struct User { pub struct User {
pub username: String, pub username: String,
pub displayname: Option<String>, pub displayname: Option<String>,
pub email: Option<String>, pub email: String,
pub password: String, pub password: String,
pub pw_verify: String, pub pw_verify: String,
} }
@ -192,11 +175,50 @@ impl Display for User {
} else { } else {
"" ""
}; };
let email = if let Some(ref e) = self.email { e } else { "" }; let email = &self.email;
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> pub(crate) fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
@ -229,3 +251,17 @@ pub(crate) fn validate_optional_length<E: Error>(
Ok(None) 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

@ -8,11 +8,12 @@
<p> <p>
<form action="/signup" enctype="application/x-www-form-urlencoded" method="post"> <form action="/signup" enctype="application/x-www-form-urlencoded" method="post">
<input type="hidden" value="{{ self.receipt }}" name="receipt">
<label for="username">Username</label> <label for="username">Username</label>
<input type="text" name="username" id="username" minlength="1" maxlength="20" required></br> <input type="text" name="username" id="username" minlength="1" maxlength="20" required></br>
<label for="displayname">Displayname (optional)</label> <label for="displayname">Displayname (optional)</label>
<input type="text" name="displayname" id="displayname"></br> <input type="text" name="displayname" id="displayname"></br>
<label for="email">Email (optional)</label> <label for="email">Email</label>
<input type="text" name="email"></br> <input type="text" name="email"></br>
<label for="password">Password</label> <label for="password">Password</label>
<input type="password" name="password" id="password" required></br> <input type="password" name="password" id="password" required></br>

View file

@ -13,7 +13,7 @@
</p> </p>
</div> </div>
<p>Now, head on over to <a href="/login?redirect_to={{ self.0.username|escape }}">the login page</a> and get watchin'! <p>Now, head on over to <a href="/login?redirect_to=%2F{{ self.0.username|escape }}">the login page</a> and git going!
</p> </p>
{% endblock %} {% endblock %}