diff --git a/src/cmds/config/mod.rs b/src/cmds/config/mod.rs index 9ec5231..4298caa 100644 --- a/src/cmds/config/mod.rs +++ b/src/cmds/config/mod.rs @@ -47,6 +47,7 @@ use std::str::FromStr; mod cameras; mod dirs; +mod users; static USAGE: &'static str = r#" Interactive configuration editor. @@ -137,8 +138,9 @@ pub fn run() -> Result<(), Error> { let db = db.clone(); move |siv, item| item(&db, siv) }) - .item("Directories and retention".to_string(), dirs::top_dialog) .item("Cameras and streams".to_string(), cameras::top_dialog) + .item("Directories and retention".to_string(), dirs::top_dialog) + .item("Users".to_string(), users::top_dialog) ) .button("Quit", |siv| siv.quit()) .title("Main menu")); diff --git a/src/cmds/config/users.rs b/src/cmds/config/users.rs new file mode 100644 index 0000000..01a7358 --- /dev/null +++ b/src/cmds/config/users.rs @@ -0,0 +1,198 @@ +// This file is part of Moonfire NVR, a security camera digital video recorder. +// Copyright (C) 2017 Scott Lamb +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// In addition, as a special exception, the copyright holders give +// permission to link the code of portions of this program with the +// OpenSSL library under certain conditions as described in each +// individual source file, and distribute linked combinations including +// the two. +// +// You must obey the GNU General Public License in all respects for all +// of the code used other than OpenSSL. If you modify file(s) with this +// exception, you may extend this exception to your version of the +// file(s), but you are not obligated to do so. If you do not wish to do +// so, delete this exception statement from your version. If you delete +// this exception statement from all source files in the program, then +// also delete it here. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +extern crate cursive; + +use self::cursive::Cursive; +use self::cursive::traits::{Boxable, Identifiable}; +use self::cursive::views; +use db; +use std::sync::Arc; + +/// Builds a `UserChange` from an active `edit_user_dialog`. +fn get_change(siv: &mut Cursive, db: &db::LockedDatabase, id: Option, + pw: PasswordChange) -> db::UserChange { + let mut change = match id { + Some(id) => db.users_by_id().get(&id).unwrap().change(), + None => db::UserChange::add_user(String::new()), + }; + change.username.clear(); + change.username += siv.find_id::("username").unwrap().get_content().as_str(); + match pw { + PasswordChange::Leave => {}, + PasswordChange::Set => { + let pwd = siv.find_id::("new_pw").unwrap().get_content(); + change.set_password(pwd.as_str().into()); + }, + PasswordChange::Clear => change.clear_password(), + }; + change +} + +fn press_edit(siv: &mut Cursive, db: &Arc, id: Option, pw: PasswordChange) { + let result = { + let mut l = db.lock(); + let c = get_change(siv, &l, id, pw); + l.apply_user_change(c).map(|_| ()) + }; + if let Err(e) = result { + siv.add_layer(views::Dialog::text(format!("Unable to apply change: {}", e)) + .title("Error") + .dismiss_button("Abort")); + } else { + siv.pop_layer(); // get rid of the add/edit user dialog. + + // Recreate the "Edit users" dialog from scratch; it's easier than adding the new entry. + siv.pop_layer(); + top_dialog(db, siv); + } +} + +fn press_delete(siv: &mut Cursive, db: &Arc, id: i32, name: String) { + siv.add_layer(views::Dialog::text(format!("Delete user {}?", name)) + .button("Delete", { + let db = db.clone(); + move |s| actually_delete(s, &db, id) + }) + .title("Delete user").dismiss_button("Cancel")); +} + +fn actually_delete(siv: &mut Cursive, db: &Arc, id: i32) { + siv.pop_layer(); // get rid of the add/edit user dialog. + let result = { + let mut l = db.lock(); + l.delete_user(id) + }; + if let Err(e) = result { + siv.add_layer(views::Dialog::text(format!("Unable to delete user: {}", e)) + .title("Error") + .dismiss_button("Abort")); + } else { + // Recreate the "Edit users" dialog from scratch; it's easier than adding the new entry. + siv.pop_layer(); + top_dialog(db, siv); + } +} + +#[derive(Copy, Clone)] +enum PasswordChange { + Leave, + Clear, + Set, +} + +fn select_set(siv: &mut Cursive) { + siv.find_id::>("pw_set").unwrap().select(); +} + +/// Adds or updates a user. +/// (The former if `item` is None; the latter otherwise.) +fn edit_user_dialog(db: &Arc, siv: &mut Cursive, item: Option) { + let username; + let id_str; + let has_password; + let mut pw_group = views::RadioGroup::new(); + { + let l = db.lock(); + let u = item.map(|id| l.users_by_id().get(&id).unwrap()); + username = u.map(|u| u.username.clone()).unwrap_or(String::new()); + id_str = item.map(|id| id.to_string()).unwrap_or("".to_string()); + has_password = u.map(|u| u.has_password()).unwrap_or(false); + } + let top_list = views::ListView::new() + .child("id", views::TextView::new(id_str)) + .child("username", views::EditView::new() + .content(username.clone()) + .with_id("username")); + let mut layout = views::LinearLayout::vertical() + .child(top_list) + .child(views::DummyView) + .child(views::TextView::new("password")); + + if has_password { + layout.add_child(pw_group.button(PasswordChange::Leave, "Leave set")); + layout.add_child(pw_group.button(PasswordChange::Clear, "Clear")); + layout.add_child(views::LinearLayout::horizontal() + .child(pw_group.button(PasswordChange::Set, "Set to:") + .with_id("pw_set")) + .child(views::DummyView) + .child(views::EditView::new() + .on_edit(|siv, _, _| select_set(siv)) + .with_id("new_pw") + .full_width())); + } else { + layout.add_child(pw_group.button(PasswordChange::Leave, "Leave unset")); + layout.add_child(views::LinearLayout::horizontal() + .child(pw_group.button(PasswordChange::Set, "Reset to:") + .with_id("pw_set")) + .child(views::DummyView) + .child(views::EditView::new() + .on_edit(|siv, _, _| select_set(siv)) + .with_id("new_pw") + .full_width())); + } + + let dialog = views::Dialog::around(layout); + let dialog = if let Some(id) = item { + dialog.title("Edit user") + .button("Edit", { + let db = db.clone(); + move |s| press_edit(s, &db, item, *pw_group.selection()) + }) + .button("Delete", { + let db = db.clone(); + move |s| press_delete(s, &db, id, username.clone()) + }) + } else { + dialog.title("Add user") + .button("Add", { + let db = db.clone(); + move |s| press_edit(s, &db, item, *pw_group.selection()) + }) + }; + siv.add_layer(dialog.dismiss_button("Cancel")); +} + +pub fn top_dialog(db: &Arc, siv: &mut Cursive) { + siv.add_layer(views::Dialog::around( + views::SelectView::new() + .on_submit({ + let db = db.clone(); + move |siv, &item| edit_user_dialog(&db, siv, item) + }) + .item("".to_string(), None) + .with_all(db.lock() + .users_by_id() + .iter() + .map(|(&id, user)| (format!("{}: {}", id, user.username), Some(id)))) + .full_width()) + .dismiss_button("Done") + .title("Edit cameras")); +}