mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
feat: Implement a web-based account management dashboard
This commit is contained in:
@@ -1,13 +1,25 @@
|
||||
use askama::{Template, filters::HtmlSafe};
|
||||
use validator::ValidationErrors;
|
||||
use validator::{ValidationError, ValidationErrors};
|
||||
|
||||
/// A reusable form component with field validation.
|
||||
#[derive(Debug, Template)]
|
||||
#[template(path = "_components/form.html.j2")]
|
||||
pub(crate) struct Form<'a> {
|
||||
pub inputs: Vec<FormInput<'a>>,
|
||||
inputs: Vec<FormInput<'a>>,
|
||||
submit_label: &'a str,
|
||||
slowdown: bool,
|
||||
pub validation_errors: Option<ValidationErrors>,
|
||||
pub submit_label: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Form<'a> {
|
||||
pub(crate) fn new(inputs: Vec<FormInput<'a>>, submit_label: &'a str, slowdown: bool) -> Self {
|
||||
Self {
|
||||
inputs,
|
||||
submit_label,
|
||||
slowdown,
|
||||
validation_errors: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HtmlSafe for Form<'_> {}
|
||||
@@ -50,6 +62,16 @@ impl Default for FormInput<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! default {
|
||||
($value:expr) => {
|
||||
$value
|
||||
};
|
||||
() => {
|
||||
Default::default()
|
||||
};
|
||||
}
|
||||
|
||||
/// Generate a deserializable struct which may be turned into a [`Form`]
|
||||
/// for inclusion in another template.
|
||||
#[macro_export]
|
||||
@@ -63,6 +85,7 @@ macro_rules! form {
|
||||
),*
|
||||
|
||||
submit: $submit_label:expr
|
||||
$(, slowdown: $slowdown:expr)?
|
||||
}
|
||||
) => {
|
||||
#[derive(Debug, serde::Deserialize, validator::Validate)]
|
||||
@@ -77,9 +100,9 @@ macro_rules! form {
|
||||
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![
|
||||
fn build() -> $crate::pages::components::form::Form<'static> {
|
||||
$crate::pages::components::form::Form::new(
|
||||
vec![
|
||||
$(
|
||||
$crate::pages::components::form::FormInput {
|
||||
id: stringify!($name),
|
||||
@@ -89,9 +112,17 @@ macro_rules! form {
|
||||
},
|
||||
)*
|
||||
],
|
||||
validation_errors,
|
||||
submit_label: $submit_label,
|
||||
}
|
||||
$submit_label,
|
||||
$crate::default!($($slowdown)?)
|
||||
)
|
||||
}
|
||||
|
||||
/// Generate a [`Form`] with validation errors.
|
||||
#[allow(unused)]
|
||||
fn with_errors(errors: validator::ValidationErrors) -> $crate::pages::components::form::Form<'static> {
|
||||
let mut form = Self::build();
|
||||
form.validation_errors = Some(errors);
|
||||
form
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+108
-11
@@ -1,8 +1,10 @@
|
||||
use std::time::SystemTime;
|
||||
|
||||
use askama::{Template, filters::HtmlSafe};
|
||||
use base64::Engine;
|
||||
use conduwuit_core::result::FlatOk;
|
||||
use conduwuit_service::{Services, media::mxc::Mxc};
|
||||
use ruma::UserId;
|
||||
use conduwuit_core::{result::FlatOk, utils};
|
||||
use conduwuit_service::{Services, media::mxc::Mxc, oauth::client_metadata::ClientMetadata};
|
||||
use ruma::{OwnedDeviceId, OwnedUserId, UserId};
|
||||
|
||||
pub(super) mod form;
|
||||
|
||||
@@ -22,20 +24,20 @@ 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(super) struct UserCard {
|
||||
pub user_id: OwnedUserId,
|
||||
pub display_name: Option<String>,
|
||||
pub avatar_src: Option<String>,
|
||||
}
|
||||
|
||||
impl HtmlSafe for UserCard<'_> {}
|
||||
impl HtmlSafe for UserCard {}
|
||||
|
||||
impl<'a> UserCard<'a> {
|
||||
pub(super) async fn for_local_user(services: &Services, user_id: &'a UserId) -> Self {
|
||||
let display_name = services.users.displayname(user_id).await.ok();
|
||||
impl UserCard {
|
||||
pub(super) async fn for_local_user(services: &Services, user_id: OwnedUserId) -> 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_url = services.users.avatar_url(&user_id).await.ok()?;
|
||||
let (server_name, media_id) = avatar_url.parts().ok()?;
|
||||
let file = services
|
||||
.media
|
||||
@@ -57,7 +59,7 @@ impl<'a> UserCard<'a> {
|
||||
Self { user_id, display_name, avatar_src }
|
||||
}
|
||||
|
||||
fn avatar(&'a self) -> Avatar<'a> {
|
||||
fn avatar(&self) -> Avatar<'_> {
|
||||
let avatar_type = if let Some(ref avatar_src) = self.avatar_src {
|
||||
AvatarType::Image(avatar_src)
|
||||
} else if let Some(initial) = self
|
||||
@@ -73,3 +75,98 @@ impl<'a> UserCard<'a> {
|
||||
Avatar { avatar_type }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Template)]
|
||||
#[template(path = "_components/device_card.html.j2")]
|
||||
pub(super) struct DeviceCard {
|
||||
pub device_id: OwnedDeviceId,
|
||||
pub display_name: Option<String>,
|
||||
pub avatar_src: Option<String>,
|
||||
pub last_active: String,
|
||||
pub oauth_metadata: Option<ClientMetadata>,
|
||||
}
|
||||
|
||||
impl HtmlSafe for DeviceCard {}
|
||||
|
||||
impl DeviceCard {
|
||||
pub(super) async fn for_device(
|
||||
services: &Services,
|
||||
user_id: &UserId,
|
||||
device_id: OwnedDeviceId,
|
||||
) -> Self {
|
||||
let device = services
|
||||
.users
|
||||
.get_device_metadata(user_id, &device_id)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let oauth_metadata = async {
|
||||
let client_id = services.oauth.get_client_id_for_device(&device_id).await?;
|
||||
|
||||
Some(
|
||||
services
|
||||
.oauth
|
||||
.get_client_registration(&client_id)
|
||||
.await
|
||||
.expect("client should exist"),
|
||||
)
|
||||
}
|
||||
.await;
|
||||
|
||||
let display_name = oauth_metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.client_name.clone())
|
||||
.or(device
|
||||
.as_ref()
|
||||
.and_then(|device| device.display_name.clone()));
|
||||
|
||||
let avatar_src = oauth_metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.logo_uri.as_ref())
|
||||
.map(|uri| uri.as_str().to_owned());
|
||||
|
||||
let last_active = device
|
||||
.as_ref()
|
||||
.and_then(|device| device.last_seen_ts)
|
||||
.map_or_else(
|
||||
|| "unknown".to_owned(),
|
||||
|active| {
|
||||
active
|
||||
.to_system_time()
|
||||
.and_then(|t| SystemTime::now().duration_since(t).ok())
|
||||
.map_or_else(
|
||||
|| "now".to_owned(),
|
||||
|duration| format!("{} ago", utils::time::pretty(duration)),
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
Self {
|
||||
device_id,
|
||||
display_name,
|
||||
avatar_src,
|
||||
last_active,
|
||||
oauth_metadata,
|
||||
}
|
||||
}
|
||||
|
||||
fn avatar(&self) -> Avatar<'_> {
|
||||
let avatar_type = if let Some(avatar_src) = &self.avatar_src {
|
||||
AvatarType::Image(avatar_src.as_str())
|
||||
} else if let Some(initial) = self
|
||||
.display_name
|
||||
.as_ref()
|
||||
.and_then(|name| name.chars().next())
|
||||
{
|
||||
if self.oauth_metadata.is_some() {
|
||||
AvatarType::Initial(initial)
|
||||
} else {
|
||||
AvatarType::Initial('❖')
|
||||
}
|
||||
} else {
|
||||
AvatarType::Initial('?')
|
||||
};
|
||||
|
||||
Avatar { avatar_type }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user