use api::{
Challenge, ChallengeAnswer, ChangePassword, Device, DeviceToken, Link, NewDevice, NewLink,
NewShare, NewUser, Passkey, ResetPassword, SetRetention, SetRole, SetTwoFactor, Share,
ShareSettings, Shares, User,
};
use leptos::prelude::*;
use leptos::task::spawn_local;
use wasm_bindgen::prelude::*;
use crate::i18n::tr;
use crate::{Account, ago, fmt_time, http, map, now_secs, passkey};
#[derive(Clone, Copy, PartialEq)]
enum Tab {
Sharing,
Links,
Devices,
History,
Security,
Users,
}
#[component]
pub fn Settings() -> impl IntoView {
let account = expect_context::();
let tab = RwSignal::new(Tab::Sharing);
let is_admin = move || account.me().is_some_and(|m| m.is_admin);
let tab_button = move |t: Tab, label: &'static str| {
let label = tr(label);
view! { }
};
view! {
{move || match tab.get() {
Tab::Sharing => view! { }.into_any(),
Tab::Links => view! { }.into_any(),
Tab::Devices => view! { }.into_any(),
Tab::History => view! { }.into_any(),
Tab::Security => view! { }.into_any(),
Tab::Users => view! { }.into_any(),
}}
}
}
fn confirm(msg: &str) -> bool {
window().confirm_with_message(&tr(msg)).unwrap_or(false)
}
fn expiry(e: Option) -> String {
match e {
None => tr("never"),
Some(t) => fmt_time(t),
}
}
/// The base URL devices connect to.
fn server_url(account: Account) -> String {
account
.me()
.and_then(|m| m.public_url)
.unwrap_or_else(|| window().location().origin().unwrap_or_default())
}
/// Shows an Ok message or an Err message.
fn status(message: RwSignal
}
>
" "
{tr("Needs a passkey, and two-factor sign-in turned off.")}
{status(pw_message)}
{tr("Passkeys")}
{move || {
passkeys
.get()
.map(|r| match r {
Err(e) => view! { {e.to_string()}
}.into_any(),
Ok(list) if list.is_empty() => view! { {tr("No passkeys yet.")}
}.into_any(),
Ok(list) => {
view! {
| {tr("Name")} | {tr("Added")} | {tr("Last used")} | |
{list
.into_iter()
.map(|k| {
view! {
| {k.name.clone()} |
{fmt_time(k.created_at)} |
{k.last_used_at.map_or(tr("never"), ago)} |
|
}
})
.collect_view()}
}
.into_any()
}
})
}}
"This browser does not support passkeys." }
>
{status(key_message)}
}
}
#[component]
fn Users() -> impl IntoView {
let account = expect_context::();
let my_id = account.me().map(|m| m.id);
let version = RwSignal::new(0);
let message = RwSignal::new(None::>);
let users = LocalResource::new(move || {
version.track();
http::get::>("/api/users")
});
let username = RwSignal::new(String::new());
let password = RwSignal::new(String::new());
let is_admin = RwSignal::new(false);
let create = move |ev: leptos::ev::SubmitEvent| {
ev.prevent_default();
let body = NewUser {
username: username.get_untracked(),
password: password.get_untracked(),
is_admin: is_admin.get_untracked(),
};
spawn_local(async move {
match http::post::("/api/users", &body).await {
Ok(u) => {
username.set(String::new());
password.set(String::new());
is_admin.set(false);
message.set(Some(Ok(tr(&format!("Created {}.", u.username)))));
}
Err(e) => message.set(Some(Err(e.to_string()))),
}
version.update(|v| *v += 1);
});
};
let reset = move |u: &User| {
let prompt = format!(
"New password for {}. This also removes their passkeys, disconnects their devices, turns off two-factor sign-in and logs them out.",
u.username
);
let Ok(Some(password)) = window().prompt_with_message(&tr(&prompt)) else {
return;
};
let (id, name) = (u.id, u.username.clone());
spawn_local(async move {
match http::post::<()>(
&format!("/api/users/{id}/password"),
&ResetPassword { password },
)
.await
{
Ok(()) => message.set(Some(Ok(tr(&format!("Password reset for {name}."))))),
Err(e) => message.set(Some(Err(e.to_string()))),
}
});
};
let set_role = move |u: &User, is_admin: bool| {
let (id, name) = (u.id, u.username.clone());
spawn_local(async move {
match http::put::<()>(&format!("/api/users/{id}/role"), &SetRole { is_admin }).await {
Ok(()) => message.set(Some(Ok(tr(&if is_admin {
format!("{name} is now an admin.")
} else {
format!("{name} is now a user.")
})))),
Err(e) => message.set(Some(Err(e.to_string()))),
}
version.update(|v| *v += 1);
});
};
let remove = move |u: &User| {
if !confirm(&format!("Delete {} and all their data?", u.username)) {
return;
}
let id = u.id;
spawn_local(async move {
if let Err(e) = http::delete(&format!("/api/users/{id}")).await {
message.set(Some(Err(e.to_string())));
}
version.update(|v| *v += 1);
});
};
view! {
{tr("Add a user")}
{status(message)}
{tr("Users")}
{move || {
users
.get()
.map(|r| match r {
Err(e) => view! { {e.to_string()}
}.into_any(),
Ok(list) => {
view! {
| {tr("Username")} | {tr("Role")} | {tr("Created")} | |
{list
.into_iter()
.map(|u| {
let me = Some(u.id) == my_id;
let u2 = u.clone();
let u3 = u.clone();
view! {
| {u.username.clone()} |
|
{fmt_time(u.created_at)} |
" "
|
}
})
.collect_view()}
}
.into_any()
}
})
}}
}
}