use chrono::{offset::Utc, DateTime}; use leptos::prelude::*; use serde::{Deserialize, Serialize}; #[cfg(feature = "ssr")] use {crate::db::user, leptos::server_fn::error::NoCustomError, sqlx::SqlitePool}; pub fn format_delta(time: chrono::TimeDelta) -> String { let seconds = time.num_seconds(); match seconds { 0..=59 => format!( "{} second{} ago", seconds, if seconds == 1 { "" } else { "s" } ), 60..=3599 => { let minutes = seconds / 60; format!( "{} minute{} ago", minutes, if minutes == 1 { "" } else { "s" } ) } 3600..=86399 => { let hours = seconds / 3600; format!("{} hours{} ago", hours, if hours == 1 { "" } else { "s" }) } _ => { let days = seconds / 86400; format!("{} day{} ago", days, if days == 1 { "" } else { "s" }) } } } #[derive(Clone, Debug, Serialize, Deserialize, Eq)] pub struct User { pub user_id: i64, pub user_name: String, } impl std::cmp::PartialEq for User { fn eq(&self, other: &Self) -> bool { self.user_id == other.user_id } } #[derive(Clone, Serialize, Deserialize)] pub struct DbUser { user_id: i64, user_name: String, last_active: Option, } #[derive(Clone, Serialize, Deserialize)] pub struct PubUser { user_id: i64, user_name: String, last_active: Option>, } #[server(prefix = "/api/users", endpoint = "delete")] async fn delete_user(user_id: i64) -> Result<(), ServerFnError> { let user = user::get_auth_session().await?; if user.is_none() { return Err(ServerFnError::::ServerError( "You are not signed in!".to_owned(), )); } let pool = crate::db::get_db()?; sqlx::query!("DELETE FROM users WHERE user_id = ?", user_id) .execute(&pool) .await?; Ok(()) } #[server(prefix = "/api/users", endpoint = "reset")] async fn reset_password(user_id: i64, password: String) -> Result<(), ServerFnError> { let user = user::get_auth_session().await?; if user.is_none() { return Err(ServerFnError::::ServerError( "You are not signed in!".to_owned(), )); } let pool = crate::db::get_db()?; crate::db::user::reset_password(&pool, user_id as i16, password).await?; Ok(()) } #[component] pub fn RenderUser(refresh_user_list: Action<(), ()>, user: PubUser) -> impl IntoView { use leptos_use::{use_interval, UseIntervalReturn}; #[cfg_attr(feature = "ssr", allow(unused_variables))] let UseIntervalReturn { counter, .. } = use_interval(1000); #[cfg_attr(feature = "ssr", allow(unused_variables))] let (time_ago, set_time_ago) = signal( user.last_active .map(|active| format_delta(Utc::now() - active)), ); #[cfg(feature = "hydrate")] Effect::watch( move || counter.get(), move |_, _, _| { set_time_ago( user.last_active .map(|active| format_delta(Utc::now() - active)), ); }, false, ); let dialog_ref = NodeRef::::new(); let new_password = RwSignal::new("".to_owned()); let error_dialog_ref = NodeRef::::new(); let (error_msg, set_error_msg) = signal(None::); let clear_error = move |_| { set_error_msg(None); }; let handle_delete = Action::new(move |&id: &i64| async move { match delete_user(id).await { Ok(()) => { refresh_user_list.dispatch(()); } Err(e) => { set_error_msg(Some(e.to_string())); } } }); let handle_reset = Action::new(move |info: &(i64, String)| { let (id, password) = info.clone(); async move { match reset_password(id, password).await { Ok(()) => {} Err(e) => { set_error_msg(Some(e.to_string())); } } } }); view! { {error_msg}
  • {user.user_id} ": " {user.user_name} {move || time_ago.get().map(|active| view! { " (last activity: " {active} ")" })}
    "Reset password"
  • } } #[server(prefix = "/api/users", endpoint = "list")] async fn list_users() -> Result, ServerFnError> { let user = user::get_auth_session().await?; if user.is_none() { return Err(ServerFnError::::ServerError( "You are not signed in!".to_owned(), )); } use futures::stream::StreamExt; let pool = crate::db::get_db()?; let users = sqlx::query_as!(DbUser, "SELECT user_id, user_name, last_active FROM users") .fetch(&pool) .map(|user| { user.map(|u| PubUser { user_id: u.user_id, user_name: u.user_name, last_active: u .last_active .map(|ts| DateTime::from_timestamp(ts, 0)) .flatten(), }) }) .collect::>>() .await; let users: Result, _> = users.into_iter().collect(); Ok(users?) } #[server(prefix = "/api/users", endpoint = "add")] async fn add_user(name: String, password: String) -> Result<(), ServerFnError> { let user = user::get_auth_session().await?; if user.is_none() { return Err(ServerFnError::::ServerError( "You are not signed in!".to_owned(), )); } let pool = crate::db::get_db()?; crate::db::user::create_user(&pool, name, password).await?; Ok(()) } #[component] pub fn UserView() -> impl IntoView { #[cfg(feature = "hydrate")] Effect::new(move || { let user = expect_context::>>(); if user.get().is_none() { let navigate = leptos_router::hooks::use_navigate(); navigate("/login?next=beacons", Default::default()); } }); let user_list = Resource::new(|| (), |_| async move { list_users().await }); let modal_ref = NodeRef::::new(); let new_user_name = RwSignal::new("".to_owned()); let new_user_pass = RwSignal::new("".to_owned()); let (user_add_error, set_user_add_error) = signal(None::); let add_user_action = Action::new(move |_: &()| async move { if let Some(modal) = modal_ref.get() { let _ = modal.close(); } match add_user(new_user_name.get(), new_user_pass.get()).await { Ok(()) => { user_list.refetch(); } Err(e) => { set_user_add_error(Some(format!("Error adding user! {e:?}"))); } }; new_user_name.set("".to_owned()); new_user_pass.set("".to_owned()); }); let refresh_user_list = Action::new(move |_: &()| async move { user_list.refetch(); }); view! {
    "Add user"
    {move || user_add_error.get().map(|err| view! {

    "Error adding user! " {err}

    })}

    "User list"

    "Loading..."

    } > "Errors loading users!"

      {move || errors.get() .into_iter() .map(|(_, e)| view! {
    • {e.to_string()}
    • }) .collect::>()}
    } > {move || Suspend::new(async move { let users_res = user_list.await; users_res.map(|users| view! {
      {users .into_iter() .map(|user| view! { }) .collect::>()}
    }) })}
    } }