use malwaredb_server::State;
use malwaredb_server::db::admin::User;
use std::collections::HashSet;
use std::sync::Arc;
use anyhow::Result;
use eframe::egui::Id;
const COLUMN_HEADERS: [&str; 8] = [
"ID",
"Username",
"First Name",
"Last Name",
"Email",
"Active",
"Read-Only",
"Has API Key",
];
#[derive(Debug, Clone)]
pub struct UserTable {
state: Arc<State>,
users: Vec<User>,
draft: Vec<User>,
changed_ids: HashSet<usize>,
}
impl UserTable {
pub async fn new(state: Arc<State>) -> Result<Self> {
let users = state.db_type.list_users().await?;
let draft = users.clone();
Ok(Self {
state,
users,
draft,
changed_ids: HashSet::new(),
})
}
fn all_valid(&self) -> bool {
self.draft.iter().all(|u| {
!u.uname.trim().is_empty()
&& !u.fname.trim().is_empty()
&& !u.lname.trim().is_empty()
&& is_valid_email(&u.email)
})
}
pub fn ui(&mut self, ui: &mut eframe::egui::Ui) {
if !self.changed_ids.is_empty() {
ui.horizontal(|ui| {
if ui
.add_enabled(self.all_valid(), eframe::egui::Button::new("Save"))
.clicked()
{
for row in &self.changed_ids {
let user = &self.draft[*row];
if let Err(e) = futures::executor::block_on(self.state.db_type.edit_user(
user.id,
&user.uname,
&user.fname,
&user.lname,
&user.email,
user.is_readonly,
)) {
eprintln!(
"Error saving changes for user {} {}: {e}",
user.id, user.uname
);
}
if !user.has_api_key
&& let Err(e) = futures::executor::block_on(
self.state.db_type.deactivate_user(user.id),
)
{
eprintln!("Error deactivating user {} {}: {e}", user.id, user.uname);
}
}
self.users.clone_from(&self.draft);
self.changed_ids.clear();
}
if ui.button("Cancel").clicked() {
self.draft.clone_from(&self.users);
self.changed_ids.clear();
}
});
}
let id_salt = Id::new("user_table");
let table = egui_table::Table::new()
.id_salt(id_salt)
.auto_size_mode(egui_table::AutoSizeMode::OnParentResize)
.num_rows(self.draft.len() as u64)
.columns(
(0..COLUMN_HEADERS.len())
.map(|_| egui_table::Column::new(100.0).resizable(true))
.collect::<Vec<_>>(),
);
table.show(ui, self);
}
}
fn is_valid_email(email: &str) -> bool {
if email.is_empty()
|| !email.contains('@')
|| email.trim().len() != email.len()
|| email.contains(' ')
{
return false;
}
let mut parts = email.splitn(2, '@');
let local = parts.next().unwrap_or("");
let domain = parts.next().unwrap_or("");
!local.is_empty()
&& !domain.is_empty()
&& domain.contains('.')
&& !domain.starts_with('.')
&& !domain.ends_with('.')
}
fn field_error(user: &User, col_nr: usize) -> Option<&'static str> {
match col_nr {
1 if user.uname.trim().is_empty() => Some("Username cannot be blank"),
1 if user.uname.contains(' ') => Some("Username cannot contain spaces"),
2 if user.fname.trim().is_empty() => Some("First name cannot be blank"),
3 if user.lname.trim().is_empty() => Some("Last name cannot be blank"),
4 if !is_valid_email(&user.email) => Some("Enter a valid email address (user@domain.tld)"),
_ => None,
}
}
impl egui_table::TableDelegate for UserTable {
fn header_cell_ui(&mut self, ui: &mut eframe::egui::Ui, cell: &egui_table::HeaderCellInfo) {
if let Some(header) = COLUMN_HEADERS.get(cell.group_index) {
ui.label(*header);
}
}
fn cell_ui(&mut self, ui: &mut eframe::egui::Ui, cell: &egui_table::CellInfo) {
let row = match usize::try_from(cell.row_nr).ok() {
Some(r) if r < self.draft.len() => r,
_ => return,
};
let error = field_error(&self.draft[row], cell.col_nr);
let user_id = self.draft[row].id;
let changed = {
let user = &mut self.draft[row];
match cell.col_nr {
0 => {
ui.label(user.id.to_string());
false
}
1 if user_id == 0 => {
ui.label(user.uname.as_str());
false
}
1..=4 => {
if error.is_some() {
ui.style_mut().visuals.extreme_bg_color = super::INVALID_BG;
}
let field: &mut String = match cell.col_nr {
1 => &mut user.uname,
2 => &mut user.fname,
3 => &mut user.lname,
4 => &mut user.email,
_ => unreachable!(),
};
let response = ui.text_edit_singleline(field);
let changed = response.changed();
if let Some(msg) = error {
response.on_hover_text(msg);
}
changed
}
5 => ui.checkbox(&mut user.has_password, "").changed(),
6 => ui.checkbox(&mut user.is_readonly, "").changed(),
7 => ui.checkbox(&mut user.has_api_key, "").changed(),
_ => false,
}
};
if changed {
self.changed_ids.insert(row);
} else if self.draft[row] == self.users[row] {
self.changed_ids.remove(&row);
}
}
}