#![allow(unused_imports)]
#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss
)]
use crate::cli::config::Config;
use malwaredb_server::State;
use std::process::ExitCode;
use std::rc::Rc;
use std::sync::Arc;
use anyhow::{ensure, Context, Result};
use clap::{Args, ValueHint};
use slint::{Model, ModelRc, SharedString, StandardListViewItem, VecModel};
use tracing::{debug, error};
slint::include_modules!();
const NO_PARENT_OPTION: &str = "--NONE--";
#[derive(Clone, Debug, Args, PartialEq)]
pub struct AdminGui {
#[arg(value_name = "FILE", value_hint = ValueHint::FilePath)]
config_file: std::path::PathBuf,
}
impl AdminGui {
#[allow(clippy::too_many_lines)]
pub async fn execute(&self) -> Result<ExitCode> {
let state = Arc::new(Config::from_file(&self.config_file)?.into_state().await?);
let gui = AdminWindow::new()?;
self.load_gui(&state, &gui).await?;
let ui_handle = gui.as_weak();
let users_state = state.clone();
gui.on_users_save_button_pressed(move || {
let gui = ui_handle.upgrade().unwrap();
gui.set_user_status("".into());
let uid = gui.get_users_index();
let uname = gui.get_user_name();
let fname = gui.get_user_fname();
let lname = gui.get_user_lname();
let email = gui.get_user_email();
let readonly = gui.get_user_readonly();
if uid < 0 {
gui.set_user_status("uid should never be negative".into());
error!("uid should never be negative");
return;
}
if uname.len() < 3 {
gui.set_user_status("username too short".into());
error!("username too short");
return;
}
if fname.len() < 3 {
gui.set_user_status("first name too short".into());
error!("first name too short");
return;
}
if lname.len() < 3 {
gui.set_user_status("last name too short".into());
error!("last name too short");
return;
}
if email.len() < 6 {
gui.set_user_status("email too short".into());
error!("email too short");
return;
}
if !email.contains('@') {
gui.set_user_status("email doesn't appear valid".into());
error!("email doesn't appear valid");
return;
}
if readonly && uid == 0 {
gui.set_user_status("cowardly refusing to set admin to be read-only".into());
error!("cowardly refusing to set admin to be read-only");
return;
}
let handle = tokio::runtime::Handle::current();
let _ = handle.enter();
futures::executor::block_on(
users_state
.db_type
.edit_user(uid as u32, &uname, &fname, &lname, &email, readonly),
)
.expect("failed to update database");
let password = gui.get_user_password();
if password.len() > 1 {
futures::executor::block_on(users_state.db_type.set_password(&uname, &password))
.expect("failed to update password");
debug!("Set new password for {uid}!");
} else {
debug!(
"Not setting password for {uid}, {} is too short",
password.len()
);
}
let users_list = gui.get_users_list();
users_list.set_row_data(uid as usize, StandardListViewItem::from(uname.clone()));
let users = gui.get_users();
if let Some(mut row) = users.row_data(uid as usize) {
row.uname = uname;
row.fname = fname;
row.lname = lname;
row.email = email;
users.set_row_data(uid as usize, row);
}
});
let group_state = state.clone();
let ui_handle = gui.as_weak();
gui.on_groups_save_button_pressed(move || {
let gui = ui_handle.upgrade().unwrap();
gui.set_group_status("".into());
let gid = gui.get_groups_index();
let gname = gui.get_group_name();
let gdesc = gui.get_group_description();
let gparent = gui.get_group_parent();
let gparent_orig = gparent.clone();
let gparent = if gparent == NO_PARENT_OPTION {
None
} else {
let handle = tokio::runtime::Handle::current();
let _ = handle.enter();
let gparent_id =
futures::executor::block_on(group_state.db_type.group_id_from_name(&gparent))
.expect("failed to get group's parent id from database");
if gid == gparent_id {
gui.set_group_status("group cannot be self-parent".into());
error!("group cannot be self-parent");
return;
}
Some(gparent_id)
};
let handle = tokio::runtime::Handle::current();
let _ = handle.enter();
futures::executor::block_on(group_state.db_type.edit_group(
gid as u32,
&gname,
&gdesc,
gparent.map(|p| p as u32),
))
.expect("failed to edit the group");
let groups_list = gui.get_groups_list();
groups_list.set_row_data(gid as usize, StandardListViewItem::from(gname.clone()));
let groups = gui.get_groups();
if let Some(mut row) = groups.row_data(gid as usize) {
row.name = gname;
row.description = gdesc;
row.parent = gparent_orig;
groups.set_row_data(gid as usize, row);
}
});
let label_state = state.clone();
let ui_handle = gui.as_weak();
gui.on_label_save_button_pressed(move || {
let gui = ui_handle.upgrade().unwrap();
gui.set_label_status("".into());
let label_id: u64 = if let Ok(id) = gui.get_label_id().parse() {
id
} else {
gui.set_label_status("Label id not an integer".into());
error!("Label id not an integer");
return;
};
let label_name = gui.get_label_name();
let label_parent = gui.get_label_parent();
let lparent_orig = label_parent.clone();
if label_parent == label_name {
gui.set_label_status("Label cannot have itself as a parent".into());
error!("Label cannot have itself as a parent");
return;
}
let label_parent = if label_parent == NO_PARENT_OPTION {
None
} else {
let handle = tokio::runtime::Handle::current();
let _ = handle.enter();
let lparent_id = futures::executor::block_on(
label_state.db_type.label_id_from_name(&label_parent),
)
.expect("failed to get label parent id from database");
if label_id == lparent_id {
gui.set_group_status("label cannot be self-parent".into());
error!("label cannot be self-parent");
return;
}
Some(lparent_id)
};
let handle = tokio::runtime::Handle::current();
let _ = handle.enter();
futures::executor::block_on(label_state.db_type.edit_label(
label_id,
&label_name,
label_parent,
))
.expect("failed to edit the label");
let lid = gui.get_labels_index();
let labels = gui.get_labels();
if let Some(mut row) = labels.row_data(lid as usize) {
row.name = label_name.clone();
row.parent = lparent_orig;
labels.set_row_data(lid as usize, row);
}
let labels = gui.get_labels_list();
labels.set_row_data(lid as usize, StandardListViewItem::from(label_name));
});
gui.run().unwrap();
Ok(ExitCode::SUCCESS)
}
async fn load_gui(&self, state: &State, gui: &AdminWindow) -> Result<()> {
self.load_gui_sources(state, gui).await?;
self.load_gui_labels(state, gui).await?;
let users = state
.db_type
.list_users()
.await
.context("failed to list users")?;
let gui_usernames: Vec<StandardListViewItem> = users
.iter()
.map(|u| StandardListViewItem::from(SharedString::from(u.uname.clone())))
.collect();
let gui_usernames = VecModel::from(gui_usernames);
let gui_users: Vec<GuiUser> = users
.iter()
.map(|u| GuiUser {
id: u.id as i32,
uname: u.uname.clone().into(),
email: u.email.clone().into(),
fname: u.fname.clone().into(),
lname: u.lname.clone().into(),
active: u.has_api_key,
readonly: u.is_readonly,
})
.collect();
let groups = state
.db_type
.list_groups()
.await
.context("failed to list groups")?;
let mut gui_groupnames_strings_parent: Vec<SharedString> = groups
.iter()
.map(|g| SharedString::from(g.name.clone()))
.collect();
gui_groupnames_strings_parent.insert(0, SharedString::from(NO_PARENT_OPTION.to_string()));
let gui_groupnames_strings: Vec<SharedString> = groups
.iter()
.map(|g| SharedString::from(g.name.clone()))
.collect();
let gui_groupnames: Vec<StandardListViewItem> = gui_groupnames_strings
.iter()
.map(|g| StandardListViewItem::from(g.clone()))
.collect();
let gui_groupnames = VecModel::from(gui_groupnames);
let gui_groupnames_strings_parent = VecModel::from(gui_groupnames_strings_parent);
let gui_groups: Vec<GuiGroup> = groups
.iter()
.map(|g| GuiGroup {
id: g.id as i32,
name: g.name.clone().into(),
description: g.description.clone().unwrap_or_default().into(),
parent: g
.parent
.clone()
.unwrap_or(NO_PARENT_OPTION.into())
.clone()
.into(),
files: g.files as i32,
users: g.members.len() as i32,
sources: g.sources.len() as i32,
})
.collect();
ensure!(gui_users[0].id == 0, "the first user should be ID 0");
ensure!(
gui_users[0].uname == "admin",
"the admin user should be ID 0"
);
ensure!(gui_groups[0].id == 0, "the first group should be ID 0");
ensure!(
gui_groups[0].name == "admin",
"the admin group should be ID 0"
);
let gui_users = VecModel::from(gui_users);
let gui_groups = VecModel::from(gui_groups);
gui.set_users_list(ModelRc::new(gui_usernames));
gui.set_groups_list(ModelRc::new(gui_groupnames));
let gui_users = ModelRc::from(Rc::new(gui_users));
gui.set_users(gui_users);
let gui_groups = ModelRc::from(Rc::new(gui_groups));
gui.set_groups(gui_groups);
let gui_groupnames_strings = VecModel::from(gui_groupnames_strings);
gui.set_groups_strings(ModelRc::new(gui_groupnames_strings));
gui.set_groups_strings_parent(ModelRc::new(gui_groupnames_strings_parent));
Ok(())
}
async fn load_gui_sources(&self, state: &State, gui: &AdminWindow) -> Result<()> {
let sources = state
.db_type
.list_sources()
.await
.context("failed to list sources")?;
let gui_source_names: Vec<StandardListViewItem> = sources
.iter()
.map(|s| StandardListViewItem::from(SharedString::from(s.name.clone())))
.collect();
let gui_source_names = VecModel::from(gui_source_names);
gui.set_sources_list(ModelRc::new(gui_source_names));
let mut gui_sourcenames_strings_parent: Vec<SharedString> = sources
.iter()
.map(|g| SharedString::from(g.name.clone()))
.collect();
gui_sourcenames_strings_parent.insert(0, SharedString::from(NO_PARENT_OPTION.to_string()));
let gui_sourcenames_strings_parent = VecModel::from(gui_sourcenames_strings_parent);
gui.set_sources_strings_parent(ModelRc::new(gui_sourcenames_strings_parent));
let gui_sources: Vec<GuiSource> = sources
.iter()
.map(|s| GuiSource {
id: s.id as i32,
name: s.name.clone().into(),
description: s.description.clone().unwrap_or_default().into(),
url: s.url.clone().unwrap_or_default().into(),
parent: s
.parent
.clone()
.unwrap_or(NO_PARENT_OPTION.into())
.clone()
.into(),
files: s.files as i32,
groups: s.groups as i32,
malicious: s.malicious.unwrap_or_default(),
})
.collect();
let gui_sources = VecModel::from(gui_sources);
gui.set_sources(ModelRc::new(gui_sources));
Ok(())
}
async fn load_gui_labels(&self, state: &State, gui: &AdminWindow) -> Result<()> {
let labels = state
.db_type
.get_labels()
.await
.context("failed to list sources")?;
let mut gui_labels_strings_parent: Vec<SharedString> = labels
.0
.iter()
.map(|l| SharedString::from(l.name.clone()))
.collect();
gui_labels_strings_parent.insert(0, SharedString::from(NO_PARENT_OPTION.to_string()));
let gui_label_names: Vec<StandardListViewItem> = labels
.0
.iter()
.map(|s| StandardListViewItem::from(SharedString::from(s.name.clone())))
.collect();
let gui_label_names = VecModel::from(gui_label_names);
gui.set_labels_list(ModelRc::new(gui_label_names));
let gui_labels: Vec<GuiLabel> = labels
.0
.iter()
.map(|l| GuiLabel {
id: l.id as i32,
name: l.name.clone().into(),
parent: l
.parent
.clone()
.unwrap_or(NO_PARENT_OPTION.into())
.clone()
.into(),
})
.collect();
let gui_labels_strings_parent = VecModel::from(gui_labels_strings_parent);
gui.set_labels_strings_parent(ModelRc::new(gui_labels_strings_parent));
let gui_labels = VecModel::from(gui_labels);
gui.set_labels(ModelRc::new(gui_labels));
Ok(())
}
}