use topcoat::{
context::Cx,
router::response::{IntoResponse, Response},
view::{BoxView, ViewExt},
};
use super::{CONFIRMATION_FIELD, EMAIL_FIELD, PASSWORD_FIELD, forgot_url, reset_url};
use crate::{
auth::{
login::login_url,
mail::{TOKEN_FIELD, private},
},
form::{FieldErrors, FormValues},
schema::{Field, Schema, Source},
};
#[derive(Debug, Clone, Copy)]
pub(super) enum ForgotAlert {
Throttled,
Unavailable,
}
impl ForgotAlert {
pub(super) fn message(self) -> &'static str {
match self {
Self::Throttled => "tablo-reset-throttled",
Self::Unavailable => "tablo-reset-unavailable",
}
}
pub(super) fn status(self) -> http::StatusCode {
match self {
Self::Throttled => http::StatusCode::TOO_MANY_REQUESTS,
Self::Unavailable => http::StatusCode::SERVICE_UNAVAILABLE,
}
}
}
pub(super) async fn forgot_response(
cx: &Cx,
email: &str,
errors: &FieldErrors,
alert: Option<ForgotAlert>,
) -> topcoat::Result<Response> {
let status = match alert {
Some(alert) => Some(alert.status()),
None => (!errors.is_empty()).then_some(http::StatusCode::UNPROCESSABLE_ENTITY),
};
let schema = Schema::new(crate::form::required_input(
Field::text_input::<String>(EMAIL_FIELD)
.email()
.label(crate::text!("tablo-auth-email")),
));
let mut values = FormValues::new();
values.insert(EMAIL_FIELD.to_string(), email.to_string());
let fields = schema.render(cx, Source::form(&values, errors)).await?;
let csrf = crate::csrf::ensure_token(cx);
let action = forgot_url(cx);
let form = topcoat::view::view! {
<form method="post" action=(action) class="flex flex-col gap-5">
(crate::csrf::field(cx, &csrf))
if let Some(alert) = alert {
tablo_ui::alert(
variant: tablo_ui::AlertVariant::Destructive,
attrs: topcoat::view::attributes! { role="alert" },
tablo_ui::alert_title((crate::t!(cx, alert.message())))
)
}
(fields)
tablo_ui::button(
variant: tablo_ui::ButtonVariant::Primary,
attrs: topcoat::view::attributes! { type="submit" class="w-full" },
(crate::t!(cx, "tablo-reset-request"))
)
</form>
}
.boxed();
let page = render(
cx,
crate::t!(cx, "tablo-reset-forgot-title"),
crate::t!(cx, "tablo-reset-forgot-description"),
status,
form,
)
.await?;
page.single().await?.into_response(cx)
}
pub(super) async fn reset_response(
cx: &Cx,
token: &str,
errors: &FieldErrors,
) -> topcoat::Result<Response> {
let status = (!errors.is_empty()).then_some(http::StatusCode::UNPROCESSABLE_ENTITY);
let field = crate::form::required_input;
let schema = Schema::new((
field(
Field::text_input::<String>(PASSWORD_FIELD)
.password()
.label(crate::text!("tablo-auth-new-password")),
),
field(
Field::text_input::<String>(CONFIRMATION_FIELD)
.password()
.label(crate::text!("tablo-auth-confirm-new-password")),
),
));
let values = FormValues::new();
let fields = schema.render(cx, Source::form(&values, errors)).await?;
let csrf = crate::csrf::ensure_token(cx);
let action = reset_url(cx);
let token = token.to_string();
let form = topcoat::view::view! {
<form method="post" action=(action) class="flex flex-col gap-5">
(crate::csrf::field(cx, &csrf))
<input type="hidden" name=(TOKEN_FIELD) value=(token)>
(fields)
tablo_ui::button(
variant: tablo_ui::ButtonVariant::Primary,
attrs: topcoat::view::attributes! { type="submit" class="w-full" },
(crate::t!(cx, "tablo-reset-submit"))
)
</form>
}
.boxed();
let page = render(
cx,
crate::t!(cx, "tablo-reset-title"),
crate::t!(
cx,
"tablo-reset-description",
min = crate::auth::MIN_PASSWORD_CHARS
),
status,
form,
)
.await?;
private(page.single().await?.into_response(cx)?)
}
pub(super) async fn invalid_link(cx: &Cx) -> topcoat::Result<Response> {
let forgot = forgot_url(cx);
let link = crate::navigation::runtime_link(cx, &forgot);
let form = topcoat::view::view! {
<div class="flex flex-col gap-5">
tablo_ui::alert(
variant: tablo_ui::AlertVariant::Destructive,
attrs: topcoat::view::attributes! { role="alert" },
tablo_ui::alert_title((crate::t!(cx, "tablo-reset-invalid")))
)
<a
(link)
class="text-center text-sm font-medium text-foreground underline underline-offset-4 hover:text-primary"
>
(crate::t!(cx, "tablo-reset-new-link"))
</a>
</div>
}
.boxed();
let page = render(
cx,
crate::t!(cx, "tablo-reset-expired-title"),
crate::t!(cx, "tablo-reset-expired-description"),
Some(http::StatusCode::NOT_FOUND),
form,
)
.await?;
private(page.single().await?.into_response(cx)?)
}
async fn render<'a>(
cx: &'a Cx,
title: String,
description: String,
status: Option<http::StatusCode>,
form: BoxView<'a>,
) -> topcoat::Result<BoxView<'a>> {
let login = login_url(cx);
crate::auth::page::render(
cx,
crate::auth::page::AuthPage {
title,
description,
status,
form,
switch: Some(crate::auth::page::switch_link(
cx,
"tablo-reset-remember",
&login,
"tablo-sign-up-sign-in-link",
)),
note: None,
},
)
.await
}