Fixes to email verification process
Also make registration form progressively enhanced.
This commit is contained in:
parent
e59c6d596e
commit
609f6d3d9f
@ -1,7 +1,7 @@
|
|||||||
use axum::extract::{Query, State};
|
use axum::extract::{Query, State};
|
||||||
use axum::response::Response;
|
use axum::response::Response;
|
||||||
use axum::{Form, TypedHeader};
|
use axum::{Form, TypedHeader};
|
||||||
use axum_login::{extractors::AuthContext, SqlxStore};
|
use axum_login::SqlxStore;
|
||||||
use lettre::SmtpTransport;
|
use lettre::SmtpTransport;
|
||||||
use maud::html;
|
use maud::html;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@ -14,7 +14,7 @@ use crate::config::Config;
|
|||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::htmx::HXTarget;
|
use crate::htmx::HXTarget;
|
||||||
use crate::mailers::email_verification::send_confirmation_email;
|
use crate::mailers::email_verification::send_confirmation_email;
|
||||||
use crate::models::user::User;
|
use crate::models::user::{AuthContext, User};
|
||||||
use crate::models::user_email_verification_token::UserEmailVerificationToken;
|
use crate::models::user_email_verification_token::UserEmailVerificationToken;
|
||||||
use crate::partials::confirm_email_form::{confirm_email_form, ConfirmEmailFormProps};
|
use crate::partials::confirm_email_form::{confirm_email_form, ConfirmEmailFormProps};
|
||||||
use crate::partials::layout::Layout;
|
use crate::partials::layout::Layout;
|
||||||
@ -27,7 +27,7 @@ pub struct ConfirmEmailQuery {
|
|||||||
|
|
||||||
pub async fn get(
|
pub async fn get(
|
||||||
State(pool): State<PgPool>,
|
State(pool): State<PgPool>,
|
||||||
auth: AuthContext<Uuid, User, SqlxStore<PgPool, User>>,
|
auth: AuthContext,
|
||||||
hx_target: Option<TypedHeader<HXTarget>>,
|
hx_target: Option<TypedHeader<HXTarget>>,
|
||||||
layout: Layout,
|
layout: Layout,
|
||||||
query: Query<ConfirmEmailQuery>,
|
query: Query<ConfirmEmailQuery>,
|
||||||
|
@ -91,5 +91,5 @@ pub async fn post(
|
|||||||
auth.login(&user)
|
auth.login(&user)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| Error::InternalServerError)?;
|
.map_err(|_| Error::InternalServerError)?;
|
||||||
Ok(HXRedirect::to("/").into_response())
|
Ok(HXRedirect::to("/").reload(true).into_response())
|
||||||
}
|
}
|
||||||
|
@ -44,17 +44,27 @@ pub async fn post(
|
|||||||
State(mailer): State<SmtpTransport>,
|
State(mailer): State<SmtpTransport>,
|
||||||
State(config): State<Config>,
|
State(config): State<Config>,
|
||||||
mut auth: AuthContext,
|
mut auth: AuthContext,
|
||||||
|
hx_target: Option<TypedHeader<HXTarget>>,
|
||||||
|
layout: Layout,
|
||||||
Form(register): Form<Register>,
|
Form(register): Form<Register>,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
if register.password != register.password_confirmation {
|
if register.password != register.password_confirmation {
|
||||||
// return Err(Error::BadRequest("passwords do not match"));
|
return Ok(layout
|
||||||
return Ok(register_form(RegisterFormProps {
|
.with_subtitle("register")
|
||||||
email: Some(register.email),
|
.targeted(hx_target)
|
||||||
name: register.name,
|
.render(html! {
|
||||||
password_error: Some("passwords do not match".to_string()),
|
div class="center-horizontal" {
|
||||||
..Default::default()
|
header class="center-text" {
|
||||||
})
|
h2 { "Register" }
|
||||||
.into_response());
|
}
|
||||||
|
(register_form(RegisterFormProps {
|
||||||
|
email: Some(register.email),
|
||||||
|
name: register.name,
|
||||||
|
password_error: Some("passwords do not match".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
let user = match User::create(
|
let user = match User::create(
|
||||||
&pool,
|
&pool,
|
||||||
@ -70,44 +80,62 @@ pub async fn post(
|
|||||||
Err(err) => {
|
Err(err) => {
|
||||||
if let Error::InvalidEntity(validation_errors) = err {
|
if let Error::InvalidEntity(validation_errors) = err {
|
||||||
let field_errors = validation_errors.field_errors();
|
let field_errors = validation_errors.field_errors();
|
||||||
return Ok(register_form(RegisterFormProps {
|
return Ok(layout
|
||||||
email: Some(register.email),
|
.with_subtitle("register")
|
||||||
name: register.name,
|
.targeted(hx_target)
|
||||||
email_error: field_errors.get("email").map(|&errors| {
|
.render(html! {
|
||||||
errors
|
div class="center-horizontal" {
|
||||||
.iter()
|
header class="center-text" {
|
||||||
.filter_map(|error| error.message.clone().map(|m| m.to_string()))
|
h2 { "Register" }
|
||||||
.collect::<Vec<String>>()
|
}
|
||||||
.join(", ")
|
(register_form(RegisterFormProps {
|
||||||
}),
|
email: Some(register.email),
|
||||||
name_error: field_errors.get("name").map(|&errors| {
|
name: register.name,
|
||||||
errors
|
email_error: field_errors.get("email").map(|&errors| {
|
||||||
.iter()
|
errors
|
||||||
.filter_map(|error| error.message.clone().map(|m| m.to_string()))
|
.iter()
|
||||||
.collect::<Vec<String>>()
|
.filter_map(|error| error.message.clone().map(|m| m.to_string()))
|
||||||
.join(", ")
|
.collect::<Vec<String>>()
|
||||||
}),
|
.join(", ")
|
||||||
password_error: field_errors.get("password").map(|&errors| {
|
}),
|
||||||
errors
|
name_error: field_errors.get("name").map(|&errors| {
|
||||||
.iter()
|
errors
|
||||||
.filter_map(|error| error.message.clone().map(|m| m.to_string()))
|
.iter()
|
||||||
.collect::<Vec<String>>()
|
.filter_map(|error| error.message.clone().map(|m| m.to_string()))
|
||||||
.join(", ")
|
.collect::<Vec<String>>()
|
||||||
}),
|
.join(", ")
|
||||||
..Default::default()
|
}),
|
||||||
})
|
password_error: field_errors.get("password").map(|&errors| {
|
||||||
.into_response());
|
errors
|
||||||
|
.iter()
|
||||||
|
.filter_map(|error| error.message.clone().map(|m| m.to_string()))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", ")
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
if let Error::Sqlx(sqlx::error::Error::Database(db_error)) = &err {
|
if let Error::Sqlx(sqlx::error::Error::Database(db_error)) = &err {
|
||||||
if let Some(constraint) = db_error.constraint() {
|
if let Some(constraint) = db_error.constraint() {
|
||||||
if constraint == "users_email_idx" {
|
if constraint == "users_email_idx" {
|
||||||
return Ok(register_form(RegisterFormProps {
|
return Ok(layout
|
||||||
email: Some(register.email),
|
.with_subtitle("register")
|
||||||
name: register.name,
|
.targeted(hx_target)
|
||||||
email_error: Some("email already exists".to_string()),
|
.render(html! {
|
||||||
..Default::default()
|
div class="center-horizontal" {
|
||||||
})
|
header class="center-text" {
|
||||||
.into_response());
|
h2 { "Register" }
|
||||||
|
}
|
||||||
|
(register_form(RegisterFormProps {
|
||||||
|
email: Some(register.email),
|
||||||
|
name: register.name,
|
||||||
|
email_error: Some("email already exists".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -120,5 +148,5 @@ pub async fn post(
|
|||||||
auth.login(&user)
|
auth.login(&user)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| Error::InternalServerError)?;
|
.map_err(|_| Error::InternalServerError)?;
|
||||||
Ok(HXRedirect::to("/").into_response())
|
Ok(HXRedirect::to("/").reload(true).into_response())
|
||||||
}
|
}
|
||||||
|
27
src/htmx.rs
27
src/htmx.rs
@ -3,6 +3,8 @@ use headers::{self, Header};
|
|||||||
use http::header::{HeaderName, HeaderValue};
|
use http::header::{HeaderName, HeaderValue};
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
|
|
||||||
|
#[allow(clippy::declare_interior_mutable_const)]
|
||||||
|
pub const HX_REDIRECT: HeaderName = HeaderName::from_static("hx-redirect");
|
||||||
#[allow(clippy::declare_interior_mutable_const)]
|
#[allow(clippy::declare_interior_mutable_const)]
|
||||||
pub const HX_LOCATION: HeaderName = HeaderName::from_static("hx-location");
|
pub const HX_LOCATION: HeaderName = HeaderName::from_static("hx-location");
|
||||||
#[allow(clippy::declare_interior_mutable_const)]
|
#[allow(clippy::declare_interior_mutable_const)]
|
||||||
@ -16,23 +18,38 @@ pub const HX_TARGET: HeaderName = HeaderName::from_static("hx-target");
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct HXRedirect {
|
pub struct HXRedirect {
|
||||||
location: HeaderValue,
|
location: HeaderValue,
|
||||||
|
reload: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HXRedirect {
|
impl HXRedirect {
|
||||||
pub fn to(uri: &str) -> Self {
|
pub fn to(uri: &str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
location: HeaderValue::try_from(uri).expect("URI isn't a valid header value"),
|
location: HeaderValue::try_from(uri).expect("URI isn't a valid header value"),
|
||||||
|
reload: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reload(mut self, reload: bool) -> Self {
|
||||||
|
self.reload = reload;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for HXRedirect {
|
impl IntoResponse for HXRedirect {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
(
|
if self.reload {
|
||||||
StatusCode::OK,
|
(
|
||||||
[(HX_LOCATION, self.location)],
|
StatusCode::OK,
|
||||||
)
|
[(HX_REDIRECT, self.location)],
|
||||||
.into_response()
|
)
|
||||||
|
.into_response()
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
StatusCode::OK,
|
||||||
|
[(HX_LOCATION, self.location)],
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,12 @@ pub fn header(title: &str, user: Option<User>) -> Markup {
|
|||||||
}
|
}
|
||||||
div class="auth" {
|
div class="auth" {
|
||||||
@if let Some(user) = user {
|
@if let Some(user) = user {
|
||||||
(user_name(user))
|
(user_name(user.clone()))
|
||||||
|
@if !user.email_verified {
|
||||||
|
span { " (" }
|
||||||
|
a href="/confirm-email" { "unverified" }
|
||||||
|
span { ")" }
|
||||||
|
}
|
||||||
span { " | " }
|
span { " | " }
|
||||||
a href="/logout" { "logout" }
|
a href="/logout" { "logout" }
|
||||||
} @else {
|
} @else {
|
||||||
|
@ -10,12 +10,10 @@ use axum::{
|
|||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
TypedHeader,
|
TypedHeader,
|
||||||
};
|
};
|
||||||
use axum_login::{extractors::AuthContext, SqlxStore};
|
|
||||||
use headers::HeaderValue;
|
use headers::HeaderValue;
|
||||||
use maud::{html, Markup, DOCTYPE};
|
use maud::{html, Markup, DOCTYPE};
|
||||||
use sqlx::PgPool;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
|
use crate::models::user::AuthContext;
|
||||||
use crate::models::user::User;
|
use crate::models::user::User;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::htmx::HXTarget;
|
use crate::htmx::HXTarget;
|
||||||
@ -45,7 +43,7 @@ where
|
|||||||
.await
|
.await
|
||||||
.map_err(|err| err.into_response())?;
|
.map_err(|err| err.into_response())?;
|
||||||
let auth_context =
|
let auth_context =
|
||||||
AuthContext::<Uuid, User, SqlxStore<PgPool, User>>::from_request_parts(parts, state)
|
AuthContext::from_request_parts(parts, state)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| err.into_response())?;
|
.map_err(|err| err.into_response())?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
@ -20,7 +20,7 @@ pub fn register_form(props: RegisterFormProps) -> Markup {
|
|||||||
general_error,
|
general_error,
|
||||||
} = props;
|
} = props;
|
||||||
html! {
|
html! {
|
||||||
form hx-post="/register" hx-swap="outerHTML" class="auth-form-grid" {
|
form action="/register" method="post" class="auth-form-grid" {
|
||||||
label for="email" { "Email *" }
|
label for="email" { "Email *" }
|
||||||
input type="email" name="email" id="email" placeholder="Email" value=(email.unwrap_or_default()) required;
|
input type="email" name="email" id="email" placeholder="Email" value=(email.unwrap_or_default()) required;
|
||||||
@if let Some(email_error) = email_error {
|
@if let Some(email_error) = email_error {
|
||||||
|
Loading…
Reference in New Issue
Block a user