/* 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) 2024 metamuffin */ use super::{format_form_error, hash_password}; use crate::{ database::DataAcid, routes::ui::{ account::{rocket_uri_macro_r_account_login, session::Session}, error::MyResult, layout::{DynLayoutPage, LayoutPage}, }, uri, }; use anyhow::anyhow; use jellybase::{ database::{ReadableTable, Ser, T_USER}, 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, #[field(validate = option_len(4..32))] display_name: Option, theme: Option, player_preference: Option, native_secret: Option, } fn option_len<'v>(value: &Option, range: Range) -> form::Result<'v, ()> { value.as_ref().map(|v| len(v, range)).unwrap_or(Ok(())) } fn settings_page(session: Session, flash: Option>) -> 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 { "Account" } a.switch_account[href=uri!(r_account_login())] { "Switch Account" } form[method="POST", action=uri!(r_account_settings_post())] { label[for="username"] { "Username" } input[type="text", id="username", disabled, value=&session.user.name]; input[type="submit", disabled, value="Immutable"]; } form[method="POST", action=uri!(r_account_settings_post())] { label[for="display_name"] { "Display Name" } input[type="text", id="display_name", name="display_name", value=&session.user.display_name]; input[type="submit", value="Update"]; } form[method="POST", action=uri!(r_account_settings_post())] { label[for="password"] { "Password" } input[type="password", id="password", name="password"]; input[type="submit", value="Update"]; } h2 { "Appearance" } form[method="POST", action=uri!(r_account_settings_post())] { fieldset { legend { "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="Apply"]; } form[method="POST", action=uri!(r_account_settings_post())] { fieldset { legend { "Preferred Media Player" } @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="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="Update"]; p { "The secret can be found in " code{"$XDG_CONFIG_HOME/jellynative_secret"} " or by clicking " a.button[href="jellynative://show-secret"] { "Show Secret" } "." } } }, ..Default::default() } } struct A(pub T); impl> RenderAttributeValue for A {} impl> Render for A { fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result { writer.write_fmt(format_args!("{}", &self.0 as &dyn UriDisplay)) } } #[get("/account/settings")] pub fn r_account_settings(session: Session) -> DynLayoutPage<'static> { settings_page(session, None) } #[post("/account/settings", data = "
")] pub fn r_account_settings_post( session: Session, database: &State, form: Form>, ) -> MyResult> { 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))))), }; let mut out = String::new(); let txn = database.begin_write()?; let mut users = txn.open_table(T_USER)?; let mut user = users .get(&*session.user.name)? .ok_or(anyhow!("user missing"))? .value() .0; if let Some(password) = &form.password { user.password = hash_password(&session.user.name, password); out += "Password updated\n"; } if let Some(display_name) = &form.display_name { user.display_name = display_name.clone(); out += "Display name updated\n"; } if let Some(theme) = form.theme { user.theme = theme; out += "Theme updated\n"; } if let Some(player_preference) = form.player_preference { user.player_preference = player_preference; out += "Player preference changed.\n"; } if let Some(native_secret) = &form.native_secret { user.native_secret = native_secret.to_owned(); out += "Native secret updated.\n"; } users.insert(&*session.user.name, Ser(user))?; drop(users); txn.commit()?; Ok(settings_page( session, // using the old session here, results in outdated theme being displayed Some(Ok(if out.is_empty() { "Nothing changed :)".to_string() } else { out })), )) }