diff options
Diffstat (limited to 'server/src/ui/account/settings.rs')
-rw-r--r-- | server/src/ui/account/settings.rs | 185 |
1 files changed, 185 insertions, 0 deletions
diff --git a/server/src/ui/account/settings.rs b/server/src/ui/account/settings.rs new file mode 100644 index 0000000..4047e4f --- /dev/null +++ b/server/src/ui/account/settings.rs @@ -0,0 +1,185 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2025 metamuffin <metamuffin.org> +*/ +use super::{format_form_error, hash_password}; +use crate::{ + database::Database, + locale::AcceptLanguage, + ui::{ + account::{rocket_uri_macro_r_account_login, session::Session}, + error::MyResult, + layout::{trs, DynLayoutPage, LayoutPage}, + }, + uri, +}; +use jellybase::{ + locale::{tr, Language}, + permission::PermissionSetExt, +}; +use jellycommon::user::{PlayerKind, Theme, UserPermission}; +use markup::{Render, RenderAttributeValue}; +use rocket::{ + form::{self, validate::len, Contextual, Form}, + get, + http::uri::fmt::{Query, UriDisplay}, + post, FromForm, State, +}; +use std::ops::Range; + +#[derive(FromForm)] +pub struct SettingsForm { + #[field(validate = option_len(4..64))] + password: Option<String>, + #[field(validate = option_len(4..32))] + display_name: Option<String>, + theme: Option<Theme>, + player_preference: Option<PlayerKind>, + native_secret: Option<String>, +} + +fn option_len<'v>(value: &Option<String>, range: Range<usize>) -> form::Result<'v, ()> { + value.as_ref().map(|v| len(v, range)).unwrap_or(Ok(())) +} + +fn settings_page( + session: Session, + flash: Option<MyResult<String>>, + lang: Language, +) -> DynLayoutPage<'static> { + LayoutPage { + title: "Settings".to_string(), + class: Some("settings"), + content: markup::new! { + h1 { "Settings" } + @if let Some(flash) = &flash { + @match flash { + Ok(mesg) => { section.message { p.success { @mesg } } } + Err(err) => { section.message { p.error { @format!("{err}") } } } + } + } + h2 { @trs(&lang, "account") } + a.switch_account[href=uri!(r_account_login())] { "Switch Account" } + form[method="POST", action=uri!(r_account_settings_post())] { + label[for="username"] { @trs(&lang, "account.username") } + input[type="text", id="username", disabled, value=&session.user.name]; + input[type="submit", disabled, value=&*tr(lang, "settings.immutable")]; + } + form[method="POST", action=uri!(r_account_settings_post())] { + label[for="display_name"] { @trs(&lang, "account.display_name") } + input[type="text", id="display_name", name="display_name", value=&session.user.display_name]; + input[type="submit", value=&*tr(lang, "settings.update")]; + } + form[method="POST", action=uri!(r_account_settings_post())] { + label[for="password"] { @trs(&lang, "account.password") } + input[type="password", id="password", name="password"]; + input[type="submit", value=&*tr(lang, "settings.update")]; + } + h2 { @trs(&lang, "settings.appearance") } + form[method="POST", action=uri!(r_account_settings_post())] { + fieldset { + legend { @trs(&lang, "settings.appearance.theme") } + @for (t, tlabel) in Theme::LIST { + label { input[type="radio", name="theme", value=A(*t), checked=session.user.theme==*t]; @tlabel } br; + } + } + input[type="submit", value=&*tr(lang, "settings.apply")]; + } + form[method="POST", action=uri!(r_account_settings_post())] { + fieldset { + legend { @trs(&lang, "settings.player_preference") } + @for (t, tlabel) in PlayerKind::LIST { + label { input[type="radio", name="player_preference", value=A(*t), checked=session.user.player_preference==*t]; @tlabel } br; + } + } + input[type="submit", value=&*tr(lang, "settings.apply")]; + } + form[method="POST", action=uri!(r_account_settings_post())] { + label[for="native_secret"] { "Native Secret" } + input[type="password", id="native_secret", name="native_secret"]; + input[type="submit", value=&*tr(lang, "settings.update")]; + p { "The secret can be found in " code{"$XDG_CONFIG_HOME/jellynative_secret"} " or by clicking " a.button[href="jellynative://show-secret-v1"] { "Show Secret" } "." } + } + }, + } +} + +struct A<T>(pub T); +impl<T: UriDisplay<Query>> RenderAttributeValue for A<T> {} +impl<T: UriDisplay<Query>> Render for A<T> { + fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result { + writer.write_fmt(format_args!("{}", &self.0 as &dyn UriDisplay<Query>)) + } +} + +#[get("/account/settings")] +pub fn r_account_settings(session: Session, lang: AcceptLanguage) -> DynLayoutPage<'static> { + let AcceptLanguage(lang) = lang; + settings_page(session, None, lang) +} + +#[post("/account/settings", data = "<form>")] +pub fn r_account_settings_post( + session: Session, + database: &State<Database>, + form: Form<Contextual<SettingsForm>>, + lang: AcceptLanguage, +) -> MyResult<DynLayoutPage<'static>> { + let AcceptLanguage(lang) = lang; + session + .user + .permissions + .assert(&UserPermission::ManageSelf)?; + + let form = match &form.value { + Some(v) => v, + None => { + return Ok(settings_page( + session, + Some(Err(format_form_error(form))), + lang, + )) + } + }; + + let mut out = String::new(); + + database.update_user(&session.user.name, |user| { + if let Some(password) = &form.password { + user.password = hash_password(&session.user.name, password); + out += &*tr(lang, "settings.account.password.changed"); + out += "\n"; + } + if let Some(display_name) = &form.display_name { + user.display_name = display_name.clone(); + out += &*tr(lang, "settings.account.display_name.changed"); + out += "\n"; + } + if let Some(theme) = form.theme { + user.theme = theme; + out += &*tr(lang, "settings.account.theme.changed"); + out += "\n"; + } + if let Some(player_preference) = form.player_preference { + user.player_preference = player_preference; + out += &*tr(lang, "settings.player_preference.changed"); + out += "\n"; + } + if let Some(native_secret) = &form.native_secret { + user.native_secret = native_secret.to_owned(); + out += "Native secret updated.\n"; + } + Ok(()) + })?; + + Ok(settings_page( + session, // using the old session here, results in outdated theme being displayed + Some(Ok(if out.is_empty() { + tr(lang, "settings.no_change").to_string() + } else { + out + })), + lang, + )) +} |