mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
feat: Implement a webpage for self-service password resets
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
use askama::{Template, filters::HtmlSafe};
|
||||
use validator::ValidationErrors;
|
||||
|
||||
/// A reusable form component with field validation.
|
||||
#[derive(Debug, Template)]
|
||||
#[template(path = "_components/form.html.j2", print = "code")]
|
||||
pub(crate) struct Form<'a> {
|
||||
pub inputs: Vec<FormInput<'a>>,
|
||||
pub validation_errors: Option<ValidationErrors>,
|
||||
pub submit_label: &'a str,
|
||||
}
|
||||
|
||||
impl HtmlSafe for Form<'_> {}
|
||||
|
||||
/// An input element in a form component.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct FormInput<'a> {
|
||||
/// The field name of the input.
|
||||
pub id: &'static str,
|
||||
/// The `type` property of the input.
|
||||
pub input_type: &'a str,
|
||||
/// The contents of the input's label.
|
||||
pub label: &'a str,
|
||||
/// Whether the input is required. Defaults to `true`.
|
||||
pub required: bool,
|
||||
/// The autocomplete mode for the input. Defaults to `on`.
|
||||
pub autocomplete: &'a str,
|
||||
|
||||
// This is a hack to make the form! macro's support for client-only fields
|
||||
// work properly. Client-only fields are specified in the macro without a type and aren't
|
||||
// included in the POST body or as a field in the generated struct.
|
||||
// To keep the field from being included in the POST body, its `name` property needs not to
|
||||
// be set in the template. Because of limitations of macro_rules!'s repetition feature, this
|
||||
// field needs to exist to allow the template to check if the field is client-only.
|
||||
#[doc(hidden)]
|
||||
pub type_name: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl Default for FormInput<'_> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: "",
|
||||
input_type: "text",
|
||||
label: "",
|
||||
required: true,
|
||||
autocomplete: "",
|
||||
|
||||
type_name: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a deserializable struct which may be turned into a [`Form`]
|
||||
/// for inclusion in another template.
|
||||
#[macro_export]
|
||||
macro_rules! form {
|
||||
(
|
||||
$(#[$struct_meta:meta])*
|
||||
struct $struct_name:ident {
|
||||
$(
|
||||
$(#[$field_meta:meta])*
|
||||
$name:ident$(: $type:ty)? where { $($prop:ident: $value:expr),* }
|
||||
),*
|
||||
|
||||
submit: $submit_label:expr
|
||||
}
|
||||
) => {
|
||||
#[derive(Debug, serde::Deserialize, validator::Validate)]
|
||||
$(#[$struct_meta])*
|
||||
struct $struct_name {
|
||||
$(
|
||||
$(#[$field_meta])*
|
||||
$(pub $name: $type,)?
|
||||
)*
|
||||
}
|
||||
|
||||
impl $struct_name {
|
||||
/// Generate a [`Form`] which matches the shape of this struct.
|
||||
#[allow(clippy::needless_update)]
|
||||
fn build(validation_errors: Option<validator::ValidationErrors>) -> $crate::pages::components::form::Form<'static> {
|
||||
$crate::pages::components::form::Form {
|
||||
inputs: vec![
|
||||
$(
|
||||
$crate::pages::components::form::FormInput {
|
||||
id: stringify!($name),
|
||||
$(type_name: Some(stringify!($type)),)?
|
||||
$($prop: $value),*,
|
||||
..Default::default()
|
||||
},
|
||||
)*
|
||||
],
|
||||
validation_errors,
|
||||
submit_label: $submit_label,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
use askama::{Template, filters::HtmlSafe};
|
||||
use base64::Engine;
|
||||
use conduwuit_core::{Result, result::FlatOk, utils::TryFutureExtExt};
|
||||
use conduwuit_service::Services;
|
||||
use futures::TryFutureExt;
|
||||
use ruma::UserId;
|
||||
|
||||
pub(super) mod form;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) enum AvatarType<'a> {
|
||||
Initial(char),
|
||||
Image(&'a str),
|
||||
}
|
||||
|
||||
#[derive(Debug, Template)]
|
||||
#[template(path = "_components/avatar.html.j2")]
|
||||
pub(super) struct Avatar<'a> {
|
||||
pub(super) avatar_type: AvatarType<'a>,
|
||||
}
|
||||
|
||||
impl HtmlSafe for Avatar<'_> {}
|
||||
|
||||
#[derive(Debug, Template)]
|
||||
#[template(path = "_components/user_card.html.j2")]
|
||||
pub(super) struct UserCard<'a> {
|
||||
pub user_id: &'a UserId,
|
||||
pub display_name: Option<String>,
|
||||
pub avatar_src: Option<String>,
|
||||
}
|
||||
|
||||
impl HtmlSafe for UserCard<'_> {}
|
||||
|
||||
impl<'a> UserCard<'a> {
|
||||
pub async fn for_local_user(services: &Services, user_id: &'a UserId) -> Self {
|
||||
let display_name = services.users.displayname(user_id).await.ok();
|
||||
|
||||
let avatar_src = async {
|
||||
let avatar_url = services.users.avatar_url(user_id).await.ok()?;
|
||||
let avatar_mxc = avatar_url.parts().ok()?;
|
||||
let file = services.media.get(&avatar_mxc).await.flat_ok()?;
|
||||
|
||||
Some(format!(
|
||||
"data:{};base64,{}",
|
||||
file.content_type
|
||||
.unwrap_or_else(|| "application/octet-stream".to_owned()),
|
||||
file.content
|
||||
.map(|content| base64::prelude::BASE64_STANDARD.encode(content))
|
||||
.unwrap_or_default(),
|
||||
))
|
||||
}
|
||||
.await;
|
||||
|
||||
Self { user_id, display_name, avatar_src }
|
||||
}
|
||||
|
||||
fn avatar(&'a self) -> Avatar<'a> {
|
||||
let avatar_type = if let Some(ref avatar_src) = self.avatar_src {
|
||||
AvatarType::Image(avatar_src)
|
||||
} else if let Some(initial) = self
|
||||
.display_name
|
||||
.as_ref()
|
||||
.and_then(|display_name| display_name.chars().next())
|
||||
{
|
||||
AvatarType::Initial(initial)
|
||||
} else {
|
||||
AvatarType::Initial(self.user_id.localpart().chars().next().unwrap())
|
||||
};
|
||||
|
||||
Avatar { avatar_type }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user