Mercurial > lbo > hg > analyrics
changeset 68:2a7b358c2710
Deploy settings page (no admin stuff yet)
author | Lewin Bormann <lbo@spheniscida.de> |
---|---|
date | Sun, 31 Jul 2022 21:17:53 -0700 |
parents | 8b0e474ba90a |
children | 3d4401e111d8 |
files | Cargo.toml assets/index.html.hbs assets/index_dashboard.html.hbs assets/settings.html.hbs assets/static/style.css src/configdb.rs src/main.rs |
diffstat | 7 files changed, 231 insertions(+), 43 deletions(-) [+] |
line wrap: on
line diff
--- a/Cargo.toml Tue Jul 26 08:40:12 2022 -0700 +++ b/Cargo.toml Sun Jul 31 21:17:53 2022 -0700 @@ -10,15 +10,17 @@ either = "1.7.0" env_logger = "0.9.0" log = "0.4.17" -rocket = { version = "0.5.0-rc.2", features = ["secrets", "json"] } +rand = "0.8" sqlx = { version = "0.5", features = ["macros"] } -rocket_db_pools = { version = "0.1.0-rc.2", features = [ "sqlx_macros"] } -rocket_dyn_templates = { version = "0.1.0-rc.2", features = ["handlebars"] } sha256 = "1.0.3" time = { version = "0.3.11", features = ["serde", "serde-well-known"] } tokio = { version = "1.19.2", features = ["fs"] } maxminddb = "0.23.0" +rocket = { version = "0.5.0-rc.2", features = ["secrets", "json"] } +rocket_db_pools = { version = "0.1.0-rc.2", features = [ "sqlx_macros"] } +rocket_dyn_templates = { version = "0.1.0-rc.2", features = ["handlebars"] } + [features] default = ["sqlite"] sqlite = ["rocket_db_pools/sqlx_sqlite"]
--- a/assets/index.html.hbs Tue Jul 26 08:40:12 2022 -0700 +++ b/assets/index.html.hbs Sun Jul 31 21:17:53 2022 -0700 @@ -9,40 +9,6 @@ <link rel="stylesheet" href="static/style.css" type="text/css" /> <link rel="icon" type="image/png" href="static/favicon.ico"> - <script src="static/chart.min.js" type="application/javascript"></script> - - <script type="application/javascript"> - let from = "{{ date.today }}"; - let duration = {{ date.duration }}; // in days - - function formatDate(d) { - let m = d.getUTCMonth()+1; m = m < 10 ? "0"+m : m; - let day = d.getUTCDate(); day = day < 10 ? "0"+day : day; - - return `${d.getUTCFullYear()}-${m}-${day}`; - } - function adjustURLDateParams(dateOffsetDays, durationMultiplier) { - let usp = new URLSearchParams(window.location.search); - let from_date = new Date(usp.get("from") || from); - console.log("from date", from_date, " ", dateOffsetDays); - let gduration = +usp.get("duration") || duration; - - let new_from_date = formatDate(new Date(from_date.getTime() + dateOffsetDays * 86400 * 1000)); - console.log(from_date, new_from_date); - let new_duration = gduration * durationMultiplier; - - usp.set("from", new_from_date); - usp.set("duration", new_duration.toFixed()); - window.location.search = usp.toString(); - }; - function toggleBots(checkboxid) { - let box = document.getElementById(checkboxid); - let usp = new URLSearchParams(window.location.search); - usp.set("includebots", box.checked); - window.location.search = usp.toString(); - }; - </script> - </head> <body>
--- a/assets/index_dashboard.html.hbs Tue Jul 26 08:40:12 2022 -0700 +++ b/assets/index_dashboard.html.hbs Sun Jul 31 21:17:53 2022 -0700 @@ -57,6 +57,7 @@ <form id="logout" action="logout" method="POST"> <input type="submit" value="Log out" /> </form> + <div class="usersettings"><a href="{{ basepath }}settings">Settings</a></div> <div class="useraction"><b>{{username}}</b>.</div> <div class="userdomain">{{#if domain}}Domain: <b>{{domain}}</b>{{else}}<i>(all domains)</i>{{/if}}</div> </div>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/assets/settings.html.hbs Sun Jul 31 21:17:53 2022 -0700 @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<html> + <head> + <title>Analyrics - Settings</title> + + <meta name="robots" content="noindex"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + + <link rel="stylesheet" href="static/style.css" type="text/css" /> + <link rel="icon" type="image/png" href="static/favicon.ico"> + + </head> + <body> + + <!-- Header --> + <div id="header"> + <span id="logo">AnaLyrics</span> + <form id="logout" action="logout" method="POST"> + <input type="submit" value="Log out" /> + </form> + <a class="useraction" href="{{ basepath }}">Dashboard</a> + </div> + {{#if flash}}<div id="flash"><span id="flashtext">{{flash}}<span></div>{{/if}} + {{#if error}}<div id="error"><span id="errortext">{{error}}</span></div>{{/if}} + + <h1>Welcome, {{user}}!</h1> + + <h2>Password</h2> + <form id="pwform" action="{{thispath}}" method="POST"> + <label for="oldpassword">Old password:</label> + <input type="password" name="oldpassword" /> + <label for="newpassword">New password:</label> + <input type="password" name="newpassword" /> + <input type="submit" value="Save" /> + </form> + + <h2>Time zone</h2> + <form id="tzform" action="{{thispath}}" method="POST"> + <label>Your time zone:</label> + <input type="number" min="-12" max="12" name="tz_offset" value="{{tz_offset}}" /> + <input type="submit" value="Save" /> + </form> + + </body> +</html>
--- a/assets/static/style.css Tue Jul 26 08:40:12 2022 -0700 +++ b/assets/static/style.css Sun Jul 31 21:17:53 2022 -0700 @@ -1,8 +1,9 @@ #logo { color: #22bb22; font-size: 20pt; font-style: bold; } #header { color: #bb2222; } -.useraction { float: right; padding: 3pt; } -.userdomain { float: right; padding: 3pt; } -#logout { float: right; padding: 3pt; } +.useraction { float: right; padding: 5pt; } +.userdomain { float: right; padding: 5pt; } +.usersettings { float: right; padding: 5pt; } +#logout { float: right; padding: 3pt; vertical-align: middle; } #flashtext { } #flash { text-align: center; border-style: solid; border-color: #22aa22; margin-left: 30%; margin-right: 30%; } #errortext { } @@ -17,6 +18,7 @@ .fullwidth { width: 95%; } .halfwidth { width: 48%; } +/* Dashboard */ #timebox { display: flex; flex-direction: column; align-items: center; padding: 0.5em; } #timenav { } #timeindicator { padding: 0.2em; } @@ -42,3 +44,6 @@ button { height: 1.8em; border: lightgray solid; text-align: center; vertical-align: middle; } input:focus { outline: 0; } label { } + +/* Settings */ +
--- a/src/configdb.rs Tue Jul 26 08:40:12 2022 -0700 +++ b/src/configdb.rs Sun Jul 31 21:17:53 2022 -0700 @@ -3,11 +3,16 @@ use anyhow::Error; use log::{debug, error, info, warn, Level}; +use rand::distributions::{Alphanumeric, DistString}; use rocket::futures::StreamExt; use rocket_db_pools::sqlx::{Executor, Row, Sqlite, SqlitePool}; use rocket_db_pools::{Connection, Database, Pool}; use sqlx::prelude::FromRow; +fn generate_salt() -> String { + Alphanumeric.sample_string(&mut rand::thread_rng(), 16) +} + // TO DO: use other databases? #[derive(Database)] #[database("sqlite_main")] @@ -22,10 +27,10 @@ pub struct ConfigDBSession<'p, DB: sqlx::Database>(pub &'p mut sqlx::pool::PoolConnection<DB>); impl<'p> ConfigDBSession<'p, Sqlite> { - pub async fn check_user_password<S: AsRef<str>>( + pub async fn check_user_password<S0: AsRef<str>, S1: AsRef<str>>( &mut self, - user: S, - password: S, + user: S0, + password: S1, ) -> Result<bool, Error> { // TODO: salt passwords. let salt: String = match sqlx::query("SELECT salt FROM users WHERE username = ? LIMIT 1;") @@ -54,4 +59,49 @@ .await?; Ok(UserRecord::from_row(&entry)?) } + + pub async fn update_user_password<S0: AsRef<str>, S1: AsRef<str>, S2: AsRef<str>>( + &mut self, + user: S0, + oldpass: S1, + newpass: S2, + ) -> Result<(), Error> { + if !self.check_user_password(user, oldpass).await? { + anyhow::bail!("User not authorized (wrong password?)") + } + + // Old password is ok. + let salt = generate_salt(); + let newpass_hash = sha256::digest(format!("{}{}", salt, newpass.as_ref())); + + sqlx::query("UPDATE users SET salt = ?, password_hash = ?;") + .bind(salt) + .bind(newpass_hash) + .execute(&mut *self.0) + .await?; + + Ok(()) + } + + pub async fn update_user_tz<S0: AsRef<str>>( + &mut self, + user: S0, + tz_offset: i64, + ) -> Result<(), Error> { + if sqlx::query("SELECT username FROM users WHERE username = ?") + .bind(user.as_ref()) + .fetch_all(&mut *self.0) + .await? + .len() + != 1 + { + anyhow::bail!("User not found, or duplicate user {}", user.as_ref()) + } + sqlx::query("UPDATE users SET tz_offset = ? WHERE username = ?") + .bind(tz_offset) + .bind(user.as_ref()) + .execute(&mut *self.0) + .await?; + Ok(()) + } }
--- a/src/main.rs Tue Jul 26 08:40:12 2022 -0700 +++ b/src/main.rs Sun Jul 31 21:17:53 2022 -0700 @@ -191,6 +191,7 @@ async fn route_index_loggedin( mut conn: Connection<LogsDB>, mut config_conn: Connection<ConfigDB>, + cc: &rocket::State<CustomConfig>, lig: LoggedInGuard, flash: Option<FlashMessage<'_>>, domain: Option<String>, @@ -373,6 +374,7 @@ "index_dashboard", context![ loggedin: true, + basepath: &cc.deploy_path, domain: domain, username: lig.0, flash: f, @@ -501,6 +503,120 @@ } } +#[derive(rocket::FromForm)] +struct SettingsForm { + oldpassword: Option<String>, + newpassword: Option<String>, + tz_offset: Option<i64>, +} + +#[rocket::post("/settings", data = "<settings>")] +async fn route_settings_post( + mut cdb: Connection<ConfigDB>, + cc: &rocket::State<CustomConfig>, + lig: LoggedInGuard, + settings: rocket::form::Form<SettingsForm>, +) -> Flash<Redirect> { + let mut db = ConfigDBSession(&mut cdb); + let SettingsForm { + oldpassword, + newpassword, + tz_offset, + } = settings.into_inner(); + let deploy_path = cc.deploy_path.clone(); + let settingspath = Path::new(&deploy_path) + .join("settings") + .to_str() + .unwrap_or("/settings") + .to_string(); + + // Update settings before rendering. + if let (Some(newpassword), Some(oldpassword)) = (&newpassword, &oldpassword) { + if let Err(e) = db + .update_user_password(&lig.0, &oldpassword, &newpassword) + .await + { + return Flash::error( + Redirect::to(settingspath), + format!("Couldn't update user password: {}", e), + ); + } + } else if newpassword.is_some() ^ oldpassword.is_some() { + return Flash::error( + Redirect::to(settingspath), + "Internal error: must supply old and new password".to_string(), + ); + } + + if let Some(tz_offset) = tz_offset { + if let Err(e) = db.update_user_tz(&lig.0, tz_offset * 3600).await { + return Flash::error( + Redirect::to(settingspath), + format!("Couldn't update timezone: {}", e), + ); + } + } + + Flash::success(Redirect::to(settingspath), format!("Saved successfully!")) +} + +#[rocket::get("/settings", rank = 1)] +async fn route_settings( + mut cdb: Connection<ConfigDB>, + flash: Option<FlashMessage<'_>>, + cc: &rocket::State<CustomConfig>, + lig: LoggedInGuard, +) -> Template { + let mut db = ConfigDBSession(&mut cdb); + let user = match db.get_user_details(&lig.0).await { + Ok(r) => r, + Err(e) => { + return Template::render( + "settings", + context![ basepath: &cc.deploy_path, error: format!("Error getting user details: {}", e) ], + ) + } + }; + + let error = if let Some(flash) = &flash { + if flash.kind() == "error" { + Some(flash.message()) + } else { + None + } + } else { + None + }; + let success = if let Some(flash) = &flash { + if flash.kind() == "success" { + Some(flash.message()) + } else { + None + } + } else { + None + }; + + Template::render( + "settings", + context![ + basepath: &cc.deploy_path, + thispath: format!("{}{}", &cc.deploy_path, "settings"), + user: &user.name, + error: error, + flash: success, + + tz_offset: format!("{:}", user.tz_offset / 3600), + + ], + ) +} + +#[rocket::get("/settings", rank = 2)] +async fn route_settings_loggedout(cc: &rocket::State<CustomConfig>) -> Redirect { + Redirect::to(cc.deploy_path.clone()) +} + #[derive(rocket::serde::Deserialize)] #[serde(crate = "rocket::serde")] struct CustomConfig { @@ -549,6 +665,9 @@ rocket::routes![ route_index_loggedin, route_index_loggedout, + route_settings, + route_settings_post, + route_settings_loggedout, route_logout, route_login_form, route_login_post,