diff --git a/crates/handlers/src/views/account/emails/add.rs b/crates/handlers/src/views/account/emails/add.rs index 9cc2b726..58f0928a 100644 --- a/crates/handlers/src/views/account/emails/add.rs +++ b/crates/handlers/src/views/account/emails/add.rs @@ -86,7 +86,7 @@ pub(crate) async fn post( return Ok((cookie_jar, login.go()).into_response()); }; - let user_email = add_user_email(&mut txn, &session.user, form.email).await?; + let user_email = add_user_email(&mut txn, &session.user, &form.email).await?; let next = mas_router::AccountVerifyEmail::new(user_email.data); let next = if let Some(action) = query.post_auth_action { next.and_then(action) diff --git a/crates/handlers/src/views/account/emails/mod.rs b/crates/handlers/src/views/account/emails/mod.rs index 1e2f6c27..13a1291f 100644 --- a/crates/handlers/src/views/account/emails/mod.rs +++ b/crates/handlers/src/views/account/emails/mod.rs @@ -141,7 +141,7 @@ pub(crate) async fn post( match form { ManagementForm::Add { email } => { - let user_email = add_user_email(&mut txn, &session.user, email).await?; + let user_email = add_user_email(&mut txn, &session.user, &email).await?; let next = mas_router::AccountVerifyEmail::new(user_email.data); start_email_verification(&mailer, &mut txn, &session.user, user_email).await?; txn.commit().await?; diff --git a/crates/handlers/src/views/register.rs b/crates/handlers/src/views/register.rs index c592b328..4afee4fd 100644 --- a/crates/handlers/src/views/register.rs +++ b/crates/handlers/src/views/register.rs @@ -14,23 +14,30 @@ #![allow(clippy::trait_duplication_in_bounds)] +use std::str::FromStr; + use argon2::Argon2; use axum::{ extract::{Extension, Form, Query}, response::{Html, IntoResponse, Response}, }; use axum_extra::extract::PrivateCookieJar; +use lettre::{message::Mailbox, Address}; use mas_axum_utils::{ csrf::{CsrfExt, CsrfToken, ProtectedForm}, FancyError, SessionInfoExt, }; use mas_config::Encrypter; +use mas_email::Mailer; use mas_router::Route; -use mas_storage::user::{register_user, start_session, username_exists}; -use mas_templates::{ - FieldError, FormError, RegisterContext, RegisterFormField, TemplateContext, Templates, - ToFormState, +use mas_storage::user::{ + add_user_email, add_user_email_verification_code, register_user, start_session, username_exists, }; +use mas_templates::{ + EmailVerificationContext, FieldError, FormError, RegisterContext, RegisterFormField, + TemplateContext, Templates, ToFormState, +}; +use rand::{distributions::Uniform, thread_rng, Rng}; use serde::{Deserialize, Serialize}; use sqlx::{PgConnection, PgPool}; @@ -39,6 +46,7 @@ use super::shared::OptionalPostAuthAction; #[derive(Debug, Deserialize, Serialize)] pub(crate) struct RegisterForm { username: String, + email: String, password: String, password_confirm: String, } @@ -78,6 +86,7 @@ pub(crate) async fn get( } pub(crate) async fn post( + Extension(mailer): Extension, Extension(templates): Extension, Extension(pool): Extension, Query(query): Query, @@ -100,6 +109,12 @@ pub(crate) async fn post( state.add_error_on_field(RegisterFormField::Username, FieldError::Exists); } + if form.email.is_empty() { + state.add_error_on_field(RegisterFormField::Email, FieldError::Required); + } else if Address::from_str(&form.email).is_err() { + state.add_error_on_field(RegisterFormField::Email, FieldError::Invalid); + } + if form.password.is_empty() { state.add_error_on_field(RegisterFormField::Password, FieldError::Required); } @@ -133,13 +148,32 @@ pub(crate) async fn post( let pfh = Argon2::default(); let user = register_user(&mut txn, pfh, &form.username, &form.password).await?; + let user_email = add_user_email(&mut txn, &user, &form.email).await?; + + // First, generate a code + let range = Uniform::::from(0..1_000_000); + let code = thread_rng().sample(range).to_string(); + + let address: Address = user_email.email.parse()?; + + let verification = add_user_email_verification_code(&mut txn, user_email, code).await?; + + // And send the verification email + let mailbox = Mailbox::new(Some(user.username.clone()), address); + + let context = EmailVerificationContext::new(user.clone().into(), verification.clone().into()); + + mailer.send_verification_email(mailbox, &context).await?; + + let next = + mas_router::AccountVerifyEmail::new(verification.data).and_maybe(query.post_auth_action); + let session = start_session(&mut txn, user).await?; txn.commit().await?; let cookie_jar = cookie_jar.set_session(&session); - let reply = query.go_next(); - Ok((cookie_jar, reply).into_response()) + Ok((cookie_jar, next.go()).into_response()) } async fn render( diff --git a/crates/router/src/endpoints.rs b/crates/router/src/endpoints.rs index 89e92cc2..2c6d758a 100644 --- a/crates/router/src/endpoints.rs +++ b/crates/router/src/endpoints.rs @@ -331,6 +331,12 @@ impl AccountVerifyEmail { } } + #[must_use] + pub fn and_maybe(mut self, action: Option) -> Self { + self.post_auth_action = action; + self + } + #[must_use] pub fn and_then(mut self, action: PostAuthAction) -> Self { self.post_auth_action = Some(action); diff --git a/crates/storage/src/user.rs b/crates/storage/src/user.rs index 9000e964..d81e0ab0 100644 --- a/crates/storage/src/user.rs +++ b/crates/storage/src/user.rs @@ -572,7 +572,7 @@ pub async fn get_user_email( pub async fn add_user_email( executor: impl PgExecutor<'_>, user: &User, - email: String, + email: &str, ) -> anyhow::Result> { let res = sqlx::query_as!( UserEmailLookup, diff --git a/crates/templates/src/context.rs b/crates/templates/src/context.rs index bb92c0d2..6773e63a 100644 --- a/crates/templates/src/context.rs +++ b/crates/templates/src/context.rs @@ -319,6 +319,9 @@ pub enum RegisterFormField { /// The username field Username, + /// The email field + Email, + /// The password field Password, @@ -329,7 +332,7 @@ pub enum RegisterFormField { impl FormField for RegisterFormField { fn keep(&self) -> bool { match self { - Self::Username => true, + Self::Username | Self::Email => true, Self::Password | Self::PasswordConfirm => false, } } diff --git a/crates/templates/src/forms.rs b/crates/templates/src/forms.rs index f3898f04..f6e30cce 100644 --- a/crates/templates/src/forms.rs +++ b/crates/templates/src/forms.rs @@ -33,6 +33,9 @@ pub enum FieldError { /// An unspecified error on the field Unspecified, + /// Invalid value for this field + Invalid, + /// That value already exists Exists, } diff --git a/crates/templates/src/res/pages/register.html b/crates/templates/src/res/pages/register.html index 47208ea6..f4c4d0f3 100644 --- a/crates/templates/src/res/pages/register.html +++ b/crates/templates/src/res/pages/register.html @@ -33,6 +33,7 @@ limitations under the License. {{ field::input(label="Username", name="username", form_state=form, autocomplete="username") }} + {{ field::input(label="Email", name="email", type="email", form_state=form, autocomplete="email") }} {{ field::input(label="Password", name="password", type="password", form_state=form, autocomplete="new-password") }} {{ field::input(label="Confirm Password", name="password_confirm", type="password", form_state=form, autocomplete="new-password") }}