Cleanup the other auth forms

Somewhat kinda progressively enhanced, but at least I'm using page partials now... mostly.
This commit is contained in:
Tyler Hallada 2023-12-19 01:18:39 -05:00
parent 7abffb2729
commit 6c23b3aaa3
8 changed files with 321 additions and 245 deletions

View File

@ -2,7 +2,7 @@ use axum::extract::{Query, State};
use axum::response::Response;
use axum::{Form, TypedHeader};
use lettre::SmtpTransport;
use maud::html;
use maud::{html, Markup};
use serde::Deserialize;
use serde_with::{serde_as, NoneAsEmptyString};
use sqlx::PgPool;
@ -24,6 +24,46 @@ pub struct ConfirmEmailQuery {
pub token_id: Option<Base62Uuid>,
}
#[derive(Debug, Default)]
pub struct ConfirmEmailPageProps<'a> {
pub hx_target: Option<TypedHeader<HXTarget>>,
pub layout: Layout,
pub form_props: ConfirmEmailFormProps,
pub header: Option<&'a str>,
pub desc: Option<Markup>,
}
pub fn confirm_email_page(
ConfirmEmailPageProps {
hx_target,
layout,
form_props,
header,
desc,
}: ConfirmEmailPageProps,
) -> Response {
layout
.with_subtitle("confirm email")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
h2 { (header.unwrap_or("Confirm your email address")) }
}
@if let Some(desc) = desc {
(desc)
} @else {
p class="readable-width" {
"Enter your email to resend the confirmation email. If you don't have an account yet, create one "
a href="/register" { "here" }
"."
}
}
(confirm_email_form(form_props))
}
})
}
pub async fn get(
State(pool): State<PgPool>,
auth: AuthContext,
@ -38,17 +78,12 @@ pub async fn get(
Err(err) => {
if let Error::NotFoundUuid(_, _) = err {
warn!(token_id = %token_id.as_uuid(), "token not found in database");
return Ok(layout
.with_subtitle("confirm email")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
h2 { "Email verification token not found" }
}
p class="readable-width" { "Enter your email to resend the confirmation email. If you don't have an account yet, create one " a href="/register" { "here" } "." }
(confirm_email_form(ConfirmEmailFormProps::default()))
}
return Ok(confirm_email_page(ConfirmEmailPageProps {
hx_target,
layout,
form_props: ConfirmEmailFormProps::default(),
header: Some("Email verification token not found"),
..Default::default()
}));
}
return Err(err);
@ -56,17 +91,19 @@ pub async fn get(
};
if token.expired() {
warn!(token_id = %token.token_id, "token expired");
Ok(layout
.with_subtitle("confirm email")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
h2 { "Email verification token is expired" }
}
p class="readable-width" { "Click the button below to resend a new confirmation email. The link in the email will be valid for another 24 hours."}
(confirm_email_form(ConfirmEmailFormProps { token: Some(token), email: None }))
Ok(confirm_email_page(ConfirmEmailPageProps {
hx_target,
layout,
form_props: ConfirmEmailFormProps {
token: Some(token),
..Default::default()
},
header: Some("Email verification token is expired"),
desc: Some(html! {
p class="readable-width" {
"Click the button below to resend a new confirmation email. The link in the email will be valid for another 24 hours."
}
}),
}))
} else {
info!(token_id = %token.token_id, "token valid, verifying email");
@ -88,22 +125,14 @@ pub async fn get(
}))
}
} else {
Ok(layout
.with_subtitle("confirm email")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
h2 { "Confirm your email address" }
}
p class="readable-width" { "An email was sent to your email address upon registration containing a link that will confirm your email address. If you can't find it or it has been more than 24 hours since it was sent, you can resend the email by submitting the form below:"}
(confirm_email_form(
ConfirmEmailFormProps {
token: None,
Ok(confirm_email_page(ConfirmEmailPageProps {
hx_target,
layout,
form_props: ConfirmEmailFormProps {
email: auth.current_user.map(|u| u.email),
}
))
}
..Default::default()
},
..Default::default()
}))
}
}
@ -172,21 +201,17 @@ pub async fn post(
}
}));
}
Ok(layout
.with_subtitle("confirm email")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
h2 { "Email verification token not found" }
}
p class="readable-width" { "Enter your email to resend the confirmation email." }
Ok(confirm_email_page(ConfirmEmailPageProps {
hx_target,
layout,
form_props: ConfirmEmailFormProps::default(),
header: Some("Email verification token not found"),
desc: Some(html! {
p class="readable-width" {
"If you don't have an account yet, create one "
"Enter your email to resend the confirmation email. If you don't have an account yet, create one "
a href="/register" { "here" }
"."
}
(confirm_email_form(ConfirmEmailFormProps::default()))
}
}),
}))
}

View File

@ -46,6 +46,26 @@ pub fn forgot_password_page(
.into_response()
}
pub fn confirm_forgot_password_sent_page(
hx_target: Option<TypedHeader<HXTarget>>,
layout: Layout,
) -> Response {
layout
.with_subtitle("forgot password")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
h2 { "Reset password email sent" }
}
p class="readable-width" {
"If the email you entered matched an existing account with a verified email, then a password reset email was sent. Please follow the link sent in the email."
}
}
})
.into_response()
}
pub async fn get(
auth: AuthContext,
hx_target: Option<TypedHeader<HXTarget>>,
@ -76,19 +96,7 @@ pub async fn post(
Err(err) => {
if let Error::NotFoundString(_, _) = err {
info!(email = forgot_password.email, "invalid email");
return Ok(layout
.with_subtitle("forgot password")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
h2 { "Reset password email sent" }
}
p class="readable-width" {
"If the email you entered matched an existing account with a verified email, then a password reset email was sent. Please follow the link sent in the email."
}
}
}));
return Ok(confirm_forgot_password_sent_page(hx_target, layout));
} else {
return Err(err);
}
@ -107,17 +115,5 @@ pub async fn post(
} else {
warn!(user_id = %user.user_id, "user exists with unverified email, skip sending password reset email");
}
Ok(layout
.with_subtitle("forgot password")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
h2 { "Reset password email sent" }
}
p class="readable-width" {
"If the email you entered matched an existing account with a verified email, then a password reset email was sent. Please follow the link sent in the email."
}
}
}))
Ok(confirm_forgot_password_sent_page(hx_target, layout))
}

View File

@ -47,7 +47,6 @@ pub fn register_page(
(register_form(form_props))
}
})
.into_response()
}
pub async fn get(hx_target: Option<TypedHeader<HXTarget>>, layout: Layout) -> Result<Response> {

View File

@ -1,5 +1,5 @@
use axum::extract::Query;
use axum::response::{IntoResponse, Response};
use axum::response::Response;
use axum::TypedHeader;
use axum::{extract::State, Form};
use axum_client_ip::SecureClientIp;
@ -36,26 +36,84 @@ pub struct ResetPasswordQuery {
pub token_id: Option<Base62Uuid>,
}
pub fn reset_password_page(
hx_target: Option<TypedHeader<HXTarget>>,
layout: Layout,
form_props: ResetPasswordFormProps,
#[derive(Debug, Default)]
pub struct InvalidTokenPageProps<'a> {
pub hx_target: Option<TypedHeader<HXTarget>>,
pub layout: Layout,
pub header: Option<&'a str>,
pub desc: Option<&'a str>,
}
pub fn invalid_token_page(
InvalidTokenPageProps {
hx_target,
layout,
header,
desc,
}: InvalidTokenPageProps,
) -> Response {
layout
.with_subtitle("forgot password")
.with_subtitle("reset password")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
h2 { "Reset Password" }
h2 { (header.unwrap_or("Reset Password")) }
}
p {
@if let Some(desc) = desc {
p class="readable-width" { (desc) }
}
p class="readable-width" {
a href="/forgot-password" {
"Follow this link to request a new password reset email"
}
"."
}
}
})
}
#[derive(Debug, Default)]
pub struct ResetPasswordPageProps<'a> {
pub hx_target: Option<TypedHeader<HXTarget>>,
pub layout: Layout,
pub form_props: ResetPasswordFormProps,
pub header: Option<&'a str>,
pub post_form_error: Option<&'a str>,
}
pub fn reset_password_page(
ResetPasswordPageProps {
hx_target,
layout,
form_props,
header,
post_form_error,
}: ResetPasswordPageProps,
) -> Response {
layout
.with_subtitle("reset password")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
h2 { (header.unwrap_or("Reset Password")) }
}
p class="readable-width" {
"A password reset email will be sent if the email submitted matches an account in the system and the email is verfied. If your email is not verified, " a href="/confirm-email" { "please verify your email first" } "."
}
(reset_password_form(form_props))
@if let Some(post_form_error) = post_form_error {
p class="error readable-width" { (post_form_error) }
p class="readable-width" {
a href="/forgot-password" {
"Follow this link to request a new password reset email"
}
". The link in the email will be valid for 24 hours."
}
}
}
})
.into_response()
}
pub async fn get(
@ -71,17 +129,11 @@ pub async fn get(
Err(err) => {
if let Error::NotFoundUuid(_, _) = err {
warn!(token_id = %token_id.as_uuid(), "token not found in database");
return Ok(layout
.with_subtitle("reset password")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
h2 { "Password reset token not found" }
}
p class="readable-width" { "The reset password link has already been used or is invalid." }
p class="readable-width" { a href="/forgot-password" { "Follow this link to request a new password reset email" } "." }
}
return Ok(invalid_token_page(InvalidTokenPageProps {
hx_target,
layout,
header: Some("Password reset token not found"),
desc: Some("The reset password link has already been used or is invalid."),
}));
}
return Err(err);
@ -89,49 +141,32 @@ pub async fn get(
};
if token.expired() {
warn!(token_id = %token.token_id, "token expired");
Ok(layout
.with_subtitle("reset password")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
h2 { "Password reset token is expired" }
}
p class="readable-width" { a href="/forgot-password" { "Follow this link to request a new password reset email" } ". The link in the email will be valid for 24 hours." }
}
Ok(invalid_token_page(InvalidTokenPageProps {
hx_target,
layout,
header: Some("Password reset token expired"),
..Default::default()
}))
} else {
info!(token_id = %token.token_id, "token valid, showing reset password form");
let user = User::get(&pool, token.user_id).await?;
Ok(layout
.with_subtitle("reset password")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
h2 { "Reset Password" }
}
(reset_password_form(ResetPasswordFormProps {
Ok(reset_password_page(ResetPasswordPageProps {
hx_target,
layout,
form_props: ResetPasswordFormProps {
token: token.token_id,
email: user.email,
password_error: None,
general_error: None,
}))
}
..Default::default()
},
..Default::default()
}))
}
} else {
Ok(layout
.with_subtitle("reset password")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
h2 { "Missing password reset token" }
}
p class="readable-width" { "Passwords can only be reset by requesting a password reset email and following the unique link within the email."}
p class="readable-width" { a href="/forgot-password" { "Follow this link to request a new password reset email" } "." }
}
Ok(invalid_token_page(InvalidTokenPageProps {
hx_target,
layout,
header: Some("Missing password reset token"),
desc: Some("Passwords can only be reset by requesting a password reset email and following the unique link within the email."),
}))
}
}
@ -147,21 +182,16 @@ pub async fn post(
Form(reset_password): Form<ResetPassword>,
) -> Result<Response> {
if reset_password.password != reset_password.password_confirmation {
return Ok(layout
.with_subtitle("reset password")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
h2 { "Reset Password" }
}
(reset_password_form(ResetPasswordFormProps {
return Ok(reset_password_page(ResetPasswordPageProps {
hx_target,
layout,
form_props: ResetPasswordFormProps {
token: reset_password.token,
email: reset_password.email,
password_error: Some("passwords do not match".to_string()),
general_error: None,
}))
}
..Default::default()
},
..Default::default()
}));
}
let token = match UserPasswordResetToken::get(&pool, reset_password.token).await {
@ -169,23 +199,19 @@ pub async fn post(
Err(err) => {
if let Error::NotFoundUuid(_, _) = err {
warn!(token_id = %reset_password.token, "token not found in database");
return Ok(layout
.with_subtitle("reset password")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
h2 { "Reset Password" }
}
(reset_password_form(ResetPasswordFormProps {
return Ok(reset_password_page(ResetPasswordPageProps {
hx_target,
layout,
form_props: ResetPasswordFormProps {
token: reset_password.token,
email: reset_password.email,
password_error: None,
general_error: Some("token not found".to_string()),
}))
p class="error readable-width" { "The reset password link has already been used or is invalid." }
p class="readable-width" { a href="/forgot-password" { "Follow this link to request a new password reset email" } "." }
}
..Default::default()
},
post_form_error: Some(
"The reset password link has already been used or is invalid.",
),
..Default::default()
}));
}
return Err(err);
@ -193,23 +219,17 @@ pub async fn post(
};
if token.expired() {
warn!(token_id = %token.token_id, "token expired");
return Ok(layout
.with_subtitle("reset password")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
h2 { "Reset Password" }
}
(reset_password_form(ResetPasswordFormProps {
return Ok(reset_password_page(ResetPasswordPageProps {
hx_target,
layout,
form_props: ResetPasswordFormProps {
token: reset_password.token,
email: reset_password.email,
password_error: None,
general_error: Some("token expired".to_string()),
}))
p class="error readable-width" { "The reset password link has expired." }
p class="readable-width" { a href="/forgot-password" { "Follow this link to request a new password reset email" } ". The link in the email will be valid for 24 hours." }
}
..Default::default()
},
post_form_error: Some("The reset password link has expired."),
..Default::default()
}));
}
let user = match User::get(&pool, token.user_id).await {
@ -217,23 +237,19 @@ pub async fn post(
Err(err) => {
if let Error::NotFoundString(_, _) = err {
info!(user_id = %token.user_id, email = reset_password.email, "invalid token user_id");
return Ok(layout
.with_subtitle("reset password")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
h2 { "Reset Password" }
}
(reset_password_form(ResetPasswordFormProps {
return Ok(reset_password_page(ResetPasswordPageProps {
hx_target,
layout,
form_props: ResetPasswordFormProps {
token: reset_password.token,
email: reset_password.email,
password_error: None,
general_error: Some("user not found".to_string()),
}))
p class="error readable-width" { "The user associated with this password reset could not be found." }
p class="readable-width" { a href="/forgot-password" { "Follow this link to request a new password reset email" } "." }
}
..Default::default()
},
post_form_error: Some(
"The user associated with this password reset could not be found.",
),
..Default::default()
}));
} else {
return Err(err);
@ -256,15 +272,10 @@ pub async fn post(
Err(err) => {
if let Error::InvalidEntity(validation_errors) = err {
let field_errors = validation_errors.field_errors();
return Ok(layout
.with_subtitle("reset password")
.targeted(hx_target)
.render(html! {
div class="center-horizontal" {
header class="center-text" {
h2 { "Reset Password" }
}
(reset_password_form(ResetPasswordFormProps {
return Ok(reset_password_page(ResetPasswordPageProps {
hx_target,
layout,
form_props: ResetPasswordFormProps {
token: reset_password.token,
email: reset_password.email,
password_error: field_errors.get("password").map(|&errors| {
@ -274,9 +285,9 @@ pub async fn post(
.collect::<Vec<String>>()
.join(", ")
}),
general_error: None,
}))
}
..Default::default()
},
..Default::default()
}));
}
return Err(err);

View File

@ -11,7 +11,13 @@ pub struct ConfirmEmailFormProps {
pub fn confirm_email_form(props: ConfirmEmailFormProps) -> Markup {
let ConfirmEmailFormProps { token, email } = props;
html! {
form action="/confirm-email" method="post" class="auth-form-grid" {
form
action="/confirm-email"
method="post"
hx-post="/confirm-email"
id="confirm-email-form"
class="auth-form-grid"
{
input
type="text"
name="token"

View File

@ -9,7 +9,13 @@ pub struct ForgotPasswordFormProps {
pub fn forgot_password_form(props: ForgotPasswordFormProps) -> Markup {
let ForgotPasswordFormProps { email, email_error } = props;
html! {
form action="forgot-password" method="post" class="auth-form-grid" {
form
action="/forgot-password"
method="post"
hx-post="/forgot-password"
id="forgot-password-form"
class="auth-form-grid"
{
label for="email" { "Email" }
input
type="email"

View File

@ -2,10 +2,20 @@ use maud::{html, Markup, PreEscaped};
pub fn opml_import_form() -> Markup {
html! {
form id="opml-import-form" hx-post="/import/opml" hx-swap="outerHTML" hx-encoding="multipart/form-data" class="feed-form" {
form
id="opml-import-form"
hx-post="/import/opml"
hx-encoding="multipart/form-data"
class="feed-form"
{
div class="form-grid" {
label for="opml" { "OPML: " }
input type="file" id="opml" name="opml" required="true" accept="text/x-opml,application/xml,text/xml";
input
type="file"
id="opml"
name="opml"
required="true"
accept="text/x-opml,application/xml,text/xml";
button type="submit" { "Import Feeds" }
progress id="opml-upload-progress" max="100" value="0" hidden="true" {}
}

View File

@ -1,7 +1,7 @@
use maud::{html, Markup};
use uuid::Uuid;
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub struct ResetPasswordFormProps {
pub token: Uuid,
pub email: String,
@ -10,9 +10,20 @@ pub struct ResetPasswordFormProps {
}
pub fn reset_password_form(props: ResetPasswordFormProps) -> Markup {
let ResetPasswordFormProps { token, email, password_error, general_error } = props;
let ResetPasswordFormProps {
token,
email,
password_error,
general_error,
} = props;
html! {
form action="reset-password" method="post" class="auth-form-grid" {
form
action="/reset-password"
method="post"
hx-post="/reset-password"
id="reset-password-form"
class="auth-form-grid"
{
input
type="text"
name="token"
@ -28,12 +39,24 @@ pub fn reset_password_form(props: ResetPasswordFormProps) -> Markup {
value=(email)
required;
label for="password" { "Password" }
input type="password" name="password" id="password" placeholder="Password" minlength="8" maxlength="255" required;
input
type="password"
name="password"
id="password"
placeholder="Password"
minlength="8"
maxlength="255"
required;
@if let Some(password_error) = password_error {
span class="error" { (password_error) }
}
label for="password_confirmation" { "Confirm Password" }
input type="password" name="password_confirmation" id="password_confirmation" placeholder="Confirm Password" required;
input
type="password"
name="password_confirmation"
id="password_confirmation"
placeholder="Confirm Password"
required;
button type="submit" { "Reset password" }
@if let Some(general_error) = general_error {
span class="error" { (general_error) }