use serde::{Deserialize, Serialize}; use crate::auth::{connect_to_db, get_db_config}; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct DeviceSecurityInfo { pub current_device_name: String, pub current_ip: String, pub is_admin: bool, pub device_binding_enabled: bool, pub trusted_device_name: Option, pub ip_binding_enabled: bool, pub trusted_ip: Option, pub is_device_allowed: bool, pub is_ip_allowed: bool, pub can_admin_manage: bool, pub pin_protection_enabled: bool, } pub fn get_admin_master_pin() -> String { crate::auth::extract_clean_env("ADMIN_MASTER_PIN", "92057856") } pub fn verify_admin_pin(input_pin: Option<&str>) -> Result<(), String> { let expected = get_admin_master_pin(); let expected_clean = expected.trim(); if expected_clean.is_empty() { return Ok(()); } let input = input_pin.unwrap_or("").trim(); if input.is_empty() { return Err("Введите 8-значный мастер-PIN администратора для подтверждения".to_string()); } if input != expected_clean { return Err("Неверный мастер-PIN администратора! Проверьте значение в файле .env (ADMIN_MASTER_PIN)".to_string()); } Ok(()) } pub fn get_current_device_name() -> String { if let Ok(name) = std::env::var("COMPUTERNAME") { if !name.trim().is_empty() { return name.trim().to_string(); } } if let Ok(name) = std::env::var("HOSTNAME") { if !name.trim().is_empty() { return name.trim().to_string(); } } if let Ok(output) = std::process::Command::new("hostname").output() { if output.status.success() { let s = String::from_utf8_lossy(&output.stdout).trim().to_string(); if !s.is_empty() { return s; } } } if let Ok(user) = std::env::var("USER") { return format!("{}-workstation", user.trim()); } "main-workstation".to_string() } pub fn get_current_local_ip() -> String { if let Ok(socket) = std::net::UdpSocket::bind("0.0.0.0:0") { if socket.connect("8.8.8.8:80").is_ok() { if let Ok(addr) = socket.local_addr() { return addr.ip().to_string(); } } } "127.0.0.1".to_string() } pub async fn init_admin_security_table(client: &tokio_postgres::Client) -> Result<(), String> { let sql = " CREATE TABLE IF NOT EXISTS admin_security_config ( id SERIAL PRIMARY KEY, device_binding_enabled BOOLEAN DEFAULT FALSE, trusted_device_name VARCHAR(255), ip_binding_enabled BOOLEAN DEFAULT FALSE, trusted_ip VARCHAR(100) ); INSERT INTO admin_security_config (id, device_binding_enabled, ip_binding_enabled) VALUES (1, false, false) ON CONFLICT (id) DO NOTHING; "; // If has_app lacks DDL permissions on public schema, do not fail admin updates if let Err(e) = client.batch_execute(sql).await { eprintln!("[admin] admin_security_config table creation skipped: {}", e); } Ok(()) } pub async fn get_admin_security_info(admin_email: &str) -> Result { let config = get_db_config(); let client = connect_to_db(&config).await?; let _ = init_admin_security_table(&client).await; let clean_email = admin_email.trim().to_lowercase(); let user_row = client .query_opt( &format!( "SELECT role, id FROM {} WHERE LOWER({}) = LOWER($1)", config.users_table, config.email_column ), &[&clean_email], ) .await .map_err(|e| format!("Ошибка проверки роли: {}", e))?; let (is_admin, _user_id) = match user_row { Some(row) => { let role: Option = row.try_get(0).ok().flatten(); let id: i32 = row.get(1); let is_adm = id == 1 || matches!(role.as_deref(), Some("admin") | Some("Администратор")); (is_adm, id) } None => (false, -1), }; let cfg_row = client .query_opt( "SELECT device_binding_enabled, trusted_device_name, ip_binding_enabled, trusted_ip FROM admin_security_config WHERE id = 1", &[], ) .await .ok() .flatten(); let (device_binding_enabled, trusted_device_name, ip_binding_enabled, trusted_ip): (bool, Option, bool, Option) = match cfg_row { Some(row) => (row.get(0), row.get(1), row.get(2), row.get(3)), None => (false, None, false, None), }; let current_device = get_current_device_name(); let current_ip = get_current_local_ip(); let is_device_allowed = if !device_binding_enabled { true } else { match &trusted_device_name { Some(t) => t.trim().eq_ignore_ascii_case(¤t_device.trim()), None => true, } }; let is_ip_allowed = if !ip_binding_enabled { true } else { match &trusted_ip { Some(t) => t.trim().eq_ignore_ascii_case(¤t_ip.trim()), None => true, } }; let can_admin_manage = is_admin; Ok(DeviceSecurityInfo { current_device_name: current_device, current_ip, is_admin, device_binding_enabled, trusted_device_name, ip_binding_enabled, trusted_ip, is_device_allowed, is_ip_allowed, can_admin_manage, pin_protection_enabled: !get_admin_master_pin().is_empty(), }) } pub async fn update_admin_security_policy( admin_email: &str, device_binding: bool, trusted_device: Option, ip_binding: bool, trusted_ip: Option, ) -> Result { let sec_info = get_admin_security_info(admin_email).await?; if !sec_info.is_admin { return Err("Доступ запрещен: действие доступно только администратору".to_string()); } let config = get_db_config(); let client = connect_to_db(&config).await?; init_admin_security_table(&client).await?; let trusted_dev_clean = trusted_device.map(|s| s.trim().to_string()).filter(|s| !s.is_empty()); let trusted_ip_clean = trusted_ip.map(|s| s.trim().to_string()).filter(|s| !s.is_empty()); client .execute( "UPDATE admin_security_config SET device_binding_enabled = $1, trusted_device_name = $2, ip_binding_enabled = $3, trusted_ip = $4 WHERE id = 1", &[&device_binding, &trusted_dev_clean, &ip_binding, &trusted_ip_clean], ) .await .map_err(|e| format!("Ошибка сохранения политики безопасности: {}", e))?; get_admin_security_info(admin_email).await } pub async fn admin_update_user( admin_email: &str, target_id: i32, new_username: Option, new_email: Option, new_role: Option, new_password: Option, admin_pin: Option, ) -> Result { let sec_info = get_admin_security_info(admin_email).await?; if !sec_info.is_admin { return Err("Отказано в доступе: требуются права администратора".to_string()); } verify_admin_pin(admin_pin.as_deref())?; let config = get_db_config(); let client = connect_to_db(&config).await?; let clean_username = new_username.map(|s| s.trim().to_string()).filter(|s| !s.is_empty()); let clean_role = new_role.map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).unwrap_or_else(|| "user".to_string()); let clean_email = new_email.map(|s| s.trim().to_string()).filter(|s| !s.is_empty()); if let Some(pwd) = new_password.map(|s| s.trim().to_string()).filter(|s| !s.is_empty()) { let hashed = bcrypt::hash(&pwd, 10).map_err(|e| format!("Ошибка хеширования: {}", e))?; if let Some(em) = clean_email { let update_sql = format!( "UPDATE {} SET username = $1, {} = $2, role = $3, {} = $4 WHERE id = $5", config.users_table, config.email_column, config.password_column ); client .execute(&update_sql, &[&clean_username, &em, &clean_role, &hashed, &target_id]) .await .map_err(|e| format!("Ошибка обновления пользователя: {}", e))?; } else { let update_sql = format!( "UPDATE {} SET username = $1, role = $2, {} = $3 WHERE id = $4", config.users_table, config.password_column ); client .execute(&update_sql, &[&clean_username, &clean_role, &hashed, &target_id]) .await .map_err(|e| format!("Ошибка обновления пользователя: {}", e))?; } } else { if let Some(em) = clean_email { let update_sql = format!( "UPDATE {} SET username = $1, {} = $2, role = $3 WHERE id = $4", config.users_table, config.email_column ); client .execute(&update_sql, &[&clean_username, &em, &clean_role, &target_id]) .await .map_err(|e| format!("Ошибка обновления пользователя: {}", e))?; } else { let update_sql = format!( "UPDATE {} SET username = $1, role = $2 WHERE id = $3", config.users_table ); client .execute(&update_sql, &[&clean_username, &clean_role, &target_id]) .await .map_err(|e| format!("Ошибка обновления пользователя: {}", e))?; } } Ok("Данные пользователя успешно обновлены".to_string()) } pub async fn admin_delete_user( admin_email: &str, target_id: i32, admin_pin: Option, ) -> Result { let sec_info = get_admin_security_info(admin_email).await?; if !sec_info.is_admin { return Err("Отказано в доступе: требуются права администратора".to_string()); } verify_admin_pin(admin_pin.as_deref())?; if target_id == 1 { return Err("Нельзя удалить основного системного администратора (ID: 1)".to_string()); } let config = get_db_config(); let client = connect_to_db(&config).await?; let self_row = client .query_opt( &format!("SELECT id FROM {} WHERE LOWER({}) = LOWER($1)", config.users_table, config.email_column), &[&admin_email.trim().to_lowercase()], ) .await .map_err(|e| format!("Ошибка проверки: {}", e))?; if let Some(r) = self_row { let self_id: i32 = r.get(0); if self_id == target_id { return Err("Нельзя удалить собственную учетную запись администратора".to_string()); } } let delete_sql = format!("DELETE FROM {} WHERE id = $1", config.users_table); client .execute(&delete_sql, &[&target_id]) .await .map_err(|e| format!("Ошибка удаления пользователя: {}", e))?; Ok("Пользователь успешно удален".to_string()) } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_init_admin_security() { let config = get_db_config(); if let Ok(client) = connect_to_db(&config).await { let res = init_admin_security_table(&client).await; assert!(res.is_ok()); } } #[tokio::test] async fn test_get_admin_security_info() { let config = get_db_config(); if let Ok(_) = connect_to_db(&config).await { let res = get_admin_security_info("dimatyilickiy0511@gmail.com").await; println!("get_admin_security_info result: {:?}", res); assert!(res.is_ok()); } } }